From fc7369835258467bf97eb64f184b93691f9a9fd5 Mon Sep 17 00:00:00 2001 From: Yaco Date: Thu, 4 Jun 2020 11:01:00 -0300 Subject: first commit --- www/wiki/includes/.htaccess | 1 + www/wiki/includes/ActorMigration.php | 383 + www/wiki/includes/AjaxDispatcher.php | 163 + www/wiki/includes/AjaxResponse.php | 315 + www/wiki/includes/AuthPlugin.php | 368 + www/wiki/includes/AutoLoader.php | 137 + www/wiki/includes/Autopromote.php | 215 + www/wiki/includes/Block.php | 1650 ++++ www/wiki/includes/CategoriesRdf.php | 128 + www/wiki/includes/Category.php | 409 + www/wiki/includes/CategoryFinder.php | 262 + www/wiki/includes/CategoryViewer.php | 752 ++ www/wiki/includes/CommentStore.php | 699 ++ www/wiki/includes/CommentStoreComment.php | 90 + www/wiki/includes/ConfiguredReadOnlyMode.php | 73 + www/wiki/includes/DefaultSettings.php | 8832 ++++++++++++++++++++ www/wiki/includes/Defines.php | 297 + www/wiki/includes/DeprecatedGlobal.php | 57 + www/wiki/includes/DerivativeRequest.php | 87 + www/wiki/includes/DevelopmentSettings.php | 57 + www/wiki/includes/DummyLinker.php | 489 ++ www/wiki/includes/EditPage.php | 4624 ++++++++++ www/wiki/includes/EventRelayerGroup.php | 72 + www/wiki/includes/FauxRequest.php | 246 + www/wiki/includes/Feed.php | 494 ++ www/wiki/includes/FeedUtils.php | 260 + www/wiki/includes/FileDeleteForm.php | 448 + www/wiki/includes/ForkController.php | 204 + www/wiki/includes/FormOptions.php | 422 + www/wiki/includes/GitInfo.php | 426 + www/wiki/includes/GlobalFunctions.php | 3444 ++++++++ www/wiki/includes/HeaderCallback.php | 69 + www/wiki/includes/HistoryBlob.php | 713 ++ www/wiki/includes/Hooks.php | 244 + www/wiki/includes/Html.php | 1059 +++ www/wiki/includes/LinkFilter.php | 191 + www/wiki/includes/Linker.php | 2173 +++++ www/wiki/includes/ListToggle.php | 69 + www/wiki/includes/MWGrants.php | 216 + www/wiki/includes/MWNamespace.php | 549 ++ www/wiki/includes/MWTimestamp.php | 210 + www/wiki/includes/MagicWord.php | 695 ++ www/wiki/includes/MagicWordArray.php | 340 + www/wiki/includes/MediaWiki.php | 1083 +++ www/wiki/includes/MediaWikiServices.php | 829 ++ www/wiki/includes/MediaWikiVersionFetcher.php | 30 + www/wiki/includes/MergeHistory.php | 355 + www/wiki/includes/Message.php | 1378 +++ www/wiki/includes/MimeMagic.php | 43 + www/wiki/includes/MovePage.php | 619 ++ www/wiki/includes/NoLocalSettings.php | 63 + .../includes/OrderedStreamingForkController.php | 216 + www/wiki/includes/OutputHandler.php | 162 + www/wiki/includes/OutputPage.php | 4012 +++++++++ www/wiki/includes/PHPVersionCheck.php | 345 + www/wiki/includes/PageProps.php | 316 + www/wiki/includes/PathRouter.php | 399 + www/wiki/includes/Pingback.php | 279 + www/wiki/includes/Preferences.php | 338 + www/wiki/includes/PrefixSearch.php | 406 + www/wiki/includes/ProtectionForm.php | 636 ++ www/wiki/includes/ProxyLookup.php | 85 + www/wiki/includes/RawMessage.php | 72 + www/wiki/includes/ReadOnlyMode.php | 68 + www/wiki/includes/Revision.php | 1300 +++ www/wiki/includes/RevisionList.php | 447 + www/wiki/includes/ServiceWiring.php | 616 ++ www/wiki/includes/Setup.php | 980 +++ www/wiki/includes/SiteConfiguration.php | 613 ++ www/wiki/includes/SiteStats.php | 301 + www/wiki/includes/SiteStatsInit.php | 202 + www/wiki/includes/Status.php | 403 + www/wiki/includes/Storage/BlobAccessException.php | 34 + www/wiki/includes/Storage/BlobStore.php | 119 + www/wiki/includes/Storage/BlobStoreFactory.php | 105 + .../Storage/IncompleteRevisionException.php | 32 + .../includes/Storage/MutableRevisionRecord.php | 328 + www/wiki/includes/Storage/MutableRevisionSlots.php | 137 + .../includes/Storage/NameTableAccessException.php | 45 + www/wiki/includes/Storage/NameTableStore.php | 366 + .../includes/Storage/RevisionAccessException.php | 34 + .../includes/Storage/RevisionArchiveRecord.php | 170 + www/wiki/includes/Storage/RevisionFactory.php | 94 + www/wiki/includes/Storage/RevisionLookup.php | 120 + www/wiki/includes/Storage/RevisionRecord.php | 492 ++ www/wiki/includes/Storage/RevisionSlots.php | 202 + www/wiki/includes/Storage/RevisionStore.php | 2017 +++++ www/wiki/includes/Storage/RevisionStoreRecord.php | 210 + www/wiki/includes/Storage/SlotRecord.php | 568 ++ www/wiki/includes/Storage/SqlBlobStore.php | 600 ++ .../includes/Storage/SuppressedDataException.php | 33 + www/wiki/includes/StreamFile.php | 144 + www/wiki/includes/StubObject.php | 208 + www/wiki/includes/TemplateParser.php | 220 + www/wiki/includes/TemplatesOnThisPageFormatter.php | 183 + www/wiki/includes/Title.php | 5135 ++++++++++++ www/wiki/includes/TitleArray.php | 59 + www/wiki/includes/TitleArrayFromResult.php | 88 + www/wiki/includes/TrackingCategories.php | 132 + www/wiki/includes/WebRequest.php | 1332 +++ www/wiki/includes/WebRequestUpload.php | 142 + www/wiki/includes/WebResponse.php | 318 + www/wiki/includes/WebStart.php | 115 + www/wiki/includes/WikiMap.php | 294 + www/wiki/includes/WikiReference.php | 124 + www/wiki/includes/Xml.php | 861 ++ www/wiki/includes/XmlJsCode.php | 45 + www/wiki/includes/XmlSelect.php | 140 + www/wiki/includes/actions/Action.php | 430 + www/wiki/includes/actions/CachedAction.php | 189 + www/wiki/includes/actions/CreditsAction.php | 246 + www/wiki/includes/actions/DeleteAction.php | 52 + www/wiki/includes/actions/EditAction.php | 67 + www/wiki/includes/actions/FormAction.php | 146 + www/wiki/includes/actions/FormlessAction.php | 45 + www/wiki/includes/actions/HistoryAction.php | 966 +++ www/wiki/includes/actions/InfoAction.php | 966 +++ www/wiki/includes/actions/MarkpatrolledAction.php | 143 + www/wiki/includes/actions/ProtectAction.php | 58 + www/wiki/includes/actions/PurgeAction.php | 109 + www/wiki/includes/actions/RawAction.php | 308 + www/wiki/includes/actions/RenderAction.php | 46 + www/wiki/includes/actions/RevertAction.php | 168 + www/wiki/includes/actions/RollbackAction.php | 164 + www/wiki/includes/actions/SpecialPageAction.php | 97 + www/wiki/includes/actions/SubmitAction.php | 40 + www/wiki/includes/actions/UnprotectAction.php | 46 + www/wiki/includes/actions/UnwatchAction.php | 65 + www/wiki/includes/actions/ViewAction.php | 70 + www/wiki/includes/actions/WatchAction.php | 194 + www/wiki/includes/api/ApiAMCreateAccount.php | 137 + www/wiki/includes/api/ApiAuthManagerHelper.php | 396 + www/wiki/includes/api/ApiBase.php | 2971 +++++++ www/wiki/includes/api/ApiBlock.php | 199 + www/wiki/includes/api/ApiCSPReport.php | 242 + .../includes/api/ApiChangeAuthenticationData.php | 98 + www/wiki/includes/api/ApiCheckToken.php | 88 + www/wiki/includes/api/ApiClearHasMsg.php | 53 + www/wiki/includes/api/ApiClientLogin.php | 137 + www/wiki/includes/api/ApiComparePages.php | 520 ++ www/wiki/includes/api/ApiContinuationManager.php | 271 + www/wiki/includes/api/ApiDelete.php | 230 + www/wiki/includes/api/ApiDisabled.php | 54 + www/wiki/includes/api/ApiEditPage.php | 625 ++ www/wiki/includes/api/ApiEmailUser.php | 115 + www/wiki/includes/api/ApiErrorFormatter.php | 460 + www/wiki/includes/api/ApiExpandTemplates.php | 229 + www/wiki/includes/api/ApiFeedContributions.php | 232 + www/wiki/includes/api/ApiFeedRecentChanges.php | 183 + www/wiki/includes/api/ApiFeedWatchlist.php | 305 + www/wiki/includes/api/ApiFileRevert.php | 132 + www/wiki/includes/api/ApiFormatBase.php | 387 + www/wiki/includes/api/ApiFormatFeedWrapper.php | 113 + www/wiki/includes/api/ApiFormatJson.php | 134 + www/wiki/includes/api/ApiFormatNone.php | 36 + www/wiki/includes/api/ApiFormatPhp.php | 83 + www/wiki/includes/api/ApiFormatRaw.php | 116 + www/wiki/includes/api/ApiFormatXml.php | 297 + www/wiki/includes/api/ApiHelp.php | 903 ++ www/wiki/includes/api/ApiHelpParamValueMessage.php | 91 + www/wiki/includes/api/ApiImageRotate.php | 198 + www/wiki/includes/api/ApiImport.php | 222 + www/wiki/includes/api/ApiLinkAccount.php | 129 + www/wiki/includes/api/ApiLogin.php | 308 + www/wiki/includes/api/ApiLogout.php | 88 + www/wiki/includes/api/ApiMain.php | 2028 +++++ www/wiki/includes/api/ApiManageTags.php | 130 + www/wiki/includes/api/ApiMergeHistory.php | 138 + www/wiki/includes/api/ApiMessage.php | 301 + www/wiki/includes/api/ApiModuleManager.php | 290 + www/wiki/includes/api/ApiMove.php | 280 + www/wiki/includes/api/ApiOpenSearch.php | 419 + www/wiki/includes/api/ApiOptions.php | 185 + www/wiki/includes/api/ApiPageSet.php | 1541 ++++ www/wiki/includes/api/ApiParamInfo.php | 583 ++ www/wiki/includes/api/ApiParse.php | 908 ++ www/wiki/includes/api/ApiPatrol.php | 115 + www/wiki/includes/api/ApiProtect.php | 200 + www/wiki/includes/api/ApiPurge.php | 177 + www/wiki/includes/api/ApiQuery.php | 544 ++ www/wiki/includes/api/ApiQueryAllCategories.php | 201 + .../includes/api/ApiQueryAllDeletedRevisions.php | 465 ++ www/wiki/includes/api/ApiQueryAllImages.php | 424 + www/wiki/includes/api/ApiQueryAllLinks.php | 309 + www/wiki/includes/api/ApiQueryAllMessages.php | 257 + www/wiki/includes/api/ApiQueryAllPages.php | 356 + www/wiki/includes/api/ApiQueryAllRevisions.php | 282 + www/wiki/includes/api/ApiQueryAllUsers.php | 412 + www/wiki/includes/api/ApiQueryAuthManagerInfo.php | 132 + www/wiki/includes/api/ApiQueryBacklinks.php | 576 ++ www/wiki/includes/api/ApiQueryBacklinksprop.php | 437 + www/wiki/includes/api/ApiQueryBase.php | 612 ++ www/wiki/includes/api/ApiQueryBlocks.php | 347 + www/wiki/includes/api/ApiQueryCategories.php | 232 + www/wiki/includes/api/ApiQueryCategoryInfo.php | 116 + www/wiki/includes/api/ApiQueryCategoryMembers.php | 392 + www/wiki/includes/api/ApiQueryContributors.php | 270 + www/wiki/includes/api/ApiQueryDeletedRevisions.php | 298 + www/wiki/includes/api/ApiQueryDeletedrevs.php | 512 ++ www/wiki/includes/api/ApiQueryDisabled.php | 54 + www/wiki/includes/api/ApiQueryDuplicateFiles.php | 190 + www/wiki/includes/api/ApiQueryExtLinksUsage.php | 231 + www/wiki/includes/api/ApiQueryExternalLinks.php | 135 + www/wiki/includes/api/ApiQueryFileRepoInfo.php | 116 + www/wiki/includes/api/ApiQueryFilearchive.php | 287 + www/wiki/includes/api/ApiQueryGeneratorBase.php | 105 + www/wiki/includes/api/ApiQueryIWBacklinks.php | 218 + www/wiki/includes/api/ApiQueryIWLinks.php | 197 + www/wiki/includes/api/ApiQueryImageInfo.php | 826 ++ www/wiki/includes/api/ApiQueryImages.php | 177 + www/wiki/includes/api/ApiQueryInfo.php | 986 +++ www/wiki/includes/api/ApiQueryLangBacklinks.php | 217 + www/wiki/includes/api/ApiQueryLangLinks.php | 191 + www/wiki/includes/api/ApiQueryLinks.php | 238 + www/wiki/includes/api/ApiQueryLogEvents.php | 482 ++ www/wiki/includes/api/ApiQueryMyStashedFiles.php | 150 + www/wiki/includes/api/ApiQueryPagePropNames.php | 110 + www/wiki/includes/api/ApiQueryPageProps.php | 121 + www/wiki/includes/api/ApiQueryPagesWithProp.php | 175 + www/wiki/includes/api/ApiQueryPrefixSearch.php | 132 + www/wiki/includes/api/ApiQueryProtectedTitles.php | 244 + www/wiki/includes/api/ApiQueryQueryPage.php | 167 + www/wiki/includes/api/ApiQueryRandom.php | 219 + www/wiki/includes/api/ApiQueryRecentChanges.php | 754 ++ www/wiki/includes/api/ApiQueryRevisions.php | 507 ++ www/wiki/includes/api/ApiQueryRevisionsBase.php | 522 ++ www/wiki/includes/api/ApiQuerySearch.php | 420 + www/wiki/includes/api/ApiQuerySiteinfo.php | 936 +++ www/wiki/includes/api/ApiQueryStashImageInfo.php | 128 + www/wiki/includes/api/ApiQueryTags.php | 165 + www/wiki/includes/api/ApiQueryTokens.php | 134 + .../includes/api/ApiQueryUserContributions.php | 832 ++ www/wiki/includes/api/ApiQueryUserInfo.php | 349 + www/wiki/includes/api/ApiQueryUsers.php | 408 + www/wiki/includes/api/ApiQueryWatchlist.php | 531 ++ www/wiki/includes/api/ApiQueryWatchlistRaw.php | 200 + .../includes/api/ApiRemoveAuthenticationData.php | 111 + www/wiki/includes/api/ApiResetPassword.php | 139 + www/wiki/includes/api/ApiResult.php | 1229 +++ www/wiki/includes/api/ApiRevisionDelete.php | 201 + www/wiki/includes/api/ApiRollback.php | 203 + www/wiki/includes/api/ApiRsd.php | 167 + www/wiki/includes/api/ApiSerializable.php | 45 + .../includes/api/ApiSetNotificationTimestamp.php | 251 + www/wiki/includes/api/ApiSetPageLanguage.php | 145 + www/wiki/includes/api/ApiStashEdit.php | 486 ++ www/wiki/includes/api/ApiTag.php | 192 + www/wiki/includes/api/ApiTokens.php | 112 + www/wiki/includes/api/ApiUnblock.php | 133 + www/wiki/includes/api/ApiUndelete.php | 149 + www/wiki/includes/api/ApiUpload.php | 929 ++ www/wiki/includes/api/ApiUsageException.php | 231 + www/wiki/includes/api/ApiUserrights.php | 224 + www/wiki/includes/api/ApiValidatePassword.php | 81 + www/wiki/includes/api/ApiWatch.php | 184 + www/wiki/includes/api/SearchApi.php | 197 + www/wiki/includes/api/i18n/ar.json | 426 + www/wiki/includes/api/i18n/ast.json | 37 + www/wiki/includes/api/i18n/av.json | 8 + www/wiki/includes/api/i18n/awa.json | 11 + www/wiki/includes/api/i18n/azb.json | 15 + www/wiki/includes/api/i18n/ba.json | 440 + www/wiki/includes/api/i18n/bcl.json | 10 + www/wiki/includes/api/i18n/be-tarask.json | 62 + www/wiki/includes/api/i18n/bg.json | 66 + www/wiki/includes/api/i18n/bgn.json | 11 + www/wiki/includes/api/i18n/bn.json | 28 + www/wiki/includes/api/i18n/br.json | 35 + www/wiki/includes/api/i18n/bs.json | 17 + www/wiki/includes/api/i18n/ca.json | 61 + www/wiki/includes/api/i18n/ce.json | 29 + www/wiki/includes/api/i18n/ckb.json | 10 + www/wiki/includes/api/i18n/cs.json | 300 + www/wiki/includes/api/i18n/cv.json | 8 + www/wiki/includes/api/i18n/da.json | 8 + www/wiki/includes/api/i18n/de.json | 1113 +++ www/wiki/includes/api/i18n/diq.json | 66 + www/wiki/includes/api/i18n/el.json | 109 + www/wiki/includes/api/i18n/en-gb.json | 154 + www/wiki/includes/api/i18n/en.json | 1890 +++++ www/wiki/includes/api/i18n/eo.json | 30 + www/wiki/includes/api/i18n/es.json | 1655 ++++ www/wiki/includes/api/i18n/et.json | 42 + www/wiki/includes/api/i18n/eu.json | 262 + www/wiki/includes/api/i18n/fa.json | 340 + www/wiki/includes/api/i18n/fi.json | 99 + www/wiki/includes/api/i18n/fo.json | 39 + www/wiki/includes/api/i18n/fr.json | 1785 ++++ www/wiki/includes/api/i18n/frc.json | 20 + www/wiki/includes/api/i18n/fy.json | 15 + www/wiki/includes/api/i18n/gl.json | 1712 ++++ www/wiki/includes/api/i18n/he.json | 1757 ++++ www/wiki/includes/api/i18n/hr.json | 9 + www/wiki/includes/api/i18n/hsb.json | 8 + www/wiki/includes/api/i18n/hsn.json | 9 + www/wiki/includes/api/i18n/ht.json | 8 + www/wiki/includes/api/i18n/hu.json | 1167 +++ www/wiki/includes/api/i18n/ia.json | 63 + www/wiki/includes/api/i18n/id.json | 105 + www/wiki/includes/api/i18n/is.json | 10 + www/wiki/includes/api/i18n/it.json | 695 ++ www/wiki/includes/api/i18n/ja.json | 977 +++ www/wiki/includes/api/i18n/jam.json | 8 + www/wiki/includes/api/i18n/jv.json | 11 + www/wiki/includes/api/i18n/ka.json | 11 + www/wiki/includes/api/i18n/kn.json | 8 + www/wiki/includes/api/i18n/ko.json | 891 ++ www/wiki/includes/api/i18n/ksh.json | 1046 +++ www/wiki/includes/api/i18n/ku-latn.json | 44 + www/wiki/includes/api/i18n/ky.json | 20 + www/wiki/includes/api/i18n/lb.json | 254 + www/wiki/includes/api/i18n/lij.json | 221 + www/wiki/includes/api/i18n/lki.json | 28 + www/wiki/includes/api/i18n/ln.json | 8 + www/wiki/includes/api/i18n/lt.json | 422 + www/wiki/includes/api/i18n/lv.json | 14 + www/wiki/includes/api/i18n/lzh.json | 8 + www/wiki/includes/api/i18n/mg.json | 37 + www/wiki/includes/api/i18n/mk.json | 431 + www/wiki/includes/api/i18n/mr.json | 107 + www/wiki/includes/api/i18n/ms.json | 58 + www/wiki/includes/api/i18n/my.json | 10 + www/wiki/includes/api/i18n/nap.json | 148 + www/wiki/includes/api/i18n/nb.json | 712 ++ www/wiki/includes/api/i18n/nds.json | 8 + www/wiki/includes/api/i18n/ne.json | 13 + www/wiki/includes/api/i18n/nl.json | 394 + www/wiki/includes/api/i18n/nso.json | 9 + www/wiki/includes/api/i18n/oc.json | 135 + www/wiki/includes/api/i18n/olo.json | 13 + www/wiki/includes/api/i18n/or.json | 14 + www/wiki/includes/api/i18n/pa.json | 8 + www/wiki/includes/api/i18n/pam.json | 32 + www/wiki/includes/api/i18n/pl.json | 725 ++ www/wiki/includes/api/i18n/ps.json | 73 + www/wiki/includes/api/i18n/pt-br.json | 1759 ++++ www/wiki/includes/api/i18n/pt.json | 1763 ++++ www/wiki/includes/api/i18n/qqq.json | 1776 ++++ www/wiki/includes/api/i18n/ro.json | 19 + www/wiki/includes/api/i18n/roa-tara.json | 11 + www/wiki/includes/api/i18n/ru.json | 1783 ++++ www/wiki/includes/api/i18n/sah.json | 9 + www/wiki/includes/api/i18n/sd.json | 15 + www/wiki/includes/api/i18n/sh.json | 8 + www/wiki/includes/api/i18n/shn.json | 8 + www/wiki/includes/api/i18n/si.json | 65 + www/wiki/includes/api/i18n/sk.json | 8 + www/wiki/includes/api/i18n/sq.json | 15 + www/wiki/includes/api/i18n/sr-ec.json | 28 + www/wiki/includes/api/i18n/sr-el.json | 12 + www/wiki/includes/api/i18n/sv.json | 557 ++ www/wiki/includes/api/i18n/ta.json | 28 + www/wiki/includes/api/i18n/tcy.json | 22 + www/wiki/includes/api/i18n/te.json | 21 + www/wiki/includes/api/i18n/th.json | 9 + www/wiki/includes/api/i18n/tl.json | 38 + www/wiki/includes/api/i18n/tr.json | 58 + www/wiki/includes/api/i18n/tt-cyrl.json | 8 + www/wiki/includes/api/i18n/udm.json | 11 + www/wiki/includes/api/i18n/uk.json | 1750 ++++ www/wiki/includes/api/i18n/ur.json | 8 + www/wiki/includes/api/i18n/vi.json | 225 + www/wiki/includes/api/i18n/wuu.json | 8 + www/wiki/includes/api/i18n/yi.json | 10 + www/wiki/includes/api/i18n/zh-hans.json | 1777 ++++ www/wiki/includes/api/i18n/zh-hant.json | 327 + www/wiki/includes/api/i18n/zu.json | 10 + .../auth/AbstractAuthenticationProvider.php | 59 + ...stractPasswordPrimaryAuthenticationProvider.php | 171 + .../auth/AbstractPreAuthenticationProvider.php | 62 + .../auth/AbstractPrimaryAuthenticationProvider.php | 118 + .../AbstractSecondaryAuthenticationProvider.php | 86 + www/wiki/includes/auth/AuthManager.php | 2455 ++++++ www/wiki/includes/auth/AuthManagerAuthPlugin.php | 231 + .../AuthPluginPrimaryAuthenticationProvider.php | 429 + www/wiki/includes/auth/AuthenticationProvider.php | 98 + www/wiki/includes/auth/AuthenticationRequest.php | 379 + www/wiki/includes/auth/AuthenticationResponse.php | 219 + .../includes/auth/ButtonAuthenticationRequest.php | 108 + .../CheckBlocksSecondaryAuthenticationProvider.php | 111 + .../auth/ConfirmLinkAuthenticationRequest.php | 80 + .../ConfirmLinkSecondaryAuthenticationProvider.php | 158 + .../auth/CreateFromLoginAuthenticationRequest.php | 96 + .../auth/CreatedAccountAuthenticationRequest.php | 48 + .../auth/CreationReasonAuthenticationRequest.php | 24 + ...NotificationSecondaryAuthenticationProvider.php | 70 + .../auth/LegacyHookPreAuthenticationProvider.php | 180 + .../LocalPasswordPrimaryAuthenticationProvider.php | 324 + .../auth/PasswordAuthenticationRequest.php | 85 + .../auth/PasswordDomainAuthenticationRequest.php | 85 + .../includes/auth/PreAuthenticationProvider.php | 148 + .../auth/PrimaryAuthenticationProvider.php | 400 + .../auth/RememberMeAuthenticationRequest.php | 65 + ...esetPasswordSecondaryAuthenticationProvider.php | 133 + .../auth/SecondaryAuthenticationProvider.php | 258 + .../TemporaryPasswordAuthenticationRequest.php | 101 + ...poraryPasswordPrimaryAuthenticationProvider.php | 477 ++ .../auth/ThrottlePreAuthenticationProvider.php | 180 + www/wiki/includes/auth/Throttler.php | 208 + .../auth/UserDataAuthenticationRequest.php | 89 + .../auth/UsernameAuthenticationRequest.php | 39 + www/wiki/includes/cache/BacklinkCache.php | 578 ++ www/wiki/includes/cache/CacheDependency.php | 293 + www/wiki/includes/cache/CacheHelper.php | 388 + www/wiki/includes/cache/FileCacheBase.php | 278 + www/wiki/includes/cache/GenderCache.php | 188 + www/wiki/includes/cache/HTMLFileCache.php | 246 + www/wiki/includes/cache/LinkBatch.php | 246 + www/wiki/includes/cache/LinkCache.php | 338 + www/wiki/includes/cache/MessageBlobStore.php | 245 + www/wiki/includes/cache/MessageCache.php | 1318 +++ www/wiki/includes/cache/ResourceFileCache.php | 117 + www/wiki/includes/cache/UserCache.php | 162 + www/wiki/includes/cache/localisation/LCStore.php | 66 + .../includes/cache/localisation/LCStoreCDB.php | 144 + www/wiki/includes/cache/localisation/LCStoreDB.php | 117 + .../includes/cache/localisation/LCStoreNull.php | 39 + .../cache/localisation/LCStoreStaticArray.php | 140 + .../cache/localisation/LocalisationCache.php | 1107 +++ .../localisation/LocalisationCacheBulkLoad.php | 126 + .../includes/changes/CategoryMembershipChange.php | 286 + www/wiki/includes/changes/ChangesFeed.php | 242 + www/wiki/includes/changes/ChangesList.php | 788 ++ .../includes/changes/ChangesListBooleanFilter.php | 260 + .../changes/ChangesListBooleanFilterGroup.php | 89 + www/wiki/includes/changes/ChangesListFilter.php | 508 ++ .../includes/changes/ChangesListFilterGroup.php | 473 ++ .../changes/ChangesListStringOptionsFilter.php | 30 + .../ChangesListStringOptionsFilterGroup.php | 248 + www/wiki/includes/changes/EnhancedChangesList.php | 806 ++ www/wiki/includes/changes/OldChangesList.php | 156 + www/wiki/includes/changes/RCCacheEntry.php | 45 + www/wiki/includes/changes/RCCacheEntryFactory.php | 298 + www/wiki/includes/changes/RecentChange.php | 1191 +++ www/wiki/includes/changetags/ChangeTags.php | 1463 ++++ www/wiki/includes/changetags/ChangeTagsList.php | 78 + www/wiki/includes/changetags/ChangeTagsLogItem.php | 110 + www/wiki/includes/changetags/ChangeTagsLogList.php | 89 + .../includes/changetags/ChangeTagsRevisionItem.php | 62 + .../includes/changetags/ChangeTagsRevisionList.php | 97 + www/wiki/includes/clientpool/SquidPurgeClient.php | 396 + .../includes/clientpool/SquidPurgeClientPool.php | 108 + .../collation/AbkhazUppercaseCollation.php | 93 + .../collation/BashkirUppercaseCollation.php | 71 + www/wiki/includes/collation/Collation.php | 135 + www/wiki/includes/collation/CollationCkb.php | 35 + www/wiki/includes/collation/CollationEt.php | 60 + www/wiki/includes/collation/CollationFa.php | 60 + .../collation/CustomUppercaseCollation.php | 98 + www/wiki/includes/collation/IcuCollation.php | 588 ++ www/wiki/includes/collation/IdentityCollation.php | 44 + .../collation/NorthernSamiUppercaseCollation.php | 83 + .../collation/NumericUppercaseCollation.php | 105 + www/wiki/includes/collation/UppercaseCollation.php | 44 + www/wiki/includes/compat/ObjectFactory.php | 27 + www/wiki/includes/compat/ScopedCallback.php | 29 + www/wiki/includes/compat/Timestamp.php | 18 + www/wiki/includes/compat/normal/UtfNormal.php | 129 + .../includes/compat/normal/UtfNormalDefines.php | 186 + www/wiki/includes/compat/normal/UtfNormalUtil.php | 104 + www/wiki/includes/composer/ComposerHookHandler.php | 37 + .../includes/composer/ComposerPackageModifier.php | 62 + .../composer/ComposerVendorHtaccessCreator.php | 43 + .../composer/ComposerVersionNormalizer.php | 66 + www/wiki/includes/config/Config.php | 47 + www/wiki/includes/config/ConfigException.php | 29 + www/wiki/includes/config/ConfigFactory.php | 155 + www/wiki/includes/config/EtcdConfig.php | 333 + www/wiki/includes/config/EtcdConfigParseError.php | 4 + www/wiki/includes/config/GlobalVarConfig.php | 87 + www/wiki/includes/config/HashConfig.php | 78 + www/wiki/includes/config/MultiConfig.php | 72 + www/wiki/includes/config/MutableConfig.php | 38 + www/wiki/includes/content/AbstractContent.php | 551 ++ www/wiki/includes/content/CodeContentHandler.php | 67 + www/wiki/includes/content/Content.php | 526 ++ www/wiki/includes/content/ContentHandler.php | 1332 +++ www/wiki/includes/content/CssContent.php | 121 + www/wiki/includes/content/CssContentHandler.php | 61 + www/wiki/includes/content/FileContentHandler.php | 65 + www/wiki/includes/content/JavaScriptContent.php | 123 + .../includes/content/JavaScriptContentHandler.php | 62 + www/wiki/includes/content/JsonContent.php | 251 + www/wiki/includes/content/JsonContentHandler.php | 47 + www/wiki/includes/content/MessageContent.php | 174 + www/wiki/includes/content/TextContent.php | 330 + www/wiki/includes/content/TextContentHandler.php | 162 + www/wiki/includes/content/WikiTextStructure.php | 254 + www/wiki/includes/content/WikitextContent.php | 363 + .../includes/content/WikitextContentHandler.php | 163 + www/wiki/includes/context/ContextSource.php | 184 + www/wiki/includes/context/DerivativeContext.php | 301 + www/wiki/includes/context/IContextSource.php | 137 + www/wiki/includes/context/MutableContext.php | 67 + www/wiki/includes/context/RequestContext.php | 616 ++ www/wiki/includes/dao/DBAccessBase.php | 95 + www/wiki/includes/dao/DBAccessObjectUtils.php | 81 + www/wiki/includes/dao/IDBAccessObject.php | 71 + www/wiki/includes/db/CloneDatabase.php | 140 + www/wiki/includes/db/DatabaseOracle.php | 1382 +++ www/wiki/includes/db/MWLBFactory.php | 252 + www/wiki/includes/db/ORAField.php | 53 + www/wiki/includes/db/ORAResult.php | 110 + www/wiki/includes/debug/MWDebug.php | 558 ++ www/wiki/includes/debug/logger/ConsoleLogger.php | 21 + www/wiki/includes/debug/logger/ConsoleSpi.php | 11 + www/wiki/includes/debug/logger/LegacyLogger.php | 482 ++ www/wiki/includes/debug/logger/LegacySpi.php | 57 + www/wiki/includes/debug/logger/LoggerFactory.php | 102 + www/wiki/includes/debug/logger/MonologSpi.php | 271 + www/wiki/includes/debug/logger/NullSpi.php | 60 + www/wiki/includes/debug/logger/Spi.php | 46 + .../debug/logger/monolog/AvroFormatter.php | 171 + .../debug/logger/monolog/BufferHandler.php | 46 + .../includes/debug/logger/monolog/KafkaHandler.php | 279 + .../debug/logger/monolog/LegacyFormatter.php | 47 + .../debug/logger/monolog/LegacyHandler.php | 236 + .../debug/logger/monolog/LineFormatter.php | 172 + .../debug/logger/monolog/LogstashFormatter.php | 111 + .../debug/logger/monolog/SyslogHandler.php | 94 + .../debug/logger/monolog/WikiProcessor.php | 48 + www/wiki/includes/deferred/AtomicSectionUpdate.php | 48 + www/wiki/includes/deferred/AutoCommitUpdate.php | 62 + www/wiki/includes/deferred/CdnCacheUpdate.php | 295 + www/wiki/includes/deferred/DataUpdate.php | 83 + www/wiki/includes/deferred/DeferrableCallback.php | 13 + www/wiki/includes/deferred/DeferrableUpdate.php | 14 + www/wiki/includes/deferred/DeferredUpdates.php | 379 + .../includes/deferred/EnqueueableDataUpdate.php | 15 + www/wiki/includes/deferred/HTMLCacheUpdate.php | 60 + www/wiki/includes/deferred/LinksDeletionUpdate.php | 242 + www/wiki/includes/deferred/LinksUpdate.php | 1182 +++ www/wiki/includes/deferred/MWCallableUpdate.php | 47 + www/wiki/includes/deferred/MergeableUpdate.php | 16 + www/wiki/includes/deferred/SearchUpdate.php | 225 + www/wiki/includes/deferred/SiteStatsUpdate.php | 286 + www/wiki/includes/deferred/SqlDataUpdate.php | 40 + .../deferred/TransactionRoundDefiningUpdate.php | 30 + www/wiki/includes/deferred/WANCacheReapUpdate.php | 133 + www/wiki/includes/diff/ArrayDiffFormatter.php | 82 + www/wiki/includes/diff/ComplexityException.php | 30 + www/wiki/includes/diff/DairikiDiff.php | 334 + www/wiki/includes/diff/DiffEngine.php | 841 ++ www/wiki/includes/diff/DiffFormatter.php | 254 + www/wiki/includes/diff/DifferenceEngine.php | 1558 ++++ www/wiki/includes/diff/TableDiffFormatter.php | 215 + www/wiki/includes/diff/UnifiedDiffFormatter.php | 84 + www/wiki/includes/diff/WordAccumulator.php | 105 + www/wiki/includes/diff/WordLevelDiff.php | 139 + www/wiki/includes/edit/PreparedEdit.php | 90 + www/wiki/includes/editpage/TextConflictHelper.php | 257 + www/wiki/includes/editpage/TextboxBuilder.php | 137 + www/wiki/includes/exception/BadRequestError.php | 34 + www/wiki/includes/exception/BadTitleError.php | 49 + .../exception/CannotCreateActorException.php | 29 + www/wiki/includes/exception/ErrorPageError.php | 72 + www/wiki/includes/exception/FatalError.php | 43 + www/wiki/includes/exception/HttpError.php | 129 + www/wiki/includes/exception/LocalizedException.php | 66 + .../exception/MWContentSerializationException.php | 8 + www/wiki/includes/exception/MWException.php | 230 + www/wiki/includes/exception/MWExceptionHandler.php | 697 ++ .../includes/exception/MWExceptionRenderer.php | 338 + .../exception/MWUnknownContentModelException.php | 25 + www/wiki/includes/exception/PermissionsError.php | 72 + www/wiki/includes/exception/ProcOpenError.php | 29 + www/wiki/includes/exception/ReadOnlyError.php | 36 + www/wiki/includes/exception/ShellDisabledError.php | 32 + www/wiki/includes/exception/ThrottledError.php | 40 + www/wiki/includes/exception/UserBlockedError.php | 33 + www/wiki/includes/exception/UserNotLoggedIn.php | 104 + www/wiki/includes/export/BaseDump.php | 219 + www/wiki/includes/export/Dump7ZipOutput.php | 76 + www/wiki/includes/export/DumpBZip2Output.php | 36 + www/wiki/includes/export/DumpDBZip2Output.php | 36 + www/wiki/includes/export/DumpFileOutput.php | 115 + www/wiki/includes/export/DumpFilter.php | 134 + www/wiki/includes/export/DumpGZipOutput.php | 36 + www/wiki/includes/export/DumpLatestFilter.php | 72 + www/wiki/includes/export/DumpMultiWriter.php | 113 + www/wiki/includes/export/DumpNamespaceFilter.php | 91 + www/wiki/includes/export/DumpNotalkFilter.php | 37 + www/wiki/includes/export/DumpOutput.php | 114 + www/wiki/includes/export/DumpPipeOutput.php | 102 + www/wiki/includes/export/DumpStringOutput.php | 45 + www/wiki/includes/export/ExportProgressFilter.php | 47 + www/wiki/includes/export/WikiExporter.php | 511 ++ www/wiki/includes/export/XmlDumpWriter.php | 449 + www/wiki/includes/externalstore/ExternalStore.php | 254 + .../includes/externalstore/ExternalStoreDB.php | 335 + .../externalstore/ExternalStoreFactory.php | 42 + .../includes/externalstore/ExternalStoreHttp.php | 41 + .../includes/externalstore/ExternalStoreMedium.php | 91 + .../externalstore/ExternalStoreMwstore.php | 105 + www/wiki/includes/filebackend/FileBackendGroup.php | 247 + www/wiki/includes/filebackend/README | 208 + .../filebackend/filejournal/DBFileJournal.php | 193 + .../filebackend/lockmanager/LockManagerGroup.php | 176 + .../filebackend/lockmanager/MySqlLockManager.php | 141 + .../includes/filerepo/FileBackendDBRepoWrapper.php | 360 + www/wiki/includes/filerepo/FileRepo.php | 1955 +++++ www/wiki/includes/filerepo/FileRepoStatus.php | 30 + www/wiki/includes/filerepo/ForeignAPIRepo.php | 605 ++ www/wiki/includes/filerepo/ForeignDBRepo.php | 156 + www/wiki/includes/filerepo/ForeignDBViaLBRepo.php | 109 + www/wiki/includes/filerepo/LocalRepo.php | 595 ++ www/wiki/includes/filerepo/NullRepo.php | 38 + www/wiki/includes/filerepo/README | 41 + www/wiki/includes/filerepo/RepoGroup.php | 472 ++ www/wiki/includes/filerepo/TempFileRepo.php | 9 + www/wiki/includes/filerepo/file/ArchivedFile.php | 626 ++ www/wiki/includes/filerepo/file/File.php | 2320 +++++ www/wiki/includes/filerepo/file/ForeignAPIFile.php | 400 + www/wiki/includes/filerepo/file/ForeignDBFile.php | 203 + www/wiki/includes/filerepo/file/LocalFile.php | 3506 ++++++++ www/wiki/includes/filerepo/file/OldLocalFile.php | 485 ++ .../filerepo/file/UnregisteredLocalFile.php | 232 + www/wiki/includes/gallery/ImageGalleryBase.php | 391 + www/wiki/includes/gallery/NolinesImageGallery.php | 37 + www/wiki/includes/gallery/PackedImageGallery.php | 112 + .../includes/gallery/PackedOverlayImageGallery.php | 64 + .../includes/gallery/SlideshowImageGallery.php | 41 + .../includes/gallery/TraditionalImageGallery.php | 385 + www/wiki/includes/htmlform/HTMLForm.php | 1904 +++++ www/wiki/includes/htmlform/HTMLFormElement.php | 65 + www/wiki/includes/htmlform/HTMLFormField.php | 1199 +++ .../HTMLFormFieldRequiredOptionsException.php | 9 + .../includes/htmlform/HTMLNestedFilterable.php | 11 + www/wiki/includes/htmlform/OOUIHTMLForm.php | 310 + www/wiki/includes/htmlform/VFormHTMLForm.php | 157 + www/wiki/includes/htmlform/fields/HTMLApiField.php | 23 + .../fields/HTMLAutoCompleteSelectField.php | 197 + .../includes/htmlform/fields/HTMLButtonField.php | 142 + .../includes/htmlform/fields/HTMLCheckField.php | 131 + .../includes/htmlform/fields/HTMLCheckMatrix.php | 269 + .../includes/htmlform/fields/HTMLComboboxField.php | 63 + .../includes/htmlform/fields/HTMLDateTimeField.php | 185 + .../includes/htmlform/fields/HTMLEditTools.php | 50 + .../includes/htmlform/fields/HTMLFloatField.php | 46 + .../htmlform/fields/HTMLFormFieldCloner.php | 397 + .../htmlform/fields/HTMLFormFieldWithButton.php | 75 + .../includes/htmlform/fields/HTMLHiddenField.php | 66 + .../includes/htmlform/fields/HTMLInfoField.php | 81 + www/wiki/includes/htmlform/fields/HTMLIntField.php | 26 + .../htmlform/fields/HTMLMultiSelectField.php | 250 + .../includes/htmlform/fields/HTMLRadioField.php | 111 + .../htmlform/fields/HTMLRestrictionsField.php | 121 + .../htmlform/fields/HTMLSelectAndOtherField.php | 202 + .../includes/htmlform/fields/HTMLSelectField.php | 72 + .../htmlform/fields/HTMLSelectLimitField.php | 35 + .../htmlform/fields/HTMLSelectNamespace.php | 44 + .../fields/HTMLSelectNamespaceWithButton.php | 17 + .../htmlform/fields/HTMLSelectOrOtherField.php | 160 + .../htmlform/fields/HTMLSizeFilterField.php | 89 + .../includes/htmlform/fields/HTMLSubmitField.php | 19 + .../includes/htmlform/fields/HTMLTagFilter.php | 50 + .../includes/htmlform/fields/HTMLTextAreaField.php | 140 + .../includes/htmlform/fields/HTMLTextField.php | 206 + .../htmlform/fields/HTMLTextFieldWithButton.php | 17 + .../htmlform/fields/HTMLTitleTextField.php | 107 + .../includes/htmlform/fields/HTMLUserTextField.php | 116 + .../htmlform/fields/HTMLUsersMultiselectField.php | 99 + www/wiki/includes/http/CurlHttpRequest.php | 161 + www/wiki/includes/http/Http.php | 184 + www/wiki/includes/http/HttpRequestFactory.php | 82 + www/wiki/includes/http/MWHttpRequest.php | 646 ++ www/wiki/includes/http/PhpHttpRequest.php | 248 + www/wiki/includes/import/ImportSource.php | 51 + www/wiki/includes/import/ImportStreamSource.php | 183 + www/wiki/includes/import/ImportStringSource.php | 57 + www/wiki/includes/import/ImportableOldRevision.php | 68 + .../import/ImportableOldRevisionImporter.php | 145 + .../includes/import/ImportableUploadRevision.php | 68 + .../import/ImportableUploadRevisionImporter.php | 155 + www/wiki/includes/import/OldRevisionImporter.php | 17 + .../includes/import/UploadRevisionImporter.php | 18 + www/wiki/includes/import/UploadSourceAdapter.php | 149 + www/wiki/includes/import/WikiImporter.php | 1120 +++ www/wiki/includes/import/WikiRevision.php | 676 ++ www/wiki/includes/installer/CliInstaller.php | 237 + www/wiki/includes/installer/DatabaseInstaller.php | 760 ++ www/wiki/includes/installer/DatabaseUpdater.php | 1290 +++ .../includes/installer/InstallDocFormatter.php | 74 + www/wiki/includes/installer/Installer.php | 1814 ++++ www/wiki/includes/installer/InstallerOverrides.php | 76 + .../installer/InstallerSessionProvider.php | 64 + .../includes/installer/LocalSettingsGenerator.php | 424 + www/wiki/includes/installer/MssqlInstaller.php | 737 ++ www/wiki/includes/installer/MssqlUpdater.php | 169 + www/wiki/includes/installer/MysqlInstaller.php | 668 ++ www/wiki/includes/installer/MysqlUpdater.php | 1232 +++ www/wiki/includes/installer/OracleInstaller.php | 340 + www/wiki/includes/installer/OracleUpdater.php | 353 + www/wiki/includes/installer/PhpBugTests.php | 49 + www/wiki/includes/installer/PostgresInstaller.php | 682 ++ www/wiki/includes/installer/PostgresUpdater.php | 1180 +++ www/wiki/includes/installer/SqliteInstaller.php | 335 + www/wiki/includes/installer/SqliteUpdater.php | 246 + www/wiki/includes/installer/WebInstaller.php | 1251 +++ .../includes/installer/WebInstallerComplete.php | 64 + .../includes/installer/WebInstallerCopying.php | 31 + .../includes/installer/WebInstallerDBConnect.php | 121 + .../includes/installer/WebInstallerDBSettings.php | 54 + .../includes/installer/WebInstallerDocument.php | 49 + .../installer/WebInstallerExistingWiki.php | 191 + .../includes/installer/WebInstallerInstall.php | 95 + .../includes/installer/WebInstallerLanguage.php | 123 + www/wiki/includes/installer/WebInstallerName.php | 263 + .../includes/installer/WebInstallerOptions.php | 565 ++ www/wiki/includes/installer/WebInstallerOutput.php | 348 + www/wiki/includes/installer/WebInstallerPage.php | 207 + www/wiki/includes/installer/WebInstallerReadme.php | 31 + .../installer/WebInstallerReleaseNotes.php | 38 + .../includes/installer/WebInstallerRestart.php | 46 + .../includes/installer/WebInstallerUpgrade.php | 110 + .../includes/installer/WebInstallerUpgradeDoc.php | 31 + .../includes/installer/WebInstallerWelcome.php | 49 + www/wiki/includes/installer/i18n/af.json | 150 + www/wiki/includes/installer/i18n/aln.json | 9 + www/wiki/includes/installer/i18n/am.json | 5 + www/wiki/includes/installer/i18n/an.json | 9 + www/wiki/includes/installer/i18n/ang.json | 9 + www/wiki/includes/installer/i18n/anp.json | 13 + www/wiki/includes/installer/i18n/ar.json | 255 + www/wiki/includes/installer/i18n/arc.json | 22 + www/wiki/includes/installer/i18n/ary.json | 10 + www/wiki/includes/installer/i18n/arz.json | 18 + www/wiki/includes/installer/i18n/as.json | 10 + www/wiki/includes/installer/i18n/ast.json | 205 + www/wiki/includes/installer/i18n/av.json | 11 + www/wiki/includes/installer/i18n/avk.json | 4 + www/wiki/includes/installer/i18n/az.json | 32 + www/wiki/includes/installer/i18n/azb.json | 40 + www/wiki/includes/installer/i18n/ba.json | 316 + www/wiki/includes/installer/i18n/bar.json | 13 + www/wiki/includes/installer/i18n/bcc.json | 5 + www/wiki/includes/installer/i18n/bcl.json | 36 + www/wiki/includes/installer/i18n/be-tarask.json | 324 + www/wiki/includes/installer/i18n/be.json | 21 + www/wiki/includes/installer/i18n/bg.json | 320 + www/wiki/includes/installer/i18n/bgn.json | 33 + www/wiki/includes/installer/i18n/bjn.json | 10 + www/wiki/includes/installer/i18n/bn.json | 147 + www/wiki/includes/installer/i18n/bpy.json | 5 + www/wiki/includes/installer/i18n/br.json | 300 + www/wiki/includes/installer/i18n/bs.json | 184 + www/wiki/includes/installer/i18n/bto.json | 62 + www/wiki/includes/installer/i18n/ca.json | 263 + www/wiki/includes/installer/i18n/ce.json | 93 + www/wiki/includes/installer/i18n/ceb.json | 5 + www/wiki/includes/installer/i18n/ckb.json | 60 + www/wiki/includes/installer/i18n/cps.json | 10 + www/wiki/includes/installer/i18n/crh-cyrl.json | 5 + www/wiki/includes/installer/i18n/crh-latn.json | 5 + www/wiki/includes/installer/i18n/cs.json | 327 + www/wiki/includes/installer/i18n/csb.json | 63 + www/wiki/includes/installer/i18n/cu.json | 14 + www/wiki/includes/installer/i18n/cv.json | 17 + www/wiki/includes/installer/i18n/cy.json | 15 + www/wiki/includes/installer/i18n/da.json | 83 + www/wiki/includes/installer/i18n/de-ch.json | 12 + www/wiki/includes/installer/i18n/de-formal.json | 12 + www/wiki/includes/installer/i18n/de.json | 333 + www/wiki/includes/installer/i18n/diq.json | 96 + www/wiki/includes/installer/i18n/dsb.json | 9 + www/wiki/includes/installer/i18n/dtp.json | 9 + www/wiki/includes/installer/i18n/dty.json | 52 + www/wiki/includes/installer/i18n/el.json | 287 + www/wiki/includes/installer/i18n/eml.json | 14 + www/wiki/includes/installer/i18n/en-gb.json | 25 + www/wiki/includes/installer/i18n/en.json | 314 + www/wiki/includes/installer/i18n/eo.json | 64 + www/wiki/includes/installer/i18n/es-formal.json | 19 + www/wiki/includes/installer/i18n/es.json | 352 + www/wiki/includes/installer/i18n/et.json | 83 + www/wiki/includes/installer/i18n/eu.json | 322 + www/wiki/includes/installer/i18n/ext.json | 5 + www/wiki/includes/installer/i18n/fa.json | 329 + www/wiki/includes/installer/i18n/fi.json | 318 + www/wiki/includes/installer/i18n/fo.json | 53 + www/wiki/includes/installer/i18n/fr.json | 346 + www/wiki/includes/installer/i18n/frc.json | 79 + www/wiki/includes/installer/i18n/frp.json | 147 + www/wiki/includes/installer/i18n/frr.json | 10 + www/wiki/includes/installer/i18n/fur.json | 15 + www/wiki/includes/installer/i18n/fy.json | 21 + www/wiki/includes/installer/i18n/ga.json | 13 + www/wiki/includes/installer/i18n/gag.json | 5 + www/wiki/includes/installer/i18n/gan-hans.json | 5 + www/wiki/includes/installer/i18n/gan-hant.json | 9 + www/wiki/includes/installer/i18n/gd.json | 9 + www/wiki/includes/installer/i18n/gl.json | 324 + www/wiki/includes/installer/i18n/gom-latn.json | 9 + www/wiki/includes/installer/i18n/gor.json | 47 + www/wiki/includes/installer/i18n/grc.json | 11 + www/wiki/includes/installer/i18n/gsw.json | 59 + www/wiki/includes/installer/i18n/gu.json | 45 + www/wiki/includes/installer/i18n/gv.json | 4 + www/wiki/includes/installer/i18n/hak.json | 5 + www/wiki/includes/installer/i18n/haw.json | 64 + www/wiki/includes/installer/i18n/he.json | 324 + www/wiki/includes/installer/i18n/hi.json | 112 + www/wiki/includes/installer/i18n/hif-latn.json | 9 + www/wiki/includes/installer/i18n/hil.json | 9 + www/wiki/includes/installer/i18n/hr.json | 26 + www/wiki/includes/installer/i18n/hrx.json | 296 + www/wiki/includes/installer/i18n/hsb.json | 245 + www/wiki/includes/installer/i18n/hsn.json | 35 + www/wiki/includes/installer/i18n/ht.json | 10 + www/wiki/includes/installer/i18n/hu-formal.json | 31 + www/wiki/includes/installer/i18n/hu.json | 314 + www/wiki/includes/installer/i18n/hy.json | 43 + www/wiki/includes/installer/i18n/ia.json | 321 + www/wiki/includes/installer/i18n/id.json | 324 + www/wiki/includes/installer/i18n/ie.json | 4 + www/wiki/includes/installer/i18n/ig.json | 17 + www/wiki/includes/installer/i18n/ilo.json | 4 + www/wiki/includes/installer/i18n/inh.json | 12 + www/wiki/includes/installer/i18n/io.json | 9 + www/wiki/includes/installer/i18n/is.json | 102 + www/wiki/includes/installer/i18n/it.json | 330 + www/wiki/includes/installer/i18n/ja.json | 340 + www/wiki/includes/installer/i18n/jam.json | 9 + www/wiki/includes/installer/i18n/jbo.json | 12 + www/wiki/includes/installer/i18n/jut.json | 10 + www/wiki/includes/installer/i18n/jv.json | 11 + www/wiki/includes/installer/i18n/ka.json | 101 + www/wiki/includes/installer/i18n/kaa.json | 5 + www/wiki/includes/installer/i18n/kbd-cyrl.json | 10 + www/wiki/includes/installer/i18n/khw.json | 8 + www/wiki/includes/installer/i18n/kiu.json | 9 + www/wiki/includes/installer/i18n/kk-arab.json | 5 + www/wiki/includes/installer/i18n/kk-cyrl.json | 5 + www/wiki/includes/installer/i18n/kk-latn.json | 5 + www/wiki/includes/installer/i18n/km.json | 35 + www/wiki/includes/installer/i18n/kn.json | 48 + www/wiki/includes/installer/i18n/ko.json | 327 + www/wiki/includes/installer/i18n/krc.json | 30 + www/wiki/includes/installer/i18n/ksh.json | 314 + www/wiki/includes/installer/i18n/ku-latn.json | 68 + www/wiki/includes/installer/i18n/lad.json | 10 + www/wiki/includes/installer/i18n/lb.json | 211 + www/wiki/includes/installer/i18n/lez.json | 27 + www/wiki/includes/installer/i18n/lfn.json | 5 + www/wiki/includes/installer/i18n/lg.json | 9 + www/wiki/includes/installer/i18n/li.json | 9 + www/wiki/includes/installer/i18n/lij.json | 311 + www/wiki/includes/installer/i18n/lki.json | 81 + www/wiki/includes/installer/i18n/lo.json | 4 + www/wiki/includes/installer/i18n/lrc.json | 28 + www/wiki/includes/installer/i18n/lt.json | 175 + www/wiki/includes/installer/i18n/lv.json | 70 + www/wiki/includes/installer/i18n/lzh.json | 11 + www/wiki/includes/installer/i18n/lzz.json | 9 + www/wiki/includes/installer/i18n/mai.json | 39 + www/wiki/includes/installer/i18n/mdf.json | 5 + www/wiki/includes/installer/i18n/mfe.json | 45 + www/wiki/includes/installer/i18n/mg.json | 88 + www/wiki/includes/installer/i18n/mhr.json | 4 + www/wiki/includes/installer/i18n/min.json | 11 + www/wiki/includes/installer/i18n/mk.json | 321 + www/wiki/includes/installer/i18n/ml.json | 120 + www/wiki/includes/installer/i18n/mn.json | 10 + www/wiki/includes/installer/i18n/mr.json | 109 + www/wiki/includes/installer/i18n/ms.json | 152 + www/wiki/includes/installer/i18n/mt.json | 90 + www/wiki/includes/installer/i18n/my.json | 10 + www/wiki/includes/installer/i18n/myv.json | 16 + www/wiki/includes/installer/i18n/mzn.json | 58 + www/wiki/includes/installer/i18n/nah.json | 9 + www/wiki/includes/installer/i18n/nan.json | 59 + www/wiki/includes/installer/i18n/nap.json | 308 + www/wiki/includes/installer/i18n/nb.json | 326 + www/wiki/includes/installer/i18n/nds-nl.json | 10 + www/wiki/includes/installer/i18n/nds.json | 14 + www/wiki/includes/installer/i18n/ne.json | 86 + www/wiki/includes/installer/i18n/nl-informal.json | 75 + www/wiki/includes/installer/i18n/nl.json | 338 + www/wiki/includes/installer/i18n/nn.json | 40 + www/wiki/includes/installer/i18n/oc.json | 180 + www/wiki/includes/installer/i18n/olo.json | 60 + www/wiki/includes/installer/i18n/or.json | 48 + www/wiki/includes/installer/i18n/os.json | 9 + www/wiki/includes/installer/i18n/pa.json | 40 + www/wiki/includes/installer/i18n/pam.json | 5 + www/wiki/includes/installer/i18n/pcd.json | 4 + www/wiki/includes/installer/i18n/pdc.json | 13 + www/wiki/includes/installer/i18n/pl.json | 338 + www/wiki/includes/installer/i18n/pms.json | 287 + www/wiki/includes/installer/i18n/pnt.json | 8 + www/wiki/includes/installer/i18n/prg.json | 9 + www/wiki/includes/installer/i18n/ps.json | 92 + www/wiki/includes/installer/i18n/pt-br.json | 339 + www/wiki/includes/installer/i18n/pt.json | 337 + www/wiki/includes/installer/i18n/qqq.json | 335 + www/wiki/includes/installer/i18n/qu.json | 20 + www/wiki/includes/installer/i18n/rgn.json | 4 + www/wiki/includes/installer/i18n/rm.json | 9 + www/wiki/includes/installer/i18n/ro.json | 160 + www/wiki/includes/installer/i18n/roa-tara.json | 70 + www/wiki/includes/installer/i18n/ru.json | 342 + www/wiki/includes/installer/i18n/rue.json | 9 + www/wiki/includes/installer/i18n/sa.json | 8 + www/wiki/includes/installer/i18n/sah.json | 39 + www/wiki/includes/installer/i18n/sc.json | 14 + www/wiki/includes/installer/i18n/scn.json | 5 + www/wiki/includes/installer/i18n/sco.json | 310 + www/wiki/includes/installer/i18n/sd.json | 38 + www/wiki/includes/installer/i18n/sdc.json | 19 + www/wiki/includes/installer/i18n/sei.json | 4 + www/wiki/includes/installer/i18n/sh.json | 12 + www/wiki/includes/installer/i18n/shi.json | 10 + www/wiki/includes/installer/i18n/si.json | 140 + www/wiki/includes/installer/i18n/sk.json | 84 + www/wiki/includes/installer/i18n/sl.json | 183 + www/wiki/includes/installer/i18n/sli.json | 9 + www/wiki/includes/installer/i18n/so.json | 9 + www/wiki/includes/installer/i18n/sq.json | 45 + www/wiki/includes/installer/i18n/sr-ec.json | 142 + www/wiki/includes/installer/i18n/sr-el.json | 44 + www/wiki/includes/installer/i18n/srn.json | 9 + www/wiki/includes/installer/i18n/ss.json | 4 + www/wiki/includes/installer/i18n/stq.json | 9 + www/wiki/includes/installer/i18n/su.json | 16 + www/wiki/includes/installer/i18n/sv.json | 323 + www/wiki/includes/installer/i18n/sw.json | 9 + www/wiki/includes/installer/i18n/szl.json | 9 + www/wiki/includes/installer/i18n/ta.json | 89 + www/wiki/includes/installer/i18n/tcy.json | 51 + www/wiki/includes/installer/i18n/te.json | 233 + www/wiki/includes/installer/i18n/tet.json | 9 + www/wiki/includes/installer/i18n/tg-cyrl.json | 9 + www/wiki/includes/installer/i18n/tg-latn.json | 9 + www/wiki/includes/installer/i18n/th.json | 239 + www/wiki/includes/installer/i18n/tk.json | 9 + www/wiki/includes/installer/i18n/tl.json | 293 + www/wiki/includes/installer/i18n/tly.json | 8 + www/wiki/includes/installer/i18n/tr.json | 257 + www/wiki/includes/installer/i18n/tt-cyrl.json | 71 + www/wiki/includes/installer/i18n/tt-latn.json | 10 + www/wiki/includes/installer/i18n/tyv.json | 8 + www/wiki/includes/installer/i18n/udm.json | 23 + www/wiki/includes/installer/i18n/ug-arab.json | 9 + www/wiki/includes/installer/i18n/uk.json | 327 + www/wiki/includes/installer/i18n/ur.json | 34 + www/wiki/includes/installer/i18n/uz.json | 10 + www/wiki/includes/installer/i18n/vec.json | 10 + www/wiki/includes/installer/i18n/vep.json | 10 + www/wiki/includes/installer/i18n/vi.json | 319 + www/wiki/includes/installer/i18n/vo.json | 5 + www/wiki/includes/installer/i18n/vro.json | 5 + www/wiki/includes/installer/i18n/wa.json | 8 + www/wiki/includes/installer/i18n/war.json | 114 + www/wiki/includes/installer/i18n/wo.json | 9 + www/wiki/includes/installer/i18n/wuu.json | 14 + www/wiki/includes/installer/i18n/xal.json | 9 + www/wiki/includes/installer/i18n/xmf.json | 33 + www/wiki/includes/installer/i18n/yi.json | 75 + www/wiki/includes/installer/i18n/yo.json | 19 + www/wiki/includes/installer/i18n/yue.json | 5 + www/wiki/includes/installer/i18n/zea.json | 9 + www/wiki/includes/installer/i18n/zh-hans.json | 336 + www/wiki/includes/installer/i18n/zh-hant.json | 335 + www/wiki/includes/installer/i18n/zh-hk.json | 8 + www/wiki/includes/installer/i18n/zh-tw.json | 4 + .../includes/interwiki/ClassicInterwikiLookup.php | 452 + www/wiki/includes/interwiki/Interwiki.php | 185 + www/wiki/includes/interwiki/InterwikiLookup.php | 74 + .../includes/interwiki/InterwikiLookupAdapter.php | 178 + .../includes/interwiki/NullInterwikiLookup.php | 57 + www/wiki/includes/jobqueue/Job.php | 426 + www/wiki/includes/jobqueue/JobQueue.php | 731 ++ www/wiki/includes/jobqueue/JobQueueDB.php | 851 ++ www/wiki/includes/jobqueue/JobQueueFederated.php | 496 ++ www/wiki/includes/jobqueue/JobQueueGroup.php | 480 ++ www/wiki/includes/jobqueue/JobQueueMemory.php | 230 + www/wiki/includes/jobqueue/JobQueueRedis.php | 820 ++ .../includes/jobqueue/JobQueueSecondTestQueue.php | 290 + www/wiki/includes/jobqueue/JobRunner.php | 607 ++ www/wiki/includes/jobqueue/JobSpecification.php | 233 + www/wiki/includes/jobqueue/README | 80 + .../jobqueue/aggregator/JobQueueAggregator.php | 180 + .../aggregator/JobQueueAggregatorRedis.php | 135 + .../includes/jobqueue/jobs/ActivityUpdateJob.php | 82 + .../jobqueue/jobs/AssembleUploadChunksJob.php | 138 + .../jobqueue/jobs/CategoryMembershipChangeJob.php | 253 + www/wiki/includes/jobqueue/jobs/CdnPurgeJob.php | 46 + .../jobqueue/jobs/ClearUserWatchlistJob.php | 118 + .../jobs/ClearWatchlistNotificationsJob.php | 79 + www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php | 67 + .../includes/jobqueue/jobs/DoubleRedirectJob.php | 252 + www/wiki/includes/jobqueue/jobs/DuplicateJob.php | 59 + www/wiki/includes/jobqueue/jobs/EmaillingJob.php | 46 + .../includes/jobqueue/jobs/EnotifNotifyJob.php | 57 + www/wiki/includes/jobqueue/jobs/EnqueueJob.php | 98 + .../includes/jobqueue/jobs/HTMLCacheUpdateJob.php | 202 + www/wiki/includes/jobqueue/jobs/NullJob.php | 76 + .../jobqueue/jobs/PublishStashedFileJob.php | 152 + .../jobqueue/jobs/RecentChangesUpdateJob.php | 246 + .../includes/jobqueue/jobs/RefreshLinksJob.php | 320 + .../includes/jobqueue/jobs/ThumbnailRenderJob.php | 111 + .../includes/jobqueue/jobs/UserGroupExpiryJob.php | 39 + .../includes/jobqueue/utils/BacklinkJobUtils.php | 149 + www/wiki/includes/jobqueue/utils/PurgeJobUtils.php | 81 + www/wiki/includes/json/FormatJson.php | 338 + www/wiki/includes/libs/APACHE-LICENSE-2.0.txt | 202 + www/wiki/includes/libs/ArrayUtils.php | 187 + www/wiki/includes/libs/CSSMin.php | 547 ++ www/wiki/includes/libs/Cookie.php | 208 + www/wiki/includes/libs/CookieJar.php | 107 + www/wiki/includes/libs/CryptHKDF.php | 282 + www/wiki/includes/libs/CryptRand.php | 401 + www/wiki/includes/libs/DeferredStringifier.php | 58 + www/wiki/includes/libs/DnsSrvDiscoverer.php | 108 + www/wiki/includes/libs/ExplodeIterator.php | 116 + www/wiki/includes/libs/GenericArrayObject.php | 239 + www/wiki/includes/libs/HashRing.php | 226 + www/wiki/includes/libs/HtmlArmor.php | 57 + www/wiki/includes/libs/HttpStatus.php | 115 + www/wiki/includes/libs/IEUrlExtension.php | 269 + www/wiki/includes/libs/IP.php | 756 ++ www/wiki/includes/libs/JavaScriptMinifier.php | 631 ++ www/wiki/includes/libs/MWCryptHash.php | 114 + www/wiki/includes/libs/MWMessagePack.php | 189 + www/wiki/includes/libs/MapCacheLRU.php | 198 + www/wiki/includes/libs/MappedIterator.php | 116 + www/wiki/includes/libs/MemoizedCallable.php | 158 + www/wiki/includes/libs/MessageSpecifier.php | 39 + www/wiki/includes/libs/MultiHttpClient.php | 445 + www/wiki/includes/libs/ProcessCacheLRU.php | 163 + www/wiki/includes/libs/README | 4 + www/wiki/includes/libs/ReplacementArray.php | 104 + www/wiki/includes/libs/ReverseArrayIterator.php | 76 + www/wiki/includes/libs/RiffExtractor.php | 99 + www/wiki/includes/libs/StatusValue.php | 351 + www/wiki/includes/libs/StringUtils.php | 330 + www/wiki/includes/libs/Timing.php | 193 + www/wiki/includes/libs/UDPTransport.php | 102 + www/wiki/includes/libs/Xhprof.php | 82 + www/wiki/includes/libs/XhprofData.php | 384 + .../includes/libs/composer/ComposerInstalled.php | 38 + www/wiki/includes/libs/composer/ComposerJson.php | 51 + www/wiki/includes/libs/composer/ComposerLock.php | 37 + .../includes/libs/eventrelayer/EventRelayer.php | 66 + .../libs/eventrelayer/EventRelayerKafka.php | 62 + .../libs/eventrelayer/EventRelayerNull.php | 28 + .../includes/libs/filebackend/FSFileBackend.php | 990 +++ www/wiki/includes/libs/filebackend/FileBackend.php | 1638 ++++ .../includes/libs/filebackend/FileBackendError.php | 9 + .../libs/filebackend/FileBackendMultiWrite.php | 758 ++ .../includes/libs/filebackend/FileBackendStore.php | 1976 +++++ www/wiki/includes/libs/filebackend/FileOpBatch.php | 204 + .../includes/libs/filebackend/HTTPFileStreamer.php | 269 + .../libs/filebackend/MemoryFileBackend.php | 262 + .../includes/libs/filebackend/SwiftFileBackend.php | 2004 +++++ .../libs/filebackend/filejournal/FileJournal.php | 199 + .../filebackend/filejournal/NullFileJournal.php | 51 + .../libs/filebackend/fileop/CopyFileOp.php | 96 + .../libs/filebackend/fileop/CreateFileOp.php | 79 + .../libs/filebackend/fileop/DeleteFileOp.php | 71 + .../libs/filebackend/fileop/DescribeFileOp.php | 64 + .../includes/libs/filebackend/fileop/FileOp.php | 469 ++ .../libs/filebackend/fileop/MoveFileOp.php | 106 + .../libs/filebackend/fileop/NullFileOp.php | 28 + .../libs/filebackend/fileop/StoreFileOp.php | 93 + .../includes/libs/filebackend/fsfile/FSFile.php | 223 + .../libs/filebackend/fsfile/TempFSFile.php | 196 + .../includes/libs/http/HttpAcceptNegotiator.php | 138 + www/wiki/includes/libs/http/HttpAcceptParser.php | 78 + .../includes/libs/iterators/IteratorDecorator.php | 50 + .../libs/iterators/NotRecursiveIterator.php | 35 + www/wiki/includes/libs/jsminplus.php | 2132 +++++ .../includes/libs/lockmanager/DBLockManager.php | 231 + .../includes/libs/lockmanager/FSLockManager.php | 253 + www/wiki/includes/libs/lockmanager/LockManager.php | 267 + .../includes/libs/lockmanager/MemcLockManager.php | 356 + .../includes/libs/lockmanager/NullLockManager.php | 36 + .../libs/lockmanager/PostgreSqlLockManager.php | 82 + .../libs/lockmanager/QuorumLockManager.php | 281 + .../includes/libs/lockmanager/RedisLockManager.php | 276 + www/wiki/includes/libs/lockmanager/ScopedLock.php | 106 + www/wiki/includes/libs/mime/IEContentAnalyzer.php | 851 ++ www/wiki/includes/libs/mime/MimeAnalyzer.php | 1200 +++ www/wiki/includes/libs/mime/XmlTypeCheck.php | 503 ++ www/wiki/includes/libs/mime/defines.php | 48 + www/wiki/includes/libs/mime/mime.info | 123 + www/wiki/includes/libs/mime/mime.types | 190 + .../includes/libs/objectcache/APCBagOStuff.php | 120 + .../includes/libs/objectcache/APCUBagOStuff.php | 89 + www/wiki/includes/libs/objectcache/BagOStuff.php | 818 ++ .../includes/libs/objectcache/CachedBagOStuff.php | 121 + .../includes/libs/objectcache/EmptyBagOStuff.php | 49 + .../includes/libs/objectcache/HashBagOStuff.php | 118 + .../includes/libs/objectcache/IExpiringStore.php | 59 + .../libs/objectcache/MemcachedBagOStuff.php | 192 + .../includes/libs/objectcache/MemcachedClient.php | 1314 +++ .../libs/objectcache/MemcachedPeclBagOStuff.php | 270 + .../libs/objectcache/MemcachedPhpBagOStuff.php | 73 + .../libs/objectcache/MultiWriteBagOStuff.php | 244 + .../includes/libs/objectcache/RESTBagOStuff.php | 137 + .../includes/libs/objectcache/RedisBagOStuff.php | 436 + .../libs/objectcache/ReplicatedBagOStuff.php | 131 + .../includes/libs/objectcache/WANObjectCache.php | 2188 +++++ .../libs/objectcache/WANObjectCacheReaper.php | 204 + .../libs/objectcache/WinCacheBagOStuff.php | 64 + .../includes/libs/rdbms/ChronologyProtector.php | 336 + .../includes/libs/rdbms/TransactionProfiler.php | 351 + .../rdbms/connectionmanager/ConnectionManager.php | 155 + .../SessionConsistentConnectionManager.php | 112 + .../rdbms/database/AtomicSectionIdentifier.php | 27 + .../includes/libs/rdbms/database/DBConnRef.php | 634 ++ www/wiki/includes/libs/rdbms/database/Database.php | 4565 ++++++++++ .../libs/rdbms/database/DatabaseDomain.php | 209 + .../includes/libs/rdbms/database/DatabaseMssql.php | 1421 ++++ .../libs/rdbms/database/DatabaseMysqlBase.php | 1577 ++++ .../libs/rdbms/database/DatabaseMysqli.php | 344 + .../libs/rdbms/database/DatabasePostgres.php | 1459 ++++ .../libs/rdbms/database/DatabaseSqlite.php | 1105 +++ .../includes/libs/rdbms/database/IDatabase.php | 2091 +++++ .../libs/rdbms/database/IMaintainableDatabase.php | 311 + .../libs/rdbms/database/MaintainableDBConnRef.php | 97 + .../libs/rdbms/database/position/DBMasterPos.php | 38 + .../rdbms/database/position/MySQLMasterPos.php | 326 + .../database/resultwrapper/FakeResultWrapper.php | 65 + .../database/resultwrapper/IResultWrapper.php | 81 + .../database/resultwrapper/MssqlResultWrapper.php | 76 + .../rdbms/database/resultwrapper/ResultWrapper.php | 122 + .../rdbms/database/utils/NextSequenceValue.php | 12 + .../rdbms/database/utils/SavepointPostgres.php | 105 + www/wiki/includes/libs/rdbms/defines.php | 27 + www/wiki/includes/libs/rdbms/encasing/Blob.php | 21 + www/wiki/includes/libs/rdbms/encasing/IBlob.php | 14 + .../includes/libs/rdbms/encasing/LikeMatch.php | 31 + .../includes/libs/rdbms/encasing/MssqlBlob.php | 41 + .../includes/libs/rdbms/encasing/PostgresBlob.php | 7 + www/wiki/includes/libs/rdbms/encasing/Subquery.php | 44 + .../libs/rdbms/exception/DBAccessError.php | 34 + .../libs/rdbms/exception/DBConnectionError.php | 41 + www/wiki/includes/libs/rdbms/exception/DBError.php | 46 + .../libs/rdbms/exception/DBExpectedError.php | 58 + .../includes/libs/rdbms/exception/DBQueryError.php | 70 + .../libs/rdbms/exception/DBQueryTimeoutError.php | 38 + .../libs/rdbms/exception/DBReadOnlyError.php | 30 + .../rdbms/exception/DBReplicationWaitError.php | 31 + .../libs/rdbms/exception/DBTransactionError.php | 30 + .../rdbms/exception/DBTransactionSizeError.php | 33 + .../rdbms/exception/DBTransactionStateError.php | 28 + .../libs/rdbms/exception/DBUnexpectedError.php | 30 + www/wiki/includes/libs/rdbms/field/Field.php | 35 + www/wiki/includes/libs/rdbms/field/MssqlField.php | 40 + www/wiki/includes/libs/rdbms/field/MySQLField.php | 108 + .../includes/libs/rdbms/field/PostgresField.php | 114 + www/wiki/includes/libs/rdbms/field/SQLiteField.php | 42 + .../includes/libs/rdbms/lbfactory/ILBFactory.php | 357 + .../includes/libs/rdbms/lbfactory/LBFactory.php | 626 ++ .../libs/rdbms/lbfactory/LBFactoryMulti.php | 431 + .../libs/rdbms/lbfactory/LBFactorySimple.php | 162 + .../libs/rdbms/lbfactory/LBFactorySingle.php | 109 + .../libs/rdbms/loadbalancer/ILoadBalancer.php | 647 ++ .../libs/rdbms/loadbalancer/LoadBalancer.php | 1864 +++++ .../libs/rdbms/loadbalancer/LoadBalancerSingle.php | 80 + .../libs/rdbms/loadmonitor/ILoadMonitor.php | 66 + .../libs/rdbms/loadmonitor/LoadMonitor.php | 252 + .../libs/rdbms/loadmonitor/LoadMonitorMySQL.php | 73 + .../libs/rdbms/loadmonitor/LoadMonitorNull.php | 46 + www/wiki/includes/libs/redis/RedisConnRef.php | 181 + .../includes/libs/redis/RedisConnectionPool.php | 410 + .../includes/libs/replacers/DoubleReplacer.php | 43 + .../includes/libs/replacers/HashtableReplacer.php | 43 + .../includes/libs/replacers/RegexlikeReplacer.php | 46 + www/wiki/includes/libs/replacers/Replacer.php | 38 + .../libs/stats/BufferingStatsdDataFactory.php | 121 + .../libs/stats/IBufferingStatsdDataFactory.php | 42 + .../includes/libs/stats/NullStatsdDataFactory.php | 127 + .../includes/libs/stats/SamplingStatsdClient.php | 157 + .../includes/libs/stats/StatsdAwareInterface.php | 21 + .../libs/virtualrest/ParsoidVirtualRESTService.php | 227 + .../virtualrest/RestbaseVirtualRESTService.php | 282 + .../libs/virtualrest/SwiftVirtualRESTService.php | 179 + .../libs/virtualrest/VirtualRESTService.php | 117 + .../libs/virtualrest/VirtualRESTServiceClient.php | 321 + www/wiki/includes/libs/xmp/XMP.php | 1443 ++++ www/wiki/includes/libs/xmp/XMPInfo.php | 1168 +++ www/wiki/includes/libs/xmp/XMPValidate.php | 399 + .../includes/linkeddata/PageDataRequestHandler.php | 188 + www/wiki/includes/linker/LinkRenderer.php | 480 ++ www/wiki/includes/linker/LinkRendererFactory.php | 95 + www/wiki/includes/linker/LinkTarget.php | 116 + www/wiki/includes/logging/BlockLogFormatter.php | 232 + .../includes/logging/ContentModelLogFormatter.php | 34 + www/wiki/includes/logging/DeleteLogFormatter.php | 313 + www/wiki/includes/logging/ImportLogFormatter.php | 42 + www/wiki/includes/logging/LogEntry.php | 865 ++ www/wiki/includes/logging/LogEventsList.php | 797 ++ www/wiki/includes/logging/LogFormatter.php | 993 +++ www/wiki/includes/logging/LogPage.php | 488 ++ www/wiki/includes/logging/LogPager.php | 462 + www/wiki/includes/logging/MergeLogFormatter.php | 91 + www/wiki/includes/logging/MoveLogFormatter.php | 113 + www/wiki/includes/logging/NewUsersLogFormatter.php | 68 + www/wiki/includes/logging/PageLangLogFormatter.php | 61 + www/wiki/includes/logging/PatrolLog.php | 89 + www/wiki/includes/logging/PatrolLogFormatter.php | 88 + www/wiki/includes/logging/ProtectLogFormatter.php | 214 + www/wiki/includes/logging/RightsLogFormatter.php | 240 + www/wiki/includes/logging/TagLogFormatter.php | 53 + www/wiki/includes/logging/UploadLogFormatter.php | 49 + www/wiki/includes/logging/WikitextLogFormatter.php | 35 + www/wiki/includes/mail/EmailNotification.php | 513 ++ www/wiki/includes/mail/MailAddress.php | 107 + www/wiki/includes/mail/UserMailer.php | 530 ++ www/wiki/includes/media/BMP.php | 80 + www/wiki/includes/media/Bitmap.php | 596 ++ www/wiki/includes/media/BitmapMetadataHandler.php | 316 + www/wiki/includes/media/Bitmap_ClientOnly.php | 59 + www/wiki/includes/media/DjVu.php | 464 + www/wiki/includes/media/DjVuImage.php | 408 + www/wiki/includes/media/Exif.php | 858 ++ www/wiki/includes/media/ExifBitmap.php | 245 + www/wiki/includes/media/FormatMetadata.php | 1895 +++++ www/wiki/includes/media/GIF.php | 211 + www/wiki/includes/media/GIFMetadataExtractor.php | 347 + www/wiki/includes/media/IPTC.php | 601 ++ www/wiki/includes/media/ImageHandler.php | 288 + www/wiki/includes/media/Jpeg.php | 290 + www/wiki/includes/media/JpegMetadataExtractor.php | 295 + www/wiki/includes/media/MediaHandler.php | 926 ++ www/wiki/includes/media/MediaHandlerFactory.php | 101 + .../MediaTransformInvalidParametersException.php | 27 + www/wiki/includes/media/MediaTransformOutput.php | 524 ++ www/wiki/includes/media/PNG.php | 203 + www/wiki/includes/media/PNGMetadataExtractor.php | 428 + www/wiki/includes/media/SVG.php | 593 ++ www/wiki/includes/media/SVGMetadataExtractor.php | 396 + www/wiki/includes/media/Tiff.php | 107 + .../media/TransformationalImageHandler.php | 623 ++ www/wiki/includes/media/WebP.php | 309 + www/wiki/includes/media/XCF.php | 228 + www/wiki/includes/media/tinyrgb.icc | Bin 0 -> 524 bytes www/wiki/includes/objectcache/ObjectCache.php | 414 + www/wiki/includes/objectcache/SqlBagOStuff.php | 825 ++ www/wiki/includes/page/Article.php | 2667 ++++++ www/wiki/includes/page/CategoryPage.php | 128 + www/wiki/includes/page/ImageHistoryList.php | 326 + www/wiki/includes/page/ImageHistoryPseudoPager.php | 228 + www/wiki/includes/page/ImagePage.php | 1229 +++ www/wiki/includes/page/Page.php | 25 + www/wiki/includes/page/PageArchive.php | 752 ++ www/wiki/includes/page/WikiCategoryPage.php | 64 + www/wiki/includes/page/WikiFilePage.php | 259 + www/wiki/includes/page/WikiPage.php | 3768 +++++++++ www/wiki/includes/pager/AlphabeticPager.php | 108 + www/wiki/includes/pager/IndexPager.php | 742 ++ www/wiki/includes/pager/Pager.php | 35 + .../includes/pager/RangeChronologicalPager.php | 114 + .../includes/pager/ReverseChronologicalPager.php | 178 + www/wiki/includes/pager/TablePager.php | 474 ++ www/wiki/includes/parser/BlockLevelPass.php | 572 ++ www/wiki/includes/parser/CacheTime.php | 175 + www/wiki/includes/parser/CoreParserFunctions.php | 1352 +++ www/wiki/includes/parser/CoreTagHooks.php | 176 + www/wiki/includes/parser/DateFormatter.php | 391 + www/wiki/includes/parser/LinkHolderArray.php | 644 ++ www/wiki/includes/parser/MWTidy.php | 145 + www/wiki/includes/parser/Parser.php | 6181 ++++++++++++++ www/wiki/includes/parser/ParserCache.php | 354 + www/wiki/includes/parser/ParserDiffTest.php | 121 + www/wiki/includes/parser/ParserOptions.php | 1395 ++++ www/wiki/includes/parser/ParserOutput.php | 1219 +++ www/wiki/includes/parser/Preprocessor.php | 436 + www/wiki/includes/parser/Preprocessor_DOM.php | 2054 +++++ www/wiki/includes/parser/Preprocessor_Hash.php | 2258 +++++ www/wiki/includes/parser/RemexStripTagHandler.php | 40 + www/wiki/includes/parser/Sanitizer.php | 2123 +++++ www/wiki/includes/parser/StripState.php | 297 + www/wiki/includes/password/BcryptPassword.php | 88 + www/wiki/includes/password/EncryptedPassword.php | 121 + www/wiki/includes/password/InvalidPassword.php | 47 + .../password/LayeredParameterizedPassword.php | 140 + www/wiki/includes/password/MWOldPassword.php | 54 + www/wiki/includes/password/MWSaltedPassword.php | 50 + .../includes/password/ParameterizedPassword.php | 121 + www/wiki/includes/password/Password.php | 209 + www/wiki/includes/password/PasswordError.php | 28 + www/wiki/includes/password/PasswordFactory.php | 224 + .../includes/password/PasswordPolicyChecks.php | 167 + www/wiki/includes/password/Pbkdf2Password.php | 97 + www/wiki/includes/password/UserPasswordPolicy.php | 196 + www/wiki/includes/poolcounter/PoolCounter.php | 230 + www/wiki/includes/poolcounter/PoolCounterRedis.php | 434 + www/wiki/includes/poolcounter/PoolCounterWork.php | 160 + .../poolcounter/PoolCounterWorkViaCallback.php | 92 + .../includes/poolcounter/PoolWorkArticleView.php | 220 + .../preferences/DefaultPreferencesFactory.php | 1741 ++++ .../includes/preferences/PreferencesFactory.php | 83 + www/wiki/includes/profiler/Profiler.php | 321 + www/wiki/includes/profiler/ProfilerSectionOnly.php | 103 + www/wiki/includes/profiler/ProfilerStub.php | 48 + www/wiki/includes/profiler/ProfilerXhprof.php | 239 + www/wiki/includes/profiler/SectionProfiler.php | 524 ++ .../includes/profiler/output/ProfilerOutput.php | 56 + .../includes/profiler/output/ProfilerOutputDb.php | 106 + .../profiler/output/ProfilerOutputDump.php | 55 + .../profiler/output/ProfilerOutputStats.php | 56 + .../profiler/output/ProfilerOutputText.php | 77 + www/wiki/includes/rcfeed/FormattedRCFeed.php | 68 + .../rcfeed/IRCColourfulRCFeedFormatter.php | 143 + www/wiki/includes/rcfeed/JSONRCFeedFormatter.php | 32 + .../rcfeed/MachineReadableRCFeedFormatter.php | 132 + www/wiki/includes/rcfeed/RCFeed.php | 59 + www/wiki/includes/rcfeed/RCFeedEngine.php | 27 + www/wiki/includes/rcfeed/RCFeedFormatter.php | 39 + www/wiki/includes/rcfeed/RedisPubSubFeedEngine.php | 75 + www/wiki/includes/rcfeed/UDPRCFeedEngine.php | 36 + www/wiki/includes/rcfeed/XMLRCFeedFormatter.php | 29 + .../registration/ExtensionDependencyError.php | 81 + .../registration/ExtensionJsonValidationError.php | 22 + .../registration/ExtensionJsonValidator.php | 119 + .../includes/registration/ExtensionProcessor.php | 549 ++ .../includes/registration/ExtensionRegistry.php | 424 + www/wiki/includes/registration/Processor.php | 53 + www/wiki/includes/registration/VersionChecker.php | 236 + .../DerivativeResourceLoaderContext.php | 199 + .../includes/resourceloader/ResourceLoader.php | 1731 ++++ .../resourceloader/ResourceLoaderClientHtml.php | 472 ++ .../resourceloader/ResourceLoaderContext.php | 395 + .../ResourceLoaderEditToolbarModule.php | 44 + .../resourceloader/ResourceLoaderFileModule.php | 1038 +++ .../resourceloader/ResourceLoaderFilePath.php | 71 + .../ResourceLoaderForeignApiModule.php | 33 + .../resourceloader/ResourceLoaderImage.php | 413 + .../resourceloader/ResourceLoaderImageModule.php | 471 ++ .../ResourceLoaderJqueryMsgModule.php | 82 + .../ResourceLoaderLanguageDataModule.php | 81 + .../ResourceLoaderLanguageNamesModule.php | 77 + .../ResourceLoaderMediaWikiUtilModule.php | 53 + .../resourceloader/ResourceLoaderModule.php | 1044 +++ .../ResourceLoaderOOUIFileModule.php | 98 + .../ResourceLoaderOOUIImageModule.php | 110 + .../resourceloader/ResourceLoaderOOUIModule.php | 146 + .../resourceloader/ResourceLoaderRawFileModule.php | 52 + .../resourceloader/ResourceLoaderSiteModule.php | 52 + .../ResourceLoaderSiteStylesModule.php | 60 + .../resourceloader/ResourceLoaderSkinModule.php | 167 + .../ResourceLoaderSpecialCharacterDataModule.php | 102 + .../resourceloader/ResourceLoaderStartUpModule.php | 458 + .../ResourceLoaderUploadDialogModule.php | 49 + .../ResourceLoaderUserDefaultsModule.php | 49 + .../resourceloader/ResourceLoaderUserModule.php | 87 + .../ResourceLoaderUserOptionsModule.php | 83 + .../ResourceLoaderUserStylesModule.php | 86 + .../ResourceLoaderUserTokensModule.php | 76 + .../resourceloader/ResourceLoaderWikiModule.php | 473 ++ .../includes/revisiondelete/RevDelArchiveItem.php | 109 + .../includes/revisiondelete/RevDelArchiveList.php | 86 + .../revisiondelete/RevDelArchivedFileItem.php | 146 + .../revisiondelete/RevDelArchivedFileList.php | 60 + .../revisiondelete/RevDelArchivedRevisionItem.php | 53 + .../includes/revisiondelete/RevDelFileItem.php | 250 + .../includes/revisiondelete/RevDelFileList.php | 134 + www/wiki/includes/revisiondelete/RevDelItem.php | 82 + www/wiki/includes/revisiondelete/RevDelList.php | 440 + www/wiki/includes/revisiondelete/RevDelLogItem.php | 150 + www/wiki/includes/revisiondelete/RevDelLogList.php | 108 + .../includes/revisiondelete/RevDelRevisionItem.php | 213 + .../includes/revisiondelete/RevDelRevisionList.php | 184 + .../includes/revisiondelete/RevisionDeleteUser.php | 208 + .../includes/revisiondelete/RevisionDeleter.php | 251 + www/wiki/includes/search/AugmentPageProps.php | 20 + .../search/DummySearchIndexFieldDefinition.php | 30 + www/wiki/includes/search/NullIndexField.php | 52 + .../search/ParserOutputSearchDataExtractor.php | 96 + www/wiki/includes/search/PerRowAugmentor.php | 37 + www/wiki/includes/search/ResultAugmentor.php | 13 + www/wiki/includes/search/ResultSetAugmentor.php | 13 + www/wiki/includes/search/SearchDatabase.php | 61 + www/wiki/includes/search/SearchEngine.php | 812 ++ www/wiki/includes/search/SearchEngineConfig.php | 117 + www/wiki/includes/search/SearchEngineFactory.php | 65 + .../includes/search/SearchExactMatchRescorer.php | 144 + www/wiki/includes/search/SearchHighlighter.php | 566 ++ www/wiki/includes/search/SearchIndexField.php | 98 + .../includes/search/SearchIndexFieldDefinition.php | 153 + www/wiki/includes/search/SearchMssql.php | 210 + www/wiki/includes/search/SearchMySQL.php | 458 + .../includes/search/SearchNearMatchResultSet.php | 30 + www/wiki/includes/search/SearchNearMatcher.php | 167 + www/wiki/includes/search/SearchOracle.php | 276 + www/wiki/includes/search/SearchPostgres.php | 192 + www/wiki/includes/search/SearchResult.php | 283 + www/wiki/includes/search/SearchResultSet.php | 279 + www/wiki/includes/search/SearchSqlite.php | 312 + www/wiki/includes/search/SearchSuggestion.php | 185 + www/wiki/includes/search/SearchSuggestionSet.php | 212 + www/wiki/includes/search/SqlSearchResultSet.php | 69 + .../CannotReplaceActiveServiceException.php | 43 + .../services/ContainerDisabledException.php | 42 + www/wiki/includes/services/DestructibleService.php | 45 + .../includes/services/NoSuchServiceException.php | 43 + www/wiki/includes/services/SalvageableService.php | 58 + .../services/ServiceAlreadyDefinedException.php | 45 + www/wiki/includes/services/ServiceContainer.php | 378 + .../includes/services/ServiceDisabledException.php | 43 + .../session/BotPasswordSessionProvider.php | 189 + .../includes/session/CookieSessionProvider.php | 439 + .../session/ImmutableSessionProviderWithCookie.php | 153 + .../includes/session/MetadataMergeException.php | 70 + www/wiki/includes/session/PHPSessionHandler.php | 397 + www/wiki/includes/session/Session.php | 699 ++ www/wiki/includes/session/SessionBackend.php | 772 ++ www/wiki/includes/session/SessionId.php | 70 + www/wiki/includes/session/SessionInfo.php | 288 + www/wiki/includes/session/SessionManager.php | 969 +++ .../includes/session/SessionManagerInterface.php | 109 + www/wiki/includes/session/SessionProvider.php | 533 ++ .../includes/session/SessionProviderInterface.php | 54 + www/wiki/includes/session/Token.php | 125 + www/wiki/includes/session/UserInfo.php | 187 + www/wiki/includes/shell/Command.php | 555 ++ www/wiki/includes/shell/CommandFactory.php | 114 + www/wiki/includes/shell/FirejailCommand.php | 160 + www/wiki/includes/shell/Result.php | 76 + www/wiki/includes/shell/Shell.php | 251 + www/wiki/includes/shell/firejail.profile | 7 + www/wiki/includes/shell/limit.sh | 122 + www/wiki/includes/site/CachingSiteStore.php | 199 + www/wiki/includes/site/DBSiteStore.php | 284 + www/wiki/includes/site/FileBasedSiteLookup.php | 139 + www/wiki/includes/site/HashSiteStore.php | 124 + .../includes/site/MediaWikiPageNameNormalizer.php | 214 + www/wiki/includes/site/MediaWikiSite.php | 216 + www/wiki/includes/site/Site.php | 703 ++ www/wiki/includes/site/SiteExporter.php | 114 + www/wiki/includes/site/SiteImporter.php | 263 + www/wiki/includes/site/SiteList.php | 352 + www/wiki/includes/site/SiteLookup.php | 50 + www/wiki/includes/site/SiteSQLStore.php | 60 + www/wiki/includes/site/SiteStore.php | 58 + www/wiki/includes/site/SitesCacheFileBuilder.php | 113 + www/wiki/includes/skins/BaseTemplate.php | 767 ++ www/wiki/includes/skins/MediaWikiI18N.php | 60 + www/wiki/includes/skins/QuickTemplate.php | 207 + www/wiki/includes/skins/Skin.php | 1640 ++++ www/wiki/includes/skins/SkinApi.php | 69 + www/wiki/includes/skins/SkinApiTemplate.php | 61 + www/wiki/includes/skins/SkinException.php | 29 + www/wiki/includes/skins/SkinFactory.php | 103 + www/wiki/includes/skins/SkinFallback.php | 36 + www/wiki/includes/skins/SkinFallbackTemplate.php | 122 + www/wiki/includes/skins/SkinTemplate.php | 1404 ++++ www/wiki/includes/sparql/SparqlClient.php | 220 + www/wiki/includes/sparql/SparqlException.php | 30 + .../specialpage/AuthManagerSpecialPage.php | 766 ++ .../specialpage/ChangesListSpecialPage.php | 1936 +++++ www/wiki/includes/specialpage/FormSpecialPage.php | 241 + www/wiki/includes/specialpage/ImageQueryPage.php | 82 + .../includes/specialpage/IncludableSpecialPage.php | 39 + .../specialpage/LoginSignupSpecialPage.php | 1597 ++++ www/wiki/includes/specialpage/PageQueryPage.php | 65 + www/wiki/includes/specialpage/QueryPage.php | 874 ++ .../includes/specialpage/RedirectSpecialPage.php | 235 + www/wiki/includes/specialpage/SpecialPage.php | 922 ++ .../includes/specialpage/SpecialPageFactory.php | 709 ++ .../includes/specialpage/UnlistedSpecialPage.php | 37 + www/wiki/includes/specialpage/WantedQueryPage.php | 157 + www/wiki/includes/specials/SpecialActiveusers.php | 172 + www/wiki/includes/specials/SpecialAllMessages.php | 74 + www/wiki/includes/specials/SpecialAllPages.php | 384 + www/wiki/includes/specials/SpecialAncientpages.php | 93 + www/wiki/includes/specials/SpecialApiHelp.php | 98 + www/wiki/includes/specials/SpecialApiSandbox.php | 59 + .../includes/specials/SpecialAutoblockList.php | 167 + www/wiki/includes/specials/SpecialBlankpage.php | 39 + www/wiki/includes/specials/SpecialBlock.php | 1038 +++ www/wiki/includes/specials/SpecialBlockList.php | 225 + www/wiki/includes/specials/SpecialBooksources.php | 214 + www/wiki/includes/specials/SpecialBotPasswords.php | 367 + .../includes/specials/SpecialBrokenRedirects.php | 179 + www/wiki/includes/specials/SpecialCachedPage.php | 201 + www/wiki/includes/specials/SpecialCategories.php | 65 + .../specials/SpecialChangeContentModel.php | 296 + .../includes/specials/SpecialChangeCredentials.php | 267 + www/wiki/includes/specials/SpecialChangeEmail.php | 206 + .../includes/specials/SpecialChangePassword.php | 36 + www/wiki/includes/specials/SpecialComparePages.php | 174 + www/wiki/includes/specials/SpecialConfirmemail.php | 168 + .../includes/specials/SpecialContributions.php | 780 ++ .../includes/specials/SpecialCreateAccount.php | 173 + www/wiki/includes/specials/SpecialDeadendpages.php | 94 + .../specials/SpecialDeletedContributions.php | 243 + www/wiki/includes/specials/SpecialDiff.php | 119 + .../includes/specials/SpecialDoubleRedirects.php | 233 + www/wiki/includes/specials/SpecialEditTags.php | 473 ++ .../includes/specials/SpecialEditWatchlist.php | 767 ++ .../includes/specials/SpecialEmailInvalidate.php | 75 + www/wiki/includes/specials/SpecialEmailuser.php | 525 ++ .../includes/specials/SpecialExpandTemplates.php | 301 + www/wiki/includes/specials/SpecialExport.php | 593 ++ .../includes/specials/SpecialFewestrevisions.php | 105 + .../specials/SpecialFileDuplicateSearch.php | 267 + www/wiki/includes/specials/SpecialFilepath.php | 55 + .../includes/specials/SpecialGoToInterwiki.php | 79 + www/wiki/includes/specials/SpecialImport.php | 566 ++ .../includes/specials/SpecialJavaScriptTest.php | 205 + www/wiki/includes/specials/SpecialLinkAccounts.php | 111 + www/wiki/includes/specials/SpecialLinkSearch.php | 274 + .../specials/SpecialListDuplicatedFiles.php | 106 + www/wiki/includes/specials/SpecialListfiles.php | 83 + www/wiki/includes/specials/SpecialListgrants.php | 91 + .../includes/specials/SpecialListgrouprights.php | 294 + .../includes/specials/SpecialListredirects.php | 151 + www/wiki/includes/specials/SpecialListusers.php | 101 + www/wiki/includes/specials/SpecialLockdb.php | 118 + www/wiki/includes/specials/SpecialLog.php | 327 + www/wiki/includes/specials/SpecialLonelypages.php | 102 + www/wiki/includes/specials/SpecialLongpages.php | 40 + www/wiki/includes/specials/SpecialMIMEsearch.php | 241 + .../includes/specials/SpecialMediaStatistics.php | 371 + www/wiki/includes/specials/SpecialMergeHistory.php | 385 + .../includes/specials/SpecialMostcategories.php | 112 + www/wiki/includes/specials/SpecialMostimages.php | 67 + .../includes/specials/SpecialMostinterwikis.php | 115 + www/wiki/includes/specials/SpecialMostlinked.php | 135 + .../specials/SpecialMostlinkedcategories.php | 98 + .../specials/SpecialMostlinkedtemplates.php | 132 + .../includes/specials/SpecialMostrevisions.php | 39 + www/wiki/includes/specials/SpecialMovepage.php | 872 ++ www/wiki/includes/specials/SpecialMyLanguage.php | 138 + .../includes/specials/SpecialMyRedirectPages.php | 185 + www/wiki/includes/specials/SpecialNewimages.php | 230 + www/wiki/includes/specials/SpecialNewpages.php | 518 ++ www/wiki/includes/specials/SpecialPageData.php | 107 + www/wiki/includes/specials/SpecialPageLanguage.php | 299 + .../includes/specials/SpecialPagesWithProp.php | 240 + .../includes/specials/SpecialPasswordReset.php | 173 + .../includes/specials/SpecialPermanentLink.php | 82 + www/wiki/includes/specials/SpecialPreferences.php | 173 + www/wiki/includes/specials/SpecialPrefixindex.php | 319 + .../includes/specials/SpecialProtectedpages.php | 207 + .../includes/specials/SpecialProtectedtitles.php | 177 + .../includes/specials/SpecialRandomInCategory.php | 315 + www/wiki/includes/specials/SpecialRandompage.php | 180 + .../includes/specials/SpecialRandomredirect.php | 35 + .../includes/specials/SpecialRandomrootpage.php | 39 + .../includes/specials/SpecialRecentchanges.php | 956 +++ .../specials/SpecialRecentchangeslinked.php | 314 + www/wiki/includes/specials/SpecialRedirect.php | 326 + .../includes/specials/SpecialRemoveCredentials.php | 26 + www/wiki/includes/specials/SpecialResetTokens.php | 156 + .../includes/specials/SpecialRevisiondelete.php | 689 ++ www/wiki/includes/specials/SpecialRunJobs.php | 121 + www/wiki/includes/specials/SpecialSearch.php | 718 ++ www/wiki/includes/specials/SpecialShortpages.php | 178 + www/wiki/includes/specials/SpecialSpecialpages.php | 158 + www/wiki/includes/specials/SpecialStatistics.php | 307 + www/wiki/includes/specials/SpecialTags.php | 482 ++ .../specials/SpecialTrackingCategories.php | 130 + www/wiki/includes/specials/SpecialUnblock.php | 278 + .../specials/SpecialUncategorizedcategories.php | 93 + .../specials/SpecialUncategorizedimages.php | 65 + .../specials/SpecialUncategorizedpages.php | 85 + .../specials/SpecialUncategorizedtemplates.php | 36 + www/wiki/includes/specials/SpecialUndelete.php | 1200 +++ .../includes/specials/SpecialUnlinkAccounts.php | 79 + www/wiki/includes/specials/SpecialUnlockdb.php | 96 + .../includes/specials/SpecialUnusedcategories.php | 83 + www/wiki/includes/specials/SpecialUnusedimages.php | 85 + .../includes/specials/SpecialUnusedtemplates.php | 97 + .../includes/specials/SpecialUnwatchedpages.php | 138 + www/wiki/includes/specials/SpecialUpload.php | 853 ++ www/wiki/includes/specials/SpecialUploadStash.php | 456 + www/wiki/includes/specials/SpecialUserLogin.php | 162 + www/wiki/includes/specials/SpecialUserLogout.php | 107 + www/wiki/includes/specials/SpecialUserrights.php | 1042 +++ www/wiki/includes/specials/SpecialVersion.php | 1201 +++ .../includes/specials/SpecialWantedcategories.php | 131 + www/wiki/includes/specials/SpecialWantedfiles.php | 153 + www/wiki/includes/specials/SpecialWantedpages.php | 98 + .../includes/specials/SpecialWantedtemplates.php | 61 + www/wiki/includes/specials/SpecialWatchlist.php | 872 ++ .../includes/specials/SpecialWhatlinkshere.php | 573 ++ .../includes/specials/SpecialWithoutinterwiki.php | 110 + .../EditWatchlistCheckboxSeriesField.php | 37 + www/wiki/includes/specials/formfields/Licenses.php | 226 + .../specials/formfields/UploadSourceField.php | 68 + .../specials/forms/EditWatchlistNormalHTMLForm.php | 36 + .../includes/specials/forms/PreferencesForm.php | 143 + www/wiki/includes/specials/forms/UploadForm.php | 446 + .../includes/specials/helpers/ImportReporter.php | 190 + www/wiki/includes/specials/helpers/License.php | 61 + www/wiki/includes/specials/helpers/LoginHelper.php | 98 + .../includes/specials/pagers/ActiveUsersPager.php | 195 + .../specials/pagers/AllMessagesTablePager.php | 424 + .../includes/specials/pagers/BlockListPager.php | 312 + .../includes/specials/pagers/CategoryPager.php | 115 + .../includes/specials/pagers/ContribsPager.php | 674 ++ .../specials/pagers/DeletedContribsPager.php | 365 + .../includes/specials/pagers/ImageListPager.php | 628 ++ .../includes/specials/pagers/MergeHistoryPager.php | 100 + .../includes/specials/pagers/NewFilesPager.php | 208 + .../includes/specials/pagers/NewPagesPager.php | 159 + .../specials/pagers/ProtectedPagesPager.php | 338 + .../specials/pagers/ProtectedTitlesPager.php | 91 + www/wiki/includes/specials/pagers/UsersPager.php | 416 + www/wiki/includes/templates/AtomHeader.mustache | 8 + www/wiki/includes/templates/AtomItem.mustache | 10 + .../templates/EnhancedChangesListGroup.mustache | 30 + .../includes/templates/NoLocalSettings.mustache | 39 + www/wiki/includes/templates/RSSHeader.mustache | 8 + www/wiki/includes/templates/RSSItem.mustache | 9 + .../templates/SpecialContributionsLine.mustache | 6 + www/wiki/includes/tidy/Balancer.php | 3584 ++++++++ www/wiki/includes/tidy/Html5Depurate.php | 47 + www/wiki/includes/tidy/Html5Internal.php | 18 + www/wiki/includes/tidy/RaggettBase.php | 47 + www/wiki/includes/tidy/RaggettExternal.php | 73 + www/wiki/includes/tidy/RaggettInternalHHVM.php | 29 + www/wiki/includes/tidy/RaggettInternalPHP.php | 52 + www/wiki/includes/tidy/RaggettWrapper.php | 100 + www/wiki/includes/tidy/RemexCompatFormatter.php | 70 + www/wiki/includes/tidy/RemexCompatMunger.php | 505 ++ www/wiki/includes/tidy/RemexDriver.php | 57 + www/wiki/includes/tidy/RemexMungerData.php | 117 + www/wiki/includes/tidy/TidyDriverBase.php | 30 + www/wiki/includes/tidy/tidy.conf | 24 + www/wiki/includes/title/ForeignTitle.php | 117 + www/wiki/includes/title/ForeignTitleFactory.php | 35 + www/wiki/includes/title/ImportTitleFactory.php | 35 + .../includes/title/MalformedTitleException.php | 83 + www/wiki/includes/title/MediaWikiTitleCodec.php | 492 ++ .../includes/title/NaiveForeignTitleFactory.php | 72 + .../includes/title/NaiveImportTitleFactory.php | 64 + .../title/NamespaceAwareForeignTitleFactory.php | 141 + .../includes/title/NamespaceImportTitleFactory.php | 51 + .../includes/title/SubpageImportTitleFactory.php | 54 + www/wiki/includes/title/TitleFormatter.php | 104 + www/wiki/includes/title/TitleParser.php | 47 + www/wiki/includes/title/TitleValue.php | 213 + www/wiki/includes/upload/UploadBase.php | 2243 +++++ www/wiki/includes/upload/UploadFromChunks.php | 430 + www/wiki/includes/upload/UploadFromFile.php | 97 + www/wiki/includes/upload/UploadFromStash.php | 161 + www/wiki/includes/upload/UploadFromUrl.php | 300 + www/wiki/includes/upload/UploadStash.php | 836 ++ www/wiki/includes/user/BotPassword.php | 521 ++ www/wiki/includes/user/CentralIdLookup.php | 263 + www/wiki/includes/user/ExternalUserNames.php | 133 + www/wiki/includes/user/LocalIdLookup.php | 116 + www/wiki/includes/user/LoggedOutEditToken.php | 47 + www/wiki/includes/user/PasswordReset.php | 312 + www/wiki/includes/user/User.php | 5685 +++++++++++++ www/wiki/includes/user/UserArray.php | 95 + www/wiki/includes/user/UserArrayFromResult.php | 92 + www/wiki/includes/user/UserGroupMembership.php | 469 ++ www/wiki/includes/user/UserIdentity.php | 57 + www/wiki/includes/user/UserIdentityValue.php | 85 + www/wiki/includes/user/UserNamePrefixSearch.php | 70 + www/wiki/includes/user/UserRightsProxy.php | 289 + www/wiki/includes/utils/AutoloadGenerator.php | 508 ++ www/wiki/includes/utils/AvroValidator.php | 181 + www/wiki/includes/utils/BatchRowIterator.php | 296 + www/wiki/includes/utils/BatchRowUpdate.php | 128 + www/wiki/includes/utils/BatchRowWriter.php | 75 + www/wiki/includes/utils/ExecutableFinder.php | 115 + www/wiki/includes/utils/FileContentsHasher.php | 114 + www/wiki/includes/utils/MWCryptHKDF.php | 103 + www/wiki/includes/utils/MWCryptRand.php | 79 + www/wiki/includes/utils/MWFileProps.php | 145 + www/wiki/includes/utils/MWRestrictions.php | 147 + www/wiki/includes/utils/README | 9 + www/wiki/includes/utils/RowUpdateGenerator.php | 39 + www/wiki/includes/utils/UIDGenerator.php | 629 ++ www/wiki/includes/utils/ZipDirectoryReader.php | 717 ++ .../includes/utils/ZipDirectoryReaderError.php | 38 + .../watcheditem/NoWriteWatchedItemStore.php | 145 + www/wiki/includes/watcheditem/WatchedItem.php | 85 + .../watcheditem/WatchedItemQueryService.php | 727 ++ .../WatchedItemQueryServiceExtension.php | 57 + www/wiki/includes/watcheditem/WatchedItemStore.php | 1044 +++ .../watcheditem/WatchedItemStoreInterface.php | 318 + www/wiki/includes/widget/AUTHORS.txt | 13 + .../widget/ComplexNamespaceInputWidget.php | 117 + .../includes/widget/ComplexTitleInputWidget.php | 66 + www/wiki/includes/widget/DateInputWidget.php | 160 + www/wiki/includes/widget/DateTimeInputWidget.php | 73 + www/wiki/includes/widget/LICENSE.txt | 25 + www/wiki/includes/widget/NamespaceInputWidget.php | 64 + www/wiki/includes/widget/SearchInputWidget.php | 72 + www/wiki/includes/widget/SelectWithInputWidget.php | 63 + www/wiki/includes/widget/SizeFilterWidget.php | 75 + www/wiki/includes/widget/TitleInputWidget.php | 79 + www/wiki/includes/widget/UserInputWidget.php | 31 + .../includes/widget/UsersMultiselectWidget.php | 66 + .../widget/search/BasicSearchResultSetWidget.php | 134 + .../includes/widget/search/DidYouMeanWidget.php | 105 + .../widget/search/FullSearchResultWidget.php | 285 + .../search/InterwikiSearchResultSetWidget.php | 190 + .../widget/search/InterwikiSearchResultWidget.php | 66 + .../includes/widget/search/SearchFormWidget.php | 314 + .../widget/search/SearchResultSetWidget.php | 18 + .../includes/widget/search/SearchResultWidget.php | 18 + .../widget/search/SimpleSearchResultSetWidget.php | 133 + .../widget/search/SimpleSearchResultWidget.php | 63 + 1703 files changed, 483021 insertions(+) create mode 100644 www/wiki/includes/.htaccess create mode 100644 www/wiki/includes/ActorMigration.php create mode 100644 www/wiki/includes/AjaxDispatcher.php create mode 100644 www/wiki/includes/AjaxResponse.php create mode 100644 www/wiki/includes/AuthPlugin.php create mode 100644 www/wiki/includes/AutoLoader.php create mode 100644 www/wiki/includes/Autopromote.php create mode 100644 www/wiki/includes/Block.php create mode 100644 www/wiki/includes/CategoriesRdf.php create mode 100644 www/wiki/includes/Category.php create mode 100644 www/wiki/includes/CategoryFinder.php create mode 100644 www/wiki/includes/CategoryViewer.php create mode 100644 www/wiki/includes/CommentStore.php create mode 100644 www/wiki/includes/CommentStoreComment.php create mode 100644 www/wiki/includes/ConfiguredReadOnlyMode.php create mode 100644 www/wiki/includes/DefaultSettings.php create mode 100644 www/wiki/includes/Defines.php create mode 100644 www/wiki/includes/DeprecatedGlobal.php create mode 100644 www/wiki/includes/DerivativeRequest.php create mode 100644 www/wiki/includes/DevelopmentSettings.php create mode 100644 www/wiki/includes/DummyLinker.php create mode 100644 www/wiki/includes/EditPage.php create mode 100644 www/wiki/includes/EventRelayerGroup.php create mode 100644 www/wiki/includes/FauxRequest.php create mode 100644 www/wiki/includes/Feed.php create mode 100644 www/wiki/includes/FeedUtils.php create mode 100644 www/wiki/includes/FileDeleteForm.php create mode 100644 www/wiki/includes/ForkController.php create mode 100644 www/wiki/includes/FormOptions.php create mode 100644 www/wiki/includes/GitInfo.php create mode 100644 www/wiki/includes/GlobalFunctions.php create mode 100644 www/wiki/includes/HeaderCallback.php create mode 100644 www/wiki/includes/HistoryBlob.php create mode 100644 www/wiki/includes/Hooks.php create mode 100644 www/wiki/includes/Html.php create mode 100644 www/wiki/includes/LinkFilter.php create mode 100644 www/wiki/includes/Linker.php create mode 100644 www/wiki/includes/ListToggle.php create mode 100644 www/wiki/includes/MWGrants.php create mode 100644 www/wiki/includes/MWNamespace.php create mode 100644 www/wiki/includes/MWTimestamp.php create mode 100644 www/wiki/includes/MagicWord.php create mode 100644 www/wiki/includes/MagicWordArray.php create mode 100644 www/wiki/includes/MediaWiki.php create mode 100644 www/wiki/includes/MediaWikiServices.php create mode 100644 www/wiki/includes/MediaWikiVersionFetcher.php create mode 100644 www/wiki/includes/MergeHistory.php create mode 100644 www/wiki/includes/Message.php create mode 100644 www/wiki/includes/MimeMagic.php create mode 100644 www/wiki/includes/MovePage.php create mode 100644 www/wiki/includes/NoLocalSettings.php create mode 100644 www/wiki/includes/OrderedStreamingForkController.php create mode 100644 www/wiki/includes/OutputHandler.php create mode 100644 www/wiki/includes/OutputPage.php create mode 100644 www/wiki/includes/PHPVersionCheck.php create mode 100644 www/wiki/includes/PageProps.php create mode 100644 www/wiki/includes/PathRouter.php create mode 100644 www/wiki/includes/Pingback.php create mode 100644 www/wiki/includes/Preferences.php create mode 100644 www/wiki/includes/PrefixSearch.php create mode 100644 www/wiki/includes/ProtectionForm.php create mode 100644 www/wiki/includes/ProxyLookup.php create mode 100644 www/wiki/includes/RawMessage.php create mode 100644 www/wiki/includes/ReadOnlyMode.php create mode 100644 www/wiki/includes/Revision.php create mode 100644 www/wiki/includes/RevisionList.php create mode 100644 www/wiki/includes/ServiceWiring.php create mode 100644 www/wiki/includes/Setup.php create mode 100644 www/wiki/includes/SiteConfiguration.php create mode 100644 www/wiki/includes/SiteStats.php create mode 100644 www/wiki/includes/SiteStatsInit.php create mode 100644 www/wiki/includes/Status.php create mode 100644 www/wiki/includes/Storage/BlobAccessException.php create mode 100644 www/wiki/includes/Storage/BlobStore.php create mode 100644 www/wiki/includes/Storage/BlobStoreFactory.php create mode 100644 www/wiki/includes/Storage/IncompleteRevisionException.php create mode 100644 www/wiki/includes/Storage/MutableRevisionRecord.php create mode 100644 www/wiki/includes/Storage/MutableRevisionSlots.php create mode 100644 www/wiki/includes/Storage/NameTableAccessException.php create mode 100644 www/wiki/includes/Storage/NameTableStore.php create mode 100644 www/wiki/includes/Storage/RevisionAccessException.php create mode 100644 www/wiki/includes/Storage/RevisionArchiveRecord.php create mode 100644 www/wiki/includes/Storage/RevisionFactory.php create mode 100644 www/wiki/includes/Storage/RevisionLookup.php create mode 100644 www/wiki/includes/Storage/RevisionRecord.php create mode 100644 www/wiki/includes/Storage/RevisionSlots.php create mode 100644 www/wiki/includes/Storage/RevisionStore.php create mode 100644 www/wiki/includes/Storage/RevisionStoreRecord.php create mode 100644 www/wiki/includes/Storage/SlotRecord.php create mode 100644 www/wiki/includes/Storage/SqlBlobStore.php create mode 100644 www/wiki/includes/Storage/SuppressedDataException.php create mode 100644 www/wiki/includes/StreamFile.php create mode 100644 www/wiki/includes/StubObject.php create mode 100644 www/wiki/includes/TemplateParser.php create mode 100644 www/wiki/includes/TemplatesOnThisPageFormatter.php create mode 100644 www/wiki/includes/Title.php create mode 100644 www/wiki/includes/TitleArray.php create mode 100644 www/wiki/includes/TitleArrayFromResult.php create mode 100644 www/wiki/includes/TrackingCategories.php create mode 100644 www/wiki/includes/WebRequest.php create mode 100644 www/wiki/includes/WebRequestUpload.php create mode 100644 www/wiki/includes/WebResponse.php create mode 100644 www/wiki/includes/WebStart.php create mode 100644 www/wiki/includes/WikiMap.php create mode 100644 www/wiki/includes/WikiReference.php create mode 100644 www/wiki/includes/Xml.php create mode 100644 www/wiki/includes/XmlJsCode.php create mode 100644 www/wiki/includes/XmlSelect.php create mode 100644 www/wiki/includes/actions/Action.php create mode 100644 www/wiki/includes/actions/CachedAction.php create mode 100644 www/wiki/includes/actions/CreditsAction.php create mode 100644 www/wiki/includes/actions/DeleteAction.php create mode 100644 www/wiki/includes/actions/EditAction.php create mode 100644 www/wiki/includes/actions/FormAction.php create mode 100644 www/wiki/includes/actions/FormlessAction.php create mode 100644 www/wiki/includes/actions/HistoryAction.php create mode 100644 www/wiki/includes/actions/InfoAction.php create mode 100644 www/wiki/includes/actions/MarkpatrolledAction.php create mode 100644 www/wiki/includes/actions/ProtectAction.php create mode 100644 www/wiki/includes/actions/PurgeAction.php create mode 100644 www/wiki/includes/actions/RawAction.php create mode 100644 www/wiki/includes/actions/RenderAction.php create mode 100644 www/wiki/includes/actions/RevertAction.php create mode 100644 www/wiki/includes/actions/RollbackAction.php create mode 100644 www/wiki/includes/actions/SpecialPageAction.php create mode 100644 www/wiki/includes/actions/SubmitAction.php create mode 100644 www/wiki/includes/actions/UnprotectAction.php create mode 100644 www/wiki/includes/actions/UnwatchAction.php create mode 100644 www/wiki/includes/actions/ViewAction.php create mode 100644 www/wiki/includes/actions/WatchAction.php create mode 100644 www/wiki/includes/api/ApiAMCreateAccount.php create mode 100644 www/wiki/includes/api/ApiAuthManagerHelper.php create mode 100644 www/wiki/includes/api/ApiBase.php create mode 100644 www/wiki/includes/api/ApiBlock.php create mode 100644 www/wiki/includes/api/ApiCSPReport.php create mode 100644 www/wiki/includes/api/ApiChangeAuthenticationData.php create mode 100644 www/wiki/includes/api/ApiCheckToken.php create mode 100644 www/wiki/includes/api/ApiClearHasMsg.php create mode 100644 www/wiki/includes/api/ApiClientLogin.php create mode 100644 www/wiki/includes/api/ApiComparePages.php create mode 100644 www/wiki/includes/api/ApiContinuationManager.php create mode 100644 www/wiki/includes/api/ApiDelete.php create mode 100644 www/wiki/includes/api/ApiDisabled.php create mode 100644 www/wiki/includes/api/ApiEditPage.php create mode 100644 www/wiki/includes/api/ApiEmailUser.php create mode 100644 www/wiki/includes/api/ApiErrorFormatter.php create mode 100644 www/wiki/includes/api/ApiExpandTemplates.php create mode 100644 www/wiki/includes/api/ApiFeedContributions.php create mode 100644 www/wiki/includes/api/ApiFeedRecentChanges.php create mode 100644 www/wiki/includes/api/ApiFeedWatchlist.php create mode 100644 www/wiki/includes/api/ApiFileRevert.php create mode 100644 www/wiki/includes/api/ApiFormatBase.php create mode 100644 www/wiki/includes/api/ApiFormatFeedWrapper.php create mode 100644 www/wiki/includes/api/ApiFormatJson.php create mode 100644 www/wiki/includes/api/ApiFormatNone.php create mode 100644 www/wiki/includes/api/ApiFormatPhp.php create mode 100644 www/wiki/includes/api/ApiFormatRaw.php create mode 100644 www/wiki/includes/api/ApiFormatXml.php create mode 100644 www/wiki/includes/api/ApiHelp.php create mode 100644 www/wiki/includes/api/ApiHelpParamValueMessage.php create mode 100644 www/wiki/includes/api/ApiImageRotate.php create mode 100644 www/wiki/includes/api/ApiImport.php create mode 100644 www/wiki/includes/api/ApiLinkAccount.php create mode 100644 www/wiki/includes/api/ApiLogin.php create mode 100644 www/wiki/includes/api/ApiLogout.php create mode 100644 www/wiki/includes/api/ApiMain.php create mode 100644 www/wiki/includes/api/ApiManageTags.php create mode 100644 www/wiki/includes/api/ApiMergeHistory.php create mode 100644 www/wiki/includes/api/ApiMessage.php create mode 100644 www/wiki/includes/api/ApiModuleManager.php create mode 100644 www/wiki/includes/api/ApiMove.php create mode 100644 www/wiki/includes/api/ApiOpenSearch.php create mode 100644 www/wiki/includes/api/ApiOptions.php create mode 100644 www/wiki/includes/api/ApiPageSet.php create mode 100644 www/wiki/includes/api/ApiParamInfo.php create mode 100644 www/wiki/includes/api/ApiParse.php create mode 100644 www/wiki/includes/api/ApiPatrol.php create mode 100644 www/wiki/includes/api/ApiProtect.php create mode 100644 www/wiki/includes/api/ApiPurge.php create mode 100644 www/wiki/includes/api/ApiQuery.php create mode 100644 www/wiki/includes/api/ApiQueryAllCategories.php create mode 100644 www/wiki/includes/api/ApiQueryAllDeletedRevisions.php create mode 100644 www/wiki/includes/api/ApiQueryAllImages.php create mode 100644 www/wiki/includes/api/ApiQueryAllLinks.php create mode 100644 www/wiki/includes/api/ApiQueryAllMessages.php create mode 100644 www/wiki/includes/api/ApiQueryAllPages.php create mode 100644 www/wiki/includes/api/ApiQueryAllRevisions.php create mode 100644 www/wiki/includes/api/ApiQueryAllUsers.php create mode 100644 www/wiki/includes/api/ApiQueryAuthManagerInfo.php create mode 100644 www/wiki/includes/api/ApiQueryBacklinks.php create mode 100644 www/wiki/includes/api/ApiQueryBacklinksprop.php create mode 100644 www/wiki/includes/api/ApiQueryBase.php create mode 100644 www/wiki/includes/api/ApiQueryBlocks.php create mode 100644 www/wiki/includes/api/ApiQueryCategories.php create mode 100644 www/wiki/includes/api/ApiQueryCategoryInfo.php create mode 100644 www/wiki/includes/api/ApiQueryCategoryMembers.php create mode 100644 www/wiki/includes/api/ApiQueryContributors.php create mode 100644 www/wiki/includes/api/ApiQueryDeletedRevisions.php create mode 100644 www/wiki/includes/api/ApiQueryDeletedrevs.php create mode 100644 www/wiki/includes/api/ApiQueryDisabled.php create mode 100644 www/wiki/includes/api/ApiQueryDuplicateFiles.php create mode 100644 www/wiki/includes/api/ApiQueryExtLinksUsage.php create mode 100644 www/wiki/includes/api/ApiQueryExternalLinks.php create mode 100644 www/wiki/includes/api/ApiQueryFileRepoInfo.php create mode 100644 www/wiki/includes/api/ApiQueryFilearchive.php create mode 100644 www/wiki/includes/api/ApiQueryGeneratorBase.php create mode 100644 www/wiki/includes/api/ApiQueryIWBacklinks.php create mode 100644 www/wiki/includes/api/ApiQueryIWLinks.php create mode 100644 www/wiki/includes/api/ApiQueryImageInfo.php create mode 100644 www/wiki/includes/api/ApiQueryImages.php create mode 100644 www/wiki/includes/api/ApiQueryInfo.php create mode 100644 www/wiki/includes/api/ApiQueryLangBacklinks.php create mode 100644 www/wiki/includes/api/ApiQueryLangLinks.php create mode 100644 www/wiki/includes/api/ApiQueryLinks.php create mode 100644 www/wiki/includes/api/ApiQueryLogEvents.php create mode 100644 www/wiki/includes/api/ApiQueryMyStashedFiles.php create mode 100644 www/wiki/includes/api/ApiQueryPagePropNames.php create mode 100644 www/wiki/includes/api/ApiQueryPageProps.php create mode 100644 www/wiki/includes/api/ApiQueryPagesWithProp.php create mode 100644 www/wiki/includes/api/ApiQueryPrefixSearch.php create mode 100644 www/wiki/includes/api/ApiQueryProtectedTitles.php create mode 100644 www/wiki/includes/api/ApiQueryQueryPage.php create mode 100644 www/wiki/includes/api/ApiQueryRandom.php create mode 100644 www/wiki/includes/api/ApiQueryRecentChanges.php create mode 100644 www/wiki/includes/api/ApiQueryRevisions.php create mode 100644 www/wiki/includes/api/ApiQueryRevisionsBase.php create mode 100644 www/wiki/includes/api/ApiQuerySearch.php create mode 100644 www/wiki/includes/api/ApiQuerySiteinfo.php create mode 100644 www/wiki/includes/api/ApiQueryStashImageInfo.php create mode 100644 www/wiki/includes/api/ApiQueryTags.php create mode 100644 www/wiki/includes/api/ApiQueryTokens.php create mode 100644 www/wiki/includes/api/ApiQueryUserContributions.php create mode 100644 www/wiki/includes/api/ApiQueryUserInfo.php create mode 100644 www/wiki/includes/api/ApiQueryUsers.php create mode 100644 www/wiki/includes/api/ApiQueryWatchlist.php create mode 100644 www/wiki/includes/api/ApiQueryWatchlistRaw.php create mode 100644 www/wiki/includes/api/ApiRemoveAuthenticationData.php create mode 100644 www/wiki/includes/api/ApiResetPassword.php create mode 100644 www/wiki/includes/api/ApiResult.php create mode 100644 www/wiki/includes/api/ApiRevisionDelete.php create mode 100644 www/wiki/includes/api/ApiRollback.php create mode 100644 www/wiki/includes/api/ApiRsd.php create mode 100644 www/wiki/includes/api/ApiSerializable.php create mode 100644 www/wiki/includes/api/ApiSetNotificationTimestamp.php create mode 100644 www/wiki/includes/api/ApiSetPageLanguage.php create mode 100644 www/wiki/includes/api/ApiStashEdit.php create mode 100644 www/wiki/includes/api/ApiTag.php create mode 100644 www/wiki/includes/api/ApiTokens.php create mode 100644 www/wiki/includes/api/ApiUnblock.php create mode 100644 www/wiki/includes/api/ApiUndelete.php create mode 100644 www/wiki/includes/api/ApiUpload.php create mode 100644 www/wiki/includes/api/ApiUsageException.php create mode 100644 www/wiki/includes/api/ApiUserrights.php create mode 100644 www/wiki/includes/api/ApiValidatePassword.php create mode 100644 www/wiki/includes/api/ApiWatch.php create mode 100644 www/wiki/includes/api/SearchApi.php create mode 100644 www/wiki/includes/api/i18n/ar.json create mode 100644 www/wiki/includes/api/i18n/ast.json create mode 100644 www/wiki/includes/api/i18n/av.json create mode 100644 www/wiki/includes/api/i18n/awa.json create mode 100644 www/wiki/includes/api/i18n/azb.json create mode 100644 www/wiki/includes/api/i18n/ba.json create mode 100644 www/wiki/includes/api/i18n/bcl.json create mode 100644 www/wiki/includes/api/i18n/be-tarask.json create mode 100644 www/wiki/includes/api/i18n/bg.json create mode 100644 www/wiki/includes/api/i18n/bgn.json create mode 100644 www/wiki/includes/api/i18n/bn.json create mode 100644 www/wiki/includes/api/i18n/br.json create mode 100644 www/wiki/includes/api/i18n/bs.json create mode 100644 www/wiki/includes/api/i18n/ca.json create mode 100644 www/wiki/includes/api/i18n/ce.json create mode 100644 www/wiki/includes/api/i18n/ckb.json create mode 100644 www/wiki/includes/api/i18n/cs.json create mode 100644 www/wiki/includes/api/i18n/cv.json create mode 100644 www/wiki/includes/api/i18n/da.json create mode 100644 www/wiki/includes/api/i18n/de.json create mode 100644 www/wiki/includes/api/i18n/diq.json create mode 100644 www/wiki/includes/api/i18n/el.json create mode 100644 www/wiki/includes/api/i18n/en-gb.json create mode 100644 www/wiki/includes/api/i18n/en.json create mode 100644 www/wiki/includes/api/i18n/eo.json create mode 100644 www/wiki/includes/api/i18n/es.json create mode 100644 www/wiki/includes/api/i18n/et.json create mode 100644 www/wiki/includes/api/i18n/eu.json create mode 100644 www/wiki/includes/api/i18n/fa.json create mode 100644 www/wiki/includes/api/i18n/fi.json create mode 100644 www/wiki/includes/api/i18n/fo.json create mode 100644 www/wiki/includes/api/i18n/fr.json create mode 100644 www/wiki/includes/api/i18n/frc.json create mode 100644 www/wiki/includes/api/i18n/fy.json create mode 100644 www/wiki/includes/api/i18n/gl.json create mode 100644 www/wiki/includes/api/i18n/he.json create mode 100644 www/wiki/includes/api/i18n/hr.json create mode 100644 www/wiki/includes/api/i18n/hsb.json create mode 100644 www/wiki/includes/api/i18n/hsn.json create mode 100644 www/wiki/includes/api/i18n/ht.json create mode 100644 www/wiki/includes/api/i18n/hu.json create mode 100644 www/wiki/includes/api/i18n/ia.json create mode 100644 www/wiki/includes/api/i18n/id.json create mode 100644 www/wiki/includes/api/i18n/is.json create mode 100644 www/wiki/includes/api/i18n/it.json create mode 100644 www/wiki/includes/api/i18n/ja.json create mode 100644 www/wiki/includes/api/i18n/jam.json create mode 100644 www/wiki/includes/api/i18n/jv.json create mode 100644 www/wiki/includes/api/i18n/ka.json create mode 100644 www/wiki/includes/api/i18n/kn.json create mode 100644 www/wiki/includes/api/i18n/ko.json create mode 100644 www/wiki/includes/api/i18n/ksh.json create mode 100644 www/wiki/includes/api/i18n/ku-latn.json create mode 100644 www/wiki/includes/api/i18n/ky.json create mode 100644 www/wiki/includes/api/i18n/lb.json create mode 100644 www/wiki/includes/api/i18n/lij.json create mode 100644 www/wiki/includes/api/i18n/lki.json create mode 100644 www/wiki/includes/api/i18n/ln.json create mode 100644 www/wiki/includes/api/i18n/lt.json create mode 100644 www/wiki/includes/api/i18n/lv.json create mode 100644 www/wiki/includes/api/i18n/lzh.json create mode 100644 www/wiki/includes/api/i18n/mg.json create mode 100644 www/wiki/includes/api/i18n/mk.json create mode 100644 www/wiki/includes/api/i18n/mr.json create mode 100644 www/wiki/includes/api/i18n/ms.json create mode 100644 www/wiki/includes/api/i18n/my.json create mode 100644 www/wiki/includes/api/i18n/nap.json create mode 100644 www/wiki/includes/api/i18n/nb.json create mode 100644 www/wiki/includes/api/i18n/nds.json create mode 100644 www/wiki/includes/api/i18n/ne.json create mode 100644 www/wiki/includes/api/i18n/nl.json create mode 100644 www/wiki/includes/api/i18n/nso.json create mode 100644 www/wiki/includes/api/i18n/oc.json create mode 100644 www/wiki/includes/api/i18n/olo.json create mode 100644 www/wiki/includes/api/i18n/or.json create mode 100644 www/wiki/includes/api/i18n/pa.json create mode 100644 www/wiki/includes/api/i18n/pam.json create mode 100644 www/wiki/includes/api/i18n/pl.json create mode 100644 www/wiki/includes/api/i18n/ps.json create mode 100644 www/wiki/includes/api/i18n/pt-br.json create mode 100644 www/wiki/includes/api/i18n/pt.json create mode 100644 www/wiki/includes/api/i18n/qqq.json create mode 100644 www/wiki/includes/api/i18n/ro.json create mode 100644 www/wiki/includes/api/i18n/roa-tara.json create mode 100644 www/wiki/includes/api/i18n/ru.json create mode 100644 www/wiki/includes/api/i18n/sah.json create mode 100644 www/wiki/includes/api/i18n/sd.json create mode 100644 www/wiki/includes/api/i18n/sh.json create mode 100644 www/wiki/includes/api/i18n/shn.json create mode 100644 www/wiki/includes/api/i18n/si.json create mode 100644 www/wiki/includes/api/i18n/sk.json create mode 100644 www/wiki/includes/api/i18n/sq.json create mode 100644 www/wiki/includes/api/i18n/sr-ec.json create mode 100644 www/wiki/includes/api/i18n/sr-el.json create mode 100644 www/wiki/includes/api/i18n/sv.json create mode 100644 www/wiki/includes/api/i18n/ta.json create mode 100644 www/wiki/includes/api/i18n/tcy.json create mode 100644 www/wiki/includes/api/i18n/te.json create mode 100644 www/wiki/includes/api/i18n/th.json create mode 100644 www/wiki/includes/api/i18n/tl.json create mode 100644 www/wiki/includes/api/i18n/tr.json create mode 100644 www/wiki/includes/api/i18n/tt-cyrl.json create mode 100644 www/wiki/includes/api/i18n/udm.json create mode 100644 www/wiki/includes/api/i18n/uk.json create mode 100644 www/wiki/includes/api/i18n/ur.json create mode 100644 www/wiki/includes/api/i18n/vi.json create mode 100644 www/wiki/includes/api/i18n/wuu.json create mode 100644 www/wiki/includes/api/i18n/yi.json create mode 100644 www/wiki/includes/api/i18n/zh-hans.json create mode 100644 www/wiki/includes/api/i18n/zh-hant.json create mode 100644 www/wiki/includes/api/i18n/zu.json create mode 100644 www/wiki/includes/auth/AbstractAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AbstractPreAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AbstractPrimaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AbstractSecondaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AuthManager.php create mode 100644 www/wiki/includes/auth/AuthManagerAuthPlugin.php create mode 100644 www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AuthenticationProvider.php create mode 100644 www/wiki/includes/auth/AuthenticationRequest.php create mode 100644 www/wiki/includes/auth/AuthenticationResponse.php create mode 100644 www/wiki/includes/auth/ButtonAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/ConfirmLinkAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/CreateFromLoginAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/CreatedAccountAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/CreationReasonAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/PasswordAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/PasswordDomainAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/PreAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/PrimaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/RememberMeAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/SecondaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/TemporaryPasswordAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/ThrottlePreAuthenticationProvider.php create mode 100644 www/wiki/includes/auth/Throttler.php create mode 100644 www/wiki/includes/auth/UserDataAuthenticationRequest.php create mode 100644 www/wiki/includes/auth/UsernameAuthenticationRequest.php create mode 100644 www/wiki/includes/cache/BacklinkCache.php create mode 100644 www/wiki/includes/cache/CacheDependency.php create mode 100644 www/wiki/includes/cache/CacheHelper.php create mode 100644 www/wiki/includes/cache/FileCacheBase.php create mode 100644 www/wiki/includes/cache/GenderCache.php create mode 100644 www/wiki/includes/cache/HTMLFileCache.php create mode 100644 www/wiki/includes/cache/LinkBatch.php create mode 100644 www/wiki/includes/cache/LinkCache.php create mode 100644 www/wiki/includes/cache/MessageBlobStore.php create mode 100644 www/wiki/includes/cache/MessageCache.php create mode 100644 www/wiki/includes/cache/ResourceFileCache.php create mode 100644 www/wiki/includes/cache/UserCache.php create mode 100644 www/wiki/includes/cache/localisation/LCStore.php create mode 100644 www/wiki/includes/cache/localisation/LCStoreCDB.php create mode 100644 www/wiki/includes/cache/localisation/LCStoreDB.php create mode 100644 www/wiki/includes/cache/localisation/LCStoreNull.php create mode 100644 www/wiki/includes/cache/localisation/LCStoreStaticArray.php create mode 100644 www/wiki/includes/cache/localisation/LocalisationCache.php create mode 100644 www/wiki/includes/cache/localisation/LocalisationCacheBulkLoad.php create mode 100644 www/wiki/includes/changes/CategoryMembershipChange.php create mode 100644 www/wiki/includes/changes/ChangesFeed.php create mode 100644 www/wiki/includes/changes/ChangesList.php create mode 100644 www/wiki/includes/changes/ChangesListBooleanFilter.php create mode 100644 www/wiki/includes/changes/ChangesListBooleanFilterGroup.php create mode 100644 www/wiki/includes/changes/ChangesListFilter.php create mode 100644 www/wiki/includes/changes/ChangesListFilterGroup.php create mode 100644 www/wiki/includes/changes/ChangesListStringOptionsFilter.php create mode 100644 www/wiki/includes/changes/ChangesListStringOptionsFilterGroup.php create mode 100644 www/wiki/includes/changes/EnhancedChangesList.php create mode 100644 www/wiki/includes/changes/OldChangesList.php create mode 100644 www/wiki/includes/changes/RCCacheEntry.php create mode 100644 www/wiki/includes/changes/RCCacheEntryFactory.php create mode 100644 www/wiki/includes/changes/RecentChange.php create mode 100644 www/wiki/includes/changetags/ChangeTags.php create mode 100644 www/wiki/includes/changetags/ChangeTagsList.php create mode 100644 www/wiki/includes/changetags/ChangeTagsLogItem.php create mode 100644 www/wiki/includes/changetags/ChangeTagsLogList.php create mode 100644 www/wiki/includes/changetags/ChangeTagsRevisionItem.php create mode 100644 www/wiki/includes/changetags/ChangeTagsRevisionList.php create mode 100644 www/wiki/includes/clientpool/SquidPurgeClient.php create mode 100644 www/wiki/includes/clientpool/SquidPurgeClientPool.php create mode 100644 www/wiki/includes/collation/AbkhazUppercaseCollation.php create mode 100644 www/wiki/includes/collation/BashkirUppercaseCollation.php create mode 100644 www/wiki/includes/collation/Collation.php create mode 100644 www/wiki/includes/collation/CollationCkb.php create mode 100644 www/wiki/includes/collation/CollationEt.php create mode 100644 www/wiki/includes/collation/CollationFa.php create mode 100644 www/wiki/includes/collation/CustomUppercaseCollation.php create mode 100644 www/wiki/includes/collation/IcuCollation.php create mode 100644 www/wiki/includes/collation/IdentityCollation.php create mode 100644 www/wiki/includes/collation/NorthernSamiUppercaseCollation.php create mode 100644 www/wiki/includes/collation/NumericUppercaseCollation.php create mode 100644 www/wiki/includes/collation/UppercaseCollation.php create mode 100644 www/wiki/includes/compat/ObjectFactory.php create mode 100644 www/wiki/includes/compat/ScopedCallback.php create mode 100644 www/wiki/includes/compat/Timestamp.php create mode 100644 www/wiki/includes/compat/normal/UtfNormal.php create mode 100644 www/wiki/includes/compat/normal/UtfNormalDefines.php create mode 100644 www/wiki/includes/compat/normal/UtfNormalUtil.php create mode 100644 www/wiki/includes/composer/ComposerHookHandler.php create mode 100644 www/wiki/includes/composer/ComposerPackageModifier.php create mode 100644 www/wiki/includes/composer/ComposerVendorHtaccessCreator.php create mode 100644 www/wiki/includes/composer/ComposerVersionNormalizer.php create mode 100644 www/wiki/includes/config/Config.php create mode 100644 www/wiki/includes/config/ConfigException.php create mode 100644 www/wiki/includes/config/ConfigFactory.php create mode 100644 www/wiki/includes/config/EtcdConfig.php create mode 100644 www/wiki/includes/config/EtcdConfigParseError.php create mode 100644 www/wiki/includes/config/GlobalVarConfig.php create mode 100644 www/wiki/includes/config/HashConfig.php create mode 100644 www/wiki/includes/config/MultiConfig.php create mode 100644 www/wiki/includes/config/MutableConfig.php create mode 100644 www/wiki/includes/content/AbstractContent.php create mode 100644 www/wiki/includes/content/CodeContentHandler.php create mode 100644 www/wiki/includes/content/Content.php create mode 100644 www/wiki/includes/content/ContentHandler.php create mode 100644 www/wiki/includes/content/CssContent.php create mode 100644 www/wiki/includes/content/CssContentHandler.php create mode 100644 www/wiki/includes/content/FileContentHandler.php create mode 100644 www/wiki/includes/content/JavaScriptContent.php create mode 100644 www/wiki/includes/content/JavaScriptContentHandler.php create mode 100644 www/wiki/includes/content/JsonContent.php create mode 100644 www/wiki/includes/content/JsonContentHandler.php create mode 100644 www/wiki/includes/content/MessageContent.php create mode 100644 www/wiki/includes/content/TextContent.php create mode 100644 www/wiki/includes/content/TextContentHandler.php create mode 100644 www/wiki/includes/content/WikiTextStructure.php create mode 100644 www/wiki/includes/content/WikitextContent.php create mode 100644 www/wiki/includes/content/WikitextContentHandler.php create mode 100644 www/wiki/includes/context/ContextSource.php create mode 100644 www/wiki/includes/context/DerivativeContext.php create mode 100644 www/wiki/includes/context/IContextSource.php create mode 100644 www/wiki/includes/context/MutableContext.php create mode 100644 www/wiki/includes/context/RequestContext.php create mode 100644 www/wiki/includes/dao/DBAccessBase.php create mode 100644 www/wiki/includes/dao/DBAccessObjectUtils.php create mode 100644 www/wiki/includes/dao/IDBAccessObject.php create mode 100644 www/wiki/includes/db/CloneDatabase.php create mode 100644 www/wiki/includes/db/DatabaseOracle.php create mode 100644 www/wiki/includes/db/MWLBFactory.php create mode 100644 www/wiki/includes/db/ORAField.php create mode 100644 www/wiki/includes/db/ORAResult.php create mode 100644 www/wiki/includes/debug/MWDebug.php create mode 100644 www/wiki/includes/debug/logger/ConsoleLogger.php create mode 100644 www/wiki/includes/debug/logger/ConsoleSpi.php create mode 100644 www/wiki/includes/debug/logger/LegacyLogger.php create mode 100644 www/wiki/includes/debug/logger/LegacySpi.php create mode 100644 www/wiki/includes/debug/logger/LoggerFactory.php create mode 100644 www/wiki/includes/debug/logger/MonologSpi.php create mode 100644 www/wiki/includes/debug/logger/NullSpi.php create mode 100644 www/wiki/includes/debug/logger/Spi.php create mode 100644 www/wiki/includes/debug/logger/monolog/AvroFormatter.php create mode 100644 www/wiki/includes/debug/logger/monolog/BufferHandler.php create mode 100644 www/wiki/includes/debug/logger/monolog/KafkaHandler.php create mode 100644 www/wiki/includes/debug/logger/monolog/LegacyFormatter.php create mode 100644 www/wiki/includes/debug/logger/monolog/LegacyHandler.php create mode 100644 www/wiki/includes/debug/logger/monolog/LineFormatter.php create mode 100644 www/wiki/includes/debug/logger/monolog/LogstashFormatter.php create mode 100644 www/wiki/includes/debug/logger/monolog/SyslogHandler.php create mode 100644 www/wiki/includes/debug/logger/monolog/WikiProcessor.php create mode 100644 www/wiki/includes/deferred/AtomicSectionUpdate.php create mode 100644 www/wiki/includes/deferred/AutoCommitUpdate.php create mode 100644 www/wiki/includes/deferred/CdnCacheUpdate.php create mode 100644 www/wiki/includes/deferred/DataUpdate.php create mode 100644 www/wiki/includes/deferred/DeferrableCallback.php create mode 100644 www/wiki/includes/deferred/DeferrableUpdate.php create mode 100644 www/wiki/includes/deferred/DeferredUpdates.php create mode 100644 www/wiki/includes/deferred/EnqueueableDataUpdate.php create mode 100644 www/wiki/includes/deferred/HTMLCacheUpdate.php create mode 100644 www/wiki/includes/deferred/LinksDeletionUpdate.php create mode 100644 www/wiki/includes/deferred/LinksUpdate.php create mode 100644 www/wiki/includes/deferred/MWCallableUpdate.php create mode 100644 www/wiki/includes/deferred/MergeableUpdate.php create mode 100644 www/wiki/includes/deferred/SearchUpdate.php create mode 100644 www/wiki/includes/deferred/SiteStatsUpdate.php create mode 100644 www/wiki/includes/deferred/SqlDataUpdate.php create mode 100644 www/wiki/includes/deferred/TransactionRoundDefiningUpdate.php create mode 100644 www/wiki/includes/deferred/WANCacheReapUpdate.php create mode 100644 www/wiki/includes/diff/ArrayDiffFormatter.php create mode 100644 www/wiki/includes/diff/ComplexityException.php create mode 100644 www/wiki/includes/diff/DairikiDiff.php create mode 100644 www/wiki/includes/diff/DiffEngine.php create mode 100644 www/wiki/includes/diff/DiffFormatter.php create mode 100644 www/wiki/includes/diff/DifferenceEngine.php create mode 100644 www/wiki/includes/diff/TableDiffFormatter.php create mode 100644 www/wiki/includes/diff/UnifiedDiffFormatter.php create mode 100644 www/wiki/includes/diff/WordAccumulator.php create mode 100644 www/wiki/includes/diff/WordLevelDiff.php create mode 100644 www/wiki/includes/edit/PreparedEdit.php create mode 100644 www/wiki/includes/editpage/TextConflictHelper.php create mode 100644 www/wiki/includes/editpage/TextboxBuilder.php create mode 100644 www/wiki/includes/exception/BadRequestError.php create mode 100644 www/wiki/includes/exception/BadTitleError.php create mode 100644 www/wiki/includes/exception/CannotCreateActorException.php create mode 100644 www/wiki/includes/exception/ErrorPageError.php create mode 100644 www/wiki/includes/exception/FatalError.php create mode 100644 www/wiki/includes/exception/HttpError.php create mode 100644 www/wiki/includes/exception/LocalizedException.php create mode 100644 www/wiki/includes/exception/MWContentSerializationException.php create mode 100644 www/wiki/includes/exception/MWException.php create mode 100644 www/wiki/includes/exception/MWExceptionHandler.php create mode 100644 www/wiki/includes/exception/MWExceptionRenderer.php create mode 100644 www/wiki/includes/exception/MWUnknownContentModelException.php create mode 100644 www/wiki/includes/exception/PermissionsError.php create mode 100644 www/wiki/includes/exception/ProcOpenError.php create mode 100644 www/wiki/includes/exception/ReadOnlyError.php create mode 100644 www/wiki/includes/exception/ShellDisabledError.php create mode 100644 www/wiki/includes/exception/ThrottledError.php create mode 100644 www/wiki/includes/exception/UserBlockedError.php create mode 100644 www/wiki/includes/exception/UserNotLoggedIn.php create mode 100644 www/wiki/includes/export/BaseDump.php create mode 100644 www/wiki/includes/export/Dump7ZipOutput.php create mode 100644 www/wiki/includes/export/DumpBZip2Output.php create mode 100644 www/wiki/includes/export/DumpDBZip2Output.php create mode 100644 www/wiki/includes/export/DumpFileOutput.php create mode 100644 www/wiki/includes/export/DumpFilter.php create mode 100644 www/wiki/includes/export/DumpGZipOutput.php create mode 100644 www/wiki/includes/export/DumpLatestFilter.php create mode 100644 www/wiki/includes/export/DumpMultiWriter.php create mode 100644 www/wiki/includes/export/DumpNamespaceFilter.php create mode 100644 www/wiki/includes/export/DumpNotalkFilter.php create mode 100644 www/wiki/includes/export/DumpOutput.php create mode 100644 www/wiki/includes/export/DumpPipeOutput.php create mode 100644 www/wiki/includes/export/DumpStringOutput.php create mode 100644 www/wiki/includes/export/ExportProgressFilter.php create mode 100644 www/wiki/includes/export/WikiExporter.php create mode 100644 www/wiki/includes/export/XmlDumpWriter.php create mode 100644 www/wiki/includes/externalstore/ExternalStore.php create mode 100644 www/wiki/includes/externalstore/ExternalStoreDB.php create mode 100644 www/wiki/includes/externalstore/ExternalStoreFactory.php create mode 100644 www/wiki/includes/externalstore/ExternalStoreHttp.php create mode 100644 www/wiki/includes/externalstore/ExternalStoreMedium.php create mode 100644 www/wiki/includes/externalstore/ExternalStoreMwstore.php create mode 100644 www/wiki/includes/filebackend/FileBackendGroup.php create mode 100644 www/wiki/includes/filebackend/README create mode 100644 www/wiki/includes/filebackend/filejournal/DBFileJournal.php create mode 100644 www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php create mode 100644 www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php create mode 100644 www/wiki/includes/filerepo/FileBackendDBRepoWrapper.php create mode 100644 www/wiki/includes/filerepo/FileRepo.php create mode 100644 www/wiki/includes/filerepo/FileRepoStatus.php create mode 100644 www/wiki/includes/filerepo/ForeignAPIRepo.php create mode 100644 www/wiki/includes/filerepo/ForeignDBRepo.php create mode 100644 www/wiki/includes/filerepo/ForeignDBViaLBRepo.php create mode 100644 www/wiki/includes/filerepo/LocalRepo.php create mode 100644 www/wiki/includes/filerepo/NullRepo.php create mode 100644 www/wiki/includes/filerepo/README create mode 100644 www/wiki/includes/filerepo/RepoGroup.php create mode 100644 www/wiki/includes/filerepo/TempFileRepo.php create mode 100644 www/wiki/includes/filerepo/file/ArchivedFile.php create mode 100644 www/wiki/includes/filerepo/file/File.php create mode 100644 www/wiki/includes/filerepo/file/ForeignAPIFile.php create mode 100644 www/wiki/includes/filerepo/file/ForeignDBFile.php create mode 100644 www/wiki/includes/filerepo/file/LocalFile.php create mode 100644 www/wiki/includes/filerepo/file/OldLocalFile.php create mode 100644 www/wiki/includes/filerepo/file/UnregisteredLocalFile.php create mode 100644 www/wiki/includes/gallery/ImageGalleryBase.php create mode 100644 www/wiki/includes/gallery/NolinesImageGallery.php create mode 100644 www/wiki/includes/gallery/PackedImageGallery.php create mode 100644 www/wiki/includes/gallery/PackedOverlayImageGallery.php create mode 100644 www/wiki/includes/gallery/SlideshowImageGallery.php create mode 100644 www/wiki/includes/gallery/TraditionalImageGallery.php create mode 100644 www/wiki/includes/htmlform/HTMLForm.php create mode 100644 www/wiki/includes/htmlform/HTMLFormElement.php create mode 100644 www/wiki/includes/htmlform/HTMLFormField.php create mode 100644 www/wiki/includes/htmlform/HTMLFormFieldRequiredOptionsException.php create mode 100644 www/wiki/includes/htmlform/HTMLNestedFilterable.php create mode 100644 www/wiki/includes/htmlform/OOUIHTMLForm.php create mode 100644 www/wiki/includes/htmlform/VFormHTMLForm.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLApiField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLAutoCompleteSelectField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLButtonField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLCheckField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLCheckMatrix.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLComboboxField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLDateTimeField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLEditTools.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLFloatField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLFormFieldCloner.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLFormFieldWithButton.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLHiddenField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLInfoField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLIntField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLMultiSelectField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLRadioField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLRestrictionsField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSelectAndOtherField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSelectField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSelectLimitField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSelectNamespace.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSelectNamespaceWithButton.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSelectOrOtherField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSizeFilterField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLSubmitField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLTagFilter.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLTextAreaField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLTextField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLTextFieldWithButton.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLTitleTextField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLUserTextField.php create mode 100644 www/wiki/includes/htmlform/fields/HTMLUsersMultiselectField.php create mode 100644 www/wiki/includes/http/CurlHttpRequest.php create mode 100644 www/wiki/includes/http/Http.php create mode 100644 www/wiki/includes/http/HttpRequestFactory.php create mode 100644 www/wiki/includes/http/MWHttpRequest.php create mode 100644 www/wiki/includes/http/PhpHttpRequest.php create mode 100644 www/wiki/includes/import/ImportSource.php create mode 100644 www/wiki/includes/import/ImportStreamSource.php create mode 100644 www/wiki/includes/import/ImportStringSource.php create mode 100644 www/wiki/includes/import/ImportableOldRevision.php create mode 100644 www/wiki/includes/import/ImportableOldRevisionImporter.php create mode 100644 www/wiki/includes/import/ImportableUploadRevision.php create mode 100644 www/wiki/includes/import/ImportableUploadRevisionImporter.php create mode 100644 www/wiki/includes/import/OldRevisionImporter.php create mode 100644 www/wiki/includes/import/UploadRevisionImporter.php create mode 100644 www/wiki/includes/import/UploadSourceAdapter.php create mode 100644 www/wiki/includes/import/WikiImporter.php create mode 100644 www/wiki/includes/import/WikiRevision.php create mode 100644 www/wiki/includes/installer/CliInstaller.php create mode 100644 www/wiki/includes/installer/DatabaseInstaller.php create mode 100644 www/wiki/includes/installer/DatabaseUpdater.php create mode 100644 www/wiki/includes/installer/InstallDocFormatter.php create mode 100644 www/wiki/includes/installer/Installer.php create mode 100644 www/wiki/includes/installer/InstallerOverrides.php create mode 100644 www/wiki/includes/installer/InstallerSessionProvider.php create mode 100644 www/wiki/includes/installer/LocalSettingsGenerator.php create mode 100644 www/wiki/includes/installer/MssqlInstaller.php create mode 100644 www/wiki/includes/installer/MssqlUpdater.php create mode 100644 www/wiki/includes/installer/MysqlInstaller.php create mode 100644 www/wiki/includes/installer/MysqlUpdater.php create mode 100644 www/wiki/includes/installer/OracleInstaller.php create mode 100644 www/wiki/includes/installer/OracleUpdater.php create mode 100644 www/wiki/includes/installer/PhpBugTests.php create mode 100644 www/wiki/includes/installer/PostgresInstaller.php create mode 100644 www/wiki/includes/installer/PostgresUpdater.php create mode 100644 www/wiki/includes/installer/SqliteInstaller.php create mode 100644 www/wiki/includes/installer/SqliteUpdater.php create mode 100644 www/wiki/includes/installer/WebInstaller.php create mode 100644 www/wiki/includes/installer/WebInstallerComplete.php create mode 100644 www/wiki/includes/installer/WebInstallerCopying.php create mode 100644 www/wiki/includes/installer/WebInstallerDBConnect.php create mode 100644 www/wiki/includes/installer/WebInstallerDBSettings.php create mode 100644 www/wiki/includes/installer/WebInstallerDocument.php create mode 100644 www/wiki/includes/installer/WebInstallerExistingWiki.php create mode 100644 www/wiki/includes/installer/WebInstallerInstall.php create mode 100644 www/wiki/includes/installer/WebInstallerLanguage.php create mode 100644 www/wiki/includes/installer/WebInstallerName.php create mode 100644 www/wiki/includes/installer/WebInstallerOptions.php create mode 100644 www/wiki/includes/installer/WebInstallerOutput.php create mode 100644 www/wiki/includes/installer/WebInstallerPage.php create mode 100644 www/wiki/includes/installer/WebInstallerReadme.php create mode 100644 www/wiki/includes/installer/WebInstallerReleaseNotes.php create mode 100644 www/wiki/includes/installer/WebInstallerRestart.php create mode 100644 www/wiki/includes/installer/WebInstallerUpgrade.php create mode 100644 www/wiki/includes/installer/WebInstallerUpgradeDoc.php create mode 100644 www/wiki/includes/installer/WebInstallerWelcome.php create mode 100644 www/wiki/includes/installer/i18n/af.json create mode 100644 www/wiki/includes/installer/i18n/aln.json create mode 100644 www/wiki/includes/installer/i18n/am.json create mode 100644 www/wiki/includes/installer/i18n/an.json create mode 100644 www/wiki/includes/installer/i18n/ang.json create mode 100644 www/wiki/includes/installer/i18n/anp.json create mode 100644 www/wiki/includes/installer/i18n/ar.json create mode 100644 www/wiki/includes/installer/i18n/arc.json create mode 100644 www/wiki/includes/installer/i18n/ary.json create mode 100644 www/wiki/includes/installer/i18n/arz.json create mode 100644 www/wiki/includes/installer/i18n/as.json create mode 100644 www/wiki/includes/installer/i18n/ast.json create mode 100644 www/wiki/includes/installer/i18n/av.json create mode 100644 www/wiki/includes/installer/i18n/avk.json create mode 100644 www/wiki/includes/installer/i18n/az.json create mode 100644 www/wiki/includes/installer/i18n/azb.json create mode 100644 www/wiki/includes/installer/i18n/ba.json create mode 100644 www/wiki/includes/installer/i18n/bar.json create mode 100644 www/wiki/includes/installer/i18n/bcc.json create mode 100644 www/wiki/includes/installer/i18n/bcl.json create mode 100644 www/wiki/includes/installer/i18n/be-tarask.json create mode 100644 www/wiki/includes/installer/i18n/be.json create mode 100644 www/wiki/includes/installer/i18n/bg.json create mode 100644 www/wiki/includes/installer/i18n/bgn.json create mode 100644 www/wiki/includes/installer/i18n/bjn.json create mode 100644 www/wiki/includes/installer/i18n/bn.json create mode 100644 www/wiki/includes/installer/i18n/bpy.json create mode 100644 www/wiki/includes/installer/i18n/br.json create mode 100644 www/wiki/includes/installer/i18n/bs.json create mode 100644 www/wiki/includes/installer/i18n/bto.json create mode 100644 www/wiki/includes/installer/i18n/ca.json create mode 100644 www/wiki/includes/installer/i18n/ce.json create mode 100644 www/wiki/includes/installer/i18n/ceb.json create mode 100644 www/wiki/includes/installer/i18n/ckb.json create mode 100644 www/wiki/includes/installer/i18n/cps.json create mode 100644 www/wiki/includes/installer/i18n/crh-cyrl.json create mode 100644 www/wiki/includes/installer/i18n/crh-latn.json create mode 100644 www/wiki/includes/installer/i18n/cs.json create mode 100644 www/wiki/includes/installer/i18n/csb.json create mode 100644 www/wiki/includes/installer/i18n/cu.json create mode 100644 www/wiki/includes/installer/i18n/cv.json create mode 100644 www/wiki/includes/installer/i18n/cy.json create mode 100644 www/wiki/includes/installer/i18n/da.json create mode 100644 www/wiki/includes/installer/i18n/de-ch.json create mode 100644 www/wiki/includes/installer/i18n/de-formal.json create mode 100644 www/wiki/includes/installer/i18n/de.json create mode 100644 www/wiki/includes/installer/i18n/diq.json create mode 100644 www/wiki/includes/installer/i18n/dsb.json create mode 100644 www/wiki/includes/installer/i18n/dtp.json create mode 100644 www/wiki/includes/installer/i18n/dty.json create mode 100644 www/wiki/includes/installer/i18n/el.json create mode 100644 www/wiki/includes/installer/i18n/eml.json create mode 100644 www/wiki/includes/installer/i18n/en-gb.json create mode 100644 www/wiki/includes/installer/i18n/en.json create mode 100644 www/wiki/includes/installer/i18n/eo.json create mode 100644 www/wiki/includes/installer/i18n/es-formal.json create mode 100644 www/wiki/includes/installer/i18n/es.json create mode 100644 www/wiki/includes/installer/i18n/et.json create mode 100644 www/wiki/includes/installer/i18n/eu.json create mode 100644 www/wiki/includes/installer/i18n/ext.json create mode 100644 www/wiki/includes/installer/i18n/fa.json create mode 100644 www/wiki/includes/installer/i18n/fi.json create mode 100644 www/wiki/includes/installer/i18n/fo.json create mode 100644 www/wiki/includes/installer/i18n/fr.json create mode 100644 www/wiki/includes/installer/i18n/frc.json create mode 100644 www/wiki/includes/installer/i18n/frp.json create mode 100644 www/wiki/includes/installer/i18n/frr.json create mode 100644 www/wiki/includes/installer/i18n/fur.json create mode 100644 www/wiki/includes/installer/i18n/fy.json create mode 100644 www/wiki/includes/installer/i18n/ga.json create mode 100644 www/wiki/includes/installer/i18n/gag.json create mode 100644 www/wiki/includes/installer/i18n/gan-hans.json create mode 100644 www/wiki/includes/installer/i18n/gan-hant.json create mode 100644 www/wiki/includes/installer/i18n/gd.json create mode 100644 www/wiki/includes/installer/i18n/gl.json create mode 100644 www/wiki/includes/installer/i18n/gom-latn.json create mode 100644 www/wiki/includes/installer/i18n/gor.json create mode 100644 www/wiki/includes/installer/i18n/grc.json create mode 100644 www/wiki/includes/installer/i18n/gsw.json create mode 100644 www/wiki/includes/installer/i18n/gu.json create mode 100644 www/wiki/includes/installer/i18n/gv.json create mode 100644 www/wiki/includes/installer/i18n/hak.json create mode 100644 www/wiki/includes/installer/i18n/haw.json create mode 100644 www/wiki/includes/installer/i18n/he.json create mode 100644 www/wiki/includes/installer/i18n/hi.json create mode 100644 www/wiki/includes/installer/i18n/hif-latn.json create mode 100644 www/wiki/includes/installer/i18n/hil.json create mode 100644 www/wiki/includes/installer/i18n/hr.json create mode 100644 www/wiki/includes/installer/i18n/hrx.json create mode 100644 www/wiki/includes/installer/i18n/hsb.json create mode 100644 www/wiki/includes/installer/i18n/hsn.json create mode 100644 www/wiki/includes/installer/i18n/ht.json create mode 100644 www/wiki/includes/installer/i18n/hu-formal.json create mode 100644 www/wiki/includes/installer/i18n/hu.json create mode 100644 www/wiki/includes/installer/i18n/hy.json create mode 100644 www/wiki/includes/installer/i18n/ia.json create mode 100644 www/wiki/includes/installer/i18n/id.json create mode 100644 www/wiki/includes/installer/i18n/ie.json create mode 100644 www/wiki/includes/installer/i18n/ig.json create mode 100644 www/wiki/includes/installer/i18n/ilo.json create mode 100644 www/wiki/includes/installer/i18n/inh.json create mode 100644 www/wiki/includes/installer/i18n/io.json create mode 100644 www/wiki/includes/installer/i18n/is.json create mode 100644 www/wiki/includes/installer/i18n/it.json create mode 100644 www/wiki/includes/installer/i18n/ja.json create mode 100644 www/wiki/includes/installer/i18n/jam.json create mode 100644 www/wiki/includes/installer/i18n/jbo.json create mode 100644 www/wiki/includes/installer/i18n/jut.json create mode 100644 www/wiki/includes/installer/i18n/jv.json create mode 100644 www/wiki/includes/installer/i18n/ka.json create mode 100644 www/wiki/includes/installer/i18n/kaa.json create mode 100644 www/wiki/includes/installer/i18n/kbd-cyrl.json create mode 100644 www/wiki/includes/installer/i18n/khw.json create mode 100644 www/wiki/includes/installer/i18n/kiu.json create mode 100644 www/wiki/includes/installer/i18n/kk-arab.json create mode 100644 www/wiki/includes/installer/i18n/kk-cyrl.json create mode 100644 www/wiki/includes/installer/i18n/kk-latn.json create mode 100644 www/wiki/includes/installer/i18n/km.json create mode 100644 www/wiki/includes/installer/i18n/kn.json create mode 100644 www/wiki/includes/installer/i18n/ko.json create mode 100644 www/wiki/includes/installer/i18n/krc.json create mode 100644 www/wiki/includes/installer/i18n/ksh.json create mode 100644 www/wiki/includes/installer/i18n/ku-latn.json create mode 100644 www/wiki/includes/installer/i18n/lad.json create mode 100644 www/wiki/includes/installer/i18n/lb.json create mode 100644 www/wiki/includes/installer/i18n/lez.json create mode 100644 www/wiki/includes/installer/i18n/lfn.json create mode 100644 www/wiki/includes/installer/i18n/lg.json create mode 100644 www/wiki/includes/installer/i18n/li.json create mode 100644 www/wiki/includes/installer/i18n/lij.json create mode 100644 www/wiki/includes/installer/i18n/lki.json create mode 100644 www/wiki/includes/installer/i18n/lo.json create mode 100644 www/wiki/includes/installer/i18n/lrc.json create mode 100644 www/wiki/includes/installer/i18n/lt.json create mode 100644 www/wiki/includes/installer/i18n/lv.json create mode 100644 www/wiki/includes/installer/i18n/lzh.json create mode 100644 www/wiki/includes/installer/i18n/lzz.json create mode 100644 www/wiki/includes/installer/i18n/mai.json create mode 100644 www/wiki/includes/installer/i18n/mdf.json create mode 100644 www/wiki/includes/installer/i18n/mfe.json create mode 100644 www/wiki/includes/installer/i18n/mg.json create mode 100644 www/wiki/includes/installer/i18n/mhr.json create mode 100644 www/wiki/includes/installer/i18n/min.json create mode 100644 www/wiki/includes/installer/i18n/mk.json create mode 100644 www/wiki/includes/installer/i18n/ml.json create mode 100644 www/wiki/includes/installer/i18n/mn.json create mode 100644 www/wiki/includes/installer/i18n/mr.json create mode 100644 www/wiki/includes/installer/i18n/ms.json create mode 100644 www/wiki/includes/installer/i18n/mt.json create mode 100644 www/wiki/includes/installer/i18n/my.json create mode 100644 www/wiki/includes/installer/i18n/myv.json create mode 100644 www/wiki/includes/installer/i18n/mzn.json create mode 100644 www/wiki/includes/installer/i18n/nah.json create mode 100644 www/wiki/includes/installer/i18n/nan.json create mode 100644 www/wiki/includes/installer/i18n/nap.json create mode 100644 www/wiki/includes/installer/i18n/nb.json create mode 100644 www/wiki/includes/installer/i18n/nds-nl.json create mode 100644 www/wiki/includes/installer/i18n/nds.json create mode 100644 www/wiki/includes/installer/i18n/ne.json create mode 100644 www/wiki/includes/installer/i18n/nl-informal.json create mode 100644 www/wiki/includes/installer/i18n/nl.json create mode 100644 www/wiki/includes/installer/i18n/nn.json create mode 100644 www/wiki/includes/installer/i18n/oc.json create mode 100644 www/wiki/includes/installer/i18n/olo.json create mode 100644 www/wiki/includes/installer/i18n/or.json create mode 100644 www/wiki/includes/installer/i18n/os.json create mode 100644 www/wiki/includes/installer/i18n/pa.json create mode 100644 www/wiki/includes/installer/i18n/pam.json create mode 100644 www/wiki/includes/installer/i18n/pcd.json create mode 100644 www/wiki/includes/installer/i18n/pdc.json create mode 100644 www/wiki/includes/installer/i18n/pl.json create mode 100644 www/wiki/includes/installer/i18n/pms.json create mode 100644 www/wiki/includes/installer/i18n/pnt.json create mode 100644 www/wiki/includes/installer/i18n/prg.json create mode 100644 www/wiki/includes/installer/i18n/ps.json create mode 100644 www/wiki/includes/installer/i18n/pt-br.json create mode 100644 www/wiki/includes/installer/i18n/pt.json create mode 100644 www/wiki/includes/installer/i18n/qqq.json create mode 100644 www/wiki/includes/installer/i18n/qu.json create mode 100644 www/wiki/includes/installer/i18n/rgn.json create mode 100644 www/wiki/includes/installer/i18n/rm.json create mode 100644 www/wiki/includes/installer/i18n/ro.json create mode 100644 www/wiki/includes/installer/i18n/roa-tara.json create mode 100644 www/wiki/includes/installer/i18n/ru.json create mode 100644 www/wiki/includes/installer/i18n/rue.json create mode 100644 www/wiki/includes/installer/i18n/sa.json create mode 100644 www/wiki/includes/installer/i18n/sah.json create mode 100644 www/wiki/includes/installer/i18n/sc.json create mode 100644 www/wiki/includes/installer/i18n/scn.json create mode 100644 www/wiki/includes/installer/i18n/sco.json create mode 100644 www/wiki/includes/installer/i18n/sd.json create mode 100644 www/wiki/includes/installer/i18n/sdc.json create mode 100644 www/wiki/includes/installer/i18n/sei.json create mode 100644 www/wiki/includes/installer/i18n/sh.json create mode 100644 www/wiki/includes/installer/i18n/shi.json create mode 100644 www/wiki/includes/installer/i18n/si.json create mode 100644 www/wiki/includes/installer/i18n/sk.json create mode 100644 www/wiki/includes/installer/i18n/sl.json create mode 100644 www/wiki/includes/installer/i18n/sli.json create mode 100644 www/wiki/includes/installer/i18n/so.json create mode 100644 www/wiki/includes/installer/i18n/sq.json create mode 100644 www/wiki/includes/installer/i18n/sr-ec.json create mode 100644 www/wiki/includes/installer/i18n/sr-el.json create mode 100644 www/wiki/includes/installer/i18n/srn.json create mode 100644 www/wiki/includes/installer/i18n/ss.json create mode 100644 www/wiki/includes/installer/i18n/stq.json create mode 100644 www/wiki/includes/installer/i18n/su.json create mode 100644 www/wiki/includes/installer/i18n/sv.json create mode 100644 www/wiki/includes/installer/i18n/sw.json create mode 100644 www/wiki/includes/installer/i18n/szl.json create mode 100644 www/wiki/includes/installer/i18n/ta.json create mode 100644 www/wiki/includes/installer/i18n/tcy.json create mode 100644 www/wiki/includes/installer/i18n/te.json create mode 100644 www/wiki/includes/installer/i18n/tet.json create mode 100644 www/wiki/includes/installer/i18n/tg-cyrl.json create mode 100644 www/wiki/includes/installer/i18n/tg-latn.json create mode 100644 www/wiki/includes/installer/i18n/th.json create mode 100644 www/wiki/includes/installer/i18n/tk.json create mode 100644 www/wiki/includes/installer/i18n/tl.json create mode 100644 www/wiki/includes/installer/i18n/tly.json create mode 100644 www/wiki/includes/installer/i18n/tr.json create mode 100644 www/wiki/includes/installer/i18n/tt-cyrl.json create mode 100644 www/wiki/includes/installer/i18n/tt-latn.json create mode 100644 www/wiki/includes/installer/i18n/tyv.json create mode 100644 www/wiki/includes/installer/i18n/udm.json create mode 100644 www/wiki/includes/installer/i18n/ug-arab.json create mode 100644 www/wiki/includes/installer/i18n/uk.json create mode 100644 www/wiki/includes/installer/i18n/ur.json create mode 100644 www/wiki/includes/installer/i18n/uz.json create mode 100644 www/wiki/includes/installer/i18n/vec.json create mode 100644 www/wiki/includes/installer/i18n/vep.json create mode 100644 www/wiki/includes/installer/i18n/vi.json create mode 100644 www/wiki/includes/installer/i18n/vo.json create mode 100644 www/wiki/includes/installer/i18n/vro.json create mode 100644 www/wiki/includes/installer/i18n/wa.json create mode 100644 www/wiki/includes/installer/i18n/war.json create mode 100644 www/wiki/includes/installer/i18n/wo.json create mode 100644 www/wiki/includes/installer/i18n/wuu.json create mode 100644 www/wiki/includes/installer/i18n/xal.json create mode 100644 www/wiki/includes/installer/i18n/xmf.json create mode 100644 www/wiki/includes/installer/i18n/yi.json create mode 100644 www/wiki/includes/installer/i18n/yo.json create mode 100644 www/wiki/includes/installer/i18n/yue.json create mode 100644 www/wiki/includes/installer/i18n/zea.json create mode 100644 www/wiki/includes/installer/i18n/zh-hans.json create mode 100644 www/wiki/includes/installer/i18n/zh-hant.json create mode 100644 www/wiki/includes/installer/i18n/zh-hk.json create mode 100644 www/wiki/includes/installer/i18n/zh-tw.json create mode 100644 www/wiki/includes/interwiki/ClassicInterwikiLookup.php create mode 100644 www/wiki/includes/interwiki/Interwiki.php create mode 100644 www/wiki/includes/interwiki/InterwikiLookup.php create mode 100644 www/wiki/includes/interwiki/InterwikiLookupAdapter.php create mode 100644 www/wiki/includes/interwiki/NullInterwikiLookup.php create mode 100644 www/wiki/includes/jobqueue/Job.php create mode 100644 www/wiki/includes/jobqueue/JobQueue.php create mode 100644 www/wiki/includes/jobqueue/JobQueueDB.php create mode 100644 www/wiki/includes/jobqueue/JobQueueFederated.php create mode 100644 www/wiki/includes/jobqueue/JobQueueGroup.php create mode 100644 www/wiki/includes/jobqueue/JobQueueMemory.php create mode 100644 www/wiki/includes/jobqueue/JobQueueRedis.php create mode 100644 www/wiki/includes/jobqueue/JobQueueSecondTestQueue.php create mode 100644 www/wiki/includes/jobqueue/JobRunner.php create mode 100644 www/wiki/includes/jobqueue/JobSpecification.php create mode 100644 www/wiki/includes/jobqueue/README create mode 100644 www/wiki/includes/jobqueue/aggregator/JobQueueAggregator.php create mode 100644 www/wiki/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php create mode 100644 www/wiki/includes/jobqueue/jobs/ActivityUpdateJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/AssembleUploadChunksJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/CategoryMembershipChangeJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/CdnPurgeJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/ClearUserWatchlistJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/ClearWatchlistNotificationsJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/DeleteLinksJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/DoubleRedirectJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/DuplicateJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/EmaillingJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/EnotifNotifyJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/EnqueueJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/HTMLCacheUpdateJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/NullJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/PublishStashedFileJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/RecentChangesUpdateJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/RefreshLinksJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/ThumbnailRenderJob.php create mode 100644 www/wiki/includes/jobqueue/jobs/UserGroupExpiryJob.php create mode 100644 www/wiki/includes/jobqueue/utils/BacklinkJobUtils.php create mode 100644 www/wiki/includes/jobqueue/utils/PurgeJobUtils.php create mode 100644 www/wiki/includes/json/FormatJson.php create mode 100644 www/wiki/includes/libs/APACHE-LICENSE-2.0.txt create mode 100644 www/wiki/includes/libs/ArrayUtils.php create mode 100644 www/wiki/includes/libs/CSSMin.php create mode 100644 www/wiki/includes/libs/Cookie.php create mode 100644 www/wiki/includes/libs/CookieJar.php create mode 100644 www/wiki/includes/libs/CryptHKDF.php create mode 100644 www/wiki/includes/libs/CryptRand.php create mode 100644 www/wiki/includes/libs/DeferredStringifier.php create mode 100644 www/wiki/includes/libs/DnsSrvDiscoverer.php create mode 100644 www/wiki/includes/libs/ExplodeIterator.php create mode 100644 www/wiki/includes/libs/GenericArrayObject.php create mode 100644 www/wiki/includes/libs/HashRing.php create mode 100644 www/wiki/includes/libs/HtmlArmor.php create mode 100644 www/wiki/includes/libs/HttpStatus.php create mode 100644 www/wiki/includes/libs/IEUrlExtension.php create mode 100644 www/wiki/includes/libs/IP.php create mode 100644 www/wiki/includes/libs/JavaScriptMinifier.php create mode 100644 www/wiki/includes/libs/MWCryptHash.php create mode 100644 www/wiki/includes/libs/MWMessagePack.php create mode 100644 www/wiki/includes/libs/MapCacheLRU.php create mode 100644 www/wiki/includes/libs/MappedIterator.php create mode 100644 www/wiki/includes/libs/MemoizedCallable.php create mode 100644 www/wiki/includes/libs/MessageSpecifier.php create mode 100644 www/wiki/includes/libs/MultiHttpClient.php create mode 100644 www/wiki/includes/libs/ProcessCacheLRU.php create mode 100644 www/wiki/includes/libs/README create mode 100644 www/wiki/includes/libs/ReplacementArray.php create mode 100644 www/wiki/includes/libs/ReverseArrayIterator.php create mode 100644 www/wiki/includes/libs/RiffExtractor.php create mode 100644 www/wiki/includes/libs/StatusValue.php create mode 100644 www/wiki/includes/libs/StringUtils.php create mode 100644 www/wiki/includes/libs/Timing.php create mode 100644 www/wiki/includes/libs/UDPTransport.php create mode 100644 www/wiki/includes/libs/Xhprof.php create mode 100644 www/wiki/includes/libs/XhprofData.php create mode 100644 www/wiki/includes/libs/composer/ComposerInstalled.php create mode 100644 www/wiki/includes/libs/composer/ComposerJson.php create mode 100644 www/wiki/includes/libs/composer/ComposerLock.php create mode 100644 www/wiki/includes/libs/eventrelayer/EventRelayer.php create mode 100644 www/wiki/includes/libs/eventrelayer/EventRelayerKafka.php create mode 100644 www/wiki/includes/libs/eventrelayer/EventRelayerNull.php create mode 100644 www/wiki/includes/libs/filebackend/FSFileBackend.php create mode 100644 www/wiki/includes/libs/filebackend/FileBackend.php create mode 100644 www/wiki/includes/libs/filebackend/FileBackendError.php create mode 100644 www/wiki/includes/libs/filebackend/FileBackendMultiWrite.php create mode 100644 www/wiki/includes/libs/filebackend/FileBackendStore.php create mode 100644 www/wiki/includes/libs/filebackend/FileOpBatch.php create mode 100644 www/wiki/includes/libs/filebackend/HTTPFileStreamer.php create mode 100644 www/wiki/includes/libs/filebackend/MemoryFileBackend.php create mode 100644 www/wiki/includes/libs/filebackend/SwiftFileBackend.php create mode 100644 www/wiki/includes/libs/filebackend/filejournal/FileJournal.php create mode 100644 www/wiki/includes/libs/filebackend/filejournal/NullFileJournal.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/CopyFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/CreateFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/DeleteFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/DescribeFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/FileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/MoveFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/NullFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fileop/StoreFileOp.php create mode 100644 www/wiki/includes/libs/filebackend/fsfile/FSFile.php create mode 100644 www/wiki/includes/libs/filebackend/fsfile/TempFSFile.php create mode 100644 www/wiki/includes/libs/http/HttpAcceptNegotiator.php create mode 100644 www/wiki/includes/libs/http/HttpAcceptParser.php create mode 100644 www/wiki/includes/libs/iterators/IteratorDecorator.php create mode 100644 www/wiki/includes/libs/iterators/NotRecursiveIterator.php create mode 100644 www/wiki/includes/libs/jsminplus.php create mode 100644 www/wiki/includes/libs/lockmanager/DBLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/FSLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/LockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/MemcLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/NullLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/PostgreSqlLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/QuorumLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/RedisLockManager.php create mode 100644 www/wiki/includes/libs/lockmanager/ScopedLock.php create mode 100644 www/wiki/includes/libs/mime/IEContentAnalyzer.php create mode 100644 www/wiki/includes/libs/mime/MimeAnalyzer.php create mode 100644 www/wiki/includes/libs/mime/XmlTypeCheck.php create mode 100644 www/wiki/includes/libs/mime/defines.php create mode 100644 www/wiki/includes/libs/mime/mime.info create mode 100644 www/wiki/includes/libs/mime/mime.types create mode 100644 www/wiki/includes/libs/objectcache/APCBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/APCUBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/BagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/CachedBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/EmptyBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/HashBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/IExpiringStore.php create mode 100644 www/wiki/includes/libs/objectcache/MemcachedBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/MemcachedClient.php create mode 100644 www/wiki/includes/libs/objectcache/MemcachedPeclBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/MemcachedPhpBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/MultiWriteBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/RESTBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/RedisBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/ReplicatedBagOStuff.php create mode 100644 www/wiki/includes/libs/objectcache/WANObjectCache.php create mode 100644 www/wiki/includes/libs/objectcache/WANObjectCacheReaper.php create mode 100644 www/wiki/includes/libs/objectcache/WinCacheBagOStuff.php create mode 100644 www/wiki/includes/libs/rdbms/ChronologyProtector.php create mode 100644 www/wiki/includes/libs/rdbms/TransactionProfiler.php create mode 100644 www/wiki/includes/libs/rdbms/connectionmanager/ConnectionManager.php create mode 100644 www/wiki/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php create mode 100644 www/wiki/includes/libs/rdbms/database/AtomicSectionIdentifier.php create mode 100644 www/wiki/includes/libs/rdbms/database/DBConnRef.php create mode 100644 www/wiki/includes/libs/rdbms/database/Database.php create mode 100644 www/wiki/includes/libs/rdbms/database/DatabaseDomain.php create mode 100644 www/wiki/includes/libs/rdbms/database/DatabaseMssql.php create mode 100644 www/wiki/includes/libs/rdbms/database/DatabaseMysqlBase.php create mode 100644 www/wiki/includes/libs/rdbms/database/DatabaseMysqli.php create mode 100644 www/wiki/includes/libs/rdbms/database/DatabasePostgres.php create mode 100644 www/wiki/includes/libs/rdbms/database/DatabaseSqlite.php create mode 100644 www/wiki/includes/libs/rdbms/database/IDatabase.php create mode 100644 www/wiki/includes/libs/rdbms/database/IMaintainableDatabase.php create mode 100644 www/wiki/includes/libs/rdbms/database/MaintainableDBConnRef.php create mode 100644 www/wiki/includes/libs/rdbms/database/position/DBMasterPos.php create mode 100644 www/wiki/includes/libs/rdbms/database/position/MySQLMasterPos.php create mode 100644 www/wiki/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php create mode 100644 www/wiki/includes/libs/rdbms/database/resultwrapper/IResultWrapper.php create mode 100644 www/wiki/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php create mode 100644 www/wiki/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php create mode 100644 www/wiki/includes/libs/rdbms/database/utils/NextSequenceValue.php create mode 100644 www/wiki/includes/libs/rdbms/database/utils/SavepointPostgres.php create mode 100644 www/wiki/includes/libs/rdbms/defines.php create mode 100644 www/wiki/includes/libs/rdbms/encasing/Blob.php create mode 100644 www/wiki/includes/libs/rdbms/encasing/IBlob.php create mode 100644 www/wiki/includes/libs/rdbms/encasing/LikeMatch.php create mode 100644 www/wiki/includes/libs/rdbms/encasing/MssqlBlob.php create mode 100644 www/wiki/includes/libs/rdbms/encasing/PostgresBlob.php create mode 100644 www/wiki/includes/libs/rdbms/encasing/Subquery.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBAccessError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBConnectionError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBExpectedError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBQueryError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBQueryTimeoutError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBReadOnlyError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBReplicationWaitError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBTransactionError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBTransactionSizeError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBTransactionStateError.php create mode 100644 www/wiki/includes/libs/rdbms/exception/DBUnexpectedError.php create mode 100644 www/wiki/includes/libs/rdbms/field/Field.php create mode 100644 www/wiki/includes/libs/rdbms/field/MssqlField.php create mode 100644 www/wiki/includes/libs/rdbms/field/MySQLField.php create mode 100644 www/wiki/includes/libs/rdbms/field/PostgresField.php create mode 100644 www/wiki/includes/libs/rdbms/field/SQLiteField.php create mode 100644 www/wiki/includes/libs/rdbms/lbfactory/ILBFactory.php create mode 100644 www/wiki/includes/libs/rdbms/lbfactory/LBFactory.php create mode 100644 www/wiki/includes/libs/rdbms/lbfactory/LBFactoryMulti.php create mode 100644 www/wiki/includes/libs/rdbms/lbfactory/LBFactorySimple.php create mode 100644 www/wiki/includes/libs/rdbms/lbfactory/LBFactorySingle.php create mode 100644 www/wiki/includes/libs/rdbms/loadbalancer/ILoadBalancer.php create mode 100644 www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancer.php create mode 100644 www/wiki/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php create mode 100644 www/wiki/includes/libs/rdbms/loadmonitor/ILoadMonitor.php create mode 100644 www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitor.php create mode 100644 www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php create mode 100644 www/wiki/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php create mode 100644 www/wiki/includes/libs/redis/RedisConnRef.php create mode 100644 www/wiki/includes/libs/redis/RedisConnectionPool.php create mode 100644 www/wiki/includes/libs/replacers/DoubleReplacer.php create mode 100644 www/wiki/includes/libs/replacers/HashtableReplacer.php create mode 100644 www/wiki/includes/libs/replacers/RegexlikeReplacer.php create mode 100644 www/wiki/includes/libs/replacers/Replacer.php create mode 100644 www/wiki/includes/libs/stats/BufferingStatsdDataFactory.php create mode 100644 www/wiki/includes/libs/stats/IBufferingStatsdDataFactory.php create mode 100644 www/wiki/includes/libs/stats/NullStatsdDataFactory.php create mode 100644 www/wiki/includes/libs/stats/SamplingStatsdClient.php create mode 100644 www/wiki/includes/libs/stats/StatsdAwareInterface.php create mode 100644 www/wiki/includes/libs/virtualrest/ParsoidVirtualRESTService.php create mode 100644 www/wiki/includes/libs/virtualrest/RestbaseVirtualRESTService.php create mode 100644 www/wiki/includes/libs/virtualrest/SwiftVirtualRESTService.php create mode 100644 www/wiki/includes/libs/virtualrest/VirtualRESTService.php create mode 100644 www/wiki/includes/libs/virtualrest/VirtualRESTServiceClient.php create mode 100644 www/wiki/includes/libs/xmp/XMP.php create mode 100644 www/wiki/includes/libs/xmp/XMPInfo.php create mode 100644 www/wiki/includes/libs/xmp/XMPValidate.php create mode 100644 www/wiki/includes/linkeddata/PageDataRequestHandler.php create mode 100644 www/wiki/includes/linker/LinkRenderer.php create mode 100644 www/wiki/includes/linker/LinkRendererFactory.php create mode 100644 www/wiki/includes/linker/LinkTarget.php create mode 100644 www/wiki/includes/logging/BlockLogFormatter.php create mode 100644 www/wiki/includes/logging/ContentModelLogFormatter.php create mode 100644 www/wiki/includes/logging/DeleteLogFormatter.php create mode 100644 www/wiki/includes/logging/ImportLogFormatter.php create mode 100644 www/wiki/includes/logging/LogEntry.php create mode 100644 www/wiki/includes/logging/LogEventsList.php create mode 100644 www/wiki/includes/logging/LogFormatter.php create mode 100644 www/wiki/includes/logging/LogPage.php create mode 100644 www/wiki/includes/logging/LogPager.php create mode 100644 www/wiki/includes/logging/MergeLogFormatter.php create mode 100644 www/wiki/includes/logging/MoveLogFormatter.php create mode 100644 www/wiki/includes/logging/NewUsersLogFormatter.php create mode 100644 www/wiki/includes/logging/PageLangLogFormatter.php create mode 100644 www/wiki/includes/logging/PatrolLog.php create mode 100644 www/wiki/includes/logging/PatrolLogFormatter.php create mode 100644 www/wiki/includes/logging/ProtectLogFormatter.php create mode 100644 www/wiki/includes/logging/RightsLogFormatter.php create mode 100644 www/wiki/includes/logging/TagLogFormatter.php create mode 100644 www/wiki/includes/logging/UploadLogFormatter.php create mode 100644 www/wiki/includes/logging/WikitextLogFormatter.php create mode 100644 www/wiki/includes/mail/EmailNotification.php create mode 100644 www/wiki/includes/mail/MailAddress.php create mode 100644 www/wiki/includes/mail/UserMailer.php create mode 100644 www/wiki/includes/media/BMP.php create mode 100644 www/wiki/includes/media/Bitmap.php create mode 100644 www/wiki/includes/media/BitmapMetadataHandler.php create mode 100644 www/wiki/includes/media/Bitmap_ClientOnly.php create mode 100644 www/wiki/includes/media/DjVu.php create mode 100644 www/wiki/includes/media/DjVuImage.php create mode 100644 www/wiki/includes/media/Exif.php create mode 100644 www/wiki/includes/media/ExifBitmap.php create mode 100644 www/wiki/includes/media/FormatMetadata.php create mode 100644 www/wiki/includes/media/GIF.php create mode 100644 www/wiki/includes/media/GIFMetadataExtractor.php create mode 100644 www/wiki/includes/media/IPTC.php create mode 100644 www/wiki/includes/media/ImageHandler.php create mode 100644 www/wiki/includes/media/Jpeg.php create mode 100644 www/wiki/includes/media/JpegMetadataExtractor.php create mode 100644 www/wiki/includes/media/MediaHandler.php create mode 100644 www/wiki/includes/media/MediaHandlerFactory.php create mode 100644 www/wiki/includes/media/MediaTransformInvalidParametersException.php create mode 100644 www/wiki/includes/media/MediaTransformOutput.php create mode 100644 www/wiki/includes/media/PNG.php create mode 100644 www/wiki/includes/media/PNGMetadataExtractor.php create mode 100644 www/wiki/includes/media/SVG.php create mode 100644 www/wiki/includes/media/SVGMetadataExtractor.php create mode 100644 www/wiki/includes/media/Tiff.php create mode 100644 www/wiki/includes/media/TransformationalImageHandler.php create mode 100644 www/wiki/includes/media/WebP.php create mode 100644 www/wiki/includes/media/XCF.php create mode 100644 www/wiki/includes/media/tinyrgb.icc create mode 100644 www/wiki/includes/objectcache/ObjectCache.php create mode 100644 www/wiki/includes/objectcache/SqlBagOStuff.php create mode 100644 www/wiki/includes/page/Article.php create mode 100644 www/wiki/includes/page/CategoryPage.php create mode 100644 www/wiki/includes/page/ImageHistoryList.php create mode 100644 www/wiki/includes/page/ImageHistoryPseudoPager.php create mode 100644 www/wiki/includes/page/ImagePage.php create mode 100644 www/wiki/includes/page/Page.php create mode 100644 www/wiki/includes/page/PageArchive.php create mode 100644 www/wiki/includes/page/WikiCategoryPage.php create mode 100644 www/wiki/includes/page/WikiFilePage.php create mode 100644 www/wiki/includes/page/WikiPage.php create mode 100644 www/wiki/includes/pager/AlphabeticPager.php create mode 100644 www/wiki/includes/pager/IndexPager.php create mode 100644 www/wiki/includes/pager/Pager.php create mode 100644 www/wiki/includes/pager/RangeChronologicalPager.php create mode 100644 www/wiki/includes/pager/ReverseChronologicalPager.php create mode 100644 www/wiki/includes/pager/TablePager.php create mode 100644 www/wiki/includes/parser/BlockLevelPass.php create mode 100644 www/wiki/includes/parser/CacheTime.php create mode 100644 www/wiki/includes/parser/CoreParserFunctions.php create mode 100644 www/wiki/includes/parser/CoreTagHooks.php create mode 100644 www/wiki/includes/parser/DateFormatter.php create mode 100644 www/wiki/includes/parser/LinkHolderArray.php create mode 100644 www/wiki/includes/parser/MWTidy.php create mode 100644 www/wiki/includes/parser/Parser.php create mode 100644 www/wiki/includes/parser/ParserCache.php create mode 100644 www/wiki/includes/parser/ParserDiffTest.php create mode 100644 www/wiki/includes/parser/ParserOptions.php create mode 100644 www/wiki/includes/parser/ParserOutput.php create mode 100644 www/wiki/includes/parser/Preprocessor.php create mode 100644 www/wiki/includes/parser/Preprocessor_DOM.php create mode 100644 www/wiki/includes/parser/Preprocessor_Hash.php create mode 100644 www/wiki/includes/parser/RemexStripTagHandler.php create mode 100644 www/wiki/includes/parser/Sanitizer.php create mode 100644 www/wiki/includes/parser/StripState.php create mode 100644 www/wiki/includes/password/BcryptPassword.php create mode 100644 www/wiki/includes/password/EncryptedPassword.php create mode 100644 www/wiki/includes/password/InvalidPassword.php create mode 100644 www/wiki/includes/password/LayeredParameterizedPassword.php create mode 100644 www/wiki/includes/password/MWOldPassword.php create mode 100644 www/wiki/includes/password/MWSaltedPassword.php create mode 100644 www/wiki/includes/password/ParameterizedPassword.php create mode 100644 www/wiki/includes/password/Password.php create mode 100644 www/wiki/includes/password/PasswordError.php create mode 100644 www/wiki/includes/password/PasswordFactory.php create mode 100644 www/wiki/includes/password/PasswordPolicyChecks.php create mode 100644 www/wiki/includes/password/Pbkdf2Password.php create mode 100644 www/wiki/includes/password/UserPasswordPolicy.php create mode 100644 www/wiki/includes/poolcounter/PoolCounter.php create mode 100644 www/wiki/includes/poolcounter/PoolCounterRedis.php create mode 100644 www/wiki/includes/poolcounter/PoolCounterWork.php create mode 100644 www/wiki/includes/poolcounter/PoolCounterWorkViaCallback.php create mode 100644 www/wiki/includes/poolcounter/PoolWorkArticleView.php create mode 100644 www/wiki/includes/preferences/DefaultPreferencesFactory.php create mode 100644 www/wiki/includes/preferences/PreferencesFactory.php create mode 100644 www/wiki/includes/profiler/Profiler.php create mode 100644 www/wiki/includes/profiler/ProfilerSectionOnly.php create mode 100644 www/wiki/includes/profiler/ProfilerStub.php create mode 100644 www/wiki/includes/profiler/ProfilerXhprof.php create mode 100644 www/wiki/includes/profiler/SectionProfiler.php create mode 100644 www/wiki/includes/profiler/output/ProfilerOutput.php create mode 100644 www/wiki/includes/profiler/output/ProfilerOutputDb.php create mode 100644 www/wiki/includes/profiler/output/ProfilerOutputDump.php create mode 100644 www/wiki/includes/profiler/output/ProfilerOutputStats.php create mode 100644 www/wiki/includes/profiler/output/ProfilerOutputText.php create mode 100644 www/wiki/includes/rcfeed/FormattedRCFeed.php create mode 100644 www/wiki/includes/rcfeed/IRCColourfulRCFeedFormatter.php create mode 100644 www/wiki/includes/rcfeed/JSONRCFeedFormatter.php create mode 100644 www/wiki/includes/rcfeed/MachineReadableRCFeedFormatter.php create mode 100644 www/wiki/includes/rcfeed/RCFeed.php create mode 100644 www/wiki/includes/rcfeed/RCFeedEngine.php create mode 100644 www/wiki/includes/rcfeed/RCFeedFormatter.php create mode 100644 www/wiki/includes/rcfeed/RedisPubSubFeedEngine.php create mode 100644 www/wiki/includes/rcfeed/UDPRCFeedEngine.php create mode 100644 www/wiki/includes/rcfeed/XMLRCFeedFormatter.php create mode 100644 www/wiki/includes/registration/ExtensionDependencyError.php create mode 100644 www/wiki/includes/registration/ExtensionJsonValidationError.php create mode 100644 www/wiki/includes/registration/ExtensionJsonValidator.php create mode 100644 www/wiki/includes/registration/ExtensionProcessor.php create mode 100644 www/wiki/includes/registration/ExtensionRegistry.php create mode 100644 www/wiki/includes/registration/Processor.php create mode 100644 www/wiki/includes/registration/VersionChecker.php create mode 100644 www/wiki/includes/resourceloader/DerivativeResourceLoaderContext.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoader.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderContext.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderEditToolbarModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderFileModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderFilePath.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderForeignApiModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderImage.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderImageModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderJqueryMsgModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderLanguageNamesModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderOOUIFileModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderOOUIModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderRawFileModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderSiteModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderSiteStylesModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderUploadDialogModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderUserDefaultsModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderUserModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderUserStylesModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php create mode 100644 www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php create mode 100644 www/wiki/includes/revisiondelete/RevDelArchiveItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelArchiveList.php create mode 100644 www/wiki/includes/revisiondelete/RevDelArchivedFileItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelArchivedFileList.php create mode 100644 www/wiki/includes/revisiondelete/RevDelArchivedRevisionItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelFileItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelFileList.php create mode 100644 www/wiki/includes/revisiondelete/RevDelItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelList.php create mode 100644 www/wiki/includes/revisiondelete/RevDelLogItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelLogList.php create mode 100644 www/wiki/includes/revisiondelete/RevDelRevisionItem.php create mode 100644 www/wiki/includes/revisiondelete/RevDelRevisionList.php create mode 100644 www/wiki/includes/revisiondelete/RevisionDeleteUser.php create mode 100644 www/wiki/includes/revisiondelete/RevisionDeleter.php create mode 100644 www/wiki/includes/search/AugmentPageProps.php create mode 100644 www/wiki/includes/search/DummySearchIndexFieldDefinition.php create mode 100644 www/wiki/includes/search/NullIndexField.php create mode 100644 www/wiki/includes/search/ParserOutputSearchDataExtractor.php create mode 100644 www/wiki/includes/search/PerRowAugmentor.php create mode 100644 www/wiki/includes/search/ResultAugmentor.php create mode 100644 www/wiki/includes/search/ResultSetAugmentor.php create mode 100644 www/wiki/includes/search/SearchDatabase.php create mode 100644 www/wiki/includes/search/SearchEngine.php create mode 100644 www/wiki/includes/search/SearchEngineConfig.php create mode 100644 www/wiki/includes/search/SearchEngineFactory.php create mode 100644 www/wiki/includes/search/SearchExactMatchRescorer.php create mode 100644 www/wiki/includes/search/SearchHighlighter.php create mode 100644 www/wiki/includes/search/SearchIndexField.php create mode 100644 www/wiki/includes/search/SearchIndexFieldDefinition.php create mode 100644 www/wiki/includes/search/SearchMssql.php create mode 100644 www/wiki/includes/search/SearchMySQL.php create mode 100644 www/wiki/includes/search/SearchNearMatchResultSet.php create mode 100644 www/wiki/includes/search/SearchNearMatcher.php create mode 100644 www/wiki/includes/search/SearchOracle.php create mode 100644 www/wiki/includes/search/SearchPostgres.php create mode 100644 www/wiki/includes/search/SearchResult.php create mode 100644 www/wiki/includes/search/SearchResultSet.php create mode 100644 www/wiki/includes/search/SearchSqlite.php create mode 100644 www/wiki/includes/search/SearchSuggestion.php create mode 100644 www/wiki/includes/search/SearchSuggestionSet.php create mode 100644 www/wiki/includes/search/SqlSearchResultSet.php create mode 100644 www/wiki/includes/services/CannotReplaceActiveServiceException.php create mode 100644 www/wiki/includes/services/ContainerDisabledException.php create mode 100644 www/wiki/includes/services/DestructibleService.php create mode 100644 www/wiki/includes/services/NoSuchServiceException.php create mode 100644 www/wiki/includes/services/SalvageableService.php create mode 100644 www/wiki/includes/services/ServiceAlreadyDefinedException.php create mode 100644 www/wiki/includes/services/ServiceContainer.php create mode 100644 www/wiki/includes/services/ServiceDisabledException.php create mode 100644 www/wiki/includes/session/BotPasswordSessionProvider.php create mode 100644 www/wiki/includes/session/CookieSessionProvider.php create mode 100644 www/wiki/includes/session/ImmutableSessionProviderWithCookie.php create mode 100644 www/wiki/includes/session/MetadataMergeException.php create mode 100644 www/wiki/includes/session/PHPSessionHandler.php create mode 100644 www/wiki/includes/session/Session.php create mode 100644 www/wiki/includes/session/SessionBackend.php create mode 100644 www/wiki/includes/session/SessionId.php create mode 100644 www/wiki/includes/session/SessionInfo.php create mode 100644 www/wiki/includes/session/SessionManager.php create mode 100644 www/wiki/includes/session/SessionManagerInterface.php create mode 100644 www/wiki/includes/session/SessionProvider.php create mode 100644 www/wiki/includes/session/SessionProviderInterface.php create mode 100644 www/wiki/includes/session/Token.php create mode 100644 www/wiki/includes/session/UserInfo.php create mode 100644 www/wiki/includes/shell/Command.php create mode 100644 www/wiki/includes/shell/CommandFactory.php create mode 100644 www/wiki/includes/shell/FirejailCommand.php create mode 100644 www/wiki/includes/shell/Result.php create mode 100644 www/wiki/includes/shell/Shell.php create mode 100644 www/wiki/includes/shell/firejail.profile create mode 100755 www/wiki/includes/shell/limit.sh create mode 100644 www/wiki/includes/site/CachingSiteStore.php create mode 100644 www/wiki/includes/site/DBSiteStore.php create mode 100644 www/wiki/includes/site/FileBasedSiteLookup.php create mode 100644 www/wiki/includes/site/HashSiteStore.php create mode 100644 www/wiki/includes/site/MediaWikiPageNameNormalizer.php create mode 100644 www/wiki/includes/site/MediaWikiSite.php create mode 100644 www/wiki/includes/site/Site.php create mode 100644 www/wiki/includes/site/SiteExporter.php create mode 100644 www/wiki/includes/site/SiteImporter.php create mode 100644 www/wiki/includes/site/SiteList.php create mode 100644 www/wiki/includes/site/SiteLookup.php create mode 100644 www/wiki/includes/site/SiteSQLStore.php create mode 100644 www/wiki/includes/site/SiteStore.php create mode 100644 www/wiki/includes/site/SitesCacheFileBuilder.php create mode 100644 www/wiki/includes/skins/BaseTemplate.php create mode 100644 www/wiki/includes/skins/MediaWikiI18N.php create mode 100644 www/wiki/includes/skins/QuickTemplate.php create mode 100644 www/wiki/includes/skins/Skin.php create mode 100644 www/wiki/includes/skins/SkinApi.php create mode 100644 www/wiki/includes/skins/SkinApiTemplate.php create mode 100644 www/wiki/includes/skins/SkinException.php create mode 100644 www/wiki/includes/skins/SkinFactory.php create mode 100644 www/wiki/includes/skins/SkinFallback.php create mode 100644 www/wiki/includes/skins/SkinFallbackTemplate.php create mode 100644 www/wiki/includes/skins/SkinTemplate.php create mode 100644 www/wiki/includes/sparql/SparqlClient.php create mode 100644 www/wiki/includes/sparql/SparqlException.php create mode 100644 www/wiki/includes/specialpage/AuthManagerSpecialPage.php create mode 100644 www/wiki/includes/specialpage/ChangesListSpecialPage.php create mode 100644 www/wiki/includes/specialpage/FormSpecialPage.php create mode 100644 www/wiki/includes/specialpage/ImageQueryPage.php create mode 100644 www/wiki/includes/specialpage/IncludableSpecialPage.php create mode 100644 www/wiki/includes/specialpage/LoginSignupSpecialPage.php create mode 100644 www/wiki/includes/specialpage/PageQueryPage.php create mode 100644 www/wiki/includes/specialpage/QueryPage.php create mode 100644 www/wiki/includes/specialpage/RedirectSpecialPage.php create mode 100644 www/wiki/includes/specialpage/SpecialPage.php create mode 100644 www/wiki/includes/specialpage/SpecialPageFactory.php create mode 100644 www/wiki/includes/specialpage/UnlistedSpecialPage.php create mode 100644 www/wiki/includes/specialpage/WantedQueryPage.php create mode 100644 www/wiki/includes/specials/SpecialActiveusers.php create mode 100644 www/wiki/includes/specials/SpecialAllMessages.php create mode 100644 www/wiki/includes/specials/SpecialAllPages.php create mode 100644 www/wiki/includes/specials/SpecialAncientpages.php create mode 100644 www/wiki/includes/specials/SpecialApiHelp.php create mode 100644 www/wiki/includes/specials/SpecialApiSandbox.php create mode 100644 www/wiki/includes/specials/SpecialAutoblockList.php create mode 100644 www/wiki/includes/specials/SpecialBlankpage.php create mode 100644 www/wiki/includes/specials/SpecialBlock.php create mode 100644 www/wiki/includes/specials/SpecialBlockList.php create mode 100644 www/wiki/includes/specials/SpecialBooksources.php create mode 100644 www/wiki/includes/specials/SpecialBotPasswords.php create mode 100644 www/wiki/includes/specials/SpecialBrokenRedirects.php create mode 100644 www/wiki/includes/specials/SpecialCachedPage.php create mode 100644 www/wiki/includes/specials/SpecialCategories.php create mode 100644 www/wiki/includes/specials/SpecialChangeContentModel.php create mode 100644 www/wiki/includes/specials/SpecialChangeCredentials.php create mode 100644 www/wiki/includes/specials/SpecialChangeEmail.php create mode 100644 www/wiki/includes/specials/SpecialChangePassword.php create mode 100644 www/wiki/includes/specials/SpecialComparePages.php create mode 100644 www/wiki/includes/specials/SpecialConfirmemail.php create mode 100644 www/wiki/includes/specials/SpecialContributions.php create mode 100644 www/wiki/includes/specials/SpecialCreateAccount.php create mode 100644 www/wiki/includes/specials/SpecialDeadendpages.php create mode 100644 www/wiki/includes/specials/SpecialDeletedContributions.php create mode 100644 www/wiki/includes/specials/SpecialDiff.php create mode 100644 www/wiki/includes/specials/SpecialDoubleRedirects.php create mode 100644 www/wiki/includes/specials/SpecialEditTags.php create mode 100644 www/wiki/includes/specials/SpecialEditWatchlist.php create mode 100644 www/wiki/includes/specials/SpecialEmailInvalidate.php create mode 100644 www/wiki/includes/specials/SpecialEmailuser.php create mode 100644 www/wiki/includes/specials/SpecialExpandTemplates.php create mode 100644 www/wiki/includes/specials/SpecialExport.php create mode 100644 www/wiki/includes/specials/SpecialFewestrevisions.php create mode 100644 www/wiki/includes/specials/SpecialFileDuplicateSearch.php create mode 100644 www/wiki/includes/specials/SpecialFilepath.php create mode 100644 www/wiki/includes/specials/SpecialGoToInterwiki.php create mode 100644 www/wiki/includes/specials/SpecialImport.php create mode 100644 www/wiki/includes/specials/SpecialJavaScriptTest.php create mode 100644 www/wiki/includes/specials/SpecialLinkAccounts.php create mode 100644 www/wiki/includes/specials/SpecialLinkSearch.php create mode 100644 www/wiki/includes/specials/SpecialListDuplicatedFiles.php create mode 100644 www/wiki/includes/specials/SpecialListfiles.php create mode 100644 www/wiki/includes/specials/SpecialListgrants.php create mode 100644 www/wiki/includes/specials/SpecialListgrouprights.php create mode 100644 www/wiki/includes/specials/SpecialListredirects.php create mode 100644 www/wiki/includes/specials/SpecialListusers.php create mode 100644 www/wiki/includes/specials/SpecialLockdb.php create mode 100644 www/wiki/includes/specials/SpecialLog.php create mode 100644 www/wiki/includes/specials/SpecialLonelypages.php create mode 100644 www/wiki/includes/specials/SpecialLongpages.php create mode 100644 www/wiki/includes/specials/SpecialMIMEsearch.php create mode 100644 www/wiki/includes/specials/SpecialMediaStatistics.php create mode 100644 www/wiki/includes/specials/SpecialMergeHistory.php create mode 100644 www/wiki/includes/specials/SpecialMostcategories.php create mode 100644 www/wiki/includes/specials/SpecialMostimages.php create mode 100644 www/wiki/includes/specials/SpecialMostinterwikis.php create mode 100644 www/wiki/includes/specials/SpecialMostlinked.php create mode 100644 www/wiki/includes/specials/SpecialMostlinkedcategories.php create mode 100644 www/wiki/includes/specials/SpecialMostlinkedtemplates.php create mode 100644 www/wiki/includes/specials/SpecialMostrevisions.php create mode 100644 www/wiki/includes/specials/SpecialMovepage.php create mode 100644 www/wiki/includes/specials/SpecialMyLanguage.php create mode 100644 www/wiki/includes/specials/SpecialMyRedirectPages.php create mode 100644 www/wiki/includes/specials/SpecialNewimages.php create mode 100644 www/wiki/includes/specials/SpecialNewpages.php create mode 100644 www/wiki/includes/specials/SpecialPageData.php create mode 100644 www/wiki/includes/specials/SpecialPageLanguage.php create mode 100644 www/wiki/includes/specials/SpecialPagesWithProp.php create mode 100644 www/wiki/includes/specials/SpecialPasswordReset.php create mode 100644 www/wiki/includes/specials/SpecialPermanentLink.php create mode 100644 www/wiki/includes/specials/SpecialPreferences.php create mode 100644 www/wiki/includes/specials/SpecialPrefixindex.php create mode 100644 www/wiki/includes/specials/SpecialProtectedpages.php create mode 100644 www/wiki/includes/specials/SpecialProtectedtitles.php create mode 100644 www/wiki/includes/specials/SpecialRandomInCategory.php create mode 100644 www/wiki/includes/specials/SpecialRandompage.php create mode 100644 www/wiki/includes/specials/SpecialRandomredirect.php create mode 100644 www/wiki/includes/specials/SpecialRandomrootpage.php create mode 100644 www/wiki/includes/specials/SpecialRecentchanges.php create mode 100644 www/wiki/includes/specials/SpecialRecentchangeslinked.php create mode 100644 www/wiki/includes/specials/SpecialRedirect.php create mode 100644 www/wiki/includes/specials/SpecialRemoveCredentials.php create mode 100644 www/wiki/includes/specials/SpecialResetTokens.php create mode 100644 www/wiki/includes/specials/SpecialRevisiondelete.php create mode 100644 www/wiki/includes/specials/SpecialRunJobs.php create mode 100644 www/wiki/includes/specials/SpecialSearch.php create mode 100644 www/wiki/includes/specials/SpecialShortpages.php create mode 100644 www/wiki/includes/specials/SpecialSpecialpages.php create mode 100644 www/wiki/includes/specials/SpecialStatistics.php create mode 100644 www/wiki/includes/specials/SpecialTags.php create mode 100644 www/wiki/includes/specials/SpecialTrackingCategories.php create mode 100644 www/wiki/includes/specials/SpecialUnblock.php create mode 100644 www/wiki/includes/specials/SpecialUncategorizedcategories.php create mode 100644 www/wiki/includes/specials/SpecialUncategorizedimages.php create mode 100644 www/wiki/includes/specials/SpecialUncategorizedpages.php create mode 100644 www/wiki/includes/specials/SpecialUncategorizedtemplates.php create mode 100644 www/wiki/includes/specials/SpecialUndelete.php create mode 100644 www/wiki/includes/specials/SpecialUnlinkAccounts.php create mode 100644 www/wiki/includes/specials/SpecialUnlockdb.php create mode 100644 www/wiki/includes/specials/SpecialUnusedcategories.php create mode 100644 www/wiki/includes/specials/SpecialUnusedimages.php create mode 100644 www/wiki/includes/specials/SpecialUnusedtemplates.php create mode 100644 www/wiki/includes/specials/SpecialUnwatchedpages.php create mode 100644 www/wiki/includes/specials/SpecialUpload.php create mode 100644 www/wiki/includes/specials/SpecialUploadStash.php create mode 100644 www/wiki/includes/specials/SpecialUserLogin.php create mode 100644 www/wiki/includes/specials/SpecialUserLogout.php create mode 100644 www/wiki/includes/specials/SpecialUserrights.php create mode 100644 www/wiki/includes/specials/SpecialVersion.php create mode 100644 www/wiki/includes/specials/SpecialWantedcategories.php create mode 100644 www/wiki/includes/specials/SpecialWantedfiles.php create mode 100644 www/wiki/includes/specials/SpecialWantedpages.php create mode 100644 www/wiki/includes/specials/SpecialWantedtemplates.php create mode 100644 www/wiki/includes/specials/SpecialWatchlist.php create mode 100644 www/wiki/includes/specials/SpecialWhatlinkshere.php create mode 100644 www/wiki/includes/specials/SpecialWithoutinterwiki.php create mode 100644 www/wiki/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php create mode 100644 www/wiki/includes/specials/formfields/Licenses.php create mode 100644 www/wiki/includes/specials/formfields/UploadSourceField.php create mode 100644 www/wiki/includes/specials/forms/EditWatchlistNormalHTMLForm.php create mode 100644 www/wiki/includes/specials/forms/PreferencesForm.php create mode 100644 www/wiki/includes/specials/forms/UploadForm.php create mode 100644 www/wiki/includes/specials/helpers/ImportReporter.php create mode 100644 www/wiki/includes/specials/helpers/License.php create mode 100644 www/wiki/includes/specials/helpers/LoginHelper.php create mode 100644 www/wiki/includes/specials/pagers/ActiveUsersPager.php create mode 100644 www/wiki/includes/specials/pagers/AllMessagesTablePager.php create mode 100644 www/wiki/includes/specials/pagers/BlockListPager.php create mode 100644 www/wiki/includes/specials/pagers/CategoryPager.php create mode 100644 www/wiki/includes/specials/pagers/ContribsPager.php create mode 100644 www/wiki/includes/specials/pagers/DeletedContribsPager.php create mode 100644 www/wiki/includes/specials/pagers/ImageListPager.php create mode 100644 www/wiki/includes/specials/pagers/MergeHistoryPager.php create mode 100644 www/wiki/includes/specials/pagers/NewFilesPager.php create mode 100644 www/wiki/includes/specials/pagers/NewPagesPager.php create mode 100644 www/wiki/includes/specials/pagers/ProtectedPagesPager.php create mode 100644 www/wiki/includes/specials/pagers/ProtectedTitlesPager.php create mode 100644 www/wiki/includes/specials/pagers/UsersPager.php create mode 100644 www/wiki/includes/templates/AtomHeader.mustache create mode 100644 www/wiki/includes/templates/AtomItem.mustache create mode 100644 www/wiki/includes/templates/EnhancedChangesListGroup.mustache create mode 100644 www/wiki/includes/templates/NoLocalSettings.mustache create mode 100644 www/wiki/includes/templates/RSSHeader.mustache create mode 100644 www/wiki/includes/templates/RSSItem.mustache create mode 100644 www/wiki/includes/templates/SpecialContributionsLine.mustache create mode 100644 www/wiki/includes/tidy/Balancer.php create mode 100644 www/wiki/includes/tidy/Html5Depurate.php create mode 100644 www/wiki/includes/tidy/Html5Internal.php create mode 100644 www/wiki/includes/tidy/RaggettBase.php create mode 100644 www/wiki/includes/tidy/RaggettExternal.php create mode 100644 www/wiki/includes/tidy/RaggettInternalHHVM.php create mode 100644 www/wiki/includes/tidy/RaggettInternalPHP.php create mode 100644 www/wiki/includes/tidy/RaggettWrapper.php create mode 100644 www/wiki/includes/tidy/RemexCompatFormatter.php create mode 100644 www/wiki/includes/tidy/RemexCompatMunger.php create mode 100644 www/wiki/includes/tidy/RemexDriver.php create mode 100644 www/wiki/includes/tidy/RemexMungerData.php create mode 100644 www/wiki/includes/tidy/TidyDriverBase.php create mode 100644 www/wiki/includes/tidy/tidy.conf create mode 100644 www/wiki/includes/title/ForeignTitle.php create mode 100644 www/wiki/includes/title/ForeignTitleFactory.php create mode 100644 www/wiki/includes/title/ImportTitleFactory.php create mode 100644 www/wiki/includes/title/MalformedTitleException.php create mode 100644 www/wiki/includes/title/MediaWikiTitleCodec.php create mode 100644 www/wiki/includes/title/NaiveForeignTitleFactory.php create mode 100644 www/wiki/includes/title/NaiveImportTitleFactory.php create mode 100644 www/wiki/includes/title/NamespaceAwareForeignTitleFactory.php create mode 100644 www/wiki/includes/title/NamespaceImportTitleFactory.php create mode 100644 www/wiki/includes/title/SubpageImportTitleFactory.php create mode 100644 www/wiki/includes/title/TitleFormatter.php create mode 100644 www/wiki/includes/title/TitleParser.php create mode 100644 www/wiki/includes/title/TitleValue.php create mode 100644 www/wiki/includes/upload/UploadBase.php create mode 100644 www/wiki/includes/upload/UploadFromChunks.php create mode 100644 www/wiki/includes/upload/UploadFromFile.php create mode 100644 www/wiki/includes/upload/UploadFromStash.php create mode 100644 www/wiki/includes/upload/UploadFromUrl.php create mode 100644 www/wiki/includes/upload/UploadStash.php create mode 100644 www/wiki/includes/user/BotPassword.php create mode 100644 www/wiki/includes/user/CentralIdLookup.php create mode 100644 www/wiki/includes/user/ExternalUserNames.php create mode 100644 www/wiki/includes/user/LocalIdLookup.php create mode 100644 www/wiki/includes/user/LoggedOutEditToken.php create mode 100644 www/wiki/includes/user/PasswordReset.php create mode 100644 www/wiki/includes/user/User.php create mode 100644 www/wiki/includes/user/UserArray.php create mode 100644 www/wiki/includes/user/UserArrayFromResult.php create mode 100644 www/wiki/includes/user/UserGroupMembership.php create mode 100644 www/wiki/includes/user/UserIdentity.php create mode 100644 www/wiki/includes/user/UserIdentityValue.php create mode 100644 www/wiki/includes/user/UserNamePrefixSearch.php create mode 100644 www/wiki/includes/user/UserRightsProxy.php create mode 100644 www/wiki/includes/utils/AutoloadGenerator.php create mode 100644 www/wiki/includes/utils/AvroValidator.php create mode 100644 www/wiki/includes/utils/BatchRowIterator.php create mode 100644 www/wiki/includes/utils/BatchRowUpdate.php create mode 100644 www/wiki/includes/utils/BatchRowWriter.php create mode 100644 www/wiki/includes/utils/ExecutableFinder.php create mode 100644 www/wiki/includes/utils/FileContentsHasher.php create mode 100644 www/wiki/includes/utils/MWCryptHKDF.php create mode 100644 www/wiki/includes/utils/MWCryptRand.php create mode 100644 www/wiki/includes/utils/MWFileProps.php create mode 100644 www/wiki/includes/utils/MWRestrictions.php create mode 100644 www/wiki/includes/utils/README create mode 100644 www/wiki/includes/utils/RowUpdateGenerator.php create mode 100644 www/wiki/includes/utils/UIDGenerator.php create mode 100644 www/wiki/includes/utils/ZipDirectoryReader.php create mode 100644 www/wiki/includes/utils/ZipDirectoryReaderError.php create mode 100644 www/wiki/includes/watcheditem/NoWriteWatchedItemStore.php create mode 100644 www/wiki/includes/watcheditem/WatchedItem.php create mode 100644 www/wiki/includes/watcheditem/WatchedItemQueryService.php create mode 100644 www/wiki/includes/watcheditem/WatchedItemQueryServiceExtension.php create mode 100644 www/wiki/includes/watcheditem/WatchedItemStore.php create mode 100644 www/wiki/includes/watcheditem/WatchedItemStoreInterface.php create mode 100644 www/wiki/includes/widget/AUTHORS.txt create mode 100644 www/wiki/includes/widget/ComplexNamespaceInputWidget.php create mode 100644 www/wiki/includes/widget/ComplexTitleInputWidget.php create mode 100644 www/wiki/includes/widget/DateInputWidget.php create mode 100644 www/wiki/includes/widget/DateTimeInputWidget.php create mode 100644 www/wiki/includes/widget/LICENSE.txt create mode 100644 www/wiki/includes/widget/NamespaceInputWidget.php create mode 100644 www/wiki/includes/widget/SearchInputWidget.php create mode 100644 www/wiki/includes/widget/SelectWithInputWidget.php create mode 100644 www/wiki/includes/widget/SizeFilterWidget.php create mode 100644 www/wiki/includes/widget/TitleInputWidget.php create mode 100644 www/wiki/includes/widget/UserInputWidget.php create mode 100644 www/wiki/includes/widget/UsersMultiselectWidget.php create mode 100644 www/wiki/includes/widget/search/BasicSearchResultSetWidget.php create mode 100644 www/wiki/includes/widget/search/DidYouMeanWidget.php create mode 100644 www/wiki/includes/widget/search/FullSearchResultWidget.php create mode 100644 www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php create mode 100644 www/wiki/includes/widget/search/InterwikiSearchResultWidget.php create mode 100644 www/wiki/includes/widget/search/SearchFormWidget.php create mode 100644 www/wiki/includes/widget/search/SearchResultSetWidget.php create mode 100644 www/wiki/includes/widget/search/SearchResultWidget.php create mode 100644 www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php create mode 100644 www/wiki/includes/widget/search/SimpleSearchResultWidget.php (limited to 'www/wiki/includes') diff --git a/www/wiki/includes/.htaccess b/www/wiki/includes/.htaccess new file mode 100644 index 00000000..3a428827 --- /dev/null +++ b/www/wiki/includes/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/www/wiki/includes/ActorMigration.php b/www/wiki/includes/ActorMigration.php new file mode 100644 index 00000000..161c7a92 --- /dev/null +++ b/www/wiki/includes/ActorMigration.php @@ -0,0 +1,383 @@ + [ + 'table' => 'revision_actor_temp', + 'pk' => 'revactor_rev', + 'field' => 'revactor_actor', + 'joinPK' => 'rev_id', + 'extra' => [ + 'revactor_timestamp' => 'rev_timestamp', + 'revactor_page' => 'rev_page', + ], + ], + ]; + + /** + * Fields that formerly used $tempTables + * @var array Key is '$key', value is the MediaWiki version in which it was + * removed from $tempTables. + */ + private static $formerTempTables = []; + + /** + * Define fields that use non-standard mapping + * @var array Keys are the user id column name, values are arrays with two + * elements (the user text column name and the actor id column name) + */ + private static $specialFields = [ + 'ipb_by' => [ 'ipb_by_text', 'ipb_by_actor' ], + ]; + + /** @var array|null Cache for `self::getJoin()` */ + private $joinCache = null; + + /** @var int One of the MIGRATION_* constants */ + private $stage; + + /** @private */ + public function __construct( $stage ) { + $this->stage = $stage; + } + + /** + * Static constructor + * @return ActorMigration + */ + public static function newMigration() { + return MediaWikiServices::getInstance()->getActorMigration(); + } + + /** + * Return an SQL condition to test if a user field is anonymous + * @param string $field Field name or SQL fragment + * @return string + */ + public function isAnon( $field ) { + return $this->stage === MIGRATION_NEW ? "$field IS NULL" : "$field = 0"; + } + + /** + * Return an SQL condition to test if a user field is non-anonymous + * @param string $field Field name or SQL fragment + * @return string + */ + public function isNotAnon( $field ) { + return $this->stage === MIGRATION_NEW ? "$field IS NOT NULL" : "$field != 0"; + } + + /** + * @param string $key A key such as "rev_user" identifying the actor + * field being fetched. + * @return string[] [ $text, $actor ] + */ + private static function getFieldNames( $key ) { + if ( isset( self::$specialFields[$key] ) ) { + return self::$specialFields[$key]; + } + + return [ $key . '_text', substr( $key, 0, -5 ) . '_actor' ]; + } + + /** + * Get SELECT fields and joins for the actor key + * + * @param string $key A key such as "rev_user" identifying the actor + * field being fetched. + * @return array With three keys: + * - tables: (string[]) to include in the `$table` to `IDatabase->select()` + * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` + * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` + * All tables, fields, and joins are aliased, so `+` is safe to use. + */ + public function getJoin( $key ) { + if ( !isset( $this->joinCache[$key] ) ) { + $tables = []; + $fields = []; + $joins = []; + + list( $text, $actor ) = self::getFieldNames( $key ); + + if ( $this->stage === MIGRATION_OLD ) { + $fields[$key] = $key; + $fields[$text] = $text; + $fields[$actor] = 'NULL'; + } else { + $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN'; + + if ( isset( self::$tempTables[$key] ) ) { + $t = self::$tempTables[$key]; + $alias = "temp_$key"; + $tables[$alias] = $t['table']; + $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ]; + $joinField = "{$alias}.{$t['field']}"; + } else { + $joinField = $actor; + } + + $alias = "actor_$key"; + $tables[$alias] = 'actor'; + $joins[$alias] = [ $join, "{$alias}.actor_id = {$joinField}" ]; + + if ( $this->stage === MIGRATION_NEW ) { + $fields[$key] = "{$alias}.actor_user"; + $fields[$text] = "{$alias}.actor_name"; + } else { + $fields[$key] = "COALESCE( {$alias}.actor_user, $key )"; + $fields[$text] = "COALESCE( {$alias}.actor_name, $text )"; + } + $fields[$actor] = $joinField; + } + + $this->joinCache[$key] = [ + 'tables' => $tables, + 'fields' => $fields, + 'joins' => $joins, + ]; + } + + return $this->joinCache[$key]; + } + + /** + * Get UPDATE fields for the actor + * + * @param IDatabase $dbw Database to use for creating an actor ID, if necessary + * @param string $key A key such as "rev_user" identifying the actor + * field being fetched. + * @param UserIdentity $user User to set in the update + * @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()` + */ + public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) { + if ( isset( self::$tempTables[$key] ) ) { + throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" ); + } + + list( $text, $actor ) = self::getFieldNames( $key ); + $ret = []; + if ( $this->stage <= MIGRATION_WRITE_BOTH ) { + $ret[$key] = $user->getId(); + $ret[$text] = $user->getName(); + } + if ( $this->stage >= MIGRATION_WRITE_BOTH ) { + // We need to be able to assign an actor ID if none exists + if ( !$user instanceof User && !$user->getActorId() ) { + $user = User::newFromAnyId( $user->getId(), $user->getName(), null ); + } + $ret[$actor] = $user->getActorId( $dbw ); + } + return $ret; + } + + /** + * Get UPDATE fields for the actor + * + * @param IDatabase $dbw Database to use for creating an actor ID, if necessary + * @param string $key A key such as "rev_user" identifying the actor + * field being fetched. + * @param UserIdentity $user User to set in the update + * @return array with two values: + * - array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()` + * - callback to call with the the primary key for the main table insert + * and extra fields needed for the temp table. + */ + public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) { + if ( isset( self::$formerTempTables[$key] ) ) { + wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] ); + } elseif ( !isset( self::$tempTables[$key] ) ) { + throw new InvalidArgumentException( "Must use getInsertValues() for $key" ); + } + + list( $text, $actor ) = self::getFieldNames( $key ); + $ret = []; + $callback = null; + if ( $this->stage <= MIGRATION_WRITE_BOTH ) { + $ret[$key] = $user->getId(); + $ret[$text] = $user->getName(); + } + if ( $this->stage >= MIGRATION_WRITE_BOTH ) { + // We need to be able to assign an actor ID if none exists + if ( !$user instanceof User && !$user->getActorId() ) { + $user = User::newFromAnyId( $user->getId(), $user->getName(), null ); + } + $id = $user->getActorId( $dbw ); + + if ( isset( self::$tempTables[$key] ) ) { + $func = __METHOD__; + $callback = function ( $pk, array $extra ) use ( $dbw, $key, $id, $func ) { + $t = self::$tempTables[$key]; + $set = [ $t['field'] => $id ]; + foreach ( $t['extra'] as $to => $from ) { + if ( !array_key_exists( $from, $extra ) ) { + throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" ); + } + $set[$to] = $extra[$from]; + } + $dbw->upsert( + $t['table'], + [ $t['pk'] => $pk ] + $set, + [ $t['pk'] ], + $set, + $func + ); + }; + } else { + $ret[$actor] = $id; + $callback = function ( $pk, array $extra ) { + }; + } + } elseif ( isset( self::$tempTables[$key] ) ) { + $func = __METHOD__; + $callback = function ( $pk, array $extra ) use ( $key, $func ) { + $t = self::$tempTables[$key]; + foreach ( $t['extra'] as $to => $from ) { + if ( !array_key_exists( $from, $extra ) ) { + throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" ); + } + } + }; + } else { + $callback = function ( $pk, array $extra ) { + }; + } + return [ $ret, $callback ]; + } + + /** + * Get WHERE condition for the actor + * + * @param IDatabase $db Database to use for quoting and list-making + * @param string $key A key such as "rev_user" identifying the actor + * field being fetched. + * @param UserIdentity|UserIdentity[] $users Users to test for + * @param bool $useId If false, don't try to query by the user ID. + * Intended for use with rc_user since it has an index on + * (rc_user_text,rc_timestamp) but not (rc_user,rc_timestamp). + * @return array With three keys: + * - tables: (string[]) to include in the `$table` to `IDatabase->select()` + * - conds: (string) to include in the `$cond` to `IDatabase->select()` + * - orconds: (array[]) array of alternatives in case a union of multiple + * queries would be more efficient than a query with OR. May have keys + * 'actor', 'userid', 'username'. + * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` + * All tables and joins are aliased, so `+` is safe to use. + */ + public function getWhere( IDatabase $db, $key, $users, $useId = true ) { + $tables = []; + $conds = []; + $joins = []; + + if ( $users instanceof UserIdentity ) { + $users = [ $users ]; + } + + // Get information about all the passed users + $ids = []; + $names = []; + $actors = []; + foreach ( $users as $user ) { + if ( $useId && $user->getId() ) { + $ids[] = $user->getId(); + } else { + $names[] = $user->getName(); + } + $actorId = $user->getActorId(); + if ( $actorId ) { + $actors[] = $actorId; + } + } + + list( $text, $actor ) = self::getFieldNames( $key ); + + // Combine data into conditions to be ORed together + $actorNotEmpty = []; + if ( $this->stage === MIGRATION_OLD ) { + $actors = []; + $actorEmpty = []; + } elseif ( isset( self::$tempTables[$key] ) ) { + $t = self::$tempTables[$key]; + $alias = "temp_$key"; + $tables[$alias] = $t['table']; + $joins[$alias] = [ + $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN', + "{$alias}.{$t['pk']} = {$t['joinPK']}" + ]; + $joinField = "{$alias}.{$t['field']}"; + $actorEmpty = [ $joinField => null ]; + if ( $this->stage !== MIGRATION_NEW ) { + // Otherwise the resulting test can evaluate to NULL, and + // NOT(NULL) is NULL rather than true. + $actorNotEmpty = [ "$joinField IS NOT NULL" ]; + } + } else { + $joinField = $actor; + $actorEmpty = [ $joinField => 0 ]; + } + + if ( $actors ) { + $conds['actor'] = $db->makeList( + $actorNotEmpty + [ $joinField => $actors ], IDatabase::LIST_AND + ); + } + if ( $this->stage < MIGRATION_NEW && $ids ) { + $conds['userid'] = $db->makeList( + $actorEmpty + [ $key => $ids ], IDatabase::LIST_AND + ); + } + if ( $this->stage < MIGRATION_NEW && $names ) { + $conds['username'] = $db->makeList( + $actorEmpty + [ $text => $names ], IDatabase::LIST_AND + ); + } + + return [ + 'tables' => $tables, + 'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0', + 'orconds' => $conds, + 'joins' => $joins, + ]; + } + +} diff --git a/www/wiki/includes/AjaxDispatcher.php b/www/wiki/includes/AjaxDispatcher.php new file mode 100644 index 00000000..75fcff36 --- /dev/null +++ b/www/wiki/includes/AjaxDispatcher.php @@ -0,0 +1,163 @@ +config = $config; + + $this->mode = ""; + + if ( !empty( $_GET["rs"] ) ) { + $this->mode = "get"; + } + + if ( !empty( $_POST["rs"] ) ) { + $this->mode = "post"; + } + + switch ( $this->mode ) { + case 'get': + $this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : ''; + if ( !empty( $_GET["rsargs"] ) ) { + $this->args = $_GET["rsargs"]; + } else { + $this->args = []; + } + break; + case 'post': + $this->func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : ''; + if ( !empty( $_POST["rsargs"] ) ) { + $this->args = $_POST["rsargs"]; + } else { + $this->args = []; + } + break; + default: + return; + # Or we could throw an exception: + # throw new MWException( __METHOD__ . ' called without any data (mode empty).' ); + } + } + + /** + * Pass the request to our internal function. + * BEWARE! Data are passed as they have been supplied by the user, + * they should be carefully handled in the function processing the + * request. + * + * @param User $user + */ + function performAction( User $user ) { + if ( empty( $this->mode ) ) { + return; + } + + if ( !in_array( $this->func_name, $this->config->get( 'AjaxExportList' ) ) ) { + wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" ); + wfHttpError( + 400, + 'Bad Request', + "unknown function " . $this->func_name + ); + } elseif ( !User::isEveryoneAllowed( 'read' ) && !$user->isAllowed( 'read' ) ) { + wfHttpError( + 403, + 'Forbidden', + 'You are not allowed to view pages.' ); + } else { + wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" ); + try { + $result = call_user_func_array( $this->func_name, $this->args ); + + if ( $result === false || $result === null ) { + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + "no data returned\n" ); + + wfHttpError( 500, 'Internal Error', + "{$this->func_name} returned no data" ); + } else { + if ( is_string( $result ) ) { + $result = new AjaxResponse( $result ); + } + + // Make sure DB commit succeeds before sending a response + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->commitMasterChanges( __METHOD__ ); + + $result->sendHeaders(); + $result->printText(); + + wfDebug( __METHOD__ . ' dispatch complete for ' . $this->func_name . "\n" ); + } + } catch ( Exception $e ) { + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + get_class( $e ) . ": " . $e->getMessage() . "\n" ); + + if ( !headers_sent() ) { + wfHttpError( 500, 'Internal Error', + $e->getMessage() ); + } else { + print $e->getMessage(); + } + } + } + } +} diff --git a/www/wiki/includes/AjaxResponse.php b/www/wiki/includes/AjaxResponse.php new file mode 100644 index 00000000..3e42c086 --- /dev/null +++ b/www/wiki/includes/AjaxResponse.php @@ -0,0 +1,315 @@ +disable() + * @var bool $mDisabled + */ + private $mDisabled; + + /** + * Date for the HTTP header Last-modified + * @var string|bool $mLastModified + */ + private $mLastModified; + + /** + * HTTP response code + * @var string $mResponseCode + */ + private $mResponseCode; + + /** + * HTTP Vary header + * @var string $mVary + */ + private $mVary; + + /** + * Content of our HTTP response + * @var string $mText + */ + private $mText; + + /** + * @var Config + */ + private $mConfig; + + /** + * @param string|null $text + * @param Config|null $config + */ + function __construct( $text = null, Config $config = null ) { + $this->mCacheDuration = null; + $this->mVary = null; + $this->mConfig = $config ?: MediaWikiServices::getInstance()->getMainConfig(); + + $this->mDisabled = false; + $this->mText = ''; + $this->mResponseCode = 200; + $this->mLastModified = false; + $this->mContentType = 'application/x-wiki'; + + if ( $text ) { + $this->addText( $text ); + } + } + + /** + * Set the number of seconds to get the response cached by a proxy + * @param int $duration + */ + function setCacheDuration( $duration ) { + $this->mCacheDuration = $duration; + } + + /** + * Set the HTTP Vary header + * @param string $vary + */ + function setVary( $vary ) { + $this->mVary = $vary; + } + + /** + * Set the HTTP response code + * @param string $code + */ + function setResponseCode( $code ) { + $this->mResponseCode = $code; + } + + /** + * Set the HTTP header Content-Type + * @param string $type + */ + function setContentType( $type ) { + $this->mContentType = $type; + } + + /** + * Disable output. + */ + function disable() { + $this->mDisabled = true; + } + + /** + * Add content to the response + * @param string $text + */ + function addText( $text ) { + if ( !$this->mDisabled && $text ) { + $this->mText .= $text; + } + } + + /** + * Output text + */ + function printText() { + if ( !$this->mDisabled ) { + print $this->mText; + } + } + + /** + * Construct the header and output it + */ + function sendHeaders() { + if ( $this->mResponseCode ) { + // For back-compat, it is supported that mResponseCode be a string like " 200 OK" + // (with leading space and the status message after). Cast response code to an integer + // to take advantage of PHP's conversion rules which will turn " 200 OK" into 200. + // https://secure.php.net/manual/en/language.types.string.php#language.types.string.conversion + $n = intval( trim( $this->mResponseCode ) ); + HttpStatus::header( $n ); + } + + header( "Content-Type: " . $this->mContentType ); + + if ( $this->mLastModified ) { + header( "Last-Modified: " . $this->mLastModified ); + } else { + header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); + } + + if ( $this->mCacheDuration ) { + # If CDN caches are configured, tell them to cache the response, + # and tell the client to always check with the CDN. Otherwise, + # tell the client to use a cached copy, without a way to purge it. + + if ( $this->mConfig->get( 'UseSquid' ) ) { + # Expect explicit purge of the proxy cache, but require end user agents + # to revalidate against the proxy on each visit. + # Surrogate-Control controls our CDN, Cache-Control downstream caches + + if ( $this->mConfig->get( 'UseESI' ) ) { + header( 'Surrogate-Control: max-age=' . $this->mCacheDuration . ', content="ESI/1.0"' ); + header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); + } else { + header( 'Cache-Control: s-maxage=' . $this->mCacheDuration . ', must-revalidate, max-age=0' ); + } + + } else { + # Let the client do the caching. Cache is not purged. + header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); + header( "Cache-Control: s-maxage={$this->mCacheDuration}," . + "public,max-age={$this->mCacheDuration}" ); + } + + } else { + # always expired, always modified + header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past + header( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 + header( "Pragma: no-cache" ); // HTTP/1.0 + } + + if ( $this->mVary ) { + header( "Vary: " . $this->mVary ); + } + } + + /** + * checkLastModified tells the client to use the client-cached response if + * possible. If successful, the AjaxResponse is disabled so that + * any future call to AjaxResponse::printText() have no effect. + * + * @param string $timestamp + * @return bool Returns true if the response code was set to 304 Not Modified. + */ + function checkLastModified( $timestamp ) { + global $wgCachePages, $wgCacheEpoch, $wgUser; + $fname = 'AjaxResponse::checkLastModified'; + + if ( !$timestamp || $timestamp == '19700101000000' ) { + wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP", 'private' ); + return false; + } + + if ( !$wgCachePages ) { + wfDebug( "$fname: CACHE DISABLED", 'private' ); + return false; + } + + $timestamp = wfTimestamp( TS_MW, $timestamp ); + $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->getTouched(), $wgCacheEpoch ) ); + + if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + # IE sends sizes after the date like this: + # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 + # this breaks strtotime(). + $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + $modsinceTime = strtotime( $modsince ); + $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); + wfDebug( "$fname: -- client send If-Modified-Since: $modsince", 'private' ); + wfDebug( "$fname: -- we might send Last-Modified : $lastmod", 'private' ); + + if ( ( $ismodsince >= $timestamp ) + && $wgUser->validateCache( $ismodsince ) && + $ismodsince >= $wgCacheEpoch + ) { + ini_set( 'zlib.output_compression', 0 ); + $this->setResponseCode( 304 ); + $this->disable(); + $this->mLastModified = $lastmod; + + wfDebug( "$fname: CACHED client: $ismodsince ; user: {$wgUser->getTouched()} ; " . + "page: $timestamp ; site $wgCacheEpoch", 'private' ); + + return true; + } else { + wfDebug( "$fname: READY client: $ismodsince ; user: {$wgUser->getTouched()} ; " . + "page: $timestamp ; site $wgCacheEpoch", 'private' ); + $this->mLastModified = $lastmod; + } + } else { + wfDebug( "$fname: client did not send If-Modified-Since header", 'private' ); + $this->mLastModified = $lastmod; + } + return false; + } + + /** + * @param string $mckey + * @param int $touched + * @return bool + */ + function loadFromMemcached( $mckey, $touched ) { + if ( !$touched ) { + return false; + } + + $mcvalue = ObjectCache::getMainWANInstance()->get( $mckey ); + if ( $mcvalue ) { + # Check to see if the value has been invalidated + if ( $touched <= $mcvalue['timestamp'] ) { + wfDebug( "Got $mckey from cache" ); + $this->mText = $mcvalue['value']; + + return true; + } else { + wfDebug( "$mckey has expired" ); + } + } + + return false; + } + + /** + * @param string $mckey + * @param int $expiry + * @return bool + */ + function storeInMemcached( $mckey, $expiry = 86400 ) { + ObjectCache::getMainWANInstance()->set( $mckey, + [ + 'timestamp' => wfTimestampNow(), + 'value' => $this->mText + ], $expiry + ); + + return true; + } +} diff --git a/www/wiki/includes/AuthPlugin.php b/www/wiki/includes/AuthPlugin.php new file mode 100644 index 00000000..b73ecbd8 --- /dev/null +++ b/www/wiki/includes/AuthPlugin.php @@ -0,0 +1,368 @@ + + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Authentication plugin interface. Instantiate a subclass of AuthPlugin + * and set $wgAuth to it to authenticate against some external tool. + * + * The default behavior is not to do anything, and use the local user + * database for all authentication. A subclass can require that all + * accounts authenticate externally, or use it only as a fallback; also + * you can transparently create internal wiki accounts the first time + * someone logs in who can be authenticated externally. + * + * @deprecated since 1.27 + */ +class AuthPlugin { + /** + * @var string + */ + protected $domain; + + /** + * Check whether there exists a user account with the given name. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param string $username Username. + * @return bool + */ + public function userExists( $username ) { + # Override this! + return false; + } + + /** + * Check if a username+password pair is a valid login. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param string $username Username. + * @param string $password User password. + * @return bool + */ + public function authenticate( $username, $password ) { + # Override this! + return false; + } + + /** + * Modify options in the login template. + * + * @param BaseTemplate &$template + * @param string &$type 'signup' or 'login'. Added in 1.16. + */ + public function modifyUITemplate( &$template, &$type ) { + # Override this! + $template->set( 'usedomain', false ); + } + + /** + * Set the domain this plugin is supposed to use when authenticating. + * + * @param string $domain Authentication domain. + */ + public function setDomain( $domain ) { + $this->domain = $domain; + } + + /** + * Get the user's domain + * + * @return string + */ + public function getDomain() { + if ( isset( $this->domain ) ) { + return $this->domain; + } else { + return 'invaliddomain'; + } + } + + /** + * Check to see if the specific domain is a valid domain. + * + * @param string $domain Authentication domain. + * @return bool + */ + public function validDomain( $domain ) { + # Override this! + return true; + } + + /** + * When a user logs in, optionally fill in preferences and such. + * For instance, you might pull the email address or real name from the + * external user database. + * + * The User object is passed by reference so it can be modified; don't + * forget the & on your function declaration. + * + * @deprecated since 1.26, use the UserLoggedIn hook instead. And assigning + * a different User object to $user is no longer supported. + * @param User &$user + * @return bool + */ + public function updateUser( &$user ) { + # Override this and do something + return true; + } + + /** + * Return true if the wiki should create a new local account automatically + * when asked to login a user who doesn't exist locally but does in the + * external auth database. + * + * If you don't automatically create accounts, you must still create + * accounts in some way. It's not possible to authenticate without + * a local account. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + */ + public function autoCreate() { + return false; + } + + /** + * Allow a property change? Properties are the same as preferences + * and use the same keys. 'Realname' 'Emailaddress' and 'Nickname' + * all reference this. + * + * @param string $prop + * + * @return bool + */ + public function allowPropChange( $prop = '' ) { + if ( $prop == 'realname' && is_callable( [ $this, 'allowRealNameChange' ] ) ) { + return $this->allowRealNameChange(); + } elseif ( $prop == 'emailaddress' && is_callable( [ $this, 'allowEmailChange' ] ) ) { + return $this->allowEmailChange(); + } elseif ( $prop == 'nickname' && is_callable( [ $this, 'allowNickChange' ] ) ) { + return $this->allowNickChange(); + } else { + return true; + } + } + + /** + * Can users change their passwords? + * + * @return bool + */ + public function allowPasswordChange() { + return true; + } + + /** + * Should MediaWiki store passwords in its local database? + * + * @return bool + */ + public function allowSetLocalPassword() { + return true; + } + + /** + * Set the given password in the authentication database. + * As a special case, the password may be set to null to request + * locking the password to an unusable value, with the expectation + * that it will be set later through a mail reset or other method. + * + * Return true if successful. + * + * @param User $user + * @param string $password Password. + * @return bool + */ + public function setPassword( $user, $password ) { + return true; + } + + /** + * Update user information in the external authentication database. + * Return true if successful. + * + * @deprecated since 1.26, use the UserSaveSettings hook instead. + * @param User $user + * @return bool + */ + public function updateExternalDB( $user ) { + return true; + } + + /** + * Update user groups in the external authentication database. + * Return true if successful. + * + * @deprecated since 1.26, use the UserGroupsChanged hook instead. + * @param User $user + * @param array $addgroups Groups to add. + * @param array $delgroups Groups to remove. + * @return bool + */ + public function updateExternalDBGroups( $user, $addgroups, $delgroups = [] ) { + return true; + } + + /** + * Check to see if external accounts can be created. + * Return true if external accounts can be created. + * @return bool + */ + public function canCreateAccounts() { + return false; + } + + /** + * Add a user to the external authentication database. + * Return true if successful. + * + * @param User $user Only the name should be assumed valid at this point + * @param string $password + * @param string $email + * @param string $realname + * @return bool + */ + public function addUser( $user, $password, $email = '', $realname = '' ) { + return true; + } + + /** + * Return true to prevent logins that don't authenticate here from being + * checked against the local database's password fields. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + */ + public function strict() { + return false; + } + + /** + * Check if a user should authenticate locally if the global authentication fails. + * If either this or strict() returns true, local authentication is not used. + * + * @param string $username Username. + * @return bool + */ + public function strictUserAuth( $username ) { + return false; + } + + /** + * When creating a user account, optionally fill in preferences and such. + * For instance, you might pull the email address or real name from the + * external user database. + * + * The User object is passed by reference so it can be modified; don't + * forget the & on your function declaration. + * + * @deprecated since 1.26, use the UserLoggedIn hook instead. And assigning + * a different User object to $user is no longer supported. + * @param User &$user + * @param bool $autocreate True if user is being autocreated on login + */ + public function initUser( &$user, $autocreate = false ) { + # Override this to do something. + } + + /** + * If you want to munge the case of an account name before the final + * check, now is your chance. + * @param string $username + * @return string + */ + public function getCanonicalName( $username ) { + return $username; + } + + /** + * Get an instance of a User object + * + * @param User &$user + * + * @return AuthPluginUser + */ + public function getUserInstance( User &$user ) { + return new AuthPluginUser( $user ); + } + + /** + * Get a list of domains (in HTMLForm options format) used. + * + * @return array + */ + public function domainList() { + return []; + } +} + +/** + * @deprecated since 1.27 + */ +class AuthPluginUser { + function __construct( $user ) { + # Override this! + } + + public function getId() { + # Override this! + return -1; + } + + /** + * Indicate whether the user is locked + * @deprecated since 1.26, use the UserIsLocked hook instead. + * @return bool + */ + public function isLocked() { + # Override this! + return false; + } + + /** + * Indicate whether the user is hidden + * @deprecated since 1.26, use the UserIsHidden hook instead. + * @return bool + */ + public function isHidden() { + # Override this! + return false; + } + + /** + * @deprecated since 1.28, use SessionManager::invalidateSessionForUser() instead. + * @return bool + */ + public function resetAuthToken() { + # Override this! + return true; + } +} diff --git a/www/wiki/includes/AutoLoader.php b/www/wiki/includes/AutoLoader.php new file mode 100644 index 00000000..6e770565 --- /dev/null +++ b/www/wiki/includes/AutoLoader.php @@ -0,0 +1,137 @@ + Path (ends with /) + */ + static public $psr4Namespaces = []; + + /** + * autoload - take a class name and attempt to load it + * + * @param string $className Name of class we're looking for. + */ + static function autoload( $className ) { + global $wgAutoloadClasses, $wgAutoloadLocalClasses, + $wgAutoloadAttemptLowercase; + + $filename = false; + + if ( isset( $wgAutoloadLocalClasses[$className] ) ) { + $filename = $wgAutoloadLocalClasses[$className]; + } elseif ( isset( $wgAutoloadClasses[$className] ) ) { + $filename = $wgAutoloadClasses[$className]; + } elseif ( $wgAutoloadAttemptLowercase ) { + /* + * Try a different capitalisation. + * + * PHP 4 objects are always serialized with the classname coerced to lowercase, + * and we are plagued with several legacy uses created by MediaWiki < 1.5, see + * https://wikitech.wikimedia.org/wiki/Text_storage_data + */ + $lowerClass = strtolower( $className ); + + if ( self::$autoloadLocalClassesLower === null ) { + self::$autoloadLocalClassesLower = array_change_key_case( $wgAutoloadLocalClasses, CASE_LOWER ); + } + + if ( isset( self::$autoloadLocalClassesLower[$lowerClass] ) ) { + if ( function_exists( 'wfDebugLog' ) ) { + wfDebugLog( 'autoloader', "Class {$className} was loaded using incorrect case" ); + } + $filename = self::$autoloadLocalClassesLower[$lowerClass]; + } + } + + if ( !$filename && strpos( $className, '\\' ) !== false ) { + // This class is namespaced, so try looking at the namespace map + $prefix = $className; + while ( false !== $pos = strrpos( $prefix, '\\' ) ) { + // Check to see if this namespace prefix is in the map + $prefix = substr( $className, 0, $pos + 1 ); + if ( isset( self::$psr4Namespaces[$prefix] ) ) { + $relativeClass = substr( $className, $pos + 1 ); + // Build the expected filename, and see if it exists + $file = self::$psr4Namespaces[$prefix] . '/' . + str_replace( '\\', '/', $relativeClass ) . '.php'; + if ( file_exists( $file ) ) { + $filename = $file; + break; + } + } + + // Remove trailing separator for next iteration + $prefix = rtrim( $prefix, '\\' ); + } + } + + if ( !$filename ) { + // Class not found; let the next autoloader try to find it + return; + } + + // Make an absolute path, this improves performance by avoiding some stat calls + if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) { + global $IP; + $filename = "$IP/$filename"; + } + + require $filename; + } + + /** + * Method to clear the protected class property $autoloadLocalClassesLower. + * Used in tests. + */ + static function resetAutoloadLocalClassesLower() { + self::$autoloadLocalClassesLower = null; + } + + /** + * Get a mapping of namespace => file path + * The namespaces should follow the PSR-4 standard for autoloading + * + * @see + * @private Only public for usage in AutoloadGenerator + * @since 1.31 + * @return string[] + */ + public static function getAutoloadNamespaces() { + return [ + 'MediaWiki\\Linker\\' => __DIR__ .'/linker/' + ]; + } +} + +AutoLoader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces(); +spl_autoload_register( [ 'AutoLoader', 'autoload' ] ); diff --git a/www/wiki/includes/Autopromote.php b/www/wiki/includes/Autopromote.php new file mode 100644 index 00000000..a01465e9 --- /dev/null +++ b/www/wiki/includes/Autopromote.php @@ -0,0 +1,215 @@ + $cond ) { + if ( self::recCheckCondition( $cond, $user ) ) { + $promote[] = $group; + } + } + + Hooks::run( 'GetAutoPromoteGroups', [ $user, &$promote ] ); + + return $promote; + } + + /** + * Get the groups for the given user based on the given criteria. + * + * Does not return groups the user already belongs to or has once belonged. + * + * @param User $user The user to get the groups for + * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria) + * + * @return array Groups the user should be promoted to. + * + * @see $wgAutopromoteOnce + */ + public static function getAutopromoteOnceGroups( User $user, $event ) { + global $wgAutopromoteOnce; + + $promote = []; + + if ( isset( $wgAutopromoteOnce[$event] ) && count( $wgAutopromoteOnce[$event] ) ) { + $currentGroups = $user->getGroups(); + $formerGroups = $user->getFormerGroups(); + foreach ( $wgAutopromoteOnce[$event] as $group => $cond ) { + // Do not check if the user's already a member + if ( in_array( $group, $currentGroups ) ) { + continue; + } + // Do not autopromote if the user has belonged to the group + if ( in_array( $group, $formerGroups ) ) { + continue; + } + // Finally - check the conditions + if ( self::recCheckCondition( $cond, $user ) ) { + $promote[] = $group; + } + } + } + + return $promote; + } + + /** + * Recursively check a condition. Conditions are in the form + * array( '&' or '|' or '^' or '!', cond1, cond2, ... ) + * where cond1, cond2, ... are themselves conditions; *OR* + * APCOND_EMAILCONFIRMED, *OR* + * array( APCOND_EMAILCONFIRMED ), *OR* + * array( APCOND_EDITCOUNT, number of edits ), *OR* + * array( APCOND_AGE, seconds since registration ), *OR* + * similar constructs defined by extensions. + * This function evaluates the former type recursively, and passes off to + * self::checkCondition for evaluation of the latter type. + * + * @param mixed $cond A condition, possibly containing other conditions + * @param User $user The user to check the conditions against + * @return bool Whether the condition is true + */ + private static function recCheckCondition( $cond, User $user ) { + $validOps = [ '&', '|', '^', '!' ]; + + if ( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) { + # Recursive condition + if ( $cond[0] == '&' ) { // AND (all conds pass) + foreach ( array_slice( $cond, 1 ) as $subcond ) { + if ( !self::recCheckCondition( $subcond, $user ) ) { + return false; + } + } + + return true; + } elseif ( $cond[0] == '|' ) { // OR (at least one cond passes) + foreach ( array_slice( $cond, 1 ) as $subcond ) { + if ( self::recCheckCondition( $subcond, $user ) ) { + return true; + } + } + + return false; + } elseif ( $cond[0] == '^' ) { // XOR (exactly one cond passes) + if ( count( $cond ) > 3 ) { + wfWarn( 'recCheckCondition() given XOR ("^") condition on three or more conditions.' . + ' Check your $wgAutopromote and $wgAutopromoteOnce settings.' ); + } + return self::recCheckCondition( $cond[1], $user ) + xor self::recCheckCondition( $cond[2], $user ); + } elseif ( $cond[0] == '!' ) { // NOT (no conds pass) + foreach ( array_slice( $cond, 1 ) as $subcond ) { + if ( self::recCheckCondition( $subcond, $user ) ) { + return false; + } + } + + return true; + } + } + // If we got here, the array presumably does not contain other conditions; + // it's not recursive. Pass it off to self::checkCondition. + if ( !is_array( $cond ) ) { + $cond = [ $cond ]; + } + + return self::checkCondition( $cond, $user ); + } + + /** + * As recCheckCondition, but *not* recursive. The only valid conditions + * are those whose first element is APCOND_EMAILCONFIRMED/APCOND_EDITCOUNT/ + * APCOND_AGE. Other types will throw an exception if no extension evaluates them. + * + * @param array $cond A condition, which must not contain other conditions + * @param User $user The user to check the condition against + * @throws MWException + * @return bool Whether the condition is true for the user + */ + private static function checkCondition( $cond, User $user ) { + global $wgEmailAuthentication; + if ( count( $cond ) < 1 ) { + return false; + } + + switch ( $cond[0] ) { + case APCOND_EMAILCONFIRMED: + if ( Sanitizer::validateEmail( $user->getEmail() ) ) { + if ( $wgEmailAuthentication ) { + return (bool)$user->getEmailAuthenticationTimestamp(); + } else { + return true; + } + } + return false; + case APCOND_EDITCOUNT: + $reqEditCount = $cond[1]; + + // T157718: Avoid edit count lookup if specified edit count is 0 or invalid + if ( $reqEditCount <= 0 ) { + return true; + } + return $user->getEditCount() >= $reqEditCount; + case APCOND_AGE: + $age = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() ); + return $age >= $cond[1]; + case APCOND_AGE_FROM_EDIT: + $age = time() - wfTimestampOrNull( TS_UNIX, $user->getFirstEditTimestamp() ); + return $age >= $cond[1]; + case APCOND_INGROUPS: + $groups = array_slice( $cond, 1 ); + return count( array_intersect( $groups, $user->getGroups() ) ) == count( $groups ); + case APCOND_ISIP: + return $cond[1] == $user->getRequest()->getIP(); + case APCOND_IPINRANGE: + return IP::isInRange( $user->getRequest()->getIP(), $cond[1] ); + case APCOND_BLOCKED: + return $user->isBlocked(); + case APCOND_ISBOT: + return in_array( 'bot', User::getGroupPermissions( $user->getGroups() ) ); + default: + $result = null; + Hooks::run( 'AutopromoteCondition', [ $cond[0], + array_slice( $cond, 1 ), $user, &$result ] ); + if ( $result === null ) { + throw new MWException( "Unrecognized condition {$cond[0]} for autopromotion!" ); + } + + return (bool)$result; + } + } +} diff --git a/www/wiki/includes/Block.php b/www/wiki/includes/Block.php new file mode 100644 index 00000000..c38c929f --- /dev/null +++ b/www/wiki/includes/Block.php @@ -0,0 +1,1650 @@ + '', + 'user' => null, + 'by' => null, + 'reason' => '', + 'timestamp' => '', + 'auto' => false, + 'expiry' => '', + 'anonOnly' => false, + 'createAccount' => false, + 'enableAutoblock' => false, + 'hideName' => false, + 'blockEmail' => false, + 'allowUsertalk' => false, + 'byText' => '', + 'systemBlock' => null, + ]; + + if ( func_num_args() > 1 || !is_array( $options ) ) { + $options = array_combine( + array_slice( array_keys( $defaults ), 0, func_num_args() ), + func_get_args() + ); + wfDeprecated( __METHOD__ . ' with multiple arguments', '1.26' ); + } + + $options += $defaults; + + $this->setTarget( $options['address'] ); + + if ( $this->target instanceof User && $options['user'] ) { + # Needed for foreign users + $this->forcedTargetID = $options['user']; + } + + if ( $options['by'] ) { + # Local user + $this->setBlocker( User::newFromId( $options['by'] ) ); + } else { + # Foreign user + $this->setBlocker( $options['byText'] ); + } + + $this->mReason = $options['reason']; + $this->mTimestamp = wfTimestamp( TS_MW, $options['timestamp'] ); + $this->mExpiry = wfGetDB( DB_REPLICA )->decodeExpiry( $options['expiry'] ); + + # Boolean settings + $this->mAuto = (bool)$options['auto']; + $this->mHideName = (bool)$options['hideName']; + $this->isHardblock( !$options['anonOnly'] ); + $this->isAutoblocking( (bool)$options['enableAutoblock'] ); + + # Prevention measures + $this->prevents( 'sendemail', (bool)$options['blockEmail'] ); + $this->prevents( 'editownusertalk', !$options['allowUsertalk'] ); + $this->prevents( 'createaccount', (bool)$options['createAccount'] ); + + $this->mFromMaster = false; + $this->systemBlockType = $options['systemBlock']; + } + + /** + * Load a blocked user from their block id. + * + * @param int $id Block id to search for + * @return Block|null + */ + public static function newFromID( $id ) { + $dbr = wfGetDB( DB_REPLICA ); + $blockQuery = self::getQueryInfo(); + $res = $dbr->selectRow( + $blockQuery['tables'], + $blockQuery['fields'], + [ 'ipb_id' => $id ], + __METHOD__, + [], + $blockQuery['joins'] + ); + if ( $res ) { + return self::newFromRow( $res ); + } else { + return null; + } + } + + /** + * Return the list of ipblocks fields that should be selected to create + * a new block. + * @deprecated since 1.31, use self::getQueryInfo() instead. + * @return array + */ + public static function selectFields() { + global $wgActorTableSchemaMigrationStage; + + if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) { + // If code is using this instead of self::getQueryInfo(), there's a + // decent chance it's going to try to directly access + // $row->ipb_by or $row->ipb_by_text and we can't give it + // useful values here once those aren't being written anymore. + throw new BadMethodCallException( + 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH' + ); + } + + wfDeprecated( __METHOD__, '1.31' ); + return [ + 'ipb_id', + 'ipb_address', + 'ipb_by', + 'ipb_by_text', + 'ipb_by_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'ipb_by_actor' : 'NULL', + 'ipb_timestamp', + 'ipb_auto', + 'ipb_anon_only', + 'ipb_create_account', + 'ipb_enable_autoblock', + 'ipb_expiry', + 'ipb_deleted', + 'ipb_block_email', + 'ipb_allow_usertalk', + 'ipb_parent_block_id', + ] + CommentStore::getStore()->getFields( 'ipb_reason' ); + } + + /** + * Return the tables, fields, and join conditions to be selected to create + * a new block object. + * @since 1.31 + * @return array With three keys: + * - tables: (string[]) to include in the `$table` to `IDatabase->select()` + * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` + * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` + */ + public static function getQueryInfo() { + $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' ); + $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' ); + return [ + 'tables' => [ 'ipblocks' ] + $commentQuery['tables'] + $actorQuery['tables'], + 'fields' => [ + 'ipb_id', + 'ipb_address', + 'ipb_timestamp', + 'ipb_auto', + 'ipb_anon_only', + 'ipb_create_account', + 'ipb_enable_autoblock', + 'ipb_expiry', + 'ipb_deleted', + 'ipb_block_email', + 'ipb_allow_usertalk', + 'ipb_parent_block_id', + ] + $commentQuery['fields'] + $actorQuery['fields'], + 'joins' => $commentQuery['joins'] + $actorQuery['joins'], + ]; + } + + /** + * Check if two blocks are effectively equal. Doesn't check irrelevant things like + * the blocking user or the block timestamp, only things which affect the blocked user + * + * @param Block $block + * + * @return bool + */ + public function equals( Block $block ) { + return ( + (string)$this->target == (string)$block->target + && $this->type == $block->type + && $this->mAuto == $block->mAuto + && $this->isHardblock() == $block->isHardblock() + && $this->prevents( 'createaccount' ) == $block->prevents( 'createaccount' ) + && $this->mExpiry == $block->mExpiry + && $this->isAutoblocking() == $block->isAutoblocking() + && $this->mHideName == $block->mHideName + && $this->prevents( 'sendemail' ) == $block->prevents( 'sendemail' ) + && $this->prevents( 'editownusertalk' ) == $block->prevents( 'editownusertalk' ) + && $this->mReason == $block->mReason + ); + } + + /** + * Load a block from the database which affects the already-set $this->target: + * 1) A block directly on the given user or IP + * 2) A rangeblock encompassing the given IP (smallest first) + * 3) An autoblock on the given IP + * @param User|string $vagueTarget Also search for blocks affecting this target. Doesn't + * make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups. + * @throws MWException + * @return bool Whether a relevant block was found + */ + protected function newLoad( $vagueTarget = null ) { + $db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_REPLICA ); + + if ( $this->type !== null ) { + $conds = [ + 'ipb_address' => [ (string)$this->target ], + ]; + } else { + $conds = [ 'ipb_address' => [] ]; + } + + # Be aware that the != '' check is explicit, since empty values will be + # passed by some callers (T31116) + if ( $vagueTarget != '' ) { + list( $target, $type ) = self::parseTarget( $vagueTarget ); + switch ( $type ) { + case self::TYPE_USER: + # Slightly weird, but who are we to argue? + $conds['ipb_address'][] = (string)$target; + break; + + case self::TYPE_IP: + $conds['ipb_address'][] = (string)$target; + $conds[] = self::getRangeCond( IP::toHex( $target ) ); + $conds = $db->makeList( $conds, LIST_OR ); + break; + + case self::TYPE_RANGE: + list( $start, $end ) = IP::parseRange( $target ); + $conds['ipb_address'][] = (string)$target; + $conds[] = self::getRangeCond( $start, $end ); + $conds = $db->makeList( $conds, LIST_OR ); + break; + + default: + throw new MWException( "Tried to load block with invalid type" ); + } + } + + $blockQuery = self::getQueryInfo(); + $res = $db->select( + $blockQuery['tables'], $blockQuery['fields'], $conds, __METHOD__, [], $blockQuery['joins'] + ); + + # This result could contain a block on the user, a block on the IP, and a russian-doll + # set of rangeblocks. We want to choose the most specific one, so keep a leader board. + $bestRow = null; + + # Lower will be better + $bestBlockScore = 100; + + # This is begging for $this = $bestBlock, but that's not allowed in PHP :( + $bestBlockPreventsEdit = null; + + foreach ( $res as $row ) { + $block = self::newFromRow( $row ); + + # Don't use expired blocks + if ( $block->isExpired() ) { + continue; + } + + # Don't use anon only blocks on users + if ( $this->type == self::TYPE_USER && !$block->isHardblock() ) { + continue; + } + + if ( $block->getType() == self::TYPE_RANGE ) { + # This is the number of bits that are allowed to vary in the block, give + # or take some floating point errors + $end = Wikimedia\base_convert( $block->getRangeEnd(), 16, 10 ); + $start = Wikimedia\base_convert( $block->getRangeStart(), 16, 10 ); + $size = log( $end - $start + 1, 2 ); + + # This has the nice property that a /32 block is ranked equally with a + # single-IP block, which is exactly what it is... + $score = self::TYPE_RANGE - 1 + ( $size / 128 ); + + } else { + $score = $block->getType(); + } + + if ( $score < $bestBlockScore ) { + $bestBlockScore = $score; + $bestRow = $row; + $bestBlockPreventsEdit = $block->prevents( 'edit' ); + } + } + + if ( $bestRow !== null ) { + $this->initFromRow( $bestRow ); + $this->prevents( 'edit', $bestBlockPreventsEdit ); + return true; + } else { + return false; + } + } + + /** + * Get a set of SQL conditions which will select rangeblocks encompassing a given range + * @param string $start Hexadecimal IP representation + * @param string $end Hexadecimal IP representation, or null to use $start = $end + * @return string + */ + public static function getRangeCond( $start, $end = null ) { + if ( $end === null ) { + $end = $start; + } + # Per T16634, we want to include relevant active rangeblocks; for + # rangeblocks, we want to include larger ranges which enclose the given + # range. We know that all blocks must be smaller than $wgBlockCIDRLimit, + # so we can improve performance by filtering on a LIKE clause + $chunk = self::getIpFragment( $start ); + $dbr = wfGetDB( DB_REPLICA ); + $like = $dbr->buildLike( $chunk, $dbr->anyString() ); + + # Fairly hard to make a malicious SQL statement out of hex characters, + # but stranger things have happened... + $safeStart = $dbr->addQuotes( $start ); + $safeEnd = $dbr->addQuotes( $end ); + + return $dbr->makeList( + [ + "ipb_range_start $like", + "ipb_range_start <= $safeStart", + "ipb_range_end >= $safeEnd", + ], + LIST_AND + ); + } + + /** + * Get the component of an IP address which is certain to be the same between an IP + * address and a rangeblock containing that IP address. + * @param string $hex Hexadecimal IP representation + * @return string + */ + protected static function getIpFragment( $hex ) { + global $wgBlockCIDRLimit; + if ( substr( $hex, 0, 3 ) == 'v6-' ) { + return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) ); + } else { + return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) ); + } + } + + /** + * Given a database row from the ipblocks table, initialize + * member variables + * @param stdClass $row A row from the ipblocks table + */ + protected function initFromRow( $row ) { + $this->setTarget( $row->ipb_address ); + $this->setBlocker( User::newFromAnyId( + $row->ipb_by, $row->ipb_by_text, isset( $row->ipb_by_actor ) ? $row->ipb_by_actor : null + ) ); + + $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp ); + $this->mAuto = $row->ipb_auto; + $this->mHideName = $row->ipb_deleted; + $this->mId = (int)$row->ipb_id; + $this->mParentBlockId = $row->ipb_parent_block_id; + + // I wish I didn't have to do this + $db = wfGetDB( DB_REPLICA ); + $this->mExpiry = $db->decodeExpiry( $row->ipb_expiry ); + $this->mReason = CommentStore::getStore() + // Legacy because $row may have come from self::selectFields() + ->getCommentLegacy( $db, 'ipb_reason', $row )->text; + + $this->isHardblock( !$row->ipb_anon_only ); + $this->isAutoblocking( $row->ipb_enable_autoblock ); + + $this->prevents( 'createaccount', $row->ipb_create_account ); + $this->prevents( 'sendemail', $row->ipb_block_email ); + $this->prevents( 'editownusertalk', !$row->ipb_allow_usertalk ); + } + + /** + * Create a new Block object from a database row + * @param stdClass $row Row from the ipblocks table + * @return Block + */ + public static function newFromRow( $row ) { + $block = new Block; + $block->initFromRow( $row ); + return $block; + } + + /** + * Delete the row from the IP blocks table. + * + * @throws MWException + * @return bool + */ + public function delete() { + if ( wfReadOnly() ) { + return false; + } + + if ( !$this->getId() ) { + throw new MWException( "Block::delete() requires that the mId member be filled\n" ); + } + + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', [ 'ipb_parent_block_id' => $this->getId() ], __METHOD__ ); + $dbw->delete( 'ipblocks', [ 'ipb_id' => $this->getId() ], __METHOD__ ); + + return $dbw->affectedRows() > 0; + } + + /** + * Insert a block into the block table. Will fail if there is a conflicting + * block (same name and options) already in the database. + * + * @param IDatabase $dbw If you have one available + * @return bool|array False on failure, assoc array on success: + * ('id' => block ID, 'autoIds' => array of autoblock IDs) + */ + public function insert( $dbw = null ) { + global $wgBlockDisablesLogin; + + if ( $this->getSystemBlockType() !== null ) { + throw new MWException( 'Cannot insert a system block into the database' ); + } + if ( !$this->getBlocker() || $this->getBlocker()->getName() === '' ) { + throw new MWException( 'Cannot insert a block without a blocker set' ); + } + + wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); + + if ( $dbw === null ) { + $dbw = wfGetDB( DB_MASTER ); + } + + self::purgeExpired(); + + $row = $this->getDatabaseArray( $dbw ); + + $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] ); + $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); + + # Don't collide with expired blocks. + # Do this after trying to insert to avoid locking. + if ( !$affected ) { + # T96428: The ipb_address index uses a prefix on a field, so + # use a standard SELECT + DELETE to avoid annoying gap locks. + $ids = $dbw->selectFieldValues( 'ipblocks', + 'ipb_id', + [ + 'ipb_address' => $row['ipb_address'], + 'ipb_user' => $row['ipb_user'], + 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) + ], + __METHOD__ + ); + if ( $ids ) { + $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ ); + $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] ); + $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); + } + } + + if ( $affected ) { + $auto_ipd_ids = $this->doRetroactiveAutoblock(); + + if ( $wgBlockDisablesLogin && $this->target instanceof User ) { + // Change user login token to force them to be logged out. + $this->target->setToken(); + $this->target->saveSettings(); + } + + return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ]; + } + + return false; + } + + /** + * Update a block in the DB with new parameters. + * The ID field needs to be loaded first. + * + * @return bool|array False on failure, array on success: + * ('id' => block ID, 'autoIds' => array of autoblock IDs) + */ + public function update() { + wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" ); + $dbw = wfGetDB( DB_MASTER ); + + $dbw->startAtomic( __METHOD__ ); + + $dbw->update( + 'ipblocks', + $this->getDatabaseArray( $dbw ), + [ 'ipb_id' => $this->getId() ], + __METHOD__ + ); + + $affected = $dbw->affectedRows(); + + if ( $this->isAutoblocking() ) { + // update corresponding autoblock(s) (T50813) + $dbw->update( + 'ipblocks', + $this->getAutoblockUpdateArray( $dbw ), + [ 'ipb_parent_block_id' => $this->getId() ], + __METHOD__ + ); + } else { + // autoblock no longer required, delete corresponding autoblock(s) + $dbw->delete( + 'ipblocks', + [ 'ipb_parent_block_id' => $this->getId() ], + __METHOD__ + ); + } + + $dbw->endAtomic( __METHOD__ ); + + if ( $affected ) { + $auto_ipd_ids = $this->doRetroactiveAutoblock(); + return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ]; + } + + return false; + } + + /** + * Get an array suitable for passing to $dbw->insert() or $dbw->update() + * @param IDatabase $dbw + * @return array + */ + protected function getDatabaseArray( IDatabase $dbw ) { + $expiry = $dbw->encodeExpiry( $this->mExpiry ); + + if ( $this->forcedTargetID ) { + $uid = $this->forcedTargetID; + } else { + $uid = $this->target instanceof User ? $this->target->getId() : 0; + } + + $a = [ + 'ipb_address' => (string)$this->target, + 'ipb_user' => $uid, + 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'ipb_auto' => $this->mAuto, + 'ipb_anon_only' => !$this->isHardblock(), + 'ipb_create_account' => $this->prevents( 'createaccount' ), + 'ipb_enable_autoblock' => $this->isAutoblocking(), + 'ipb_expiry' => $expiry, + 'ipb_range_start' => $this->getRangeStart(), + 'ipb_range_end' => $this->getRangeEnd(), + 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite + 'ipb_block_email' => $this->prevents( 'sendemail' ), + 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ), + 'ipb_parent_block_id' => $this->mParentBlockId + ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason ) + + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() ); + + return $a; + } + + /** + * @param IDatabase $dbw + * @return array + */ + protected function getAutoblockUpdateArray( IDatabase $dbw ) { + return [ + 'ipb_create_account' => $this->prevents( 'createaccount' ), + 'ipb_deleted' => (int)$this->mHideName, // typecast required for SQLite + 'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ), + ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason ) + + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() ); + } + + /** + * Retroactively autoblocks the last IP used by the user (if it is a user) + * blocked by this Block. + * + * @return array Block IDs of retroactive autoblocks made + */ + protected function doRetroactiveAutoblock() { + $blockIds = []; + # If autoblock is enabled, autoblock the LAST IP(s) used + if ( $this->isAutoblocking() && $this->getType() == self::TYPE_USER ) { + wfDebug( "Doing retroactive autoblocks for " . $this->getTarget() . "\n" ); + + $continue = Hooks::run( + 'PerformRetroactiveAutoblock', [ $this, &$blockIds ] ); + + if ( $continue ) { + self::defaultRetroactiveAutoblock( $this, $blockIds ); + } + } + return $blockIds; + } + + /** + * Retroactively autoblocks the last IP used by the user (if it is a user) + * blocked by this Block. This will use the recentchanges table. + * + * @param Block $block + * @param array &$blockIds + */ + protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) { + global $wgPutIPinRC; + + // No IPs are in recentchanges table, so nothing to select + if ( !$wgPutIPinRC ) { + return; + } + + $target = $block->getTarget(); + if ( is_string( $target ) ) { + $target = User::newFromName( $target, false ); + } + + $dbr = wfGetDB( DB_REPLICA ); + $rcQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $target, false ); + + $options = [ 'ORDER BY' => 'rc_timestamp DESC' ]; + + // Just the last IP used. + $options['LIMIT'] = 1; + + $res = $dbr->select( + [ 'recentchanges' ] + $rcQuery['tables'], + [ 'rc_ip' ], + $rcQuery['conds'], + __METHOD__, + $options, + $rcQuery['joins'] + ); + + if ( !$res->numRows() ) { + # No results, don't autoblock anything + wfDebug( "No IP found to retroactively autoblock\n" ); + } else { + foreach ( $res as $row ) { + if ( $row->rc_ip ) { + $id = $block->doAutoblock( $row->rc_ip ); + if ( $id ) { + $blockIds[] = $id; + } + } + } + } + } + + /** + * Checks whether a given IP is on the autoblock whitelist. + * TODO: this probably belongs somewhere else, but not sure where... + * + * @param string $ip The IP to check + * @return bool + */ + public static function isWhitelistedFromAutoblocks( $ip ) { + // Try to get the autoblock_whitelist from the cache, as it's faster + // than getting the msg raw and explode()'ing it. + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $lines = $cache->getWithSetCallback( + $cache->makeKey( 'ip-autoblock', 'whitelist' ), + $cache::TTL_DAY, + function ( $curValue, &$ttl, array &$setOpts ) { + $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); + + return explode( "\n", + wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() ); + } + ); + + wfDebug( "Checking the autoblock whitelist..\n" ); + + foreach ( $lines as $line ) { + # List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + $wlEntry = substr( $line, 1 ); + $wlEntry = trim( $wlEntry ); + + wfDebug( "Checking $ip against $wlEntry..." ); + + # Is the IP in this range? + if ( IP::isInRange( $ip, $wlEntry ) ) { + wfDebug( " IP $ip matches $wlEntry, not autoblocking\n" ); + return true; + } else { + wfDebug( " No match\n" ); + } + } + + return false; + } + + /** + * Autoblocks the given IP, referring to this Block. + * + * @param string $autoblockIP The IP to autoblock. + * @return int|bool Block ID if an autoblock was inserted, false if not. + */ + public function doAutoblock( $autoblockIP ) { + # If autoblocks are disabled, go away. + if ( !$this->isAutoblocking() ) { + return false; + } + + # Don't autoblock for system blocks + if ( $this->getSystemBlockType() !== null ) { + throw new MWException( 'Cannot autoblock from a system block' ); + } + + # Check for presence on the autoblock whitelist. + if ( self::isWhitelistedFromAutoblocks( $autoblockIP ) ) { + return false; + } + + // Avoid PHP 7.1 warning of passing $this by reference + $block = $this; + # Allow hooks to cancel the autoblock. + if ( !Hooks::run( 'AbortAutoblock', [ $autoblockIP, &$block ] ) ) { + wfDebug( "Autoblock aborted by hook.\n" ); + return false; + } + + # It's okay to autoblock. Go ahead and insert/update the block... + + # Do not add a *new* block if the IP is already blocked. + $ipblock = self::newFromTarget( $autoblockIP ); + if ( $ipblock ) { + # Check if the block is an autoblock and would exceed the user block + # if renewed. If so, do nothing, otherwise prolong the block time... + if ( $ipblock->mAuto && // @todo Why not compare $ipblock->mExpiry? + $this->mExpiry > self::getAutoblockExpiry( $ipblock->mTimestamp ) + ) { + # Reset block timestamp to now and its expiry to + # $wgAutoblockExpiry in the future + $ipblock->updateTimestamp(); + } + return false; + } + + # Make a new block object with the desired properties. + $autoblock = new Block; + wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" ); + $autoblock->setTarget( $autoblockIP ); + $autoblock->setBlocker( $this->getBlocker() ); + $autoblock->mReason = wfMessage( 'autoblocker', $this->getTarget(), $this->mReason ) + ->inContentLanguage()->plain(); + $timestamp = wfTimestampNow(); + $autoblock->mTimestamp = $timestamp; + $autoblock->mAuto = 1; + $autoblock->prevents( 'createaccount', $this->prevents( 'createaccount' ) ); + # Continue suppressing the name if needed + $autoblock->mHideName = $this->mHideName; + $autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) ); + $autoblock->mParentBlockId = $this->mId; + + if ( $this->mExpiry == 'infinity' ) { + # Original block was indefinite, start an autoblock now + $autoblock->mExpiry = self::getAutoblockExpiry( $timestamp ); + } else { + # If the user is already blocked with an expiry date, we don't + # want to pile on top of that. + $autoblock->mExpiry = min( $this->mExpiry, self::getAutoblockExpiry( $timestamp ) ); + } + + # Insert the block... + $status = $autoblock->insert(); + return $status + ? $status['id'] + : false; + } + + /** + * Check if a block has expired. Delete it if it is. + * @return bool + */ + public function deleteIfExpired() { + if ( $this->isExpired() ) { + wfDebug( "Block::deleteIfExpired() -- deleting\n" ); + $this->delete(); + $retVal = true; + } else { + wfDebug( "Block::deleteIfExpired() -- not expired\n" ); + $retVal = false; + } + + return $retVal; + } + + /** + * Has the block expired? + * @return bool + */ + public function isExpired() { + $timestamp = wfTimestampNow(); + wfDebug( "Block::isExpired() checking current " . $timestamp . " vs $this->mExpiry\n" ); + + if ( !$this->mExpiry ) { + return false; + } else { + return $timestamp > $this->mExpiry; + } + } + + /** + * Is the block address valid (i.e. not a null string?) + * @return bool + */ + public function isValid() { + return $this->getTarget() != null; + } + + /** + * Update the timestamp on autoblocks. + */ + public function updateTimestamp() { + if ( $this->mAuto ) { + $this->mTimestamp = wfTimestamp(); + $this->mExpiry = self::getAutoblockExpiry( $this->mTimestamp ); + + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'ipblocks', + [ /* SET */ + 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'ipb_expiry' => $dbw->timestamp( $this->mExpiry ), + ], + [ /* WHERE */ + 'ipb_id' => $this->getId(), + ], + __METHOD__ + ); + } + } + + /** + * Get the IP address at the start of the range in Hex form + * @throws MWException + * @return string IP in Hex form + */ + public function getRangeStart() { + switch ( $this->type ) { + case self::TYPE_USER: + return ''; + case self::TYPE_IP: + return IP::toHex( $this->target ); + case self::TYPE_RANGE: + list( $start, /*...*/ ) = IP::parseRange( $this->target ); + return $start; + default: + throw new MWException( "Block with invalid type" ); + } + } + + /** + * Get the IP address at the end of the range in Hex form + * @throws MWException + * @return string IP in Hex form + */ + public function getRangeEnd() { + switch ( $this->type ) { + case self::TYPE_USER: + return ''; + case self::TYPE_IP: + return IP::toHex( $this->target ); + case self::TYPE_RANGE: + list( /*...*/, $end ) = IP::parseRange( $this->target ); + return $end; + default: + throw new MWException( "Block with invalid type" ); + } + } + + /** + * Get the user id of the blocking sysop + * + * @return int (0 for foreign users) + */ + public function getBy() { + $blocker = $this->getBlocker(); + return ( $blocker instanceof User ) + ? $blocker->getId() + : 0; + } + + /** + * Get the username of the blocking sysop + * + * @return string + */ + public function getByName() { + $blocker = $this->getBlocker(); + return ( $blocker instanceof User ) + ? $blocker->getName() + : (string)$blocker; // username + } + + /** + * Get the block ID + * @return int + */ + public function getId() { + return $this->mId; + } + + /** + * Get the system block type, if any + * @since 1.29 + * @return string|null + */ + public function getSystemBlockType() { + return $this->systemBlockType; + } + + /** + * Get/set a flag determining whether the master is used for reads + * + * @param bool|null $x + * @return bool + */ + public function fromMaster( $x = null ) { + return wfSetVar( $this->mFromMaster, $x ); + } + + /** + * Get/set whether the Block is a hardblock (affects logged-in users on a given IP/range) + * @param bool|null $x + * @return bool + */ + public function isHardblock( $x = null ) { + wfSetVar( $this->isHardblock, $x ); + + # You can't *not* hardblock a user + return $this->getType() == self::TYPE_USER + ? true + : $this->isHardblock; + } + + /** + * @param null|bool $x + * @return bool + */ + public function isAutoblocking( $x = null ) { + wfSetVar( $this->isAutoblocking, $x ); + + # You can't put an autoblock on an IP or range as we don't have any history to + # look over to get more IPs from + return $this->getType() == self::TYPE_USER + ? $this->isAutoblocking + : false; + } + + /** + * Get/set whether the Block prevents a given action + * + * @param string $action Action to check + * @param bool|null $x Value for set, or null to just get value + * @return bool|null Null for unrecognized rights. + */ + public function prevents( $action, $x = null ) { + global $wgBlockDisablesLogin; + $res = null; + switch ( $action ) { + case 'edit': + # For now... + $res = true; + break; + case 'createaccount': + $res = wfSetVar( $this->mCreateAccount, $x ); + break; + case 'sendemail': + $res = wfSetVar( $this->mBlockEmail, $x ); + break; + case 'editownusertalk': + $res = wfSetVar( $this->mDisableUsertalk, $x ); + break; + case 'read': + $res = false; + break; + } + if ( !$res && $wgBlockDisablesLogin ) { + // If a block would disable login, then it should + // prevent any action that all users cannot do + $anon = new User; + $res = $anon->isAllowed( $action ) ? $res : true; + } + + return $res; + } + + /** + * Get the block name, but with autoblocked IPs hidden as per standard privacy policy + * @return string Text is escaped + */ + public function getRedactedName() { + if ( $this->mAuto ) { + return Html::rawElement( + 'span', + [ 'class' => 'mw-autoblockid' ], + wfMessage( 'autoblockid', $this->mId ) + ); + } else { + return htmlspecialchars( $this->getTarget() ); + } + } + + /** + * Get a timestamp of the expiry for autoblocks + * + * @param string|int $timestamp + * @return string + */ + public static function getAutoblockExpiry( $timestamp ) { + global $wgAutoblockExpiry; + + return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); + } + + /** + * Purge expired blocks from the ipblocks table + */ + public static function purgeExpired() { + if ( wfReadOnly() ) { + return; + } + + DeferredUpdates::addUpdate( new AutoCommitUpdate( + wfGetDB( DB_MASTER ), + __METHOD__, + function ( IDatabase $dbw, $fname ) { + $ids = $dbw->selectFieldValues( 'ipblocks', + 'ipb_id', + [ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ], + $fname + ); + if ( $ids ) { + $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname ); + } + } + ) ); + } + + /** + * Given a target and the target's type, get an existing Block object if possible. + * @param string|User|int $specificTarget A block target, which may be one of several types: + * * A user to block, in which case $target will be a User + * * An IP to block, in which case $target will be a User generated by using + * User::newFromName( $ip, false ) to turn off name validation + * * An IP range, in which case $target will be a String "123.123.123.123/18" etc + * * The ID of an existing block, in the format "#12345" (since pure numbers are valid + * usernames + * Calling this with a user, IP address or range will not select autoblocks, and will + * only select a block where the targets match exactly (so looking for blocks on + * 1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32) + * @param string|User|int $vagueTarget As above, but we will search for *any* block which + * affects that target (so for an IP address, get ranges containing that IP; and also + * get any relevant autoblocks). Leave empty or blank to skip IP-based lookups. + * @param bool $fromMaster Whether to use the DB_MASTER database + * @return Block|null (null if no relevant block could be found). The target and type + * of the returned Block will refer to the actual block which was found, which might + * not be the same as the target you gave if you used $vagueTarget! + */ + public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) { + list( $target, $type ) = self::parseTarget( $specificTarget ); + if ( $type == self::TYPE_ID || $type == self::TYPE_AUTO ) { + return self::newFromID( $target ); + + } elseif ( $target === null && $vagueTarget == '' ) { + # We're not going to find anything useful here + # Be aware that the == '' check is explicit, since empty values will be + # passed by some callers (T31116) + return null; + + } elseif ( in_array( + $type, + [ self::TYPE_USER, self::TYPE_IP, self::TYPE_RANGE, null ] ) + ) { + $block = new Block(); + $block->fromMaster( $fromMaster ); + + if ( $type !== null ) { + $block->setTarget( $target ); + } + + if ( $block->newLoad( $vagueTarget ) ) { + return $block; + } + } + return null; + } + + /** + * Get all blocks that match any IP from an array of IP addresses + * + * @param array $ipChain List of IPs (strings), usually retrieved from the + * X-Forwarded-For header of the request + * @param bool $isAnon Exclude anonymous-only blocks if false + * @param bool $fromMaster Whether to query the master or replica DB + * @return array Array of Blocks + * @since 1.22 + */ + public static function getBlocksForIPList( array $ipChain, $isAnon, $fromMaster = false ) { + if ( !count( $ipChain ) ) { + return []; + } + + $conds = []; + $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup(); + foreach ( array_unique( $ipChain ) as $ipaddr ) { + # Discard invalid IP addresses. Since XFF can be spoofed and we do not + # necessarily trust the header given to us, make sure that we are only + # checking for blocks on well-formatted IP addresses (IPv4 and IPv6). + # Do not treat private IP spaces as special as it may be desirable for wikis + # to block those IP ranges in order to stop misbehaving proxies that spoof XFF. + if ( !IP::isValid( $ipaddr ) ) { + continue; + } + # Don't check trusted IPs (includes local squids which will be in every request) + if ( $proxyLookup->isTrustedProxy( $ipaddr ) ) { + continue; + } + # Check both the original IP (to check against single blocks), as well as build + # the clause to check for rangeblocks for the given IP. + $conds['ipb_address'][] = $ipaddr; + $conds[] = self::getRangeCond( IP::toHex( $ipaddr ) ); + } + + if ( !count( $conds ) ) { + return []; + } + + if ( $fromMaster ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_REPLICA ); + } + $conds = $db->makeList( $conds, LIST_OR ); + if ( !$isAnon ) { + $conds = [ $conds, 'ipb_anon_only' => 0 ]; + } + $blockQuery = self::getQueryInfo(); + $rows = $db->select( + $blockQuery['tables'], + array_merge( [ 'ipb_range_start', 'ipb_range_end' ], $blockQuery['fields'] ), + $conds, + __METHOD__, + [], + $blockQuery['joins'] + ); + + $blocks = []; + foreach ( $rows as $row ) { + $block = self::newFromRow( $row ); + if ( !$block->isExpired() ) { + $blocks[] = $block; + } + } + + return $blocks; + } + + /** + * From a list of multiple blocks, find the most exact and strongest Block. + * + * The logic for finding the "best" block is: + * - Blocks that match the block's target IP are preferred over ones in a range + * - Hardblocks are chosen over softblocks that prevent account creation + * - Softblocks that prevent account creation are chosen over other softblocks + * - Other softblocks are chosen over autoblocks + * - If there are multiple exact or range blocks at the same level, the one chosen + * is random + * This should be used when $blocks where retrieved from the user's IP address + * and $ipChain is populated from the same IP address information. + * + * @param array $blocks Array of Block objects + * @param array $ipChain List of IPs (strings). This is used to determine how "close" + * a block is to the server, and if a block matches exactly, or is in a range. + * The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2, + * local-squid, ...) + * @throws MWException + * @return Block|null The "best" block from the list + */ + public static function chooseBlock( array $blocks, array $ipChain ) { + if ( !count( $blocks ) ) { + return null; + } elseif ( count( $blocks ) == 1 ) { + return $blocks[0]; + } + + // Sort hard blocks before soft ones and secondarily sort blocks + // that disable account creation before those that don't. + usort( $blocks, function ( Block $a, Block $b ) { + $aWeight = (int)$a->isHardblock() . (int)$a->prevents( 'createaccount' ); + $bWeight = (int)$b->isHardblock() . (int)$b->prevents( 'createaccount' ); + return strcmp( $bWeight, $aWeight ); // highest weight first + } ); + + $blocksListExact = [ + 'hard' => false, + 'disable_create' => false, + 'other' => false, + 'auto' => false + ]; + $blocksListRange = [ + 'hard' => false, + 'disable_create' => false, + 'other' => false, + 'auto' => false + ]; + $ipChain = array_reverse( $ipChain ); + + /** @var Block $block */ + foreach ( $blocks as $block ) { + // Stop searching if we have already have a "better" block. This + // is why the order of the blocks matters + if ( !$block->isHardblock() && $blocksListExact['hard'] ) { + break; + } elseif ( !$block->prevents( 'createaccount' ) && $blocksListExact['disable_create'] ) { + break; + } + + foreach ( $ipChain as $checkip ) { + $checkipHex = IP::toHex( $checkip ); + if ( (string)$block->getTarget() === $checkip ) { + if ( $block->isHardblock() ) { + $blocksListExact['hard'] = $blocksListExact['hard'] ?: $block; + } elseif ( $block->prevents( 'createaccount' ) ) { + $blocksListExact['disable_create'] = $blocksListExact['disable_create'] ?: $block; + } elseif ( $block->mAuto ) { + $blocksListExact['auto'] = $blocksListExact['auto'] ?: $block; + } else { + $blocksListExact['other'] = $blocksListExact['other'] ?: $block; + } + // We found closest exact match in the ip list, so go to the next Block + break; + } elseif ( array_filter( $blocksListExact ) == [] + && $block->getRangeStart() <= $checkipHex + && $block->getRangeEnd() >= $checkipHex + ) { + if ( $block->isHardblock() ) { + $blocksListRange['hard'] = $blocksListRange['hard'] ?: $block; + } elseif ( $block->prevents( 'createaccount' ) ) { + $blocksListRange['disable_create'] = $blocksListRange['disable_create'] ?: $block; + } elseif ( $block->mAuto ) { + $blocksListRange['auto'] = $blocksListRange['auto'] ?: $block; + } else { + $blocksListRange['other'] = $blocksListRange['other'] ?: $block; + } + break; + } + } + } + + if ( array_filter( $blocksListExact ) == [] ) { + $blocksList = &$blocksListRange; + } else { + $blocksList = &$blocksListExact; + } + + $chosenBlock = null; + if ( $blocksList['hard'] ) { + $chosenBlock = $blocksList['hard']; + } elseif ( $blocksList['disable_create'] ) { + $chosenBlock = $blocksList['disable_create']; + } elseif ( $blocksList['other'] ) { + $chosenBlock = $blocksList['other']; + } elseif ( $blocksList['auto'] ) { + $chosenBlock = $blocksList['auto']; + } else { + throw new MWException( "Proxy block found, but couldn't be classified." ); + } + + return $chosenBlock; + } + + /** + * From an existing Block, get the target and the type of target. + * Note that, except for null, it is always safe to treat the target + * as a string; for User objects this will return User::__toString() + * which in turn gives User::getName(). + * + * @param string|int|User|null $target + * @return array [ User|String|null, Block::TYPE_ constant|null ] + */ + public static function parseTarget( $target ) { + # We may have been through this before + if ( $target instanceof User ) { + if ( IP::isValid( $target->getName() ) ) { + return [ $target, self::TYPE_IP ]; + } else { + return [ $target, self::TYPE_USER ]; + } + } elseif ( $target === null ) { + return [ null, null ]; + } + + $target = trim( $target ); + + if ( IP::isValid( $target ) ) { + # We can still create a User if it's an IP address, but we need to turn + # off validation checking (which would exclude IP addresses) + return [ + User::newFromName( IP::sanitizeIP( $target ), false ), + self::TYPE_IP + ]; + + } elseif ( IP::isValidRange( $target ) ) { + # Can't create a User from an IP range + return [ IP::sanitizeRange( $target ), self::TYPE_RANGE ]; + } + + # Consider the possibility that this is not a username at all + # but actually an old subpage (bug #29797) + if ( strpos( $target, '/' ) !== false ) { + # An old subpage, drill down to the user behind it + $target = explode( '/', $target )[0]; + } + + $userObj = User::newFromName( $target ); + if ( $userObj instanceof User ) { + # Note that since numbers are valid usernames, a $target of "12345" will be + # considered a User. If you want to pass a block ID, prepend a hash "#12345", + # since hash characters are not valid in usernames or titles generally. + return [ $userObj, self::TYPE_USER ]; + + } elseif ( preg_match( '/^#\d+$/', $target ) ) { + # Autoblock reference in the form "#12345" + return [ substr( $target, 1 ), self::TYPE_AUTO ]; + + } else { + # WTF? + return [ null, null ]; + } + } + + /** + * Get the type of target for this particular block + * @return int Block::TYPE_ constant, will never be TYPE_ID + */ + public function getType() { + return $this->mAuto + ? self::TYPE_AUTO + : $this->type; + } + + /** + * Get the target and target type for this particular Block. Note that for autoblocks, + * this returns the unredacted name; frontend functions need to call $block->getRedactedName() + * in this situation. + * @return array [ User|String, Block::TYPE_ constant ] + * @todo FIXME: This should be an integral part of the Block member variables + */ + public function getTargetAndType() { + return [ $this->getTarget(), $this->getType() ]; + } + + /** + * Get the target for this particular Block. Note that for autoblocks, + * this returns the unredacted name; frontend functions need to call $block->getRedactedName() + * in this situation. + * @return User|string + */ + public function getTarget() { + return $this->target; + } + + /** + * @since 1.19 + * + * @return mixed|string + */ + public function getExpiry() { + return $this->mExpiry; + } + + /** + * Set the target for this block, and update $this->type accordingly + * @param mixed $target + */ + public function setTarget( $target ) { + list( $this->target, $this->type ) = self::parseTarget( $target ); + } + + /** + * Get the user who implemented this block + * @return User User object. May name a foreign user. + */ + public function getBlocker() { + return $this->blocker; + } + + /** + * Set the user who implemented (or will implement) this block + * @param User|string $user Local User object or username string + */ + public function setBlocker( $user ) { + if ( is_string( $user ) ) { + $user = User::newFromName( $user, false ); + } + + if ( $user->isAnon() && User::isUsableName( $user->getName() ) ) { + throw new InvalidArgumentException( + 'Blocker must be a local user or a name that cannot be a local user' + ); + } + + $this->blocker = $user; + } + + /** + * Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be + * the same as the block's, to a maximum of 24 hours. + * + * @since 1.29 + * + * @param WebResponse $response The response on which to set the cookie. + */ + public function setCookie( WebResponse $response ) { + // Calculate the default expiry time. + $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) ); + + // Use the Block's expiry time only if it's less than the default. + $expiryTime = $this->getExpiry(); + if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) { + $expiryTime = $maxExpiryTime; + } + + // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie. + $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' ); + $cookieOptions = [ 'httpOnly' => false ]; + $cookieValue = $this->getCookieValue(); + $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions ); + } + + /** + * Unset the 'BlockID' cookie. + * + * @since 1.29 + * + * @param WebResponse $response The response on which to unset the cookie. + */ + public static function clearCookie( WebResponse $response ) { + $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] ); + } + + /** + * Get the BlockID cookie's value for this block. This is usually the block ID concatenated + * with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just + * be the block ID. + * + * @since 1.29 + * + * @return string The block ID, probably concatenated with "!" and the HMAC. + */ + public function getCookieValue() { + $config = RequestContext::getMain()->getConfig(); + $id = $this->getId(); + $secretKey = $config->get( 'SecretKey' ); + if ( !$secretKey ) { + // If there's no secret key, don't append a HMAC. + return $id; + } + $hmac = MWCryptHash::hmac( $id, $secretKey, false ); + $cookieValue = $id . '!' . $hmac; + return $cookieValue; + } + + /** + * Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of + * the ID and a HMAC (see Block::setCookie), but will sometimes only be the ID. + * + * @since 1.29 + * + * @param string $cookieValue The string in which to find the ID. + * + * @return int|null The block ID, or null if the HMAC is present and invalid. + */ + public static function getIdFromCookieValue( $cookieValue ) { + // Extract the ID prefix from the cookie value (may be the whole value, if no bang found). + $bangPos = strpos( $cookieValue, '!' ); + $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos ); + // Get the site-wide secret key. + $config = RequestContext::getMain()->getConfig(); + $secretKey = $config->get( 'SecretKey' ); + if ( !$secretKey ) { + // If there's no secret key, just use the ID as given. + return $id; + } + $storedHmac = substr( $cookieValue, $bangPos + 1 ); + $calculatedHmac = MWCryptHash::hmac( $id, $secretKey, false ); + if ( $calculatedHmac === $storedHmac ) { + return $id; + } else { + return null; + } + } + + /** + * Get the key and parameters for the corresponding error message. + * + * @since 1.22 + * @param IContextSource $context + * @return array + */ + public function getPermissionsError( IContextSource $context ) { + $blocker = $this->getBlocker(); + if ( $blocker instanceof User ) { // local user + $blockerUserpage = $blocker->getUserPage(); + $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]"; + } else { // foreign user + $link = $blocker; + } + + $reason = $this->mReason; + if ( $reason == '' ) { + $reason = $context->msg( 'blockednoreason' )->text(); + } + + /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked. + * This could be a username, an IP range, or a single IP. */ + $intended = $this->getTarget(); + + $systemBlockType = $this->getSystemBlockType(); + + $lang = $context->getLanguage(); + return [ + $systemBlockType !== null + ? 'systemblockedtext' + : ( $this->mAuto ? 'autoblockedtext' : 'blockedtext' ), + $link, + $reason, + $context->getRequest()->getIP(), + $this->getByName(), + $systemBlockType !== null ? $systemBlockType : $this->getId(), + $lang->formatExpiry( $this->mExpiry ), + (string)$intended, + $lang->userTimeAndDate( $this->mTimestamp, $context->getUser() ), + ]; + } +} diff --git a/www/wiki/includes/CategoriesRdf.php b/www/wiki/includes/CategoriesRdf.php new file mode 100644 index 00000000..fc296d4c --- /dev/null +++ b/www/wiki/includes/CategoriesRdf.php @@ -0,0 +1,128 @@ +rdfWriter = $writer; + } + + /** + * Setup prefixes relevant for the dump + */ + public function setupPrefixes() { + $this->rdfWriter->prefix( self::ONTOLOGY_PREFIX, self::ONTOLOGY_URL ); + $this->rdfWriter->prefix( 'rdfs', 'http://www.w3.org/2000/01/rdf-schema#' ); + $this->rdfWriter->prefix( 'owl', 'http://www.w3.org/2002/07/owl#' ); + $this->rdfWriter->prefix( 'schema', 'http://schema.org/' ); + $this->rdfWriter->prefix( 'cc', 'http://creativecommons.org/ns#' ); + } + + /** + * Write RDF data for link between categories. + * @param string $fromName Child category name + * @param string $toName Parent category name + */ + public function writeCategoryLinkData( $fromName, $toName ) { + $titleFrom = Title::makeTitle( NS_CATEGORY, $fromName ); + $titleTo = Title::makeTitle( NS_CATEGORY, $toName ); + $this->rdfWriter->about( $this->titleToUrl( $titleFrom ) ) + ->say( self::ONTOLOGY_PREFIX, 'isInCategory' ) + ->is( $this->titleToUrl( $titleTo ) ); + } + + /** + * Write out the data for single category. + * @param string $categoryName Category name + * @param bool $isHidden Hidden category? + * @param int $pages Page count (note this includes only Wiki articles, not subcats or files) + * @param int $subcategories Subcategory count + */ + public function writeCategoryData( $categoryName, $isHidden, $pages, $subcategories ) { + $title = Title::makeTitle( NS_CATEGORY, $categoryName ); + $this->rdfWriter->about( $this->titleToUrl( $title ) ) + ->say( 'a' ) + ->is( self::ONTOLOGY_PREFIX, 'Category' ); + if ( $isHidden ) { + $this->rdfWriter->is( self::ONTOLOGY_PREFIX, 'HiddenCategory' ); + } + $titletext = $title->getText(); + $this->rdfWriter->say( 'rdfs', 'label' )->value( $titletext ); + $this->rdfWriter->say( self::ONTOLOGY_PREFIX, 'pages' )->value( $pages ); + $this->rdfWriter->say( self::ONTOLOGY_PREFIX, 'subcategories' )->value( $subcategories ); + // TODO: do we want files too here? Easy to add, but don't have use case so far. + } + + /** + * Make URL from title label + * @param string $titleLabel Short label (without namespace) of the category + * @return string URL for the category + */ + public function labelToUrl( $titleLabel ) { + return $this->titleToUrl( Title::makeTitle( NS_CATEGORY, $titleLabel ) ); + } + + /** + * Convert Title to link to target page. + * @param Title $title + * @return string URL for the category + */ + private function titleToUrl( Title $title ) { + return $title->getFullURL( '', false, PROTO_CANONICAL ); + } + + /** + * Get URI of the dump for this particular wiki. + * @return false|string + */ + public function getDumpURI() { + return $this->titleToUrl( Title::makeTitle( NS_MAIN, self::SPECIAL_DUMP ) ); + } + +} diff --git a/www/wiki/includes/Category.php b/www/wiki/includes/Category.php new file mode 100644 index 00000000..6104b8a6 --- /dev/null +++ b/www/wiki/includes/Category.php @@ -0,0 +1,409 @@ +mName === null && $this->mID === null ) { + throw new MWException( __METHOD__ . ' has both names and IDs null' ); + } elseif ( $this->mID === null ) { + $where = [ 'cat_title' => $this->mName ]; + } elseif ( $this->mName === null ) { + $where = [ 'cat_id' => $this->mID ]; + } else { + # Already initialized + return true; + } + + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( + 'category', + [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ], + $where, + __METHOD__ + ); + + if ( !$row ) { + # Okay, there were no contents. Nothing to initialize. + if ( $this->mTitle ) { + # If there is a title object but no record in the category table, + # treat this as an empty category. + $this->mID = false; + $this->mName = $this->mTitle->getDBkey(); + $this->mPages = 0; + $this->mSubcats = 0; + $this->mFiles = 0; + + # If the title exists, call refreshCounts to add a row for it. + if ( $mode === self::LAZY_INIT_ROW && $this->mTitle->exists() ) { + DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] ); + } + + return true; + } else { + return false; # Fail + } + } + + $this->mID = $row->cat_id; + $this->mName = $row->cat_title; + $this->mPages = $row->cat_pages; + $this->mSubcats = $row->cat_subcats; + $this->mFiles = $row->cat_files; + + # (T15683) If the count is negative, then 1) it's obviously wrong + # and should not be kept, and 2) we *probably* don't have to scan many + # rows to obtain the correct figure, so let's risk a one-time recount. + if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { + $this->mPages = max( $this->mPages, 0 ); + $this->mSubcats = max( $this->mSubcats, 0 ); + $this->mFiles = max( $this->mFiles, 0 ); + + if ( $mode === self::LAZY_INIT_ROW ) { + DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] ); + } + } + + return true; + } + + /** + * Factory function. + * + * @param string $name A category name (no "Category:" prefix). It need + * not be normalized, with spaces replaced by underscores. + * @return Category|bool Category, or false on a totally invalid name + */ + public static function newFromName( $name ) { + $cat = new self(); + $title = Title::makeTitleSafe( NS_CATEGORY, $name ); + + if ( !is_object( $title ) ) { + return false; + } + + $cat->mTitle = $title; + $cat->mName = $title->getDBkey(); + + return $cat; + } + + /** + * Factory function. + * + * @param Title $title Title for the category page + * @return Category|bool On a totally invalid name + */ + public static function newFromTitle( $title ) { + $cat = new self(); + + $cat->mTitle = $title; + $cat->mName = $title->getDBkey(); + + return $cat; + } + + /** + * Factory function. + * + * @param int $id A category id + * @return Category + */ + public static function newFromID( $id ) { + $cat = new self(); + $cat->mID = intval( $id ); + return $cat; + } + + /** + * Factory function, for constructing a Category object from a result set + * + * @param object $row Result set row, must contain the cat_xxx fields. If the + * fields are null, the resulting Category object will represent an empty + * category if a title object was given. If the fields are null and no + * title was given, this method fails and returns false. + * @param Title $title Optional title object for the category represented by + * the given row. May be provided if it is already known, to avoid having + * to re-create a title object later. + * @return Category|false + */ + public static function newFromRow( $row, $title = null ) { + $cat = new self(); + $cat->mTitle = $title; + + # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in + # all the cat_xxx fields being null, if the category page exists, but nothing + # was ever added to the category. This case should be treated link an empty + # category, if possible. + + if ( $row->cat_title === null ) { + if ( $title === null ) { + # the name is probably somewhere in the row, for example as page_title, + # but we can't know that here... + return false; + } else { + # if we have a title object, fetch the category name from there + $cat->mName = $title->getDBkey(); + } + + $cat->mID = false; + $cat->mSubcats = 0; + $cat->mPages = 0; + $cat->mFiles = 0; + } else { + $cat->mName = $row->cat_title; + $cat->mID = $row->cat_id; + $cat->mSubcats = $row->cat_subcats; + $cat->mPages = $row->cat_pages; + $cat->mFiles = $row->cat_files; + } + + return $cat; + } + + /** + * @return mixed DB key name, or false on failure + */ + public function getName() { + return $this->getX( 'mName' ); + } + + /** + * @return mixed Category ID, or false on failure + */ + public function getID() { + return $this->getX( 'mID' ); + } + + /** + * @return mixed Total number of member pages, or false on failure + */ + public function getPageCount() { + return $this->getX( 'mPages' ); + } + + /** + * @return mixed Number of subcategories, or false on failure + */ + public function getSubcatCount() { + return $this->getX( 'mSubcats' ); + } + + /** + * @return mixed Number of member files, or false on failure + */ + public function getFileCount() { + return $this->getX( 'mFiles' ); + } + + /** + * @return Title|bool Title for this category, or false on failure. + */ + public function getTitle() { + if ( $this->mTitle ) { + return $this->mTitle; + } + + if ( !$this->initialize( self::LAZY_INIT_ROW ) ) { + return false; + } + + $this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName ); + return $this->mTitle; + } + + /** + * Fetch a TitleArray of up to $limit category members, beginning after the + * category sort key $offset. + * @param int|bool $limit + * @param string $offset + * @return TitleArray TitleArray object for category members. + */ + public function getMembers( $limit = false, $offset = '' ) { + $dbr = wfGetDB( DB_REPLICA ); + + $conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ]; + $options = [ 'ORDER BY' => 'cl_sortkey' ]; + + if ( $limit ) { + $options['LIMIT'] = $limit; + } + + if ( $offset !== '' ) { + $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset ); + } + + $result = TitleArray::newFromResult( + $dbr->select( + [ 'page', 'categorylinks' ], + [ 'page_id', 'page_namespace', 'page_title', 'page_len', + 'page_is_redirect', 'page_latest' ], + $conds, + __METHOD__, + $options + ) + ); + + return $result; + } + + /** + * Generic accessor + * @param string $key + * @return bool + */ + private function getX( $key ) { + if ( !$this->initialize( self::LAZY_INIT_ROW ) ) { + return false; + } + return $this->{$key}; + } + + /** + * Refresh the counts for this category. + * + * @return bool True on success, false on failure + */ + public function refreshCounts() { + if ( wfReadOnly() ) { + return false; + } + + # If we have just a category name, find out whether there is an + # existing row. Or if we have just an ID, get the name, because + # that's what categorylinks uses. + if ( !$this->initialize( self::LOAD_ONLY ) ) { + return false; + } + + $dbw = wfGetDB( DB_MASTER ); + # Avoid excess contention on the same category (T162121) + $name = __METHOD__ . ':' . md5( $this->mName ); + $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 0 ); + if ( !$scopedLock ) { + return false; + } + + $dbw->startAtomic( __METHOD__ ); + + $cond1 = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], 1, 'NULL' ); + $cond2 = $dbw->conditional( [ 'page_namespace' => NS_FILE ], 1, 'NULL' ); + $result = $dbw->selectRow( + [ 'categorylinks', 'page' ], + [ 'pages' => 'COUNT(*)', + 'subcats' => "COUNT($cond1)", + 'files' => "COUNT($cond2)" + ], + [ 'cl_to' => $this->mName, 'page_id = cl_from' ], + __METHOD__, + [ 'LOCK IN SHARE MODE' ] + ); + + $shouldExist = $result->pages > 0 || $this->getTitle()->exists(); + + if ( $this->mID ) { + if ( $shouldExist ) { + # The category row already exists, so do a plain UPDATE instead + # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap + # in the cat_id sequence. The row may or may not be "affected". + $dbw->update( + 'category', + [ + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ], + [ 'cat_title' => $this->mName ], + __METHOD__ + ); + } else { + # The category is empty and has no description page, delete it + $dbw->delete( + 'category', + [ 'cat_title' => $this->mName ], + __METHOD__ + ); + $this->mID = false; + } + } elseif ( $shouldExist ) { + # The category row doesn't exist but should, so create it. Use + # upsert in case of races. + $dbw->upsert( + 'category', + [ + 'cat_title' => $this->mName, + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ], + [ 'cat_title' ], + [ + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ], + __METHOD__ + ); + // @todo: Should we update $this->mID here? Or not since Category + // objects tend to be short lived enough to not matter? + } + + $dbw->endAtomic( __METHOD__ ); + + # Now we should update our local counts. + $this->mPages = $result->pages; + $this->mSubcats = $result->subcats; + $this->mFiles = $result->files; + + return true; + } +} diff --git a/www/wiki/includes/CategoryFinder.php b/www/wiki/includes/CategoryFinder.php new file mode 100644 index 00000000..7446b590 --- /dev/null +++ b/www/wiki/includes/CategoryFinder.php @@ -0,0 +1,262 @@ +seed( + * [ 12345 ], + * [ 'Category 1', 'Category 2' ], + * 'AND' + * ); + * $a = $cf->run(); + * print implode( ',' , $a ); + * @endcode + * + * @deprecated since 1.31 + */ +class CategoryFinder { + /** @var int[] The original article IDs passed to the seed function */ + protected $articles = []; + + /** @var array Array of DBKEY category names for categories that don't have a page */ + protected $deadend = []; + + /** @var array Array of [ ID => [] ] */ + protected $parents = []; + + /** @var array Array of article/category IDs */ + protected $next = []; + + /** @var int Max layer depth **/ + protected $maxdepth = -1; + + /** @var array Array of DBKEY category names */ + protected $targets = []; + + /** @var array */ + protected $name2id = []; + + /** @var string "AND" or "OR" */ + protected $mode; + + /** @var IDatabase Read-DB replica DB */ + protected $dbr; + + /** + * Initializes the instance. Do this prior to calling run(). + * @param array $articleIds Array of article IDs + * @param array $categories FIXME + * @param string $mode FIXME, default 'AND'. + * @param int $maxdepth Maximum layer depth. Where: + * -1 means deep recursion (default); + * 0 means no-parents; + * 1 means one parent layer, etc. + * @todo FIXME: $categories/$mode + */ + public function seed( $articleIds, $categories, $mode = 'AND', $maxdepth = -1 ) { + $this->articles = $articleIds; + $this->next = $articleIds; + $this->mode = $mode; + $this->maxdepth = $maxdepth; + + # Set the list of target categories; convert them to DBKEY form first + $this->targets = []; + foreach ( $categories as $c ) { + $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); + if ( $ct ) { + $c = $ct->getDBkey(); + $this->targets[$c] = $c; + } + } + } + + /** + * Iterates through the parent tree starting with the seed values, + * then checks the articles if they match the conditions + * @return array Array of page_ids (those given to seed() that match the conditions) + */ + public function run() { + $this->dbr = wfGetDB( DB_REPLICA ); + + $i = 0; + $dig = true; + while ( count( $this->next ) && $dig ) { + $this->scanNextLayer(); + + // Is there any depth limit? + if ( $this->maxdepth !== -1 ) { + $dig = $i < $this->maxdepth; + $i++; + } + } + + # Now check if this applies to the individual articles + $ret = []; + + foreach ( $this->articles as $article ) { + $conds = $this->targets; + if ( $this->check( $article, $conds ) ) { + # Matches the conditions + $ret[] = $article; + } + } + return $ret; + } + + /** + * Get the parents. Only really useful if run() has been called already + * @return array + */ + public function getParents() { + return $this->parents; + } + + /** + * This functions recurses through the parent representation, trying to match the conditions + * @param int $id The article/category to check + * @param array $conds The array of categories to match + * @param array $path Used to check for recursion loops + * @return bool Does this match the conditions? + */ + private function check( $id, &$conds, $path = [] ) { + // Check for loops and stop! + if ( in_array( $id, $path ) ) { + return false; + } + + $path[] = $id; + + # Shortcut (runtime paranoia): No conditions=all matched + if ( count( $conds ) == 0 ) { + return true; + } + + if ( !isset( $this->parents[$id] ) ) { + return false; + } + + # iterate through the parents + foreach ( $this->parents[$id] as $p ) { + $pname = $p->cl_to; + + # Is this a condition? + if ( isset( $conds[$pname] ) ) { + # This key is in the category list! + if ( $this->mode == 'OR' ) { + # One found, that's enough! + $conds = []; + return true; + } else { + # Assuming "AND" as default + unset( $conds[$pname] ); + if ( count( $conds ) == 0 ) { + # All conditions met, done + return true; + } + } + } + + # Not done yet, try sub-parents + if ( !isset( $this->name2id[$pname] ) ) { + # No sub-parent + continue; + } + $done = $this->check( $this->name2id[$pname], $conds, $path ); + if ( $done || count( $conds ) == 0 ) { + # Subparents have done it! + return true; + } + } + return false; + } + + /** + * Scans a "parent layer" of the articles/categories in $this->next + */ + private function scanNextLayer() { + # Find all parents of the article currently in $this->next + $layer = []; + $res = $this->dbr->select( + /* FROM */ 'categorylinks', + /* SELECT */ [ 'cl_to', 'cl_from' ], + /* WHERE */ [ 'cl_from' => $this->next ], + __METHOD__ . '-1' + ); + foreach ( $res as $o ) { + $k = $o->cl_to; + + # Update parent tree + if ( !isset( $this->parents[$o->cl_from] ) ) { + $this->parents[$o->cl_from] = []; + } + $this->parents[$o->cl_from][$k] = $o; + + # Ignore those we already have + if ( in_array( $k, $this->deadend ) ) { + continue; + } + + if ( isset( $this->name2id[$k] ) ) { + continue; + } + + # Hey, new category! + $layer[$k] = $k; + } + + $this->next = []; + + # Find the IDs of all category pages in $layer, if they exist + if ( count( $layer ) > 0 ) { + $res = $this->dbr->select( + /* FROM */ 'page', + /* SELECT */ [ 'page_id', 'page_title' ], + /* WHERE */ [ 'page_namespace' => NS_CATEGORY, 'page_title' => $layer ], + __METHOD__ . '-2' + ); + foreach ( $res as $o ) { + $id = $o->page_id; + $name = $o->page_title; + $this->name2id[$name] = $id; + $this->next[] = $id; + unset( $layer[$name] ); + } + } + + # Mark dead ends + foreach ( $layer as $v ) { + $this->deadend[$v] = $v; + } + } +} diff --git a/www/wiki/includes/CategoryViewer.php b/www/wiki/includes/CategoryViewer.php new file mode 100644 index 00000000..f36c7580 --- /dev/null +++ b/www/wiki/includes/CategoryViewer.php @@ -0,0 +1,752 @@ +title = $title; + $this->setContext( $context ); + $this->getOutput()->addModuleStyles( [ + 'mediawiki.action.view.categoryPage.styles' + ] ); + $this->from = $from; + $this->until = $until; + $this->limit = $context->getConfig()->get( 'CategoryPagingLimit' ); + $this->cat = Category::newFromTitle( $title ); + $this->query = $query; + $this->collation = Collation::singleton(); + unset( $this->query['title'] ); + } + + /** + * Format the category data list. + * + * @return string HTML output + */ + public function getHTML() { + $this->showGallery = $this->getConfig()->get( 'CategoryMagicGallery' ) + && !$this->getOutput()->mNoGallery; + + $this->clearCategoryState(); + $this->doCategoryQuery(); + $this->finaliseCategoryState(); + + $r = $this->getSubcategorySection() . + $this->getPagesSection() . + $this->getImageSection(); + + if ( $r == '' ) { + // If there is no category content to display, only + // show the top part of the navigation links. + // @todo FIXME: Cannot be completely suppressed because it + // is unknown if 'until' or 'from' makes this + // give 0 results. + $r = $r . $this->getCategoryTop(); + } else { + $r = $this->getCategoryTop() . + $r . + $this->getCategoryBottom(); + } + + // Give a proper message if category is empty + if ( $r == '' ) { + $r = $this->msg( 'category-empty' )->parseAsBlock(); + } + + $lang = $this->getLanguage(); + $attribs = [ + 'class' => 'mw-category-generated', + 'lang' => $lang->getHtmlCode(), + 'dir' => $lang->getDir() + ]; + # put a div around the headings which are in the user language + $r = Html::openElement( 'div', $attribs ) . $r . ''; + + return $r; + } + + function clearCategoryState() { + $this->articles = []; + $this->articles_start_char = []; + $this->children = []; + $this->children_start_char = []; + if ( $this->showGallery ) { + // Note that null for mode is taken to mean use default. + $mode = $this->getRequest()->getVal( 'gallerymode', null ); + try { + $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); + } catch ( Exception $e ) { + // User specified something invalid, fallback to default. + $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); + } + + $this->gallery->setHideBadImages(); + } else { + $this->imgsNoGallery = []; + $this->imgsNoGallery_start_char = []; + } + } + + /** + * Add a subcategory to the internal lists, using a Category object + * @param Category $cat + * @param string $sortkey + * @param int $pageLength + */ + function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) { + // Subcategory; strip the 'Category' namespace from the link text. + $title = $cat->getTitle(); + + $this->children[] = $this->generateLink( + 'subcat', + $title, + $title->isRedirect(), + htmlspecialchars( $title->getText() ) + ); + + $this->children_start_char[] = + $this->getSubcategorySortChar( $cat->getTitle(), $sortkey ); + } + + function generateLink( $type, Title $title, $isRedirect, $html = null ) { + $link = null; + Hooks::run( 'CategoryViewer::generateLink', [ $type, $title, $html, &$link ] ); + if ( $link === null ) { + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + if ( $html !== null ) { + $html = new HtmlArmor( $html ); + } + $link = $linkRenderer->makeLink( $title, $html ); + } + if ( $isRedirect ) { + $link = '' . $link . ''; + } + + return $link; + } + + /** + * Get the character to be used for sorting subcategories. + * If there's a link from Category:A to Category:B, the sortkey of the resulting + * entry in the categorylinks table is Category:A, not A, which it SHOULD be. + * Workaround: If sortkey == "Category:".$title, than use $title for sorting, + * else use sortkey... + * + * @param Title $title + * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever). + * @return string + */ + function getSubcategorySortChar( $title, $sortkey ) { + global $wgContLang; + + if ( $title->getPrefixedText() == $sortkey ) { + $word = $title->getDBkey(); + } else { + $word = $sortkey; + } + + $firstChar = $this->collation->getFirstLetter( $word ); + + return $wgContLang->convert( $firstChar ); + } + + /** + * Add a page in the image namespace + * @param Title $title + * @param string $sortkey + * @param int $pageLength + * @param bool $isRedirect + */ + function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { + global $wgContLang; + if ( $this->showGallery ) { + $flip = $this->flip['file']; + if ( $flip ) { + $this->gallery->insert( $title ); + } else { + $this->gallery->add( $title ); + } + } else { + $this->imgsNoGallery[] = $this->generateLink( 'image', $title, $isRedirect ); + + $this->imgsNoGallery_start_char[] = $wgContLang->convert( + $this->collation->getFirstLetter( $sortkey ) ); + } + } + + /** + * Add a miscellaneous page + * @param Title $title + * @param string $sortkey + * @param int $pageLength + * @param bool $isRedirect + */ + function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { + global $wgContLang; + + $this->articles[] = $this->generateLink( 'page', $title, $isRedirect ); + + $this->articles_start_char[] = $wgContLang->convert( + $this->collation->getFirstLetter( $sortkey ) ); + } + + function finaliseCategoryState() { + if ( $this->flip['subcat'] ) { + $this->children = array_reverse( $this->children ); + $this->children_start_char = array_reverse( $this->children_start_char ); + } + if ( $this->flip['page'] ) { + $this->articles = array_reverse( $this->articles ); + $this->articles_start_char = array_reverse( $this->articles_start_char ); + } + if ( !$this->showGallery && $this->flip['file'] ) { + $this->imgsNoGallery = array_reverse( $this->imgsNoGallery ); + $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char ); + } + } + + function doCategoryQuery() { + $dbr = wfGetDB( DB_REPLICA, 'category' ); + + $this->nextPage = [ + 'page' => null, + 'subcat' => null, + 'file' => null, + ]; + $this->prevPage = [ + 'page' => null, + 'subcat' => null, + 'file' => null, + ]; + + $this->flip = [ 'page' => false, 'subcat' => false, 'file' => false ]; + + foreach ( [ 'page', 'subcat', 'file' ] as $type ) { + # Get the sortkeys for start/end, if applicable. Note that if + # the collation in the database differs from the one + # set in $wgCategoryCollation, pagination might go totally haywire. + $extraConds = [ 'cl_type' => $type ]; + if ( isset( $this->from[$type] ) && $this->from[$type] !== null ) { + $extraConds[] = 'cl_sortkey >= ' + . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) ); + } elseif ( isset( $this->until[$type] ) && $this->until[$type] !== null ) { + $extraConds[] = 'cl_sortkey < ' + . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) ); + $this->flip[$type] = true; + } + + $res = $dbr->select( + [ 'page', 'categorylinks', 'category' ], + array_merge( + LinkCache::getSelectFields(), + [ + 'page_namespace', + 'page_title', + 'cl_sortkey', + 'cat_id', + 'cat_title', + 'cat_subcats', + 'cat_pages', + 'cat_files', + 'cl_sortkey_prefix', + 'cl_collation' + ] + ), + array_merge( [ 'cl_to' => $this->title->getDBkey() ], $extraConds ), + __METHOD__, + [ + 'USE INDEX' => [ 'categorylinks' => 'cl_sortkey' ], + 'LIMIT' => $this->limit + 1, + 'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey', + ], + [ + 'categorylinks' => [ 'INNER JOIN', 'cl_from = page_id' ], + 'category' => [ 'LEFT JOIN', [ + 'cat_title = page_title', + 'page_namespace' => NS_CATEGORY + ] ] + ] + ); + + Hooks::run( 'CategoryViewer::doCategoryQuery', [ $type, $res ] ); + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); + + $count = 0; + foreach ( $res as $row ) { + $title = Title::newFromRow( $row ); + $linkCache->addGoodLinkObjFromRow( $title, $row ); + + if ( $row->cl_collation === '' ) { + // Hack to make sure that while updating from 1.16 schema + // and db is inconsistent, that the sky doesn't fall. + // See r83544. Could perhaps be removed in a couple decades... + $humanSortkey = $row->cl_sortkey; + } else { + $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix ); + } + + if ( ++$count > $this->limit ) { + # We've reached the one extra which shows that there + # are additional pages to be had. Stop here... + $this->nextPage[$type] = $humanSortkey; + break; + } + if ( $count == $this->limit ) { + $this->prevPage[$type] = $humanSortkey; + } + + if ( $title->getNamespace() == NS_CATEGORY ) { + $cat = Category::newFromRow( $row, $title ); + $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len ); + } elseif ( $title->getNamespace() == NS_FILE ) { + $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); + } else { + $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); + } + } + } + } + + /** + * @return string + */ + function getCategoryTop() { + $r = $this->getCategoryBottom(); + return $r === '' + ? $r + : "
\n" . $r; + } + + /** + * @return string + */ + function getSubcategorySection() { + # Don't show subcategories section if there are none. + $r = ''; + $rescnt = count( $this->children ); + $dbcnt = $this->cat->getSubcatCount(); + // This function should be called even if the result isn't used, it has side-effects + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' ); + + if ( $rescnt > 0 ) { + # Showing subcategories + $r .= "
\n"; + $r .= '

' . $this->msg( 'subcategories' )->parse() . "

\n"; + $r .= $countmsg; + $r .= $this->getSectionPagingLinks( 'subcat' ); + $r .= $this->formatList( $this->children, $this->children_start_char ); + $r .= $this->getSectionPagingLinks( 'subcat' ); + $r .= "\n
"; + } + return $r; + } + + /** + * @return string + */ + function getPagesSection() { + $ti = wfEscapeWikiText( $this->title->getText() ); + # Don't show articles section if there are none. + $r = ''; + + # @todo FIXME: Here and in the other two sections: we don't need to bother + # with this rigmarole if the entire category contents fit on one page + # and have already been retrieved. We can just use $rescnt in that + # case and save a query and some logic. + $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount() + - $this->cat->getFileCount(); + $rescnt = count( $this->articles ); + // This function should be called even if the result isn't used, it has side-effects + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' ); + + if ( $rescnt > 0 ) { + $r = "
\n"; + $r .= '

' . $this->msg( 'category_header', $ti )->parse() . "

\n"; + $r .= $countmsg; + $r .= $this->getSectionPagingLinks( 'page' ); + $r .= $this->formatList( $this->articles, $this->articles_start_char ); + $r .= $this->getSectionPagingLinks( 'page' ); + $r .= "\n
"; + } + return $r; + } + + /** + * @return string + */ + function getImageSection() { + $r = ''; + $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery ); + $dbcnt = $this->cat->getFileCount(); + // This function should be called even if the result isn't used, it has side-effects + $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); + + if ( $rescnt > 0 ) { + $r .= "
\n"; + $r .= '

' . + $this->msg( + 'category-media-header', + wfEscapeWikiText( $this->title->getText() ) + )->text() . + "

\n"; + $r .= $countmsg; + $r .= $this->getSectionPagingLinks( 'file' ); + if ( $this->showGallery ) { + $r .= $this->gallery->toHTML(); + } else { + $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char ); + } + $r .= $this->getSectionPagingLinks( 'file' ); + $r .= "\n
"; + } + return $r; + } + + /** + * Get the paging links for a section (subcats/pages/files), to go at the top and bottom + * of the output. + * + * @param string $type 'page', 'subcat', or 'file' + * @return string HTML output, possibly empty if there are no other pages + */ + private function getSectionPagingLinks( $type ) { + if ( isset( $this->until[$type] ) && $this->until[$type] !== null ) { + // The new value for the until parameter should be pointing to the first + // result displayed on the page which is the second last result retrieved + // from the database.The next link should have a from parameter pointing + // to the until parameter of the current page. + if ( $this->nextPage[$type] !== null ) { + return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type ); + } else { + // If the nextPage variable is null, it means that we have reached the first page + // and therefore the previous link should be disabled. + return $this->pagingLinks( null, $this->until[$type], $type ); + } + } elseif ( $this->nextPage[$type] !== null + || ( isset( $this->from[$type] ) && $this->from[$type] !== null ) + ) { + return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type ); + } else { + return ''; + } + } + + /** + * @return string + */ + function getCategoryBottom() { + return ''; + } + + /** + * Format a list of articles chunked by letter, either as a + * bullet list or a columnar format, depending on the length. + * + * @param array $articles + * @param array $articles_start_char + * @param int $cutoff + * @return string + * @private + */ + function formatList( $articles, $articles_start_char, $cutoff = 6 ) { + $list = ''; + if ( count( $articles ) > $cutoff ) { + $list = self::columnList( $articles, $articles_start_char ); + } elseif ( count( $articles ) > 0 ) { + // for short lists of articles in categories. + $list = self::shortList( $articles, $articles_start_char ); + } + + $pageLang = $this->title->getPageLanguage(); + $attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(), + 'class' => 'mw-content-' . $pageLang->getDir() ]; + $list = Html::rawElement( 'div', $attribs, $list ); + + return $list; + } + + /** + * Format a list of articles chunked by letter in a three-column list, ordered + * vertically. This is used for categories with a significant number of pages. + * + * TODO: Take the headers into account when creating columns, so they're + * more visually equal. + * + * TODO: shortList and columnList are similar, need merging + * + * @param string[] $articles HTML links to each article + * @param string[] $articles_start_char The header characters for each article + * @return string HTML to output + * @private + */ + static function columnList( $articles, $articles_start_char ) { + $columns = array_combine( $articles, $articles_start_char ); + + $ret = Html::openElement( 'div', [ 'class' => 'mw-category' ] ); + + $colContents = []; + + # Kind of like array_flip() here, but we keep duplicates in an + # array instead of dropping them. + foreach ( $columns as $article => $char ) { + if ( !isset( $colContents[$char] ) ) { + $colContents[$char] = []; + } + $colContents[$char][] = $article; + } + + foreach ( $colContents as $char => $articles ) { + # Change space to non-breaking space to keep headers aligned + $h3char = $char === ' ' ? ' ' : htmlspecialchars( $char ); + + $ret .= '

' . $h3char; + $ret .= "

\n"; + + $ret .= '
  • '; + $ret .= implode( "
  • \n
  • ", $articles ); + $ret .= '
'; + + } + + $ret .= Html::closeElement( 'div' ); + return $ret; + } + + /** + * Format a list of articles chunked by letter in a bullet list. This is used + * for categories with a small number of pages (when columns aren't needed). + * @param string[] $articles HTML links to each article + * @param string[] $articles_start_char The header characters for each article + * @return string HTML to output + * @private + */ + static function shortList( $articles, $articles_start_char ) { + $r = '

' . htmlspecialchars( $articles_start_char[0] ) . "

\n"; + $r .= '
  • ' . $articles[0] . '
  • '; + $articleCount = count( $articles ); + for ( $index = 1; $index < $articleCount; $index++ ) { + if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { + $r .= "

" . htmlspecialchars( $articles_start_char[$index] ) . "

\n
    "; + } + + $r .= "
  • {$articles[$index]}
  • "; + } + $r .= '
'; + return $r; + } + + /** + * Create paging links, as a helper method to getSectionPagingLinks(). + * + * @param string $first The 'until' parameter for the generated URL + * @param string $last The 'from' parameter for the generated URL + * @param string $type A prefix for parameters, 'page' or 'subcat' or + * 'file' + * @return string HTML + */ + private function pagingLinks( $first, $last, $type = '' ) { + $prevLink = $this->msg( 'prev-page' )->escaped(); + + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + if ( $first != '' ) { + $prevQuery = $this->query; + $prevQuery["{$type}until"] = $first; + unset( $prevQuery["{$type}from"] ); + $prevLink = $linkRenderer->makeKnownLink( + $this->addFragmentToTitle( $this->title, $type ), + new HtmlArmor( $prevLink ), + [], + $prevQuery + ); + } + + $nextLink = $this->msg( 'next-page' )->escaped(); + + if ( $last != '' ) { + $lastQuery = $this->query; + $lastQuery["{$type}from"] = $last; + unset( $lastQuery["{$type}until"] ); + $nextLink = $linkRenderer->makeKnownLink( + $this->addFragmentToTitle( $this->title, $type ), + new HtmlArmor( $nextLink ), + [], + $lastQuery + ); + } + + return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped(); + } + + /** + * Takes a title, and adds the fragment identifier that + * corresponds to the correct segment of the category. + * + * @param Title $title The title (usually $this->title) + * @param string $section Which section + * @throws MWException + * @return Title + */ + private function addFragmentToTitle( $title, $section ) { + switch ( $section ) { + case 'page': + $fragment = 'mw-pages'; + break; + case 'subcat': + $fragment = 'mw-subcategories'; + break; + case 'file': + $fragment = 'mw-category-media'; + break; + default: + throw new MWException( __METHOD__ . + " Invalid section $section." ); + } + + return Title::makeTitle( $title->getNamespace(), + $title->getDBkey(), $fragment ); + } + + /** + * What to do if the category table conflicts with the number of results + * returned? This function says what. Each type is considered independently + * of the other types. + * + * @param int $rescnt The number of items returned by our database query. + * @param int $dbcnt The number of items according to the category table. + * @param string $type 'subcat', 'article', or 'file' + * @return string A message giving the number of items, to output to HTML. + */ + private function getCountMessage( $rescnt, $dbcnt, $type ) { + // There are three cases: + // 1) The category table figure seems sane. It might be wrong, but + // we can't do anything about it if we don't recalculate it on ev- + // ery category view. + // 2) The category table figure isn't sane, like it's smaller than the + // number of actual results, *but* the number of results is less + // than $this->limit and there's no offset. In this case we still + // know the right figure. + // 3) We have no idea. + + // Check if there's a "from" or "until" for anything + + // This is a little ugly, but we seem to use different names + // for the paging types then for the messages. + if ( $type === 'article' ) { + $pagingType = 'page'; + } else { + $pagingType = $type; + } + + $fromOrUntil = false; + if ( ( isset( $this->from[$pagingType] ) && $this->from[$pagingType] !== null ) || + ( isset( $this->until[$pagingType] ) && $this->until[$pagingType] !== null ) + ) { + $fromOrUntil = true; + } + + if ( $dbcnt == $rescnt || + ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt ) + ) { + // Case 1: seems sane. + $totalcnt = $dbcnt; + } elseif ( $rescnt < $this->limit && !$fromOrUntil ) { + // Case 2: not sane, but salvageable. Use the number of results. + // Since there are fewer than 200, we can also take this opportunity + // to refresh the incorrect category table entry -- which should be + // quick due to the small number of entries. + $totalcnt = $rescnt; + DeferredUpdates::addCallableUpdate( [ $this->cat, 'refreshCounts' ] ); + } else { + // Case 3: hopeless. Don't give a total count at all. + // Messages: category-subcat-count-limited, category-article-count-limited, + // category-file-count-limited + return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock(); + } + // Messages: category-subcat-count, category-article-count, category-file-count + return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock(); + } +} diff --git a/www/wiki/includes/CommentStore.php b/www/wiki/includes/CommentStore.php new file mode 100644 index 00000000..55f68572 --- /dev/null +++ b/www/wiki/includes/CommentStore.php @@ -0,0 +1,699 @@ + [ + 'table' => 'revision_comment_temp', + 'pk' => 'revcomment_rev', + 'field' => 'revcomment_comment_id', + 'joinPK' => 'rev_id', + ], + 'img_description' => [ + 'table' => 'image_comment_temp', + 'pk' => 'imgcomment_name', + 'field' => 'imgcomment_description_id', + 'joinPK' => 'img_name', + ], + ]; + + /** + * Fields that formerly used $tempTables + * @var array Key is '$key', value is the MediaWiki version in which it was + * removed from $tempTables. + */ + protected static $formerTempTables = []; + + /** + * @since 1.30 + * @deprecated in 1.31 + * @var string|null + */ + protected $key = null; + + /** @var int One of the MIGRATION_* constants */ + protected $stage; + + /** @var array[] Cache for `self::getJoin()` */ + protected $joinCache = []; + + /** @var Language Language to use for comment truncation */ + protected $lang; + + /** + * @param Language $lang Language to use for comment truncation. Defaults + * to $wgContLang. + * @param int $migrationStage One of the MIGRATION_* constants + */ + public function __construct( Language $lang, $migrationStage ) { + $this->stage = $migrationStage; + $this->lang = $lang; + } + + /** + * Static constructor for easier chaining + * @deprecated in 1.31 Should not be constructed with a $key, use CommentStore::getStore + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @return CommentStore + */ + public static function newKey( $key ) { + global $wgCommentTableSchemaMigrationStage, $wgContLang; + // TODO uncomment once not used in extensions + // wfDeprecated( __METHOD__, '1.31' ); + $store = new CommentStore( $wgContLang, $wgCommentTableSchemaMigrationStage ); + $store->key = $key; + return $store; + } + + /** + * @since 1.31 + * @deprecated in 1.31 Use DI to inject a CommentStore instance into your class. + * @return CommentStore + */ + public static function getStore() { + return MediaWikiServices::getInstance()->getCommentStore(); + } + + /** + * Compat method allowing use of self::newKey until removed. + * @param string|null $methodKey + * @throw InvalidArgumentException + * @return string + */ + private function getKey( $methodKey = null ) { + $key = $this->key !== null ? $this->key : $methodKey; + if ( $key === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$key should not be null' ); + // @codeCoverageIgnoreEnd + } + return $key; + } + + /** + * Get SELECT fields for the comment key + * + * Each resulting row should be passed to `self::getCommentLegacy()` to get the + * actual comment. + * + * @note Use of this method may require a subsequent database query to + * actually fetch the comment. If possible, use `self::getJoin()` instead. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat) + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @return string[] to include in the `$vars` to `IDatabase->select()`. All + * fields are aliased, so `+` is safe to use. + */ + public function getFields( $key = null ) { + $key = $this->getKey( $key ); + $fields = []; + if ( $this->stage === MIGRATION_OLD ) { + $fields["{$key}_text"] = $key; + $fields["{$key}_data"] = 'NULL'; + $fields["{$key}_cid"] = 'NULL'; + } else { + if ( $this->stage < MIGRATION_NEW ) { + $fields["{$key}_old"] = $key; + } + if ( isset( self::$tempTables[$key] ) ) { + $fields["{$key}_pk"] = self::$tempTables[$key]['joinPK']; + } else { + $fields["{$key}_id"] = "{$key}_id"; + } + } + return $fields; + } + + /** + * Get SELECT fields and joins for the comment key + * + * Each resulting row should be passed to `self::getComment()` to get the + * actual comment. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat) + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @return array With three keys: + * - tables: (string[]) to include in the `$table` to `IDatabase->select()` + * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` + * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` + * All tables, fields, and joins are aliased, so `+` is safe to use. + */ + public function getJoin( $key = null ) { + $key = $this->getKey( $key ); + if ( !array_key_exists( $key, $this->joinCache ) ) { + $tables = []; + $fields = []; + $joins = []; + + if ( $this->stage === MIGRATION_OLD ) { + $fields["{$key}_text"] = $key; + $fields["{$key}_data"] = 'NULL'; + $fields["{$key}_cid"] = 'NULL'; + } else { + $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN'; + + if ( isset( self::$tempTables[$key] ) ) { + $t = self::$tempTables[$key]; + $alias = "temp_$key"; + $tables[$alias] = $t['table']; + $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ]; + $joinField = "{$alias}.{$t['field']}"; + } else { + $joinField = "{$key}_id"; + } + + $alias = "comment_$key"; + $tables[$alias] = 'comment'; + $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ]; + + if ( $this->stage === MIGRATION_NEW ) { + $fields["{$key}_text"] = "{$alias}.comment_text"; + } else { + $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )"; + } + $fields["{$key}_data"] = "{$alias}.comment_data"; + $fields["{$key}_cid"] = "{$alias}.comment_id"; + } + + $this->joinCache[$key] = [ + 'tables' => $tables, + 'fields' => $fields, + 'joins' => $joins, + ]; + } + + return $this->joinCache[$key]; + } + + /** + * Extract the comment from a row + * + * Shared implementation for getComment() and getCommentLegacy() + * + * @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment() + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param object|array $row + * @param bool $fallback + * @return CommentStoreComment + */ + private function getCommentInternal( IDatabase $db = null, $key, $row, $fallback = false ) { + $row = (array)$row; + if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) { + $cid = isset( $row["{$key}_cid"] ) ? $row["{$key}_cid"] : null; + $text = $row["{$key}_text"]; + $data = $row["{$key}_data"]; + } elseif ( $this->stage === MIGRATION_OLD ) { + $cid = null; + if ( $fallback && isset( $row[$key] ) ) { + wfLogWarning( "Using deprecated fallback handling for comment $key" ); + $text = $row[$key]; + } else { + wfLogWarning( "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD" ); + $text = ''; + } + $data = null; + } else { + if ( isset( self::$tempTables[$key] ) ) { + if ( array_key_exists( "{$key}_pk", $row ) ) { + if ( !$db ) { + throw new InvalidArgumentException( + "\$row does not contain fields needed for comment $key and getComment(), but " + . "does have fields for getCommentLegacy()" + ); + } + $t = self::$tempTables[$key]; + $id = $row["{$key}_pk"]; + $row2 = $db->selectRow( + [ $t['table'], 'comment' ], + [ 'comment_id', 'comment_text', 'comment_data' ], + [ $t['pk'] => $id ], + __METHOD__, + [], + [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ] + ); + } elseif ( $fallback && isset( $row[$key] ) ) { + wfLogWarning( "Using deprecated fallback handling for comment $key" ); + $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ]; + } else { + throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" ); + } + } else { + if ( array_key_exists( "{$key}_id", $row ) ) { + if ( !$db ) { + throw new InvalidArgumentException( + "\$row does not contain fields needed for comment $key and getComment(), but " + . "does have fields for getCommentLegacy()" + ); + } + $id = $row["{$key}_id"]; + $row2 = $db->selectRow( + 'comment', + [ 'comment_id', 'comment_text', 'comment_data' ], + [ 'comment_id' => $id ], + __METHOD__ + ); + } elseif ( $fallback && isset( $row[$key] ) ) { + wfLogWarning( "Using deprecated fallback handling for comment $key" ); + $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ]; + } else { + throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" ); + } + } + + if ( $row2 ) { + $cid = $row2->comment_id; + $text = $row2->comment_text; + $data = $row2->comment_data; + } elseif ( $this->stage < MIGRATION_NEW && array_key_exists( "{$key}_old", $row ) ) { + $cid = null; + $text = $row["{$key}_old"]; + $data = null; + } else { + // @codeCoverageIgnoreStart + wfLogWarning( "Missing comment row for $key, id=$id" ); + $cid = null; + $text = ''; + $data = null; + // @codeCoverageIgnoreEnd + } + } + + $msg = null; + if ( $data !== null ) { + $data = FormatJson::decode( $data ); + if ( !is_object( $data ) ) { + // @codeCoverageIgnoreStart + wfLogWarning( "Invalid JSON object in comment: $data" ); + $data = null; + // @codeCoverageIgnoreEnd + } else { + $data = (array)$data; + if ( isset( $data['_message'] ) ) { + $msg = self::decodeMessage( $data['_message'] ) + ->setInterfaceMessageFlag( true ); + } + if ( !empty( $data['_null'] ) ) { + $data = null; + } else { + foreach ( $data as $k => $v ) { + if ( substr( $k, 0, 1 ) === '_' ) { + unset( $data[$k] ); + } + } + } + } + } + + return new CommentStoreComment( $cid, $text, $msg, $data ); + } + + /** + * Extract the comment from a row + * + * Use `self::getJoin()` to ensure the row contains the needed data. + * + * If you need to fake a comment in a row for some reason, set fields + * `{$key}_text` (string) and `{$key}_data` (JSON string or null). + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat) + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param object|array $row Result row. + * @param bool $fallback If true, fall back as well as possible instead of throwing an exception. + * @return CommentStoreComment + */ + public function getComment( $key, $row = null, $fallback = false ) { + // Compat for method sig change in 1.31 (introduction of $key) + if ( $this->key !== null ) { + $fallback = $row; + $row = $key; + $key = $this->getKey(); + } + if ( $row === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$row must not be null' ); + // @codeCoverageIgnoreEnd + } + return $this->getCommentInternal( null, $key, $row, $fallback ); + } + + /** + * Extract the comment from a row, with legacy lookups. + * + * If `$row` might have been generated using `self::getFields()` rather + * than `self::getJoin()`, use this. Prefer `self::getComment()` if you + * know callers used `self::getJoin()` for the row fetch. + * + * If you need to fake a comment in a row for some reason, set fields + * `{$key}_text` (string) and `{$key}_data` (JSON string or null). + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat) + * @param IDatabase $db Database handle to use for lookup + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param object|array $row Result row. + * @param bool $fallback If true, fall back as well as possible instead of throwing an exception. + * @return CommentStoreComment + */ + public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) { + // Compat for method sig change in 1.31 (introduction of $key) + if ( $this->key !== null ) { + $fallback = $row; + $row = $key; + $key = $this->getKey(); + } + if ( $row === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$row must not be null' ); + // @codeCoverageIgnoreEnd + } + return $this->getCommentInternal( $db, $key, $row, $fallback ); + } + + /** + * Create a new CommentStoreComment, inserting it into the database if necessary + * + * If a comment is going to be passed to `self::insert()` or the like + * multiple times, it will be more efficient to pass a CommentStoreComment + * once rather than making `self::insert()` do it every time through. + * + * @note When passing a CommentStoreComment, this may set `$comment->id` if + * it's not already set. If `$comment->id` is already set, it will not be + * verified that the specified comment actually exists or that it + * corresponds to the comment text, message, and/or data in the + * CommentStoreComment. + * @param IDatabase $dbw Database handle to insert on. Unused if `$comment` + * is a CommentStoreComment and `$comment->id` is set. + * @param string|Message|CommentStoreComment $comment Comment text or Message object, or + * a CommentStoreComment. + * @param array|null $data Structured data to store. Keys beginning with '_' are reserved. + * Ignored if $comment is a CommentStoreComment. + * @return CommentStoreComment + */ + public function createComment( IDatabase $dbw, $comment, array $data = null ) { + $comment = CommentStoreComment::newUnsavedComment( $comment, $data ); + + # Truncate comment in a Unicode-sensitive manner + $comment->text = $this->lang->truncate( $comment->text, self::MAX_COMMENT_LENGTH ); + if ( mb_strlen( $comment->text, 'UTF-8' ) > self::COMMENT_CHARACTER_LIMIT ) { + $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this->lang )->escaped(); + if ( mb_strlen( $ellipsis ) >= self::COMMENT_CHARACTER_LIMIT ) { + // WTF? + $ellipsis = '...'; + } + $maxLength = self::COMMENT_CHARACTER_LIMIT - mb_strlen( $ellipsis, 'UTF-8' ); + $comment->text = mb_substr( $comment->text, 0, $maxLength, 'UTF-8' ) . $ellipsis; + } + + if ( $this->stage > MIGRATION_OLD && !$comment->id ) { + $dbData = $comment->data; + if ( !$comment->message instanceof RawMessage ) { + if ( $dbData === null ) { + $dbData = [ '_null' => true ]; + } + $dbData['_message'] = self::encodeMessage( $comment->message ); + } + if ( $dbData !== null ) { + $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK ); + $len = strlen( $dbData ); + if ( $len > self::MAX_DATA_LENGTH ) { + $max = self::MAX_DATA_LENGTH; + throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" ); + } + } + + $hash = self::hash( $comment->text, $dbData ); + $comment->id = $dbw->selectField( + 'comment', + 'comment_id', + [ + 'comment_hash' => $hash, + 'comment_text' => $comment->text, + 'comment_data' => $dbData, + ], + __METHOD__ + ); + if ( !$comment->id ) { + $dbw->insert( + 'comment', + [ + 'comment_hash' => $hash, + 'comment_text' => $comment->text, + 'comment_data' => $dbData, + ], + __METHOD__ + ); + $comment->id = $dbw->insertId(); + } + } + + return $comment; + } + + /** + * Implementation for `self::insert()` and `self::insertWithTempTable()` + * @param IDatabase $dbw + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param string|Message|CommentStoreComment $comment + * @param array|null $data + * @return array [ array $fields, callable $callback ] + */ + private function insertInternal( IDatabase $dbw, $key, $comment, $data ) { + $fields = []; + $callback = null; + + $comment = $this->createComment( $dbw, $comment, $data ); + + if ( $this->stage <= MIGRATION_WRITE_BOTH ) { + $fields[$key] = $this->lang->truncate( $comment->text, 255 ); + } + + if ( $this->stage >= MIGRATION_WRITE_BOTH ) { + if ( isset( self::$tempTables[$key] ) ) { + $t = self::$tempTables[$key]; + $func = __METHOD__; + $commentId = $comment->id; + $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) { + $dbw->insert( + $t['table'], + [ + $t['pk'] => $id, + $t['field'] => $commentId, + ], + $func + ); + }; + } else { + $fields["{$key}_id"] = $comment->id; + } + } + + return [ $fields, $callback ]; + } + + /** + * Insert a comment in preparation for a row that references it + * + * @note It's recommended to include both the call to this method and the + * row insert in the same transaction. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat) + * @param IDatabase $dbw Database handle to insert on + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param string|Message|CommentStoreComment $comment As for `self::createComment()` + * @param array|null $data As for `self::createComment()` + * @return array Fields for the insert or update + */ + public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) { + // Compat for method sig change in 1.31 (introduction of $key) + if ( $this->key !== null ) { + $data = $comment; + $comment = $key; + $key = $this->key; + } + if ( $comment === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$comment can not be null' ); + // @codeCoverageIgnoreEnd + } + + if ( isset( self::$tempTables[$key] ) ) { + throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" ); + } + + list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data ); + return $fields; + } + + /** + * Insert a comment in a temporary table in preparation for a row that references it + * + * This is currently needed for "rev_comment" and "img_description". In the + * future that requirement will be removed. + * + * @note It's recommended to include both the call to this method and the + * row insert in the same transaction. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat) + * @param IDatabase $dbw Database handle to insert on + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param string|Message|CommentStoreComment $comment As for `self::createComment()` + * @param array|null $data As for `self::createComment()` + * @return array Two values: + * - array Fields for the insert or update + * - callable Function to call when the primary key of the row being + * inserted/updated is known. Pass it that primary key. + */ + public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) { + // Compat for method sig change in 1.31 (introduction of $key) + if ( $this->key !== null ) { + $data = $comment; + $comment = $key; + $key = $this->getKey(); + } + if ( $comment === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$comment can not be null' ); + // @codeCoverageIgnoreEnd + } + + if ( isset( self::$formerTempTables[$key] ) ) { + wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] ); + } elseif ( !isset( self::$tempTables[$key] ) ) { + throw new InvalidArgumentException( "Must use insert() for $key" ); + } + + list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data ); + if ( !$callback ) { + $callback = function () { + // Do nothing. + }; + } + return [ $fields, $callback ]; + } + + /** + * Encode a Message as a PHP data structure + * @param Message $msg + * @return array + */ + protected static function encodeMessage( Message $msg ) { + $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey(); + $params = $msg->getParams(); + foreach ( $params as &$param ) { + if ( $param instanceof Message ) { + $param = [ + 'message' => self::encodeMessage( $param ) + ]; + } + } + array_unshift( $params, $key ); + return $params; + } + + /** + * Decode a message that was encoded by self::encodeMessage() + * @param array $data + * @return Message + */ + protected static function decodeMessage( $data ) { + $key = array_shift( $data ); + foreach ( $data as &$param ) { + if ( is_object( $param ) ) { + $param = (array)$param; + } + if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) { + $param = self::decodeMessage( $param['message'] ); + } + } + return new Message( $key, $data ); + } + + /** + * Hashing function for comment storage + * @param string $text Comment text + * @param string|null $data Comment data + * @return int 32-bit signed integer + */ + public static function hash( $text, $data ) { + $hash = crc32( $text ) ^ crc32( (string)$data ); + + // 64-bit PHP returns an unsigned CRC, change it to signed for + // insertion into the database. + if ( $hash >= 0x80000000 ) { + $hash |= -1 << 32; + } + + return $hash; + } + +} diff --git a/www/wiki/includes/CommentStoreComment.php b/www/wiki/includes/CommentStoreComment.php new file mode 100644 index 00000000..7ed86d66 --- /dev/null +++ b/www/wiki/includes/CommentStoreComment.php @@ -0,0 +1,90 @@ +id = $id; + $this->text = $text; + $this->message = $message ?: new RawMessage( '$1', [ $text ] ); + $this->data = $data; + } + + /** + * Create a new, unsaved CommentStoreComment + * + * @param string|Message|CommentStoreComment $comment Comment text or Message object. + * A CommentStoreComment is also accepted here, in which case it is returned unchanged. + * @param array|null $data Structured data to store. Keys beginning with '_' are reserved. + * Ignored if $comment is a CommentStoreComment. + * @return CommentStoreComment + */ + public static function newUnsavedComment( $comment, array $data = null ) { + global $wgContLang; + + if ( $comment instanceof CommentStoreComment ) { + return $comment; + } + + if ( $data !== null ) { + foreach ( $data as $k => $v ) { + if ( substr( $k, 0, 1 ) === '_' ) { + throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' ); + } + } + } + + if ( $comment instanceof Message ) { + $message = clone $comment; + $text = $message->inLanguage( $wgContLang ) // Avoid $wgForceUIMsgAsContentMsg + ->setInterfaceMessageFlag( true ) + ->text(); + return new CommentStoreComment( null, $text, $message, $data ); + } else { + return new CommentStoreComment( null, $comment, null, $data ); + } + } +} diff --git a/www/wiki/includes/ConfiguredReadOnlyMode.php b/www/wiki/includes/ConfiguredReadOnlyMode.php new file mode 100644 index 00000000..af7c7cbd --- /dev/null +++ b/www/wiki/includes/ConfiguredReadOnlyMode.php @@ -0,0 +1,73 @@ +config = $config; + } + + /** + * Check whether the wiki is in read-only mode. + * + * @return bool + */ + public function isReadOnly() { + return $this->getReason() !== false; + } + + /** + * Get the value of $wgReadOnly or the contents of $wgReadOnlyFile. + * + * @return string|bool String when in read-only mode; false otherwise + */ + public function getReason() { + if ( $this->overrideReason !== null ) { + return $this->overrideReason; + } + $confReason = $this->config->get( 'ReadOnly' ); + if ( $confReason !== null ) { + return $confReason; + } + if ( $this->fileReason === null ) { + // Cache for faster access next time + $readOnlyFile = $this->config->get( 'ReadOnlyFile' ); + if ( is_file( $readOnlyFile ) && filesize( $readOnlyFile ) > 0 ) { + $this->fileReason = file_get_contents( $readOnlyFile ); + } else { + $this->fileReason = false; + } + } + return $this->fileReason; + } + + /** + * Set the read-only mode, which will apply for the remainder of the + * request or until a service reset. + * + * @param string|null $msg + */ + public function setReason( $msg ) { + $this->overrideReason = $msg; + } + + /** + * Clear the cache of the read only file + */ + public function clearCache() { + $this->fileReason = null; + } +} diff --git a/www/wiki/includes/DefaultSettings.php b/www/wiki/includes/DefaultSettings.php new file mode 100644 index 00000000..0fb01731 --- /dev/null +++ b/www/wiki/includes/DefaultSettings.php @@ -0,0 +1,8832 @@ + 'GlobalVarConfig::newInstance' +]; + +/** + * MediaWiki version number + * @since 1.2 + */ +$wgVersion = '1.31.3'; + +/** + * Name of the site. It must be changed in LocalSettings.php + */ +$wgSitename = 'MediaWiki'; + +/** + * When the wiki is running behind a proxy and this is set to true, assumes that the proxy exposes + * the wiki on the standard ports (443 for https and 80 for http). + * @var bool + * @since 1.26 + */ +$wgAssumeProxiesUseDefaultProtocolPorts = true; + +/** + * URL of the server. + * + * @par Example: + * @code + * $wgServer = 'http://example.com'; + * @endcode + * + * This is usually detected correctly by MediaWiki. If MediaWiki detects the + * wrong server, it will redirect incorrectly after you save a page. In that + * case, set this variable to fix it. + * + * If you want to use protocol-relative URLs on your wiki, set this to a + * protocol-relative URL like '//example.com' and set $wgCanonicalServer + * to a fully qualified URL. + */ +$wgServer = WebRequest::detectServer(); + +/** + * Canonical URL of the server, to use in IRC feeds and notification e-mails. + * Must be fully qualified, even if $wgServer is protocol-relative. + * + * Defaults to $wgServer, expanded to a fully qualified http:// URL if needed. + * @since 1.18 + */ +$wgCanonicalServer = false; + +/** + * Server name. This is automatically computed by parsing the bare + * hostname out of $wgCanonicalServer. It should not be customized. + * @since 1.24 + */ +$wgServerName = false; + +/************************************************************************//** + * @name Script path settings + * @{ + */ + +/** + * The path we should point to. + * It might be a virtual path in case with use apache mod_rewrite for example. + * + * This *needs* to be set correctly. + * + * Other paths will be set to defaults based on it unless they are directly + * set in LocalSettings.php + */ +$wgScriptPath = '/wiki'; + +/** + * Whether to support URLs like index.php/Page_title These often break when PHP + * is set up in CGI mode. PATH_INFO *may* be correct if cgi.fix_pathinfo is set, + * but then again it may not; lighttpd converts incoming path data to lowercase + * on systems with case-insensitive filesystems, and there have been reports of + * problems on Apache as well. + * + * To be safe we'll continue to keep it off by default. + * + * Override this to false if $_SERVER['PATH_INFO'] contains unexpectedly + * incorrect garbage, or to true if it is really correct. + * + * The default $wgArticlePath will be set based on this value at runtime, but if + * you have customized it, having this incorrectly set to true can cause + * redirect loops when "pretty URLs" are used. + * @since 1.2.1 + */ +$wgUsePathInfo = ( strpos( PHP_SAPI, 'cgi' ) === false ) && + ( strpos( PHP_SAPI, 'apache2filter' ) === false ) && + ( strpos( PHP_SAPI, 'isapi' ) === false ); + +/**@}*/ + +/************************************************************************//** + * @name URLs and file paths + * + * These various web and file path variables are set to their defaults + * in Setup.php if they are not explicitly set from LocalSettings.php. + * + * These will relatively rarely need to be set manually, unless you are + * splitting style sheets or images outside the main document root. + * + * In this section, a "path" is usually a host-relative URL, i.e. a URL without + * the host part, that starts with a slash. In most cases a full URL is also + * acceptable. A "directory" is a local file path. + * + * In both paths and directories, trailing slashes should not be included. + * + * @{ + */ + +/** + * The URL path to index.php. + * + * Defaults to "{$wgScriptPath}/index.php". + */ +$wgScript = false; + +/** + * The URL path to load.php. + * + * Defaults to "{$wgScriptPath}/load.php". + * @since 1.17 + */ +$wgLoadScript = false; + +/** + * The URL path of the skins directory. + * Defaults to "{$wgResourceBasePath}/skins". + * @since 1.3 + */ +$wgStylePath = false; +$wgStyleSheetPath = &$wgStylePath; + +/** + * The URL path of the skins directory. Should not point to an external domain. + * Defaults to "{$wgScriptPath}/skins". + * @since 1.17 + */ +$wgLocalStylePath = false; + +/** + * The URL path of the extensions directory. + * Defaults to "{$wgResourceBasePath}/extensions". + * @since 1.16 + */ +$wgExtensionAssetsPath = false; + +/** + * Filesystem extensions directory. + * Defaults to "{$IP}/extensions". + * @since 1.25 + */ +$wgExtensionDirectory = "{$IP}/extensions"; + +/** + * Filesystem stylesheets directory. + * Defaults to "{$IP}/skins". + * @since 1.3 + */ +$wgStyleDirectory = "{$IP}/skins"; + +/** + * The URL path for primary article page views. This path should contain $1, + * which is replaced by the article title. + * + * Defaults to "{$wgScript}/$1" or "{$wgScript}?title=$1", + * depending on $wgUsePathInfo. + */ +$wgArticlePath = false; + +/** + * The URL path for the images directory. + * Defaults to "{$wgScriptPath}/images". + */ +$wgUploadPath = false; + +/** + * The filesystem path of the images directory. Defaults to "{$IP}/images". + */ +$wgUploadDirectory = false; + +/** + * Directory where the cached page will be saved. + * Defaults to "{$wgUploadDirectory}/cache". + */ +$wgFileCacheDirectory = false; + +/** + * The URL path of the wiki logo. The logo size should be 135x135 pixels. + * Defaults to "$wgResourceBasePath/resources/assets/wiki.png". + */ +$wgLogo = false; + +/** + * Array with URL paths to HD versions of the wiki logo. The scaled logo size + * should be under 135x155 pixels. + * Only 1.5x and 2x versions are supported. + * + * @par Example: + * @code + * $wgLogoHD = [ + * "1.5x" => "path/to/1.5x_version.png", + * "2x" => "path/to/2x_version.png" + * ]; + * @endcode + * + * SVG is also supported but when enabled, it + * disables 1.5x and 2x as svg will already + * be optimised for screen resolution. + * + * @par Example: + * @code + * $wgLogoHD = [ + * "svg" => "path/to/svg_version.svg", + * ]; + * @endcode + * + * @since 1.25 + */ +$wgLogoHD = false; + +/** + * The URL path of the shortcut icon. + * @since 1.6 + */ +$wgFavicon = '/favicon.ico'; + +/** + * The URL path of the icon for iPhone and iPod Touch web app bookmarks. + * Defaults to no icon. + * @since 1.12 + */ +$wgAppleTouchIcon = false; + +/** + * Value for the referrer policy meta tag. + * One or more of the values defined in the Referrer Policy specification: + * https://w3c.github.io/webappsec-referrer-policy/ + * ('no-referrer', 'no-referrer-when-downgrade', 'same-origin', + * 'origin', 'strict-origin', 'origin-when-cross-origin', + * 'strict-origin-when-cross-origin', or 'unsafe-url') + * Setting it to false prevents the meta tag from being output + * (which results in falling back to the Referrer-Policy header, + * or 'no-referrer-when-downgrade' if that's not set either.) + * Setting it to an array (supported since 1.31) will create a meta tag for + * each value, in the reverse of the order (meaning that the first array element + * will be the default and the others used as fallbacks for browsers which do not + * understand it). + * + * @var array|string|bool + * @since 1.25 + */ +$wgReferrerPolicy = false; + +/** + * The local filesystem path to a temporary directory. This is not required to + * be web accessible. + * + * When this setting is set to false, its value will be set through a call + * to wfTempDir(). See that methods implementation for the actual detection + * logic. + * + * Developers should use the global function wfTempDir() instead of this + * variable. + * + * @see wfTempDir() + * @note Default changed to false in MediaWiki 1.20. + */ +$wgTmpDirectory = false; + +/** + * If set, this URL is added to the start of $wgUploadPath to form a complete + * upload URL. + * @since 1.4 + */ +$wgUploadBaseUrl = ''; + +/** + * To enable remote on-demand scaling, set this to the thumbnail base URL. + * Full thumbnail URL will be like $wgUploadStashScalerBaseUrl/e/e6/Foo.jpg/123px-Foo.jpg + * where 'e6' are the first two characters of the MD5 hash of the file name. + * If $wgUploadStashScalerBaseUrl is set to false, thumbs are rendered locally as needed. + * @since 1.17 + */ +$wgUploadStashScalerBaseUrl = false; + +/** + * To set 'pretty' URL paths for actions other than + * plain page views, add to this array. + * + * @par Example: + * Set pretty URL for the edit action: + * @code + * 'edit' => "$wgScriptPath/edit/$1" + * @endcode + * + * There must be an appropriate script or rewrite rule in place to handle these + * URLs. + * @since 1.5 + */ +$wgActionPaths = []; + +/**@}*/ + +/************************************************************************//** + * @name Files and file uploads + * @{ + */ + +/** + * Uploads have to be specially set up to be secure + */ +$wgEnableUploads = false; + +/** + * The maximum age of temporary (incomplete) uploaded files + */ +$wgUploadStashMaxAge = 6 * 3600; // 6 hours + +/** + * Allows to move images and other media files + */ +$wgAllowImageMoving = true; + +/** + * Enable deferred upload tasks that use the job queue. + * Only enable this if job runners are set up for both the + * 'AssembleUploadChunks' and 'PublishStashedFile' job types. + * + * @note If you use suhosin, this setting is incompatible with + * suhosin.session.encrypt. + */ +$wgEnableAsyncUploads = false; + +/** + * Additional characters that are not allowed in filenames. They are replaced with '-' when + * uploading. Like $wgLegalTitleChars, this is a regexp character class. + * + * Slashes and backslashes are disallowed regardless of this setting, but included here for + * completeness. + */ +$wgIllegalFileChars = ":\\/\\\\"; + +/** + * What directory to place deleted uploads in. + * Defaults to "{$wgUploadDirectory}/deleted". + */ +$wgDeletedDirectory = false; + +/** + * Set this to true if you use img_auth and want the user to see details on why access failed. + */ +$wgImgAuthDetails = false; + +/** + * Map of relative URL directories to match to internal mwstore:// base storage paths. + * For img_auth.php requests, everything after "img_auth.php/" is checked to see + * if starts with any of the prefixes defined here. The prefixes should not overlap. + * The prefix that matches has a corresponding storage path, which the rest of the URL + * is assumed to be relative to. The file at that path (or a 404) is send to the client. + * + * Example: + * $wgImgAuthUrlPathMap['/timeline/'] = 'mwstore://local-fs/timeline-render/'; + * The above maps ".../img_auth.php/timeline/X" to "mwstore://local-fs/timeline-render/". + * The name "local-fs" should correspond by name to an entry in $wgFileBackends. + * + * @see $wgFileBackends + */ +$wgImgAuthUrlPathMap = []; + +/** + * File repository structures + * + * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepos is + * an array of such structures. Each repository structure is an associative + * array of properties configuring the repository. + * + * Properties required for all repos: + * - class The class name for the repository. May come from the core or an extension. + * The core repository classes are FileRepo, LocalRepo, ForeignDBRepo. + * + * - name A unique name for the repository (but $wgLocalFileRepo should be 'local'). + * The name should consist of alpha-numeric characters. + * - backend A file backend name (see $wgFileBackends). + * + * For most core repos: + * - zones Associative array of zone names that each map to an array with: + * container : backend container name the zone is in + * directory : root path within container for the zone + * url : base URL to the root of the zone + * urlsByExt : map of file extension types to base URLs + * (useful for using a different cache for videos) + * Zones default to using "-" as the container name + * and default to using the container root as the zone's root directory. + * Nesting of zone locations within other zones should be avoided. + * - url Public zone URL. The 'zones' settings take precedence. + * - hashLevels The number of directory levels for hash-based division of files + * - thumbScriptUrl The URL for thumb.php (optional, not recommended) + * - transformVia404 Whether to skip media file transformation on parse and rely on a 404 + * handler instead. + * - initialCapital Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE], + * determines whether filenames implicitly start with a capital letter. + * The current implementation may give incorrect description page links + * when the local $wgCapitalLinks and initialCapital are mismatched. + * - pathDisclosureProtection + * May be 'paranoid' to remove all parameters from error messages, 'none' to + * leave the paths in unchanged, or 'simple' to replace paths with + * placeholders. Default for LocalRepo is 'simple'. + * - fileMode This allows wikis to set the file mode when uploading/moving files. Default + * is 0644. + * - directory The local filesystem directory where public files are stored. Not used for + * some remote repos. + * - thumbDir The base thumbnail directory. Defaults to "/thumb". + * - thumbUrl The base thumbnail URL. Defaults to "/thumb". + * - isPrivate Set this if measures should always be taken to keep the files private. + * One should not trust this to assure that the files are not web readable; + * the server configuration should be done manually depending on the backend. + * + * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored + * for local repositories: + * - descBaseUrl URL of image description pages, e.g. https://en.wikipedia.org/wiki/File: + * - scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g. + * https://en.wikipedia.org/w + * - articleUrl Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 + * - fetchDescription Fetch the text of the remote file description page. Equivalent to + * $wgFetchCommonsDescriptions. + * - abbrvThreshold File names over this size will use the short form of thumbnail names. + * Short thumbnail names only have the width, parameters, and the extension. + * + * ForeignDBRepo: + * - dbType, dbServer, dbUser, dbPassword, dbName, dbFlags + * equivalent to the corresponding member of $wgDBservers + * - tablePrefix Table prefix, the foreign wiki's $wgDBprefix + * - hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc + * + * ForeignAPIRepo: + * - apibase Use for the foreign API's URL + * - apiThumbCacheExpiry How long to locally cache thumbs for + * + * If you leave $wgLocalFileRepo set to false, Setup will fill in appropriate values. + * Otherwise, set $wgLocalFileRepo to a repository structure as described above. + * If you set $wgUseInstantCommons to true, it will add an entry for Commons. + * If you set $wgForeignFileRepos to an array of repository structures, those will + * be searched after the local file repo. + * Otherwise, you will only have access to local media files. + * + * @see Setup.php for an example usage and default initialization. + */ +$wgLocalFileRepo = false; + +/** + * @see $wgLocalFileRepo + */ +$wgForeignFileRepos = []; + +/** + * Use Commons as a remote file repository. Essentially a wrapper, when this + * is enabled $wgForeignFileRepos will point at Commons with a set of default + * settings + */ +$wgUseInstantCommons = false; + +/** + * Array of foreign file repo names (set in $wgForeignFileRepos above) that + * are allowable upload targets. These wikis must have some method of + * authentication (i.e. CentralAuth), and be CORS-enabled for this wiki. + * The string 'local' signifies the default local file repository. + * + * Example: + * $wgForeignUploadTargets = [ 'shared' ]; + */ +$wgForeignUploadTargets = [ 'local' ]; + +/** + * Configuration for file uploads using the embeddable upload dialog + * (https://www.mediawiki.org/wiki/Upload_dialog). + * + * This applies also to foreign uploads to this wiki (the configuration is loaded by remote wikis + * using the action=query&meta=siteinfo API). + * + * See below for documentation of each property. None of the properties may be omitted. + */ +$wgUploadDialog = [ + // Fields to make available in the dialog. `true` means that this field is visible, `false` means + // that it is hidden. The "Name" field can't be hidden. Note that you also have to add the + // matching replacement to the 'filepage' format key below to make use of these. + 'fields' => [ + 'description' => true, + 'date' => false, + 'categories' => false, + ], + // Suffix of localisation messages used to describe the license under which the uploaded file will + // be released. The same value may be set for both 'local' and 'foreign' uploads. + 'licensemessages' => [ + // The 'local' messages are used for local uploads on this wiki: + // * upload-form-label-own-work-message-generic-local + // * upload-form-label-not-own-work-message-generic-local + // * upload-form-label-not-own-work-local-generic-local + 'local' => 'generic-local', + // The 'foreign' messages are used for cross-wiki uploads from other wikis to this wiki: + // * upload-form-label-own-work-message-generic-foreign + // * upload-form-label-not-own-work-message-generic-foreign + // * upload-form-label-not-own-work-local-generic-foreign + 'foreign' => 'generic-foreign', + ], + // Upload comments to use for 'local' and 'foreign' uploads. This can also be set to a single + // string value, in which case it is used for both kinds of uploads. Available replacements: + // * $HOST - domain name from which a cross-wiki upload originates + // * $PAGENAME - wiki page name from which an upload originates + 'comment' => [ + 'local' => '', + 'foreign' => '', + ], + // Format of the file page wikitext to be generated from the fields input by the user. + 'format' => [ + // Wrapper for the whole page. Available replacements: + // * $DESCRIPTION - file description, as input by the user (only if the 'description' field is + // enabled), wrapped as defined below in the 'description' key + // * $DATE - file creation date, as input by the user (only if the 'date' field is enabled) + // * $SOURCE - as defined below in the 'ownwork' key, may be extended in the future + // * $AUTHOR - linked user name, may be extended in the future + // * $LICENSE - as defined below in the 'license' key, may be extended in the future + // * $CATEGORIES - file categories wikitext, as input by the user (only if the 'categories' + // field is enabled), or if no input, as defined below in the 'uncategorized' key + 'filepage' => '$DESCRIPTION', + // Wrapped for file description. Available replacements: + // * $LANGUAGE - source wiki's content language + // * $TEXT - input by the user + 'description' => '$TEXT', + 'ownwork' => '', + 'license' => '', + 'uncategorized' => '', + ], +]; + +/** + * File backend structure configuration. + * + * This is an array of file backend configuration arrays. + * Each backend configuration has the following parameters: + * - 'name' : A unique name for the backend + * - 'class' : The file backend class to use + * - 'wikiId' : A unique string that identifies the wiki (container prefix) + * - 'lockManager' : The name of a lock manager (see $wgLockManagers) + * + * See FileBackend::__construct() for more details. + * Additional parameters are specific to the file backend class used. + * These settings should be global to all wikis when possible. + * + * FileBackendMultiWrite::__construct() is augmented with a 'template' option that + * can be used in any of the values of the 'backends' array. Its value is the name of + * another backend in $wgFileBackends. When set, it pre-fills the array with all of the + * configuration of the named backend. Explicitly set values in the array take precedence. + * + * There are two particularly important aspects about each backend: + * - a) Whether it is fully qualified or wiki-relative. + * By default, the paths of files are relative to the current wiki, + * which works via prefixing them with the current wiki ID when accessed. + * Setting 'wikiId' forces the backend to be fully qualified by prefixing + * all paths with the specified value instead. This can be useful if + * multiple wikis need to share the same data. Note that 'name' is *not* + * part of any prefix and thus should not be relied upon for namespacing. + * - b) Whether it is only defined for some wikis or is defined on all + * wikis in the wiki farm. Defining a backend globally is useful + * if multiple wikis need to share the same data. + * One should be aware of these aspects when configuring a backend for use with + * any basic feature or plugin. For example, suppose an extension stores data for + * different wikis in different directories and sometimes needs to access data from + * a foreign wiki's directory in order to render a page on given wiki. The extension + * would need a fully qualified backend that is defined on all wikis in the wiki farm. + */ +$wgFileBackends = []; + +/** + * Array of configuration arrays for each lock manager. + * Each backend configuration has the following parameters: + * - 'name' : A unique name for the lock manager + * - 'class' : The lock manger class to use + * + * See LockManager::__construct() for more details. + * Additional parameters are specific to the lock manager class used. + * These settings should be global to all wikis. + * + * When using DBLockManager, the 'dbsByBucket' map can reference 'localDBMaster' as + * a peer database in each bucket. This will result in an extra connection to the domain + * that the LockManager services, which must also be a valid wiki ID. + */ +$wgLockManagers = []; + +/** + * Show Exif data, on by default if available. + * Requires PHP's Exif extension: https://secure.php.net/manual/en/ref.exif.php + * + * @note FOR WINDOWS USERS: + * To enable Exif functions, add the following line to the "Windows + * extensions" section of php.ini: + * @code{.ini} + * extension=extensions/php_exif.dll + * @endcode + */ +$wgShowEXIF = function_exists( 'exif_read_data' ); + +/** + * If to automatically update the img_metadata field + * if the metadata field is outdated but compatible with the current version. + * Defaults to false. + */ +$wgUpdateCompatibleMetadata = false; + +/** + * If you operate multiple wikis, you can define a shared upload path here. + * Uploads to this wiki will NOT be put there - they will be put into + * $wgUploadDirectory. + * If $wgUseSharedUploads is set, the wiki will look in the shared repository if + * no file of the given name is found in the local repository (for [[File:..]], + * [[Media:..]] links). Thumbnails will also be looked for and generated in this + * directory. + * + * Note that these configuration settings can now be defined on a per- + * repository basis for an arbitrary number of file repositories, using the + * $wgForeignFileRepos variable. + */ +$wgUseSharedUploads = false; + +/** + * Full path on the web server where shared uploads can be found + */ +$wgSharedUploadPath = null; + +/** + * Fetch commons image description pages and display them on the local wiki? + */ +$wgFetchCommonsDescriptions = false; + +/** + * Path on the file system where shared uploads can be found. + */ +$wgSharedUploadDirectory = null; + +/** + * DB name with metadata about shared directory. + * Set this to false if the uploads do not come from a wiki. + */ +$wgSharedUploadDBname = false; + +/** + * Optional table prefix used in database. + */ +$wgSharedUploadDBprefix = ''; + +/** + * Cache shared metadata in memcached. + * Don't do this if the commons wiki is in a different memcached domain + */ +$wgCacheSharedUploads = true; + +/** + * Allow for upload to be copied from an URL. + * The timeout for copy uploads is set by $wgCopyUploadTimeout. + * You have to assign the user right 'upload_by_url' to a user group, to use this. + */ +$wgAllowCopyUploads = false; + +/** + * A list of domains copy uploads can come from + * + * @since 1.20 + */ +$wgCopyUploadsDomains = []; + +/** + * Enable copy uploads from Special:Upload. $wgAllowCopyUploads must also be + * true. If $wgAllowCopyUploads is true, but this is false, you will only be + * able to perform copy uploads from the API or extensions (e.g. UploadWizard). + */ +$wgCopyUploadsFromSpecialUpload = false; + +/** + * Proxy to use for copy upload requests. + * @since 1.20 + */ +$wgCopyUploadProxy = false; + +/** + * Different timeout for upload by url + * This could be useful since when fetching large files, you may want a + * timeout longer than the default $wgHTTPTimeout. False means fallback + * to default. + * + * @var int|bool + * + * @since 1.22 + */ +$wgCopyUploadTimeout = false; + +/** + * Max size for uploads, in bytes. If not set to an array, applies to all + * uploads. If set to an array, per upload type maximums can be set, using the + * file and url keys. If the * key is set this value will be used as maximum + * for non-specified types. + * + * @par Example: + * @code + * $wgMaxUploadSize = [ + * '*' => 250 * 1024, + * 'url' => 500 * 1024, + * ]; + * @endcode + * Sets the maximum for all uploads to 250 kB except for upload-by-url, which + * will have a maximum of 500 kB. + */ +$wgMaxUploadSize = 1024 * 1024 * 100; # 100MB + +/** + * Minimum upload chunk size, in bytes. When using chunked upload, non-final + * chunks smaller than this will be rejected. May be reduced based on the + * 'upload_max_filesize' or 'post_max_size' PHP settings. + * @since 1.26 + */ +$wgMinUploadChunkSize = 1024; # 1KB + +/** + * Point the upload navigation link to an external URL + * Useful if you want to use a shared repository by default + * without disabling local uploads (use $wgEnableUploads = false for that). + * + * @par Example: + * @code + * $wgUploadNavigationUrl = 'https://commons.wikimedia.org/wiki/Special:Upload'; + * @endcode + */ +$wgUploadNavigationUrl = false; + +/** + * Point the upload link for missing files to an external URL, as with + * $wgUploadNavigationUrl. The URL will get "(?|&)wpDestFile=" + * appended to it as appropriate. + */ +$wgUploadMissingFileUrl = false; + +/** + * Give a path here to use thumb.php for thumbnail generation on client + * request, instead of generating them on render and outputting a static URL. + * This is necessary if some of your apache servers don't have read/write + * access to the thumbnail path. + * + * @par Example: + * @code + * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb.php"; + * @endcode + */ +$wgThumbnailScriptPath = false; + +/** + * @see $wgThumbnailScriptPath + */ +$wgSharedThumbnailScriptPath = false; + +/** + * Set this to false if you do not want MediaWiki to divide your images + * directory into many subdirectories, for improved performance. + * + * It's almost always good to leave this enabled. In previous versions of + * MediaWiki, some users set this to false to allow images to be added to the + * wiki by simply copying them into $wgUploadDirectory and then running + * maintenance/rebuildImages.php to register them in the database. This is no + * longer recommended, use maintenance/importImages.php instead. + * + * @note That this variable may be ignored if $wgLocalFileRepo is set. + * @todo Deprecate the setting and ultimately remove it from Core. + */ +$wgHashedUploadDirectory = true; + +/** + * Set the following to false especially if you have a set of files that need to + * be accessible by all wikis, and you do not want to use the hash (path/a/aa/) + * directory layout. + */ +$wgHashedSharedUploadDirectory = true; + +/** + * Base URL for a repository wiki. Leave this blank if uploads are just stored + * in a shared directory and not meant to be accessible through a separate wiki. + * Otherwise the image description pages on the local wiki will link to the + * image description page on this wiki. + * + * Please specify the namespace, as in the example below. + */ +$wgRepositoryBaseUrl = "https://commons.wikimedia.org/wiki/File:"; + +/** + * This is the list of preferred extensions for uploading files. Uploading files + * with extensions not in this list will trigger a warning. + * + * @warning If you add any OpenOffice or Microsoft Office file formats here, + * such as odt or doc, and untrusted users are allowed to upload files, then + * your wiki will be vulnerable to cross-site request forgery (CSRF). + */ +$wgFileExtensions = [ 'png', 'gif', 'jpg', 'jpeg', 'webp' ]; + +/** + * Files with these extensions will never be allowed as uploads. + * An array of file extensions to blacklist. You should append to this array + * if you want to blacklist additional files. + */ +$wgFileBlacklist = [ + # HTML may contain cookie-stealing JavaScript and web bugs + 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', + # PHP scripts may execute arbitrary code on the server + 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', + # Other types that may be interpreted by some servers + 'shtml', 'jhtml', 'pl', 'py', 'cgi', + # May contain harmful executables for Windows victims + 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl' ]; + +/** + * Files with these MIME types will never be allowed as uploads + * if $wgVerifyMimeType is enabled. + */ +$wgMimeTypeBlacklist = [ + # HTML may contain cookie-stealing JavaScript and web bugs + 'text/html', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', + # PHP scripts may execute arbitrary code on the server + 'application/x-php', 'text/x-php', + # Other types that may be interpreted by some servers + 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', + # Client-side hazards on Internet Explorer + 'text/scriptlet', 'application/x-msdownload', + # Windows metafile, client-side vulnerability on some systems + 'application/x-msmetafile', +]; + +/** + * Allow Java archive uploads. + * This is not recommended for public wikis since a maliciously-constructed + * applet running on the same domain as the wiki can steal the user's cookies. + */ +$wgAllowJavaUploads = false; + +/** + * This is a flag to determine whether or not to check file extensions on upload. + * + * @warning Setting this to false is insecure for public wikis. + */ +$wgCheckFileExtensions = true; + +/** + * If this is turned off, users may override the warning for files not covered + * by $wgFileExtensions. + * + * @warning Setting this to false is insecure for public wikis. + */ +$wgStrictFileExtensions = true; + +/** + * Setting this to true will disable the upload system's checks for HTML/JavaScript. + * + * @warning THIS IS VERY DANGEROUS on a publicly editable site, so USE + * $wgGroupPermissions TO RESTRICT UPLOADING to only those that you trust + */ +$wgDisableUploadScriptChecks = false; + +/** + * Warn if uploaded files are larger than this (in bytes), or false to disable + */ +$wgUploadSizeWarning = false; + +/** + * list of trusted media-types and MIME types. + * Use the MEDIATYPE_xxx constants to represent media types. + * This list is used by File::isSafeFile + * + * Types not listed here will have a warning about unsafe content + * displayed on the images description page. It would also be possible + * to use this for further restrictions, like disabling direct + * [[media:...]] links for non-trusted formats. + */ +$wgTrustedMediaFormats = [ + MEDIATYPE_BITMAP, // all bitmap formats + MEDIATYPE_AUDIO, // all audio formats + MEDIATYPE_VIDEO, // all plain video formats + "image/svg+xml", // svg (only needed if inline rendering of svg is not supported) + "application/pdf", // PDF files + # "application/x-shockwave-flash", //flash/shockwave movie +]; + +/** + * Plugins for media file type handling. + * Each entry in the array maps a MIME type to a class name + * + * Core media handlers are listed in MediaHandlerFactory, + * and extensions should use extension.json. + */ +$wgMediaHandlers = []; + +/** + * Media handler overrides for parser tests (they don't need to generate actual + * thumbnails, so a mock will do) + */ +$wgParserTestMediaHandlers = [ + 'image/jpeg' => 'MockBitmapHandler', + 'image/png' => 'MockBitmapHandler', + 'image/gif' => 'MockBitmapHandler', + 'image/tiff' => 'MockBitmapHandler', + 'image/webp' => 'MockBitmapHandler', + 'image/x-ms-bmp' => 'MockBitmapHandler', + 'image/x-bmp' => 'MockBitmapHandler', + 'image/x-xcf' => 'MockBitmapHandler', + 'image/svg+xml' => 'MockSvgHandler', + 'image/vnd.djvu' => 'MockDjVuHandler', +]; + +/** + * Plugins for page content model handling. + * Each entry in the array maps a model id to a class name or callback + * that creates an instance of the appropriate ContentHandler subclass. + * + * @since 1.21 + */ +$wgContentHandlers = [ + // the usual case + CONTENT_MODEL_WIKITEXT => WikitextContentHandler::class, + // dumb version, no syntax highlighting + CONTENT_MODEL_JAVASCRIPT => JavaScriptContentHandler::class, + // simple implementation, for use by extensions, etc. + CONTENT_MODEL_JSON => JsonContentHandler::class, + // dumb version, no syntax highlighting + CONTENT_MODEL_CSS => CssContentHandler::class, + // plain text, for use by extensions, etc. + CONTENT_MODEL_TEXT => TextContentHandler::class, +]; + +/** + * Whether to enable server-side image thumbnailing. If false, images will + * always be sent to the client in full resolution, with appropriate width= and + * height= attributes on the tag for the client to do its own scaling. + */ +$wgUseImageResize = true; + +/** + * Resizing can be done using PHP's internal image libraries or using + * ImageMagick or another third-party converter, e.g. GraphicMagick. + * These support more file formats than PHP, which only supports PNG, + * GIF, JPG, XBM and WBMP. + * + * Use Image Magick instead of PHP builtin functions. + */ +$wgUseImageMagick = false; + +/** + * The convert command shipped with ImageMagick + */ +$wgImageMagickConvertCommand = '/usr/bin/convert'; + +/** + * Array of max pixel areas for interlacing per MIME type + * @since 1.27 + */ +$wgMaxInterlacingAreas = []; + +/** + * Sharpening parameter to ImageMagick + */ +$wgSharpenParameter = '0x0.4'; + +/** + * Reduction in linear dimensions below which sharpening will be enabled + */ +$wgSharpenReductionThreshold = 0.85; + +/** + * Temporary directory used for ImageMagick. The directory must exist. Leave + * this set to false to let ImageMagick decide for itself. + */ +$wgImageMagickTempDir = false; + +/** + * Use another resizing converter, e.g. GraphicMagick + * %s will be replaced with the source path, %d with the destination + * %w and %h will be replaced with the width and height. + * + * @par Example for GraphicMagick: + * @code + * $wgCustomConvertCommand = "gm convert %s -resize %wx%h %d" + * @endcode + * + * Leave as false to skip this. + */ +$wgCustomConvertCommand = false; + +/** + * used for lossless jpeg rotation + * + * @since 1.21 + */ +$wgJpegTran = '/usr/bin/jpegtran'; + +/** + * At default setting of 'yuv420', JPEG thumbnails will use 4:2:0 chroma + * subsampling to reduce file size, at the cost of possible color fringing + * at sharp edges. + * + * See https://en.wikipedia.org/wiki/Chroma_subsampling + * + * Supported values: + * false - use scaling system's default (same as pre-1.27 behavior) + * 'yuv444' - luma and chroma at same resolution + * 'yuv422' - chroma at 1/2 resolution horizontally, full vertically + * 'yuv420' - chroma at 1/2 resolution in both dimensions + * + * This setting is currently supported only for the ImageMagick backend; + * others may default to 4:2:0 or 4:4:4 or maintaining the source file's + * sampling in the thumbnail. + * + * @since 1.27 + */ +$wgJpegPixelFormat = 'yuv420'; + +/** + * Some tests and extensions use exiv2 to manipulate the Exif metadata in some + * image formats. + */ +$wgExiv2Command = '/usr/bin/exiv2'; + +/** + * Path to exiftool binary. Used for lossless ICC profile swapping. + * + * @since 1.26 + */ +$wgExiftool = '/usr/bin/exiftool'; + +/** + * Scalable Vector Graphics (SVG) may be uploaded as images. + * Since SVG support is not yet standard in browsers, it is + * necessary to rasterize SVGs to PNG as a fallback format. + * + * An external program is required to perform this conversion. + * If set to an array, the first item is a PHP callable and any further items + * are passed as parameters after $srcPath, $dstPath, $width, $height + */ +$wgSVGConverters = [ + 'ImageMagick' => + '$path/convert -background "#ffffff00" -thumbnail $widthx$height\! $input PNG:$output', + 'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output', + 'inkscape' => '$path/inkscape -z -w $width -f $input -e $output', + 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d ' + . '$output $input', + 'rsvg' => '$path/rsvg-convert -w $width -h $height -o $output $input', + 'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output', + 'ImagickExt' => [ 'SvgHandler::rasterizeImagickExt' ], +]; + +/** + * Pick a converter defined in $wgSVGConverters + */ +$wgSVGConverter = 'ImageMagick'; + +/** + * If not in the executable PATH, specify the SVG converter path. + */ +$wgSVGConverterPath = ''; + +/** + * Don't scale a SVG larger than this + */ +$wgSVGMaxSize = 5120; + +/** + * Don't read SVG metadata beyond this point. + * Default is 1024*256 bytes + */ +$wgSVGMetadataCutoff = 262144; + +/** + * Disallow element in SVG files. + * + * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic + * browsers which can not perform basic stuff like MIME detection and which are + * vulnerable to further idiots uploading crap files as images. + * + * When this directive is on, "<title>" will be allowed in files with an + * "image/svg+xml" MIME type. You should leave this disabled if your web server + * is misconfigured and doesn't send appropriate MIME types for SVG images. + */ +$wgAllowTitlesInSVG = false; + +/** + * The maximum number of pixels a source image can have if it is to be scaled + * down by a scaler that requires the full source image to be decompressed + * and stored in decompressed form, before the thumbnail is generated. + * + * This provides a limit on memory usage for the decompression side of the + * image scaler. The limit is used when scaling PNGs with any of the + * built-in image scalers, such as ImageMagick or GD. It is ignored for + * JPEGs with ImageMagick, and when using the VipsScaler extension. + * + * The default is 50 MB if decompressed to RGBA form, which corresponds to + * 12.5 million pixels or 3500x3500. + */ +$wgMaxImageArea = 1.25e7; + +/** + * Force thumbnailing of animated GIFs above this size to a single + * frame instead of an animated thumbnail. As of MW 1.17 this limit + * is checked against the total size of all frames in the animation. + * It probably makes sense to keep this equal to $wgMaxImageArea. + */ +$wgMaxAnimatedGifArea = 1.25e7; + +/** + * Browsers don't support TIFF inline generally... + * For inline display, we need to convert to PNG or JPEG. + * Note scaling should work with ImageMagick, but may not with GD scaling. + * + * @par Example: + * @code + * // PNG is lossless, but inefficient for photos + * $wgTiffThumbnailType = [ 'png', 'image/png' ]; + * // JPEG is good for photos, but has no transparency support. Bad for diagrams. + * $wgTiffThumbnailType = [ 'jpg', 'image/jpeg' ]; + * @endcode + */ +$wgTiffThumbnailType = false; + +/** + * If rendered thumbnail files are older than this timestamp, they + * will be rerendered on demand as if the file didn't already exist. + * Update if there is some need to force thumbs and SVG rasterizations + * to rerender, such as fixes to rendering bugs. + */ +$wgThumbnailEpoch = '20030516000000'; + +/** + * Certain operations are avoided if there were too many recent failures, + * for example, thumbnail generation. Bump this value to invalidate all + * memory of failed operations and thus allow further attempts to resume. + * This is useful when a cause for the failures has been found and fixed. + */ +$wgAttemptFailureEpoch = 1; + +/** + * If set, inline scaled images will still produce "<img>" tags ready for + * output instead of showing an error message. + * + * This may be useful if errors are transitory, especially if the site + * is configured to automatically render thumbnails on request. + * + * On the other hand, it may obscure error conditions from debugging. + * Enable the debug log or the 'thumbnail' log group to make sure errors + * are logged to a file for review. + */ +$wgIgnoreImageErrors = false; + +/** + * Allow thumbnail rendering on page view. If this is false, a valid + * thumbnail URL is still output, but no file will be created at + * the target location. This may save some time if you have a + * thumb.php or 404 handler set up which is faster than the regular + * webserver(s). + */ +$wgGenerateThumbnailOnParse = true; + +/** + * Show thumbnails for old images on the image description page + */ +$wgShowArchiveThumbnails = true; + +/** + * If set to true, images that contain certain the exif orientation tag will + * be rotated accordingly. If set to null, try to auto-detect whether a scaler + * is available that can rotate. + */ +$wgEnableAutoRotation = null; + +/** + * Internal name of virus scanner. This serves as a key to the + * $wgAntivirusSetup array. Set this to NULL to disable virus scanning. If not + * null, every file uploaded will be scanned for viruses. + */ +$wgAntivirus = null; + +/** + * Configuration for different virus scanners. This an associative array of + * associative arrays. It contains one setup array per known scanner type. + * The entry is selected by $wgAntivirus, i.e. + * valid values for $wgAntivirus are the keys defined in this array. + * + * The configuration array for each scanner contains the following keys: + * "command", "codemap", "messagepattern": + * + * "command" is the full command to call the virus scanner - %f will be + * replaced with the name of the file to scan. If not present, the filename + * will be appended to the command. Note that this must be overwritten if the + * scanner is not in the system path; in that case, please set + * $wgAntivirusSetup[$wgAntivirus]['command'] to the desired command with full + * path. + * + * "codemap" is a mapping of exit code to return codes of the detectVirus + * function in SpecialUpload. + * - An exit code mapped to AV_SCAN_FAILED causes the function to consider + * the scan to be failed. This will pass the file if $wgAntivirusRequired + * is not set. + * - An exit code mapped to AV_SCAN_ABORTED causes the function to consider + * the file to have an unsupported format, which is probably immune to + * viruses. This causes the file to pass. + * - An exit code mapped to AV_NO_VIRUS will cause the file to pass, meaning + * no virus was found. + * - All other codes (like AV_VIRUS_FOUND) will cause the function to report + * a virus. + * - You may use "*" as a key in the array to catch all exit codes not mapped otherwise. + * + * "messagepattern" is a perl regular expression to extract the meaningful part of the scanners + * output. The relevant part should be matched as group one (\1). + * If not defined or the pattern does not match, the full message is shown to the user. + */ +$wgAntivirusSetup = [ + + # setup for clamav + 'clamav' => [ + 'command' => 'clamscan --no-summary ', + 'codemap' => [ + "0" => AV_NO_VIRUS, # no virus + "1" => AV_VIRUS_FOUND, # virus found + "52" => AV_SCAN_ABORTED, # unsupported file format (probably immune) + "*" => AV_SCAN_FAILED, # else scan failed + ], + 'messagepattern' => '/.*?:(.*)/sim', + ], +]; + +/** + * Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. + */ +$wgAntivirusRequired = true; + +/** + * Determines if the MIME type of uploaded files should be checked + */ +$wgVerifyMimeType = true; + +/** + * Sets the MIME type definition file to use by MimeMagic.php. + * Set to null, to use built-in defaults only. + * example: $wgMimeTypeFile = '/etc/mime.types'; + */ +$wgMimeTypeFile = 'includes/mime.types'; + +/** + * Sets the MIME type info file to use by MimeMagic.php. + * Set to null, to use built-in defaults only. + */ +$wgMimeInfoFile = 'includes/mime.info'; + +/** + * Sets an external MIME detector program. The command must print only + * the MIME type to standard output. + * The name of the file to process will be appended to the command given here. + * If not set or NULL, PHP's mime_content_type function will be used. + * + * @par Example: + * @code + * #$wgMimeDetectorCommand = "file -bi"; # use external MIME detector (Linux) + * @endcode + */ +$wgMimeDetectorCommand = null; + +/** + * Switch for trivial MIME detection. Used by thumb.php to disable all fancy + * things, because only a few types of images are needed and file extensions + * can be trusted. + */ +$wgTrivialMimeDetection = false; + +/** + * Additional XML types we can allow via MIME-detection. + * array = [ 'rootElement' => 'associatedMimeType' ] + */ +$wgXMLMimeTypes = [ + 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml', + 'svg' => 'image/svg+xml', + 'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram', + 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml? + 'html' => 'text/html', // application/xhtml+xml? +]; + +/** + * Limit images on image description pages to a user-selectable limit. In order + * to reduce disk usage, limits can only be selected from a list. + * The user preference is saved as an array offset in the database, by default + * the offset is set with $wgDefaultUserOptions['imagesize']. Make sure you + * change it if you alter the array (see T10858). + * This is the list of settings the user can choose from: + */ +$wgImageLimits = [ + [ 320, 240 ], + [ 640, 480 ], + [ 800, 600 ], + [ 1024, 768 ], + [ 1280, 1024 ] +]; + +/** + * Adjust thumbnails on image pages according to a user setting. In order to + * reduce disk usage, the values can only be selected from a list. This is the + * list of settings the user can choose from: + */ +$wgThumbLimits = [ + 120, + 150, + 180, + 200, + 250, + 300 +]; + +/** + * When defined, is an array of image widths used as buckets for thumbnail generation. + * The goal is to save resources by generating thumbnails based on reference buckets instead of + * always using the original. This will incur a speed gain but cause a quality loss. + * + * The buckets generation is chained, with each bucket generated based on the above bucket + * when possible. File handlers have to opt into using that feature. For now only BitmapHandler + * supports it. + */ +$wgThumbnailBuckets = null; + +/** + * When using thumbnail buckets as defined above, this sets the minimum distance to the bucket + * above the requested size. The distance represents how many extra pixels of width the bucket + * needs in order to be used as the reference for a given thumbnail. For example, with the + * following buckets: + * + * $wgThumbnailBuckets = [ 128, 256, 512 ]; + * + * and a distance of 50: + * + * $wgThumbnailMinimumBucketDistance = 50; + * + * If we want to render a thumbnail of width 220px, the 512px bucket will be used, + * because 220 + 50 = 270 and the closest bucket bigger than 270px is 512. + */ +$wgThumbnailMinimumBucketDistance = 50; + +/** + * When defined, is an array of thumbnail widths to be rendered at upload time. The idea is to + * prerender common thumbnail sizes, in order to avoid the necessity to render them on demand, which + * has a performance impact for the first client to view a certain size. + * + * This obviously means that more disk space is needed per upload upfront. + * + * @since 1.25 + */ + +$wgUploadThumbnailRenderMap = []; + +/** + * The method through which the thumbnails will be prerendered for the entries in + * $wgUploadThumbnailRenderMap + * + * The method can be either "http" or "jobqueue". The former uses an http request to hit the + * thumbnail's URL. + * This method only works if thumbnails are configured to be rendered by a 404 handler. The latter + * option uses the job queue to render the thumbnail. + * + * @since 1.25 + */ +$wgUploadThumbnailRenderMethod = 'jobqueue'; + +/** + * When using the "http" wgUploadThumbnailRenderMethod, lets one specify a custom Host HTTP header. + * + * @since 1.25 + */ +$wgUploadThumbnailRenderHttpCustomHost = false; + +/** + * When using the "http" wgUploadThumbnailRenderMethod, lets one specify a custom domain to send the + * HTTP request to. + * + * @since 1.25 + */ +$wgUploadThumbnailRenderHttpCustomDomain = false; + +/** + * When this variable is true and JPGs use the sRGB ICC profile, swaps it for the more lightweight + * (and free) TinyRGB profile when generating thumbnails. + * + * @since 1.26 + */ +$wgUseTinyRGBForJPGThumbnails = false; + +/** + * Parameters for the "<gallery>" tag. + * Fields are: + * - imagesPerRow: Default number of images per-row in the gallery. 0 -> Adapt to screensize + * - imageWidth: Width of the cells containing images in galleries (in "px") + * - imageHeight: Height of the cells containing images in galleries (in "px") + * - captionLength: Length to truncate filename to in caption when using "showfilename". + * A value of 'true' will truncate the filename to one line using CSS + * and will be the behaviour after deprecation. + * @deprecated since 1.28 + * - showBytes: Show the filesize in bytes in categories + * - showDimensions: Show the dimensions (width x height) in categories + * - mode: Gallery mode + */ +$wgGalleryOptions = []; + +/** + * Adjust width of upright images when parameter 'upright' is used + * This allows a nicer look for upright images without the need to fix the width + * by hardcoded px in wiki sourcecode. + */ +$wgThumbUpright = 0.75; + +/** + * Default value for chmoding of new directories. + */ +$wgDirectoryMode = 0777; + +/** + * Generate and use thumbnails suitable for screens with 1.5 and 2.0 pixel densities. + * + * This means a 320x240 use of an image on the wiki will also generate 480x360 and 640x480 + * thumbnails, output via the srcset attribute. + * + * On older browsers, a JavaScript polyfill switches the appropriate images in after loading + * the original low-resolution versions depending on the reported window.devicePixelRatio. + * The polyfill can be found in the jquery.hidpi module. + */ +$wgResponsiveImages = true; + +/** + * @name DJVU settings + * @{ + */ + +/** + * Path of the djvudump executable + * Enable this and $wgDjvuRenderer to enable djvu rendering + * example: $wgDjvuDump = 'djvudump'; + */ +$wgDjvuDump = null; + +/** + * Path of the ddjvu DJVU renderer + * Enable this and $wgDjvuDump to enable djvu rendering + * example: $wgDjvuRenderer = 'ddjvu'; + */ +$wgDjvuRenderer = null; + +/** + * Path of the djvutxt DJVU text extraction utility + * Enable this and $wgDjvuDump to enable text layer extraction from djvu files + * example: $wgDjvuTxt = 'djvutxt'; + */ +$wgDjvuTxt = null; + +/** + * Path of the djvutoxml executable + * This works like djvudump except much, much slower as of version 3.5. + * + * For now we recommend you use djvudump instead. The djvuxml output is + * probably more stable, so we'll switch back to it as soon as they fix + * the efficiency problem. + * https://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 + * + * @par Example: + * @code + * $wgDjvuToXML = 'djvutoxml'; + * @endcode + */ +$wgDjvuToXML = null; + +/** + * Shell command for the DJVU post processor + * Default: pnmtojpeg, since ddjvu generates ppm output + * Set this to false to output the ppm file directly. + */ +$wgDjvuPostProcessor = 'pnmtojpeg'; + +/** + * File extension for the DJVU post processor output + */ +$wgDjvuOutputExtension = 'jpg'; + +/** @} */ # end of DJvu } + +/** @} */ # end of file uploads } + +/************************************************************************//** + * @name Email settings + * @{ + */ + +/** + * Site admin email address. + * + * Defaults to "wikiadmin@$wgServerName". + */ +$wgEmergencyContact = false; + +/** + * Sender email address for e-mail notifications. + * + * The address we use as sender when a user requests a password reminder. + * + * Defaults to "apache@$wgServerName". + */ +$wgPasswordSender = false; + +/** + * Sender name for e-mail notifications. + * + * @deprecated since 1.23; use the system message 'emailsender' instead. + */ +$wgPasswordSenderName = 'MediaWiki Mail'; + +/** + * Reply-To address for e-mail notifications. + * + * Defaults to $wgPasswordSender. + */ +$wgNoReplyAddress = false; + +/** + * Set to true to enable the e-mail basic features: + * Password reminders, etc. If sending e-mail on your + * server doesn't work, you might want to disable this. + */ +$wgEnableEmail = true; + +/** + * Set to true to enable user-to-user e-mail. + * This can potentially be abused, as it's hard to track. + */ +$wgEnableUserEmail = true; + +/** + * Set to true to enable user-to-user e-mail blacklist. + * + * @since 1.30 + */ +$wgEnableUserEmailBlacklist = false; + +/** + * If true put the sending user's email in a Reply-To header + * instead of From (false). ($wgPasswordSender will be used as From.) + * + * Some mailers (eg SMTP) set the SMTP envelope sender to the From value, + * which can cause problems with SPF validation and leak recipient addresses + * when bounces are sent to the sender. In addition, DMARC restrictions + * can cause emails to fail to be received when false. + */ +$wgUserEmailUseReplyTo = true; + +/** + * Minimum time, in hours, which must elapse between password reminder + * emails for a given account. This is to prevent abuse by mail flooding. + */ +$wgPasswordReminderResendTime = 24; + +/** + * The time, in seconds, when an emailed temporary password expires. + */ +$wgNewPasswordExpiry = 3600 * 24 * 7; + +/** + * The time, in seconds, when an email confirmation email expires + */ +$wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60; + +/** + * The number of days that a user's password is good for. After this number of days, the + * user will be asked to reset their password. Set to false to disable password expiration. + */ +$wgPasswordExpirationDays = false; + +/** + * If a user's password is expired, the number of seconds when they can still login, + * and cancel their password change, but are sent to the password change form on each login. + */ +$wgPasswordExpireGrace = 3600 * 24 * 7; // 7 days + +/** + * SMTP Mode. + * + * For using a direct (authenticated) SMTP server connection. + * Default to false or fill an array : + * + * @code + * $wgSMTP = [ + * 'host' => 'SMTP domain', + * 'IDHost' => 'domain for MessageID', + * 'port' => '25', + * 'auth' => [true|false], + * 'username' => [SMTP username], + * 'password' => [SMTP password], + * ]; + * @endcode + */ +$wgSMTP = false; + +/** + * Additional email parameters, will be passed as the last argument to mail() call. + */ +$wgAdditionalMailParams = null; + +/** + * For parts of the system that have been updated to provide HTML email content, send + * both text and HTML parts as the body of the email + */ +$wgAllowHTMLEmail = false; + +/** + * True: from page editor if s/he opted-in. False: Enotif mails appear to come + * from $wgEmergencyContact + */ +$wgEnotifFromEditor = false; + +// TODO move UPO to preferences probably ? +# If set to true, users get a corresponding option in their preferences and can choose to +# enable or disable at their discretion +# If set to false, the corresponding input form on the user preference page is suppressed +# It call this to be a "user-preferences-option (UPO)" + +/** + * Require email authentication before sending mail to an email address. + * This is highly recommended. It prevents MediaWiki from being used as an open + * spam relay. + */ +$wgEmailAuthentication = true; + +/** + * Allow users to enable email notification ("enotif") on watchlist changes. + */ +$wgEnotifWatchlist = false; + +/** + * Allow users to enable email notification ("enotif") when someone edits their + * user talk page. + * + * The owner of the user talk page must also have the 'enotifusertalkpages' user + * preference set to true. + */ +$wgEnotifUserTalk = false; + +/** + * Set the Reply-to address in notifications to the editor's address, if user + * allowed this in the preferences. + */ +$wgEnotifRevealEditorAddress = false; + +/** + * Potentially send notification mails on minor edits to pages. This is enabled + * by default. If this is false, users will never be notified on minor edits. + * + * If it is true, editors with the 'nominornewtalk' right (typically bots) will still not + * trigger notifications for minor edits they make (to any page, not just user talk). + * + * Finally, if the watcher/recipient has the 'enotifminoredits' user preference set to + * false, they will not receive notifications for minor edits. + * + * User talk notifications are also affected by $wgEnotifMinorEdits, the above settings, + * $wgEnotifUserTalk, and the preference described there. + */ +$wgEnotifMinorEdits = true; + +/** + * Send a generic mail instead of a personalised mail for each user. This + * always uses UTC as the time zone, and doesn't include the username. + * + * For pages with many users watching, this can significantly reduce mail load. + * Has no effect when using sendmail rather than SMTP. + */ +$wgEnotifImpersonal = false; + +/** + * Maximum number of users to mail at once when using impersonal mail. Should + * match the limit on your mail server. + */ +$wgEnotifMaxRecips = 500; + +/** + * Use real name instead of username in e-mail "from" field. + */ +$wgEnotifUseRealName = false; + +/** + * Array of usernames who will be sent a notification email for every change + * which occurs on a wiki. Users will not be notified of their own changes. + */ +$wgUsersNotifiedOnAllChanges = []; + +/** @} */ # end of email settings + +/************************************************************************//** + * @name Database settings + * @{ + */ + +/** + * Database host name or IP address + */ +$wgDBserver = 'localhost'; + +/** + * Database port number (for PostgreSQL and Microsoft SQL Server). + */ +$wgDBport = 5432; + +/** + * Name of the database + */ +$wgDBname = 'my_wiki'; + +/** + * Database username + */ +$wgDBuser = 'wikiuser'; + +/** + * Database user's password + */ +$wgDBpassword = ''; + +/** + * Database type + */ +$wgDBtype = 'mysql'; + +/** + * Whether to use SSL in DB connection. + * + * This setting is only used if $wgLBFactoryConf['class'] is set to + * '\Wikimedia\Rdbms\LBFactorySimple' and $wgDBservers is an empty array; otherwise + * the DBO_SSL flag must be set in the 'flags' option of the database + * connection to achieve the same functionality. + */ +$wgDBssl = false; + +/** + * Whether to use compression in DB connection. + * + * This setting is only used $wgLBFactoryConf['class'] is set to + * '\Wikimedia\Rdbms\LBFactorySimple' and $wgDBservers is an empty array; otherwise + * the DBO_COMPRESS flag must be set in the 'flags' option of the database + * connection to achieve the same functionality. + */ +$wgDBcompress = false; + +/** + * Separate username for maintenance tasks. Leave as null to use the default. + */ +$wgDBadminuser = null; + +/** + * Separate password for maintenance tasks. Leave as null to use the default. + */ +$wgDBadminpassword = null; + +/** + * Search type. + * Leave as null to select the default search engine for the + * selected database type (eg SearchMySQL), or set to a class + * name to override to a custom search engine. + */ +$wgSearchType = null; + +/** + * Alternative search types + * Sometimes you want to support multiple search engines for testing. This + * allows users to select their search engine of choice via url parameters + * to Special:Search and the action=search API. If using this, there's no + * need to add $wgSearchType to it, that is handled automatically. + */ +$wgSearchTypeAlternatives = null; + +/** + * Table name prefix + */ +$wgDBprefix = ''; + +/** + * MySQL table options to use during installation or update + */ +$wgDBTableOptions = 'ENGINE=InnoDB'; + +/** + * SQL Mode - default is turning off all modes, including strict, if set. + * null can be used to skip the setting for performance reasons and assume + * DBA has done his best job. + * String override can be used for some additional fun :-) + */ +$wgSQLMode = ''; + +/** + * Mediawiki schema + */ +$wgDBmwschema = null; + +/** + * To override default SQLite data directory ($docroot/../data) + */ +$wgSQLiteDataDir = ''; + +/** + * Shared database for multiple wikis. Commonly used for storing a user table + * for single sign-on. The server for this database must be the same as for the + * main database. + * + * For backwards compatibility the shared prefix is set to the same as the local + * prefix, and the user table is listed in the default list of shared tables. + * The user_properties table is also added so that users will continue to have their + * preferences shared (preferences were stored in the user table prior to 1.16) + * + * $wgSharedTables may be customized with a list of tables to share in the shared + * database. However it is advised to limit what tables you do share as many of + * MediaWiki's tables may have side effects if you try to share them. + * + * $wgSharedPrefix is the table prefix for the shared database. It defaults to + * $wgDBprefix. + * + * $wgSharedSchema is the table schema for the shared database. It defaults to + * $wgDBmwschema. + * + * @deprecated since 1.21 In new code, use the $wiki parameter to wfGetLB() to + * access remote databases. Using wfGetLB() allows the shared database to + * reside on separate servers to the wiki's own database, with suitable + * configuration of $wgLBFactoryConf. + */ +$wgSharedDB = null; + +/** + * @see $wgSharedDB + */ +$wgSharedPrefix = false; + +/** + * @see $wgSharedDB + */ +$wgSharedTables = [ 'user', 'user_properties' ]; + +/** + * @see $wgSharedDB + * @since 1.23 + */ +$wgSharedSchema = false; + +/** + * Database load balancer + * This is a two-dimensional array, an array of server info structures + * Fields are: + * - host: Host name + * - dbname: Default database name + * - user: DB user + * - password: DB password + * - type: DB type + * - driver: DB driver (when there are multiple drivers) + * + * - load: Ratio of DB_REPLICA load, must be >=0, the sum of all loads must be >0. + * If this is zero for any given server, no normal query traffic will be + * sent to it. It will be excluded from lag checks in maintenance scripts. + * The only way it can receive traffic is if groupLoads is used. + * + * - groupLoads: array of load ratios, the key is the query group name. A query may belong + * to several groups, the most specific group defined here is used. + * + * - flags: bit field + * - DBO_DEFAULT -- turns on DBO_TRX only if "cliMode" is off (recommended) + * - DBO_DEBUG -- equivalent of $wgDebugDumpSql + * - DBO_TRX -- wrap entire request in a transaction + * - DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) + * - DBO_PERSISTENT -- enables persistent database connections + * - DBO_SSL -- uses SSL/TLS encryption in database connections, if available + * - DBO_COMPRESS -- uses internal compression in database connections, + * if available + * + * - max lag: (optional) Maximum replication lag before a replica DB goes out of rotation + * - is static: (optional) Set to true if the dataset is static and no replication is used. + * - cliMode: (optional) Connection handles will not assume that requests are short-lived + * nor that INSERT..SELECT can be rewritten into a buffered SELECT and INSERT. + * [Default: uses value of $wgCommandLineMode] + * + * These and any other user-defined properties will be assigned to the mLBInfo member + * variable of the Database object. + * + * Leave at false to use the single-server variables above. If you set this + * variable, the single-server variables will generally be ignored (except + * perhaps in some command-line scripts). + * + * The first server listed in this array (with key 0) will be the master. The + * rest of the servers will be replica DBs. To prevent writes to your replica DBs due to + * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your + * replica DBs in my.cnf. You can set read_only mode at runtime using: + * + * @code + * SET @@read_only=1; + * @endcode + * + * Since the effect of writing to a replica DB is so damaging and difficult to clean + * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even + * our masters, and then set read_only=0 on masters at runtime. + */ +$wgDBservers = false; + +/** + * Load balancer factory configuration + * To set up a multi-master wiki farm, set the class here to something that + * can return a LoadBalancer with an appropriate master on a call to getMainLB(). + * The class identified here is responsible for reading $wgDBservers, + * $wgDBserver, etc., so overriding it may cause those globals to be ignored. + * + * The LBFactoryMulti class is provided for this purpose, please see + * includes/db/LBFactoryMulti.php for configuration information. + */ +$wgLBFactoryConf = [ 'class' => \Wikimedia\Rdbms\LBFactorySimple::class ]; + +/** + * After a state-changing request is done by a client, this determines + * how many seconds that client should keep using the master datacenter. + * This avoids unexpected stale or 404 responses due to replication lag. + * @since 1.27 + */ +$wgDataCenterUpdateStickTTL = 10; + +/** + * File to log database errors to + */ +$wgDBerrorLog = false; + +/** + * Timezone to use in the error log. + * Defaults to the wiki timezone ($wgLocaltimezone). + * + * A list of usable timezones can found at: + * https://secure.php.net/manual/en/timezones.php + * + * @par Examples: + * @code + * $wgDBerrorLogTZ = 'UTC'; + * $wgDBerrorLogTZ = 'GMT'; + * $wgDBerrorLogTZ = 'PST8PDT'; + * $wgDBerrorLogTZ = 'Europe/Sweden'; + * $wgDBerrorLogTZ = 'CET'; + * @endcode + * + * @since 1.20 + */ +$wgDBerrorLogTZ = false; + +/** + * Set to true to engage MySQL 4.1/5.0 charset-related features; + * for now will just cause sending of 'SET NAMES=utf8' on connect. + * + * @warning THIS IS EXPERIMENTAL! + * + * May break if you're not using the table defs from mysql5/tables.sql. + * May break if you're upgrading an existing wiki if set differently. + * Broken symptoms likely to include incorrect behavior with page titles, + * usernames, comments etc containing non-ASCII characters. + * Might also cause failures on the object cache and other things. + * + * Even correct usage may cause failures with Unicode supplementary + * characters (those not in the Basic Multilingual Plane) unless MySQL + * has enhanced their Unicode support. + * + * @deprecated since 1.31 + */ +$wgDBmysql5 = false; + +/** + * Set true to enable Oracle DCRP (supported from 11gR1 onward) + * + * To use this feature set to true and use a datasource defined as + * POOLED (i.e. in tnsnames definition set server=pooled in connect_data + * block). + * + * Starting from 11gR1 you can use DCRP (Database Resident Connection + * Pool) that maintains established sessions and reuses them on new + * connections. + * + * Not completely tested, but it should fall back on normal connection + * in case the pool is full or the datasource is not configured as + * pooled. + * And the other way around; using oci_pconnect on a non pooled + * datasource should produce a normal connection. + * + * When it comes to frequent shortlived DB connections like with MW + * Oracle tends to s***. The problem is the driver connects to the + * database reasonably fast, but establishing a session takes time and + * resources. MW does not rely on session state (as it does not use + * features such as package variables) so establishing a valid session + * is in this case an unwanted overhead that just slows things down. + * + * @warning EXPERIMENTAL! + */ +$wgDBOracleDRCP = false; + +/** + * Other wikis on this site, can be administered from a single developer account. + * + * Array numeric key => database name + */ +$wgLocalDatabases = []; + +/** + * If lag is higher than $wgSlaveLagWarning, show a warning in some special + * pages (like watchlist). If the lag is higher than $wgSlaveLagCritical, + * show a more obvious warning. + */ +$wgSlaveLagWarning = 10; + +/** + * @see $wgSlaveLagWarning + */ +$wgSlaveLagCritical = 30; + +/** + * Use Windows Authentication instead of $wgDBuser / $wgDBpassword for MS SQL Server + */ +$wgDBWindowsAuthentication = false; + +/**@}*/ # End of DB settings } + +/************************************************************************//** + * @name Text storage + * @{ + */ + +/** + * We can also compress text stored in the 'text' table. If this is set on, new + * revisions will be compressed on page save if zlib support is available. Any + * compressed revisions will be decompressed on load regardless of this setting, + * but will not be readable at all* if zlib support is not available. + */ +$wgCompressRevisions = false; + +/** + * External stores allow including content + * from non database sources following URL links. + * + * Short names of ExternalStore classes may be specified in an array here: + * @code + * $wgExternalStores = [ "http","file","custom" ]... + * @endcode + * + * CAUTION: Access to database might lead to code execution + */ +$wgExternalStores = []; + +/** + * An array of external MySQL servers. + * + * @par Example: + * Create a cluster named 'cluster1' containing three servers: + * @code + * $wgExternalServers = [ + * 'cluster1' => <array in the same format as $wgDBservers> + * ]; + * @endcode + * + * Used by \Wikimedia\Rdbms\LBFactorySimple, may be ignored if $wgLBFactoryConf is set to + * another class. + */ +$wgExternalServers = []; + +/** + * The place to put new revisions, false to put them in the local text table. + * Part of a URL, e.g. DB://cluster1 + * + * Can be an array instead of a single string, to enable data distribution. Keys + * must be consecutive integers, starting at zero. + * + * @par Example: + * @code + * $wgDefaultExternalStore = [ 'DB://cluster1', 'DB://cluster2' ]; + * @endcode + * + * @var array + */ +$wgDefaultExternalStore = false; + +/** + * Revision text may be cached in $wgMemc to reduce load on external storage + * servers and object extraction overhead for frequently-loaded revisions. + * + * Set to 0 to disable, or number of seconds before cache expiry. + */ +$wgRevisionCacheExpiry = 86400 * 7; + +/** @} */ # end text storage } + +/************************************************************************//** + * @name Performance hacks and limits + * @{ + */ + +/** + * Disable database-intensive features + */ +$wgMiserMode = false; + +/** + * Disable all query pages if miser mode is on, not just some + */ +$wgDisableQueryPages = false; + +/** + * Number of rows to cache in 'querycache' table when miser mode is on + */ +$wgQueryCacheLimit = 1000; + +/** + * Number of links to a page required before it is deemed "wanted" + */ +$wgWantedPagesThreshold = 1; + +/** + * Enable slow parser functions + */ +$wgAllowSlowParserFunctions = false; + +/** + * Allow schema updates + */ +$wgAllowSchemaUpdates = true; + +/** + * Maximum article size in kilobytes + */ +$wgMaxArticleSize = 2048; + +/** + * The minimum amount of memory that MediaWiki "needs"; MediaWiki will try to + * raise PHP's memory limit if it's below this amount. + */ +$wgMemoryLimit = "50M"; + +/** + * The minimum amount of time that MediaWiki needs for "slow" write request, + * particularly ones with multiple non-atomic writes that *should* be as + * transactional as possible; MediaWiki will call set_time_limit() if needed. + * @since 1.26 + */ +$wgTransactionalTimeLimit = 120; + +/** @} */ # end performance hacks } + +/************************************************************************//** + * @name Cache settings + * @{ + */ + +/** + * Directory for caching data in the local filesystem. Should not be accessible + * from the web. + * + * Note: if multiple wikis share the same localisation cache directory, they + * must all have the same set of extensions. You can set a directory just for + * the localisation cache using $wgLocalisationCacheConf['storeDirectory']. + */ +$wgCacheDirectory = false; + +/** + * Main cache type. This should be a cache with fast access, but it may have + * limited space. By default, it is disabled, since the stock database cache + * is not fast enough to make it worthwhile. + * + * The options are: + * + * - CACHE_ANYTHING: Use anything, as long as it works + * - CACHE_NONE: Do not cache + * - CACHE_DB: Store cache objects in the DB + * - CACHE_MEMCACHED: MemCached, must specify servers in $wgMemCachedServers + * - CACHE_ACCEL: APC, APCU or WinCache + * - (other): A string may be used which identifies a cache + * configuration in $wgObjectCaches. + * + * @see $wgMessageCacheType, $wgParserCacheType + */ +$wgMainCacheType = CACHE_NONE; + +/** + * The cache type for storing the contents of the MediaWiki namespace. This + * cache is used for a small amount of data which is expensive to regenerate. + * + * For available types see $wgMainCacheType. + */ +$wgMessageCacheType = CACHE_ANYTHING; + +/** + * The cache type for storing article HTML. This is used to store data which + * is expensive to regenerate, and benefits from having plenty of storage space. + * + * For available types see $wgMainCacheType. + */ +$wgParserCacheType = CACHE_ANYTHING; + +/** + * The cache type for storing session data. + * + * For available types see $wgMainCacheType. + */ +$wgSessionCacheType = CACHE_ANYTHING; + +/** + * The cache type for storing language conversion tables, + * which are used when parsing certain text and interface messages. + * + * For available types see $wgMainCacheType. + * + * @since 1.20 + */ +$wgLanguageConverterCacheType = CACHE_ANYTHING; + +/** + * Advanced object cache configuration. + * + * Use this to define the class names and constructor parameters which are used + * for the various cache types. Custom cache types may be defined here and + * referenced from $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, + * or $wgLanguageConverterCacheType. + * + * The format is an associative array where the key is a cache identifier, and + * the value is an associative array of parameters. The "class" parameter is the + * class name which will be used. Alternatively, a "factory" parameter may be + * given, giving a callable function which will generate a suitable cache object. + */ +$wgObjectCaches = [ + CACHE_NONE => [ 'class' => EmptyBagOStuff::class, 'reportDupes' => false ], + CACHE_DB => [ 'class' => SqlBagOStuff::class, 'loggroup' => 'SQLBagOStuff' ], + + CACHE_ANYTHING => [ 'factory' => 'ObjectCache::newAnything' ], + CACHE_ACCEL => [ 'factory' => 'ObjectCache::getLocalServerInstance' ], + CACHE_MEMCACHED => [ 'class' => MemcachedPhpBagOStuff::class, 'loggroup' => 'memcached' ], + + 'db-replicated' => [ + 'class' => ReplicatedBagOStuff::class, + 'readFactory' => [ + 'class' => SqlBagOStuff::class, + 'args' => [ [ 'slaveOnly' => true ] ] + ], + 'writeFactory' => [ + 'class' => SqlBagOStuff::class, + 'args' => [ [ 'slaveOnly' => false ] ] + ], + 'loggroup' => 'SQLBagOStuff', + 'reportDupes' => false + ], + + 'apc' => [ 'class' => APCBagOStuff::class, 'reportDupes' => false ], + 'apcu' => [ 'class' => APCUBagOStuff::class, 'reportDupes' => false ], + 'wincache' => [ 'class' => WinCacheBagOStuff::class, 'reportDupes' => false ], + 'memcached-php' => [ 'class' => MemcachedPhpBagOStuff::class, 'loggroup' => 'memcached' ], + 'memcached-pecl' => [ 'class' => MemcachedPeclBagOStuff::class, 'loggroup' => 'memcached' ], + 'hash' => [ 'class' => HashBagOStuff::class, 'reportDupes' => false ], +]; + +/** + * Main Wide-Area-Network cache type. This should be a cache with fast access, + * but it may have limited space. By default, it is disabled, since the basic stock + * cache is not fast enough to make it worthwhile. For single data-center setups, this can + * simply be pointed to a cache in $wgWANObjectCaches that uses a local $wgObjectCaches + * cache with a relayer of type EventRelayerNull. + * + * The options are: + * - false: Configure the cache using $wgMainCacheType, without using + * a relayer (only matters if there are multiple data-centers) + * - CACHE_NONE: Do not cache + * - (other): A string may be used which identifies a cache + * configuration in $wgWANObjectCaches + * @since 1.26 + */ +$wgMainWANCache = false; + +/** + * Advanced WAN object cache configuration. + * + * Each WAN cache wraps a registered object cache (for the local cluster) + * and it must also be configured to point to a PubSub instance. Subscribers + * must be configured to relay purges to the actual cache servers. + * + * The format is an associative array where the key is a cache identifier, and + * the value is an associative array of parameters. The "cacheId" parameter is + * a cache identifier from $wgObjectCaches. The "channels" parameter is a map of + * actions ('purge') to PubSub channels defined in $wgEventRelayerConfig. + * The "loggroup" parameter controls where log events are sent. + * + * @since 1.26 + */ +$wgWANObjectCaches = [ + CACHE_NONE => [ + 'class' => WANObjectCache::class, + 'cacheId' => CACHE_NONE, + 'channels' => [] + ] + /* Example of a simple single data-center cache: + 'memcached-php' => [ + 'class' => WANObjectCache::class, + 'cacheId' => 'memcached-php', + 'channels' => [ 'purge' => 'wancache-main-memcached-purge' ] + ] + */ +]; + +/** + * Verify and enforce WAN cache purges using reliable DB sources as streams. + * + * These secondary cache purges are de-duplicated via simple cache mutexes. + * This improves consistency when cache purges are lost, which becomes more likely + * as more cache servers are added or if there are multiple datacenters. Only keys + * related to important mutable content will be checked. + * + * @var bool + * @since 1.29 + */ +$wgEnableWANCacheReaper = false; + +/** + * Main object stash type. This should be a fast storage system for storing + * lightweight data like hit counters and user activity. Sites with multiple + * data-centers should have this use a store that replicates all writes. The + * store should have enough consistency for CAS operations to be usable. + * Reads outside of those needed for merge() may be eventually consistent. + * + * The options are: + * - db: Store cache objects in the DB + * - (other): A string may be used which identifies a cache + * configuration in $wgObjectCaches + * + * @since 1.26 + */ +$wgMainStash = 'db-replicated'; + +/** + * The expiry time for the parser cache, in seconds. + * The default is 86400 (one day). + */ +$wgParserCacheExpireTime = 86400; + +/** + * @deprecated since 1.27, session data is always stored in object cache. + */ +$wgSessionsInObjectCache = true; + +/** + * The expiry time to use for session storage, in seconds. + */ +$wgObjectCacheSessionExpiry = 3600; + +/** + * @deprecated since 1.27, MediaWiki\Session\SessionManager doesn't use PHP session storage. + */ +$wgSessionHandler = null; + +/** + * Whether to use PHP session handling ($_SESSION and session_*() functions) + * + * If the constant MW_NO_SESSION is defined, this is forced to 'disable'. + * + * If the constant MW_NO_SESSION_HANDLER is defined, this is ignored and PHP + * session handling will function independently of SessionHandler. + * SessionHandler and PHP's session handling may attempt to override each + * others' cookies. + * + * @since 1.27 + * @var string + * - 'enable': Integrate with PHP's session handling as much as possible. + * - 'warn': Integrate but log warnings if anything changes $_SESSION. + * - 'disable': Throw exceptions if PHP session handling is used. + */ +$wgPHPSessionHandling = 'enable'; + +/** + * Number of internal PBKDF2 iterations to use when deriving session secrets. + * + * @since 1.28 + */ +$wgSessionPbkdf2Iterations = 10001; + +/** + * If enabled, will send MemCached debugging information to $wgDebugLogFile + */ +$wgMemCachedDebug = false; + +/** + * The list of MemCached servers and port numbers + */ +$wgMemCachedServers = [ '127.0.0.1:11211' ]; + +/** + * Use persistent connections to MemCached, which are shared across multiple + * requests. + */ +$wgMemCachedPersistent = false; + +/** + * Read/write timeout for MemCached server communication, in microseconds. + */ +$wgMemCachedTimeout = 500000; + +/** + * Set this to true to maintain a copy of the message cache on the local server. + * + * This layer of message cache is in addition to the one configured by $wgMessageCacheType. + * + * The local copy is put in APC. If APC is not installed, this setting does nothing. + * + * Note that this is about the message cache, which stores interface messages + * maintained as wiki pages. This is separate from the localisation cache for interface + * messages provided by the software, which is configured by $wgLocalisationCacheConf. + */ +$wgUseLocalMessageCache = false; + +/** + * Instead of caching everything, only cache those messages which have + * been customised in the site content language. This means that + * MediaWiki:Foo/ja is ignored if MediaWiki:Foo doesn't exist. + * This option is probably only useful for translatewiki.net. + */ +$wgAdaptiveMessageCache = false; + +/** + * Localisation cache configuration. Associative array with keys: + * class: The class to use. May be overridden by extensions. + * + * store: The location to store cache data. May be 'files', 'array', 'db' or + * 'detect'. If set to "files", data will be in CDB files. If set + * to "db", data will be stored to the database. If set to + * "detect", files will be used if $wgCacheDirectory is set, + * otherwise the database will be used. + * "array" is an experimental option that uses PHP files that + * store static arrays. + * + * storeClass: The class name for the underlying storage. If set to a class + * name, it overrides the "store" setting. + * + * storeDirectory: If the store class puts its data in files, this is the + * directory it will use. If this is false, $wgCacheDirectory + * will be used. + * + * manualRecache: Set this to true to disable cache updates on web requests. + * Use maintenance/rebuildLocalisationCache.php instead. + */ +$wgLocalisationCacheConf = [ + 'class' => LocalisationCache::class, + 'store' => 'detect', + 'storeClass' => false, + 'storeDirectory' => false, + 'manualRecache' => false, +]; + +/** + * Allow client-side caching of pages + */ +$wgCachePages = true; + +/** + * Set this to current time to invalidate all prior cached pages. Affects both + * client-side and server-side caching. + * You can get the current date on your server by using the command: + * @verbatim + * date +%Y%m%d%H%M%S + * @endverbatim + */ +$wgCacheEpoch = '20030516000000'; + +/** + * Directory where GitInfo will look for pre-computed cache files. If false, + * $wgCacheDirectory/gitinfo will be used. + */ +$wgGitInfoCacheDirectory = false; + +/** + * Bump this number when changing the global style sheets and JavaScript. + * + * It should be appended in the query string of static CSS and JS includes, + * to ensure that client-side caches do not keep obsolete copies of global + * styles. + * + * @deprecated since 1.31 + */ +$wgStyleVersion = '303'; + +/** + * This will cache static pages for non-logged-in users to reduce + * database traffic on public sites. ResourceLoader requests to default + * language and skins are cached as well as single module requests. + */ +$wgUseFileCache = false; + +/** + * Depth of the subdirectory hierarchy to be created under + * $wgFileCacheDirectory. The subdirectories will be named based on + * the MD5 hash of the title. A value of 0 means all cache files will + * be put directly into the main file cache directory. + */ +$wgFileCacheDepth = 2; + +/** + * Kept for extension compatibility; see $wgParserCacheType + * @deprecated since 1.26 + */ +$wgEnableParserCache = true; + +/** + * Append a configured value to the parser cache and the sitenotice key so + * that they can be kept separate for some class of activity. + */ +$wgRenderHashAppend = ''; + +/** + * If on, the sidebar navigation links are cached for users with the + * current language set. This can save a touch of load on a busy site + * by shaving off extra message lookups. + * + * However it is also fragile: changing the site configuration, or + * having a variable $wgArticlePath, can produce broken links that + * don't update as expected. + */ +$wgEnableSidebarCache = false; + +/** + * Expiry time for the sidebar cache, in seconds + */ +$wgSidebarCacheExpiry = 86400; + +/** + * When using the file cache, we can store the cached HTML gzipped to save disk + * space. Pages will then also be served compressed to clients that support it. + * + * Requires zlib support enabled in PHP. + */ +$wgUseGzip = false; + +/** + * Clock skew or the one-second resolution of time() can occasionally cause cache + * problems when the user requests two pages within a short period of time. This + * variable adds a given number of seconds to vulnerable timestamps, thereby giving + * a grace period. + */ +$wgClockSkewFudge = 5; + +/** + * Invalidate various caches when LocalSettings.php changes. This is equivalent + * to setting $wgCacheEpoch to the modification time of LocalSettings.php, as + * was previously done in the default LocalSettings.php file. + * + * On high-traffic wikis, this should be set to false, to avoid the need to + * check the file modification time, and to avoid the performance impact of + * unnecessary cache invalidations. + */ +$wgInvalidateCacheOnLocalSettingsChange = true; + +/** + * When loading extensions through the extension registration system, this + * can be used to invalidate the cache. A good idea would be to set this to + * one file, you can just `touch` that one to invalidate the cache + * + * @par Example: + * @code + * $wgExtensionInfoMtime = filemtime( "$IP/LocalSettings.php" ); + * @endcode + * + * If set to false, the mtime for each individual JSON file will be checked, + * which can be slow if a large number of extensions are being loaded. + * + * @var int|bool + */ +$wgExtensionInfoMTime = false; + +/** @} */ # end of cache settings + +/************************************************************************//** + * @name HTTP proxy (CDN) settings + * + * Many of these settings apply to any HTTP proxy used in front of MediaWiki, + * although they are referred to as Squid settings for historical reasons. + * + * Achieving a high hit ratio with an HTTP proxy requires special + * configuration. See https://www.mediawiki.org/wiki/Manual:Squid_caching for + * more details. + * + * @{ + */ + +/** + * Enable/disable CDN. + * See https://www.mediawiki.org/wiki/Manual:Squid_caching + */ +$wgUseSquid = false; + +/** + * If you run Squid3 with ESI support, enable this (default:false): + */ +$wgUseESI = false; + +/** + * Send the Key HTTP header for better caching. + * See https://datatracker.ietf.org/doc/draft-fielding-http-key/ for details. + * @since 1.27 + */ +$wgUseKeyHeader = false; + +/** + * Add X-Forwarded-Proto to the Vary and Key headers for API requests and + * RSS/Atom feeds. Use this if you have an SSL termination setup + * and need to split the cache between HTTP and HTTPS for API requests, + * feed requests and HTTP redirect responses in order to prevent cache + * pollution. This does not affect 'normal' requests to index.php other than + * HTTP redirects. + */ +$wgVaryOnXFP = false; + +/** + * Internal server name as known to CDN, if different. + * + * @par Example: + * @code + * $wgInternalServer = 'http://yourinternal.tld:8000'; + * @endcode + */ +$wgInternalServer = false; + +/** + * Cache TTL for the CDN sent as s-maxage (without ESI) or + * Surrogate-Control (with ESI). Without ESI, you should strip + * out s-maxage in the CDN config. + * + * 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days. + */ +$wgSquidMaxage = 18000; + +/** + * Cache timeout for the CDN when DB replica DB lag is high + * @see $wgSquidMaxage + * @since 1.27 + */ +$wgCdnMaxageLagged = 30; + +/** + * If set, any SquidPurge call on a URL or URLs will send a second purge no less than + * this many seconds later via the job queue. This requires delayed job support. + * This should be safely higher than the 'max lag' value in $wgLBFactoryConf, so that + * replica DB lag does not cause page to be stuck in stales states in CDN. + * + * This also fixes race conditions in two-tiered CDN setups (e.g. cdn2 => cdn1 => MediaWiki). + * If a purge for a URL reaches cdn2 before cdn1 and a request reaches cdn2 for that URL, + * it will populate the response from the stale cdn1 value. When cdn1 gets the purge, cdn2 + * will still be stale. If the rebound purge delay is safely higher than the time to relay + * a purge to all nodes, then the rebound puge will clear cdn2 after cdn1 was cleared. + * + * @since 1.27 + */ +$wgCdnReboundPurgeDelay = 0; + +/** + * Cache timeout for the CDN when a response is known to be wrong or incomplete (due to load) + * @see $wgSquidMaxage + * @since 1.27 + */ +$wgCdnMaxageSubstitute = 60; + +/** + * Default maximum age for raw CSS/JS accesses + * + * 300 seconds = 5 minutes. + */ +$wgForcedRawSMaxage = 300; + +/** + * List of proxy servers to purge on changes; default port is 80. Use IP addresses. + * + * When MediaWiki is running behind a proxy, it will trust X-Forwarded-For + * headers sent/modified from these proxies when obtaining the remote IP address + * + * For a list of trusted servers which *aren't* purged, see $wgSquidServersNoPurge. + */ +$wgSquidServers = []; + +/** + * As above, except these servers aren't purged on page changes; use to set a + * list of trusted proxies, etc. Supports both individual IP addresses and + * CIDR blocks. + * @since 1.23 Supports CIDR ranges + */ +$wgSquidServersNoPurge = []; + +/** + * Whether to use a Host header in purge requests sent to the proxy servers + * configured in $wgSquidServers. Set this to false to support Squid + * configured in forward-proxy mode. + * + * If this is set to true, a Host header will be sent, and only the path + * component of the URL will appear on the request line, as if the request + * were a non-proxy HTTP 1.1 request. Varnish only supports this style of + * request. Squid supports this style of request only if reverse-proxy mode + * (http_port ... accel) is enabled. + * + * If this is set to false, no Host header will be sent, and the absolute URL + * will be sent in the request line, as is the standard for an HTTP proxy + * request in both HTTP 1.0 and 1.1. This style of request is not supported + * by Varnish, but is supported by Squid in either configuration (forward or + * reverse). + * + * @since 1.21 + */ +$wgSquidPurgeUseHostHeader = true; + +/** + * Routing configuration for HTCP multicast purging. Add elements here to + * enable HTCP and determine which purges are sent where. If set to an empty + * array, HTCP is disabled. + * + * Each key in this array is a regular expression to match against the purged + * URL, or an empty string to match all URLs. The purged URL is matched against + * the regexes in the order specified, and the first rule whose regex matches + * is used, all remaining rules will thus be ignored. + * + * @par Example configuration to send purges for upload.wikimedia.org to one + * multicast group and all other purges to another: + * @code + * $wgHTCPRouting = [ + * '|^https?://upload\.wikimedia\.org|' => [ + * 'host' => '239.128.0.113', + * 'port' => 4827, + * ], + * '' => [ + * 'host' => '239.128.0.112', + * 'port' => 4827, + * ], + * ]; + * @endcode + * + * You can also pass an array of hosts to send purges too. This is useful when + * you have several multicast groups or unicast address that should receive a + * given purge. Multiple hosts support was introduced in MediaWiki 1.22. + * + * @par Example of sending purges to multiple hosts: + * @code + * $wgHTCPRouting = [ + * '' => [ + * // Purges to text caches using multicast + * [ 'host' => '239.128.0.114', 'port' => '4827' ], + * // Purges to a hardcoded list of caches + * [ 'host' => '10.88.66.1', 'port' => '4827' ], + * [ 'host' => '10.88.66.2', 'port' => '4827' ], + * [ 'host' => '10.88.66.3', 'port' => '4827' ], + * ], + * ]; + * @endcode + * + * @since 1.22 + * + * $wgHTCPRouting replaces $wgHTCPMulticastRouting that was introduced in 1.20. + * For back compatibility purposes, whenever its array is empty + * $wgHTCPMutlicastRouting will be used as a fallback if it not null. + * + * @see $wgHTCPMulticastTTL + */ +$wgHTCPRouting = []; + +/** + * HTCP multicast TTL. + * @see $wgHTCPRouting + */ +$wgHTCPMulticastTTL = 1; + +/** + * Should forwarded Private IPs be accepted? + */ +$wgUsePrivateIPs = false; + +/** @} */ # end of HTTP proxy settings + +/************************************************************************//** + * @name Language, regional and character encoding settings + * @{ + */ + +/** + * Site language code. See languages/data/Names.php for languages supported by + * MediaWiki out of the box. Not all languages listed there have translations, + * see languages/messages/ for the list of languages with some localisation. + * + * Warning: Don't use any of MediaWiki's deprecated language codes listed in + * LanguageCode::getDeprecatedCodeMapping or $wgDummyLanguageCodes, like "no" + * for Norwegian (use "nb" instead). If you do, things will break unexpectedly. + * + * This defines the default interface language for all users, but users can + * change it in their preferences. + * + * This also defines the language of pages in the wiki. The content is wrapped + * in a html element with lang=XX attribute. This behavior can be overridden + * via hooks, see Title::getPageLanguage. + */ +$wgLanguageCode = 'en'; + +/** + * Language cache size, or really how many languages can we handle + * simultaneously without degrading to crawl speed. + */ +$wgLangObjCacheSize = 10; + +/** + * Some languages need different word forms, usually for different cases. + * Used in Language::convertGrammar(). + * + * @par Example: + * @code + * $wgGrammarForms['en']['genitive']['car'] = 'car\'s'; + * @endcode + */ +$wgGrammarForms = []; + +/** + * Treat language links as magic connectors, not inline links + */ +$wgInterwikiMagic = true; + +/** + * Hide interlanguage links from the sidebar + */ +$wgHideInterlanguageLinks = false; + +/** + * List of additional interwiki prefixes that should be treated as + * interlanguage links (i.e. placed in the sidebar). + * Notes: + * - This will not do anything unless the prefixes are defined in the interwiki + * map. + * - The display text for these custom interlanguage links will be fetched from + * the system message "interlanguage-link-xyz" where xyz is the prefix in + * this array. + * - A friendly name for each site, used for tooltip text, may optionally be + * placed in the system message "interlanguage-link-sitename-xyz" where xyz is + * the prefix in this array. + */ +$wgExtraInterlanguageLinkPrefixes = []; + +/** + * List of language names or overrides for default names in Names.php + */ +$wgExtraLanguageNames = []; + +/** + * List of mappings from one language code to another. + * This array makes the codes not appear as a selectable language on the + * installer, and excludes them when running the transstat.php script. + * + * In Setup.php, the variable $wgDummyLanguageCodes is created by combining + * these codes with a list of "deprecated" codes, which are mostly leftovers + * from renames or other legacy things, and the internal codes 'qqq' and 'qqx'. + * If a mapping in $wgExtraLanguageCodes collide with a built-in mapping, the + * value in $wgExtraLanguageCodes will be used. + * + * @since 1.29 + */ +$wgExtraLanguageCodes = [ + 'bh' => 'bho', // Bihari language family + 'no' => 'nb', // Norwegian language family + 'simple' => 'en', // Simple English +]; + +/** + * Functionally the same as $wgExtraLanguageCodes, but deprecated. Instead of + * appending values to this array, append them to $wgExtraLanguageCodes. + * + * @deprecated since 1.29 + */ +$wgDummyLanguageCodes = []; + +/** + * Set this to true to replace Arabic presentation forms with their standard + * forms in the U+0600-U+06FF block. This only works if $wgLanguageCode is + * set to "ar". + * + * Note that pages with titles containing presentation forms will become + * inaccessible, run maintenance/cleanupTitles.php to fix this. + */ +$wgFixArabicUnicode = true; + +/** + * Set this to true to replace ZWJ-based chillu sequences in Malayalam text + * with their Unicode 5.1 equivalents. This only works if $wgLanguageCode is + * set to "ml". Note that some clients (even new clients as of 2010) do not + * support these characters. + * + * If you enable this on an existing wiki, run maintenance/cleanupTitles.php to + * fix any ZWJ sequences in existing page titles. + */ +$wgFixMalayalamUnicode = true; + +/** + * Set this to always convert certain Unicode sequences to modern ones + * regardless of the content language. This has a small performance + * impact. + * + * See $wgFixArabicUnicode and $wgFixMalayalamUnicode for conversion + * details. + * + * @since 1.17 + */ +$wgAllUnicodeFixes = false; + +/** + * Set this to eg 'ISO-8859-1' to perform character set conversion when + * loading old revisions not marked with "utf-8" flag. Use this when + * converting a wiki from MediaWiki 1.4 or earlier to UTF-8 without the + * burdensome mass conversion of old text data. + * + * @note This DOES NOT touch any fields other than old_text. Titles, comments, + * user names, etc still must be converted en masse in the database before + * continuing as a UTF-8 wiki. + */ +$wgLegacyEncoding = false; + +/** + * @deprecated since 1.30, does nothing + */ +$wgBrowserBlackList = []; + +/** + * If set to true, the MediaWiki 1.4 to 1.5 schema conversion will + * create stub reference rows in the text table instead of copying + * the full text of all current entries from 'cur' to 'text'. + * + * This will speed up the conversion step for large sites, but + * requires that the cur table be kept around for those revisions + * to remain viewable. + * + * This option affects the updaters *only*. Any present cur stub + * revisions will be readable at runtime regardless of this setting. + */ +$wgLegacySchemaConversion = false; + +/** + * Enable dates like 'May 12' instead of '12 May', if the default date format + * is 'dmy or mdy'. + */ +$wgAmericanDates = false; + +/** + * For Hindi and Arabic use local numerals instead of Western style (0-9) + * numerals in interface. + */ +$wgTranslateNumerals = true; + +/** + * Translation using MediaWiki: namespace. + * Interface messages will be loaded from the database. + */ +$wgUseDatabaseMessages = true; + +/** + * Expiry time for the message cache key + */ +$wgMsgCacheExpiry = 86400; + +/** + * Maximum entry size in the message cache, in bytes + */ +$wgMaxMsgCacheEntrySize = 10000; + +/** + * Whether to enable language variant conversion. + */ +$wgDisableLangConversion = false; + +/** + * Whether to enable language variant conversion for links. + */ +$wgDisableTitleConversion = false; + +/** + * Default variant code, if false, the default will be the language code + */ +$wgDefaultLanguageVariant = false; + +/** + * Whether to enable the pig latin variant of English (en-x-piglatin), + * used to ease variant development work. + */ +$wgUsePigLatinVariant = false; + +/** + * Disabled variants array of language variant conversion. + * + * @par Example: + * @code + * $wgDisabledVariants[] = 'zh-mo'; + * $wgDisabledVariants[] = 'zh-my'; + * @endcode + */ +$wgDisabledVariants = []; + +/** + * Like $wgArticlePath, but on multi-variant wikis, this provides a + * path format that describes which parts of the URL contain the + * language variant. + * + * @par Example: + * @code + * $wgLanguageCode = 'sr'; + * $wgVariantArticlePath = '/$2/$1'; + * $wgArticlePath = '/wiki/$1'; + * @endcode + * + * A link to /wiki/ would be redirected to /sr/Главна_страна + * + * It is important that $wgArticlePath not overlap with possible values + * of $wgVariantArticlePath. + */ +$wgVariantArticlePath = false; + +/** + * Show a bar of language selection links in the user login and user + * registration forms; edit the "loginlanguagelinks" message to + * customise these. + */ +$wgLoginLanguageSelector = false; + +/** + * When translating messages with wfMessage(), it is not always clear what + * should be considered UI messages and what should be content messages. + * + * For example, for the English Wikipedia, there should be only one 'mainpage', + * so when getting the link for 'mainpage', we should treat it as site content + * and call ->inContentLanguage()->text(), but for rendering the text of the + * link, we call ->text(). The code behaves this way by default. However, + * sites like the Wikimedia Commons do offer different versions of 'mainpage' + * and the like for different languages. This array provides a way to override + * the default behavior. + * + * @par Example: + * To allow language-specific main page and community + * portal: + * @code + * $wgForceUIMsgAsContentMsg = [ 'mainpage', 'portal-url' ]; + * @endcode + */ +$wgForceUIMsgAsContentMsg = []; + +/** + * Fake out the timezone that the server thinks it's in. This will be used for + * date display and not for what's stored in the DB. Leave to null to retain + * your server's OS-based timezone value. + * + * This variable is currently used only for signature formatting and for local + * time/date parser variables ({{LOCALTIME}} etc.) + * + * Timezones can be translated by editing MediaWiki messages of type + * timezone-nameinlowercase like timezone-utc. + * + * A list of usable timezones can found at: + * https://secure.php.net/manual/en/timezones.php + * + * @par Examples: + * @code + * $wgLocaltimezone = 'UTC'; + * $wgLocaltimezone = 'GMT'; + * $wgLocaltimezone = 'PST8PDT'; + * $wgLocaltimezone = 'Europe/Sweden'; + * $wgLocaltimezone = 'CET'; + * @endcode + */ +$wgLocaltimezone = null; + +/** + * Set an offset from UTC in minutes to use for the default timezone setting + * for anonymous users and new user accounts. + * + * This setting is used for most date/time displays in the software, and is + * overridable in user preferences. It is *not* used for signature timestamps. + * + * By default, this will be set to match $wgLocaltimezone. + */ +$wgLocalTZoffset = null; + +/** @} */ # End of language/charset settings + +/*************************************************************************//** + * @name Output format and skin settings + * @{ + */ + +/** + * The default Content-Type header. + */ +$wgMimeType = 'text/html'; + +/** + * Previously used as content type in HTML script tags. This is now ignored since + * HTML5 doesn't require a MIME type for script tags (javascript is the default). + * It was also previously used by RawAction to determine the ctype query parameter + * value that will result in a javascript response. + * @deprecated since 1.22 + */ +$wgJsMimeType = null; + +/** + * The default xmlns attribute. The option to define this has been removed. + * The value of this variable is no longer used by core and is set to a fixed + * value in Setup.php for compatibility with extensions that depend on the value + * of this variable being set. Such a dependency however is deprecated. + * @deprecated since 1.22 + */ +$wgXhtmlDefaultNamespace = null; + +/** + * Previously used to determine if we should output an HTML5 doctype. + * This is no longer used as we always output HTML5 now. For compatibility with + * extensions that still check the value of this config it's value is now forced + * to true by Setup.php. + * @deprecated since 1.22 + */ +$wgHtml5 = true; + +/** + * Defines the value of the version attribute in the <html> tag, if any. + * + * If your wiki uses RDFa, set it to the correct value for RDFa+HTML5. + * Correct current values are 'HTML+RDFa 1.0' or 'XHTML+RDFa 1.0'. + * See also https://www.w3.org/TR/rdfa-in-html/#document-conformance + * @since 1.16 + */ +$wgHtml5Version = null; + +/** + * Temporary variable that allows HTMLForms to be rendered as tables. + * Table based layouts cause various issues when designing for mobile. + * This global allows skins or extensions a means to force non-table based rendering. + * Setting to false forces form components to always render as div elements. + * @since 1.24 + */ +$wgHTMLFormAllowTableFormat = true; + +/** + * Temporary variable that applies MediaWiki UI wherever it can be supported. + * Temporary variable that should be removed when mediawiki ui is more + * stable and change has been communicated. + * @since 1.24 + */ +$wgUseMediaWikiUIEverywhere = false; + +/** + * Whether to label the store-to-database-and-show-to-others button in the editor + * as "Save page"/"Save changes" if false (the default) or, if true, instead as + * "Publish page"/"Publish changes". + * + * @since 1.28 + */ +$wgEditSubmitButtonLabelPublish = false; + +/** + * Permit other namespaces in addition to the w3.org default. + * + * Use the prefix for the key and the namespace for the value. + * + * @par Example: + * @code + * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; + * @endcode + * Normally we wouldn't have to define this in the root "<html>" + * element, but IE needs it there in some circumstances. + * + * This is ignored if $wgMimeType is set to a non-XML MIME type. + */ +$wgXhtmlNamespaces = []; + +/** + * Site notice shown at the top of each page + * + * MediaWiki:Sitenotice page, which will override this. You can also + * provide a separate message for logged-out users using the + * MediaWiki:Anonnotice page. + */ +$wgSiteNotice = ''; + +/** + * If this is set, a "donate" link will appear in the sidebar. Set it to a URL. + */ +$wgSiteSupportPage = ''; + +/** + * Default skin, for new users and anonymous visitors. Registered users may + * change this to any one of the other available skins in their preferences. + */ +$wgDefaultSkin = 'vector'; + +/** + * Fallback skin used when the skin defined by $wgDefaultSkin can't be found. + * + * @since 1.24 + */ +$wgFallbackSkin = 'fallback'; + +/** + * Specify the names of skins that should not be presented in the list of + * available skins in user preferences. If you want to remove a skin entirely, + * remove it from the skins/ directory and its entry from LocalSettings.php. + */ +$wgSkipSkins = []; + +/** + * @deprecated since 1.23; use $wgSkipSkins instead + */ +$wgSkipSkin = ''; + +/** + * Allow user Javascript page? + * This enables a lot of neat customizations, but may + * increase security risk to users and server load. + */ +$wgAllowUserJs = false; + +/** + * Allow user Cascading Style Sheets (CSS)? + * This enables a lot of neat customizations, but may + * increase security risk to users and server load. + */ +$wgAllowUserCss = false; + +/** + * Allow style-related user-preferences? + * + * This controls whether the `editfont` and `underline` preferences + * are availabe to users. + */ +$wgAllowUserCssPrefs = true; + +/** + * Use the site's Javascript page? + */ +$wgUseSiteJs = true; + +/** + * Use the site's Cascading Style Sheets (CSS)? + */ +$wgUseSiteCss = true; + +/** + * Break out of framesets. This can be used to prevent clickjacking attacks, + * or to prevent external sites from framing your site with ads. + */ +$wgBreakFrames = false; + +/** + * The X-Frame-Options header to send on pages sensitive to clickjacking + * attacks, such as edit pages. This prevents those pages from being displayed + * in a frame or iframe. The options are: + * + * - 'DENY': Do not allow framing. This is recommended for most wikis. + * + * - 'SAMEORIGIN': Allow framing by pages on the same domain. This can be used + * to allow framing within a trusted domain. This is insecure if there + * is a page on the same domain which allows framing of arbitrary URLs. + * + * - false: Allow all framing. This opens up the wiki to XSS attacks and thus + * full compromise of local user accounts. Private wikis behind a + * corporate firewall are especially vulnerable. This is not + * recommended. + * + * For extra safety, set $wgBreakFrames = true, to prevent framing on all pages, + * not just edit pages. + */ +$wgEditPageFrameOptions = 'DENY'; + +/** + * Disallow framing of API pages directly, by setting the X-Frame-Options + * header. Since the API returns CSRF tokens, allowing the results to be + * framed can compromise your user's account security. + * Options are: + * - 'DENY': Do not allow framing. This is recommended for most wikis. + * - 'SAMEORIGIN': Allow framing by pages on the same domain. + * - false: Allow all framing. + * Note: $wgBreakFrames will override this for human formatted API output. + */ +$wgApiFrameOptions = 'DENY'; + +/** + * Disable output compression (enabled by default if zlib is available) + */ +$wgDisableOutputCompression = false; + +/** + * Abandoned experiment with HTML5-style ID escaping. Normalized IDs a bit + * too aggressively, breaking preexisting content (particularly Cite). + * See T29733, T29694, T29474. + * + * @deprecated since 1.30, use $wgFragmentMode + */ +$wgExperimentalHtmlIds = false; + +/** + * How should section IDs be encoded? + * This array can contain 1 or 2 elements, each of them can be one of: + * - 'html5' is modern HTML5 style encoding with minimal escaping. Displays Unicode + * characters in most browsers' address bars. + * - 'legacy' is old MediaWiki-style encoding, e.g. 啤酒 turns into .E5.95.A4.E9.85.92 + * - 'html5-legacy' corresponds to DEPRECATED $wgExperimentalHtmlIds mode. DO NOT use + * it for anything but migration off that mode (see below). + * + * The first element of this array specifies the primary mode of escaping IDs. This + * is what users will see when they e.g. follow an [[#internal link]] to a section of + * a page. + * + * The optional second element defines a fallback mode, useful for migrations. + * If present, it will direct MediaWiki to add empty <span>s to every section with its + * id attribute set to fallback encoded title so that links using the previous encoding + * would still work. + * + * Example: you want to migrate your wiki from 'legacy' to 'html5' + * + * On the first step, set this variable to [ 'legacy', 'html5' ]. After a while, when + * all caches (parser, HTTP, etc.) contain only pages generated with this setting, + * flip the value to [ 'html5', 'legacy' ]. This will result in all internal links being + * generated in the new encoding while old links (both external and cached internal) will + * still work. After a long time, you might want to ditch backwards compatibility and + * set it to [ 'html5' ]. After all, pages get edited, breaking incoming links no matter which + * fragment mode is used. + * + * @since 1.30 + */ +$wgFragmentMode = [ 'legacy', 'html5' ]; + +/** + * Which ID escaping mode should be used for external interwiki links? See documentation + * for $wgFragmentMode above for details of each mode. Because you can't control external sites, + * this setting should probably always be 'legacy', unless every wiki you link to has converted + * to 'html5'. + * + * @since 1.30 + */ +$wgExternalInterwikiFragmentMode = 'legacy'; + +/** + * Abstract list of footer icons for skins in place of old copyrightico and poweredbyico code + * You can add new icons to the built in copyright or poweredby, or you can create + * a new block. Though note that you may need to add some custom css to get good styling + * of new blocks in monobook. vector and modern should work without any special css. + * + * $wgFooterIcons itself is a key/value array. + * The key is the name of a block that the icons will be wrapped in. The final id varies + * by skin; Monobook and Vector will turn poweredby into f-poweredbyico while Modern + * turns it into mw_poweredby. + * The value is either key/value array of icons or a string. + * In the key/value array the key may or may not be used by the skin but it can + * be used to find the icon and unset it or change the icon if needed. + * This is useful for disabling icons that are set by extensions. + * The value should be either a string or an array. If it is a string it will be output + * directly as html, however some skins may choose to ignore it. An array is the preferred format + * for the icon, the following keys are used: + * - src: An absolute url to the image to use for the icon, this is recommended + * but not required, however some skins will ignore icons without an image + * - srcset: optional additional-resolution images; see HTML5 specs + * - url: The url to use in the a element around the text or icon, if not set an a element will + * not be outputted + * - alt: This is the text form of the icon, it will be displayed without an image in + * skins like Modern or if src is not set, and will otherwise be used as + * the alt="" for the image. This key is required. + * - width and height: If the icon specified by src is not of the standard size + * you can specify the size of image to use with these keys. + * Otherwise they will default to the standard 88x31. + * @todo Reformat documentation. + */ +$wgFooterIcons = [ + "copyright" => [ + "copyright" => [], // placeholder for the built in copyright icon + ], + "poweredby" => [ + "mediawiki" => [ + // Defaults to point at + // "$wgResourceBasePath/resources/assets/poweredby_mediawiki_88x31.png" + // plus srcset for 1.5x, 2x resolution variants. + "src" => null, + "url" => "//www.mediawiki.org/", + "alt" => "Powered by MediaWiki", + ] + ], +]; + +/** + * Login / create account link behavior when it's possible for anonymous users + * to create an account. + * - true = use a combined login / create account link + * - false = split login and create account into two separate links + */ +$wgUseCombinedLoginLink = false; + +/** + * Display user edit counts in various prominent places. + */ +$wgEdititis = false; + +/** + * Some web hosts attempt to rewrite all responses with a 404 (not found) + * status code, mangling or hiding MediaWiki's output. If you are using such a + * host, you should start looking for a better one. While you're doing that, + * set this to false to convert some of MediaWiki's 404 responses to 200 so + * that the generated error pages can be seen. + * + * In cases where for technical reasons it is more important for MediaWiki to + * send the correct status code than for the body to be transmitted intact, + * this configuration variable is ignored. + */ +$wgSend404Code = true; + +/** + * The $wgShowRollbackEditCount variable is used to show how many edits can be rolled back. + * The numeric value of the variable controls how many edits MediaWiki will look back to + * determine whether a rollback is allowed (by checking that they are all from the same author). + * If the value is false or 0, the edits are not counted. Disabling this will prevent MediaWiki + * from hiding some useless rollback links. + * + * @since 1.20 + */ +$wgShowRollbackEditCount = 10; + +/** + * Output a <link rel="canonical"> tag on every page indicating the canonical + * server which should be used, i.e. $wgServer or $wgCanonicalServer. Since + * detection of the current server is unreliable, the link is sent + * unconditionally. + */ +$wgEnableCanonicalServerLink = false; + +/** + * When OutputHandler is used, mangle any output that contains + * <cross-domain-policy>. Without this, an attacker can send their own + * cross-domain policy unless it is prevented by the crossdomain.xml file at + * the domain root. + * + * @since 1.25 + */ +$wgMangleFlashPolicy = true; + +/** @} */ # End of output format settings } + +/*************************************************************************//** + * @name ResourceLoader settings + * @{ + */ + +/** + * Client-side resource modules. + * + * Extensions should add their ResourceLoader module definitions + * to the $wgResourceModules variable. + * + * @par Example: + * @code + * $wgResourceModules['ext.myExtension'] = [ + * 'scripts' => 'myExtension.js', + * 'styles' => 'myExtension.css', + * 'dependencies' => [ 'jquery.cookie', 'jquery.tabIndex' ], + * 'localBasePath' => __DIR__, + * 'remoteExtPath' => 'MyExtension', + * ]; + * @endcode + */ +$wgResourceModules = []; + +/** + * Skin-specific styles for resource modules. + * + * These are later added to the 'skinStyles' list of the existing module. The 'styles' list can + * not be modified or disabled. + * + * For example, here is a module "bar" and how skin Foo would provide additional styles for it. + * + * @par Example: + * @code + * $wgResourceModules['bar'] = [ + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/main.css', + * ]; + * + * $wgResourceModuleSkinStyles['foo'] = [ + * 'bar' => 'skins/Foo/bar.css', + * ]; + * @endcode + * + * This is mostly equivalent to: + * + * @par Equivalent: + * @code + * $wgResourceModules['bar'] = [ + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/main.css', + * 'skinStyles' => [ + * 'foo' => skins/Foo/bar.css', + * ], + * ]; + * @endcode + * + * If the module already defines its own entry in `skinStyles` for a given skin, then + * $wgResourceModuleSkinStyles is ignored. + * + * If a module defines a `skinStyles['default']` the skin may want to extend that instead + * of replacing them. This can be done using the `+` prefix. + * + * @par Example: + * @code + * $wgResourceModules['bar'] = [ + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/basic.css', + * 'skinStyles' => [ + * 'default' => 'resources/bar/additional.css', + * ], + * ]; + * // Note the '+' character: + * $wgResourceModuleSkinStyles['foo'] = [ + * '+bar' => 'skins/Foo/bar.css', + * ]; + * @endcode + * + * This is mostly equivalent to: + * + * @par Equivalent: + * @code + * $wgResourceModules['bar'] = [ + * 'scripts' => 'resources/bar/bar.js', + * 'styles' => 'resources/bar/basic.css', + * 'skinStyles' => [ + * 'default' => 'resources/bar/additional.css', + * 'foo' => [ + * 'resources/bar/additional.css', + * 'skins/Foo/bar.css', + * ], + * ], + * ]; + * @endcode + * + * In other words, as a module author, use the `styles` list for stylesheets that may not be + * disabled by a skin. To provide default styles that may be extended or replaced, + * use `skinStyles['default']`. + * + * As with $wgResourceModules, paths default to being relative to the MediaWiki root. + * You should always provide a localBasePath and remoteBasePath (or remoteExtPath/remoteSkinPath). + * + * @par Example: + * @code + * $wgResourceModuleSkinStyles['foo'] = [ + * 'bar' => 'bar.css', + * 'quux' => 'quux.css', + * 'remoteSkinPath' => 'Foo', + * 'localBasePath' => __DIR__, + * ]; + * @endcode + */ +$wgResourceModuleSkinStyles = []; + +/** + * Extensions should register foreign module sources here. 'local' is a + * built-in source that is not in this array, but defined by + * ResourceLoader::__construct() so that it cannot be unset. + * + * @par Example: + * @code + * $wgResourceLoaderSources['foo'] = 'http://example.org/w/load.php'; + * @endcode + */ +$wgResourceLoaderSources = []; + +/** + * The default 'remoteBasePath' value for instances of ResourceLoaderFileModule. + * Defaults to $wgScriptPath. + */ +$wgResourceBasePath = null; + +/** + * Maximum time in seconds to cache resources served by ResourceLoader. + * Used to set last modified headers (max-age/s-maxage). + * + * Following options to distinguish: + * - versioned: Used for modules with a version, because changing version + * numbers causes cache misses. This normally has a long expiry time. + * - unversioned: Used for modules without a version to propagate changes + * quickly to clients. Also used for modules with errors to recover quickly. + * This normally has a short expiry time. + * + * Expiry time for the options to distinguish: + * - server: Squid/Varnish but also any other public proxy cache between the + * client and MediaWiki. + * - client: On the client side (e.g. in the browser cache). + */ +$wgResourceLoaderMaxage = [ + 'versioned' => [ + 'server' => 30 * 24 * 60 * 60, // 30 days + 'client' => 30 * 24 * 60 * 60, // 30 days + ], + 'unversioned' => [ + 'server' => 5 * 60, // 5 minutes + 'client' => 5 * 60, // 5 minutes + ], +]; + +/** + * The default debug mode (on/off) for of ResourceLoader requests. + * + * This will still be overridden when the debug URL parameter is used. + */ +$wgResourceLoaderDebug = false; + +/** + * Whether to ensure the mediawiki.legacy library is loaded before other modules. + * + * @deprecated since 1.26: Always declare dependencies. + */ +$wgIncludeLegacyJavaScript = false; + +/** + * Whether or not to assign configuration variables to the global window object. + * + * If this is set to false, old code using deprecated variables will no longer + * work. + * + * @par Example of legacy code: + * @code{,js} + * if ( window.wgRestrictionEdit ) { ... } + * @endcode + * or: + * @code{,js} + * if ( wgIsArticle ) { ... } + * @endcode + * + * Instead, one needs to use mw.config. + * @par Example using mw.config global configuration: + * @code{,js} + * if ( mw.config.exists('wgRestrictionEdit') ) { ... } + * @endcode + * or: + * @code{,js} + * if ( mw.config.get('wgIsArticle') ) { ... } + * @endcode + */ +$wgLegacyJavaScriptGlobals = true; + +/** + * If set to a positive number, ResourceLoader will not generate URLs whose + * query string is more than this many characters long, and will instead use + * multiple requests with shorter query strings. This degrades performance, + * but may be needed if your web server has a low (less than, say 1024) + * query string length limit or a low value for suhosin.get.max_value_length + * that you can't increase. + * + * If set to a negative number, ResourceLoader will assume there is no query + * string length limit. + * + * Defaults to a value based on php configuration. + */ +$wgResourceLoaderMaxQueryLength = false; + +/** + * If set to true, JavaScript modules loaded from wiki pages will be parsed + * prior to minification to validate it. + * + * Parse errors will result in a JS exception being thrown during module load, + * which avoids breaking other modules loaded in the same request. + */ +$wgResourceLoaderValidateJS = true; + +/** + * If set to true, statically-sourced (file-backed) JavaScript resources will + * be parsed for validity before being bundled up into ResourceLoader modules. + * + * This can be helpful for development by providing better error messages in + * default (non-debug) mode, but JavaScript parsing is slow and memory hungry + * and may fail on large pre-bundled frameworks. + */ +$wgResourceLoaderValidateStaticJS = false; + +/** + * Global LESS variables. An associative array binding variable names to + * LESS code snippets representing their values. + * + * Adding an item here is equivalent to writing `@variable: value;` + * at the beginning of all your .less files, with all the consequences. + * In particular, string values must be escaped and quoted. + * + * Changes to this configuration do NOT trigger cache invalidation. + * + * @par Example: + * @code + * $wgResourceLoaderLESSVars = [ + * 'exampleFontSize' => '1em', + * 'exampleBlue' => '#36c', + * ]; + * @endcode + * @since 1.22 + * @deprecated since 1.30 Use ResourceLoaderModule::getLessVars() instead to + * add variables to individual modules that need them. + */ +$wgResourceLoaderLESSVars = [ + /** + * Minimum available screen width at which a device can be considered a tablet + * The number is currently based on the device width of a Samsung Galaxy S5 mini and is low + * enough to cover iPad (768px). Number is prone to change with new information. + * @since 1.27 + * @deprecated 1.31 Use mediawiki.ui/variables instead + */ + 'deviceWidthTablet' => '720px', +]; + +/** + * Default import paths for LESS modules. LESS files referenced in @import + * statements will be looked up here first, and relative to the importing file + * second. To avoid collisions, it's important for the LESS files in these + * directories to have a common, predictable file name prefix. + * + * Extensions need not (and should not) register paths in + * $wgResourceLoaderLESSImportPaths. The import path includes the path of the + * currently compiling LESS file, which allows each extension to freely import + * files from its own tree. + * + * @since 1.22 + */ +$wgResourceLoaderLESSImportPaths = [ + "$IP/resources/src/mediawiki.less/", +]; + +/** + * Whether ResourceLoader should attempt to persist modules in localStorage on + * browsers that support the Web Storage API. + */ +$wgResourceLoaderStorageEnabled = true; + +/** + * Cache version for client-side ResourceLoader module storage. You can trigger + * invalidation of the contents of the module store by incrementing this value. + * + * @since 1.23 + */ +$wgResourceLoaderStorageVersion = 1; + +/** + * Whether to allow site-wide CSS (MediaWiki:Common.css and friends) on + * restricted pages like Special:UserLogin or Special:Preferences where + * JavaScript is disabled for security reasons. As it is possible to + * execute JavaScript through CSS, setting this to true opens up a + * potential security hole. Some sites may "skin" their wiki by using + * site-wide CSS, causing restricted pages to look unstyled and different + * from the rest of the site. + * + * @since 1.25 + */ +$wgAllowSiteCSSOnRestrictedPages = false; + +/** @} */ # End of ResourceLoader settings } + +/*************************************************************************//** + * @name Page title and interwiki link settings + * @{ + */ + +/** + * Name of the project namespace. If left set to false, $wgSitename will be + * used instead. + */ +$wgMetaNamespace = false; + +/** + * Name of the project talk namespace. + * + * Normally you can ignore this and it will be something like + * $wgMetaNamespace . "_talk". In some languages, you may want to set this + * manually for grammatical reasons. + */ +$wgMetaNamespaceTalk = false; + +/** + * Additional namespaces. If the namespaces defined in Language.php and + * Namespace.php are insufficient, you can create new ones here, for example, + * to import Help files in other languages. You can also override the namespace + * names of existing namespaces. Extensions should use the CanonicalNamespaces + * hook or extension.json. + * + * @warning Once you delete a namespace, the pages in that namespace will + * no longer be accessible. If you rename it, then you can access them through + * the new namespace name. + * + * Custom namespaces should start at 100 to avoid conflicting with standard + * namespaces, and should always follow the even/odd main/talk pattern. + * + * @par Example: + * @code + * $wgExtraNamespaces = [ + * 100 => "Hilfe", + * 101 => "Hilfe_Diskussion", + * 102 => "Aide", + * 103 => "Discussion_Aide" + * ]; + * @endcode + * + * @todo Add a note about maintenance/namespaceDupes.php + */ +$wgExtraNamespaces = []; + +/** + * Same as above, but for namespaces with gender distinction. + * Note: the default form for the namespace should also be set + * using $wgExtraNamespaces for the same index. + * @since 1.18 + */ +$wgExtraGenderNamespaces = []; + +/** + * Namespace aliases. + * + * These are alternate names for the primary localised namespace names, which + * are defined by $wgExtraNamespaces and the language file. If a page is + * requested with such a prefix, the request will be redirected to the primary + * name. + * + * Set this to a map from namespace names to IDs. + * + * @par Example: + * @code + * $wgNamespaceAliases = [ + * 'Wikipedian' => NS_USER, + * 'Help' => 100, + * ]; + * @endcode + */ +$wgNamespaceAliases = []; + +/** + * Allowed title characters -- regex character class + * Don't change this unless you know what you're doing + * + * Problematic punctuation: + * - []{}|# Are needed for link syntax, never enable these + * - <> Causes problems with HTML escaping, don't use + * - % Enabled by default, minor problems with path to query rewrite rules, see below + * - + Enabled by default, but doesn't work with path to query rewrite rules, + * corrupted by apache + * - ? Enabled by default, but doesn't work with path to PATH_INFO rewrites + * + * All three of these punctuation problems can be avoided by using an alias, + * instead of a rewrite rule of either variety. + * + * The problem with % is that when using a path to query rewrite rule, URLs are + * double-unescaped: once by Apache's path conversion code, and again by PHP. So + * %253F, for example, becomes "?". Our code does not double-escape to compensate + * for this, indeed double escaping would break if the double-escaped title was + * passed in the query string rather than the path. This is a minor security issue + * because articles can be created such that they are hard to view or edit. + * + * In some rare cases you may wish to remove + for compatibility with old links. + */ +$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+"; + +/** + * The interwiki prefix of the current wiki, or false if it doesn't have one. + * + * @deprecated since 1.23; use $wgLocalInterwikis instead + */ +$wgLocalInterwiki = false; + +/** + * Array for multiple $wgLocalInterwiki values, in case there are several + * interwiki prefixes that point to the current wiki. If $wgLocalInterwiki is + * set, its value is prepended to this array, for backwards compatibility. + * + * Note, recent changes feeds use only the first entry in this array (or + * $wgLocalInterwiki, if it is set). See $wgRCFeeds + */ +$wgLocalInterwikis = []; + +/** + * Expiry time for cache of interwiki table + */ +$wgInterwikiExpiry = 10800; + +/** + * @name Interwiki caching settings. + * @{ + */ + +/** + * Interwiki cache, either as an associative array or a path to a constant + * database (.cdb) file. + * + * This data structure database is generated by the `dumpInterwiki` maintenance + * script (which lives in the WikimediaMaintenance repository) and has key + * formats such as the following: + * + * - dbname:key - a simple key (e.g. enwiki:meta) + * - _sitename:key - site-scope key (e.g. wiktionary:meta) + * - __global:key - global-scope key (e.g. __global:meta) + * - __sites:dbname - site mapping (e.g. __sites:enwiki) + * + * Sites mapping just specifies site name, other keys provide "local url" + * data layout. + * + * @var bool|array|string + */ +$wgInterwikiCache = false; + +/** + * Specify number of domains to check for messages. + * - 1: Just wiki(db)-level + * - 2: wiki and global levels + * - 3: site levels + */ +$wgInterwikiScopes = 3; + +/** + * Fallback site, if unable to resolve from cache + */ +$wgInterwikiFallbackSite = 'wiki'; + +/** @} */ # end of Interwiki caching settings. + +/** + * @name SiteStore caching settings. + * @{ + */ + +/** + * Specify the file location for the Sites json cache file. + */ +$wgSitesCacheFile = false; + +/** @} */ # end of SiteStore caching settings. + +/** + * If local interwikis are set up which allow redirects, + * set this regexp to restrict URLs which will be displayed + * as 'redirected from' links. + * + * @par Example: + * It might look something like this: + * @code + * $wgRedirectSources = '!^https?://[a-z-]+\.wikipedia\.org/!'; + * @endcode + * + * Leave at false to avoid displaying any incoming redirect markers. + * This does not affect intra-wiki redirects, which don't change + * the URL. + */ +$wgRedirectSources = false; + +/** + * Set this to false to avoid forcing the first letter of links to capitals. + * + * @warning may break links! This makes links COMPLETELY case-sensitive. Links + * appearing with a capital at the beginning of a sentence will *not* go to the + * same place as links in the middle of a sentence using a lowercase initial. + */ +$wgCapitalLinks = true; + +/** + * @since 1.16 - This can now be set per-namespace. Some special namespaces (such + * as Special, see MWNamespace::$alwaysCapitalizedNamespaces for the full list) must be + * true by default (and setting them has no effect), due to various things that + * require them to be so. Also, since Talk namespaces need to directly mirror their + * associated content namespaces, the values for those are ignored in favor of the + * subject namespace's setting. Setting for NS_MEDIA is taken automatically from + * NS_FILE. + * + * @par Example: + * @code + * $wgCapitalLinkOverrides[ NS_FILE ] = false; + * @endcode + */ +$wgCapitalLinkOverrides = []; + +/** + * Which namespaces should support subpages? + * See Language.php for a list of namespaces. + */ +$wgNamespacesWithSubpages = [ + NS_TALK => true, + NS_USER => true, + NS_USER_TALK => true, + NS_PROJECT => true, + NS_PROJECT_TALK => true, + NS_FILE_TALK => true, + NS_MEDIAWIKI => true, + NS_MEDIAWIKI_TALK => true, + NS_TEMPLATE => true, + NS_TEMPLATE_TALK => true, + NS_HELP => true, + NS_HELP_TALK => true, + NS_CATEGORY_TALK => true +]; + +/** + * Array holding default tracking category names. + * + * Array contains the system messages for each tracking category. + * Tracking categories allow pages with certain characteristics to be tracked. + * It works by adding any such page to a category automatically. + * + * A message with the suffix '-desc' should be added as a description message + * to have extra information on Special:TrackingCategories. + * + * @deprecated since 1.25 Extensions should now register tracking categories using + * the new extension registration system. + * + * @since 1.23 + */ +$wgTrackingCategories = []; + +/** + * Array of namespaces which can be deemed to contain valid "content", as far + * as the site statistics are concerned. Useful if additional namespaces also + * contain "content" which should be considered when generating a count of the + * number of articles in the wiki. + */ +$wgContentNamespaces = [ NS_MAIN ]; + +/** + * Optional array of namespaces which should be blacklisted from Special:ShortPages + * Only pages inside $wgContentNamespaces but not $wgShortPagesNamespaceBlacklist will + * be shown on that page. + * @since 1.30 + */ +$wgShortPagesNamespaceBlacklist = []; + +/** + * Array of namespaces, in addition to the talk namespaces, where signatures + * (~~~~) are likely to be used. This determines whether to display the + * Signature button on the edit toolbar, and may also be used by extensions. + * For example, "traditional" style wikis, where content and discussion are + * intermixed, could place NS_MAIN and NS_PROJECT namespaces in this array. + */ +$wgExtraSignatureNamespaces = []; + +/** + * Max number of redirects to follow when resolving redirects. + * 1 means only the first redirect is followed (default behavior). + * 0 or less means no redirects are followed. + */ +$wgMaxRedirects = 1; + +/** + * Array of invalid page redirect targets. + * Attempting to create a redirect to any of the pages in this array + * will make the redirect fail. + * Userlogout is hard-coded, so it does not need to be listed here. + * (T12569) Disallow Mypage and Mytalk as well. + * + * As of now, this only checks special pages. Redirects to pages in + * other namespaces cannot be invalidated by this variable. + */ +$wgInvalidRedirectTargets = [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect' ]; + +/** @} */ # End of title and interwiki settings } + +/************************************************************************//** + * @name Parser settings + * These settings configure the transformation from wikitext to HTML. + * @{ + */ + +/** + * Parser configuration. Associative array with the following members: + * + * class The class name + * + * preprocessorClass The preprocessor class. Two classes are currently available: + * Preprocessor_Hash, which uses plain PHP arrays for temporary + * storage, and Preprocessor_DOM, which uses the DOM module for + * temporary storage. Preprocessor_DOM generally uses less memory; + * the speed of the two is roughly the same. + * + * If this parameter is not given, it uses Preprocessor_DOM if the + * DOM module is available, otherwise it uses Preprocessor_Hash. + * + * The entire associative array will be passed through to the constructor as + * the first parameter. Note that only Setup.php can use this variable -- + * the configuration will change at runtime via $wgParser member functions, so + * the contents of this variable will be out-of-date. The variable can only be + * changed during LocalSettings.php, in particular, it can't be changed during + * an extension setup function. + */ +$wgParserConf = [ + 'class' => Parser::class, + # 'preprocessorClass' => Preprocessor_Hash::class, +]; + +/** + * Maximum indent level of toc. + */ +$wgMaxTocLevel = 999; + +/** + * A complexity limit on template expansion: the maximum number of nodes visited + * by PPFrame::expand() + */ +$wgMaxPPNodeCount = 1000000; + +/** + * A complexity limit on template expansion: the maximum number of elements + * generated by Preprocessor::preprocessToObj(). This allows you to limit the + * amount of memory used by the Preprocessor_DOM node cache: testing indicates + * that each element uses about 160 bytes of memory on a 64-bit processor, so + * this default corresponds to about 155 MB. + * + * When the limit is exceeded, an exception is thrown. + */ +$wgMaxGeneratedPPNodeCount = 1000000; + +/** + * Maximum recursion depth for templates within templates. + * The current parser adds two levels to the PHP call stack for each template, + * and xdebug limits the call stack to 100 by default. So this should hopefully + * stop the parser before it hits the xdebug limit. + */ +$wgMaxTemplateDepth = 40; + +/** + * @see $wgMaxTemplateDepth + */ +$wgMaxPPExpandDepth = 40; + +/** + * URL schemes that should be recognized as valid by wfParseUrl(). + * + * WARNING: Do not add 'file:' to this or internal file links will be broken. + * Instead, if you want to support file links, add 'file://'. The same applies + * to any other protocols with the same name as a namespace. See task T46011 for + * more information. + * + * @see wfParseUrl + */ +$wgUrlProtocols = [ + 'bitcoin:', 'ftp://', 'ftps://', 'geo:', 'git://', 'gopher://', 'http://', + 'https://', 'irc://', 'ircs://', 'magnet:', 'mailto:', 'mms://', 'news:', + 'nntp://', 'redis://', 'sftp://', 'sip:', 'sips:', 'sms:', 'ssh://', + 'svn://', 'tel:', 'telnet://', 'urn:', 'worldwind://', 'xmpp:', '//' +]; + +/** + * If true, removes (by substituting) templates in signatures. + */ +$wgCleanSignatures = true; + +/** + * Whether to allow inline image pointing to other websites + */ +$wgAllowExternalImages = false; + +/** + * If the above is false, you can specify an exception here. Image URLs + * that start with this string are then rendered, while all others are not. + * You can use this to set up a trusted, simple repository of images. + * You may also specify an array of strings to allow multiple sites + * + * @par Examples: + * @code + * $wgAllowExternalImagesFrom = 'http://127.0.0.1/'; + * $wgAllowExternalImagesFrom = [ 'http://127.0.0.1/', 'http://example.com' ]; + * @endcode + */ +$wgAllowExternalImagesFrom = ''; + +/** + * If $wgAllowExternalImages is false, you can allow an on-wiki + * whitelist of regular expression fragments to match the image URL + * against. If the image matches one of the regular expression fragments, + * The image will be displayed. + * + * Set this to true to enable the on-wiki whitelist (MediaWiki:External image whitelist) + * Or false to disable it + */ +$wgEnableImageWhitelist = true; + +/** + * A different approach to the above: simply allow the "<img>" tag to be used. + * This allows you to specify alt text and other attributes, copy-paste HTML to + * your wiki more easily, etc. However, allowing external images in any manner + * will allow anyone with editing rights to snoop on your visitors' IP + * addresses and so forth, if they wanted to, by inserting links to images on + * sites they control. + */ +$wgAllowImageTag = false; + +/** + * Configuration for HTML postprocessing tool. Set this to a configuration + * array to enable an external tool. By default, we now use the RemexHtml + * library; historically, Dave Raggett's "HTML Tidy" was typically used. + * See https://www.w3.org/People/Raggett/tidy/ + * + * If this is null and $wgUseTidy is true, the deprecated configuration + * parameters will be used instead. + * + * If this is null and $wgUseTidy is false, a pure PHP fallback will be used. + * + * Keys are: + * - driver: May be: + * - RaggettInternalHHVM: Use the limited-functionality HHVM extension + * - RaggettInternalPHP: Use the PECL extension + * - RaggettExternal: Shell out to an external binary (tidyBin) + * - Html5Depurate: Use external Depurate service + * - Html5Internal: Use the Balancer library in PHP + * - RemexHtml: Use the RemexHtml library in PHP + * + * - tidyConfigFile: Path to configuration file for any of the Raggett drivers + * - debugComment: True to add a comment to the output with warning messages + * - tidyBin: For RaggettExternal, the path to the tidy binary. + * - tidyCommandLine: For RaggettExternal, additional command line options. + */ +$wgTidyConfig = [ 'driver' => 'RemexHtml' ]; + +/** + * Set this to true to use the deprecated tidy configuration parameters. + * @deprecated use $wgTidyConfig + */ +$wgUseTidy = false; + +/** + * The path to the tidy binary. + * @deprecated Use $wgTidyConfig['tidyBin'] + */ +$wgTidyBin = 'tidy'; + +/** + * The path to the tidy config file + * @deprecated Use $wgTidyConfig['tidyConfigFile'] + */ +$wgTidyConf = $IP . '/includes/tidy/tidy.conf'; + +/** + * The command line options to the tidy binary + * @deprecated Use $wgTidyConfig['tidyCommandLine'] + */ +$wgTidyOpts = ''; + +/** + * Set this to true to use the tidy extension + * @deprecated Use $wgTidyConfig['driver'] + */ +$wgTidyInternal = extension_loaded( 'tidy' ); + +/** + * Put tidy warnings in HTML comments + * Only works for internal tidy. + */ +$wgDebugTidy = false; + +/** + * Allow raw, unchecked HTML in "<html>...</html>" sections. + * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions + * TO RESTRICT EDITING to only those that you trust + */ +$wgRawHtml = false; + +/** + * Set a default target for external links, e.g. _blank to pop up a new window. + * + * This will also set the "noreferrer" and "noopener" link rel to prevent the + * attack described at https://mathiasbynens.github.io/rel-noopener/ . + * Some older browsers may not support these link attributes, hence + * setting $wgExternalLinkTarget to _blank may represent a security risk + * to some of your users. + */ +$wgExternalLinkTarget = false; + +/** + * If true, external URL links in wiki text will be given the + * rel="nofollow" attribute as a hint to search engines that + * they should not be followed for ranking purposes as they + * are user-supplied and thus subject to spamming. + */ +$wgNoFollowLinks = true; + +/** + * Namespaces in which $wgNoFollowLinks doesn't apply. + * See Language.php for a list of namespaces. + */ +$wgNoFollowNsExceptions = []; + +/** + * If this is set to an array of domains, external links to these domain names + * (or any subdomains) will not be set to rel="nofollow" regardless of the + * value of $wgNoFollowLinks. For instance: + * + * $wgNoFollowDomainExceptions = [ 'en.wikipedia.org', 'wiktionary.org', 'mediawiki.org' ]; + * + * This would add rel="nofollow" to links to de.wikipedia.org, but not + * en.wikipedia.org, wiktionary.org, en.wiktionary.org, us.en.wikipedia.org, + * etc. + * + * Defaults to mediawiki.org for the links included in the software by default. + */ +$wgNoFollowDomainExceptions = [ 'mediawiki.org' ]; + +/** + * Allow DISPLAYTITLE to change title display + */ +$wgAllowDisplayTitle = true; + +/** + * For consistency, restrict DISPLAYTITLE to text that normalizes to the same + * canonical DB key. Also disallow some inline CSS rules like display: none; + * which can cause the text to be hidden or unselectable. + */ +$wgRestrictDisplayTitle = true; + +/** + * Maximum number of calls per parse to expensive parser functions such as + * PAGESINCATEGORY. + */ +$wgExpensiveParserFunctionLimit = 100; + +/** + * Preprocessor caching threshold + * Setting it to 'false' will disable the preprocessor cache. + */ +$wgPreprocessorCacheThreshold = 1000; + +/** + * Enable interwiki transcluding. Only when iw_trans=1 in the interwiki table. + */ +$wgEnableScaryTranscluding = false; + +/** + * Expiry time for transcluded templates cached in transcache database table. + * Only used $wgEnableInterwikiTranscluding is set to true. + */ +$wgTranscludeCacheExpiry = 3600; + +/** + * Enable the magic links feature of automatically turning ISBN xxx, + * PMID xxx, RFC xxx into links + * + * @since 1.28 + */ +$wgEnableMagicLinks = [ + 'ISBN' => false, + 'PMID' => false, + 'RFC' => false +]; + +/** @} */ # end of parser settings } + +/************************************************************************//** + * @name Statistics + * @{ + */ + +/** + * Method used to determine if a page in a content namespace should be counted + * as a valid article. + * + * Redirect pages will never be counted as valid articles. + * + * This variable can have the following values: + * - 'any': all pages as considered as valid articles + * - 'link': the page must contain a [[wiki link]] to be considered valid + * + * See also See https://www.mediawiki.org/wiki/Manual:Article_count + * + * Retroactively changing this variable will not affect the existing count, + * to update it, you will need to run the maintenance/updateArticleCount.php + * script. + */ +$wgArticleCountMethod = 'link'; + +/** + * How many days user must be idle before he is considered inactive. Will affect + * the number shown on Special:Statistics, Special:ActiveUsers, and the + * {{NUMBEROFACTIVEUSERS}} magic word in wikitext. + * You might want to leave this as the default value, to provide comparable + * numbers between different wikis. + */ +$wgActiveUserDays = 30; + +/** @} */ # End of statistics } + +/************************************************************************//** + * @name User accounts, authentication + * @{ + */ + +/** + * Central ID lookup providers + * Key is the provider ID, value is a specification for ObjectFactory + * @since 1.27 + */ +$wgCentralIdLookupProviders = [ + 'local' => [ 'class' => LocalIdLookup::class ], +]; + +/** + * Central ID lookup provider to use by default + * @var string + */ +$wgCentralIdLookupProvider = 'local'; + +/** + * Password policy for local wiki users. A user's effective policy + * is the superset of all policy statements from the policies for the + * groups where the user is a member. If more than one group policy + * include the same policy statement, the value is the max() of the + * values. Note true > false. The 'default' policy group is required, + * and serves as the minimum policy for all users. New statements can + * be added by appending to $wgPasswordPolicy['checks']. + * Statements: + * - MinimalPasswordLength - minimum length a user can set + * - MinimumPasswordLengthToLogin - passwords shorter than this will + * not be allowed to login, regardless if it is correct. + * - MaximalPasswordLength - maximum length password a user is allowed + * to attempt. Prevents DoS attacks with pbkdf2. + * - PasswordCannotMatchUsername - Password cannot match username to + * - PasswordCannotMatchBlacklist - Username/password combination cannot + * match a specific, hardcoded blacklist. + * - PasswordCannotBePopular - Blacklist passwords which are known to be + * commonly chosen. Set to integer n to ban the top n passwords. + * If you want to ban all common passwords on file, use the + * PHP_INT_MAX constant. + * @since 1.26 + */ +$wgPasswordPolicy = [ + 'policies' => [ + 'bureaucrat' => [ + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotBePopular' => 25, + ], + 'sysop' => [ + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotBePopular' => 25, + ], + 'bot' => [ + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ], + 'default' => [ + 'MinimalPasswordLength' => 1, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ], + ], + 'checks' => [ + 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength', + 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin', + 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', + 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', + 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + 'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist' + ], +]; + +/** + * Configure AuthManager + * + * All providers are constructed using ObjectFactory, see that for the general + * structure. The array may also contain a key "sort" used to order providers: + * providers are stably sorted by this value, which should be an integer + * (default is 0). + * + * Elements are: + * - preauth: Array (keys ignored) of specifications for PreAuthenticationProviders + * - primaryauth: Array (keys ignored) of specifications for PrimaryAuthenticationProviders + * - secondaryauth: Array (keys ignored) of specifications for SecondaryAuthenticationProviders + * + * @since 1.27 + * @note If this is null or empty, the value from $wgAuthManagerAutoConfig is + * used instead. Local customization should generally set this variable from + * scratch to the desired configuration. Extensions that want to + * auto-configure themselves should use $wgAuthManagerAutoConfig instead. + */ +$wgAuthManagerConfig = null; + +/** + * @see $wgAuthManagerConfig + * @since 1.27 + */ +$wgAuthManagerAutoConfig = [ + 'preauth' => [ + MediaWiki\Auth\LegacyHookPreAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\LegacyHookPreAuthenticationProvider::class, + 'sort' => 0, + ], + MediaWiki\Auth\ThrottlePreAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\ThrottlePreAuthenticationProvider::class, + 'sort' => 0, + ], + ], + 'primaryauth' => [ + // TemporaryPasswordPrimaryAuthenticationProvider should come before + // any other PasswordAuthenticationRequest-based + // PrimaryAuthenticationProvider (or at least any that might return + // FAIL rather than ABSTAIN for a wrong password), or password reset + // won't work right. Do not remove this (or change the key) or + // auto-configuration of other such providers in extensions will + // probably auto-insert themselves in the wrong place. + MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider::class, + 'args' => [ [ + // Fall through to LocalPasswordPrimaryAuthenticationProvider + 'authoritative' => false, + ] ], + 'sort' => 0, + ], + MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider::class, + 'args' => [ [ + // Last one should be authoritative, or else the user will get + // a less-than-helpful error message (something like "supplied + // authentication info not supported" rather than "wrong + // password") if it too fails. + 'authoritative' => true, + ] ], + 'sort' => 100, + ], + ], + 'secondaryauth' => [ + MediaWiki\Auth\CheckBlocksSecondaryAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\CheckBlocksSecondaryAuthenticationProvider::class, + 'sort' => 0, + ], + MediaWiki\Auth\ResetPasswordSecondaryAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\ResetPasswordSecondaryAuthenticationProvider::class, + 'sort' => 100, + ], + // Linking during login is experimental, enable at your own risk - T134952 + // MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class => [ + // 'class' => MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class, + // 'sort' => 100, + // ], + MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class => [ + 'class' => MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class, + 'sort' => 200, + ], + ], +]; + +/** + * Time frame for re-authentication. + * + * With only password-based authentication, you'd just ask the user to re-enter + * their password to verify certain operations like changing the password or + * changing the account's email address. But under AuthManager, the user might + * not have a password (you might even have to redirect the browser to a + * third-party service or something complex like that), you might want to have + * both factors of a two-factor authentication, and so on. So, the options are: + * - Incorporate the whole multi-step authentication flow within everything + * that needs to do this. + * - Consider it good if they used Special:UserLogin during this session within + * the last X seconds. + * - Come up with a third option. + * + * MediaWiki currently takes the second option. This setting configures the + * "X seconds". + * + * This allows for configuring different time frames for different + * "operations". The operations used in MediaWiki core include: + * - LinkAccounts + * - UnlinkAccount + * - ChangeCredentials + * - RemoveCredentials + * - ChangeEmail + * + * Additional operations may be used by extensions, either explicitly by + * calling AuthManager::securitySensitiveOperationStatus(), + * ApiAuthManagerHelper::securitySensitiveOperation() or + * SpecialPage::checkLoginSecurityLevel(), or implicitly by overriding + * SpecialPage::getLoginSecurityLevel() or by subclassing + * AuthManagerSpecialPage. + * + * The key 'default' is used if a requested operation isn't defined in the array. + * + * @since 1.27 + * @var int[] operation => time in seconds. A 'default' key must always be provided. + */ +$wgReauthenticateTime = [ + 'default' => 300, +]; + +/** + * Whether to allow security-sensitive operations when re-authentication is not possible. + * + * If AuthManager::canAuthenticateNow() is false (e.g. the current + * SessionProvider is not able to change users, such as when OAuth is in use), + * AuthManager::securitySensitiveOperationStatus() cannot sensibly return + * SEC_REAUTH. Setting an operation true here will have it return SEC_OK in + * that case, while setting it false will have it return SEC_FAIL. + * + * The key 'default' is used if a requested operation isn't defined in the array. + * + * @since 1.27 + * @see $wgReauthenticateTime + * @var bool[] operation => boolean. A 'default' key must always be provided. + */ +$wgAllowSecuritySensitiveOperationIfCannotReauthenticate = [ + 'default' => true, +]; + +/** + * List of AuthenticationRequest class names which are not changeable through + * Special:ChangeCredentials and the changeauthenticationdata API. + * This is only enforced on the client level; AuthManager itself (e.g. + * AuthManager::allowsAuthenticationDataChange calls) is not affected. + * Class names are checked for exact match (not for subclasses). + * @since 1.27 + * @var string[] + */ +$wgChangeCredentialsBlacklist = [ + \MediaWiki\Auth\TemporaryPasswordAuthenticationRequest::class +]; + +/** + * List of AuthenticationRequest class names which are not removable through + * Special:RemoveCredentials and the removeauthenticationdata API. + * This is only enforced on the client level; AuthManager itself (e.g. + * AuthManager::allowsAuthenticationDataChange calls) is not affected. + * Class names are checked for exact match (not for subclasses). + * @since 1.27 + * @var string[] + */ +$wgRemoveCredentialsBlacklist = [ + \MediaWiki\Auth\PasswordAuthenticationRequest::class, +]; + +/** + * For compatibility with old installations set to false + * @deprecated since 1.24 will be removed in future + */ +$wgPasswordSalt = true; + +/** + * Specifies the minimal length of a user password. If set to 0, empty pass- + * words are allowed. + * @deprecated since 1.26, use $wgPasswordPolicy's MinimalPasswordLength. + */ +$wgMinimalPasswordLength = false; + +/** + * Specifies the maximal length of a user password (T64685). + * + * It is not recommended to make this greater than the default, as it can + * allow DoS attacks by users setting really long passwords. In addition, + * this should not be lowered too much, as it enforces weak passwords. + * + * @warning Unlike other password settings, user with passwords greater than + * the maximum will not be able to log in. + * @deprecated since 1.26, use $wgPasswordPolicy's MaximalPasswordLength. + */ +$wgMaximalPasswordLength = false; + +/** + * Specifies if users should be sent to a password-reset form on login, if their + * password doesn't meet the requirements of User::isValidPassword(). + * @since 1.23 + */ +$wgInvalidPasswordReset = true; + +/** + * Default password type to use when hashing user passwords + * + * @since 1.24 + */ +$wgPasswordDefault = 'pbkdf2'; + +/** + * Configuration for built-in password types. Maps the password type + * to an array of options. The 'class' option is the Password class to + * use. All other options are class-dependent. + * + * An advanced example: + * @code + * $wgPasswordConfig['bcrypt-peppered'] = [ + * 'class' => EncryptedPassword::class, + * 'underlying' => 'bcrypt', + * 'secrets' => [], + * 'cipher' => MCRYPT_RIJNDAEL_256, + * 'mode' => MCRYPT_MODE_CBC, + * 'cost' => 5, + * ]; + * @endcode + * + * @since 1.24 + */ +$wgPasswordConfig = [ + 'A' => [ + 'class' => MWOldPassword::class, + ], + 'B' => [ + 'class' => MWSaltedPassword::class, + ], + 'pbkdf2-legacyA' => [ + 'class' => LayeredParameterizedPassword::class, + 'types' => [ + 'A', + 'pbkdf2', + ], + ], + 'pbkdf2-legacyB' => [ + 'class' => LayeredParameterizedPassword::class, + 'types' => [ + 'B', + 'pbkdf2', + ], + ], + 'bcrypt' => [ + 'class' => BcryptPassword::class, + 'cost' => 9, + ], + 'pbkdf2' => [ + 'class' => Pbkdf2Password::class, + 'algo' => 'sha512', + 'cost' => '30000', + 'length' => '64', + ], +]; + +/** + * Whether to allow password resets ("enter some identifying data, and we'll send an email + * with a temporary password you can use to get back into the account") identified by + * various bits of data. Setting all of these to false (or the whole variable to false) + * has the effect of disabling password resets entirely + */ +$wgPasswordResetRoutes = [ + 'username' => true, + 'email' => true, +]; + +/** + * Maximum number of Unicode characters in signature + */ +$wgMaxSigChars = 255; + +/** + * Maximum number of bytes in username. You want to run the maintenance + * script ./maintenance/checkUsernames.php once you have changed this value. + */ +$wgMaxNameChars = 255; + +/** + * Array of usernames which may not be registered or logged in from + * Maintenance scripts can still use these + */ +$wgReservedUsernames = [ + 'MediaWiki default', // Default 'Main Page' and MediaWiki: message pages + 'Conversion script', // Used for the old Wikipedia software upgrade + 'Maintenance script', // Maintenance scripts which perform editing, image import script + 'Template namespace initialisation script', // Used in 1.2->1.3 upgrade + 'ScriptImporter', // Default user name used by maintenance/importSiteScripts.php + 'Unknown user', // Used in WikiImporter when importing revisions with no author + 'msg:double-redirect-fixer', // Automatic double redirect fix + 'msg:usermessage-editor', // Default user for leaving user messages + 'msg:proxyblocker', // For $wgProxyList and Special:Blockme (removed in 1.22) + 'msg:sorbs', // For $wgEnableDnsBlacklist etc. + 'msg:spambot_username', // Used by cleanupSpam.php + 'msg:autochange-username', // Used by anon category RC entries (parser functions, Lua & purges) +]; + +/** + * Settings added to this array will override the default globals for the user + * preferences used by anonymous visitors and newly created accounts. + * For instance, to disable editing on double clicks: + * $wgDefaultUserOptions ['editondblclick'] = 0; + */ +$wgDefaultUserOptions = [ + 'ccmeonemails' => 0, + 'cols' => 80, // @deprecated since 1.29 No longer used in core + 'date' => 'default', + 'diffonly' => 0, + 'disablemail' => 0, + 'editfont' => 'monospace', + 'editondblclick' => 0, + 'editsectiononrightclick' => 0, + 'email-allow-new-users' => 1, + 'enotifminoredits' => 0, + 'enotifrevealaddr' => 0, + 'enotifusertalkpages' => 1, + 'enotifwatchlistpages' => 1, + 'extendwatchlist' => 1, + 'fancysig' => 0, + 'forceeditsummary' => 0, + 'gender' => 'unknown', + 'hideminor' => 0, + 'hidepatrolled' => 0, + 'hidecategorization' => 1, + 'imagesize' => 2, + 'minordefault' => 0, + 'newpageshidepatrolled' => 0, + 'nickname' => '', + 'norollbackdiff' => 0, + 'numberheadings' => 0, + 'previewonfirst' => 0, + 'previewontop' => 1, + 'rcdays' => 7, + 'rcenhancedfilters' => 0, + 'rcenhancedfilters-disable' => 0, + 'rclimit' => 50, + 'rows' => 25, // @deprecated since 1.29 No longer used in core + 'showhiddencats' => 0, + 'shownumberswatching' => 1, + 'showtoolbar' => 1, + 'skin' => false, + 'stubthreshold' => 0, + 'thumbsize' => 5, + 'underline' => 2, + 'uselivepreview' => 0, + 'usenewrc' => 1, + 'watchcreations' => 1, + 'watchdefault' => 1, + 'watchdeletion' => 0, + 'watchuploads' => 1, + 'watchlistdays' => 3.0, + 'watchlisthideanons' => 0, + 'watchlisthidebots' => 0, + 'watchlisthideliu' => 0, + 'watchlisthideminor' => 0, + 'watchlisthideown' => 0, + 'watchlisthidepatrolled' => 0, + 'watchlisthidecategorization' => 1, + 'watchlistreloadautomatically' => 0, + 'watchlistunwatchlinks' => 0, + 'watchmoves' => 0, + 'watchrollback' => 0, + 'wllimit' => 250, + 'useeditwarning' => 1, + 'prefershttps' => 1, +]; + +/** + * An array of preferences to not show for the user + */ +$wgHiddenPrefs = []; + +/** + * Characters to prevent during new account creations. + * This is used in a regular expression character class during + * registration (regex metacharacters like / are escaped). + */ +$wgInvalidUsernameCharacters = '@:'; + +/** + * Character used as a delimiter when testing for interwiki userrights + * (In Special:UserRights, it is possible to modify users on different + * databases if the delimiter is used, e.g. "Someuser@enwiki"). + * + * It is recommended that you have this delimiter in + * $wgInvalidUsernameCharacters above, or you will not be able to + * modify the user rights of those users via Special:UserRights + */ +$wgUserrightsInterwikiDelimiter = '@'; + +/** + * This is to let user authenticate using https when they come from http. + * Based on an idea by George Herbert on wikitech-l: + * https://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html + * @since 1.17 + */ +$wgSecureLogin = false; + +/** + * Versioning for authentication tokens. + * + * If non-null, this is combined with the user's secret (the user_token field + * in the DB) to generate the token cookie. Changing this will invalidate all + * active sessions (i.e. it will log everyone out). + * + * @since 1.27 + * @var string|null + */ +$wgAuthenticationTokenVersion = null; + +/** + * MediaWiki\Session\SessionProvider configuration. + * + * Value is an array of ObjectFactory specifications for the SessionProviders + * to be used. Keys in the array are ignored. Order is not significant. + * + * @since 1.27 + */ +$wgSessionProviders = [ + MediaWiki\Session\CookieSessionProvider::class => [ + 'class' => MediaWiki\Session\CookieSessionProvider::class, + 'args' => [ [ + 'priority' => 30, + 'callUserSetCookiesHook' => true, + ] ], + ], + MediaWiki\Session\BotPasswordSessionProvider::class => [ + 'class' => MediaWiki\Session\BotPasswordSessionProvider::class, + 'args' => [ [ + 'priority' => 75, + ] ], + ], +]; + +/** @} */ # end user accounts } + +/************************************************************************//** + * @name User rights, access control and monitoring + * @{ + */ + +/** + * Number of seconds before autoblock entries expire. Default 86400 = 1 day. + */ +$wgAutoblockExpiry = 86400; + +/** + * Set this to true to allow blocked users to edit their own user talk page. + */ +$wgBlockAllowsUTEdit = true; + +/** + * Allow sysops to ban users from accessing Emailuser + */ +$wgSysopEmailBans = true; + +/** + * Limits on the possible sizes of range blocks. + * + * CIDR notation is hard to understand, it's easy to mistakenly assume that a + * /1 is a small range and a /31 is a large range. For IPv4, setting a limit of + * half the number of bits avoids such errors, and allows entire ISPs to be + * blocked using a small number of range blocks. + * + * For IPv6, RFC 3177 recommends that a /48 be allocated to every residential + * customer, so range blocks larger than /64 (half the number of bits) will + * plainly be required. RFC 4692 implies that a very large ISP may be + * allocated a /19 if a generous HD-Ratio of 0.8 is used, so we will use that + * as our limit. As of 2012, blocking the whole world would require a /4 range. + */ +$wgBlockCIDRLimit = [ + 'IPv4' => 16, # Blocks larger than a /16 (64k addresses) will not be allowed + 'IPv6' => 19, +]; + +/** + * If true, blocked users will not be allowed to login. When using this with + * a public wiki, the effect of logging out blocked users may actually be + * avers: unless the user's address is also blocked (e.g. auto-block), + * logging the user out will again allow reading and editing, just as for + * anonymous visitors. + */ +$wgBlockDisablesLogin = false; + +/** + * Pages anonymous user may see, set as an array of pages titles. + * + * @par Example: + * @code + * $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help"); + * @endcode + * + * Special:Userlogin and Special:ChangePassword are always whitelisted. + * + * @note This will only work if $wgGroupPermissions['*']['read'] is false -- + * see below. Otherwise, ALL pages are accessible, regardless of this setting. + * + * @note Also that this will only protect _pages in the wiki_. Uploaded files + * will remain readable. You can use img_auth.php to protect uploaded files, + * see https://www.mediawiki.org/wiki/Manual:Image_Authorization + * + * @note Extensions should not modify this, but use the TitleReadWhitelist + * hook instead. + */ +$wgWhitelistRead = false; + +/** + * Pages anonymous user may see, set as an array of regular expressions. + * + * This function will match the regexp against the title name, which + * is without underscore. + * + * @par Example: + * To whitelist [[Main Page]]: + * @code + * $wgWhitelistReadRegexp = [ "/Main Page/" ]; + * @endcode + * + * @note Unless ^ and/or $ is specified, a regular expression might match + * pages not intended to be whitelisted. The above example will also + * whitelist a page named 'Security Main Page'. + * + * @par Example: + * To allow reading any page starting with 'User' regardless of the case: + * @code + * $wgWhitelistReadRegexp = [ "@^UsEr.*@i" ]; + * @endcode + * Will allow both [[User is banned]] and [[User:JohnDoe]] + * + * @note This will only work if $wgGroupPermissions['*']['read'] is false -- + * see below. Otherwise, ALL pages are accessible, regardless of this setting. + */ +$wgWhitelistReadRegexp = false; + +/** + * Should editors be required to have a validated e-mail + * address before being allowed to edit? + */ +$wgEmailConfirmToEdit = false; + +/** + * Should MediaWiki attempt to protect user's privacy when doing redirects? + * Keep this true if access counts to articles are made public. + */ +$wgHideIdentifiableRedirects = true; + +/** + * Permission keys given to users in each group. + * + * This is an array where the keys are all groups and each value is an + * array of the format (right => boolean). + * + * The second format is used to support per-namespace permissions. + * Note that this feature does not fully work for all permission types. + * + * All users are implicitly in the '*' group including anonymous visitors; + * logged-in users are all implicitly in the 'user' group. These will be + * combined with the permissions of all groups that a given user is listed + * in in the user_groups table. + * + * Note: Don't set $wgGroupPermissions = []; unless you know what you're + * doing! This will wipe all permissions, and may mean that your users are + * unable to perform certain essential tasks or access new functionality + * when new permissions are introduced and default grants established. + * + * Functionality to make pages inaccessible has not been extensively tested + * for security. Use at your own risk! + * + * This replaces $wgWhitelistAccount and $wgWhitelistEdit + */ +$wgGroupPermissions = []; + +/** @cond file_level_code */ +// Implicit group for all visitors +$wgGroupPermissions['*']['createaccount'] = true; +$wgGroupPermissions['*']['read'] = true; +$wgGroupPermissions['*']['edit'] = true; +$wgGroupPermissions['*']['createpage'] = true; +$wgGroupPermissions['*']['createtalk'] = true; +$wgGroupPermissions['*']['writeapi'] = true; +$wgGroupPermissions['*']['viewmywatchlist'] = true; +$wgGroupPermissions['*']['editmywatchlist'] = true; +$wgGroupPermissions['*']['viewmyprivateinfo'] = true; +$wgGroupPermissions['*']['editmyprivateinfo'] = true; +$wgGroupPermissions['*']['editmyoptions'] = true; +# $wgGroupPermissions['*']['patrolmarks'] = false; // let anons see what was patrolled + +// Implicit group for all logged-in accounts +$wgGroupPermissions['user']['move'] = true; +$wgGroupPermissions['user']['move-subpages'] = true; +$wgGroupPermissions['user']['move-rootuserpages'] = true; // can move root userpages +$wgGroupPermissions['user']['move-categorypages'] = true; +$wgGroupPermissions['user']['movefile'] = true; +$wgGroupPermissions['user']['read'] = true; +$wgGroupPermissions['user']['edit'] = true; +$wgGroupPermissions['user']['createpage'] = true; +$wgGroupPermissions['user']['createtalk'] = true; +$wgGroupPermissions['user']['writeapi'] = true; +$wgGroupPermissions['user']['upload'] = true; +$wgGroupPermissions['user']['reupload'] = true; +$wgGroupPermissions['user']['reupload-shared'] = true; +$wgGroupPermissions['user']['minoredit'] = true; +$wgGroupPermissions['user']['editmyusercss'] = true; +$wgGroupPermissions['user']['editmyuserjson'] = true; +$wgGroupPermissions['user']['editmyuserjs'] = true; +$wgGroupPermissions['user']['purge'] = true; +$wgGroupPermissions['user']['sendemail'] = true; +$wgGroupPermissions['user']['applychangetags'] = true; +$wgGroupPermissions['user']['changetags'] = true; +$wgGroupPermissions['user']['editcontentmodel'] = true; + +// Implicit group for accounts that pass $wgAutoConfirmAge +$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; +$wgGroupPermissions['autoconfirmed']['editsemiprotected'] = true; + +// Users with bot privilege can have their edits hidden +// from various log pages by default +$wgGroupPermissions['bot']['bot'] = true; +$wgGroupPermissions['bot']['autoconfirmed'] = true; +$wgGroupPermissions['bot']['editsemiprotected'] = true; +$wgGroupPermissions['bot']['nominornewtalk'] = true; +$wgGroupPermissions['bot']['autopatrol'] = true; +$wgGroupPermissions['bot']['suppressredirect'] = true; +$wgGroupPermissions['bot']['apihighlimits'] = true; +$wgGroupPermissions['bot']['writeapi'] = true; + +// Most extra permission abilities go to this group +$wgGroupPermissions['sysop']['block'] = true; +$wgGroupPermissions['sysop']['createaccount'] = true; +$wgGroupPermissions['sysop']['delete'] = true; +// can be separately configured for pages with > $wgDeleteRevisionsLimit revs +$wgGroupPermissions['sysop']['bigdelete'] = true; +// can view deleted history entries, but not see or restore the text +$wgGroupPermissions['sysop']['deletedhistory'] = true; +// can view deleted revision text +$wgGroupPermissions['sysop']['deletedtext'] = true; +$wgGroupPermissions['sysop']['undelete'] = true; +$wgGroupPermissions['sysop']['editinterface'] = true; +$wgGroupPermissions['sysop']['editusercss'] = true; +$wgGroupPermissions['sysop']['edituserjson'] = true; +$wgGroupPermissions['sysop']['edituserjs'] = true; +$wgGroupPermissions['sysop']['import'] = true; +$wgGroupPermissions['sysop']['importupload'] = true; +$wgGroupPermissions['sysop']['move'] = true; +$wgGroupPermissions['sysop']['move-subpages'] = true; +$wgGroupPermissions['sysop']['move-rootuserpages'] = true; +$wgGroupPermissions['sysop']['move-categorypages'] = true; +$wgGroupPermissions['sysop']['patrol'] = true; +$wgGroupPermissions['sysop']['autopatrol'] = true; +$wgGroupPermissions['sysop']['protect'] = true; +$wgGroupPermissions['sysop']['editprotected'] = true; +$wgGroupPermissions['sysop']['rollback'] = true; +$wgGroupPermissions['sysop']['upload'] = true; +$wgGroupPermissions['sysop']['reupload'] = true; +$wgGroupPermissions['sysop']['reupload-shared'] = true; +$wgGroupPermissions['sysop']['unwatchedpages'] = true; +$wgGroupPermissions['sysop']['autoconfirmed'] = true; +$wgGroupPermissions['sysop']['editsemiprotected'] = true; +$wgGroupPermissions['sysop']['ipblock-exempt'] = true; +$wgGroupPermissions['sysop']['blockemail'] = true; +$wgGroupPermissions['sysop']['markbotedits'] = true; +$wgGroupPermissions['sysop']['apihighlimits'] = true; +$wgGroupPermissions['sysop']['browsearchive'] = true; +$wgGroupPermissions['sysop']['noratelimit'] = true; +$wgGroupPermissions['sysop']['movefile'] = true; +$wgGroupPermissions['sysop']['unblockself'] = true; +$wgGroupPermissions['sysop']['suppressredirect'] = true; +# $wgGroupPermissions['sysop']['pagelang'] = true; +# $wgGroupPermissions['sysop']['upload_by_url'] = true; +$wgGroupPermissions['sysop']['mergehistory'] = true; +$wgGroupPermissions['sysop']['managechangetags'] = true; +$wgGroupPermissions['sysop']['deletechangetags'] = true; + +// Permission to change users' group assignments +$wgGroupPermissions['bureaucrat']['userrights'] = true; +$wgGroupPermissions['bureaucrat']['noratelimit'] = true; +// Permission to change users' groups assignments across wikis +# $wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true; +// Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth +# $wgGroupPermissions['bureaucrat']['override-export-depth'] = true; + +# $wgGroupPermissions['sysop']['deletelogentry'] = true; +# $wgGroupPermissions['sysop']['deleterevision'] = true; +// To hide usernames from users and Sysops +# $wgGroupPermissions['suppress']['hideuser'] = true; +// To hide revisions/log items from users and Sysops +# $wgGroupPermissions['suppress']['suppressrevision'] = true; +// To view revisions/log items hidden from users and Sysops +# $wgGroupPermissions['suppress']['viewsuppressed'] = true; +// For private suppression log access +# $wgGroupPermissions['suppress']['suppressionlog'] = true; + +/** + * The developer group is deprecated, but can be activated if need be + * to use the 'lockdb' and 'unlockdb' special pages. Those require + * that a lock file be defined and creatable/removable by the web + * server. + */ +# $wgGroupPermissions['developer']['siteadmin'] = true; + +/** @endcond */ + +/** + * Permission keys revoked from users in each group. + * + * This acts the same way as wgGroupPermissions above, except that + * if the user is in a group here, the permission will be removed from them. + * + * Improperly setting this could mean that your users will be unable to perform + * certain essential tasks, so use at your own risk! + */ +$wgRevokePermissions = []; + +/** + * Implicit groups, aren't shown on Special:Listusers or somewhere else + */ +$wgImplicitGroups = [ '*', 'user', 'autoconfirmed' ]; + +/** + * A map of group names that the user is in, to group names that those users + * are allowed to add or revoke. + * + * Setting the list of groups to add or revoke to true is equivalent to "any + * group". + * + * @par Example: + * To allow sysops to add themselves to the "bot" group: + * @code + * $wgGroupsAddToSelf = [ 'sysop' => [ 'bot' ] ]; + * @endcode + * + * @par Example: + * Implicit groups may be used for the source group, for instance: + * @code + * $wgGroupsRemoveFromSelf = [ '*' => true ]; + * @endcode + * This allows users in the '*' group (i.e. any user) to remove themselves from + * any group that they happen to be in. + */ +$wgGroupsAddToSelf = []; + +/** + * @see $wgGroupsAddToSelf + */ +$wgGroupsRemoveFromSelf = []; + +/** + * Set of available actions that can be restricted via action=protect + * You probably shouldn't change this. + * Translated through restriction-* messages. + * Title::getRestrictionTypes() will remove restrictions that are not + * applicable to a specific title (create and upload) + */ +$wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ]; + +/** + * Rights which can be required for each protection level (via action=protect) + * + * You can add a new protection level that requires a specific + * permission by manipulating this array. The ordering of elements + * dictates the order on the protection form's lists. + * + * - '' will be ignored (i.e. unprotected) + * - 'autoconfirmed' is quietly rewritten to 'editsemiprotected' for backwards compatibility + * - 'sysop' is quietly rewritten to 'editprotected' for backwards compatibility + */ +$wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ]; + +/** + * Restriction levels that can be used with cascading protection + * + * A page can only be protected with cascading protection if the + * requested restriction level is included in this array. + * + * 'autoconfirmed' is quietly rewritten to 'editsemiprotected' for backwards compatibility. + * 'sysop' is quietly rewritten to 'editprotected' for backwards compatibility. + */ +$wgCascadingRestrictionLevels = [ 'sysop' ]; + +/** + * Restriction levels that should be considered "semiprotected" + * + * Certain places in the interface recognize a dichotomy between "protected" + * and "semiprotected", without further distinguishing the specific levels. In + * general, if anyone can be eligible to edit a protection level merely by + * reaching some condition in $wgAutopromote, it should probably be considered + * "semiprotected". + * + * 'autoconfirmed' is quietly rewritten to 'editsemiprotected' for backwards compatibility. + * 'sysop' is not changed, since it really shouldn't be here. + */ +$wgSemiprotectedRestrictionLevels = [ 'autoconfirmed' ]; + +/** + * Set the minimum permissions required to edit pages in each + * namespace. If you list more than one permission, a user must + * have all of them to edit pages in that namespace. + * + * @note NS_MEDIAWIKI is implicitly restricted to 'editinterface'. + */ +$wgNamespaceProtection = []; + +/** + * Pages in namespaces in this array can not be used as templates. + * + * Elements MUST be numeric namespace ids, you can safely use the MediaWiki + * namespaces constants (NS_USER, NS_MAIN...). + * + * Among other things, this may be useful to enforce read-restrictions + * which may otherwise be bypassed by using the template mechanism. + */ +$wgNonincludableNamespaces = []; + +/** + * Number of seconds an account is required to age before it's given the + * implicit 'autoconfirm' group membership. This can be used to limit + * privileges of new accounts. + * + * Accounts created by earlier versions of the software may not have a + * recorded creation date, and will always be considered to pass the age test. + * + * When left at 0, all registered accounts will pass. + * + * @par Example: + * Set automatic confirmation to 10 minutes (which is 600 seconds): + * @code + * $wgAutoConfirmAge = 600; // ten minutes + * @endcode + * Set age to one day: + * @code + * $wgAutoConfirmAge = 3600*24; // one day + * @endcode + */ +$wgAutoConfirmAge = 0; + +/** + * Number of edits an account requires before it is autoconfirmed. + * Passing both this AND the time requirement is needed. Example: + * + * @par Example: + * @code + * $wgAutoConfirmCount = 50; + * @endcode + */ +$wgAutoConfirmCount = 0; + +/** + * Array containing the conditions of automatic promotion of a user to specific groups. + * + * The basic syntax for `$wgAutopromote` is: + * + * $wgAutopromote = array( + * 'groupname' => cond, + * 'group2' => cond2, + * ); + * + * A `cond` may be: + * - a single condition without arguments: + * Note that Autopromote wraps a single non-array value into an array + * e.g. `APCOND_EMAILCONFIRMED` OR + * array( `APCOND_EMAILCONFIRMED` ) + * - a single condition with arguments: + * e.g. `array( APCOND_EDITCOUNT, 100 )` + * - a set of conditions: + * e.g. `array( 'operand', cond1, cond2, ... )` + * + * When constructing a set of conditions, the following conditions are available: + * - `&` (**AND**): + * promote if user matches **ALL** conditions + * - `|` (**OR**): + * promote if user matches **ANY** condition + * - `^` (**XOR**): + * promote if user matches **ONLY ONE OF THE CONDITIONS** + * - `!` (**NOT**): + * promote if user matces **NO** condition + * - array( APCOND_EMAILCONFIRMED ): + * true if user has a confirmed e-mail + * - array( APCOND_EDITCOUNT, number of edits ): + * true if user has the at least the number of edits as the passed parameter + * - array( APCOND_AGE, seconds since registration ): + * true if the length of time since the user created his/her account + * is at least the same length of time as the passed parameter + * - array( APCOND_AGE_FROM_EDIT, seconds since first edit ): + * true if the length of time since the user made his/her first edit + * is at least the same length of time as the passed parameter + * - array( APCOND_INGROUPS, group1, group2, ... ): + * true if the user is a member of each of the passed groups + * - array( APCOND_ISIP, ip ): + * true if the user has the passed IP address + * - array( APCOND_IPINRANGE, range ): + * true if the user has an IP address in the range of the passed parameter + * - array( APCOND_BLOCKED ): + * true if the user is blocked + * - array( APCOND_ISBOT ): + * true if the user is a bot + * - similar constructs can be defined by extensions + * + * The sets of conditions are evaluated recursively, so you can use nested sets of conditions + * linked by operands. + * + * Note that if $wgEmailAuthentication is disabled, APCOND_EMAILCONFIRMED will be true for any + * user who has provided an e-mail address. + */ +$wgAutopromote = [ + 'autoconfirmed' => [ '&', + [ APCOND_EDITCOUNT, &$wgAutoConfirmCount ], + [ APCOND_AGE, &$wgAutoConfirmAge ], + ], +]; + +/** + * Automatically add a usergroup to any user who matches certain conditions. + * + * Does not add the user to the group again if it has been removed. + * Also, does not remove the group if the user no longer meets the criteria. + * + * The format is: + * @code + * [ event => criteria, ... ] + * @endcode + * Where event is either: + * - 'onEdit' (when user edits) + * + * Criteria has the same format as $wgAutopromote + * + * @see $wgAutopromote + * @since 1.18 + */ +$wgAutopromoteOnce = [ + 'onEdit' => [], +]; + +/** + * Put user rights log entries for autopromotion in recent changes? + * @since 1.18 + */ +$wgAutopromoteOnceLogInRC = true; + +/** + * $wgAddGroups and $wgRemoveGroups can be used to give finer control over who + * can assign which groups at Special:Userrights. + * + * @par Example: + * Bureaucrats can add any group: + * @code + * $wgAddGroups['bureaucrat'] = true; + * @endcode + * Bureaucrats can only remove bots and sysops: + * @code + * $wgRemoveGroups['bureaucrat'] = [ 'bot', 'sysop' ]; + * @endcode + * Sysops can make bots: + * @code + * $wgAddGroups['sysop'] = [ 'bot' ]; + * @endcode + * Sysops can disable other sysops in an emergency, and disable bots: + * @code + * $wgRemoveGroups['sysop'] = [ 'sysop', 'bot' ]; + * @endcode + */ +$wgAddGroups = []; + +/** + * @see $wgAddGroups + */ +$wgRemoveGroups = []; + +/** + * A list of available rights, in addition to the ones defined by the core. + * For extensions only. + */ +$wgAvailableRights = []; + +/** + * Optional to restrict deletion of pages with higher revision counts + * to users with the 'bigdelete' permission. (Default given to sysops.) + */ +$wgDeleteRevisionsLimit = 0; + +/** + * The maximum number of edits a user can have and + * can still be hidden by users with the hideuser permission. + * This is limited for performance reason. + * Set to false to disable the limit. + * @since 1.23 + */ +$wgHideUserContribLimit = 1000; + +/** + * Number of accounts each IP address may create per specified period(s). + * + * @par Example: + * @code + * $wgAccountCreationThrottle = [ + * // no more than 100 per month + * [ + * 'count' => 100, + * 'seconds' => 30*86400, + * ], + * // no more than 10 per day + * [ + * 'count' => 10, + * 'seconds' => 86400, + * ], + * ]; + * @endcode + * + * @warning Requires $wgMainCacheType to be enabled + */ +$wgAccountCreationThrottle = [ [ + 'count' => 0, + 'seconds' => 86400, +] ]; + +/** + * Edits matching these regular expressions in body text + * will be recognised as spam and rejected automatically. + * + * There's no administrator override on-wiki, so be careful what you set. :) + * May be an array of regexes or a single string for backwards compatibility. + * + * @see https://en.wikipedia.org/wiki/Regular_expression + * + * @note Each regex needs a beginning/end delimiter, eg: # or / + */ +$wgSpamRegex = []; + +/** + * Same as the above except for edit summaries + */ +$wgSummarySpamRegex = []; + +/** + * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open + * proxies + * @since 1.16 + */ +$wgEnableDnsBlacklist = false; + +/** + * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. + * + * This is an array of either a URL or an array with the URL and a key (should + * the blacklist require a key). + * + * @par Example: + * @code + * $wgDnsBlacklistUrls = [ + * // String containing URL + * 'http.dnsbl.sorbs.net.', + * // Array with URL and key, for services that require a key + * [ 'dnsbl.httpbl.net.', 'mykey' ], + * // Array with just the URL. While this works, it is recommended that you + * // just use a string as shown above + * [ 'opm.tornevall.org.' ] + * ]; + * @endcode + * + * @note You should end the domain name with a . to avoid searching your + * eventual domain search suffixes. + * @since 1.16 + */ +$wgDnsBlacklistUrls = [ 'http.dnsbl.sorbs.net.' ]; + +/** + * Proxy whitelist, list of addresses that are assumed to be non-proxy despite + * what the other methods might say. + */ +$wgProxyWhitelist = []; + +/** + * IP ranges that should be considered soft-blocked (anon-only, account + * creation allowed). The intent is to use this to prevent anonymous edits from + * shared resources such as Wikimedia Labs. + * @since 1.29 + * @var string[] + */ +$wgSoftBlockRanges = []; + +/** + * Whether to look at the X-Forwarded-For header's list of (potentially spoofed) + * IPs and apply IP blocks to them. This allows for IP blocks to work with correctly-configured + * (transparent) proxies without needing to block the proxies themselves. + */ +$wgApplyIpBlocksToXff = false; + +/** + * Simple rate limiter options to brake edit floods. + * + * Maximum number actions allowed in the given number of seconds; after that + * the violating client receives HTTP 500 error pages until the period + * elapses. + * + * @par Example: + * Limits per configured per action and then type of users. + * @code + * $wgRateLimits = [ + * 'edit' => [ + * 'anon' => [ x, y ], // any and all anonymous edits (aggregate) + * 'user' => [ x, y ], // each logged-in user + * 'newbie' => [ x, y ], // each new autoconfirmed accounts; overrides 'user' + * 'ip' => [ x, y ], // each anon and recent account + * 'subnet' => [ x, y ], // ... within a /24 subnet in IPv4 or /64 in IPv6 + * 'groupName' => [ x, y ], // by group membership + * ] + * ]; + * @endcode + * + * @par Normally, the 'noratelimit' right allows a user to bypass any rate + * limit checks. This can be disabled on a per-action basis by setting the + * special '&can-bypass' key to false in that action's configuration. + * @code + * $wgRateLimits = [ + * 'some-action' => [ + * '&can-bypass' => false, + * 'user' => [ x, y ], + * ]; + * @endcode + * + * @warning Requires that $wgMainCacheType is set to something persistent + */ +$wgRateLimits = [ + // Page edits + 'edit' => [ + 'ip' => [ 8, 60 ], + 'newbie' => [ 8, 60 ], + 'user' => [ 90, 60 ], + ], + // Page moves + 'move' => [ + 'newbie' => [ 2, 120 ], + 'user' => [ 8, 60 ], + ], + // File uploads + 'upload' => [ + 'ip' => [ 8, 60 ], + 'newbie' => [ 8, 60 ], + ], + // Page rollbacks + 'rollback' => [ + 'user' => [ 10, 60 ], + 'newbie' => [ 5, 120 ] + ], + // Triggering password resets emails + 'mailpassword' => [ + 'ip' => [ 5, 3600 ], + ], + // Emailing other users using MediaWiki + 'emailuser' => [ + 'ip' => [ 5, 86400 ], + 'newbie' => [ 5, 86400 ], + 'user' => [ 20, 86400 ], + ], + 'changeemail' => [ + 'ip-all' => [ 10, 3600 ], + 'user' => [ 4, 86400 ] + ], + // Purging pages + 'purge' => [ + 'ip' => [ 30, 60 ], + 'user' => [ 30, 60 ], + ], + // Purges of link tables + 'linkpurge' => [ + 'ip' => [ 30, 60 ], + 'user' => [ 30, 60 ], + ], + // Files rendered via thumb.php or thumb_handler.php + 'renderfile' => [ + 'ip' => [ 700, 30 ], + 'user' => [ 700, 30 ], + ], + // Same as above but for non-standard thumbnails + 'renderfile-nonstandard' => [ + 'ip' => [ 70, 30 ], + 'user' => [ 70, 30 ], + ], + // Stashing edits into cache before save + 'stashedit' => [ + 'ip' => [ 30, 60 ], + 'newbie' => [ 30, 60 ], + ], + // Adding or removing change tags + 'changetag' => [ + 'ip' => [ 8, 60 ], + 'newbie' => [ 8, 60 ], + ], + // Changing the content model of a page + 'editcontentmodel' => [ + 'newbie' => [ 2, 120 ], + 'user' => [ 8, 60 ], + ], +]; + +/** + * Array of IPs / CIDR ranges which should be excluded from rate limits. + * This may be useful for whitelisting NAT gateways for conferences, etc. + */ +$wgRateLimitsExcludedIPs = []; + +/** + * Log IP addresses in the recentchanges table; can be accessed only by + * extensions (e.g. CheckUser) or a DB admin + * Used for retroactive autoblocks + */ +$wgPutIPinRC = true; + +/** + * Integer defining default number of entries to show on + * special pages which are query-pages such as Special:Whatlinkshere. + */ +$wgQueryPageDefaultLimit = 50; + +/** + * Limit password attempts to X attempts per Y seconds per IP per account. + * + * Value is an array of arrays. Each sub-array must have a key for count + * (ie count of how many attempts before throttle) and a key for seconds. + * If the key 'allIPs' (case sensitive) is present, then the limit is + * just per account instead of per IP per account. + * + * @since 1.27 allIps support and multiple limits added in 1.27. Prior + * to 1.27 this only supported having a single throttle. + * @warning Requires $wgMainCacheType to be enabled + */ +$wgPasswordAttemptThrottle = [ + // Short term limit + [ 'count' => 5, 'seconds' => 300 ], + // Long term limit. We need to balance the risk + // of somebody using this as a DoS attack to lock someone + // out of their account, and someone doing a brute force attack. + [ 'count' => 150, 'seconds' => 60 * 60 * 48 ], +]; + +/** + * @var array Map of (grant => right => boolean) + * Users authorize consumers (like Apps) to act on their behalf but only with + * a subset of the user's normal account rights (signed off on by the user). + * The possible rights to grant to a consumer are bundled into groups called + * "grants". Each grant defines some rights it lets consumers inherit from the + * account they may act on behalf of. Note that a user granting a right does + * nothing if that user does not actually have that right to begin with. + * @since 1.27 + */ +$wgGrantPermissions = []; + +// @TODO: clean up grants +// @TODO: auto-include read/editsemiprotected rights? + +$wgGrantPermissions['basic']['autoconfirmed'] = true; +$wgGrantPermissions['basic']['autopatrol'] = true; +$wgGrantPermissions['basic']['editsemiprotected'] = true; +$wgGrantPermissions['basic']['ipblock-exempt'] = true; +$wgGrantPermissions['basic']['nominornewtalk'] = true; +$wgGrantPermissions['basic']['patrolmarks'] = true; +$wgGrantPermissions['basic']['purge'] = true; +$wgGrantPermissions['basic']['read'] = true; +$wgGrantPermissions['basic']['skipcaptcha'] = true; +$wgGrantPermissions['basic']['writeapi'] = true; + +$wgGrantPermissions['highvolume']['bot'] = true; +$wgGrantPermissions['highvolume']['apihighlimits'] = true; +$wgGrantPermissions['highvolume']['noratelimit'] = true; +$wgGrantPermissions['highvolume']['markbotedits'] = true; + +$wgGrantPermissions['editpage']['edit'] = true; +$wgGrantPermissions['editpage']['minoredit'] = true; +$wgGrantPermissions['editpage']['applychangetags'] = true; +$wgGrantPermissions['editpage']['changetags'] = true; + +$wgGrantPermissions['editprotected'] = $wgGrantPermissions['editpage']; +$wgGrantPermissions['editprotected']['editprotected'] = true; + +// FIXME: Rename editmycssjs to editmyconfig +$wgGrantPermissions['editmycssjs'] = $wgGrantPermissions['editpage']; +$wgGrantPermissions['editmycssjs']['editmyusercss'] = true; +$wgGrantPermissions['editmycssjs']['editmyuserjson'] = true; +$wgGrantPermissions['editmycssjs']['editmyuserjs'] = true; + +$wgGrantPermissions['editmyoptions']['editmyoptions'] = true; + +$wgGrantPermissions['editinterface'] = $wgGrantPermissions['editpage']; +$wgGrantPermissions['editinterface']['editinterface'] = true; +$wgGrantPermissions['editinterface']['editusercss'] = true; +$wgGrantPermissions['editinterface']['edituserjson'] = true; +$wgGrantPermissions['editinterface']['edituserjs'] = true; + +$wgGrantPermissions['createeditmovepage'] = $wgGrantPermissions['editpage']; +$wgGrantPermissions['createeditmovepage']['createpage'] = true; +$wgGrantPermissions['createeditmovepage']['createtalk'] = true; +$wgGrantPermissions['createeditmovepage']['move'] = true; +$wgGrantPermissions['createeditmovepage']['move-rootuserpages'] = true; +$wgGrantPermissions['createeditmovepage']['move-subpages'] = true; +$wgGrantPermissions['createeditmovepage']['move-categorypages'] = true; + +$wgGrantPermissions['uploadfile']['upload'] = true; +$wgGrantPermissions['uploadfile']['reupload-own'] = true; + +$wgGrantPermissions['uploadeditmovefile'] = $wgGrantPermissions['uploadfile']; +$wgGrantPermissions['uploadeditmovefile']['reupload'] = true; +$wgGrantPermissions['uploadeditmovefile']['reupload-shared'] = true; +$wgGrantPermissions['uploadeditmovefile']['upload_by_url'] = true; +$wgGrantPermissions['uploadeditmovefile']['movefile'] = true; +$wgGrantPermissions['uploadeditmovefile']['suppressredirect'] = true; + +$wgGrantPermissions['patrol']['patrol'] = true; + +$wgGrantPermissions['rollback']['rollback'] = true; + +$wgGrantPermissions['blockusers']['block'] = true; +$wgGrantPermissions['blockusers']['blockemail'] = true; + +$wgGrantPermissions['viewdeleted']['browsearchive'] = true; +$wgGrantPermissions['viewdeleted']['deletedhistory'] = true; +$wgGrantPermissions['viewdeleted']['deletedtext'] = true; + +$wgGrantPermissions['viewrestrictedlogs']['suppressionlog'] = true; + +$wgGrantPermissions['delete'] = $wgGrantPermissions['editpage'] + + $wgGrantPermissions['viewdeleted']; +$wgGrantPermissions['delete']['delete'] = true; +$wgGrantPermissions['delete']['bigdelete'] = true; +$wgGrantPermissions['delete']['deletelogentry'] = true; +$wgGrantPermissions['delete']['deleterevision'] = true; +$wgGrantPermissions['delete']['undelete'] = true; + +$wgGrantPermissions['protect'] = $wgGrantPermissions['editprotected']; +$wgGrantPermissions['protect']['protect'] = true; + +$wgGrantPermissions['viewmywatchlist']['viewmywatchlist'] = true; + +$wgGrantPermissions['editmywatchlist']['editmywatchlist'] = true; + +$wgGrantPermissions['sendemail']['sendemail'] = true; + +$wgGrantPermissions['createaccount']['createaccount'] = true; + +$wgGrantPermissions['privateinfo']['viewmyprivateinfo'] = true; + +/** + * @var array Map of grants to their UI grouping + * @since 1.27 + */ +$wgGrantPermissionGroups = [ + // Hidden grants are implicitly present + 'basic' => 'hidden', + + 'editpage' => 'page-interaction', + 'createeditmovepage' => 'page-interaction', + 'editprotected' => 'page-interaction', + 'patrol' => 'page-interaction', + + 'uploadfile' => 'file-interaction', + 'uploadeditmovefile' => 'file-interaction', + + 'sendemail' => 'email', + + 'viewmywatchlist' => 'watchlist-interaction', + 'editviewmywatchlist' => 'watchlist-interaction', + + 'editmycssjs' => 'customization', + 'editmyoptions' => 'customization', + + 'editinterface' => 'administration', + 'rollback' => 'administration', + 'blockusers' => 'administration', + 'delete' => 'administration', + 'viewdeleted' => 'administration', + 'viewrestrictedlogs' => 'administration', + 'protect' => 'administration', + 'createaccount' => 'administration', + + 'highvolume' => 'high-volume', + + 'privateinfo' => 'private-information', +]; + +/** + * @var bool Whether to enable bot passwords + * @since 1.27 + */ +$wgEnableBotPasswords = true; + +/** + * Cluster for the bot_passwords table + * @var string|bool If false, the normal cluster will be used + * @since 1.27 + */ +$wgBotPasswordsCluster = false; + +/** + * Database name for the bot_passwords table + * + * To use a database with a table prefix, set this variable to + * "{$database}-{$prefix}". + * @var string|bool If false, the normal database will be used + * @since 1.27 + */ +$wgBotPasswordsDatabase = false; + +/** @} */ # end of user rights settings + +/************************************************************************//** + * @name Proxy scanner settings + * @{ + */ + +/** + * This should always be customised in LocalSettings.php + */ +$wgSecretKey = false; + +/** + * Big list of banned IP addresses. + * + * This can have the following formats: + * - An array of addresses, either in the values + * or the keys (for backward compatibility, deprecated since 1.30) + * - A string, in that case this is the path to a file + * containing the list of IP addresses, one per line + */ +$wgProxyList = []; + +/** @} */ # end of proxy scanner settings + +/************************************************************************//** + * @name Cookie settings + * @{ + */ + +/** + * Default cookie lifetime, in seconds. Setting to 0 makes all cookies session-only. + */ +$wgCookieExpiration = 30 * 86400; + +/** + * Default login cookie lifetime, in seconds. Setting + * $wgExtendLoginCookieExpiration to null will use $wgCookieExpiration to + * calculate the cookie lifetime. As with $wgCookieExpiration, 0 will make + * login cookies session-only. + */ +$wgExtendedLoginCookieExpiration = 180 * 86400; + +/** + * Set to set an explicit domain on the login cookies eg, "justthis.domain.org" + * or ".any.subdomain.net" + */ +$wgCookieDomain = ''; + +/** + * Set this variable if you want to restrict cookies to a certain path within + * the domain specified by $wgCookieDomain. + */ +$wgCookiePath = '/'; + +/** + * Whether the "secure" flag should be set on the cookie. This can be: + * - true: Set secure flag + * - false: Don't set secure flag + * - "detect": Set the secure flag if $wgServer is set to an HTTPS URL + */ +$wgCookieSecure = 'detect'; + +/** + * By default, MediaWiki checks if the client supports cookies during the + * login process, so that it can display an informative error message if + * cookies are disabled. Set this to true if you want to disable this cookie + * check. + */ +$wgDisableCookieCheck = false; + +/** + * Cookies generated by MediaWiki have names starting with this prefix. Set it + * to a string to use a custom prefix. Setting it to false causes the database + * name to be used as a prefix. + */ +$wgCookiePrefix = false; + +/** + * Set authentication cookies to HttpOnly to prevent access by JavaScript, + * in browsers that support this feature. This can mitigates some classes of + * XSS attack. + */ +$wgCookieHttpOnly = true; + +/** + * A list of cookies that vary the cache (for use by extensions) + */ +$wgCacheVaryCookies = []; + +/** + * Override to customise the session name + */ +$wgSessionName = false; + +/** + * Whether to set a cookie when a user is autoblocked. Doing so means that a blocked user, even + * after logging out and moving to a new IP address, will still be blocked. This cookie will contain + * an authentication code if $wgSecretKey is set, or otherwise will just be the block ID (in + * which case there is a possibility of an attacker discovering the names of revdeleted users, so + * it is best to use this in conjunction with $wgSecretKey being set). + */ +$wgCookieSetOnAutoblock = false; + +/** @} */ # end of cookie settings } + +/************************************************************************//** + * @name LaTeX (mathematical formulas) + * @{ + */ + +/** + * To use inline TeX, you need to compile 'texvc' (in the 'math' subdirectory of + * the MediaWiki package and have latex, dvips, gs (ghostscript), andconvert + * (ImageMagick) installed and available in the PATH. + * Please see math/README for more information. + */ +$wgUseTeX = false; + +/** @} */ # end LaTeX } + +/************************************************************************//** + * @name Profiling, testing and debugging + * + * To enable profiling, edit StartProfiler.php + * + * @{ + */ + +/** + * Filename for debug logging. See https://www.mediawiki.org/wiki/How_to_debug + * The debug log file should be not be publicly accessible if it is used, as it + * may contain private data. + */ +$wgDebugLogFile = ''; + +/** + * Prefix for debug log lines + */ +$wgDebugLogPrefix = ''; + +/** + * If true, instead of redirecting, show a page with a link to the redirect + * destination. This allows for the inspection of PHP error messages, and easy + * resubmission of form data. For developer use only. + */ +$wgDebugRedirects = false; + +/** + * If true, log debugging data from action=raw and load.php. + * This is normally false to avoid overlapping debug entries due to gen=css + * and gen=js requests. + */ +$wgDebugRawPage = false; + +/** + * Send debug data to an HTML comment in the output. + * + * This may occasionally be useful when supporting a non-technical end-user. + * It's more secure than exposing the debug log file to the web, since the + * output only contains private data for the current user. But it's not ideal + * for development use since data is lost on fatal errors and redirects. + */ +$wgDebugComments = false; + +/** + * Write SQL queries to the debug log. + * + * This setting is only used $wgLBFactoryConf['class'] is set to + * '\Wikimedia\Rdbms\LBFactorySimple' and $wgDBservers is an empty array; otherwise + * the DBO_DEBUG flag must be set in the 'flags' option of the database + * connection to achieve the same functionality. + */ +$wgDebugDumpSql = false; + +/** + * Performance expectations for DB usage + * + * @since 1.26 + */ +$wgTrxProfilerLimits = [ + // HTTP GET/HEAD requests. + // Master queries should not happen on GET requests + 'GET' => [ + 'masterConns' => 0, + 'writes' => 0, + 'readQueryTime' => 5 + ], + // HTTP POST requests. + // Master reads and writes will happen for a subset of these. + 'POST' => [ + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 1000 + ], + 'POST-nonwrite' => [ + 'masterConns' => 0, + 'writes' => 0, + 'readQueryTime' => 5 + ], + // Deferred updates that run after HTTP response is sent for GET requests + 'PostSend-GET' => [ + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 1000, + // Log master queries under the post-send entry point as they are discouraged + 'masterConns' => 0, + 'writes' => 0, + ], + // Deferred updates that run after HTTP response is sent for POST requests + 'PostSend-POST' => [ + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 1000 + ], + // Background job runner + 'JobRunner' => [ + 'readQueryTime' => 30, + 'writeQueryTime' => 5, + 'maxAffected' => 500 // ballpark of $wgUpdateRowsPerQuery + ], + // Command-line scripts + 'Maintenance' => [ + 'writeQueryTime' => 5, + 'maxAffected' => 1000 + ] +]; + +/** + * Map of string log group names to log destinations. + * + * If set, wfDebugLog() output for that group will go to that file instead + * of the regular $wgDebugLogFile. Useful for enabling selective logging + * in production. + * + * Log destinations may be one of the following: + * - false to completely remove from the output, including from $wgDebugLogFile. + * - string values specifying a filename or URI. + * - associative array with keys: + * - 'destination' desired filename or URI. + * - 'sample' an integer value, specifying a sampling factor (optional) + * - 'level' A \Psr\Log\LogLevel constant, indicating the minimum level + * to log (optional, since 1.25) + * + * @par Example: + * @code + * $wgDebugLogGroups['redis'] = '/var/log/mediawiki/redis.log'; + * @endcode + * + * @par Advanced example: + * @code + * $wgDebugLogGroups['memcached'] = [ + * 'destination' => '/var/log/mediawiki/memcached.log', + * 'sample' => 1000, // log 1 message out of every 1,000. + * 'level' => \Psr\Log\LogLevel::WARNING + * ]; + * @endcode + */ +$wgDebugLogGroups = []; + +/** + * Default service provider for creating Psr\Log\LoggerInterface instances. + * + * The value should be an array suitable for use with + * ObjectFactory::getObjectFromSpec(). The created object is expected to + * implement the MediaWiki\Logger\Spi interface. See ObjectFactory for additional + * details. + * + * Alternately the MediaWiki\Logger\LoggerFactory::registerProvider method can + * be called to inject an MediaWiki\Logger\Spi instance into the LoggerFactory + * and bypass the use of this configuration variable entirely. + * + * @par To completely disable logging: + * @code + * $wgMWLoggerDefaultSpi = [ 'class' => \MediaWiki\Logger\NullSpi::class ]; + * @endcode + * + * @since 1.25 + * @var array $wgMWLoggerDefaultSpi + * @see MwLogger + */ +$wgMWLoggerDefaultSpi = [ + 'class' => \MediaWiki\Logger\LegacySpi::class, +]; + +/** + * Display debug data at the bottom of the main content area. + * + * Useful for developers and technical users trying to working on a closed wiki. + */ +$wgShowDebug = false; + +/** + * Prefix debug messages with relative timestamp. Very-poor man's profiler. + * Since 1.19 also includes memory usage. + */ +$wgDebugTimestamps = false; + +/** + * Print HTTP headers for every request in the debug information. + */ +$wgDebugPrintHttpHeaders = true; + +/** + * Show the contents of $wgHooks in Special:Version + */ +$wgSpecialVersionShowHooks = false; + +/** + * Whether to show "we're sorry, but there has been a database error" pages. + * Displaying errors aids in debugging, but may display information useful + * to an attacker. + */ +$wgShowSQLErrors = false; + +/** + * If set to true, uncaught exceptions will print a complete stack trace + * to output. This should only be used for debugging, as it may reveal + * private information in function parameters due to PHP's backtrace + * formatting. + */ +$wgShowExceptionDetails = false; + +/** + * If true, show a backtrace for database errors + * + * @note This setting only applies when connection errors and query errors are + * reported in the normal manner. $wgShowExceptionDetails applies in other cases, + * including those in which an uncaught exception is thrown from within the + * exception handler. + */ +$wgShowDBErrorBacktrace = false; + +/** + * If true, send the exception backtrace to the error log + */ +$wgLogExceptionBacktrace = true; + +/** + * If true, the MediaWiki error handler passes errors/warnings to the default error handler + * after logging them. The setting is ignored when the track_errors php.ini flag is true. + */ +$wgPropagateErrors = true; + +/** + * Expose backend server host names through the API and various HTML comments + */ +$wgShowHostnames = false; + +/** + * Override server hostname detection with a hardcoded value. + * Should be a string, default false. + * @since 1.20 + */ +$wgOverrideHostname = false; + +/** + * If set to true MediaWiki will throw notices for some possible error + * conditions and for deprecated functions. + */ +$wgDevelopmentWarnings = false; + +/** + * Release limitation to wfDeprecated warnings, if set to a release number + * development warnings will not be generated for deprecations added in releases + * after the limit. + */ +$wgDeprecationReleaseLimit = false; + +/** + * Only record profiling info for pages that took longer than this + * @deprecated since 1.25: set $wgProfiler['threshold'] instead. + */ +$wgProfileLimit = 0.0; + +/** + * Don't put non-profiling info into log file + * + * @deprecated since 1.23, set the log file in + * $wgDebugLogGroups['profileoutput'] instead. + */ +$wgProfileOnly = false; + +/** + * Destination of statsd metrics. + * + * A host or host:port of a statsd server. Port defaults to 8125. + * + * If not set, statsd metrics will not be collected. + * + * @see wfLogProfilingData + * @since 1.25 + */ +$wgStatsdServer = false; + +/** + * Prefix for metric names sent to $wgStatsdServer. + * + * @see MediaWikiServices::getStatsdDataFactory + * @see BufferingStatsdDataFactory + * @since 1.25 + */ +$wgStatsdMetricPrefix = 'MediaWiki'; + +/** + * Sampling rate for statsd metrics as an associative array of patterns and rates. + * Patterns are Unix shell patterns (e.g. 'MediaWiki.api.*'). + * Rates are sampling probabilities (e.g. 0.1 means 1 in 10 events are sampled). + * @since 1.28 + */ +$wgStatsdSamplingRates = [ + 'wanobjectcache:*' => 0.001 +]; + +/** + * InfoAction retrieves a list of transclusion links (both to and from). + * This number puts a limit on that query in the case of highly transcluded + * templates. + */ +$wgPageInfoTransclusionLimit = 50; + +/** + * Set this to an integer to only do synchronous site_stats updates + * one every *this many* updates. The other requests go into pending + * delta values in $wgMemc. Make sure that $wgMemc is a global cache. + * If set to -1, updates *only* go to $wgMemc (useful for daemons). + */ +$wgSiteStatsAsyncFactor = false; + +/** + * Parser test suite files to be run by parserTests.php when no specific + * filename is passed to it. + * + * Extensions using extension.json will have any *.txt file in a + * tests/parser/ directory automatically run. + * + * Core tests can be added to ParserTestRunner::$coreTestFiles. + * + * Use full paths. + * + * @deprecated since 1.30 + */ +$wgParserTestFiles = []; + +/** + * Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit). + */ +$wgEnableJavaScriptTest = false; + +/** + * Overwrite the caching key prefix with custom value. + * @since 1.19 + */ +$wgCachePrefix = false; + +/** + * Display the new debugging toolbar. This also enables profiling on database + * queries and other useful output. + * Will be ignored if $wgUseFileCache or $wgUseSquid is enabled. + * + * @since 1.19 + */ +$wgDebugToolbar = false; + +/** @} */ # end of profiling, testing and debugging } + +/************************************************************************//** + * @name Search + * @{ + */ + +/** + * Set this to true to disable the full text search feature. + */ +$wgDisableTextSearch = false; + +/** + * Set to true to have nicer highlighted text in search results, + * by default off due to execution overhead + */ +$wgAdvancedSearchHighlighting = false; + +/** + * Regexp to match word boundaries, defaults for non-CJK languages + * should be empty for CJK since the words are not separate + */ +$wgSearchHighlightBoundaries = '[\p{Z}\p{P}\p{C}]'; + +/** + * Template for OpenSearch suggestions, defaults to API action=opensearch + * + * Sites with heavy load would typically have these point to a custom + * PHP wrapper to avoid firing up mediawiki for every keystroke + * + * Placeholders: {searchTerms} + * + * @deprecated since 1.25 Use $wgOpenSearchTemplates['application/x-suggestions+json'] instead + */ +$wgOpenSearchTemplate = false; + +/** + * Templates for OpenSearch suggestions, defaults to API action=opensearch + * + * Sites with heavy load would typically have these point to a custom + * PHP wrapper to avoid firing up mediawiki for every keystroke + * + * Placeholders: {searchTerms} + */ +$wgOpenSearchTemplates = [ + 'application/x-suggestions+json' => false, + 'application/x-suggestions+xml' => false, +]; + +/** + * Enable OpenSearch suggestions requested by MediaWiki. Set this to + * false if you've disabled scripts that use api?action=opensearch and + * want reduce load caused by cached scripts still pulling suggestions. + * It will let the API fallback by responding with an empty array. + */ +$wgEnableOpenSearchSuggest = true; + +/** + * Integer defining default number of entries to show on + * OpenSearch call. + */ +$wgOpenSearchDefaultLimit = 10; + +/** + * Minimum length of extract in <Description>. Actual extracts will last until the end of sentence. + */ +$wgOpenSearchDescriptionLength = 100; + +/** + * Expiry time for search suggestion responses + */ +$wgSearchSuggestCacheExpiry = 1200; + +/** + * If you've disabled search semi-permanently, this also disables updates to the + * table. If you ever re-enable, be sure to rebuild the search table. + */ +$wgDisableSearchUpdate = false; + +/** + * List of namespaces which are searched by default. + * + * @par Example: + * @code + * $wgNamespacesToBeSearchedDefault[NS_MAIN] = true; + * $wgNamespacesToBeSearchedDefault[NS_PROJECT] = true; + * @endcode + */ +$wgNamespacesToBeSearchedDefault = [ + NS_MAIN => true, +]; + +/** + * Disable the internal MySQL-based search, to allow it to be + * implemented by an extension instead. + */ +$wgDisableInternalSearch = false; + +/** + * Set this to a URL to forward search requests to some external location. + * If the URL includes '$1', this will be replaced with the URL-encoded + * search term. + * + * @par Example: + * To forward to Google you'd have something like: + * @code + * $wgSearchForwardUrl = + * 'https://www.google.com/search?q=$1' . + * '&domains=https://example.com' . + * '&sitesearch=https://example.com' . + * '&ie=utf-8&oe=utf-8'; + * @endcode + */ +$wgSearchForwardUrl = null; + +/** + * Search form behavior. + * - true = use Go & Search buttons + * - false = use Go button & Advanced search link + */ +$wgUseTwoButtonsSearchForm = true; + +/** + * Array of namespaces to generate a Google sitemap for when the + * maintenance/generateSitemap.php script is run, or false if one is to be + * generated for all namespaces. + */ +$wgSitemapNamespaces = false; + +/** + * Custom namespace priorities for sitemaps. Setting this will allow you to + * set custom priorities to namespaces when sitemaps are generated using the + * maintenance/generateSitemap.php script. + * + * This should be a map of namespace IDs to priority + * @par Example: + * @code + * $wgSitemapNamespacesPriorities = [ + * NS_USER => '0.9', + * NS_HELP => '0.0', + * ]; + * @endcode + */ +$wgSitemapNamespacesPriorities = false; + +/** + * If true, searches for IP addresses will be redirected to that IP's + * contributions page. E.g. searching for "1.2.3.4" will redirect to + * [[Special:Contributions/1.2.3.4]] + */ +$wgEnableSearchContributorsByIP = true; + +/** @} */ # end of search settings + +/************************************************************************//** + * @name Edit user interface + * @{ + */ + +/** + * Path to the GNU diff3 utility. If the file doesn't exist, edit conflicts will + * fall back to the old behavior (no merging). + */ +$wgDiff3 = '/usr/bin/diff3'; + +/** + * Path to the GNU diff utility. + */ +$wgDiff = '/usr/bin/diff'; + +/** + * Which namespaces have special treatment where they should be preview-on-open + * Internally only Category: pages apply, but using this extensions (e.g. Semantic MediaWiki) + * can specify namespaces of pages they have special treatment for + */ +$wgPreviewOnOpenNamespaces = [ + NS_CATEGORY => true +]; + +/** + * Enable the UniversalEditButton for browsers that support it + * (currently only Firefox with an extension) + * See http://universaleditbutton.org for more background information + */ +$wgUniversalEditButton = true; + +/** + * If user doesn't specify any edit summary when making a an edit, MediaWiki + * will try to automatically create one. This feature can be disabled by set- + * ting this variable false. + */ +$wgUseAutomaticEditSummaries = true; + +/** @} */ # end edit UI } + +/************************************************************************//** + * @name Maintenance + * See also $wgSiteNotice + * @{ + */ + +/** + * @cond file_level_code + * Set $wgCommandLineMode if it's not set already, to avoid notices + */ +if ( !isset( $wgCommandLineMode ) ) { + $wgCommandLineMode = false; +} +/** @endcond */ + +/** + * For colorized maintenance script output, is your terminal background dark ? + */ +$wgCommandLineDarkBg = false; + +/** + * Set this to a string to put the wiki into read-only mode. The text will be + * used as an explanation to users. + * + * This prevents most write operations via the web interface. Cache updates may + * still be possible. To prevent database writes completely, use the read_only + * option in MySQL. + */ +$wgReadOnly = null; + +/** + * Set this to true to put the wiki watchlists into read-only mode. + * @var bool + * @since 1.31 + */ +$wgReadOnlyWatchedItemStore = false; + +/** + * If this lock file exists (size > 0), the wiki will be forced into read-only mode. + * Its contents will be shown to users as part of the read-only warning + * message. + * + * Will default to "{$wgUploadDirectory}/lock_yBgMBwiR" in Setup.php + */ +$wgReadOnlyFile = false; + +/** + * When you run the web-based upgrade utility, it will tell you what to set + * this to in order to authorize the upgrade process. It will subsequently be + * used as a password, to authorize further upgrades. + * + * For security, do not set this to a guessable string. Use the value supplied + * by the install/upgrade process. To cause the upgrader to generate a new key, + * delete the old key from LocalSettings.php. + */ +$wgUpgradeKey = false; + +/** + * Fully specified path to git binary + */ +$wgGitBin = '/usr/bin/git'; + +/** + * Map GIT repository URLs to viewer URLs to provide links in Special:Version + * + * Key is a pattern passed to preg_match() and preg_replace(), + * without the delimiters (which are #) and must match the whole URL. + * The value is the replacement for the key (it can contain $1, etc.) + * %h will be replaced by the short SHA-1 (7 first chars) and %H by the + * full SHA-1 of the HEAD revision. + * %r will be replaced with a URL-encoded version of $1. + * %R will be replaced with $1 and no URL-encoding + * + * @since 1.20 + */ +$wgGitRepositoryViewers = [ + 'https://(?:[a-z0-9_]+@)?gerrit.wikimedia.org/r/(?:p/)?(.*)' => + 'https://gerrit.wikimedia.org/g/%R/+/%H', + 'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' => + 'https://gerrit.wikimedia.org/g/%R/+/%H', +]; + +/** @} */ # End of maintenance } + +/************************************************************************//** + * @name Recent changes, new pages, watchlist and history + * @{ + */ + +/** + * Recentchanges items are periodically purged; entries older than this many + * seconds will go. + * Default: 90 days = about three months + */ +$wgRCMaxAge = 90 * 24 * 3600; + +/** + * Page watchers inactive for more than this many seconds are considered inactive. + * Used mainly by action=info. Default: 180 days = about six months. + * @since 1.26 + */ +$wgWatchersMaxAge = 180 * 24 * 3600; + +/** + * If active watchers (per above) are this number or less, do not disclose it. + * Left to 1, prevents unprivileged users from knowing for sure that there are 0. + * Set to -1 if you want to always complement watchers count with this info. + * @since 1.26 + */ +$wgUnwatchedPageSecret = 1; + +/** + * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers + * higher than what will be stored. Note that this is disabled by default + * because we sometimes do have RC data which is beyond the limit for some + * reason, and some users may use the high numbers to display that data which + * is still there. + */ +$wgRCFilterByAge = false; + +/** + * List of Limits options to list in the Special:Recentchanges and + * Special:Recentchangeslinked pages. + */ +$wgRCLinkLimits = [ 50, 100, 250, 500 ]; + +/** + * List of Days options to list in the Special:Recentchanges and + * Special:Recentchangeslinked pages. + */ +$wgRCLinkDays = [ 1, 3, 7, 14, 30 ]; + +/** + * Configuration for feeds to which notifications about recent changes will be sent. + * + * The following feed classes are available by default: + * - 'UDPRCFeedEngine' - sends recent changes over UDP to the specified server. + * - 'RedisPubSubFeedEngine' - send recent changes to Redis. + * + * Only 'class' or 'uri' is required. If 'uri' is set instead of 'class', then + * RecentChange::getEngine() is used to determine the class. All options are + * passed to the constructor. + * + * Common options: + * - 'class' -- The class to use for this feed (must implement RCFeed). + * - 'omit_bots' -- Exclude bot edits from the feed. (default: false) + * - 'omit_anon' -- Exclude anonymous edits from the feed. (default: false) + * - 'omit_user' -- Exclude edits by registered users from the feed. (default: false) + * - 'omit_minor' -- Exclude minor edits from the feed. (default: false) + * - 'omit_patrolled' -- Exclude patrolled edits from the feed. (default: false) + * + * FormattedRCFeed-specific options: + * - 'uri' -- [required] The address to which the messages are sent. + * The uri scheme of this string will be looked up in $wgRCEngines + * to determine which RCFeedEngine class to use. + * - 'formatter' -- [required] The class (implementing RCFeedFormatter) which will + * produce the text to send. This can also be an object of the class. + * Formatters available by default: JSONRCFeedFormatter, XMLRCFeedFormatter, + * IRCColourfulRCFeedFormatter. + * + * IRCColourfulRCFeedFormatter-specific options: + * - 'add_interwiki_prefix' -- whether the titles should be prefixed with + * the first entry in the $wgLocalInterwikis array (or the value of + * $wgLocalInterwiki, if set) + * + * JSONRCFeedFormatter-specific options: + * - 'channel' -- if set, the 'channel' parameter is also set in JSON values. + * + * @example $wgRCFeeds['example'] = [ + * 'uri' => 'udp://localhost:1336', + * 'formatter' => 'JSONRCFeedFormatter', + * 'add_interwiki_prefix' => false, + * 'omit_bots' => true, + * ]; + * @example $wgRCFeeds['example'] = [ + * 'uri' => 'udp://localhost:1338', + * 'formatter' => 'IRCColourfulRCFeedFormatter', + * 'add_interwiki_prefix' => false, + * 'omit_bots' => true, + * ]; + * @example $wgRCFeeds['example'] = [ + * 'class' => ExampleRCFeed::class, + * ]; + * @since 1.22 + */ +$wgRCFeeds = []; + +/** + * Used by RecentChange::getEngine to find the correct engine for a given URI scheme. + * Keys are scheme names, values are names of FormattedRCFeed sub classes. + * @since 1.22 + */ +$wgRCEngines = [ + 'redis' => RedisPubSubFeedEngine::class, + 'udp' => UDPRCFeedEngine::class, +]; + +/** + * Treat category membership changes as a RecentChange. + * Changes are mentioned in RC for page actions as follows: + * - creation: pages created with categories are mentioned + * - edit: category additions/removals to existing pages are mentioned + * - move: nothing is mentioned (unless templates used depend on the title) + * - deletion: nothing is mentioned + * - undeletion: nothing is mentioned + * + * @since 1.27 + */ +$wgRCWatchCategoryMembership = false; + +/** + * Use RC Patrolling to check for vandalism (from recent changes and watchlists) + * New pages and new files are included. + * + * @note If you disable all patrolling features, you probably also want to + * remove 'patrol' from $wgFilterLogTypes so a show/hide link isn't shown on + * Special:Log. + */ +$wgUseRCPatrol = true; + +/** + * Whether a preference is displayed for structured change filters. + * If false, no preference is displayed and structured change filters are disabled. + * If true, structured change filters are *enabled* by default, and a preference is displayed + * that lets users disable them. + * + * Temporary variable during development and will be removed. + * + * @since 1.30 + */ +$wgStructuredChangeFiltersShowPreference = false; + +/** + * Whether to enable RCFilters app on Special:Watchlist + * + * Temporary variable during development and will be removed. + */ +$wgStructuredChangeFiltersOnWatchlist = false; + +/** + * Polling rate, in seconds, used by the 'live update' and 'view newest' features + * of the RCFilters app on SpecialRecentChanges and Special:Watchlist. + * 0 to disable completely. + */ +$wgStructuredChangeFiltersLiveUpdatePollingRate = 3; + +/** + * Use new page patrolling to check new pages on Special:Newpages + * + * @note If you disable all patrolling features, you probably also want to + * remove 'patrol' from $wgFilterLogTypes so a show/hide link isn't shown on + * Special:Log. + */ +$wgUseNPPatrol = true; + +/** + * Use file patrolling to check new files on Special:Newfiles + * + * @note If you disable all patrolling features, you probably also want to + * remove 'patrol' from $wgFilterLogTypes so a show/hide link isn't shown on + * Special:Log. + * + * @since 1.27 + */ +$wgUseFilePatrol = true; + +/** + * Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages + */ +$wgFeed = true; + +/** + * Set maximum number of results to return in syndication feeds (RSS, Atom) for + * eg Recentchanges, Newpages. + */ +$wgFeedLimit = 50; + +/** + * _Minimum_ timeout for cached Recentchanges feed, in seconds. + * A cached version will continue to be served out even if changes + * are made, until this many seconds runs out since the last render. + * + * If set to 0, feed caching is disabled. Use this for debugging only; + * feed generation can be pretty slow with diffs. + */ +$wgFeedCacheTimeout = 60; + +/** + * When generating Recentchanges RSS/Atom feed, diffs will not be generated for + * pages larger than this size. + */ +$wgFeedDiffCutoff = 32768; + +/** + * Override the site's default RSS/ATOM feed for recentchanges that appears on + * every page. Some sites might have a different feed they'd like to promote + * instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one). + * Should be a format as key (either 'rss' or 'atom') and an URL to the feed + * as value. + * @par Example: + * Configure the 'atom' feed to https://example.com/somefeed.xml + * @code + * $wgSiteFeed['atom'] = "https://example.com/somefeed.xml"; + * @endcode + */ +$wgOverrideSiteFeed = []; + +/** + * Available feeds objects. + * Should probably only be defined when a page is syndicated ie when + * $wgOut->isSyndicated() is true. + */ +$wgFeedClasses = [ + 'rss' => RSSFeed::class, + 'atom' => AtomFeed::class, +]; + +/** + * Which feed types should we provide by default? This can include 'rss', + * 'atom', neither, or both. + */ +$wgAdvertisedFeedTypes = [ 'atom' ]; + +/** + * Show watching users in recent changes, watchlist and page history views + */ +$wgRCShowWatchingUsers = false; # UPO + +/** + * Show the amount of changed characters in recent changes + */ +$wgRCShowChangedSize = true; + +/** + * If the difference between the character counts of the text + * before and after the edit is below that value, the value will be + * highlighted on the RC page. + */ +$wgRCChangedSizeThreshold = 500; + +/** + * Show "Updated (since my last visit)" marker in RC view, watchlist and history + * view for watched pages with new changes + */ +$wgShowUpdatedMarker = true; + +/** + * Disable links to talk pages of anonymous users (IPs) in listings on special + * pages like page history, Special:Recentchanges, etc. + */ +$wgDisableAnonTalk = false; + +/** + * Allow filtering by change tag in recentchanges, history, etc + * Has no effect if no tags are defined in valid_tag. + */ +$wgUseTagFilter = true; + +/** + * List of core tags to enable. Available tags are: + * - 'mw-contentmodelchange': Edit changes content model of a page + * - 'mw-new-redirect': Edit makes new redirect page (new page or by changing content page) + * - 'mw-removed-redirect': Edit changes an existing redirect into a non-redirect + * - 'mw-changed-redirect-target': Edit changes redirect target + * - 'mw-blank': Edit completely blanks the page + * - 'mw-replace': Edit removes more than 90% of the content + * - 'mw-rollback': Edit is a rollback, made through the rollback link or rollback API + * - 'mw-undo': Edit made through an undo link + * + * @var array + * @since 1.31 + */ +$wgSoftwareTags = [ + 'mw-contentmodelchange' => true, + 'mw-new-redirect' => true, + 'mw-removed-redirect' => true, + 'mw-changed-redirect-target' => true, + 'mw-blank' => true, + 'mw-replace' => true, + 'mw-rollback' => true, + 'mw-undo' => true, +]; + +/** + * If set to an integer, pages that are watched by this many users or more + * will not require the unwatchedpages permission to view the number of + * watchers. + * + * @since 1.21 + */ +$wgUnwatchedPageThreshold = false; + +/** + * Flags (letter symbols) shown in recent changes and watchlist to indicate + * certain types of edits. + * + * To register a new one: + * @code + * $wgRecentChangesFlags['flag'] => [ + * // message for the letter displayed next to rows on changes lists + * 'letter' => 'letter-msg', + * // message for the tooltip of the letter + * 'title' => 'tooltip-msg', + * // optional (defaults to 'tooltip-msg'), message to use in the legend box + * 'legend' => 'legend-msg', + * // optional (defaults to 'flag'), CSS class to put on changes lists rows + * 'class' => 'css-class', + * // optional (defaults to 'any'), how top-level flag is determined. 'any' + * // will set the top-level flag if any line contains the flag, 'all' will + * // only be set if all lines contain the flag. + * 'grouping' => 'any', + * ]; + * @endcode + * + * @since 1.22 + */ +$wgRecentChangesFlags = [ + 'newpage' => [ + 'letter' => 'newpageletter', + 'title' => 'recentchanges-label-newpage', + 'legend' => 'recentchanges-legend-newpage', + 'grouping' => 'any', + ], + 'minor' => [ + 'letter' => 'minoreditletter', + 'title' => 'recentchanges-label-minor', + 'legend' => 'recentchanges-legend-minor', + 'class' => 'minoredit', + 'grouping' => 'all', + ], + 'bot' => [ + 'letter' => 'boteditletter', + 'title' => 'recentchanges-label-bot', + 'legend' => 'recentchanges-legend-bot', + 'class' => 'botedit', + 'grouping' => 'all', + ], + 'unpatrolled' => [ + 'letter' => 'unpatrolledletter', + 'title' => 'recentchanges-label-unpatrolled', + 'legend' => 'recentchanges-legend-unpatrolled', + 'grouping' => 'any', + ], +]; + +/** @} */ # end RC/watchlist } + +/************************************************************************//** + * @name Copyright and credits settings + * @{ + */ + +/** + * Override for copyright metadata. + * + * This is the name of the page containing information about the wiki's copyright status, + * which will be added as a link in the footer if it is specified. It overrides + * $wgRightsUrl if both are specified. + */ +$wgRightsPage = null; + +/** + * Set this to specify an external URL containing details about the content license used on your + * wiki. + * If $wgRightsPage is set then this setting is ignored. + */ +$wgRightsUrl = null; + +/** + * If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the + * link. + * If using $wgRightsUrl then this value must be specified. If using $wgRightsPage then the name + * of the page will also be used as the link if this variable is not set. + */ +$wgRightsText = null; + +/** + * Override for copyright metadata. + */ +$wgRightsIcon = null; + +/** + * Set this to true if you want detailed copyright information forms on Upload. + */ +$wgUseCopyrightUpload = false; + +/** + * Set this to the number of authors that you want to be credited below an + * article text. Set it to zero to hide the attribution block, and a negative + * number (like -1) to show all authors. Note that this will require 2-3 extra + * database hits, which can have a not insignificant impact on performance for + * large wikis. + */ +$wgMaxCredits = 0; + +/** + * If there are more than $wgMaxCredits authors, show $wgMaxCredits of them. + * Otherwise, link to a separate credits page. + */ +$wgShowCreditsIfMax = true; + +/** @} */ # end of copyright and credits settings } + +/************************************************************************//** + * @name Import / Export + * @{ + */ + +/** + * List of interwiki prefixes for wikis we'll accept as sources for + * Special:Import and API action=import. Since complete page history can be + * imported, these should be 'trusted'. + * + * This can either be a regular array, or an associative map specifying + * subprojects on the interwiki map of the target wiki, or a mix of the two, + * e.g. + * @code + * $wgImportSources = [ + * 'wikipedia' => [ 'cs', 'en', 'fr', 'zh' ], + * 'wikispecies', + * 'wikia' => [ 'animanga', 'brickipedia', 'desserts' ], + * ]; + * @endcode + * + * If you have a very complex import sources setup, you can lazy-load it using + * the ImportSources hook. + * + * If a user has the 'import' permission but not the 'importupload' permission, + * they will only be able to run imports through this transwiki interface. + */ +$wgImportSources = []; + +/** + * Optional default target namespace for interwiki imports. + * Can use this to create an incoming "transwiki"-style queue. + * Set to numeric key, not the name. + * + * Users may override this in the Special:Import dialog. + */ +$wgImportTargetNamespace = null; + +/** + * If set to false, disables the full-history option on Special:Export. + * This is currently poorly optimized for long edit histories, so is + * disabled on Wikimedia's sites. + */ +$wgExportAllowHistory = true; + +/** + * If set nonzero, Special:Export requests for history of pages with + * more revisions than this will be rejected. On some big sites things + * could get bogged down by very very long pages. + */ +$wgExportMaxHistory = 0; + +/** + * Return distinct author list (when not returning full history) + */ +$wgExportAllowListContributors = false; + +/** + * If non-zero, Special:Export accepts a "pagelink-depth" parameter + * up to this specified level, which will cause it to include all + * pages linked to from the pages you specify. Since this number + * can become *insanely large* and could easily break your wiki, + * it's disabled by default for now. + * + * @warning There's a HARD CODED limit of 5 levels of recursion to prevent a + * crazy-big export from being done by someone setting the depth number too + * high. In other words, last resort safety net. + */ +$wgExportMaxLinkDepth = 0; + +/** + * Whether to allow the "export all pages in namespace" option + */ +$wgExportFromNamespaces = false; + +/** + * Whether to allow exporting the entire wiki into a single file + */ +$wgExportAllowAll = false; + +/** + * Maximum number of pages returned by the GetPagesFromCategory and + * GetPagesFromNamespace functions. + * + * @since 1.27 + */ +$wgExportPagelistLimit = 5000; + +/** @} */ # end of import/export } + +/*************************************************************************//** + * @name Extensions + * @{ + */ + +/** + * A list of callback functions which are called once MediaWiki is fully + * initialised + */ +$wgExtensionFunctions = []; + +/** + * Extension messages files. + * + * Associative array mapping extension name to the filename where messages can be + * found. The file should contain variable assignments. Any of the variables + * present in languages/messages/MessagesEn.php may be defined, but $messages + * is the most common. + * + * Variables defined in extensions will override conflicting variables defined + * in the core. + * + * Since MediaWiki 1.23, use of this variable to define messages is discouraged; instead, store + * messages in JSON format and use $wgMessagesDirs. For setting other variables than + * $messages, $wgExtensionMessagesFiles should still be used. Use a DIFFERENT key because + * any entry having a key that also exists in $wgMessagesDirs will be ignored. + * + * Extensions using the JSON message format can preserve backward compatibility with + * earlier versions of MediaWiki by using a compatibility shim, such as one generated + * by the generateJsonI18n.php maintenance script, listing it under the SAME key + * as for the $wgMessagesDirs entry. + * + * @par Example: + * @code + * $wgExtensionMessagesFiles['ConfirmEdit'] = __DIR__.'/ConfirmEdit.i18n.php'; + * @endcode + */ +$wgExtensionMessagesFiles = []; + +/** + * Extension messages directories. + * + * Associative array mapping extension name to the path of the directory where message files can + * be found. The message files are expected to be JSON files named for their language code, e.g. + * en.json, de.json, etc. Extensions with messages in multiple places may specify an array of + * message directories. + * + * Message directories in core should be added to LocalisationCache::getMessagesDirs() + * + * @par Simple example: + * @code + * $wgMessagesDirs['Example'] = __DIR__ . '/i18n'; + * @endcode + * + * @par Complex example: + * @code + * $wgMessagesDirs['Example'] = [ + * __DIR__ . '/lib/ve/i18n', + * __DIR__ . '/lib/oojs-ui/i18n', + * __DIR__ . '/i18n', + * ] + * @endcode + * @since 1.23 + */ +$wgMessagesDirs = []; + +/** + * Array of files with list(s) of extension entry points to be used in + * maintenance/mergeMessageFileList.php + * @since 1.22 + */ +$wgExtensionEntryPointListFiles = []; + +/** + * Parser output hooks. + * This is an associative array where the key is an extension-defined tag + * (typically the extension name), and the value is a PHP callback. + * These will be called as an OutputPageParserOutput hook, if the relevant + * tag has been registered with the parser output object. + * + * Registration is done with $pout->addOutputHook( $tag, $data ). + * + * The callback has the form: + * @code + * function outputHook( $outputPage, $parserOutput, $data ) { ... } + * @endcode + */ +$wgParserOutputHooks = []; + +/** + * Whether to include the NewPP limit report as a HTML comment + */ +$wgEnableParserLimitReporting = true; + +/** + * List of valid skin names + * + * The key should be the name in all lower case, the value should be a properly + * cased name for the skin. This value will be prefixed with "Skin" to create + * the class name of the skin to load. Use Skin::getSkinNames() as an accessor + * if you wish to have access to the full list. + */ +$wgValidSkinNames = []; + +/** + * Special page list. This is an associative array mapping the (canonical) names of + * special pages to either a class name to be instantiated, or a callback to use for + * creating the special page object. In both cases, the result must be an instance of + * SpecialPage. + */ +$wgSpecialPages = []; + +/** + * Array mapping class names to filenames, for autoloading. + */ +$wgAutoloadClasses = []; + +/** + * Switch controlling legacy case-insensitive classloading. + * Do not disable if your wiki must support data created by PHP4, or by + * MediaWiki 1.4 or earlier. + */ +$wgAutoloadAttemptLowercase = true; + +/** + * An array of information about installed extensions keyed by their type. + * + * All but 'name', 'path' and 'author' can be omitted. + * + * @code + * $wgExtensionCredits[$type][] = [ + * 'path' => __FILE__, + * 'name' => 'Example extension', + * 'namemsg' => 'exampleextension-name', + * 'author' => [ + * 'Foo Barstein', + * ], + * 'version' => '1.9.0', + * 'url' => 'https://example.org/example-extension/', + * 'descriptionmsg' => 'exampleextension-desc', + * 'license-name' => 'GPL-2.0-or-later', + * ]; + * @endcode + * + * The extensions are listed on Special:Version. This page also looks for a file + * named COPYING or LICENSE (optional .txt extension) and provides a link to + * view said file. When the 'license-name' key is specified, this file is + * interpreted as wikitext. + * + * - $type: One of 'specialpage', 'parserhook', 'variable', 'media', 'antispam', + * 'skin', 'api', or 'other', or any additional types as specified through the + * ExtensionTypes hook as used in SpecialVersion::getExtensionTypes(). + * + * - name: Name of extension as an inline string instead of localizable message. + * Do not omit this even if 'namemsg' is provided, as it is used to override + * the path Special:Version uses to find extension's license info, and is + * required for backwards-compatibility with MediaWiki 1.23 and older. + * + * - namemsg (since MW 1.24): A message key for a message containing the + * extension's name, if the name is localizable. (For example, skin names + * usually are.) + * + * - author: A string or an array of strings. Authors can be linked using + * the regular wikitext link syntax. To have an internationalized version of + * "and others" show, add an element "...". This element can also be linked, + * for instance "[https://example ...]". + * + * - descriptionmsg: A message key or an an array with message key and parameters: + * `'descriptionmsg' => 'exampleextension-desc',` + * + * - description: Description of extension as an inline string instead of + * localizable message (omit in favour of 'descriptionmsg'). + * + * - license-name: Short name of the license (used as label for the link), such + * as "GPL-2.0-or-later" or "MIT" (https://spdx.org/licenses/ for a list of identifiers). + */ +$wgExtensionCredits = []; + +/** + * Authentication plugin. + * @var $wgAuth AuthPlugin + * @deprecated since 1.27 use $wgAuthManagerConfig instead + */ +$wgAuth = null; + +/** + * Global list of hooks. + * + * The key is one of the events made available by MediaWiki, you can find + * a description for most of them in docs/hooks.txt. The array is used + * internally by Hook:run(). + * + * The value can be one of: + * + * - A function name: + * @code + * $wgHooks['event_name'][] = $function; + * @endcode + * - A function with some data: + * @code + * $wgHooks['event_name'][] = [ $function, $data ]; + * @endcode + * - A an object method: + * @code + * $wgHooks['event_name'][] = [ $object, 'method' ]; + * @endcode + * - A closure: + * @code + * $wgHooks['event_name'][] = function ( $hookParam ) { + * // Handler code goes here. + * }; + * @endcode + * + * @warning You should always append to an event array or you will end up + * deleting a previous registered hook. + * + * @warning Hook handlers should be registered at file scope. Registering + * handlers after file scope can lead to unexpected results due to caching. + */ +$wgHooks = []; + +/** + * List of service wiring files to be loaded by the default instance of MediaWikiServices. + * Each file listed here is expected to return an associative array mapping service names + * to instantiator functions. Extensions may add wiring files to define their own services. + * However, this cannot be used to replace existing services - use the MediaWikiServices + * hook for that. + * + * @see MediaWikiServices + * @see ServiceContainer::loadWiringFiles() for details on loading service instantiator functions. + * @see docs/injection.txt for an overview of dependency injection in MediaWiki. + */ +$wgServiceWiringFiles = [ + __DIR__ . '/ServiceWiring.php' +]; + +/** + * Maps jobs to their handlers; extensions + * can add to this to provide custom jobs. + * A job handler should either be a class name to be instantiated, + * or (since 1.30) a callback to use for creating the job object. + */ +$wgJobClasses = [ + 'refreshLinks' => RefreshLinksJob::class, + 'deleteLinks' => DeleteLinksJob::class, + 'htmlCacheUpdate' => HTMLCacheUpdateJob::class, + 'sendMail' => EmaillingJob::class, + 'enotifNotify' => EnotifNotifyJob::class, + 'fixDoubleRedirect' => DoubleRedirectJob::class, + 'AssembleUploadChunks' => AssembleUploadChunksJob::class, + 'PublishStashedFile' => PublishStashedFileJob::class, + 'ThumbnailRender' => ThumbnailRenderJob::class, + 'recentChangesUpdate' => RecentChangesUpdateJob::class, + 'refreshLinksPrioritized' => RefreshLinksJob::class, + 'refreshLinksDynamic' => RefreshLinksJob::class, + 'activityUpdateJob' => ActivityUpdateJob::class, + 'categoryMembershipChange' => CategoryMembershipChangeJob::class, + 'clearUserWatchlist' => ClearUserWatchlistJob::class, + 'cdnPurge' => CdnPurgeJob::class, + 'userGroupExpiry' => UserGroupExpiryJob::class, + 'clearWatchlistNotifications' => ClearWatchlistNotificationsJob::class, + 'enqueue' => EnqueueJob::class, // local queue for multi-DC setups + 'null' => NullJob::class, +]; + +/** + * Jobs that must be explicitly requested, i.e. aren't run by job runners unless + * special flags are set. The values here are keys of $wgJobClasses. + * + * These can be: + * - Very long-running jobs. + * - Jobs that you would never want to run as part of a page rendering request. + * - Jobs that you want to run on specialized machines ( like transcoding, or a particular + * machine on your cluster has 'outside' web access you could restrict uploadFromUrl ) + * These settings should be global to all wikis. + */ +$wgJobTypesExcludedFromDefaultQueue = [ 'AssembleUploadChunks', 'PublishStashedFile' ]; + +/** + * Map of job types to how many job "work items" should be run per second + * on each job runner process. The meaning of "work items" varies per job, + * but typically would be something like "pages to update". A single job + * may have a variable number of work items, as is the case with batch jobs. + * This is used by runJobs.php and not jobs run via $wgJobRunRate. + * These settings should be global to all wikis. + * @var float[] + */ +$wgJobBackoffThrottling = []; + +/** + * Make job runners commit changes for replica DB-lag prone jobs one job at a time. + * This is useful if there are many job workers that race on replica DB lag checks. + * If set, jobs taking this many seconds of DB write time have serialized commits. + * + * Note that affected jobs may have worse lock contention. Also, if they affect + * several DBs at once they may have a smaller chance of being atomic due to the + * possibility of connection loss while queueing up to commit. Affected jobs may + * also fail due to the commit lock acquisition timeout. + * + * @var float|bool + * @since 1.26 + */ +$wgJobSerialCommitThreshold = false; + +/** + * Map of job types to configuration arrays. + * This determines which queue class and storage system is used for each job type. + * Job types that do not have explicit configuration will use the 'default' config. + * These settings should be global to all wikis. + */ +$wgJobTypeConf = [ + 'default' => [ 'class' => JobQueueDB::class, 'order' => 'random', 'claimTTL' => 3600 ], +]; + +/** + * Which aggregator to use for tracking which queues have jobs. + * These settings should be global to all wikis. + */ +$wgJobQueueAggregator = [ + 'class' => JobQueueAggregatorNull::class +]; + +/** + * Whether to include the number of jobs that are queued + * for the API's maxlag parameter. + * The total number of jobs will be divided by this to get an + * estimated second of maxlag. Typically bots backoff at maxlag=5, + * so setting this to the max number of jobs that should be in your + * queue divided by 5 should have the effect of stopping bots once + * that limit is hit. + * + * @since 1.29 + */ +$wgJobQueueIncludeInMaxLagFactor = false; + +/** + * Additional functions to be performed with updateSpecialPages. + * Expensive Querypages are already updated. + */ +$wgSpecialPageCacheUpdates = [ + 'Statistics' => [ SiteStatsUpdate::class, 'cacheUpdate' ] +]; + +/** + * Page property link table invalidation lists. When a page property + * changes, this may require other link tables to be updated (eg + * adding __HIDDENCAT__ means the hiddencat tracking category will + * have been added, so the categorylinks table needs to be rebuilt). + * This array can be added to by extensions. + */ +$wgPagePropLinkInvalidations = [ + 'hiddencat' => 'categorylinks', +]; + +/** @} */ # End extensions } + +/*************************************************************************//** + * @name Categories + * @{ + */ + +/** + * Use experimental, DMOZ-like category browser + */ +$wgUseCategoryBrowser = false; + +/** + * On category pages, show thumbnail gallery for images belonging to that + * category instead of listing them as articles. + */ +$wgCategoryMagicGallery = true; + +/** + * Paging limit for categories + */ +$wgCategoryPagingLimit = 200; + +/** + * Specify how category names should be sorted, when listed on a category page. + * A sorting scheme is also known as a collation. + * + * Available values are: + * + * - uppercase: Converts the category name to upper case, and sorts by that. + * + * - identity: Does no conversion. Sorts by binary value of the string. + * + * - uca-default: Provides access to the Unicode Collation Algorithm with + * the default element table. This is a compromise collation which sorts + * all languages in a mediocre way. However, it is better than "uppercase". + * + * To use the uca-default collation, you must have PHP's intl extension + * installed. See https://secure.php.net/manual/en/intl.setup.php . The details of the + * resulting collation will depend on the version of ICU installed on the + * server. + * + * After you change this, you must run maintenance/updateCollation.php to fix + * the sort keys in the database. + * + * Extensions can define there own collations by subclassing Collation + * and using the Collation::factory hook. + */ +$wgCategoryCollation = 'uppercase'; + +/** @} */ # End categories } + +/*************************************************************************//** + * @name Logging + * @{ + */ + +/** + * The logging system has two levels: an event type, which describes the + * general category and can be viewed as a named subset of all logs; and + * an action, which is a specific kind of event that can exist in that + * log type. + */ +$wgLogTypes = [ + '', + 'block', + 'protect', + 'rights', + 'delete', + 'upload', + 'move', + 'import', + 'patrol', + 'merge', + 'suppress', + 'tag', + 'managetags', + 'contentmodel', +]; + +/** + * This restricts log access to those who have a certain right + * Users without this will not see it in the option menu and can not view it + * Restricted logs are not added to recent changes + * Logs should remain non-transcludable + * Format: logtype => permissiontype + */ +$wgLogRestrictions = [ + 'suppress' => 'suppressionlog' +]; + +/** + * Show/hide links on Special:Log will be shown for these log types. + * + * This is associative array of log type => boolean "hide by default" + * + * See $wgLogTypes for a list of available log types. + * + * @par Example: + * @code + * $wgFilterLogTypes = [ 'move' => true, 'import' => false ]; + * @endcode + * + * Will display show/hide links for the move and import logs. Move logs will be + * hidden by default unless the link is clicked. Import logs will be shown by + * default, and hidden when the link is clicked. + * + * A message of the form log-show-hide-[type] should be added, and will be used + * for the link text. + */ +$wgFilterLogTypes = [ + 'patrol' => true, + 'tag' => true, +]; + +/** + * Lists the message key string for each log type. The localized messages + * will be listed in the user interface. + * + * Extensions with custom log types may add to this array. + * + * @since 1.19, if you follow the naming convention log-name-TYPE, + * where TYPE is your log type, yoy don't need to use this array. + */ +$wgLogNames = [ + '' => 'all-logs-page', + 'block' => 'blocklogpage', + 'protect' => 'protectlogpage', + 'rights' => 'rightslog', + 'delete' => 'dellogpage', + 'upload' => 'uploadlogpage', + 'move' => 'movelogpage', + 'import' => 'importlogpage', + 'patrol' => 'patrol-log-page', + 'merge' => 'mergelog', + 'suppress' => 'suppressionlog', +]; + +/** + * Lists the message key string for descriptive text to be shown at the + * top of each log type. + * + * Extensions with custom log types may add to this array. + * + * @since 1.19, if you follow the naming convention log-description-TYPE, + * where TYPE is your log type, yoy don't need to use this array. + */ +$wgLogHeaders = [ + '' => 'alllogstext', + 'block' => 'blocklogtext', + 'delete' => 'dellogpagetext', + 'import' => 'importlogpagetext', + 'merge' => 'mergelogpagetext', + 'move' => 'movelogpagetext', + 'patrol' => 'patrol-log-header', + 'protect' => 'protectlogtext', + 'rights' => 'rightslogtext', + 'suppress' => 'suppressionlogtext', + 'upload' => 'uploadlogpagetext', +]; + +/** + * Lists the message key string for formatting individual events of each + * type and action when listed in the logs. + * + * Extensions with custom log types may add to this array. + */ +$wgLogActions = []; + +/** + * The same as above, but here values are names of classes, + * not messages. + * @see LogPage::actionText + * @see LogFormatter + */ +$wgLogActionsHandlers = [ + 'block/block' => BlockLogFormatter::class, + 'block/reblock' => BlockLogFormatter::class, + 'block/unblock' => BlockLogFormatter::class, + 'contentmodel/change' => ContentModelLogFormatter::class, + 'contentmodel/new' => ContentModelLogFormatter::class, + 'delete/delete' => DeleteLogFormatter::class, + 'delete/delete_redir' => DeleteLogFormatter::class, + 'delete/event' => DeleteLogFormatter::class, + 'delete/restore' => DeleteLogFormatter::class, + 'delete/revision' => DeleteLogFormatter::class, + 'import/interwiki' => ImportLogFormatter::class, + 'import/upload' => ImportLogFormatter::class, + 'managetags/activate' => LogFormatter::class, + 'managetags/create' => LogFormatter::class, + 'managetags/deactivate' => LogFormatter::class, + 'managetags/delete' => LogFormatter::class, + 'merge/merge' => MergeLogFormatter::class, + 'move/move' => MoveLogFormatter::class, + 'move/move_redir' => MoveLogFormatter::class, + 'patrol/patrol' => PatrolLogFormatter::class, + 'patrol/autopatrol' => PatrolLogFormatter::class, + 'protect/modify' => ProtectLogFormatter::class, + 'protect/move_prot' => ProtectLogFormatter::class, + 'protect/protect' => ProtectLogFormatter::class, + 'protect/unprotect' => ProtectLogFormatter::class, + 'rights/autopromote' => RightsLogFormatter::class, + 'rights/rights' => RightsLogFormatter::class, + 'suppress/block' => BlockLogFormatter::class, + 'suppress/delete' => DeleteLogFormatter::class, + 'suppress/event' => DeleteLogFormatter::class, + 'suppress/reblock' => BlockLogFormatter::class, + 'suppress/revision' => DeleteLogFormatter::class, + 'tag/update' => TagLogFormatter::class, + 'upload/overwrite' => UploadLogFormatter::class, + 'upload/revert' => UploadLogFormatter::class, + 'upload/upload' => UploadLogFormatter::class, +]; + +/** + * List of log types that can be filtered by action types + * + * To each action is associated the list of log_action + * subtypes to search for, usually one, but not necessarily so + * Extensions may append to this array + * @since 1.27 + */ +$wgActionFilteredLogs = [ + 'block' => [ + 'block' => [ 'block' ], + 'reblock' => [ 'reblock' ], + 'unblock' => [ 'unblock' ], + ], + 'contentmodel' => [ + 'change' => [ 'change' ], + 'new' => [ 'new' ], + ], + 'delete' => [ + 'delete' => [ 'delete' ], + 'delete_redir' => [ 'delete_redir' ], + 'restore' => [ 'restore' ], + 'event' => [ 'event' ], + 'revision' => [ 'revision' ], + ], + 'import' => [ + 'interwiki' => [ 'interwiki' ], + 'upload' => [ 'upload' ], + ], + 'managetags' => [ + 'create' => [ 'create' ], + 'delete' => [ 'delete' ], + 'activate' => [ 'activate' ], + 'deactivate' => [ 'deactivate' ], + ], + 'move' => [ + 'move' => [ 'move' ], + 'move_redir' => [ 'move_redir' ], + ], + 'newusers' => [ + 'create' => [ 'create', 'newusers' ], + 'create2' => [ 'create2' ], + 'autocreate' => [ 'autocreate' ], + 'byemail' => [ 'byemail' ], + ], + 'patrol' => [ + 'patrol' => [ 'patrol' ], + 'autopatrol' => [ 'autopatrol' ], + ], + 'protect' => [ + 'protect' => [ 'protect' ], + 'modify' => [ 'modify' ], + 'unprotect' => [ 'unprotect' ], + 'move_prot' => [ 'move_prot' ], + ], + 'rights' => [ + 'rights' => [ 'rights' ], + 'autopromote' => [ 'autopromote' ], + ], + 'suppress' => [ + 'event' => [ 'event' ], + 'revision' => [ 'revision' ], + 'delete' => [ 'delete' ], + 'block' => [ 'block' ], + 'reblock' => [ 'reblock' ], + ], + 'upload' => [ + 'upload' => [ 'upload' ], + 'overwrite' => [ 'overwrite' ], + ], +]; + +/** + * Maintain a log of newusers at Log/newusers? + */ +$wgNewUserLog = true; + +/** @} */ # end logging } + +/*************************************************************************//** + * @name Special pages (general and miscellaneous) + * @{ + */ + +/** + * Allow special page inclusions such as {{Special:Allpages}} + */ +$wgAllowSpecialInclusion = true; + +/** + * Set this to an array of special page names to prevent + * maintenance/updateSpecialPages.php from updating those pages. + */ +$wgDisableQueryPageUpdate = false; + +/** + * On Special:Unusedimages, consider images "used", if they are put + * into a category. Default (false) is not to count those as used. + */ +$wgCountCategorizedImagesAsUsed = false; + +/** + * Maximum number of links to a redirect page listed on + * Special:Whatlinkshere/RedirectDestination + */ +$wgMaxRedirectLinksRetrieved = 500; + +/** @} */ # end special pages } + +/*************************************************************************//** + * @name Actions + * @{ + */ + +/** + * Array of allowed values for the "title=foo&action=<action>" parameter. Syntax is: + * 'foo' => 'ClassName' Load the specified class which subclasses Action + * 'foo' => true Load the class FooAction which subclasses Action + * If something is specified in the getActionOverrides() + * of the relevant Page object it will be used + * instead of the default class. + * 'foo' => false The action is disabled; show an error message + * Unsetting core actions will probably cause things to complain loudly. + */ +$wgActions = [ + 'credits' => true, + 'delete' => true, + 'edit' => true, + 'editchangetags' => SpecialPageAction::class, + 'history' => true, + 'info' => true, + 'markpatrolled' => true, + 'protect' => true, + 'purge' => true, + 'raw' => true, + 'render' => true, + 'revert' => true, + 'revisiondelete' => SpecialPageAction::class, + 'rollback' => true, + 'submit' => true, + 'unprotect' => true, + 'unwatch' => true, + 'view' => true, + 'watch' => true, +]; + +/** @} */ # end actions } + +/*************************************************************************//** + * @name Robot (search engine crawler) policy + * See also $wgNoFollowLinks. + * @{ + */ + +/** + * Default robot policy. The default policy is to encourage indexing and fol- + * lowing of links. It may be overridden on a per-namespace and/or per-page + * basis. + */ +$wgDefaultRobotPolicy = 'index,follow'; + +/** + * Robot policies per namespaces. The default policy is given above, the array + * is made of namespace constants as defined in includes/Defines.php. You can- + * not specify a different default policy for NS_SPECIAL: it is always noindex, + * nofollow. This is because a number of special pages (e.g., ListPages) have + * many permutations of options that display the same data under redundant + * URLs, so search engine spiders risk getting lost in a maze of twisty special + * pages, all alike, and never reaching your actual content. + * + * @par Example: + * @code + * $wgNamespaceRobotPolicies = [ NS_TALK => 'noindex' ]; + * @endcode + */ +$wgNamespaceRobotPolicies = []; + +/** + * Robot policies per article. These override the per-namespace robot policies. + * Must be in the form of an array where the key part is a properly canonicalised + * text form title and the value is a robot policy. + * + * @par Example: + * @code + * $wgArticleRobotPolicies = [ + * 'Main Page' => 'noindex,follow', + * 'User:Bob' => 'index,follow', + * ]; + * @endcode + * + * @par Example that DOES NOT WORK because the names are not canonical text + * forms: + * @code + * $wgArticleRobotPolicies = [ + * # Underscore, not space! + * 'Main_Page' => 'noindex,follow', + * # "Project", not the actual project name! + * 'Project:X' => 'index,follow', + * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)! + * 'abc' => 'noindex,nofollow' + * ]; + * @endcode + */ +$wgArticleRobotPolicies = []; + +/** + * An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words + * will not function, so users can't decide whether pages in that namespace are + * indexed by search engines. If set to null, default to $wgContentNamespaces. + * + * @par Example: + * @code + * $wgExemptFromUserRobotsControl = [ NS_MAIN, NS_TALK, NS_PROJECT ]; + * @endcode + */ +$wgExemptFromUserRobotsControl = null; + +/** @} */ # End robot policy } + +/************************************************************************//** + * @name AJAX and API + * Note: The AJAX entry point which this section refers to is gradually being + * replaced by the API entry point, api.php. They are essentially equivalent. + * Both of them are used for dynamic client-side features, via XHR. + * @{ + */ + +/** + * Enable the MediaWiki API for convenient access to + * machine-readable data via api.php + * + * See https://www.mediawiki.org/wiki/API + * + * @deprecated since 1.31 + */ +$wgEnableAPI = true; + +/** + * Allow the API to be used to perform write operations + * (page edits, rollback, etc.) when an authorised user + * accesses it + * + * @deprecated since 1.31 + */ +$wgEnableWriteAPI = true; + +/** + * + * WARNING: SECURITY THREAT - debug use only + * + * Disables many security checks in the API for debugging purposes. + * This flag should never be used on the production servers, as it introduces + * a number of potential security holes. Even when enabled, the validation + * will still be performed, but instead of failing, API will return a warning. + * Also, there will always be a warning notifying that this flag is set. + * At this point, the flag allows GET requests to go through for modules + * requiring POST. + * + * @since 1.21 + */ +$wgDebugAPI = false; + +/** + * API module extensions. + * + * Associative array mapping module name to modules specs; + * Each module spec is an associative array containing at least + * the 'class' key for the module's class, and optionally a + * 'factory' key for the factory function to use for the module. + * + * That factory function will be called with two parameters, + * the parent module (an instance of ApiBase, usually ApiMain) + * and the name the module was registered under. The return + * value must be an instance of the class given in the 'class' + * field. + * + * For backward compatibility, the module spec may also be a + * simple string containing the module's class name. In that + * case, the class' constructor will be called with the parent + * module and module name as parameters, as described above. + * + * Examples for registering API modules: + * + * @code + * $wgAPIModules['foo'] = 'ApiFoo'; + * $wgAPIModules['bar'] = [ + * 'class' => ApiBar::class, + * 'factory' => function( $main, $name ) { ... } + * ]; + * $wgAPIModules['xyzzy'] = [ + * 'class' => ApiXyzzy::class, + * 'factory' => [ XyzzyFactory::class, 'newApiModule' ] + * ]; + * @endcode + * + * Extension modules may override the core modules. + * See ApiMain::$Modules for a list of the core modules. + */ +$wgAPIModules = []; + +/** + * API format module extensions. + * Associative array mapping format module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiMain::$Formats for a list of the core format modules. + */ +$wgAPIFormatModules = []; + +/** + * API Query meta module extensions. + * Associative array mapping meta module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiQuery::$QueryMetaModules for a list of the core meta modules. + */ +$wgAPIMetaModules = []; + +/** + * API Query prop module extensions. + * Associative array mapping prop module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiQuery::$QueryPropModules for a list of the core prop modules. + */ +$wgAPIPropModules = []; + +/** + * API Query list module extensions. + * Associative array mapping list module name to module specs (see $wgAPIModules). + * Extension modules may override the core modules. + * + * See ApiQuery::$QueryListModules for a list of the core list modules. + */ +$wgAPIListModules = []; + +/** + * Maximum amount of rows to scan in a DB query in the API + * The default value is generally fine + */ +$wgAPIMaxDBRows = 5000; + +/** + * The maximum size (in bytes) of an API result. + * @warning Do not set this lower than $wgMaxArticleSize*1024 + */ +$wgAPIMaxResultSize = 8388608; + +/** + * The maximum number of uncached diffs that can be retrieved in one API + * request. Set this to 0 to disable API diffs altogether + */ +$wgAPIMaxUncachedDiffs = 1; + +/** + * Maximum amount of DB lag on a majority of DB replica DBs to tolerate + * before forcing bots to retry any write requests via API errors. + * This should be lower than the 'max lag' value in $wgLBFactoryConf. + */ +$wgAPIMaxLagThreshold = 7; + +/** + * Log file or URL (TCP or UDP) to log API requests to, or false to disable + * API request logging + */ +$wgAPIRequestLog = false; + +/** + * Set the timeout for the API help text cache. If set to 0, caching disabled + */ +$wgAPICacheHelpTimeout = 60 * 60; + +/** + * The ApiQueryQueryPages module should skip pages that are redundant to true + * API queries. + */ +$wgAPIUselessQueryPages = [ + 'MIMEsearch', // aiprop=mime + 'LinkSearch', // list=exturlusage + 'FileDuplicateSearch', // prop=duplicatefiles +]; + +/** + * Enable AJAX framework + * + * @deprecated (officially) since MediaWiki 1.31 + */ +$wgUseAjax = true; + +/** + * List of Ajax-callable functions. + * Extensions acting as Ajax callbacks must register here + * @deprecated (officially) since 1.27; use the API instead + */ +$wgAjaxExportList = []; + +/** + * Enable AJAX check for file overwrite, pre-upload + */ +$wgAjaxUploadDestCheck = true; + +/** + * Enable previewing licences via AJAX. Also requires $wgEnableAPI to be true. + */ +$wgAjaxLicensePreview = true; + +/** + * Have clients send edits to be prepared when filling in edit summaries. + * This gives the server a head start on the expensive parsing operation. + */ +$wgAjaxEditStash = true; + +/** + * Settings for incoming cross-site AJAX requests: + * Newer browsers support cross-site AJAX when the target resource allows requests + * from the origin domain by the Access-Control-Allow-Origin header. + * This is currently only used by the API (requests to api.php) + * $wgCrossSiteAJAXdomains can be set using a wildcard syntax: + * + * - '*' matches any number of characters + * - '?' matches any 1 character + * + * @par Example: + * @code + * $wgCrossSiteAJAXdomains = [ + * 'www.mediawiki.org', + * '*.wikipedia.org', + * '*.wikimedia.org', + * '*.wiktionary.org', + * ]; + * @endcode + */ +$wgCrossSiteAJAXdomains = []; + +/** + * Domains that should not be allowed to make AJAX requests, + * even if they match one of the domains allowed by $wgCrossSiteAJAXdomains + * Uses the same syntax as $wgCrossSiteAJAXdomains + */ +$wgCrossSiteAJAXdomainExceptions = []; + +/** @} */ # End AJAX and API } + +/************************************************************************//** + * @name Shell and process control + * @{ + */ + +/** + * Maximum amount of virtual memory available to shell processes under linux, in KB. + */ +$wgMaxShellMemory = 307200; + +/** + * Maximum file size created by shell processes under linux, in KB + * ImageMagick convert for example can be fairly hungry for scratch space + */ +$wgMaxShellFileSize = 102400; + +/** + * Maximum CPU time in seconds for shell processes under Linux + */ +$wgMaxShellTime = 180; + +/** + * Maximum wall clock time (i.e. real time, of the kind the clock on the wall + * would measure) in seconds for shell processes under Linux + */ +$wgMaxShellWallClockTime = 180; + +/** + * Under Linux: a cgroup directory used to constrain memory usage of shell + * commands. The directory must be writable by the user which runs MediaWiki. + * + * If specified, this is used instead of ulimit, which is inaccurate, and + * causes malloc() to return NULL, which exposes bugs in C applications, making + * them segfault or deadlock. + * + * A wrapper script will create a cgroup for each shell command that runs, as + * a subgroup of the specified cgroup. If the memory limit is exceeded, the + * kernel will send a SIGKILL signal to a process in the subgroup. + * + * @par Example: + * @code + * mkdir -p /sys/fs/cgroup/memory/mediawiki + * mkdir -m 0777 /sys/fs/cgroup/memory/mediawiki/job + * echo '$wgShellCgroup = "/sys/fs/cgroup/memory/mediawiki/job";' >> LocalSettings.php + * @endcode + * + * The reliability of cgroup cleanup can be improved by installing a + * notify_on_release script in the root cgroup, see e.g. + * https://gerrit.wikimedia.org/r/#/c/40784 + */ +$wgShellCgroup = false; + +/** + * Executable path of the PHP cli binary. Should be set up on install. + */ +$wgPhpCli = '/usr/bin/php'; + +/** + * Locale for LC_ALL, to provide a known environment for locale-sensitive operations + * + * For Unix-like operating systems, this should be set to C.UTF-8 or an + * equivalent to provide the most consistent behavior for locale-sensitive + * C library operations across different-language wikis. If that locale is not + * available, use another locale that has a UTF-8 character set. + * + * This setting mainly affects the behavior of C library functions, including: + * - String collation (order when sorting using locale-sensitive comparison) + * - For example, whether "Å" and "A" are considered to be the same letter or + * different letters and if different whether it comes after "A" or after + * "Z", and whether sorting is case sensitive. + * - String character set (how characters beyond basic ASCII are represented) + * - We need this to be a UTF-8 character set to work around + * https://bugs.php.net/bug.php?id=45132 + * - Language used for low-level error messages. + * - Formatting of date/time and numeric values (e.g. '.' versus ',' as the + * decimal separator) + * + * MediaWiki provides its own methods and classes to perform many + * locale-sensitive operations, which are designed to be able to vary locale + * based on wiki language or user preference: + * - MediaWiki's Collation class should generally be used instead of the C + * library collation functions when locale-sensitive sorting is needed. + * - MediaWiki's Message class should be used for localization of messages + * displayed to the user. + * - MediaWiki's Language class should be used for formatting numeric and + * date/time values. + * + * @note If multiple wikis are being served from the same process (e.g. the + * same fastCGI or Apache server), this setting must be the same on all those + * wikis. + */ +$wgShellLocale = 'C.UTF-8'; + +/** + * Method to use to restrict shell commands + * + * Supported options: + * - 'autodetect': Autodetect if any restriction methods are available + * - 'firejail': Use firejail <https://firejail.wordpress.com/> + * - false: Don't use any restrictions + * + * @note If using firejail with MediaWiki running in a home directory different + * from the webserver user, firejail 0.9.44+ is required. + * + * @since 1.31 + * @var string|bool + */ +$wgShellRestrictionMethod = false; + +/** @} */ # End shell } + +/************************************************************************//** + * @name HTTP client + * @{ + */ + +/** + * Timeout for HTTP requests done internally, in seconds. + * @var int + */ +$wgHTTPTimeout = 25; + +/** + * Timeout for HTTP requests done internally for transwiki imports, in seconds. + * @since 1.29 + */ +$wgHTTPImportTimeout = 25; + +/** + * Timeout for Asynchronous (background) HTTP requests, in seconds. + */ +$wgAsyncHTTPTimeout = 25; + +/** + * Proxy to use for CURL requests. + */ +$wgHTTPProxy = false; + +/** + * Local virtual hosts. + * + * This lists domains that are configured as virtual hosts on the same machine. + * + * This affects the following: + * - MWHttpRequest: If a request is to be made to a domain listed here, or any + * subdomain thereof, then no proxy will be used. + * Command-line scripts are not affected by this setting and will always use + * the proxy if it is configured. + * + * @since 1.25 + */ +$wgLocalVirtualHosts = []; + +/** + * Timeout for connections done internally (in seconds) + * Only works for curl + */ +$wgHTTPConnectTimeout = 5e0; + +/** @} */ # End HTTP client } + +/************************************************************************//** + * @name Job queue + * @{ + */ + +/** + * Number of jobs to perform per request. May be less than one in which case + * jobs are performed probabalistically. If this is zero, jobs will not be done + * during ordinary apache requests. In this case, maintenance/runJobs.php should + * be run periodically. + */ +$wgJobRunRate = 1; + +/** + * When $wgJobRunRate > 0, try to run jobs asynchronously, spawning a new process + * to handle the job execution, instead of blocking the request until the job + * execution finishes. + * + * @since 1.23 + */ +$wgRunJobsAsync = false; + +/** + * Number of rows to update per job + */ +$wgUpdateRowsPerJob = 300; + +/** + * Number of rows to update per query + */ +$wgUpdateRowsPerQuery = 100; + +/** @} */ # End job queue } + +/************************************************************************//** + * @name Miscellaneous + * @{ + */ + +/** + * Name of the external diff engine to use. Supported values: + * * string: path to an external diff executable + * * false: wikidiff2 PHP/HHVM module if installed, otherwise the default PHP implementation + * * 'wikidiff', 'wikidiff2', and 'wikidiff3' are treated as false for backwards compatibility + */ +$wgExternalDiffEngine = false; + +/** + * wikidiff2 supports detection of changes in moved paragraphs. + * This setting controls the maximum number of paragraphs to compare before it bails out. + * Supported values: + * * 0: detection of moved paragraphs is disabled + * * int > 0: maximum number of paragraphs to compare + * Note: number of paragraph comparisons is in O(n^2). + * This setting is only effective if the wikidiff2 PHP/HHVM module is used as diffengine. + * See $wgExternalDiffEngine. + * + * @since 1.30 + */ +$wgWikiDiff2MovedParagraphDetectionCutoff = 0; + +/** + * Disable redirects to special pages and interwiki redirects, which use a 302 + * and have no "redirected from" link. + * + * @note This is only for articles with #REDIRECT in them. URL's containing a + * local interwiki prefix (or a non-canonical special page name) are still hard + * redirected regardless of this setting. + */ +$wgDisableHardRedirects = false; + +/** + * LinkHolderArray batch size + * For debugging + */ +$wgLinkHolderBatchSize = 1000; + +/** + * By default MediaWiki does not register links pointing to same server in + * externallinks dataset, use this value to override: + */ +$wgRegisterInternalExternals = false; + +/** + * Maximum number of pages to move at once when moving subpages with a page. + */ +$wgMaximumMovedPages = 100; + +/** + * Fix double redirects after a page move. + * Tends to conflict with page move vandalism, use only on a private wiki. + */ +$wgFixDoubleRedirects = false; + +/** + * Allow redirection to another page when a user logs in. + * To enable, set to a string like 'Main Page' + */ +$wgRedirectOnLogin = null; + +/** + * Configuration for processing pool control, for use in high-traffic wikis. + * An implementation is provided in the PoolCounter extension. + * + * This configuration array maps pool types to an associative array. The only + * defined key in the associative array is "class", which gives the class name. + * The remaining elements are passed through to the class as constructor + * parameters. + * + * @par Example using local redis instance: + * @code + * $wgPoolCounterConf = [ 'ArticleView' => [ + * 'class' => PoolCounterRedis::class, + * 'timeout' => 15, // wait timeout in seconds + * 'workers' => 1, // maximum number of active threads in each pool + * 'maxqueue' => 5, // maximum number of total threads in each pool + * 'servers' => [ '127.0.0.1' ], + * 'redisConfig' => [] + * ] ]; + * @endcode + * + * @par Example using C daemon from https://www.mediawiki.org/wiki/Extension:PoolCounter: + * @code + * $wgPoolCounterConf = [ 'ArticleView' => [ + * 'class' => PoolCounter_Client::class, + * 'timeout' => 15, // wait timeout in seconds + * 'workers' => 5, // maximum number of active threads in each pool + * 'maxqueue' => 50, // maximum number of total threads in each pool + * ... any extension-specific options... + * ] ]; + * @endcode + */ +$wgPoolCounterConf = null; + +/** + * To disable file delete/restore temporarily + */ +$wgUploadMaintenance = false; + +/** + * Associative array mapping namespace IDs to the name of the content model pages in that namespace + * should have by default (use the CONTENT_MODEL_XXX constants). If no special content type is + * defined for a given namespace, pages in that namespace will use the CONTENT_MODEL_WIKITEXT + * (except for the special case of JS and CS pages). + * + * @since 1.21 + */ +$wgNamespaceContentModels = []; + +/** + * How to react if a plain text version of a non-text Content object is requested using + * ContentHandler::getContentText(): + * + * * 'ignore': return null + * * 'fail': throw an MWException + * * 'serialize': serialize to default format + * + * @since 1.21 + */ +$wgContentHandlerTextFallback = 'ignore'; + +/** + * Set to false to disable use of the database fields introduced by the ContentHandler facility. + * This way, the ContentHandler facility can be used without any additional information in the + * database. A page's content model is then derived solely from the page's title. This however + * means that changing a page's default model (e.g. using $wgNamespaceContentModels) will break + * the page and/or make the content inaccessible. This also means that pages can not be moved to + * a title that would default to a different content model. + * + * Overall, with $wgContentHandlerUseDB = false, no database updates are needed, but content + * handling is less robust and less flexible. + * + * @since 1.21 + */ +$wgContentHandlerUseDB = true; + +/** + * Determines which types of text are parsed as wikitext. This does not imply that these kinds + * of texts are also rendered as wikitext, it only means that links, magic words, etc will have + * the effect on the database they would have on a wikitext page. + * + * @todo On the long run, it would be nice to put categories etc into a separate structure, + * or at least parse only the contents of comments in the scripts. + * + * @since 1.21 + */ +$wgTextModelsToParse = [ + CONTENT_MODEL_WIKITEXT, // Just for completeness, wikitext will always be parsed. + CONTENT_MODEL_JAVASCRIPT, // Make categories etc work, people put them into comments. + CONTENT_MODEL_CSS, // Make categories etc work, people put them into comments. +]; + +/** + * Register handlers for specific types of sites. + * + * @since 1.20 + */ +$wgSiteTypes = [ + 'mediawiki' => MediaWikiSite::class, +]; + +/** + * Whether the page_props table has a pp_sortkey column. Set to false in case + * the respective database schema change was not applied. + * @since 1.23 + */ +$wgPagePropsHaveSortkey = true; + +/** + * Port where you have HTTPS running + * Supports HTTPS on non-standard ports + * @see T67184 + * @since 1.24 + */ +$wgHttpsPort = 443; + +/** + * Secret for session storage. + * This should be set in LocalSettings.php, otherwise wgSecretKey will + * be used. + * @since 1.27 + */ +$wgSessionSecret = false; + +/** + * If for some reason you can't install the PHP OpenSSL or mcrypt extensions, + * you can set this to true to make MediaWiki work again at the cost of storing + * sensitive session data insecurely. But it would be much more secure to just + * install the OpenSSL extension. + * @since 1.27 + */ +$wgSessionInsecureSecrets = false; + +/** + * Secret for hmac-based key derivation function (fast, + * cryptographically secure random numbers). + * This should be set in LocalSettings.php, otherwise wgSecretKey will + * be used. + * See also: $wgHKDFAlgorithm + * @since 1.24 + */ +$wgHKDFSecret = false; + +/** + * Algorithm for hmac-based key derivation function (fast, + * cryptographically secure random numbers). + * See also: $wgHKDFSecret + * @since 1.24 + */ +$wgHKDFAlgorithm = 'sha256'; + +/** + * Enable page language feature + * Allows setting page language in database + * @var bool + * @since 1.24 + */ +$wgPageLanguageUseDB = false; + +/** + * Global configuration variable for Virtual REST Services. + * + * Use the 'path' key to define automatically mounted services. The value for this + * key is a map of path prefixes to service configuration. The latter is an array of: + * - class : the fully qualified class name + * - options : map of arguments to the class constructor + * Such services will be available to handle queries under their path from the VRS + * singleton, e.g. MediaWikiServices::getInstance()->getVirtualRESTServiceClient(); + * + * Auto-mounting example for Parsoid: + * + * $wgVirtualRestConfig['paths']['/parsoid/'] = [ + * 'class' => ParsoidVirtualRESTService::class, + * 'options' => [ + * 'url' => 'http://localhost:8000', + * 'prefix' => 'enwiki', + * 'domain' => 'en.wikipedia.org' + * ] + * ]; + * + * Parameters for different services can also be declared inside the 'modules' value, + * which is to be treated as an associative array. The parameters in 'global' will be + * merged with service-specific ones. The result will then be passed to + * VirtualRESTService::__construct() in the module. + * + * Example config for Parsoid: + * + * $wgVirtualRestConfig['modules']['parsoid'] = [ + * 'url' => 'http://localhost:8000', + * 'prefix' => 'enwiki', + * 'domain' => 'en.wikipedia.org', + * ]; + * + * @var array + * @since 1.25 + */ +$wgVirtualRestConfig = [ + 'paths' => [], + 'modules' => [], + 'global' => [ + # Timeout in seconds + 'timeout' => 360, + # 'domain' is set to $wgCanonicalServer in Setup.php + 'forwardCookies' => false, + 'HTTPProxy' => null + ] +]; + +/** + * Controls whether zero-result search queries with suggestions should display results for + * these suggestions. + * + * @var bool + * @since 1.26 + */ +$wgSearchRunSuggestedQuery = true; + +/** + * Where popular password file is located. + * + * Default in core contains 10,000 most popular. This config + * allows you to change which file, in case you want to generate + * a password file with > 10000 entries in it. + * + * @see maintenance/createCommonPasswordCdb.php + * @since 1.27 + * @var string path to file + */ +$wgPopularPasswordFile = __DIR__ . '/../serialized/commonpasswords.cdb'; + +/* + * Max time (in seconds) a user-generated transaction can spend in writes. + * If exceeded, the transaction is rolled back with an error instead of being committed. + * + * @var int|bool Disabled if false + * @since 1.27 + */ +$wgMaxUserDBWriteDuration = false; + +/* + * Max time (in seconds) a job-generated transaction can spend in writes. + * If exceeded, the transaction is rolled back with an error instead of being committed. + * + * @var int|bool Disabled if false + * @since 1.30 + */ +$wgMaxJobDBWriteDuration = false; + +/** + * Mapping of event channels (or channel categories) to EventRelayer configuration. + * + * By setting up a PubSub system (like Kafka) and enabling a corresponding EventRelayer class + * that uses it, MediaWiki can broadcast events to all subscribers. Certain features like WAN + * cache purging and CDN cache purging will emit events to this system. Appropriate listers can + * subscribe to the channel and take actions based on the events. For example, a local daemon + * can run on each CDN cache node and perfom local purges based on the URL purge channel events. + * + * Some extensions may want to use "channel categories" so that different channels can also share + * the same custom relayer instance (e.g. when it's likely to be overriden). They can use + * EventRelayerGroup::getRelayer() based on the category but call notify() on various different + * actual channels. One reason for this would be that some system have very different performance + * vs durability needs, so one system (e.g. Kafka) may not be suitable for all uses. + * + * The 'default' key is for all channels (or channel categories) without an explicit entry here. + * + * @since 1.27 + */ +$wgEventRelayerConfig = [ + 'default' => [ + 'class' => EventRelayerNull::class, + ] +]; + +/** + * Share data about this installation with MediaWiki developers + * + * When set to true, MediaWiki will periodically ping https://www.mediawiki.org/ with basic + * data about this MediaWiki instance. This data includes, for example, the type of system, + * PHP version, and chosen database backend. The Wikimedia Foundation shares this data with + * MediaWiki developers to help guide future development efforts. + * + * For details about what data is sent, see: https://www.mediawiki.org/wiki/Manual:$wgPingback + * + * @var bool + * @since 1.28 + */ +$wgPingback = false; + +/** + * List of urls which appear often to be triggering CSP reports + * but do not appear to be caused by actual content, but by client + * software inserting scripts (i.e. Ad-Ware). + * List based on results from Wikimedia logs. + * + * @since 1.28 + */ +$wgCSPFalsePositiveUrls = [ + 'https://3hub.co' => true, + 'https://morepro.info' => true, + 'https://p.ato.mx' => true, + 'https://s.ato.mx' => true, + 'https://adserver.adtech.de' => true, + 'https://ums.adtechus.com' => true, + 'https://cas.criteo.com' => true, + 'https://cat.nl.eu.criteo.com' => true, + 'https://atpixel.alephd.com' => true, + 'https://rtb.metrigo.com' => true, + 'https://d5p.de17a.com' => true, + 'https://ad.lkqd.net/vpaid/vpaid.js' => true, +]; + +/** + * Shortest CIDR limits that can be checked in any individual range check + * at Special:Contributions. + * + * @var array + * @since 1.30 + */ +$wgRangeContributionsCIDRLimit = [ + 'IPv4' => 16, + 'IPv6' => 32, +]; + +/** + * The following variables define 3 user experience levels: + * + * - newcomer: has not yet reached the 'learner' level + * + * - learner: has at least $wgLearnerEdits and has been + * a member for $wgLearnerMemberSince days + * but has not yet reached the 'experienced' level. + * + * - experienced: has at least $wgExperiencedUserEdits edits and + * has been a member for $wgExperiencedUserMemberSince days. + */ +$wgLearnerEdits = 10; +$wgLearnerMemberSince = 4; # days +$wgExperiencedUserEdits = 500; +$wgExperiencedUserMemberSince = 30; # days + +/** + * Mapping of interwiki index prefixes to descriptors that + * can be used to change the display of interwiki search results. + * + * Descriptors are appended to CSS classes of interwiki results + * which using InterwikiSearchResultWidget. + * + * Predefined descriptors include the following words: + * definition, textbook, news, quotation, book, travel, course + * + * @par Example: + * @code + * $wgInterwikiPrefixDisplayTypes = [ + * 'iwprefix' => 'definition' + *]; + * @endcode + */ +$wgInterwikiPrefixDisplayTypes = []; + +/** + * Comment table schema migration stage. + * @since 1.30 + * @var int One of the MIGRATION_* constants + */ +$wgCommentTableSchemaMigrationStage = MIGRATION_OLD; + +/** + * Actor table schema migration stage. + * @since 1.31 + * @var int One of the MIGRATION_* constants + */ +$wgActorTableSchemaMigrationStage = MIGRATION_OLD; + +/** + * For really cool vim folding this needs to be at the end: + * vim: foldmarker=@{,@} foldmethod=marker + * @} + */ diff --git a/www/wiki/includes/Defines.php b/www/wiki/includes/Defines.php new file mode 100644 index 00000000..087af39d --- /dev/null +++ b/www/wiki/includes/Defines.php @@ -0,0 +1,297 @@ +<?php +/** + * A few constants that might be needed during LocalSettings.php. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +require_once __DIR__ . '/libs/mime/defines.php'; +require_once __DIR__ . '/libs/rdbms/defines.php'; +require_once __DIR__ . '/compat/normal/UtfNormalDefines.php'; + +use Wikimedia\Rdbms\IDatabase; + +/** + * @defgroup Constants MediaWiki constants + */ + +# Obsolete aliases +/** + * @deprecated since 1.28 + */ +define( 'DB_SLAVE', -1 ); + +/**@{ + * Obsolete IDatabase::makeList() constants + * These are also available as Database class constants + */ +define( 'LIST_COMMA', IDatabase::LIST_COMMA ); +define( 'LIST_AND', IDatabase::LIST_AND ); +define( 'LIST_SET', IDatabase::LIST_SET ); +define( 'LIST_NAMES', IDatabase::LIST_NAMES ); +define( 'LIST_OR', IDatabase::LIST_OR ); +/**@}*/ + +/**@{ + * Virtual namespaces; don't appear in the page database + */ +define( 'NS_MEDIA', -2 ); +define( 'NS_SPECIAL', -1 ); +/**@}*/ + +/**@{ + * Real namespaces + * + * Number 100 and beyond are reserved for custom namespaces; + * DO NOT assign standard namespaces at 100 or beyond. + * DO NOT Change integer values as they are most probably hardcoded everywhere + * see bug #696 which talked about that. + */ +define( 'NS_MAIN', 0 ); +define( 'NS_TALK', 1 ); +define( 'NS_USER', 2 ); +define( 'NS_USER_TALK', 3 ); +define( 'NS_PROJECT', 4 ); +define( 'NS_PROJECT_TALK', 5 ); +define( 'NS_FILE', 6 ); +define( 'NS_FILE_TALK', 7 ); +define( 'NS_MEDIAWIKI', 8 ); +define( 'NS_MEDIAWIKI_TALK', 9 ); +define( 'NS_TEMPLATE', 10 ); +define( 'NS_TEMPLATE_TALK', 11 ); +define( 'NS_HELP', 12 ); +define( 'NS_HELP_TALK', 13 ); +define( 'NS_CATEGORY', 14 ); +define( 'NS_CATEGORY_TALK', 15 ); + +/** + * NS_IMAGE and NS_IMAGE_TALK are the pre-v1.14 names for NS_FILE and + * NS_FILE_TALK respectively, and are kept for compatibility. + * + * When writing code that should be compatible with older MediaWiki + * versions, either stick to the old names or define the new constants + * yourself, if they're not defined already. + * + * @deprecated since 1.14 + */ +define( 'NS_IMAGE', NS_FILE ); +/** + * @deprecated since 1.14 + */ +define( 'NS_IMAGE_TALK', NS_FILE_TALK ); +/**@}*/ + +/**@{ + * Cache type + */ +define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works +define( 'CACHE_NONE', 0 ); // Do not cache +define( 'CACHE_DB', 1 ); // Store cache objects in the DB +define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers +define( 'CACHE_ACCEL', 3 ); // APC or WinCache +/**@}*/ + +/**@{ + * Antivirus result codes, for use in $wgAntivirusSetup. + */ +define( 'AV_NO_VIRUS', 0 ); # scan ok, no virus found +define( 'AV_VIRUS_FOUND', 1 ); # virus found! +define( 'AV_SCAN_ABORTED', -1 ); # scan aborted, the file is probably immune +define( 'AV_SCAN_FAILED', false ); # scan failed (scanner not found or error in scanner) +/**@}*/ + +/**@{ + * Anti-lock flags + * Was used by $wgAntiLockFlags, which was removed with 1.25 + * Constants kept to not have warnings when used in LocalSettings + */ +define( 'ALF_PRELOAD_LINKS', 1 ); // unused +define( 'ALF_PRELOAD_EXISTENCE', 2 ); // unused +define( 'ALF_NO_LINK_LOCK', 4 ); // unused +define( 'ALF_NO_BLOCK_LOCK', 8 ); // unused +/**@}*/ + +/**@{ + * Date format selectors; used in user preference storage and by + * Language::date() and co. + */ +define( 'MW_DATE_DEFAULT', 'default' ); +define( 'MW_DATE_MDY', 'mdy' ); +define( 'MW_DATE_DMY', 'dmy' ); +define( 'MW_DATE_YMD', 'ymd' ); +define( 'MW_DATE_ISO', 'ISO 8601' ); +/**@}*/ + +/**@{ + * RecentChange type identifiers + */ +define( 'RC_EDIT', 0 ); +define( 'RC_NEW', 1 ); +define( 'RC_LOG', 3 ); +define( 'RC_EXTERNAL', 5 ); +define( 'RC_CATEGORIZE', 6 ); +/**@}*/ + +/**@{ + * Article edit flags + */ +define( 'EDIT_NEW', 1 ); +define( 'EDIT_UPDATE', 2 ); +define( 'EDIT_MINOR', 4 ); +define( 'EDIT_SUPPRESS_RC', 8 ); +define( 'EDIT_FORCE_BOT', 16 ); +define( 'EDIT_DEFER_UPDATES', 32 ); // Unused since 1.27 +define( 'EDIT_AUTOSUMMARY', 64 ); +define( 'EDIT_INTERNAL', 128 ); +/**@}*/ + +/**@{ + * Hook support constants + */ +define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 ); +define( 'MW_SUPPORTS_LOCALISATIONCACHE', 1 ); +define( 'MW_SUPPORTS_CONTENTHANDLER', 1 ); +define( 'MW_EDITFILTERMERGED_SUPPORTS_API', 1 ); +/**@}*/ + +/** Support for $wgResourceModules */ +define( 'MW_SUPPORTS_RESOURCE_MODULES', 1 ); + +/**@{ + * Allowed values for Parser::$mOutputType + * Parameter to Parser::startExternalParse(). + * Use of Parser consts is preferred: + * - Parser::OT_HTML + * - Parser::OT_WIKI + * - Parser::OT_PREPROCESS + * - Parser::OT_MSG + * - Parser::OT_PLAIN + */ +define( 'OT_HTML', 1 ); +define( 'OT_WIKI', 2 ); +define( 'OT_PREPROCESS', 3 ); +define( 'OT_MSG', 3 ); // b/c alias for OT_PREPROCESS +define( 'OT_PLAIN', 4 ); +/**@}*/ + +/**@{ + * Flags for Parser::setFunctionHook + * Use of Parser consts is preferred: + * - Parser::SFH_NO_HASH + * - Parser::SFH_OBJECT_ARGS + */ +define( 'SFH_NO_HASH', 1 ); +define( 'SFH_OBJECT_ARGS', 2 ); +/**@}*/ + +/**@{ + * Autopromote conditions (must be here and not in Autopromote.php, so that + * they're loaded for DefaultSettings.php before AutoLoader.php) + */ +define( 'APCOND_EDITCOUNT', 1 ); +define( 'APCOND_AGE', 2 ); +define( 'APCOND_EMAILCONFIRMED', 3 ); +define( 'APCOND_INGROUPS', 4 ); +define( 'APCOND_ISIP', 5 ); +define( 'APCOND_IPINRANGE', 6 ); +define( 'APCOND_AGE_FROM_EDIT', 7 ); +define( 'APCOND_BLOCKED', 8 ); +define( 'APCOND_ISBOT', 9 ); +/**@}*/ + +/** @{ + * Protocol constants for wfExpandUrl() + */ +define( 'PROTO_HTTP', 'http://' ); +define( 'PROTO_HTTPS', 'https://' ); +define( 'PROTO_RELATIVE', '//' ); +define( 'PROTO_CURRENT', null ); +define( 'PROTO_CANONICAL', 1 ); +define( 'PROTO_INTERNAL', 2 ); +/**@}*/ + +/**@{ + * Content model ids, used by Content and ContentHandler. + * These IDs will be exposed in the API and XML dumps. + * + * Extensions that define their own content model IDs should take + * care to avoid conflicts. Using the extension name as a prefix is recommended, + * for example 'myextension-somecontent'. + */ +define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' ); +define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' ); +define( 'CONTENT_MODEL_CSS', 'css' ); +define( 'CONTENT_MODEL_TEXT', 'text' ); +define( 'CONTENT_MODEL_JSON', 'json' ); +/**@}*/ + +/**@{ + * Content formats, used by Content and ContentHandler. + * These should be MIME types, and will be exposed in the API and XML dumps. + * + * Extensions are free to use the below formats, or define their own. + * It is recommended to stick with the conventions for MIME types. + */ +// wikitext +define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); +// for js pages +define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); +// for css pages +define( 'CONTENT_FORMAT_CSS', 'text/css' ); +// for future use, e.g. with some plain-html messages. +define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); +// for future use, e.g. with some plain-html messages. +define( 'CONTENT_FORMAT_HTML', 'text/html' ); +// for future use with the api and for extensions +define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); +// for future use with the api, and for use by extensions +define( 'CONTENT_FORMAT_JSON', 'application/json' ); +// for future use with the api, and for use by extensions +define( 'CONTENT_FORMAT_XML', 'application/xml' ); +/**@}*/ + +/**@{ + * Max string length for shell invocations; based on binfmts.h + */ +define( 'SHELL_MAX_ARG_STRLEN', '100000' ); +/**@}*/ + +/**@{ + * Schema change migration flags. + * + * Used as values of a feature flag for an orderly transition from an old + * schema to a new schema. + * + * - MIGRATION_OLD: Only read and write the old schema. The new schema need not + * even exist. This is used from when the patch is merged until the schema + * change is actually applied to the database. + * - MIGRATION_WRITE_BOTH: Write both the old and new schema. Read the new + * schema preferentially, falling back to the old. This is used while the + * change is being tested, allowing easy roll-back to the old schema. + * - MIGRATION_WRITE_NEW: Write only the new schema. Read the new schema + * preferentially, falling back to the old. This is used while running the + * maintenance script to migrate existing entries in the old schema to the + * new schema. + * - MIGRATION_NEW: Only read and write the new schema. The old schema (and the + * feature flag) may now be removed. + */ +define( 'MIGRATION_OLD', 0 ); +define( 'MIGRATION_WRITE_BOTH', 1 ); +define( 'MIGRATION_WRITE_NEW', 2 ); +define( 'MIGRATION_NEW', 3 ); +/**@}*/ diff --git a/www/wiki/includes/DeprecatedGlobal.php b/www/wiki/includes/DeprecatedGlobal.php new file mode 100644 index 00000000..242cecf1 --- /dev/null +++ b/www/wiki/includes/DeprecatedGlobal.php @@ -0,0 +1,57 @@ +<?php +/** + * Delayed loading of deprecated global objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class to allow throwing wfDeprecated warnings + * when people use globals that we do not want them to. + */ +class DeprecatedGlobal extends StubObject { + protected $version; + + /** + * @param string $name Global name + * @param callable|string $callback Factory function or class name to construct + * @param bool|string $version Version global was deprecated in + */ + function __construct( $name, $callback, $version = false ) { + parent::__construct( $name, $callback ); + $this->version = $version; + } + + // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore,PSR2.Classes.PropertyDeclaration.ScopeMissing + function _newObject() { + /* Put the caller offset for wfDeprecated as 6, as + * that gives the function that uses this object, since: + * 1 = this function ( _newObject ) + * 2 = StubObject::_unstub + * 3 = StubObject::_call + * 4 = StubObject::__call + * 5 = DeprecatedGlobal::<method of global called> + * 6 = Actual function using the global. + * Of course its theoretically possible to have other call + * sequences for this method, but that seems to be + * rather unlikely. + */ + wfDeprecated( '$' . $this->global, $this->version, false, 6 ); + return parent::_newObject(); + } +} diff --git a/www/wiki/includes/DerivativeRequest.php b/www/wiki/includes/DerivativeRequest.php new file mode 100644 index 00000000..487e86c8 --- /dev/null +++ b/www/wiki/includes/DerivativeRequest.php @@ -0,0 +1,87 @@ +<?php +/** + * Deal with importing all those nasty globals and things + * + * Copyright © 2003 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Similar to FauxRequest, but only fakes URL parameters and method + * (POST or GET) and use the base request for the remaining stuff + * (cookies, session and headers). + * + * @ingroup HTTP + * @since 1.19 + */ +class DerivativeRequest extends FauxRequest { + private $base; + + /** + * @param WebRequest $base + * @param array $data Array of *non*-urlencoded key => value pairs, the + * fake GET/POST values + * @param bool $wasPosted Whether to treat the data as POST + */ + public function __construct( WebRequest $base, $data, $wasPosted = false ) { + $this->base = $base; + parent::__construct( $data, $wasPosted ); + } + + public function getCookie( $key, $prefix = null, $default = null ) { + return $this->base->getCookie( $key, $prefix, $default ); + } + + public function getHeader( $name, $flags = 0 ) { + return $this->base->getHeader( $name, $flags ); + } + + public function getAllHeaders() { + return $this->base->getAllHeaders(); + } + + public function getSession() { + return $this->base->getSession(); + } + + public function getSessionData( $key ) { + return $this->base->getSessionData( $key ); + } + + public function setSessionData( $key, $data ) { + $this->base->setSessionData( $key, $data ); + } + + public function getAcceptLang() { + return $this->base->getAcceptLang(); + } + + public function getIP() { + return $this->base->getIP(); + } + + public function getProtocol() { + return $this->base->getProtocol(); + } + + public function getElapsedTime() { + return $this->base->getElapsedTime(); + } +} diff --git a/www/wiki/includes/DevelopmentSettings.php b/www/wiki/includes/DevelopmentSettings.php new file mode 100644 index 00000000..55f16edb --- /dev/null +++ b/www/wiki/includes/DevelopmentSettings.php @@ -0,0 +1,57 @@ +<?php +/** + * Extra settings useful for MediaWiki development. + * + * To enable built-in debug and development settings, add the + * following to your LocalSettings.php file. + * + * require "$IP/includes/DevelopmentSettings.php"; + * + * Alternatively, if running phpunit.php (or another Maintenance script), + * you can use the --mwdebug option to automatically load these settings. + * + * @file + */ + +/** + * Debugging: PHP + */ + +// Enable showing of errors +error_reporting( -1 ); +ini_set( 'display_errors', 1 ); + +/** + * Debugging: MediaWiki + */ +global $wgDevelopmentWarnings, $wgShowDBErrorBacktrace, $wgShowExceptionDetails, + $wgShowSQLErrors, $wgDebugRawPage, + $wgDebugComments, $wgDebugDumpSql, $wgDebugTimestamps, + $wgCommandLineMode, $wgDebugLogFile, $wgDBerrorLog, $wgDebugLogGroups; + +// Use of wfWarn() should cause tests to fail +$wgDevelopmentWarnings = true; + +// Enable showing of errors +$wgShowDBErrorBacktrace = true; +$wgShowExceptionDetails = true; +$wgShowSQLErrors = true; +$wgDebugRawPage = true; // T49960 + +// Enable log files +$logDir = getenv( 'MW_LOG_DIR' ); +if ( $logDir ) { + if ( $wgCommandLineMode ) { + $wgDebugLogFile = "$logDir/mw-debug-cli.log"; + } else { + $wgDebugLogFile = "$logDir/mw-debug-www.log"; + } + $wgDBerrorLog = "$logDir/mw-dberror.log"; + $wgDebugLogGroups['ratelimit'] = "$logDir/mw-ratelimit.log"; + $wgDebugLogGroups['exception'] = "$logDir/mw-exception.log"; + $wgDebugLogGroups['error'] = "$logDir/mw-error.log"; +} +unset( $logDir ); + +// Disable rate-limiting +$wgRateLimits = []; diff --git a/www/wiki/includes/DummyLinker.php b/www/wiki/includes/DummyLinker.php new file mode 100644 index 00000000..9aa6aeb0 --- /dev/null +++ b/www/wiki/includes/DummyLinker.php @@ -0,0 +1,489 @@ +<?php + +/** + * @since 1.18 + */ +class DummyLinker { + + /** + * @deprecated since 1.28, use LinkRenderer::getLinkClasses() instead + */ + public function getLinkColour( $t, $threshold ) { + wfDeprecated( __METHOD__, '1.28' ); + return Linker::getLinkColour( $t, $threshold ); + } + + public function link( + $target, + $html = null, + $customAttribs = [], + $query = [], + $options = [] + ) { + return Linker::link( + $target, + $html, + $customAttribs, + $query, + $options + ); + } + + public function linkKnown( + $target, + $html = null, + $customAttribs = [], + $query = [], + $options = [ 'known' ] + ) { + return Linker::linkKnown( + $target, + $html, + $customAttribs, + $query, + $options + ); + } + + public function makeSelfLinkObj( + $nt, + $html = '', + $query = '', + $trail = '', + $prefix = '' + ) { + return Linker::makeSelfLinkObj( + $nt, + $html, + $query, + $trail, + $prefix + ); + } + + public function getInvalidTitleDescription( + IContextSource $context, + $namespace, + $title + ) { + return Linker::getInvalidTitleDescription( + $context, + $namespace, + $title + ); + } + + public function normaliseSpecialPage( Title $title ) { + return Linker::normaliseSpecialPage( $title ); + } + + public function makeExternalImage( $url, $alt = '' ) { + return Linker::makeExternalImage( $url, $alt ); + } + + public function makeImageLink( + Parser $parser, + Title $title, + $file, + $frameParams = [], + $handlerParams = [], + $time = false, + $query = "", + $widthOption = null + ) { + return Linker::makeImageLink( + $parser, + $title, + $file, + $frameParams, + $handlerParams, + $time, + $query, + $widthOption + ); + } + + public function makeThumbLinkObj( + Title $title, + $file, + $label = '', + $alt, + $align = 'right', + $params = [], + $framed = false, + $manualthumb = "" + ) { + return Linker::makeThumbLinkObj( + $title, + $file, + $label, + $alt, + $align, + $params, + $framed, + $manualthumb + ); + } + + public function makeThumbLink2( + Title $title, + $file, + $frameParams = [], + $handlerParams = [], + $time = false, + $query = "" + ) { + return Linker::makeThumbLink2( + $title, + $file, + $frameParams, + $handlerParams, + $time, + $query + ); + } + + public function processResponsiveImages( $file, $thumb, $hp ) { + Linker::processResponsiveImages( + $file, + $thumb, + $hp + ); + } + + public function makeBrokenImageLinkObj( + $title, + $label = '', + $query = '', + $unused1 = '', + $unused2 = '', + $time = false + ) { + return Linker::makeBrokenImageLinkObj( + $title, + $label, + $query, + $unused1, + $unused2, + $time + ); + } + + public function makeMediaLinkObj( $title, $html = '', $time = false ) { + return Linker::makeMediaLinkObj( + $title, + $html, + $time + ); + } + + public function makeMediaLinkFile( Title $title, $file, $html = '' ) { + return Linker::makeMediaLinkFile( + $title, + $file, + $html + ); + } + + public function specialLink( $name, $key = '' ) { + return Linker::specialLink( $name, $key ); + } + + public function makeExternalLink( + $url, + $text, + $escape = true, + $linktype = '', + $attribs = [], + $title = null + ) { + return Linker::makeExternalLink( + $url, + $text, + $escape, + $linktype, + $attribs, + $title + ); + } + + public function userLink( $userId, $userName, $altUserName = false ) { + return Linker::userLink( + $userId, + $userName, + $altUserName + ); + } + + public function userToolLinks( + $userId, + $userText, + $redContribsWhenNoEdits = false, + $flags = 0, + $edits = null + ) { + return Linker::userToolLinks( + $userId, + $userText, + $redContribsWhenNoEdits, + $flags, + $edits + ); + } + + public function userToolLinksRedContribs( $userId, $userText, $edits = null ) { + return Linker::userToolLinksRedContribs( + $userId, + $userText, + $edits + ); + } + + public function userTalkLink( $userId, $userText ) { + return Linker::userTalkLink( $userId, $userText ); + } + + public function blockLink( $userId, $userText ) { + return Linker::blockLink( $userId, $userText ); + } + + public function emailLink( $userId, $userText ) { + return Linker::emailLink( $userId, $userText ); + } + + public function revUserLink( $rev, $isPublic = false ) { + return Linker::revUserLink( $rev, $isPublic ); + } + + public function revUserTools( $rev, $isPublic = false ) { + return Linker::revUserTools( $rev, $isPublic ); + } + + public function formatComment( + $comment, + $title = null, + $local = false, + $wikiId = null + ) { + return Linker::formatComment( + $comment, + $title, + $local, + $wikiId + ); + } + + public function formatLinksInComment( + $comment, + $title = null, + $local = false, + $wikiId = null + ) { + return Linker::formatLinksInComment( + $comment, + $title, + $local, + $wikiId + ); + } + + public function makeCommentLink( + Title $title, + $text, + $wikiId = null, + $options = [] + ) { + return Linker::makeCommentLink( + $title, + $text, + $wikiId, + $options + ); + } + + public function normalizeSubpageLink( $contextTitle, $target, &$text ) { + return Linker::normalizeSubpageLink( + $contextTitle, + $target, + $text + ); + } + + public function commentBlock( + $comment, + $title = null, + $local = false, + $wikiId = null + ) { + return Linker::commentBlock( + $comment, + $title, + $local, + $wikiId + ); + } + + public function revComment( Revision $rev, $local = false, $isPublic = false ) { + return Linker::revComment( $rev, $local, $isPublic ); + } + + public function formatRevisionSize( $size ) { + return Linker::formatRevisionSize( $size ); + } + + public function tocIndent() { + return Linker::tocIndent(); + } + + public function tocUnindent( $level ) { + return Linker::tocUnindent( $level ); + } + + public function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) { + return Linker::tocLine( + $anchor, + $tocline, + $tocnumber, + $level, + $sectionIndex + ); + } + + public function tocLineEnd() { + return Linker::tocLineEnd(); + } + + public function tocList( $toc, $lang = false ) { + return Linker::tocList( $toc, $lang ); + } + + public function generateTOC( $tree, $lang = false ) { + return Linker::generateTOC( $tree, $lang ); + } + + public function makeHeadline( + $level, + $attribs, + $anchor, + $html, + $link, + $legacyAnchor = false + ) { + return Linker::makeHeadline( + $level, + $attribs, + $anchor, + $html, + $link, + $legacyAnchor + ); + } + + public function splitTrail( $trail ) { + return Linker::splitTrail( $trail ); + } + + public function generateRollback( + $rev, + IContextSource $context = null, + $options = [ 'verify' ] + ) { + return Linker::generateRollback( + $rev, + $context, + $options + ); + } + + public function getRollbackEditCount( $rev, $verify ) { + return Linker::getRollbackEditCount( $rev, $verify ); + } + + public function buildRollbackLink( + $rev, + IContextSource $context = null, + $editCount = false + ) { + return Linker::buildRollbackLink( + $rev, + $context, + $editCount + ); + } + + /** + * @deprecated since 1.28, use TemplatesOnThisPageFormatter directly + */ + public function formatTemplates( + $templates, + $preview = false, + $section = false, + $more = null + ) { + wfDeprecated( __METHOD__, '1.28' ); + + return Linker::formatTemplates( + $templates, + $preview, + $section, + $more + ); + } + + public function formatHiddenCategories( $hiddencats ) { + return Linker::formatHiddenCategories( $hiddencats ); + } + + /** + * @deprecated since 1.28, use Language::formatSize() directly + */ + public function formatSize( $size ) { + wfDeprecated( __METHOD__, '1.28' ); + + return Linker::formatSize( $size ); + } + + public function titleAttrib( $name, $options = null, array $msgParams = [] ) { + return Linker::titleAttrib( + $name, + $options, + $msgParams + ); + } + + public function accesskey( $name ) { + return Linker::accesskey( $name ); + } + + public function getRevDeleteLink( User $user, Revision $rev, Title $title ) { + return Linker::getRevDeleteLink( + $user, + $rev, + $title + ); + } + + public function revDeleteLink( $query = [], $restricted = false, $delete = true ) { + return Linker::revDeleteLink( + $query, + $restricted, + $delete + ); + } + + public function revDeleteLinkDisabled( $delete = true ) { + return Linker::revDeleteLinkDisabled( $delete ); + } + + public function tooltipAndAccesskeyAttribs( $name, array $msgParams = [] ) { + return Linker::tooltipAndAccesskeyAttribs( + $name, + $msgParams + ); + } + + public function tooltip( $name, $options = null ) { + return Linker::tooltip( $name, $options ); + } + +} diff --git a/www/wiki/includes/EditPage.php b/www/wiki/includes/EditPage.php new file mode 100644 index 00000000..5c37c425 --- /dev/null +++ b/www/wiki/includes/EditPage.php @@ -0,0 +1,4624 @@ +<?php +/** + * User interface for page editing. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\EditPage\TextboxBuilder; +use MediaWiki\EditPage\TextConflictHelper; +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; +use Wikimedia\ScopedCallback; + +/** + * The edit page/HTML interface (split from Article) + * The actual database and text munging is still in Article, + * but it should get easier to call those from alternate + * interfaces. + * + * EditPage cares about two distinct titles: + * $this->mContextTitle is the page that forms submit to, links point to, + * redirects go to, etc. $this->mTitle (as well as $mArticle) is the + * page in the database that is actually being edited. These are + * usually the same, but they are now allowed to be different. + * + * Surgeon General's Warning: prolonged exposure to this class is known to cause + * headaches, which may be fatal. + */ +class EditPage { + /** + * Used for Unicode support checks + */ + const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ'; + + /** + * Status: Article successfully updated + */ + const AS_SUCCESS_UPDATE = 200; + + /** + * Status: Article successfully created + */ + const AS_SUCCESS_NEW_ARTICLE = 201; + + /** + * Status: Article update aborted by a hook function + */ + const AS_HOOK_ERROR = 210; + + /** + * Status: A hook function returned an error + */ + const AS_HOOK_ERROR_EXPECTED = 212; + + /** + * Status: User is blocked from editing this page + */ + const AS_BLOCKED_PAGE_FOR_USER = 215; + + /** + * Status: Content too big (> $wgMaxArticleSize) + */ + const AS_CONTENT_TOO_BIG = 216; + + /** + * Status: this anonymous user is not allowed to edit this page + */ + const AS_READ_ONLY_PAGE_ANON = 218; + + /** + * Status: this logged in user is not allowed to edit this page + */ + const AS_READ_ONLY_PAGE_LOGGED = 219; + + /** + * Status: wiki is in readonly mode (wfReadOnly() == true) + */ + const AS_READ_ONLY_PAGE = 220; + + /** + * Status: rate limiter for action 'edit' was tripped + */ + const AS_RATE_LIMITED = 221; + + /** + * Status: article was deleted while editing and param wpRecreate == false or form + * was not posted + */ + const AS_ARTICLE_WAS_DELETED = 222; + + /** + * Status: user tried to create this page, but is not allowed to do that + * ( Title->userCan('create') == false ) + */ + const AS_NO_CREATE_PERMISSION = 223; + + /** + * Status: user tried to create a blank page and wpIgnoreBlankArticle == false + */ + const AS_BLANK_ARTICLE = 224; + + /** + * Status: (non-resolvable) edit conflict + */ + const AS_CONFLICT_DETECTED = 225; + + /** + * Status: no edit summary given and the user has forceeditsummary set and the user is not + * editing in his own userspace or talkspace and wpIgnoreBlankSummary == false + */ + const AS_SUMMARY_NEEDED = 226; + + /** + * Status: user tried to create a new section without content + */ + const AS_TEXTBOX_EMPTY = 228; + + /** + * Status: article is too big (> $wgMaxArticleSize), after merging in the new section + */ + const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229; + + /** + * Status: WikiPage::doEdit() was unsuccessful + */ + const AS_END = 231; + + /** + * Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex + */ + const AS_SPAM_ERROR = 232; + + /** + * Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false) + */ + const AS_IMAGE_REDIRECT_ANON = 233; + + /** + * Status: logged in user is not allowed to upload (User::isAllowed('upload') == false) + */ + const AS_IMAGE_REDIRECT_LOGGED = 234; + + /** + * Status: user tried to modify the content model, but is not allowed to do that + * ( User::isAllowed('editcontentmodel') == false ) + */ + const AS_NO_CHANGE_CONTENT_MODEL = 235; + + /** + * Status: user tried to create self-redirect (redirect to the same article) and + * wpIgnoreSelfRedirect == false + */ + const AS_SELF_REDIRECT = 236; + + /** + * Status: an error relating to change tagging. Look at the message key for + * more details + */ + const AS_CHANGE_TAG_ERROR = 237; + + /** + * Status: can't parse content + */ + const AS_PARSE_ERROR = 240; + + /** + * Status: when changing the content model is disallowed due to + * $wgContentHandlerUseDB being false + */ + const AS_CANNOT_USE_CUSTOM_MODEL = 241; + + /** + * Status: edit rejected because browser doesn't support Unicode. + */ + const AS_UNICODE_NOT_SUPPORTED = 242; + + /** + * HTML id and name for the beginning of the edit form. + */ + const EDITFORM_ID = 'editform'; + + /** + * Prefix of key for cookie used to pass post-edit state. + * The revision id edited is added after this + */ + const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision'; + + /** + * Duration of PostEdit cookie, in seconds. + * The cookie will be removed instantly if the JavaScript runs. + * + * Otherwise, though, we don't want the cookies to accumulate. + * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible + * limit of only 20 cookies per domain. This still applies at least to some + * versions of IE without full updates: + * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx + * + * A value of 20 minutes should be enough to take into account slow loads and minor + * clock skew while still avoiding cookie accumulation when JavaScript is turned off. + */ + const POST_EDIT_COOKIE_DURATION = 1200; + + /** + * @deprecated for public usage since 1.30 use EditPage::getArticle() + * @var Article + */ + public $mArticle; + /** @var WikiPage */ + private $page; + + /** + * @deprecated for public usage since 1.30 use EditPage::getTitle() + * @var Title + */ + public $mTitle; + + /** @var null|Title */ + private $mContextTitle = null; + + /** @var string */ + public $action = 'submit'; + + /** @var bool */ + public $isConflict = false; + + /** @var bool New page or new section */ + public $isNew = false; + + /** @var bool */ + public $deletedSinceEdit; + + /** @var string */ + public $formtype; + + /** @var bool */ + public $firsttime; + + /** @var bool|stdClass */ + public $lastDelete; + + /** @var bool */ + public $mTokenOk = false; + + /** @var bool */ + public $mTokenOkExceptSuffix = false; + + /** @var bool */ + public $mTriedSave = false; + + /** @var bool */ + public $incompleteForm = false; + + /** @var bool */ + public $tooBig = false; + + /** @var bool */ + public $missingComment = false; + + /** @var bool */ + public $missingSummary = false; + + /** @var bool */ + public $allowBlankSummary = false; + + /** @var bool */ + protected $blankArticle = false; + + /** @var bool */ + protected $allowBlankArticle = false; + + /** @var bool */ + protected $selfRedirect = false; + + /** @var bool */ + protected $allowSelfRedirect = false; + + /** @var string */ + public $autoSumm = ''; + + /** @var string */ + public $hookError = ''; + + /** @var ParserOutput */ + public $mParserOutput; + + /** @var bool Has a summary been preset using GET parameter &summary= ? */ + public $hasPresetSummary = false; + + /** @var Revision|bool|null */ + public $mBaseRevision = false; + + /** @var bool */ + public $mShowSummaryField = true; + + # Form values + + /** @var bool */ + public $save = false; + + /** @var bool */ + public $preview = false; + + /** @var bool */ + public $diff = false; + + /** @var bool */ + public $minoredit = false; + + /** @var bool */ + public $watchthis = false; + + /** @var bool */ + public $recreate = false; + + /** @var string */ + public $textbox1 = ''; + + /** @var string */ + public $textbox2 = ''; + + /** @var string */ + public $summary = ''; + + /** @var bool */ + public $nosummary = false; + + /** @var string */ + public $edittime = ''; + + /** @var int */ + private $editRevId = null; + + /** @var string */ + public $section = ''; + + /** @var string */ + public $sectiontitle = ''; + + /** @var string */ + public $starttime = ''; + + /** @var int */ + public $oldid = 0; + + /** @var int */ + public $parentRevId = 0; + + /** @var string */ + public $editintro = ''; + + /** @var null */ + public $scrolltop = null; + + /** @var bool */ + public $bot = true; + + /** @var string */ + public $contentModel; + + /** @var null|string */ + public $contentFormat = null; + + /** @var null|array */ + private $changeTags = null; + + # Placeholders for text injection by hooks (must be HTML) + # extensions should take care to _append_ to the present value + + /** @var string Before even the preview */ + public $editFormPageTop = ''; + public $editFormTextTop = ''; + public $editFormTextBeforeContent = ''; + public $editFormTextAfterWarn = ''; + public $editFormTextAfterTools = ''; + public $editFormTextBottom = ''; + public $editFormTextAfterContent = ''; + public $previewTextAfterContent = ''; + public $mPreloadContent = null; + + /* $didSave should be set to true whenever an article was successfully altered. */ + public $didSave = false; + public $undidRev = 0; + + public $suppressIntro = false; + + /** @var bool */ + protected $edit; + + /** @var bool|int */ + protected $contentLength = false; + + /** + * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing + */ + private $enableApiEditOverride = false; + + /** + * @var IContextSource + */ + protected $context; + + /** + * @var bool Whether an old revision is edited + */ + private $isOldRev = false; + + /** + * @var string|null What the user submitted in the 'wpUnicodeCheck' field + */ + private $unicodeCheck; + + /** + * Factory function to create an edit conflict helper + * + * @var callable + */ + private $editConflictHelperFactory; + + /** + * @var TextConflictHelper|null + */ + private $editConflictHelper; + + /** + * @param Article $article + */ + public function __construct( Article $article ) { + $this->mArticle = $article; + $this->page = $article->getPage(); // model object + $this->mTitle = $article->getTitle(); + $this->context = $article->getContext(); + + $this->contentModel = $this->mTitle->getContentModel(); + + $handler = ContentHandler::getForModelID( $this->contentModel ); + $this->contentFormat = $handler->getDefaultFormat(); + $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ]; + } + + /** + * @return Article + */ + public function getArticle() { + return $this->mArticle; + } + + /** + * @since 1.28 + * @return IContextSource + */ + public function getContext() { + return $this->context; + } + + /** + * @since 1.19 + * @return Title + */ + public function getTitle() { + return $this->mTitle; + } + + /** + * Set the context Title object + * + * @param Title|null $title Title object or null + */ + public function setContextTitle( $title ) { + $this->mContextTitle = $title; + } + + /** + * Get the context title object. + * If not set, $wgTitle will be returned. This behavior might change in + * the future to return $this->mTitle instead. + * + * @return Title + */ + public function getContextTitle() { + if ( is_null( $this->mContextTitle ) ) { + wfDebugLog( + 'GlobalTitleFail', + __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.' + ); + global $wgTitle; + return $wgTitle; + } else { + return $this->mContextTitle; + } + } + + /** + * Check if the edit page is using OOUI controls + * @return bool Always true + * @deprecated since 1.30 + */ + public function isOouiEnabled() { + wfDeprecated( __METHOD__, '1.30' ); + return true; + } + + /** + * Returns if the given content model is editable. + * + * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants. + * @return bool + * @throws MWException If $modelId has no known handler + */ + public function isSupportedContentModel( $modelId ) { + return $this->enableApiEditOverride === true || + ContentHandler::getForModelID( $modelId )->supportsDirectEditing(); + } + + /** + * Allow editing of content that supports API direct editing, but not general + * direct editing. Set to false by default. + * + * @param bool $enableOverride + */ + public function setApiEditOverride( $enableOverride ) { + $this->enableApiEditOverride = $enableOverride; + } + + /** + * @deprecated since 1.29, call edit directly + */ + public function submit() { + wfDeprecated( __METHOD__, '1.29' ); + $this->edit(); + } + + /** + * This is the function that gets called for "action=edit". It + * sets up various member variables, then passes execution to + * another function, usually showEditForm() + * + * The edit form is self-submitting, so that when things like + * preview and edit conflicts occur, we get the same form back + * with the extra stuff added. Only when the final submission + * is made and all is well do we actually save and redirect to + * the newly-edited page. + */ + public function edit() { + // Allow extensions to modify/prevent this form or submission + if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) { + return; + } + + wfDebug( __METHOD__ . ": enter\n" ); + + $request = $this->context->getRequest(); + // If they used redlink=1 and the page exists, redirect to the main article + if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) { + $this->context->getOutput()->redirect( $this->mTitle->getFullURL() ); + return; + } + + $this->importFormData( $request ); + $this->firsttime = false; + + if ( wfReadOnly() && $this->save ) { + // Force preview + $this->save = false; + $this->preview = true; + } + + if ( $this->save ) { + $this->formtype = 'save'; + } elseif ( $this->preview ) { + $this->formtype = 'preview'; + } elseif ( $this->diff ) { + $this->formtype = 'diff'; + } else { # First time through + $this->firsttime = true; + if ( $this->previewOnOpen() ) { + $this->formtype = 'preview'; + } else { + $this->formtype = 'initial'; + } + } + + $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' ); + if ( $permErrors ) { + wfDebug( __METHOD__ . ": User can't edit\n" ); + // Auto-block user's IP if the account was "hard" blocked + if ( !wfReadOnly() ) { + DeferredUpdates::addCallableUpdate( function () { + $this->context->getUser()->spreadAnyEditBlock(); + } ); + } + $this->displayPermissionsError( $permErrors ); + + return; + } + + $revision = $this->mArticle->getRevisionFetched(); + // Disallow editing revisions with content models different from the current one + // Undo edits being an exception in order to allow reverting content model changes. + if ( $revision + && $revision->getContentModel() !== $this->contentModel + ) { + $prevRev = null; + if ( $this->undidRev ) { + $undidRevObj = Revision::newFromId( $this->undidRev ); + $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null; + } + if ( !$this->undidRev + || !$prevRev + || $prevRev->getContentModel() !== $this->contentModel + ) { + $this->displayViewSourcePage( + $this->getContentObject(), + $this->context->msg( + 'contentmodelediterror', + $revision->getContentModel(), + $this->contentModel + )->plain() + ); + return; + } + } + + $this->isConflict = false; + + # Show applicable editing introductions + if ( $this->formtype == 'initial' || $this->firsttime ) { + $this->showIntro(); + } + + # Attempt submission here. This will check for edit conflicts, + # and redundantly check for locked database, blocked IPs, etc. + # that edit() already checked just in case someone tries to sneak + # in the back door with a hand-edited submission URL. + + if ( 'save' == $this->formtype ) { + $resultDetails = null; + $status = $this->attemptSave( $resultDetails ); + if ( !$this->handleStatus( $status, $resultDetails ) ) { + return; + } + } + + # First time through: get contents, set time for conflict + # checking, etc. + if ( 'initial' == $this->formtype || $this->firsttime ) { + if ( $this->initialiseForm() === false ) { + $this->noSuchSectionPage(); + return; + } + + if ( !$this->mTitle->getArticleID() ) { + Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] ); + } else { + Hooks::run( 'EditFormInitialText', [ $this ] ); + } + + } + + $this->showEditForm(); + } + + /** + * @param string $rigor Same format as Title::getUserPermissionErrors() + * @return array + */ + protected function getEditPermissionErrors( $rigor = 'secure' ) { + $user = $this->context->getUser(); + $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor ); + # Can this title be created? + if ( !$this->mTitle->exists() ) { + $permErrors = array_merge( + $permErrors, + wfArrayDiff2( + $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ), + $permErrors + ) + ); + } + # Ignore some permissions errors when a user is just previewing/viewing diffs + $remove = []; + foreach ( $permErrors as $error ) { + if ( ( $this->preview || $this->diff ) + && ( + $error[0] == 'blockedtext' || + $error[0] == 'autoblockedtext' || + $error[0] == 'systemblockedtext' + ) + ) { + $remove[] = $error; + } + } + $permErrors = wfArrayDiff2( $permErrors, $remove ); + + return $permErrors; + } + + /** + * Display a permissions error page, like OutputPage::showPermissionsErrorPage(), + * but with the following differences: + * - If redlink=1, the user will be redirected to the page + * - If there is content to display or the error occurs while either saving, + * previewing or showing the difference, it will be a + * "View source for ..." page displaying the source code after the error message. + * + * @since 1.19 + * @param array $permErrors Array of permissions errors, as returned by + * Title::getUserPermissionsErrors(). + * @throws PermissionsError + */ + protected function displayPermissionsError( array $permErrors ) { + $out = $this->context->getOutput(); + if ( $this->context->getRequest()->getBool( 'redlink' ) ) { + // The edit page was reached via a red link. + // Redirect to the article page and let them click the edit tab if + // they really want a permission error. + $out->redirect( $this->mTitle->getFullURL() ); + return; + } + + $content = $this->getContentObject(); + + # Use the normal message if there's nothing to display + if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) { + $action = $this->mTitle->exists() ? 'edit' : + ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' ); + throw new PermissionsError( $action, $permErrors ); + } + + $this->displayViewSourcePage( + $content, + $out->formatPermissionsErrorMessage( $permErrors, 'edit' ) + ); + } + + /** + * Display a read-only View Source page + * @param Content $content + * @param string $errorMessage additional wikitext error message to display + */ + protected function displayViewSourcePage( Content $content, $errorMessage = '' ) { + $out = $this->context->getOutput(); + Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] ); + + $out->setRobotPolicy( 'noindex,nofollow' ); + $out->setPageTitle( $this->context->msg( + 'viewsource-title', + $this->getContextTitle()->getPrefixedText() + ) ); + $out->addBacklinkSubtitle( $this->getContextTitle() ); + $out->addHTML( $this->editFormPageTop ); + $out->addHTML( $this->editFormTextTop ); + + if ( $errorMessage !== '' ) { + $out->addWikiText( $errorMessage ); + $out->addHTML( "<hr />\n" ); + } + + # If the user made changes, preserve them when showing the markup + # (This happens when a user is blocked during edit, for instance) + if ( !$this->firsttime ) { + $text = $this->textbox1; + $out->addWikiMsg( 'viewyourtext' ); + } else { + try { + $text = $this->toEditText( $content ); + } catch ( MWException $e ) { + # Serialize using the default format if the content model is not supported + # (e.g. for an old revision with a different model) + $text = $content->serialize(); + } + $out->addWikiMsg( 'viewsourcetext' ); + } + + $out->addHTML( $this->editFormTextBeforeContent ); + $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] ); + $out->addHTML( $this->editFormTextAfterContent ); + + $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) ); + + $out->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + + $out->addHTML( $this->editFormTextBottom ); + if ( $this->mTitle->exists() ) { + $out->returnToMain( null, $this->mTitle ); + } + } + + /** + * Should we show a preview when the edit form is first shown? + * + * @return bool + */ + protected function previewOnOpen() { + $config = $this->context->getConfig(); + $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' ); + $request = $this->context->getRequest(); + if ( $config->get( 'RawHtml' ) ) { + // If raw HTML is enabled, disable preview on open + // since it has to be posted with a token for + // security reasons + return false; + } + if ( $request->getVal( 'preview' ) == 'yes' ) { + // Explicit override from request + return true; + } elseif ( $request->getVal( 'preview' ) == 'no' ) { + // Explicit override from request + return false; + } elseif ( $this->section == 'new' ) { + // Nothing *to* preview for new sections + return false; + } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() ) + && $this->context->getUser()->getOption( 'previewonfirst' ) + ) { + // Standard preference behavior + return true; + } elseif ( !$this->mTitle->exists() + && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] ) + && $previewOnOpenNamespaces[$this->mTitle->getNamespace()] + ) { + // Categories are special + return true; + } else { + return false; + } + } + + /** + * Checks whether the user entered a skin name in uppercase, + * e.g. "User:Example/Monobook.css" instead of "monobook.css" + * + * @return bool + */ + protected function isWrongCaseUserConfigPage() { + if ( $this->mTitle->isUserConfigPage() ) { + $name = $this->mTitle->getSkinFromConfigSubpage(); + $skins = array_merge( + array_keys( Skin::getSkinNames() ), + [ 'common' ] + ); + return !in_array( $name, $skins ) + && in_array( strtolower( $name ), $skins ); + } else { + return false; + } + } + + /** + * Returns whether section editing is supported for the current page. + * Subclasses may override this to replace the default behavior, which is + * to check ContentHandler::supportsSections. + * + * @return bool True if this edit page supports sections, false otherwise. + */ + protected function isSectionEditSupported() { + $contentHandler = ContentHandler::getForTitle( $this->mTitle ); + return $contentHandler->supportsSections(); + } + + /** + * This function collects the form data and uses it to populate various member variables. + * @param WebRequest &$request + * @throws ErrorPageError + */ + public function importFormData( &$request ) { + # Section edit can come from either the form or a link + $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); + + if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) { + throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); + } + + $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; + + if ( $request->wasPosted() ) { + # These fields need to be checked for encoding. + # Also remove trailing whitespace, but don't remove _initial_ + # whitespace from the text boxes. This may be significant formatting. + $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) ); + if ( !$request->getCheck( 'wpTextbox2' ) ) { + // Skip this if wpTextbox2 has input, it indicates that we came + // from a conflict page with raw page text, not a custom form + // modified by subclasses + $textbox1 = $this->importContentFormData( $request ); + if ( $textbox1 !== null ) { + $this->textbox1 = $textbox1; + } + } + + $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' ); + + $this->summary = $request->getText( 'wpSummary' ); + + # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the + # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for + # section titles. + $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary ); + + # Treat sectiontitle the same way as summary. + # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is + # currently doing double duty as both edit summary and section title. Right now this + # is just to allow API edits to work around this limitation, but this should be + # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312). + $this->sectiontitle = $request->getText( 'wpSectionTitle' ); + $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle ); + + $this->edittime = $request->getVal( 'wpEdittime' ); + $this->editRevId = $request->getIntOrNull( 'editRevId' ); + $this->starttime = $request->getVal( 'wpStarttime' ); + + $undidRev = $request->getInt( 'wpUndidRevision' ); + if ( $undidRev ) { + $this->undidRev = $undidRev; + } + + $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); + + if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) { + // wpTextbox1 field is missing, possibly due to being "too big" + // according to some filter rules such as Suhosin's setting for + // suhosin.request.max_value_length (d'oh) + $this->incompleteForm = true; + } else { + // If we receive the last parameter of the request, we can fairly + // claim the POST request has not been truncated. + + // TODO: softened the check for cutover. Once we determine + // that it is safe, we should complete the transition by + // removing the "edittime" clause. + $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' ) + && is_null( $this->edittime ) ); + } + if ( $this->incompleteForm ) { + # If the form is incomplete, force to preview. + wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" ); + wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); + $this->preview = true; + } else { + $this->preview = $request->getCheck( 'wpPreview' ); + $this->diff = $request->getCheck( 'wpDiff' ); + + // Remember whether a save was requested, so we can indicate + // if we forced preview due to session failure. + $this->mTriedSave = !$this->preview; + + if ( $this->tokenOk( $request ) ) { + # Some browsers will not report any submit button + # if the user hits enter in the comment box. + # The unmarked state will be assumed to be a save, + # if the form seems otherwise complete. + wfDebug( __METHOD__ . ": Passed token check.\n" ); + } elseif ( $this->diff ) { + # Failed token check, but only requested "Show Changes". + wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" ); + } else { + # Page might be a hack attempt posted from + # an external site. Preview instead of saving. + wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" ); + $this->preview = true; + } + } + $this->save = !$this->preview && !$this->diff; + if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) { + $this->edittime = null; + } + + if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) { + $this->starttime = null; + } + + $this->recreate = $request->getCheck( 'wpRecreate' ); + + $this->minoredit = $request->getCheck( 'wpMinoredit' ); + $this->watchthis = $request->getCheck( 'wpWatchthis' ); + + $user = $this->context->getUser(); + # Don't force edit summaries when a user is editing their own user or talk page + if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) + && $this->mTitle->getText() == $user->getName() + ) { + $this->allowBlankSummary = true; + } else { + $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ) + || !$user->getOption( 'forceeditsummary' ); + } + + $this->autoSumm = $request->getText( 'wpAutoSummary' ); + + $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' ); + $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' ); + + $changeTags = $request->getVal( 'wpChangeTags' ); + if ( is_null( $changeTags ) || $changeTags === '' ) { + $this->changeTags = []; + } else { + $this->changeTags = array_filter( array_map( 'trim', explode( ',', + $changeTags ) ) ); + } + } else { + # Not a posted form? Start with nothing. + wfDebug( __METHOD__ . ": Not a posted form.\n" ); + $this->textbox1 = ''; + $this->summary = ''; + $this->sectiontitle = ''; + $this->edittime = ''; + $this->editRevId = null; + $this->starttime = wfTimestampNow(); + $this->edit = false; + $this->preview = false; + $this->save = false; + $this->diff = false; + $this->minoredit = false; + // Watch may be overridden by request parameters + $this->watchthis = $request->getBool( 'watchthis', false ); + $this->recreate = false; + + // When creating a new section, we can preload a section title by passing it as the + // preloadtitle parameter in the URL (T15100) + if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { + $this->sectiontitle = $request->getVal( 'preloadtitle' ); + // Once wpSummary isn't being use for setting section titles, we should delete this. + $this->summary = $request->getVal( 'preloadtitle' ); + } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { + $this->summary = $request->getText( 'summary' ); + if ( $this->summary !== '' ) { + $this->hasPresetSummary = true; + } + } + + if ( $request->getVal( 'minor' ) ) { + $this->minoredit = true; + } + } + + $this->oldid = $request->getInt( 'oldid' ); + $this->parentRevId = $request->getInt( 'parentRevId' ); + + $this->bot = $request->getBool( 'bot', true ); + $this->nosummary = $request->getBool( 'nosummary' ); + + // May be overridden by revision. + $this->contentModel = $request->getText( 'model', $this->contentModel ); + // May be overridden by revision. + $this->contentFormat = $request->getText( 'format', $this->contentFormat ); + + try { + $handler = ContentHandler::getForModelID( $this->contentModel ); + } catch ( MWUnknownContentModelException $e ) { + throw new ErrorPageError( + 'editpage-invalidcontentmodel-title', + 'editpage-invalidcontentmodel-text', + [ wfEscapeWikiText( $this->contentModel ) ] + ); + } + + if ( !$handler->isSupportedFormat( $this->contentFormat ) ) { + throw new ErrorPageError( + 'editpage-notsupportedcontentformat-title', + 'editpage-notsupportedcontentformat-text', + [ + wfEscapeWikiText( $this->contentFormat ), + wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) ) + ] + ); + } + + /** + * @todo Check if the desired model is allowed in this namespace, and if + * a transition from the page's current model to the new model is + * allowed. + */ + + $this->editintro = $request->getText( 'editintro', + // Custom edit intro for new sections + $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' ); + + // Allow extensions to modify form data + Hooks::run( 'EditPage::importFormData', [ $this, $request ] ); + } + + /** + * Subpage overridable method for extracting the page content data from the + * posted form to be placed in $this->textbox1, if using customized input + * this method should be overridden and return the page text that will be used + * for saving, preview parsing and so on... + * + * @param WebRequest &$request + * @return string|null + */ + protected function importContentFormData( &$request ) { + return; // Don't do anything, EditPage already extracted wpTextbox1 + } + + /** + * Initialise form fields in the object + * Called on the first invocation, e.g. when a user clicks an edit link + * @return bool If the requested section is valid + */ + public function initialiseForm() { + $this->edittime = $this->page->getTimestamp(); + $this->editRevId = $this->page->getLatest(); + + $content = $this->getContentObject( false ); # TODO: track content object?! + if ( $content === false ) { + return false; + } + $this->textbox1 = $this->toEditText( $content ); + + $user = $this->context->getUser(); + // activate checkboxes if user wants them to be always active + # Sort out the "watch" checkbox + if ( $user->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif ( $user->isWatched( $this->mTitle ) ) { + # Already watched + $this->watchthis = true; + } + if ( $user->getOption( 'minordefault' ) && !$this->isNew ) { + $this->minoredit = true; + } + if ( $this->textbox1 === false ) { + return false; + } + return true; + } + + /** + * @param Content|null $def_content The default value to return + * + * @return Content|null Content on success, $def_content for invalid sections + * + * @since 1.21 + */ + protected function getContentObject( $def_content = null ) { + global $wgContLang; + + $content = false; + + $user = $this->context->getUser(); + $request = $this->context->getRequest(); + // For message page not locally set, use the i18n message. + // For other non-existent articles, use preload text if any. + if ( !$this->mTitle->exists() || $this->section == 'new' ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) { + # If this is a system message, get the default text. + $msg = $this->mTitle->getDefaultMessageText(); + + $content = $this->toEditContent( $msg ); + } + if ( $content === false ) { + # If requested, preload some text. + $preload = $request->getVal( 'preload', + // Custom preload text for new sections + $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' ); + $params = $request->getArray( 'preloadparams', [] ); + + $content = $this->getPreloadedContent( $preload, $params ); + } + // For existing pages, get text based on "undo" or section parameters. + } else { + if ( $this->section != '' ) { + // Get section edit text (returns $def_text for invalid sections) + $orig = $this->getOriginalContent( $user ); + $content = $orig ? $orig->getSection( $this->section ) : null; + + if ( !$content ) { + $content = $def_content; + } + } else { + $undoafter = $request->getInt( 'undoafter' ); + $undo = $request->getInt( 'undo' ); + + if ( $undo > 0 && $undoafter > 0 ) { + $undorev = Revision::newFromId( $undo ); + $oldrev = Revision::newFromId( $undoafter ); + + # Sanity check, make sure it's the right page, + # the revisions exist and they were not deleted. + # Otherwise, $content will be left as-is. + if ( !is_null( $undorev ) && !is_null( $oldrev ) && + !$undorev->isDeleted( Revision::DELETED_TEXT ) && + !$oldrev->isDeleted( Revision::DELETED_TEXT ) + ) { + $content = $this->page->getUndoContent( $undorev, $oldrev ); + + if ( $content === false ) { + # Warn the user that something went wrong + $undoMsg = 'failure'; + } else { + $oldContent = $this->page->getContent( Revision::RAW ); + $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); + $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts ); + if ( $newContent->getModel() !== $oldContent->getModel() ) { + // The undo may change content + // model if its reverting the top + // edit. This can result in + // mismatched content model/format. + $this->contentModel = $newContent->getModel(); + $this->contentFormat = $oldrev->getContentFormat(); + } + + if ( $newContent->equals( $oldContent ) ) { + # Tell the user that the undo results in no change, + # i.e. the revisions were already undone. + $undoMsg = 'nochange'; + $content = false; + } else { + # Inform the user of our success and set an automatic edit summary + $undoMsg = 'success'; + + # If we just undid one rev, use an autosummary + $firstrev = $oldrev->getNext(); + if ( $firstrev && $firstrev->getId() == $undo ) { + $userText = $undorev->getUserText(); + if ( $userText === '' ) { + $undoSummary = $this->context->msg( + 'undo-summary-username-hidden', + $undo + )->inContentLanguage()->text(); + } else { + $undoSummary = $this->context->msg( + 'undo-summary', + $undo, + $userText + )->inContentLanguage()->text(); + } + if ( $this->summary === '' ) { + $this->summary = $undoSummary; + } else { + $this->summary = $undoSummary . $this->context->msg( 'colon-separator' ) + ->inContentLanguage()->text() . $this->summary; + } + $this->undidRev = $undo; + } + $this->formtype = 'diff'; + } + } + } else { + // Failed basic sanity checks. + // Older revisions may have been removed since the link + // was created, or we may simply have got bogus input. + $undoMsg = 'norev'; + } + + $out = $this->context->getOutput(); + // Messages: undo-success, undo-failure, undo-norev, undo-nochange + $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}"; + $this->editFormPageTop .= $out->parse( "<div class=\"{$class}\">" . + $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true ); + } + + if ( $content === false ) { + $content = $this->getOriginalContent( $user ); + } + } + } + + return $content; + } + + /** + * Get the content of the wanted revision, without section extraction. + * + * The result of this function can be used to compare user's input with + * section replaced in its context (using WikiPage::replaceSectionAtRev()) + * to the original text of the edit. + * + * This differs from Article::getContent() that when a missing revision is + * encountered the result will be null and not the + * 'missing-revision' message. + * + * @since 1.19 + * @param User $user The user to get the revision for + * @return Content|null + */ + private function getOriginalContent( User $user ) { + if ( $this->section == 'new' ) { + return $this->getCurrentContent(); + } + $revision = $this->mArticle->getRevisionFetched(); + if ( $revision === null ) { + $handler = ContentHandler::getForModelID( $this->contentModel ); + return $handler->makeEmptyContent(); + } + $content = $revision->getContent( Revision::FOR_THIS_USER, $user ); + return $content; + } + + /** + * Get the edit's parent revision ID + * + * The "parent" revision is the ancestor that should be recorded in this + * page's revision history. It is either the revision ID of the in-memory + * article content, or in the case of a 3-way merge in order to rebase + * across a recoverable edit conflict, the ID of the newer revision to + * which we have rebased this page. + * + * @since 1.27 + * @return int Revision ID + */ + public function getParentRevId() { + if ( $this->parentRevId ) { + return $this->parentRevId; + } else { + return $this->mArticle->getRevIdFetched(); + } + } + + /** + * Get the current content of the page. This is basically similar to + * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty + * content object is returned instead of null. + * + * @since 1.21 + * @return Content + */ + protected function getCurrentContent() { + $rev = $this->page->getRevision(); + $content = $rev ? $rev->getContent( Revision::RAW ) : null; + + if ( $content === false || $content === null ) { + $handler = ContentHandler::getForModelID( $this->contentModel ); + return $handler->makeEmptyContent(); + } elseif ( !$this->undidRev ) { + // Content models should always be the same since we error + // out if they are different before this point (in ->edit()). + // The exception being, during an undo, the current revision might + // differ from the prior revision. + $logger = LoggerFactory::getInstance( 'editpage' ); + if ( $this->contentModel !== $rev->getContentModel() ) { + $logger->warning( "Overriding content model from current edit {prev} to {new}", [ + 'prev' => $this->contentModel, + 'new' => $rev->getContentModel(), + 'title' => $this->getTitle()->getPrefixedDBkey(), + 'method' => __METHOD__ + ] ); + $this->contentModel = $rev->getContentModel(); + } + + // Given that the content models should match, the current selected + // format should be supported. + if ( !$content->isSupportedFormat( $this->contentFormat ) ) { + $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [ + + 'prev' => $this->contentFormat, + 'new' => $rev->getContentFormat(), + 'title' => $this->getTitle()->getPrefixedDBkey(), + 'method' => __METHOD__ + ] ); + $this->contentFormat = $rev->getContentFormat(); + } + } + return $content; + } + + /** + * Use this method before edit() to preload some content into the edit box + * + * @param Content $content + * + * @since 1.21 + */ + public function setPreloadedContent( Content $content ) { + $this->mPreloadContent = $content; + } + + /** + * Get the contents to be preloaded into the box, either set by + * an earlier setPreloadText() or by loading the given page. + * + * @param string $preload Representing the title to preload from. + * @param array $params Parameters to use (interface-message style) in the preloaded text + * + * @return Content + * + * @since 1.21 + */ + protected function getPreloadedContent( $preload, $params = [] ) { + if ( !empty( $this->mPreloadContent ) ) { + return $this->mPreloadContent; + } + + $handler = ContentHandler::getForModelID( $this->contentModel ); + + if ( $preload === '' ) { + return $handler->makeEmptyContent(); + } + + $user = $this->context->getUser(); + $title = Title::newFromText( $preload ); + # Check for existence to avoid getting MediaWiki:Noarticletext + if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) { + // TODO: somehow show a warning to the user! + return $handler->makeEmptyContent(); + } + + $page = WikiPage::factory( $title ); + if ( $page->isRedirect() ) { + $title = $page->getRedirectTarget(); + # Same as before + if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) { + // TODO: somehow show a warning to the user! + return $handler->makeEmptyContent(); + } + $page = WikiPage::factory( $title ); + } + + $parserOptions = ParserOptions::newFromUser( $user ); + $content = $page->getContent( Revision::RAW ); + + if ( !$content ) { + // TODO: somehow show a warning to the user! + return $handler->makeEmptyContent(); + } + + if ( $content->getModel() !== $handler->getModelID() ) { + $converted = $content->convert( $handler->getModelID() ); + + if ( !$converted ) { + // TODO: somehow show a warning to the user! + wfDebug( "Attempt to preload incompatible content: " . + "can't convert " . $content->getModel() . + " to " . $handler->getModelID() ); + + return $handler->makeEmptyContent(); + } + + $content = $converted; + } + + return $content->preloadTransform( $title, $parserOptions, $params ); + } + + /** + * Make sure the form isn't faking a user's credentials. + * + * @param WebRequest &$request + * @return bool + * @private + */ + public function tokenOk( &$request ) { + $token = $request->getVal( 'wpEditToken' ); + $user = $this->context->getUser(); + $this->mTokenOk = $user->matchEditToken( $token ); + $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token ); + return $this->mTokenOk; + } + + /** + * Sets post-edit cookie indicating the user just saved a particular revision. + * + * This uses a temporary cookie for each revision ID so separate saves will never + * interfere with each other. + * + * Article::view deletes the cookie on server-side after the redirect and + * converts the value to the global JavaScript variable wgPostEdit. + * + * If the variable were set on the server, it would be cached, which is unwanted + * since the post-edit state should only apply to the load right after the save. + * + * @param int $statusValue The status value (to check for new article status) + */ + protected function setPostEditCookie( $statusValue ) { + $revisionId = $this->page->getLatest(); + $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId; + + $val = 'saved'; + if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) { + $val = 'created'; + } elseif ( $this->oldid ) { + $val = 'restored'; + } + + $response = $this->context->getRequest()->response(); + $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION ); + } + + /** + * Attempt submission + * @param array|bool &$resultDetails See docs for $result in internalAttemptSave + * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError + * @return Status The resulting status object. + */ + public function attemptSave( &$resultDetails = false ) { + # Allow bots to exempt some edits from bot flagging + $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot; + $status = $this->internalAttemptSave( $resultDetails, $bot ); + + Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] ); + + return $status; + } + + /** + * Log when a page was successfully saved after the edit conflict view + */ + private function incrementResolvedConflicts() { + if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) { + return; + } + + $this->getEditConflictHelper()->incrementResolvedStats(); + } + + /** + * Handle status, such as after attempt save + * + * @param Status $status + * @param array|bool $resultDetails + * + * @throws ErrorPageError + * @return bool False, if output is done, true if rest of the form should be displayed + */ + private function handleStatus( Status $status, $resultDetails ) { + /** + * @todo FIXME: once the interface for internalAttemptSave() is made + * nicer, this should use the message in $status + */ + if ( $status->value == self::AS_SUCCESS_UPDATE + || $status->value == self::AS_SUCCESS_NEW_ARTICLE + ) { + $this->incrementResolvedConflicts(); + + $this->didSave = true; + if ( !$resultDetails['nullEdit'] ) { + $this->setPostEditCookie( $status->value ); + } + } + + $out = $this->context->getOutput(); + + // "wpExtraQueryRedirect" is a hidden input to modify + // after save URL and is not used by actual edit form + $request = $this->context->getRequest(); + $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' ); + + switch ( $status->value ) { + case self::AS_HOOK_ERROR_EXPECTED: + case self::AS_CONTENT_TOO_BIG: + case self::AS_ARTICLE_WAS_DELETED: + case self::AS_CONFLICT_DETECTED: + case self::AS_SUMMARY_NEEDED: + case self::AS_TEXTBOX_EMPTY: + case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: + case self::AS_END: + case self::AS_BLANK_ARTICLE: + case self::AS_SELF_REDIRECT: + return true; + + case self::AS_HOOK_ERROR: + return false; + + case self::AS_CANNOT_USE_CUSTOM_MODEL: + case self::AS_PARSE_ERROR: + case self::AS_UNICODE_NOT_SUPPORTED: + $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' ); + return true; + + case self::AS_SUCCESS_NEW_ARTICLE: + $query = $resultDetails['redirect'] ? 'redirect=no' : ''; + if ( $extraQueryRedirect ) { + if ( $query === '' ) { + $query = $extraQueryRedirect; + } else { + $query = $query . '&' . $extraQueryRedirect; + } + } + $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : ''; + $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor ); + return false; + + case self::AS_SUCCESS_UPDATE: + $extraQuery = ''; + $sectionanchor = $resultDetails['sectionanchor']; + + // Give extensions a chance to modify URL query on update + Hooks::run( + 'ArticleUpdateBeforeRedirect', + [ $this->mArticle, &$sectionanchor, &$extraQuery ] + ); + + if ( $resultDetails['redirect'] ) { + if ( $extraQuery == '' ) { + $extraQuery = 'redirect=no'; + } else { + $extraQuery = 'redirect=no&' . $extraQuery; + } + } + if ( $extraQueryRedirect ) { + if ( $extraQuery === '' ) { + $extraQuery = $extraQueryRedirect; + } else { + $extraQuery = $extraQuery . '&' . $extraQueryRedirect; + } + } + + $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); + return false; + + case self::AS_SPAM_ERROR: + $this->spamPageWithContent( $resultDetails['spam'] ); + return false; + + case self::AS_BLOCKED_PAGE_FOR_USER: + throw new UserBlockedError( $this->context->getUser()->getBlock() ); + + case self::AS_IMAGE_REDIRECT_ANON: + case self::AS_IMAGE_REDIRECT_LOGGED: + throw new PermissionsError( 'upload' ); + + case self::AS_READ_ONLY_PAGE_ANON: + case self::AS_READ_ONLY_PAGE_LOGGED: + throw new PermissionsError( 'edit' ); + + case self::AS_READ_ONLY_PAGE: + throw new ReadOnlyError; + + case self::AS_RATE_LIMITED: + throw new ThrottledError(); + + case self::AS_NO_CREATE_PERMISSION: + $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage'; + throw new PermissionsError( $permission ); + + case self::AS_NO_CHANGE_CONTENT_MODEL: + throw new PermissionsError( 'editcontentmodel' ); + + default: + // We don't recognize $status->value. The only way that can happen + // is if an extension hook aborted from inside ArticleSave. + // Render the status object into $this->hookError + // FIXME this sucks, we should just use the Status object throughout + $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() . + '</div>'; + return true; + } + } + + /** + * Run hooks that can filter edits just before they get saved. + * + * @param Content $content The Content to filter. + * @param Status $status For reporting the outcome to the caller + * @param User $user The user performing the edit + * + * @return bool + */ + protected function runPostMergeFilters( Content $content, Status $status, User $user ) { + // Run old style post-section-merge edit filter + if ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; + return false; + } + + // Run new style post-section-merge edit filter + if ( !Hooks::run( 'EditFilterMergedContent', + [ $this->context, $content, $status, $this->summary, + $user, $this->minoredit ] ) + ) { + # Error messages etc. could be handled within the hook... + if ( $status->isGood() ) { + $status->fatal( 'hookaborted' ); + // Not setting $this->hookError here is a hack to allow the hook + // to cause a return to the edit page without $this->hookError + // being set. This is used by ConfirmEdit to display a captcha + // without any error message cruft. + } else { + $this->hookError = $this->formatStatusErrors( $status ); + } + // Use the existing $status->value if the hook set it + if ( !$status->value ) { + $status->value = self::AS_HOOK_ERROR; + } + return false; + } elseif ( !$status->isOK() ) { + # ...or the hook could be expecting us to produce an error + // FIXME this sucks, we should just use the Status object throughout + $this->hookError = $this->formatStatusErrors( $status ); + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; + return false; + } + + return true; + } + + /** + * Wrap status errors in an errorbox for increased visibility + * + * @param Status $status + * @return string Wikitext + */ + private function formatStatusErrors( Status $status ) { + $errmsg = $status->getWikiText( + 'edit-error-short', + 'edit-error-long', + $this->context->getLanguage() + ); + return <<<ERROR +<div class="errorbox"> +{$errmsg} +</div> +<br clear="all" /> +ERROR; + } + + /** + * Return the summary to be used for a new section. + * + * @param string $sectionanchor Set to the section anchor text + * @return string + */ + private function newSectionSummary( &$sectionanchor = null ) { + global $wgParser; + + if ( $this->sectiontitle !== '' ) { + $sectionanchor = $this->guessSectionName( $this->sectiontitle ); + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + return $this->context->msg( 'newsectionsummary' ) + ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); + } + } elseif ( $this->summary !== '' ) { + $sectionanchor = $this->guessSectionName( $this->summary ); + # This is a new section, so create a link to the new section + # in the revision summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + return $this->context->msg( 'newsectionsummary' ) + ->rawParams( $cleanSummary )->inContentLanguage()->text(); + } + return $this->summary; + } + + /** + * Attempt submission (no UI) + * + * @param array &$result Array to add statuses to, currently with the + * possible keys: + * - spam (string): Spam string from content if any spam is detected by + * matchSpamRegex. + * - sectionanchor (string): Section anchor for a section save. + * - nullEdit (bool): Set if doEditContent is OK. True if null edit, + * false otherwise. + * - redirect (bool): Set if doEditContent is OK. True if resulting + * revision is a redirect. + * @param bool $bot True if edit is being made under the bot right. + * + * @return Status Status object, possibly with a message, but always with + * one of the AS_* constants in $status->value, + * + * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to + * various error display idiosyncrasies. There are also lots of cases + * where error metadata is set in the object and retrieved later instead + * of being returned, e.g. AS_CONTENT_TOO_BIG and + * AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some + * time. + */ + public function internalAttemptSave( &$result, $bot = false ) { + $status = Status::newGood(); + $user = $this->context->getUser(); + + if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) { + wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + return $status; + } + + if ( $this->unicodeCheck !== self::UNICODE_CHECK ) { + $status->fatal( 'unicode-support-fail' ); + $status->value = self::AS_UNICODE_NOT_SUPPORTED; + return $status; + } + + $request = $this->context->getRequest(); + $spam = $request->getText( 'wpAntispam' ); + if ( $spam !== '' ) { + wfDebugLog( + 'SimpleAntiSpam', + $user->getName() . + ' editing "' . + $this->mTitle->getPrefixedText() . + '" submitted bogus field "' . + $spam . + '"' + ); + $status->fatal( 'spamprotectionmatch', false ); + $status->value = self::AS_SPAM_ERROR; + return $status; + } + + try { + # Construct Content object + $textbox_content = $this->toEditContent( $this->textbox1 ); + } catch ( MWContentSerializationException $ex ) { + $status->fatal( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); + $status->value = self::AS_PARSE_ERROR; + return $status; + } + + # Check image redirect + if ( $this->mTitle->getNamespace() == NS_FILE && + $textbox_content->isRedirect() && + !$user->isAllowed( 'upload' ) + ) { + $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; + $status->setResult( false, $code ); + + return $status; + } + + # Check for spam + $match = self::matchSummarySpamRegex( $this->summary ); + if ( $match === false && $this->section == 'new' ) { + # $wgSpamRegex is enforced on this new heading/summary because, unlike + # regular summaries, it is added to the actual wikitext. + if ( $this->sectiontitle !== '' ) { + # This branch is taken when the API is used with the 'sectiontitle' parameter. + $match = self::matchSpamRegex( $this->sectiontitle ); + } else { + # This branch is taken when the "Add Topic" user interface is used, or the API + # is used with the 'summary' parameter. + $match = self::matchSpamRegex( $this->summary ); + } + } + if ( $match === false ) { + $match = self::matchSpamRegex( $this->textbox1 ); + } + if ( $match !== false ) { + $result['spam'] = $match; + $ip = $request->getIP(); + $pdbk = $this->mTitle->getPrefixedDBkey(); + $match = str_replace( "\n", '', $match ); + wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); + $status->fatal( 'spamprotectionmatch', $match ); + $status->value = self::AS_SPAM_ERROR; + return $status; + } + if ( !Hooks::run( + 'EditFilter', + [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] ) + ) { + # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + return $status; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; + return $status; + } + + if ( $user->isBlockedFrom( $this->mTitle, false ) ) { + // Auto-block user's IP if the account was "hard" blocked + if ( !wfReadOnly() ) { + $user->spreadAnyEditBlock(); + } + # Check block state against master, thus 'false'. + $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); + return $status; + } + + $this->contentLength = strlen( $this->textbox1 ); + $config = $this->context->getConfig(); + $maxArticleSize = $config->get( 'MaxArticleSize' ); + if ( $this->contentLength > $maxArticleSize * 1024 ) { + // Error will be displayed by showEditForm() + $this->tooBig = true; + $status->setResult( false, self::AS_CONTENT_TOO_BIG ); + return $status; + } + + if ( !$user->isAllowed( 'edit' ) ) { + if ( $user->isAnon() ) { + $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); + return $status; + } else { + $status->fatal( 'readonlytext' ); + $status->value = self::AS_READ_ONLY_PAGE_LOGGED; + return $status; + } + } + + $changingContentModel = false; + if ( $this->contentModel !== $this->mTitle->getContentModel() ) { + if ( !$config->get( 'ContentHandlerUseDB' ) ) { + $status->fatal( 'editpage-cannot-use-custom-model' ); + $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; + return $status; + } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) { + $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + return $status; + } + // Make sure the user can edit the page under the new content model too + $titleWithNewContentModel = clone $this->mTitle; + $titleWithNewContentModel->setContentModel( $this->contentModel ); + if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user ) + || !$titleWithNewContentModel->userCan( 'edit', $user ) + ) { + $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + return $status; + } + + $changingContentModel = true; + $oldContentModel = $this->mTitle->getContentModel(); + } + + if ( $this->changeTags ) { + $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange( + $this->changeTags, $user ); + if ( !$changeTagsStatus->isOK() ) { + $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR; + return $changeTagsStatus; + } + } + + if ( wfReadOnly() ) { + $status->fatal( 'readonlytext' ); + $status->value = self::AS_READ_ONLY_PAGE; + return $status; + } + if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) + || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) ) + ) { + $status->fatal( 'actionthrottledtext' ); + $status->value = self::AS_RATE_LIMITED; + return $status; + } + + # If the article has been deleted while editing, don't save it without + # confirmation + if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { + $status->setResult( false, self::AS_ARTICLE_WAS_DELETED ); + return $status; + } + + # Load the page data from the master. If anything changes in the meantime, + # we detect it by using page_latest like a token in a 1 try compare-and-swap. + $this->page->loadPageData( 'fromdbmaster' ); + $new = !$this->page->exists(); + + if ( $new ) { + // Late check for create permission, just in case *PARANOIA* + if ( !$this->mTitle->userCan( 'create', $user ) ) { + $status->fatal( 'nocreatetext' ); + $status->value = self::AS_NO_CREATE_PERMISSION; + wfDebug( __METHOD__ . ": no create permission\n" ); + return $status; + } + + // Don't save a new page if it's blank or if it's a MediaWiki: + // message with content equivalent to default (allow empty pages + // in this case to disable messages, see T52124) + $defaultMessageText = $this->mTitle->getDefaultMessageText(); + if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) { + $defaultText = $defaultMessageText; + } else { + $defaultText = ''; + } + + if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) { + $this->blankArticle = true; + $status->fatal( 'blankarticle' ); + $status->setResult( false, self::AS_BLANK_ARTICLE ); + return $status; + } + + if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) { + return $status; + } + + $content = $textbox_content; + + $result['sectionanchor'] = ''; + if ( $this->section == 'new' ) { + if ( $this->sectiontitle !== '' ) { + // Insert the section title above the content. + $content = $content->addSectionHeader( $this->sectiontitle ); + } elseif ( $this->summary !== '' ) { + // Insert the section title above the content. + $content = $content->addSectionHeader( $this->summary ); + } + $this->summary = $this->newSectionSummary( $result['sectionanchor'] ); + } + + $status->value = self::AS_SUCCESS_NEW_ARTICLE; + + } else { # not $new + + # Article exists. Check for edit conflict. + + $this->page->clear(); # Force reload of dates, etc. + $timestamp = $this->page->getTimestamp(); + $latest = $this->page->getLatest(); + + wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); + + // Check editRevId if set, which handles same-second timestamp collisions + if ( $timestamp != $this->edittime + || ( $this->editRevId !== null && $this->editRevId != $latest ) + ) { + $this->isConflict = true; + if ( $this->section == 'new' ) { + if ( $this->page->getUserText() == $user->getName() && + $this->page->getComment() == $this->newSectionSummary() + ) { + // Probably a duplicate submission of a new comment. + // This can happen when CDN resends a request after + // a timeout but the first one actually went through. + wfDebug( __METHOD__ + . ": duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( __METHOD__ . ": conflict suppressed; new section\n" ); + } + } elseif ( $this->section == '' + && Revision::userWasLastToEdit( + DB_MASTER, $this->mTitle->getArticleID(), + $user->getId(), $this->edittime + ) + ) { + # Suppress edit conflict with self, except for section edits where merging is required. + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; + } + } + + // If sectiontitle is set, use it, otherwise use the summary as the section title. + if ( $this->sectiontitle !== '' ) { + $sectionTitle = $this->sectiontitle; + } else { + $sectionTitle = $this->summary; + } + + $content = null; + + if ( $this->isConflict ) { + wfDebug( __METHOD__ + . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'" + . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" ); + // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case + // ...or disable section editing for non-current revisions (not exposed anyway). + if ( $this->editRevId !== null ) { + $content = $this->page->replaceSectionAtRev( + $this->section, + $textbox_content, + $sectionTitle, + $this->editRevId + ); + } else { + $content = $this->page->replaceSectionContent( + $this->section, + $textbox_content, + $sectionTitle, + $this->edittime + ); + } + } else { + wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" ); + $content = $this->page->replaceSectionContent( + $this->section, + $textbox_content, + $sectionTitle + ); + } + + if ( is_null( $content ) ) { + wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $content = $textbox_content; // do not try to merge here! + } elseif ( $this->isConflict ) { + # Attempt merge + if ( $this->mergeChangesIntoContent( $content ) ) { + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = ContentHandler::getContentText( $content ); + wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); + } + } + + if ( $this->isConflict ) { + $status->setResult( false, self::AS_CONFLICT_DETECTED ); + return $status; + } + + if ( !$this->runPostMergeFilters( $content, $status, $user ) ) { + return $status; + } + + if ( $this->section == 'new' ) { + // Handle the user preference to force summaries here + if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; + return $status; + } + + // Do not allow the user to post an empty comment + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + $status->fatal( 'missingcommenttext' ); + $status->value = self::AS_TEXTBOX_EMPTY; + return $status; + } + } elseif ( !$this->allowBlankSummary + && !$content->equals( $this->getOriginalContent( $user ) ) + && !$content->isRedirect() + && md5( $this->summary ) == $this->autoSumm + ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); + $status->value = self::AS_SUMMARY_NEEDED; + return $status; + } + + # All's well + $sectionanchor = ''; + if ( $this->section == 'new' ) { + $this->summary = $this->newSectionSummary( $sectionanchor ); + } elseif ( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect + # to edited section if header found. + # XXX: Might be better to integrate this into Article::replaceSectionAtRev + # for duplicate heading checking and maybe parsing. + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # We can't deal with anchors, includes, html etc in the header for now, + # headline would need to be parsed to improve this. + if ( $hasmatch && strlen( $matches[2] ) > 0 ) { + $sectionanchor = $this->guessSectionName( $matches[2] ); + } + } + $result['sectionanchor'] = $sectionanchor; + + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $this->toEditText( $content ); + $this->section = ''; + + $status->value = self::AS_SUCCESS_UPDATE; + } + + if ( !$this->allowSelfRedirect + && $content->isRedirect() + && $content->getRedirectTarget()->equals( $this->getTitle() ) + ) { + // If the page already redirects to itself, don't warn. + $currentTarget = $this->getCurrentContent()->getRedirectTarget(); + if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) { + $this->selfRedirect = true; + $status->fatal( 'selfredirect' ); + $status->value = self::AS_SELF_REDIRECT; + return $status; + } + } + + // Check for length errors again now that the section is merged in + $this->contentLength = strlen( $this->toEditText( $content ) ); + if ( $this->contentLength > $maxArticleSize * 1024 ) { + $this->tooBig = true; + $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); + return $status; + } + + $flags = EDIT_AUTOSUMMARY | + ( $new ? EDIT_NEW : EDIT_UPDATE ) | + ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ); + + $doEditStatus = $this->page->doEditContent( + $content, + $this->summary, + $flags, + false, + $user, + $content->getDefaultFormat(), + $this->changeTags, + $this->undidRev + ); + + if ( !$doEditStatus->isOK() ) { + // Failure from doEdit() + // Show the edit conflict page for certain recognized errors from doEdit(), + // but don't show it for errors from extension hooks + $errors = $doEditStatus->getErrorsArray(); + if ( in_array( $errors[0][0], + [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] ) + ) { + $this->isConflict = true; + // Destroys data doEdit() put in $status->value but who cares + $doEditStatus->value = self::AS_END; + } + return $doEditStatus; + } + + $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' ); + if ( $result['nullEdit'] ) { + // We don't know if it was a null edit until now, so increment here + $user->pingLimiter( 'linkpurge' ); + } + $result['redirect'] = $content->isRedirect(); + + $this->updateWatchlist(); + + // If the content model changed, add a log entry + if ( $changingContentModel ) { + $this->addContentModelChangeLogEntry( + $user, + $new ? false : $oldContentModel, + $this->contentModel, + $this->summary + ); + } + + return $status; + } + + /** + * @param User $user + * @param string|false $oldModel false if the page is being newly created + * @param string $newModel + * @param string $reason + */ + protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) { + $new = $oldModel === false; + $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' ); + $log->setPerformer( $user ); + $log->setTarget( $this->mTitle ); + $log->setComment( $reason ); + $log->setParameters( [ + '4::oldmodel' => $oldModel, + '5::newmodel' => $newModel + ] ); + $logid = $log->insert(); + $log->publish( $logid ); + } + + /** + * Register the change of watch status + */ + protected function updateWatchlist() { + $user = $this->context->getUser(); + if ( !$user->isLoggedIn() ) { + return; + } + + $title = $this->mTitle; + $watch = $this->watchthis; + // Do this in its own transaction to reduce contention... + DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) { + if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) { + return; // nothing to change + } + WatchAction::doWatchOrUnwatch( $watch, $title, $user ); + } ); + } + + /** + * Attempts to do 3-way merge of edit content with a base revision + * and current content, in case of edit conflict, in whichever way appropriate + * for the content type. + * + * @since 1.21 + * + * @param Content $editContent + * + * @return bool + */ + private function mergeChangesIntoContent( &$editContent ) { + $db = wfGetDB( DB_MASTER ); + + // This is the revision the editor started from + $baseRevision = $this->getBaseRevision(); + $baseContent = $baseRevision ? $baseRevision->getContent() : null; + + if ( is_null( $baseContent ) ) { + return false; + } + + // The current state, we want to merge updates into it + $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); + $currentContent = $currentRevision ? $currentRevision->getContent() : null; + + if ( is_null( $currentContent ) ) { + return false; + } + + $handler = ContentHandler::getForModelID( $baseContent->getModel() ); + + $result = $handler->merge3( $baseContent, $editContent, $currentContent ); + + if ( $result ) { + $editContent = $result; + // Update parentRevId to what we just merged. + $this->parentRevId = $currentRevision->getId(); + return true; + } + + return false; + } + + /** + * @note: this method is very poorly named. If the user opened the form with ?oldid=X, + * one might think of X as the "base revision", which is NOT what this returns. + * @return Revision|null Current version when the edit was started + */ + public function getBaseRevision() { + if ( !$this->mBaseRevision ) { + $db = wfGetDB( DB_MASTER ); + $this->mBaseRevision = $this->editRevId + ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST ) + : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime ); + } + return $this->mBaseRevision; + } + + /** + * Check given input text against $wgSpamRegex, and return the text of the first match. + * + * @param string $text + * + * @return string|bool Matching string or false + */ + public static function matchSpamRegex( $text ) { + global $wgSpamRegex; + // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. + $regexes = (array)$wgSpamRegex; + return self::matchSpamRegexInternal( $text, $regexes ); + } + + /** + * Check given input text against $wgSummarySpamRegex, and return the text of the first match. + * + * @param string $text + * + * @return string|bool Matching string or false + */ + public static function matchSummarySpamRegex( $text ) { + global $wgSummarySpamRegex; + $regexes = (array)$wgSummarySpamRegex; + return self::matchSpamRegexInternal( $text, $regexes ); + } + + /** + * @param string $text + * @param array $regexes + * @return bool|string + */ + protected static function matchSpamRegexInternal( $text, $regexes ) { + foreach ( $regexes as $regex ) { + $matches = []; + if ( preg_match( $regex, $text, $matches ) ) { + return $matches[0]; + } + } + return false; + } + + public function setHeaders() { + $out = $this->context->getOutput(); + + $out->addModules( 'mediawiki.action.edit' ); + $out->addModuleStyles( 'mediawiki.action.edit.styles' ); + $out->addModuleStyles( 'mediawiki.editfont.styles' ); + + $user = $this->context->getUser(); + if ( $user->getOption( 'showtoolbar' ) ) { + // The addition of default buttons is handled by getEditToolbar() which + // has its own dependency on this module. The call here ensures the module + // is loaded in time (it has position "top") for other modules to register + // buttons (e.g. extensions, gadgets, user scripts). + $out->addModules( 'mediawiki.toolbar' ); + } + + if ( $user->getOption( 'uselivepreview' ) ) { + $out->addModules( 'mediawiki.action.edit.preview' ); + } + + if ( $user->getOption( 'useeditwarning' ) ) { + $out->addModules( 'mediawiki.action.edit.editWarning' ); + } + + # Enabled article-related sidebar, toplinks, etc. + $out->setArticleRelated( true ); + + $contextTitle = $this->getContextTitle(); + if ( $this->isConflict ) { + $msg = 'editconflict'; + } elseif ( $contextTitle->exists() && $this->section != '' ) { + $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; + } else { + $msg = $contextTitle->exists() + || ( $contextTitle->getNamespace() == NS_MEDIAWIKI + && $contextTitle->getDefaultMessageText() !== false + ) + ? 'editing' + : 'creating'; + } + + # Use the title defined by DISPLAYTITLE magic word when present + # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text. + # setPageTitle() treats the input as wikitext, which should be safe in either case. + $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false; + if ( $displayTitle === false ) { + $displayTitle = $contextTitle->getPrefixedText(); + } + $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) ); + + $config = $this->context->getConfig(); + + # Transmit the name of the message to JavaScript for live preview + # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys + $out->addJsConfigVars( [ + 'wgEditMessage' => $msg, + 'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ), + ] ); + + // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other + // editors, etc. + $out->addJsConfigVars( + 'wgEditSubmitButtonLabelPublish', + $config->get( 'EditSubmitButtonLabelPublish' ) + ); + } + + /** + * Show all applicable editing introductions + */ + protected function showIntro() { + if ( $this->suppressIntro ) { + return; + } + + $out = $this->context->getOutput(); + $namespace = $this->mTitle->getNamespace(); + + if ( $namespace == NS_MEDIAWIKI ) { + # Show a warning if editing an interface message + $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' ); + # If this is a default message (but not css, json, or js), + # show a hint that it is translatable on translatewiki.net + if ( + !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS ) + && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON ) + && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) + ) { + $defaultMessageText = $this->mTitle->getDefaultMessageText(); + if ( $defaultMessageText !== false ) { + $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>", + 'translateinterface' ); + } + } + } elseif ( $namespace == NS_FILE ) { + # Show a hint to shared repo + $file = wfFindFile( $this->mTitle ); + if ( $file && !$file->isLocal() ) { + $descUrl = $file->getDescriptionUrl(); + # there must be a description url to show a hint to shared repo + if ( $descUrl ) { + if ( !$this->mTitle->exists() ) { + $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [ + 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl + ] ); + } else { + $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [ + 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl + ] ); + } + } + } + } + + # Show a warning message when someone creates/edits a user (talk) page but the user does not exist + # Show log extract when the user is currently blocked + if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { + $username = explode( '/', $this->mTitle->getText(), 2 )[0]; + $user = User::newFromName( $username, false /* allow IP users */ ); + $ip = User::isIP( $username ); + $block = Block::newFromTarget( $user, $user ); + if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist + $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", + [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] ); + } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { + # Show log extract if the user is currently blocked + LogEventsList::showLogExtract( + $out, + 'block', + MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(), + '', + [ + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => [ + 'blocked-notice-logextract', + $user->getName() # Support GENDER in notice + ] + ] + ); + } + } + # Try to add a custom edit intro, or use the standard one if this is not possible. + if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) { + $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl( + $this->context->msg( 'helppage' )->inContentLanguage()->text() + ) ); + if ( $this->context->getUser()->isLoggedIn() ) { + $out->wrapWikiMsg( + // Suppress the external link icon, consider the help url an internal one + "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>", + [ + 'newarticletext', + $helpLink + ] + ); + } else { + $out->wrapWikiMsg( + // Suppress the external link icon, consider the help url an internal one + "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>", + [ + 'newarticletextanon', + $helpLink + ] + ); + } + } + # Give a notice if the user is editing a deleted/moved page... + if ( !$this->mTitle->exists() ) { + $dbr = wfGetDB( DB_REPLICA ); + + LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle, + '', + [ + 'lim' => 10, + 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ], + 'showIfEmpty' => false, + 'msgKey' => [ 'recreate-moveddeleted-warn' ] + ] + ); + } + } + + /** + * Attempt to show a custom editing introduction, if supplied + * + * @return bool + */ + protected function showCustomIntro() { + if ( $this->editintro ) { + $title = Title::newFromText( $this->editintro ); + if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) { + // Added using template syntax, to take <noinclude>'s into account. + $this->context->getOutput()->addWikiTextTitleTidy( + '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>', + $this->mTitle + ); + return true; + } + } + return false; + } + + /** + * Gets an editable textual representation of $content. + * The textual representation can be turned by into a Content object by the + * toEditContent() method. + * + * If $content is null or false or a string, $content is returned unchanged. + * + * If the given Content object is not of a type that can be edited using + * the text base EditPage, an exception will be raised. Set + * $this->allowNonTextContent to true to allow editing of non-textual + * content. + * + * @param Content|null|bool|string $content + * @return string The editable text form of the content. + * + * @throws MWException If $content is not an instance of TextContent and + * $this->allowNonTextContent is not true. + */ + protected function toEditText( $content ) { + if ( $content === null || $content === false || is_string( $content ) ) { + return $content; + } + + if ( !$this->isSupportedContentModel( $content->getModel() ) ) { + throw new MWException( 'This content model is not supported: ' . $content->getModel() ); + } + + return $content->serialize( $this->contentFormat ); + } + + /** + * Turns the given text into a Content object by unserializing it. + * + * If the resulting Content object is not of a type that can be edited using + * the text base EditPage, an exception will be raised. Set + * $this->allowNonTextContent to true to allow editing of non-textual + * content. + * + * @param string|null|bool $text Text to unserialize + * @return Content|bool|null The content object created from $text. If $text was false + * or null, false resp. null will be returned instead. + * + * @throws MWException If unserializing the text results in a Content + * object that is not an instance of TextContent and + * $this->allowNonTextContent is not true. + */ + protected function toEditContent( $text ) { + if ( $text === false || $text === null ) { + return $text; + } + + $content = ContentHandler::makeContent( $text, $this->getTitle(), + $this->contentModel, $this->contentFormat ); + + if ( !$this->isSupportedContentModel( $content->getModel() ) ) { + throw new MWException( 'This content model is not supported: ' . $content->getModel() ); + } + + return $content; + } + + /** + * Send the edit form and related headers to OutputPage + * @param callable|null $formCallback That takes an OutputPage parameter; will be called + * during form output near the top, for captchas and the like. + * + * The $formCallback parameter is deprecated since MediaWiki 1.25. Please + * use the EditPage::showEditForm:fields hook instead. + */ + public function showEditForm( $formCallback = null ) { + # need to parse the preview early so that we know which templates are used, + # otherwise users with "show preview after edit box" will get a blank list + # we parse this near the beginning so that setHeaders can do the title + # setting work instead of leaving it in getPreviewText + $previewOutput = ''; + if ( $this->formtype == 'preview' ) { + $previewOutput = $this->getPreviewText(); + } + + $out = $this->context->getOutput(); + + // Avoid PHP 7.1 warning of passing $this by reference + $editPage = $this; + Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] ); + + $this->setHeaders(); + + $this->addTalkPageText(); + $this->addEditNotices(); + + if ( !$this->isConflict && + $this->section != '' && + !$this->isSectionEditSupported() ) { + // We use $this->section to much before this and getVal('wgSection') directly in other places + // at this point we can't reset $this->section to '' to fallback to non-section editing. + // Someone is welcome to try refactoring though + $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' ); + return; + } + + $this->showHeader(); + + $out->addHTML( $this->editFormPageTop ); + + $user = $this->context->getUser(); + if ( $user->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, true ); + } + + $out->addHTML( $this->editFormTextTop ); + + $showToolbar = true; + if ( $this->wasDeletedSinceLastEdit() ) { + if ( $this->formtype == 'save' ) { + // Hide the toolbar and edit area, user can click preview to get it back + // Add an confirmation checkbox and explanation. + $showToolbar = false; + } else { + $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>", + 'deletedwhileediting' ); + } + } + + // @todo add EditForm plugin interface and use it here! + // search for textarea1 and textarea2, and allow EditForm to override all uses. + $out->addHTML( Html::openElement( + 'form', + [ + 'class' => 'mw-editform', + 'id' => self::EDITFORM_ID, + 'name' => self::EDITFORM_ID, + 'method' => 'post', + 'action' => $this->getActionURL( $this->getContextTitle() ), + 'enctype' => 'multipart/form-data' + ] + ) ); + + if ( is_callable( $formCallback ) ) { + wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' ); + call_user_func_array( $formCallback, [ &$out ] ); + } + + // Add a check for Unicode support + $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) ); + + // Add an empty field to trip up spambots + $out->addHTML( + Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] ) + . Html::rawElement( + 'label', + [ 'for' => 'wpAntispam' ], + $this->context->msg( 'simpleantispam-label' )->parse() + ) + . Xml::element( + 'input', + [ + 'type' => 'text', + 'name' => 'wpAntispam', + 'id' => 'wpAntispam', + 'value' => '' + ] + ) + . Xml::closeElement( 'div' ) + ); + + // Avoid PHP 7.1 warning of passing $this by reference + $editPage = $this; + Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] ); + + // Put these up at the top to ensure they aren't lost on early form submission + $this->showFormBeforeText(); + + if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) { + $username = $this->lastDelete->user_name; + $comment = CommentStore::getStore() + ->getComment( 'log_comment', $this->lastDelete )->text; + + // It is better to not parse the comment at all than to have templates expanded in the middle + // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used? + $key = $comment === '' + ? 'confirmrecreate-noreason' + : 'confirmrecreate'; + $out->addHTML( + '<div class="mw-confirm-recreate">' . + $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() . + Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false, + [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ] + ) . + '</div>' + ); + } + + # When the summary is hidden, also hide them on preview/show changes + if ( $this->nosummary ) { + $out->addHTML( Html::hidden( 'nosummary', true ) ); + } + + # If a blank edit summary was previously provided, and the appropriate + # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the + # user being bounced back more than once in the event that a summary + # is not required. + # #### + # For a bit more sophisticated detection of blank summaries, hash the + # automatic one and pass that in the hidden field wpAutoSummary. + if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) { + $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) ); + } + + if ( $this->undidRev ) { + $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) ); + } + + if ( $this->selfRedirect ) { + $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) ); + } + + if ( $this->hasPresetSummary ) { + // If a summary has been preset using &summary= we don't want to prompt for + // a different summary. Only prompt for a summary if the summary is blanked. + // (T19416) + $this->autoSumm = md5( '' ); + } + + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) ); + + $out->addHTML( Html::hidden( 'oldid', $this->oldid ) ); + $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) ); + + $out->addHTML( Html::hidden( 'format', $this->contentFormat ) ); + $out->addHTML( Html::hidden( 'model', $this->contentModel ) ); + + $out->enableOOUI(); + + if ( $this->section == 'new' ) { + $this->showSummaryInput( true, $this->summary ); + $out->addHTML( $this->getSummaryPreview( true, $this->summary ) ); + } + + $out->addHTML( $this->editFormTextBeforeContent ); + if ( $this->isConflict ) { + // In an edit conflict, we turn textbox2 into the user's text, + // and textbox1 into the stored version + $this->textbox2 = $this->textbox1; + + $content = $this->getCurrentContent(); + $this->textbox1 = $this->toEditText( $content ); + + $editConflictHelper = $this->getEditConflictHelper(); + $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 ); + $editConflictHelper->setContentModel( $this->contentModel ); + $editConflictHelper->setContentFormat( $this->contentFormat ); + $out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() ); + } + + if ( !$this->mTitle->isUserConfigPage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) { + $out->addHTML( self::getEditToolbar( $this->mTitle ) ); + } + + if ( $this->blankArticle ) { + $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) ); + } + + if ( $this->isConflict ) { + // In an edit conflict bypass the overridable content form method + // and fallback to the raw wpTextbox1 since editconflicts can't be + // resolved between page source edits and custom ui edits using the + // custom edit ui. + $conflictTextBoxAttribs = []; + if ( $this->wasDeletedSinceLastEdit() ) { + $conflictTextBoxAttribs['style'] = 'display:none;'; + } elseif ( $this->isOldRev ) { + $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev'; + } + + $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) ); + $out->addHTML( $editConflictHelper->getEditFormHtmlAfterContent() ); + } else { + $this->showContentForm(); + } + + $out->addHTML( $this->editFormTextAfterContent ); + + $this->showStandardInputs(); + + $this->showFormAfterText(); + + $this->showTosSummary(); + + $this->showEditTools(); + + $out->addHTML( $this->editFormTextAfterTools . "\n" ); + + $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) ); + + $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ], + Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) ); + + $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ], + self::getPreviewLimitReport( $this->mParserOutput ) ) ); + + $out->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + + if ( $this->isConflict ) { + try { + $this->showConflict(); + } catch ( MWContentSerializationException $ex ) { + // this can't really happen, but be nice if it does. + $msg = $this->context->msg( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); + $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' ); + } + } + + // Set a hidden field so JS knows what edit form mode we are in + if ( $this->isConflict ) { + $mode = 'conflict'; + } elseif ( $this->preview ) { + $mode = 'preview'; + } elseif ( $this->diff ) { + $mode = 'diff'; + } else { + $mode = 'text'; + } + $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) ); + + // Marker for detecting truncated form data. This must be the last + // parameter sent in order to be of use, so do not move me. + $out->addHTML( Html::hidden( 'wpUltimateParam', true ) ); + $out->addHTML( $this->editFormTextBottom . "\n</form>\n" ); + + if ( !$user->getOption( 'previewontop' ) ) { + $this->displayPreviewArea( $previewOutput, false ); + } + } + + /** + * Wrapper around TemplatesOnThisPageFormatter to make + * a "templates on this page" list. + * + * @param Title[] $templates + * @return string HTML + */ + public function makeTemplatesOnThisPageList( array $templates ) { + $templateListFormatter = new TemplatesOnThisPageFormatter( + $this->context, MediaWikiServices::getInstance()->getLinkRenderer() + ); + + // preview if preview, else section if section, else false + $type = false; + if ( $this->preview ) { + $type = 'preview'; + } elseif ( $this->section != '' ) { + $type = 'section'; + } + + return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ], + $templateListFormatter->format( $templates, $type ) + ); + } + + /** + * Extract the section title from current section text, if any. + * + * @param string $text + * @return string|bool String or false + */ + public static function extractSectionTitle( $text ) { + preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ); + if ( !empty( $matches[2] ) ) { + global $wgParser; + return $wgParser->stripSectionName( trim( $matches[2] ) ); + } else { + return false; + } + } + + protected function showHeader() { + $out = $this->context->getOutput(); + $user = $this->context->getUser(); + if ( $this->isConflict ) { + $this->addExplainConflictHeader( $out ); + $this->editRevId = $this->page->getLatest(); + } else { + if ( $this->section != '' && $this->section != 'new' ) { + if ( !$this->summary && !$this->preview && !$this->diff ) { + $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object + if ( $sectionTitle !== false ) { + $this->summary = "/* $sectionTitle */ "; + } + } + } + + $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text(); + + if ( $this->missingComment ) { + $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' ); + } + + if ( $this->missingSummary && $this->section != 'new' ) { + $out->wrapWikiMsg( + "<div id='mw-missingsummary'>\n$1\n</div>", + [ 'missingsummary', $buttonLabel ] + ); + } + + if ( $this->missingSummary && $this->section == 'new' ) { + $out->wrapWikiMsg( + "<div id='mw-missingcommentheader'>\n$1\n</div>", + [ 'missingcommentheader', $buttonLabel ] + ); + } + + if ( $this->blankArticle ) { + $out->wrapWikiMsg( + "<div id='mw-blankarticle'>\n$1\n</div>", + [ 'blankarticle', $buttonLabel ] + ); + } + + if ( $this->selfRedirect ) { + $out->wrapWikiMsg( + "<div id='mw-selfredirect'>\n$1\n</div>", + [ 'selfredirect', $buttonLabel ] + ); + } + + if ( $this->hookError !== '' ) { + $out->addWikiText( $this->hookError ); + } + + if ( $this->section != 'new' ) { + $revision = $this->mArticle->getRevisionFetched(); + if ( $revision ) { + // Let sysop know that this will make private content public if saved + + if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) { + $out->wrapWikiMsg( + "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + 'rev-deleted-text-permission' + ); + } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + $out->wrapWikiMsg( + "<div class='mw-warning plainlinks'>\n$1\n</div>\n", + 'rev-deleted-text-view' + ); + } + + if ( !$revision->isCurrent() ) { + $this->mArticle->setOldSubtitle( $revision->getId() ); + $out->addWikiMsg( 'editingold' ); + $this->isOldRev = true; + } + } elseif ( $this->mTitle->exists() ) { + // Something went wrong + + $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n", + [ 'missing-revision', $this->oldid ] ); + } + } + } + + if ( wfReadOnly() ) { + $out->wrapWikiMsg( + "<div id=\"mw-read-only-warning\">\n$1\n</div>", + [ 'readonlywarning', wfReadOnlyReason() ] + ); + } elseif ( $user->isAnon() ) { + if ( $this->formtype != 'preview' ) { + $out->wrapWikiMsg( + "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>", + [ 'anoneditwarning', + // Log-in link + SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ + 'returnto' => $this->getTitle()->getPrefixedDBkey() + ] ), + // Sign-up link + SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ + 'returnto' => $this->getTitle()->getPrefixedDBkey() + ] ) + ] + ); + } else { + $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>", + 'anonpreviewwarning' + ); + } + } else { + if ( $this->mTitle->isUserConfigPage() ) { + # Check the skin exists + if ( $this->isWrongCaseUserConfigPage() ) { + $out->wrapWikiMsg( + "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>", + [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ] + ); + } + if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) { + $isUserCssConfig = $this->mTitle->isUserCssConfigPage(); + $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage(); + $isUserJsConfig = $this->mTitle->isUserJsConfigPage(); + + $warning = $isUserCssConfig + ? 'usercssispublic' + : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' ); + + $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning ); + + if ( $this->formtype !== 'preview' ) { + $config = $this->context->getConfig(); + if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) { + $out->wrapWikiMsg( + "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", + [ 'usercssyoucanpreview' ] + ); + } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) { + $out->wrapWikiMsg( + "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>", + [ 'userjsonyoucanpreview' ] + ); + } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) { + $out->wrapWikiMsg( + "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", + [ 'userjsyoucanpreview' ] + ); + } + } + } + } + } + + $this->addPageProtectionWarningHeaders(); + + $this->addLongPageWarningHeader(); + + # Add header copyright warning + $this->showHeaderCopyrightWarning(); + } + + /** + * Helper function for summary input functions, which returns the neccessary + * attributes for the input. + * + * @param array|null $inputAttrs Array of attrs to use on the input + * @return array + */ + private function getSummaryInputAttributes( array $inputAttrs = null ) { + $conf = $this->context->getConfig(); + $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD; + // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP + // (e.g. emojis) count for two each. This limit is overridden in JS to instead count + // Unicode codepoints (or 255 UTF-8 bytes for old schema). + return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [ + 'id' => 'wpSummary', + 'name' => 'wpSummary', + 'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT, + 'tabindex' => 1, + 'size' => 60, + 'spellcheck' => 'true', + ]; + } + + /** + * Builds a standard summary input with a label. + * + * @param string $summary The value of the summary input + * @param string $labelText The html to place inside the label + * @param array $inputAttrs Array of attrs to use on the input + * + * @return OOUI\FieldLayout OOUI FieldLayout with Label and Input + */ + function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) { + $inputAttrs = OOUI\Element::configFromHtmlAttributes( + $this->getSummaryInputAttributes( $inputAttrs ) + ); + $inputAttrs += [ + 'title' => Linker::titleAttrib( 'summary' ), + 'accessKey' => Linker::accesskey( 'summary' ), + ]; + + // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>` + $inputAttrs['inputId'] = $inputAttrs['id']; + $inputAttrs['id'] = 'wpSummaryWidget'; + + return new OOUI\FieldLayout( + new OOUI\TextInputWidget( [ + 'value' => $summary, + 'infusable' => true, + ] + $inputAttrs ), + [ + 'label' => new OOUI\HtmlSnippet( $labelText ), + 'align' => 'top', + 'id' => 'wpSummaryLabel', + 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ], + ] + ); + } + + /** + * @param bool $isSubjectPreview True if this is the section subject/title + * up top, or false if this is the comment summary + * down below the textarea + * @param string $summary The text of the summary to display + */ + protected function showSummaryInput( $isSubjectPreview, $summary = "" ) { + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + if ( $isSubjectPreview ) { + if ( $this->nosummary ) { + return; + } + } else { + if ( !$this->mShowSummaryField ) { + return; + } + } + + $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse(); + $this->context->getOutput()->addHTML( $this->getSummaryInputWidget( + $summary, + $labelText, + [ 'class' => $summaryClass ] + ) ); + } + + /** + * @param bool $isSubjectPreview True if this is the section subject/title + * up top, or false if this is the comment summary + * down below the textarea + * @param string $summary The text of the summary to display + * @return string + */ + protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) { + // avoid spaces in preview, gets always trimmed on save + $summary = trim( $summary ); + if ( !$summary || ( !$this->preview && !$this->diff ) ) { + return ""; + } + + global $wgParser; + + if ( $isSubjectPreview ) { + $summary = $this->context->msg( 'newsectionsummary' ) + ->rawParams( $wgParser->stripSectionName( $summary ) ) + ->inContentLanguage()->text(); + } + + $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview'; + + $summary = $this->context->msg( $message )->parse() + . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); + return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary ); + } + + protected function showFormBeforeText() { + $out = $this->context->getOutput(); + $out->addHTML( Html::hidden( 'wpSection', $this->section ) ); + $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) ); + $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) ); + $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) ); + $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) ); + } + + protected function showFormAfterText() { + /** + * To make it harder for someone to slip a user a page + * which submits an edit form to the wiki without their + * knowledge, a random token is associated with the login + * session. If it's not passed back with the submission, + * we won't save the page, or render user JavaScript and + * CSS previews. + * + * For anon editors, who may not have a session, we just + * include the constant suffix to prevent editing from + * broken text-mangling proxies. + */ + $this->context->getOutput()->addHTML( + "\n" . + Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) . + "\n" + ); + } + + /** + * Subpage overridable method for printing the form for page content editing + * By default this simply outputs wpTextbox1 + * Subclasses can override this to provide a custom UI for editing; + * be it a form, or simply wpTextbox1 with a modified content that will be + * reverse modified when extracted from the post data. + * Note that this is basically the inverse for importContentFormData + */ + protected function showContentForm() { + $this->showTextbox1(); + } + + /** + * Method to output wpTextbox1 + * The $textoverride method can be used by subclasses overriding showContentForm + * to pass back to this method. + * + * @param array $customAttribs Array of html attributes to use in the textarea + * @param string $textoverride Optional text to override $this->textarea1 with + */ + protected function showTextbox1( $customAttribs = null, $textoverride = null ) { + if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) { + $attribs = [ 'style' => 'display:none;' ]; + } else { + $builder = new TextboxBuilder(); + $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() ); + + # Is an old revision being edited? + if ( $this->isOldRev ) { + $classes[] = 'mw-textarea-oldrev'; + } + + $attribs = [ 'tabindex' => 1 ]; + + if ( is_array( $customAttribs ) ) { + $attribs += $customAttribs; + } + + $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs ); + } + + $this->showTextbox( + $textoverride !== null ? $textoverride : $this->textbox1, + 'wpTextbox1', + $attribs + ); + } + + protected function showTextbox2() { + $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] ); + } + + protected function showTextbox( $text, $name, $customAttribs = [] ) { + $builder = new TextboxBuilder(); + $attribs = $builder->buildTextboxAttribs( + $name, + $customAttribs, + $this->context->getUser(), + $this->mTitle + ); + + $this->context->getOutput()->addHTML( + Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs ) + ); + } + + protected function displayPreviewArea( $previewOutput, $isOnTop = false ) { + $classes = []; + if ( $isOnTop ) { + $classes[] = 'ontop'; + } + + $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ]; + + if ( $this->formtype != 'preview' ) { + $attribs['style'] = 'display: none;'; + } + + $out = $this->context->getOutput(); + $out->addHTML( Xml::openElement( 'div', $attribs ) ); + + if ( $this->formtype == 'preview' ) { + $this->showPreview( $previewOutput ); + } else { + // Empty content container for LivePreview + $pageViewLang = $this->mTitle->getPageViewLanguage(); + $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), + 'class' => 'mw-content-' . $pageViewLang->getDir() ]; + $out->addHTML( Html::rawElement( 'div', $attribs ) ); + } + + $out->addHTML( '</div>' ); + + if ( $this->formtype == 'diff' ) { + try { + $this->showDiff(); + } catch ( MWContentSerializationException $ex ) { + $msg = $this->context->msg( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); + $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' ); + } + } + } + + /** + * Append preview output to OutputPage. + * Includes category rendering if this is a category page. + * + * @param string $text The HTML to be output for the preview. + */ + protected function showPreview( $text ) { + if ( $this->mArticle instanceof CategoryPage ) { + $this->mArticle->openShowCategory(); + } + # This hook seems slightly odd here, but makes things more + # consistent for extensions. + $out = $this->context->getOutput(); + Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] ); + $out->addHTML( $text ); + if ( $this->mArticle instanceof CategoryPage ) { + $this->mArticle->closeShowCategory(); + } + } + + /** + * Get a diff between the current contents of the edit box and the + * version of the page we're editing from. + * + * If this is a section edit, we'll replace the section as for final + * save and then make a comparison. + */ + public function showDiff() { + global $wgContLang; + + $oldtitlemsg = 'currentrev'; + # if message does not exist, show diff against the preloaded default + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) { + $oldtext = $this->mTitle->getDefaultMessageText(); + if ( $oldtext !== false ) { + $oldtitlemsg = 'defaultmessagetext'; + $oldContent = $this->toEditContent( $oldtext ); + } else { + $oldContent = null; + } + } else { + $oldContent = $this->getCurrentContent(); + } + + $textboxContent = $this->toEditContent( $this->textbox1 ); + if ( $this->editRevId !== null ) { + $newContent = $this->page->replaceSectionAtRev( + $this->section, $textboxContent, $this->summary, $this->editRevId + ); + } else { + $newContent = $this->page->replaceSectionContent( + $this->section, $textboxContent, $this->summary, $this->edittime + ); + } + + if ( $newContent ) { + Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] ); + + $user = $this->context->getUser(); + $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); + $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts ); + } + + if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) { + $oldtitle = $this->context->msg( $oldtitlemsg )->parse(); + $newtitle = $this->context->msg( 'yourtext' )->parse(); + + if ( !$oldContent ) { + $oldContent = $newContent->getContentHandler()->makeEmptyContent(); + } + + if ( !$newContent ) { + $newContent = $oldContent->getContentHandler()->makeEmptyContent(); + } + + $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context ); + $de->setContent( $oldContent, $newContent ); + + $difftext = $de->getDiff( $oldtitle, $newtitle ); + $de->showDiffStyle(); + } else { + $difftext = ''; + } + + $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' ); + } + + /** + * Show the header copyright warning. + */ + protected function showHeaderCopyrightWarning() { + $msg = 'editpage-head-copy-warn'; + if ( !$this->context->msg( $msg )->isDisabled() ) { + $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>", + 'editpage-head-copy-warn' ); + } + } + + /** + * Give a chance for site and per-namespace customizations of + * terms of service summary link that might exist separately + * from the copyright notice. + * + * This will display between the save button and the edit tools, + * so should remain short! + */ + protected function showTosSummary() { + $msg = 'editpage-tos-summary'; + Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] ); + if ( !$this->context->msg( $msg )->isDisabled() ) { + $out = $this->context->getOutput(); + $out->addHTML( '<div class="mw-tos-summary">' ); + $out->addWikiMsg( $msg ); + $out->addHTML( '</div>' ); + } + } + + /** + * Inserts optional text shown below edit and upload forms. Can be used to offer special + * characters not present on most keyboards for copying/pasting. + */ + protected function showEditTools() { + $this->context->getOutput()->addHTML( '<div class="mw-editTools">' . + $this->context->msg( 'edittools' )->inContentLanguage()->parse() . + '</div>' ); + } + + /** + * Get the copyright warning + * + * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility + * @return string + */ + protected function getCopywarn() { + return self::getCopyrightWarning( $this->mTitle ); + } + + /** + * Get the copyright warning, by default returns wikitext + * + * @param Title $title + * @param string $format Output format, valid values are any function of a Message object + * @param Language|string|null $langcode Language code or Language object. + * @return string + */ + public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) { + global $wgRightsText; + if ( $wgRightsText ) { + $copywarnMsg = [ 'copyrightwarning', + '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]', + $wgRightsText ]; + } else { + $copywarnMsg = [ 'copyrightwarning2', + '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ]; + } + // Allow for site and per-namespace customization of contribution/copyright notice. + Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] ); + + $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title ); + if ( $langcode ) { + $msg->inLanguage( $langcode ); + } + return "<div id=\"editpage-copywarn\">\n" . + $msg->$format() . "\n</div>"; + } + + /** + * Get the Limit report for page previews + * + * @since 1.22 + * @param ParserOutput $output ParserOutput object from the parse + * @return string HTML + */ + public static function getPreviewLimitReport( $output ) { + global $wgLang; + + if ( !$output || !$output->getLimitReportData() ) { + return ''; + } + + $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ], + wfMessage( 'limitreport-title' )->parseAsBlock() + ); + + // Show/hide animation doesn't work correctly on a table, so wrap it in a div. + $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] ); + + $limitReport .= Html::openElement( 'table', [ + 'class' => 'preview-limit-report wikitable' + ] ) . + Html::openElement( 'tbody' ); + + foreach ( $output->getLimitReportData() as $key => $value ) { + if ( Hooks::run( 'ParserLimitReportFormat', + [ $key, &$value, &$limitReport, true, true ] + ) ) { + $keyMsg = wfMessage( $key ); + $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] ); + if ( !$valueMsg->exists() ) { + $valueMsg = new RawMessage( '$1' ); + } + if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) { + $limitReport .= Html::openElement( 'tr' ) . + Html::rawElement( 'th', null, $keyMsg->parse() ) . + Html::rawElement( 'td', null, + $wgLang->formatNum( $valueMsg->params( $value )->parse() ) + ) . + Html::closeElement( 'tr' ); + } + } + } + + $limitReport .= Html::closeElement( 'tbody' ) . + Html::closeElement( 'table' ) . + Html::closeElement( 'div' ); + + return $limitReport; + } + + protected function showStandardInputs( &$tabindex = 2 ) { + $out = $this->context->getOutput(); + $out->addHTML( "<div class='editOptions'>\n" ); + + if ( $this->section != 'new' ) { + $this->showSummaryInput( false, $this->summary ); + $out->addHTML( $this->getSummaryPreview( false, $this->summary ) ); + } + + $checkboxes = $this->getCheckboxesWidget( + $tabindex, + [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] + ); + $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] ); + + $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" ); + + // Show copyright warning. + $out->addWikiText( $this->getCopywarn() ); + $out->addHTML( $this->editFormTextAfterWarn ); + + $out->addHTML( "<div class='editButtons'>\n" ); + $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" ); + + $cancel = $this->getCancelLink(); + + $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text(); + $edithelpurl = Skin::makeInternalOrExternalUrl( $message ); + $edithelp = + Html::linkButton( + $this->context->msg( 'edithelp' )->text(), + [ 'target' => 'helpwindow', 'href' => $edithelpurl ], + [ 'mw-ui-quiet' ] + ) . + $this->context->msg( 'word-separator' )->escaped() . + $this->context->msg( 'newwindow' )->parse(); + + $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" ); + $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" ); + $out->addHTML( "</div><!-- editButtons -->\n" ); + + Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] ); + + $out->addHTML( "</div><!-- editOptions -->\n" ); + } + + /** + * Show an edit conflict. textbox1 is already shown in showEditForm(). + * If you want to use another entry point to this function, be careful. + */ + protected function showConflict() { + $out = $this->context->getOutput(); + // Avoid PHP 7.1 warning of passing $this by reference + $editPage = $this; + if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) { + $this->incrementConflictStats(); + + $this->getEditConflictHelper()->showEditFormTextAfterFooters(); + } + } + + protected function incrementConflictStats() { + $this->getEditConflictHelper()->incrementConflictStats(); + } + + /** + * @return string + */ + public function getCancelLink() { + $cancelParams = []; + if ( !$this->isConflict && $this->oldid > 0 ) { + $cancelParams['oldid'] = $this->oldid; + } elseif ( $this->getContextTitle()->isRedirect() ) { + $cancelParams['redirect'] = 'no'; + } + + return new OOUI\ButtonWidget( [ + 'id' => 'mw-editform-cancel', + 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ), + 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ), + 'framed' => false, + 'infusable' => true, + 'flags' => 'destructive', + ] ); + } + + /** + * Returns the URL to use in the form's action attribute. + * This is used by EditPage subclasses when simply customizing the action + * variable in the constructor is not enough. This can be used when the + * EditPage lives inside of a Special page rather than a custom page action. + * + * @param Title $title Title object for which is being edited (where we go to for &action= links) + * @return string + */ + protected function getActionURL( Title $title ) { + return $title->getLocalURL( [ 'action' => $this->action ] ); + } + + /** + * Check if a page was deleted while the user was editing it, before submit. + * Note that we rely on the logging table, which hasn't been always there, + * but that doesn't matter, because this only applies to brand new + * deletes. + * @return bool + */ + protected function wasDeletedSinceLastEdit() { + if ( $this->deletedSinceEdit !== null ) { + return $this->deletedSinceEdit; + } + + $this->deletedSinceEdit = false; + + if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) { + $this->lastDelete = $this->getLastDelete(); + if ( $this->lastDelete ) { + $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); + if ( $deleteTime > $this->starttime ) { + $this->deletedSinceEdit = true; + } + } + } + + return $this->deletedSinceEdit; + } + + /** + * @return bool|stdClass + */ + protected function getLastDelete() { + $dbr = wfGetDB( DB_REPLICA ); + $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' ); + $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' ); + $data = $dbr->selectRow( + array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ), + [ + 'log_type', + 'log_action', + 'log_timestamp', + 'log_namespace', + 'log_title', + 'log_params', + 'log_deleted', + 'user_name' + ] + $commentQuery['fields'] + $actorQuery['fields'], + [ + 'log_namespace' => $this->mTitle->getNamespace(), + 'log_title' => $this->mTitle->getDBkey(), + 'log_type' => 'delete', + 'log_action' => 'delete', + ], + __METHOD__, + [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ], + [ + 'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ], + ] + $commentQuery['joins'] + $actorQuery['joins'] + ); + // Quick paranoid permission checks... + if ( is_object( $data ) ) { + if ( $data->log_deleted & LogPage::DELETED_USER ) { + $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped(); + } + + if ( $data->log_deleted & LogPage::DELETED_COMMENT ) { + $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped(); + $data->log_comment_data = null; + } + } + + return $data; + } + + /** + * Get the rendered text for previewing. + * @throws MWException + * @return string + */ + public function getPreviewText() { + $out = $this->context->getOutput(); + $config = $this->context->getConfig(); + + if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) { + // Could be an offsite preview attempt. This is very unsafe if + // HTML is enabled, as it could be an attack. + $parsedNote = ''; + if ( $this->textbox1 !== '' ) { + // Do not put big scary notice, if previewing the empty + // string, which happens when you initially edit + // a category page, due to automatic preview-on-open. + $parsedNote = $out->parse( "<div class='previewnote'>" . + $this->context->msg( 'session_fail_preview_html' )->text() . "</div>", + true, /* interface */true ); + } + $this->incrementEditFailureStats( 'session_loss' ); + return $parsedNote; + } + + $note = ''; + + try { + $content = $this->toEditContent( $this->textbox1 ); + + $previewHTML = ''; + if ( !Hooks::run( + 'AlternateEditPreview', + [ $this, &$content, &$previewHTML, &$this->mParserOutput ] ) + ) { + return $previewHTML; + } + + # provide a anchor link to the editform + $continueEditing = '<span class="mw-continue-editing">' . + '[[#' . self::EDITFORM_ID . '|' . + $this->context->getLanguage()->getArrow() . ' ' . + $this->context->msg( 'continue-editing' )->text() . ']]</span>'; + if ( $this->mTriedSave && !$this->mTokenOk ) { + if ( $this->mTokenOkExceptSuffix ) { + $note = $this->context->msg( 'token_suffix_mismatch' )->plain(); + $this->incrementEditFailureStats( 'bad_token' ); + } else { + $note = $this->context->msg( 'session_fail_preview' )->plain(); + $this->incrementEditFailureStats( 'session_loss' ); + } + } elseif ( $this->incompleteForm ) { + $note = $this->context->msg( 'edit_form_incomplete' )->plain(); + if ( $this->mTriedSave ) { + $this->incrementEditFailureStats( 'incomplete_form' ); + } + } else { + $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing; + } + + # don't parse non-wikitext pages, show message about preview + if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) { + if ( $this->mTitle->isUserConfigPage() ) { + $level = 'user'; + } elseif ( $this->mTitle->isSiteConfigPage() ) { + $level = 'site'; + } else { + $level = false; + } + + if ( $content->getModel() == CONTENT_MODEL_CSS ) { + $format = 'css'; + if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) { + $format = false; + } + } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) { + $format = 'json'; + if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) { + $format = false; + } + } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { + $format = 'js'; + if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) { + $format = false; + } + } else { + $format = false; + } + + # Used messages to make sure grep find them: + # Messages: usercsspreview, userjsonpreview, userjspreview, + # sitecsspreview, sitejsonpreview, sitejspreview + if ( $level && $format ) { + $note = "<div id='mw-{$level}{$format}preview'>" . + $this->context->msg( "{$level}{$format}preview" )->text() . + ' ' . $continueEditing . "</div>"; + } + } + + # If we're adding a comment, we need to show the + # summary as the headline + if ( $this->section === "new" && $this->summary !== "" ) { + $content = $content->addSectionHeader( $this->summary ); + } + + $hook_args = [ $this, &$content ]; + Hooks::run( 'EditPageGetPreviewContent', $hook_args ); + + $parserResult = $this->doPreviewParse( $content ); + $parserOutput = $parserResult['parserOutput']; + $previewHTML = $parserResult['html']; + $this->mParserOutput = $parserOutput; + $out->addParserOutputMetadata( $parserOutput ); + + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + } + + } catch ( MWContentSerializationException $ex ) { + $m = $this->context->msg( + 'content-failed-to-parse', + $this->contentModel, + $this->contentFormat, + $ex->getMessage() + ); + $note .= "\n\n" . $m->parse(); + $previewHTML = ''; + } + + if ( $this->isConflict ) { + $conflict = '<h2 id="mw-previewconflict">' + . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n"; + } else { + $conflict = '<hr />'; + } + + $previewhead = "<div class='previewnote'>\n" . + '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" . + $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n"; + + $pageViewLang = $this->mTitle->getPageViewLanguage(); + $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), + 'class' => 'mw-content-' . $pageViewLang->getDir() ]; + $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML ); + + return $previewhead . $previewHTML . $this->previewTextAfterContent; + } + + private function incrementEditFailureStats( $failureType ) { + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); + $stats->increment( 'edit.failures.' . $failureType ); + } + + /** + * Get parser options for a preview + * @return ParserOptions + */ + protected function getPreviewParserOptions() { + $parserOptions = $this->page->makeParserOptions( $this->context ); + $parserOptions->setIsPreview( true ); + $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' ); + $parserOptions->enableLimitReport(); + return $parserOptions; + } + + /** + * Parse the page for a preview. Subclasses may override this class, in order + * to parse with different options, or to otherwise modify the preview HTML. + * + * @param Content $content The page content + * @return array with keys: + * - parserOutput: The ParserOutput object + * - html: The HTML to be displayed + */ + protected function doPreviewParse( Content $content ) { + $user = $this->context->getUser(); + $parserOptions = $this->getPreviewParserOptions(); + $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions ); + $scopedCallback = $parserOptions->setupFakeRevision( + $this->mTitle, $pstContent, $user ); + $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions ); + ScopedCallback::consume( $scopedCallback ); + return [ + 'parserOutput' => $parserOutput, + 'html' => $parserOutput->getText( [ + 'enableSectionEditLinks' => false + ] ) + ]; + } + + /** + * @return array + */ + public function getTemplates() { + if ( $this->preview || $this->section != '' ) { + $templates = []; + if ( !isset( $this->mParserOutput ) ) { + return $templates; + } + foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) { + foreach ( array_keys( $template ) as $dbk ) { + $templates[] = Title::makeTitle( $ns, $dbk ); + } + } + return $templates; + } else { + return $this->mTitle->getTemplateLinksFrom(); + } + } + + /** + * Shows a bulletin board style toolbar for common editing functions. + * It can be disabled in the user preferences. + * + * @param Title $title Title object for the page being edited (optional) + * @return string + */ + public static function getEditToolbar( $title = null ) { + global $wgContLang, $wgOut; + global $wgEnableUploads, $wgForeignFileRepos; + + $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); + $showSignature = true; + if ( $title ) { + $showSignature = MWNamespace::wantSignatures( $title->getNamespace() ); + } + + /** + * $toolarray is an array of arrays each of which includes the + * opening tag, the closing tag, optionally a sample text that is + * inserted between the two when no selection is highlighted + * and. The tip text is shown when the user moves the mouse + * over the button. + * + * Images are defined in ResourceLoaderEditToolbarModule. + */ + $toolarray = [ + [ + 'id' => 'mw-editbutton-bold', + 'open' => '\'\'\'', + 'close' => '\'\'\'', + 'sample' => wfMessage( 'bold_sample' )->text(), + 'tip' => wfMessage( 'bold_tip' )->text(), + ], + [ + 'id' => 'mw-editbutton-italic', + 'open' => '\'\'', + 'close' => '\'\'', + 'sample' => wfMessage( 'italic_sample' )->text(), + 'tip' => wfMessage( 'italic_tip' )->text(), + ], + [ + 'id' => 'mw-editbutton-link', + 'open' => '[[', + 'close' => ']]', + 'sample' => wfMessage( 'link_sample' )->text(), + 'tip' => wfMessage( 'link_tip' )->text(), + ], + [ + 'id' => 'mw-editbutton-extlink', + 'open' => '[', + 'close' => ']', + 'sample' => wfMessage( 'extlink_sample' )->text(), + 'tip' => wfMessage( 'extlink_tip' )->text(), + ], + [ + 'id' => 'mw-editbutton-headline', + 'open' => "\n== ", + 'close' => " ==\n", + 'sample' => wfMessage( 'headline_sample' )->text(), + 'tip' => wfMessage( 'headline_tip' )->text(), + ], + $imagesAvailable ? [ + 'id' => 'mw-editbutton-image', + 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':', + 'close' => ']]', + 'sample' => wfMessage( 'image_sample' )->text(), + 'tip' => wfMessage( 'image_tip' )->text(), + ] : false, + $imagesAvailable ? [ + 'id' => 'mw-editbutton-media', + 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':', + 'close' => ']]', + 'sample' => wfMessage( 'media_sample' )->text(), + 'tip' => wfMessage( 'media_tip' )->text(), + ] : false, + [ + 'id' => 'mw-editbutton-nowiki', + 'open' => "<nowiki>", + 'close' => "</nowiki>", + 'sample' => wfMessage( 'nowiki_sample' )->text(), + 'tip' => wfMessage( 'nowiki_tip' )->text(), + ], + $showSignature ? [ + 'id' => 'mw-editbutton-signature', + 'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(), + 'close' => '', + 'sample' => '', + 'tip' => wfMessage( 'sig_tip' )->text(), + ] : false, + [ + 'id' => 'mw-editbutton-hr', + 'open' => "\n----\n", + 'close' => '', + 'sample' => '', + 'tip' => wfMessage( 'hr_tip' )->text(), + ] + ]; + + $script = 'mw.loader.using("mediawiki.toolbar", function () {'; + foreach ( $toolarray as $tool ) { + if ( !$tool ) { + continue; + } + + $params = [ + // Images are defined in ResourceLoaderEditToolbarModule + false, + // Note that we use the tip both for the ALT tag and the TITLE tag of the image. + // Older browsers show a "speedtip" type message only for ALT. + // Ideally these should be different, realistically they + // probably don't need to be. + $tool['tip'], + $tool['open'], + $tool['close'], + $tool['sample'], + $tool['id'], + ]; + + $script .= Xml::encodeJsCall( + 'mw.toolbar.addButton', + $params, + ResourceLoader::inDebugMode() + ); + } + + $script .= '});'; + + $toolbar = '<div id="toolbar"></div>'; + + if ( Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) { + // Only add the old toolbar cruft to the page payload if the toolbar has not + // been over-written by a hook caller + $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) ); + }; + + return $toolbar; + } + + /** + * Return an array of checkbox definitions. + * + * Array keys correspond to the `<input>` 'name' attribute to use for each checkbox. + * + * Array values are associative arrays with the following keys: + * - 'label-message' (required): message for label text + * - 'id' (required): 'id' attribute for the `<input>` + * - 'default' (required): default checkedness (true or false) + * - 'title-message' (optional): used to generate 'title' attribute for the `<label>` + * - 'tooltip' (optional): used to generate 'title' and 'accesskey' attributes + * from messages like 'tooltip-foo', 'accesskey-foo' + * - 'label-id' (optional): 'id' attribute for the `<label>` + * - 'legacy-name' (optional): short name for backwards-compatibility + * @param array $checked Array of checkbox name (matching the 'legacy-name') => bool, + * where bool indicates the checked status of the checkbox + * @return array + */ + public function getCheckboxesDefinition( $checked ) { + $checkboxes = []; + + $user = $this->context->getUser(); + // don't show the minor edit checkbox if it's a new page or section + if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) { + $checkboxes['wpMinoredit'] = [ + 'id' => 'wpMinoredit', + 'label-message' => 'minoredit', + // Uses messages: tooltip-minoredit, accesskey-minoredit + 'tooltip' => 'minoredit', + 'label-id' => 'mw-editpage-minoredit', + 'legacy-name' => 'minor', + 'default' => $checked['minor'], + ]; + } + + if ( $user->isLoggedIn() ) { + $checkboxes['wpWatchthis'] = [ + 'id' => 'wpWatchthis', + 'label-message' => 'watchthis', + // Uses messages: tooltip-watch, accesskey-watch + 'tooltip' => 'watch', + 'label-id' => 'mw-editpage-watch', + 'legacy-name' => 'watch', + 'default' => $checked['watch'], + ]; + } + + $editPage = $this; + Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] ); + + return $checkboxes; + } + + /** + * Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and + * any other added by extensions. + * + * @param int &$tabindex Current tabindex + * @param array $checked Array of checkbox => bool, where bool indicates the checked + * status of the checkbox + * + * @return array Associative array of string keys to OOUI\FieldLayout instances + */ + public function getCheckboxesWidget( &$tabindex, $checked ) { + $checkboxes = []; + $checkboxesDef = $this->getCheckboxesDefinition( $checked ); + + foreach ( $checkboxesDef as $name => $options ) { + $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name; + + $title = null; + $accesskey = null; + if ( isset( $options['tooltip'] ) ) { + $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text(); + $title = Linker::titleAttrib( $options['tooltip'] ); + } + if ( isset( $options['title-message'] ) ) { + $title = $this->context->msg( $options['title-message'] )->text(); + } + + $checkboxes[ $legacyName ] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( [ + 'tabIndex' => ++$tabindex, + 'accessKey' => $accesskey, + 'id' => $options['id'] . 'Widget', + 'inputId' => $options['id'], + 'name' => $name, + 'selected' => $options['default'], + 'infusable' => true, + ] ), + [ + 'align' => 'inline', + 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ), + 'title' => $title, + 'id' => isset( $options['label-id'] ) ? $options['label-id'] : null, + ] + ); + } + + // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important, + // people have used it for the weirdest things completely unrelated to checkboxes... + // And if we're gonna run it, might as well allow its legacy checkboxes to be shown. + $legacyCheckboxes = []; + if ( !$this->isNew ) { + $legacyCheckboxes['minor'] = ''; + } + $legacyCheckboxes['watch'] = ''; + // Copy new-style checkboxes into an old-style structure + foreach ( $checkboxes as $name => $oouiLayout ) { + $legacyCheckboxes[$name] = (string)$oouiLayout; + } + // Avoid PHP 7.1 warning of passing $this by reference + $ep = $this; + Hooks::run( 'EditPageBeforeEditChecks', [ &$ep, &$legacyCheckboxes, &$tabindex ], '1.29' ); + // Copy back any additional old-style checkboxes into the new-style structure + foreach ( $legacyCheckboxes as $name => $html ) { + if ( $html && !isset( $checkboxes[$name] ) ) { + $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] ); + } + } + + return $checkboxes; + } + + /** + * Get the message key of the label for the button to save the page + * + * @since 1.30 + * @return string + */ + protected function getSubmitButtonLabel() { + $labelAsPublish = + $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' ); + + // Can't use $this->isNew as that's also true if we're adding a new section to an extant page + $newPage = !$this->mTitle->exists(); + + if ( $labelAsPublish ) { + $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges'; + } else { + $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges'; + } + + return $buttonLabelKey; + } + + /** + * Returns an array of html code of the following buttons: + * save, diff and preview + * + * @param int &$tabindex Current tabindex + * + * @return array + */ + public function getEditButtons( &$tabindex ) { + $buttons = []; + + $labelAsPublish = + $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' ); + + $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text(); + $buttonTooltip = $labelAsPublish ? 'publish' : 'save'; + + $buttons['save'] = new OOUI\ButtonInputWidget( [ + 'name' => 'wpSave', + 'tabIndex' => ++$tabindex, + 'id' => 'wpSaveWidget', + 'inputId' => 'wpSave', + // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked + 'useInputTag' => true, + 'flags' => [ 'progressive', 'primary' ], + 'label' => $buttonLabel, + 'infusable' => true, + 'type' => 'submit', + // Messages used: tooltip-save, tooltip-publish + 'title' => Linker::titleAttrib( $buttonTooltip ), + // Messages used: accesskey-save, accesskey-publish + 'accessKey' => Linker::accesskey( $buttonTooltip ), + ] ); + + $buttons['preview'] = new OOUI\ButtonInputWidget( [ + 'name' => 'wpPreview', + 'tabIndex' => ++$tabindex, + 'id' => 'wpPreviewWidget', + 'inputId' => 'wpPreview', + // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked + 'useInputTag' => true, + 'label' => $this->context->msg( 'showpreview' )->text(), + 'infusable' => true, + 'type' => 'submit', + // Message used: tooltip-preview + 'title' => Linker::titleAttrib( 'preview' ), + // Message used: accesskey-preview + 'accessKey' => Linker::accesskey( 'preview' ), + ] ); + + $buttons['diff'] = new OOUI\ButtonInputWidget( [ + 'name' => 'wpDiff', + 'tabIndex' => ++$tabindex, + 'id' => 'wpDiffWidget', + 'inputId' => 'wpDiff', + // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked + 'useInputTag' => true, + 'label' => $this->context->msg( 'showdiff' )->text(), + 'infusable' => true, + 'type' => 'submit', + // Message used: tooltip-diff + 'title' => Linker::titleAttrib( 'diff' ), + // Message used: accesskey-diff + 'accessKey' => Linker::accesskey( 'diff' ), + ] ); + + // Avoid PHP 7.1 warning of passing $this by reference + $editPage = $this; + Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] ); + + return $buttons; + } + + /** + * Creates a basic error page which informs the user that + * they have attempted to edit a nonexistent section. + */ + public function noSuchSectionPage() { + $out = $this->context->getOutput(); + $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) ); + + $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock(); + + // Avoid PHP 7.1 warning of passing $this by reference + $editPage = $this; + Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] ); + $out->addHTML( $res ); + + $out->returnToMain( false, $this->mTitle ); + } + + /** + * Show "your edit contains spam" page with your diff and text + * + * @param string|array|bool $match Text (or array of texts) which triggered one or more filters + */ + public function spamPageWithContent( $match = false ) { + $this->textbox2 = $this->textbox1; + + if ( is_array( $match ) ) { + $match = $this->context->getLanguage()->listToText( $match ); + } + $out = $this->context->getOutput(); + $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) ); + + $out->addHTML( '<div id="spamprotected">' ); + $out->addWikiMsg( 'spamprotectiontext' ); + if ( $match ) { + $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) ); + } + $out->addHTML( '</div>' ); + + $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" ); + $this->showDiff(); + + $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" ); + $this->showTextbox2(); + + $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] ); + } + + /** + * Filter an input field through a Unicode de-armoring process if it + * came from an old browser with known broken Unicode editing issues. + * + * @deprecated since 1.30, does nothing + * + * @param WebRequest $request + * @param string $field + * @return string + */ + protected function safeUnicodeInput( $request, $field ) { + return rtrim( $request->getText( $field ) ); + } + + /** + * Filter an output field through a Unicode armoring process if it is + * going to an old browser with known broken Unicode editing issues. + * + * @deprecated since 1.30, does nothing + * + * @param string $text + * @return string + */ + protected function safeUnicodeOutput( $text ) { + return $text; + } + + /** + * @since 1.29 + */ + protected function addEditNotices() { + $out = $this->context->getOutput(); + $editNotices = $this->mTitle->getEditNotices( $this->oldid ); + if ( count( $editNotices ) ) { + $out->addHTML( implode( "\n", $editNotices ) ); + } else { + $msg = $this->context->msg( 'editnotice-notext' ); + if ( !$msg->isDisabled() ) { + $out->addHTML( + '<div class="mw-editnotice-notext">' + . $msg->parseAsBlock() + . '</div>' + ); + } + } + } + + /** + * @since 1.29 + */ + protected function addTalkPageText() { + if ( $this->mTitle->isTalkPage() ) { + $this->context->getOutput()->addWikiMsg( 'talkpagetext' ); + } + } + + /** + * @since 1.29 + */ + protected function addLongPageWarningHeader() { + if ( $this->contentLength === false ) { + $this->contentLength = strlen( $this->textbox1 ); + } + + $out = $this->context->getOutput(); + $lang = $this->context->getLanguage(); + $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' ); + if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) { + $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>", + [ + 'longpageerror', + $lang->formatNum( round( $this->contentLength / 1024, 3 ) ), + $lang->formatNum( $maxArticleSize ) + ] + ); + } else { + if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) { + $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>", + [ + 'longpage-hint', + $lang->formatSize( strlen( $this->textbox1 ) ), + strlen( $this->textbox1 ) + ] + ); + } + } + } + + /** + * @since 1.29 + */ + protected function addPageProtectionWarningHeaders() { + $out = $this->context->getOutput(); + if ( $this->mTitle->isProtected( 'edit' ) && + MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ] + ) { + # Is the title semi-protected? + if ( $this->mTitle->isSemiProtected() ) { + $noticeMsg = 'semiprotectedpagewarning'; + } else { + # Then it must be protected based on static groups (regular) + $noticeMsg = 'protectedpagewarning'; + } + LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '', + [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] ); + } + if ( $this->mTitle->isCascadeProtected() ) { + # Is this page under cascading protection from some source pages? + /** @var Title[] $cascadeSources */ + list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources(); + $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n"; + $cascadeSourcesCount = count( $cascadeSources ); + if ( $cascadeSourcesCount > 0 ) { + # Explain, and list the titles responsible + foreach ( $cascadeSources as $page ) { + $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; + } + } + $notice .= '</div>'; + $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] ); + } + if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { + LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '', + [ 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => [ 'titleprotectedwarning' ], + 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] ); + } + } + + /** + * @param OutputPage $out + * @since 1.29 + */ + protected function addExplainConflictHeader( OutputPage $out ) { + $out->addHTML( + $this->getEditConflictHelper()->getExplainHeader() + ); + } + + /** + * @param string $name + * @param mixed[] $customAttribs + * @param User $user + * @return mixed[] + * @since 1.29 + */ + protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) { + return ( new TextboxBuilder() )->buildTextboxAttribs( + $name, $customAttribs, $user, $this->mTitle + ); + } + + /** + * @param string $wikitext + * @return string + * @since 1.29 + */ + protected function addNewLineAtEnd( $wikitext ) { + return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext ); + } + + /** + * Turns section name wikitext into anchors for use in HTTP redirects. Various + * versions of Microsoft browsers misinterpret fragment encoding of Location: headers + * resulting in mojibake in address bar. Redirect them to legacy section IDs, + * if possible. All the other browsers get HTML5 if the wiki is configured for it, to + * spread the new style links more efficiently. + * + * @param string $text + * @return string + */ + private function guessSectionName( $text ) { + global $wgParser; + + // Detect Microsoft browsers + $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' ); + if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) { + // ...and redirect them to legacy encoding, if available + return $wgParser->guessLegacySectionNameFromWikiText( $text ); + } + // Meanwhile, real browsers get real anchors + $name = $wgParser->guessSectionNameFromWikiText( $text ); + // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded, + // otherwise Chrome double-escapes the rest of the URL. + return '#' . urlencode( mb_substr( $name, 1 ) ); + } + + /** + * Set a factory function to create an EditConflictHelper + * + * @param callable $factory Factory function + * @since 1.31 + */ + public function setEditConflictHelperFactory( callable $factory ) { + $this->editConflictHelperFactory = $factory; + $this->editConflictHelper = null; + } + + /** + * @return TextConflictHelper + */ + private function getEditConflictHelper() { + if ( !$this->editConflictHelper ) { + $this->editConflictHelper = call_user_func( + $this->editConflictHelperFactory, + $this->getSubmitButtonLabel() + ); + } + + return $this->editConflictHelper; + } + + /** + * @param string $submitButtonLabel + * @return TextConflictHelper + */ + private function newTextConflictHelper( $submitButtonLabel ) { + return new TextConflictHelper( + $this->getTitle(), + $this->getContext()->getOutput(), + MediaWikiServices::getInstance()->getStatsdDataFactory(), + $submitButtonLabel + ); + } +} diff --git a/www/wiki/includes/EventRelayerGroup.php b/www/wiki/includes/EventRelayerGroup.php new file mode 100644 index 00000000..18b1cd3f --- /dev/null +++ b/www/wiki/includes/EventRelayerGroup.php @@ -0,0 +1,72 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\MediaWikiServices; + +/** + * Factory class for spawning EventRelayer objects using configuration + * + * @since 1.27 + */ +class EventRelayerGroup { + /** @var array[] */ + protected $configByChannel = []; + + /** @var EventRelayer[] */ + protected $relayers = []; + + /** + * @param array[] $config Channel configuration + */ + public function __construct( array $config ) { + $this->configByChannel = $config; + } + + /** + * @deprecated since 1.27 Use MediaWikiServices::getInstance()->getEventRelayerGroup() + * @return EventRelayerGroup + */ + public static function singleton() { + return MediaWikiServices::getInstance()->getEventRelayerGroup(); + } + + /** + * @param string $channel + * @return EventRelayer Relayer instance that handles the given channel + */ + public function getRelayer( $channel ) { + $channelKey = isset( $this->configByChannel[$channel] ) + ? $channel + : 'default'; + + if ( !isset( $this->relayers[$channelKey] ) ) { + if ( !isset( $this->configByChannel[$channelKey] ) ) { + throw new UnexpectedValueException( "No config for '$channelKey'" ); + } + + $config = $this->configByChannel[$channelKey]; + $class = $config['class']; + + $this->relayers[$channelKey] = new $class( $config ); + } + + return $this->relayers[$channelKey]; + } +} diff --git a/www/wiki/includes/FauxRequest.php b/www/wiki/includes/FauxRequest.php new file mode 100644 index 00000000..2f7f75b4 --- /dev/null +++ b/www/wiki/includes/FauxRequest.php @@ -0,0 +1,246 @@ +<?php +/** + * Deal with importing all those nasty globals and things + * + * Copyright © 2003 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\Session\SessionManager; + +/** + * WebRequest clone which takes values from a provided array. + * + * @ingroup HTTP + */ +class FauxRequest extends WebRequest { + private $wasPosted = false; + private $requestUrl; + protected $cookies = []; + + /** + * @param array $data Array of *non*-urlencoded key => value pairs, the + * fake GET/POST values + * @param bool $wasPosted Whether to treat the data as POST + * @param MediaWiki\Session\Session|array|null $session Session, session + * data array, or null + * @param string $protocol 'http' or 'https' + * @throws MWException + */ + public function __construct( $data = [], $wasPosted = false, + $session = null, $protocol = 'http' + ) { + $this->requestTime = microtime( true ); + + if ( is_array( $data ) ) { + $this->data = $data; + } else { + throw new MWException( "FauxRequest() got bogus data" ); + } + $this->wasPosted = $wasPosted; + if ( $session instanceof MediaWiki\Session\Session ) { + $this->sessionId = $session->getSessionId(); + } elseif ( is_array( $session ) ) { + $mwsession = SessionManager::singleton()->getEmptySession( $this ); + $this->sessionId = $mwsession->getSessionId(); + foreach ( $session as $key => $value ) { + $mwsession->set( $key, $value ); + } + } elseif ( $session !== null ) { + throw new MWException( "FauxRequest() got bogus session" ); + } + $this->protocol = $protocol; + } + + /** + * Initialise the header list + */ + protected function initHeaders() { + // Nothing to init + } + + /** + * @param string $name + * @param string $default + * @return string + */ + public function getText( $name, $default = '' ) { + # Override; don't recode since we're using internal data + return (string)$this->getVal( $name, $default ); + } + + /** + * @return array + */ + public function getValues() { + return $this->data; + } + + /** + * @return array + */ + public function getQueryValues() { + if ( $this->wasPosted ) { + return []; + } else { + return $this->data; + } + } + + public function getMethod() { + return $this->wasPosted ? 'POST' : 'GET'; + } + + /** + * @return bool + */ + public function wasPosted() { + return $this->wasPosted; + } + + public function getCookie( $key, $prefix = null, $default = null ) { + if ( $prefix === null ) { + global $wgCookiePrefix; + $prefix = $wgCookiePrefix; + } + $name = $prefix . $key; + return isset( $this->cookies[$name] ) ? $this->cookies[$name] : $default; + } + + /** + * @since 1.26 + * @param string $key Unprefixed name of the cookie to set + * @param string|null $value Value of the cookie to set + * @param string|null $prefix Cookie prefix. Defaults to $wgCookiePrefix + */ + public function setCookie( $key, $value, $prefix = null ) { + $this->setCookies( [ $key => $value ], $prefix ); + } + + /** + * @since 1.26 + * @param array $cookies + * @param string|null $prefix Cookie prefix. Defaults to $wgCookiePrefix + */ + public function setCookies( $cookies, $prefix = null ) { + if ( $prefix === null ) { + global $wgCookiePrefix; + $prefix = $wgCookiePrefix; + } + foreach ( $cookies as $key => $value ) { + $name = $prefix . $key; + $this->cookies[$name] = $value; + } + } + + /** + * @since 1.25 + * @param string $url + */ + public function setRequestURL( $url ) { + $this->requestUrl = $url; + } + + /** + * @since 1.25 MWException( "getRequestURL not implemented" ) + * no longer thrown. + * @return string + */ + public function getRequestURL() { + if ( $this->requestUrl === null ) { + throw new MWException( 'Request URL not set' ); + } + return $this->requestUrl; + } + + public function getProtocol() { + return $this->protocol; + } + + /** + * @param string $name + * @param string $val + */ + public function setHeader( $name, $val ) { + $this->setHeaders( [ $name => $val ] ); + } + + /** + * @since 1.26 + * @param array $headers + */ + public function setHeaders( $headers ) { + foreach ( $headers as $name => $val ) { + $name = strtoupper( $name ); + $this->headers[$name] = $val; + } + } + + /** + * @return array|null + */ + public function getSessionArray() { + if ( $this->sessionId !== null ) { + return iterator_to_array( $this->getSession() ); + } + return null; + } + + /** + * FauxRequests shouldn't depend on raw request data (but that could be implemented here) + * @return string + */ + public function getRawQueryString() { + return ''; + } + + /** + * FauxRequests shouldn't depend on raw request data (but that could be implemented here) + * @return string + */ + public function getRawPostString() { + return ''; + } + + /** + * FauxRequests shouldn't depend on raw request data (but that could be implemented here) + * @return string + */ + public function getRawInput() { + return ''; + } + + /** + * @codeCoverageIgnore + * @param array $extWhitelist + * @return bool + */ + public function checkUrlExtension( $extWhitelist = [] ) { + return true; + } + + /** + * @codeCoverageIgnore + * @return string + */ + protected function getRawIP() { + return '127.0.0.1'; + } +} diff --git a/www/wiki/includes/Feed.php b/www/wiki/includes/Feed.php new file mode 100644 index 00000000..86e9bee6 --- /dev/null +++ b/www/wiki/includes/Feed.php @@ -0,0 +1,494 @@ +<?php +/** + * Basic support for outputting syndication feeds in RSS, other formats. + * + * Contain a feed class as well as classes to build rss / atom ... feeds + * Available feeds are defined in Defines.php + * + * Copyright © 2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * @defgroup Feed Feed + */ + +/** + * A base class for basic support for outputting syndication feeds in RSS and other formats. + * + * @ingroup Feed + */ +class FeedItem { + /** @var Title */ + public $title; + + public $description; + + public $url; + + public $date; + + public $author; + + public $uniqueId; + + public $comments; + + public $rssIsPermalink = false; + + /** + * @param string|Title $title Item's title + * @param string $description + * @param string $url URL uniquely designating the item. + * @param string $date Item's date + * @param string $author Author's user name + * @param string $comments + */ + function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) { + $this->title = $title; + $this->description = $description; + $this->url = $url; + $this->uniqueId = $url; + $this->date = $date; + $this->author = $author; + $this->comments = $comments; + } + + /** + * Encode $string so that it can be safely embedded in a XML document + * + * @param string $string String to encode + * @return string + */ + public function xmlEncode( $string ) { + $string = str_replace( "\r\n", "\n", $string ); + $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string ); + return htmlspecialchars( $string ); + } + + /** + * Get the unique id of this item; already xml-encoded + * @return string + */ + public function getUniqueID() { + $id = $this->getUniqueIdUnescaped(); + if ( $id ) { + return $this->xmlEncode( $id ); + } + } + + /** + * Get the unique id of this item, without any escaping + * @return string + */ + public function getUniqueIdUnescaped() { + if ( $this->uniqueId ) { + return wfExpandUrl( $this->uniqueId, PROTO_CURRENT ); + } + } + + /** + * Set the unique id of an item + * + * @param string $uniqueId Unique id for the item + * @param bool $rssIsPermalink Set to true if the guid (unique id) is a permalink (RSS feeds only) + */ + public function setUniqueId( $uniqueId, $rssIsPermalink = false ) { + $this->uniqueId = $uniqueId; + $this->rssIsPermalink = $rssIsPermalink; + } + + /** + * Get the title of this item; already xml-encoded + * + * @return string + */ + public function getTitle() { + return $this->xmlEncode( $this->title ); + } + + /** + * Get the URL of this item; already xml-encoded + * + * @return string + */ + public function getUrl() { + return $this->xmlEncode( $this->url ); + } + + /** Get the URL of this item without any escaping + * + * @return string + */ + public function getUrlUnescaped() { + return $this->url; + } + + /** + * Get the description of this item; already xml-encoded + * + * @return string + */ + public function getDescription() { + return $this->xmlEncode( $this->description ); + } + + /** + * Get the description of this item without any escaping + * + * @return string + */ + public function getDescriptionUnescaped() { + return $this->description; + } + + /** + * Get the language of this item + * + * @return string + */ + public function getLanguage() { + global $wgLanguageCode; + return LanguageCode::bcp47( $wgLanguageCode ); + } + + /** + * Get the date of this item + * + * @return string + */ + public function getDate() { + return $this->date; + } + + /** + * Get the author of this item; already xml-encoded + * + * @return string + */ + public function getAuthor() { + return $this->xmlEncode( $this->author ); + } + + /** + * Get the author of this item without any escaping + * + * @return string + */ + public function getAuthorUnescaped() { + return $this->author; + } + + /** + * Get the comment of this item; already xml-encoded + * + * @return string + */ + public function getComments() { + return $this->xmlEncode( $this->comments ); + } + + /** + * Get the comment of this item without any escaping + * + * @return string + */ + public function getCommentsUnescaped() { + return $this->comments; + } + + /** + * Quickie hack... strip out wikilinks to more legible form from the comment. + * + * @param string $text Wikitext + * @return string + */ + public static function stripComment( $text ) { + return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); + } + /**#@-*/ +} + +/** + * Class to support the outputting of syndication feeds in Atom and RSS format. + * + * @ingroup Feed + */ +abstract class ChannelFeed extends FeedItem { + + /** @var TemplateParser */ + protected $templateParser; + + /** + * @param string|Title $title Feed's title + * @param string $description + * @param string $url URL uniquely designating the feed. + * @param string $date Feed's date + * @param string $author Author's user name + * @param string $comments + */ + function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) { + parent::__construct( $title, $description, $url, $date, $author, $comments ); + $this->templateParser = new TemplateParser(); + } + + /** + * Generate Header of the feed + * @par Example: + * @code + * print "<feed>"; + * @endcode + */ + abstract public function outHeader(); + + /** + * Generate an item + * @par Example: + * @code + * print "<item>...</item>"; + * @endcode + * @param FeedItem $item + */ + abstract public function outItem( $item ); + + /** + * Generate Footer of the feed + * @par Example: + * @code + * print "</feed>"; + * @endcode + */ + abstract public function outFooter(); + + /** + * Setup and send HTTP headers. Don't send any content; + * content might end up being cached and re-sent with + * these same headers later. + * + * This should be called from the outHeader() method, + * but can also be called separately. + */ + public function httpHeaders() { + global $wgOut, $wgVaryOnXFP; + + # We take over from $wgOut, excepting its cache header info + $wgOut->disable(); + $mimetype = $this->contentType(); + header( "Content-type: $mimetype; charset=UTF-8" ); + + // Set a sane filename + $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() + ->getExtensionsForType( $mimetype ); + $ext = $exts ? strtok( $exts, ' ' ) : 'xml'; + header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" ); + + if ( $wgVaryOnXFP ) { + $wgOut->addVaryHeader( 'X-Forwarded-Proto' ); + } + $wgOut->sendCacheControl(); + } + + /** + * Return an internet media type to be sent in the headers. + * + * @return string + */ + private function contentType() { + global $wgRequest; + + $ctype = $wgRequest->getVal( 'ctype', 'application/xml' ); + $allowedctypes = [ + 'application/xml', + 'text/xml', + 'application/rss+xml', + 'application/atom+xml' + ]; + + return ( in_array( $ctype, $allowedctypes ) ? $ctype : 'application/xml' ); + } + + /** + * Output the initial XML headers. + */ + protected function outXmlHeader() { + $this->httpHeaders(); + echo '<?xml version="1.0"?>' . "\n"; + } +} + +/** + * Generate a RSS feed + * + * @ingroup Feed + */ +class RSSFeed extends ChannelFeed { + + /** + * Format a date given a timestamp. If a timestamp is not given, nothing is returned + * + * @param int|null $ts Timestamp + * @return string|null Date string + */ + function formatTime( $ts ) { + if ( $ts ) { + return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) ); + } + } + + /** + * Output an RSS 2.0 header + */ + function outHeader() { + global $wgVersion; + + $this->outXmlHeader(); + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + 'title' => $this->getTitle(), + 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ), + 'description' => $this->getDescription(), + 'language' => $this->xmlEncode( $this->getLanguage() ), + 'version' => $this->xmlEncode( $wgVersion ), + 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ) + ]; + print $this->templateParser->processTemplate( 'RSSHeader', $templateParams ); + } + + /** + * Output an RSS 2.0 item + * @param FeedItem $item Item to be output + */ + function outItem( $item ) { + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + "title" => $item->getTitle(), + "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ), + "permalink" => $item->rssIsPermalink, + "uniqueID" => $item->getUniqueID(), + "description" => $item->getDescription(), + "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ), + "author" => $item->getAuthor() + ]; + $comments = $item->getCommentsUnescaped(); + if ( $comments ) { + $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) ); + $templateParams["comments"] = $commentsEscaped; + } + print $this->templateParser->processTemplate( 'RSSItem', $templateParams ); + } + + /** + * Output an RSS 2.0 footer + */ + function outFooter() { + print "</channel></rss>"; + } +} + +/** + * Generate an Atom feed + * + * @ingroup Feed + */ +class AtomFeed extends ChannelFeed { + /** + * Format a date given timestamp, if one is given. + * + * @param string|int|null $timestamp + * @return string|null + */ + function formatTime( $timestamp ) { + if ( $timestamp ) { + // need to use RFC 822 time format at least for rss2.0 + return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) ); + } + } + + /** + * Outputs a basic header for Atom 1.0 feeds. + */ + function outHeader() { + global $wgVersion; + $this->outXmlHeader(); + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + 'language' => $this->xmlEncode( $this->getLanguage() ), + 'feedID' => $this->getFeedId(), + 'title' => $this->getTitle(), + 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ), + 'selfUrl' => $this->getSelfUrl(), + 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ), + 'description' => $this->getDescription(), + 'version' => $this->xmlEncode( $wgVersion ), + ]; + print $this->templateParser->processTemplate( 'AtomHeader', $templateParams ); + } + + /** + * Atom 1.0 requires a unique, opaque IRI as a unique identifier + * for every feed we create. For now just use the URL, but who + * can tell if that's right? If we put options on the feed, do we + * have to change the id? Maybe? Maybe not. + * + * @return string + */ + private function getFeedId() { + return $this->getSelfUrl(); + } + + /** + * Atom 1.0 requests a self-reference to the feed. + * @return string + */ + private function getSelfUrl() { + global $wgRequest; + return htmlspecialchars( $wgRequest->getFullRequestURL() ); + } + + /** + * Output a given item. + * @param FeedItem $item + */ + function outItem( $item ) { + global $wgMimeType; + // Manually escaping rather than letting Mustache do it because Mustache + // uses htmlentities, which does not work with XML + $templateParams = [ + "uniqueID" => $item->getUniqueID(), + "title" => $item->getTitle(), + "mimeType" => $this->xmlEncode( $wgMimeType ), + "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ), + "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ), + "description" => $item->getDescription(), + "author" => $item->getAuthor() + ]; + print $this->templateParser->processTemplate( 'AtomItem', $templateParams ); + } + + /** + * Outputs the footer for Atom 1.0 feed (basically '\</feed\>'). + */ + function outFooter() { + print "</feed>"; + } +} diff --git a/www/wiki/includes/FeedUtils.php b/www/wiki/includes/FeedUtils.php new file mode 100644 index 00000000..4dde52d0 --- /dev/null +++ b/www/wiki/includes/FeedUtils.php @@ -0,0 +1,260 @@ +<?php +/** + * Helper functions for feeds. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Feed + */ + +/** + * Helper functions for feeds + * + * @ingroup Feed + */ +class FeedUtils { + + /** + * Check whether feed's cache should be cleared; for changes feeds + * If the feed should be purged; $timekey and $key will be removed from cache + * + * @param string $timekey Cache key of the timestamp of the last item + * @param string $key Cache key of feed's content + */ + public static function checkPurge( $timekey, $key ) { + global $wgRequest, $wgUser; + + $purge = $wgRequest->getVal( 'action' ) === 'purge'; + // Allow users with 'purge' right to clear feed caches + if ( $purge && $wgUser->isAllowed( 'purge' ) ) { + $cache = ObjectCache::getMainWANInstance(); + $cache->delete( $timekey, 1 ); + $cache->delete( $key, 1 ); + } + } + + /** + * Check whether feeds can be used and that $type is a valid feed type + * + * @param string $type Feed type, as requested by the user + * @return bool + */ + public static function checkFeedOutput( $type ) { + global $wgOut, $wgFeed, $wgFeedClasses; + + if ( !$wgFeed ) { + $wgOut->addWikiMsg( 'feed-unavailable' ); + return false; + } + + if ( !isset( $wgFeedClasses[$type] ) ) { + $wgOut->addWikiMsg( 'feed-invalid' ); + return false; + } + + return true; + } + + /** + * Format a diff for the newsfeed + * + * @param object $row Row from the recentchanges table, including fields as + * appropriate for CommentStore + * @return string + */ + public static function formatDiff( $row ) { + $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); + $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp ); + $actiontext = ''; + if ( $row->rc_type == RC_LOG ) { + $rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows + $actiontext = LogFormatter::newFromRow( $rcRow )->getActionText(); + } + return self::formatDiffRow( $titleObj, + $row->rc_last_oldid, $row->rc_this_oldid, + $timestamp, + $row->rc_deleted & Revision::DELETED_COMMENT + ? wfMessage( 'rev-deleted-comment' )->escaped() + : CommentStore::getStore()->getComment( 'rc_comment', $row )->text, + $actiontext + ); + } + + /** + * Really format a diff for the newsfeed + * + * @param Title $title + * @param int $oldid Old revision's id + * @param int $newid New revision's id + * @param int $timestamp New revision's timestamp + * @param string $comment New revision's comment + * @param string $actiontext Text of the action; in case of log event + * @return string + */ + public static function formatDiffRow( $title, $oldid, $newid, $timestamp, + $comment, $actiontext = '' + ) { + global $wgFeedDiffCutoff, $wgLang; + + // log entries + $completeText = '<p>' . implode( ' ', + array_filter( + [ + $actiontext, + Linker::formatComment( $comment ) ] ) ) . "</p>\n"; + + // NOTE: Check permissions for anonymous users, not current user. + // No "privileged" version should end up in the cache. + // Most feed readers will not log in anyway. + $anon = new User(); + $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true ); + + // Can't diff special pages, unreadable pages or pages with no new revision + // to compare against: just return the text. + if ( $title->getNamespace() < 0 || $accErrors || !$newid ) { + return $completeText; + } + + if ( $oldid ) { + $diffText = ''; + // Don't bother generating the diff if we won't be able to show it + if ( $wgFeedDiffCutoff > 0 ) { + $rev = Revision::newFromId( $oldid ); + + if ( !$rev ) { + $diffText = false; + } else { + $context = clone RequestContext::getMain(); + $context->setTitle( $title ); + + $contentHandler = $rev->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid ); + $diffText = $de->getDiff( + wfMessage( 'previousrevision' )->text(), // hack + wfMessage( 'revisionasof', + $wgLang->timeanddate( $timestamp ), + $wgLang->date( $timestamp ), + $wgLang->time( $timestamp ) )->text() ); + } + } + + if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) { + // Omit large diffs + $diffText = self::getDiffLink( $title, $newid, $oldid ); + } elseif ( $diffText === false ) { + // Error in diff engine, probably a missing revision + $diffText = "<p>Can't load revision $newid</p>"; + } else { + // Diff output fine, clean up any illegal UTF-8 + $diffText = UtfNormal\Validator::cleanUp( $diffText ); + $diffText = self::applyDiffStyle( $diffText ); + } + } else { + $rev = Revision::newFromId( $newid ); + if ( $wgFeedDiffCutoff <= 0 || is_null( $rev ) ) { + $newContent = ContentHandler::getForTitle( $title )->makeEmptyContent(); + } else { + $newContent = $rev->getContent(); + } + + if ( $newContent instanceof TextContent ) { + // only textual content has a "source view". + $text = $newContent->getNativeData(); + + if ( $wgFeedDiffCutoff <= 0 || strlen( $text ) > $wgFeedDiffCutoff ) { + $html = null; + } else { + $html = nl2br( htmlspecialchars( $text ) ); + } + } else { + // XXX: we could get an HTML representation of the content via getParserOutput, but that may + // contain JS magic and generally may not be suitable for inclusion in a feed. + // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. + // Compare also ApiFeedContributions::feedItemDesc + $html = null; + } + + if ( $html === null ) { + // Omit large new page diffs, T31110 + // Also use diff link for non-textual content + $diffText = self::getDiffLink( $title, $newid ); + } else { + $diffText = '<p><b>' . wfMessage( 'newpage' )->text() . '</b></p>' . + '<div>' . $html . '</div>'; + } + } + $completeText .= $diffText; + + return $completeText; + } + + /** + * Generates a diff link. Used when the full diff is not wanted for example + * when $wgFeedDiffCutoff is 0. + * + * @param Title $title Title object: used to generate the diff URL + * @param int $newid Newid for this diff + * @param int|null $oldid Oldid for the diff. Null means it is a new article + * @return string + */ + protected static function getDiffLink( Title $title, $newid, $oldid = null ) { + $queryParameters = [ 'diff' => $newid ]; + if ( $oldid != null ) { + $queryParameters['oldid'] = $oldid; + } + $diffUrl = $title->getFullURL( $queryParameters ); + + $diffLink = Html::element( 'a', [ 'href' => $diffUrl ], + wfMessage( 'showdiff' )->inContentLanguage()->text() ); + + return $diffLink; + } + + /** + * Hacky application of diff styles for the feeds. + * Might be 'cleaner' to use DOM or XSLT or something, + * but *gack* it's a pain in the ass. + * + * @param string $text Diff's HTML output + * @return string Modified HTML + */ + public static function applyDiffStyle( $text ) { + $styles = [ + 'diff' => 'background-color: #fff; color: #222;', + 'diff-otitle' => 'background-color: #fff; color: #222; text-align: center;', + 'diff-ntitle' => 'background-color: #fff; color: #222; text-align: center;', + 'diff-addedline' => 'color: #222; font-size: 88%; border-style: solid; ' + . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; ' + . 'vertical-align: top; white-space: pre-wrap;', + 'diff-deletedline' => 'color: #222; font-size: 88%; border-style: solid; ' + . 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; ' + . 'vertical-align: top; white-space: pre-wrap;', + 'diff-context' => 'background-color: #f8f9fa; color: #222; font-size: 88%; ' + . 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; ' + . 'border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;', + 'diffchange' => 'font-weight: bold; text-decoration: none;', + ]; + + foreach ( $styles as $class => $style ) { + $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/", + "\\1style=\"$style\"\\3", $text ); + } + + return $text; + } + +} diff --git a/www/wiki/includes/FileDeleteForm.php b/www/wiki/includes/FileDeleteForm.php new file mode 100644 index 00000000..783de1c0 --- /dev/null +++ b/www/wiki/includes/FileDeleteForm.php @@ -0,0 +1,448 @@ +<?php +/** + * File deletion user interface. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Rob Church <robchur@gmail.com> + * @ingroup Media + */ +use MediaWiki\MediaWikiServices; + +/** + * File deletion user interface + * + * @ingroup Media + */ +class FileDeleteForm { + + /** + * @var Title + */ + private $title = null; + + /** + * @var File + */ + private $file = null; + + /** + * @var File + */ + private $oldfile = null; + private $oldimage = ''; + + /** + * @param File $file File object we're deleting + */ + public function __construct( $file ) { + $this->title = $file->getTitle(); + $this->file = $file; + } + + /** + * Fulfil the request; shows the form or deletes the file, + * pending authentication, confirmation, etc. + */ + public function execute() { + global $wgOut, $wgRequest, $wgUser, $wgUploadMaintenance; + + $permissionErrors = $this->title->getUserPermissionsErrors( 'delete', $wgUser ); + if ( count( $permissionErrors ) ) { + throw new PermissionsError( 'delete', $permissionErrors ); + } + + if ( wfReadOnly() ) { + throw new ReadOnlyError; + } + + if ( $wgUploadMaintenance ) { + throw new ErrorPageError( 'filedelete-maintenance-title', 'filedelete-maintenance' ); + } + + $this->setHeaders(); + + $this->oldimage = $wgRequest->getText( 'oldimage', false ); + $token = $wgRequest->getText( 'wpEditToken' ); + # Flag to hide all contents of the archived revisions + $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' ); + + if ( $this->oldimage ) { + $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( + $this->title, + $this->oldimage + ); + } + + if ( !self::haveDeletableFile( $this->file, $this->oldfile, $this->oldimage ) ) { + $wgOut->addHTML( $this->prepareMessage( 'filedelete-nofile' ) ); + $wgOut->addReturnTo( $this->title ); + return; + } + + // Perform the deletion if appropriate + if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { + $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' ); + $deleteReason = $wgRequest->getText( 'wpReason' ); + + if ( $deleteReasonList == 'other' ) { + $reason = $deleteReason; + } elseif ( $deleteReason != '' ) { + // Entry from drop down menu + additional comment + $reason = $deleteReasonList . wfMessage( 'colon-separator' ) + ->inContentLanguage()->text() . $deleteReason; + } else { + $reason = $deleteReasonList; + } + + $status = self::doDelete( + $this->title, + $this->file, + $this->oldimage, + $reason, + $suppress, + $wgUser + ); + + if ( !$status->isGood() ) { + $wgOut->addHTML( '<h2>' . $this->prepareMessage( 'filedeleteerror-short' ) . "</h2>\n" ); + $wgOut->addWikiText( '<div class="error">' . + $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) + . '</div>' ); + } + if ( $status->isOK() ) { + $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) ); + $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) ); + // Return to the main page if we just deleted all versions of the + // file, otherwise go back to the description page + $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() ); + + WatchAction::doWatchOrUnwatch( $wgRequest->getCheck( 'wpWatch' ), $this->title, $wgUser ); + } + return; + } + + $this->showForm(); + $this->showLogEntries(); + } + + /** + * Really delete the file + * + * @param Title &$title + * @param File &$file + * @param string &$oldimage Archive name + * @param string $reason Reason of the deletion + * @param bool $suppress Whether to mark all deleted versions as restricted + * @param User $user User object performing the request + * @param array $tags Tags to apply to the deletion action + * @throws MWException + * @return Status + */ + public static function doDelete( &$title, &$file, &$oldimage, $reason, + $suppress, User $user = null, $tags = [] + ) { + if ( $user === null ) { + global $wgUser; + $user = $wgUser; + } + + if ( $oldimage ) { + $page = null; + $status = $file->deleteOld( $oldimage, $reason, $suppress, $user ); + if ( $status->ok ) { + // Need to do a log item + $logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text(); + if ( trim( $reason ) != '' ) { + $logComment .= wfMessage( 'colon-separator' ) + ->inContentLanguage()->text() . $reason; + } + + $logtype = $suppress ? 'suppress' : 'delete'; + + $logEntry = new ManualLogEntry( $logtype, 'delete' ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( $title ); + $logEntry->setComment( $logComment ); + $logEntry->setTags( $tags ); + $logid = $logEntry->insert(); + $logEntry->publish( $logid ); + + $status->value = $logid; + } + } else { + $status = Status::newFatal( 'cannotdelete', + wfEscapeWikiText( $title->getPrefixedText() ) + ); + $page = WikiPage::factory( $title ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->startAtomic( __METHOD__ ); + // delete the associated article first + $error = ''; + $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, + $user, $tags ); + // doDeleteArticleReal() returns a non-fatal error status if the page + // or revision is missing, so check for isOK() rather than isGood() + if ( $deleteStatus->isOK() ) { + $status = $file->delete( $reason, $suppress, $user ); + if ( $status->isOK() ) { + if ( $deleteStatus->value === null ) { + // No log ID from doDeleteArticleReal(), probably + // because the page/revision didn't exist, so create + // one here. + $logtype = $suppress ? 'suppress' : 'delete'; + $logEntry = new ManualLogEntry( $logtype, 'delete' ); + $logEntry->setPerformer( $user ); + $logEntry->setTarget( clone $title ); + $logEntry->setComment( $reason ); + $logEntry->setTags( $tags ); + $logid = $logEntry->insert(); + $dbw->onTransactionPreCommitOrIdle( + function () use ( $dbw, $logEntry, $logid ) { + $logEntry->publish( $logid ); + }, + __METHOD__ + ); + $status->value = $logid; + } else { + $status->value = $deleteStatus->value; // log id + } + $dbw->endAtomic( __METHOD__ ); + } else { + // Page deleted but file still there? rollback page delete + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->rollbackMasterChanges( __METHOD__ ); + } + } else { + // Done; nothing changed + $dbw->endAtomic( __METHOD__ ); + } + } + + if ( $status->isOK() ) { + Hooks::run( 'FileDeleteComplete', [ &$file, &$oldimage, &$page, &$user, &$reason ] ); + } + + return $status; + } + + /** + * Show the confirmation form + */ + private function showForm() { + global $wgOut, $wgUser, $wgRequest; + + $conf = RequestContext::getMain()->getConfig(); + $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD; + + if ( $wgUser->isAllowed( 'suppressrevision' ) ) { + $suppress = "<tr id=\"wpDeleteSuppressRow\"> + <td></td> + <td class='mw-input'><strong>" . + Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), + 'wpSuppress', 'wpSuppress', false, [ 'tabindex' => '3' ] ) . + "</strong></td> + </tr>"; + } else { + $suppress = ''; + } + + $wgOut->addModules( 'mediawiki.action.delete.file' ); + + $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title ); + $form = Xml::openElement( 'form', [ 'method' => 'post', 'action' => $this->getAction(), + 'id' => 'mw-img-deleteconfirm' ] ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMessage( 'filedelete-legend' )->text() ) . + Html::hidden( 'wpEditToken', $wgUser->getEditToken( $this->oldimage ) ) . + $this->prepareMessage( 'filedelete-intro' ) . + Xml::openElement( 'table', [ 'id' => 'mw-img-deleteconfirm-table' ] ) . + "<tr> + <td class='mw-label'>" . + Xml::label( wfMessage( 'filedelete-comment' )->text(), 'wpDeleteReasonList' ) . + "</td> + <td class='mw-input'>" . + Xml::listDropDown( + 'wpDeleteReasonList', + wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->text(), + wfMessage( 'filedelete-reason-otherlist' )->inContentLanguage()->text(), + '', + 'wpReasonDropDown', + 1 + ) . + "</td> + </tr> + <tr> + <td class='mw-label'>" . + Xml::label( wfMessage( 'filedelete-otherreason' )->text(), 'wpReason' ) . + "</td> + <td class='mw-input'>" . + Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), [ + 'type' => 'text', + // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP + // (e.g. emojis) count for two each. This limit is overridden in JS to instead count + // Unicode codepoints (or 255 UTF-8 bytes for old schema). + 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT, + 'tabindex' => '2', + 'id' => 'wpReason' + ] ) . + "</td> + </tr> + {$suppress}"; + if ( $wgUser->isLoggedIn() ) { + $form .= " + <tr> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( wfMessage( 'watchthis' )->text(), + 'wpWatch', 'wpWatch', $checkWatch, [ 'tabindex' => '3' ] ) . + "</td> + </tr>"; + } + $form .= " + <tr> + <td></td> + <td class='mw-submit'>" . + Xml::submitButton( + wfMessage( 'filedelete-submit' )->text(), + [ + 'name' => 'mw-filedelete-submit', + 'id' => 'mw-filedelete-submit', + 'tabindex' => '4' + ] + ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $title = wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $link = $linkRenderer->makeKnownLink( + $title, + wfMessage( 'filedelete-edit-reasonlist' )->text(), + [], + [ 'action' => 'edit' ] + ); + $form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>'; + } + + $wgOut->addHTML( $form ); + } + + /** + * Show deletion log fragments pertaining to the current file + */ + private function showLogEntries() { + global $wgOut; + $deleteLogPage = new LogPage( 'delete' ); + $wgOut->addHTML( '<h2>' . $deleteLogPage->getName()->escaped() . "</h2>\n" ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->title ); + } + + /** + * Prepare a message referring to the file being deleted, + * showing an appropriate message depending upon whether + * it's a current file or an old version + * + * @param string $message Message base + * @return string + */ + private function prepareMessage( $message ) { + global $wgLang; + if ( $this->oldimage ) { + # Message keys used: + # 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old' + return wfMessage( + "{$message}-old", + wfEscapeWikiText( $this->title->getText() ), + $wgLang->date( $this->getTimestamp(), true ), + $wgLang->time( $this->getTimestamp(), true ), + wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) )->parseAsBlock(); + } else { + return wfMessage( + $message, + wfEscapeWikiText( $this->title->getText() ) + )->parseAsBlock(); + } + } + + /** + * Set headers, titles and other bits + */ + private function setHeaders() { + global $wgOut; + $wgOut->setPageTitle( wfMessage( 'filedelete', $this->title->getText() ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->addBacklinkSubtitle( $this->title ); + } + + /** + * Is the provided `oldimage` value valid? + * + * @param string $oldimage + * @return bool + */ + public static function isValidOldSpec( $oldimage ) { + return strlen( $oldimage ) >= 16 + && strpos( $oldimage, '/' ) === false + && strpos( $oldimage, '\\' ) === false; + } + + /** + * Could we delete the file specified? If an `oldimage` + * value was provided, does it correspond to an + * existing, local, old version of this file? + * + * @param File &$file + * @param File &$oldfile + * @param File $oldimage + * @return bool + */ + public static function haveDeletableFile( &$file, &$oldfile, $oldimage ) { + return $oldimage + ? $oldfile && $oldfile->exists() && $oldfile->isLocal() + : $file && $file->exists() && $file->isLocal(); + } + + /** + * Prepare the form action + * + * @return string + */ + private function getAction() { + $q = []; + $q['action'] = 'delete'; + + if ( $this->oldimage ) { + $q['oldimage'] = $this->oldimage; + } + + return $this->title->getLocalURL( $q ); + } + + /** + * Extract the timestamp of the old version + * + * @return string + */ + private function getTimestamp() { + return $this->oldfile->getTimestamp(); + } +} diff --git a/www/wiki/includes/ForkController.php b/www/wiki/includes/ForkController.php new file mode 100644 index 00000000..cc16964e --- /dev/null +++ b/www/wiki/includes/ForkController.php @@ -0,0 +1,204 @@ +<?php +/** + * Class for managing forking command line scripts. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ +use MediaWiki\MediaWikiServices; + +/** + * Class for managing forking command line scripts. + * Currently just does forking and process control, but it could easily be extended + * to provide IPC and job dispatch. + * + * This class requires the posix and pcntl extensions. + * + * @ingroup Maintenance + */ +class ForkController { + protected $children = [], $childNumber = 0; + protected $termReceived = false; + protected $flags = 0, $procsToStart = 0; + + protected static $restartableSignals = [ + SIGFPE, + SIGILL, + SIGSEGV, + SIGBUS, + SIGABRT, + SIGSYS, + SIGPIPE, + SIGXCPU, + SIGXFSZ, + ]; + + /** + * Pass this flag to __construct() to cause the class to automatically restart + * workers that exit with non-zero exit status or a signal such as SIGSEGV. + */ + const RESTART_ON_ERROR = 1; + + public function __construct( $numProcs, $flags = 0 ) { + if ( !wfIsCLI() ) { + throw new MWException( "ForkController cannot be used from the web." ); + } + $this->procsToStart = $numProcs; + $this->flags = $flags; + } + + /** + * Start the child processes. + * + * This should only be called from the command line. It should be called + * as early as possible during execution. + * + * This will return 'child' in the child processes. In the parent process, + * it will run until all the child processes exit or a TERM signal is + * received. It will then return 'done'. + * @return string + */ + public function start() { + // Trap SIGTERM + pcntl_signal( SIGTERM, [ $this, 'handleTermSignal' ], false ); + + do { + // Start child processes + if ( $this->procsToStart ) { + if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) { + return 'child'; + } + $this->procsToStart = 0; + } + + // Check child status + $status = false; + $deadPid = pcntl_wait( $status ); + + if ( $deadPid > 0 ) { + // Respond to child process termination + unset( $this->children[$deadPid] ); + if ( $this->flags & self::RESTART_ON_ERROR ) { + if ( pcntl_wifsignaled( $status ) ) { + // Restart if the signal was abnormal termination + // Don't restart if it was deliberately killed + $signal = pcntl_wtermsig( $status ); + if ( in_array( $signal, self::$restartableSignals ) ) { + echo "Worker exited with signal $signal, restarting\n"; + $this->procsToStart++; + } + } elseif ( pcntl_wifexited( $status ) ) { + // Restart on non-zero exit status + $exitStatus = pcntl_wexitstatus( $status ); + if ( $exitStatus != 0 ) { + echo "Worker exited with status $exitStatus, restarting\n"; + $this->procsToStart++; + } else { + echo "Worker exited normally\n"; + } + } + } + // Throttle restarts + if ( $this->procsToStart ) { + usleep( 500000 ); + } + } + + // Run signal handlers + if ( function_exists( 'pcntl_signal_dispatch' ) ) { + pcntl_signal_dispatch(); + } else { + declare( ticks = 1 ) { + $status = $status; + } + } + // Respond to TERM signal + if ( $this->termReceived ) { + foreach ( $this->children as $childPid => $unused ) { + posix_kill( $childPid, SIGTERM ); + } + $this->termReceived = false; + } + } while ( count( $this->children ) ); + pcntl_signal( SIGTERM, SIG_DFL ); + return 'done'; + } + + /** + * Get the number of the child currently running. Note, this + * is not the pid, but rather which of the total number of children + * we are + * @return int + */ + public function getChildNumber() { + return $this->childNumber; + } + + protected function prepareEnvironment() { + global $wgMemc; + // Don't share DB, storage, or memcached connections + MediaWikiServices::resetChildProcessServices(); + FileBackendGroup::destroySingleton(); + LockManagerGroup::destroySingletons(); + JobQueueGroup::destroySingletons(); + ObjectCache::clear(); + RedisConnectionPool::destroySingletons(); + $wgMemc = null; + } + + /** + * Fork a number of worker processes. + * + * @param int $numProcs + * @return string + */ + protected function forkWorkers( $numProcs ) { + $this->prepareEnvironment(); + + // Create the child processes + for ( $i = 0; $i < $numProcs; $i++ ) { + // Do the fork + $pid = pcntl_fork(); + if ( $pid === -1 || $pid === false ) { + echo "Error creating child processes\n"; + exit( 1 ); + } + + if ( !$pid ) { + $this->initChild(); + $this->childNumber = $i; + return 'child'; + } else { + // This is the parent process + $this->children[$pid] = true; + } + } + + return 'parent'; + } + + protected function initChild() { + global $wgMemc, $wgMainCacheType; + $wgMemc = wfGetCache( $wgMainCacheType ); + $this->children = null; + pcntl_signal( SIGTERM, SIG_DFL ); + } + + protected function handleTermSignal( $signal ) { + $this->termReceived = true; + } +} diff --git a/www/wiki/includes/FormOptions.php b/www/wiki/includes/FormOptions.php new file mode 100644 index 00000000..53c8d3bf --- /dev/null +++ b/www/wiki/includes/FormOptions.php @@ -0,0 +1,422 @@ +<?php +/** + * Helper class to keep track of options when mixing links and form elements. + * + * Copyright © 2008, Niklas Laxström + * Copyright © 2011, Antoine Musso + * Copyright © 2013, Bartosz Dziewoński + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @author Antoine Musso + */ + +/** + * Helper class to keep track of options when mixing links and form elements. + * + * @todo This badly needs some examples and tests :) The usage in SpecialRecentchanges class is a + * good ersatz in the meantime. + */ +class FormOptions implements ArrayAccess { + /** @name Type constants + * Used internally to map an option value to a WebRequest accessor + */ + /* @{ */ + /** Mark value for automatic detection (for simple data types only) */ + const AUTO = -1; + /** String type, maps guessType() to WebRequest::getText() */ + const STRING = 0; + /** Integer type, maps guessType() to WebRequest::getInt() */ + const INT = 1; + /** Float type, maps guessType() to WebRequest::getFloat() + * @since 1.23 */ + const FLOAT = 4; + /** Boolean type, maps guessType() to WebRequest::getBool() */ + const BOOL = 2; + /** Integer type or null, maps to WebRequest::getIntOrNull() + * This is useful for the namespace selector. + */ + const INTNULL = 3; + /** Array type, maps guessType() to WebRequest::getArray() + * @since 1.29 */ + const ARR = 5; + /* @} */ + + /** + * Map of known option names to information about them. + * + * Each value is an array with the following keys: + * - 'default' - the default value as passed to add() + * - 'value' - current value, start with null, can be set by various functions + * - 'consumed' - true/false, whether the option was consumed using + * consumeValue() or consumeValues() + * - 'type' - one of the type constants (but never AUTO) + */ + protected $options = []; + + # Setting up + + /** + * Add an option to be handled by this FormOptions instance. + * + * @param string $name Request parameter name + * @param mixed $default Default value when the request parameter is not present + * @param int $type One of the type constants (optional, defaults to AUTO) + */ + public function add( $name, $default, $type = self::AUTO ) { + $option = []; + $option['default'] = $default; + $option['value'] = null; + $option['consumed'] = false; + + if ( $type !== self::AUTO ) { + $option['type'] = $type; + } else { + $option['type'] = self::guessType( $default ); + } + + $this->options[$name] = $option; + } + + /** + * Remove an option being handled by this FormOptions instance. This is the inverse of add(). + * + * @param string $name Request parameter name + */ + public function delete( $name ) { + $this->validateName( $name, true ); + unset( $this->options[$name] ); + } + + /** + * Used to find out which type the data is. All types are defined in the 'Type constants' section + * of this class. + * + * Detection of the INTNULL type is not supported; INT will be assumed if the data is an integer, + * MWException will be thrown if it's null. + * + * @param mixed $data Value to guess the type for + * @throws MWException If unable to guess the type + * @return int Type constant + */ + public static function guessType( $data ) { + if ( is_bool( $data ) ) { + return self::BOOL; + } elseif ( is_int( $data ) ) { + return self::INT; + } elseif ( is_float( $data ) ) { + return self::FLOAT; + } elseif ( is_string( $data ) ) { + return self::STRING; + } elseif ( is_array( $data ) ) { + return self::ARR; + } else { + throw new MWException( 'Unsupported datatype' ); + } + } + + # Handling values + + /** + * Verify that the given option name exists. + * + * @param string $name Option name + * @param bool $strict Throw an exception when the option doesn't exist instead of returning false + * @throws MWException + * @return bool True if the option exists, false otherwise + */ + public function validateName( $name, $strict = false ) { + if ( !isset( $this->options[$name] ) ) { + if ( $strict ) { + throw new MWException( "Invalid option $name" ); + } else { + return false; + } + } + + return true; + } + + /** + * Use to set the value of an option. + * + * @param string $name Option name + * @param mixed $value Value for the option + * @param bool $force Whether to set the value when it is equivalent to the default value for this + * option (default false). + */ + public function setValue( $name, $value, $force = false ) { + $this->validateName( $name, true ); + + if ( !$force && $value === $this->options[$name]['default'] ) { + // null default values as unchanged + $this->options[$name]['value'] = null; + } else { + $this->options[$name]['value'] = $value; + } + } + + /** + * Get the value for the given option name. Uses getValueReal() internally. + * + * @param string $name Option name + * @return mixed + */ + public function getValue( $name ) { + $this->validateName( $name, true ); + + return $this->getValueReal( $this->options[$name] ); + } + + /** + * Return current option value, based on a structure taken from $options. + * + * @param array $option Array structure describing the option + * @return mixed Value, or the default value if it is null + */ + protected function getValueReal( $option ) { + if ( $option['value'] !== null ) { + return $option['value']; + } else { + return $option['default']; + } + } + + /** + * Delete the option value. + * This will make future calls to getValue() return the default value. + * @param string $name Option name + */ + public function reset( $name ) { + $this->validateName( $name, true ); + $this->options[$name]['value'] = null; + } + + /** + * Get the value of given option and mark it as 'consumed'. Consumed options are not returned + * by getUnconsumedValues(). + * + * @see consumeValues() + * @throws MWException If the option does not exist + * @param string $name Option name + * @return mixed Value, or the default value if it is null + */ + public function consumeValue( $name ) { + $this->validateName( $name, true ); + $this->options[$name]['consumed'] = true; + + return $this->getValueReal( $this->options[$name] ); + } + + /** + * Get the values of given options and mark them as 'consumed'. Consumed options are not returned + * by getUnconsumedValues(). + * + * @see consumeValue() + * @throws MWException If any option does not exist + * @param array $names Array of option names as strings + * @return array Array of option values, or the default values if they are null + */ + public function consumeValues( $names ) { + $out = []; + + foreach ( $names as $name ) { + $this->validateName( $name, true ); + $this->options[$name]['consumed'] = true; + $out[] = $this->getValueReal( $this->options[$name] ); + } + + return $out; + } + + /** + * @see validateBounds() + * @param string $name + * @param int $min + * @param int $max + */ + public function validateIntBounds( $name, $min, $max ) { + $this->validateBounds( $name, $min, $max ); + } + + /** + * Constrain a numeric value for a given option to a given range. The value will be altered to fit + * in the range. + * + * @since 1.23 + * + * @param string $name Option name + * @param int|float $min Minimum value + * @param int|float $max Maximum value + * @throws MWException If option is not of type INT + */ + public function validateBounds( $name, $min, $max ) { + $this->validateName( $name, true ); + $type = $this->options[$name]['type']; + + if ( $type !== self::INT && $type !== self::FLOAT ) { + throw new MWException( "Option $name is not of type INT or FLOAT" ); + } + + $value = $this->getValueReal( $this->options[$name] ); + $value = max( $min, min( $max, $value ) ); + + $this->setValue( $name, $value ); + } + + /** + * Get all remaining values which have not been consumed by consumeValue() or consumeValues(). + * + * @param bool $all Whether to include unchanged options (default: false) + * @return array + */ + public function getUnconsumedValues( $all = false ) { + $values = []; + + foreach ( $this->options as $name => $data ) { + if ( !$data['consumed'] ) { + if ( $all || $data['value'] !== null ) { + $values[$name] = $this->getValueReal( $data ); + } + } + } + + return $values; + } + + /** + * Return options modified as an array ( name => value ) + * @return array + */ + public function getChangedValues() { + $values = []; + + foreach ( $this->options as $name => $data ) { + if ( $data['value'] !== null ) { + $values[$name] = $data['value']; + } + } + + return $values; + } + + /** + * Format options to an array ( name => value ) + * @return array + */ + public function getAllValues() { + $values = []; + + foreach ( $this->options as $name => $data ) { + $values[$name] = $this->getValueReal( $data ); + } + + return $values; + } + + # Reading values + + /** + * Fetch values for all options (or selected options) from the given WebRequest, making them + * available for accessing with getValue() or consumeValue() etc. + * + * @param WebRequest $r The request to fetch values from + * @param array $optionKeys Which options to fetch the values for (default: + * all of them). Note that passing an empty array will also result in + * values for all keys being fetched. + * @throws MWException If the type of any option is invalid + */ + public function fetchValuesFromRequest( WebRequest $r, $optionKeys = null ) { + if ( !$optionKeys ) { + $optionKeys = array_keys( $this->options ); + } + + foreach ( $optionKeys as $name ) { + $default = $this->options[$name]['default']; + $type = $this->options[$name]['type']; + + switch ( $type ) { + case self::BOOL: + $value = $r->getBool( $name, $default ); + break; + case self::INT: + $value = $r->getInt( $name, $default ); + break; + case self::FLOAT: + $value = $r->getFloat( $name, $default ); + break; + case self::STRING: + $value = $r->getText( $name, $default ); + break; + case self::INTNULL: + $value = $r->getIntOrNull( $name ); + break; + case self::ARR: + $value = $r->getArray( $name ); + break; + default: + throw new MWException( 'Unsupported datatype' ); + } + + if ( $value !== null ) { + $this->options[$name]['value'] = $value === $default ? null : $value; + } + } + } + + /** @name ArrayAccess functions + * These functions implement the ArrayAccess PHP interface. + * @see https://secure.php.net/manual/en/class.arrayaccess.php + */ + /* @{ */ + /** + * Whether the option exists. + * @param string $name + * @return bool + */ + public function offsetExists( $name ) { + return isset( $this->options[$name] ); + } + + /** + * Retrieve an option value. + * @param string $name + * @return mixed + */ + public function offsetGet( $name ) { + return $this->getValue( $name ); + } + + /** + * Set an option to given value. + * @param string $name + * @param mixed $value + */ + public function offsetSet( $name, $value ) { + $this->setValue( $name, $value ); + } + + /** + * Delete the option. + * @param string $name + */ + public function offsetUnset( $name ) { + $this->delete( $name ); + } + /* @} */ +} diff --git a/www/wiki/includes/GitInfo.php b/www/wiki/includes/GitInfo.php new file mode 100644 index 00000000..363d7b80 --- /dev/null +++ b/www/wiki/includes/GitInfo.php @@ -0,0 +1,426 @@ +<?php +/** + * A class to help return information about a git repo MediaWiki may be inside + * This is used by Special:Version and is also useful for the LocalSettings.php + * of anyone working on large branches in git to setup config that show up only + * when specific branches are currently checked out. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\Shell\Shell; + +class GitInfo { + + /** + * Singleton for the repo at $IP + */ + protected static $repo = null; + + /** + * Location of the .git directory + */ + protected $basedir; + + /** + * Location of the repository + */ + protected $repoDir; + + /** + * Path to JSON cache file for pre-computed git information. + */ + protected $cacheFile; + + /** + * Cached git information. + */ + protected $cache = []; + + /** + * @var array|false Map of repo URLs to viewer URLs. Access via static method getViewers(). + */ + private static $viewers = false; + + /** + * @param string $repoDir The root directory of the repo where .git can be found + * @param bool $usePrecomputed Use precomputed information if available + * @see precomputeValues + */ + public function __construct( $repoDir, $usePrecomputed = true ) { + $this->repoDir = $repoDir; + $this->cacheFile = self::getCacheFilePath( $repoDir ); + wfDebugLog( 'gitinfo', + "Computed cacheFile={$this->cacheFile} for {$repoDir}" + ); + if ( $usePrecomputed && + $this->cacheFile !== null && + is_readable( $this->cacheFile ) + ) { + $this->cache = FormatJson::decode( + file_get_contents( $this->cacheFile ), + true + ); + wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" ); + } + + if ( !$this->cacheIsComplete() ) { + wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" ); + $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git'; + if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) { + $GITfile = file_get_contents( $this->basedir ); + if ( strlen( $GITfile ) > 8 && + substr( $GITfile, 0, 8 ) === 'gitdir: ' + ) { + $path = rtrim( substr( $GITfile, 8 ), "\r\n" ); + if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) { + // Path from GITfile is absolute + $this->basedir = $path; + } else { + $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path; + } + } + } + } + } + + /** + * Compute the path to the cache file for a given directory. + * + * @param string $repoDir The root directory of the repo where .git can be found + * @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or + * fallback in the extension directory itself + * @since 1.24 + */ + protected static function getCacheFilePath( $repoDir ) { + global $IP, $wgGitInfoCacheDirectory; + + if ( $wgGitInfoCacheDirectory ) { + // Convert both $IP and $repoDir to canonical paths to protect against + // $IP having changed between the settings files and runtime. + $realIP = realpath( $IP ); + $repoName = realpath( $repoDir ); + if ( $repoName === false ) { + // Unit tests use fake path names + $repoName = $repoDir; + } + if ( strpos( $repoName, $realIP ) === 0 ) { + // Strip $IP from path + $repoName = substr( $repoName, strlen( $realIP ) ); + } + // Transform path to git repo to something we can safely embed in + // a filename + $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' ); + $fileName = 'info' . $repoName . '.json'; + $cachePath = "{$wgGitInfoCacheDirectory}/{$fileName}"; + if ( is_readable( $cachePath ) ) { + return $cachePath; + } + } + + return "$repoDir/gitinfo.json"; + } + + /** + * Get the singleton for the repo at $IP + * + * @return GitInfo + */ + public static function repo() { + if ( is_null( self::$repo ) ) { + global $IP; + self::$repo = new self( $IP ); + } + return self::$repo; + } + + /** + * Check if a string looks like a hex encoded SHA1 hash + * + * @param string $str The string to check + * @return bool Whether or not the string looks like a SHA1 + */ + public static function isSHA1( $str ) { + return !!preg_match( '/^[0-9A-F]{40}$/i', $str ); + } + + /** + * Get the HEAD of the repo (without any opening "ref: ") + * + * @return string|bool The HEAD (git reference or SHA1) or false + */ + public function getHead() { + if ( !isset( $this->cache['head'] ) ) { + $headFile = "{$this->basedir}/HEAD"; + $head = false; + + if ( is_readable( $headFile ) ) { + $head = file_get_contents( $headFile ); + + if ( preg_match( "/ref: (.*)/", $head, $m ) ) { + $head = rtrim( $m[1] ); + } else { + $head = rtrim( $head ); + } + } + $this->cache['head'] = $head; + } + return $this->cache['head']; + } + + /** + * Get the SHA1 for the current HEAD of the repo + * + * @return string|bool A SHA1 or false + */ + public function getHeadSHA1() { + if ( !isset( $this->cache['headSHA1'] ) ) { + $head = $this->getHead(); + $sha1 = false; + + // If detached HEAD may be a SHA1 + if ( self::isSHA1( $head ) ) { + $sha1 = $head; + } else { + // If not a SHA1 it may be a ref: + $refFile = "{$this->basedir}/{$head}"; + $packedRefs = "{$this->basedir}/packed-refs"; + $headRegex = preg_quote( $head, '/' ); + if ( is_readable( $refFile ) ) { + $sha1 = rtrim( file_get_contents( $refFile ) ); + } elseif ( is_readable( $packedRefs ) && + preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches ) + ) { + $sha1 = $matches[1]; + } + } + $this->cache['headSHA1'] = $sha1; + } + return $this->cache['headSHA1']; + } + + /** + * Get the commit date of HEAD entry of the git code repository + * + * @since 1.22 + * @return int|bool Commit date (UNIX timestamp) or false + */ + public function getHeadCommitDate() { + global $wgGitBin; + + if ( !isset( $this->cache['headCommitDate'] ) ) { + $date = false; + if ( is_file( $wgGitBin ) && + is_executable( $wgGitBin ) && + !Shell::isDisabled() && + $this->getHead() !== false + ) { + $cmd = [ + $wgGitBin, + 'show', + '-s', + '--format=format:%ct', + 'HEAD', + ]; + $gitDir = realpath( $this->basedir ); + $result = Shell::command( $cmd ) + ->environment( [ 'GIT_DIR' => $gitDir ] ) + ->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK ) + ->whitelistPaths( [ $gitDir, $this->repoDir ] ) + ->execute(); + + if ( $result->getExitCode() === 0 ) { + $date = (int)$result->getStdout(); + } + } + $this->cache['headCommitDate'] = $date; + } + return $this->cache['headCommitDate']; + } + + /** + * Get the name of the current branch, or HEAD if not found + * + * @return string|bool The branch name, HEAD, or false + */ + public function getCurrentBranch() { + if ( !isset( $this->cache['branch'] ) ) { + $branch = $this->getHead(); + if ( $branch && + preg_match( "#^refs/heads/(.*)$#", $branch, $m ) + ) { + $branch = $m[1]; + } + $this->cache['branch'] = $branch; + } + return $this->cache['branch']; + } + + /** + * Get an URL to a web viewer link to the HEAD revision. + * + * @return string|bool String if a URL is available or false otherwise + */ + public function getHeadViewUrl() { + $url = $this->getRemoteUrl(); + if ( $url === false ) { + return false; + } + foreach ( self::getViewers() as $repo => $viewer ) { + $pattern = '#^' . $repo . '$#'; + if ( preg_match( $pattern, $url, $matches ) ) { + $viewerUrl = preg_replace( $pattern, $viewer, $url ); + $headSHA1 = $this->getHeadSHA1(); + $replacements = [ + '%h' => substr( $headSHA1, 0, 7 ), + '%H' => $headSHA1, + '%r' => urlencode( $matches[1] ), + '%R' => $matches[1], + ]; + return strtr( $viewerUrl, $replacements ); + } + } + return false; + } + + /** + * Get the URL of the remote origin. + * @return string|bool String if a URL is available or false otherwise. + */ + protected function getRemoteUrl() { + if ( !isset( $this->cache['remoteURL'] ) ) { + $config = "{$this->basedir}/config"; + $url = false; + if ( is_readable( $config ) ) { + Wikimedia\suppressWarnings(); + $configArray = parse_ini_file( $config, true ); + Wikimedia\restoreWarnings(); + $remote = false; + + // Use the "origin" remote repo if available or any other repo if not. + if ( isset( $configArray['remote origin'] ) ) { + $remote = $configArray['remote origin']; + } elseif ( is_array( $configArray ) ) { + foreach ( $configArray as $sectionName => $sectionConf ) { + if ( substr( $sectionName, 0, 6 ) == 'remote' ) { + $remote = $sectionConf; + } + } + } + + if ( $remote !== false && isset( $remote['url'] ) ) { + $url = $remote['url']; + } + } + $this->cache['remoteURL'] = $url; + } + return $this->cache['remoteURL']; + } + + /** + * Check to see if the current cache is fully populated. + * + * Note: This method is public only to make unit testing easier. There's + * really no strong reason that anything other than a test should want to + * call this method. + * + * @return bool True if all expected cache keys exist, false otherwise + */ + public function cacheIsComplete() { + return isset( $this->cache['head'] ) && + isset( $this->cache['headSHA1'] ) && + isset( $this->cache['headCommitDate'] ) && + isset( $this->cache['branch'] ) && + isset( $this->cache['remoteURL'] ); + } + + /** + * Precompute and cache git information. + * + * Creates a JSON file in the cache directory associated with this + * GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing + * the same directory to avoid needing to examine the .git directory again. + * + * @since 1.24 + */ + public function precomputeValues() { + if ( $this->cacheFile !== null ) { + // Try to completely populate the cache + $this->getHead(); + $this->getHeadSHA1(); + $this->getHeadCommitDate(); + $this->getCurrentBranch(); + $this->getRemoteUrl(); + + if ( !$this->cacheIsComplete() ) { + wfDebugLog( 'gitinfo', + "Failed to compute GitInfo for \"{$this->basedir}\"" + ); + return; + } + + $cacheDir = dirname( $this->cacheFile ); + if ( !file_exists( $cacheDir ) && + !wfMkdirParents( $cacheDir, null, __METHOD__ ) + ) { + throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" ); + } + + file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) ); + } + } + + /** + * @see self::getHeadSHA1 + * @return string + */ + public static function headSHA1() { + return self::repo()->getHeadSHA1(); + } + + /** + * @see self::getCurrentBranch + * @return string + */ + public static function currentBranch() { + return self::repo()->getCurrentBranch(); + } + + /** + * @see self::getHeadViewUrl() + * @return bool|string + */ + public static function headViewUrl() { + return self::repo()->getHeadViewUrl(); + } + + /** + * Gets the list of repository viewers + * @return array + */ + protected static function getViewers() { + global $wgGitRepositoryViewers; + + if ( self::$viewers === false ) { + self::$viewers = $wgGitRepositoryViewers; + Hooks::run( 'GitViewers', [ &self::$viewers ] ); + } + + return self::$viewers; + } +} diff --git a/www/wiki/includes/GlobalFunctions.php b/www/wiki/includes/GlobalFunctions.php new file mode 100644 index 00000000..0152209d --- /dev/null +++ b/www/wiki/includes/GlobalFunctions.php @@ -0,0 +1,3444 @@ +<?php +/** + * Global functions used everywhere. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +if ( !defined( 'MEDIAWIKI' ) ) { + die( "This file is part of MediaWiki, it is not a valid entry point" ); +} + +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\ProcOpenError; +use MediaWiki\Session\SessionManager; +use MediaWiki\MediaWikiServices; +use MediaWiki\Shell\Shell; +use Wikimedia\ScopedCallback; +use Wikimedia\Rdbms\DBReplicationWaitError; + +/** + * Load an extension + * + * This queues an extension to be loaded through + * the ExtensionRegistry system. + * + * @param string $ext Name of the extension to load + * @param string|null $path Absolute path of where to find the extension.json file + * @since 1.25 + */ +function wfLoadExtension( $ext, $path = null ) { + if ( !$path ) { + global $wgExtensionDirectory; + $path = "$wgExtensionDirectory/$ext/extension.json"; + } + ExtensionRegistry::getInstance()->queue( $path ); +} + +/** + * Load multiple extensions at once + * + * Same as wfLoadExtension, but more efficient if you + * are loading multiple extensions. + * + * If you want to specify custom paths, you should interact with + * ExtensionRegistry directly. + * + * @see wfLoadExtension + * @param string[] $exts Array of extension names to load + * @since 1.25 + */ +function wfLoadExtensions( array $exts ) { + global $wgExtensionDirectory; + $registry = ExtensionRegistry::getInstance(); + foreach ( $exts as $ext ) { + $registry->queue( "$wgExtensionDirectory/$ext/extension.json" ); + } +} + +/** + * Load a skin + * + * @see wfLoadExtension + * @param string $skin Name of the extension to load + * @param string|null $path Absolute path of where to find the skin.json file + * @since 1.25 + */ +function wfLoadSkin( $skin, $path = null ) { + if ( !$path ) { + global $wgStyleDirectory; + $path = "$wgStyleDirectory/$skin/skin.json"; + } + ExtensionRegistry::getInstance()->queue( $path ); +} + +/** + * Load multiple skins at once + * + * @see wfLoadExtensions + * @param string[] $skins Array of extension names to load + * @since 1.25 + */ +function wfLoadSkins( array $skins ) { + global $wgStyleDirectory; + $registry = ExtensionRegistry::getInstance(); + foreach ( $skins as $skin ) { + $registry->queue( "$wgStyleDirectory/$skin/skin.json" ); + } +} + +/** + * Like array_diff( $a, $b ) except that it works with two-dimensional arrays. + * @param array $a + * @param array $b + * @return array + */ +function wfArrayDiff2( $a, $b ) { + return array_udiff( $a, $b, 'wfArrayDiff2_cmp' ); +} + +/** + * @param array|string $a + * @param array|string $b + * @return int + */ +function wfArrayDiff2_cmp( $a, $b ) { + if ( is_string( $a ) && is_string( $b ) ) { + return strcmp( $a, $b ); + } elseif ( count( $a ) !== count( $b ) ) { + return count( $a ) < count( $b ) ? -1 : 1; + } else { + reset( $a ); + reset( $b ); + while ( key( $a ) !== null && key( $b ) !== null ) { + $valueA = current( $a ); + $valueB = current( $b ); + $cmp = strcmp( $valueA, $valueB ); + if ( $cmp !== 0 ) { + return $cmp; + } + next( $a ); + next( $b ); + } + return 0; + } +} + +/** + * Like array_filter with ARRAY_FILTER_USE_BOTH, but works pre-5.6. + * + * @param array $arr + * @param callable $callback Will be called with the array value and key (in that order) and + * should return a bool which will determine whether the array element is kept. + * @return array + */ +function wfArrayFilter( array $arr, callable $callback ) { + if ( defined( 'ARRAY_FILTER_USE_BOTH' ) ) { + return array_filter( $arr, $callback, ARRAY_FILTER_USE_BOTH ); + } + $filteredKeys = array_filter( array_keys( $arr ), function ( $key ) use ( $arr, $callback ) { + return call_user_func( $callback, $arr[$key], $key ); + } ); + return array_intersect_key( $arr, array_fill_keys( $filteredKeys, true ) ); +} + +/** + * Like array_filter with ARRAY_FILTER_USE_KEY, but works pre-5.6. + * + * @param array $arr + * @param callable $callback Will be called with the array key and should return a bool which + * will determine whether the array element is kept. + * @return array + */ +function wfArrayFilterByKey( array $arr, callable $callback ) { + return wfArrayFilter( $arr, function ( $val, $key ) use ( $callback ) { + return call_user_func( $callback, $key ); + } ); +} + +/** + * Appends to second array if $value differs from that in $default + * + * @param string|int $key + * @param mixed $value + * @param mixed $default + * @param array &$changed Array to alter + * @throws MWException + */ +function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { + if ( is_null( $changed ) ) { + throw new MWException( 'GlobalFunctions::wfAppendToArrayIfNotDefault got null' ); + } + if ( $default[$key] !== $value ) { + $changed[$key] = $value; + } +} + +/** + * Merge arrays in the style of getUserPermissionsErrors, with duplicate removal + * e.g. + * wfMergeErrorArrays( + * [ [ 'x' ] ], + * [ [ 'x', '2' ] ], + * [ [ 'x' ] ], + * [ [ 'y' ] ] + * ); + * returns: + * [ + * [ 'x', '2' ], + * [ 'x' ], + * [ 'y' ] + * ] + * + * @param array $array1,... + * @return array + */ +function wfMergeErrorArrays( /*...*/ ) { + $args = func_get_args(); + $out = []; + foreach ( $args as $errors ) { + foreach ( $errors as $params ) { + $originalParams = $params; + if ( $params[0] instanceof MessageSpecifier ) { + $msg = $params[0]; + $params = array_merge( [ $msg->getKey() ], $msg->getParams() ); + } + # @todo FIXME: Sometimes get nested arrays for $params, + # which leads to E_NOTICEs + $spec = implode( "\t", $params ); + $out[$spec] = $originalParams; + } + } + return array_values( $out ); +} + +/** + * Insert array into another array after the specified *KEY* + * + * @param array $array The array. + * @param array $insert The array to insert. + * @param mixed $after The key to insert after + * @return array + */ +function wfArrayInsertAfter( array $array, array $insert, $after ) { + // Find the offset of the element to insert after. + $keys = array_keys( $array ); + $offsetByKey = array_flip( $keys ); + + $offset = $offsetByKey[$after]; + + // Insert at the specified offset + $before = array_slice( $array, 0, $offset + 1, true ); + $after = array_slice( $array, $offset + 1, count( $array ) - $offset, true ); + + $output = $before + $insert + $after; + + return $output; +} + +/** + * Recursively converts the parameter (an object) to an array with the same data + * + * @param object|array $objOrArray + * @param bool $recursive + * @return array + */ +function wfObjectToArray( $objOrArray, $recursive = true ) { + $array = []; + if ( is_object( $objOrArray ) ) { + $objOrArray = get_object_vars( $objOrArray ); + } + foreach ( $objOrArray as $key => $value ) { + if ( $recursive && ( is_object( $value ) || is_array( $value ) ) ) { + $value = wfObjectToArray( $value ); + } + + $array[$key] = $value; + } + + return $array; +} + +/** + * Get a random decimal value between 0 and 1, in a way + * not likely to give duplicate values for any realistic + * number of articles. + * + * @note This is designed for use in relation to Special:RandomPage + * and the page_random database field. + * + * @return string + */ +function wfRandom() { + // The maximum random value is "only" 2^31-1, so get two random + // values to reduce the chance of dupes + $max = mt_getrandmax() + 1; + $rand = number_format( ( mt_rand() * $max + mt_rand() ) / $max / $max, 12, '.', '' ); + return $rand; +} + +/** + * Get a random string containing a number of pseudo-random hex characters. + * + * @note This is not secure, if you are trying to generate some sort + * of token please use MWCryptRand instead. + * + * @param int $length The length of the string to generate + * @return string + * @since 1.20 + */ +function wfRandomString( $length = 32 ) { + $str = ''; + for ( $n = 0; $n < $length; $n += 7 ) { + $str .= sprintf( '%07x', mt_rand() & 0xfffffff ); + } + return substr( $str, 0, $length ); +} + +/** + * We want some things to be included as literal characters in our title URLs + * for prettiness, which urlencode encodes by default. According to RFC 1738, + * all of the following should be safe: + * + * ;:@&=$-_.+!*'(), + * + * RFC 1738 says ~ is unsafe, however RFC 3986 considers it an unreserved + * character which should not be encoded. More importantly, google chrome + * always converts %7E back to ~, and converting it in this function can + * cause a redirect loop (T105265). + * + * But + is not safe because it's used to indicate a space; &= are only safe in + * paths and not in queries (and we don't distinguish here); ' seems kind of + * scary; and urlencode() doesn't touch -_. to begin with. Plus, although / + * is reserved, we don't care. So the list we unescape is: + * + * ;:@$!*(),/~ + * + * However, IIS7 redirects fail when the url contains a colon (see T24709), + * so no fancy : for IIS7. + * + * %2F in the page titles seems to fatally break for some reason. + * + * @param string $s + * @return string + */ +function wfUrlencode( $s ) { + static $needle; + + if ( is_null( $s ) ) { + $needle = null; + return ''; + } + + if ( is_null( $needle ) ) { + $needle = [ '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F', '%7E' ]; + if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || + ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) + ) { + $needle[] = '%3A'; + } + } + + $s = urlencode( $s ); + $s = str_ireplace( + $needle, + [ ';', '@', '$', '!', '*', '(', ')', ',', '/', '~', ':' ], + $s + ); + + return $s; +} + +/** + * This function takes one or two arrays as input, and returns a CGI-style string, e.g. + * "days=7&limit=100". Options in the first array override options in the second. + * Options set to null or false will not be output. + * + * @param array $array1 ( String|Array ) + * @param array|null $array2 ( String|Array ) + * @param string $prefix + * @return string + */ +function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) { + if ( !is_null( $array2 ) ) { + $array1 = $array1 + $array2; + } + + $cgi = ''; + foreach ( $array1 as $key => $value ) { + if ( !is_null( $value ) && $value !== false ) { + if ( $cgi != '' ) { + $cgi .= '&'; + } + if ( $prefix !== '' ) { + $key = $prefix . "[$key]"; + } + if ( is_array( $value ) ) { + $firstTime = true; + foreach ( $value as $k => $v ) { + $cgi .= $firstTime ? '' : '&'; + if ( is_array( $v ) ) { + $cgi .= wfArrayToCgi( $v, null, $key . "[$k]" ); + } else { + $cgi .= urlencode( $key . "[$k]" ) . '=' . urlencode( $v ); + } + $firstTime = false; + } + } else { + if ( is_object( $value ) ) { + $value = $value->__toString(); + } + $cgi .= urlencode( $key ) . '=' . urlencode( $value ); + } + } + } + return $cgi; +} + +/** + * This is the logical opposite of wfArrayToCgi(): it accepts a query string as + * its argument and returns the same string in array form. This allows compatibility + * with legacy functions that accept raw query strings instead of nice + * arrays. Of course, keys and values are urldecode()d. + * + * @param string $query Query string + * @return string[] Array version of input + */ +function wfCgiToArray( $query ) { + if ( isset( $query[0] ) && $query[0] == '?' ) { + $query = substr( $query, 1 ); + } + $bits = explode( '&', $query ); + $ret = []; + foreach ( $bits as $bit ) { + if ( $bit === '' ) { + continue; + } + if ( strpos( $bit, '=' ) === false ) { + // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does) + $key = $bit; + $value = ''; + } else { + list( $key, $value ) = explode( '=', $bit ); + } + $key = urldecode( $key ); + $value = urldecode( $value ); + if ( strpos( $key, '[' ) !== false ) { + $keys = array_reverse( explode( '[', $key ) ); + $key = array_pop( $keys ); + $temp = $value; + foreach ( $keys as $k ) { + $k = substr( $k, 0, -1 ); + $temp = [ $k => $temp ]; + } + if ( isset( $ret[$key] ) ) { + $ret[$key] = array_merge( $ret[$key], $temp ); + } else { + $ret[$key] = $temp; + } + } else { + $ret[$key] = $value; + } + } + return $ret; +} + +/** + * Append a query string to an existing URL, which may or may not already + * have query string parameters already. If so, they will be combined. + * + * @param string $url + * @param string|string[] $query String or associative array + * @return string + */ +function wfAppendQuery( $url, $query ) { + if ( is_array( $query ) ) { + $query = wfArrayToCgi( $query ); + } + if ( $query != '' ) { + // Remove the fragment, if there is one + $fragment = false; + $hashPos = strpos( $url, '#' ); + if ( $hashPos !== false ) { + $fragment = substr( $url, $hashPos ); + $url = substr( $url, 0, $hashPos ); + } + + // Add parameter + if ( false === strpos( $url, '?' ) ) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= $query; + + // Put the fragment back + if ( $fragment !== false ) { + $url .= $fragment; + } + } + return $url; +} + +/** + * Expand a potentially local URL to a fully-qualified URL. Assumes $wgServer + * is correct. + * + * The meaning of the PROTO_* constants is as follows: + * PROTO_HTTP: Output a URL starting with http:// + * PROTO_HTTPS: Output a URL starting with https:// + * PROTO_RELATIVE: Output a URL starting with // (protocol-relative URL) + * PROTO_CURRENT: Output a URL starting with either http:// or https:// , depending + * on which protocol was used for the current incoming request + * PROTO_CANONICAL: For URLs without a domain, like /w/index.php , use $wgCanonicalServer. + * For protocol-relative URLs, use the protocol of $wgCanonicalServer + * PROTO_INTERNAL: Like PROTO_CANONICAL, but uses $wgInternalServer instead of $wgCanonicalServer + * + * @todo this won't work with current-path-relative URLs + * like "subdir/foo.html", etc. + * + * @param string $url Either fully-qualified or a local path + query + * @param string|int|null $defaultProto One of the PROTO_* constants. Determines the + * protocol to use if $url or $wgServer is protocol-relative + * @return string|false Fully-qualified URL, current-path-relative URL or false if + * no valid URL can be constructed + */ +function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { + global $wgServer, $wgCanonicalServer, $wgInternalServer, $wgRequest, + $wgHttpsPort; + if ( $defaultProto === PROTO_CANONICAL ) { + $serverUrl = $wgCanonicalServer; + } elseif ( $defaultProto === PROTO_INTERNAL && $wgInternalServer !== false ) { + // Make $wgInternalServer fall back to $wgServer if not set + $serverUrl = $wgInternalServer; + } else { + $serverUrl = $wgServer; + if ( $defaultProto === PROTO_CURRENT ) { + $defaultProto = $wgRequest->getProtocol() . '://'; + } + } + + // Analyze $serverUrl to obtain its protocol + $bits = wfParseUrl( $serverUrl ); + $serverHasProto = $bits && $bits['scheme'] != ''; + + if ( $defaultProto === PROTO_CANONICAL || $defaultProto === PROTO_INTERNAL ) { + if ( $serverHasProto ) { + $defaultProto = $bits['scheme'] . '://'; + } else { + // $wgCanonicalServer or $wgInternalServer doesn't have a protocol. + // This really isn't supposed to happen. Fall back to HTTP in this + // ridiculous case. + $defaultProto = PROTO_HTTP; + } + } + + $defaultProtoWithoutSlashes = substr( $defaultProto, 0, -2 ); + + if ( substr( $url, 0, 2 ) == '//' ) { + $url = $defaultProtoWithoutSlashes . $url; + } elseif ( substr( $url, 0, 1 ) == '/' ) { + // If $serverUrl is protocol-relative, prepend $defaultProtoWithoutSlashes, + // otherwise leave it alone. + $url = ( $serverHasProto ? '' : $defaultProtoWithoutSlashes ) . $serverUrl . $url; + } + + $bits = wfParseUrl( $url ); + + // ensure proper port for HTTPS arrives in URL + // https://phabricator.wikimedia.org/T67184 + if ( $defaultProto === PROTO_HTTPS && $wgHttpsPort != 443 ) { + $bits['port'] = $wgHttpsPort; + } + + if ( $bits && isset( $bits['path'] ) ) { + $bits['path'] = wfRemoveDotSegments( $bits['path'] ); + return wfAssembleUrl( $bits ); + } elseif ( $bits ) { + # No path to expand + return $url; + } elseif ( substr( $url, 0, 1 ) != '/' ) { + # URL is a relative path + return wfRemoveDotSegments( $url ); + } + + # Expanded URL is not valid. + return false; +} + +/** + * This function will reassemble a URL parsed with wfParseURL. This is useful + * if you need to edit part of a URL and put it back together. + * + * This is the basic structure used (brackets contain keys for $urlParts): + * [scheme][delimiter][user]:[pass]@[host]:[port][path]?[query]#[fragment] + * + * @todo Need to integrate this into wfExpandUrl (see T34168) + * + * @since 1.19 + * @param array $urlParts URL parts, as output from wfParseUrl + * @return string URL assembled from its component parts + */ +function wfAssembleUrl( $urlParts ) { + $result = ''; + + if ( isset( $urlParts['delimiter'] ) ) { + if ( isset( $urlParts['scheme'] ) ) { + $result .= $urlParts['scheme']; + } + + $result .= $urlParts['delimiter']; + } + + if ( isset( $urlParts['host'] ) ) { + if ( isset( $urlParts['user'] ) ) { + $result .= $urlParts['user']; + if ( isset( $urlParts['pass'] ) ) { + $result .= ':' . $urlParts['pass']; + } + $result .= '@'; + } + + $result .= $urlParts['host']; + + if ( isset( $urlParts['port'] ) ) { + $result .= ':' . $urlParts['port']; + } + } + + if ( isset( $urlParts['path'] ) ) { + $result .= $urlParts['path']; + } + + if ( isset( $urlParts['query'] ) ) { + $result .= '?' . $urlParts['query']; + } + + if ( isset( $urlParts['fragment'] ) ) { + $result .= '#' . $urlParts['fragment']; + } + + return $result; +} + +/** + * Remove all dot-segments in the provided URL path. For example, + * '/a/./b/../c/' becomes '/a/c/'. For details on the algorithm, please see + * RFC3986 section 5.2.4. + * + * @todo Need to integrate this into wfExpandUrl (see T34168) + * + * @param string $urlPath URL path, potentially containing dot-segments + * @return string URL path with all dot-segments removed + */ +function wfRemoveDotSegments( $urlPath ) { + $output = ''; + $inputOffset = 0; + $inputLength = strlen( $urlPath ); + + while ( $inputOffset < $inputLength ) { + $prefixLengthOne = substr( $urlPath, $inputOffset, 1 ); + $prefixLengthTwo = substr( $urlPath, $inputOffset, 2 ); + $prefixLengthThree = substr( $urlPath, $inputOffset, 3 ); + $prefixLengthFour = substr( $urlPath, $inputOffset, 4 ); + $trimOutput = false; + + if ( $prefixLengthTwo == './' ) { + # Step A, remove leading "./" + $inputOffset += 2; + } elseif ( $prefixLengthThree == '../' ) { + # Step A, remove leading "../" + $inputOffset += 3; + } elseif ( ( $prefixLengthTwo == '/.' ) && ( $inputOffset + 2 == $inputLength ) ) { + # Step B, replace leading "/.$" with "/" + $inputOffset += 1; + $urlPath[$inputOffset] = '/'; + } elseif ( $prefixLengthThree == '/./' ) { + # Step B, replace leading "/./" with "/" + $inputOffset += 2; + } elseif ( $prefixLengthThree == '/..' && ( $inputOffset + 3 == $inputLength ) ) { + # Step C, replace leading "/..$" with "/" and + # remove last path component in output + $inputOffset += 2; + $urlPath[$inputOffset] = '/'; + $trimOutput = true; + } elseif ( $prefixLengthFour == '/../' ) { + # Step C, replace leading "/../" with "/" and + # remove last path component in output + $inputOffset += 3; + $trimOutput = true; + } elseif ( ( $prefixLengthOne == '.' ) && ( $inputOffset + 1 == $inputLength ) ) { + # Step D, remove "^.$" + $inputOffset += 1; + } elseif ( ( $prefixLengthTwo == '..' ) && ( $inputOffset + 2 == $inputLength ) ) { + # Step D, remove "^..$" + $inputOffset += 2; + } else { + # Step E, move leading path segment to output + if ( $prefixLengthOne == '/' ) { + $slashPos = strpos( $urlPath, '/', $inputOffset + 1 ); + } else { + $slashPos = strpos( $urlPath, '/', $inputOffset ); + } + if ( $slashPos === false ) { + $output .= substr( $urlPath, $inputOffset ); + $inputOffset = $inputLength; + } else { + $output .= substr( $urlPath, $inputOffset, $slashPos - $inputOffset ); + $inputOffset += $slashPos - $inputOffset; + } + } + + if ( $trimOutput ) { + $slashPos = strrpos( $output, '/' ); + if ( $slashPos === false ) { + $output = ''; + } else { + $output = substr( $output, 0, $slashPos ); + } + } + } + + return $output; +} + +/** + * Returns a regular expression of url protocols + * + * @param bool $includeProtocolRelative If false, remove '//' from the returned protocol list. + * DO NOT USE this directly, use wfUrlProtocolsWithoutProtRel() instead + * @return string + */ +function wfUrlProtocols( $includeProtocolRelative = true ) { + global $wgUrlProtocols; + + // Cache return values separately based on $includeProtocolRelative + static $withProtRel = null, $withoutProtRel = null; + $cachedValue = $includeProtocolRelative ? $withProtRel : $withoutProtRel; + if ( !is_null( $cachedValue ) ) { + return $cachedValue; + } + + // Support old-style $wgUrlProtocols strings, for backwards compatibility + // with LocalSettings files from 1.5 + if ( is_array( $wgUrlProtocols ) ) { + $protocols = []; + foreach ( $wgUrlProtocols as $protocol ) { + // Filter out '//' if !$includeProtocolRelative + if ( $includeProtocolRelative || $protocol !== '//' ) { + $protocols[] = preg_quote( $protocol, '/' ); + } + } + + $retval = implode( '|', $protocols ); + } else { + // Ignore $includeProtocolRelative in this case + // This case exists for pre-1.6 compatibility, and we can safely assume + // that '//' won't appear in a pre-1.6 config because protocol-relative + // URLs weren't supported until 1.18 + $retval = $wgUrlProtocols; + } + + // Cache return value + if ( $includeProtocolRelative ) { + $withProtRel = $retval; + } else { + $withoutProtRel = $retval; + } + return $retval; +} + +/** + * Like wfUrlProtocols(), but excludes '//' from the protocol list. Use this if + * you need a regex that matches all URL protocols but does not match protocol- + * relative URLs + * @return string + */ +function wfUrlProtocolsWithoutProtRel() { + return wfUrlProtocols( false ); +} + +/** + * parse_url() work-alike, but non-broken. Differences: + * + * 1) Does not raise warnings on bad URLs (just returns false). + * 2) Handles protocols that don't use :// (e.g., mailto: and news:, as well as + * protocol-relative URLs) correctly. + * 3) Adds a "delimiter" element to the array (see (2)). + * 4) Verifies that the protocol is on the $wgUrlProtocols whitelist. + * 5) Rejects some invalid URLs that parse_url doesn't, e.g. the empty string or URLs starting with + * a line feed character. + * + * @param string $url A URL to parse + * @return string[]|bool Bits of the URL in an associative array, or false on failure. + * Possible fields: + * - scheme: URI scheme (protocol), e.g. 'http', 'mailto'. Lowercase, always present, but can + * be an empty string for protocol-relative URLs. + * - delimiter: either '://', ':' or '//'. Always present. + * - host: domain name / IP. Always present, but could be an empty string, e.g. for file: URLs. + * - user: user name, e.g. for HTTP Basic auth URLs such as http://user:pass@example.com/ + * Missing when there is no username. + * - pass: password, same as above. + * - path: path including the leading /. Will be missing when empty (e.g. 'http://example.com') + * - query: query string (as a string; see wfCgiToArray() for parsing it), can be missing. + * - fragment: the part after #, can be missing. + */ +function wfParseUrl( $url ) { + global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php + + // Protocol-relative URLs are handled really badly by parse_url(). It's so + // bad that the easiest way to handle them is to just prepend 'http:' and + // strip the protocol out later. + $wasRelative = substr( $url, 0, 2 ) == '//'; + if ( $wasRelative ) { + $url = "http:$url"; + } + Wikimedia\suppressWarnings(); + $bits = parse_url( $url ); + Wikimedia\restoreWarnings(); + // parse_url() returns an array without scheme for some invalid URLs, e.g. + // parse_url("%0Ahttp://example.com") == [ 'host' => '%0Ahttp', 'path' => 'example.com' ] + if ( !$bits || !isset( $bits['scheme'] ) ) { + return false; + } + + // parse_url() incorrectly handles schemes case-sensitively. Convert it to lowercase. + $bits['scheme'] = strtolower( $bits['scheme'] ); + + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it + if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) { + $bits['delimiter'] = '://'; + } elseif ( in_array( $bits['scheme'] . ':', $wgUrlProtocols ) ) { + $bits['delimiter'] = ':'; + // parse_url detects for news: and mailto: the host part of an url as path + // We have to correct this wrong detection + if ( isset( $bits['path'] ) ) { + $bits['host'] = $bits['path']; + $bits['path'] = ''; + } + } else { + return false; + } + + /* Provide an empty host for eg. file:/// urls (see T30627) */ + if ( !isset( $bits['host'] ) ) { + $bits['host'] = ''; + + // See T47069 + if ( isset( $bits['path'] ) ) { + /* parse_url loses the third / for file:///c:/ urls (but not on variants) */ + if ( substr( $bits['path'], 0, 1 ) !== '/' ) { + $bits['path'] = '/' . $bits['path']; + } + } else { + $bits['path'] = ''; + } + } + + // If the URL was protocol-relative, fix scheme and delimiter + if ( $wasRelative ) { + $bits['scheme'] = ''; + $bits['delimiter'] = '//'; + } + return $bits; +} + +/** + * Take a URL, make sure it's expanded to fully qualified, and replace any + * encoded non-ASCII Unicode characters with their UTF-8 original forms + * for more compact display and legibility for local audiences. + * + * @todo handle punycode domains too + * + * @param string $url + * @return string + */ +function wfExpandIRI( $url ) { + return preg_replace_callback( + '/((?:%[89A-F][0-9A-F])+)/i', + 'wfExpandIRI_callback', + wfExpandUrl( $url ) + ); +} + +/** + * Private callback for wfExpandIRI + * @param array $matches + * @return string + */ +function wfExpandIRI_callback( $matches ) { + return urldecode( $matches[1] ); +} + +/** + * Make URL indexes, appropriate for the el_index field of externallinks. + * + * @param string $url + * @return array + */ +function wfMakeUrlIndexes( $url ) { + $bits = wfParseUrl( $url ); + + // Reverse the labels in the hostname, convert to lower case + // For emails reverse domainpart only + if ( $bits['scheme'] == 'mailto' ) { + $mailparts = explode( '@', $bits['host'], 2 ); + if ( count( $mailparts ) === 2 ) { + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + } else { + // No domain specified, don't mangle it + $domainpart = ''; + } + $reversedHost = $domainpart . '@' . $mailparts[0]; + } else { + $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + } + // Add an extra dot to the end + // Why? Is it in wrong place in mailto links? + if ( substr( $reversedHost, -1, 1 ) !== '.' ) { + $reversedHost .= '.'; + } + // Reconstruct the pseudo-URL + $prot = $bits['scheme']; + $index = $prot . $bits['delimiter'] . $reversedHost; + // Leave out user and password. Add the port, path, query and fragment + if ( isset( $bits['port'] ) ) { + $index .= ':' . $bits['port']; + } + if ( isset( $bits['path'] ) ) { + $index .= $bits['path']; + } else { + $index .= '/'; + } + if ( isset( $bits['query'] ) ) { + $index .= '?' . $bits['query']; + } + if ( isset( $bits['fragment'] ) ) { + $index .= '#' . $bits['fragment']; + } + + if ( $prot == '' ) { + return [ "http:$index", "https:$index" ]; + } else { + return [ $index ]; + } +} + +/** + * Check whether a given URL has a domain that occurs in a given set of domains + * @param string $url + * @param array $domains Array of domains (strings) + * @return bool True if the host part of $url ends in one of the strings in $domains + */ +function wfMatchesDomainList( $url, $domains ) { + $bits = wfParseUrl( $url ); + if ( is_array( $bits ) && isset( $bits['host'] ) ) { + $host = '.' . $bits['host']; + foreach ( (array)$domains as $domain ) { + $domain = '.' . $domain; + if ( substr( $host, -strlen( $domain ) ) === $domain ) { + return true; + } + } + } + return false; +} + +/** + * Sends a line to the debug log if enabled or, optionally, to a comment in output. + * In normal operation this is a NOP. + * + * Controlling globals: + * $wgDebugLogFile - points to the log file + * $wgDebugRawPage - if false, 'action=raw' hits will not result in debug output. + * $wgDebugComments - if on, some debug items may appear in comments in the HTML output. + * + * @since 1.25 support for additional context data + * + * @param string $text + * @param string|bool $dest Destination of the message: + * - 'all': both to the log and HTML (debug toolbar or HTML comments) + * - 'private': excluded from HTML output + * For backward compatibility, it can also take a boolean: + * - true: same as 'all' + * - false: same as 'private' + * @param array $context Additional logging context data + */ +function wfDebug( $text, $dest = 'all', array $context = [] ) { + global $wgDebugRawPage, $wgDebugLogPrefix; + global $wgDebugTimestamps; + + if ( !$wgDebugRawPage && wfIsDebugRawPage() ) { + return; + } + + $text = trim( $text ); + + if ( $wgDebugTimestamps ) { + $context['seconds_elapsed'] = sprintf( + '%6.4f', + microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'] + ); + $context['memory_used'] = sprintf( + '%5.1fM', + ( memory_get_usage( true ) / ( 1024 * 1024 ) ) + ); + } + + if ( $wgDebugLogPrefix !== '' ) { + $context['prefix'] = $wgDebugLogPrefix; + } + $context['private'] = ( $dest === false || $dest === 'private' ); + + $logger = LoggerFactory::getInstance( 'wfDebug' ); + $logger->debug( $text, $context ); +} + +/** + * Returns true if debug logging should be suppressed if $wgDebugRawPage = false + * @return bool + */ +function wfIsDebugRawPage() { + static $cache; + if ( $cache !== null ) { + return $cache; + } + # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet + if ( ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' ) + || ( + isset( $_SERVER['SCRIPT_NAME'] ) + && substr( $_SERVER['SCRIPT_NAME'], -8 ) == 'load.php' + ) + ) { + $cache = true; + } else { + $cache = false; + } + return $cache; +} + +/** + * Send a line giving PHP memory usage. + * + * @param bool $exact Print exact byte values instead of kibibytes (default: false) + */ +function wfDebugMem( $exact = false ) { + $mem = memory_get_usage(); + if ( !$exact ) { + $mem = floor( $mem / 1024 ) . ' KiB'; + } else { + $mem .= ' B'; + } + wfDebug( "Memory usage: $mem\n" ); +} + +/** + * Send a line to a supplementary debug log file, if configured, or main debug + * log if not. + * + * To configure a supplementary log file, set $wgDebugLogGroups[$logGroup] to + * a string filename or an associative array mapping 'destination' to the + * desired filename. The associative array may also contain a 'sample' key + * with an integer value, specifying a sampling factor. Sampled log events + * will be emitted with a 1 in N random chance. + * + * @since 1.23 support for sampling log messages via $wgDebugLogGroups. + * @since 1.25 support for additional context data + * @since 1.25 sample behavior dependent on configured $wgMWLoggerDefaultSpi + * + * @param string $logGroup + * @param string $text + * @param string|bool $dest Destination of the message: + * - 'all': both to the log and HTML (debug toolbar or HTML comments) + * - 'private': only to the specific log if set in $wgDebugLogGroups and + * discarded otherwise + * For backward compatibility, it can also take a boolean: + * - true: same as 'all' + * - false: same as 'private' + * @param array $context Additional logging context data + */ +function wfDebugLog( + $logGroup, $text, $dest = 'all', array $context = [] +) { + $text = trim( $text ); + + $logger = LoggerFactory::getInstance( $logGroup ); + $context['private'] = ( $dest === false || $dest === 'private' ); + $logger->info( $text, $context ); +} + +/** + * Log for database errors + * + * @since 1.25 support for additional context data + * + * @param string $text Database error message. + * @param array $context Additional logging context data + */ +function wfLogDBError( $text, array $context = [] ) { + $logger = LoggerFactory::getInstance( 'wfLogDBError' ); + $logger->error( trim( $text ), $context ); +} + +/** + * Throws a warning that $function is deprecated + * + * @param string $function + * @param string|bool $version Version of MediaWiki that the function + * was deprecated in (Added in 1.19). + * @param string|bool $component Added in 1.19. + * @param int $callerOffset How far up the call stack is the original + * caller. 2 = function that called the function that called + * wfDeprecated (Added in 1.20) + * + * @return null + */ +function wfDeprecated( $function, $version = false, $component = false, $callerOffset = 2 ) { + MWDebug::deprecated( $function, $version, $component, $callerOffset + 1 ); +} + +/** + * Send a warning either to the debug log or in a PHP error depending on + * $wgDevelopmentWarnings. To log warnings in production, use wfLogWarning() instead. + * + * @param string $msg Message to send + * @param int $callerOffset Number of items to go back in the backtrace to + * find the correct caller (1 = function calling wfWarn, ...) + * @param int $level PHP error level; defaults to E_USER_NOTICE; + * only used when $wgDevelopmentWarnings is true + */ +function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { + MWDebug::warning( $msg, $callerOffset + 1, $level, 'auto' ); +} + +/** + * Send a warning as a PHP error and the debug log. This is intended for logging + * warnings in production. For logging development warnings, use WfWarn instead. + * + * @param string $msg Message to send + * @param int $callerOffset Number of items to go back in the backtrace to + * find the correct caller (1 = function calling wfLogWarning, ...) + * @param int $level PHP error level; defaults to E_USER_WARNING + */ +function wfLogWarning( $msg, $callerOffset = 1, $level = E_USER_WARNING ) { + MWDebug::warning( $msg, $callerOffset + 1, $level, 'production' ); +} + +/** + * Log to a file without getting "file size exceeded" signals. + * + * Can also log to TCP or UDP with the syntax udp://host:port/prefix. This will + * send lines to the specified port, prefixed by the specified prefix and a space. + * @since 1.25 support for additional context data + * + * @param string $text + * @param string $file Filename + * @param array $context Additional logging context data + * @throws MWException + * @deprecated since 1.25 Use \MediaWiki\Logger\LegacyLogger::emit or UDPTransport + */ +function wfErrorLog( $text, $file, array $context = [] ) { + wfDeprecated( __METHOD__, '1.25' ); + $logger = LoggerFactory::getInstance( 'wfErrorLog' ); + $context['destination'] = $file; + $logger->info( trim( $text ), $context ); +} + +/** + * @todo document + * @todo Move logic to MediaWiki.php + */ +function wfLogProfilingData() { + global $wgDebugLogGroups, $wgDebugRawPage; + + $context = RequestContext::getMain(); + $request = $context->getRequest(); + + $profiler = Profiler::instance(); + $profiler->setContext( $context ); + $profiler->logData(); + + // Send out any buffered statsd metrics as needed + MediaWiki::emitBufferedStatsdData( + MediaWikiServices::getInstance()->getStatsdDataFactory(), + $context->getConfig() + ); + + // Profiling must actually be enabled... + if ( $profiler instanceof ProfilerStub ) { + return; + } + + if ( isset( $wgDebugLogGroups['profileoutput'] ) + && $wgDebugLogGroups['profileoutput'] === false + ) { + // Explicitly disabled + return; + } + if ( !$wgDebugRawPage && wfIsDebugRawPage() ) { + return; + } + + $ctx = [ 'elapsed' => $request->getElapsedTime() ]; + if ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + $ctx['forwarded_for'] = $_SERVER['HTTP_X_FORWARDED_FOR']; + } + if ( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { + $ctx['client_ip'] = $_SERVER['HTTP_CLIENT_IP']; + } + if ( !empty( $_SERVER['HTTP_FROM'] ) ) { + $ctx['from'] = $_SERVER['HTTP_FROM']; + } + if ( isset( $ctx['forwarded_for'] ) || + isset( $ctx['client_ip'] ) || + isset( $ctx['from'] ) ) { + $ctx['proxy'] = $_SERVER['REMOTE_ADDR']; + } + + // Don't load $wgUser at this late stage just for statistics purposes + // @todo FIXME: We can detect some anons even if it is not loaded. + // See User::getId() + $user = $context->getUser(); + $ctx['anon'] = $user->isItemLoaded( 'id' ) && $user->isAnon(); + + // Command line script uses a FauxRequest object which does not have + // any knowledge about an URL and throw an exception instead. + try { + $ctx['url'] = urldecode( $request->getRequestURL() ); + } catch ( Exception $ignored ) { + // no-op + } + + $ctx['output'] = $profiler->getOutput(); + + $log = LoggerFactory::getInstance( 'profileoutput' ); + $log->info( "Elapsed: {elapsed}; URL: <{url}>\n{output}", $ctx ); +} + +/** + * Increment a statistics counter + * + * @param string $key + * @param int $count + * @return void + */ +function wfIncrStats( $key, $count = 1 ) { + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); + $stats->updateCount( $key, $count ); +} + +/** + * Check whether the wiki is in read-only mode. + * + * @return bool + */ +function wfReadOnly() { + return MediaWikiServices::getInstance()->getReadOnlyMode() + ->isReadOnly(); +} + +/** + * Check if the site is in read-only mode and return the message if so + * + * This checks wfConfiguredReadOnlyReason() and the main load balancer + * for replica DB lag. This may result in DB connection being made. + * + * @return string|bool String when in read-only mode; false otherwise + */ +function wfReadOnlyReason() { + return MediaWikiServices::getInstance()->getReadOnlyMode() + ->getReason(); +} + +/** + * Get the value of $wgReadOnly or the contents of $wgReadOnlyFile. + * + * @return string|bool String when in read-only mode; false otherwise + * @since 1.27 + */ +function wfConfiguredReadOnlyReason() { + return MediaWikiServices::getInstance()->getConfiguredReadOnlyMode() + ->getReason(); +} + +/** + * Return a Language object from $langcode + * + * @param Language|string|bool $langcode Either: + * - a Language object + * - code of the language to get the message for, if it is + * a valid code create a language for that language, if + * it is a string but not a valid code then make a basic + * language object + * - a boolean: if it's false then use the global object for + * the current user's language (as a fallback for the old parameter + * functionality), or if it is true then use global object + * for the wiki's content language. + * @return Language + */ +function wfGetLangObj( $langcode = false ) { + # Identify which language to get or create a language object for. + # Using is_object here due to Stub objects. + if ( is_object( $langcode ) ) { + # Great, we already have the object (hopefully)! + return $langcode; + } + + global $wgContLang, $wgLanguageCode; + if ( $langcode === true || $langcode === $wgLanguageCode ) { + # $langcode is the language code of the wikis content language object. + # or it is a boolean and value is true + return $wgContLang; + } + + global $wgLang; + if ( $langcode === false || $langcode === $wgLang->getCode() ) { + # $langcode is the language code of user language object. + # or it was a boolean and value is false + return $wgLang; + } + + $validCodes = array_keys( Language::fetchLanguageNames() ); + if ( in_array( $langcode, $validCodes ) ) { + # $langcode corresponds to a valid language. + return Language::factory( $langcode ); + } + + # $langcode is a string, but not a valid language code; use content language. + wfDebug( "Invalid language code passed to wfGetLangObj, falling back to content language.\n" ); + return $wgContLang; +} + +/** + * This is the function for getting translated interface messages. + * + * @see Message class for documentation how to use them. + * @see https://www.mediawiki.org/wiki/Manual:Messages_API + * + * This function replaces all old wfMsg* functions. + * + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, or a MessageSpecifier + * @param mixed $params,... Normal message parameters + * @return Message + * + * @since 1.17 + * + * @see Message::__construct + */ +function wfMessage( $key /*...*/ ) { + $message = new Message( $key ); + + // We call Message::params() to reduce code duplication + $params = func_get_args(); + array_shift( $params ); + if ( $params ) { + call_user_func_array( [ $message, 'params' ], $params ); + } + + return $message; +} + +/** + * This function accepts multiple message keys and returns a message instance + * for the first message which is non-empty. If all messages are empty then an + * instance of the first message key is returned. + * + * @param string|string[] $keys,... Message keys + * @return Message + * + * @since 1.18 + * + * @see Message::newFallbackSequence + */ +function wfMessageFallback( /*...*/ ) { + $args = func_get_args(); + return call_user_func_array( 'Message::newFallbackSequence', $args ); +} + +/** + * Replace message parameter keys on the given formatted output. + * + * @param string $message + * @param array $args + * @return string + * @private + */ +function wfMsgReplaceArgs( $message, $args ) { + # Fix windows line-endings + # Some messages are split with explode("\n", $msg) + $message = str_replace( "\r", '', $message ); + + // Replace arguments + if ( is_array( $args ) && $args ) { + if ( is_array( $args[0] ) ) { + $args = array_values( $args[0] ); + } + $replacementKeys = []; + foreach ( $args as $n => $param ) { + $replacementKeys['$' . ( $n + 1 )] = $param; + } + $message = strtr( $message, $replacementKeys ); + } + + return $message; +} + +/** + * Fetch server name for use in error reporting etc. + * Use real server name if available, so we know which machine + * in a server farm generated the current page. + * + * @return string + */ +function wfHostname() { + static $host; + if ( is_null( $host ) ) { + # Hostname overriding + global $wgOverrideHostname; + if ( $wgOverrideHostname !== false ) { + # Set static and skip any detection + $host = $wgOverrideHostname; + return $host; + } + + if ( function_exists( 'posix_uname' ) ) { + // This function not present on Windows + $uname = posix_uname(); + } else { + $uname = false; + } + if ( is_array( $uname ) && isset( $uname['nodename'] ) ) { + $host = $uname['nodename']; + } elseif ( getenv( 'COMPUTERNAME' ) ) { + # Windows computer name + $host = getenv( 'COMPUTERNAME' ); + } else { + # This may be a virtual server. + $host = $_SERVER['SERVER_NAME']; + } + } + return $host; +} + +/** + * Returns a script tag that stores the amount of time it took MediaWiki to + * handle the request in milliseconds as 'wgBackendResponseTime'. + * + * If $wgShowHostnames is true, the script will also set 'wgHostname' to the + * hostname of the server handling the request. + * + * @return string + */ +function wfReportTime() { + global $wgShowHostnames; + + $elapsed = ( microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'] ); + // seconds to milliseconds + $responseTime = round( $elapsed * 1000 ); + $reportVars = [ 'wgBackendResponseTime' => $responseTime ]; + if ( $wgShowHostnames ) { + $reportVars['wgHostname'] = wfHostname(); + } + return Skin::makeVariablesScript( $reportVars ); +} + +/** + * Safety wrapper for debug_backtrace(). + * + * Will return an empty array if debug_backtrace is disabled, otherwise + * the output from debug_backtrace() (trimmed). + * + * @param int $limit This parameter can be used to limit the number of stack frames returned + * + * @return array Array of backtrace information + */ +function wfDebugBacktrace( $limit = 0 ) { + static $disabled = null; + + if ( is_null( $disabled ) ) { + $disabled = !function_exists( 'debug_backtrace' ); + if ( $disabled ) { + wfDebug( "debug_backtrace() is disabled\n" ); + } + } + if ( $disabled ) { + return []; + } + + if ( $limit ) { + return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit + 1 ), 1 ); + } else { + return array_slice( debug_backtrace(), 1 ); + } +} + +/** + * Get a debug backtrace as a string + * + * @param bool|null $raw If true, the return value is plain text. If false, HTML. + * Defaults to $wgCommandLineMode if unset. + * @return string + * @since 1.25 Supports $raw parameter. + */ +function wfBacktrace( $raw = null ) { + global $wgCommandLineMode; + + if ( $raw === null ) { + $raw = $wgCommandLineMode; + } + + if ( $raw ) { + $frameFormat = "%s line %s calls %s()\n"; + $traceFormat = "%s"; + } else { + $frameFormat = "<li>%s line %s calls %s()</li>\n"; + $traceFormat = "<ul>\n%s</ul>\n"; + } + + $frames = array_map( function ( $frame ) use ( $frameFormat ) { + $file = !empty( $frame['file'] ) ? basename( $frame['file'] ) : '-'; + $line = isset( $frame['line'] ) ? $frame['line'] : '-'; + $call = $frame['function']; + if ( !empty( $frame['class'] ) ) { + $call = $frame['class'] . $frame['type'] . $call; + } + return sprintf( $frameFormat, $file, $line, $call ); + }, wfDebugBacktrace() ); + + return sprintf( $traceFormat, implode( '', $frames ) ); +} + +/** + * Get the name of the function which called this function + * wfGetCaller( 1 ) is the function with the wfGetCaller() call (ie. __FUNCTION__) + * wfGetCaller( 2 ) [default] is the caller of the function running wfGetCaller() + * wfGetCaller( 3 ) is the parent of that. + * + * @param int $level + * @return string + */ +function wfGetCaller( $level = 2 ) { + $backtrace = wfDebugBacktrace( $level + 1 ); + if ( isset( $backtrace[$level] ) ) { + return wfFormatStackFrame( $backtrace[$level] ); + } else { + return 'unknown'; + } +} + +/** + * Return a string consisting of callers in the stack. Useful sometimes + * for profiling specific points. + * + * @param int $limit The maximum depth of the stack frame to return, or false for the entire stack. + * @return string + */ +function wfGetAllCallers( $limit = 3 ) { + $trace = array_reverse( wfDebugBacktrace() ); + if ( !$limit || $limit > count( $trace ) - 1 ) { + $limit = count( $trace ) - 1; + } + $trace = array_slice( $trace, -$limit - 1, $limit ); + return implode( '/', array_map( 'wfFormatStackFrame', $trace ) ); +} + +/** + * Return a string representation of frame + * + * @param array $frame + * @return string + */ +function wfFormatStackFrame( $frame ) { + if ( !isset( $frame['function'] ) ) { + return 'NO_FUNCTION_GIVEN'; + } + return isset( $frame['class'] ) && isset( $frame['type'] ) ? + $frame['class'] . $frame['type'] . $frame['function'] : + $frame['function']; +} + +/* Some generic result counters, pulled out of SearchEngine */ + +/** + * @todo document + * + * @param int $offset + * @param int $limit + * @return string + */ +function wfShowingResults( $offset, $limit ) { + return wfMessage( 'showingresults' )->numParams( $limit, $offset + 1 )->parse(); +} + +/** + * Whether the client accept gzip encoding + * + * Uses the Accept-Encoding header to check if the client supports gzip encoding. + * Use this when considering to send a gzip-encoded response to the client. + * + * @param bool $force Forces another check even if we already have a cached result. + * @return bool + */ +function wfClientAcceptsGzip( $force = false ) { + static $result = null; + if ( $result === null || $force ) { + $result = false; + if ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) { + # @todo FIXME: We may want to blacklist some broken browsers + $m = []; + if ( preg_match( + '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', + $_SERVER['HTTP_ACCEPT_ENCODING'], + $m + ) + ) { + if ( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) { + $result = false; + return $result; + } + wfDebug( "wfClientAcceptsGzip: client accepts gzip.\n" ); + $result = true; + } + } + } + return $result; +} + +/** + * Escapes the given text so that it may be output using addWikiText() + * without any linking, formatting, etc. making its way through. This + * is achieved by substituting certain characters with HTML entities. + * As required by the callers, "<nowiki>" is not used. + * + * @param string $text Text to be escaped + * @return string + */ +function wfEscapeWikiText( $text ) { + global $wgEnableMagicLinks; + static $repl = null, $repl2 = null; + if ( $repl === null || defined( 'MW_PARSER_TEST' ) || defined( 'MW_PHPUNIT_TEST' ) ) { + // Tests depend upon being able to change $wgEnableMagicLinks, so don't cache + // in those situations + $repl = [ + '"' => '"', '&' => '&', "'" => ''', '<' => '<', + '=' => '=', '>' => '>', '[' => '[', ']' => ']', + '{' => '{', '|' => '|', '}' => '}', ';' => ';', + "\n#" => "\n#", "\r#" => "\r#", + "\n*" => "\n*", "\r*" => "\r*", + "\n:" => "\n:", "\r:" => "\r:", + "\n " => "\n ", "\r " => "\r ", + "\n\n" => "\n ", "\r\n" => " \n", + "\n\r" => "\n ", "\r\r" => "\r ", + "\n\t" => "\n ", "\r\t" => "\r ", // "\n\t\n" is treated like "\n\n" + "\n----" => "\n----", "\r----" => "\r----", + '__' => '__', '://' => '://', + ]; + + $magicLinks = array_keys( array_filter( $wgEnableMagicLinks ) ); + // We have to catch everything "\s" matches in PCRE + foreach ( $magicLinks as $magic ) { + $repl["$magic "] = "$magic "; + $repl["$magic\t"] = "$magic "; + $repl["$magic\r"] = "$magic "; + $repl["$magic\n"] = "$magic "; + $repl["$magic\f"] = "$magic "; + } + + // And handle protocols that don't use "://" + global $wgUrlProtocols; + $repl2 = []; + foreach ( $wgUrlProtocols as $prot ) { + if ( substr( $prot, -1 ) === ':' ) { + $repl2[] = preg_quote( substr( $prot, 0, -1 ), '/' ); + } + } + $repl2 = $repl2 ? '/\b(' . implode( '|', $repl2 ) . '):/i' : '/^(?!)/'; + } + $text = substr( strtr( "\n$text", $repl ), 1 ); + $text = preg_replace( $repl2, '$1:', $text ); + return $text; +} + +/** + * Sets dest to source and returns the original value of dest + * If source is NULL, it just returns the value, it doesn't set the variable + * If force is true, it will set the value even if source is NULL + * + * @param mixed &$dest + * @param mixed $source + * @param bool $force + * @return mixed + */ +function wfSetVar( &$dest, $source, $force = false ) { + $temp = $dest; + if ( !is_null( $source ) || $force ) { + $dest = $source; + } + return $temp; +} + +/** + * As for wfSetVar except setting a bit + * + * @param int &$dest + * @param int $bit + * @param bool $state + * + * @return bool + */ +function wfSetBit( &$dest, $bit, $state = true ) { + $temp = (bool)( $dest & $bit ); + if ( !is_null( $state ) ) { + if ( $state ) { + $dest |= $bit; + } else { + $dest &= ~$bit; + } + } + return $temp; +} + +/** + * A wrapper around the PHP function var_export(). + * Either print it or add it to the regular output ($wgOut). + * + * @param mixed $var A PHP variable to dump. + */ +function wfVarDump( $var ) { + global $wgOut; + $s = str_replace( "\n", "<br />\n", var_export( $var, true ) . "\n" ); + if ( headers_sent() || !isset( $wgOut ) || !is_object( $wgOut ) ) { + print $s; + } else { + $wgOut->addHTML( $s ); + } +} + +/** + * Provide a simple HTTP error. + * + * @param int|string $code + * @param string $label + * @param string $desc + */ +function wfHttpError( $code, $label, $desc ) { + global $wgOut; + HttpStatus::header( $code ); + if ( $wgOut ) { + $wgOut->disable(); + $wgOut->sendCacheControl(); + } + + MediaWiki\HeaderCallback::warnIfHeadersSent(); + header( 'Content-type: text/html; charset=utf-8' ); + print '<!DOCTYPE html>' . + '<html><head><title>' . + htmlspecialchars( $label ) . + '

' . + htmlspecialchars( $label ) . + '

' . + nl2br( htmlspecialchars( $desc ) ) . + "

\n"; +} + +/** + * Clear away any user-level output buffers, discarding contents. + * + * Suitable for 'starting afresh', for instance when streaming + * relatively large amounts of data without buffering, or wanting to + * output image files without ob_gzhandler's compression. + * + * The optional $resetGzipEncoding parameter controls suppression of + * the Content-Encoding header sent by ob_gzhandler; by default it + * is left. See comments for wfClearOutputBuffers() for why it would + * be used. + * + * Note that some PHP configuration options may add output buffer + * layers which cannot be removed; these are left in place. + * + * @param bool $resetGzipEncoding + */ +function wfResetOutputBuffers( $resetGzipEncoding = true ) { + if ( $resetGzipEncoding ) { + // Suppress Content-Encoding and Content-Length + // headers from OutputHandler::handle. + global $wgDisableOutputCompression; + $wgDisableOutputCompression = true; + } + while ( $status = ob_get_status() ) { + if ( isset( $status['flags'] ) ) { + $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE; + $deleteable = ( $status['flags'] & $flags ) === $flags; + } elseif ( isset( $status['del'] ) ) { + $deleteable = $status['del']; + } else { + // Guess that any PHP-internal setting can't be removed. + $deleteable = $status['type'] !== 0; /* PHP_OUTPUT_HANDLER_INTERNAL */ + } + if ( !$deleteable ) { + // Give up, and hope the result doesn't break + // output behavior. + break; + } + if ( $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ) { + // Unit testing barrier to prevent this function from breaking PHPUnit. + break; + } + if ( !ob_end_clean() ) { + // Could not remove output buffer handler; abort now + // to avoid getting in some kind of infinite loop. + break; + } + if ( $resetGzipEncoding ) { + if ( $status['name'] == 'ob_gzhandler' ) { + // Reset the 'Content-Encoding' field set by this handler + // so we can start fresh. + header_remove( 'Content-Encoding' ); + break; + } + } + } +} + +/** + * More legible than passing a 'false' parameter to wfResetOutputBuffers(): + * + * Clear away output buffers, but keep the Content-Encoding header + * produced by ob_gzhandler, if any. + * + * This should be used for HTTP 304 responses, where you need to + * preserve the Content-Encoding header of the real result, but + * also need to suppress the output of ob_gzhandler to keep to spec + * and avoid breaking Firefox in rare cases where the headers and + * body are broken over two packets. + */ +function wfClearOutputBuffers() { + wfResetOutputBuffers( false ); +} + +/** + * Converts an Accept-* header into an array mapping string values to quality + * factors + * + * @param string $accept + * @param string $def Default + * @return float[] Associative array of string => float pairs + */ +function wfAcceptToPrefs( $accept, $def = '*/*' ) { + # No arg means accept anything (per HTTP spec) + if ( !$accept ) { + return [ $def => 1.0 ]; + } + + $prefs = []; + + $parts = explode( ',', $accept ); + + foreach ( $parts as $part ) { + # @todo FIXME: Doesn't deal with params like 'text/html; level=1' + $values = explode( ';', trim( $part ) ); + $match = []; + if ( count( $values ) == 1 ) { + $prefs[$values[0]] = 1.0; + } elseif ( preg_match( '/q\s*=\s*(\d*\.\d+)/', $values[1], $match ) ) { + $prefs[$values[0]] = floatval( $match[1] ); + } + } + + return $prefs; +} + +/** + * Checks if a given MIME type matches any of the keys in the given + * array. Basic wildcards are accepted in the array keys. + * + * Returns the matching MIME type (or wildcard) if a match, otherwise + * NULL if no match. + * + * @param string $type + * @param array $avail + * @return string + * @private + */ +function mimeTypeMatch( $type, $avail ) { + if ( array_key_exists( $type, $avail ) ) { + return $type; + } else { + $mainType = explode( '/', $type )[0]; + if ( array_key_exists( "$mainType/*", $avail ) ) { + return "$mainType/*"; + } elseif ( array_key_exists( '*/*', $avail ) ) { + return '*/*'; + } else { + return null; + } + } +} + +/** + * Returns the 'best' match between a client's requested internet media types + * and the server's list of available types. Each list should be an associative + * array of type to preference (preference is a float between 0.0 and 1.0). + * Wildcards in the types are acceptable. + * + * @param array $cprefs Client's acceptable type list + * @param array $sprefs Server's offered types + * @return string + * + * @todo FIXME: Doesn't handle params like 'text/plain; charset=UTF-8' + * XXX: generalize to negotiate other stuff + */ +function wfNegotiateType( $cprefs, $sprefs ) { + $combine = []; + + foreach ( array_keys( $sprefs ) as $type ) { + $subType = explode( '/', $type )[1]; + if ( $subType != '*' ) { + $ckey = mimeTypeMatch( $type, $cprefs ); + if ( $ckey ) { + $combine[$type] = $sprefs[$type] * $cprefs[$ckey]; + } + } + } + + foreach ( array_keys( $cprefs ) as $type ) { + $subType = explode( '/', $type )[1]; + if ( $subType != '*' && !array_key_exists( $type, $sprefs ) ) { + $skey = mimeTypeMatch( $type, $sprefs ); + if ( $skey ) { + $combine[$type] = $sprefs[$skey] * $cprefs[$type]; + } + } + } + + $bestq = 0; + $besttype = null; + + foreach ( array_keys( $combine ) as $type ) { + if ( $combine[$type] > $bestq ) { + $besttype = $type; + $bestq = $combine[$type]; + } + } + + return $besttype; +} + +/** + * Reference-counted warning suppression + * + * @deprecated since 1.26, use Wikimedia\suppressWarnings() directly + * @param bool $end + */ +function wfSuppressWarnings( $end = false ) { + Wikimedia\suppressWarnings( $end ); +} + +/** + * @deprecated since 1.26, use Wikimedia\restoreWarnings() directly + * Restore error level to previous value + */ +function wfRestoreWarnings() { + Wikimedia\restoreWarnings(); +} + +/** + * Get a timestamp string in one of various formats + * + * @param mixed $outputtype A timestamp in one of the supported formats, the + * function will autodetect which format is supplied and act accordingly. + * @param mixed $ts Optional timestamp to convert, default 0 for the current time + * @return string|bool String / false The same date in the format specified in $outputtype or false + */ +function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { + $ret = MWTimestamp::convert( $outputtype, $ts ); + if ( $ret === false ) { + wfDebug( "wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n" ); + } + return $ret; +} + +/** + * Return a formatted timestamp, or null if input is null. + * For dealing with nullable timestamp columns in the database. + * + * @param int $outputtype + * @param string $ts + * @return string + */ +function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) { + if ( is_null( $ts ) ) { + return null; + } else { + return wfTimestamp( $outputtype, $ts ); + } +} + +/** + * Convenience function; returns MediaWiki timestamp for the present time. + * + * @return string + */ +function wfTimestampNow() { + # return NOW + return MWTimestamp::now( TS_MW ); +} + +/** + * Check if the operating system is Windows + * + * @return bool True if it's Windows, false otherwise. + */ +function wfIsWindows() { + static $isWindows = null; + if ( $isWindows === null ) { + $isWindows = strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; + } + return $isWindows; +} + +/** + * Check if we are running under HHVM + * + * @return bool + */ +function wfIsHHVM() { + return defined( 'HHVM_VERSION' ); +} + +/** + * Check if we are running from the commandline + * + * @since 1.31 + * @return bool + */ +function wfIsCLI() { + return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg'; +} + +/** + * Tries to get the system directory for temporary files. First + * $wgTmpDirectory is checked, and then the TMPDIR, TMP, and TEMP + * environment variables are then checked in sequence, then + * sys_get_temp_dir(), then upload_tmp_dir from php.ini. + * + * NOTE: When possible, use instead the tmpfile() function to create + * temporary files to avoid race conditions on file creation, etc. + * + * @return string + */ +function wfTempDir() { + global $wgTmpDirectory; + + if ( $wgTmpDirectory !== false ) { + return $wgTmpDirectory; + } + + return TempFSFile::getUsableTempDirectory(); +} + +/** + * Make directory, and make all parent directories if they don't exist + * + * @param string $dir Full path to directory to create + * @param int $mode Chmod value to use, default is $wgDirectoryMode + * @param string $caller Optional caller param for debugging. + * @throws MWException + * @return bool + */ +function wfMkdirParents( $dir, $mode = null, $caller = null ) { + global $wgDirectoryMode; + + if ( FileBackend::isStoragePath( $dir ) ) { // sanity + throw new MWException( __FUNCTION__ . " given storage path '$dir'." ); + } + + if ( !is_null( $caller ) ) { + wfDebug( "$caller: called wfMkdirParents($dir)\n" ); + } + + if ( strval( $dir ) === '' || is_dir( $dir ) ) { + return true; + } + + $dir = str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $dir ); + + if ( is_null( $mode ) ) { + $mode = $wgDirectoryMode; + } + + // Turn off the normal warning, we're doing our own below + Wikimedia\suppressWarnings(); + $ok = mkdir( $dir, $mode, true ); // PHP5 <3 + Wikimedia\restoreWarnings(); + + if ( !$ok ) { + // directory may have been created on another request since we last checked + if ( is_dir( $dir ) ) { + return true; + } + + // PHP doesn't report the path in its warning message, so add our own to aid in diagnosis. + wfLogWarning( sprintf( "failed to mkdir \"%s\" mode 0%o", $dir, $mode ) ); + } + return $ok; +} + +/** + * Remove a directory and all its content. + * Does not hide error. + * @param string $dir + */ +function wfRecursiveRemoveDir( $dir ) { + wfDebug( __FUNCTION__ . "( $dir )\n" ); + // taken from https://secure.php.net/manual/en/function.rmdir.php#98622 + if ( is_dir( $dir ) ) { + $objects = scandir( $dir ); + foreach ( $objects as $object ) { + if ( $object != "." && $object != ".." ) { + if ( filetype( $dir . '/' . $object ) == "dir" ) { + wfRecursiveRemoveDir( $dir . '/' . $object ); + } else { + unlink( $dir . '/' . $object ); + } + } + } + reset( $objects ); + rmdir( $dir ); + } +} + +/** + * @param int $nr The number to format + * @param int $acc The number of digits after the decimal point, default 2 + * @param bool $round Whether or not to round the value, default true + * @return string + */ +function wfPercent( $nr, $acc = 2, $round = true ) { + $ret = sprintf( "%.${acc}f", $nr ); + return $round ? round( $ret, $acc ) . '%' : "$ret%"; +} + +/** + * Safety wrapper around ini_get() for boolean settings. + * The values returned from ini_get() are pre-normalized for settings + * set via php.ini or php_flag/php_admin_flag... but *not* + * for those set via php_value/php_admin_value. + * + * It's fairly common for people to use php_value instead of php_flag, + * which can leave you with an 'off' setting giving a false positive + * for code that just takes the ini_get() return value as a boolean. + * + * To make things extra interesting, setting via php_value accepts + * "true" and "yes" as true, but php.ini and php_flag consider them false. :) + * Unrecognized values go false... again opposite PHP's own coercion + * from string to bool. + * + * Luckily, 'properly' set settings will always come back as '0' or '1', + * so we only have to worry about them and the 'improper' settings. + * + * I frickin' hate PHP... :P + * + * @param string $setting + * @return bool + */ +function wfIniGetBool( $setting ) { + return wfStringToBool( ini_get( $setting ) ); +} + +/** + * Convert string value to boolean, when the following are interpreted as true: + * - on + * - true + * - yes + * - Any number, except 0 + * All other strings are interpreted as false. + * + * @param string $val + * @return bool + * @since 1.31 + */ +function wfStringToBool( $val ) { + $val = strtolower( $val ); + // 'on' and 'true' can't have whitespace around them, but '1' can. + return $val == 'on' + || $val == 'true' + || $val == 'yes' + || preg_match( "/^\s*[+-]?0*[1-9]/", $val ); // approx C atoi() function +} + +/** + * Version of escapeshellarg() that works better on Windows. + * + * Originally, this fixed the incorrect use of single quotes on Windows + * (https://bugs.php.net/bug.php?id=26285) and the locale problems on Linux in + * PHP 5.2.6+ (bug backported to earlier distro releases of PHP). + * + * @param string $args,... strings to escape and glue together, + * or a single array of strings parameter + * @return string + * @deprecated since 1.30 use MediaWiki\Shell::escape() + */ +function wfEscapeShellArg( /*...*/ ) { + $args = func_get_args(); + + return call_user_func_array( Shell::class . '::escape', $args ); +} + +/** + * Check if wfShellExec() is effectively disabled via php.ini config + * + * @return bool|string False or 'disabled' + * @since 1.22 + * @deprecated since 1.30 use MediaWiki\Shell::isDisabled() + */ +function wfShellExecDisabled() { + wfDeprecated( __FUNCTION__, '1.30' ); + return Shell::isDisabled() ? 'disabled' : false; +} + +/** + * Execute a shell command, with time and memory limits mirrored from the PHP + * configuration if supported. + * + * @param string|string[] $cmd If string, a properly shell-escaped command line, + * or an array of unescaped arguments, in which case each value will be escaped + * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'" + * @param null|mixed &$retval Optional, will receive the program's exit code. + * (non-zero is usually failure). If there is an error from + * read, select, or proc_open(), this will be set to -1. + * @param array $environ Optional environment variables which should be + * added to the executed command environment. + * @param array $limits Optional array with limits(filesize, memory, time, walltime) + * this overwrites the global wgMaxShell* limits. + * @param array $options Array of options: + * - duplicateStderr: Set this to true to duplicate stderr to stdout, + * including errors from limit.sh + * - profileMethod: By default this function will profile based on the calling + * method. Set this to a string for an alternative method to profile from + * + * @return string Collected stdout as a string + * @deprecated since 1.30 use class MediaWiki\Shell\Shell + */ +function wfShellExec( $cmd, &$retval = null, $environ = [], + $limits = [], $options = [] +) { + if ( Shell::isDisabled() ) { + $retval = 1; + // Backwards compatibility be upon us... + return 'Unable to run external programs, proc_open() is disabled.'; + } + + if ( is_array( $cmd ) ) { + $cmd = Shell::escape( $cmd ); + } + + $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr']; + $profileMethod = isset( $options['profileMethod'] ) ? $options['profileMethod'] : wfGetCaller(); + + try { + $result = Shell::command( [] ) + ->unsafeParams( (array)$cmd ) + ->environment( $environ ) + ->limits( $limits ) + ->includeStderr( $includeStderr ) + ->profileMethod( $profileMethod ) + // For b/c + ->restrict( Shell::RESTRICT_NONE ) + ->execute(); + } catch ( ProcOpenError $ex ) { + $retval = -1; + return ''; + } + + $retval = $result->getExitCode(); + + return $result->getStdout(); +} + +/** + * Execute a shell command, returning both stdout and stderr. Convenience + * function, as all the arguments to wfShellExec can become unwieldy. + * + * @note This also includes errors from limit.sh, e.g. if $wgMaxShellFileSize is exceeded. + * @param string|string[] $cmd If string, a properly shell-escaped command line, + * or an array of unescaped arguments, in which case each value will be escaped + * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'" + * @param null|mixed &$retval Optional, will receive the program's exit code. + * (non-zero is usually failure) + * @param array $environ Optional environment variables which should be + * added to the executed command environment. + * @param array $limits Optional array with limits(filesize, memory, time, walltime) + * this overwrites the global wgMaxShell* limits. + * @return string Collected stdout and stderr as a string + * @deprecated since 1.30 use class MediaWiki\Shell\Shell + */ +function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits = [] ) { + return wfShellExec( $cmd, $retval, $environ, $limits, + [ 'duplicateStderr' => true, 'profileMethod' => wfGetCaller() ] ); +} + +/** + * Formerly set the locale for locale-sensitive operations + * + * This is now done in Setup.php. + * + * @deprecated since 1.30, no longer needed + * @see $wgShellLocale + */ +function wfInitShellLocale() { + wfDeprecated( __FUNCTION__, '1.30' ); +} + +/** + * Generate a shell-escaped command line string to run a MediaWiki cli script. + * Note that $parameters should be a flat array and an option with an argument + * should consist of two consecutive items in the array (do not use "--option value"). + * + * @deprecated since 1.31, use Shell::makeScriptCommand() + * + * @param string $script MediaWiki cli script path + * @param array $parameters Arguments and options to the script + * @param array $options Associative array of options: + * 'php': The path to the php executable + * 'wrapper': Path to a PHP wrapper to handle the maintenance script + * @return string + */ +function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) { + global $wgPhpCli; + // Give site config file a chance to run the script in a wrapper. + // The caller may likely want to call wfBasename() on $script. + Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] ); + $cmd = isset( $options['php'] ) ? [ $options['php'] ] : [ $wgPhpCli ]; + if ( isset( $options['wrapper'] ) ) { + $cmd[] = $options['wrapper']; + } + $cmd[] = $script; + // Escape each parameter for shell + return Shell::escape( array_merge( $cmd, $parameters ) ); +} + +/** + * wfMerge attempts to merge differences between three texts. + * Returns true for a clean merge and false for failure or a conflict. + * + * @param string $old + * @param string $mine + * @param string $yours + * @param string &$result + * @param string &$mergeAttemptResult + * @return bool + */ +function wfMerge( $old, $mine, $yours, &$result, &$mergeAttemptResult = null ) { + global $wgDiff3; + + # This check may also protect against code injection in + # case of broken installations. + Wikimedia\suppressWarnings(); + $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); + Wikimedia\restoreWarnings(); + + if ( !$haveDiff3 ) { + wfDebug( "diff3 not found\n" ); + return false; + } + + # Make temporary files + $td = wfTempDir(); + $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); + $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' ); + $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' ); + + # NOTE: diff3 issues a warning to stderr if any of the files does not end with + # a newline character. To avoid this, we normalize the trailing whitespace before + # creating the diff. + + fwrite( $oldtextFile, rtrim( $old ) . "\n" ); + fclose( $oldtextFile ); + fwrite( $mytextFile, rtrim( $mine ) . "\n" ); + fclose( $mytextFile ); + fwrite( $yourtextFile, rtrim( $yours ) . "\n" ); + fclose( $yourtextFile ); + + # Check for a conflict + $cmd = Shell::escape( $wgDiff3, '-a', '--overlap-only', $mytextName, + $oldtextName, $yourtextName ); + $handle = popen( $cmd, 'r' ); + + $mergeAttemptResult = ''; + do { + $data = fread( $handle, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $mergeAttemptResult .= $data; + } while ( true ); + pclose( $handle ); + + $conflict = $mergeAttemptResult !== ''; + + # Merge differences + $cmd = Shell::escape( $wgDiff3, '-a', '-e', '--merge', $mytextName, + $oldtextName, $yourtextName ); + $handle = popen( $cmd, 'r' ); + $result = ''; + do { + $data = fread( $handle, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $result .= $data; + } while ( true ); + pclose( $handle ); + unlink( $mytextName ); + unlink( $oldtextName ); + unlink( $yourtextName ); + + if ( $result === '' && $old !== '' && !$conflict ) { + wfDebug( "Unexpected null result from diff3. Command: $cmd\n" ); + $conflict = true; + } + return !$conflict; +} + +/** + * Returns unified plain-text diff of two texts. + * "Useful" for machine processing of diffs. + * + * @deprecated since 1.25, use DiffEngine/UnifiedDiffFormatter directly + * + * @param string $before The text before the changes. + * @param string $after The text after the changes. + * @param string $params Command-line options for the diff command. + * @return string Unified diff of $before and $after + */ +function wfDiff( $before, $after, $params = '-u' ) { + if ( $before == $after ) { + return ''; + } + + global $wgDiff; + Wikimedia\suppressWarnings(); + $haveDiff = $wgDiff && file_exists( $wgDiff ); + Wikimedia\restoreWarnings(); + + # This check may also protect against code injection in + # case of broken installations. + if ( !$haveDiff ) { + wfDebug( "diff executable not found\n" ); + $diffs = new Diff( explode( "\n", $before ), explode( "\n", $after ) ); + $format = new UnifiedDiffFormatter(); + return $format->format( $diffs ); + } + + # Make temporary files + $td = wfTempDir(); + $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' ); + $newtextFile = fopen( $newtextName = tempnam( $td, 'merge-your-' ), 'w' ); + + fwrite( $oldtextFile, $before ); + fclose( $oldtextFile ); + fwrite( $newtextFile, $after ); + fclose( $newtextFile ); + + // Get the diff of the two files + $cmd = "$wgDiff " . $params . ' ' . Shell::escape( $oldtextName, $newtextName ); + + $h = popen( $cmd, 'r' ); + if ( !$h ) { + unlink( $oldtextName ); + unlink( $newtextName ); + throw new Exception( __METHOD__ . '(): popen() failed' ); + } + + $diff = ''; + + do { + $data = fread( $h, 8192 ); + if ( strlen( $data ) == 0 ) { + break; + } + $diff .= $data; + } while ( true ); + + // Clean up + pclose( $h ); + unlink( $oldtextName ); + unlink( $newtextName ); + + // Kill the --- and +++ lines. They're not useful. + $diff_lines = explode( "\n", $diff ); + if ( isset( $diff_lines[0] ) && strpos( $diff_lines[0], '---' ) === 0 ) { + unset( $diff_lines[0] ); + } + if ( isset( $diff_lines[1] ) && strpos( $diff_lines[1], '+++' ) === 0 ) { + unset( $diff_lines[1] ); + } + + $diff = implode( "\n", $diff_lines ); + + return $diff; +} + +/** + * This function works like "use VERSION" in Perl, the program will die with a + * backtrace if the current version of PHP is less than the version provided + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases, and might depend on other versions of PHP than the main code + * + * Note: PHP might die due to parsing errors in some cases before it ever + * manages to call this function, such is life + * + * @see perldoc -f use + * + * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float + * + * @deprecated since 1.30 + * + * @throws MWException + */ +function wfUsePHP( $req_ver ) { + wfDeprecated( __FUNCTION__, '1.30' ); + $php_ver = PHP_VERSION; + + if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) { + throw new MWException( "PHP $req_ver required--this is only $php_ver" ); + } +} + +/** + * This function works like "use VERSION" in Perl except it checks the version + * of MediaWiki, the program will die with a backtrace if the current version + * of MediaWiki is less than the version provided. + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases + * + * Note: Due to the behavior of PHP's version_compare() which is used in this + * function, if you want to allow the 'wmf' development versions add a 'c' (or + * any single letter other than 'a', 'b' or 'p') as a post-fix to your + * targeted version number. For example if you wanted to allow any variation + * of 1.22 use `wfUseMW( '1.22c' )`. Using an 'a' or 'b' instead of 'c' will + * not result in the same comparison due to the internal logic of + * version_compare(). + * + * @see perldoc -f use + * + * @deprecated since 1.26, use the "requires" property of extension.json + * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float + * @throws MWException + */ +function wfUseMW( $req_ver ) { + global $wgVersion; + + if ( version_compare( $wgVersion, (string)$req_ver, '<' ) ) { + throw new MWException( "MediaWiki $req_ver required--this is only $wgVersion" ); + } +} + +/** + * Return the final portion of a pathname. + * Reimplemented because PHP5's "basename()" is buggy with multibyte text. + * https://bugs.php.net/bug.php?id=33898 + * + * PHP's basename() only considers '\' a pathchar on Windows and Netware. + * We'll consider it so always, as we don't want '\s' in our Unix paths either. + * + * @param string $path + * @param string $suffix String to remove if present + * @return string + */ +function wfBaseName( $path, $suffix = '' ) { + if ( $suffix == '' ) { + $encSuffix = ''; + } else { + $encSuffix = '(?:' . preg_quote( $suffix, '#' ) . ')?'; + } + + $matches = []; + if ( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { + return $matches[1]; + } else { + return ''; + } +} + +/** + * Generate a relative path name to the given file. + * May explode on non-matching case-insensitive paths, + * funky symlinks, etc. + * + * @param string $path Absolute destination path including target filename + * @param string $from Absolute source path, directory only + * @return string + */ +function wfRelativePath( $path, $from ) { + // Normalize mixed input on Windows... + $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); + $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); + + // Trim trailing slashes -- fix for drive root + $path = rtrim( $path, DIRECTORY_SEPARATOR ); + $from = rtrim( $from, DIRECTORY_SEPARATOR ); + + $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); + $against = explode( DIRECTORY_SEPARATOR, $from ); + + if ( $pieces[0] !== $against[0] ) { + // Non-matching Windows drive letters? + // Return a full path. + return $path; + } + + // Trim off common prefix + while ( count( $pieces ) && count( $against ) + && $pieces[0] == $against[0] ) { + array_shift( $pieces ); + array_shift( $against ); + } + + // relative dots to bump us to the parent + while ( count( $against ) ) { + array_unshift( $pieces, '..' ); + array_shift( $against ); + } + + array_push( $pieces, wfBaseName( $path ) ); + + return implode( DIRECTORY_SEPARATOR, $pieces ); +} + +/** + * Reset the session id + * + * @deprecated since 1.27, use MediaWiki\Session\SessionManager instead + * @since 1.22 + */ +function wfResetSessionID() { + wfDeprecated( __FUNCTION__, '1.27' ); + $session = SessionManager::getGlobalSession(); + $delay = $session->delaySave(); + + $session->resetId(); + + // Make sure a session is started, since that's what the old + // wfResetSessionID() did. + if ( session_id() !== $session->getId() ) { + wfSetupSession( $session->getId() ); + } + + ScopedCallback::consume( $delay ); +} + +/** + * Initialise php session + * + * @deprecated since 1.27, use MediaWiki\Session\SessionManager instead. + * Generally, "using" SessionManager will be calling ->getSessionById() or + * ::getGlobalSession() (depending on whether you were passing $sessionId + * here), then calling $session->persist(). + * @param bool|string $sessionId + */ +function wfSetupSession( $sessionId = false ) { + wfDeprecated( __FUNCTION__, '1.27' ); + + if ( $sessionId ) { + session_id( $sessionId ); + } + + $session = SessionManager::getGlobalSession(); + $session->persist(); + + if ( session_id() !== $session->getId() ) { + session_id( $session->getId() ); + } + Wikimedia\quietCall( 'session_start' ); +} + +/** + * Get an object from the precompiled serialized directory + * + * @param string $name + * @return mixed The variable on success, false on failure + */ +function wfGetPrecompiledData( $name ) { + global $IP; + + $file = "$IP/serialized/$name"; + if ( file_exists( $file ) ) { + $blob = file_get_contents( $file ); + if ( $blob ) { + return unserialize( $blob ); + } + } + return false; +} + +/** + * Make a cache key for the local wiki. + * + * @deprecated since 1.30 Call makeKey on a BagOStuff instance + * @param string $args,... + * @return string + */ +function wfMemcKey( /*...*/ ) { + return call_user_func_array( + [ ObjectCache::getLocalClusterInstance(), 'makeKey' ], + func_get_args() + ); +} + +/** + * Make a cache key for a foreign DB. + * + * Must match what wfMemcKey() would produce in context of the foreign wiki. + * + * @param string $db + * @param string $prefix + * @param string $args,... + * @return string + */ +function wfForeignMemcKey( $db, $prefix /*...*/ ) { + $args = array_slice( func_get_args(), 2 ); + $keyspace = $prefix ? "$db-$prefix" : $db; + return call_user_func_array( + [ ObjectCache::getLocalClusterInstance(), 'makeKeyInternal' ], + [ $keyspace, $args ] + ); +} + +/** + * Make a cache key with database-agnostic prefix. + * + * Doesn't have a wiki-specific namespace. Uses a generic 'global' prefix + * instead. Must have a prefix as otherwise keys that use a database name + * in the first segment will clash with wfMemcKey/wfForeignMemcKey. + * + * @deprecated since 1.30 Call makeGlobalKey on a BagOStuff instance + * @since 1.26 + * @param string $args,... + * @return string + */ +function wfGlobalCacheKey( /*...*/ ) { + return call_user_func_array( + [ ObjectCache::getLocalClusterInstance(), 'makeGlobalKey' ], + func_get_args() + ); +} + +/** + * Get an ASCII string identifying this wiki + * This is used as a prefix in memcached keys + * + * @return string + */ +function wfWikiID() { + global $wgDBprefix, $wgDBname; + if ( $wgDBprefix ) { + return "$wgDBname-$wgDBprefix"; + } else { + return $wgDBname; + } +} + +/** + * Split a wiki ID into DB name and table prefix + * + * @param string $wiki + * + * @return array + */ +function wfSplitWikiID( $wiki ) { + $bits = explode( '-', $wiki, 2 ); + if ( count( $bits ) < 2 ) { + $bits[] = ''; + } + return $bits; +} + +/** + * Get a Database object. + * + * @param int $db Index of the connection to get. May be DB_MASTER for the + * master (for write queries), DB_REPLICA for potentially lagged read + * queries, or an integer >= 0 for a particular server. + * + * @param string|string[] $groups Query groups. An array of group names that this query + * belongs to. May contain a single string if the query is only + * in one group. + * + * @param string|bool $wiki The wiki ID, or false for the current wiki + * + * Note: multiple calls to wfGetDB(DB_REPLICA) during the course of one request + * will always return the same object, unless the underlying connection or load + * balancer is manually destroyed. + * + * Note 2: use $this->getDB() in maintenance scripts that may be invoked by + * updater to ensure that a proper database is being updated. + * + * @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection() + * on an injected instance of LoadBalancer. + * + * @return \Wikimedia\Rdbms\Database + */ +function wfGetDB( $db, $groups = [], $wiki = false ) { + return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki ); +} + +/** + * Get a load balancer object. + * + * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancer() + * or MediaWikiServices::getDBLoadBalancerFactory() instead. + * + * @param string|bool $wiki Wiki ID, or false for the current wiki + * @return \Wikimedia\Rdbms\LoadBalancer + */ +function wfGetLB( $wiki = false ) { + if ( $wiki === false ) { + return MediaWikiServices::getInstance()->getDBLoadBalancer(); + } else { + $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + return $factory->getMainLB( $wiki ); + } +} + +/** + * Get the load balancer factory object + * + * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead. + * + * @return \Wikimedia\Rdbms\LBFactory + */ +function wfGetLBFactory() { + return MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); +} + +/** + * Find a file. + * Shortcut for RepoGroup::singleton()->findFile() + * + * @param string|Title $title String or Title object + * @param array $options Associative array of options (see RepoGroup::findFile) + * @return File|bool File, or false if the file does not exist + */ +function wfFindFile( $title, $options = [] ) { + return RepoGroup::singleton()->findFile( $title, $options ); +} + +/** + * Get an object referring to a locally registered file. + * Returns a valid placeholder object if the file does not exist. + * + * @param Title|string $title + * @return LocalFile|null A File, or null if passed an invalid Title + */ +function wfLocalFile( $title ) { + return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); +} + +/** + * Should low-performance queries be disabled? + * + * @return bool + * @codeCoverageIgnore + */ +function wfQueriesMustScale() { + global $wgMiserMode; + return $wgMiserMode + || ( SiteStats::pages() > 100000 + && SiteStats::edits() > 1000000 + && SiteStats::users() > 10000 ); +} + +/** + * Get the path to a specified script file, respecting file + * extensions; this is a wrapper around $wgScriptPath etc. + * except for 'index' and 'load' which use $wgScript/$wgLoadScript + * + * @param string $script Script filename, sans extension + * @return string + */ +function wfScript( $script = 'index' ) { + global $wgScriptPath, $wgScript, $wgLoadScript; + if ( $script === 'index' ) { + return $wgScript; + } elseif ( $script === 'load' ) { + return $wgLoadScript; + } else { + return "{$wgScriptPath}/{$script}.php"; + } +} + +/** + * Get the script URL. + * + * @return string Script URL + */ +function wfGetScriptUrl() { + if ( isset( $_SERVER['SCRIPT_NAME'] ) ) { + /* as it was called, minus the query string. + * + * Some sites use Apache rewrite rules to handle subdomains, + * and have PHP set up in a weird way that causes PHP_SELF + * to contain the rewritten URL instead of the one that the + * outside world sees. + * + * If in this mode, use SCRIPT_URL instead, which mod_rewrite + * provides containing the "before" URL. + */ + return $_SERVER['SCRIPT_NAME']; + } else { + return $_SERVER['URL']; + } +} + +/** + * Convenience function converts boolean values into "true" + * or "false" (string) values + * + * @param bool $value + * @return string + */ +function wfBoolToStr( $value ) { + return $value ? 'true' : 'false'; +} + +/** + * Get a platform-independent path to the null file, e.g. /dev/null + * + * @return string + */ +function wfGetNull() { + return wfIsWindows() ? 'NUL' : '/dev/null'; +} + +/** + * Waits for the replica DBs to catch up to the master position + * + * Use this when updating very large numbers of rows, as in maintenance scripts, + * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs. + * + * By default this waits on the main DB cluster of the current wiki. + * If $cluster is set to "*" it will wait on all DB clusters, including + * external ones. If the lag being waiting on is caused by the code that + * does this check, it makes since to use $ifWritesSince, particularly if + * cluster is "*", to avoid excess overhead. + * + * Never call this function after a big DB write that is still in a transaction. + * This only makes sense after the possible lag inducing changes were committed. + * + * @param float|null $ifWritesSince Only wait if writes were done since this UNIX timestamp + * @param string|bool $wiki Wiki identifier accepted by wfGetLB + * @param string|bool $cluster Cluster name accepted by LBFactory. Default: false. + * @param int|null $timeout Max wait time. Default: 1 day (cli), ~10 seconds (web) + * @return bool Success (able to connect and no timeouts reached) + * @deprecated since 1.27 Use LBFactory::waitForReplication + */ +function wfWaitForSlaves( + $ifWritesSince = null, $wiki = false, $cluster = false, $timeout = null +) { + if ( $timeout === null ) { + $timeout = wfIsCLI() ? 60 : 10; + } + + if ( $cluster === '*' ) { + $cluster = false; + $wiki = false; + } elseif ( $wiki === false ) { + $wiki = wfWikiID(); + } + + try { + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->waitForReplication( [ + 'wiki' => $wiki, + 'cluster' => $cluster, + 'timeout' => $timeout, + // B/C: first argument used to be "max seconds of lag"; ignore such values + 'ifWritesSince' => ( $ifWritesSince > 1e9 ) ? $ifWritesSince : null + ] ); + } catch ( DBReplicationWaitError $e ) { + return false; + } + + return true; +} + +/** + * Count down from $seconds to zero on the terminal, with a one-second pause + * between showing each number. For use in command-line scripts. + * + * @deprecated since 1.31, use Maintenance::countDown() + * + * @codeCoverageIgnore + * @param int $seconds + */ +function wfCountDown( $seconds ) { + for ( $i = $seconds; $i >= 0; $i-- ) { + if ( $i != $seconds ) { + echo str_repeat( "\x08", strlen( $i + 1 ) ); + } + echo $i; + flush(); + if ( $i ) { + sleep( 1 ); + } + } + echo "\n"; +} + +/** + * Replace all invalid characters with '-'. + * Additional characters can be defined in $wgIllegalFileChars (see T22489). + * By default, $wgIllegalFileChars includes ':', '/', '\'. + * + * @param string $name Filename to process + * @return string + */ +function wfStripIllegalFilenameChars( $name ) { + global $wgIllegalFileChars; + $illegalFileChars = $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : ''; + $name = preg_replace( + "/[^" . Title::legalChars() . "]" . $illegalFileChars . "/", + '-', + $name + ); + // $wgIllegalFileChars may not include '/' and '\', so we still need to do this + $name = wfBaseName( $name ); + return $name; +} + +/** + * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit + * + * @return int Resulting value of the memory limit. + */ +function wfMemoryLimit() { + global $wgMemoryLimit; + $memlimit = wfShorthandToInteger( ini_get( 'memory_limit' ) ); + if ( $memlimit != -1 ) { + $conflimit = wfShorthandToInteger( $wgMemoryLimit ); + if ( $conflimit == -1 ) { + wfDebug( "Removing PHP's memory limit\n" ); + Wikimedia\suppressWarnings(); + ini_set( 'memory_limit', $conflimit ); + Wikimedia\restoreWarnings(); + return $conflimit; + } elseif ( $conflimit > $memlimit ) { + wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" ); + Wikimedia\suppressWarnings(); + ini_set( 'memory_limit', $conflimit ); + Wikimedia\restoreWarnings(); + return $conflimit; + } + } + return $memlimit; +} + +/** + * Set PHP's time limit to the larger of php.ini or $wgTransactionalTimeLimit + * + * @return int Prior time limit + * @since 1.26 + */ +function wfTransactionalTimeLimit() { + global $wgTransactionalTimeLimit; + + $timeLimit = ini_get( 'max_execution_time' ); + // Note that CLI scripts use 0 + if ( $timeLimit > 0 && $wgTransactionalTimeLimit > $timeLimit ) { + set_time_limit( $wgTransactionalTimeLimit ); + } + + ignore_user_abort( true ); // ignore client disconnects + + return $timeLimit; +} + +/** + * Converts shorthand byte notation to integer form + * + * @param string $string + * @param int $default Returned if $string is empty + * @return int + */ +function wfShorthandToInteger( $string = '', $default = -1 ) { + $string = trim( $string ); + if ( $string === '' ) { + return $default; + } + $last = $string[strlen( $string ) - 1]; + $val = intval( $string ); + switch ( $last ) { + case 'g': + case 'G': + $val *= 1024; + // break intentionally missing + case 'm': + case 'M': + $val *= 1024; + // break intentionally missing + case 'k': + case 'K': + $val *= 1024; + } + + return $val; +} + +/** + * Get the normalised IETF language tag + * See unit test for examples. + * See mediawiki.language.bcp47 for the JavaScript implementation. + * + * @deprecated since 1.31, use LanguageCode::bcp47() directly. + * + * @param string $code The language code. + * @return string The language code which complying with BCP 47 standards. + */ +function wfBCP47( $code ) { + return LanguageCode::bcp47( $code ); +} + +/** + * Get a specific cache object. + * + * @param int|string $cacheType A CACHE_* constants, or other key in $wgObjectCaches + * @return BagOStuff + */ +function wfGetCache( $cacheType ) { + return ObjectCache::getInstance( $cacheType ); +} + +/** + * Get the main cache object + * + * @return BagOStuff + */ +function wfGetMainCache() { + global $wgMainCacheType; + return ObjectCache::getInstance( $wgMainCacheType ); +} + +/** + * Get the cache object used by the message cache + * + * @return BagOStuff + */ +function wfGetMessageCacheStorage() { + global $wgMessageCacheType; + return ObjectCache::getInstance( $wgMessageCacheType ); +} + +/** + * Get the cache object used by the parser cache + * + * @deprecated since 1.30, use MediaWikiServices::getParserCache()->getCacheStorage() + * @return BagOStuff + */ +function wfGetParserCacheStorage() { + global $wgParserCacheType; + return ObjectCache::getInstance( $wgParserCacheType ); +} + +/** + * Call hook functions defined in $wgHooks + * + * @param string $event Event name + * @param array $args Parameters passed to hook functions + * @param string|null $deprecatedVersion Optionally mark hook as deprecated with version number + * + * @return bool True if no handler aborted the hook + * @deprecated since 1.25 - use Hooks::run + */ +function wfRunHooks( $event, array $args = [], $deprecatedVersion = null ) { + wfDeprecated( __METHOD__, '1.25' ); + return Hooks::run( $event, $args, $deprecatedVersion ); +} + +/** + * Wrapper around php's unpack. + * + * @param string $format The format string (See php's docs) + * @param string $data A binary string of binary data + * @param int|bool $length The minimum length of $data or false. This is to + * prevent reading beyond the end of $data. false to disable the check. + * + * Also be careful when using this function to read unsigned 32 bit integer + * because php might make it negative. + * + * @throws MWException If $data not long enough, or if unpack fails + * @return array Associative array of the extracted data + */ +function wfUnpack( $format, $data, $length = false ) { + if ( $length !== false ) { + $realLen = strlen( $data ); + if ( $realLen < $length ) { + throw new MWException( "Tried to use wfUnpack on a " + . "string of length $realLen, but needed one " + . "of at least length $length." + ); + } + } + + Wikimedia\suppressWarnings(); + $result = unpack( $format, $data ); + Wikimedia\restoreWarnings(); + + if ( $result === false ) { + // If it cannot extract the packed data. + throw new MWException( "unpack could not unpack binary data" ); + } + return $result; +} + +/** + * Determine if an image exists on the 'bad image list'. + * + * The format of MediaWiki:Bad_image_list is as follows: + * * Only list items (lines starting with "*") are considered + * * The first link on a line must be a link to a bad image + * * Any subsequent links on the same line are considered to be exceptions, + * i.e. articles where the image may occur inline. + * + * @param string $name The image name to check + * @param Title|bool $contextTitle The page on which the image occurs, if known + * @param string $blacklist Wikitext of a file blacklist + * @return bool + */ +function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) { + # Handle redirects; callers almost always hit wfFindFile() anyway, + # so just use that method because it has a fast process cache. + $file = wfFindFile( $name ); // get the final name + $name = $file ? $file->getTitle()->getDBkey() : $name; + + # Run the extension hook + $bad = false; + if ( !Hooks::run( 'BadImage', [ $name, &$bad ] ) ) { + return (bool)$bad; + } + + $cache = ObjectCache::getLocalServerInstance( 'hash' ); + $key = $cache->makeKey( + 'bad-image-list', ( $blacklist === null ) ? 'default' : md5( $blacklist ) + ); + $badImages = $cache->get( $key ); + + if ( $badImages === false ) { // cache miss + if ( $blacklist === null ) { + $blacklist = wfMessage( 'bad_image_list' )->inContentLanguage()->plain(); // site list + } + # Build the list now + $badImages = []; + $lines = explode( "\n", $blacklist ); + foreach ( $lines as $line ) { + # List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + # Find all links + $m = []; + if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) { + continue; + } + + $exceptions = []; + $imageDBkey = false; + foreach ( $m[1] as $i => $titleText ) { + $title = Title::newFromText( $titleText ); + if ( !is_null( $title ) ) { + if ( $i == 0 ) { + $imageDBkey = $title->getDBkey(); + } else { + $exceptions[$title->getPrefixedDBkey()] = true; + } + } + } + + if ( $imageDBkey !== false ) { + $badImages[$imageDBkey] = $exceptions; + } + } + $cache->set( $key, $badImages, 60 ); + } + + $contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false; + $bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] ); + + return $bad; +} + +/** + * Determine whether the client at a given source IP is likely to be able to + * access the wiki via HTTPS. + * + * @param string $ip The IPv4/6 address in the normal human-readable form + * @return bool + */ +function wfCanIPUseHTTPS( $ip ) { + $canDo = true; + Hooks::run( 'CanIPUseHTTPS', [ $ip, &$canDo ] ); + return !!$canDo; +} + +/** + * Determine input string is represents as infinity + * + * @param string $str The string to determine + * @return bool + * @since 1.25 + */ +function wfIsInfinity( $str ) { + // These are hardcoded elsewhere in MediaWiki (e.g. mediawiki.special.block.js). + $infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ]; + return in_array( $str, $infinityValues ); +} + +/** + * Returns true if these thumbnail parameters match one that MediaWiki + * requests from file description pages and/or parser output. + * + * $params is considered non-standard if they involve a non-standard + * width or any non-default parameters aside from width and page number. + * The number of possible files with standard parameters is far less than + * that of all combinations; rate-limiting for them can thus be more generious. + * + * @param File $file + * @param array $params + * @return bool + * @since 1.24 Moved from thumb.php to GlobalFunctions in 1.25 + */ +function wfThumbIsStandard( File $file, array $params ) { + global $wgThumbLimits, $wgImageLimits, $wgResponsiveImages; + + $multipliers = [ 1 ]; + if ( $wgResponsiveImages ) { + // These available sizes are hardcoded currently elsewhere in MediaWiki. + // @see Linker::processResponsiveImages + $multipliers[] = 1.5; + $multipliers[] = 2; + } + + $handler = $file->getHandler(); + if ( !$handler || !isset( $params['width'] ) ) { + return false; + } + + $basicParams = []; + if ( isset( $params['page'] ) ) { + $basicParams['page'] = $params['page']; + } + + $thumbLimits = []; + $imageLimits = []; + // Expand limits to account for multipliers + foreach ( $multipliers as $multiplier ) { + $thumbLimits = array_merge( $thumbLimits, array_map( + function ( $width ) use ( $multiplier ) { + return round( $width * $multiplier ); + }, $wgThumbLimits ) + ); + $imageLimits = array_merge( $imageLimits, array_map( + function ( $pair ) use ( $multiplier ) { + return [ + round( $pair[0] * $multiplier ), + round( $pair[1] * $multiplier ), + ]; + }, $wgImageLimits ) + ); + } + + // Check if the width matches one of $wgThumbLimits + if ( in_array( $params['width'], $thumbLimits ) ) { + $normalParams = $basicParams + [ 'width' => $params['width'] ]; + // Append any default values to the map (e.g. "lossy", "lossless", ...) + $handler->normaliseParams( $file, $normalParams ); + } else { + // If not, then check if the width matchs one of $wgImageLimits + $match = false; + foreach ( $imageLimits as $pair ) { + $normalParams = $basicParams + [ 'width' => $pair[0], 'height' => $pair[1] ]; + // Decide whether the thumbnail should be scaled on width or height. + // Also append any default values to the map (e.g. "lossy", "lossless", ...) + $handler->normaliseParams( $file, $normalParams ); + // Check if this standard thumbnail size maps to the given width + if ( $normalParams['width'] == $params['width'] ) { + $match = true; + break; + } + } + if ( !$match ) { + return false; // not standard for description pages + } + } + + // Check that the given values for non-page, non-width, params are just defaults + foreach ( $params as $key => $value ) { + if ( !isset( $normalParams[$key] ) || $normalParams[$key] != $value ) { + return false; + } + } + + return true; +} + +/** + * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray). + * + * Values that exist in both values will be combined with += (all values of the array + * of $newValues will be added to the values of the array of $baseArray, while values, + * that exists in both, the value of $baseArray will be used). + * + * @param array $baseArray The array where you want to add the values of $newValues to + * @param array $newValues An array with new values + * @return array The combined array + * @since 1.26 + */ +function wfArrayPlus2d( array $baseArray, array $newValues ) { + // First merge items that are in both arrays + foreach ( $baseArray as $name => &$groupVal ) { + if ( isset( $newValues[$name] ) ) { + $groupVal += $newValues[$name]; + } + } + // Now add items that didn't exist yet + $baseArray += $newValues; + + return $baseArray; +} + +/** + * Get system resource usage of current request context. + * Invokes the getrusage(2) system call, requesting RUSAGE_SELF if on PHP5 + * or RUSAGE_THREAD if on HHVM. Returns false if getrusage is not available. + * + * @since 1.24 + * @return array|bool Resource usage data or false if no data available. + */ +function wfGetRusage() { + if ( !function_exists( 'getrusage' ) ) { + return false; + } elseif ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) { + return getrusage( 2 /* RUSAGE_THREAD */ ); + } else { + return getrusage( 0 /* RUSAGE_SELF */ ); + } +} diff --git a/www/wiki/includes/HeaderCallback.php b/www/wiki/includes/HeaderCallback.php new file mode 100644 index 00000000..b2ca6733 --- /dev/null +++ b/www/wiki/includes/HeaderCallback.php @@ -0,0 +1,69 @@ +warning( + 'Cookies set on {url} with Cache-Control "{cache-control}"', [ + 'url' => \WebRequest::getGlobalRequestURL(), + 'cookies' => $headers['set-cookie'], + 'cache-control' => $cacheControl ?: '', + ] + ); + } + } + + // Save a backtrace for logging in case it turns out that headers were sent prematurely + self::$headersSentException = new \Exception( 'Headers already sent from this point' ); + } + + /** + * Log a warning message if headers have already been sent. This can be + * called before flushing the output. + */ + public static function warnIfHeadersSent() { + if ( headers_sent() && !self::$messageSent ) { + self::$messageSent = true; + \MWDebug::warning( 'Headers already sent, should send headers earlier than ' . + wfGetCaller( 3 ) ); + $logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'headers-sent' ); + $logger->error( 'Warning: headers were already sent from the location below', [ + 'exception' => self::$headersSentException, + 'detection-trace' => new \Exception( 'Detected here' ), + ] ); + } + } +} diff --git a/www/wiki/includes/HistoryBlob.php b/www/wiki/includes/HistoryBlob.php new file mode 100644 index 00000000..1d4f6e4e --- /dev/null +++ b/www/wiki/includes/HistoryBlob.php @@ -0,0 +1,713 @@ +uncompress(); + $hash = md5( $text ); + if ( !isset( $this->mItems[$hash] ) ) { + $this->mItems[$hash] = $text; + $this->mSize += strlen( $text ); + } + return $hash; + } + + /** + * @param string $hash + * @return array|bool + */ + public function getItem( $hash ) { + $this->uncompress(); + if ( array_key_exists( $hash, $this->mItems ) ) { + return $this->mItems[$hash]; + } else { + return false; + } + } + + /** + * @param string $text + * @return void + */ + public function setText( $text ) { + $this->uncompress(); + $this->mDefaultHash = $this->addItem( $text ); + } + + /** + * @return array|bool + */ + public function getText() { + $this->uncompress(); + return $this->getItem( $this->mDefaultHash ); + } + + /** + * Remove an item + * + * @param string $hash + */ + public function removeItem( $hash ) { + $this->mSize -= strlen( $this->mItems[$hash] ); + unset( $this->mItems[$hash] ); + } + + /** + * Compress the bulk data in the object + */ + public function compress() { + if ( !$this->mCompressed ) { + $this->mItems = gzdeflate( serialize( $this->mItems ) ); + $this->mCompressed = true; + } + } + + /** + * Uncompress bulk data + */ + public function uncompress() { + if ( $this->mCompressed ) { + $this->mItems = unserialize( gzinflate( $this->mItems ) ); + $this->mCompressed = false; + } + } + + /** + * @return array + */ + function __sleep() { + $this->compress(); + return [ 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' ]; + } + + function __wakeup() { + $this->uncompress(); + } + + /** + * Helper function for compression jobs + * Returns true until the object is "full" and ready to be committed + * + * @return bool + */ + public function isHappy() { + return $this->mSize < $this->mMaxSize + && count( $this->mItems ) < $this->mMaxCount; + } +} + +/** + * Pointer object for an item within a CGZ blob stored in the text table. + */ +class HistoryBlobStub { + /** + * @var array One-step cache variable to hold base blobs; operations that + * pull multiple revisions may often pull multiple times from the same + * blob. By keeping the last-used one open, we avoid redundant + * unserialization and decompression overhead. + */ + protected static $blobCache = []; + + /** @var int */ + public $mOldId; + + /** @var string */ + public $mHash; + + /** @var string */ + public $mRef; + + /** + * @param string $hash The content hash of the text + * @param int $oldid The old_id for the CGZ object + */ + function __construct( $hash = '', $oldid = 0 ) { + $this->mHash = $hash; + } + + /** + * Sets the location (old_id) of the main object to which this object + * points + * @param int $id + */ + function setLocation( $id ) { + $this->mOldId = $id; + } + + /** + * Sets the location (old_id) of the referring object + * @param string $id + */ + function setReferrer( $id ) { + $this->mRef = $id; + } + + /** + * Gets the location of the referring object + * @return string + */ + function getReferrer() { + return $this->mRef; + } + + /** + * @return string|false + */ + function getText() { + if ( isset( self::$blobCache[$this->mOldId] ) ) { + $obj = self::$blobCache[$this->mOldId]; + } else { + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( + 'text', + [ 'old_flags', 'old_text' ], + [ 'old_id' => $this->mOldId ] + ); + + if ( !$row ) { + return false; + } + + $flags = explode( ',', $row->old_flags ); + if ( in_array( 'external', $flags ) ) { + $url = $row->old_text; + $parts = explode( '://', $url, 2 ); + if ( !isset( $parts[1] ) || $parts[1] == '' ) { + return false; + } + $row->old_text = ExternalStore::fetchFromURL( $url ); + + } + + if ( !in_array( 'object', $flags ) ) { + return false; + } + + if ( in_array( 'gzip', $flags ) ) { + // This shouldn't happen, but a bug in the compress script + // may at times gzip-compress a HistoryBlob object row. + $obj = unserialize( gzinflate( $row->old_text ) ); + } else { + $obj = unserialize( $row->old_text ); + } + + if ( !is_object( $obj ) ) { + // Correct for old double-serialization bug. + $obj = unserialize( $obj ); + } + + // Save this item for reference; if pulling many + // items in a row we'll likely use it again. + $obj->uncompress(); + self::$blobCache = [ $this->mOldId => $obj ]; + } + + return $obj->getItem( $this->mHash ); + } + + /** + * Get the content hash + * + * @return string + */ + function getHash() { + return $this->mHash; + } +} + +/** + * To speed up conversion from 1.4 to 1.5 schema, text rows can refer to the + * leftover cur table as the backend. This avoids expensively copying hundreds + * of megabytes of data during the conversion downtime. + * + * Serialized HistoryBlobCurStub objects will be inserted into the text table + * on conversion if $wgLegacySchemaConversion is set to true. + */ +class HistoryBlobCurStub { + /** @var int */ + public $mCurId; + + /** + * @param int $curid The cur_id pointed to + */ + function __construct( $curid = 0 ) { + $this->mCurId = $curid; + } + + /** + * Sets the location (cur_id) of the main object to which this object + * points + * + * @param int $id + */ + function setLocation( $id ) { + $this->mCurId = $id; + } + + /** + * @return string|bool + */ + function getText() { + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( 'cur', [ 'cur_text' ], [ 'cur_id' => $this->mCurId ] ); + if ( !$row ) { + return false; + } + return $row->cur_text; + } +} + +/** + * Diff-based history compression + * Requires xdiff 1.5+ and zlib + */ +class DiffHistoryBlob implements HistoryBlob { + /** @var array Uncompressed item cache */ + public $mItems = []; + + /** @var int Total uncompressed size */ + public $mSize = 0; + + /** + * @var array Array of diffs. If a diff D from A to B is notated D = B - A, + * and Z is an empty string: + * + * { item[map[i]] - item[map[i-1]] where i > 0 + * diff[i] = { + * { item[map[i]] - Z where i = 0 + */ + public $mDiffs; + + /** @var array The diff map, see above */ + public $mDiffMap; + + /** @var int The key for getText() + */ + public $mDefaultKey; + + /** @var string Compressed storage */ + public $mCompressed; + + /** @var bool True if the object is locked against further writes */ + public $mFrozen = false; + + /** + * @var int The maximum uncompressed size before the object becomes sad + * Should be less than max_allowed_packet + */ + public $mMaxSize = 10000000; + + /** @var int The maximum number of text items before the object becomes sad */ + public $mMaxCount = 100; + + /** Constants from xdiff.h */ + const XDL_BDOP_INS = 1; + const XDL_BDOP_CPY = 2; + const XDL_BDOP_INSB = 3; + + function __construct() { + if ( !function_exists( 'gzdeflate' ) ) { + throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" ); + } + } + + /** + * @throws MWException + * @param string $text + * @return int + */ + function addItem( $text ) { + if ( $this->mFrozen ) { + throw new MWException( __METHOD__ . ": Cannot add more items after sleep/wakeup" ); + } + + $this->mItems[] = $text; + $this->mSize += strlen( $text ); + $this->mDiffs = null; // later + return count( $this->mItems ) - 1; + } + + /** + * @param string $key + * @return string + */ + function getItem( $key ) { + return $this->mItems[$key]; + } + + /** + * @param string $text + */ + function setText( $text ) { + $this->mDefaultKey = $this->addItem( $text ); + } + + /** + * @return string + */ + function getText() { + return $this->getItem( $this->mDefaultKey ); + } + + /** + * @throws MWException + */ + function compress() { + if ( !function_exists( 'xdiff_string_rabdiff' ) ) { + throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" ); + } + if ( isset( $this->mDiffs ) ) { + // Already compressed + return; + } + if ( !count( $this->mItems ) ) { + // Empty + return; + } + + // Create two diff sequences: one for main text and one for small text + $sequences = [ + 'small' => [ + 'tail' => '', + 'diffs' => [], + 'map' => [], + ], + 'main' => [ + 'tail' => '', + 'diffs' => [], + 'map' => [], + ], + ]; + $smallFactor = 0.5; + + $mItemsCount = count( $this->mItems ); + for ( $i = 0; $i < $mItemsCount; $i++ ) { + $text = $this->mItems[$i]; + if ( $i == 0 ) { + $seqName = 'main'; + } else { + $mainTail = $sequences['main']['tail']; + if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) { + $seqName = 'small'; + } else { + $seqName = 'main'; + } + } + $seq =& $sequences[$seqName]; + $tail = $seq['tail']; + $diff = $this->diff( $tail, $text ); + $seq['diffs'][] = $diff; + $seq['map'][] = $i; + $seq['tail'] = $text; + } + unset( $seq ); // unlink dangerous alias + + // Knit the sequences together + $tail = ''; + $this->mDiffs = []; + $this->mDiffMap = []; + foreach ( $sequences as $seq ) { + if ( !count( $seq['diffs'] ) ) { + continue; + } + if ( $tail === '' ) { + $this->mDiffs[] = $seq['diffs'][0]; + } else { + $head = $this->patch( '', $seq['diffs'][0] ); + $this->mDiffs[] = $this->diff( $tail, $head ); + } + $this->mDiffMap[] = $seq['map'][0]; + $diffsCount = count( $seq['diffs'] ); + for ( $i = 1; $i < $diffsCount; $i++ ) { + $this->mDiffs[] = $seq['diffs'][$i]; + $this->mDiffMap[] = $seq['map'][$i]; + } + $tail = $seq['tail']; + } + } + + /** + * @param string $t1 + * @param string $t2 + * @return string + */ + function diff( $t1, $t2 ) { + # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff + # "String is not zero-terminated" + Wikimedia\suppressWarnings(); + $diff = xdiff_string_rabdiff( $t1, $t2 ) . ''; + Wikimedia\restoreWarnings(); + return $diff; + } + + /** + * @param string $base + * @param string $diff + * @return bool|string + */ + function patch( $base, $diff ) { + if ( function_exists( 'xdiff_string_bpatch' ) ) { + Wikimedia\suppressWarnings(); + $text = xdiff_string_bpatch( $base, $diff ) . ''; + Wikimedia\restoreWarnings(); + return $text; + } + + # Pure PHP implementation + + $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) ); + + # Check the checksum if hash extension is available + $ofp = $this->xdiffAdler32( $base ); + if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) { + wfDebug( __METHOD__ . ": incorrect base checksum\n" ); + return false; + } + if ( $header['csize'] != strlen( $base ) ) { + wfDebug( __METHOD__ . ": incorrect base length\n" ); + return false; + } + + $p = 8; + $out = ''; + while ( $p < strlen( $diff ) ) { + $x = unpack( 'Cop', substr( $diff, $p, 1 ) ); + $op = $x['op']; + ++$p; + switch ( $op ) { + case self::XDL_BDOP_INS: + $x = unpack( 'Csize', substr( $diff, $p, 1 ) ); + $p++; + $out .= substr( $diff, $p, $x['size'] ); + $p += $x['size']; + break; + case self::XDL_BDOP_INSB: + $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) ); + $p += 4; + $out .= substr( $diff, $p, $x['csize'] ); + $p += $x['csize']; + break; + case self::XDL_BDOP_CPY: + $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) ); + $p += 8; + $out .= substr( $base, $x['off'], $x['csize'] ); + break; + default: + wfDebug( __METHOD__ . ": invalid op\n" ); + return false; + } + } + return $out; + } + + /** + * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with + * the bytes backwards and initialised with 0 instead of 1. See T36428. + * + * @param string $s + * @return string|bool False if the hash extension is not available + */ + function xdiffAdler32( $s ) { + if ( !function_exists( 'hash' ) ) { + return false; + } + + static $init; + if ( $init === null ) { + $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02"; + } + + // The real Adler-32 checksum of $init is zero, so it initialises the + // state to zero, as it is at the start of LibXDiff's checksum + // algorithm. Appending the subject string then simulates LibXDiff. + return strrev( hash( 'adler32', $init . $s, true ) ); + } + + function uncompress() { + if ( !$this->mDiffs ) { + return; + } + $tail = ''; + $mDiffsCount = count( $this->mDiffs ); + for ( $diffKey = 0; $diffKey < $mDiffsCount; $diffKey++ ) { + $textKey = $this->mDiffMap[$diffKey]; + $text = $this->patch( $tail, $this->mDiffs[$diffKey] ); + $this->mItems[$textKey] = $text; + $tail = $text; + } + } + + /** + * @return array + */ + function __sleep() { + $this->compress(); + if ( !count( $this->mItems ) ) { + // Empty object + $info = false; + } else { + // Take forward differences to improve the compression ratio for sequences + $map = ''; + $prev = 0; + foreach ( $this->mDiffMap as $i ) { + if ( $map !== '' ) { + $map .= ','; + } + $map .= $i - $prev; + $prev = $i; + } + $info = [ + 'diffs' => $this->mDiffs, + 'map' => $map + ]; + } + if ( isset( $this->mDefaultKey ) ) { + $info['default'] = $this->mDefaultKey; + } + $this->mCompressed = gzdeflate( serialize( $info ) ); + return [ 'mCompressed' ]; + } + + function __wakeup() { + // addItem() doesn't work if mItems is partially filled from mDiffs + $this->mFrozen = true; + $info = unserialize( gzinflate( $this->mCompressed ) ); + unset( $this->mCompressed ); + + if ( !$info ) { + // Empty object + return; + } + + if ( isset( $info['default'] ) ) { + $this->mDefaultKey = $info['default']; + } + $this->mDiffs = $info['diffs']; + if ( isset( $info['base'] ) ) { + // Old format + $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 ); + array_unshift( $this->mDiffs, + pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) . + $info['base'] ); + } else { + // New format + $map = explode( ',', $info['map'] ); + $cur = 0; + $this->mDiffMap = []; + foreach ( $map as $i ) { + $cur += $i; + $this->mDiffMap[] = $cur; + } + } + $this->uncompress(); + } + + /** + * Helper function for compression jobs + * Returns true until the object is "full" and ready to be committed + * + * @return bool + */ + function isHappy() { + return $this->mSize < $this->mMaxSize + && count( $this->mItems ) < $this->mMaxCount; + } + +} + +// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found +if ( false ) { + // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the + // class name coerced to lowercase. We can improve efficiency by adding + // autoload entries for the lowercase variants of these classes (T166759). + // The code below is never executed, but it is picked up by the AutoloadGenerator + // parser, which scans for class_alias() calls. + class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' ); + class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' ); + class_alias( HistoryBlobStub::class, 'historyblobstub' ); +} diff --git a/www/wiki/includes/Hooks.php b/www/wiki/includes/Hooks.php new file mode 100644 index 00000000..c22dc97f --- /dev/null +++ b/www/wiki/includes/Hooks.php @@ -0,0 +1,244 @@ +. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * @author Evan Prodromou + * @see hooks.txt + * @file + */ + +/** + * Hooks class. + * + * Used to supersede $wgHooks, because globals are EVIL. + * + * @since 1.18 + */ +class Hooks { + /** + * Array of events mapped to an array of callbacks to be run + * when that event is triggered. + */ + protected static $handlers = []; + + /** + * Attach an event handler to a given hook. + * + * @param string $name Name of hook + * @param callable $callback Callback function to attach + * + * @since 1.18 + */ + public static function register( $name, $callback ) { + if ( !isset( self::$handlers[$name] ) ) { + self::$handlers[$name] = []; + } + + self::$handlers[$name][] = $callback; + } + + /** + * Clears hooks registered via Hooks::register(). Does not touch $wgHooks. + * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined. + * + * @param string $name The name of the hook to clear. + * + * @since 1.21 + * @throws MWException If not in testing mode. + */ + public static function clear( $name ) { + if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) { + throw new MWException( 'Cannot reset hooks in operation.' ); + } + + unset( self::$handlers[$name] ); + } + + /** + * Returns true if a hook has a function registered to it. + * The function may have been registered either via Hooks::register or in $wgHooks. + * + * @since 1.18 + * + * @param string $name Name of hook + * @return bool True if the hook has a function registered to it + */ + public static function isRegistered( $name ) { + global $wgHooks; + return !empty( $wgHooks[$name] ) || !empty( self::$handlers[$name] ); + } + + /** + * Returns an array of all the event functions attached to a hook + * This combines functions registered via Hooks::register and with $wgHooks. + * + * @since 1.18 + * + * @param string $name Name of the hook + * @return array + */ + public static function getHandlers( $name ) { + global $wgHooks; + + if ( !self::isRegistered( $name ) ) { + return []; + } elseif ( !isset( self::$handlers[$name] ) ) { + return $wgHooks[$name]; + } elseif ( !isset( $wgHooks[$name] ) ) { + return self::$handlers[$name]; + } else { + return array_merge( self::$handlers[$name], $wgHooks[$name] ); + } + } + + /** + * @param string $event Event name + * @param array|callable $hook + * @param array $args Array of parameters passed to hook functions + * @param string|null $deprecatedVersion [optional] + * @param string &$fname [optional] Readable name of hook [returned] + * @return null|string|bool + */ + private static function callHook( $event, $hook, array $args, $deprecatedVersion = null, + &$fname = null + ) { + // Turn non-array values into an array. (Can't use casting because of objects.) + if ( !is_array( $hook ) ) { + $hook = [ $hook ]; + } + + if ( !array_filter( $hook ) ) { + // Either array is empty or it's an array filled with null/false/empty. + return null; + } + + if ( is_array( $hook[0] ) ) { + // First element is an array, meaning the developer intended + // the first element to be a callback. Merge it in so that + // processing can be uniform. + $hook = array_merge( $hook[0], array_slice( $hook, 1 ) ); + } + + /** + * $hook can be: a function, an object, an array of $function and + * $data, an array of just a function, an array of object and + * method, or an array of object, method, and data. + */ + if ( $hook[0] instanceof Closure ) { + $fname = "hook-$event-closure"; + $callback = array_shift( $hook ); + } elseif ( is_object( $hook[0] ) ) { + $object = array_shift( $hook ); + $method = array_shift( $hook ); + + // If no method was specified, default to on$event. + if ( $method === null ) { + $method = "on$event"; + } + + $fname = get_class( $object ) . '::' . $method; + $callback = [ $object, $method ]; + } elseif ( is_string( $hook[0] ) ) { + $fname = $callback = array_shift( $hook ); + } else { + throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" ); + } + + // Run autoloader (workaround for call_user_func_array bug) + // and throw error if not callable. + if ( !is_callable( $callback ) ) { + throw new MWException( 'Invalid callback ' . $fname . ' in hooks for ' . $event . "\n" ); + } + + // mark hook as deprecated, if deprecation version is specified + if ( $deprecatedVersion !== null ) { + wfDeprecated( "$event hook (used in $fname)", $deprecatedVersion ); + } + + // Call the hook. + $hook_args = array_merge( $hook, $args ); + return call_user_func_array( $callback, $hook_args ); + } + + /** + * Call hook functions defined in Hooks::register and $wgHooks. + * + * For the given hook event, fetch the array of hook events and + * process them. Determine the proper callback for each hook and + * then call the actual hook using the appropriate arguments. + * Finally, process the return value and return/throw accordingly. + * + * For hook event that are not abortable through a handler's return value, + * use runWithoutAbort() instead. + * + * @param string $event Event name + * @param array $args Array of parameters passed to hook functions + * @param string|null $deprecatedVersion [optional] Mark hook as deprecated with version number + * @return bool True if no handler aborted the hook + * + * @throws Exception + * @throws FatalError + * @throws MWException + * @since 1.22 A hook function is not required to return a value for + * processing to continue. Not returning a value (or explicitly + * returning null) is equivalent to returning true. + */ + public static function run( $event, array $args = [], $deprecatedVersion = null ) { + foreach ( self::getHandlers( $event ) as $hook ) { + $retval = self::callHook( $event, $hook, $args, $deprecatedVersion ); + if ( $retval === null ) { + continue; + } + + // Process the return value. + if ( is_string( $retval ) ) { + // String returned means error. + throw new FatalError( $retval ); + } elseif ( $retval === false ) { + // False was returned. Stop processing, but no error. + return false; + } + } + + return true; + } + + /** + * Call hook functions defined in Hooks::register and $wgHooks. + * + * @param string $event Event name + * @param array $args Array of parameters passed to hook functions + * @param string|null $deprecatedVersion [optional] Mark hook as deprecated with version number + * @return bool Always true + * @throws MWException If a callback is invalid, unknown + * @throws UnexpectedValueException If a callback returns an abort value. + * @since 1.30 + */ + public static function runWithoutAbort( $event, array $args = [], $deprecatedVersion = null ) { + foreach ( self::getHandlers( $event ) as $hook ) { + $fname = null; + $retval = self::callHook( $event, $hook, $args, $deprecatedVersion, $fname ); + if ( $retval !== null && $retval !== true ) { + throw new UnexpectedValueException( "Invalid return from $fname for unabortable $event." ); + } + } + return true; + } +} diff --git a/www/wiki/includes/Html.php b/www/wiki/includes/Html.php new file mode 100644 index 00000000..3bcf1313 --- /dev/null +++ b/www/wiki/includes/Html.php @@ -0,0 +1,1059 @@ + elements. + * + * @since 1.16 + */ +class Html { + // List of void elements from HTML5, section 8.1.2 as of 2016-09-19 + private static $voidElements = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ]; + + // Boolean attributes, which may have the value omitted entirely. Manually + // collected from the HTML5 spec as of 2011-08-12. + private static $boolAttribs = [ + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'controls', + 'default', + 'defer', + 'disabled', + 'formnovalidate', + 'hidden', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'novalidate', + 'open', + 'pubdate', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'truespeed', + 'typemustmatch', + // HTML5 Microdata + 'itemscope', + ]; + + /** + * Modifies a set of attributes meant for button elements + * and apply a set of default attributes when $wgUseMediaWikiUIEverywhere enabled. + * @param array $attrs HTML attributes in an associative array + * @param string[] $modifiers classes to add to the button + * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers + * @return array $attrs A modified attribute array + */ + public static function buttonAttributes( array $attrs, array $modifiers = [] ) { + global $wgUseMediaWikiUIEverywhere; + if ( $wgUseMediaWikiUIEverywhere ) { + if ( isset( $attrs['class'] ) ) { + if ( is_array( $attrs['class'] ) ) { + $attrs['class'][] = 'mw-ui-button'; + $attrs['class'] = array_merge( $attrs['class'], $modifiers ); + // ensure compatibility with Xml + $attrs['class'] = implode( ' ', $attrs['class'] ); + } else { + $attrs['class'] .= ' mw-ui-button ' . implode( ' ', $modifiers ); + } + } else { + // ensure compatibility with Xml + $attrs['class'] = 'mw-ui-button ' . implode( ' ', $modifiers ); + } + } + return $attrs; + } + + /** + * Modifies a set of attributes meant for text input elements + * and apply a set of default attributes. + * Removes size attribute when $wgUseMediaWikiUIEverywhere enabled. + * @param array $attrs An attribute array. + * @return array $attrs A modified attribute array + */ + public static function getTextInputAttributes( array $attrs ) { + global $wgUseMediaWikiUIEverywhere; + if ( $wgUseMediaWikiUIEverywhere ) { + if ( isset( $attrs['class'] ) ) { + if ( is_array( $attrs['class'] ) ) { + $attrs['class'][] = 'mw-ui-input'; + } else { + $attrs['class'] .= ' mw-ui-input'; + } + } else { + $attrs['class'] = 'mw-ui-input'; + } + } + return $attrs; + } + + /** + * Returns an HTML link element in a string styled as a button + * (when $wgUseMediaWikiUIEverywhere is enabled). + * + * @param string $contents The raw HTML contents of the element: *not* + * escaped! + * @param array $attrs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. + * @param string[] $modifiers classes to add to the button + * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers + * @return string Raw HTML + */ + public static function linkButton( $contents, array $attrs, array $modifiers = [] ) { + return self::element( 'a', + self::buttonAttributes( $attrs, $modifiers ), + $contents + ); + } + + /** + * Returns an HTML link element in a string styled as a button + * (when $wgUseMediaWikiUIEverywhere is enabled). + * + * @param string $contents The raw HTML contents of the element: *not* + * escaped! + * @param array $attrs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. + * @param string[] $modifiers classes to add to the button + * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers + * @return string Raw HTML + */ + public static function submitButton( $contents, array $attrs, array $modifiers = [] ) { + $attrs['type'] = 'submit'; + $attrs['value'] = $contents; + return self::element( 'input', self::buttonAttributes( $attrs, $modifiers ) ); + } + + /** + * Returns an HTML element in a string. The major advantage here over + * manually typing out the HTML is that it will escape all attribute + * values. + * + * This is quite similar to Xml::tags(), but it implements some useful + * HTML-specific logic. For instance, there is no $allowShortTag + * parameter: the closing tag is magically omitted if $element has an empty + * content model. + * + * @param string $element The element's name, e.g., 'a' + * @param array $attribs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. + * @param string $contents The raw HTML contents of the element: *not* + * escaped! + * @return string Raw HTML + */ + public static function rawElement( $element, $attribs = [], $contents = '' ) { + $start = self::openElement( $element, $attribs ); + if ( in_array( $element, self::$voidElements ) ) { + // Silly XML. + return substr( $start, 0, -1 ) . '/>'; + } else { + return "$start$contents" . self::closeElement( $element ); + } + } + + /** + * Identical to rawElement(), but HTML-escapes $contents (like + * Xml::element()). + * + * @param string $element Name of the element, e.g., 'a' + * @param array $attribs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. + * @param string $contents + * + * @return string + */ + public static function element( $element, $attribs = [], $contents = '' ) { + return self::rawElement( $element, $attribs, strtr( $contents, [ + // There's no point in escaping quotes, >, etc. in the contents of + // elements. + '&' => '&', + '<' => '<' + ] ) ); + } + + /** + * Identical to rawElement(), but has no third parameter and omits the end + * tag (and the self-closing '/' in XML mode for empty elements). + * + * @param string $element Name of the element, e.g., 'a' + * @param array $attribs Associative array of attributes, e.g., [ + * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for + * further documentation. + * + * @return string + */ + public static function openElement( $element, $attribs = [] ) { + $attribs = (array)$attribs; + // This is not required in HTML5, but let's do it anyway, for + // consistency and better compression. + $element = strtolower( $element ); + + // Remove invalid input types + if ( $element == 'input' ) { + $validTypes = [ + 'hidden', + 'text', + 'password', + 'checkbox', + 'radio', + 'file', + 'submit', + 'image', + 'reset', + 'button', + + // HTML input types + 'datetime', + 'datetime-local', + 'date', + 'month', + 'time', + 'week', + 'number', + 'range', + 'email', + 'url', + 'search', + 'tel', + 'color', + ]; + if ( isset( $attribs['type'] ) && !in_array( $attribs['type'], $validTypes ) ) { + unset( $attribs['type'] ); + } + } + + // According to standard the default type for