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/maintenance/.htaccess | 1 + www/wiki/maintenance/7zip.inc | 96 + www/wiki/maintenance/CodeCleanerGlobalsPass.inc | 51 + www/wiki/maintenance/Doxyfile | 398 ++ www/wiki/maintenance/Maintenance.php | 1704 +++++++ www/wiki/maintenance/Makefile | 19 + www/wiki/maintenance/README | 103 + www/wiki/maintenance/addRFCandPMIDInterwiki.php | 95 + www/wiki/maintenance/addSite.php | 92 + www/wiki/maintenance/archives/.htaccess | 1 + .../maintenance/archives/patch-actor-table.sql | 57 + www/wiki/maintenance/archives/patch-add-3d.sql | 11 + .../archives/patch-add-cl_collation_ext_index.sql | 2 + ...-add-rc_name_type_patrolled_timestamp_index.sql | 2 + www/wiki/maintenance/archives/patch-ar_deleted.sql | 3 + www/wiki/maintenance/archives/patch-ar_len.sql | 3 + .../maintenance/archives/patch-ar_parent_id.sql | 3 + .../archives/patch-ar_rev_id-not-null.sql | 3 + www/wiki/maintenance/archives/patch-ar_sha1.sql | 3 + .../archives/patch-archive-ar_content_format.sql | 2 + .../archives/patch-archive-ar_content_model.sql | 2 + .../maintenance/archives/patch-archive-ar_id.sql | 8 + .../maintenance/archives/patch-archive-page_id.sql | 6 + .../maintenance/archives/patch-archive-rev_id.sql | 6 + .../maintenance/archives/patch-archive-text_id.sql | 14 + .../archives/patch-archive-user-index.sql | 4 + .../archives/patch-archive_ar_revid.sql | 3 + .../archives/patch-archive_kill_ar_page_revid.sql | 4 + .../maintenance/archives/patch-backlinkindexes.sql | 19 + www/wiki/maintenance/archives/patch-bot.sql | 11 + .../patch-bot_passwords-bp_user-unsigned.sql | 1 + .../maintenance/archives/patch-bot_passwords.sql | 25 + www/wiki/maintenance/archives/patch-cache.sql | 41 + www/wiki/maintenance/archives/patch-cat_hidden.sql | 3 + www/wiki/maintenance/archives/patch-category.sql | 17 + .../patch-categorylinks-better-collation.sql | 19 + .../patch-categorylinks-better-collation2.sql | 12 + .../archives/patch-categorylinks-fix-pk.sql | 1 + .../maintenance/archives/patch-categorylinks.sql | 37 + .../archives/patch-categorylinksindex.sql | 11 + .../archives/patch-change_tag-ct_id.sql | 5 + .../patch-change_tag-ct_log_id-unsigned.sql | 1 + .../patch-change_tag-ct_rev_id-unsigned.sql | 1 + .../archives/patch-change_tag-indexes.sql | 21 + www/wiki/maintenance/archives/patch-change_tag.sql | 15 + .../maintenance/archives/patch-comment-table.sql | 59 + www/wiki/maintenance/archives/patch-content.sql | 21 + .../maintenance/archives/patch-content_models.sql | 10 + .../maintenance/archives/patch-drop-ar_text.sql | 7 + .../archives/patch-drop-page_counter.sql | 2 + .../archives/patch-drop-rc_cur_time.sql | 2 + .../maintenance/archives/patch-drop-ss_admins.sql | 2 + .../archives/patch-drop-ss_total_views.sql | 2 + .../archives/patch-drop-user_options.sql | 1 + .../maintenance/archives/patch-drop_img_type.sql | 3 + .../archives/patch-editsummary-length.sql | 11 + .../archives/patch-email-authentication.sql | 3 + .../archives/patch-email-notification.sql | 11 + .../archives/patch-externallinks-el_id.sql | 8 + .../archives/patch-externallinks-el_index_60.sql | 4 + .../maintenance/archives/patch-externallinks.sql | 13 + www/wiki/maintenance/archives/patch-fa_deleted.sql | 3 + .../archives/patch-fa_major_mime-chemical.sql | 3 + www/wiki/maintenance/archives/patch-fa_sha1.sql | 4 + .../archives/patch-filearchive-user-index.sql | 5 + .../maintenance/archives/patch-filearchive.sql | 51 + .../maintenance/archives/patch-filejournal.sql | 20 + .../maintenance/archives/patch-fix-il_from.sql | 11 + .../archives/patch-il_from_namespace.sql | 4 + .../archives/patch-image-img_description_id.sql | 7 + .../archives/patch-image-user-index-2.sql | 1 + .../archives/patch-image-user-index.sql | 8 + .../archives/patch-image_name_primary.sql | 6 + .../archives/patch-image_name_unique.sql | 6 + .../archives/patch-imagelinks-fix-pk.sql | 1 + www/wiki/maintenance/archives/patch-img_exif.sql | 3 + .../archives/patch-img_major_mime-chemical.sql | 3 + .../archives/patch-img_media_mime-index.sql | 4 + .../maintenance/archives/patch-img_media_type.sql | 17 + .../maintenance/archives/patch-img_metadata.sql | 6 + www/wiki/maintenance/archives/patch-img_sha1.sql | 8 + www/wiki/maintenance/archives/patch-img_width.sql | 18 + www/wiki/maintenance/archives/patch-indexes.sql | 24 + .../maintenance/archives/patch-interwiki-trans.sql | 2 + www/wiki/maintenance/archives/patch-interwiki.sql | 20 + .../archives/patch-inverse_timestamp.sql | 15 + www/wiki/maintenance/archives/patch-ip_changes.sql | 23 + .../archives/patch-ipb-parent-block-id-index.sql | 2 + .../archives/patch-ipb-parent-block-id.sql | 3 + .../archives/patch-ipb_allow_usertalk.sql | 3 + .../maintenance/archives/patch-ipb_anon_only.sql | 44 + .../maintenance/archives/patch-ipb_by_text.sql | 10 + .../maintenance/archives/patch-ipb_deleted.sql | 3 + .../maintenance/archives/patch-ipb_emailban.sql | 4 + www/wiki/maintenance/archives/patch-ipb_expiry.sql | 8 + .../archives/patch-ipb_optional_autoblock.sql | 3 + .../maintenance/archives/patch-ipb_range_start.sql | 25 + www/wiki/maintenance/archives/patch-ipblocks.sql | 6 + .../archives/patch-iw_api_and_wikiid.sql | 9 + .../patch-iwl_prefix_title_from-non-unique.sql | 5 + .../maintenance/archives/patch-iwlinks-fix-pk.sql | 1 + .../archives/patch-iwlinks-from-title-index.sql | 4 + www/wiki/maintenance/archives/patch-iwlinks.sql | 16 + www/wiki/maintenance/archives/patch-job.sql | 20 + .../maintenance/archives/patch-job_attempts.sql | 4 + www/wiki/maintenance/archives/patch-job_token.sql | 9 + .../archives/patch-jobs-add-timestamp.sql | 2 + .../archives/patch-kill-cl_collation_index.sql | 7 + .../maintenance/archives/patch-kill-iwl_prefix.sql | 7 + .../archives/patch-l10n_cache-primary-key.sql | 8 + www/wiki/maintenance/archives/patch-l10n_cache.sql | 8 + .../archives/patch-langlinks-fix-pk.sql | 1 + .../archives/patch-langlinks-ll_lang-20.sql | 3 + www/wiki/maintenance/archives/patch-langlinks.sql | 14 + .../maintenance/archives/patch-linkscc-1.3.sql | 6 + www/wiki/maintenance/archives/patch-linkscc.sql | 12 + www/wiki/maintenance/archives/patch-linktables.sql | 70 + .../maintenance/archives/patch-log_deleted.sql | 3 + www/wiki/maintenance/archives/patch-log_id.sql | 8 + www/wiki/maintenance/archives/patch-log_params.sql | 1 + .../archives/patch-log_search-fix-pk.sql | 1 + www/wiki/maintenance/archives/patch-log_search.sql | 10 + .../maintenance/archives/patch-log_user_text.sql | 8 + .../archives/patch-logging-times-index.sql | 9 + .../maintenance/archives/patch-logging-title.sql | 6 + .../archives/patch-logging-type-action-index.sql | 1 + www/wiki/maintenance/archives/patch-logging.sql | 37 + .../patch-logging_user_text_time_index.sql | 1 + .../patch-logging_user_text_type_time_index.sql | 1 + .../archives/patch-mime_minor_length.sql | 10 + .../archives/patch-mimesearch-indexes.sql | 22 + .../archives/patch-module_deps-fix-pk.sql | 1 + .../maintenance/archives/patch-module_deps.sql | 12 + .../archives/patch-nullable-ar_text.sql | 13 + .../archives/patch-objectcache-fix-pk.sql | 1 + .../maintenance/archives/patch-objectcache.sql | 9 + .../archives/patch-oi_major_mime-chemical.sql | 3 + .../maintenance/archives/patch-oi_metadata.sql | 17 + .../maintenance/archives/patch-oldestindex.sql | 5 + .../archives/patch-oldimage-user-index.sql | 8 + .../archives/patch-page-page_content_model.sql | 2 + www/wiki/maintenance/archives/patch-page_lang.sql | 2 + www/wiki/maintenance/archives/patch-page_len.sql | 16 + .../archives/patch-page_links_updated.sql | 2 + .../patch-page_props-propname-page-index.sql | 4 + www/wiki/maintenance/archives/patch-page_props.sql | 9 + .../archives/patch-page_redirect_namespace_len.sql | 6 + .../patch-page_restrictions-pr_user-unsigned.sql | 1 + .../archives/patch-page_restrictions.sql | 20 + .../archives/patch-page_restrictions_sortkey.sql | 8 + .../archives/patch-pagelinks-fix-pk.sql | 1 + www/wiki/maintenance/archives/patch-pagelinks.sql | 56 + .../maintenance/archives/patch-parsercache.sql | 15 + .../archives/patch-pl-tl-il-nonunique.sql | 11 + .../archives/patch-pl_from_namespace.sql | 4 + www/wiki/maintenance/archives/patch-pp_sortkey.sql | 8 + .../archives/patch-profiling-memory.sql | 2 + www/wiki/maintenance/archives/patch-profiling.sql | 12 + .../archives/patch-protected_titles.sql | 12 + .../archives/patch-pt_title-encoding.sql | 5 + www/wiki/maintenance/archives/patch-querycache.sql | 16 + .../archives/patch-querycache_info-fix-pk.sql | 1 + .../maintenance/archives/patch-querycacheinfo.sql | 12 + .../maintenance/archives/patch-querycachetwo.sql | 22 + .../archives/patch-random-dateindex.sql | 54 + .../maintenance/archives/patch-rc-newindex.sql | 9 + www/wiki/maintenance/archives/patch-rc-patrol.sql | 9 + www/wiki/maintenance/archives/patch-rc_deleted.sql | 8 + www/wiki/maintenance/archives/patch-rc_id.sql | 7 + www/wiki/maintenance/archives/patch-rc_ip.sql | 7 + .../maintenance/archives/patch-rc_ip_modify.sql | 1 + www/wiki/maintenance/archives/patch-rc_len.sql | 9 + www/wiki/maintenance/archives/patch-rc_moved.sql | 4 + www/wiki/maintenance/archives/patch-rc_source.sql | 16 + www/wiki/maintenance/archives/patch-rc_type.sql | 9 + .../archives/patch-rc_user_text-index.sql | 7 + .../maintenance/archives/patch-rd_interwiki.sql | 6 + .../archives/patch-recentchanges-nttindex.sql | 11 + .../archives/patch-recentchanges-utindex.sql | 4 + www/wiki/maintenance/archives/patch-redirect.sql | 28 + .../patch-rename-ar_usertext_timestamp.sql | 7 + .../archives/patch-rename-iwl_prefix.sql | 4 + .../patch-rename-user_groups-and_rights.sql | 9 + .../maintenance/archives/patch-rev_deleted.sql | 11 + www/wiki/maintenance/archives/patch-rev_len.sql | 3 + .../maintenance/archives/patch-rev_parent_id.sql | 9 + www/wiki/maintenance/archives/patch-rev_sha1.sql | 3 + .../archives/patch-rev_text_id-default.sql | 10 + .../maintenance/archives/patch-rev_text_id.sql | 17 + .../patch-revision-page-rev-index-nonunique.sql | 5 + .../archives/patch-revision-rev_content_format.sql | 2 + .../archives/patch-revision-rev_content_model.sql | 2 + .../archives/patch-revision-user-page-index.sql | 4 + .../maintenance/archives/patch-searchindex.sql | 40 + .../archives/patch-site_stats-fix-pk.sql | 1 + .../archives/patch-site_stats-modify.sql | 7 + www/wiki/maintenance/archives/patch-sites.sql | 71 + .../maintenance/archives/patch-slot-origin.sql | 15 + www/wiki/maintenance/archives/patch-slot_roles.sql | 10 + www/wiki/maintenance/archives/patch-slots.sql | 25 + .../maintenance/archives/patch-ss_active_users.sql | 3 + www/wiki/maintenance/archives/patch-ss_images.sql | 5 + .../archives/patch-ss_total_articles.sql | 6 + .../archives/patch-tag_summary-ts_id.sql | 5 + .../patch-tag_summary-ts_log_id-unsigned.sql | 1 + .../patch-tag_summary-ts_rev_id-unsigned.sql | 1 + .../maintenance/archives/patch-tag_summary.sql | 12 + .../maintenance/archives/patch-tc-timestamp.sql | 4 + .../archives/patch-templatelinks-fix-pk.sql | 1 + .../maintenance/archives/patch-templatelinks.sql | 18 + www/wiki/maintenance/archives/patch-testrun.sql | 35 + .../maintenance/archives/patch-text-fix-pk.sql | 1 + .../archives/patch-tl_from_namespace.sql | 4 + .../archives/patch-transcache-fix-pk.sql | 1 + www/wiki/maintenance/archives/patch-transcache.sql | 7 + .../patch-ufg_group-length-increase-255.sql | 2 + .../patch-ug_group-length-increase-255.sql | 2 + www/wiki/maintenance/archives/patch-ul_value.sql | 4 + .../maintenance/archives/patch-up_property.sql | 4 + www/wiki/maintenance/archives/patch-updatelog.sql | 4 + .../archives/patch-uploadstash-us_props.sql | 2 + .../maintenance/archives/patch-uploadstash.sql | 48 + .../archives/patch-uploadstash_chunk.sql | 3 + .../archives/patch-user-newtalk-timestamp-null.sql | 1 + .../maintenance/archives/patch-user-realname.sql | 5 + .../maintenance/archives/patch-user_editcount.sql | 5 + .../archives/patch-user_email_index.sql | 1 + .../archives/patch-user_email_token.sql | 12 + .../archives/patch-user_former_groups-fix-pk.sql | 1 + .../archives/patch-user_former_groups.sql | 9 + .../archives/patch-user_groups-primary-key.sql | 5 + .../archives/patch-user_groups-ug_expiry.sql | 5 + .../maintenance/archives/patch-user_groups.sql | 25 + .../archives/patch-user_last_timestamp.sql | 3 + .../maintenance/archives/patch-user_nameindex.sql | 13 + .../archives/patch-user_newpass_time.sql | 4 + .../patch-user_newtalk-user_id-unsigned.sql | 1 + .../archives/patch-user_password_expire.sql | 3 + .../archives/patch-user_properties-fix-pk.sql | 1 + .../patch-user_properties-up_user-unsigned.sql | 1 + .../maintenance/archives/patch-user_properties.sql | 22 + .../archives/patch-user_registration.sql | 9 + .../maintenance/archives/patch-user_rights.sql | 21 + www/wiki/maintenance/archives/patch-user_token.sql | 15 + www/wiki/maintenance/archives/patch-userindex.sql | 1 + www/wiki/maintenance/archives/patch-userlevels.sql | 8 + .../maintenance/archives/patch-usernewtalk.sql | 20 + www/wiki/maintenance/archives/patch-valid_tag.sql | 4 + .../maintenance/archives/patch-watchlist-null.sql | 9 + ...-watchlist-user-notificationtimestamp-index.sql | 4 + .../maintenance/archives/patch-watchlist-wl_id.sql | 5 + www/wiki/maintenance/archives/patch-watchlist.sql | 30 + www/wiki/maintenance/archives/upgradeLogging.php | 219 + www/wiki/maintenance/attachLatest.php | 92 + www/wiki/maintenance/backup.inc | 423 ++ www/wiki/maintenance/benchmarks/Benchmarker.php | 165 + www/wiki/maintenance/benchmarks/README.md | 15 + .../benchmarks/australia-untidy.html.gz | Bin 0 -> 120864 bytes .../maintenance/benchmarks/bench_HTTP_HTTPS.php | 63 + .../benchmarks/bench_Wikimedia_base_convert.php | 77 + .../benchmarks/bench_delete_truncate.php | 105 + .../maintenance/benchmarks/bench_if_switch.php | 110 + .../benchmarks/bench_strtr_str_replace.php | 74 + .../benchmarks/bench_utf8_title_check.php | 114 + .../maintenance/benchmarks/bench_wfIsWindows.php | 68 + .../maintenance/benchmarks/benchmarkCSSMin.php | 76 + www/wiki/maintenance/benchmarks/benchmarkHooks.php | 73 + .../maintenance/benchmarks/benchmarkJSMinPlus.php | 62 + .../maintenance/benchmarks/benchmarkLruHash.php | 95 + www/wiki/maintenance/benchmarks/benchmarkParse.php | 192 + www/wiki/maintenance/benchmarks/benchmarkPurge.php | 118 + .../maintenance/benchmarks/benchmarkSanitizer.php | 99 + www/wiki/maintenance/benchmarks/benchmarkTidy.php | 78 + www/wiki/maintenance/benchmarks/cssmin/circle.svg | 4 + www/wiki/maintenance/benchmarks/cssmin/styles.css | 32 + www/wiki/maintenance/benchmarks/cssmin/wiki.png | Bin 0 -> 22589 bytes www/wiki/maintenance/cdb.php | 132 + www/wiki/maintenance/changePassword.php | 73 + www/wiki/maintenance/checkBadRedirects.php | 64 + www/wiki/maintenance/checkComposerLockUpToDate.php | 65 + www/wiki/maintenance/checkImages.php | 86 + www/wiki/maintenance/checkLess.php | 66 + www/wiki/maintenance/checkUsernames.php | 69 + www/wiki/maintenance/cleanupAncientTables.php | 114 + www/wiki/maintenance/cleanupBlocks.php | 152 + www/wiki/maintenance/cleanupCaps.php | 173 + www/wiki/maintenance/cleanupEmptyCategories.php | 203 + www/wiki/maintenance/cleanupImages.php | 224 + www/wiki/maintenance/cleanupInvalidDbKeys.php | 311 ++ www/wiki/maintenance/cleanupPreferences.php | 157 + www/wiki/maintenance/cleanupRemovedModules.php | 81 + www/wiki/maintenance/cleanupSpam.php | 160 + www/wiki/maintenance/cleanupTable.inc | 174 + www/wiki/maintenance/cleanupTitles.php | 199 + www/wiki/maintenance/cleanupUploadStash.php | 156 + www/wiki/maintenance/cleanupUsersWithNoId.php | 212 + www/wiki/maintenance/cleanupWatchlist.php | 99 + www/wiki/maintenance/clearInterwikiCache.php | 58 + www/wiki/maintenance/commandLine.inc | 71 + www/wiki/maintenance/compareParserCache.php | 112 + www/wiki/maintenance/compareParsers.php | 189 + .../maintenance/convertExtensionToRegistration.php | 312 ++ www/wiki/maintenance/convertLinks.php | 306 ++ www/wiki/maintenance/convertUserOptions.php | 124 + www/wiki/maintenance/copyFileBackend.php | 378 ++ www/wiki/maintenance/copyJobQueue.php | 98 + www/wiki/maintenance/createAndPromote.php | 154 + www/wiki/maintenance/createCommonPasswordCdb.php | 118 + www/wiki/maintenance/deleteArchivedFiles.php | 134 + www/wiki/maintenance/deleteArchivedRevisions.php | 65 + www/wiki/maintenance/deleteAutoPatrolLogs.php | 198 + www/wiki/maintenance/deleteBatch.php | 127 + www/wiki/maintenance/deleteDefaultMessages.php | 105 + www/wiki/maintenance/deleteEqualMessages.php | 206 + www/wiki/maintenance/deleteOldRevisions.php | 103 + www/wiki/maintenance/deleteOrphanedRevisions.php | 102 + www/wiki/maintenance/deleteSelfExternals.php | 57 + www/wiki/maintenance/dev/README | 7 + www/wiki/maintenance/dev/includes/php.sh | 14 + www/wiki/maintenance/dev/includes/require-php.sh | 8 + www/wiki/maintenance/dev/includes/router.php | 97 + www/wiki/maintenance/dev/install.sh | 8 + www/wiki/maintenance/dev/installmw.sh | 18 + www/wiki/maintenance/dev/installphp.sh | 58 + www/wiki/maintenance/dev/start.sh | 14 + www/wiki/maintenance/dictionary/mediawiki.dic | 4664 ++++++++++++++++++++ www/wiki/maintenance/doMaintenance.php | 113 + www/wiki/maintenance/dumpBackup.php | 137 + www/wiki/maintenance/dumpCategoriesAsRdf.php | 184 + www/wiki/maintenance/dumpIterator.php | 189 + www/wiki/maintenance/dumpLinks.php | 79 + www/wiki/maintenance/dumpTextPass.php | 992 +++++ www/wiki/maintenance/dumpUploads.php | 128 + www/wiki/maintenance/edit.php | 107 + www/wiki/maintenance/eraseArchivedFile.php | 119 + www/wiki/maintenance/eval.php | 93 + www/wiki/maintenance/exportSites.php | 56 + www/wiki/maintenance/fetchText.php | 96 + www/wiki/maintenance/fileOpPerfTest.php | 145 + www/wiki/maintenance/findDeprecated.php | 206 + www/wiki/maintenance/findHooks.php | 353 ++ www/wiki/maintenance/findMissingFiles.php | 119 + www/wiki/maintenance/findOrphanedFiles.php | 155 + .../maintenance/fixDefaultJsonContentPages.php | 128 + www/wiki/maintenance/fixDoubleRedirects.php | 140 + .../maintenance/fixExtLinksProtocolRelative.php | 99 + www/wiki/maintenance/fixTimestamps.php | 129 + www/wiki/maintenance/fixUserRegistration.php | 95 + www/wiki/maintenance/formatInstallDoc.php | 76 + www/wiki/maintenance/generateJsonI18n.php | 196 + www/wiki/maintenance/generateLocalAutoload.php | 22 + www/wiki/maintenance/generateSitemap.php | 559 +++ www/wiki/maintenance/getConfiguration.php | 196 + www/wiki/maintenance/getLagTimes.php | 79 + www/wiki/maintenance/getReplicaServer.php | 55 + www/wiki/maintenance/getSlaveServer.php | 3 + www/wiki/maintenance/getText.php | 66 + www/wiki/maintenance/hhvm/makeRepo.php | 161 + www/wiki/maintenance/hhvm/run-server | 28 + www/wiki/maintenance/hhvm/server.conf | 30 + www/wiki/maintenance/importDump.php | 350 ++ www/wiki/maintenance/importImages.php | 523 +++ www/wiki/maintenance/importSiteScripts.php | 118 + www/wiki/maintenance/importSites.php | 54 + www/wiki/maintenance/importTextFiles.php | 208 + www/wiki/maintenance/initEditCount.php | 191 + www/wiki/maintenance/initSiteStats.php | 82 + www/wiki/maintenance/initUserPreference.php | 84 + www/wiki/maintenance/install.php | 175 + www/wiki/maintenance/interwiki.list | 68 + www/wiki/maintenance/interwiki.sql | 71 + www/wiki/maintenance/invalidateUserSessions.php | 94 + www/wiki/maintenance/jsduck/categories.json | 136 + www/wiki/maintenance/jsduck/custom_tags.rb | 102 + www/wiki/maintenance/jsduck/eg-iframe.html | 117 + www/wiki/maintenance/jsduck/external.js | 43 + www/wiki/maintenance/jsparse.php | 77 + www/wiki/maintenance/lag.php | 72 + www/wiki/maintenance/language/StatOutputs.php | 146 + www/wiki/maintenance/language/alltrans.php | 47 + .../maintenance/language/checkDupeMessages.php | 137 + www/wiki/maintenance/language/checkExtensions.php | 40 + www/wiki/maintenance/language/checkLanguage.inc | 781 ++++ www/wiki/maintenance/language/checkLanguage.php | 40 + www/wiki/maintenance/language/date-formats.php | 82 + www/wiki/maintenance/language/digit2html.php | 69 + www/wiki/maintenance/language/dumpMessages.php | 52 + .../maintenance/language/generateCollationData.php | 463 ++ .../language/generateNormalizerDataAr.php | 131 + .../language/generateNormalizerDataMl.php | 70 + www/wiki/maintenance/language/langmemusage.php | 65 + www/wiki/maintenance/language/languages.inc | 827 ++++ www/wiki/maintenance/language/listVariants.php | 73 + www/wiki/maintenance/language/transstat.php | 152 + www/wiki/maintenance/language/zhtable/Makefile | 2 + www/wiki/maintenance/language/zhtable/Makefile.py | 452 ++ www/wiki/maintenance/language/zhtable/README | 35 + .../maintenance/language/zhtable/simp2trad.manual | 413 ++ .../language/zhtable/simp2trad_noconvert.manual | 18 + .../language/zhtable/simp2trad_supp_set.manual | 2 + .../language/zhtable/simpphrases.manual | 266 ++ .../language/zhtable/simpphrases_exclude.manual | 33 + .../maintenance/language/zhtable/symme_supp.manual | 27 + www/wiki/maintenance/language/zhtable/toCN.manual | 2693 +++++++++++ www/wiki/maintenance/language/zhtable/toHK.manual | 3057 +++++++++++++ .../maintenance/language/zhtable/toSimp.manual | 280 ++ www/wiki/maintenance/language/zhtable/toTW.manual | 797 ++++ .../maintenance/language/zhtable/toTrad.manual | 561 +++ .../maintenance/language/zhtable/trad2simp.manual | 978 ++++ .../language/zhtable/trad2simp_noconvert.manual | 19 + .../language/zhtable/trad2simp_supp_set.manual | 3 + .../language/zhtable/tradphrases.manual | 3741 ++++++++++++++++ .../language/zhtable/tradphrases_exclude.manual | 783 ++++ www/wiki/maintenance/locking/file_locks.sql | 11 + www/wiki/maintenance/makeTestEdits.php | 68 + www/wiki/maintenance/manageJobs.php | 97 + www/wiki/maintenance/mcc.php | 226 + www/wiki/maintenance/mctest.php | 106 + www/wiki/maintenance/mergeMessageFileList.php | 208 + www/wiki/maintenance/migrateActors.php | 550 +++ www/wiki/maintenance/migrateArchiveText.php | 159 + www/wiki/maintenance/migrateComments.php | 294 ++ www/wiki/maintenance/migrateFileRepoLayout.php | 239 + www/wiki/maintenance/migrateUserGroup.php | 109 + www/wiki/maintenance/minify.php | 133 + www/wiki/maintenance/moveBatch.php | 125 + .../mssql/archives/patch-actor-table.sql | 53 + .../maintenance/mssql/archives/patch-add-3d.sql | 27 + .../archives/patch-add-cl_collation_ext_index.sql | 2 + .../mssql/archives/patch-alter-table-oldimage.sql | 1 + .../mssql/archives/patch-ar_rev_id-not-null.sql | 1 + .../mssql/archives/patch-archive-drop-fks.sql | 59 + .../mssql/archives/patch-bot_passwords.sql | 13 + .../archives/patch-categorylinks-constraints.sql | 20 + .../mssql/archives/patch-change_tag-ct_id.sql | 4 + .../mssql/archives/patch-comment-table.sql | 57 + .../maintenance/mssql/archives/patch-content.sql | 21 + .../mssql/archives/patch-content_models.sql | 11 + .../mssql/archives/patch-drop-ar_text.sql | 21 + .../mssql/archives/patch-drop-page_counter.sql | 19 + .../mssql/archives/patch-drop-rc_cur_time.sql | 19 + .../mssql/archives/patch-drop-ss_total_views.sql | 19 + .../mssql/archives/patch-drop-user_options.sql | 19 + .../archives/patch-fa_major_mime-chemical.sql | 4 + .../archives/patch-filearchive-constraints.sql | 34 + .../mssql/archives/patch-filearchive-schema.sql | 120 + .../mssql/archives/patch-il_from_namespace.sql | 4 + .../mssql/archives/patch-image-constraints.sql | 34 + .../archives/patch-image-img_description_id.sql | 6 + .../mssql/archives/patch-image-schema.sql | 84 + .../archives/patch-img_major_mime-chemical.sql | 4 + .../archives/patch-kill-cl_collation_index.sql | 7 + .../mssql/archives/patch-logging-drop-fks.sql | 37 + .../archives/patch-oi_major_mime-chemical.sql | 4 + .../mssql/archives/patch-oldimage-constraints.sql | 34 + .../mssql/archives/patch-oldimage-schema.sql | 91 + .../mssql/archives/patch-page_page_lang.sql | 1 + .../mssql/archives/patch-pl_from_namespace.sql | 4 + .../mssql/archives/patch-pp_sortkey.sql | 8 + .../mssql/archives/patch-rc_patrolled_type.sql | 22 + .../archives/patch-recentchanges-drop-fks.sql | 76 + .../mssql/archives/patch-rev_text_id-default.sql | 10 + .../mssql/archives/patch-site_stats-modify.sql | 32 + .../mssql/archives/patch-site_stats-pk.sql | 2 + .../mssql/archives/patch-slot-origin.sql | 14 + .../mssql/archives/patch-slot_roles.sql | 10 + .../maintenance/mssql/archives/patch-slots.sql | 25 + .../mssql/archives/patch-tag_summary-ts_id.sql | 4 + .../mssql/archives/patch-tl_from_namespace.sql | 4 + .../archives/patch-uploadstash-constraints.sql | 20 + .../mssql/archives/patch-user_groups-ug_expiry.sql | 6 + .../mssql/archives/patch-user_password_expires.sql | 1 + .../mssql/archives/patch-watchlist-wl_id.sql | 2 + www/wiki/maintenance/mssql/tables.sql | 1509 +++++++ www/wiki/maintenance/mssql/update-keys.sql | 31 + www/wiki/maintenance/mwdoc-filter.php | 101 + www/wiki/maintenance/mwdocgen.php | 169 + www/wiki/maintenance/mwjsduck-gen | 4 + www/wiki/maintenance/namespaceDupes.php | 620 +++ www/wiki/maintenance/nukeNS.php | 122 + www/wiki/maintenance/nukePage.php | 119 + .../maintenance/oracle/alterSharedConstraints.php | 97 + .../oracle/archives/patch-actor-table.sql | 64 + ...-add-rc_name_type_patrolled_timestamp_index.sql | 4 + .../oracle/archives/patch-ar_rev_id-not-null.sql | 5 + .../oracle/archives/patch-ar_sha1_field.sql | 3 + .../archives/patch-archive-ar_content_format.sql | 3 + .../archives/patch-archive-ar_content_model.sql | 3 + .../oracle/archives/patch-archive-ar_id.sql | 6 + .../archives/patch-auto_increment_triggers.sql | 144 + .../oracle/archives/patch-cat_hidden.sql | 4 + .../oracle/archives/patch-change_tag-ct_id.sql | 6 + .../oracle/archives/patch-comment-table.sql | 68 + .../maintenance/oracle/archives/patch-content.sql | 18 + .../oracle/archives/patch-content_models.sql | 18 + .../oracle/archives/patch-drop-ar_text.sql | 7 + .../oracle/archives/patch-externallinks-el_id.sql | 4 + .../archives/patch-externallinks-el_index_60.sql | 5 + .../maintenance/oracle/archives/patch-fa_sha1.sql | 5 + .../archives/patch-image-img_description_id.sql | 7 + .../oracle/archives/patch-ipblocks_i05_index.sql | 4 + .../oracle/archives/patch-job_attempts.sql | 4 + .../oracle/archives/patch-job_timestamp_field.sql | 4 + .../oracle/archives/patch-job_timestamp_index.sql | 4 + .../oracle/archives/patch-job_token.sql | 12 + .../archives/patch-logging_type_action_index.sql | 4 + .../patch-logging_user_text_time_index.sql | 4 + .../patch-logging_user_text_type_time_index.sql | 4 + .../archives/patch-page-page_content_model.sql | 3 + .../oracle/archives/patch-page-page_lang.sql | 3 + .../oracle/archives/patch-page_links_updated.sql | 4 + .../archives/patch-page_redirect_namespace_len.sql | 4 + .../archives/patch-page_restrictions_pkuk_fix.sql | 7 + .../maintenance/oracle/archives/patch-rc_moved.sql | 4 + .../oracle/archives/patch-rc_source.sql | 3 + .../archives/patch-recentchanges-nttindex.sql | 4 + .../oracle/archives/patch-rev_sha1_field.sql | 4 + .../archives/patch-revision-rev_content_format.sql | 3 + .../archives/patch-revision-rev_content_model.sql | 3 + .../oracle/archives/patch-revision_i05_index.sql | 4 + .../oracle/archives/patch-site_stats-modify.sql | 7 + .../oracle/archives/patch-site_stats-pk.sql | 4 + .../maintenance/oracle/archives/patch-sites.sql | 34 + .../oracle/archives/patch-slot-origin.sql | 14 + .../oracle/archives/patch-slot_roles.sql | 17 + .../maintenance/oracle/archives/patch-slots.sql | 10 + .../oracle/archives/patch-ss_admins.sql | 4 + .../oracle/archives/patch-tag_summary-ts_id.sql | 6 + .../maintenance/oracle/archives/patch-testrun.sql | 37 + .../patch-ufg_group-length-increase-255.sql | 9 + .../patch-ug_group-length-increase-255.sql | 9 + .../oracle/archives/patch-up_property.sql | 3 + .../oracle/archives/patch-uploadstash-us_props.sql | 4 + .../oracle/archives/patch-uploadstash.sql | 25 + .../oracle/archives/patch-us_chunk_inx_field.sql | 4 + .../oracle/archives/patch-user_email_index.sql | 4 + .../oracle/archives/patch-user_former_groups.sql | 9 + .../archives/patch-user_groups-ug_expiry.sql | 8 + .../oracle/archives/patch-user_password_expire.sql | 3 + .../oracle/archives/patch-watchlist-wl_id.sql | 6 + .../oracle/archives/patch_16_17_schema_changes.sql | 84 + .../oracle/archives/patch_create_17_functions.sql | 125 + .../oracle/archives/patch_fk_rename_deferred.sql | 40 + .../oracle/archives/patch_namespace_defaults.sql | 17 + .../oracle/archives/patch_rebuild_dupfunc.sql | 149 + .../archives/patch_recentchanges_fk2_cascade.sql | 5 + .../archives/patch_remove_not_null_empty_defs.sql | 9 + .../archives/patch_remove_not_null_empty_defs2.sql | 3 + .../maintenance/oracle/patch_seq_names_pre1.16.sql | 8 + www/wiki/maintenance/oracle/tables.sql | 1266 ++++++ www/wiki/maintenance/oracle/update-keys.sql | 29 + www/wiki/maintenance/oracle/user.sql | 18 + www/wiki/maintenance/orphans.php | 258 ++ www/wiki/maintenance/pageExists.php | 53 + www/wiki/maintenance/parse.php | 144 + www/wiki/maintenance/patchSql.php | 70 + www/wiki/maintenance/populateArchiveRevId.php | 177 + www/wiki/maintenance/populateBacklinkNamespace.php | 98 + www/wiki/maintenance/populateCategory.php | 154 + www/wiki/maintenance/populateContentModel.php | 254 ++ www/wiki/maintenance/populateFilearchiveSha1.php | 108 + www/wiki/maintenance/populateImageSha1.php | 182 + www/wiki/maintenance/populateInterwiki.php | 156 + www/wiki/maintenance/populateIpChanges.php | 153 + www/wiki/maintenance/populateLogSearch.php | 203 + www/wiki/maintenance/populateLogUsertext.php | 96 + www/wiki/maintenance/populatePPSortKey.php | 104 + www/wiki/maintenance/populateParentId.php | 131 + .../maintenance/populateRecentChangesSource.php | 108 + www/wiki/maintenance/populateRevisionLength.php | 168 + www/wiki/maintenance/populateRevisionSha1.php | 219 + .../postgres/archives/patch-actor-table.sql | 24 + .../postgres/archives/patch-add_interwiki.sql | 14 + .../postgres/archives/patch-ar_rev_id-not-null.sql | 3 + .../postgres/archives/patch-bot_passwords.sql | 9 + .../postgres/archives/patch-category.sql | 15 + .../patch-categorylinks-better-collation.sql | 8 + .../postgres/archives/patch-change_tag.sql | 11 + .../postgres/archives/patch-comment-table.sql | 27 + .../postgres/archives/patch-content-table.sql | 8 + .../archives/patch-content_models-table.sql | 7 + .../postgres/archives/patch-drop-ar_text.sql | 8 + .../postgres/archives/patch-ip_changes.sql | 10 + .../postgres/archives/patch-iwlinks.sql | 8 + .../postgres/archives/patch-kill-iwl_prefix.sql | 7 + .../postgres/archives/patch-l10n_cache.sql | 8 + .../postgres/archives/patch-log_search.sql | 9 + .../postgres/archives/patch-module_deps.sql | 7 + .../maintenance/postgres/archives/patch-page.sql | 24 + .../postgres/archives/patch-page_deleted.sql | 11 + .../postgres/archives/patch-page_props.sql | 9 + .../postgres/archives/patch-page_restrictions.sql | 10 + .../postgres/archives/patch-profiling.sql | 8 + .../postgres/archives/patch-protected_titles.sql | 10 + .../postgres/archives/patch-querycachetwo.sql | 12 + .../postgres/archives/patch-rc_cur_id-not-null.sql | 1 + .../postgres/archives/patch-redirect.sql | 7 + .../postgres/archives/patch-remove-archive2.sql | 3 + .../postgres/archives/patch-rename-iwl_prefix.sql | 2 + .../archives/patch-revision_rev_user_fkey.sql | 4 + .../postgres/archives/patch-site_stats-modify.sql | 7 + .../postgres/archives/patch-site_stats-pk.sql | 3 + .../maintenance/postgres/archives/patch-sites.sql | 31 + .../postgres/archives/patch-slot_roles-table.sql | 7 + .../postgres/archives/patch-slots-table.sql | 9 + .../postgres/archives/patch-tag_summary.sql | 9 + .../postgres/archives/patch-testrun.sql | 30 + .../archives/patch-textsearch_bug66650.sql | 5 + .../postgres/archives/patch-ts2pagetitle.sql | 13 + .../postgres/archives/patch-tsearch2funcs.sql | 29 + .../postgres/archives/patch-update_sequences.sql | 20 + .../postgres/archives/patch-updatelog.sql | 4 + .../postgres/archives/patch-uploadstash.sql | 24 + .../archives/patch-uploadstash_sequence.sql | 2 + .../postgres/archives/patch-user_former_groups.sql | 5 + .../postgres/archives/patch-user_properties.sql | 8 + .../postgres/archives/patch-valid_tag.sql | 3 + www/wiki/maintenance/postgres/compare_schemas.pl | 567 +++ .../postgres/mediawiki_mysql2postgres.pl | 441 ++ www/wiki/maintenance/postgres/tables.sql | 884 ++++ www/wiki/maintenance/postgres/update-keys.sql | 34 + www/wiki/maintenance/preprocessDump.php | 98 + www/wiki/maintenance/preprocessorFuzzTest.php | 274 ++ www/wiki/maintenance/protect.php | 93 + www/wiki/maintenance/pruneFileCache.php | 111 + www/wiki/maintenance/purgeChangedFiles.php | 262 ++ www/wiki/maintenance/purgeChangedPages.php | 194 + www/wiki/maintenance/purgeExpiredUserrights.php | 49 + www/wiki/maintenance/purgeList.php | 147 + www/wiki/maintenance/purgeModuleDeps.php | 72 + www/wiki/maintenance/purgeOldText.php | 45 + www/wiki/maintenance/purgePage.php | 78 + www/wiki/maintenance/purgeParserCache.php | 97 + www/wiki/maintenance/reassignEdits.php | 232 + www/wiki/maintenance/rebuildFileCache.php | 187 + www/wiki/maintenance/rebuildImages.php | 237 + www/wiki/maintenance/rebuildLocalisationCache.php | 181 + www/wiki/maintenance/rebuildSitesCache.php | 68 + www/wiki/maintenance/rebuildall.php | 67 + www/wiki/maintenance/rebuildmessages.php | 57 + www/wiki/maintenance/rebuildrecentchanges.php | 520 +++ www/wiki/maintenance/rebuildtextindex.php | 165 + www/wiki/maintenance/recountCategories.php | 172 + www/wiki/maintenance/refreshFileHeaders.php | 156 + www/wiki/maintenance/refreshImageMetadata.php | 264 ++ www/wiki/maintenance/refreshLinks.php | 493 +++ www/wiki/maintenance/removeInvalidEmails.php | 78 + www/wiki/maintenance/removeUnusedAccounts.php | 195 + www/wiki/maintenance/renameDbPrefix.php | 94 + www/wiki/maintenance/renderDump.php | 127 + www/wiki/maintenance/resetUserEmail.php | 72 + www/wiki/maintenance/resetUserTokens.php | 119 + www/wiki/maintenance/resources/update-oojs.sh | 62 + www/wiki/maintenance/resources/update-ooui.sh | 108 + www/wiki/maintenance/rollbackEdits.php | 121 + www/wiki/maintenance/runBatchedQuery.php | 115 + www/wiki/maintenance/runJobs.php | 122 + www/wiki/maintenance/runScript.php | 64 + www/wiki/maintenance/shell.php | 100 + www/wiki/maintenance/showJobs.php | 109 + www/wiki/maintenance/showSiteStats.php | 78 + www/wiki/maintenance/sql.php | 205 + www/wiki/maintenance/sqlite.inc | 96 + www/wiki/maintenance/sqlite.php | 146 + .../sqlite/archives/initial-indexes.sql | 462 ++ .../sqlite/archives/patch-actor-table.sql | 368 ++ .../maintenance/sqlite/archives/patch-add-3d.sql | 249 ++ .../sqlite/archives/patch-ar_rev_id-not-null.sql | 47 + .../sqlite/archives/patch-archive-ar_id.sql | 39 + .../archives/patch-archive_kill_ar_page_revid.sql | 3 + .../sqlite/archives/patch-cat_hidden.sql | 20 + .../patch-categorylinks-better-collation.sql | 7 + .../sqlite/archives/patch-categorylinks-fix-pk.sql | 60 + .../sqlite/archives/patch-change_tag-ct_id.sql | 25 + .../sqlite/archives/patch-comment-table.sql | 332 ++ .../sqlite/archives/patch-drop-ar_text.sql | 44 + .../sqlite/archives/patch-drop-page_counter.sql | 31 + .../sqlite/archives/patch-drop-rc_cur_time.sql | 45 + .../sqlite/archives/patch-drop-ss_admins.sql | 21 + .../sqlite/archives/patch-drop-ss_total_views.sql | 21 + .../sqlite/archives/patch-drop-user_options.sql | 31 + .../sqlite/archives/patch-editsummary-length.sql | 65 + .../sqlite/archives/patch-externallinks-el_id.sql | 19 + .../archives/patch-image-img_description_id.sql | 47 + .../sqlite/archives/patch-imagelinks-fix-pk.sql | 25 + .../sqlite/archives/patch-ip_changes.sql | 23 + .../sqlite/archives/patch-iw_api_and_wikiid.sql | 19 + .../sqlite/archives/patch-iwlinks-fix-pk.sql | 24 + .../sqlite/archives/patch-job_token.sql | 8 + .../sqlite/archives/patch-jobs-add-timestamp.sql | 2 + .../sqlite/archives/patch-kill-iwl_prefix.sql | 7 + .../archives/patch-l10n_cache-primary-key.sql | 12 + .../sqlite/archives/patch-langlinks-fix-pk.sql | 21 + .../sqlite/archives/patch-log_search-fix-pk.sql | 18 + .../sqlite/archives/patch-log_user_text.sql | 5 + .../sqlite/archives/patch-module_deps-fix-pk.sql | 16 + .../sqlite/archives/patch-objectcache-fix-pk.sql | 14 + .../sqlite/archives/patch-page-page_lang.sql | 3 + .../archives/patch-page_redirect_namespace_len.sql | 7 + .../sqlite/archives/patch-pagelinks-fix-pk.sql | 27 + .../sqlite/archives/patch-profiling.sql | 12 + .../archives/patch-querycache_info-fix-pk.sql | 15 + .../maintenance/sqlite/archives/patch-rc_moved.sql | 46 + .../sqlite/archives/patch-rd_interwiki.sql | 5 + .../archives/patch-recentchanges-nttindex.sql | 10 + .../sqlite/archives/patch-rename-iwl_prefix.sql | 5 + .../sqlite/archives/patch-rev_text_id-default.sql | 53 + .../archives/patch-revision-user-page-index.sql | 4 + .../sqlite/archives/patch-site_stats-fix-pk.sql | 33 + .../sqlite/archives/patch-site_stats-modify.sql | 35 + .../maintenance/sqlite/archives/patch-sites.sql | 71 + .../sqlite/archives/patch-slot-origin.sql | 34 + .../sqlite/archives/patch-tag_summary-ts_id.sql | 23 + .../sqlite/archives/patch-tc-timestamp.sql | 3 + .../sqlite/archives/patch-templatelinks-fix-pk.sql | 27 + .../sqlite/archives/patch-text-fix-pk.sql | 37 + .../sqlite/archives/patch-transcache-fix-pk.sql | 12 + .../patch-ufg_group-length-increase-255.sql | 15 + .../patch-ug_group-length-increase-255.sql | 15 + .../archives/patch-user_former_groups-fix-pk.sql | 13 + .../archives/patch-user_groups-ug_expiry.sql | 21 + .../archives/patch-user_properties-fix-pk.sql | 20 + .../sqlite/archives/patch-watchlist-wl_id.sql | 23 + .../sqlite/archives/searchindex-fts3.sql | 18 + .../sqlite/archives/searchindex-no-fts.sql | 25 + www/wiki/maintenance/storage/blob_tracking.sql | 56 + www/wiki/maintenance/storage/blobs.sql | 7 + www/wiki/maintenance/storage/checkStorage.php | 556 +++ www/wiki/maintenance/storage/compressOld.php | 474 ++ .../storage/drop_content_model_info.sql | 7 + www/wiki/maintenance/storage/dumpRev.php | 88 + www/wiki/maintenance/storage/fixT22757.php | 339 ++ www/wiki/maintenance/storage/make-blobs | 14 + www/wiki/maintenance/storage/moveToExternal.php | 126 + www/wiki/maintenance/storage/orphanStats.php | 87 + www/wiki/maintenance/storage/recompressTracked.php | 842 ++++ www/wiki/maintenance/storage/resolveStubs.php | 119 + www/wiki/maintenance/storage/storageTypeStats.php | 115 + www/wiki/maintenance/storage/testCompression.php | 104 + www/wiki/maintenance/storage/trackBlobs.php | 383 ++ www/wiki/maintenance/syncFileBackend.php | 307 ++ www/wiki/maintenance/tables.sql | 1971 +++++++++ www/wiki/maintenance/term/MWTerm.php | 80 + www/wiki/maintenance/tidyUpBug37714.php | 48 + www/wiki/maintenance/undelete.php | 62 + www/wiki/maintenance/update-keys.sql | 29 + www/wiki/maintenance/update.php | 248 ++ www/wiki/maintenance/updateArticleCount.php | 73 + www/wiki/maintenance/updateCollation.php | 352 ++ www/wiki/maintenance/updateCredits.php | 80 + www/wiki/maintenance/updateDoubleWidthSearch.php | 81 + www/wiki/maintenance/updateExtensionJsonSchema.php | 69 + www/wiki/maintenance/updateRestrictions.php | 130 + www/wiki/maintenance/updateSearchIndex.php | 125 + www/wiki/maintenance/updateSpecialPages.php | 174 + www/wiki/maintenance/userDupes.inc | 297 ++ www/wiki/maintenance/userOptions.php | 203 + www/wiki/maintenance/validateRegistrationFile.php | 26 + www/wiki/maintenance/view.php | 59 + www/wiki/maintenance/wrapOldPasswords.php | 125 + 760 files changed, 74221 insertions(+) create mode 100644 www/wiki/maintenance/.htaccess create mode 100644 www/wiki/maintenance/7zip.inc create mode 100644 www/wiki/maintenance/CodeCleanerGlobalsPass.inc create mode 100644 www/wiki/maintenance/Doxyfile create mode 100644 www/wiki/maintenance/Maintenance.php create mode 100644 www/wiki/maintenance/Makefile create mode 100644 www/wiki/maintenance/README create mode 100644 www/wiki/maintenance/addRFCandPMIDInterwiki.php create mode 100644 www/wiki/maintenance/addSite.php create mode 100644 www/wiki/maintenance/archives/.htaccess create mode 100644 www/wiki/maintenance/archives/patch-actor-table.sql create mode 100644 www/wiki/maintenance/archives/patch-add-3d.sql create mode 100644 www/wiki/maintenance/archives/patch-add-cl_collation_ext_index.sql create mode 100644 www/wiki/maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql create mode 100644 www/wiki/maintenance/archives/patch-ar_deleted.sql create mode 100644 www/wiki/maintenance/archives/patch-ar_len.sql create mode 100644 www/wiki/maintenance/archives/patch-ar_parent_id.sql create mode 100644 www/wiki/maintenance/archives/patch-ar_rev_id-not-null.sql create mode 100644 www/wiki/maintenance/archives/patch-ar_sha1.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-ar_content_format.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-ar_content_model.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-ar_id.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-page_id.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-rev_id.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-text_id.sql create mode 100644 www/wiki/maintenance/archives/patch-archive-user-index.sql create mode 100644 www/wiki/maintenance/archives/patch-archive_ar_revid.sql create mode 100644 www/wiki/maintenance/archives/patch-archive_kill_ar_page_revid.sql create mode 100644 www/wiki/maintenance/archives/patch-backlinkindexes.sql create mode 100644 www/wiki/maintenance/archives/patch-bot.sql create mode 100644 www/wiki/maintenance/archives/patch-bot_passwords-bp_user-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-bot_passwords.sql create mode 100644 www/wiki/maintenance/archives/patch-cache.sql create mode 100644 www/wiki/maintenance/archives/patch-cat_hidden.sql create mode 100644 www/wiki/maintenance/archives/patch-category.sql create mode 100644 www/wiki/maintenance/archives/patch-categorylinks-better-collation.sql create mode 100644 www/wiki/maintenance/archives/patch-categorylinks-better-collation2.sql create mode 100644 www/wiki/maintenance/archives/patch-categorylinks-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-categorylinks.sql create mode 100644 www/wiki/maintenance/archives/patch-categorylinksindex.sql create mode 100644 www/wiki/maintenance/archives/patch-change_tag-ct_id.sql create mode 100644 www/wiki/maintenance/archives/patch-change_tag-ct_log_id-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-change_tag-ct_rev_id-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-change_tag-indexes.sql create mode 100644 www/wiki/maintenance/archives/patch-change_tag.sql create mode 100644 www/wiki/maintenance/archives/patch-comment-table.sql create mode 100644 www/wiki/maintenance/archives/patch-content.sql create mode 100644 www/wiki/maintenance/archives/patch-content_models.sql create mode 100644 www/wiki/maintenance/archives/patch-drop-ar_text.sql create mode 100644 www/wiki/maintenance/archives/patch-drop-page_counter.sql create mode 100644 www/wiki/maintenance/archives/patch-drop-rc_cur_time.sql create mode 100644 www/wiki/maintenance/archives/patch-drop-ss_admins.sql create mode 100644 www/wiki/maintenance/archives/patch-drop-ss_total_views.sql create mode 100644 www/wiki/maintenance/archives/patch-drop-user_options.sql create mode 100644 www/wiki/maintenance/archives/patch-drop_img_type.sql create mode 100644 www/wiki/maintenance/archives/patch-editsummary-length.sql create mode 100644 www/wiki/maintenance/archives/patch-email-authentication.sql create mode 100644 www/wiki/maintenance/archives/patch-email-notification.sql create mode 100644 www/wiki/maintenance/archives/patch-externallinks-el_id.sql create mode 100644 www/wiki/maintenance/archives/patch-externallinks-el_index_60.sql create mode 100644 www/wiki/maintenance/archives/patch-externallinks.sql create mode 100644 www/wiki/maintenance/archives/patch-fa_deleted.sql create mode 100644 www/wiki/maintenance/archives/patch-fa_major_mime-chemical.sql create mode 100644 www/wiki/maintenance/archives/patch-fa_sha1.sql create mode 100644 www/wiki/maintenance/archives/patch-filearchive-user-index.sql create mode 100644 www/wiki/maintenance/archives/patch-filearchive.sql create mode 100644 www/wiki/maintenance/archives/patch-filejournal.sql create mode 100644 www/wiki/maintenance/archives/patch-fix-il_from.sql create mode 100644 www/wiki/maintenance/archives/patch-il_from_namespace.sql create mode 100644 www/wiki/maintenance/archives/patch-image-img_description_id.sql create mode 100644 www/wiki/maintenance/archives/patch-image-user-index-2.sql create mode 100644 www/wiki/maintenance/archives/patch-image-user-index.sql create mode 100644 www/wiki/maintenance/archives/patch-image_name_primary.sql create mode 100644 www/wiki/maintenance/archives/patch-image_name_unique.sql create mode 100644 www/wiki/maintenance/archives/patch-imagelinks-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-img_exif.sql create mode 100644 www/wiki/maintenance/archives/patch-img_major_mime-chemical.sql create mode 100644 www/wiki/maintenance/archives/patch-img_media_mime-index.sql create mode 100644 www/wiki/maintenance/archives/patch-img_media_type.sql create mode 100644 www/wiki/maintenance/archives/patch-img_metadata.sql create mode 100644 www/wiki/maintenance/archives/patch-img_sha1.sql create mode 100644 www/wiki/maintenance/archives/patch-img_width.sql create mode 100644 www/wiki/maintenance/archives/patch-indexes.sql create mode 100644 www/wiki/maintenance/archives/patch-interwiki-trans.sql create mode 100644 www/wiki/maintenance/archives/patch-interwiki.sql create mode 100644 www/wiki/maintenance/archives/patch-inverse_timestamp.sql create mode 100644 www/wiki/maintenance/archives/patch-ip_changes.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb-parent-block-id-index.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb-parent-block-id.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_allow_usertalk.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_anon_only.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_by_text.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_deleted.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_emailban.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_expiry.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_optional_autoblock.sql create mode 100644 www/wiki/maintenance/archives/patch-ipb_range_start.sql create mode 100644 www/wiki/maintenance/archives/patch-ipblocks.sql create mode 100644 www/wiki/maintenance/archives/patch-iw_api_and_wikiid.sql create mode 100644 www/wiki/maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql create mode 100644 www/wiki/maintenance/archives/patch-iwlinks-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-iwlinks-from-title-index.sql create mode 100644 www/wiki/maintenance/archives/patch-iwlinks.sql create mode 100644 www/wiki/maintenance/archives/patch-job.sql create mode 100644 www/wiki/maintenance/archives/patch-job_attempts.sql create mode 100644 www/wiki/maintenance/archives/patch-job_token.sql create mode 100644 www/wiki/maintenance/archives/patch-jobs-add-timestamp.sql create mode 100644 www/wiki/maintenance/archives/patch-kill-cl_collation_index.sql create mode 100644 www/wiki/maintenance/archives/patch-kill-iwl_prefix.sql create mode 100644 www/wiki/maintenance/archives/patch-l10n_cache-primary-key.sql create mode 100644 www/wiki/maintenance/archives/patch-l10n_cache.sql create mode 100644 www/wiki/maintenance/archives/patch-langlinks-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-langlinks-ll_lang-20.sql create mode 100644 www/wiki/maintenance/archives/patch-langlinks.sql create mode 100644 www/wiki/maintenance/archives/patch-linkscc-1.3.sql create mode 100644 www/wiki/maintenance/archives/patch-linkscc.sql create mode 100644 www/wiki/maintenance/archives/patch-linktables.sql create mode 100644 www/wiki/maintenance/archives/patch-log_deleted.sql create mode 100644 www/wiki/maintenance/archives/patch-log_id.sql create mode 100644 www/wiki/maintenance/archives/patch-log_params.sql create mode 100644 www/wiki/maintenance/archives/patch-log_search-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-log_search.sql create mode 100644 www/wiki/maintenance/archives/patch-log_user_text.sql create mode 100644 www/wiki/maintenance/archives/patch-logging-times-index.sql create mode 100644 www/wiki/maintenance/archives/patch-logging-title.sql create mode 100644 www/wiki/maintenance/archives/patch-logging-type-action-index.sql create mode 100644 www/wiki/maintenance/archives/patch-logging.sql create mode 100644 www/wiki/maintenance/archives/patch-logging_user_text_time_index.sql create mode 100644 www/wiki/maintenance/archives/patch-logging_user_text_type_time_index.sql create mode 100644 www/wiki/maintenance/archives/patch-mime_minor_length.sql create mode 100644 www/wiki/maintenance/archives/patch-mimesearch-indexes.sql create mode 100644 www/wiki/maintenance/archives/patch-module_deps-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-module_deps.sql create mode 100644 www/wiki/maintenance/archives/patch-nullable-ar_text.sql create mode 100644 www/wiki/maintenance/archives/patch-objectcache-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-objectcache.sql create mode 100644 www/wiki/maintenance/archives/patch-oi_major_mime-chemical.sql create mode 100644 www/wiki/maintenance/archives/patch-oi_metadata.sql create mode 100644 www/wiki/maintenance/archives/patch-oldestindex.sql create mode 100644 www/wiki/maintenance/archives/patch-oldimage-user-index.sql create mode 100644 www/wiki/maintenance/archives/patch-page-page_content_model.sql create mode 100644 www/wiki/maintenance/archives/patch-page_lang.sql create mode 100644 www/wiki/maintenance/archives/patch-page_len.sql create mode 100644 www/wiki/maintenance/archives/patch-page_links_updated.sql create mode 100644 www/wiki/maintenance/archives/patch-page_props-propname-page-index.sql create mode 100644 www/wiki/maintenance/archives/patch-page_props.sql create mode 100644 www/wiki/maintenance/archives/patch-page_redirect_namespace_len.sql create mode 100644 www/wiki/maintenance/archives/patch-page_restrictions-pr_user-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-page_restrictions.sql create mode 100644 www/wiki/maintenance/archives/patch-page_restrictions_sortkey.sql create mode 100644 www/wiki/maintenance/archives/patch-pagelinks-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-pagelinks.sql create mode 100644 www/wiki/maintenance/archives/patch-parsercache.sql create mode 100644 www/wiki/maintenance/archives/patch-pl-tl-il-nonunique.sql create mode 100644 www/wiki/maintenance/archives/patch-pl_from_namespace.sql create mode 100644 www/wiki/maintenance/archives/patch-pp_sortkey.sql create mode 100644 www/wiki/maintenance/archives/patch-profiling-memory.sql create mode 100644 www/wiki/maintenance/archives/patch-profiling.sql create mode 100644 www/wiki/maintenance/archives/patch-protected_titles.sql create mode 100644 www/wiki/maintenance/archives/patch-pt_title-encoding.sql create mode 100644 www/wiki/maintenance/archives/patch-querycache.sql create mode 100644 www/wiki/maintenance/archives/patch-querycache_info-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-querycacheinfo.sql create mode 100644 www/wiki/maintenance/archives/patch-querycachetwo.sql create mode 100644 www/wiki/maintenance/archives/patch-random-dateindex.sql create mode 100644 www/wiki/maintenance/archives/patch-rc-newindex.sql create mode 100644 www/wiki/maintenance/archives/patch-rc-patrol.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_deleted.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_id.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_ip.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_ip_modify.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_len.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_moved.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_source.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_type.sql create mode 100644 www/wiki/maintenance/archives/patch-rc_user_text-index.sql create mode 100644 www/wiki/maintenance/archives/patch-rd_interwiki.sql create mode 100644 www/wiki/maintenance/archives/patch-recentchanges-nttindex.sql create mode 100644 www/wiki/maintenance/archives/patch-recentchanges-utindex.sql create mode 100644 www/wiki/maintenance/archives/patch-redirect.sql create mode 100644 www/wiki/maintenance/archives/patch-rename-ar_usertext_timestamp.sql create mode 100644 www/wiki/maintenance/archives/patch-rename-iwl_prefix.sql create mode 100644 www/wiki/maintenance/archives/patch-rename-user_groups-and_rights.sql create mode 100644 www/wiki/maintenance/archives/patch-rev_deleted.sql create mode 100644 www/wiki/maintenance/archives/patch-rev_len.sql create mode 100644 www/wiki/maintenance/archives/patch-rev_parent_id.sql create mode 100644 www/wiki/maintenance/archives/patch-rev_sha1.sql create mode 100644 www/wiki/maintenance/archives/patch-rev_text_id-default.sql create mode 100644 www/wiki/maintenance/archives/patch-rev_text_id.sql create mode 100644 www/wiki/maintenance/archives/patch-revision-page-rev-index-nonunique.sql create mode 100644 www/wiki/maintenance/archives/patch-revision-rev_content_format.sql create mode 100644 www/wiki/maintenance/archives/patch-revision-rev_content_model.sql create mode 100644 www/wiki/maintenance/archives/patch-revision-user-page-index.sql create mode 100644 www/wiki/maintenance/archives/patch-searchindex.sql create mode 100644 www/wiki/maintenance/archives/patch-site_stats-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-site_stats-modify.sql create mode 100644 www/wiki/maintenance/archives/patch-sites.sql create mode 100644 www/wiki/maintenance/archives/patch-slot-origin.sql create mode 100644 www/wiki/maintenance/archives/patch-slot_roles.sql create mode 100644 www/wiki/maintenance/archives/patch-slots.sql create mode 100644 www/wiki/maintenance/archives/patch-ss_active_users.sql create mode 100644 www/wiki/maintenance/archives/patch-ss_images.sql create mode 100644 www/wiki/maintenance/archives/patch-ss_total_articles.sql create mode 100644 www/wiki/maintenance/archives/patch-tag_summary-ts_id.sql create mode 100644 www/wiki/maintenance/archives/patch-tag_summary-ts_log_id-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-tag_summary-ts_rev_id-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-tag_summary.sql create mode 100644 www/wiki/maintenance/archives/patch-tc-timestamp.sql create mode 100644 www/wiki/maintenance/archives/patch-templatelinks-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-templatelinks.sql create mode 100644 www/wiki/maintenance/archives/patch-testrun.sql create mode 100644 www/wiki/maintenance/archives/patch-text-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-tl_from_namespace.sql create mode 100644 www/wiki/maintenance/archives/patch-transcache-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-transcache.sql create mode 100644 www/wiki/maintenance/archives/patch-ufg_group-length-increase-255.sql create mode 100644 www/wiki/maintenance/archives/patch-ug_group-length-increase-255.sql create mode 100644 www/wiki/maintenance/archives/patch-ul_value.sql create mode 100644 www/wiki/maintenance/archives/patch-up_property.sql create mode 100644 www/wiki/maintenance/archives/patch-updatelog.sql create mode 100644 www/wiki/maintenance/archives/patch-uploadstash-us_props.sql create mode 100644 www/wiki/maintenance/archives/patch-uploadstash.sql create mode 100644 www/wiki/maintenance/archives/patch-uploadstash_chunk.sql create mode 100644 www/wiki/maintenance/archives/patch-user-newtalk-timestamp-null.sql create mode 100644 www/wiki/maintenance/archives/patch-user-realname.sql create mode 100644 www/wiki/maintenance/archives/patch-user_editcount.sql create mode 100644 www/wiki/maintenance/archives/patch-user_email_index.sql create mode 100644 www/wiki/maintenance/archives/patch-user_email_token.sql create mode 100644 www/wiki/maintenance/archives/patch-user_former_groups-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-user_former_groups.sql create mode 100644 www/wiki/maintenance/archives/patch-user_groups-primary-key.sql create mode 100644 www/wiki/maintenance/archives/patch-user_groups-ug_expiry.sql create mode 100644 www/wiki/maintenance/archives/patch-user_groups.sql create mode 100644 www/wiki/maintenance/archives/patch-user_last_timestamp.sql create mode 100644 www/wiki/maintenance/archives/patch-user_nameindex.sql create mode 100644 www/wiki/maintenance/archives/patch-user_newpass_time.sql create mode 100644 www/wiki/maintenance/archives/patch-user_newtalk-user_id-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-user_password_expire.sql create mode 100644 www/wiki/maintenance/archives/patch-user_properties-fix-pk.sql create mode 100644 www/wiki/maintenance/archives/patch-user_properties-up_user-unsigned.sql create mode 100644 www/wiki/maintenance/archives/patch-user_properties.sql create mode 100644 www/wiki/maintenance/archives/patch-user_registration.sql create mode 100644 www/wiki/maintenance/archives/patch-user_rights.sql create mode 100644 www/wiki/maintenance/archives/patch-user_token.sql create mode 100644 www/wiki/maintenance/archives/patch-userindex.sql create mode 100644 www/wiki/maintenance/archives/patch-userlevels.sql create mode 100644 www/wiki/maintenance/archives/patch-usernewtalk.sql create mode 100644 www/wiki/maintenance/archives/patch-valid_tag.sql create mode 100644 www/wiki/maintenance/archives/patch-watchlist-null.sql create mode 100644 www/wiki/maintenance/archives/patch-watchlist-user-notificationtimestamp-index.sql create mode 100644 www/wiki/maintenance/archives/patch-watchlist-wl_id.sql create mode 100644 www/wiki/maintenance/archives/patch-watchlist.sql create mode 100644 www/wiki/maintenance/archives/upgradeLogging.php create mode 100644 www/wiki/maintenance/attachLatest.php create mode 100644 www/wiki/maintenance/backup.inc create mode 100644 www/wiki/maintenance/benchmarks/Benchmarker.php create mode 100644 www/wiki/maintenance/benchmarks/README.md create mode 100644 www/wiki/maintenance/benchmarks/australia-untidy.html.gz create mode 100644 www/wiki/maintenance/benchmarks/bench_HTTP_HTTPS.php create mode 100644 www/wiki/maintenance/benchmarks/bench_Wikimedia_base_convert.php create mode 100644 www/wiki/maintenance/benchmarks/bench_delete_truncate.php create mode 100644 www/wiki/maintenance/benchmarks/bench_if_switch.php create mode 100644 www/wiki/maintenance/benchmarks/bench_strtr_str_replace.php create mode 100644 www/wiki/maintenance/benchmarks/bench_utf8_title_check.php create mode 100644 www/wiki/maintenance/benchmarks/bench_wfIsWindows.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkCSSMin.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkHooks.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkJSMinPlus.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkLruHash.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkParse.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkPurge.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkSanitizer.php create mode 100644 www/wiki/maintenance/benchmarks/benchmarkTidy.php create mode 100644 www/wiki/maintenance/benchmarks/cssmin/circle.svg create mode 100644 www/wiki/maintenance/benchmarks/cssmin/styles.css create mode 100644 www/wiki/maintenance/benchmarks/cssmin/wiki.png create mode 100644 www/wiki/maintenance/cdb.php create mode 100644 www/wiki/maintenance/changePassword.php create mode 100644 www/wiki/maintenance/checkBadRedirects.php create mode 100644 www/wiki/maintenance/checkComposerLockUpToDate.php create mode 100644 www/wiki/maintenance/checkImages.php create mode 100644 www/wiki/maintenance/checkLess.php create mode 100644 www/wiki/maintenance/checkUsernames.php create mode 100644 www/wiki/maintenance/cleanupAncientTables.php create mode 100644 www/wiki/maintenance/cleanupBlocks.php create mode 100644 www/wiki/maintenance/cleanupCaps.php create mode 100644 www/wiki/maintenance/cleanupEmptyCategories.php create mode 100644 www/wiki/maintenance/cleanupImages.php create mode 100644 www/wiki/maintenance/cleanupInvalidDbKeys.php create mode 100644 www/wiki/maintenance/cleanupPreferences.php create mode 100644 www/wiki/maintenance/cleanupRemovedModules.php create mode 100644 www/wiki/maintenance/cleanupSpam.php create mode 100644 www/wiki/maintenance/cleanupTable.inc create mode 100644 www/wiki/maintenance/cleanupTitles.php create mode 100644 www/wiki/maintenance/cleanupUploadStash.php create mode 100644 www/wiki/maintenance/cleanupUsersWithNoId.php create mode 100644 www/wiki/maintenance/cleanupWatchlist.php create mode 100644 www/wiki/maintenance/clearInterwikiCache.php create mode 100644 www/wiki/maintenance/commandLine.inc create mode 100644 www/wiki/maintenance/compareParserCache.php create mode 100644 www/wiki/maintenance/compareParsers.php create mode 100644 www/wiki/maintenance/convertExtensionToRegistration.php create mode 100644 www/wiki/maintenance/convertLinks.php create mode 100644 www/wiki/maintenance/convertUserOptions.php create mode 100644 www/wiki/maintenance/copyFileBackend.php create mode 100644 www/wiki/maintenance/copyJobQueue.php create mode 100644 www/wiki/maintenance/createAndPromote.php create mode 100644 www/wiki/maintenance/createCommonPasswordCdb.php create mode 100644 www/wiki/maintenance/deleteArchivedFiles.php create mode 100644 www/wiki/maintenance/deleteArchivedRevisions.php create mode 100644 www/wiki/maintenance/deleteAutoPatrolLogs.php create mode 100644 www/wiki/maintenance/deleteBatch.php create mode 100644 www/wiki/maintenance/deleteDefaultMessages.php create mode 100644 www/wiki/maintenance/deleteEqualMessages.php create mode 100644 www/wiki/maintenance/deleteOldRevisions.php create mode 100644 www/wiki/maintenance/deleteOrphanedRevisions.php create mode 100644 www/wiki/maintenance/deleteSelfExternals.php create mode 100644 www/wiki/maintenance/dev/README create mode 100644 www/wiki/maintenance/dev/includes/php.sh create mode 100644 www/wiki/maintenance/dev/includes/require-php.sh create mode 100644 www/wiki/maintenance/dev/includes/router.php create mode 100755 www/wiki/maintenance/dev/install.sh create mode 100755 www/wiki/maintenance/dev/installmw.sh create mode 100755 www/wiki/maintenance/dev/installphp.sh create mode 100755 www/wiki/maintenance/dev/start.sh create mode 100644 www/wiki/maintenance/dictionary/mediawiki.dic create mode 100644 www/wiki/maintenance/doMaintenance.php create mode 100644 www/wiki/maintenance/dumpBackup.php create mode 100644 www/wiki/maintenance/dumpCategoriesAsRdf.php create mode 100644 www/wiki/maintenance/dumpIterator.php create mode 100644 www/wiki/maintenance/dumpLinks.php create mode 100644 www/wiki/maintenance/dumpTextPass.php create mode 100644 www/wiki/maintenance/dumpUploads.php create mode 100644 www/wiki/maintenance/edit.php create mode 100644 www/wiki/maintenance/eraseArchivedFile.php create mode 100644 www/wiki/maintenance/eval.php create mode 100644 www/wiki/maintenance/exportSites.php create mode 100644 www/wiki/maintenance/fetchText.php create mode 100644 www/wiki/maintenance/fileOpPerfTest.php create mode 100644 www/wiki/maintenance/findDeprecated.php create mode 100644 www/wiki/maintenance/findHooks.php create mode 100644 www/wiki/maintenance/findMissingFiles.php create mode 100644 www/wiki/maintenance/findOrphanedFiles.php create mode 100644 www/wiki/maintenance/fixDefaultJsonContentPages.php create mode 100644 www/wiki/maintenance/fixDoubleRedirects.php create mode 100644 www/wiki/maintenance/fixExtLinksProtocolRelative.php create mode 100644 www/wiki/maintenance/fixTimestamps.php create mode 100644 www/wiki/maintenance/fixUserRegistration.php create mode 100644 www/wiki/maintenance/formatInstallDoc.php create mode 100644 www/wiki/maintenance/generateJsonI18n.php create mode 100644 www/wiki/maintenance/generateLocalAutoload.php create mode 100644 www/wiki/maintenance/generateSitemap.php create mode 100644 www/wiki/maintenance/getConfiguration.php create mode 100644 www/wiki/maintenance/getLagTimes.php create mode 100644 www/wiki/maintenance/getReplicaServer.php create mode 100644 www/wiki/maintenance/getSlaveServer.php create mode 100644 www/wiki/maintenance/getText.php create mode 100644 www/wiki/maintenance/hhvm/makeRepo.php create mode 100755 www/wiki/maintenance/hhvm/run-server create mode 100644 www/wiki/maintenance/hhvm/server.conf create mode 100644 www/wiki/maintenance/importDump.php create mode 100644 www/wiki/maintenance/importImages.php create mode 100644 www/wiki/maintenance/importSiteScripts.php create mode 100644 www/wiki/maintenance/importSites.php create mode 100644 www/wiki/maintenance/importTextFiles.php create mode 100644 www/wiki/maintenance/initEditCount.php create mode 100644 www/wiki/maintenance/initSiteStats.php create mode 100644 www/wiki/maintenance/initUserPreference.php create mode 100644 www/wiki/maintenance/install.php create mode 100644 www/wiki/maintenance/interwiki.list create mode 100644 www/wiki/maintenance/interwiki.sql create mode 100644 www/wiki/maintenance/invalidateUserSessions.php create mode 100644 www/wiki/maintenance/jsduck/categories.json create mode 100644 www/wiki/maintenance/jsduck/custom_tags.rb create mode 100644 www/wiki/maintenance/jsduck/eg-iframe.html create mode 100644 www/wiki/maintenance/jsduck/external.js create mode 100644 www/wiki/maintenance/jsparse.php create mode 100644 www/wiki/maintenance/lag.php create mode 100644 www/wiki/maintenance/language/StatOutputs.php create mode 100644 www/wiki/maintenance/language/alltrans.php create mode 100644 www/wiki/maintenance/language/checkDupeMessages.php create mode 100644 www/wiki/maintenance/language/checkExtensions.php create mode 100644 www/wiki/maintenance/language/checkLanguage.inc create mode 100644 www/wiki/maintenance/language/checkLanguage.php create mode 100644 www/wiki/maintenance/language/date-formats.php create mode 100644 www/wiki/maintenance/language/digit2html.php create mode 100644 www/wiki/maintenance/language/dumpMessages.php create mode 100644 www/wiki/maintenance/language/generateCollationData.php create mode 100644 www/wiki/maintenance/language/generateNormalizerDataAr.php create mode 100644 www/wiki/maintenance/language/generateNormalizerDataMl.php create mode 100644 www/wiki/maintenance/language/langmemusage.php create mode 100644 www/wiki/maintenance/language/languages.inc create mode 100644 www/wiki/maintenance/language/listVariants.php create mode 100644 www/wiki/maintenance/language/transstat.php create mode 100644 www/wiki/maintenance/language/zhtable/Makefile create mode 100755 www/wiki/maintenance/language/zhtable/Makefile.py create mode 100644 www/wiki/maintenance/language/zhtable/README create mode 100644 www/wiki/maintenance/language/zhtable/simp2trad.manual create mode 100644 www/wiki/maintenance/language/zhtable/simp2trad_noconvert.manual create mode 100644 www/wiki/maintenance/language/zhtable/simp2trad_supp_set.manual create mode 100644 www/wiki/maintenance/language/zhtable/simpphrases.manual create mode 100644 www/wiki/maintenance/language/zhtable/simpphrases_exclude.manual create mode 100644 www/wiki/maintenance/language/zhtable/symme_supp.manual create mode 100644 www/wiki/maintenance/language/zhtable/toCN.manual create mode 100644 www/wiki/maintenance/language/zhtable/toHK.manual create mode 100644 www/wiki/maintenance/language/zhtable/toSimp.manual create mode 100644 www/wiki/maintenance/language/zhtable/toTW.manual create mode 100644 www/wiki/maintenance/language/zhtable/toTrad.manual create mode 100644 www/wiki/maintenance/language/zhtable/trad2simp.manual create mode 100644 www/wiki/maintenance/language/zhtable/trad2simp_noconvert.manual create mode 100644 www/wiki/maintenance/language/zhtable/trad2simp_supp_set.manual create mode 100644 www/wiki/maintenance/language/zhtable/tradphrases.manual create mode 100644 www/wiki/maintenance/language/zhtable/tradphrases_exclude.manual create mode 100644 www/wiki/maintenance/locking/file_locks.sql create mode 100644 www/wiki/maintenance/makeTestEdits.php create mode 100644 www/wiki/maintenance/manageJobs.php create mode 100644 www/wiki/maintenance/mcc.php create mode 100644 www/wiki/maintenance/mctest.php create mode 100644 www/wiki/maintenance/mergeMessageFileList.php create mode 100644 www/wiki/maintenance/migrateActors.php create mode 100644 www/wiki/maintenance/migrateArchiveText.php create mode 100644 www/wiki/maintenance/migrateComments.php create mode 100644 www/wiki/maintenance/migrateFileRepoLayout.php create mode 100644 www/wiki/maintenance/migrateUserGroup.php create mode 100644 www/wiki/maintenance/minify.php create mode 100644 www/wiki/maintenance/moveBatch.php create mode 100644 www/wiki/maintenance/mssql/archives/patch-actor-table.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-add-3d.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-add-cl_collation_ext_index.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-alter-table-oldimage.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-ar_rev_id-not-null.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-archive-drop-fks.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-bot_passwords.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-categorylinks-constraints.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-change_tag-ct_id.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-comment-table.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-content.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-content_models.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-drop-ar_text.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-drop-page_counter.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-drop-rc_cur_time.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-drop-ss_total_views.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-drop-user_options.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-fa_major_mime-chemical.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-filearchive-constraints.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-filearchive-schema.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-il_from_namespace.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-image-constraints.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-image-img_description_id.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-image-schema.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-img_major_mime-chemical.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-kill-cl_collation_index.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-logging-drop-fks.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-oi_major_mime-chemical.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-oldimage-constraints.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-oldimage-schema.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-page_page_lang.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-pl_from_namespace.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-pp_sortkey.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-rc_patrolled_type.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-recentchanges-drop-fks.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-rev_text_id-default.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-site_stats-modify.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-site_stats-pk.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-slot-origin.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-slot_roles.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-slots.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-tag_summary-ts_id.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-tl_from_namespace.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-uploadstash-constraints.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-user_groups-ug_expiry.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-user_password_expires.sql create mode 100644 www/wiki/maintenance/mssql/archives/patch-watchlist-wl_id.sql create mode 100644 www/wiki/maintenance/mssql/tables.sql create mode 100644 www/wiki/maintenance/mssql/update-keys.sql create mode 100644 www/wiki/maintenance/mwdoc-filter.php create mode 100644 www/wiki/maintenance/mwdocgen.php create mode 100755 www/wiki/maintenance/mwjsduck-gen create mode 100644 www/wiki/maintenance/namespaceDupes.php create mode 100644 www/wiki/maintenance/nukeNS.php create mode 100644 www/wiki/maintenance/nukePage.php create mode 100644 www/wiki/maintenance/oracle/alterSharedConstraints.php create mode 100644 www/wiki/maintenance/oracle/archives/patch-actor-table.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-ar_rev_id-not-null.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-ar_sha1_field.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-archive-ar_content_format.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-archive-ar_content_model.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-archive-ar_id.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-auto_increment_triggers.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-cat_hidden.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-change_tag-ct_id.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-comment-table.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-content.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-content_models.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-drop-ar_text.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-externallinks-el_id.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-externallinks-el_index_60.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-fa_sha1.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-image-img_description_id.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-ipblocks_i05_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-job_attempts.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-job_timestamp_field.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-job_timestamp_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-job_token.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-logging_type_action_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-logging_user_text_time_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-logging_user_text_type_time_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-page-page_content_model.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-page-page_lang.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-page_links_updated.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-page_redirect_namespace_len.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-page_restrictions_pkuk_fix.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-rc_moved.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-rc_source.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-recentchanges-nttindex.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-rev_sha1_field.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-revision-rev_content_format.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-revision-rev_content_model.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-revision_i05_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-site_stats-modify.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-site_stats-pk.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-sites.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-slot-origin.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-slot_roles.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-slots.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-ss_admins.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-tag_summary-ts_id.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-testrun.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-ufg_group-length-increase-255.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-ug_group-length-increase-255.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-up_property.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-uploadstash-us_props.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-uploadstash.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-us_chunk_inx_field.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-user_email_index.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-user_former_groups.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-user_groups-ug_expiry.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-user_password_expire.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch-watchlist-wl_id.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_16_17_schema_changes.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_create_17_functions.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_fk_rename_deferred.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_namespace_defaults.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_rebuild_dupfunc.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_recentchanges_fk2_cascade.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_remove_not_null_empty_defs.sql create mode 100644 www/wiki/maintenance/oracle/archives/patch_remove_not_null_empty_defs2.sql create mode 100644 www/wiki/maintenance/oracle/patch_seq_names_pre1.16.sql create mode 100644 www/wiki/maintenance/oracle/tables.sql create mode 100644 www/wiki/maintenance/oracle/update-keys.sql create mode 100644 www/wiki/maintenance/oracle/user.sql create mode 100644 www/wiki/maintenance/orphans.php create mode 100644 www/wiki/maintenance/pageExists.php create mode 100644 www/wiki/maintenance/parse.php create mode 100644 www/wiki/maintenance/patchSql.php create mode 100644 www/wiki/maintenance/populateArchiveRevId.php create mode 100644 www/wiki/maintenance/populateBacklinkNamespace.php create mode 100644 www/wiki/maintenance/populateCategory.php create mode 100644 www/wiki/maintenance/populateContentModel.php create mode 100644 www/wiki/maintenance/populateFilearchiveSha1.php create mode 100644 www/wiki/maintenance/populateImageSha1.php create mode 100644 www/wiki/maintenance/populateInterwiki.php create mode 100644 www/wiki/maintenance/populateIpChanges.php create mode 100644 www/wiki/maintenance/populateLogSearch.php create mode 100644 www/wiki/maintenance/populateLogUsertext.php create mode 100644 www/wiki/maintenance/populatePPSortKey.php create mode 100644 www/wiki/maintenance/populateParentId.php create mode 100644 www/wiki/maintenance/populateRecentChangesSource.php create mode 100644 www/wiki/maintenance/populateRevisionLength.php create mode 100644 www/wiki/maintenance/populateRevisionSha1.php create mode 100644 www/wiki/maintenance/postgres/archives/patch-actor-table.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-add_interwiki.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-ar_rev_id-not-null.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-bot_passwords.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-category.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-categorylinks-better-collation.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-change_tag.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-comment-table.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-content-table.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-content_models-table.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-drop-ar_text.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-ip_changes.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-iwlinks.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-kill-iwl_prefix.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-l10n_cache.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-log_search.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-module_deps.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-page.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-page_deleted.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-page_props.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-page_restrictions.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-profiling.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-protected_titles.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-querycachetwo.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-rc_cur_id-not-null.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-redirect.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-remove-archive2.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-rename-iwl_prefix.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-revision_rev_user_fkey.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-site_stats-modify.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-site_stats-pk.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-sites.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-slot_roles-table.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-slots-table.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-tag_summary.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-testrun.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-textsearch_bug66650.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-ts2pagetitle.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-tsearch2funcs.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-update_sequences.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-updatelog.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-uploadstash.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-uploadstash_sequence.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-user_former_groups.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-user_properties.sql create mode 100644 www/wiki/maintenance/postgres/archives/patch-valid_tag.sql create mode 100755 www/wiki/maintenance/postgres/compare_schemas.pl create mode 100755 www/wiki/maintenance/postgres/mediawiki_mysql2postgres.pl create mode 100644 www/wiki/maintenance/postgres/tables.sql create mode 100644 www/wiki/maintenance/postgres/update-keys.sql create mode 100644 www/wiki/maintenance/preprocessDump.php create mode 100644 www/wiki/maintenance/preprocessorFuzzTest.php create mode 100644 www/wiki/maintenance/protect.php create mode 100644 www/wiki/maintenance/pruneFileCache.php create mode 100644 www/wiki/maintenance/purgeChangedFiles.php create mode 100644 www/wiki/maintenance/purgeChangedPages.php create mode 100644 www/wiki/maintenance/purgeExpiredUserrights.php create mode 100644 www/wiki/maintenance/purgeList.php create mode 100644 www/wiki/maintenance/purgeModuleDeps.php create mode 100644 www/wiki/maintenance/purgeOldText.php create mode 100644 www/wiki/maintenance/purgePage.php create mode 100644 www/wiki/maintenance/purgeParserCache.php create mode 100644 www/wiki/maintenance/reassignEdits.php create mode 100644 www/wiki/maintenance/rebuildFileCache.php create mode 100644 www/wiki/maintenance/rebuildImages.php create mode 100644 www/wiki/maintenance/rebuildLocalisationCache.php create mode 100644 www/wiki/maintenance/rebuildSitesCache.php create mode 100644 www/wiki/maintenance/rebuildall.php create mode 100644 www/wiki/maintenance/rebuildmessages.php create mode 100644 www/wiki/maintenance/rebuildrecentchanges.php create mode 100644 www/wiki/maintenance/rebuildtextindex.php create mode 100644 www/wiki/maintenance/recountCategories.php create mode 100644 www/wiki/maintenance/refreshFileHeaders.php create mode 100644 www/wiki/maintenance/refreshImageMetadata.php create mode 100644 www/wiki/maintenance/refreshLinks.php create mode 100644 www/wiki/maintenance/removeInvalidEmails.php create mode 100644 www/wiki/maintenance/removeUnusedAccounts.php create mode 100644 www/wiki/maintenance/renameDbPrefix.php create mode 100644 www/wiki/maintenance/renderDump.php create mode 100644 www/wiki/maintenance/resetUserEmail.php create mode 100644 www/wiki/maintenance/resetUserTokens.php create mode 100755 www/wiki/maintenance/resources/update-oojs.sh create mode 100755 www/wiki/maintenance/resources/update-ooui.sh create mode 100644 www/wiki/maintenance/rollbackEdits.php create mode 100644 www/wiki/maintenance/runBatchedQuery.php create mode 100644 www/wiki/maintenance/runJobs.php create mode 100644 www/wiki/maintenance/runScript.php create mode 100644 www/wiki/maintenance/shell.php create mode 100644 www/wiki/maintenance/showJobs.php create mode 100644 www/wiki/maintenance/showSiteStats.php create mode 100644 www/wiki/maintenance/sql.php create mode 100644 www/wiki/maintenance/sqlite.inc create mode 100644 www/wiki/maintenance/sqlite.php create mode 100644 www/wiki/maintenance/sqlite/archives/initial-indexes.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-actor-table.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-add-3d.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-ar_rev_id-not-null.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-archive-ar_id.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-archive_kill_ar_page_revid.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-cat_hidden.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-categorylinks-better-collation.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-categorylinks-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-change_tag-ct_id.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-comment-table.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-drop-ar_text.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-drop-page_counter.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-drop-rc_cur_time.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-drop-ss_admins.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-drop-ss_total_views.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-drop-user_options.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-editsummary-length.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-externallinks-el_id.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-image-img_description_id.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-imagelinks-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-ip_changes.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-iw_api_and_wikiid.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-iwlinks-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-job_token.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-jobs-add-timestamp.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-kill-iwl_prefix.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-l10n_cache-primary-key.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-langlinks-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-log_search-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-log_user_text.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-module_deps-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-objectcache-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-page-page_lang.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-page_redirect_namespace_len.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-pagelinks-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-profiling.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-querycache_info-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-rc_moved.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-rd_interwiki.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-recentchanges-nttindex.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-rename-iwl_prefix.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-rev_text_id-default.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-revision-user-page-index.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-site_stats-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-site_stats-modify.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-sites.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-slot-origin.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-tag_summary-ts_id.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-tc-timestamp.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-templatelinks-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-text-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-transcache-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-ufg_group-length-increase-255.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-ug_group-length-increase-255.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-user_former_groups-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-user_groups-ug_expiry.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-user_properties-fix-pk.sql create mode 100644 www/wiki/maintenance/sqlite/archives/patch-watchlist-wl_id.sql create mode 100644 www/wiki/maintenance/sqlite/archives/searchindex-fts3.sql create mode 100644 www/wiki/maintenance/sqlite/archives/searchindex-no-fts.sql create mode 100644 www/wiki/maintenance/storage/blob_tracking.sql create mode 100644 www/wiki/maintenance/storage/blobs.sql create mode 100644 www/wiki/maintenance/storage/checkStorage.php create mode 100644 www/wiki/maintenance/storage/compressOld.php create mode 100644 www/wiki/maintenance/storage/drop_content_model_info.sql create mode 100644 www/wiki/maintenance/storage/dumpRev.php create mode 100644 www/wiki/maintenance/storage/fixT22757.php create mode 100755 www/wiki/maintenance/storage/make-blobs create mode 100644 www/wiki/maintenance/storage/moveToExternal.php create mode 100644 www/wiki/maintenance/storage/orphanStats.php create mode 100644 www/wiki/maintenance/storage/recompressTracked.php create mode 100644 www/wiki/maintenance/storage/resolveStubs.php create mode 100644 www/wiki/maintenance/storage/storageTypeStats.php create mode 100644 www/wiki/maintenance/storage/testCompression.php create mode 100644 www/wiki/maintenance/storage/trackBlobs.php create mode 100644 www/wiki/maintenance/syncFileBackend.php create mode 100644 www/wiki/maintenance/tables.sql create mode 100644 www/wiki/maintenance/term/MWTerm.php create mode 100644 www/wiki/maintenance/tidyUpBug37714.php create mode 100644 www/wiki/maintenance/undelete.php create mode 100644 www/wiki/maintenance/update-keys.sql create mode 100755 www/wiki/maintenance/update.php create mode 100644 www/wiki/maintenance/updateArticleCount.php create mode 100644 www/wiki/maintenance/updateCollation.php create mode 100644 www/wiki/maintenance/updateCredits.php create mode 100644 www/wiki/maintenance/updateDoubleWidthSearch.php create mode 100644 www/wiki/maintenance/updateExtensionJsonSchema.php create mode 100644 www/wiki/maintenance/updateRestrictions.php create mode 100644 www/wiki/maintenance/updateSearchIndex.php create mode 100644 www/wiki/maintenance/updateSpecialPages.php create mode 100644 www/wiki/maintenance/userDupes.inc create mode 100644 www/wiki/maintenance/userOptions.php create mode 100644 www/wiki/maintenance/validateRegistrationFile.php create mode 100644 www/wiki/maintenance/view.php create mode 100644 www/wiki/maintenance/wrapOldPasswords.php (limited to 'www/wiki/maintenance') diff --git a/www/wiki/maintenance/.htaccess b/www/wiki/maintenance/.htaccess new file mode 100644 index 00000000..3a428827 --- /dev/null +++ b/www/wiki/maintenance/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/www/wiki/maintenance/7zip.inc b/www/wiki/maintenance/7zip.inc new file mode 100644 index 00000000..9c1093be --- /dev/null +++ b/www/wiki/maintenance/7zip.inc @@ -0,0 +1,96 @@ + + * 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 + * @ingroup Maintenance + */ + +/** + * Stream wrapper around 7za filter program. + * Required since we can't pass an open file resource to XMLReader->open() + * which is used for the text prefetch. + * + * @ingroup Maintenance + */ +class SevenZipStream { + protected $stream; + + private function stripPath( $path ) { + $prefix = 'mediawiki.compress.7z://'; + + return substr( $path, strlen( $prefix ) ); + } + + function stream_open( $path, $mode, $options, &$opened_path ) { + if ( $mode[0] == 'r' ) { + $options = 'e -bd -so'; + } elseif ( $mode[0] == 'w' ) { + $options = 'a -bd -si'; + } else { + return false; + } + $arg = wfEscapeShellArg( $this->stripPath( $path ) ); + $command = "7za $options $arg"; + if ( !wfIsWindows() ) { + // Suppress the stupid messages on stderr + $command .= ' 2>/dev/null'; + } + $this->stream = popen( $command, $mode[0] ); // popen() doesn't like two-letter modes + return ( $this->stream !== false ); + } + + function url_stat( $path, $flags ) { + return stat( $this->stripPath( $path ) ); + } + + // This is all so lame; there should be a default class we can extend + + function stream_close() { + return fclose( $this->stream ); + } + + function stream_flush() { + return fflush( $this->stream ); + } + + function stream_read( $count ) { + return fread( $this->stream, $count ); + } + + function stream_write( $data ) { + return fwrite( $this->stream, $data ); + } + + function stream_tell() { + return ftell( $this->stream ); + } + + function stream_eof() { + return feof( $this->stream ); + } + + function stream_seek( $offset, $whence ) { + return fseek( $this->stream, $offset, $whence ); + } +} + +stream_wrapper_register( 'mediawiki.compress.7z', SevenZipStream::class ); diff --git a/www/wiki/maintenance/CodeCleanerGlobalsPass.inc b/www/wiki/maintenance/CodeCleanerGlobalsPass.inc new file mode 100644 index 00000000..9ccf6d63 --- /dev/null +++ b/www/wiki/maintenance/CodeCleanerGlobalsPass.inc @@ -0,0 +1,51 @@ + + * + * 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 Maintenance + * + * @author Justin Hileman + */ + +/** + * Prefix the real command with a bunch of 'global $VAR;' commands, one for each global. + * This will make the shell behave as if it was running in the global scope (almost; + * variables created in the shell won't become global if no global variable by that name + * existed before). + */ +class CodeCleanerGlobalsPass extends \Psy\CodeCleaner\CodeCleanerPass { + private static $superglobals = [ + 'GLOBALS', '_SERVER', '_ENV', '_FILES', '_COOKIE', '_POST', '_GET', '_SESSION' + ]; + + public function beforeTraverse( array $nodes ) { + $names = []; + foreach ( array_diff( array_keys( $GLOBALS ), self::$superglobals ) as $name ) { + array_push( $names, new \PhpParser\Node\Expr\Variable( $name ) ); + } + + array_unshift( $nodes, new \PhpParser\Node\Stmt\Global_( $names ) ); + + return $nodes; + } +} diff --git a/www/wiki/maintenance/Doxyfile b/www/wiki/maintenance/Doxyfile new file mode 100644 index 00000000..7e9220c7 --- /dev/null +++ b/www/wiki/maintenance/Doxyfile @@ -0,0 +1,398 @@ +# Doxyfile 1.8.6 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for MediaWiki. +# +# Some placeholders have been added for MediaWiki usage: +# OUTPUT_DIRECTORY = {{OUTPUT_DIRECTORY}} +# CURRENT_VERSION = {{CURRENT_VERSION}} +# STRIP_FROM_PATH = {{STRIP_FROM_PATH}} +# INPUT = {{INPUT}} +# +# To generate documentation run: php mwdocgen.php --no-extensions + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = MediaWiki +PROJECT_NUMBER = {{CURRENT_VERSION}} +PROJECT_BRIEF = +PROJECT_LOGO = +OUTPUT_DIRECTORY = {{OUTPUT_DIRECTORY}} +CREATE_SUBDIRS = NO +OUTPUT_LANGUAGE = English +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = {{STRIP_FROM_PATH}} +STRIP_FROM_INC_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +TAB_SIZE = 4 +ALIASES = "type{1}= \1 :" \ + "types{2}= \1 or \2 :" \ + "types{3}= \1 , \2 , or \3 :" \ + "arrayof{2}= Array of \2" \ + "null=\type{Null}" \ + "boolean=\type{Boolean}" \ + "bool=\type{Boolean}" \ + "integer=\type{Integer}" \ + "int=\type{Integer}" \ + "string=\type{String}" \ + "str=\type{String}" \ + "mixed=\type{Mixed}" \ + "access=\par Access:\n" \ + "private=\access private" \ + "protected=\access protected" \ + "public=\access public" \ + "copyright=\note" \ + "license=\note" \ + "codeCoverageIgnore=" \ + "codingStandardsIgnoreStart=" \ + "group=" \ + "covers=" \ + "dataProvider=" \ + "expectedException=" \ + "expectedExceptionMessage=" +TCL_SUBST = +OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_JAVA = NO +OPTIMIZE_FOR_FORTRAN = NO +OPTIMIZE_OUTPUT_VHDL = NO +EXTENSION_MAPPING = +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES +BUILTIN_STL_SUPPORT = NO +CPP_CLI_SUPPORT = NO +SIP_SUPPORT = NO +IDL_PROPERTY_SUPPORT = YES +DISTRIBUTE_GROUP_DOC = YES +SUBGROUPING = YES +INLINE_GROUPED_CLASSES = NO +INLINE_SIMPLE_STRUCTS = NO +TYPEDEF_HIDES_STRUCT = NO +LOOKUP_CACHE_SIZE = 2 +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +EXTRACT_PACKAGE = NO +EXTRACT_STATIC = YES +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = NO +EXTRACT_ANON_NSPACES = NO +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = YES +INTERNAL_DOCS = NO +CASE_SENSE_NAMES = YES +HIDE_SCOPE_NAMES = NO +SHOW_INCLUDE_FILES = YES +SHOW_GROUPED_MEMB_INC = NO +FORCE_LOCAL_INCLUDES = NO +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +SORT_BRIEF_DOCS = YES +SORT_MEMBERS_CTORS_1ST = NO +SORT_GROUP_NAMES = NO +SORT_BY_SCOPE_NAME = NO +STRICT_PROTO_MATCHING = NO +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +GENERATE_DEPRECATEDLIST= YES +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = YES +SHOW_FILES = YES +SHOW_NAMESPACES = NO +FILE_VERSION_FILTER = +LAYOUT_FILE = +CITE_BIB_FILES = +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_NO_PARAMDOC = NO +WARN_FORMAT = "$file:$line: $text" +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = {{INPUT}} +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.d \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.idl \ + *.odl \ + *.cs \ + *.php \ + *.php5 \ + *.inc \ + *.m \ + *.mm \ + *.dox \ + *.py \ + *.C \ + *.CC \ + *.C++ \ + *.II \ + *.I++ \ + *.H \ + *.HH \ + *.H++ \ + *.CS \ + *.PHP \ + *.PHP5 \ + *.M \ + *.MM \ + *.PY \ + *.txt \ + README +RECURSIVE = YES +EXCLUDE = {{EXCLUDE}} +EXCLUDE_SYMLINKS = YES +EXCLUDE_PATTERNS = LocalSettings.php \ + AdminSettings.php \ + StartProfiler.php \ + .svn \ + */.git/* \ + {{EXCLUDE_PATTERNS}} +EXCLUDE_SYMBOLS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = * +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = "{{INPUT_FILTER}}" +FILTER_PATTERNS = +FILTER_SOURCE_FILES = NO +FILTER_SOURCE_PATTERNS = +USE_MDFILE_AS_MAINPAGE = +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES +REFERENCES_LINK_SOURCE = YES +SOURCE_TOOLTIPS = YES +USE_HTAGS = NO +VERBATIM_HEADERS = YES +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = NO +COLS_IN_ALPHA_INDEX = 5 +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = +HTML_FOOTER = +HTML_STYLESHEET = +HTML_EXTRA_STYLESHEET = +HTML_EXTRA_FILES = +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_TIMESTAMP = YES +HTML_DYNAMIC_SECTIONS = NO +HTML_INDEX_NUM_ENTRIES = 100 +GENERATE_DOCSET = NO +DOCSET_FEEDNAME = "Doxygen generated docs" +DOCSET_BUNDLE_ID = org.doxygen.Project +DOCSET_PUBLISHER_ID = org.doxygen.Publisher +DOCSET_PUBLISHER_NAME = Publisher +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +CHM_INDEX_ENCODING = +BINARY_TOC = NO +TOC_EXPAND = YES +GENERATE_QHP = NO +QCH_FILE = +QHP_NAMESPACE = org.doxygen.Project +QHP_VIRTUAL_FOLDER = doc +QHP_CUST_FILTER_NAME = +QHP_CUST_FILTER_ATTRS = +QHP_SECT_FILTER_ATTRS = +QHG_LOCATION = +GENERATE_ECLIPSEHELP = NO +ECLIPSE_DOC_ID = org.doxygen.Project +DISABLE_INDEX = NO +GENERATE_TREEVIEW = YES +ENUM_VALUES_PER_LINE = 4 +TREEVIEW_WIDTH = 250 +EXT_LINKS_IN_WINDOW = NO +FORMULA_FONTSIZE = 10 +FORMULA_TRANSPARENT = YES +USE_MATHJAX = NO +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = http://www.mathjax.org/mathjax +MATHJAX_EXTENSIONS = +MATHJAX_CODEFILE = +SEARCHENGINE = YES +SERVER_BASED_SEARCH = YES +EXTERNAL_SEARCH = NO +SEARCHENGINE_URL = +SEARCHDATA_FILE = searchdata.xml +EXTERNAL_SEARCH_ID = +EXTRA_SEARCH_MAPPINGS = +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = latex +LATEX_CMD_NAME = latex +MAKEINDEX_CMD_NAME = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = a4wide +EXTRA_PACKAGES = +LATEX_HEADER = +LATEX_FOOTER = +LATEX_EXTRA_FILES = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +LATEX_SOURCE_CODE = NO +LATEX_BIB_STYLE = plain +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = {{GENERATE_MAN}} +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_LINKS = NO +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = NO +XML_OUTPUT = xml +XML_PROGRAMLISTING = YES +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- +GENERATE_DOCBOOK = NO +DOCBOOK_OUTPUT = docbook +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- +GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- +TAGFILES = +GENERATE_TAGFILE = {{OUTPUT_DIRECTORY}}/html/tagfile.xml +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +EXTERNAL_PAGES = YES +PERL_PATH = /usr/bin/perl +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = NO +MSCGEN_PATH = +DIA_PATH = +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = {{HAVE_DOT}} +DOT_NUM_THREADS = 0 +DOT_FONTNAME = Helvetica +DOT_FONTSIZE = 10 +DOT_FONTPATH = +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +UML_LOOK = NO +UML_LIMIT_NUM_FIELDS = 10 +TEMPLATE_RELATIONS = NO +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DOT_IMAGE_FORMAT = png +INTERACTIVE_SVG = NO +DOT_PATH = +DOTFILE_DIRS = +MSCFILE_DIRS = +DIAFILE_DIRS = +DOT_GRAPH_MAX_NODES = 50 +MAX_DOT_GRAPH_DEPTH = 1000 +DOT_TRANSPARENT = NO +DOT_MULTI_TARGETS = YES +GENERATE_LEGEND = YES +DOT_CLEANUP = YES diff --git a/www/wiki/maintenance/Maintenance.php b/www/wiki/maintenance/Maintenance.php new file mode 100644 index 00000000..13fee9c6 --- /dev/null +++ b/www/wiki/maintenance/Maintenance.php @@ -0,0 +1,1704 @@ + the option and 1 => parameter value. + * + * @var array + */ + public $orderedOptions = []; + + /** + * Default constructor. Children should call this *first* if implementing + * their own constructors + */ + public function __construct() { + // Setup $IP, using MW_INSTALL_PATH if it exists + global $IP; + $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== '' + ? getenv( 'MW_INSTALL_PATH' ) + : realpath( __DIR__ . '/..' ); + + $this->addDefaultParams(); + register_shutdown_function( [ $this, 'outputChanneled' ], false ); + } + + /** + * Should we execute the maintenance script, or just allow it to be included + * as a standalone class? It checks that the call stack only includes this + * function and "requires" (meaning was called from the file scope) + * + * @return bool + */ + public static function shouldExecute() { + global $wgCommandLineMode; + + if ( !function_exists( 'debug_backtrace' ) ) { + // If someone has a better idea... + return $wgCommandLineMode; + } + + $bt = debug_backtrace(); + $count = count( $bt ); + if ( $count < 2 ) { + return false; // sanity + } + if ( $bt[0]['class'] !== self::class || $bt[0]['function'] !== 'shouldExecute' ) { + return false; // last call should be to this function + } + $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ]; + for ( $i = 1; $i < $count; $i++ ) { + if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) { + return false; // previous calls should all be "requires" + } + } + + return true; + } + + /** + * Do the actual work. All child classes will need to implement this + */ + abstract public function execute(); + + /** + * Add a parameter to the script. Will be displayed on --help + * with the associated description + * + * @param string $name The name of the param (help, version, etc) + * @param string $description The description of the param to show on --help + * @param bool $required Is the param required? + * @param bool $withArg Is an argument required with this option? + * @param string|bool $shortName Character to use as short name + * @param bool $multiOccurrence Can this option be passed multiple times? + */ + protected function addOption( $name, $description, $required = false, + $withArg = false, $shortName = false, $multiOccurrence = false + ) { + $this->mParams[$name] = [ + 'desc' => $description, + 'require' => $required, + 'withArg' => $withArg, + 'shortName' => $shortName, + 'multiOccurrence' => $multiOccurrence + ]; + + if ( $shortName !== false ) { + $this->mShortParamsMap[$shortName] = $name; + } + } + + /** + * Checks to see if a particular param exists. + * @param string $name The name of the param + * @return bool + */ + protected function hasOption( $name ) { + return isset( $this->mOptions[$name] ); + } + + /** + * Get an option, or return the default. + * + * If the option was added to support multiple occurrences, + * this will return an array. + * + * @param string $name The name of the param + * @param mixed $default Anything you want, default null + * @return mixed + */ + protected function getOption( $name, $default = null ) { + if ( $this->hasOption( $name ) ) { + return $this->mOptions[$name]; + } else { + // Set it so we don't have to provide the default again + $this->mOptions[$name] = $default; + + return $this->mOptions[$name]; + } + } + + /** + * Add some args that are needed + * @param string $arg Name of the arg, like 'start' + * @param string $description Short description of the arg + * @param bool $required Is this required? + */ + protected function addArg( $arg, $description, $required = true ) { + $this->mArgList[] = [ + 'name' => $arg, + 'desc' => $description, + 'require' => $required + ]; + } + + /** + * Remove an option. Useful for removing options that won't be used in your script. + * @param string $name The option to remove. + */ + protected function deleteOption( $name ) { + unset( $this->mParams[$name] ); + } + + /** + * Set the description text. + * @param string $text The text of the description + */ + protected function addDescription( $text ) { + $this->mDescription = $text; + } + + /** + * Does a given argument exist? + * @param int $argId The integer value (from zero) for the arg + * @return bool + */ + protected function hasArg( $argId = 0 ) { + return isset( $this->mArgs[$argId] ); + } + + /** + * Get an argument. + * @param int $argId The integer value (from zero) for the arg + * @param mixed $default The default if it doesn't exist + * @return mixed + */ + protected function getArg( $argId = 0, $default = null ) { + return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default; + } + + /** + * Returns batch size + * + * @since 1.31 + * + * @return int|null + */ + protected function getBatchSize() { + return $this->mBatchSize; + } + + /** + * Set the batch size. + * @param int $s The number of operations to do in a batch + */ + protected function setBatchSize( $s = 0 ) { + $this->mBatchSize = $s; + + // If we support $mBatchSize, show the option. + // Used to be in addDefaultParams, but in order for that to + // work, subclasses would have to call this function in the constructor + // before they called parent::__construct which is just weird + // (and really wasn't done). + if ( $this->mBatchSize ) { + $this->addOption( 'batch-size', 'Run this many operations ' . + 'per batch, default: ' . $this->mBatchSize, false, true ); + if ( isset( $this->mParams['batch-size'] ) ) { + // This seems a little ugly... + $this->mDependantParameters['batch-size'] = $this->mParams['batch-size']; + } + } + } + + /** + * Get the script's name + * @return string + */ + public function getName() { + return $this->mSelf; + } + + /** + * Return input from stdin. + * @param int $len The number of bytes to read. If null, just return the handle. + * Maintenance::STDIN_ALL returns the full length + * @return mixed + */ + protected function getStdin( $len = null ) { + if ( $len == self::STDIN_ALL ) { + return file_get_contents( 'php://stdin' ); + } + $f = fopen( 'php://stdin', 'rt' ); + if ( !$len ) { + return $f; + } + $input = fgets( $f, $len ); + fclose( $f ); + + return rtrim( $input ); + } + + /** + * @return bool + */ + public function isQuiet() { + return $this->mQuiet; + } + + /** + * Throw some output to the user. Scripts can call this with no fears, + * as we handle all --quiet stuff here + * @param string $out The text to show to the user + * @param mixed $channel Unique identifier for the channel. See function outputChanneled. + */ + protected function output( $out, $channel = null ) { + // This is sometimes called very early, before Setup.php is included. + if ( class_exists( MediaWikiServices::class ) ) { + // Try to periodically flush buffered metrics to avoid OOMs + $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); + if ( $stats->getDataCount() > 1000 ) { + MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() ); + } + } + + if ( $this->mQuiet ) { + return; + } + if ( $channel === null ) { + $this->cleanupChanneled(); + print $out; + } else { + $out = preg_replace( '/\n\z/', '', $out ); + $this->outputChanneled( $out, $channel ); + } + } + + /** + * Throw an error to the user. Doesn't respect --quiet, so don't use + * this for non-error output + * @param string $err The error to display + * @param int $die Deprecated since 1.31, use Maintenance::fatalError() instead + */ + protected function error( $err, $die = 0 ) { + if ( intval( $die ) !== 0 ) { + wfDeprecated( __METHOD__ . '( $err, $die )', '1.31' ); + $this->fatalError( $err, intval( $die ) ); + } + $this->outputChanneled( false ); + if ( + ( PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg' ) && + !defined( 'MW_PHPUNIT_TEST' ) + ) { + fwrite( STDERR, $err . "\n" ); + } else { + print $err; + } + } + + /** + * Output a message and terminate the current script. + * + * @param string $msg Error message + * @param int $exitCode PHP exit status. Should be in range 1-254. + * @since 1.31 + */ + protected function fatalError( $msg, $exitCode = 1 ) { + $this->error( $msg ); + exit( $exitCode ); + } + + private $atLineStart = true; + private $lastChannel = null; + + /** + * Clean up channeled output. Output a newline if necessary. + */ + public function cleanupChanneled() { + if ( !$this->atLineStart ) { + print "\n"; + $this->atLineStart = true; + } + } + + /** + * Message outputter with channeled message support. Messages on the + * same channel are concatenated, but any intervening messages in another + * channel start a new line. + * @param string $msg The message without trailing newline + * @param string $channel Channel identifier or null for no + * channel. Channel comparison uses ===. + */ + public function outputChanneled( $msg, $channel = null ) { + if ( $msg === false ) { + $this->cleanupChanneled(); + + return; + } + + // End the current line if necessary + if ( !$this->atLineStart && $channel !== $this->lastChannel ) { + print "\n"; + } + + print $msg; + + $this->atLineStart = false; + if ( $channel === null ) { + // For unchanneled messages, output trailing newline immediately + print "\n"; + $this->atLineStart = true; + } + $this->lastChannel = $channel; + } + + /** + * Does the script need different DB access? By default, we give Maintenance + * scripts normal rights to the DB. Sometimes, a script needs admin rights + * access for a reason and sometimes they want no access. Subclasses should + * override and return one of the following values, as needed: + * Maintenance::DB_NONE - For no DB access at all + * Maintenance::DB_STD - For normal DB access, default + * Maintenance::DB_ADMIN - For admin DB access + * @return int + */ + public function getDbType() { + return self::DB_STD; + } + + /** + * Add the default parameters to the scripts + */ + protected function addDefaultParams() { + # Generic (non script dependant) options: + + $this->addOption( 'help', 'Display this help message', false, false, 'h' ); + $this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' ); + $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true ); + $this->addOption( 'wiki', 'For specifying the wiki ID', false, true ); + $this->addOption( 'globals', 'Output globals at the end of processing for debugging' ); + $this->addOption( + 'memory-limit', + 'Set a specific memory limit for the script, ' + . '"max" for no limit or "default" to avoid changing it', + false, + true + ); + $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " . + "http://en.wikipedia.org. This is sometimes necessary because " . + "server name detection may fail in command line scripts.", false, true ); + $this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true ); + // This is named --mwdebug, because --debug would conflict in the phpunit.php CLI script. + $this->addOption( 'mwdebug', 'Enable built-in MediaWiki development settings', false, true ); + + # Save generic options to display them separately in help + $this->mGenericParameters = $this->mParams; + + # Script dependant options: + + // If we support a DB, show the options + if ( $this->getDbType() > 0 ) { + $this->addOption( 'dbuser', 'The DB user to use for this script', false, true ); + $this->addOption( 'dbpass', 'The password to use for this script', false, true ); + } + + # Save additional script dependant options to display + #  them separately in help + $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters ); + } + + /** + * @since 1.24 + * @return Config + */ + public function getConfig() { + if ( $this->config === null ) { + $this->config = MediaWikiServices::getInstance()->getMainConfig(); + } + + return $this->config; + } + + /** + * @since 1.24 + * @param Config $config + */ + public function setConfig( Config $config ) { + $this->config = $config; + } + + /** + * Indicate that the specified extension must be + * loaded before the script can run. + * + * This *must* be called in the constructor. + * + * @since 1.28 + * @param string $name + */ + protected function requireExtension( $name ) { + $this->requiredExtensions[] = $name; + } + + /** + * Verify that the required extensions are installed + * + * @since 1.28 + */ + public function checkRequiredExtensions() { + $registry = ExtensionRegistry::getInstance(); + $missing = []; + foreach ( $this->requiredExtensions as $name ) { + if ( !$registry->isLoaded( $name ) ) { + $missing[] = $name; + } + } + + if ( $missing ) { + $joined = implode( ', ', $missing ); + $msg = "The following extensions are required to be installed " + . "for this script to run: $joined. Please enable them and then try again."; + $this->fatalError( $msg ); + } + } + + /** + * Set triggers like when to try to run deferred updates + * @since 1.28 + */ + public function setAgentAndTriggers() { + if ( function_exists( 'posix_getpwuid' ) ) { + $agent = posix_getpwuid( posix_geteuid() )['name']; + } else { + $agent = 'sysadmin'; + } + $agent .= '@' . wfHostname(); + + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + // Add a comment for easy SHOW PROCESSLIST interpretation + $lbFactory->setAgentName( + mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent + ); + self::setLBFactoryTriggers( $lbFactory, $this->getConfig() ); + } + + /** + * @param LBFactory $LBFactory + * @param Config $config + * @since 1.28 + */ + public static function setLBFactoryTriggers( LBFactory $LBFactory, Config $config ) { + $services = MediaWikiServices::getInstance(); + $stats = $services->getStatsdDataFactory(); + // Hook into period lag checks which often happen in long-running scripts + $lbFactory = $services->getDBLoadBalancerFactory(); + $lbFactory->setWaitForReplicationListener( + __METHOD__, + function () use ( $stats, $config ) { + // Check config in case of JobRunner and unit tests + if ( $config->get( 'CommandLineMode' ) ) { + DeferredUpdates::tryOpportunisticExecute( 'run' ); + } + // Try to periodically flush buffered metrics to avoid OOMs + MediaWiki::emitBufferedStatsdData( $stats, $config ); + } + ); + // Check for other windows to run them. A script may read or do a few writes + // to the master but mostly be writing to something else, like a file store. + $lbFactory->getMainLB()->setTransactionListener( + __METHOD__, + function ( $trigger ) use ( $stats, $config ) { + // Check config in case of JobRunner and unit tests + if ( $config->get( 'CommandLineMode' ) && $trigger === IDatabase::TRIGGER_COMMIT ) { + DeferredUpdates::tryOpportunisticExecute( 'run' ); + } + // Try to periodically flush buffered metrics to avoid OOMs + MediaWiki::emitBufferedStatsdData( $stats, $config ); + } + ); + } + + /** + * Run a child maintenance script. Pass all of the current arguments + * to it. + * @param string $maintClass A name of a child maintenance class + * @param string $classFile Full path of where the child is + * @return Maintenance + */ + public function runChild( $maintClass, $classFile = null ) { + // Make sure the class is loaded first + if ( !class_exists( $maintClass ) ) { + if ( $classFile ) { + require_once $classFile; + } + if ( !class_exists( $maintClass ) ) { + $this->error( "Cannot spawn child: $maintClass" ); + } + } + + /** + * @var $child Maintenance + */ + $child = new $maintClass(); + $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs ); + if ( !is_null( $this->mDb ) ) { + $child->setDB( $this->mDb ); + } + + return $child; + } + + /** + * Do some sanity checking and basic setup + */ + public function setup() { + global $IP, $wgCommandLineMode; + + # Abort if called from a web server + # wfIsCLI() is not available yet + if ( PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ) { + $this->fatalError( 'This script must be run from the command line' ); + } + + if ( $IP === null ) { + $this->fatalError( "\$IP not set, aborting!\n" . + '(Did you forget to call parent::__construct() in your maintenance script?)' ); + } + + # Make sure we can handle script parameters + if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) { + $this->fatalError( 'Cannot get command line arguments, register_argc_argv is set to false' ); + } + + // Send PHP warnings and errors to stderr instead of stdout. + // This aids in diagnosing problems, while keeping messages + // out of redirected output. + if ( ini_get( 'display_errors' ) ) { + ini_set( 'display_errors', 'stderr' ); + } + + $this->loadParamsAndArgs(); + $this->maybeHelp(); + + # Set the memory limit + # Note we need to set it again later in cache LocalSettings changed it + $this->adjustMemoryLimit(); + + # Set max execution time to 0 (no limit). PHP.net says that + # "When running PHP from the command line the default setting is 0." + # But sometimes this doesn't seem to be the case. + ini_set( 'max_execution_time', 0 ); + + # Define us as being in MediaWiki + define( 'MEDIAWIKI', true ); + + $wgCommandLineMode = true; + + # Turn off output buffering if it's on + while ( ob_get_level() > 0 ) { + ob_end_flush(); + } + + $this->validateParamsAndArgs(); + } + + /** + * Normally we disable the memory_limit when running admin scripts. + * Some scripts may wish to actually set a limit, however, to avoid + * blowing up unexpectedly. We also support a --memory-limit option, + * to allow sysadmins to explicitly set one if they'd prefer to override + * defaults (or for people using Suhosin which yells at you for trying + * to disable the limits) + * @return string + */ + public function memoryLimit() { + $limit = $this->getOption( 'memory-limit', 'max' ); + $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood + return $limit; + } + + /** + * Adjusts PHP's memory limit to better suit our needs, if needed. + */ + protected function adjustMemoryLimit() { + $limit = $this->memoryLimit(); + if ( $limit == 'max' ) { + $limit = -1; // no memory limit + } + if ( $limit != 'default' ) { + ini_set( 'memory_limit', $limit ); + } + } + + /** + * Activate the profiler (assuming $wgProfiler is set) + */ + protected function activateProfiler() { + global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits; + + $output = $this->getOption( 'profiler' ); + if ( !$output ) { + return; + } + + if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) { + $class = $wgProfiler['class']; + /** @var Profiler $profiler */ + $profiler = new $class( + [ 'sampling' => 1, 'output' => [ $output ] ] + + $wgProfiler + + [ 'threshold' => $wgProfileLimit ] + ); + $profiler->setTemplated( true ); + Profiler::replaceStubInstance( $profiler ); + } + + $trxProfiler = Profiler::instance()->getTransactionProfiler(); + $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) ); + $trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ ); + } + + /** + * Clear all params and arguments. + */ + public function clearParamsAndArgs() { + $this->mOptions = []; + $this->mArgs = []; + $this->mInputLoaded = false; + } + + /** + * Load params and arguments from a given array + * of command-line arguments + * + * @since 1.27 + * @param array $argv + */ + public function loadWithArgv( $argv ) { + $options = []; + $args = []; + $this->orderedOptions = []; + + # Parse arguments + for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) { + if ( $arg == '--' ) { + # End of options, remainder should be considered arguments + $arg = next( $argv ); + while ( $arg !== false ) { + $args[] = $arg; + $arg = next( $argv ); + } + break; + } elseif ( substr( $arg, 0, 2 ) == '--' ) { + # Long options + $option = substr( $arg, 2 ); + if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) { + $param = next( $argv ); + if ( $param === false ) { + $this->error( "\nERROR: $option parameter needs a value after it\n" ); + $this->maybeHelp( true ); + } + + $this->setParam( $options, $option, $param ); + } else { + $bits = explode( '=', $option, 2 ); + if ( count( $bits ) > 1 ) { + $option = $bits[0]; + $param = $bits[1]; + } else { + $param = 1; + } + + $this->setParam( $options, $option, $param ); + } + } elseif ( $arg == '-' ) { + # Lonely "-", often used to indicate stdin or stdout. + $args[] = $arg; + } elseif ( substr( $arg, 0, 1 ) == '-' ) { + # Short options + $argLength = strlen( $arg ); + for ( $p = 1; $p < $argLength; $p++ ) { + $option = $arg[$p]; + if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) { + $option = $this->mShortParamsMap[$option]; + } + + if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) { + $param = next( $argv ); + if ( $param === false ) { + $this->error( "\nERROR: $option parameter needs a value after it\n" ); + $this->maybeHelp( true ); + } + $this->setParam( $options, $option, $param ); + } else { + $this->setParam( $options, $option, 1 ); + } + } + } else { + $args[] = $arg; + } + } + + $this->mOptions = $options; + $this->mArgs = $args; + $this->loadSpecialVars(); + $this->mInputLoaded = true; + } + + /** + * Helper function used solely by loadParamsAndArgs + * to prevent code duplication + * + * This sets the param in the options array based on + * whether or not it can be specified multiple times. + * + * @since 1.27 + * @param array $options + * @param string $option + * @param mixed $value + */ + private function setParam( &$options, $option, $value ) { + $this->orderedOptions[] = [ $option, $value ]; + + if ( isset( $this->mParams[$option] ) ) { + $multi = $this->mParams[$option]['multiOccurrence']; + } else { + $multi = false; + } + $exists = array_key_exists( $option, $options ); + if ( $multi && $exists ) { + $options[$option][] = $value; + } elseif ( $multi ) { + $options[$option] = [ $value ]; + } elseif ( !$exists ) { + $options[$option] = $value; + } else { + $this->error( "\nERROR: $option parameter given twice\n" ); + $this->maybeHelp( true ); + } + } + + /** + * Process command line arguments + * $mOptions becomes an array with keys set to the option names + * $mArgs becomes a zero-based array containing the non-option arguments + * + * @param string $self The name of the script, if any + * @param array $opts An array of options, in form of key=>value + * @param array $args An array of command line arguments + */ + public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) { + # If we were given opts or args, set those and return early + if ( $self ) { + $this->mSelf = $self; + $this->mInputLoaded = true; + } + if ( $opts ) { + $this->mOptions = $opts; + $this->mInputLoaded = true; + } + if ( $args ) { + $this->mArgs = $args; + $this->mInputLoaded = true; + } + + # If we've already loaded input (either by user values or from $argv) + # skip on loading it again. The array_shift() will corrupt values if + # it's run again and again + if ( $this->mInputLoaded ) { + $this->loadSpecialVars(); + + return; + } + + global $argv; + $this->mSelf = $argv[0]; + $this->loadWithArgv( array_slice( $argv, 1 ) ); + } + + /** + * Run some validation checks on the params, etc + */ + protected function validateParamsAndArgs() { + $die = false; + # Check to make sure we've got all the required options + foreach ( $this->mParams as $opt => $info ) { + if ( $info['require'] && !$this->hasOption( $opt ) ) { + $this->error( "Param $opt required!" ); + $die = true; + } + } + # Check arg list too + foreach ( $this->mArgList as $k => $info ) { + if ( $info['require'] && !$this->hasArg( $k ) ) { + $this->error( 'Argument <' . $info['name'] . '> required!' ); + $die = true; + } + } + + if ( $die ) { + $this->maybeHelp( true ); + } + } + + /** + * Handle the special variables that are global to all scripts + */ + protected function loadSpecialVars() { + if ( $this->hasOption( 'dbuser' ) ) { + $this->mDbUser = $this->getOption( 'dbuser' ); + } + if ( $this->hasOption( 'dbpass' ) ) { + $this->mDbPass = $this->getOption( 'dbpass' ); + } + if ( $this->hasOption( 'quiet' ) ) { + $this->mQuiet = true; + } + if ( $this->hasOption( 'batch-size' ) ) { + $this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); + } + } + + /** + * Maybe show the help. + * @param bool $force Whether to force the help to show, default false + */ + protected function maybeHelp( $force = false ) { + if ( !$force && !$this->hasOption( 'help' ) ) { + return; + } + + $screenWidth = 80; // TODO: Calculate this! + $tab = " "; + $descWidth = $screenWidth - ( 2 * strlen( $tab ) ); + + ksort( $this->mParams ); + $this->mQuiet = false; + + // Description ... + if ( $this->mDescription ) { + $this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" ); + } + $output = "\nUsage: php " . basename( $this->mSelf ); + + // ... append parameters ... + if ( $this->mParams ) { + $output .= " [--" . implode( "|--", array_keys( $this->mParams ) ) . "]"; + } + + // ... and append arguments. + if ( $this->mArgList ) { + $output .= ' '; + foreach ( $this->mArgList as $k => $arg ) { + if ( $arg['require'] ) { + $output .= '<' . $arg['name'] . '>'; + } else { + $output .= '[' . $arg['name'] . ']'; + } + if ( $k < count( $this->mArgList ) - 1 ) { + $output .= ' '; + } + } + } + $this->output( "$output\n\n" ); + + # TODO abstract some repetitive code below + + // Generic parameters + $this->output( "Generic maintenance parameters:\n" ); + foreach ( $this->mGenericParameters as $par => $info ) { + if ( $info['shortName'] !== false ) { + $par .= " (-{$info['shortName']})"; + } + $this->output( + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, + "\n$tab$tab" ) . "\n" + ); + } + $this->output( "\n" ); + + $scriptDependantParams = $this->mDependantParameters; + if ( count( $scriptDependantParams ) > 0 ) { + $this->output( "Script dependant parameters:\n" ); + // Parameters description + foreach ( $scriptDependantParams as $par => $info ) { + if ( $info['shortName'] !== false ) { + $par .= " (-{$info['shortName']})"; + } + $this->output( + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, + "\n$tab$tab" ) . "\n" + ); + } + $this->output( "\n" ); + } + + // Script specific parameters not defined on construction by + // Maintenance::addDefaultParams() + $scriptSpecificParams = array_diff_key( + # all script parameters: + $this->mParams, + # remove the Maintenance default parameters: + $this->mGenericParameters, + $this->mDependantParameters + ); + if ( count( $scriptSpecificParams ) > 0 ) { + $this->output( "Script specific parameters:\n" ); + // Parameters description + foreach ( $scriptSpecificParams as $par => $info ) { + if ( $info['shortName'] !== false ) { + $par .= " (-{$info['shortName']})"; + } + $this->output( + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, + "\n$tab$tab" ) . "\n" + ); + } + $this->output( "\n" ); + } + + // Print arguments + if ( count( $this->mArgList ) > 0 ) { + $this->output( "Arguments:\n" ); + // Arguments description + foreach ( $this->mArgList as $info ) { + $openChar = $info['require'] ? '<' : '['; + $closeChar = $info['require'] ? '>' : ']'; + $this->output( + wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " . + $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" + ); + } + $this->output( "\n" ); + } + + die( 1 ); + } + + /** + * Handle some last-minute setup here. + */ + public function finalSetup() { + global $wgCommandLineMode, $wgShowSQLErrors, $wgServer; + global $wgDBadminuser, $wgDBadminpassword; + global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf; + + # Turn off output buffering again, it might have been turned on in the settings files + if ( ob_get_level() ) { + ob_end_flush(); + } + # Same with these + $wgCommandLineMode = true; + + # Override $wgServer + if ( $this->hasOption( 'server' ) ) { + $wgServer = $this->getOption( 'server', $wgServer ); + } + + # If these were passed, use them + if ( $this->mDbUser ) { + $wgDBadminuser = $this->mDbUser; + } + if ( $this->mDbPass ) { + $wgDBadminpassword = $this->mDbPass; + } + + if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) { + $wgDBuser = $wgDBadminuser; + $wgDBpassword = $wgDBadminpassword; + + if ( $wgDBservers ) { + /** + * @var $wgDBservers array + */ + foreach ( $wgDBservers as $i => $server ) { + $wgDBservers[$i]['user'] = $wgDBuser; + $wgDBservers[$i]['password'] = $wgDBpassword; + } + } + if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) { + $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser; + $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword; + } + MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy(); + } + + # Apply debug settings + if ( $this->hasOption( 'mwdebug' ) ) { + require __DIR__ . '/../includes/DevelopmentSettings.php'; + } + + // Per-script profiling; useful for debugging + $this->activateProfiler(); + + $this->afterFinalSetup(); + + $wgShowSQLErrors = true; + + Wikimedia\suppressWarnings(); + set_time_limit( 0 ); + Wikimedia\restoreWarnings(); + + $this->adjustMemoryLimit(); + } + + /** + * Execute a callback function at the end of initialisation + */ + protected function afterFinalSetup() { + if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { + call_user_func( MW_CMDLINE_CALLBACK ); + } + } + + /** + * Potentially debug globals. Originally a feature only + * for refreshLinks + */ + public function globals() { + if ( $this->hasOption( 'globals' ) ) { + print_r( $GLOBALS ); + } + } + + /** + * Generic setup for most installs. Returns the location of LocalSettings + * @return string + */ + public function loadSettings() { + global $wgCommandLineMode, $IP; + + if ( isset( $this->mOptions['conf'] ) ) { + $settingsFile = $this->mOptions['conf']; + } elseif ( defined( "MW_CONFIG_FILE" ) ) { + $settingsFile = MW_CONFIG_FILE; + } else { + $settingsFile = "$IP/LocalSettings.php"; + } + if ( isset( $this->mOptions['wiki'] ) ) { + $bits = explode( '-', $this->mOptions['wiki'] ); + if ( count( $bits ) == 1 ) { + $bits[] = ''; + } + define( 'MW_DB', $bits[0] ); + define( 'MW_PREFIX', $bits[1] ); + } elseif ( isset( $this->mOptions['server'] ) ) { + // Provide the option for site admins to detect and configure + // multiple wikis based on server names. This offers --server + // as alternative to --wiki. + // See https://www.mediawiki.org/wiki/Manual:Wiki_family + $_SERVER['SERVER_NAME'] = $this->mOptions['server']; + } + + if ( !is_readable( $settingsFile ) ) { + $this->fatalError( "A copy of your installation's LocalSettings.php\n" . + "must exist and be readable in the source directory.\n" . + "Use --conf to specify it." ); + } + $wgCommandLineMode = true; + + return $settingsFile; + } + + /** + * Support function for cleaning up redundant text records + * @param bool $delete Whether or not to actually delete the records + * @author Rob Church + */ + public function purgeRedundantText( $delete = true ) { + # Data should come off the master, wrapped in a transaction + $dbw = $this->getDB( DB_MASTER ); + $this->beginTransaction( $dbw, __METHOD__ ); + + # Get "active" text records from the revisions table + $cur = []; + $this->output( 'Searching for active text records in revisions table...' ); + $res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] ); + foreach ( $res as $row ) { + $cur[] = $row->rev_text_id; + } + $this->output( "done.\n" ); + + # Get "active" text records from the archive table + $this->output( 'Searching for active text records in archive table...' ); + $res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] ); + foreach ( $res as $row ) { + # old pre-MW 1.5 records can have null ar_text_id's. + if ( $row->ar_text_id !== null ) { + $cur[] = $row->ar_text_id; + } + } + $this->output( "done.\n" ); + + # Get the IDs of all text records not in these sets + $this->output( 'Searching for inactive text records...' ); + $cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )'; + $res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] ); + $old = []; + foreach ( $res as $row ) { + $old[] = $row->old_id; + } + $this->output( "done.\n" ); + + # Inform the user of what we're going to do + $count = count( $old ); + $this->output( "$count inactive items found.\n" ); + + # Delete as appropriate + if ( $delete && $count ) { + $this->output( 'Deleting...' ); + $dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ ); + $this->output( "done.\n" ); + } + + # Done + $this->commitTransaction( $dbw, __METHOD__ ); + } + + /** + * Get the maintenance directory. + * @return string + */ + protected function getDir() { + return __DIR__; + } + + /** + * Returns a database to be used by current maintenance script. It can be set by setDB(). + * If not set, wfGetDB() will be used. + * This function has the same parameters as wfGetDB() + * + * @param int $db DB index (DB_REPLICA/DB_MASTER) + * @param string|string[] $groups default: empty array + * @param string|bool $wiki default: current wiki + * @return IMaintainableDatabase + */ + protected function getDB( $db, $groups = [], $wiki = false ) { + if ( is_null( $this->mDb ) ) { + return wfGetDB( $db, $groups, $wiki ); + } else { + return $this->mDb; + } + } + + /** + * Sets database object to be returned by getDB(). + * + * @param IDatabase $db + */ + public function setDB( IDatabase $db ) { + $this->mDb = $db; + } + + /** + * Begin a transcation on a DB + * + * This method makes it clear that begin() is called from a maintenance script, + * which has outermost scope. This is safe, unlike $dbw->begin() called in other places. + * + * @param IDatabase $dbw + * @param string $fname Caller name + * @since 1.27 + */ + protected function beginTransaction( IDatabase $dbw, $fname ) { + $dbw->begin( $fname ); + } + + /** + * Commit the transcation on a DB handle and wait for replica DBs to catch up + * + * This method makes it clear that commit() is called from a maintenance script, + * which has outermost scope. This is safe, unlike $dbw->commit() called in other places. + * + * @param IDatabase $dbw + * @param string $fname Caller name + * @return bool Whether the replica DB wait succeeded + * @since 1.27 + */ + protected function commitTransaction( IDatabase $dbw, $fname ) { + $dbw->commit( $fname ); + try { + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->waitForReplication( + [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] + ); + $this->lastReplicationWait = microtime( true ); + + return true; + } catch ( DBReplicationWaitError $e ) { + return false; + } + } + + /** + * Rollback the transcation on a DB handle + * + * This method makes it clear that rollback() is called from a maintenance script, + * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places. + * + * @param IDatabase $dbw + * @param string $fname Caller name + * @since 1.27 + */ + protected function rollbackTransaction( IDatabase $dbw, $fname ) { + $dbw->rollback( $fname ); + } + + /** + * Lock the search index + * @param IMaintainableDatabase &$db + */ + private function lockSearchindex( $db ) { + $write = [ 'searchindex' ]; + $read = [ + 'page', + 'revision', + 'text', + 'interwiki', + 'l10n_cache', + 'user', + 'page_restrictions' + ]; + $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ ); + } + + /** + * Unlock the tables + * @param IMaintainableDatabase &$db + */ + private function unlockSearchindex( $db ) { + $db->unlockTables( __CLASS__ . '::' . __METHOD__ ); + } + + /** + * Unlock and lock again + * Since the lock is low-priority, queued reads will be able to complete + * @param IMaintainableDatabase &$db + */ + private function relockSearchindex( $db ) { + $this->unlockSearchindex( $db ); + $this->lockSearchindex( $db ); + } + + /** + * Perform a search index update with locking + * @param int $maxLockTime The maximum time to keep the search index locked. + * @param string $callback The function that will update the function. + * @param IMaintainableDatabase $dbw + * @param array $results + */ + public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) { + $lockTime = time(); + + # Lock searchindex + if ( $maxLockTime ) { + $this->output( " --- Waiting for lock ---" ); + $this->lockSearchindex( $dbw ); + $lockTime = time(); + $this->output( "\n" ); + } + + # Loop through the results and do a search update + foreach ( $results as $row ) { + # Allow reads to be processed + if ( $maxLockTime && time() > $lockTime + $maxLockTime ) { + $this->output( " --- Relocking ---" ); + $this->relockSearchindex( $dbw ); + $lockTime = time(); + $this->output( "\n" ); + } + call_user_func( $callback, $dbw, $row ); + } + + # Unlock searchindex + if ( $maxLockTime ) { + $this->output( " --- Unlocking --" ); + $this->unlockSearchindex( $dbw ); + $this->output( "\n" ); + } + } + + /** + * Update the searchindex table for a given pageid + * @param IDatabase $dbw A database write handle + * @param int $pageId The page ID to update. + * @return null|string + */ + public function updateSearchIndexForPage( $dbw, $pageId ) { + // Get current revision + $rev = Revision::loadFromPageId( $dbw, $pageId ); + $title = null; + if ( $rev ) { + $titleObj = $rev->getTitle(); + $title = $titleObj->getPrefixedDBkey(); + $this->output( "$title..." ); + # Update searchindex + $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() ); + $u->doUpdate(); + $this->output( "\n" ); + } + + return $title; + } + + /** + * Count down from $seconds to zero on the terminal, with a one-second pause + * between showing each number. If the maintenance script is in quiet mode, + * this function does nothing. + * + * @since 1.31 + * + * @codeCoverageIgnore + * @param int $seconds + */ + protected function countDown( $seconds ) { + if ( $this->isQuiet() ) { + return; + } + for ( $i = $seconds; $i >= 0; $i-- ) { + if ( $i != $seconds ) { + $this->output( str_repeat( "\x08", strlen( $i + 1 ) ) ); + } + $this->output( $i ); + if ( $i ) { + sleep( 1 ); + } + } + $this->output( "\n" ); + } + + /** + * Wrapper for posix_isatty() + * We default as considering stdin a tty (for nice readline methods) + * but treating stout as not a tty to avoid color codes + * + * @param mixed $fd File descriptor + * @return bool + */ + public static function posix_isatty( $fd ) { + if ( !function_exists( 'posix_isatty' ) ) { + return !$fd; + } else { + return posix_isatty( $fd ); + } + } + + /** + * Prompt the console for input + * @param string $prompt What to begin the line with, like '> ' + * @return string Response + */ + public static function readconsole( $prompt = '> ' ) { + static $isatty = null; + if ( is_null( $isatty ) ) { + $isatty = self::posix_isatty( 0 /*STDIN*/ ); + } + + if ( $isatty && function_exists( 'readline' ) ) { + return readline( $prompt ); + } else { + if ( $isatty ) { + $st = self::readlineEmulation( $prompt ); + } else { + if ( feof( STDIN ) ) { + $st = false; + } else { + $st = fgets( STDIN, 1024 ); + } + } + if ( $st === false ) { + return false; + } + $resp = trim( $st ); + + return $resp; + } + } + + /** + * Emulate readline() + * @param string $prompt What to begin the line with, like '> ' + * @return string + */ + private static function readlineEmulation( $prompt ) { + $bash = ExecutableFinder::findInDefaultPaths( 'bash' ); + if ( !wfIsWindows() && $bash ) { + $retval = false; + $encPrompt = wfEscapeShellArg( $prompt ); + $command = "read -er -p $encPrompt && echo \"\$REPLY\""; + $encCommand = wfEscapeShellArg( $command ); + $line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] ); + + if ( $retval == 0 ) { + return $line; + } elseif ( $retval == 127 ) { + // Couldn't execute bash even though we thought we saw it. + // Shell probably spit out an error message, sorry :( + // Fall through to fgets()... + } else { + // EOF/ctrl+D + return false; + } + } + + // Fallback... we'll have no editing controls, EWWW + if ( feof( STDIN ) ) { + return false; + } + print $prompt; + + return fgets( STDIN, 1024 ); + } + + /** + * Get the terminal size as a two-element array where the first element + * is the width (number of columns) and the second element is the height + * (number of rows). + * + * @return array + */ + public static function getTermSize() { + $default = [ 80, 50 ]; + if ( wfIsWindows() ) { + return $default; + } + // It's possible to get the screen size with VT-100 terminal escapes, + // but reading the responses is not possible without setting raw mode + // (unless you want to require the user to press enter), and that + // requires an ioctl(), which we can't do. So we have to shell out to + // something that can do the relevant syscalls. There are a few + // options. Linux and Mac OS X both have "stty size" which does the + // job directly. + $result = Shell::command( 'stty', 'size' ) + ->execute(); + if ( $result->getExitCode() !== 0 ) { + return $default; + } + if ( !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m ) ) { + return $default; + } + return [ intval( $m[2] ), intval( $m[1] ) ]; + } + + /** + * Call this to set up the autoloader to allow classes to be used from the + * tests directory. + */ + public static function requireTestsAutoloader() { + require_once __DIR__ . '/../tests/common/TestsAutoLoader.php'; + } +} + +/** + * Fake maintenance wrapper, mostly used for the web installer/updater + */ +class FakeMaintenance extends Maintenance { + protected $mSelf = "FakeMaintenanceScript"; + + public function execute() { + return; + } +} + +/** + * Class for scripts that perform database maintenance and want to log the + * update in `updatelog` so we can later skip it + */ +abstract class LoggedUpdateMaintenance extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addOption( 'force', 'Run the update even if it was completed already' ); + $this->setBatchSize( 200 ); + } + + public function execute() { + $db = $this->getDB( DB_MASTER ); + $key = $this->getUpdateKey(); + + if ( !$this->hasOption( 'force' ) + && $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ ) + ) { + $this->output( "..." . $this->updateSkippedMessage() . "\n" ); + + return true; + } + + if ( !$this->doDBUpdates() ) { + return false; + } + + if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) { + return true; + } else { + $this->output( $this->updatelogFailedMessage() . "\n" ); + + return false; + } + } + + /** + * Message to show that the update was done already and was just skipped + * @return string + */ + protected function updateSkippedMessage() { + $key = $this->getUpdateKey(); + + return "Update '{$key}' already logged as completed."; + } + + /** + * Message to show that the update log was unable to log the completion of this update + * @return string + */ + protected function updatelogFailedMessage() { + $key = $this->getUpdateKey(); + + return "Unable to log update '{$key}' as completed."; + } + + /** + * Do the actual work. All child classes will need to implement this. + * Return true to log the update as done or false (usually on failure). + * @return bool + */ + abstract protected function doDBUpdates(); + + /** + * Get the update key name to go in the update log table + * @return string + */ + abstract protected function getUpdateKey(); +} diff --git a/www/wiki/maintenance/Makefile b/www/wiki/maintenance/Makefile new file mode 100644 index 00000000..a348e856 --- /dev/null +++ b/www/wiki/maintenance/Makefile @@ -0,0 +1,19 @@ +help: + @echo "Run 'make test' to run the parser tests." + @echo "Run 'make doc' to run the doxygen generation." + @echo "Run 'make man' to run the doxygen generation with man pages." + +test: + php tests/parser/parserTests.php --quiet + +doc: + php mwdocgen.php --all + ./mwjsduck-gen + @echo 'PHP documentation (by Doxygen) in ./docs/html/' + @echo 'JS documentation (by JSDuck) in ./docs/js/' + +man: + php mwdocgen.php --all --generate-man + @echo 'Doc generation done. Look at ./docs/html/ and ./docs/man' + @echo 'You might want to update your MANPATH currently:' + @echo 'MANPATH: $(MANPATH)' diff --git a/www/wiki/maintenance/README b/www/wiki/maintenance/README new file mode 100644 index 00000000..8d0b1c45 --- /dev/null +++ b/www/wiki/maintenance/README @@ -0,0 +1,103 @@ +== MediaWiki Maintenance == + +The .sql scripts in this directory are not intended to be run standalone, +although this is appropriate in some cases, e.g. manual creation of blank tables +prior to an import. + +Most of the PHP scripts need to be run from the command line. Prior to doing so, +ensure that the LocalSettings.php file in the directory above points to the +proper installation. + +Certain scripts will require elevated access to the database. In order to +provide this, first create a MySQL user with "all" permissions on the wiki +database, and then set $wgDBadminuser and $wgDBadminpassword in your +LocalSettings.php + +=== Brief explanation of files === + +A lot of the files in this directory are PHP scripts used to perform various +maintenance tasks on the wiki database, e.g. rebuilding link tables, updating +the search indices, etc. The files in the "archives" directory are used to +upgrade the database schema when updating the software. Some schema definitions +for alternative (as yet unsupported) database management systems are stored +here too. + +The "storage" directory contains scripts and resources useful for working with +external storage clusters, and are not likely to be particularly useful to the +vast majority of installations. This directory does contain the compressOld +scripts, however, which can be useful for compacting old data. + +=== Maintenance scripts === + +As noted above, these should be run from the command line. Not all scripts are +listed, as some are Wikimedia-specific, and some are not applicable to most +installations. + + changePassword.php + Reset the password of a specified user + + cleanupSpam.php + Mass-revert insertion of linkspam + + createAndPromote.php + Create a user with administrator (and optionally, bureaucrat) permissions + + deleteOldRevisions.php + Erase old revisions of pages from the database + + dumpBackup.php + Backup dump script + + edit.php + Edit a page to change its content + + findHooks.php + Find hooks that aren't documented in docs/hooks.txt + + importDump.php + XML dump importer + + importImages.php + Import images into the wiki + + moveBatch.php + Move a batch of pages + + namespaceDupes.php + Check articles name to see if they conflict with new/existing namespaces + + nukePage.php + Wipe a page and all revisions from the database + + reassignEdits.php + Reassign edits from one user to another + + rebuildImages.php + Update image metadata records + + rebuildmessages.php + Update the MediaWiki namespace after changing site language + + rebuildtextindex.php + Rebuild the fulltext search indices + + refreshLinks.php + Rebuild the link tables + + removeUnusedAccounts.php + Remove user accounts which have made no edits + + runJobs.php + Immediately complete all jobs in the job queue + + undelete.php + Undelete all revisions of a page + + update.php + Check and upgrade the database schema to the current version + + updateRestrictions.php + Update pages restriction to the new schema + + userOptions.php + Change user options diff --git a/www/wiki/maintenance/addRFCandPMIDInterwiki.php b/www/wiki/maintenance/addRFCandPMIDInterwiki.php new file mode 100644 index 00000000..409afb5f --- /dev/null +++ b/www/wiki/maintenance/addRFCandPMIDInterwiki.php @@ -0,0 +1,95 @@ +addDescription( 'Add RFC and PMID to the interwiki database table' ); + } + + protected function getUpdateKey() { + return __CLASS__; + } + + protected function updateSkippedMessage() { + return 'RFC and PMID already added to interwiki database table.'; + } + + protected function doDBUpdates() { + $interwikiCache = $this->getConfig()->get( 'InterwikiCache' ); + // Using something other than the database, + if ( $interwikiCache !== false ) { + return true; + } + $dbw = $this->getDB( DB_MASTER ); + $rfc = $dbw->selectField( + 'interwiki', + 'iw_url', + [ 'iw_prefix' => 'rfc' ], + __METHOD__ + ); + + // Old pre-1.28 default value, or not set at all + if ( $rfc === false || $rfc === 'http://www.rfc-editor.org/rfc/rfc$1.txt' ) { + $dbw->replace( + 'interwiki', + [ 'iw_prefix' ], + [ + 'iw_prefix' => 'rfc', + 'iw_url' => 'https://tools.ietf.org/html/rfc$1', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0, + ], + __METHOD__ + ); + } + + $dbw->insert( + 'interwiki', + [ + 'iw_prefix' => 'pmid', + 'iw_url' => 'https://www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract', + 'iw_api' => '', + 'iw_wikiid' => '', + 'iw_local' => 0, + ], + __METHOD__, + // If there's already a pmid interwiki link, don't + // overwrite it + [ 'IGNORE' ] + ); + + return true; + } +} + +$maintClass = AddRFCAndPMIDInterwiki::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/addSite.php b/www/wiki/maintenance/addSite.php new file mode 100644 index 00000000..4953343f --- /dev/null +++ b/www/wiki/maintenance/addSite.php @@ -0,0 +1,92 @@ +addDescription( 'Add a site definition into the sites table.' ); + + $this->addArg( 'globalid', 'The global id of the site to add, e.g. "wikipedia".', true ); + $this->addArg( 'group', 'In which group this site should be sorted in.', true ); + $this->addOption( 'language', 'The language code of the site, e.g. "de".' ); + $this->addOption( 'interwiki-id', 'The interwiki ID of the site.' ); + $this->addOption( 'navigation-id', 'The navigation ID of the site.' ); + $this->addOption( 'pagepath', 'The URL to pages of this site, e.g.' . + ' https://example.com/wiki/\$1.' ); + $this->addOption( 'filepath', 'The URL to files of this site, e.g. https://example + .com/w/\$1.' ); + + parent::__construct(); + } + + /** + * Imports the site described by the parameters (see self::__construct()) passed to this + * maintenance sccript into the sites table of MediaWiki. + * @return bool + */ + public function execute() { + $siteStore = MediaWikiServices::getInstance()->getSiteStore(); + $siteStore->reset(); + + $globalId = $this->getArg( 0 ); + $group = $this->getArg( 1 ); + $language = $this->getOption( 'language' ); + $interwikiId = $this->getOption( 'interwiki-id' ); + $navigationId = $this->getOption( 'navigation-id' ); + $pagepath = $this->getOption( 'pagepath' ); + $filepath = $this->getOption( 'filepath' ); + + if ( !is_string( $globalId ) || !is_string( $group ) ) { + echo "Arguments globalid and group need to be strings.\n"; + return false; + } + + if ( $siteStore->getSite( $globalId ) !== null ) { + echo "Site with global id $globalId already exists.\n"; + return false; + } + + $site = new MediaWikiSite(); + $site->setGlobalId( $globalId ); + $site->setGroup( $group ); + if ( $language !== null ) { + $site->setLanguageCode( $language ); + } + if ( $interwikiId !== null ) { + $site->addInterwikiId( $interwikiId ); + } + if ( $navigationId !== null ) { + $site->addNavigationId( $navigationId ); + } + if ( $pagepath !== null ) { + $site->setPagePath( $pagepath ); + } + if ( $filepath !== null ) { + $site->setFilePath( $filepath ); + } + + $siteStore->saveSites( [ $site ] ); + + if ( method_exists( $siteStore, 'reset' ) ) { + $siteStore->reset(); + } + + echo "Done.\n"; + } +} + +$maintClass = AddSite::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/archives/.htaccess b/www/wiki/maintenance/archives/.htaccess new file mode 100644 index 00000000..3a428827 --- /dev/null +++ b/www/wiki/maintenance/archives/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/www/wiki/maintenance/archives/patch-actor-table.sql b/www/wiki/maintenance/archives/patch-actor-table.sql new file mode 100644 index 00000000..fdd95e80 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-actor-table.sql @@ -0,0 +1,57 @@ +-- +-- patch-actor-table.sql +-- +-- T167246. Add an `actor` table and various columns (and temporary tables) to reference it. + +CREATE TABLE /*_*/actor ( + actor_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + actor_user int unsigned, + actor_name varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/actor_user ON /*_*/actor (actor_user); +CREATE UNIQUE INDEX /*i*/actor_name ON /*_*/actor (actor_name); + +CREATE TABLE /*_*/revision_actor_temp ( + revactor_rev int unsigned NOT NULL, + revactor_actor bigint unsigned NOT NULL, + revactor_timestamp binary(14) NOT NULL default '', + revactor_page int unsigned NOT NULL, + PRIMARY KEY (revactor_rev, revactor_actor) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/revactor_rev ON /*_*/revision_actor_temp (revactor_rev); +CREATE INDEX /*i*/actor_timestamp ON /*_*/revision_actor_temp (revactor_actor,revactor_timestamp); +CREATE INDEX /*i*/page_actor_timestamp ON /*_*/revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp); + +ALTER TABLE /*_*/archive + ALTER COLUMN ar_user_text SET DEFAULT '', + ADD COLUMN ar_actor bigint unsigned NOT NULL DEFAULT 0 AFTER ar_user_text; +CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp); + +ALTER TABLE /*_*/ipblocks + ADD COLUMN ipb_by_actor bigint unsigned NOT NULL DEFAULT 0 AFTER ipb_by_text; + +ALTER TABLE /*_*/image + ALTER COLUMN img_user_text SET DEFAULT '', + ADD COLUMN img_actor bigint unsigned NOT NULL DEFAULT 0 AFTER img_user_text; +CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor, img_timestamp); + +ALTER TABLE /*_*/oldimage + ALTER COLUMN oi_user_text SET DEFAULT '', + ADD COLUMN oi_actor bigint unsigned NOT NULL DEFAULT 0 AFTER oi_user_text; +CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp); + +ALTER TABLE /*_*/filearchive + ALTER COLUMN fa_user_text SET DEFAULT '', + ADD COLUMN fa_actor bigint unsigned NOT NULL DEFAULT 0 AFTER fa_user_text; +CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp); + +ALTER TABLE /*_*/recentchanges + ALTER COLUMN rc_user_text SET DEFAULT '', + ADD COLUMN rc_actor bigint unsigned NOT NULL DEFAULT 0 AFTER rc_user_text; +CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor); +CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp); + +ALTER TABLE /*_*/logging + ADD COLUMN log_actor bigint unsigned NOT NULL DEFAULT 0 AFTER log_user_text; +CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp); +CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp); diff --git a/www/wiki/maintenance/archives/patch-add-3d.sql b/www/wiki/maintenance/archives/patch-add-3d.sql new file mode 100644 index 00000000..13ea4ed2 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-add-3d.sql @@ -0,0 +1,11 @@ +ALTER TABLE /*$wgDBprefix*/image + MODIFY img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL; + +ALTER TABLE /*$wgDBprefix*/oldimage + MODIFY oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL; + +ALTER TABLE /*$wgDBprefix*/filearchive + MODIFY fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL; + +ALTER TABLE /*$wgDBprefix*/uploadstash + MODIFY us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL; diff --git a/www/wiki/maintenance/archives/patch-add-cl_collation_ext_index.sql b/www/wiki/maintenance/archives/patch-add-cl_collation_ext_index.sql new file mode 100644 index 00000000..8137dc64 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-add-cl_collation_ext_index.sql @@ -0,0 +1,2 @@ +-- @since 1.27 +CREATE INDEX /*i*/cl_collation_ext ON /*_*/categorylinks (cl_collation, cl_to, cl_type, cl_from); diff --git a/www/wiki/maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql b/www/wiki/maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql new file mode 100644 index 00000000..aa54e753 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql @@ -0,0 +1,2 @@ +-- @since 1.28 +CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp); diff --git a/www/wiki/maintenance/archives/patch-ar_deleted.sql b/www/wiki/maintenance/archives/patch-ar_deleted.sql new file mode 100644 index 00000000..2e5edc44 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ar_deleted.sql @@ -0,0 +1,3 @@ +-- Adding ar_deleted field for revisiondelete +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_deleted tinyint unsigned NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-ar_len.sql b/www/wiki/maintenance/archives/patch-ar_len.sql new file mode 100644 index 00000000..1710e099 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ar_len.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_len INT UNSIGNED; + diff --git a/www/wiki/maintenance/archives/patch-ar_parent_id.sql b/www/wiki/maintenance/archives/patch-ar_parent_id.sql new file mode 100644 index 00000000..b24cf46c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ar_parent_id.sql @@ -0,0 +1,3 @@ +-- Adding ar_deleted field for revisiondelete +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_parent_id int unsigned default NULL; diff --git a/www/wiki/maintenance/archives/patch-ar_rev_id-not-null.sql b/www/wiki/maintenance/archives/patch-ar_rev_id-not-null.sql new file mode 100644 index 00000000..8418f20d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ar_rev_id-not-null.sql @@ -0,0 +1,3 @@ +-- T182678: Make ar_rev_id not nullable +ALTER TABLE /*_*/archive + CHANGE COLUMN ar_rev_id ar_rev_id int unsigned NOT NULL; diff --git a/www/wiki/maintenance/archives/patch-ar_sha1.sql b/www/wiki/maintenance/archives/patch-ar_sha1.sql new file mode 100644 index 00000000..1c7d8e91 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ar_sha1.sql @@ -0,0 +1,3 @@ +-- Adding ar_sha1 field +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_sha1 varbinary(32) NOT NULL default ''; diff --git a/www/wiki/maintenance/archives/patch-archive-ar_content_format.sql b/www/wiki/maintenance/archives/patch-archive-ar_content_format.sql new file mode 100644 index 00000000..81f9fca8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-ar_content_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_content_format varbinary(64) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-archive-ar_content_model.sql b/www/wiki/maintenance/archives/patch-archive-ar_content_model.sql new file mode 100644 index 00000000..1a8b630e --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-ar_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_content_model varbinary(32) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-archive-ar_id.sql b/www/wiki/maintenance/archives/patch-archive-ar_id.sql new file mode 100644 index 00000000..08287cd5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-ar_id.sql @@ -0,0 +1,8 @@ +-- +-- patch-archive-ar_id.sql +-- +-- T41675. Add archive.ar_id. + +ALTER TABLE /*$wgDBprefix*/archive + ADD COLUMN ar_id int unsigned NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (ar_id); diff --git a/www/wiki/maintenance/archives/patch-archive-page_id.sql b/www/wiki/maintenance/archives/patch-archive-page_id.sql new file mode 100644 index 00000000..47a1c47e --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-page_id.sql @@ -0,0 +1,6 @@ +-- Reference to page_id. Useful for sysadmin fixing of large +-- pages merged together in the archives +-- Added 2007-07-21 + +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_page_id int unsigned; diff --git a/www/wiki/maintenance/archives/patch-archive-rev_id.sql b/www/wiki/maintenance/archives/patch-archive-rev_id.sql new file mode 100644 index 00000000..b9d789ee --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-rev_id.sql @@ -0,0 +1,6 @@ +-- New field in archive table to preserve revision IDs across undeletion. +-- Added 2005-03-10 + +ALTER TABLE /*$wgDBprefix*/archive + ADD + ar_rev_id int unsigned; diff --git a/www/wiki/maintenance/archives/patch-archive-text_id.sql b/www/wiki/maintenance/archives/patch-archive-text_id.sql new file mode 100644 index 00000000..8557f2ad --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-text_id.sql @@ -0,0 +1,14 @@ +-- New field in archive table to preserve text source IDs across undeletion. +-- +-- Older entries containing NULL in this field will contain text in the +-- ar_text and ar_flags fields, and will cause the (re)creation of a new +-- text record upon undeletion. +-- +-- Newer ones will reference a text.old_id with this field, and the existing +-- entries will be used as-is; only a revision record need be created. +-- +-- Added 2005-05-01 + +ALTER TABLE /*$wgDBprefix*/archive + ADD + ar_text_id int unsigned; diff --git a/www/wiki/maintenance/archives/patch-archive-user-index.sql b/www/wiki/maintenance/archives/patch-archive-user-index.sql new file mode 100644 index 00000000..997b4a97 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive-user-index.sql @@ -0,0 +1,4 @@ +-- Adds a user,timestamp index to the archive table +-- Used for browsing deleted contributions and renames +ALTER TABLE /*$wgDBprefix*/archive + ADD INDEX usertext_timestamp ( ar_user_text , ar_timestamp ); diff --git a/www/wiki/maintenance/archives/patch-archive_ar_revid.sql b/www/wiki/maintenance/archives/patch-archive_ar_revid.sql new file mode 100644 index 00000000..f3b9c3c9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive_ar_revid.sql @@ -0,0 +1,3 @@ +-- Hopefully temporary index. +-- For https://phabricator.wikimedia.org/T23279 +CREATE INDEX /*i*/ar_revid ON /*$wgDBprefix*/archive ( ar_rev_id ); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-archive_kill_ar_page_revid.sql b/www/wiki/maintenance/archives/patch-archive_kill_ar_page_revid.sql new file mode 100644 index 00000000..2e6fe453 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-archive_kill_ar_page_revid.sql @@ -0,0 +1,4 @@ +-- Used for killing the wrong index added during SVN for 1.17 +-- Won't affect most people, but it doesn't need to exist +ALTER TABLE /*$wgDBprefix*/archive + DROP INDEX ar_page_revid; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-backlinkindexes.sql b/www/wiki/maintenance/archives/patch-backlinkindexes.sql new file mode 100644 index 00000000..9a991b81 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-backlinkindexes.sql @@ -0,0 +1,19 @@ +-- +-- patch-backlinkindexes.sql +-- +-- Per task T8440 / https://phabricator.wikimedia.org/T8440 +-- +-- Improve performance of the "what links here"-type queries +-- + +ALTER TABLE /*$wgDBprefix*/pagelinks + DROP INDEX pl_namespace, + ADD INDEX pl_namespace(pl_namespace, pl_title, pl_from); + +ALTER TABLE /*$wgDBprefix*/templatelinks + DROP INDEX tl_namespace, + ADD INDEX tl_namespace(tl_namespace, tl_title, tl_from); + +ALTER TABLE /*$wgDBprefix*/imagelinks + DROP INDEX il_to, + ADD INDEX il_to(il_to, il_from); diff --git a/www/wiki/maintenance/archives/patch-bot.sql b/www/wiki/maintenance/archives/patch-bot.sql new file mode 100644 index 00000000..7625889c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-bot.sql @@ -0,0 +1,11 @@ +-- Add field to recentchanges for easy filtering of bot entries +-- edits by a user with 'bot' in user.user_rights should be +-- marked 1 in rc_bot. + +-- Change made 2002-12-15 by Brion VIBBER +-- this affects code in Article.php, User.php SpecialRecentchanges.php +-- column also added to buildTables.inc + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD COLUMN rc_bot tinyint unsigned NOT NULL default '0' + AFTER rc_minor; diff --git a/www/wiki/maintenance/archives/patch-bot_passwords-bp_user-unsigned.sql b/www/wiki/maintenance/archives/patch-bot_passwords-bp_user-unsigned.sql new file mode 100644 index 00000000..163609ab --- /dev/null +++ b/www/wiki/maintenance/archives/patch-bot_passwords-bp_user-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/bot_passwords MODIFY bp_user int unsigned NOT NULL; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-bot_passwords.sql b/www/wiki/maintenance/archives/patch-bot_passwords.sql new file mode 100644 index 00000000..bd60ff72 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-bot_passwords.sql @@ -0,0 +1,25 @@ +-- +-- This table contains a user's bot passwords: passwords that allow access to +-- the account via the API with limited rights. +-- +CREATE TABLE /*_*/bot_passwords ( + -- Foreign key to user.user_id + bp_user int NOT NULL, + + -- Application identifier + bp_app_id varbinary(32) NOT NULL, + + -- Password hashes, like user.user_password + bp_password tinyblob NOT NULL, + + -- Like user.user_token + bp_token binary(32) NOT NULL default '', + + -- JSON blob for MWRestrictions + bp_restrictions blob NOT NULL, + + -- Grants allowed to the account when authenticated with this bot-password + bp_grants blob NOT NULL, + + PRIMARY KEY ( bp_user, bp_app_id ) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-cache.sql b/www/wiki/maintenance/archives/patch-cache.sql new file mode 100644 index 00000000..0545da8b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-cache.sql @@ -0,0 +1,41 @@ +-- patch-cache.sql +-- 2003-03-22 +-- +-- Add 'last touched' fields to cur and user tables. +-- These are useful for maintaining cache consistency. +-- (Updates to OutputPage.php and elsewhere.) +-- +-- cur_touched should be set to the current time whenever: +-- * the page is updated +-- * a linked page is created +-- * a linked page is destroyed +-- +-- The cur_touched time will then be compared against the +-- timestamps of cached pages to ensure consistency; if +-- cur_touched is later, the page must be regenerated. + +ALTER TABLE /*$wgDBprefix*/cur + ADD COLUMN cur_touched binary(14) NOT NULL default ''; + +-- Existing pages should be initialized to the current +-- time so they don't needlessly rerender until they are +-- changed for the first time: + +UPDATE /*$wgDBprefix*/cur + SET cur_touched=NOW()+0; + +-- user_touched should be set to the current time whenever: +-- * the user logs in +-- * the user saves preferences (if no longer default...?) +-- * the user's newtalk status is altered +-- +-- The user_touched time should also be checked against the +-- timestamp reported by a browser requesting revalidation. +-- If user_touched is later than the reported last modified +-- time, the page should be rerendered with new options and +-- sent again. + +ALTER TABLE /*$wgDBprefix*/user + ADD COLUMN user_touched binary(14) NOT NULL default ''; +UPDATE /*$wgDBprefix*/user + SET user_touched=NOW()+0; diff --git a/www/wiki/maintenance/archives/patch-cat_hidden.sql b/www/wiki/maintenance/archives/patch-cat_hidden.sql new file mode 100644 index 00000000..933188ce --- /dev/null +++ b/www/wiki/maintenance/archives/patch-cat_hidden.sql @@ -0,0 +1,3 @@ +-- cat_hidden is no longer used, delete it + +ALTER TABLE /*$wgDBprefix*/category DROP COLUMN cat_hidden; diff --git a/www/wiki/maintenance/archives/patch-category.sql b/www/wiki/maintenance/archives/patch-category.sql new file mode 100644 index 00000000..97a5690d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-category.sql @@ -0,0 +1,17 @@ +CREATE TABLE /*$wgDBprefix*/category ( + cat_id int unsigned NOT NULL auto_increment, + + cat_title varchar(255) binary NOT NULL, + + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0, + + cat_hidden tinyint(1) unsigned NOT NULL default 0, + + PRIMARY KEY (cat_id), + UNIQUE KEY (cat_title), + + KEY (cat_pages) +) /*$wgDBTableOptions*/; + diff --git a/www/wiki/maintenance/archives/patch-categorylinks-better-collation.sql b/www/wiki/maintenance/archives/patch-categorylinks-better-collation.sql new file mode 100644 index 00000000..f8b63405 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-categorylinks-better-collation.sql @@ -0,0 +1,19 @@ +-- +-- patch-categorylinks-better-collation.sql +-- +-- T2164, T3211, T25682. This is the second version of this patch; the +-- changes are also incorporated into patch-categorylinks-better-collation2.sql, +-- for the benefit of trunk users who applied the original. +-- +-- Due to T27254, the length limit of 255 bytes for cl_sortkey_prefix +-- is also enforced in php. If you change the length of that field, make +-- sure to also change the check in LinksUpdate.php. +ALTER TABLE /*$wgDBprefix*/categorylinks + CHANGE COLUMN cl_sortkey cl_sortkey varbinary(230) NOT NULL default '', + ADD COLUMN cl_sortkey_prefix varchar(255) binary NOT NULL default '', + ADD COLUMN cl_collation varbinary(32) NOT NULL default '', + ADD COLUMN cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page', +-- rm'd in 1.27 ADD INDEX (cl_collation), + DROP INDEX cl_sortkey, + ADD INDEX cl_sortkey (cl_to, cl_type, cl_sortkey, cl_from); +INSERT IGNORE INTO /*$wgDBprefix*/updatelog (ul_key) VALUES ('cl_fields_update'); diff --git a/www/wiki/maintenance/archives/patch-categorylinks-better-collation2.sql b/www/wiki/maintenance/archives/patch-categorylinks-better-collation2.sql new file mode 100644 index 00000000..e9574693 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-categorylinks-better-collation2.sql @@ -0,0 +1,12 @@ +-- +-- patch-categorylinks-better-collation2.sql +-- +-- Bugs 164, 1211, 23682. This patch exists for trunk users who already +-- applied the first patch in its original version. The first patch was +-- updated to incorporate the changes as well, so as not to do two alters on a +-- large table unnecessarily for people upgrading from 1.16, so this will be +-- skipped if unneeded. +ALTER TABLE /*$wgDBprefix*/categorylinks + CHANGE COLUMN cl_sortkey cl_sortkey varbinary(230) NOT NULL default '', + CHANGE COLUMN cl_collation cl_collation varbinary(32) NOT NULL default ''; +INSERT IGNORE INTO /*$wgDBprefix*/updatelog (ul_key) VALUES ('cl_fields_update'); diff --git a/www/wiki/maintenance/archives/patch-categorylinks-fix-pk.sql b/www/wiki/maintenance/archives/patch-categorylinks-fix-pk.sql new file mode 100644 index 00000000..20bc7160 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-categorylinks-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/categorylinks DROP KEY /*i*/cl_from, ADD PRIMARY KEY (cl_from,cl_to); diff --git a/www/wiki/maintenance/archives/patch-categorylinks.sql b/www/wiki/maintenance/archives/patch-categorylinks.sql new file mode 100644 index 00000000..0af0cf91 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-categorylinks.sql @@ -0,0 +1,37 @@ +-- +-- Track category inclusions *used inline* +-- This tracks a single level of category membership +-- (folksonomic tagging, really). +-- +CREATE TABLE /*$wgDBprefix*/categorylinks ( + -- Key to page_id of the page defined as a category member. + cl_from int unsigned NOT NULL default '0', + + -- Name of the category. + -- This is also the page_title of the category's description page; + -- all such pages are in namespace 14 (NS_CATEGORY). + cl_to varchar(255) binary NOT NULL default '', + + -- The title of the linking page, or an optional override + -- to determine sort order. Sorting is by binary order, which + -- isn't always ideal, but collations seem to be an exciting + -- and dangerous new world in MySQL... + -- + -- Truncate so that the cl_sortkey key fits in 1000 bytes + -- (MyISAM 5 with server_character_set=utf8) + cl_sortkey varchar(70) binary NOT NULL default '', + + -- This isn't really used at present. Provided for an optional + -- sorting method by approximate addition time. + cl_timestamp timestamp NOT NULL, + + UNIQUE KEY cl_from(cl_from,cl_to), + + -- This key is trouble. It's incomplete, AND it's too big + -- when collation is set to UTF-8. Bleeeacch! + KEY cl_sortkey(cl_to,cl_sortkey), + + -- Not really used? + KEY cl_timestamp(cl_to,cl_timestamp) + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-categorylinksindex.sql b/www/wiki/maintenance/archives/patch-categorylinksindex.sql new file mode 100644 index 00000000..e2b2c3ac --- /dev/null +++ b/www/wiki/maintenance/archives/patch-categorylinksindex.sql @@ -0,0 +1,11 @@ +-- +-- patch-categorylinksindex.sql +-- +-- Per task T12280 / https://phabricator.wikimedia.org/T12280 +-- +-- Improve enum continuation performance of the what pages belong to a category query +-- + +ALTER TABLE /*$wgDBprefix*/categorylinks + DROP INDEX cl_sortkey, + ADD INDEX cl_sortkey(cl_to, cl_sortkey, cl_from); diff --git a/www/wiki/maintenance/archives/patch-change_tag-ct_id.sql b/www/wiki/maintenance/archives/patch-change_tag-ct_id.sql new file mode 100644 index 00000000..7b986d67 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-change_tag-ct_id.sql @@ -0,0 +1,5 @@ +-- Primary key in change_tag table + +ALTER TABLE /*$wgDBprefix*/change_tag + ADD COLUMN ct_id INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (ct_id); diff --git a/www/wiki/maintenance/archives/patch-change_tag-ct_log_id-unsigned.sql b/www/wiki/maintenance/archives/patch-change_tag-ct_log_id-unsigned.sql new file mode 100644 index 00000000..1371c474 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-change_tag-ct_log_id-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/change_tag MODIFY ct_log_id int unsigned NULL; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-change_tag-ct_rev_id-unsigned.sql b/www/wiki/maintenance/archives/patch-change_tag-ct_rev_id-unsigned.sql new file mode 100644 index 00000000..b7e1f02e --- /dev/null +++ b/www/wiki/maintenance/archives/patch-change_tag-ct_rev_id-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/change_tag MODIFY ct_rev_id int unsigned NULL; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-change_tag-indexes.sql b/www/wiki/maintenance/archives/patch-change_tag-indexes.sql new file mode 100644 index 00000000..9f8d8f80 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-change_tag-indexes.sql @@ -0,0 +1,21 @@ +-- +-- Rename indexes on change_tag from implicit to explicit names +-- + +DROP INDEX ct_rc_id ON /*_*/change_tag; +DROP INDEX ct_log_id ON /*_*/change_tag; +DROP INDEX ct_rev_id ON /*_*/change_tag; +DROP INDEX ct_tag ON /*_*/change_tag; + +DROP INDEX ts_rc_id ON /*_*/tag_summary; +DROP INDEX ts_log_id ON /*_*/tag_summary; +DROP INDEX ts_rev_id ON /*_*/tag_summary; + +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); + +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); diff --git a/www/wiki/maintenance/archives/patch-change_tag.sql b/www/wiki/maintenance/archives/patch-change_tag.sql new file mode 100644 index 00000000..3079a5bb --- /dev/null +++ b/www/wiki/maintenance/archives/patch-change_tag.sql @@ -0,0 +1,15 @@ +-- A table to track tags for revisions, logs and recent changes. +-- Andrew Garrett, 2009-01 +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params BLOB NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +-- Covering index, so we can pull all the info only out of the index. +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); diff --git a/www/wiki/maintenance/archives/patch-comment-table.sql b/www/wiki/maintenance/archives/patch-comment-table.sql new file mode 100644 index 00000000..c8bf9580 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-comment-table.sql @@ -0,0 +1,59 @@ +-- +-- patch-comment-table.sql +-- +-- T166732. Add a `comment` table and various columns (and temporary tables) to reference it. + +CREATE TABLE /*_*/comment ( + comment_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + comment_hash INT NOT NULL, + comment_text BLOB NOT NULL, + comment_data BLOB +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash); + +CREATE TABLE /*_*/revision_comment_temp ( + revcomment_rev int unsigned NOT NULL, + revcomment_comment_id bigint unsigned NOT NULL, + PRIMARY KEY (revcomment_rev, revcomment_comment_id) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/revcomment_rev ON /*_*/revision_comment_temp (revcomment_rev); + +CREATE TABLE /*_*/image_comment_temp ( + imgcomment_name varchar(255) binary NOT NULL, + imgcomment_description_id bigint unsigned NOT NULL, + PRIMARY KEY (imgcomment_name, imgcomment_description_id) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/imgcomment_name ON /*_*/image_comment_temp (imgcomment_name); + +ALTER TABLE /*_*/revision + ALTER COLUMN rev_comment SET DEFAULT ''; + +ALTER TABLE /*_*/archive + ALTER COLUMN ar_comment SET DEFAULT '', + ADD COLUMN ar_comment_id bigint unsigned NOT NULL DEFAULT 0 AFTER ar_comment; + +ALTER TABLE /*_*/ipblocks + ALTER COLUMN ipb_reason SET DEFAULT '', + ADD COLUMN ipb_reason_id bigint unsigned NOT NULL DEFAULT 0 AFTER ipb_reason; + +ALTER TABLE /*_*/image + ALTER COLUMN img_description SET DEFAULT ''; + +ALTER TABLE /*_*/oldimage + ALTER COLUMN oi_description SET DEFAULT '', + ADD COLUMN oi_description_id bigint unsigned NOT NULL DEFAULT 0 AFTER oi_description; + +ALTER TABLE /*_*/filearchive + ADD COLUMN fa_deleted_reason_id bigint unsigned NOT NULL DEFAULT 0 AFTER fa_deleted_reason, + ALTER COLUMN fa_description SET DEFAULT '', + ADD COLUMN fa_description_id bigint unsigned NOT NULL DEFAULT 0 AFTER fa_description; + +ALTER TABLE /*_*/recentchanges + ADD COLUMN rc_comment_id bigint unsigned NOT NULL DEFAULT 0 AFTER rc_comment; + +ALTER TABLE /*_*/logging + ADD COLUMN log_comment_id bigint unsigned NOT NULL DEFAULT 0 AFTER log_comment; + +ALTER TABLE /*_*/protected_titles + ALTER COLUMN pt_reason SET DEFAULT '', + ADD COLUMN pt_reason_id bigint unsigned NOT NULL DEFAULT 0 AFTER pt_reason; diff --git a/www/wiki/maintenance/archives/patch-content.sql b/www/wiki/maintenance/archives/patch-content.sql new file mode 100644 index 00000000..2cc4de8c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-content.sql @@ -0,0 +1,21 @@ +-- +-- The content table represents content objects. It's primary purpose is to provide the necessary +-- meta-data for loading and interpreting a serialized data blob to create a content object. +-- +CREATE TABLE /*_*/content ( + + -- ID of the content object + content_id bigint unsigned PRIMARY KEY AUTO_INCREMENT, + + -- Nominal size of the content object (not necessarily of the serialized blob) + content_size int unsigned NOT NULL, + + -- Nominal hash of the content object (not necessarily of the serialized blob) + content_sha1 varbinary(32) NOT NULL, + + -- reference to model_id + content_model smallint unsigned NOT NULL, + + -- URL-like address of the content blob + content_address varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-content_models.sql b/www/wiki/maintenance/archives/patch-content_models.sql new file mode 100644 index 00000000..12c4c5bb --- /dev/null +++ b/www/wiki/maintenance/archives/patch-content_models.sql @@ -0,0 +1,10 @@ +-- +-- Normalization table for content model names +-- +CREATE TABLE /*_*/content_models ( + model_id smallint PRIMARY KEY AUTO_INCREMENT, + model_name varbinary(64) NOT NULL +) /*$wgDBTableOptions*/; + +-- Index for looking of the internal ID of for a name +CREATE UNIQUE INDEX /*i*/model_name ON /*_*/content_models (model_name); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-drop-ar_text.sql b/www/wiki/maintenance/archives/patch-drop-ar_text.sql new file mode 100644 index 00000000..6dae71e3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop-ar_text.sql @@ -0,0 +1,7 @@ +-- T33223: Remove obsolete ar_text and ar_flags columns +-- (and make ar_text_id not nullable and default 0) + +ALTER TABLE /*_*/archive + DROP COLUMN ar_text, + DROP COLUMN ar_flags, + CHANGE COLUMN ar_text_id ar_text_id int unsigned NOT NULL DEFAULT 0; diff --git a/www/wiki/maintenance/archives/patch-drop-page_counter.sql b/www/wiki/maintenance/archives/patch-drop-page_counter.sql new file mode 100644 index 00000000..1d8e701b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop-page_counter.sql @@ -0,0 +1,2 @@ +-- field is deprecated and no longer updated as of 1.25 +ALTER TABLE /*_*/page DROP COLUMN page_counter; diff --git a/www/wiki/maintenance/archives/patch-drop-rc_cur_time.sql b/www/wiki/maintenance/archives/patch-drop-rc_cur_time.sql new file mode 100644 index 00000000..f1bc9e8b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop-rc_cur_time.sql @@ -0,0 +1,2 @@ +-- rc_cur_time is no longer used, delete the field +ALTER TABLE /*$wgDBprefix*/recentchanges DROP COLUMN rc_cur_time; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-drop-ss_admins.sql b/www/wiki/maintenance/archives/patch-drop-ss_admins.sql new file mode 100644 index 00000000..13c3d3b0 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop-ss_admins.sql @@ -0,0 +1,2 @@ +-- field is deprecated and no longer updated as of 1.5 +ALTER TABLE /*_*/site_stats DROP COLUMN ss_admins; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-drop-ss_total_views.sql b/www/wiki/maintenance/archives/patch-drop-ss_total_views.sql new file mode 100644 index 00000000..00591939 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop-ss_total_views.sql @@ -0,0 +1,2 @@ +-- field is deprecated and no longer updated as of 1.24 +ALTER TABLE /*_*/site_stats DROP COLUMN ss_total_views; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-drop-user_options.sql b/www/wiki/maintenance/archives/patch-drop-user_options.sql new file mode 100644 index 00000000..15b7d278 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop-user_options.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/user DROP COLUMN user_options; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-drop_img_type.sql b/www/wiki/maintenance/archives/patch-drop_img_type.sql new file mode 100644 index 00000000..e3737617 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-drop_img_type.sql @@ -0,0 +1,3 @@ +-- img_type is no longer used, delete it + +ALTER TABLE /*$wgDBprefix*/image DROP COLUMN img_type; diff --git a/www/wiki/maintenance/archives/patch-editsummary-length.sql b/www/wiki/maintenance/archives/patch-editsummary-length.sql new file mode 100644 index 00000000..996d562d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-editsummary-length.sql @@ -0,0 +1,11 @@ +ALTER TABLE /*_*/revision MODIFY rev_comment varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/archive MODIFY ar_comment varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/image MODIFY img_description varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/oldimage MODIFY oi_description varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/filearchive MODIFY fa_description varbinary(767) default ''; +ALTER TABLE /*_*/filearchive MODIFY fa_deleted_reason varbinary(767) default ''; +ALTER TABLE /*_*/recentchanges MODIFY rc_comment varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/logging MODIFY log_comment varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/ipblocks MODIFY ipb_reason varbinary(767) NOT NULL default ''; +ALTER TABLE /*_*/protected_titles MODIFY pt_reason varbinary(767) default ''; + diff --git a/www/wiki/maintenance/archives/patch-email-authentication.sql b/www/wiki/maintenance/archives/patch-email-authentication.sql new file mode 100644 index 00000000..b35b10f1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-email-authentication.sql @@ -0,0 +1,3 @@ +-- Added early in 1.5 alpha development, removed 2005-04-25 + +ALTER TABLE /*$wgDBprefix*/user DROP COLUMN user_emailauthenticationtimestamp; diff --git a/www/wiki/maintenance/archives/patch-email-notification.sql b/www/wiki/maintenance/archives/patch-email-notification.sql new file mode 100644 index 00000000..337e1ac2 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-email-notification.sql @@ -0,0 +1,11 @@ +-- Patch for email notification on page changes T.Gries/M.Arndt 11.09.2004 + +-- A new column 'wl_notificationtimestamp' is added to the table 'watchlist'. +-- When a page watched by a user X is changed by someone else, an email is sent to the watching user X +-- if and only if the field 'wl_notificationtimestamp' is '0'. The time/date of sending the mail is then stored in that field. +-- Further pages changes do not trigger new notification mails as long as user X has not re-visited that page. +-- The field is reset to '0' when user X re-visits the page or when he or she resets all notification timestamps +-- ("notification flags") at once by clicking the new button on his/her watchlist page. +-- T. Gries/M. Arndt 11.09.2004 - December 2004 + +ALTER TABLE /*$wgDBprefix*/watchlist ADD (wl_notificationtimestamp varbinary(14)); diff --git a/www/wiki/maintenance/archives/patch-externallinks-el_id.sql b/www/wiki/maintenance/archives/patch-externallinks-el_id.sql new file mode 100644 index 00000000..ded84543 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-externallinks-el_id.sql @@ -0,0 +1,8 @@ +-- +-- patch-extenallinks-el_id.sql +-- +-- T17441. Add externallinks.el_id. + +ALTER TABLE /*$wgDBprefix*/externallinks + ADD COLUMN el_id int unsigned NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (el_id); diff --git a/www/wiki/maintenance/archives/patch-externallinks-el_index_60.sql b/www/wiki/maintenance/archives/patch-externallinks-el_index_60.sql new file mode 100644 index 00000000..eacb1074 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-externallinks-el_index_60.sql @@ -0,0 +1,4 @@ +-- @since 1.29 +ALTER TABLE /*$wgDBprefix*/externallinks ADD COLUMN el_index_60 varbinary(60) NOT NULL DEFAULT ''; +CREATE INDEX /*i*/el_index_60 ON /*_*/externallinks (el_index_60, el_id); +CREATE INDEX /*i*/el_from_index_60 ON /*_*/externallinks (el_from, el_index_60, el_id); diff --git a/www/wiki/maintenance/archives/patch-externallinks.sql b/www/wiki/maintenance/archives/patch-externallinks.sql new file mode 100644 index 00000000..fc5017db --- /dev/null +++ b/www/wiki/maintenance/archives/patch-externallinks.sql @@ -0,0 +1,13 @@ +-- +-- Track links to external URLs +-- +CREATE TABLE /*$wgDBprefix*/externallinks ( + el_from int(8) unsigned NOT NULL default '0', + el_to blob NOT NULL, + el_index blob NOT NULL, + + KEY (el_from, el_to(40)), + KEY (el_to(60), el_from), + KEY (el_index(60)) +) /*$wgDBTableOptions*/; + diff --git a/www/wiki/maintenance/archives/patch-fa_deleted.sql b/www/wiki/maintenance/archives/patch-fa_deleted.sql new file mode 100644 index 00000000..7ab65239 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-fa_deleted.sql @@ -0,0 +1,3 @@ +-- Adding fa_deleted field for additional content suppression +ALTER TABLE /*$wgDBprefix*/filearchive + ADD fa_deleted tinyint unsigned NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-fa_major_mime-chemical.sql b/www/wiki/maintenance/archives/patch-fa_major_mime-chemical.sql new file mode 100644 index 00000000..be9b0ff5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-fa_major_mime-chemical.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*$wgDBprefix*/filearchive + CHANGE fa_major_mime fa_major_mime ENUM('unknown','application','audio','image','text','video','message','model','multipart','chemical'); + diff --git a/www/wiki/maintenance/archives/patch-fa_sha1.sql b/www/wiki/maintenance/archives/patch-fa_sha1.sql new file mode 100644 index 00000000..931bc44d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-fa_sha1.sql @@ -0,0 +1,4 @@ +-- Add fa_sha1 and related index +ALTER TABLE /*$wgDBprefix*/filearchive + ADD COLUMN fa_sha1 varbinary(32) NOT NULL default ''; +CREATE INDEX /*i*/fa_sha1 ON /*$wgDBprefix*/filearchive (fa_sha1(10)); diff --git a/www/wiki/maintenance/archives/patch-filearchive-user-index.sql b/www/wiki/maintenance/archives/patch-filearchive-user-index.sql new file mode 100644 index 00000000..0d8c3ab1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-filearchive-user-index.sql @@ -0,0 +1,5 @@ +-- Adding index to sort by uploader +ALTER TABLE /*$wgDBprefix*/filearchive + ADD INDEX fa_user_timestamp (fa_user_text,fa_timestamp), + -- Remove useless, incomplete index + DROP INDEX fa_deleted_user; diff --git a/www/wiki/maintenance/archives/patch-filearchive.sql b/www/wiki/maintenance/archives/patch-filearchive.sql new file mode 100644 index 00000000..f75da8be --- /dev/null +++ b/www/wiki/maintenance/archives/patch-filearchive.sql @@ -0,0 +1,51 @@ +-- +-- Record of deleted file data +-- +CREATE TABLE /*$wgDBprefix*/filearchive ( + -- Unique row id + fa_id int not null auto_increment, + + -- Original base filename; key to image.img_name, page.page_title, etc + fa_name varchar(255) binary NOT NULL default '', + + -- Filename of archived file, if an old revision + fa_archive_name varchar(255) binary default '', + + -- Which storage bin (directory tree or object store) the file data + -- is stored in. Should be 'deleted' for files that have been deleted; + -- any other bin is not yet in use. + fa_storage_group varbinary(16), + + -- SHA-1 of the file contents plus extension, used as a key for storage. + -- eg 8f8a562add37052a1848ff7771a2c515db94baa9.jpg + -- + -- If NULL, the file was missing at deletion time or has been purged + -- from the archival storage. + fa_storage_key varbinary(64) default '', + + -- Deletion information, if this file is deleted. + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + + -- Duped fields from image + fa_size int unsigned default '0', + fa_width int default '0', + fa_height int default '0', + fa_metadata mediumblob, + fa_bits int default '0', + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(32) default "unknown", + fa_description tinyblob, + fa_user int unsigned default '0', + fa_user_text varchar(255) binary default '', + fa_timestamp binary(14) default '', + + PRIMARY KEY (fa_id), + INDEX (fa_name, fa_timestamp), -- pick out by image name + INDEX (fa_storage_group, fa_storage_key), -- pick out dupe files + INDEX (fa_deleted_timestamp), -- sort by deletion time + INDEX (fa_deleted_user) -- sort by deleter + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-filejournal.sql b/www/wiki/maintenance/archives/patch-filejournal.sql new file mode 100644 index 00000000..4356d70d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-filejournal.sql @@ -0,0 +1,20 @@ +-- File backend operation journal +CREATE TABLE /*_*/filejournal ( + -- Unique ID for each file operation + fj_id bigint unsigned NOT NULL PRIMARY KEY auto_increment, + -- UUID of the batch this operation belongs to + fj_batch_uuid varbinary(32) NOT NULL, + -- The registered file backend name + fj_backend varchar(255) NOT NULL, + -- The storage path that was affected (may be internal paths) + fj_path blob NOT NULL, + -- Primitive operation description (create/update/delete) + fj_op varchar(16) NOT NULL default '', + -- SHA-1 file content hash in base-36 + fj_new_sha1 varbinary(32) NOT NULL default '', + -- Timestamp of the batch operation + fj_timestamp varbinary(14) NOT NULL default '' +) /*$wgDBTableOptions*/; + +CREATE INDEX /*i*/fj_batch_id ON /*_*/filejournal (fj_batch_uuid); +CREATE INDEX /*i*/fj_timestamp ON /*_*/filejournal (fj_timestamp); diff --git a/www/wiki/maintenance/archives/patch-fix-il_from.sql b/www/wiki/maintenance/archives/patch-fix-il_from.sql new file mode 100644 index 00000000..0a199e4d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-fix-il_from.sql @@ -0,0 +1,11 @@ +-- Fix a bug from the 1.2 -> 1.3 upgrader by moving away the imagelinks table +-- and recreating it. +RENAME TABLE /*_*/imagelinks TO /*_*/imagelinks_old; +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); + diff --git a/www/wiki/maintenance/archives/patch-il_from_namespace.sql b/www/wiki/maintenance/archives/patch-il_from_namespace.sql new file mode 100644 index 00000000..2a2d361b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-il_from_namespace.sql @@ -0,0 +1,4 @@ +ALTER TABLE /*_*/imagelinks + ADD COLUMN il_from_namespace int NOT NULL default 0; + +CREATE INDEX /*i*/il_backlinks_namespace ON /*_*/imagelinks (il_from_namespace,il_to,il_from); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-image-img_description_id.sql b/www/wiki/maintenance/archives/patch-image-img_description_id.sql new file mode 100644 index 00000000..d098c80b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-image-img_description_id.sql @@ -0,0 +1,7 @@ +-- +-- patch-image-img_description_id.sql +-- +-- T188132. Add `img_description_id` to the `image` table. + +ALTER TABLE /*_*/image + ADD COLUMN img_description_id bigint unsigned NOT NULL DEFAULT 0 AFTER img_description; diff --git a/www/wiki/maintenance/archives/patch-image-user-index-2.sql b/www/wiki/maintenance/archives/patch-image-user-index-2.sql new file mode 100644 index 00000000..8b19d820 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-image-user-index-2.sql @@ -0,0 +1 @@ +CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp); diff --git a/www/wiki/maintenance/archives/patch-image-user-index.sql b/www/wiki/maintenance/archives/patch-image-user-index.sql new file mode 100644 index 00000000..b44930fc --- /dev/null +++ b/www/wiki/maintenance/archives/patch-image-user-index.sql @@ -0,0 +1,8 @@ +-- +-- image-user-index.sql +-- +-- Add user_text/timestamp index to current image versions +-- + +ALTER TABLE /*$wgDBprefix*/image + ADD INDEX img_usertext_timestamp (img_user_text,img_timestamp); diff --git a/www/wiki/maintenance/archives/patch-image_name_primary.sql b/www/wiki/maintenance/archives/patch-image_name_primary.sql new file mode 100644 index 00000000..5bd88264 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-image_name_primary.sql @@ -0,0 +1,6 @@ +-- Make the image name index unique + +ALTER TABLE /*$wgDBprefix*/image DROP INDEX img_name; + +ALTER TABLE /*$wgDBprefix*/image + ADD PRIMARY KEY img_name (img_name); diff --git a/www/wiki/maintenance/archives/patch-image_name_unique.sql b/www/wiki/maintenance/archives/patch-image_name_unique.sql new file mode 100644 index 00000000..5cf02d41 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-image_name_unique.sql @@ -0,0 +1,6 @@ +-- Make the image name index unique + +ALTER TABLE /*$wgDBprefix*/image DROP INDEX img_name; + +ALTER TABLE /*$wgDBprefix*/image + ADD UNIQUE INDEX img_name (img_name); diff --git a/www/wiki/maintenance/archives/patch-imagelinks-fix-pk.sql b/www/wiki/maintenance/archives/patch-imagelinks-fix-pk.sql new file mode 100644 index 00000000..e66500f7 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-imagelinks-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/imagelinks DROP KEY /*i*/il_from, ADD PRIMARY KEY (il_from,il_to); diff --git a/www/wiki/maintenance/archives/patch-img_exif.sql b/www/wiki/maintenance/archives/patch-img_exif.sql new file mode 100644 index 00000000..2fd78f76 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_exif.sql @@ -0,0 +1,3 @@ +-- Extra image exif metadata, added for 1.5 but quickly removed. + +ALTER TABLE /*$wgDBprefix*/image DROP img_exif; diff --git a/www/wiki/maintenance/archives/patch-img_major_mime-chemical.sql b/www/wiki/maintenance/archives/patch-img_major_mime-chemical.sql new file mode 100644 index 00000000..4bde446e --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_major_mime-chemical.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*$wgDBprefix*/image + CHANGE img_major_mime img_major_mime ENUM('unknown','application','audio','image','text','video','message','model','multipart','chemical'); + diff --git a/www/wiki/maintenance/archives/patch-img_media_mime-index.sql b/www/wiki/maintenance/archives/patch-img_media_mime-index.sql new file mode 100644 index 00000000..bfaf84f9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_media_mime-index.sql @@ -0,0 +1,4 @@ +-- New index on image table to allow searches for types i.e. video webm +-- Added 2013-01-08 + +CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime); diff --git a/www/wiki/maintenance/archives/patch-img_media_type.sql b/www/wiki/maintenance/archives/patch-img_media_type.sql new file mode 100644 index 00000000..b0f9ece5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_media_type.sql @@ -0,0 +1,17 @@ +-- media type columns, added for 1.5 +-- this alters the scheme for 1.5, img_type is no longer used. + +ALTER TABLE /*$wgDBprefix*/image ADD ( + -- Media type as defined by the MEDIATYPE_xxx constants + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + + -- major part of a MIME media type as defined by IANA + -- see https://www.iana.org/assignments/media-types/ + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + + -- minor part of a MIME media type as defined by IANA + -- the minor parts are not required to adher to any standard + -- but should be consistent throughout the database + -- see https://www.iana.org/assignments/media-types/ + img_minor_mime varbinary(32) NOT NULL default "unknown" +); diff --git a/www/wiki/maintenance/archives/patch-img_metadata.sql b/www/wiki/maintenance/archives/patch-img_metadata.sql new file mode 100644 index 00000000..407e4325 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_metadata.sql @@ -0,0 +1,6 @@ +-- Moving img_exif to img_metadata, so the name won't be so confusing when we +-- Use it for Ogg metadata or something like that. + +ALTER TABLE /*$wgDBprefix*/image ADD ( + img_metadata mediumblob NOT NULL +); diff --git a/www/wiki/maintenance/archives/patch-img_sha1.sql b/www/wiki/maintenance/archives/patch-img_sha1.sql new file mode 100644 index 00000000..0a375c4f --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_sha1.sql @@ -0,0 +1,8 @@ +-- Add img_sha1, oi_sha1 and related indexes +ALTER TABLE /*$wgDBprefix*/image + ADD COLUMN img_sha1 varbinary(32) NOT NULL default '', + ADD INDEX img_sha1 (img_sha1(10)); + +ALTER TABLE /*$wgDBprefix*/oldimage + ADD COLUMN oi_sha1 varbinary(32) NOT NULL default '', + ADD INDEX oi_sha1 (oi_sha1(10)); diff --git a/www/wiki/maintenance/archives/patch-img_width.sql b/www/wiki/maintenance/archives/patch-img_width.sql new file mode 100644 index 00000000..06889ea6 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-img_width.sql @@ -0,0 +1,18 @@ +-- Extra image metadata, added for 1.5 + +-- NOTE: as by patch-img_media_type.sql, the img_type +-- column is no longer used and has therefore be removed from this patch + +ALTER TABLE /*$wgDBprefix*/image ADD ( + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_bits int NOT NULL default 0 +); + +ALTER TABLE /*$wgDBprefix*/oldimage ADD ( + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0 +); + + diff --git a/www/wiki/maintenance/archives/patch-indexes.sql b/www/wiki/maintenance/archives/patch-indexes.sql new file mode 100644 index 00000000..c24d9953 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-indexes.sql @@ -0,0 +1,24 @@ +-- +-- patch-indexes.sql +-- +-- Fix up table indexes; new to stable release in November 2003 +-- + +ALTER TABLE IF EXISTS /*$wgDBprefix*/links + DROP INDEX l_from, + ADD INDEX l_from (l_from); + +ALTER TABLE /*$wgDBprefix*/brokenlinks + DROP INDEX bl_to, + ADD INDEX bl_to (bL_to); + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD INDEX rc_timestamp (rc_timestamp), + ADD INDEX rc_namespace_title (rc_namespace, rc_title), + ADD INDEX rc_cur_id (rc_cur_id); + +ALTER TABLE /*$wgDBprefix*/archive + ADD KEY name_title_timestamp (ar_namespace,ar_title,ar_timestamp); + +ALTER TABLE /*$wgDBprefix*/watchlist + ADD KEY namespace_title (wl_namespace,wl_title); diff --git a/www/wiki/maintenance/archives/patch-interwiki-trans.sql b/www/wiki/maintenance/archives/patch-interwiki-trans.sql new file mode 100644 index 00000000..5cc4d0b5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-interwiki-trans.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/interwiki + ADD COLUMN iw_trans TINYINT NOT NULL DEFAULT 0; diff --git a/www/wiki/maintenance/archives/patch-interwiki.sql b/www/wiki/maintenance/archives/patch-interwiki.sql new file mode 100644 index 00000000..57b79456 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-interwiki.sql @@ -0,0 +1,20 @@ +-- Creates interwiki prefix<->url mapping table +-- used from 2003-08-21 dev version. +-- Import the default mappings from maintenance/interwiki.sql + +CREATE TABLE /*$wgDBprefix*/interwiki ( + -- The interwiki prefix, (e.g. "Meatball", or the language prefix "de") + iw_prefix varchar(32) NOT NULL, + + -- The URL of the wiki, with "$1" as a placeholder for an article name. + -- Any spaces in the name will be transformed to underscores before + -- insertion. + iw_url blob NOT NULL, + + -- A boolean value indicating whether the wiki is in this project + -- (used, for example, to detect redirect loops) + iw_local BOOL NOT NULL, + + UNIQUE KEY iw_prefix (iw_prefix) + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-inverse_timestamp.sql b/www/wiki/maintenance/archives/patch-inverse_timestamp.sql new file mode 100644 index 00000000..0f7d66f1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-inverse_timestamp.sql @@ -0,0 +1,15 @@ +-- Removes the inverse_timestamp field from early 1.5 alphas. +-- This field was used in the olden days as a crutch for sorting +-- limitations in MySQL 3.x, but is being dropped now as an +-- unnecessary burden. Serious wikis should be running on 4.x. +-- +-- Updater added 2005-03-13 + +ALTER TABLE /*$wgDBprefix*/revision + DROP COLUMN inverse_timestamp, + DROP INDEX page_timestamp, + DROP INDEX user_timestamp, + DROP INDEX usertext_timestamp, + ADD INDEX page_timestamp (rev_page,rev_timestamp), + ADD INDEX user_timestamp (rev_user,rev_timestamp), + ADD INDEX usertext_timestamp (rev_user_text,rev_timestamp); diff --git a/www/wiki/maintenance/archives/patch-ip_changes.sql b/www/wiki/maintenance/archives/patch-ip_changes.sql new file mode 100644 index 00000000..5f05672e --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ip_changes.sql @@ -0,0 +1,23 @@ +-- +-- Every time an edit by a logged out user is saved, +-- a row is created in ip_changes. This stores +-- the IP as a hex representation so that we can more +-- easily find edits within an IP range. +-- +CREATE TABLE /*_*/ip_changes ( + -- Foreign key to the revision table, also serves as the unique primary key + ipc_rev_id int unsigned NOT NULL PRIMARY KEY DEFAULT '0', + + -- The timestamp of the revision + ipc_rev_timestamp binary(14) NOT NULL DEFAULT '', + + -- Hex representation of the IP address, as returned by IP::toHex() + -- For IPv4 it will resemble: ABCD1234 + -- For IPv6: v6-ABCD1234000000000000000000000000 + -- BETWEEN is then used to identify revisions within a given range + ipc_hex varbinary(35) NOT NULL DEFAULT '' + +) /*$wgDBTableOptions*/; + +CREATE INDEX /*i*/ipc_rev_timestamp ON /*_*/ip_changes (ipc_rev_timestamp); +CREATE INDEX /*i*/ipc_hex_time ON /*_*/ip_changes (ipc_hex,ipc_rev_timestamp); diff --git a/www/wiki/maintenance/archives/patch-ipb-parent-block-id-index.sql b/www/wiki/maintenance/archives/patch-ipb-parent-block-id-index.sql new file mode 100644 index 00000000..1f413f37 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb-parent-block-id-index.sql @@ -0,0 +1,2 @@ +-- index for ipblocks.ipb_parent_block_id +CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id); diff --git a/www/wiki/maintenance/archives/patch-ipb-parent-block-id.sql b/www/wiki/maintenance/archives/patch-ipb-parent-block-id.sql new file mode 100644 index 00000000..8ebcf786 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb-parent-block-id.sql @@ -0,0 +1,3 @@ +-- Adding ipb_parent_block_id to track the block that caused an autoblock +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_parent_block_id int DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-ipb_allow_usertalk.sql b/www/wiki/maintenance/archives/patch-ipb_allow_usertalk.sql new file mode 100644 index 00000000..92e7d9a4 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_allow_usertalk.sql @@ -0,0 +1,3 @@ +-- Adding ipb_allow_usertalk for blocks +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_allow_usertalk bool NOT NULL default 1; diff --git a/www/wiki/maintenance/archives/patch-ipb_anon_only.sql b/www/wiki/maintenance/archives/patch-ipb_anon_only.sql new file mode 100644 index 00000000..bb39c1d9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_anon_only.sql @@ -0,0 +1,44 @@ +-- Add extra option fields to the ipblocks table, add some extra indexes, +-- convert infinity values in ipb_expiry to something that sorts better, +-- extend ipb_address and range fields, add a unique index for block conflict +-- detection. + +-- Conflicts in the new unique index can be handled by creating a new +-- table and inserting into it instead of doing an ALTER TABLE. + + +DROP TABLE IF EXISTS /*$wgDBprefix*/ipblocks_newunique; + +CREATE TABLE /*$wgDBprefix*/ipblocks_newunique ( + ipb_id int NOT NULL auto_increment, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default '0', + ipb_by int unsigned NOT NULL default '0', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + + PRIMARY KEY ipb_id (ipb_id), + UNIQUE INDEX ipb_address_unique (ipb_address(255), ipb_user, ipb_auto), + INDEX ipb_user (ipb_user), + INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)), + INDEX ipb_timestamp (ipb_timestamp), + INDEX ipb_expiry (ipb_expiry) + +) /*$wgDBTableOptions*/; + +INSERT IGNORE INTO /*$wgDBprefix*/ipblocks_newunique + (ipb_id, ipb_address, ipb_user, ipb_by, ipb_reason, ipb_timestamp, ipb_auto, ipb_expiry, ipb_range_start, ipb_range_end, ipb_anon_only, ipb_create_account) + SELECT ipb_id, ipb_address, ipb_user, ipb_by, ipb_reason, ipb_timestamp, ipb_auto, ipb_expiry, ipb_range_start, ipb_range_end, 0 , ipb_user=0 + FROM /*$wgDBprefix*/ipblocks; + +DROP TABLE IF EXISTS /*$wgDBprefix*/ipblocks_old; +RENAME TABLE /*$wgDBprefix*/ipblocks TO /*$wgDBprefix*/ipblocks_old; +RENAME TABLE /*$wgDBprefix*/ipblocks_newunique TO /*$wgDBprefix*/ipblocks; + +UPDATE /*$wgDBprefix*/ipblocks SET ipb_expiry='infinity' WHERE ipb_expiry=''; diff --git a/www/wiki/maintenance/archives/patch-ipb_by_text.sql b/www/wiki/maintenance/archives/patch-ipb_by_text.sql new file mode 100644 index 00000000..e809d102 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_by_text.sql @@ -0,0 +1,10 @@ +-- Adding colomn with username of blocker and sets it. +-- Required for crosswiki blocks. + +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_by_text varchar(255) binary NOT NULL default ''; + +UPDATE /*$wgDBprefix*/ipblocks + JOIN /*$wgDBprefix*/user ON ipb_by = user_id + SET ipb_by_text = user_name + WHERE ipb_by != 0; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-ipb_deleted.sql b/www/wiki/maintenance/archives/patch-ipb_deleted.sql new file mode 100644 index 00000000..b12ddaaa --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_deleted.sql @@ -0,0 +1,3 @@ +-- Adding ipb_deleted field for hiding usernames +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_deleted bool NOT NULL default 0; diff --git a/www/wiki/maintenance/archives/patch-ipb_emailban.sql b/www/wiki/maintenance/archives/patch-ipb_emailban.sql new file mode 100644 index 00000000..e05c20b3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_emailban.sql @@ -0,0 +1,4 @@ +-- Add row for email blocks -- + +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_block_email tinyint NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-ipb_expiry.sql b/www/wiki/maintenance/archives/patch-ipb_expiry.sql new file mode 100644 index 00000000..f3b6a82b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_expiry.sql @@ -0,0 +1,8 @@ +-- Adds the ipb_expiry field to ipblocks + +ALTER TABLE /*$wgDBprefix*/ipblocks ADD ipb_expiry varbinary(14) NOT NULL default ''; + +-- All IP blocks have one day expiry +UPDATE /*$wgDBprefix*/ipblocks SET ipb_expiry = date_format(date_add(ipb_timestamp,INTERVAL 1 DAY),"%Y%m%d%H%i%s") WHERE ipb_user = 0; + +-- Null string is fine for user blocks, since this indicates infinity diff --git a/www/wiki/maintenance/archives/patch-ipb_optional_autoblock.sql b/www/wiki/maintenance/archives/patch-ipb_optional_autoblock.sql new file mode 100644 index 00000000..f31b8359 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_optional_autoblock.sql @@ -0,0 +1,3 @@ +-- Add an extra option field "ipb_enable_autoblock" into the ipblocks table. This allows a block to be placed that does not trigger any autoblocks. + +ALTER TABLE /*$wgDBprefix*/ipblocks ADD COLUMN ipb_enable_autoblock bool NOT NULL default '1'; diff --git a/www/wiki/maintenance/archives/patch-ipb_range_start.sql b/www/wiki/maintenance/archives/patch-ipb_range_start.sql new file mode 100644 index 00000000..84cba8f6 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipb_range_start.sql @@ -0,0 +1,25 @@ +-- Add the range handling fields +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_range_start tinyblob NOT NULL default '', + ADD ipb_range_end tinyblob NOT NULL default '', + ADD INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)); + + +-- Initialise fields +-- Only range blocks match ipb_address LIKE '%/%', this fact is used in the code already +UPDATE /*$wgDBprefix*/ipblocks + SET + ipb_range_start = LPAD(HEX( + (SUBSTRING_INDEX(ipb_address, '.', 1) << 24) + + (SUBSTRING_INDEX(SUBSTRING_INDEX(ipb_address, '.', 2), '.', -1) << 16) + + (SUBSTRING_INDEX(SUBSTRING_INDEX(ipb_address, '.', 3), '.', -1) << 24) + + (SUBSTRING_INDEX(SUBSTRING_INDEX(ipb_address, '/', 1), '.', -1)) ), 8, '0' ), + + ipb_range_end = LPAD(HEX( + (SUBSTRING_INDEX(ipb_address, '.', 1) << 24) + + (SUBSTRING_INDEX(SUBSTRING_INDEX(ipb_address, '.', 2), '.', -1) << 16) + + (SUBSTRING_INDEX(SUBSTRING_INDEX(ipb_address, '.', 3), '.', -1) << 24) + + (SUBSTRING_INDEX(SUBSTRING_INDEX(ipb_address, '/', 1), '.', -1)) + + ((1 << (32 - SUBSTRING_INDEX(ipb_address, '/', -1))) - 1) ), 8, '0' ) + + WHERE ipb_address LIKE '%/%'; diff --git a/www/wiki/maintenance/archives/patch-ipblocks.sql b/www/wiki/maintenance/archives/patch-ipblocks.sql new file mode 100644 index 00000000..634fa78c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ipblocks.sql @@ -0,0 +1,6 @@ +-- For auto-expiring blocks -- + +ALTER TABLE /*$wgDBprefix*/ipblocks + ADD ipb_auto tinyint NOT NULL default '0', + ADD ipb_id int NOT NULL auto_increment, + ADD PRIMARY KEY (ipb_id); diff --git a/www/wiki/maintenance/archives/patch-iw_api_and_wikiid.sql b/www/wiki/maintenance/archives/patch-iw_api_and_wikiid.sql new file mode 100644 index 00000000..4384a715 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-iw_api_and_wikiid.sql @@ -0,0 +1,9 @@ +-- +-- Add iw_api and iw_wikiid to interwiki table +-- + +ALTER TABLE /*_*/interwiki + ADD iw_api BLOB NOT NULL; +ALTER TABLE /*_*/interwiki + ADD iw_wikiid varchar(64) NOT NULL; + diff --git a/www/wiki/maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql b/www/wiki/maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql new file mode 100644 index 00000000..bff63c74 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-iwl_prefix_title_from-non-unique.sql @@ -0,0 +1,5 @@ +-- +-- Makes the iwl_prefix_title_from index for the iwlinks table non-unique +-- +DROP INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks; +CREATE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); diff --git a/www/wiki/maintenance/archives/patch-iwlinks-fix-pk.sql b/www/wiki/maintenance/archives/patch-iwlinks-fix-pk.sql new file mode 100644 index 00000000..1dd5220d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-iwlinks-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/iwlinks DROP KEY /*i*/iwl_from, ADD PRIMARY KEY (iwl_from,iwl_prefix,iwl_title); diff --git a/www/wiki/maintenance/archives/patch-iwlinks-from-title-index.sql b/www/wiki/maintenance/archives/patch-iwlinks-from-title-index.sql new file mode 100644 index 00000000..8b73f9e3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-iwlinks-from-title-index.sql @@ -0,0 +1,4 @@ +-- +-- Recreates the iwl_prefix_from_title index for the iwlinks table +-- +CREATE INDEX /*i*/iwl_prefix_from_title ON /*_*/iwlinks (iwl_prefix, iwl_from, iwl_title); diff --git a/www/wiki/maintenance/archives/patch-iwlinks.sql b/www/wiki/maintenance/archives/patch-iwlinks.sql new file mode 100644 index 00000000..b7bd3f13 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-iwlinks.sql @@ -0,0 +1,16 @@ +-- +-- Track inline interwiki links +-- +CREATE TABLE /*_*/iwlinks ( + -- page_id of the referring page + iwl_from int unsigned NOT NULL default 0, + + -- Interwiki prefix code of the target + iwl_prefix varbinary(20) NOT NULL default '', + + -- Title of the target, including namespace + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); diff --git a/www/wiki/maintenance/archives/patch-job.sql b/www/wiki/maintenance/archives/patch-job.sql new file mode 100644 index 00000000..662f5d27 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-job.sql @@ -0,0 +1,20 @@ +-- Jobs performed by parallel apache threads or a command-line daemon +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + + -- Command name + -- Limited to 60 to prevent key length overflow + job_cmd varbinary(60) NOT NULL default '', + + -- Namespace and title to act on + -- Should be 0 and '' if the command does not operate on a title + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + + -- Any other parameters to the command + -- Stored as a PHP serialized array, or an empty string if there are no parameters + job_params blob NOT NULL +) /*$wgDBTableOptions*/; + +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); + diff --git a/www/wiki/maintenance/archives/patch-job_attempts.sql b/www/wiki/maintenance/archives/patch-job_attempts.sql new file mode 100644 index 00000000..47b73e81 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-job_attempts.sql @@ -0,0 +1,4 @@ +ALTER TABLE /*_*/job + ADD COLUMN job_attempts integer unsigned NOT NULL default 0; + +CREATE INDEX /*i*/job_cmd_token_id ON /*_*/job (job_cmd,job_token,job_id); diff --git a/www/wiki/maintenance/archives/patch-job_token.sql b/www/wiki/maintenance/archives/patch-job_token.sql new file mode 100644 index 00000000..080fa97c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-job_token.sql @@ -0,0 +1,9 @@ +ALTER TABLE /*_*/job + ADD COLUMN job_random integer unsigned NOT NULL default 0, + ADD COLUMN job_token varbinary(32) NOT NULL default '', + ADD COLUMN job_token_timestamp varbinary(14) NULL default NULL, + ADD COLUMN job_sha1 varbinary(32) NOT NULL default ''; + +CREATE INDEX /*i*/job_sha1 ON /*_*/job (job_sha1); +CREATE INDEX /*i*/job_cmd_token ON /*_*/job (job_cmd,job_token,job_random); + diff --git a/www/wiki/maintenance/archives/patch-jobs-add-timestamp.sql b/www/wiki/maintenance/archives/patch-jobs-add-timestamp.sql new file mode 100644 index 00000000..c5e6e711 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-jobs-add-timestamp.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*_*/job ADD COLUMN job_timestamp varbinary(14) NULL default NULL; +CREATE INDEX /*i*/job_timestamp ON /*_*/job(job_timestamp); diff --git a/www/wiki/maintenance/archives/patch-kill-cl_collation_index.sql b/www/wiki/maintenance/archives/patch-kill-cl_collation_index.sql new file mode 100644 index 00000000..7f75a623 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-kill-cl_collation_index.sql @@ -0,0 +1,7 @@ +-- +-- Kill cl_collation index. +-- @since 1.27 +-- + +DROP INDEX /*i*/cl_collation ON /*_*/categorylinks; + diff --git a/www/wiki/maintenance/archives/patch-kill-iwl_prefix.sql b/www/wiki/maintenance/archives/patch-kill-iwl_prefix.sql new file mode 100644 index 00000000..1cd9b454 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-kill-iwl_prefix.sql @@ -0,0 +1,7 @@ +-- +-- Kill the old iwl_prefix index, which may be present on some +-- installs if they ran update.php between it being added and being renamed +-- + +DROP INDEX /*i*/iwl_prefix ON /*_*/iwlinks; + diff --git a/www/wiki/maintenance/archives/patch-l10n_cache-primary-key.sql b/www/wiki/maintenance/archives/patch-l10n_cache-primary-key.sql new file mode 100644 index 00000000..d5830396 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-l10n_cache-primary-key.sql @@ -0,0 +1,8 @@ +-- +-- patch-l10n_cache-primary-key.sql +-- +-- Bug T146591. Add l10n_cache primary key + +DELETE FROM /*$wgDBprefix*/l10n_cache; + +ALTER TABLE /*$wgDBprefix*/l10n_cache DROP KEY /*i*/lc_lang_key, ADD PRIMARY KEY(lc_lang, lc_key); diff --git a/www/wiki/maintenance/archives/patch-l10n_cache.sql b/www/wiki/maintenance/archives/patch-l10n_cache.sql new file mode 100644 index 00000000..4c865d71 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-l10n_cache.sql @@ -0,0 +1,8 @@ +-- Table for storing localisation data +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); + diff --git a/www/wiki/maintenance/archives/patch-langlinks-fix-pk.sql b/www/wiki/maintenance/archives/patch-langlinks-fix-pk.sql new file mode 100644 index 00000000..e3ac3125 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-langlinks-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/langlinks DROP KEY /*i*/ll_from, ADD PRIMARY KEY (ll_from,ll_lang); diff --git a/www/wiki/maintenance/archives/patch-langlinks-ll_lang-20.sql b/www/wiki/maintenance/archives/patch-langlinks-ll_lang-20.sql new file mode 100644 index 00000000..ce026382 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-langlinks-ll_lang-20.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*$wgDBprefix*/langlinks + MODIFY `ll_lang` + VARBINARY(20) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-langlinks.sql b/www/wiki/maintenance/archives/patch-langlinks.sql new file mode 100644 index 00000000..5594acd5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-langlinks.sql @@ -0,0 +1,14 @@ +CREATE TABLE /*$wgDBprefix*/langlinks ( + -- page_id of the referring page + ll_from int unsigned NOT NULL default '0', + + -- Language code of the target + ll_lang varbinary(20) NOT NULL default '', + + -- Title of the target, including namespace + ll_title varchar(255) binary NOT NULL default '', + + UNIQUE KEY (ll_from, ll_lang), + KEY (ll_lang, ll_title) +) /*$wgDBTableOptions*/; + diff --git a/www/wiki/maintenance/archives/patch-linkscc-1.3.sql b/www/wiki/maintenance/archives/patch-linkscc-1.3.sql new file mode 100644 index 00000000..e397fcb9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-linkscc-1.3.sql @@ -0,0 +1,6 @@ +-- +-- linkscc table used to cache link lists in easier to digest form. +-- New schema for 1.3 - removes old lcc_title column. +-- May 2004 +-- +ALTER TABLE /*$wgDBprefix*/linkscc DROP COLUMN lcc_title; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-linkscc.sql b/www/wiki/maintenance/archives/patch-linkscc.sql new file mode 100644 index 00000000..684384f5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-linkscc.sql @@ -0,0 +1,12 @@ +-- +-- linkscc table used to cache link lists in easier to digest form +-- November 2003 +-- +-- Format later updated. +-- + +CREATE TABLE /*$wgDBprefix*/linkscc ( + lcc_pageid INT UNSIGNED NOT NULL UNIQUE KEY, + lcc_cacheobj MEDIUMBLOB NOT NULL + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-linktables.sql b/www/wiki/maintenance/archives/patch-linktables.sql new file mode 100644 index 00000000..d53d2ea3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-linktables.sql @@ -0,0 +1,70 @@ +-- +-- Track links that do exist +-- l_from and l_to key to cur_id +-- +DROP TABLE IF EXISTS /*$wgDBprefix*/links; +CREATE TABLE /*$wgDBprefix*/links ( + -- Key to the page_id of the page containing the link. + l_from int unsigned NOT NULL default '0', + + -- Key to the page_id of the link target. + -- An unfortunate consequence of this is that rename + -- operations require changing the links entries for + -- all links to the moved page. + l_to int unsigned NOT NULL default '0', + + UNIQUE KEY l_from(l_from,l_to), + KEY (l_to) + +) /*$wgDBTableOptions*/; + +-- +-- Track links to pages that don't yet exist. +-- bl_from keys to cur_id +-- bl_to is a text link (namespace:title) +-- +DROP TABLE IF EXISTS /*$wgDBprefix*/brokenlinks; +CREATE TABLE /*$wgDBprefix*/brokenlinks ( + -- Key to the page_id of the page containing the link. + bl_from int unsigned NOT NULL default '0', + + -- Text of the target page title ("namesapce:title"). + -- Unfortunately this doesn't split the namespace index + -- key and therefore can't easily be joined to anything. + bl_to varchar(255) binary NOT NULL default '', + UNIQUE KEY bl_from(bl_from,bl_to), + KEY (bl_to) + +) /*$wgDBTableOptions*/; + +-- +-- Track links to images *used inline* +-- il_from keys to cur_id, il_to keys to image_name. +-- We don't distinguish live from broken links. +-- +DROP TABLE IF EXISTS /*$wgDBprefix*/imagelinks; +CREATE TABLE /*$wgDBprefix*/imagelinks ( + -- Key to page_id of the page containing the image / media link. + il_from int unsigned NOT NULL default '0', + + -- Filename of target image. + -- This is also the page_title of the file's description page; + -- all such pages are in namespace 6 (NS_FILE). + il_to varchar(255) binary NOT NULL default '', + + UNIQUE KEY il_from(il_from,il_to), + KEY (il_to) + +) /*$wgDBTableOptions*/; + +-- +-- Stores (possibly gzipped) serialized objects with +-- cache arrays to reduce database load slurping up +-- from links and brokenlinks. +-- +DROP TABLE IF EXISTS /*$wgDBprefix*/linkscc; +CREATE TABLE /*$wgDBprefix*/linkscc ( + lcc_pageid INT UNSIGNED NOT NULL UNIQUE KEY, + lcc_cacheobj MEDIUMBLOB NOT NULL + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-log_deleted.sql b/www/wiki/maintenance/archives/patch-log_deleted.sql new file mode 100644 index 00000000..0fce0f51 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-log_deleted.sql @@ -0,0 +1,3 @@ +-- Adding ar_deleted field for revisiondelete +ALTER TABLE /*$wgDBprefix*/logging + ADD log_deleted tinyint unsigned NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-log_id.sql b/www/wiki/maintenance/archives/patch-log_id.sql new file mode 100644 index 00000000..bd69ddb6 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-log_id.sql @@ -0,0 +1,8 @@ +-- Log_id field that means one log entry can be referred to with a single number, +-- rather than a dirty great big mess of features. +-- This might be useful for single-log-entry deletion, et cetera. +-- Andrew Garrett, February 2007. + +ALTER TABLE /*$wgDBprefix*/logging + ADD COLUMN log_id int unsigned not null auto_increment, + ADD PRIMARY KEY log_id (log_id); diff --git a/www/wiki/maintenance/archives/patch-log_params.sql b/www/wiki/maintenance/archives/patch-log_params.sql new file mode 100644 index 00000000..ff6527ec --- /dev/null +++ b/www/wiki/maintenance/archives/patch-log_params.sql @@ -0,0 +1 @@ +ALTER TABLE /*$wgDBprefix*/logging ADD log_params blob NOT NULL; diff --git a/www/wiki/maintenance/archives/patch-log_search-fix-pk.sql b/www/wiki/maintenance/archives/patch-log_search-fix-pk.sql new file mode 100644 index 00000000..51bfdf59 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-log_search-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/log_search DROP KEY /*i*/ls_field_val, ADD PRIMARY KEY (ls_field,ls_value,ls_log_id); diff --git a/www/wiki/maintenance/archives/patch-log_search.sql b/www/wiki/maintenance/archives/patch-log_search.sql new file mode 100644 index 00000000..8d92030b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-log_search.sql @@ -0,0 +1,10 @@ +CREATE TABLE /*_*/log_search ( + -- The type of ID (rev ID, log ID, rev timestamp, username) + ls_field varbinary(32) NOT NULL, + -- The value of the ID + ls_value varchar(255) NOT NULL, + -- Key to log_id + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); diff --git a/www/wiki/maintenance/archives/patch-log_user_text.sql b/www/wiki/maintenance/archives/patch-log_user_text.sql new file mode 100644 index 00000000..12ca75e5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-log_user_text.sql @@ -0,0 +1,8 @@ +ALTER TABLE /*$wgDBprefix*/logging + ADD log_user_text varchar(255) binary NOT NULL default '', + ADD log_page int unsigned NULL, + CHANGE log_type log_type varbinary(32) NOT NULL, + CHANGE log_action log_action varbinary(32) NOT NULL; + +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); diff --git a/www/wiki/maintenance/archives/patch-logging-times-index.sql b/www/wiki/maintenance/archives/patch-logging-times-index.sql new file mode 100644 index 00000000..5f24f5c3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-logging-times-index.sql @@ -0,0 +1,9 @@ +-- +-- patch-logging-times-index.sql +-- +-- Add a very humble index on logging times +-- + +ALTER TABLE /*$wgDBprefix*/logging + ADD INDEX times (log_timestamp); + diff --git a/www/wiki/maintenance/archives/patch-logging-title.sql b/www/wiki/maintenance/archives/patch-logging-title.sql new file mode 100644 index 00000000..c5da0dc0 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-logging-title.sql @@ -0,0 +1,6 @@ +-- 1.4 betas were missing the 'binary' marker from logging.log_title, +-- which causes a collation mismatch error on joins in MySQL 4.1. + +ALTER TABLE /*$wgDBprefix*/logging + CHANGE COLUMN log_title + log_title varchar(255) binary NOT NULL default ''; diff --git a/www/wiki/maintenance/archives/patch-logging-type-action-index.sql b/www/wiki/maintenance/archives/patch-logging-type-action-index.sql new file mode 100644 index 00000000..5edc61a5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-logging-type-action-index.sql @@ -0,0 +1 @@ +CREATE INDEX /*i*/type_action ON /*_*/logging(log_type, log_action, log_timestamp); diff --git a/www/wiki/maintenance/archives/patch-logging.sql b/www/wiki/maintenance/archives/patch-logging.sql new file mode 100644 index 00000000..79df0dd4 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-logging.sql @@ -0,0 +1,37 @@ +-- Add the logging table and adjust recentchanges to accomodate special pages +-- 2004-08-24 + +CREATE TABLE /*$wgDBprefix*/logging ( + -- Symbolic keys for the general log type and the action type + -- within the log. The output format will be controlled by the + -- action field, but only the type controls categorization. + log_type varbinary(10) NOT NULL default '', + log_action varbinary(10) NOT NULL default '', + + -- Timestamp. Duh. + log_timestamp binary(14) NOT NULL default '19700101000000', + + -- The user who performed this action; key to user_id + log_user int unsigned NOT NULL default 0, + + -- Key to the page affected. Where a user is the target, + -- this will point to the user page. + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + + -- Freeform text. Interpreted as edit history comments. + log_comment varchar(255) NOT NULL default '', + + -- LF separated list of miscellaneous parameters + log_params blob NOT NULL, + + KEY type_time (log_type, log_timestamp), + KEY user_time (log_user, log_timestamp), + KEY page_time (log_namespace, log_title, log_timestamp) + +) /*$wgDBTableOptions*/; + + +-- Change from unsigned to signed so we can store special pages +ALTER TABLE recentchanges + MODIFY rc_namespace tinyint(3) NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-logging_user_text_time_index.sql b/www/wiki/maintenance/archives/patch-logging_user_text_time_index.sql new file mode 100644 index 00000000..06f29861 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-logging_user_text_time_index.sql @@ -0,0 +1 @@ +CREATE INDEX /*i*/log_user_text_time ON /*_*/logging (log_user_text, log_timestamp); diff --git a/www/wiki/maintenance/archives/patch-logging_user_text_type_time_index.sql b/www/wiki/maintenance/archives/patch-logging_user_text_type_time_index.sql new file mode 100644 index 00000000..2801bc86 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-logging_user_text_type_time_index.sql @@ -0,0 +1 @@ +CREATE INDEX /*i*/log_user_text_type_time ON /*_*/logging (log_user_text, log_type, log_timestamp); diff --git a/www/wiki/maintenance/archives/patch-mime_minor_length.sql b/www/wiki/maintenance/archives/patch-mime_minor_length.sql new file mode 100644 index 00000000..88dd64cf --- /dev/null +++ b/www/wiki/maintenance/archives/patch-mime_minor_length.sql @@ -0,0 +1,10 @@ +ALTER TABLE /*_*/filearchive + MODIFY COLUMN fa_minor_mime varbinary(100) default "unknown"; + +ALTER TABLE /*_*/image + MODIFY COLUMN img_minor_mime varbinary(100) NOT NULL default "unknown"; + +ALTER TABLE /*_*/oldimage + MODIFY COLUMN oi_minor_mime varbinary(100) NOT NULL default "unknown"; + +INSERT INTO /*_*/updatelog(ul_key) VALUES ('mime_minor_length'); diff --git a/www/wiki/maintenance/archives/patch-mimesearch-indexes.sql b/www/wiki/maintenance/archives/patch-mimesearch-indexes.sql new file mode 100644 index 00000000..8d9426ea --- /dev/null +++ b/www/wiki/maintenance/archives/patch-mimesearch-indexes.sql @@ -0,0 +1,22 @@ +-- Add indexes to the MIME types in image for use on Special:MIMEsearch, +-- changes a query like +-- +-- SELECT img_name FROM image WHERE img_major_mime = "image" AND img_minor_mime = "svg"; +-- from: +-- +-------+------+---------------+------+---------+------+------+-------------+ +-- | table | type | possible_keys | key | key_len | ref | rows | Extra | +-- +-------+------+---------------+------+---------+------+------+-------------+ +-- | image | ALL | NULL | NULL | NULL | NULL | 194 | Using where | +-- +-------+------+---------------+------+---------+------+------+-------------+ +-- to: +-- +-------+------+-------------------------------+----------------+---------+-------+------+-------------+ +-- | table | type | possible_keys | key | key_len | ref | rows | Extra | +-- +-------+------+-------------------------------+----------------+---------+-------+------+-------------+ +-- | image | ref | img_major_mime,img_minor_mime | img_minor_mime | 32 | const | 4 | Using where | +-- +-------+------+-------------------------------+----------------+---------+-------+------+-------------+ + +ALTER TABLE /*$wgDBprefix*/image + ADD INDEX img_major_mime (img_major_mime); +ALTER TABLE /*$wgDBprefix*/image + ADD INDEX img_minor_mime (img_minor_mime); + diff --git a/www/wiki/maintenance/archives/patch-module_deps-fix-pk.sql b/www/wiki/maintenance/archives/patch-module_deps-fix-pk.sql new file mode 100644 index 00000000..2338df0a --- /dev/null +++ b/www/wiki/maintenance/archives/patch-module_deps-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/module_deps DROP KEY /*i*/md_module_skin, ADD PRIMARY KEY (md_module,md_skin); diff --git a/www/wiki/maintenance/archives/patch-module_deps.sql b/www/wiki/maintenance/archives/patch-module_deps.sql new file mode 100644 index 00000000..ffc94829 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-module_deps.sql @@ -0,0 +1,12 @@ +-- Table for tracking which local files a module depends on that aren't +-- registered directly. +-- Currently only used for tracking images that CSS depends on +CREATE TABLE /*_*/module_deps ( + -- Module name + md_module varbinary(255) NOT NULL, + -- Skin name + md_skin varbinary(32) NOT NULL, + -- JSON blob with file dependencies + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); diff --git a/www/wiki/maintenance/archives/patch-nullable-ar_text.sql b/www/wiki/maintenance/archives/patch-nullable-ar_text.sql new file mode 100644 index 00000000..b62ebfac --- /dev/null +++ b/www/wiki/maintenance/archives/patch-nullable-ar_text.sql @@ -0,0 +1,13 @@ +-- +-- patch-nullable-ar_text.sql +-- +-- This patch is provided as an example for people not using update.php. +-- You need to make a change like this before running a version of MediaWiki +-- containing Gerrit change 5ca2d4a551, then you can run maintenance/migrateArchiveText.php +-- and apply patch-drop-ar_text.sql at your leisure. +-- +-- See also T33223. + +ALTER TABLE /*_*/archive + MODIFY COLUMN ar_text mediumblob NULL, + MODIFY COLUMN ar_flags tinyblob NULL; diff --git a/www/wiki/maintenance/archives/patch-objectcache-fix-pk.sql b/www/wiki/maintenance/archives/patch-objectcache-fix-pk.sql new file mode 100644 index 00000000..cd557160 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-objectcache-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/objectcache DROP KEY /*i*/keyname, ADD PRIMARY KEY (keyname); diff --git a/www/wiki/maintenance/archives/patch-objectcache.sql b/www/wiki/maintenance/archives/patch-objectcache.sql new file mode 100644 index 00000000..5edf305b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-objectcache.sql @@ -0,0 +1,9 @@ +-- For a few generic cache operations if not using Memcached +CREATE TABLE /*$wgDBprefix*/objectcache ( + keyname varbinary(255) NOT NULL default '', + value mediumblob, + exptime datetime, + UNIQUE KEY (keyname), + KEY (exptime) + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-oi_major_mime-chemical.sql b/www/wiki/maintenance/archives/patch-oi_major_mime-chemical.sql new file mode 100644 index 00000000..e3b4552d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-oi_major_mime-chemical.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*$wgDBprefix*/oldimage + CHANGE oi_major_mime oi_major_mime ENUM('unknown','application','audio','image','text','video','message','model','multipart','chemical'); + diff --git a/www/wiki/maintenance/archives/patch-oi_metadata.sql b/www/wiki/maintenance/archives/patch-oi_metadata.sql new file mode 100644 index 00000000..df043c55 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-oi_metadata.sql @@ -0,0 +1,17 @@ +-- +-- patch-oi_metadata.sql +-- +-- Add data to allow for direct reference to old images +-- Some re-indexing here. +-- Old images can be included into pages effeciently now. +-- + +ALTER TABLE /*$wgDBprefix*/oldimage + DROP INDEX oi_name, + ADD INDEX oi_name_timestamp (oi_name,oi_timestamp), + ADD INDEX oi_name_archive_name (oi_name,oi_archive_name(14)), + ADD oi_metadata mediumblob NOT NULL, + ADD oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + ADD oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + ADD oi_minor_mime varbinary(32) NOT NULL default "unknown", + ADD oi_deleted tinyint unsigned NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-oldestindex.sql b/www/wiki/maintenance/archives/patch-oldestindex.sql new file mode 100644 index 00000000..930214fd --- /dev/null +++ b/www/wiki/maintenance/archives/patch-oldestindex.sql @@ -0,0 +1,5 @@ +-- Add index for "Oldest articles" (Special:Ancientpages) +-- 2003-05-23 Erik Moeller + +ALTER TABLE /*$wgDBprefix*/cur + ADD INDEX namespace_redirect_timestamp(cur_namespace,cur_is_redirect,cur_timestamp); diff --git a/www/wiki/maintenance/archives/patch-oldimage-user-index.sql b/www/wiki/maintenance/archives/patch-oldimage-user-index.sql new file mode 100644 index 00000000..2c7f8071 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-oldimage-user-index.sql @@ -0,0 +1,8 @@ +-- +-- oldimage-user-index.sql +-- +-- Add user/timestamp index to old image versions +-- + +ALTER TABLE /*$wgDBprefix*/oldimage + ADD INDEX oi_usertext_timestamp (oi_user_text,oi_timestamp); diff --git a/www/wiki/maintenance/archives/patch-page-page_content_model.sql b/www/wiki/maintenance/archives/patch-page-page_content_model.sql new file mode 100644 index 00000000..30434d93 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page-page_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/page + ADD page_content_model varbinary(32) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-page_lang.sql b/www/wiki/maintenance/archives/patch-page_lang.sql new file mode 100644 index 00000000..c792b4ad --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_lang.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/page + ADD page_lang varbinary(35) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-page_len.sql b/www/wiki/maintenance/archives/patch-page_len.sql new file mode 100644 index 00000000..7d01d90a --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_len.sql @@ -0,0 +1,16 @@ +-- Page length field (in bytes) for current revision of page. +-- Since page text is now stored separately, it may be compressed +-- or otherwise difficult to calculate. Additionally, the field +-- can be indexed for handy 'long' and 'short' page lists. +-- +-- Added 2005-03-12 + +ALTER TABLE /*$wgDBprefix*/page + ADD page_len int unsigned NOT NULL, + ADD INDEX (page_len); + +-- Not accurate if upgrading from intermediate +-- 1.5 alpha and have revision compression on. +UPDATE /*$wgDBprefix*/page, /*$wgDBprefix*/text + SET page_len=LENGTH(old_text) + WHERE page_latest=old_id; diff --git a/www/wiki/maintenance/archives/patch-page_links_updated.sql b/www/wiki/maintenance/archives/patch-page_links_updated.sql new file mode 100644 index 00000000..18d9e2d9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_links_updated.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/page + ADD page_links_updated varbinary(14) NULL default NULL; diff --git a/www/wiki/maintenance/archives/patch-page_props-propname-page-index.sql b/www/wiki/maintenance/archives/patch-page_props-propname-page-index.sql new file mode 100644 index 00000000..822fa04d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_props-propname-page-index.sql @@ -0,0 +1,4 @@ +-- +-- Creates the pp_propname_page index on page_props +-- +CREATE UNIQUE INDEX /*i*/pp_propname_page ON /*_*/page_props (pp_propname, pp_page); diff --git a/www/wiki/maintenance/archives/patch-page_props.sql b/www/wiki/maintenance/archives/patch-page_props.sql new file mode 100644 index 00000000..15a35581 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_props.sql @@ -0,0 +1,9 @@ +-- Name/value pairs indexed by page_id +CREATE TABLE /*$wgDBprefix*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL, + + PRIMARY KEY (pp_page,pp_propname) +) /*$wgDBTableOptions*/; + diff --git a/www/wiki/maintenance/archives/patch-page_redirect_namespace_len.sql b/www/wiki/maintenance/archives/patch-page_redirect_namespace_len.sql new file mode 100644 index 00000000..392945fb --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_redirect_namespace_len.sql @@ -0,0 +1,6 @@ +-- +-- Add the page_redirect_namespace_len index +-- + +CREATE INDEX /*i*/page_redirect_namespace_len ON /*_*/page (page_is_redirect, page_namespace, page_len); + diff --git a/www/wiki/maintenance/archives/patch-page_restrictions-pr_user-unsigned.sql b/www/wiki/maintenance/archives/patch-page_restrictions-pr_user-unsigned.sql new file mode 100644 index 00000000..2337ff0c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_restrictions-pr_user-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/page_restrictions MODIFY pr_user int unsigned NULL; diff --git a/www/wiki/maintenance/archives/patch-page_restrictions.sql b/www/wiki/maintenance/archives/patch-page_restrictions.sql new file mode 100644 index 00000000..6813c1e7 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_restrictions.sql @@ -0,0 +1,20 @@ +--- Used for storing page restrictions (i.e. protection levels) +CREATE TABLE /*$wgDBprefix*/page_restrictions ( + -- Page to apply restrictions to (Foreign Key to page). + pr_page int NOT NULL, + -- The protection type (edit, move, etc) + pr_type varbinary(60) NOT NULL, + -- The protection level (Sysop, autoconfirmed, etc) + pr_level varbinary(60) NOT NULL, + -- Whether or not to cascade the protection down to pages transcluded. + pr_cascade tinyint NOT NULL, + -- Field for future support of per-user restriction. + pr_user int NULL, + -- Field for time-limited protection. + pr_expiry varbinary(14) NULL, + + PRIMARY KEY pr_pagetype (pr_page,pr_type), + KEY pr_typelevel (pr_type,pr_level), + KEY pr_level (pr_level), + KEY pr_cascade (pr_cascade) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-page_restrictions_sortkey.sql b/www/wiki/maintenance/archives/patch-page_restrictions_sortkey.sql new file mode 100644 index 00000000..6b24e3a5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-page_restrictions_sortkey.sql @@ -0,0 +1,8 @@ +-- Add a sort-key to page_restrictions table. +-- First immediate use of this is as a sort-key for coming modifications +-- of Special:Protectedpages. +-- Andrew Garrett, February 2007 + +ALTER TABLE /*$wgDBprefix*/page_restrictions + ADD COLUMN pr_id int unsigned not null auto_increment, + ADD UNIQUE KEY pr_id (pr_id); diff --git a/www/wiki/maintenance/archives/patch-pagelinks-fix-pk.sql b/www/wiki/maintenance/archives/patch-pagelinks-fix-pk.sql new file mode 100644 index 00000000..e2691439 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-pagelinks-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/pagelinks DROP INDEX /*i*/pl_from, ADD PRIMARY KEY (pl_from,pl_namespace,pl_title); diff --git a/www/wiki/maintenance/archives/patch-pagelinks.sql b/www/wiki/maintenance/archives/patch-pagelinks.sql new file mode 100644 index 00000000..cea89b52 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-pagelinks.sql @@ -0,0 +1,56 @@ +-- +-- Create the new pagelinks table to merge links and brokenlinks data, +-- and populate it. +-- +-- Unlike the old links and brokenlinks, these records will not need to be +-- altered when target pages are created, deleted, or renamed. This should +-- reduce the amount of severe database frustration that happens when widely- +-- linked pages are altered. +-- +-- Fixups for brokenlinks to pages in namespaces need to be run after this; +-- this is done by updaters.inc if run through the regular update scripts. +-- +-- 2005-05-26 +-- + +-- +-- Track page-to-page hyperlinks within the wiki. +-- +CREATE TABLE /*$wgDBprefix*/pagelinks ( + -- Key to the page_id of the page containing the link. + pl_from int unsigned NOT NULL default '0', + + -- Key to page_namespace/page_title of the target page. + -- The target page may or may not exist, and due to renames + -- and deletions may refer to different page records as time + -- goes by. + pl_namespace int NOT NULL default '0', + pl_title varchar(255) binary NOT NULL default '', + + UNIQUE KEY pl_from(pl_from,pl_namespace,pl_title), + KEY (pl_namespace,pl_title) + +) /*$wgDBTableOptions*/; + + +-- Import existing-page links +INSERT + INTO /*$wgDBprefix*/pagelinks (pl_from,pl_namespace,pl_title) + SELECT l_from,page_namespace,page_title + FROM /*$wgDBprefix*/links, /*$wgDBprefix*/page + WHERE l_to=page_id; + +-- import brokenlinks +-- NOTE: We'll have to fix up individual entries that aren't in main NS +INSERT INTO /*$wgDBprefix*/pagelinks (pl_from,pl_namespace,pl_title) + SELECT bl_from, 0, bl_to + FROM /*$wgDBprefix*/brokenlinks; + +-- For each namespace do something like: +-- +-- UPDATE /*$wgDBprefix*/pagelinks +-- SET pl_namespace=$ns, +-- pl_title=TRIM(LEADING '$prefix:' FROM pl_title) +-- WHERE pl_namespace=0 +-- AND pl_title LIKE '$likeprefix:%'"; +-- diff --git a/www/wiki/maintenance/archives/patch-parsercache.sql b/www/wiki/maintenance/archives/patch-parsercache.sql new file mode 100644 index 00000000..5fe241c3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-parsercache.sql @@ -0,0 +1,15 @@ +-- +-- parsercache table, for cacheing complete parsed articles +-- before they are imbedded in the skin. +-- + +CREATE TABLE /*$wgDBprefix*/parsercache ( + pc_pageid INT(11) NOT NULL, + pc_title VARCHAR(255) NOT NULL, + pc_prefhash CHAR(32) NOT NULL, + pc_expire DATETIME NOT NULL, + pc_data MEDIUMBLOB NOT NULL, + PRIMARY KEY (pc_pageid, pc_prefhash), + KEY(pc_title), + KEY(pc_expire) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-pl-tl-il-nonunique.sql b/www/wiki/maintenance/archives/patch-pl-tl-il-nonunique.sql new file mode 100644 index 00000000..8e1715b3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-pl-tl-il-nonunique.sql @@ -0,0 +1,11 @@ +-- Make reorderings of UNIQUE indices non-UNIQUE +-- Since 1.24, these indices have been non-UNIQUE in tables.sql. +-- However, an earlier update from 1.15 that made the indices +-- UNIQUE was not removed until 1.28 (T78513). + +DROP INDEX /*i*/pl_namespace ON /*_*/pagelinks; +CREATE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace, pl_title, pl_from); +DROP INDEX /*i*/tl_namespace ON /*_*/templatelinks; +CREATE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace, tl_title, tl_from); +DROP INDEX /*i*/il_to ON /*_*/imagelinks; +CREATE INDEX /*i*/il_to ON /*_*/imagelinks (il_to, il_from); diff --git a/www/wiki/maintenance/archives/patch-pl_from_namespace.sql b/www/wiki/maintenance/archives/patch-pl_from_namespace.sql new file mode 100644 index 00000000..dcf2b60a --- /dev/null +++ b/www/wiki/maintenance/archives/patch-pl_from_namespace.sql @@ -0,0 +1,4 @@ +ALTER TABLE /*_*/pagelinks + ADD COLUMN pl_from_namespace int NOT NULL default 0; + +CREATE INDEX /*i*/pl_backlinks_namespace ON /*_*/pagelinks (pl_from_namespace,pl_namespace,pl_title,pl_from); diff --git a/www/wiki/maintenance/archives/patch-pp_sortkey.sql b/www/wiki/maintenance/archives/patch-pp_sortkey.sql new file mode 100644 index 00000000..b13b6055 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-pp_sortkey.sql @@ -0,0 +1,8 @@ +-- Add a 'sortkey' field to page_props so pages can be efficiently +-- queried by the numeric value of a property. + +ALTER TABLE /*_*/page_props + ADD pp_sortkey float DEFAULT NULL; + +CREATE UNIQUE INDEX /*i*/pp_propname_sortkey_page + ON /*_*/page_props ( pp_propname, pp_sortkey, pp_page ); diff --git a/www/wiki/maintenance/archives/patch-profiling-memory.sql b/www/wiki/maintenance/archives/patch-profiling-memory.sql new file mode 100644 index 00000000..ddd851e1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-profiling-memory.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/profiling + ADD pf_memory float NOT NULL default 0; diff --git a/www/wiki/maintenance/archives/patch-profiling.sql b/www/wiki/maintenance/archives/patch-profiling.sql new file mode 100644 index 00000000..6ad16224 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-profiling.sql @@ -0,0 +1,12 @@ +-- profiling table +-- This is optional + +CREATE TABLE /*_*/profiling ( + pf_count int NOT NULL default 0, + pf_time float NOT NULL default 0, + pf_memory float NOT NULL default 0, + pf_name varchar(255) NOT NULL default '', + pf_server varchar(30) NOT NULL default '' +) ENGINE=MEMORY; + +CREATE UNIQUE INDEX /*i*/pf_name_server ON /*_*/profiling (pf_name, pf_server); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-protected_titles.sql b/www/wiki/maintenance/archives/patch-protected_titles.sql new file mode 100644 index 00000000..20b6035d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-protected_titles.sql @@ -0,0 +1,12 @@ +-- Protected titles - nonexistent pages that have been protected +CREATE TABLE /*$wgDBprefix*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL, + PRIMARY KEY (pt_namespace,pt_title), + KEY pt_timestamp (pt_timestamp) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-pt_title-encoding.sql b/www/wiki/maintenance/archives/patch-pt_title-encoding.sql new file mode 100644 index 00000000..b0a23932 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-pt_title-encoding.sql @@ -0,0 +1,5 @@ +-- pt_title was accidentally left with the wrong collation. +-- This might cause failures with JOINs, and could protect the wrong pages +-- with different case variants or unrelated UTF-8 chars. +ALTER TABLE /*$wgDBprefix*/protected_titles + CHANGE COLUMN pt_title pt_title varchar(255) binary NOT NULL; diff --git a/www/wiki/maintenance/archives/patch-querycache.sql b/www/wiki/maintenance/archives/patch-querycache.sql new file mode 100644 index 00000000..8e1a5188 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-querycache.sql @@ -0,0 +1,16 @@ +-- Used for caching expensive grouped queries + +CREATE TABLE /*$wgDBprefix*/querycache ( + -- A key name, generally the base name of of the special page. + qc_type varbinary(32) NOT NULL, + + -- Some sort of stored value. Sizes, counts... + qc_value int unsigned NOT NULL default '0', + + -- Target namespace+title + qc_namespace int NOT NULL default '0', + qc_title varchar(255) binary NOT NULL default '', + + KEY (qc_type,qc_value) + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-querycache_info-fix-pk.sql b/www/wiki/maintenance/archives/patch-querycache_info-fix-pk.sql new file mode 100644 index 00000000..94f3c1d6 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-querycache_info-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/querycache_info DROP KEY /*i*/qci_type, ADD PRIMARY KEY (qci_type); diff --git a/www/wiki/maintenance/archives/patch-querycacheinfo.sql b/www/wiki/maintenance/archives/patch-querycacheinfo.sql new file mode 100644 index 00000000..7ad2bca6 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-querycacheinfo.sql @@ -0,0 +1,12 @@ +CREATE TABLE /*$wgDBprefix*/querycache_info ( + + -- Special page name + -- Corresponds to a qc_type value + qci_type varbinary(32) NOT NULL default '', + + -- Timestamp of last update + qci_timestamp binary(14) NOT NULL default '19700101000000', + + UNIQUE KEY ( qci_type ) + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-querycachetwo.sql b/www/wiki/maintenance/archives/patch-querycachetwo.sql new file mode 100644 index 00000000..79131310 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-querycachetwo.sql @@ -0,0 +1,22 @@ +-- Used for caching expensive grouped queries that need two links (for example double-redirects) + +CREATE TABLE /*$wgDBprefix*/querycachetwo ( + -- A key name, generally the base name of of the special page. + qcc_type varbinary(32) NOT NULL, + + -- Some sort of stored value. Sizes, counts... + qcc_value int unsigned NOT NULL default '0', + + -- Target namespace+title + qcc_namespace int NOT NULL default '0', + qcc_title varchar(255) binary NOT NULL default '', + + -- Target namespace+title2 + qcc_namespacetwo int NOT NULL default '0', + qcc_titletwo varchar(255) binary NOT NULL default '', + + KEY qcc_type (qcc_type,qcc_value), + KEY qcc_title (qcc_type,qcc_namespace,qcc_title), + KEY qcc_titletwo (qcc_type,qcc_namespacetwo,qcc_titletwo) + +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-random-dateindex.sql b/www/wiki/maintenance/archives/patch-random-dateindex.sql new file mode 100644 index 00000000..5d514cc3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-random-dateindex.sql @@ -0,0 +1,54 @@ +-- patch-random-dateindex.sql +-- 2003-02-09 +-- +-- This patch does two things: +-- * Adds cur_random column to replace random table +-- (Requires change to SpecialRandom.php) +-- random table no longer needs refilling +-- Note: short-term duplicate results *are* possible, but very unlikely on large wiki +-- +-- * Adds inverse_timestamp columns to cur and old and indexes +-- to allow descending timestamp sort in history, contribs, etc +-- (Requires changes to Article.php, DatabaseFunctions.php, +-- ... ) +-- cur_timestamp inverse_timestamp +-- 99999999999999 - 20030209222556 = 79969790777443 +-- 99999999999999 - 20030211083412 = 79969788916587 +-- +-- We won't need this on MySQL 4; there will be a removal patch later. + +-- Indexes: +-- cur needs (cur_random) for random sort +-- cur and old need (namespace,title,timestamp) index for history,watchlist,rclinked +-- cur and old need (user,timestamp) index for contribs +-- cur and old need (user_text,timestamp) index for contribs + +ALTER TABLE /*$wgDBprefix*/cur + DROP INDEX cur_user, + DROP INDEX cur_user_text, + ADD COLUMN cur_random real unsigned NOT NULL, + ADD COLUMN inverse_timestamp char(14) binary NOT NULL default '', + ADD INDEX (cur_random), + ADD INDEX name_title_timestamp (cur_namespace,cur_title,inverse_timestamp), + ADD INDEX user_timestamp (cur_user,inverse_timestamp), + ADD INDEX usertext_timestamp (cur_user_text,inverse_timestamp); + +UPDATE /*$wgDBprefix*/cur SET + inverse_timestamp=99999999999999-cur_timestamp, + cur_random=RAND(); + +ALTER TABLE /*$wgDBprefix*/old + DROP INDEX old_user, + DROP INDEX old_user_text, + ADD COLUMN inverse_timestamp char(14) binary NOT NULL default '', + ADD INDEX name_title_timestamp (old_namespace,old_title,inverse_timestamp), + ADD INDEX user_timestamp (old_user,inverse_timestamp), + ADD INDEX usertext_timestamp (old_user_text,inverse_timestamp); + +UPDATE /*$wgDBprefix*/old SET + inverse_timestamp=99999999999999-old_timestamp; + +-- If leaving wiki publicly accessible in read-only mode during +-- the upgrade, comment out the below line; leave 'random' table +-- in place until the new software is installed. +DROP TABLE /*$wgDBprefix*/random; diff --git a/www/wiki/maintenance/archives/patch-rc-newindex.sql b/www/wiki/maintenance/archives/patch-rc-newindex.sql new file mode 100644 index 00000000..2315ff37 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc-newindex.sql @@ -0,0 +1,9 @@ +-- +-- patch-rc-newindex.sql +-- Adds an index to recentchanges to optimize Special:Newpages +-- 2004-01-25 +-- + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD INDEX new_name_timestamp(rc_new,rc_namespace,rc_timestamp); + diff --git a/www/wiki/maintenance/archives/patch-rc-patrol.sql b/www/wiki/maintenance/archives/patch-rc-patrol.sql new file mode 100644 index 00000000..1839c1ee --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc-patrol.sql @@ -0,0 +1,9 @@ +-- +-- patch-rc-patrol.sql +-- Adds a row to recentchanges for the patrolling feature +-- 2004-08-09 +-- + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD COLUMN rc_patrolled tinyint(3) unsigned NOT NULL default '0'; + diff --git a/www/wiki/maintenance/archives/patch-rc_deleted.sql b/www/wiki/maintenance/archives/patch-rc_deleted.sql new file mode 100644 index 00000000..f4bbd0f9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_deleted.sql @@ -0,0 +1,8 @@ +-- Adding rc_deleted field for revisiondelete +-- Add rc_logid to match log_id +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD rc_deleted tinyint unsigned NOT NULL default '0', + ADD rc_logid int unsigned NOT NULL default '0', + ADD rc_log_type varbinary(255) NULL default NULL, + ADD rc_log_action varbinary(255) NULL default NULL, + ADD rc_params BLOB NULL; diff --git a/www/wiki/maintenance/archives/patch-rc_id.sql b/www/wiki/maintenance/archives/patch-rc_id.sql new file mode 100644 index 00000000..28caee0e --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_id.sql @@ -0,0 +1,7 @@ +-- Primary key in recentchanges + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD rc_id int NOT NULL auto_increment, + ADD PRIMARY KEY rc_id (rc_id); + + diff --git a/www/wiki/maintenance/archives/patch-rc_ip.sql b/www/wiki/maintenance/archives/patch-rc_ip.sql new file mode 100644 index 00000000..4d93300f --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_ip.sql @@ -0,0 +1,7 @@ +-- Adding the rc_ip field for logging of IP addresses in recentchanges + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD rc_ip varbinary(40) NOT NULL default '', + ADD INDEX rc_ip (rc_ip); + + diff --git a/www/wiki/maintenance/archives/patch-rc_ip_modify.sql b/www/wiki/maintenance/archives/patch-rc_ip_modify.sql new file mode 100644 index 00000000..e889b5c5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_ip_modify.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/recentchanges MODIFY COLUMN rc_ip varbinary(40) NOT NULL default ''; diff --git a/www/wiki/maintenance/archives/patch-rc_len.sql b/www/wiki/maintenance/archives/patch-rc_len.sql new file mode 100644 index 00000000..6c781a00 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_len.sql @@ -0,0 +1,9 @@ +-- +-- patch-rc_len.sql +-- Adds two rows to recentchanges to hold the text size befor and after the edit +-- 2006-12-03 +-- + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD COLUMN rc_old_len int, ADD COLUMN rc_new_len int; + diff --git a/www/wiki/maintenance/archives/patch-rc_moved.sql b/www/wiki/maintenance/archives/patch-rc_moved.sql new file mode 100644 index 00000000..2fa1de6b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_moved.sql @@ -0,0 +1,4 @@ +-- rc_moved_to_ns and rc_moved_to_title is no longer used, delete the fields + +ALTER TABLE /*$wgDBprefix*/recentchanges DROP COLUMN rc_moved_to_ns, + DROP COLUMN rc_moved_to_title; diff --git a/www/wiki/maintenance/archives/patch-rc_source.sql b/www/wiki/maintenance/archives/patch-rc_source.sql new file mode 100644 index 00000000..7dedd745 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_source.sql @@ -0,0 +1,16 @@ +-- first step of migrating recentchanges rc_type to rc_source +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD rc_source varbinary(16) NOT NULL default ''; + +-- Populate rc_source field with the data from rc_type +-- Large wiki's might prefer the PopulateRecentChangeSource maintenance +-- script to batch updates into groups rather than all at once. +UPDATE /*$wgDBprefix*/recentchanges + SET rc_source = CASE + WHEN rc_type = 0 THEN 'mw.edit' + WHEN rc_type = 1 THEN 'mw.new' + WHEN rc_type = 3 THEN 'mw.log' + WHEN rc_type = 5 THEN 'mw.external' + ELSE '' + END +WHERE rc_source = ''; diff --git a/www/wiki/maintenance/archives/patch-rc_type.sql b/www/wiki/maintenance/archives/patch-rc_type.sql new file mode 100644 index 00000000..f1fb18e5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_type.sql @@ -0,0 +1,9 @@ +-- recentchanges improvements -- + +ALTER TABLE /*$wgDBprefix*/recentchanges + ADD rc_type tinyint unsigned NOT NULL default '0', + ADD rc_moved_to_ns tinyint unsigned NOT NULL default '0', + ADD rc_moved_to_title varchar(255) binary NOT NULL default ''; + +UPDATE /*$wgDBprefix*/recentchanges SET rc_type=1 WHERE rc_new; +UPDATE /*$wgDBprefix*/recentchanges SET rc_type=3 WHERE rc_namespace=4 AND (rc_title='Deletion_log' OR rc_title='Upload_log'); diff --git a/www/wiki/maintenance/archives/patch-rc_user_text-index.sql b/www/wiki/maintenance/archives/patch-rc_user_text-index.sql new file mode 100644 index 00000000..f6acc992 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rc_user_text-index.sql @@ -0,0 +1,7 @@ +-- Add an index to recentchanges on rc_user_text +-- +-- Added 2006-11-08 +-- + + ALTER TABLE /*$wgDBprefix*/recentchanges +ADD INDEX rc_user_text(rc_user_text, rc_timestamp); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-rd_interwiki.sql b/www/wiki/maintenance/archives/patch-rd_interwiki.sql new file mode 100644 index 00000000..a12f1a7d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rd_interwiki.sql @@ -0,0 +1,6 @@ +-- Add interwiki and fragment columns to redirect table + +ALTER TABLE /*$wgDBprefix*/redirect + ADD rd_interwiki varchar(32) default NULL, + ADD rd_fragment varchar(255) binary default NULL; + diff --git a/www/wiki/maintenance/archives/patch-recentchanges-nttindex.sql b/www/wiki/maintenance/archives/patch-recentchanges-nttindex.sql new file mode 100644 index 00000000..11794e80 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-recentchanges-nttindex.sql @@ -0,0 +1,11 @@ +-- +-- patch-recentchanges-nttindex.sql +-- +-- Per task T57377 +-- +-- Improve performance API queries to ask for a certain pages +-- + + +DROP INDEX /*i*/rc_namespace_title ON /*_*/recentchanges; +CREATE INDEX /*i*/rc_namespace_title_timestamp ON /*_*/recentchanges (rc_namespace, rc_title, rc_timestamp); diff --git a/www/wiki/maintenance/archives/patch-recentchanges-utindex.sql b/www/wiki/maintenance/archives/patch-recentchanges-utindex.sql new file mode 100644 index 00000000..4ebe3165 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-recentchanges-utindex.sql @@ -0,0 +1,4 @@ +--- July 2006 +--- Index on recentchanges.( rc_namespace, rc_user_text ) +--- Helps the username filtering in Special:Newpages +ALTER TABLE /*$wgDBprefix*/recentchanges ADD INDEX `rc_ns_usertext` ( `rc_namespace` , `rc_user_text` ); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-redirect.sql b/www/wiki/maintenance/archives/patch-redirect.sql new file mode 100644 index 00000000..d2957df4 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-redirect.sql @@ -0,0 +1,28 @@ +-- +-- Create the new redirect table. +-- For each redirect, this table contains exactly one row defining its target +-- +CREATE TABLE /*$wgDBprefix*/redirect ( + -- Key to the page_id of the redirect page + rd_from int unsigned NOT NULL default '0', + + -- Key to page_namespace/page_title of the target page. + -- The target page may or may not exist, and due to renames + -- and deletions may refer to different page records as time + -- goes by. + rd_namespace int NOT NULL default '0', + rd_title varchar(255) binary NOT NULL default '', + + PRIMARY KEY rd_from (rd_from), + KEY rd_ns_title (rd_namespace,rd_title,rd_from) +) /*$wgDBTableOptions*/; + +-- Import existing redirects +-- Using ignore because some of the redirect pages contain more than one link +INSERT IGNORE + INTO /*$wgDBprefix*/redirect (rd_from,rd_namespace,rd_title) + SELECT pl_from,pl_namespace,pl_title + FROM /*$wgDBprefix*/pagelinks, /*$wgDBprefix*/page + WHERE pl_from=page_id AND page_is_redirect=1; + + diff --git a/www/wiki/maintenance/archives/patch-rename-ar_usertext_timestamp.sql b/www/wiki/maintenance/archives/patch-rename-ar_usertext_timestamp.sql new file mode 100644 index 00000000..658c179a --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rename-ar_usertext_timestamp.sql @@ -0,0 +1,7 @@ +-- Rename the archive.ar_usertext_timestamp index to usertext_timestamp. +-- This is for MySQL only and is only necessary on wikis freshly installed on +-- 1.28.0 when bug T154872 was present. The patch will probably be removed in +-- 1.29 since we plan on renaming the index properly to ar_usertext_timestamp. +ALTER TABLE /*$wgDBprefix*/archive + DROP INDEX ar_usertext_timestamp, + ADD INDEX usertext_timestamp (ar_user_text,ar_timestamp); diff --git a/www/wiki/maintenance/archives/patch-rename-iwl_prefix.sql b/www/wiki/maintenance/archives/patch-rename-iwl_prefix.sql new file mode 100644 index 00000000..4a410037 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rename-iwl_prefix.sql @@ -0,0 +1,4 @@ +-- +-- Recreates the iwl_prefix index for the iwlinks table +-- +CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); diff --git a/www/wiki/maintenance/archives/patch-rename-user_groups-and_rights.sql b/www/wiki/maintenance/archives/patch-rename-user_groups-and_rights.sql new file mode 100644 index 00000000..978b31f7 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rename-user_groups-and_rights.sql @@ -0,0 +1,9 @@ + +ALTER TABLE /*$wgDBprefix*/user_groups + CHANGE user_id ug_user INT UNSIGNED NOT NULL DEFAULT '0', + CHANGE group_id ug_group INT UNSIGNED NOT NULL DEFAULT '0'; + +ALTER TABLE /*$wgDBprefix*/user_rights + CHANGE user_id ur_user INT UNSIGNED NOT NULL, + CHANGE user_rights ur_rights TINYBLOB NOT NULL; + diff --git a/www/wiki/maintenance/archives/patch-rev_deleted.sql b/www/wiki/maintenance/archives/patch-rev_deleted.sql new file mode 100644 index 00000000..ba47f789 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rev_deleted.sql @@ -0,0 +1,11 @@ +-- +-- Add rev_deleted flag to revision table. +-- Deleted revisions can thus continue to be listed in history +-- and user contributions, and their text storage doesn't have +-- to be disturbed. +-- +-- 2005-03-31 +-- + +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_deleted tinyint unsigned NOT NULL default '0'; diff --git a/www/wiki/maintenance/archives/patch-rev_len.sql b/www/wiki/maintenance/archives/patch-rev_len.sql new file mode 100644 index 00000000..ccdae8b8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rev_len.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_len INT UNSIGNED; + diff --git a/www/wiki/maintenance/archives/patch-rev_parent_id.sql b/www/wiki/maintenance/archives/patch-rev_parent_id.sql new file mode 100644 index 00000000..4baf7927 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rev_parent_id.sql @@ -0,0 +1,9 @@ +-- +-- Key to revision.rev_id +-- This field is used to add support for a tree structure (The Adjacency List Model) +-- +-- 2007-03-04 +-- + +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_parent_id int unsigned default NULL; diff --git a/www/wiki/maintenance/archives/patch-rev_sha1.sql b/www/wiki/maintenance/archives/patch-rev_sha1.sql new file mode 100644 index 00000000..0100c365 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rev_sha1.sql @@ -0,0 +1,3 @@ +-- Adding rev_sha1 field +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_sha1 varbinary(32) NOT NULL default ''; diff --git a/www/wiki/maintenance/archives/patch-rev_text_id-default.sql b/www/wiki/maintenance/archives/patch-rev_text_id-default.sql new file mode 100644 index 00000000..dc6e4c66 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rev_text_id-default.sql @@ -0,0 +1,10 @@ +-- +-- Adds a default value to the rev_text_id field in the revision table. +-- This is to allow the Multi Content Revisions migration to happen where +-- rows will have to be added to the revision table with no rev_text_id. +-- +-- 2018-03-12 +-- + +ALTER TABLE /*$wgDBprefix*/revision + ALTER COLUMN rev_text_id SET DEFAULT 0; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-rev_text_id.sql b/www/wiki/maintenance/archives/patch-rev_text_id.sql new file mode 100644 index 00000000..3dd9127d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-rev_text_id.sql @@ -0,0 +1,17 @@ +-- +-- Adds rev_text_id field to revision table. +-- This is a key to text.old_id, so that revisions can be stored +-- for non-save operations without duplicating text, and so that +-- a back-end storage system can provide its own numbering system +-- if necessary. +-- +-- rev.rev_id and text.old_id are no longer assumed to be the same. +-- +-- 2005-03-28 +-- + +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_text_id int unsigned NOT NULL; + +UPDATE /*$wgDBprefix*/revision + SET rev_text_id=rev_id; diff --git a/www/wiki/maintenance/archives/patch-revision-page-rev-index-nonunique.sql b/www/wiki/maintenance/archives/patch-revision-page-rev-index-nonunique.sql new file mode 100644 index 00000000..dbb03257 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-revision-page-rev-index-nonunique.sql @@ -0,0 +1,5 @@ +-- Makes rev_page_id index non-unique +ALTER TABLE /*_*/revision +DROP INDEX /*i*/rev_page_id; + +CREATE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); diff --git a/www/wiki/maintenance/archives/patch-revision-rev_content_format.sql b/www/wiki/maintenance/archives/patch-revision-rev_content_format.sql new file mode 100644 index 00000000..22aeb8a7 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-revision-rev_content_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_content_format varbinary(64) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-revision-rev_content_model.sql b/www/wiki/maintenance/archives/patch-revision-rev_content_model.sql new file mode 100644 index 00000000..1ba05721 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-revision-rev_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_content_model varbinary(32) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-revision-user-page-index.sql b/www/wiki/maintenance/archives/patch-revision-user-page-index.sql new file mode 100644 index 00000000..a4554c8f --- /dev/null +++ b/www/wiki/maintenance/archives/patch-revision-user-page-index.sql @@ -0,0 +1,4 @@ +-- New index on revision table to allow searches for all edits by a given user +-- to a given page. Added 2007-08-28 + +CREATE INDEX /*i*/page_user_timestamp ON /*_*/revision (rev_page,rev_user,rev_timestamp); diff --git a/www/wiki/maintenance/archives/patch-searchindex.sql b/www/wiki/maintenance/archives/patch-searchindex.sql new file mode 100644 index 00000000..36507a2b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-searchindex.sql @@ -0,0 +1,40 @@ +-- Break fulltext search index out to separate table from cur +-- This is being done mainly to allow us to use InnoDB tables +-- for the main db while keeping the MyISAM fulltext index for +-- search. + +-- 2002-12-16, 2003-01-25 Brion VIBBER + +-- Creating searchindex table... +DROP TABLE IF EXISTS /*$wgDBprefix*/searchindex; +CREATE TABLE /*$wgDBprefix*/searchindex ( + -- Key to page_id + si_page int unsigned NOT NULL, + + -- Munged version of title + si_title varchar(255) NOT NULL default '', + + -- Munged version of body text + si_text mediumtext NOT NULL, + + UNIQUE KEY (si_page) + +) ENGINE=MyISAM; + +-- Copying data into new table... +INSERT INTO /*$wgDBprefix*/searchindex + (si_page,si_title,si_text) + SELECT + cur_id,cur_ind_title,cur_ind_text + FROM /*$wgDBprefix*/cur; + + +-- Creating fulltext index... +ALTER TABLE /*$wgDBprefix*/searchindex + ADD FULLTEXT si_title (si_title), + ADD FULLTEXT si_text (si_text); + +-- Dropping index columns from cur table. +ALTER TABLE /*$wgDBprefix*/cur + DROP COLUMN cur_ind_title, + DROP COLUMN cur_ind_text; diff --git a/www/wiki/maintenance/archives/patch-site_stats-fix-pk.sql b/www/wiki/maintenance/archives/patch-site_stats-fix-pk.sql new file mode 100644 index 00000000..d32adf34 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-site_stats-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/site_stats DROP KEY /*i*/ss_row_id, ADD PRIMARY KEY (ss_row_id); diff --git a/www/wiki/maintenance/archives/patch-site_stats-modify.sql b/www/wiki/maintenance/archives/patch-site_stats-modify.sql new file mode 100644 index 00000000..c70dd001 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-site_stats-modify.sql @@ -0,0 +1,7 @@ +ALTER TABLE /*_*/site_stats + ALTER ss_total_edits SET DEFAULT NULL, + ALTER ss_good_articles SET DEFAULT NULL, + MODIFY COLUMN ss_total_pages bigint unsigned DEFAULT NULL, + MODIFY COLUMN ss_users bigint unsigned DEFAULT NULL, + MODIFY COLUMN ss_active_users bigint unsigned DEFAULT NULL, + MODIFY COLUMN ss_images bigint unsigned DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-sites.sql b/www/wiki/maintenance/archives/patch-sites.sql new file mode 100644 index 00000000..88392748 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-sites.sql @@ -0,0 +1,71 @@ +-- Patch to add the sites and site_identifiers tables. +-- Licence: GNU GPL v2+ +-- Author: Jeroen De Dauw < jeroendedauw@gmail.com > + + +-- Holds all the sites known to the wiki. +CREATE TABLE IF NOT EXISTS /*_*/sites ( +-- Numeric id of the site + site_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + + -- Global identifier for the site, ie 'enwiktionary' + site_global_key varbinary(32) NOT NULL, + + -- Type of the site, ie 'mediawiki' + site_type varbinary(32) NOT NULL, + + -- Group of the site, ie 'wikipedia' + site_group varbinary(32) NOT NULL, + + -- Source of the site data, ie 'local', 'wikidata', 'my-magical-repo' + site_source varbinary(32) NOT NULL, + + -- Language code of the sites primary language. + site_language varbinary(32) NOT NULL, + + -- Protocol of the site, ie 'http://', 'irc://', '//' + -- This field is an index for lookups and is build from type specific data in site_data. + site_protocol varbinary(32) NOT NULL, + + -- Domain of the site in reverse order, ie 'org.mediawiki.www.' + -- This field is an index for lookups and is build from type specific data in site_data. + site_domain VARCHAR(255) NOT NULL, + + -- Type dependent site data. + site_data BLOB NOT NULL, + + -- If site.tld/path/key:pageTitle should forward users to the page on + -- the actual site, where "key" is the local identifier. + site_forward bool NOT NULL, + + -- Type dependent site config. + -- For instance if template transclusion should be allowed if it's a MediaWiki. + site_config BLOB NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/sites_global_key ON /*_*/sites (site_global_key); +CREATE INDEX /*i*/sites_type ON /*_*/sites (site_type); +CREATE INDEX /*i*/sites_group ON /*_*/sites (site_group); +CREATE INDEX /*i*/sites_source ON /*_*/sites (site_source); +CREATE INDEX /*i*/sites_language ON /*_*/sites (site_language); +CREATE INDEX /*i*/sites_protocol ON /*_*/sites (site_protocol); +CREATE INDEX /*i*/sites_domain ON /*_*/sites (site_domain); +CREATE INDEX /*i*/sites_forward ON /*_*/sites (site_forward); + + + +-- Links local site identifiers to their corresponding site. +CREATE TABLE IF NOT EXISTS /*_*/site_identifiers ( + -- Key on site.site_id + si_site INT UNSIGNED NOT NULL, + + -- local key type, ie 'interwiki' or 'langlink' + si_type varbinary(32) NOT NULL, + + -- local key value, ie 'en' or 'wiktionary' + si_key varbinary(32) NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/site_ids_type ON /*_*/site_identifiers (si_type, si_key); +CREATE INDEX /*i*/site_ids_site ON /*_*/site_identifiers (si_site); +CREATE INDEX /*i*/site_ids_key ON /*_*/site_identifiers (si_key); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-slot-origin.sql b/www/wiki/maintenance/archives/patch-slot-origin.sql new file mode 100644 index 00000000..ee069231 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-slot-origin.sql @@ -0,0 +1,15 @@ +-- +-- Replace slot_inherited with slot_origin. +-- +-- NOTE: There is no release that has slot_inherited. This is only needed to transition between +-- snapshot versions of 1.30. +-- +-- NOTE: No code that writes to the slots table was merged yet, the table is assumed to be empty. +-- +DROP INDEX /*i*/slot_role_inherited ON /*_*/slots; + +ALTER TABLE /*_*/slots + DROP COLUMN slot_inherited, + ADD COLUMN slot_origin bigint unsigned NOT NULL; + +CREATE INDEX /*i*/slot_revision_origin_role ON /*_*/slots (slot_revision_id, slot_origin, slot_role_id); diff --git a/www/wiki/maintenance/archives/patch-slot_roles.sql b/www/wiki/maintenance/archives/patch-slot_roles.sql new file mode 100644 index 00000000..0b13caa8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-slot_roles.sql @@ -0,0 +1,10 @@ +-- +-- Normalization table for role names +-- +CREATE TABLE /*_*/slot_roles ( + role_id smallint PRIMARY KEY AUTO_INCREMENT, + role_name varbinary(64) NOT NULL +) /*$wgDBTableOptions*/; + +-- Index for looking of the internal ID of for a name +CREATE UNIQUE INDEX /*i*/role_name ON /*_*/slot_roles (role_name); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-slots.sql b/www/wiki/maintenance/archives/patch-slots.sql new file mode 100644 index 00000000..5fafe6d3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-slots.sql @@ -0,0 +1,25 @@ +-- +-- Slots represent an n:m relation between revisions and content objects. +-- A content object can have a specific "role" in one or more revisions. +-- Each revision can have multiple content objects, each having a different role. +-- +CREATE TABLE /*_*/slots ( + + -- reference to rev_id + slot_revision_id bigint unsigned NOT NULL, + + -- reference to role_id + slot_role_id smallint unsigned NOT NULL, + + -- reference to content_id + slot_content_id bigint unsigned NOT NULL, + + -- The revision ID of the revision that originated the slot's content. + -- To find revisions that changed slots, look for slot_origin = slot_revision_id. + slot_origin bigint unsigned NOT NULL, + + PRIMARY KEY ( slot_revision_id, slot_role_id ) +) /*$wgDBTableOptions*/; + +-- Index for finding revisions that modified a specific slot +CREATE INDEX /*i*/slot_revision_origin_role ON /*_*/slots (slot_revision_id, slot_origin, slot_role_id); diff --git a/www/wiki/maintenance/archives/patch-ss_active_users.sql b/www/wiki/maintenance/archives/patch-ss_active_users.sql new file mode 100644 index 00000000..a583cdc8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ss_active_users.sql @@ -0,0 +1,3 @@ +-- More statistics, for version 1.14 + +ALTER TABLE /*$wgDBprefix*/site_stats ADD ss_active_users bigint default '-1'; diff --git a/www/wiki/maintenance/archives/patch-ss_images.sql b/www/wiki/maintenance/archives/patch-ss_images.sql new file mode 100644 index 00000000..80f1295f --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ss_images.sql @@ -0,0 +1,5 @@ +-- More statistics, for version 1.6 + +ALTER TABLE /*$wgDBprefix*/site_stats ADD ss_images int default '0'; +SELECT @images := COUNT(*) FROM /*$wgDBprefix*/image; +UPDATE /*$wgDBprefix*/site_stats SET ss_images=@images; diff --git a/www/wiki/maintenance/archives/patch-ss_total_articles.sql b/www/wiki/maintenance/archives/patch-ss_total_articles.sql new file mode 100644 index 00000000..ce804ce5 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ss_total_articles.sql @@ -0,0 +1,6 @@ +-- Faster statistics, as of 1.4.3 + +ALTER TABLE /*$wgDBprefix*/site_stats + ADD ss_total_pages bigint default -1, + ADD ss_users bigint default -1, + ADD ss_admins int default -1; diff --git a/www/wiki/maintenance/archives/patch-tag_summary-ts_id.sql b/www/wiki/maintenance/archives/patch-tag_summary-ts_id.sql new file mode 100644 index 00000000..66fa72e1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-tag_summary-ts_id.sql @@ -0,0 +1,5 @@ +-- Primary key in tag_summary table + +ALTER TABLE /*$wgDBprefix*/tag_summary + ADD COLUMN ts_id INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (ts_id); diff --git a/www/wiki/maintenance/archives/patch-tag_summary-ts_log_id-unsigned.sql b/www/wiki/maintenance/archives/patch-tag_summary-ts_log_id-unsigned.sql new file mode 100644 index 00000000..617073db --- /dev/null +++ b/www/wiki/maintenance/archives/patch-tag_summary-ts_log_id-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/tag_summary MODIFY ts_log_id int unsigned NULL; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-tag_summary-ts_rev_id-unsigned.sql b/www/wiki/maintenance/archives/patch-tag_summary-ts_rev_id-unsigned.sql new file mode 100644 index 00000000..e6a5bcde --- /dev/null +++ b/www/wiki/maintenance/archives/patch-tag_summary-ts_rev_id-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/tag_summary MODIFY ts_rev_id int unsigned NULL; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-tag_summary.sql b/www/wiki/maintenance/archives/patch-tag_summary.sql new file mode 100644 index 00000000..a81b3680 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-tag_summary.sql @@ -0,0 +1,12 @@ +-- Rollup table to pull a LIST of tags simply without ugly GROUP_CONCAT that only works on MySQL 4.1+ +-- Andrew Garrett, 2009-01 +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags BLOB NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); diff --git a/www/wiki/maintenance/archives/patch-tc-timestamp.sql b/www/wiki/maintenance/archives/patch-tc-timestamp.sql new file mode 100644 index 00000000..3f7dde41 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-tc-timestamp.sql @@ -0,0 +1,4 @@ +ALTER TABLE /*_*/transcache MODIFY tc_time binary(14); +UPDATE /*_*/transcache SET tc_time = DATE_FORMAT(FROM_UNIXTIME(tc_time), "%Y%c%d%H%i%s"); + +INSERT INTO /*_*/updatelog(ul_key) VALUES ('convert transcache field'); diff --git a/www/wiki/maintenance/archives/patch-templatelinks-fix-pk.sql b/www/wiki/maintenance/archives/patch-templatelinks-fix-pk.sql new file mode 100644 index 00000000..8aca5105 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-templatelinks-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/templatelinks DROP INDEX /*i*/tl_from, ADD PRIMARY KEY (tl_from,tl_namespace,tl_title); diff --git a/www/wiki/maintenance/archives/patch-templatelinks.sql b/www/wiki/maintenance/archives/patch-templatelinks.sql new file mode 100644 index 00000000..086b6a1b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-templatelinks.sql @@ -0,0 +1,18 @@ +-- +-- Track template inclusions. +-- +CREATE TABLE /*$wgDBprefix*/templatelinks ( + -- Key to the page_id of the page containing the link. + tl_from int unsigned NOT NULL default '0', + + -- Key to page_namespace/page_title of the target page. + -- The target page may or may not exist, and due to renames + -- and deletions may refer to different page records as time + -- goes by. + tl_namespace int NOT NULL default '0', + tl_title varchar(255) binary NOT NULL default '', + + UNIQUE KEY tl_from(tl_from,tl_namespace,tl_title), + KEY (tl_namespace,tl_title) +) /*$wgDBTableOptions*/; + diff --git a/www/wiki/maintenance/archives/patch-testrun.sql b/www/wiki/maintenance/archives/patch-testrun.sql new file mode 100644 index 00000000..6699b554 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-testrun.sql @@ -0,0 +1,35 @@ +-- +-- Optional tables for parserTests recording mode +-- With --record option, success data will be saved to these tables, +-- and comparisons of what's changed from the previous run will be +-- displayed at the end of each run. +-- +-- These tables currently require MySQL 5 (or maybe 4.1?) for subselects. +-- + +drop table if exists /*$wgDBprefix*/testitem; +drop table if exists /*$wgDBprefix*/testrun; + +create table /*$wgDBprefix*/testrun ( + tr_id int not null auto_increment, + + tr_date char(14) binary, + tr_mw_version blob, + tr_php_version blob, + tr_db_version blob, + tr_uname blob, + + primary key (tr_id) +) engine=InnoDB; + +create table /*$wgDBprefix*/testitem ( + ti_run int not null, + ti_name varchar(255), + ti_success bool, + + unique key (ti_run, ti_name), + key (ti_run, ti_success), + + foreign key (ti_run) references /*$wgDBprefix*/testrun(tr_id) + on delete cascade +) engine=InnoDB; diff --git a/www/wiki/maintenance/archives/patch-text-fix-pk.sql b/www/wiki/maintenance/archives/patch-text-fix-pk.sql new file mode 100644 index 00000000..b546333b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-text-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/text DROP KEY /*i*/old_id, ADD PRIMARY KEY (old_id); diff --git a/www/wiki/maintenance/archives/patch-tl_from_namespace.sql b/www/wiki/maintenance/archives/patch-tl_from_namespace.sql new file mode 100644 index 00000000..edfb7a52 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-tl_from_namespace.sql @@ -0,0 +1,4 @@ +ALTER TABLE /*_*/templatelinks + ADD COLUMN tl_from_namespace int NOT NULL default 0; + +CREATE INDEX /*i*/tl_backlinks_namespace ON /*_*/templatelinks (tl_from_namespace,tl_namespace,tl_title,tl_from); diff --git a/www/wiki/maintenance/archives/patch-transcache-fix-pk.sql b/www/wiki/maintenance/archives/patch-transcache-fix-pk.sql new file mode 100644 index 00000000..2e8fea1b --- /dev/null +++ b/www/wiki/maintenance/archives/patch-transcache-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/transcache DROP KEY /*i*/tc_url_idx, ADD PRIMARY KEY (tc_url); diff --git a/www/wiki/maintenance/archives/patch-transcache.sql b/www/wiki/maintenance/archives/patch-transcache.sql new file mode 100644 index 00000000..70870efa --- /dev/null +++ b/www/wiki/maintenance/archives/patch-transcache.sql @@ -0,0 +1,7 @@ +CREATE TABLE /*$wgDBprefix*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents TEXT, + tc_time binary(14) NOT NULL, + UNIQUE INDEX tc_url_idx(tc_url) +) /*$wgDBTableOptions*/; + diff --git a/www/wiki/maintenance/archives/patch-ufg_group-length-increase-255.sql b/www/wiki/maintenance/archives/patch-ufg_group-length-increase-255.sql new file mode 100644 index 00000000..4b7f0d38 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ufg_group-length-increase-255.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*_*/user_former_groups + MODIFY COLUMN ufg_group varbinary(255) NOT NULL default ''; diff --git a/www/wiki/maintenance/archives/patch-ug_group-length-increase-255.sql b/www/wiki/maintenance/archives/patch-ug_group-length-increase-255.sql new file mode 100644 index 00000000..79e17ac0 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ug_group-length-increase-255.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*_*/user_groups + MODIFY COLUMN ug_group varbinary(255) NOT NULL default ''; diff --git a/www/wiki/maintenance/archives/patch-ul_value.sql b/www/wiki/maintenance/archives/patch-ul_value.sql new file mode 100644 index 00000000..50f4e9a8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-ul_value.sql @@ -0,0 +1,4 @@ +-- Add the ul_value column to updatelog + +ALTER TABLE /*_*/updatelog + add ul_value blob; diff --git a/www/wiki/maintenance/archives/patch-up_property.sql b/www/wiki/maintenance/archives/patch-up_property.sql new file mode 100644 index 00000000..c516aafd --- /dev/null +++ b/www/wiki/maintenance/archives/patch-up_property.sql @@ -0,0 +1,4 @@ +-- Increase the length of up_property from 32 -> 255 bytes. T21408 + +ALTER TABLE /*_*/user_properties + MODIFY up_property varbinary(255); diff --git a/www/wiki/maintenance/archives/patch-updatelog.sql b/www/wiki/maintenance/archives/patch-updatelog.sql new file mode 100644 index 00000000..168ad082 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-updatelog.sql @@ -0,0 +1,4 @@ +CREATE TABLE /*$wgDBprefix*/updatelog ( + ul_key varchar(255) NOT NULL, + PRIMARY KEY (ul_key) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-uploadstash-us_props.sql b/www/wiki/maintenance/archives/patch-uploadstash-us_props.sql new file mode 100644 index 00000000..d64515a8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-uploadstash-us_props.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/uploadstash + ADD COLUMN us_props blob; diff --git a/www/wiki/maintenance/archives/patch-uploadstash.sql b/www/wiki/maintenance/archives/patch-uploadstash.sql new file mode 100644 index 00000000..c1d93ef3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-uploadstash.sql @@ -0,0 +1,48 @@ +-- +-- Store information about newly uploaded files before they're +-- moved into the actual filestore +-- +CREATE TABLE /*_*/uploadstash ( + us_id int unsigned NOT NULL PRIMARY KEY auto_increment, + + -- the user who uploaded the file. + us_user int unsigned NOT NULL, + + -- file key. this is how applications actually search for the file. + -- this might go away, or become the primary key. + us_key varchar(255) NOT NULL, + + -- the original path + us_orig_path varchar(255) NOT NULL, + + -- the temporary path at which the file is actually stored + us_path varchar(255) NOT NULL, + + -- which type of upload the file came from (sometimes) + us_source_type varchar(50), + + -- the date/time on which the file was added + us_timestamp varbinary(14) not null, + + us_status varchar(50) not null, + + -- file properties from FSFile::getProps(). these may prove unnecessary. + -- + us_size int unsigned NOT NULL, + -- this hash comes from FSFile::getSha1Base36(), and is 31 characters + us_sha1 varchar(31) NOT NULL, + us_mime varchar(255), + -- Media type as defined by the MEDIATYPE_xxx constants, should duplicate definition in the image table + us_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + -- image-specific properties + us_image_width int unsigned, + us_image_height int unsigned, + us_image_bits smallint unsigned +) /*$wgDBTableOptions*/; + +-- sometimes there's a delete for all of a user's stuff. +CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user); +-- pick out files by key, enforce key uniqueness +CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key); +-- the abandoned upload cleanup script needs this +CREATE INDEX /*i*/us_timestamp ON /*_*/uploadstash (us_timestamp); diff --git a/www/wiki/maintenance/archives/patch-uploadstash_chunk.sql b/www/wiki/maintenance/archives/patch-uploadstash_chunk.sql new file mode 100644 index 00000000..29e41870 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-uploadstash_chunk.sql @@ -0,0 +1,3 @@ +-- Adding us_chunk_inx field +ALTER TABLE /*$wgDBprefix*/uploadstash + ADD us_chunk_inx int unsigned NULL; diff --git a/www/wiki/maintenance/archives/patch-user-newtalk-timestamp-null.sql b/www/wiki/maintenance/archives/patch-user-newtalk-timestamp-null.sql new file mode 100644 index 00000000..7234362d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user-newtalk-timestamp-null.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/user_newtalk MODIFY user_last_timestamp varbinary(14) NULL default NULL; diff --git a/www/wiki/maintenance/archives/patch-user-realname.sql b/www/wiki/maintenance/archives/patch-user-realname.sql new file mode 100644 index 00000000..de7cee75 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user-realname.sql @@ -0,0 +1,5 @@ +-- Add a 'real name' field where users can specify the name they want +-- used for author attribution or other places that real names matter. + +ALTER TABLE user + ADD (user_real_name varchar(255) binary NOT NULL default ''); diff --git a/www/wiki/maintenance/archives/patch-user_editcount.sql b/www/wiki/maintenance/archives/patch-user_editcount.sql new file mode 100644 index 00000000..cdde36dc --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_editcount.sql @@ -0,0 +1,5 @@ +ALTER TABLE /*$wgDBprefix*/user + ADD COLUMN user_editcount int; + +-- Don't initialize values immediately... or should we? +-- They will be lazy-evaluated, or batch-filled via maintenance/initEditCount.php diff --git a/www/wiki/maintenance/archives/patch-user_email_index.sql b/www/wiki/maintenance/archives/patch-user_email_index.sql new file mode 100644 index 00000000..6a3d6208 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_email_index.sql @@ -0,0 +1 @@ +CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50)); diff --git a/www/wiki/maintenance/archives/patch-user_email_token.sql b/www/wiki/maintenance/archives/patch-user_email_token.sql new file mode 100644 index 00000000..f8e66ca4 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_email_token.sql @@ -0,0 +1,12 @@ +-- +-- E-mail confirmation token and expiration timestamp, +-- for verification of e-mail addresses. +-- +-- 2005-04-25 +-- + +ALTER TABLE /*$wgDBprefix*/user + ADD COLUMN user_email_authenticated binary(14), + ADD COLUMN user_email_token binary(32), + ADD COLUMN user_email_token_expires binary(14), + ADD INDEX (user_email_token); diff --git a/www/wiki/maintenance/archives/patch-user_former_groups-fix-pk.sql b/www/wiki/maintenance/archives/patch-user_former_groups-fix-pk.sql new file mode 100644 index 00000000..9a776caf --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_former_groups-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/user_former_groups DROP KEY /*i*/ufg_user_group, ADD PRIMARY KEY (ufg_user,ufg_group); diff --git a/www/wiki/maintenance/archives/patch-user_former_groups.sql b/www/wiki/maintenance/archives/patch-user_former_groups.sql new file mode 100644 index 00000000..b043196d --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_former_groups.sql @@ -0,0 +1,9 @@ +-- Stores the groups the user has once belonged to. +-- The user may still belong these groups. Check user_groups. +CREATE TABLE /*_*/user_former_groups ( + -- Key to user_id + ufg_user int unsigned NOT NULL default 0, + ufg_group varbinary(255) NOT NULL default '' +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/ufg_user_group ON /*_*/user_former_groups (ufg_user,ufg_group); diff --git a/www/wiki/maintenance/archives/patch-user_groups-primary-key.sql b/www/wiki/maintenance/archives/patch-user_groups-primary-key.sql new file mode 100644 index 00000000..e3c87356 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_groups-primary-key.sql @@ -0,0 +1,5 @@ +-- Convert unique index into a primary key on user_groups + +ALTER TABLE /*$wgDBprefix*/user_groups + DROP INDEX ug_user_group, + ADD PRIMARY KEY (ug_user, ug_group); diff --git a/www/wiki/maintenance/archives/patch-user_groups-ug_expiry.sql b/www/wiki/maintenance/archives/patch-user_groups-ug_expiry.sql new file mode 100644 index 00000000..b329f948 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_groups-ug_expiry.sql @@ -0,0 +1,5 @@ +-- Add expiry column in user_groups table + +ALTER TABLE /*$wgDBprefix*/user_groups + ADD COLUMN ug_expiry varbinary(14) NULL default NULL, + ADD INDEX ug_expiry (ug_expiry); diff --git a/www/wiki/maintenance/archives/patch-user_groups.sql b/www/wiki/maintenance/archives/patch-user_groups.sql new file mode 100644 index 00000000..1683cf2a --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_groups.sql @@ -0,0 +1,25 @@ +-- +-- User permissions have been broken out to a separate table; +-- this allows sites with a shared user table to have different +-- permissions assigned to a user in each project. +-- +-- This table replaces the old user_rights field which used a +-- comma-separated blob. +-- +CREATE TABLE /*$wgDBprefix*/user_groups ( + -- Key to user_id + ug_user int unsigned NOT NULL default '0', + + -- Group names are short symbolic string keys. + -- The set of group names is open-ended, though in practice + -- only some predefined ones are likely to be used. + -- + -- At runtime $wgGroupPermissions will associate group keys + -- with particular permissions. A user will have the combined + -- permissions of any group they're explicitly in, plus + -- the implicit '*' and 'user' groups. + ug_group varbinary(16) NOT NULL default '', + + PRIMARY KEY (ug_user,ug_group), + KEY (ug_group) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-user_last_timestamp.sql b/www/wiki/maintenance/archives/patch-user_last_timestamp.sql new file mode 100644 index 00000000..e5f85bf1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_last_timestamp.sql @@ -0,0 +1,3 @@ +-- For getting diff since last view +ALTER TABLE /*$wgDBprefix*/user_newtalk + ADD user_last_timestamp varbinary(14) NULL default NULL; diff --git a/www/wiki/maintenance/archives/patch-user_nameindex.sql b/www/wiki/maintenance/archives/patch-user_nameindex.sql new file mode 100644 index 00000000..9bf0aab1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_nameindex.sql @@ -0,0 +1,13 @@ +-- +-- Change the index on user_name to a unique index to prevent +-- duplicate registrations from creeping in. +-- +-- Run maintenance/userDupes.php or through the updater first +-- to clean up any prior duplicate accounts. +-- +-- Added 2005-06-05 +-- + + ALTER TABLE /*$wgDBprefix*/user + DROP INDEX user_name, +ADD UNIQUE INDEX user_name(user_name); diff --git a/www/wiki/maintenance/archives/patch-user_newpass_time.sql b/www/wiki/maintenance/archives/patch-user_newpass_time.sql new file mode 100644 index 00000000..c323f238 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_newpass_time.sql @@ -0,0 +1,4 @@ +-- Timestamp of the last time when a new password was +-- sent, for throttling purposes +ALTER TABLE /*$wgDBprefix*/user ADD user_newpass_time binary(14); + diff --git a/www/wiki/maintenance/archives/patch-user_newtalk-user_id-unsigned.sql b/www/wiki/maintenance/archives/patch-user_newtalk-user_id-unsigned.sql new file mode 100644 index 00000000..a83e03b9 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_newtalk-user_id-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/user_newtalk MODIFY user_id int unsigned NOT NULL default 0; diff --git a/www/wiki/maintenance/archives/patch-user_password_expire.sql b/www/wiki/maintenance/archives/patch-user_password_expire.sql new file mode 100644 index 00000000..3e716d33 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_password_expire.sql @@ -0,0 +1,3 @@ +-- For setting a password expiration date for users +ALTER TABLE /*$wgDBprefix*/user + ADD COLUMN user_password_expires varbinary(14) DEFAULT NULL; diff --git a/www/wiki/maintenance/archives/patch-user_properties-fix-pk.sql b/www/wiki/maintenance/archives/patch-user_properties-fix-pk.sql new file mode 100644 index 00000000..5d51b785 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_properties-fix-pk.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/user_properties DROP KEY /*i*/user_properties_user_property, ADD PRIMARY KEY (up_user,up_property); diff --git a/www/wiki/maintenance/archives/patch-user_properties-up_user-unsigned.sql b/www/wiki/maintenance/archives/patch-user_properties-up_user-unsigned.sql new file mode 100644 index 00000000..f4f563f8 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_properties-up_user-unsigned.sql @@ -0,0 +1 @@ +ALTER TABLE /*_*/user_properties MODIFY up_user int unsigned NOT NULL; \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-user_properties.sql b/www/wiki/maintenance/archives/patch-user_properties.sql new file mode 100644 index 00000000..85b00616 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_properties.sql @@ -0,0 +1,22 @@ +-- +-- User preferences and perhaps other fun stuff. :) +-- Replaces the old user.user_options blob, with a couple nice properties: +-- +-- 1) We only store non-default settings, so changes to the defauls +-- are now reflected for everybody, not just new accounts. +-- 2) We can more easily do bulk lookups, statistics, or modifications of +-- saved options since it's a sane table structure. +-- +CREATE TABLE /*_*/user_properties( + -- Foreign key to user.user_id + up_user int not null, + + -- Name of the option being saved. This is indexed for bulk lookup. + up_property varbinary(32) not null, + + -- Property value as a string. + up_value blob +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/user_properties_user_property on /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property on /*_*/user_properties (up_property); diff --git a/www/wiki/maintenance/archives/patch-user_registration.sql b/www/wiki/maintenance/archives/patch-user_registration.sql new file mode 100644 index 00000000..906a6954 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_registration.sql @@ -0,0 +1,9 @@ +-- +-- New user field for tracking registration time +-- 2005-12-21 +-- + +ALTER TABLE /*$wgDBprefix*/user + -- Timestamp of account registration. + -- Accounts predating this schema addition may contain NULL. + ADD user_registration binary(14); diff --git a/www/wiki/maintenance/archives/patch-user_rights.sql b/www/wiki/maintenance/archives/patch-user_rights.sql new file mode 100644 index 00000000..a39ac0af --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_rights.sql @@ -0,0 +1,21 @@ +-- Split user table into two parts: +-- user +-- user_rights +-- The latter contains only the permissions of the user. This way, +-- you can store the accounts for several wikis in one central +-- database but keep user rights local to the wiki. + +CREATE TABLE /*$wgDBprefix*/user_rights ( + -- Key to user_id + ur_user int unsigned NOT NULL, + + -- Comma-separated list of permission keys + ur_rights tinyblob NOT NULL, + + UNIQUE KEY ur_user (ur_user) + +) /*$wgDBTableOptions*/; + +INSERT INTO /*$wgDBprefix*/user_rights SELECT user_id,user_rights FROM /*$wgDBprefix*/user; + +ALTER TABLE /*$wgDBprefix*/user DROP COLUMN user_rights; diff --git a/www/wiki/maintenance/archives/patch-user_token.sql b/www/wiki/maintenance/archives/patch-user_token.sql new file mode 100644 index 00000000..a3eb0bfd --- /dev/null +++ b/www/wiki/maintenance/archives/patch-user_token.sql @@ -0,0 +1,15 @@ +-- user_token patch +-- 2004-09-23 + +ALTER TABLE /*$wgDBprefix*/user ADD user_token binary(32) NOT NULL default ''; + +UPDATE /*$wgDBprefix*/user SET user_token = concat( + substring(rand(),3,4), + substring(rand(),3,4), + substring(rand(),3,4), + substring(rand(),3,4), + substring(rand(),3,4), + substring(rand(),3,4), + substring(rand(),3,4), + substring(rand(),3,4) +); diff --git a/www/wiki/maintenance/archives/patch-userindex.sql b/www/wiki/maintenance/archives/patch-userindex.sql new file mode 100644 index 00000000..c039b2f3 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-userindex.sql @@ -0,0 +1 @@ + ALTER TABLE /*$wgDBprefix*/user ADD INDEX ( `user_name` ); \ No newline at end of file diff --git a/www/wiki/maintenance/archives/patch-userlevels.sql b/www/wiki/maintenance/archives/patch-userlevels.sql new file mode 100644 index 00000000..399d6cb2 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-userlevels.sql @@ -0,0 +1,8 @@ + +-- Relation table between user and groups +CREATE TABLE /*$wgDBprefix*/user_groups ( + ug_user int unsigned NOT NULL default '0', + ug_group varbinary(16) NOT NULL default '0', + PRIMARY KEY (ug_user,ug_group) + KEY (ug_group) +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-usernewtalk.sql b/www/wiki/maintenance/archives/patch-usernewtalk.sql new file mode 100644 index 00000000..34fae946 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-usernewtalk.sql @@ -0,0 +1,20 @@ +--- This table stores all the IDs of users whose talk +--- page has been changed (the respective row is deleted +--- when the user looks at the page). +--- The respective column in the user table is no longer +--- required and therefore dropped. + +CREATE TABLE /*$wgDBprefix*/user_newtalk ( + user_id int NOT NULL default '0', + user_ip varbinary(40) NOT NULL default '', + KEY user_id (user_id), + KEY user_ip (user_ip) +) /*$wgDBTableOptions*/; + +INSERT INTO + /*$wgDBprefix*/user_newtalk (user_id, user_ip) + SELECT user_id, '' + FROM user + WHERE user_newtalk != 0; + +ALTER TABLE /*$wgDBprefix*/user DROP COLUMN user_newtalk; diff --git a/www/wiki/maintenance/archives/patch-valid_tag.sql b/www/wiki/maintenance/archives/patch-valid_tag.sql new file mode 100644 index 00000000..994a5d53 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-valid_tag.sql @@ -0,0 +1,4 @@ +-- Andrew Garrett, 2009-01 +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; diff --git a/www/wiki/maintenance/archives/patch-watchlist-null.sql b/www/wiki/maintenance/archives/patch-watchlist-null.sql new file mode 100644 index 00000000..d4869a02 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-watchlist-null.sql @@ -0,0 +1,9 @@ +-- Set up wl_notificationtimestamp with NULL support. +-- 2005-08-17 + +ALTER TABLE /*$wgDBprefix*/watchlist + CHANGE wl_notificationtimestamp wl_notificationtimestamp varbinary(14); + +UPDATE /*$wgDBprefix*/watchlist + SET wl_notificationtimestamp=NULL + WHERE wl_notificationtimestamp='0'; diff --git a/www/wiki/maintenance/archives/patch-watchlist-user-notificationtimestamp-index.sql b/www/wiki/maintenance/archives/patch-watchlist-user-notificationtimestamp-index.sql new file mode 100644 index 00000000..22ae44f1 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-watchlist-user-notificationtimestamp-index.sql @@ -0,0 +1,4 @@ +-- +-- Creates the wl_user_notificationtimestamp index for the watchlist table +-- +CREATE INDEX /*i*/wl_user_notificationtimestamp ON /*_*/watchlist (wl_user, wl_notificationtimestamp); diff --git a/www/wiki/maintenance/archives/patch-watchlist-wl_id.sql b/www/wiki/maintenance/archives/patch-watchlist-wl_id.sql new file mode 100644 index 00000000..a73e514c --- /dev/null +++ b/www/wiki/maintenance/archives/patch-watchlist-wl_id.sql @@ -0,0 +1,5 @@ +-- Primary key in watchlist + +ALTER TABLE /*$wgDBprefix*/watchlist + ADD COLUMN wl_id int unsigned NOT NULL AUTO_INCREMENT FIRST, + ADD PRIMARY KEY (wl_id); diff --git a/www/wiki/maintenance/archives/patch-watchlist.sql b/www/wiki/maintenance/archives/patch-watchlist.sql new file mode 100644 index 00000000..83826b72 --- /dev/null +++ b/www/wiki/maintenance/archives/patch-watchlist.sql @@ -0,0 +1,30 @@ +-- Convert watchlists to new new format ;) + +-- Ids just aren't convenient when what we want is to +-- treat article and talk pages as equivalent. +-- Better to use namespace (drop the 1 bit!) and title + +-- 2002-12-17 by Brion Vibber +-- affects, affected by changes to SpecialWatchlist.php, User.php, +-- Article.php, Title.php, SpecialRecentchanges.php + +DROP TABLE IF EXISTS watchlist2; +CREATE TABLE watchlist2 ( + wl_user int unsigned NOT NULL, + wl_namespace int unsigned NOT NULL default '0', + wl_title varchar(255) binary NOT NULL default '', + UNIQUE KEY (wl_user, wl_namespace, wl_title) +) /*$wgDBTableOptions*/; + +INSERT INTO watchlist2 (wl_user,wl_namespace,wl_title) + SELECT DISTINCT wl_user,(cur_namespace | 1) - 1,cur_title + FROM watchlist,cur WHERE wl_page=cur_id; + +ALTER TABLE watchlist RENAME TO oldwatchlist; +ALTER TABLE watchlist2 RENAME TO watchlist; + +-- Check that the new one is correct, then: +-- DROP TABLE oldwatchlist; + +-- Also should probably drop the ancient and now unused: +ALTER TABLE user DROP COLUMN user_watch; diff --git a/www/wiki/maintenance/archives/upgradeLogging.php b/www/wiki/maintenance/archives/upgradeLogging.php new file mode 100644 index 00000000..bcf70230 --- /dev/null +++ b/www/wiki/maintenance/archives/upgradeLogging.php @@ -0,0 +1,219 @@ +dbw = $this->getDB( DB_MASTER ); + $logging = $this->dbw->tableName( 'logging' ); + $logging_1_10 = $this->dbw->tableName( 'logging_1_10' ); + $logging_pre_1_10 = $this->dbw->tableName( 'logging_pre_1_10' ); + + if ( $this->dbw->tableExists( 'logging_pre_1_10' ) && !$this->dbw->tableExists( 'logging' ) ) { + # Fix previous aborted run + echo "Cleaning up from previous aborted run\n"; + $this->dbw->query( "RENAME TABLE $logging_pre_1_10 TO $logging", __METHOD__ ); + } + + if ( $this->dbw->tableExists( 'logging_pre_1_10' ) ) { + echo "This script has already been run to completion\n"; + + return; + } + + # Create the target table + if ( !$this->dbw->tableExists( 'logging_1_10' ) ) { + global $wgDBTableOptions; + + $sql = <<dbw->query( $sql, __METHOD__ ); + } + + # Synchronise the tables + echo "Doing initial sync...\n"; + $this->sync( 'logging', 'logging_1_10' ); + echo "Sync done\n\n"; + + # Rename the old table away + echo "Renaming the old table to $logging_pre_1_10\n"; + $this->dbw->query( "RENAME TABLE $logging TO $logging_pre_1_10", __METHOD__ ); + + # Copy remaining old rows + # Done before the new table is active so that $copyPos is accurate + echo "Doing final sync...\n"; + $this->sync( 'logging_pre_1_10', 'logging_1_10' ); + + # Move the new table in + echo "Moving the new table in...\n"; + $this->dbw->query( "RENAME TABLE $logging_1_10 TO $logging", __METHOD__ ); + echo "Finished.\n"; + } + + /** + * Copy all rows from $srcTable to $dstTable + * @param string $srcTable + * @param string $dstTable + */ + function sync( $srcTable, $dstTable ) { + $batchSize = 1000; + $minTs = $this->dbw->selectField( $srcTable, 'MIN(log_timestamp)', '', __METHOD__ ); + $minTsUnix = wfTimestamp( TS_UNIX, $minTs ); + $numRowsCopied = 0; + + while ( true ) { + $maxTs = $this->dbw->selectField( $srcTable, 'MAX(log_timestamp)', '', __METHOD__ ); + $copyPos = $this->dbw->selectField( $dstTable, 'MAX(log_timestamp)', '', __METHOD__ ); + $maxTsUnix = wfTimestamp( TS_UNIX, $maxTs ); + $copyPosUnix = wfTimestamp( TS_UNIX, $copyPos ); + + if ( $copyPos === null ) { + $percent = 0; + } else { + $percent = ( $copyPosUnix - $minTsUnix ) / ( $maxTsUnix - $minTsUnix ) * 100; + } + printf( "%s %.2f%%\n", $copyPos, $percent ); + + # Handle all entries with timestamp equal to $copyPos + if ( $copyPos !== null ) { + $numRowsCopied += $this->copyExactMatch( $srcTable, $dstTable, $copyPos ); + } + + # Now copy a batch of rows + if ( $copyPos === null ) { + $conds = false; + } else { + $conds = [ 'log_timestamp > ' . $this->dbw->addQuotes( $copyPos ) ]; + } + $srcRes = $this->dbw->select( $srcTable, '*', $conds, __METHOD__, + [ 'LIMIT' => $batchSize, 'ORDER BY' => 'log_timestamp' ] ); + + if ( !$srcRes->numRows() ) { + # All done + break; + } + + $batch = []; + foreach ( $srcRes as $srcRow ) { + $batch[] = (array)$srcRow; + } + $this->dbw->insert( $dstTable, $batch, __METHOD__ ); + $numRowsCopied += count( $batch ); + + wfWaitForSlaves(); + } + echo "Copied $numRowsCopied rows\n"; + } + + function copyExactMatch( $srcTable, $dstTable, $copyPos ) { + $numRowsCopied = 0; + $srcRes = $this->dbw->select( $srcTable, '*', [ 'log_timestamp' => $copyPos ], __METHOD__ ); + $dstRes = $this->dbw->select( $dstTable, '*', [ 'log_timestamp' => $copyPos ], __METHOD__ ); + + if ( $srcRes->numRows() ) { + $srcRow = $srcRes->fetchObject(); + $srcFields = array_keys( (array)$srcRow ); + $srcRes->seek( 0 ); + $dstRowsSeen = []; + + # Make a hashtable of rows that already exist in the destination + foreach ( $dstRes as $dstRow ) { + $reducedDstRow = []; + foreach ( $srcFields as $field ) { + $reducedDstRow[$field] = $dstRow->$field; + } + $hash = md5( serialize( $reducedDstRow ) ); + $dstRowsSeen[$hash] = true; + } + + # Copy all the source rows that aren't already in the destination + foreach ( $srcRes as $srcRow ) { + $hash = md5( serialize( (array)$srcRow ) ); + if ( !isset( $dstRowsSeen[$hash] ) ) { + $this->dbw->insert( $dstTable, (array)$srcRow, __METHOD__ ); + $numRowsCopied++; + } + } + } + + return $numRowsCopied; + } +} + +$ul = new UpdateLogging; +$ul->execute(); diff --git a/www/wiki/maintenance/attachLatest.php b/www/wiki/maintenance/attachLatest.php new file mode 100644 index 00000000..897972c7 --- /dev/null +++ b/www/wiki/maintenance/attachLatest.php @@ -0,0 +1,92 @@ + + * 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 + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to correct wrong values in the `page_latest` field + * in the database. + * + * @ingroup Maintenance + */ +class AttachLatest extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addOption( "fix", "Actually fix the entries, will dry run otherwise" ); + $this->addOption( "regenerate-all", + "Regenerate the page_latest field for all records in table page" ); + $this->addDescription( 'Fix page_latest entries in the page table' ); + } + + public function execute() { + $this->output( "Looking for pages with page_latest set to 0...\n" ); + $dbw = $this->getDB( DB_MASTER ); + $conds = [ 'page_latest' => 0 ]; + if ( $this->hasOption( 'regenerate-all' ) ) { + $conds = ''; + } + $result = $dbw->select( 'page', + [ 'page_id', 'page_namespace', 'page_title' ], + $conds, + __METHOD__ ); + + $n = 0; + foreach ( $result as $row ) { + $pageId = intval( $row->page_id ); + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $name = $title->getPrefixedText(); + $latestTime = $dbw->selectField( 'revision', + 'MAX(rev_timestamp)', + [ 'rev_page' => $pageId ], + __METHOD__ ); + if ( !$latestTime ) { + $this->output( wfWikiID() . " $pageId [[$name]] can't find latest rev time?!\n" ); + continue; + } + + $revision = Revision::loadFromTimestamp( $dbw, $title, $latestTime ); + if ( is_null( $revision ) ) { + $this->output( wfWikiID() + . " $pageId [[$name]] latest time $latestTime, can't find revision id\n" ); + continue; + } + $id = $revision->getId(); + $this->output( wfWikiID() . " $pageId [[$name]] latest time $latestTime, rev id $id\n" ); + if ( $this->hasOption( 'fix' ) ) { + $page = WikiPage::factory( $title ); + $page->updateRevisionOn( $dbw, $revision ); + } + $n++; + } + $this->output( "Done! Processed $n pages.\n" ); + if ( !$this->hasOption( 'fix' ) ) { + $this->output( "This was a dry run; rerun with --fix to update page_latest.\n" ); + } + } +} + +$maintClass = AttachLatest::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/backup.inc b/www/wiki/maintenance/backup.inc new file mode 100644 index 00000000..0fdd417f --- /dev/null +++ b/www/wiki/maintenance/backup.inc @@ -0,0 +1,423 @@ + + * 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 + * @ingroup Dump Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\IDatabase; + +/** + * @ingroup Dump Maintenance + */ +class BackupDumper extends Maintenance { + public $reporting = true; + public $pages = null; // all pages + public $skipHeader = false; // don't output and + public $skipFooter = false; // don't output + public $startId = 0; + public $endId = 0; + public $revStartId = 0; + public $revEndId = 0; + public $dumpUploads = false; + public $dumpUploadFileContents = false; + public $orderRevs = false; + + protected $reportingInterval = 100; + protected $pageCount = 0; + protected $revCount = 0; + protected $server = null; // use default + protected $sink = null; // Output filters + protected $lastTime = 0; + protected $pageCountLast = 0; + protected $revCountLast = 0; + + protected $outputTypes = []; + protected $filterTypes = []; + + protected $ID = 0; + + /** + * The dependency-injected database to use. + * + * @var IDatabase|null + * + * @see self::setDB + */ + protected $forcedDb = null; + + /** @var LoadBalancer */ + protected $lb; + + // @todo Unused? + private $stubText = false; // include rev_text_id instead of text; for 2-pass dump + + /** + * @param array $args For backward compatibility + */ + function __construct( $args = null ) { + parent::__construct(); + $this->stderr = fopen( "php://stderr", "wt" ); + + // Built-in output and filter plugins + $this->registerOutput( 'file', DumpFileOutput::class ); + $this->registerOutput( 'gzip', DumpGZipOutput::class ); + $this->registerOutput( 'bzip2', DumpBZip2Output::class ); + $this->registerOutput( 'dbzip2', DumpDBZip2Output::class ); + $this->registerOutput( '7zip', Dump7ZipOutput::class ); + + $this->registerFilter( 'latest', DumpLatestFilter::class ); + $this->registerFilter( 'notalk', DumpNotalkFilter::class ); + $this->registerFilter( 'namespace', DumpNamespaceFilter::class ); + + // These three can be specified multiple times + $this->addOption( 'plugin', 'Load a dump plugin class. Specify as [:].', + false, true, false, true ); + $this->addOption( 'output', 'Begin a filtered output stream; Specify as :. ' . + 's: file, gzip, bzip2, 7zip, dbzip2', false, true, false, true ); + $this->addOption( 'filter', 'Add a filter on an output branch. Specify as ' . + '[:]. s: latest, notalk, namespace', false, true, false, true ); + $this->addOption( 'report', 'Report position and speed after every n pages processed. ' . + 'Default: 100.', false, true ); + $this->addOption( 'server', 'Force reading from MySQL server', false, true ); + $this->addOption( '7ziplevel', '7zip compression level for all 7zip outputs. Used for ' . + '-mx option to 7za command.', false, true ); + + if ( $args ) { + // Args should be loaded and processed so that dump() can be called directly + // instead of execute() + $this->loadWithArgv( $args ); + $this->processOptions(); + } + } + + /** + * @param string $name + * @param string $class Name of output filter plugin class + */ + function registerOutput( $name, $class ) { + $this->outputTypes[$name] = $class; + } + + /** + * @param string $name + * @param string $class Name of filter plugin class + */ + function registerFilter( $name, $class ) { + $this->filterTypes[$name] = $class; + } + + /** + * Load a plugin and register it + * + * @param string $class Name of plugin class; must have a static 'register' + * method that takes a BackupDumper as a parameter. + * @param string $file Full or relative path to the PHP file to load, or empty + */ + function loadPlugin( $class, $file ) { + if ( $file != '' ) { + require_once $file; + } + $register = [ $class, 'register' ]; + call_user_func_array( $register, [ $this ] ); + } + + function execute() { + throw new MWException( 'execute() must be overridden in subclasses' ); + } + + /** + * Processes arguments and sets $this->$sink accordingly + */ + function processOptions() { + $sink = null; + $sinks = []; + + $options = $this->orderedOptions; + foreach ( $options as $arg ) { + $opt = $arg[0]; + $param = $arg[1]; + + switch ( $opt ) { + case 'plugin': + $val = explode( ':', $param ); + + if ( count( $val ) === 1 ) { + $this->loadPlugin( $val[0], '' ); + } elseif ( count( $val ) === 2 ) { + $this->loadPlugin( $val[0], $val[1] ); + } else { + $this->fatalError( 'Invalid plugin parameter' ); + return; + } + + break; + case 'output': + $split = explode( ':', $param, 2 ); + if ( count( $split ) !== 2 ) { + $this->fatalError( 'Invalid output parameter' ); + } + list( $type, $file ) = $split; + if ( !is_null( $sink ) ) { + $sinks[] = $sink; + } + if ( !isset( $this->outputTypes[$type] ) ) { + $this->fatalError( "Unrecognized output sink type '$type'" ); + } + $class = $this->outputTypes[$type]; + if ( $type === "7zip" ) { + $sink = new $class( $file, intval( $this->getOption( '7ziplevel' ) ) ); + } else { + $sink = new $class( $file ); + } + + break; + case 'filter': + if ( is_null( $sink ) ) { + $sink = new DumpOutput(); + } + + $split = explode( ':', $param ); + $key = $split[0]; + + if ( !isset( $this->filterTypes[$key] ) ) { + $this->fatalError( "Unrecognized filter type '$key'" ); + } + + $type = $this->filterTypes[$key]; + + if ( count( $split ) === 1 ) { + $filter = new $type( $sink ); + } elseif ( count( $split ) === 2 ) { + $filter = new $type( $sink, $split[1] ); + } else { + $this->fatalError( 'Invalid filter parameter' ); + } + + // references are lame in php... + unset( $sink ); + $sink = $filter; + + break; + } + } + + if ( $this->hasOption( 'report' ) ) { + $this->reportingInterval = intval( $this->getOption( 'report' ) ); + } + + if ( $this->hasOption( 'server' ) ) { + $this->server = $this->getOption( 'server' ); + } + + if ( is_null( $sink ) ) { + $sink = new DumpOutput(); + } + $sinks[] = $sink; + + if ( count( $sinks ) > 1 ) { + $this->sink = new DumpMultiWriter( $sinks ); + } else { + $this->sink = $sink; + } + } + + function dump( $history, $text = WikiExporter::TEXT ) { + # Notice messages will foul up your XML output even if they're + # relatively harmless. + if ( ini_get( 'display_errors' ) ) { + ini_set( 'display_errors', 'stderr' ); + } + + $this->initProgress( $history ); + + $db = $this->backupDb(); + $exporter = new WikiExporter( $db, $history, WikiExporter::STREAM, $text ); + $exporter->dumpUploads = $this->dumpUploads; + $exporter->dumpUploadFileContents = $this->dumpUploadFileContents; + + $wrapper = new ExportProgressFilter( $this->sink, $this ); + $exporter->setOutputSink( $wrapper ); + + if ( !$this->skipHeader ) { + $exporter->openStream(); + } + # Log item dumps: all or by range + if ( $history & WikiExporter::LOGS ) { + if ( $this->startId || $this->endId ) { + $exporter->logsByRange( $this->startId, $this->endId ); + } else { + $exporter->allLogs(); + } + } elseif ( is_null( $this->pages ) ) { + # Page dumps: all or by page ID range + if ( $this->startId || $this->endId ) { + $exporter->pagesByRange( $this->startId, $this->endId, $this->orderRevs ); + } elseif ( $this->revStartId || $this->revEndId ) { + $exporter->revsByRange( $this->revStartId, $this->revEndId ); + } else { + $exporter->allPages(); + } + } else { + # Dump of specific pages + $exporter->pagesByName( $this->pages ); + } + + if ( !$this->skipFooter ) { + $exporter->closeStream(); + } + + $this->report( true ); + } + + /** + * Initialise starting time and maximum revision count. + * We'll make ETA calculations based an progress, assuming relatively + * constant per-revision rate. + * @param int $history WikiExporter::CURRENT or WikiExporter::FULL + */ + function initProgress( $history = WikiExporter::FULL ) { + $table = ( $history == WikiExporter::CURRENT ) ? 'page' : 'revision'; + $field = ( $history == WikiExporter::CURRENT ) ? 'page_id' : 'rev_id'; + + $dbr = $this->forcedDb; + if ( $this->forcedDb === null ) { + $dbr = wfGetDB( DB_REPLICA ); + } + $this->maxCount = $dbr->selectField( $table, "MAX($field)", '', __METHOD__ ); + $this->startTime = microtime( true ); + $this->lastTime = $this->startTime; + $this->ID = getmypid(); + } + + /** + * @todo Fixme: the --server parameter is currently not respected, as it + * doesn't seem terribly easy to ask the load balancer for a particular + * connection by name. + * @return IDatabase + */ + function backupDb() { + if ( $this->forcedDb !== null ) { + return $this->forcedDb; + } + + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $this->lb = $lbFactory->newMainLB(); + $db = $this->lb->getConnection( DB_REPLICA, 'dump' ); + + // Discourage the server from disconnecting us if it takes a long time + // to read out the big ol' batch query. + $db->setSessionOptions( [ 'connTimeout' => 3600 * 24 ] ); + + return $db; + } + + /** + * Force the dump to use the provided database connection for database + * operations, wherever possible. + * + * @param IDatabase|null $db (Optional) the database connection to use. If null, resort to + * use the globally provided ways to get database connections. + */ + function setDB( IDatabase $db = null ) { + parent::setDB( $db ); + $this->forcedDb = $db; + } + + function __destruct() { + if ( isset( $this->lb ) ) { + $this->lb->closeAll(); + } + } + + function backupServer() { + global $wgDBserver; + + return $this->server + ? $this->server + : $wgDBserver; + } + + function reportPage() { + $this->pageCount++; + } + + function revCount() { + $this->revCount++; + $this->report(); + } + + function report( $final = false ) { + if ( $final xor ( $this->revCount % $this->reportingInterval == 0 ) ) { + $this->showReport(); + } + } + + function showReport() { + if ( $this->reporting ) { + $now = wfTimestamp( TS_DB ); + $nowts = microtime( true ); + $deltaAll = $nowts - $this->startTime; + $deltaPart = $nowts - $this->lastTime; + $this->pageCountPart = $this->pageCount - $this->pageCountLast; + $this->revCountPart = $this->revCount - $this->revCountLast; + + if ( $deltaAll ) { + $portion = $this->revCount / $this->maxCount; + $eta = $this->startTime + $deltaAll / $portion; + $etats = wfTimestamp( TS_DB, intval( $eta ) ); + $pageRate = $this->pageCount / $deltaAll; + $revRate = $this->revCount / $deltaAll; + } else { + $pageRate = '-'; + $revRate = '-'; + $etats = '-'; + } + if ( $deltaPart ) { + $pageRatePart = $this->pageCountPart / $deltaPart; + $revRatePart = $this->revCountPart / $deltaPart; + } else { + $pageRatePart = '-'; + $revRatePart = '-'; + } + $this->progress( sprintf( + "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), " + . "%d revs (%0.1f|%0.1f/sec all|curr), ETA %s [max %d]", + $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate, + $pageRatePart, $this->revCount, $revRate, $revRatePart, $etats, + $this->maxCount + ) ); + $this->lastTime = $nowts; + $this->revCountLast = $this->revCount; + } + } + + function progress( $string ) { + if ( $this->reporting ) { + fwrite( $this->stderr, $string . "\n" ); + } + } +} diff --git a/www/wiki/maintenance/benchmarks/Benchmarker.php b/www/wiki/maintenance/benchmarks/Benchmarker.php new file mode 100644 index 00000000..e1eef07e --- /dev/null +++ b/www/wiki/maintenance/benchmarks/Benchmarker.php @@ -0,0 +1,165 @@ +addOption( 'count', 'How many times to run a benchmark', false, true ); + $this->addOption( 'verbose', 'Verbose logging of resource usage', false, false, 'v' ); + } + + public function bench( array $benchs ) { + $this->lang = Language::factory( 'en' ); + + $this->startBench(); + $count = $this->getOption( 'count', $this->defaultCount ); + $verbose = $this->hasOption( 'verbose' ); + foreach ( $benchs as $key => $bench ) { + // Shortcut for simple functions + if ( is_callable( $bench ) ) { + $bench = [ 'function' => $bench ]; + } + + // Default to no arguments + if ( !isset( $bench['args'] ) ) { + $bench['args'] = []; + } + + // Optional setup called outside time measure + if ( isset( $bench['setup'] ) ) { + call_user_func( $bench['setup'] ); + } + + // Run benchmarks + $stat = new RunningStat(); + for ( $i = 0; $i < $count; $i++ ) { + $t = microtime( true ); + call_user_func_array( $bench['function'], $bench['args'] ); + $t = ( microtime( true ) - $t ) * 1000; + if ( $verbose ) { + $this->verboseRun( $i ); + } + $stat->addObservation( $t ); + } + + // Name defaults to name of called function + if ( is_string( $key ) ) { + $name = $key; + } else { + if ( is_array( $bench['function'] ) ) { + $name = get_class( $bench['function'][0] ) . '::' . $bench['function'][1]; + } else { + $name = strval( $bench['function'] ); + } + $name = sprintf( "%s(%s)", + $name, + implode( ', ', $bench['args'] ) + ); + } + + $this->addResult( [ + 'name' => $name, + 'count' => $stat->getCount(), + // Get rate per second from mean (in ms) + 'rate' => $stat->getMean() == 0 ? INF : ( 1.0 / ( $stat->getMean() / 1000.0 ) ), + 'total' => $stat->getMean() * $stat->getCount(), + 'mean' => $stat->getMean(), + 'max' => $stat->max, + 'stddev' => $stat->getStdDev(), + 'usage' => [ + 'mem' => memory_get_usage( true ), + 'mempeak' => memory_get_peak_usage( true ), + ], + ] ); + } + } + + public function startBench() { + $this->output( + sprintf( "Running PHP version %s (%s) on %s %s %s\n\n", + phpversion(), + php_uname( 'm' ), + php_uname( 's' ), + php_uname( 'r' ), + php_uname( 'v' ) + ) + ); + } + + public function addResult( $res ) { + $ret = sprintf( "%s\n %' 6s: %d\n", + $res['name'], + 'count', + $res['count'] + ); + $ret .= sprintf( " %' 6s: %8.1f/s\n", + 'rate', + $res['rate'] + ); + foreach ( [ 'total', 'mean', 'max', 'stddev' ] as $metric ) { + $ret .= sprintf( " %' 6s: %8.2fms\n", + $metric, + $res[$metric] + ); + } + + foreach ( [ + 'mem' => 'Current memory usage', + 'mempeak' => 'Peak memory usage' + ] as $key => $label ) { + $ret .= sprintf( "%' 20s: %s\n", + $label, + $this->lang->formatSize( $res['usage'][$key] ) + ); + } + + $this->output( "$ret\n" ); + } + + protected function verboseRun( $iteration ) { + $this->output( sprintf( "#%3d - memory: %-10s - peak: %-10s\n", + $iteration, + $this->lang->formatSize( memory_get_usage( true ) ), + $this->lang->formatSize( memory_get_peak_usage( true ) ) + ) ); + } +} diff --git a/www/wiki/maintenance/benchmarks/README.md b/www/wiki/maintenance/benchmarks/README.md new file mode 100644 index 00000000..b411c526 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/README.md @@ -0,0 +1,15 @@ +This directory hold several benchmarking scripts used track performances of +MediaWiki and/or PHP. + +## Consistency + +On Linux, use of `taskset` and `nice` can help get more consistent results. + +For example: + + $ taskset 1 nice -n-10 php bench_wfIsWindows.php + +## Fixtures + +* australia-untidy.html.gz: Representative input text for benchmarkTidy.php. + It needs to be decompressed before use. diff --git a/www/wiki/maintenance/benchmarks/australia-untidy.html.gz b/www/wiki/maintenance/benchmarks/australia-untidy.html.gz new file mode 100644 index 00000000..148481da Binary files /dev/null and b/www/wiki/maintenance/benchmarks/australia-untidy.html.gz differ diff --git a/www/wiki/maintenance/benchmarks/bench_HTTP_HTTPS.php b/www/wiki/maintenance/benchmarks/bench_HTTP_HTTPS.php new file mode 100644 index 00000000..5e1feb73 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_HTTP_HTTPS.php @@ -0,0 +1,63 @@ +addDescription( 'Benchmark HTTP request vs HTTPS request.' ); + } + + public function execute() { + $this->bench( [ + [ 'function' => [ $this, 'getHTTP' ] ], + [ 'function' => [ $this, 'getHTTPS' ] ], + ] ); + } + + private function doRequest( $proto ) { + Http::get( "$proto://localhost/", [], __METHOD__ ); + } + + // bench function 1 + protected function getHTTP() { + $this->doRequest( 'http' ); + } + + // bench function 2 + protected function getHTTPS() { + $this->doRequest( 'https' ); + } +} + +$maintClass = BenchHttpHttps::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/bench_Wikimedia_base_convert.php b/www/wiki/maintenance/benchmarks/bench_Wikimedia_base_convert.php new file mode 100644 index 00000000..f9b3a74e --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_Wikimedia_base_convert.php @@ -0,0 +1,77 @@ +addDescription( 'Benchmark for Wikimedia\base_convert.' ); + $this->addOption( "inbase", "Input base", false, true ); + $this->addOption( "outbase", "Output base", false, true ); + $this->addOption( "length", "Size in digits to generate for input", false, true ); + } + + public function execute() { + $inbase = $this->getOption( "inbase", 36 ); + $outbase = $this->getOption( "outbase", 16 ); + $length = $this->getOption( "length", 128 ); + $number = self::makeRandomNumber( $inbase, $length ); + + $this->bench( [ + [ + 'function' => 'Wikimedia\base_convert', + 'args' => [ $number, $inbase, $outbase, 0, true, 'php' ] + ], + [ + 'function' => 'Wikimedia\base_convert', + 'args' => [ $number, $inbase, $outbase, 0, true, 'bcmath' ] + ], + [ + 'function' => 'Wikimedia\base_convert', + 'args' => [ $number, $inbase, $outbase, 0, true, 'gmp' ] + ], + ] ); + } + + protected static function makeRandomNumber( $base, $length ) { + $baseChars = '0123456789abcdefghijklmnopqrstuvwxyz'; + $res = ''; + for ( $i = 0; $i < $length; $i++ ) { + $res .= $baseChars[mt_rand( 0, $base - 1 )]; + } + + return $res; + } +} + +$maintClass = BenchWikimediaBaseConvert::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/bench_delete_truncate.php b/www/wiki/maintenance/benchmarks/bench_delete_truncate.php new file mode 100644 index 00000000..794b743e --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_delete_truncate.php @@ -0,0 +1,105 @@ +addDescription( 'Benchmarks SQL DELETE vs SQL TRUNCATE.' ); + } + + public function execute() { + $dbw = $this->getDB( DB_MASTER ); + + $test = $dbw->tableName( 'test' ); + $dbw->query( "CREATE TABLE IF NOT EXISTS /*_*/$test ( + test_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + text varbinary(255) NOT NULL +);" ); + + $this->bench( [ + 'Delete' => [ + 'setup' => function () use ( $dbw ) { + $this->insertData( $dbw ); + }, + 'function' => function () use ( $dbw ) { + $this->delete( $dbw ); + } + ], + 'Truncate' => [ + 'setup' => function () use ( $dbw ) { + $this->insertData( $dbw ); + }, + 'function' => function () use ( $dbw ) { + $this->truncate( $dbw ); + } + ] + ] ); + + $dbw->dropTable( 'test' ); + } + + /** + * @param IDatabase $dbw + * @return void + */ + private function insertData( $dbw ) { + $range = range( 0, 1024 ); + $data = []; + foreach ( $range as $r ) { + $data[] = [ 'text' => $r ]; + } + $dbw->insert( 'test', $data, __METHOD__ ); + } + + /** + * @param IDatabase $dbw + * @return void + */ + private function delete( $dbw ) { + $dbw->delete( 'text', '*', __METHOD__ ); + } + + /** + * @param IMaintainableDatabase $dbw + * @return void + */ + private function truncate( $dbw ) { + $test = $dbw->tableName( 'test' ); + $dbw->query( "TRUNCATE TABLE $test" ); + } +} + +$maintClass = BenchmarkDeleteTruncate::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/bench_if_switch.php b/www/wiki/maintenance/benchmarks/bench_if_switch.php new file mode 100644 index 00000000..5f661f2d --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_if_switch.php @@ -0,0 +1,110 @@ +addDescription( 'Benchmark if elseif... versus switch case.' ); + } + + public function execute() { + $this->bench( [ + [ 'function' => [ $this, 'doElseIf' ] ], + [ 'function' => [ $this, 'doSwitch' ] ], + ] ); + } + + // bench function 1 + protected function doElseIf() { + $a = 'z'; + if ( $a == 'a' ) { + } elseif ( $a == 'b' ) { + } elseif ( $a == 'c' ) { + } elseif ( $a == 'd' ) { + } elseif ( $a == 'e' ) { + } elseif ( $a == 'f' ) { + } elseif ( $a == 'g' ) { + } elseif ( $a == 'h' ) { + } elseif ( $a == 'i' ) { + } elseif ( $a == 'j' ) { + } elseif ( $a == 'k' ) { + } elseif ( $a == 'l' ) { + } elseif ( $a == 'm' ) { + } elseif ( $a == 'n' ) { + } elseif ( $a == 'o' ) { + } elseif ( $a == 'p' ) { + } else { + } + } + + // bench function 2 + protected function doSwitch() { + $a = 'z'; + switch ( $a ) { + case 'b': + break; + case 'c': + break; + case 'd': + break; + case 'e': + break; + case 'f': + break; + case 'g': + break; + case 'h': + break; + case 'i': + break; + case 'j': + break; + case 'k': + break; + case 'l': + break; + case 'm': + break; + case 'n': + break; + case 'o': + break; + case 'p': + break; + default: + } + } +} + +$maintClass = BenchIfSwitch::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/bench_strtr_str_replace.php b/www/wiki/maintenance/benchmarks/bench_strtr_str_replace.php new file mode 100644 index 00000000..2c065f6c --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_strtr_str_replace.php @@ -0,0 +1,74 @@ +addDescription( 'Benchmark for strtr() vs str_replace().' ); + } + + public function execute() { + $this->bench( [ + [ 'function' => [ $this, 'benchstrtr' ] ], + [ 'function' => [ $this, 'benchstr_replace' ] ], + [ 'function' => [ $this, 'benchstrtr_indirect' ] ], + [ 'function' => [ $this, 'benchstr_replace_indirect' ] ], + ] ); + } + + protected function benchstrtr() { + strtr( "[[MediaWiki:Some_random_test_page]]", "_", " " ); + } + + protected function benchstr_replace() { + str_replace( "_", " ", "[[MediaWiki:Some_random_test_page]]" ); + } + + protected function benchstrtr_indirect() { + bfNormalizeTitleStrTr( "[[MediaWiki:Some_random_test_page]]" ); + } + + protected function benchstr_replace_indirect() { + bfNormalizeTitleStrReplace( "[[MediaWiki:Some_random_test_page]]" ); + } +} + +$maintClass = BenchStrtrStrReplace::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/bench_utf8_title_check.php b/www/wiki/maintenance/benchmarks/bench_utf8_title_check.php new file mode 100644 index 00000000..b13b8632 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_utf8_title_check.php @@ -0,0 +1,114 @@ +checkTitleEncoding() + * and compares its execution time against that of mb_check_encoding. + * + * @ingroup Benchmark + */ +class BenchUtf8TitleCheck extends Benchmarker { + private $data; + + private $isutf8; + + public function __construct() { + parent::__construct(); + + // phpcs:disable Generic.Files.LineLength + $this->data = [ + "", + "United States of America", // 7bit ASCII + "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e", + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn", + // This comes from T38839 + "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C" + . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C" + . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C" + . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C" + . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C" + . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C" + . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C" + . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C" + . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C" + . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C" + . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C" + . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C" + . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C" + . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis" + ]; + // phpcs:enable + + $this->addDescription( "Benchmark for using a regexp vs. mb_check_encoding " . + "to check for UTF-8 encoding." ); + } + + public function execute() { + $benchmarks = []; + foreach ( $this->data as $val ) { + $benchmarks[] = [ + 'function' => [ $this, 'use_regexp' ], + 'args' => [ rawurldecode( $val ) ] + ]; + $benchmarks[] = [ + 'function' => [ $this, 'use_regexp_non_capturing' ], + 'args' => [ rawurldecode( $val ) ] + ]; + $benchmarks[] = [ + 'function' => [ $this, 'use_regexp_once_only' ], + 'args' => [ rawurldecode( $val ) ] + ]; + $benchmarks[] = [ + 'function' => [ $this, 'use_mb_check_encoding' ], + 'args' => [ rawurldecode( $val ) ] + ]; + } + $this->bench( $benchmarks ); + } + + protected function use_regexp( $s ) { + $this->isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' . + '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s ); + } + + protected function use_regexp_non_capturing( $s ) { + // Same as above with a non-capturing subgroup. + $this->isutf8 = preg_match( '/^(?:[\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' . + '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s ); + } + + protected function use_regexp_once_only( $s ) { + // Same as above with a once-only subgroup. + $this->isutf8 = preg_match( '/^(?>[\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' . + '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s ); + } + + protected function use_mb_check_encoding( $s ) { + $this->isutf8 = mb_check_encoding( $s, 'UTF-8' ); + } +} + +$maintClass = BenchUtf8TitleCheck::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/bench_wfIsWindows.php b/www/wiki/maintenance/benchmarks/bench_wfIsWindows.php new file mode 100644 index 00000000..6943182b --- /dev/null +++ b/www/wiki/maintenance/benchmarks/bench_wfIsWindows.php @@ -0,0 +1,68 @@ +addDescription( 'Benchmark for wfIsWindows.' ); + } + + public function execute() { + $this->bench( [ + [ 'function' => [ $this, 'wfIsWindows' ] ], + [ 'function' => [ $this, 'wfIsWindowsCached' ] ], + ] ); + } + + protected static function is_win() { + return substr( php_uname(), 0, 7 ) == 'Windows'; + } + + // bench function 1 + protected function wfIsWindows() { + return self::is_win(); + } + + // bench function 2 + protected function wfIsWindowsCached() { + static $isWindows = null; + if ( $isWindows == null ) { + $isWindows = self::is_win(); + } + + return $isWindows; + } +} + +$maintClass = BenchWfIsWindows::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkCSSMin.php b/www/wiki/maintenance/benchmarks/benchmarkCSSMin.php new file mode 100644 index 00000000..a7d998d2 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkCSSMin.php @@ -0,0 +1,76 @@ +addDescription( 'Benchmarks CSSMin.' ); + $this->addOption( 'file', 'Path to CSS file (may be gzipped)', false, true ); + $this->addOption( 'out', 'Echo output of one run to stdout for inspection', false, false ); + } + + public function execute() { + $file = $this->getOption( 'file', __DIR__ . '/cssmin/styles.css' ); + $filename = basename( $file ); + $css = $this->loadFile( $file ); + + if ( $this->hasOption( 'out' ) ) { + echo "## minify\n\n", + CSSMin::minify( $css ), + "\n\n"; + echo "## remap\n\n", + CSSMin::remap( $css, dirname( $file ), 'https://example.org/test/', true ), + "\n"; + return; + } + + $this->bench( [ + "minify ($filename)" => [ + 'function' => [ CSSMin::class, 'minify' ], + 'args' => [ $css ] + ], + "remap ($filename)" => [ + 'function' => [ CSSMin::class, 'remap' ], + 'args' => [ $css, dirname( $file ), 'https://example.org/test/', true ] + ], + ] ); + } + + private function loadFile( $file ) { + $css = file_get_contents( $file ); + // Detect GZIP compression header + if ( substr( $css, 0, 2 ) === "\037\213" ) { + $css = gzdecode( $css ); + } + return $css; + } +} + +$maintClass = BenchmarkCSSMin::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkHooks.php b/www/wiki/maintenance/benchmarks/benchmarkHooks.php new file mode 100644 index 00000000..0bfe0394 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkHooks.php @@ -0,0 +1,73 @@ +addDescription( 'Benchmark MediaWiki Hooks.' ); + } + + public function execute() { + $cases = [ + 'Loaded 0 hooks' => 0, + 'Loaded 1 hook' => 1, + 'Loaded 10 hooks' => 10, + 'Loaded 100 hooks' => 100, + ]; + $benches = []; + foreach ( $cases as $label => $load ) { + $benches[$label] = [ + 'setup' => function () use ( $load ) { + global $wgHooks; + $wgHooks['Test'] = []; + for ( $i = 1; $i <= $load; $i++ ) { + $wgHooks['Test'][] = [ $this, 'test' ]; + } + }, + 'function' => function () { + Hooks::run( 'Test' ); + } + ]; + } + $this->bench( $benches ); + } + + /** + * @return bool + */ + public function test() { + return true; + } +} + +$maintClass = BenchmarkHooks::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkJSMinPlus.php b/www/wiki/maintenance/benchmarks/benchmarkJSMinPlus.php new file mode 100644 index 00000000..3aa7af71 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkJSMinPlus.php @@ -0,0 +1,62 @@ +addDescription( 'Benchmarks JSMinPlus.' ); + $this->addOption( 'file', 'Path to JS file', true, true ); + } + + public function execute() { + Wikimedia\suppressWarnings(); + $content = file_get_contents( $this->getOption( 'file' ) ); + Wikimedia\restoreWarnings(); + if ( $content === false ) { + $this->fatalError( 'Unable to open input file' ); + } + + $filename = basename( $this->getOption( 'file' ) ); + $parser = new JSParser(); + + $this->bench( [ + "JSParser::parse ($filename)" => [ + 'function' => function ( $parser, $content, $filename ) { + $parser->parse( $content, $filename, 1 ); + }, + 'args' => [ $parser, $content, $filename ] + ] + ] ); + } +} + +$maintClass = BenchmarkJSMinPlus::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkLruHash.php b/www/wiki/maintenance/benchmarks/benchmarkLruHash.php new file mode 100644 index 00000000..6b1fcd36 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkLruHash.php @@ -0,0 +1,95 @@ +addDescription( 'Benchmarks HashBagOStuff and MapCacheLRU.' ); + $this->addOption( 'method', 'One of "construct" or "set". Default: [All]', false, true ); + } + + public function execute() { + $exampleKeys = []; + $max = 100; + $count = 500; + while ( $count-- ) { + $exampleKeys[] = wfRandomString(); + } + // 1000 keys (1...500, 500...1) + $keys = array_merge( $exampleKeys, array_reverse( $exampleKeys ) ); + + $method = $this->getOption( 'method' ); + $benches = []; + + if ( !$method || $method === 'construct' ) { + $benches['HashBagOStuff::__construct'] = [ + 'function' => function () use ( $max ) { + $obj = new HashBagOStuff( [ 'maxKeys' => $max ] ); + }, + ]; + $benches['MapCacheLRU::__construct'] = [ + 'function' => function () use ( $max ) { + $obj = new MapCacheLRU( $max ); + }, + ]; + } + + if ( !$method || $method === 'set' ) { + // For the set bechmark, do object creation in setup (not measured) + $hObj = null; + $benches['HashBagOStuff::set'] = [ + 'setup' => function () use ( &$hObj, $max ) { + $hObj = new HashBagOStuff( [ 'maxKeys' => $max ] ); + }, + 'function' => function () use ( &$hObj, &$keys ) { + foreach ( $keys as $i => $key ) { + $hObj->set( $key, $i ); + } + } + ]; + $mObj = null; + $benches['MapCacheLRU::set'] = [ + 'setup' => function () use ( &$mObj, $max ) { + $mObj = new MapCacheLRU( $max ); + }, + 'function' => function () use ( &$mObj, &$keys ) { + foreach ( $keys as $i => $key ) { + $mObj->set( $key, $i ); + } + } + ]; + } + + $this->bench( $benches ); + } +} + +$maintClass = BenchmarkLruHash::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkParse.php b/www/wiki/maintenance/benchmarks/benchmarkParse.php new file mode 100644 index 00000000..3a79ad3e --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkParse.php @@ -0,0 +1,192 @@ + + * @ingroup Benchmark + */ + +require __DIR__ . '/../Maintenance.php'; + +use MediaWiki\MediaWikiServices; + +/** + * Maintenance script to benchmark how long it takes to parse a given title at an optionally + * specified timestamp + * + * @since 1.23 + */ +class BenchmarkParse extends Maintenance { + /** @var string MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS) */ + private $templateTimestamp = null; + + private $clearLinkCache = false; + + /** + * @var LinkCache + */ + private $linkCache; + + /** @var array Cache that maps a Title DB key to revision ID for the requested timestamp */ + private $idCache = []; + + function __construct() { + parent::__construct(); + $this->addDescription( 'Benchmark parse operation' ); + $this->addArg( 'title', 'The name of the page to parse' ); + $this->addOption( 'warmup', 'Repeat the parse operation this number of times to warm the cache', + false, true ); + $this->addOption( 'loops', 'Number of times to repeat parse operation post-warmup', + false, true ); + $this->addOption( 'page-time', + 'Use the version of the page which was current at the given time', + false, true ); + $this->addOption( 'tpl-time', + 'Use templates which were current at the given time (except that moves and ' . + 'deletes are not handled properly)', + false, true ); + $this->addOption( 'reset-linkcache', 'Reset the LinkCache after every parse.', + false, false ); + } + + function execute() { + if ( $this->hasOption( 'tpl-time' ) ) { + $this->templateTimestamp = wfTimestamp( TS_MW, strtotime( $this->getOption( 'tpl-time' ) ) ); + Hooks::register( 'BeforeParserFetchTemplateAndtitle', [ $this, 'onFetchTemplate' ] ); + } + + $this->clearLinkCache = $this->hasOption( 'reset-linkcache' ); + // Set as a member variable to avoid function calls when we're timing the parse + $this->linkCache = MediaWikiServices::getInstance()->getLinkCache(); + + $title = Title::newFromText( $this->getArg() ); + if ( !$title ) { + $this->error( "Invalid title" ); + exit( 1 ); + } + + if ( $this->hasOption( 'page-time' ) ) { + $pageTimestamp = wfTimestamp( TS_MW, strtotime( $this->getOption( 'page-time' ) ) ); + $id = $this->getRevIdForTime( $title, $pageTimestamp ); + if ( !$id ) { + $this->error( "The page did not exist at that time" ); + exit( 1 ); + } + + $revision = Revision::newFromId( $id ); + } else { + $revision = Revision::newFromTitle( $title ); + } + + if ( !$revision ) { + $this->error( "Unable to load revision, incorrect title?" ); + exit( 1 ); + } + + $warmup = $this->getOption( 'warmup', 1 ); + for ( $i = 0; $i < $warmup; $i++ ) { + $this->runParser( $revision ); + } + + $loops = $this->getOption( 'loops', 1 ); + if ( $loops < 1 ) { + $this->fatalError( 'Invalid number of loops specified' ); + } + $startUsage = getrusage(); + $startTime = microtime( true ); + for ( $i = 0; $i < $loops; $i++ ) { + $this->runParser( $revision ); + } + $endUsage = getrusage(); + $endTime = microtime( true ); + + printf( "CPU time = %.3f s, wall clock time = %.3f s\n", + // CPU time + ( $endUsage['ru_utime.tv_sec'] + $endUsage['ru_utime.tv_usec'] * 1e-6 + - $startUsage['ru_utime.tv_sec'] - $startUsage['ru_utime.tv_usec'] * 1e-6 ) / $loops, + // Wall clock time + ( $endTime - $startTime ) / $loops + ); + } + + /** + * Fetch the ID of the revision of a Title that occurred + * + * @param Title $title + * @param string $timestamp + * @return bool|string Revision ID, or false if not found or error + */ + function getRevIdForTime( Title $title, $timestamp ) { + $dbr = $this->getDB( DB_REPLICA ); + + $id = $dbr->selectField( + [ 'revision', 'page' ], + 'rev_id', + [ + 'page_namespace' => $title->getNamespace(), + 'page_title' => $title->getDBkey(), + 'rev_timestamp <= ' . $dbr->addQuotes( $timestamp ) + ], + __METHOD__, + [ 'ORDER BY' => 'rev_timestamp DESC', 'LIMIT' => 1 ], + [ 'revision' => [ 'INNER JOIN', 'rev_page=page_id' ] ] + ); + + return $id; + } + + /** + * Parse the text from a given Revision + * + * @param Revision $revision + */ + function runParser( Revision $revision ) { + $content = $revision->getContent(); + $content->getParserOutput( $revision->getTitle(), $revision->getId() ); + if ( $this->clearLinkCache ) { + $this->linkCache->clear(); + } + } + + /** + * Hook into the parser's revision ID fetcher. Make sure that the parser only + * uses revisions around the specified timestamp. + * + * @param Parser $parser + * @param Title $title + * @param bool &$skip + * @param string|bool &$id + * @return bool + */ + function onFetchTemplate( Parser $parser, Title $title, &$skip, &$id ) { + $pdbk = $title->getPrefixedDBkey(); + if ( !isset( $this->idCache[$pdbk] ) ) { + $proposedId = $this->getRevIdForTime( $title, $this->templateTimestamp ); + $this->idCache[$pdbk] = $proposedId; + } + if ( $this->idCache[$pdbk] !== false ) { + $id = $this->idCache[$pdbk]; + } + + return true; + } +} + +$maintClass = BenchmarkParse::class; +require RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkPurge.php b/www/wiki/maintenance/benchmarks/benchmarkPurge.php new file mode 100644 index 00000000..cbab6775 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkPurge.php @@ -0,0 +1,118 @@ +addDescription( 'Benchmark the Squid purge functions.' ); + } + + public function execute() { + global $wgUseSquid, $wgSquidServers; + if ( !$wgUseSquid ) { + $this->fatalError( "Squid purge benchmark doesn't do much without squid support on." ); + } else { + $this->output( "There are " . count( $wgSquidServers ) . " defined squid servers:\n" ); + if ( $this->hasOption( 'count' ) ) { + $lengths = [ intval( $this->getOption( 'count' ) ) ]; + } else { + $lengths = [ 1, 10, 100 ]; + } + foreach ( $lengths as $length ) { + $urls = $this->randomUrlList( $length ); + $trial = $this->benchSquid( $urls ); + $this->output( $trial . "\n" ); + } + } + } + + /** + * Run a bunch of URLs through SquidUpdate::purge() + * to benchmark Squid response times. + * @param array $urls A bunch of URLs to purge + * @param int $trials How many times to run the test? + * @return string + */ + private function benchSquid( $urls, $trials = 1 ) { + $start = microtime( true ); + for ( $i = 0; $i < $trials; $i++ ) { + CdnCacheUpdate::purge( $urls ); + } + $delta = microtime( true ) - $start; + $pertrial = $delta / $trials; + $pertitle = $pertrial / count( $urls ); + + return sprintf( "%4d titles in %6.2fms (%6.2fms each)", + count( $urls ), $pertrial * 1000.0, $pertitle * 1000.0 ); + } + + /** + * Get an array of randomUrl()'s. + * @param int $length How many urls to add to the array + * @return array + */ + private function randomUrlList( $length ) { + $list = []; + for ( $i = 0; $i < $length; $i++ ) { + $list[] = $this->randomUrl(); + } + + return $list; + } + + /** + * Return a random URL of the wiki. Not necessarily an actual title in the + * database, but at least a URL that looks like one. + * @return string + */ + private function randomUrl() { + global $wgServer, $wgArticlePath; + + return $wgServer . str_replace( '$1', $this->randomTitle(), $wgArticlePath ); + } + + /** + * Create a random title string (not necessarily a Title object). + * For use with randomUrl(). + * @return string + */ + private function randomTitle() { + $str = ''; + $length = mt_rand( 1, 20 ); + for ( $i = 0; $i < $length; $i++ ) { + $str .= chr( mt_rand( ord( 'a' ), ord( 'z' ) ) ); + } + + return ucfirst( $str ); + } +} + +$maintClass = BenchmarkPurge::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkSanitizer.php b/www/wiki/maintenance/benchmarks/benchmarkSanitizer.php new file mode 100644 index 00000000..c264750b --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkSanitizer.php @@ -0,0 +1,99 @@ +addDescription( 'Benchmark for Sanitizer methods.' ); + $this->addOption( 'method', 'One of "validateEmail", "encodeAttribute", ' + . '"safeEncodeAttribute", "removeHTMLtags", or "stripAllTags". ' + . 'Default: (All)', false, true ); + } + + public function execute() { + $textWithHtmlSm = 'Before and another word.'; + $textWithHtmlLg = str_repeat( + // 28K (28 chars * 1000) + wfRandomString( 3 ) . ' ' . wfRandomString( 5 ) . ' ' . wfRandomString( 7 ), + 1000 + ); + + $method = $this->getOption( 'method' ); + $benches = []; + + if ( !$method || $method === 'validateEmail' ) { + $benches['Sanitizer::validateEmail (valid)'] = function () { + Sanitizer::validateEmail( 'user@example.org' ); + }; + $benches['Sanitizer::validateEmail (invalid)'] = function () { + Sanitizer::validateEmail( 'username@example! org' ); + }; + } + if ( !$method || $method === 'encodeAttribute' ) { + $benches['Sanitizer::encodeAttribute (simple)'] = function () { + Sanitizer::encodeAttribute( 'simple' ); + }; + $benches['Sanitizer::encodeAttribute (special)'] = function () { + Sanitizer::encodeAttribute( ":'\"\n https://example" ); + }; + } + if ( !$method || $method === 'safeEncodeAttribute' ) { + $benches['Sanitizer::safeEncodeAttribute (simple)'] = function () { + Sanitizer::safeEncodeAttribute( 'simple' ); + }; + $benches['Sanitizer::safeEncodeAttribute (special)'] = function () { + Sanitizer::safeEncodeAttribute( ":'\"\n https://example" ); + }; + } + if ( !$method || $method === 'removeHTMLtags' ) { + $sm = strlen( $textWithHtmlSm ); + $lg = round( strlen( $textWithHtmlLg ) / 1000 ) . 'K'; + $benches["Sanitizer::removeHTMLtags (input: $sm)"] = function () use ( $textWithHtmlSm ) { + Sanitizer::removeHTMLtags( $textWithHtmlSm ); + }; + $benches["Sanitizer::removeHTMLtags (input: $lg)"] = function () use ( $textWithHtmlLg ) { + Sanitizer::removeHTMLtags( $textWithHtmlLg ); + }; + } + if ( !$method || $method === 'stripAllTags' ) { + $sm = strlen( $textWithHtmlSm ); + $lg = round( strlen( $textWithHtmlLg ) / 1000 ) . 'K'; + $benches["Sanitizer::stripAllTags (input: $sm)"] = function () use ( $textWithHtmlSm ) { + Sanitizer::stripAllTags( $textWithHtmlSm ); + }; + $benches["Sanitizer::stripAllTags (input: $lg)"] = function () use ( $textWithHtmlLg ) { + Sanitizer::stripAllTags( $textWithHtmlLg ); + }; + } + + $this->bench( $benches ); + } +} + +$maintClass = BenchmarkSanitizer::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/benchmarkTidy.php b/www/wiki/maintenance/benchmarks/benchmarkTidy.php new file mode 100644 index 00000000..f2939b33 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/benchmarkTidy.php @@ -0,0 +1,78 @@ +addOption( 'file', 'A filename which contains the input text', true, true ); + $this->addOption( 'driver', 'The Tidy driver name, or false to use the configured instance', + false, true ); + $this->addOption( 'tidy-config', 'JSON encoded value for the tidy configuration array', + false, true ); + } + + public function execute() { + $html = file_get_contents( $this->getOption( 'file' ) ); + if ( $html === false ) { + $this->fatalError( "Unable to open input file" ); + } + if ( $this->hasOption( 'driver' ) || $this->hasOption( 'tidy-config' ) ) { + $config = json_decode( $this->getOption( 'tidy-config', '{}' ), true ); + if ( !is_array( $config ) ) { + $this->fatalError( "Invalid JSON tidy config" ); + } + $config += [ 'driver' => $this->getOption( 'driver', 'RemexHtml' ) ]; + $driver = MWTidy::factory( $config ); + } else { + $driver = MWTidy::singleton(); + if ( !$driver ) { + $this->fatalError( "Tidy disabled or not installed" ); + } + } + + $this->benchmark( $driver, $html ); + } + + private function benchmark( $driver, $html ) { + global $wgContLang; + + $times = []; + $innerCount = 10; + $outerCount = 10; + for ( $j = 1; $j <= $outerCount; $j++ ) { + $t = microtime( true ); + for ( $i = 0; $i < $innerCount; $i++ ) { + $driver->tidy( $html ); + print $wgContLang->formatSize( memory_get_usage( true ) ) . "\n"; + } + $t = ( ( microtime( true ) - $t ) / $innerCount ) * 1000; + $times[] = $t; + print "Run $j: $t\n"; + } + print "\n"; + + sort( $times, SORT_NUMERIC ); + $n = $outerCount; + $min = $times[0]; + $max = end( $times ); + if ( $n % 2 ) { + $median = $times[ ( $n - 1 ) / 2 ]; + } else { + $median = ( $times[$n / 2] + $times[$n / 2 - 1] ) / 2; + } + $mean = array_sum( $times ) / $n; + + print "Minimum: $min ms\n"; + print "Median: $median ms\n"; + print "Mean: $mean ms\n"; + print "Maximum: $max ms\n"; + print "Memory usage: " . + $wgContLang->formatSize( memory_get_usage( true ) ) . "\n"; + print "Peak memory usage: " . + $wgContLang->formatSize( memory_get_peak_usage( true ) ) . "\n"; + } +} + +$maintClass = BenchmarkTidy::class; +require RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/benchmarks/cssmin/circle.svg b/www/wiki/maintenance/benchmarks/cssmin/circle.svg new file mode 100644 index 00000000..4f7af217 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/cssmin/circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/www/wiki/maintenance/benchmarks/cssmin/styles.css b/www/wiki/maintenance/benchmarks/cssmin/styles.css new file mode 100644 index 00000000..3cc15206 --- /dev/null +++ b/www/wiki/maintenance/benchmarks/cssmin/styles.css @@ -0,0 +1,32 @@ +/** + * Header + */ + +.foo { + background: url(wiki.png); +} + +.foo { + background: url(unknown.png); +} + +.foo { + background: url(https://example.org/foo.png); + background: url('https://example.org/foo.png'); + background: url("https://example.org/foo.png"); +} + +.foo { + /* @embed */ + background: url(wiki.png); +} + +.foo { + /* @embed */ + background: url(circle.svg); +} + +.foo { + /* @embed */ + background: url(wiki.png), url(wiki.png); +} diff --git a/www/wiki/maintenance/benchmarks/cssmin/wiki.png b/www/wiki/maintenance/benchmarks/cssmin/wiki.png new file mode 100644 index 00000000..8c421183 Binary files /dev/null and b/www/wiki/maintenance/benchmarks/cssmin/wiki.png differ diff --git a/www/wiki/maintenance/cdb.php b/www/wiki/maintenance/cdb.php new file mode 100644 index 00000000..0870d6d4 --- /dev/null +++ b/www/wiki/maintenance/cdb.php @@ -0,0 +1,132 @@ + 'load a cdb file for reading', + 'get' => 'get a value for a key', + 'exit' => 'exit cdb', + 'quit' => 'exit cdb', + 'help' => 'help about a command', + ]; + if ( !$command ) { + $command = 'fullhelp'; + } + if ( $command === 'fullhelp' ) { + $max_cmd_len = max( array_map( 'strlen', array_keys( $commandList ) ) ); + foreach ( $commandList as $cmd => $desc ) { + printf( "%-{$max_cmd_len}s: %s\n", $cmd, $desc ); + } + } elseif ( isset( $commandList[$command] ) ) { + print "$command: $commandList[$command]\n"; + } else { + print "$command: command does not exist or no help for it\n"; + } +} + +do { + $bad = false; + $showhelp = false; + $quit = false; + static $fileHandle = false; + + $line = Maintenance::readconsole(); + if ( $line === false ) { + exit; + } + + $args = explode( ' ', $line, 2 ); + $command = array_shift( $args ); + + // process command + switch ( $command ) { + case 'help': + // show an help message + cdbShowHelp( array_shift( $args ) ); + break; + case 'load': + if ( !isset( $args[0] ) ) { + print "Need a filename there buddy\n"; + break; + } + $file = $args[0]; + print "Loading cdb file $file..."; + try { + $fileHandle = CdbReader::open( $file ); + } catch ( CdbException $e ) { + } + + if ( !$fileHandle ) { + print "not a cdb file or unable to read it\n"; + } else { + print "ok\n"; + } + break; + case 'get': + if ( !$fileHandle ) { + print "Need to load a cdb file first\n"; + break; + } + if ( !isset( $args[0] ) ) { + print "Need to specify a key, Luke\n"; + break; + } + try { + $res = $fileHandle->get( $args[0] ); + } catch ( CdbException $e ) { + print "Unable to read key from file\n"; + break; + } + if ( $res === false ) { + print "No such key/value pair\n"; + } elseif ( is_string( $res ) ) { + print "$res\n"; + } else { + var_dump( $res ); + } + break; + case 'quit': + case 'exit': + $quit = true; + break; + + default: + $bad = true; + } // switch() end + + if ( $bad ) { + if ( $command ) { + print "Bad command\n"; + } + } else { + if ( function_exists( 'readline_add_history' ) ) { + readline_add_history( $line ); + } + } +} while ( !$quit ); diff --git a/www/wiki/maintenance/changePassword.php b/www/wiki/maintenance/changePassword.php new file mode 100644 index 00000000..316004bd --- /dev/null +++ b/www/wiki/maintenance/changePassword.php @@ -0,0 +1,73 @@ + + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to change the password of a given user. + * + * @ingroup Maintenance + */ +class ChangePassword extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addOption( "user", "The username to operate on", false, true ); + $this->addOption( "userid", "The user id to operate on", false, true ); + $this->addOption( "password", "The password to use", true, true ); + $this->addDescription( "Change a user's password" ); + } + + public function execute() { + if ( $this->hasOption( "user" ) ) { + $user = User::newFromName( $this->getOption( 'user' ) ); + } elseif ( $this->hasOption( "userid" ) ) { + $user = User::newFromId( $this->getOption( 'userid' ) ); + } else { + $this->fatalError( "A \"user\" or \"userid\" must be set to change the password for" ); + } + if ( !$user || !$user->getId() ) { + $this->fatalError( "No such user: " . $this->getOption( 'user' ) ); + } + $password = $this->getOption( 'password' ); + try { + $status = $user->changeAuthenticationData( [ + 'username' => $user->getName(), + 'password' => $password, + 'retype' => $password, + ] ); + if ( !$status->isGood() ) { + throw new PasswordError( $status->getWikiText( null, null, 'en' ) ); + } + $user->saveSettings(); + $this->output( "Password set for " . $user->getName() . "\n" ); + } catch ( PasswordError $pwe ) { + $this->fatalError( $pwe->getText() ); + } + } +} + +$maintClass = ChangePassword::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/checkBadRedirects.php b/www/wiki/maintenance/checkBadRedirects.php new file mode 100644 index 00000000..b22432a0 --- /dev/null +++ b/www/wiki/maintenance/checkBadRedirects.php @@ -0,0 +1,64 @@ +addDescription( 'Check for bad redirects' ); + } + + public function execute() { + $this->output( "Fetching redirects...\n" ); + $dbr = $this->getDB( DB_REPLICA ); + $result = $dbr->select( + [ 'page' ], + [ 'page_namespace', 'page_title', 'page_latest' ], + [ 'page_is_redirect' => 1 ] ); + + $count = $result->numRows(); + $this->output( "Found $count redirects.\n" . + "Checking for bad redirects:\n\n" ); + + foreach ( $result as $row ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $rev = Revision::newFromId( $row->page_latest ); + if ( $rev ) { + $target = $rev->getContent()->getRedirectTarget(); + if ( !$target ) { + $this->output( $title->getPrefixedText() . "\n" ); + } + } + } + $this->output( "\nDone.\n" ); + } +} + +$maintClass = CheckBadRedirects::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/checkComposerLockUpToDate.php b/www/wiki/maintenance/checkComposerLockUpToDate.php new file mode 100644 index 00000000..69f16f50 --- /dev/null +++ b/www/wiki/maintenance/checkComposerLockUpToDate.php @@ -0,0 +1,65 @@ +addDescription( + 'Checks whether your composer.lock file is up to date with the current composer.json' ); + } + + public function execute() { + global $IP; + $lockLocation = "$IP/composer.lock"; + $jsonLocation = "$IP/composer.json"; + if ( !file_exists( $lockLocation ) ) { + // Maybe they're using mediawiki/vendor? + $lockLocation = "$IP/vendor/composer.lock"; + if ( !file_exists( $lockLocation ) ) { + $this->fatalError( + 'Could not find composer.lock file. Have you run "composer install --no-dev"?' + ); + } + } + + $lock = new ComposerLock( $lockLocation ); + $json = new ComposerJson( $jsonLocation ); + + // Check all the dependencies to see if any are old + $found = false; + $installed = $lock->getInstalledDependencies(); + foreach ( $json->getRequiredDependencies() as $name => $version ) { + if ( isset( $installed[$name] ) ) { + if ( $installed[$name]['version'] !== $version ) { + $this->output( + "$name: {$installed[$name]['version']} installed, $version required.\n" + ); + $found = true; + } + } else { + $this->output( "$name: not installed, $version required.\n" ); + $found = true; + } + } + if ( $found ) { + $this->fatalError( + 'Error: your composer.lock file is not up to date. ' . + 'Run "composer update --no-dev" to install newer dependencies' + ); + } else { + // We couldn't find any out-of-date dependencies, so assume everything is ok! + $this->output( "Your composer.lock file is up to date with current dependencies!\n" ); + } + } +} + +$maintClass = CheckComposerLockUpToDate::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/checkImages.php b/www/wiki/maintenance/checkImages.php new file mode 100644 index 00000000..f858f030 --- /dev/null +++ b/www/wiki/maintenance/checkImages.php @@ -0,0 +1,86 @@ +addDescription( 'Check images to see if they exist, are readable, etc' ); + $this->setBatchSize( 1000 ); + } + + public function execute() { + $start = ''; + $dbr = $this->getDB( DB_REPLICA ); + + $numImages = 0; + $numGood = 0; + + $repo = RepoGroup::singleton()->getLocalRepo(); + $fileQuery = LocalFile::getQueryInfo(); + do { + $res = $dbr->select( $fileQuery['tables'], $fileQuery['fields'], + [ 'img_name > ' . $dbr->addQuotes( $start ) ], + __METHOD__, [ 'LIMIT' => $this->getBatchSize() ], $fileQuery['joins'] ); + foreach ( $res as $row ) { + $numImages++; + $start = $row->img_name; + $file = $repo->newFileFromRow( $row ); + $path = $file->getPath(); + if ( !$path ) { + $this->output( "{$row->img_name}: not locally accessible\n" ); + continue; + } + $size = $repo->getFileSize( $file->getPath() ); + if ( $size === false ) { + $this->output( "{$row->img_name}: missing\n" ); + continue; + } + + if ( $size == 0 && $row->img_size != 0 ) { + $this->output( "{$row->img_name}: truncated, was {$row->img_size}\n" ); + continue; + } + + if ( $size != $row->img_size ) { + $this->output( "{$row->img_name}: size mismatch DB={$row->img_size}, " + . "actual={$size}\n" ); + continue; + } + + $numGood++; + } + } while ( $res->numRows() ); + + $this->output( "Good images: $numGood/$numImages\n" ); + } +} + +$maintClass = CheckImages::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/checkLess.php b/www/wiki/maintenance/checkLess.php new file mode 100644 index 00000000..55ffcb83 --- /dev/null +++ b/www/wiki/maintenance/checkLess.php @@ -0,0 +1,66 @@ +addDescription( + 'Checks LESS files for errors by running the LessTestSuite PHPUnit test suite' ); + } + + public function execute() { + global $IP; + + // NOTE (phuedx, 2014-03-26) wgAutoloadClasses isn't set up + // by either of the dependencies at the top of the file, so + // require it here. + self::requireTestsAutoloader(); + + // If phpunit isn't available by autoloader try pulling it in + if ( !class_exists( 'PHPUnit\\Framework\\TestCase' ) ) { + require_once 'PHPUnit/Autoload.php'; + } + + // RequestContext::resetMain() will print warnings unless this + // is defined. + if ( !defined( 'MW_PHPUNIT_TEST' ) ) { + define( 'MW_PHPUNIT_TEST', true ); + } + + $textUICommand = new PHPUnit_TextUI_Command(); + $argv = [ + "$IP/tests/phpunit/phpunit.php", + "$IP/tests/phpunit/suites/LessTestSuite.php" + ]; + $textUICommand->run( $argv ); + } +} + +$maintClass = CheckLess::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/checkUsernames.php b/www/wiki/maintenance/checkUsernames.php new file mode 100644 index 00000000..6c1343aa --- /dev/null +++ b/www/wiki/maintenance/checkUsernames.php @@ -0,0 +1,69 @@ +addDescription( 'Verify that database usernames are actually valid' ); + $this->setBatchSize( 1000 ); + } + + function execute() { + $dbr = $this->getDB( DB_REPLICA ); + + $maxUserId = 0; + do { + $res = $dbr->select( 'user', + [ 'user_id', 'user_name' ], + [ 'user_id > ' . $maxUserId ], + __METHOD__, + [ + 'ORDER BY' => 'user_id', + 'LIMIT' => $this->getBatchSize(), + ] + ); + + foreach ( $res as $row ) { + if ( !User::isValidUserName( $row->user_name ) ) { + $this->output( sprintf( "Found: %6d: '%s'\n", $row->user_id, $row->user_name ) ); + wfDebugLog( 'checkUsernames', $row->user_name ); + } + } + $maxUserId = $row->user_id; + } while ( $res->numRows() ); + } +} + +$maintClass = CheckUsernames::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupAncientTables.php b/www/wiki/maintenance/cleanupAncientTables.php new file mode 100644 index 00000000..bcf4af21 --- /dev/null +++ b/www/wiki/maintenance/cleanupAncientTables.php @@ -0,0 +1,114 @@ +addDescription( 'Cleanup ancient tables and indexes' ); + $this->addOption( 'force', 'Actually run this script' ); + } + + public function execute() { + if ( !$this->hasOption( 'force' ) ) { + $this->error( "This maintenance script will remove old columns and indexes.\n" + . "It is recommended to backup your database first, and ensure all your data has\n" + . "been migrated to newer tables. If you want to continue, run this script again\n" + . "with --force.\n" + ); + } + + $db = $this->getDB( DB_MASTER ); + $ancientTables = [ + 'blobs', // 1.4 + 'brokenlinks', // 1.4 + 'cur', // 1.4 + 'ip_blocks_old', // Temporary in 1.6 + 'links', // 1.4 + 'linkscc', // 1.4 + // 'math', // 1.18, but don't want to drop if math extension is enabled... + 'old', // 1.4 + 'oldwatchlist', // pre 1.1? + 'trackback', // 1.19 + 'user_rights', // 1.5 + 'validate', // 1.6 + ]; + + foreach ( $ancientTables as $table ) { + if ( $db->tableExists( $table, __METHOD__ ) ) { + $this->output( "Dropping table $table..." ); + $db->dropTable( $table, __METHOD__ ); + $this->output( "done.\n" ); + } + } + + $this->output( "Cleaning up text table\n" ); + + $oldIndexes = [ + 'old_namespace', + 'old_timestamp', + 'name_title_timestamp', + 'user_timestamp', + 'usertext_timestamp', + ]; + foreach ( $oldIndexes as $index ) { + if ( $db->indexExists( 'text', $index, __METHOD__ ) ) { + $this->output( "Dropping index $index from the text table..." ); + $db->query( "DROP INDEX " . $db->addIdentifierQuotes( $index ) + . " ON " . $db->tableName( 'text' ) ); + $this->output( "done.\n" ); + } + } + + $oldFields = [ + 'old_namespace', + 'old_title', + 'old_comment', + 'old_user', + 'old_user_text', + 'old_timestamp', + 'old_minor_edit', + 'inverse_timestamp', + ]; + foreach ( $oldFields as $field ) { + if ( $db->fieldExists( 'text', $field, __METHOD__ ) ) { + $this->output( "Dropping the $field field from the text table..." ); + $db->query( "ALTER TABLE " . $db->tableName( 'text' ) + . " DROP COLUMN " . $db->addIdentifierQuotes( $field ) ); + $this->output( "done.\n" ); + } + } + $this->output( "Done!\n" ); + } +} + +$maintClass = CleanupAncientTables::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupBlocks.php b/www/wiki/maintenance/cleanupBlocks.php new file mode 100644 index 00000000..cbf00841 --- /dev/null +++ b/www/wiki/maintenance/cleanupBlocks.php @@ -0,0 +1,152 @@ +addDescription( "Cleanup user blocks with user names not matching the 'user' table" ); + $this->setBatchSize( 1000 ); + } + + public function execute() { + $db = $this->getDB( DB_MASTER ); + $blockQuery = Block::getQueryInfo(); + + $max = $db->selectField( 'ipblocks', 'MAX(ipb_user)' ); + + // Step 1: Clean up any duplicate user blocks + $batchSize = $this->getBatchSize(); + for ( $from = 1; $from <= $max; $from += $batchSize ) { + $to = min( $max, $from + $batchSize - 1 ); + $this->output( "Cleaning up duplicate ipb_user ($from-$to of $max)\n" ); + + $delete = []; + + $res = $db->select( + 'ipblocks', + [ 'ipb_user' ], + [ + "ipb_user >= " . (int)$from, + "ipb_user <= " . (int)$to, + ], + __METHOD__, + [ + 'GROUP BY' => 'ipb_user', + 'HAVING' => 'COUNT(*) > 1', + ] + ); + foreach ( $res as $row ) { + $bestBlock = null; + $res2 = $db->select( + $blockQuery['tables'], + $blockQuery['fields'], + [ + 'ipb_user' => $row->ipb_user, + ], + __METHOD__, + [], + $blockQuery['joins'] + ); + foreach ( $res2 as $row2 ) { + $block = Block::newFromRow( $row2 ); + if ( !$bestBlock ) { + $bestBlock = $block; + continue; + } + + // Find the most-restrictive block. Can't use + // Block::chooseBlock because that's for IP blocks, not + // user blocks. + $keep = null; + if ( $keep === null && $block->getExpiry() !== $bestBlock->getExpiry() ) { + // This works for infinite blocks because 'infinity' > '20141024234513' + $keep = $block->getExpiry() > $bestBlock->getExpiry(); + } + if ( $keep === null ) { + foreach ( [ 'createaccount', 'sendemail', 'editownusertalk' ] as $action ) { + if ( $block->prevents( $action ) xor $bestBlock->prevents( $action ) ) { + $keep = $block->prevents( $action ); + break; + } + } + } + + if ( $keep ) { + $delete[] = $bestBlock->getId(); + $bestBlock = $block; + } else { + $delete[] = $block->getId(); + } + } + } + + if ( $delete ) { + $db->delete( + 'ipblocks', + [ 'ipb_id' => $delete ], + __METHOD__ + ); + } + } + + // Step 2: Update the user name in any blocks where it doesn't match + for ( $from = 1; $from <= $max; $from += $batchSize ) { + $to = min( $max, $from + $batchSize - 1 ); + $this->output( "Cleaning up mismatched user name ($from-$to of $max)\n" ); + + $res = $db->select( + [ 'ipblocks', 'user' ], + [ 'ipb_id', 'user_name' ], + [ + 'ipb_user = user_id', + "ipb_user >= " . (int)$from, + "ipb_user <= " . (int)$to, + 'ipb_address != user_name', + ], + __METHOD__ + ); + foreach ( $res as $row ) { + $db->update( + 'ipblocks', + [ 'ipb_address' => $row->user_name ], + [ 'ipb_id' => $row->ipb_id ], + __METHOD__ + ); + } + } + + $this->output( "Done!\n" ); + } +} + +$maintClass = CleanupBlocks::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupCaps.php b/www/wiki/maintenance/cleanupCaps.php new file mode 100644 index 00000000..546825bf --- /dev/null +++ b/www/wiki/maintenance/cleanupCaps.php @@ -0,0 +1,173 @@ + + * 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 + * @author Brion Vibber + * @ingroup Maintenance + */ + +require_once __DIR__ . '/cleanupTable.inc'; + +/** + * Maintenance script to clean up broken page links when somebody turns + * on or off $wgCapitalLinks. + * + * @ingroup Maintenance + */ +class CapsCleanup extends TableCleanup { + + private $user; + private $namespace; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Script to cleanup capitalization' ); + $this->addOption( 'namespace', 'Namespace number to run caps cleanup on', false, true ); + } + + public function execute() { + $this->user = User::newSystemUser( 'Conversion script', [ 'steal' => true ] ); + + $this->namespace = intval( $this->getOption( 'namespace', 0 ) ); + + if ( MWNamespace::isCapitalized( $this->namespace ) ) { + $this->output( "Will be moving pages to first letter capitalized titles" ); + $callback = 'processRowToUppercase'; + } else { + $this->output( "Will be moving pages to first letter lowercase titles" ); + $callback = 'processRowToLowercase'; + } + + $this->dryrun = $this->hasOption( 'dry-run' ); + + $this->runTable( [ + 'table' => 'page', + 'conds' => [ 'page_namespace' => $this->namespace ], + 'index' => 'page_id', + 'callback' => $callback ] ); + } + + protected function processRowToUppercase( $row ) { + global $wgContLang; + + $current = Title::makeTitle( $row->page_namespace, $row->page_title ); + $display = $current->getPrefixedText(); + $lower = $row->page_title; + $upper = $wgContLang->ucfirst( $row->page_title ); + if ( $upper == $lower ) { + $this->output( "\"$display\" already uppercase.\n" ); + + return $this->progress( 0 ); + } + + $target = Title::makeTitle( $row->page_namespace, $upper ); + if ( $target->exists() ) { + // Prefix "CapsCleanup" to bypass the conflict + $target = Title::newFromText( __CLASS__ . '/' . $display ); + } + $ok = $this->movePage( + $current, + $target, + 'Converting page title to first-letter uppercase', + false + ); + if ( $ok ) { + $this->progress( 1 ); + if ( $row->page_namespace == $this->namespace ) { + $talk = $target->getTalkPage(); + $row->page_namespace = $talk->getNamespace(); + if ( $talk->exists() ) { + return $this->processRowToUppercase( $row ); + } + } + } + + return $this->progress( 0 ); + } + + protected function processRowToLowercase( $row ) { + global $wgContLang; + + $current = Title::makeTitle( $row->page_namespace, $row->page_title ); + $display = $current->getPrefixedText(); + $upper = $row->page_title; + $lower = $wgContLang->lcfirst( $row->page_title ); + if ( $upper == $lower ) { + $this->output( "\"$display\" already lowercase.\n" ); + + return $this->progress( 0 ); + } + + $target = Title::makeTitle( $row->page_namespace, $lower ); + if ( $target->exists() ) { + $targetDisplay = $target->getPrefixedText(); + $this->output( "\"$display\" skipped; \"$targetDisplay\" already exists\n" ); + + return $this->progress( 0 ); + } + + $ok = $this->movePage( $current, $target, 'Converting page titles to lowercase', true ); + if ( $ok === true ) { + $this->progress( 1 ); + if ( $row->page_namespace == $this->namespace ) { + $talk = $target->getTalkPage(); + $row->page_namespace = $talk->getNamespace(); + if ( $talk->exists() ) { + return $this->processRowToLowercase( $row ); + } + } + } + + return $this->progress( 0 ); + } + + /** + * @param Title $current + * @param Title $target + * @param string $reason + * @param bool $createRedirect + * @return bool Success + */ + private function movePage( Title $current, Title $target, $reason, $createRedirect ) { + $display = $current->getPrefixedText(); + $targetDisplay = $target->getPrefixedText(); + + if ( $this->dryrun ) { + $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" ); + $ok = 'OK'; + } else { + $mp = new MovePage( $current, $target ); + $status = $mp->move( $this->user, $reason, $createRedirect ); + $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' ); + $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" ); + } + + return $ok === 'OK'; + } +} + +$maintClass = CapsCleanup::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupEmptyCategories.php b/www/wiki/maintenance/cleanupEmptyCategories.php new file mode 100644 index 00000000..786c20a5 --- /dev/null +++ b/www/wiki/maintenance/cleanupEmptyCategories.php @@ -0,0 +1,203 @@ +addDescription( + <<addOption( + 'mode', + '"add" empty categories with description pages, "remove" empty categories ' + . 'without description pages, or "both"', + false, + true + ); + $this->addOption( + 'begin', + 'Only do categories whose names are alphabetically after the provided name', + false, + true + ); + $this->addOption( + 'throttle', + 'Wait this many milliseconds after each batch. Default: 0', + false, + true + ); + } + + protected function getUpdateKey() { + return 'cleanup empty categories'; + } + + protected function doDBUpdates() { + $mode = $this->getOption( 'mode', 'both' ); + $begin = $this->getOption( 'begin', '' ); + $throttle = $this->getOption( 'throttle', 0 ); + + if ( !in_array( $mode, [ 'add', 'remove', 'both' ] ) ) { + $this->output( "--mode must be 'add', 'remove', or 'both'.\n" ); + return false; + } + + $dbw = $this->getDB( DB_MASTER ); + + $throttle = intval( $throttle ); + + if ( $mode === 'add' || $mode === 'both' ) { + if ( $begin !== '' ) { + $where = [ 'page_title > ' . $dbw->addQuotes( $begin ) ]; + } else { + $where = []; + } + + $this->output( "Adding empty categories with description pages...\n" ); + while ( true ) { + # Find which category to update + $rows = $dbw->select( + [ 'page', 'category' ], + 'page_title', + array_merge( $where, [ + 'page_namespace' => NS_CATEGORY, + 'cat_title' => null, + ] ), + __METHOD__, + [ + 'ORDER BY' => 'page_title', + 'LIMIT' => $this->getBatchSize(), + ], + [ + 'category' => [ 'LEFT JOIN', 'page_title = cat_title' ], + ] + ); + if ( !$rows || $rows->numRows() <= 0 ) { + # Done, hopefully. + break; + } + + foreach ( $rows as $row ) { + $name = $row->page_title; + $where = [ 'page_title > ' . $dbw->addQuotes( $name ) ]; + + # Use the row to update the category count + $cat = Category::newFromName( $name ); + if ( !is_object( $cat ) ) { + $this->output( "The category named $name is not valid?!\n" ); + } else { + $cat->refreshCounts(); + } + } + $this->output( "--mode=$mode --begin=$name\n" ); + + wfWaitForSlaves(); + usleep( $throttle * 1000 ); + } + + $begin = ''; + } + + if ( $mode === 'remove' || $mode === 'both' ) { + if ( $begin !== '' ) { + $where = [ 'cat_title > ' . $dbw->addQuotes( $begin ) ]; + } else { + $where = []; + } + + $this->output( "Removing empty categories without description pages...\n" ); + while ( true ) { + # Find which category to update + $rows = $dbw->select( + [ 'category', 'page' ], + 'cat_title', + array_merge( $where, [ + 'page_title' => null, + 'cat_pages' => 0, + ] ), + __METHOD__, + [ + 'ORDER BY' => 'cat_title', + 'LIMIT' => $this->getBatchSize(), + ], + [ + 'page' => [ 'LEFT JOIN', [ + 'page_namespace' => NS_CATEGORY, 'page_title = cat_title' + ] ], + ] + ); + if ( !$rows || $rows->numRows() <= 0 ) { + # Done, hopefully. + break; + } + foreach ( $rows as $row ) { + $name = $row->cat_title; + $where = [ 'cat_title > ' . $dbw->addQuotes( $name ) ]; + + # Use the row to update the category count + $cat = Category::newFromName( $name ); + if ( !is_object( $cat ) ) { + $this->output( "The category named $name is not valid?!\n" ); + } else { + $cat->refreshCounts(); + } + } + + $this->output( "--mode=remove --begin=$name\n" ); + + wfWaitForSlaves(); + usleep( $throttle * 1000 ); + } + } + + $this->output( "Category cleanup complete.\n" ); + + return true; + } +} + +$maintClass = CleanupEmptyCategories::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupImages.php b/www/wiki/maintenance/cleanupImages.php new file mode 100644 index 00000000..fbdc7c20 --- /dev/null +++ b/www/wiki/maintenance/cleanupImages.php @@ -0,0 +1,224 @@ + + * 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 + * @author Brion Vibber + * @ingroup Maintenance + */ + +require_once __DIR__ . '/cleanupTable.inc'; + +/** + * Maintenance script to clean up broken, unparseable upload filenames. + * + * @ingroup Maintenance + */ +class ImageCleanup extends TableCleanup { + protected $defaultParams = [ + 'table' => 'image', + 'conds' => [], + 'index' => 'img_name', + 'callback' => 'processRow', + ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Script to clean up broken, unparseable upload filenames' ); + } + + protected function processRow( $row ) { + global $wgContLang; + + $source = $row->img_name; + if ( $source == '' ) { + // Ye olde empty rows. Just kill them. + $this->killRow( $source ); + + return $this->progress( 1 ); + } + + $cleaned = $source; + + // About half of old bad image names have percent-codes + $cleaned = rawurldecode( $cleaned ); + + // We also have some HTML entities there + $cleaned = Sanitizer::decodeCharReferences( $cleaned ); + + // Some are old latin-1 + $cleaned = $wgContLang->checkTitleEncoding( $cleaned ); + + // Many of remainder look like non-normalized unicode + $cleaned = $wgContLang->normalize( $cleaned ); + + $title = Title::makeTitleSafe( NS_FILE, $cleaned ); + + if ( is_null( $title ) ) { + $this->output( "page $source ($cleaned) is illegal.\n" ); + $safe = $this->buildSafeTitle( $cleaned ); + if ( $safe === false ) { + return $this->progress( 0 ); + } + $this->pokeFile( $source, $safe ); + + return $this->progress( 1 ); + } + + if ( $title->getDBkey() !== $source ) { + $munged = $title->getDBkey(); + $this->output( "page $source ($munged) doesn't match self.\n" ); + $this->pokeFile( $source, $munged ); + + return $this->progress( 1 ); + } + + return $this->progress( 0 ); + } + + /** + * @param string $name + */ + private function killRow( $name ) { + if ( $this->dryrun ) { + $this->output( "DRY RUN: would delete bogus row '$name'\n" ); + } else { + $this->output( "deleting bogus row '$name'\n" ); + $db = $this->getDB( DB_MASTER ); + $db->delete( 'image', + [ 'img_name' => $name ], + __METHOD__ ); + } + } + + private function filePath( $name ) { + if ( !isset( $this->repo ) ) { + $this->repo = RepoGroup::singleton()->getLocalRepo(); + } + + return $this->repo->getRootDirectory() . '/' . $this->repo->getHashPath( $name ) . $name; + } + + private function imageExists( $name, $db ) { + return $db->selectField( 'image', '1', [ 'img_name' => $name ], __METHOD__ ); + } + + private function pageExists( $name, $db ) { + return $db->selectField( + 'page', + '1', + [ 'page_namespace' => NS_FILE, 'page_title' => $name ], + __METHOD__ + ); + } + + private function pokeFile( $orig, $new ) { + $path = $this->filePath( $orig ); + if ( !file_exists( $path ) ) { + $this->output( "missing file: $path\n" ); + $this->killRow( $orig ); + + return; + } + + $db = $this->getDB( DB_MASTER ); + + /* + * To prevent key collisions in the update() statements below, + * if the target title exists in the image table, or if both the + * original and target titles exist in the page table, append + * increasing version numbers until the target title exists in + * neither. (See also T18916.) + */ + $version = 0; + $final = $new; + $conflict = ( $this->imageExists( $final, $db ) || + ( $this->pageExists( $orig, $db ) && $this->pageExists( $final, $db ) ) ); + + while ( $conflict ) { + $this->output( "Rename conflicts with '$final'...\n" ); + $version++; + $final = $this->appendTitle( $new, "_$version" ); + $conflict = ( $this->imageExists( $final, $db ) || $this->pageExists( $final, $db ) ); + } + + $finalPath = $this->filePath( $final ); + + if ( $this->dryrun ) { + $this->output( "DRY RUN: would rename $path to $finalPath\n" ); + } else { + $this->output( "renaming $path to $finalPath\n" ); + // @todo FIXME: Should this use File::move()? + $this->beginTransaction( $db, __METHOD__ ); + $db->update( 'image', + [ 'img_name' => $final ], + [ 'img_name' => $orig ], + __METHOD__ ); + $db->update( 'oldimage', + [ 'oi_name' => $final ], + [ 'oi_name' => $orig ], + __METHOD__ ); + $db->update( 'page', + [ 'page_title' => $final ], + [ 'page_title' => $orig, 'page_namespace' => NS_FILE ], + __METHOD__ ); + $dir = dirname( $finalPath ); + if ( !file_exists( $dir ) ) { + if ( !wfMkdirParents( $dir, null, __METHOD__ ) ) { + $this->output( "RENAME FAILED, COULD NOT CREATE $dir" ); + $this->rollbackTransaction( $db, __METHOD__ ); + + return; + } + } + if ( rename( $path, $finalPath ) ) { + $this->commitTransaction( $db, __METHOD__ ); + } else { + $this->error( "RENAME FAILED" ); + $this->rollbackTransaction( $db, __METHOD__ ); + } + } + } + + private function appendTitle( $name, $suffix ) { + return preg_replace( '/^(.*)(\..*?)$/', + "\\1$suffix\\2", $name ); + } + + private function buildSafeTitle( $name ) { + $x = preg_replace_callback( + '/([^' . Title::legalChars() . ']|~)/', + [ $this, 'hexChar' ], + $name ); + + $test = Title::makeTitleSafe( NS_FILE, $x ); + if ( is_null( $test ) || $test->getDBkey() !== $x ) { + $this->error( "Unable to generate safe title from '$name', got '$x'" ); + + return false; + } + + return $x; + } +} + +$maintClass = ImageCleanup::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupInvalidDbKeys.php b/www/wiki/maintenance/cleanupInvalidDbKeys.php new file mode 100644 index 00000000..54ed3aad --- /dev/null +++ b/www/wiki/maintenance/cleanupInvalidDbKeys.php @@ -0,0 +1,311 @@ + 'rd_from' ], + [ 'archive', 'ar' ], + [ 'logging', 'log' ], + [ 'protected_titles', 'pt', 'idField' => 0 ], + [ 'category', 'cat', 'nsField' => 14 ], + [ 'recentchanges', 'rc' ], + [ 'watchlist', 'wl' ], + // The querycache tables' qc(c)_title and qcc_titletwo may contain titles, + // but also usernames or other things like that, so we leave them alone + + // Links tables + [ 'pagelinks', 'pl', 'idField' => 'pl_from' ], + [ 'templatelinks', 'tl', 'idField' => 'tl_from' ], + [ 'categorylinks', 'cl', 'idField' => 'cl_from', 'nsField' => 14, 'titleField' => 'cl_to' ], + ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( <<<'TEXT' +This script cleans up the title fields in various tables to remove entries that +will be rejected by the constructor of TitleValue. This constructor throws an +exception when invalid data is encountered, which will not normally occur on +regular page views, but can happen on query special pages. + +The script targets titles matching the regular expression /^_|[ \r\n\t]|_$/. +Because any foreign key relationships involving these titles will already be +broken, the titles are corrected to a valid version or the rows are deleted +entirely, depending on the table. + +The script runs with the expectation that STDOUT is redirected to a file. +TEXT + ); + $this->addOption( 'fix', 'Actually clean up invalid titles. If this parameter is ' . + 'not specified, the script will report invalid titles but not clean them up.', + false, false ); + $this->addOption( 'table', 'The table(s) to process. This option can be specified ' . + 'more than once (e.g. -t category -t watchlist). If not specified, all available ' . + 'tables will be processed. Available tables are: ' . + implode( ', ', array_column( static::$tables, 0 ) ), false, true, 't', true ); + + $this->setBatchSize( 500 ); + } + + public function execute() { + $tablesToProcess = $this->getOption( 'table' ); + foreach ( static::$tables as $tableParams ) { + if ( !$tablesToProcess || in_array( $tableParams[0], $tablesToProcess ) ) { + $this->cleanupTable( $tableParams ); + } + } + + $this->outputStatus( 'Done!' ); + if ( $this->hasOption( 'fix' ) ) { + $this->outputStatus( ' Cleaned up invalid DB keys on ' . wfWikiID() . "!\n" ); + } + } + + /** + * Prints text to STDOUT, and STDERR if STDOUT was redirected to a file. + * Used for progress reporting. + * + * @param string $str Text to write to both places + * @param string|null $channel Ignored + */ + protected function outputStatus( $str, $channel = null ) { + // Make it easier to find progress lines in the STDOUT log + if ( trim( $str ) ) { + fwrite( STDOUT, '*** ' . trim( $str ) . "\n" ); + } + fwrite( STDERR, $str ); + } + + /** + * Prints text to STDOUT. Used for logging output. + * + * @param string $str Text to write + */ + protected function writeToReport( $str ) { + fwrite( STDOUT, $str ); + } + + /** + * Identifies, and optionally cleans up, invalid titles. + * + * @param array $tableParams A child array of self::$tables + */ + protected function cleanupTable( $tableParams ) { + $table = $tableParams[0]; + $prefix = $tableParams[1]; + $idField = isset( $tableParams['idField'] ) ? + $tableParams['idField'] : + "{$prefix}_id"; + $nsField = isset( $tableParams['nsField'] ) ? + $tableParams['nsField'] : + "{$prefix}_namespace"; + $titleField = isset( $tableParams['titleField'] ) ? + $tableParams['titleField'] : + "{$prefix}_title"; + + $this->outputStatus( "Looking for invalid $titleField entries in $table...\n" ); + + // Do all the select queries on the replicas, as they are slow (they use + // unanchored LIKEs). Naturally this could cause problems if rows are + // modified after selecting and before deleting/updating, but working on + // the hypothesis that invalid rows will be old and in all likelihood + // unreferenced, we should be fine to do it like this. + $dbr = $this->getDB( DB_REPLICA, 'vslow' ); + + // Find all TitleValue-invalid titles. + $percent = $dbr->anyString(); // DBMS-agnostic equivalent of '%' LIKE wildcard + $res = $dbr->select( + $table, + [ + 'id' => $idField, + 'ns' => $nsField, + 'title' => $titleField, + ], + // The REGEXP operator is not cross-DBMS, so we have to use lots of LIKEs + [ $dbr->makeList( [ + $titleField . $dbr->buildLike( $percent, ' ', $percent ), + $titleField . $dbr->buildLike( $percent, "\r", $percent ), + $titleField . $dbr->buildLike( $percent, "\n", $percent ), + $titleField . $dbr->buildLike( $percent, "\t", $percent ), + $titleField . $dbr->buildLike( '_', $percent ), + $titleField . $dbr->buildLike( $percent, '_' ), + ], LIST_OR ) ], + __METHOD__, + [ 'LIMIT' => $this->getBatchSize() ] + ); + + $this->outputStatus( "Number of invalid rows: " . $res->numRows() . "\n" ); + if ( !$res->numRows() ) { + $this->outputStatus( "\n" ); + return; + } + + // Write a table of titles to the report file. Also keep a list of the found + // IDs, as we might need it later for DB updates + $this->writeToReport( sprintf( "%10s | ns | dbkey\n", $idField ) ); + $ids = []; + foreach ( $res as $row ) { + $this->writeToReport( sprintf( "%10d | %3d | %s\n", $row->id, $row->ns, $row->title ) ); + $ids[] = $row->id; + } + + // If we're doing a dry run, output the new titles we would use for the UPDATE + // queries (if relevant), and finish + if ( !$this->hasOption( 'fix' ) ) { + if ( $table === 'logging' || $table === 'archive' ) { + $this->writeToReport( "The following updates would be run with the --fix flag:\n" ); + foreach ( $res as $row ) { + $newTitle = self::makeValidTitle( $row->title ); + $this->writeToReport( + "$idField={$row->id}: update '{$row->title}' to '$newTitle'\n" ); + } + } + + if ( $table !== 'page' && $table !== 'redirect' ) { + $this->outputStatus( "Run with --fix to clean up these rows\n" ); + } + $this->outputStatus( "\n" ); + return; + } + + // Fix the bad data, using different logic for the various tables + $dbw = $this->getDB( DB_MASTER ); + switch ( $table ) { + case 'page': + case 'redirect': + // This shouldn't happen on production wikis, and we already have a script + // to handle 'page' rows anyway, so just notify the user and let them decide + // what to do next. + $this->outputStatus( <<outputStatus( + "Updating these rows, setting $titleField to the closest valid DB key...\n" ); + $affectedRowCount = 0; + foreach ( $res as $row ) { + $newTitle = self::makeValidTitle( $row->title ); + $this->writeToReport( + "$idField={$row->id}: updating '{$row->title}' to '$newTitle'\n" ); + + $dbw->update( $table, + [ $titleField => $newTitle ], + [ $idField => $row->id ], + __METHOD__ ); + $affectedRowCount += $dbw->affectedRows(); + } + wfWaitForSlaves(); + $this->outputStatus( "Updated $affectedRowCount rows on $table.\n" ); + + break; + + case 'recentchanges': + case 'watchlist': + case 'category': + // Since these broken titles can't exist, there's really nothing to watch, + // nothing can be categorised in them, and they can't have been changed + // recently, so we can just remove these rows. + $this->outputStatus( "Deleting invalid $table rows...\n" ); + $dbw->delete( $table, [ $idField => $ids ], __METHOD__ ); + wfWaitForSlaves(); + $this->outputStatus( 'Deleted ' . $dbw->affectedRows() . " rows from $table.\n" ); + break; + + case 'protected_titles': + // Since these broken titles can't exist, there's really nothing to protect, + // so we can just remove these rows. Made more complicated by this table + // not having an ID field + $this->outputStatus( "Deleting invalid $table rows...\n" ); + $affectedRowCount = 0; + foreach ( $res as $row ) { + $dbw->delete( $table, + [ $nsField => $row->ns, $titleField => $row->title ], + __METHOD__ ); + $affectedRowCount += $dbw->affectedRows(); + } + wfWaitForSlaves(); + $this->outputStatus( "Deleted $affectedRowCount rows from $table.\n" ); + break; + + case 'pagelinks': + case 'templatelinks': + case 'categorylinks': + // Update links tables for each page where these bogus links are supposedly + // located. If the invalid rows don't go away after these jobs go through, + // they're probably being added by a buggy hook. + $this->outputStatus( "Queueing link update jobs for the pages in $idField...\n" ); + foreach ( $res as $row ) { + $wp = WikiPage::newFromID( $row->id ); + if ( $wp ) { + RefreshLinks::fixLinksFromArticle( $row->id ); + } else { + // This link entry points to a nonexistent page, so just get rid of it + $dbw->delete( $table, + [ $idField => $row->id, $nsField => $row->ns, $titleField => $row->title ], + __METHOD__ ); + } + } + wfWaitForSlaves(); + $this->outputStatus( "Link update jobs have been added to the job queue.\n" ); + break; + } + + $this->outputStatus( "\n" ); + return; + } + + /** + * Fix possible validation issues in the given title (DB key). + * + * @param string $invalidTitle + * @return string + */ + protected static function makeValidTitle( $invalidTitle ) { + return strtr( trim( $invalidTitle, '_' ), + [ ' ' => '_', "\r" => '', "\n" => '', "\t" => '_' ] ); + } +} + +$maintClass = CleanupInvalidDbKeys::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupPreferences.php b/www/wiki/maintenance/cleanupPreferences.php new file mode 100644 index 00000000..b24d72dd --- /dev/null +++ b/www/wiki/maintenance/cleanupPreferences.php @@ -0,0 +1,157 @@ + + * @author Chad + * @see https://phabricator.wikimedia.org/T32976 + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that removes bogus preferences from the database. + * + * @ingroup Maintenance + */ +class CleanupPreferences extends Maintenance { + public function __construct() { + parent::__construct(); + $this->mDescription = 'Clean up hidden preferences, removed preferences, and normalizes values'; + $this->setBatchSize( 50 ); + $this->addOption( 'dry-run', 'Print debug info instead of actually deleting' ); + $this->addOption( 'hidden', 'Drop hidden preferences ($wgHiddenPrefs)' ); + $this->addOption( 'unknown', + 'Drop unknown preferences (not in $wgDefaultUserOptions or prefixed with "userjs-")' ); + // TODO: actually implement this + // $this->addOption( 'bogus', 'Drop preferences that have invalid/unaccepted values' ); + } + + /** + * We will do this in three passes + * 1) The easiest is to drop the hidden preferences from the database. We + * don't actually want them + * 2) Drop preference keys that we don't know about. They could've been + * removed from core, provided by a now-disabled extension, or the result + * of a bug. We don't want them. + * 3) TODO: Normalize accepted preference values. This is the biggest part of the work. + * For each preference we know about, iterate over it and if it's got a + * limited set of accepted values (so it's not text, basically), make sure + * all values are in that range. Drop ones that aren't. + */ + public function execute() { + global $wgHiddenPrefs, $wgDefaultUserOptions; + + $dbw = $this->getDB( DB_MASTER ); + $didWork = false; + $hidden = $this->hasOption( 'hidden' ); + $unknown = $this->hasOption( 'unknown' ); + $bogus = $this->hasOption( 'bogus' ); + + if ( !$hidden && !$unknown && !$bogus ) { + $this->output( "Did not select one of --hidden, --unknown or --bogus, exiting\n" ); + return; + } + + // Remove hidden prefs. Iterate over them to avoid the IN on a large table + if ( $hidden ) { + if ( !$wgHiddenPrefs ) { + $this->output( "No hidden preferences, skipping\n" ); + } + foreach ( $wgHiddenPrefs as $hiddenPref ) { + $this->deleteByWhere( + $dbw, + 'Dropping hidden preferences', + [ 'up_property' => $hiddenPref ] + ); + } + } + + // Remove unknown preferences. Special-case 'userjs-' as we can't control those names. + if ( $unknown ) { + $where = [ + 'up_property NOT' . $dbw->buildLike( 'userjs-', $dbw->anyString() ), + 'up_property NOT IN (' . $dbw->makeList( array_keys( $wgDefaultUserOptions ) ) . ')', + ]; + // Allow extensions to add to the where clause to prevent deletion of their own prefs. + Hooks::run( 'DeleteUnknownPreferences', [ &$where, $dbw ] ); + $this->deleteByWhere( $dbw, 'Dropping unknown preferences', $where ); + } + + // Something something phase 3 + if ( $bogus ) { + } + } + + /** + * + */ + private function deleteByWhere( $dbw, $startMessage, $where ) { + $this->output( $startMessage . "...\n" ); + $total = 0; + while ( true ) { + $res = $dbw->select( + 'user_properties', + '*', // The table lacks a primary key, so select the whole row + $where, + __METHOD__, + [ 'LIMIT' => $this->mBatchSize ] + ); + + $numRows = $res->numRows(); + $total += $numRows; + if ( $res->numRows() <= 0 ) { + // All done! + $this->output( "DONE! (handled $total entries)\n" ); + break; + } + + // Progress or something + $this->output( "..doing $numRows entries\n" ); + + // Delete our batch, then wait + foreach ( $res as $row ) { + if ( $this->hasOption( 'dry-run' ) ) { + $this->output( + " DRY RUN, would drop: " . + "[up_user] => '{$row->up_user}' " . + "[up_property] => '{$row->up_property}' " . + "[up_value] => '{$row->up_value}'\n" + ); + continue; + } + $this->beginTransaction( $dbw, __METHOD__ ); + $dbw->delete( + 'user_properties', + [ + 'up_user' => $row->up_user, + 'up_property' => $row->up_property, + 'up_value' => $row->up_value, + ], + __METHOD__ + ); + $this->commitTransaction( $dbw, __METHOD__ ); + } + } + } +} + +$maintClass = CleanupPreferences::class; // Tells it to run the class +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupRemovedModules.php b/www/wiki/maintenance/cleanupRemovedModules.php new file mode 100644 index 00000000..63838d25 --- /dev/null +++ b/www/wiki/maintenance/cleanupRemovedModules.php @@ -0,0 +1,81 @@ +addDescription( + 'Remove cache entries for removed ResourceLoader modules from the database' ); + $this->setBatchSize( 500 ); + } + + public function execute() { + $this->output( "Cleaning up module_deps table...\n" ); + + $dbw = $this->getDB( DB_MASTER ); + $rl = new ResourceLoader( MediaWikiServices::getInstance()->getMainConfig() ); + $moduleNames = $rl->getModuleNames(); + $res = $dbw->select( 'module_deps', + [ 'md_module', 'md_skin' ], + $moduleNames ? 'md_module NOT IN (' . $dbw->makeList( $moduleNames ) . ')' : '1=1', + __METHOD__ + ); + $rows = iterator_to_array( $res, false ); + + $modDeps = $dbw->tableName( 'module_deps' ); + $i = 1; + foreach ( array_chunk( $rows, $this->getBatchSize() ) as $chunk ) { + // WHERE ( mod=A AND skin=A ) OR ( mod=A AND skin=B) .. + $conds = array_map( function ( stdClass $row ) use ( $dbw ) { + return $dbw->makeList( (array)$row, IDatabase::LIST_AND ); + }, $chunk ); + $conds = $dbw->makeList( $conds, IDatabase::LIST_OR ); + + $this->beginTransaction( $dbw, __METHOD__ ); + $dbw->query( "DELETE FROM $modDeps WHERE $conds", __METHOD__ ); + $numRows = $dbw->affectedRows(); + $this->output( "Batch $i: $numRows rows\n" ); + $this->commitTransaction( $dbw, __METHOD__ ); + + $i++; + } + + $this->output( "Done\n" ); + } +} + +$maintClass = CleanupRemovedModules::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupSpam.php b/www/wiki/maintenance/cleanupSpam.php new file mode 100644 index 00000000..038b28ce --- /dev/null +++ b/www/wiki/maintenance/cleanupSpam.php @@ -0,0 +1,160 @@ +addDescription( 'Cleanup all spam from a given hostname' ); + $this->addOption( 'all', 'Check all wikis in $wgLocalDatabases' ); + $this->addOption( 'delete', 'Delete pages containing only spam instead of blanking them' ); + $this->addArg( + 'hostname', + 'Hostname that was spamming, single * wildcard in the beginning allowed' + ); + } + + public function execute() { + global $IP, $wgLocalDatabases, $wgUser; + + $username = wfMessage( 'spambot_username' )->text(); + $wgUser = User::newSystemUser( $username ); + if ( !$wgUser ) { + $this->fatalError( "Invalid username specified in 'spambot_username' message: $username" ); + } + // Hack: Grant bot rights so we don't flood RecentChanges + $wgUser->addGroup( 'bot' ); + + $spec = $this->getArg(); + $like = LinkFilter::makeLikeArray( $spec ); + if ( !$like ) { + $this->fatalError( "Not a valid hostname specification: $spec" ); + } + + if ( $this->hasOption( 'all' ) ) { + // Clean up spam on all wikis + $this->output( "Finding spam on " . count( $wgLocalDatabases ) . " wikis\n" ); + $found = false; + foreach ( $wgLocalDatabases as $wikiID ) { + $dbr = $this->getDB( DB_REPLICA, [], $wikiID ); + + $count = $dbr->selectField( 'externallinks', 'COUNT(*)', + [ 'el_index' . $dbr->buildLike( $like ) ], __METHOD__ ); + if ( $count ) { + $found = true; + $cmd = wfShellWikiCmd( "$IP/maintenance/cleanupSpam.php", + [ '--wiki', $wikiID, $spec ] ); + passthru( "$cmd | sed 's/^/$wikiID: /'" ); + } + } + if ( $found ) { + $this->output( "All done\n" ); + } else { + $this->output( "None found\n" ); + } + } else { + // Clean up spam on this wiki + + $dbr = $this->getDB( DB_REPLICA ); + $res = $dbr->select( 'externallinks', [ 'DISTINCT el_from' ], + [ 'el_index' . $dbr->buildLike( $like ) ], __METHOD__ ); + $count = $dbr->numRows( $res ); + $this->output( "Found $count articles containing $spec\n" ); + foreach ( $res as $row ) { + $this->cleanupArticle( $row->el_from, $spec ); + } + if ( $count ) { + $this->output( "Done\n" ); + } + } + } + + private function cleanupArticle( $id, $domain ) { + $title = Title::newFromID( $id ); + if ( !$title ) { + $this->error( "Internal error: no page for ID $id" ); + + return; + } + + $this->output( $title->getPrefixedDBkey() . " ..." ); + $rev = Revision::newFromTitle( $title ); + $currentRevId = $rev->getId(); + + while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT ) + || LinkFilter::matchEntry( $rev->getContent( Revision::RAW ), $domain ) ) + ) { + $rev = $rev->getPrevious(); + } + + if ( $rev && $rev->getId() == $currentRevId ) { + // The regex didn't match the current article text + // This happens e.g. when a link comes from a template rather than the page itself + $this->output( "False match\n" ); + } else { + $dbw = $this->getDB( DB_MASTER ); + $this->beginTransaction( $dbw, __METHOD__ ); + $page = WikiPage::factory( $title ); + if ( $rev ) { + // Revert to this revision + $content = $rev->getContent( Revision::RAW ); + + $this->output( "reverting\n" ); + $page->doEditContent( + $content, + wfMessage( 'spam_reverting', $domain )->inContentLanguage()->text(), + EDIT_UPDATE | EDIT_FORCE_BOT, + $rev->getId() + ); + } elseif ( $this->hasOption( 'delete' ) ) { + // Didn't find a non-spammy revision, blank the page + $this->output( "deleting\n" ); + $page->doDeleteArticle( + wfMessage( 'spam_deleting', $domain )->inContentLanguage()->text() + ); + } else { + // Didn't find a non-spammy revision, blank the page + $handler = ContentHandler::getForTitle( $title ); + $content = $handler->makeEmptyContent(); + + $this->output( "blanking\n" ); + $page->doEditContent( + $content, + wfMessage( 'spam_blanking', $domain )->inContentLanguage()->text(), + EDIT_UPDATE | EDIT_FORCE_BOT + ); + } + $this->commitTransaction( $dbw, __METHOD__ ); + } + } +} + +$maintClass = CleanupSpam::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupTable.inc b/www/wiki/maintenance/cleanupTable.inc new file mode 100644 index 00000000..3ace09cb --- /dev/null +++ b/www/wiki/maintenance/cleanupTable.inc @@ -0,0 +1,174 @@ + 'page', + 'conds' => [], + 'index' => 'page_id', + 'callback' => 'processRow', + ]; + + protected $dryrun = false; + public $batchSize = 100; + public $reportInterval = 100; + + protected $processed, $updated, $count, $startTime, $table; + + public function __construct() { + parent::__construct(); + $this->addOption( 'dry-run', 'Perform a dry run' ); + } + + public function execute() { + global $wgUser; + $this->dryrun = $this->hasOption( 'dry-run' ); + if ( $this->dryrun ) { + $wgUser = User::newFromName( 'Conversion script' ); + $this->output( "Checking for bad titles...\n" ); + } else { + $wgUser = User::newSystemUser( 'Conversion script', [ 'steal' => true ] ); + $this->output( "Checking and fixing bad titles...\n" ); + } + $this->runTable( $this->defaultParams ); + } + + protected function init( $count, $table ) { + $this->processed = 0; + $this->updated = 0; + $this->count = $count; + $this->startTime = microtime( true ); + $this->table = $table; + } + + /** + * @param int $updated + */ + protected function progress( $updated ) { + $this->updated += $updated; + $this->processed++; + if ( $this->processed % $this->reportInterval != 0 ) { + return; + } + $portion = $this->processed / $this->count; + $updateRate = $this->updated / $this->processed; + + $now = microtime( true ); + $delta = $now - $this->startTime; + $estimatedTotalTime = $delta / $portion; + $eta = $this->startTime + $estimatedTotalTime; + + $this->output( + sprintf( "%s %s: %6.2f%% done on %s; ETA %s [%d/%d] %.2f/sec <%.2f%% updated>\n", + wfWikiID(), + wfTimestamp( TS_DB, intval( $now ) ), + $portion * 100.0, + $this->table, + wfTimestamp( TS_DB, intval( $eta ) ), + $this->processed, + $this->count, + $this->processed / $delta, + $updateRate * 100.0 + ) + ); + flush(); + } + + /** + * @param array $params + * @throws MWException + */ + public function runTable( $params ) { + $dbr = $this->getDB( DB_REPLICA ); + + if ( array_diff( array_keys( $params ), + [ 'table', 'conds', 'index', 'callback' ] ) + ) { + throw new MWException( __METHOD__ . ': Missing parameter ' . implode( ', ', $params ) ); + } + + $table = $params['table']; + // count(*) would melt the DB for huge tables, we can estimate here + $count = $dbr->estimateRowCount( $table, '*', '', __METHOD__ ); + $this->init( $count, $table ); + $this->output( "Processing $table...\n" ); + + $index = (array)$params['index']; + $indexConds = []; + $options = [ + 'ORDER BY' => implode( ',', $index ), + 'LIMIT' => $this->batchSize + ]; + $callback = [ $this, $params['callback'] ]; + + while ( true ) { + $conds = array_merge( $params['conds'], $indexConds ); + $res = $dbr->select( $table, '*', $conds, __METHOD__, $options ); + if ( !$res->numRows() ) { + // Done + break; + } + + foreach ( $res as $row ) { + call_user_func( $callback, $row ); + } + + if ( $res->numRows() < $this->batchSize ) { + // Done + break; + } + + // Update the conditions to select the next batch. + // Construct a condition string by starting with the least significant part + // of the index, and adding more significant parts progressively to the left + // of the string. + $nextCond = ''; + foreach ( array_reverse( $index ) as $field ) { + $encValue = $dbr->addQuotes( $row->$field ); + if ( $nextCond === '' ) { + $nextCond = "$field > $encValue"; + } else { + $nextCond = "$field > $encValue OR ($field = $encValue AND ($nextCond))"; + } + } + $indexConds = [ $nextCond ]; + } + + $this->output( "Finished $table... $this->updated of $this->processed rows updated\n" ); + } + + /** + * @param array $matches + * @return string + */ + protected function hexChar( $matches ) { + return sprintf( "\\x%02x", ord( $matches[1] ) ); + } +} diff --git a/www/wiki/maintenance/cleanupTitles.php b/www/wiki/maintenance/cleanupTitles.php new file mode 100644 index 00000000..5b441f90 --- /dev/null +++ b/www/wiki/maintenance/cleanupTitles.php @@ -0,0 +1,199 @@ + + * 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 + * @author Brion Vibber + * @ingroup Maintenance + */ + +use MediaWiki\MediaWikiServices; + +require_once __DIR__ . '/cleanupTable.inc'; + +/** + * Maintenance script to clean up broken, unparseable titles. + * + * @ingroup Maintenance + */ +class TitleCleanup extends TableCleanup { + public function __construct() { + parent::__construct(); + $this->addDescription( 'Script to clean up broken, unparseable titles' ); + } + + /** + * @param object $row + */ + protected function processRow( $row ) { + global $wgContLang; + $display = Title::makeName( $row->page_namespace, $row->page_title ); + $verified = $wgContLang->normalize( $display ); + $title = Title::newFromText( $verified ); + + if ( !is_null( $title ) + && $title->canExist() + && $title->getNamespace() == $row->page_namespace + && $title->getDBkey() === $row->page_title + ) { + $this->progress( 0 ); // all is fine + + return; + } + + if ( $row->page_namespace == NS_FILE && $this->fileExists( $row->page_title ) ) { + $this->output( "file $row->page_title needs cleanup, please run cleanupImages.php.\n" ); + $this->progress( 0 ); + } elseif ( is_null( $title ) ) { + $this->output( "page $row->page_id ($display) is illegal.\n" ); + $this->moveIllegalPage( $row ); + $this->progress( 1 ); + } else { + $this->output( "page $row->page_id ($display) doesn't match self.\n" ); + $this->moveInconsistentPage( $row, $title ); + $this->progress( 1 ); + } + } + + /** + * @param string $name + * @return bool + */ + protected function fileExists( $name ) { + // XXX: Doesn't actually check for file existence, just presence of image record. + // This is reasonable, since cleanupImages.php only iterates over the image table. + $dbr = $this->getDB( DB_REPLICA ); + $row = $dbr->selectRow( 'image', [ 'img_name' ], [ 'img_name' => $name ], __METHOD__ ); + + return $row !== false; + } + + /** + * @param object $row + */ + protected function moveIllegalPage( $row ) { + $legal = 'A-Za-z0-9_/\\\\-'; + $legalized = preg_replace_callback( "!([^$legal])!", + [ $this, 'hexChar' ], + $row->page_title ); + if ( $legalized == '.' ) { + $legalized = '(dot)'; + } + if ( $legalized == '_' ) { + $legalized = '(space)'; + } + $legalized = 'Broken/' . $legalized; + + $title = Title::newFromText( $legalized ); + if ( is_null( $title ) ) { + $clean = 'Broken/id:' . $row->page_id; + $this->output( "Couldn't legalize; form '$legalized' still invalid; using '$clean'\n" ); + $title = Title::newFromText( $clean ); + } elseif ( $title->exists() ) { + $clean = 'Broken/id:' . $row->page_id; + $this->output( "Legalized for '$legalized' exists; using '$clean'\n" ); + $title = Title::newFromText( $clean ); + } + + $dest = $title->getDBkey(); + if ( $this->dryrun ) { + $this->output( "DRY RUN: would rename $row->page_id ($row->page_namespace," . + "'$row->page_title') to ($row->page_namespace,'$dest')\n" ); + } else { + $this->output( "renaming $row->page_id ($row->page_namespace," . + "'$row->page_title') to ($row->page_namespace,'$dest')\n" ); + $dbw = $this->getDB( DB_MASTER ); + $dbw->update( 'page', + [ 'page_title' => $dest ], + [ 'page_id' => $row->page_id ], + __METHOD__ ); + } + } + + /** + * @param object $row + * @param Title $title + */ + protected function moveInconsistentPage( $row, Title $title ) { + if ( $title->exists( Title::GAID_FOR_UPDATE ) + || $title->getInterwiki() + || !$title->canExist() + ) { + $titleImpossible = $title->getInterwiki() || !$title->canExist(); + if ( $titleImpossible ) { + $prior = $title->getPrefixedDBkey(); + } else { + $prior = $title->getDBkey(); + } + + # Old cleanupTitles could move articles there. See T25147. + $ns = $row->page_namespace; + if ( $ns < 0 ) { + $ns = 0; + } + + # Namespace which no longer exists. Put the page in the main namespace + # since we don't have any idea of the old namespace name. See T70501. + if ( !MWNamespace::exists( $ns ) ) { + $ns = 0; + } + + if ( !$titleImpossible && !$title->exists() ) { + // Looks like the current title, after cleaning it up, is valid and available + $clean = $prior; + } else { + $clean = 'Broken/' . $prior; + } + $verified = Title::makeTitleSafe( $ns, $clean ); + if ( !$verified || $verified->exists() ) { + $blah = "Broken/id:" . $row->page_id; + $this->output( "Couldn't legalize; form '$clean' exists; using '$blah'\n" ); + $verified = Title::makeTitleSafe( $ns, $blah ); + } + $title = $verified; + } + if ( is_null( $title ) ) { + $this->fatalError( "Something awry; empty title." ); + } + $ns = $title->getNamespace(); + $dest = $title->getDBkey(); + + if ( $this->dryrun ) { + $this->output( "DRY RUN: would rename $row->page_id ($row->page_namespace," . + "'$row->page_title') to ($ns,'$dest')\n" ); + } else { + $this->output( "renaming $row->page_id ($row->page_namespace," . + "'$row->page_title') to ($ns,'$dest')\n" ); + $dbw = $this->getDB( DB_MASTER ); + $dbw->update( 'page', + [ + 'page_namespace' => $ns, + 'page_title' => $dest + ], + [ 'page_id' => $row->page_id ], + __METHOD__ ); + MediaWikiServices::getInstance()->getLinkCache()->clear(); + } + } +} + +$maintClass = TitleCleanup::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupUploadStash.php b/www/wiki/maintenance/cleanupUploadStash.php new file mode 100644 index 00000000..61cd9c24 --- /dev/null +++ b/www/wiki/maintenance/cleanupUploadStash.php @@ -0,0 +1,156 @@ + + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to remove old or broken uploads from temporary uploaded + * file storage and clean up associated database records. + * + * @ingroup Maintenance + */ +class UploadStashCleanup extends Maintenance { + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Clean up abandoned files in temporary uploaded file stash' ); + $this->setBatchSize( 50 ); + } + + public function execute() { + global $wgUploadStashMaxAge; + + $repo = RepoGroup::singleton()->getLocalRepo(); + $tempRepo = $repo->getTempRepo(); + + $dbr = $repo->getReplicaDB(); + + // how far back should this look for files to delete? + $cutoff = time() - $wgUploadStashMaxAge; + + $this->output( "Getting list of files to clean up...\n" ); + $res = $dbr->select( + 'uploadstash', + 'us_key', + 'us_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $cutoff ) ), + __METHOD__ + ); + + // Delete all registered stash files... + if ( $res->numRows() == 0 ) { + $this->output( "No stashed files to cleanup according to the DB.\n" ); + } else { + // finish the read before starting writes. + $keys = []; + foreach ( $res as $row ) { + array_push( $keys, $row->us_key ); + } + + $this->output( 'Removing ' . count( $keys ) . " file(s)...\n" ); + // this could be done some other, more direct/efficient way, but using + // UploadStash's own methods means it's less likely to fall accidentally + // out-of-date someday + $stash = new UploadStash( $repo ); + + $i = 0; + foreach ( $keys as $key ) { + $i++; + try { + $stash->getFile( $key, true ); + $stash->removeFileNoAuth( $key ); + } catch ( UploadStashException $ex ) { + $type = get_class( $ex ); + $this->output( "Failed removing stashed upload with key: $key ($type)\n" ); + } + if ( $i % 100 == 0 ) { + wfWaitForSlaves(); + $this->output( "$i\n" ); + } + } + $this->output( "$i done\n" ); + } + + // Delete all the corresponding thumbnails... + $dir = $tempRepo->getZonePath( 'thumb' ); + $iterator = $tempRepo->getBackend()->getFileList( [ 'dir' => $dir, 'adviseStat' => 1 ] ); + $this->output( "Deleting old thumbnails...\n" ); + $i = 0; + $batch = []; // operation batch + foreach ( $iterator as $file ) { + if ( wfTimestamp( TS_UNIX, $tempRepo->getFileTimestamp( "$dir/$file" ) ) < $cutoff ) { + $batch[] = [ 'op' => 'delete', 'src' => "$dir/$file" ]; + if ( count( $batch ) >= $this->getBatchSize() ) { + $this->doOperations( $tempRepo, $batch ); + $i += count( $batch ); + $batch = []; + $this->output( "$i\n" ); + } + } + } + if ( count( $batch ) ) { + $this->doOperations( $tempRepo, $batch ); + $i += count( $batch ); + } + $this->output( "$i done\n" ); + + // Apparently lots of stash files are not registered in the DB... + $dir = $tempRepo->getZonePath( 'public' ); + $iterator = $tempRepo->getBackend()->getFileList( [ 'dir' => $dir, 'adviseStat' => 1 ] ); + $this->output( "Deleting orphaned temp files...\n" ); + if ( strpos( $dir, '/local-temp' ) === false ) { // sanity check + $this->fatalError( "Temp repo is not using the temp container." ); + } + $i = 0; + $batch = []; // operation batch + foreach ( $iterator as $file ) { + if ( wfTimestamp( TS_UNIX, $tempRepo->getFileTimestamp( "$dir/$file" ) ) < $cutoff ) { + $batch[] = [ 'op' => 'delete', 'src' => "$dir/$file" ]; + if ( count( $batch ) >= $this->getBatchSize() ) { + $this->doOperations( $tempRepo, $batch ); + $i += count( $batch ); + $batch = []; + $this->output( "$i\n" ); + } + } + } + if ( count( $batch ) ) { + $this->doOperations( $tempRepo, $batch ); + $i += count( $batch ); + } + $this->output( "$i done\n" ); + } + + protected function doOperations( FileRepo $tempRepo, array $ops ) { + $status = $tempRepo->getBackend()->doQuickOperations( $ops ); + if ( !$status->isOK() ) { + $this->error( print_r( $status->getErrorsArray(), true ) ); + } + } +} + +$maintClass = UploadStashCleanup::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupUsersWithNoId.php b/www/wiki/maintenance/cleanupUsersWithNoId.php new file mode 100644 index 00000000..b2fdf2f9 --- /dev/null +++ b/www/wiki/maintenance/cleanupUsersWithNoId.php @@ -0,0 +1,212 @@ +addDescription( 'Cleans up tables that have valid usernames with no user ID' ); + $this->addOption( 'prefix', 'Interwiki prefix to apply to the usernames', true, true, 'p' ); + $this->addOption( 'table', 'Only clean up this table', false, true ); + $this->addOption( 'assign', 'Assign edits to existing local users if they exist', false, false ); + $this->setBatchSize( 100 ); + } + + protected function getUpdateKey() { + return __CLASS__; + } + + protected function doDBUpdates() { + $this->prefix = $this->getOption( 'prefix' ); + $this->table = $this->getOption( 'table', null ); + $this->assign = $this->getOption( 'assign' ); + + $this->cleanup( + 'revision', 'rev_id', 'rev_user', 'rev_user_text', + [ 'rev_user' => 0 ], [ 'rev_timestamp', 'rev_id' ] + ); + $this->cleanup( + 'archive', 'ar_id', 'ar_user', 'ar_user_text', + [], [ 'ar_id' ] + ); + $this->cleanup( + 'logging', 'log_id', 'log_user', 'log_user_text', + [ 'log_user' => 0 ], [ 'log_timestamp', 'log_id' ] + ); + $this->cleanup( + 'image', 'img_name', 'img_user', 'img_user_text', + [ 'img_user' => 0 ], [ 'img_timestamp', 'img_name' ] + ); + $this->cleanup( + 'oldimage', [ 'oi_name', 'oi_timestamp' ], 'oi_user', 'oi_user_text', + [], [ 'oi_name', 'oi_timestamp' ] + ); + $this->cleanup( + 'filearchive', 'fa_id', 'fa_user', 'fa_user_text', + [], [ 'fa_id' ] + ); + $this->cleanup( + 'ipblocks', 'ipb_id', 'ipb_by', 'ipb_by_text', + [], [ 'ipb_id' ] + ); + $this->cleanup( + 'recentchanges', 'rc_id', 'rc_user', 'rc_user_text', + [], [ 'rc_id' ] + ); + + return true; + } + + /** + * Calculate a "next" condition and progress display string + * @param IDatabase $dbw + * @param string[] $indexFields Fields in the index being ordered by + * @param object $row Database row + * @return array [ string $next, string $display ] + */ + private function makeNextCond( $dbw, $indexFields, $row ) { + $next = ''; + $display = []; + for ( $i = count( $indexFields ) - 1; $i >= 0; $i-- ) { + $field = $indexFields[$i]; + $display[] = $field . '=' . $row->$field; + $value = $dbw->addQuotes( $row->$field ); + if ( $next === '' ) { + $next = "$field > $value"; + } else { + $next = "$field > $value OR $field = $value AND ($next)"; + } + } + $display = implode( ' ', array_reverse( $display ) ); + return [ $next, $display ]; + } + + /** + * Cleanup a table + * + * @param string $table Table to migrate + * @param string|string[] $primaryKey Primary key of the table. + * @param string $idField User ID field name + * @param string $nameField User name field name + * @param array $conds Query conditions + * @param string[] $orderby Fields to order by + */ + protected function cleanup( + $table, $primaryKey, $idField, $nameField, array $conds, array $orderby + ) { + if ( $this->table !== null && $this->table !== $table ) { + return; + } + + $primaryKey = (array)$primaryKey; + $pkFilter = array_flip( $primaryKey ); + $this->output( + "Beginning cleanup of $table\n" + ); + + $dbw = $this->getDB( DB_MASTER ); + $next = '1=1'; + $countAssigned = 0; + $countPrefixed = 0; + while ( true ) { + // Fetch the rows needing update + $res = $dbw->select( + $table, + array_merge( $primaryKey, [ $idField, $nameField ], $orderby ), + array_merge( $conds, [ $next ] ), + __METHOD__, + [ + 'ORDER BY' => $orderby, + 'LIMIT' => $this->mBatchSize, + ] + ); + if ( !$res->numRows() ) { + break; + } + + // Update the existing rows + foreach ( $res as $row ) { + $name = $row->$nameField; + if ( $row->$idField || !User::isUsableName( $name ) ) { + continue; + } + + $id = 0; + if ( $this->assign ) { + $id = (int)User::idFromName( $name ); + if ( !$id ) { + // See if any extension wants to create it. + if ( !isset( $this->triedCreations[$name] ) ) { + $this->triedCreations[$name] = true; + if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) ) { + $id = (int)User::idFromName( $name, User::READ_LATEST ); + } + } + } + } + if ( $id ) { + $set = [ $idField => $id ]; + $counter = &$countAssigned; + } else { + $set = [ $nameField => substr( $this->prefix . '>' . $name, 0, 255 ) ]; + $counter = &$countPrefixed; + } + + $dbw->update( + $table, + $set, + array_intersect_key( (array)$row, $pkFilter ) + [ + $idField => 0, + $nameField => $name, + ], + __METHOD__ + ); + $counter += $dbw->affectedRows(); + } + + list( $next, $display ) = $this->makeNextCond( $dbw, $orderby, $row ); + $this->output( "... $display\n" ); + wfWaitForSlaves(); + } + + $this->output( + "Completed cleanup, assigned $countAssigned and prefixed $countPrefixed row(s)\n" + ); + } +} + +$maintClass = CleanupUsersWithNoId::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/cleanupWatchlist.php b/www/wiki/maintenance/cleanupWatchlist.php new file mode 100644 index 00000000..64d39dd6 --- /dev/null +++ b/www/wiki/maintenance/cleanupWatchlist.php @@ -0,0 +1,99 @@ + + * 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 + * @author Brion Vibber + * @ingroup Maintenance + */ + +require_once __DIR__ . '/cleanupTable.inc'; + +/** + * Maintenance script to remove broken, unparseable titles in the watchlist table. + * + * @ingroup Maintenance + */ +class WatchlistCleanup extends TableCleanup { + protected $defaultParams = [ + 'table' => 'watchlist', + 'index' => [ 'wl_user', 'wl_namespace', 'wl_title' ], + 'conds' => [], + 'callback' => 'processRow' + ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Script to remove broken, unparseable titles in the Watchlist' ); + $this->addOption( 'fix', 'Actually remove entries; without will only report.' ); + } + + function execute() { + if ( !$this->hasOption( 'fix' ) ) { + $this->output( "Dry run only: use --fix to enable updates\n" ); + } + parent::execute(); + } + + protected function processRow( $row ) { + global $wgContLang; + $current = Title::makeTitle( $row->wl_namespace, $row->wl_title ); + $display = $current->getPrefixedText(); + $verified = $wgContLang->normalize( $display ); + $title = Title::newFromText( $verified ); + + if ( $row->wl_user == 0 || is_null( $title ) || !$title->equals( $current ) ) { + $this->output( "invalid watch by {$row->wl_user} for " + . "({$row->wl_namespace}, \"{$row->wl_title}\")\n" ); + $updated = $this->removeWatch( $row ); + $this->progress( $updated ); + + return; + } + $this->progress( 0 ); + } + + private function removeWatch( $row ) { + if ( !$this->dryrun && $this->hasOption( 'fix' ) ) { + $dbw = $this->getDB( DB_MASTER ); + $dbw->delete( + 'watchlist', [ + 'wl_user' => $row->wl_user, + 'wl_namespace' => $row->wl_namespace, + 'wl_title' => $row->wl_title ], + __METHOD__ + ); + + $this->output( "- removed\n" ); + + return 1; + } else { + return 0; + } + } +} + +$maintClass = WatchlistCleanup::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/clearInterwikiCache.php b/www/wiki/maintenance/clearInterwikiCache.php new file mode 100644 index 00000000..8579f0f4 --- /dev/null +++ b/www/wiki/maintenance/clearInterwikiCache.php @@ -0,0 +1,58 @@ +addDescription( 'Clear all interwiki links for all languages from the cache' ); + } + + public function execute() { + global $wgLocalDatabases, $wgMemc; + $dbr = $this->getDB( DB_REPLICA ); + $res = $dbr->select( 'interwiki', [ 'iw_prefix' ], '', __METHOD__ ); + $prefixes = []; + foreach ( $res as $row ) { + $prefixes[] = $row->iw_prefix; + } + + foreach ( $wgLocalDatabases as $db ) { + $this->output( "$db..." ); + foreach ( $prefixes as $prefix ) { + $wgMemc->delete( "$db:interwiki:$prefix" ); + } + $this->output( "done\n" ); + } + } +} + +$maintClass = ClearInterwikiCache::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/commandLine.inc b/www/wiki/maintenance/commandLine.inc new file mode 100644 index 00000000..8232d529 --- /dev/null +++ b/www/wiki/maintenance/commandLine.inc @@ -0,0 +1,71 @@ +addOption( $name, '', false, true ); + } + foreach ( $optionsWithoutArgs as $name ) { + $this->addOption( $name, '', false, false ); + } + } + + /** + * No help, it would just be misleading since it misses custom options + * @param bool $force + */ + protected function maybeHelp( $force = false ) { + if ( !$force ) { + return; + } + parent::maybeHelp( true ); + } + + public function execute() { + // phpcs:ignore MediaWiki.NamingConventions.ValidGlobalName.wgPrefix + global $args, $options; + + $args = $this->mArgs; + $options = $this->mOptions; + } +} + +$maintClass = CommandLineInc::class; +require RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/compareParserCache.php b/www/wiki/maintenance/compareParserCache.php new file mode 100644 index 00000000..b12974b0 --- /dev/null +++ b/www/wiki/maintenance/compareParserCache.php @@ -0,0 +1,112 @@ +addDescription( 'Parse random pages and compare output to cache.' ); + $this->addOption( 'namespace', 'Page namespace number', true, true ); + $this->addOption( 'maxpages', 'Number of pages to try', true, true ); + } + + public function execute() { + $pages = $this->getOption( 'maxpages' ); + + $dbr = $this->getDB( DB_REPLICA ); + + $totalsec = 0.0; + $scanned = 0; + $withcache = 0; + $withdiff = 0; + $parserCache = MediaWikiServices::getInstance()->getParserCache(); + while ( $pages-- > 0 ) { + $row = $dbr->selectRow( 'page', + // @todo Title::selectFields() or Title::getQueryInfo() or something + [ + 'page_namespace', 'page_title', 'page_id', + 'page_len', 'page_is_redirect', 'page_latest', + ], + [ + 'page_namespace' => $this->getOption( 'namespace' ), + 'page_is_redirect' => 0, + 'page_random >= ' . wfRandom() + ], + __METHOD__, + [ + 'ORDER BY' => 'page_random', + ] + ); + + if ( !$row ) { + continue; + } + ++$scanned; + + $title = Title::newFromRow( $row ); + $page = WikiPage::factory( $title ); + $revision = $page->getRevision(); + $content = $revision->getContent( Revision::RAW ); + + $parserOptions = $page->makeParserOptions( 'canonical' ); + + $parserOutputOld = $parserCache->get( $page, $parserOptions ); + + if ( $parserOutputOld ) { + $t1 = microtime( true ); + $parserOutputNew = $content->getParserOutput( + $title, $revision->getId(), $parserOptions, false ); + $sec = microtime( true ) - $t1; + $totalsec += $sec; + + $this->output( "Parsed '{$title->getPrefixedText()}' in $sec seconds.\n" ); + + $this->output( "Found cache entry found for '{$title->getPrefixedText()}'..." ); + $oldHtml = trim( preg_replace( '##Us', '', $parserOutputOld->getText() ) ); + $newHtml = trim( preg_replace( '##Us', '', $parserOutputNew->getText() ) ); + $diff = wfDiff( $oldHtml, $newHtml ); + if ( strlen( $diff ) ) { + $this->output( "differences found:\n\n$diff\n\n" ); + ++$withdiff; + } else { + $this->output( "No differences found.\n" ); + } + ++$withcache; + } else { + $this->output( "No parser cache entry found for '{$title->getPrefixedText()}'.\n" ); + } + } + + $ave = $totalsec ? $totalsec / $scanned : 0; + $this->output( "Checked $scanned pages; $withcache had prior cache entries.\n" ); + $this->output( "Pages with differences found: $withdiff\n" ); + $this->output( "Average parse time: $ave sec\n" ); + } +} + +$maintClass = CompareParserCache::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/compareParsers.php b/www/wiki/maintenance/compareParsers.php new file mode 100644 index 00000000..fe6e604d --- /dev/null +++ b/www/wiki/maintenance/compareParsers.php @@ -0,0 +1,189 @@ +saveFailed = false; + $this->addDescription( 'Run a file or dump with several parsers' ); + $this->addOption( 'parser1', 'The first parser to compare.', true, true ); + $this->addOption( 'parser2', 'The second parser to compare.', true, true ); + $this->addOption( 'tidy', 'Run tidy on the articles.', false, false ); + $this->addOption( + 'save-failed', + 'Folder in which articles which differ will be stored.', + false, + true + ); + $this->addOption( 'show-diff', 'Show a diff of the two renderings.', false, false ); + $this->addOption( + 'diff-bin', + 'Binary to use for diffing (can also be provided by DIFF env var).', + false, + false + ); + $this->addOption( + 'strip-parameters', + 'Remove parameters of html tags to increase readability.', + false, + false + ); + $this->addOption( + 'show-parsed-output', + 'Show the parsed html if both Parsers give the same output.', + false, + false + ); + } + + public function checkOptions() { + if ( $this->hasOption( 'save-failed' ) ) { + $this->saveFailed = $this->getOption( 'save-failed' ); + } + + $this->stripParametersEnabled = $this->hasOption( 'strip-parameters' ); + $this->showParsedOutput = $this->hasOption( 'show-parsed-output' ); + + $this->showDiff = $this->hasOption( 'show-diff' ); + if ( $this->showDiff ) { + $bin = $this->getOption( 'diff-bin', getenv( 'DIFF' ) ); + if ( $bin != '' ) { + global $wgDiff; + $wgDiff = $bin; + } + } + + $user = new User(); + $this->options = ParserOptions::newFromUser( $user ); + + if ( $this->hasOption( 'tidy' ) ) { + global $wgUseTidy; + if ( !$wgUseTidy ) { + $this->fatalError( 'Tidy was requested but $wgUseTidy is not set in LocalSettings.php' ); + } + $this->options->setTidy( true ); + } + + $this->failed = 0; + } + + public function conclusions() { + $this->error( "{$this->failed} failed revisions out of {$this->count}" ); + if ( $this->count > 0 ) { + $this->output( " (" . ( $this->failed / $this->count ) . "%)\n" ); + } + } + + function stripParameters( $text ) { + if ( !$this->stripParametersEnabled ) { + return $text; + } + + return preg_replace( '/(]+>/', '$1>', $text ); + } + + /** + * Callback function for each revision, parse with both parsers and compare + * @param Revision $rev + */ + public function processRevision( $rev ) { + $title = $rev->getTitle(); + + $parser1Name = $this->getOption( 'parser1' ); + $parser2Name = $this->getOption( 'parser2' ); + + self::checkParserLocally( $parser1Name ); + self::checkParserLocally( $parser2Name ); + + $parser1 = new $parser1Name(); + $parser2 = new $parser2Name(); + + $content = $rev->getContent(); + + if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) { + $this->error( "Page {$title->getPrefixedText()} does not contain wikitext " + . "but {$content->getModel()}\n" ); + + return; + } + + $text = strval( $content->getNativeData() ); + + $output1 = $parser1->parse( $text, $title, $this->options ); + $output2 = $parser2->parse( $text, $title, $this->options ); + + if ( $output1->getText() != $output2->getText() ) { + $this->failed++; + $this->error( "Parsing for {$title->getPrefixedText()} differs\n" ); + + if ( $this->saveFailed ) { + file_put_contents( + $this->saveFailed . '/' . rawurlencode( $title->getPrefixedText() ) . ".txt", + $text + ); + } + if ( $this->showDiff ) { + $this->output( wfDiff( + $this->stripParameters( $output1->getText() ), + $this->stripParameters( $output2->getText() ), + '' + ) ); + } + } else { + $this->output( $title->getPrefixedText() . "\tOK\n" ); + + if ( $this->showParsedOutput ) { + $this->output( $this->stripParameters( $output1->getText() ) ); + } + } + } + + private static function checkParserLocally( $parserName ) { + /* Look for the parser in a file appropiately named in the current folder */ + if ( !class_exists( $parserName ) && file_exists( "$parserName.php" ) ) { + global $wgAutoloadClasses; + $wgAutoloadClasses[$parserName] = realpath( '.' ) . "/$parserName.php"; + } + } +} + +$maintClass = CompareParsers::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/convertExtensionToRegistration.php b/www/wiki/maintenance/convertExtensionToRegistration.php new file mode 100644 index 00000000..4ae95587 --- /dev/null +++ b/www/wiki/maintenance/convertExtensionToRegistration.php @@ -0,0 +1,312 @@ + 'handleMessagesDirs', + 'ExtensionMessagesFiles' => 'handleExtensionMessagesFiles', + 'AutoloadClasses' => 'removeAbsolutePath', + 'ExtensionCredits' => 'handleCredits', + 'ResourceModules' => 'handleResourceModules', + 'ResourceModuleSkinStyles' => 'handleResourceModules', + 'Hooks' => 'handleHooks', + 'ExtensionFunctions' => 'handleExtensionFunctions', + 'ParserTestFiles' => 'removeAbsolutePath', + ]; + + /** + * Things that were formerly globals and should still be converted + * + * @var array + */ + protected $formerGlobals = [ + 'TrackingCategories', + ]; + + /** + * No longer supported globals (with reason) should not be converted and emit a warning + * + * @var array + */ + protected $noLongerSupportedGlobals = [ + 'SpecialPageGroups' => 'deprecated', // Deprecated 1.21, removed in 1.26 + ]; + + /** + * Keys that should be put at the top of the generated JSON file (T86608) + * + * @var array + */ + protected $promote = [ + 'name', + 'namemsg', + 'version', + 'author', + 'url', + 'description', + 'descriptionmsg', + 'license-name', + 'type', + ]; + + private $json, $dir, $hasWarning = false; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Converts extension entry points to the new JSON registration format' ); + $this->addArg( 'path', 'Location to the PHP entry point you wish to convert', + /* $required = */ true ); + $this->addOption( 'skin', 'Whether to write to skin.json', false, false ); + $this->addOption( 'config-prefix', 'Custom prefix for configuration settings', false, true ); + } + + protected function getAllGlobals() { + $processor = new ReflectionClass( ExtensionProcessor::class ); + $settings = $processor->getProperty( 'globalSettings' ); + $settings->setAccessible( true ); + return array_merge( $settings->getValue(), $this->formerGlobals ); + } + + public function execute() { + // Extensions will do stuff like $wgResourceModules += array(...) which is a + // fatal unless an array is already set. So set an empty value. + // And use the weird $__settings name to avoid any conflicts + // with real poorly named settings. + $__settings = array_merge( $this->getAllGlobals(), array_keys( $this->custom ) ); + foreach ( $__settings as $var ) { + $var = 'wg' . $var; + $$var = []; + } + unset( $var ); + $arg = $this->getArg( 0 ); + if ( !is_file( $arg ) ) { + $this->fatalError( "$arg is not a file." ); + } + require $arg; + unset( $arg ); + // Try not to create any local variables before this line + $vars = get_defined_vars(); + unset( $vars['this'] ); + unset( $vars['__settings'] ); + $this->dir = dirname( realpath( $this->getArg( 0 ) ) ); + $this->json = []; + $globalSettings = $this->getAllGlobals(); + $configPrefix = $this->getOption( 'config-prefix', 'wg' ); + if ( $configPrefix !== 'wg' ) { + $this->json['config']['_prefix'] = $configPrefix; + } + foreach ( $vars as $name => $value ) { + $realName = substr( $name, 2 ); // Strip 'wg' + if ( $realName === false ) { + continue; + } + + // If it's an empty array that we likely set, skip it + if ( is_array( $value ) && count( $value ) === 0 && in_array( $realName, $__settings ) ) { + continue; + } + + if ( isset( $this->custom[$realName] ) ) { + call_user_func_array( [ $this, $this->custom[$realName] ], + [ $realName, $value, $vars ] ); + } elseif ( in_array( $realName, $globalSettings ) ) { + $this->json[$realName] = $value; + } elseif ( array_key_exists( $realName, $this->noLongerSupportedGlobals ) ) { + $this->output( 'Warning: Skipped global "' . $name . '" (' . + $this->noLongerSupportedGlobals[$realName] . '). ' . + "Please update the entry point before convert to registration.\n" ); + $this->hasWarning = true; + } elseif ( strpos( $name, $configPrefix ) === 0 ) { + // Most likely a config setting + $this->json['config'][substr( $name, strlen( $configPrefix ) )] = [ 'value' => $value ]; + } elseif ( $configPrefix !== 'wg' && strpos( $name, 'wg' ) === 0 ) { + // Warn about this + $this->output( 'Warning: Skipped global "' . $name . '" (' . + 'config prefix is "' . $configPrefix . '"). ' . + "Please check that this setting isn't needed.\n" ); + } + } + + // check, if the extension requires composer libraries + if ( $this->needsComposerAutoloader( dirname( $this->getArg( 0 ) ) ) ) { + // set the load composer autoloader automatically property + $this->output( "Detected composer dependencies, setting 'load_composer_autoloader' to true.\n" ); + $this->json['load_composer_autoloader'] = true; + } + + // Move some keys to the top + $out = []; + foreach ( $this->promote as $key ) { + if ( isset( $this->json[$key] ) ) { + $out[$key] = $this->json[$key]; + unset( $this->json[$key] ); + } + } + // Set a requirement on the MediaWiki version that the current MANIFEST_VERSION + // was introduced in. + $out['requires'] = [ + ExtensionRegistry::MEDIAWIKI_CORE => ExtensionRegistry::MANIFEST_VERSION_MW_VERSION + ]; + $out += $this->json; + // Put this at the bottom + $out['manifest_version'] = ExtensionRegistry::MANIFEST_VERSION; + $type = $this->hasOption( 'skin' ) ? 'skin' : 'extension'; + $fname = "{$this->dir}/$type.json"; + $prettyJSON = FormatJson::encode( $out, "\t", FormatJson::ALL_OK ); + file_put_contents( $fname, $prettyJSON . "\n" ); + $this->output( "Wrote output to $fname.\n" ); + if ( $this->hasWarning ) { + $this->output( "Found warnings! Please resolve the warnings and rerun this script.\n" ); + } + } + + protected function handleExtensionFunctions( $realName, $value ) { + foreach ( $value as $func ) { + if ( $func instanceof Closure ) { + $this->fatalError( "Error: Closures cannot be converted to JSON. " . + "Please move your extension function somewhere else." + ); + } + // check if $func exists in the global scope + if ( function_exists( $func ) ) { + $this->fatalError( "Error: Global functions cannot be converted to JSON. " . + "Please move your extension function ($func) into a class." + ); + } + } + + $this->json[$realName] = $value; + } + + protected function handleMessagesDirs( $realName, $value ) { + foreach ( $value as $key => $dirs ) { + foreach ( (array)$dirs as $dir ) { + $this->json[$realName][$key][] = $this->stripPath( $dir, $this->dir ); + } + } + } + + protected function handleExtensionMessagesFiles( $realName, $value, $vars ) { + foreach ( $value as $key => $file ) { + $strippedFile = $this->stripPath( $file, $this->dir ); + if ( isset( $vars['wgMessagesDirs'][$key] ) ) { + $this->output( + "Note: Ignoring PHP shim $strippedFile. " . + "If your extension no longer supports versions of MediaWiki " . + "older than 1.23.0, you can safely delete it.\n" + ); + } else { + $this->json[$realName][$key] = $strippedFile; + } + } + } + + private function stripPath( $val, $dir ) { + if ( $val === $dir ) { + $val = ''; + } elseif ( strpos( $val, $dir ) === 0 ) { + // +1 is for the trailing / that won't be in $this->dir + $val = substr( $val, strlen( $dir ) + 1 ); + } + + return $val; + } + + protected function removeAbsolutePath( $realName, $value ) { + $out = []; + foreach ( $value as $key => $val ) { + $out[$key] = $this->stripPath( $val, $this->dir ); + } + $this->json[$realName] = $out; + } + + protected function handleCredits( $realName, $value ) { + $keys = array_keys( $value ); + $this->json['type'] = $keys[0]; + $values = array_values( $value ); + foreach ( $values[0][0] as $name => $val ) { + if ( $name !== 'path' ) { + $this->json[$name] = $val; + } + } + } + + public function handleHooks( $realName, $value ) { + foreach ( $value as $hookName => &$handlers ) { + if ( $hookName === 'UnitTestsList' ) { + $this->output( "Note: the UnitTestsList hook is no longer necessary as " . + "long as your tests are located in the \"tests/phpunit/\" directory. " . + "Please see for more details.\n" + ); + } + foreach ( $handlers as $func ) { + if ( $func instanceof Closure ) { + $this->fatalError( "Error: Closures cannot be converted to JSON. " . + "Please move the handler for $hookName somewhere else." + ); + } + // Check if $func exists in the global scope + if ( function_exists( $func ) ) { + $this->fatalError( "Error: Global functions cannot be converted to JSON. " . + "Please move the handler for $hookName inside a class." + ); + } + } + if ( count( $handlers ) === 1 ) { + $handlers = $handlers[0]; + } + } + $this->json[$realName] = $value; + } + + protected function handleResourceModules( $realName, $value ) { + $defaults = []; + $remote = $this->hasOption( 'skin' ) ? 'remoteSkinPath' : 'remoteExtPath'; + foreach ( $value as $name => $data ) { + if ( isset( $data['localBasePath'] ) ) { + $data['localBasePath'] = $this->stripPath( $data['localBasePath'], $this->dir ); + if ( !$defaults ) { + $defaults['localBasePath'] = $data['localBasePath']; + unset( $data['localBasePath'] ); + if ( isset( $data[$remote] ) ) { + $defaults[$remote] = $data[$remote]; + unset( $data[$remote] ); + } + } else { + if ( $data['localBasePath'] === $defaults['localBasePath'] ) { + unset( $data['localBasePath'] ); + } + if ( isset( $data[$remote] ) && isset( $defaults[$remote] ) + && $data[$remote] === $defaults[$remote] + ) { + unset( $data[$remote] ); + } + } + } + + $this->json[$realName][$name] = $data; + } + if ( $defaults ) { + $this->json['ResourceFileModulePaths'] = $defaults; + } + } + + protected function needsComposerAutoloader( $path ) { + $path .= '/composer.json'; + if ( file_exists( $path ) ) { + // assume, that the composer.json file is in the root of the extension path + $composerJson = new ComposerJson( $path ); + // check, if there are some dependencies in the require section + if ( $composerJson->getRequiredDependencies() ) { + return true; + } + } + return false; + } +} + +$maintClass = ConvertExtensionToRegistration::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/convertLinks.php b/www/wiki/maintenance/convertLinks.php new file mode 100644 index 00000000..8cd02976 --- /dev/null +++ b/www/wiki/maintenance/convertLinks.php @@ -0,0 +1,306 @@ +ID) to the new schema (ID->ID). + * + * 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 Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to convert from the old links schema (string->ID) + * to the new schema (ID->ID). + * + * The wiki should be put into read-only mode while this script executes. + * + * @ingroup Maintenance + */ +class ConvertLinks extends Maintenance { + private $logPerformance; + + public function __construct() { + parent::__construct(); + $this->addDescription( + 'Convert from the old links schema (string->ID) to the new schema (ID->ID). ' + . 'The wiki should be put into read-only mode while this script executes' ); + + $this->addArg( 'logperformance', "Log performance to perfLogFilename.", false ); + $this->addArg( + 'perfLogFilename', + "Filename where performance is logged if --logperformance was set " + . "(defaults to 'convLinksPerf.txt').", + false + ); + $this->addArg( + 'keep-links-table', + "Don't overwrite the old links table with the new one, leave the new table at links_temp.", + false + ); + $this->addArg( + 'nokeys', + /* (What about InnoDB?) */ + "Don't create keys, and so allow duplicates in the new links table.\n" + . "This gives a huge speed improvement for very large links tables which are MyISAM.", + false + ); + } + + public function getDbType() { + return Maintenance::DB_ADMIN; + } + + public function execute() { + $dbw = $this->getDB( DB_MASTER ); + + $type = $dbw->getType(); + if ( $type != 'mysql' ) { + $this->output( "Link table conversion not necessary for $type\n" ); + + return; + } + + global $wgContLang; + + # counters etc + $numBadLinks = $curRowsRead = 0; + + # total tuples INSERTed into links_temp + $totalTuplesInserted = 0; + + # whether or not to give progress reports while reading IDs from cur table + $reportCurReadProgress = true; + + # number of rows between progress reports + $curReadReportInterval = 1000; + + # whether or not to give progress reports during conversion + $reportLinksConvProgress = true; + + # number of rows per INSERT + $linksConvInsertInterval = 1000; + + $initialRowOffset = 0; + + # not used yet; highest row number from links table to process + # $finalRowOffset = 0; + + $overwriteLinksTable = !$this->hasOption( 'keep-links-table' ); + $noKeys = $this->hasOption( 'noKeys' ); + $this->logPerformance = $this->hasOption( 'logperformance' ); + $perfLogFilename = $this->getArg( 'perfLogFilename', "convLinksPerf.txt" ); + + # -------------------------------------------------------------------- + + list( $cur, $links, $links_temp, $links_backup ) = + $dbw->tableNamesN( 'cur', 'links', 'links_temp', 'links_backup' ); + + if ( $dbw->tableExists( 'pagelinks' ) ) { + $this->output( "...have pagelinks; skipping old links table updates\n" ); + + return; + } + + $res = $dbw->query( "SELECT l_from FROM $links LIMIT 1" ); + if ( $dbw->fieldType( $res, 0 ) == "int" ) { + $this->output( "Schema already converted\n" ); + + return; + } + + $res = $dbw->query( "SELECT COUNT(*) AS count FROM $links" ); + $row = $dbw->fetchObject( $res ); + $numRows = $row->count; + $dbw->freeResult( $res ); + + if ( $numRows == 0 ) { + $this->output( "Updating schema (no rows to convert)...\n" ); + $this->createTempTable(); + } else { + $fh = false; + if ( $this->logPerformance ) { + $fh = fopen( $perfLogFilename, "w" ); + if ( !$fh ) { + $this->error( "Couldn't open $perfLogFilename" ); + $this->logPerformance = false; + } + } + $baseTime = $startTime = microtime( true ); + # Create a title -> cur_id map + $this->output( "Loading IDs from $cur table...\n" ); + $this->performanceLog( $fh, "Reading $numRows rows from cur table...\n" ); + $this->performanceLog( $fh, "rows read vs seconds elapsed:\n" ); + + $dbw->bufferResults( false ); + $res = $dbw->query( "SELECT cur_namespace,cur_title,cur_id FROM $cur" ); + $ids = []; + + foreach ( $res as $row ) { + $title = $row->cur_title; + if ( $row->cur_namespace ) { + $title = $wgContLang->getNsText( $row->cur_namespace ) . ":$title"; + } + $ids[$title] = $row->cur_id; + $curRowsRead++; + if ( $reportCurReadProgress ) { + if ( ( $curRowsRead % $curReadReportInterval ) == 0 ) { + $this->performanceLog( + $fh, + $curRowsRead . " " . ( microtime( true ) - $baseTime ) . "\n" + ); + $this->output( "\t$curRowsRead rows of $cur table read.\n" ); + } + } + } + $dbw->freeResult( $res ); + $dbw->bufferResults( true ); + $this->output( "Finished loading IDs.\n\n" ); + $this->performanceLog( + $fh, + "Took " . ( microtime( true ) - $baseTime ) . " seconds to load IDs.\n\n" + ); + + # -------------------------------------------------------------------- + + # Now, step through the links table (in chunks of $linksConvInsertInterval rows), + # convert, and write to the new table. + $this->createTempTable(); + $this->performanceLog( $fh, "Resetting timer.\n\n" ); + $baseTime = microtime( true ); + $this->output( "Processing $numRows rows from $links table...\n" ); + $this->performanceLog( $fh, "Processing $numRows rows from $links table...\n" ); + $this->performanceLog( $fh, "rows inserted vs seconds elapsed:\n" ); + + for ( $rowOffset = $initialRowOffset; $rowOffset < $numRows; + $rowOffset += $linksConvInsertInterval + ) { + $sqlRead = "SELECT * FROM $links "; + $sqlRead = $dbw->limitResult( $sqlRead, $linksConvInsertInterval, $rowOffset ); + $res = $dbw->query( $sqlRead ); + if ( $noKeys ) { + $sqlWrite = [ "INSERT INTO $links_temp (l_from,l_to) VALUES " ]; + } else { + $sqlWrite = [ "INSERT IGNORE INTO $links_temp (l_from,l_to) VALUES " ]; + } + + $tuplesAdded = 0; # no tuples added to INSERT yet + foreach ( $res as $row ) { + $fromTitle = $row->l_from; + if ( array_key_exists( $fromTitle, $ids ) ) { # valid title + $from = $ids[$fromTitle]; + $to = $row->l_to; + if ( $tuplesAdded != 0 ) { + $sqlWrite[] = ","; + } + $sqlWrite[] = "($from,$to)"; + $tuplesAdded++; + } else { # invalid title + $numBadLinks++; + } + } + $dbw->freeResult( $res ); + # $this->output( "rowOffset: $rowOffset\ttuplesAdded: " + # . "$tuplesAdded\tnumBadLinks: $numBadLinks\n" ); + if ( $tuplesAdded != 0 ) { + if ( $reportLinksConvProgress ) { + $this->output( "Inserting $tuplesAdded tuples into $links_temp..." ); + } + $dbw->query( implode( "", $sqlWrite ) ); + $totalTuplesInserted += $tuplesAdded; + if ( $reportLinksConvProgress ) { + $this->output( " done. Total $totalTuplesInserted tuples inserted.\n" ); + $this->performanceLog( + $fh, + $totalTuplesInserted . " " . ( microtime( true ) - $baseTime ) . "\n" + ); + } + } + } + $this->output( "$totalTuplesInserted valid titles and " + . "$numBadLinks invalid titles were processed.\n\n" ); + $this->performanceLog( + $fh, + "$totalTuplesInserted valid titles and $numBadLinks invalid titles were processed.\n" + ); + $this->performanceLog( + $fh, + "Total execution time: " . ( microtime( true ) - $startTime ) . " seconds.\n" + ); + if ( $this->logPerformance ) { + fclose( $fh ); + } + } + # -------------------------------------------------------------------- + + if ( $overwriteLinksTable ) { + # Check for existing links_backup, and delete it if it exists. + $this->output( "Dropping backup links table if it exists..." ); + $dbw->query( "DROP TABLE IF EXISTS $links_backup", __METHOD__ ); + $this->output( " done.\n" ); + + # Swap in the new table, and move old links table to links_backup + $this->output( "Swapping tables '$links' to '$links_backup'; '$links_temp' to '$links'..." ); + $dbw->query( "RENAME TABLE links TO $links_backup, $links_temp TO $links", __METHOD__ ); + $this->output( " done.\n\n" ); + + $this->output( "Conversion complete. The old table remains at $links_backup;\n" ); + $this->output( "delete at your leisure.\n" ); + } else { + $this->output( "Conversion complete. The converted table is at $links_temp;\n" ); + $this->output( "the original links table is unchanged.\n" ); + } + } + + private function createTempTable() { + $dbConn = $this->getDB( DB_MASTER ); + + if ( !( $dbConn->isOpen() ) ) { + $this->output( "Opening connection to database failed.\n" ); + + return; + } + $links_temp = $dbConn->tableName( 'links_temp' ); + + $this->output( "Dropping temporary links table if it exists..." ); + $dbConn->query( "DROP TABLE IF EXISTS $links_temp" ); + $this->output( " done.\n" ); + + $this->output( "Creating temporary links table..." ); + if ( $this->hasOption( 'noKeys' ) ) { + $dbConn->query( "CREATE TABLE $links_temp ( " . + "l_from int(8) unsigned NOT NULL default '0', " . + "l_to int(8) unsigned NOT NULL default '0')" ); + } else { + $dbConn->query( "CREATE TABLE $links_temp ( " . + "l_from int(8) unsigned NOT NULL default '0', " . + "l_to int(8) unsigned NOT NULL default '0', " . + "UNIQUE KEY l_from(l_from,l_to), " . + "KEY (l_to))" ); + } + $this->output( " done.\n\n" ); + } + + private function performanceLog( $fh, $text ) { + if ( $this->logPerformance ) { + fwrite( $fh, $text ); + } + } +} + +$maintClass = ConvertLinks::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/convertUserOptions.php b/www/wiki/maintenance/convertUserOptions.php new file mode 100644 index 00000000..c1a096fd --- /dev/null +++ b/www/wiki/maintenance/convertUserOptions.php @@ -0,0 +1,124 @@ +addDescription( 'Convert user options from old to new system' ); + $this->setBatchSize( 50 ); + } + + public function execute() { + $this->output( "...batch conversion of user_options: " ); + $id = 0; + $dbw = $this->getDB( DB_MASTER ); + + if ( !$dbw->fieldExists( 'user', 'user_options', __METHOD__ ) ) { + $this->output( "nothing to migrate. " ); + + return; + } + while ( $id !== null ) { + $res = $dbw->select( 'user', + [ 'user_id', 'user_options' ], + [ + 'user_id > ' . $dbw->addQuotes( $id ), + "user_options != " . $dbw->addQuotes( '' ), + ], + __METHOD__, + [ + 'ORDER BY' => 'user_id', + 'LIMIT' => $this->getBatchSize(), + ] + ); + $id = $this->convertOptionBatch( $res, $dbw ); + + wfWaitForSlaves(); + + if ( $id ) { + $this->output( "--Converted to ID $id\n" ); + } + } + $this->output( "done. Converted " . $this->mConversionCount . " user records.\n" ); + } + + /** + * @param ResultWrapper $res + * @param IDatabase $dbw + * @return null|int + */ + function convertOptionBatch( $res, $dbw ) { + $id = null; + foreach ( $res as $row ) { + $this->mConversionCount++; + $insertRows = []; + foreach ( explode( "\n", $row->user_options ) as $s ) { + $m = []; + if ( !preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) { + continue; + } + + // MW < 1.16 would save even default values. Filter them out + // here (as in User) to avoid adding many unnecessary rows. + $defaultOption = User::getDefaultOption( $m[1] ); + if ( is_null( $defaultOption ) || $m[2] != $defaultOption ) { + $insertRows[] = [ + 'up_user' => $row->user_id, + 'up_property' => $m[1], + 'up_value' => $m[2], + ]; + } + } + + if ( count( $insertRows ) ) { + $dbw->insert( 'user_properties', $insertRows, __METHOD__, [ 'IGNORE' ] ); + } + + $dbw->update( + 'user', + [ 'user_options' => '' ], + [ 'user_id' => $row->user_id ], + __METHOD__ + ); + $id = $row->user_id; + } + + return $id; + } +} + +$maintClass = ConvertUserOptions::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/copyFileBackend.php b/www/wiki/maintenance/copyFileBackend.php new file mode 100644 index 00000000..3c7ffba5 --- /dev/null +++ b/www/wiki/maintenance/copyFileBackend.php @@ -0,0 +1,378 @@ + stat) Pre-computed dst stat entries from listings */ + protected $statCache = null; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Copy files in one backend to another.' ); + $this->addOption( 'src', 'Backend containing the source files', true, true ); + $this->addOption( 'dst', 'Backend where files should be copied to', true, true ); + $this->addOption( 'containers', 'Pipe separated list of containers', true, true ); + $this->addOption( 'subdir', 'Only do items in this child directory', false, true ); + $this->addOption( 'ratefile', 'File to check periodically for batch size', false, true ); + $this->addOption( 'prestat', 'Stat the destination files first (try to use listings)' ); + $this->addOption( 'skiphash', 'Skip SHA-1 sync checks for files' ); + $this->addOption( 'missingonly', 'Only copy files missing from destination listing' ); + $this->addOption( 'syncviadelete', 'Delete destination files missing from source listing' ); + $this->addOption( 'utf8only', 'Skip source files that do not have valid UTF-8 names' ); + $this->setBatchSize( 50 ); + } + + public function execute() { + $src = FileBackendGroup::singleton()->get( $this->getOption( 'src' ) ); + $dst = FileBackendGroup::singleton()->get( $this->getOption( 'dst' ) ); + $containers = explode( '|', $this->getOption( 'containers' ) ); + $subDir = rtrim( $this->getOption( 'subdir', '' ), '/' ); + + $rateFile = $this->getOption( 'ratefile' ); + + foreach ( $containers as $container ) { + if ( $subDir != '' ) { + $backendRel = "$container/$subDir"; + $this->output( "Doing container '$container', directory '$subDir'...\n" ); + } else { + $backendRel = $container; + $this->output( "Doing container '$container'...\n" ); + } + + if ( $this->hasOption( 'missingonly' ) ) { + $this->output( "\tBuilding list of missing files..." ); + $srcPathsRel = $this->getListingDiffRel( $src, $dst, $backendRel ); + $this->output( count( $srcPathsRel ) . " file(s) need to be copied.\n" ); + } else { + $srcPathsRel = $src->getFileList( [ + 'dir' => $src->getRootStoragePath() . "/$backendRel", + 'adviseStat' => true // avoid HEADs + ] ); + if ( $srcPathsRel === null ) { + $this->fatalError( "Could not list files in $container." ); + } + } + + if ( $this->getOption( 'prestat' ) && !$this->hasOption( 'missingonly' ) ) { + // Build the stat cache for the destination files + $this->output( "\tBuilding destination stat cache..." ); + $dstPathsRel = $dst->getFileList( [ + 'dir' => $dst->getRootStoragePath() . "/$backendRel", + 'adviseStat' => true // avoid HEADs + ] ); + if ( $dstPathsRel === null ) { + $this->fatalError( "Could not list files in $container." ); + } + $this->statCache = []; + foreach ( $dstPathsRel as $dstPathRel ) { + $path = $dst->getRootStoragePath() . "/$backendRel/$dstPathRel"; + $this->statCache[sha1( $path )] = $dst->getFileStat( [ 'src' => $path ] ); + } + $this->output( "done [" . count( $this->statCache ) . " file(s)]\n" ); + } + + $this->output( "\tCopying file(s)...\n" ); + $count = 0; + $batchPaths = []; + foreach ( $srcPathsRel as $srcPathRel ) { + // Check up on the rate file periodically to adjust the concurrency + if ( $rateFile && ( !$count || ( $count % 500 ) == 0 ) ) { + $this->setBatchSize( max( 1, (int)file_get_contents( $rateFile ) ) ); + $this->output( "\tBatch size is now {$this->getBatchSize()}.\n" ); + } + $batchPaths[$srcPathRel] = 1; // remove duplicates + if ( count( $batchPaths ) >= $this->getBatchSize() ) { + $this->copyFileBatch( array_keys( $batchPaths ), $backendRel, $src, $dst ); + $batchPaths = []; // done + } + ++$count; + } + if ( count( $batchPaths ) ) { // left-overs + $this->copyFileBatch( array_keys( $batchPaths ), $backendRel, $src, $dst ); + $batchPaths = []; // done + } + $this->output( "\tCopied $count file(s).\n" ); + + if ( $this->hasOption( 'syncviadelete' ) ) { + $this->output( "\tBuilding list of excess destination files..." ); + $delPathsRel = $this->getListingDiffRel( $dst, $src, $backendRel ); + $this->output( count( $delPathsRel ) . " file(s) need to be deleted.\n" ); + + $this->output( "\tDeleting file(s)...\n" ); + $count = 0; + $batchPaths = []; + foreach ( $delPathsRel as $delPathRel ) { + // Check up on the rate file periodically to adjust the concurrency + if ( $rateFile && ( !$count || ( $count % 500 ) == 0 ) ) { + $this->setBatchSize( max( 1, (int)file_get_contents( $rateFile ) ) ); + $this->output( "\tBatch size is now {$this->getBatchSize()}.\n" ); + } + $batchPaths[$delPathRel] = 1; // remove duplicates + if ( count( $batchPaths ) >= $this->getBatchSize() ) { + $this->delFileBatch( array_keys( $batchPaths ), $backendRel, $dst ); + $batchPaths = []; // done + } + ++$count; + } + if ( count( $batchPaths ) ) { // left-overs + $this->delFileBatch( array_keys( $batchPaths ), $backendRel, $dst ); + $batchPaths = []; // done + } + + $this->output( "\tDeleted $count file(s).\n" ); + } + + if ( $subDir != '' ) { + $this->output( "Finished container '$container', directory '$subDir'.\n" ); + } else { + $this->output( "Finished container '$container'.\n" ); + } + } + + $this->output( "Done.\n" ); + } + + /** + * @param FileBackend $src + * @param FileBackend $dst + * @param string $backendRel + * @return array (rel paths in $src minus those in $dst) + */ + protected function getListingDiffRel( FileBackend $src, FileBackend $dst, $backendRel ) { + $srcPathsRel = $src->getFileList( [ + 'dir' => $src->getRootStoragePath() . "/$backendRel" ] ); + if ( $srcPathsRel === null ) { + $this->fatalError( "Could not list files in source container." ); + } + $dstPathsRel = $dst->getFileList( [ + 'dir' => $dst->getRootStoragePath() . "/$backendRel" ] ); + if ( $dstPathsRel === null ) { + $this->fatalError( "Could not list files in destination container." ); + } + // Get the list of destination files + $relFilesDstSha1 = []; + foreach ( $dstPathsRel as $dstPathRel ) { + $relFilesDstSha1[sha1( $dstPathRel )] = 1; + } + unset( $dstPathsRel ); // free + // Get the list of missing files + $missingPathsRel = []; + foreach ( $srcPathsRel as $srcPathRel ) { + if ( !isset( $relFilesDstSha1[sha1( $srcPathRel )] ) ) { + $missingPathsRel[] = $srcPathRel; + } + } + unset( $srcPathsRel ); // free + + return $missingPathsRel; + } + + /** + * @param array $srcPathsRel + * @param string $backendRel + * @param FileBackend $src + * @param FileBackend $dst + * @return void + */ + protected function copyFileBatch( + array $srcPathsRel, $backendRel, FileBackend $src, FileBackend $dst + ) { + $ops = []; + $fsFiles = []; + $copiedRel = []; // for output message + $wikiId = $src->getWikiId(); + + // Download the batch of source files into backend cache... + if ( $this->hasOption( 'missingonly' ) ) { + $srcPaths = []; + foreach ( $srcPathsRel as $srcPathRel ) { + $srcPaths[] = $src->getRootStoragePath() . "/$backendRel/$srcPathRel"; + } + $t_start = microtime( true ); + $fsFiles = $src->getLocalReferenceMulti( [ 'srcs' => $srcPaths, 'latest' => 1 ] ); + $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 ); + $this->output( "\n\tDownloaded these file(s) [{$elapsed_ms}ms]:\n\t" . + implode( "\n\t", $srcPaths ) . "\n\n" ); + } + + // Determine what files need to be copied over... + foreach ( $srcPathsRel as $srcPathRel ) { + $srcPath = $src->getRootStoragePath() . "/$backendRel/$srcPathRel"; + $dstPath = $dst->getRootStoragePath() . "/$backendRel/$srcPathRel"; + if ( $this->hasOption( 'utf8only' ) && !mb_check_encoding( $srcPath, 'UTF-8' ) ) { + $this->error( "$wikiId: Detected illegal (non-UTF8) path for $srcPath." ); + continue; + } elseif ( !$this->hasOption( 'missingonly' ) + && $this->filesAreSame( $src, $dst, $srcPath, $dstPath ) + ) { + $this->output( "\tAlready have $srcPathRel.\n" ); + continue; // assume already copied... + } + $fsFile = array_key_exists( $srcPath, $fsFiles ) + ? $fsFiles[$srcPath] + : $src->getLocalReference( [ 'src' => $srcPath, 'latest' => 1 ] ); + if ( !$fsFile ) { + $src->clearCache( [ $srcPath ] ); + if ( $src->fileExists( [ 'src' => $srcPath, 'latest' => 1 ] ) === false ) { + $this->error( "$wikiId: File '$srcPath' was listed but does not exist." ); + } else { + $this->error( "$wikiId: Could not get local copy of $srcPath." ); + } + continue; + } elseif ( !$fsFile->exists() ) { + // FSFileBackends just return the path for getLocalReference() and paths with + // illegal slashes may get normalized to a different path. This can cause the + // local reference to not exist...skip these broken files. + $this->error( "$wikiId: Detected possible illegal path for $srcPath." ); + continue; + } + $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed + // Note: prepare() is usually fast for key/value backends + $status = $dst->prepare( [ 'dir' => dirname( $dstPath ), 'bypassReadOnly' => 1 ] ); + if ( !$status->isOK() ) { + $this->error( print_r( $status->getErrorsArray(), true ) ); + $this->fatalError( "$wikiId: Could not copy $srcPath to $dstPath." ); + } + $ops[] = [ 'op' => 'store', + 'src' => $fsFile->getPath(), 'dst' => $dstPath, 'overwrite' => 1 ]; + $copiedRel[] = $srcPathRel; + } + + // Copy in the batch of source files... + $t_start = microtime( true ); + $status = $dst->doQuickOperations( $ops, [ 'bypassReadOnly' => 1 ] ); + if ( !$status->isOK() ) { + sleep( 10 ); // wait and retry copy again + $status = $dst->doQuickOperations( $ops, [ 'bypassReadOnly' => 1 ] ); + } + $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 ); + if ( !$status->isOK() ) { + $this->error( print_r( $status->getErrorsArray(), true ) ); + $this->fatalError( "$wikiId: Could not copy file batch." ); + } elseif ( count( $copiedRel ) ) { + $this->output( "\n\tCopied these file(s) [{$elapsed_ms}ms]:\n\t" . + implode( "\n\t", $copiedRel ) . "\n\n" ); + } + } + + /** + * @param array $dstPathsRel + * @param string $backendRel + * @param FileBackend $dst + * @return void + */ + protected function delFileBatch( + array $dstPathsRel, $backendRel, FileBackend $dst + ) { + $ops = []; + $deletedRel = []; // for output message + $wikiId = $dst->getWikiId(); + + // Determine what files need to be copied over... + foreach ( $dstPathsRel as $dstPathRel ) { + $dstPath = $dst->getRootStoragePath() . "/$backendRel/$dstPathRel"; + $ops[] = [ 'op' => 'delete', 'src' => $dstPath ]; + $deletedRel[] = $dstPathRel; + } + + // Delete the batch of source files... + $t_start = microtime( true ); + $status = $dst->doQuickOperations( $ops, [ 'bypassReadOnly' => 1 ] ); + if ( !$status->isOK() ) { + sleep( 10 ); // wait and retry copy again + $status = $dst->doQuickOperations( $ops, [ 'bypassReadOnly' => 1 ] ); + } + $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 ); + if ( !$status->isOK() ) { + $this->error( print_r( $status->getErrorsArray(), true ) ); + $this->fatalError( "$wikiId: Could not delete file batch." ); + } elseif ( count( $deletedRel ) ) { + $this->output( "\n\tDeleted these file(s) [{$elapsed_ms}ms]:\n\t" . + implode( "\n\t", $deletedRel ) . "\n\n" ); + } + } + + /** + * @param FileBackend $src + * @param FileBackend $dst + * @param string $sPath + * @param string $dPath + * @return bool + */ + protected function filesAreSame( FileBackend $src, FileBackend $dst, $sPath, $dPath ) { + $skipHash = $this->hasOption( 'skiphash' ); + $srcStat = $src->getFileStat( [ 'src' => $sPath ] ); + $dPathSha1 = sha1( $dPath ); + if ( $this->statCache !== null ) { + // All dst files are already in stat cache + $dstStat = isset( $this->statCache[$dPathSha1] ) + ? $this->statCache[$dPathSha1] + : false; + } else { + $dstStat = $dst->getFileStat( [ 'src' => $dPath ] ); + } + // Initial fast checks to see if files are obviously different + $sameFast = ( + is_array( $srcStat ) // sanity check that source exists + && is_array( $dstStat ) // dest exists + && $srcStat['size'] === $dstStat['size'] + ); + // More thorough checks against files + if ( !$sameFast ) { + $same = false; // no need to look farther + } elseif ( isset( $srcStat['md5'] ) && isset( $dstStat['md5'] ) ) { + // If MD5 was already in the stat info, just use it. + // This is useful as many objects stores can return this in object listing, + // so we can use it to avoid slow per-file HEADs. + $same = ( $srcStat['md5'] === $dstStat['md5'] ); + } elseif ( $skipHash ) { + // This mode is good for copying to a backup location or resyncing clone + // backends in FileBackendMultiWrite (since they get writes second, they have + // higher timestamps). However, when copying the other way, this hits loads of + // false positives (possibly 100%) and wastes a bunch of time on GETs/PUTs. + $same = ( $srcStat['mtime'] <= $dstStat['mtime'] ); + } else { + // This is the slowest method which does many per-file HEADs (unless an object + // store tracks SHA-1 in listings). + $same = ( $src->getFileSha1Base36( [ 'src' => $sPath, 'latest' => 1 ] ) + === $dst->getFileSha1Base36( [ 'src' => $dPath, 'latest' => 1 ] ) ); + } + + return $same; + } +} + +$maintClass = CopyFileBackend::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/copyJobQueue.php b/www/wiki/maintenance/copyJobQueue.php new file mode 100644 index 00000000..dc70e9c2 --- /dev/null +++ b/www/wiki/maintenance/copyJobQueue.php @@ -0,0 +1,98 @@ +addDescription( 'Copy jobs from one queue system to another.' ); + $this->addOption( 'src', 'Key to $wgJobQueueMigrationConfig for source', true, true ); + $this->addOption( 'dst', 'Key to $wgJobQueueMigrationConfig for destination', true, true ); + $this->addOption( 'type', 'Types of jobs to copy (use "all" for all)', true, true ); + $this->setBatchSize( 500 ); + } + + public function execute() { + global $wgJobQueueMigrationConfig; + + $srcKey = $this->getOption( 'src' ); + $dstKey = $this->getOption( 'dst' ); + + if ( !isset( $wgJobQueueMigrationConfig[$srcKey] ) ) { + $this->fatalError( "\$wgJobQueueMigrationConfig not set for '$srcKey'." ); + } elseif ( !isset( $wgJobQueueMigrationConfig[$dstKey] ) ) { + $this->fatalError( "\$wgJobQueueMigrationConfig not set for '$dstKey'." ); + } + + $types = ( $this->getOption( 'type' ) === 'all' ) + ? JobQueueGroup::singleton()->getQueueTypes() + : [ $this->getOption( 'type' ) ]; + + foreach ( $types as $type ) { + $baseConfig = [ 'type' => $type, 'wiki' => wfWikiID() ]; + $src = JobQueue::factory( $baseConfig + $wgJobQueueMigrationConfig[$srcKey] ); + $dst = JobQueue::factory( $baseConfig + $wgJobQueueMigrationConfig[$dstKey] ); + + list( $total, $totalOK ) = $this->copyJobs( $src, $dst, $src->getAllQueuedJobs() ); + $this->output( "Copied $totalOK/$total queued $type jobs.\n" ); + + list( $total, $totalOK ) = $this->copyJobs( $src, $dst, $src->getAllDelayedJobs() ); + $this->output( "Copied $totalOK/$total delayed $type jobs.\n" ); + } + } + + protected function copyJobs( JobQueue $src, JobQueue $dst, $jobs ) { + $total = 0; + $totalOK = 0; + $batch = []; + foreach ( $jobs as $job ) { + ++$total; + $batch[] = $job; + if ( count( $batch ) >= $this->getBatchSize() ) { + $dst->push( $batch ); + $totalOK += count( $batch ); + $batch = []; + $dst->waitForBackups(); + } + } + if ( count( $batch ) ) { + $dst->push( $batch ); + $totalOK += count( $batch ); + $dst->waitForBackups(); + } + + return [ $total, $totalOK ]; + } +} + +$maintClass = CopyJobQueue::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/createAndPromote.php b/www/wiki/maintenance/createAndPromote.php new file mode 100644 index 00000000..d3efca6f --- /dev/null +++ b/www/wiki/maintenance/createAndPromote.php @@ -0,0 +1,154 @@ + + * @author Pablo Castellano + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to create an account and grant it rights. + * + * @ingroup Maintenance + */ +class CreateAndPromote extends Maintenance { + private static $permitRoles = [ 'sysop', 'bureaucrat', 'bot' ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Create a new user account and/or grant it additional rights' ); + $this->addOption( + 'force', + 'If acccount exists already, just grant it rights or change password.' + ); + foreach ( self::$permitRoles as $role ) { + $this->addOption( $role, "Add the account to the {$role} group" ); + } + + $this->addOption( + 'custom-groups', + 'Comma-separated list of groups to add the user to', + false, + true + ); + + $this->addArg( "username", "Username of new user" ); + $this->addArg( "password", "Password to set (not required if --force is used)", false ); + } + + public function execute() { + $username = $this->getArg( 0 ); + $password = $this->getArg( 1 ); + $force = $this->hasOption( 'force' ); + $inGroups = []; + + $user = User::newFromName( $username ); + if ( !is_object( $user ) ) { + $this->fatalError( "invalid username." ); + } + + $exists = ( 0 !== $user->idForName() ); + + if ( $exists && !$force ) { + $this->fatalError( "Account exists. Perhaps you want the --force option?" ); + } elseif ( !$exists && !$password ) { + $this->error( "Argument required!" ); + $this->maybeHelp( true ); + } elseif ( $exists ) { + $inGroups = $user->getGroups(); + } + + $groups = array_filter( self::$permitRoles, [ $this, 'hasOption' ] ); + if ( $this->hasOption( 'custom-groups' ) ) { + $allGroups = array_flip( User::getAllGroups() ); + $customGroupsText = $this->getOption( 'custom-groups' ); + if ( $customGroupsText !== '' ) { + $customGroups = explode( ',', $customGroupsText ); + foreach ( $customGroups as $customGroup ) { + if ( isset( $allGroups[$customGroup] ) ) { + $groups[] = trim( $customGroup ); + } else { + $this->output( "$customGroup is not a valid group, ignoring!\n" ); + } + } + } + } + + $promotions = array_diff( + $groups, + $inGroups + ); + + if ( $exists && !$password && count( $promotions ) === 0 ) { + $this->output( "Account exists and nothing to do.\n" ); + + return; + } elseif ( count( $promotions ) !== 0 ) { + $promoText = "User:{$username} into " . implode( ', ', $promotions ) . "...\n"; + if ( $exists ) { + $this->output( wfWikiID() . ": Promoting $promoText" ); + } else { + $this->output( wfWikiID() . ": Creating and promoting $promoText" ); + } + } + + if ( !$exists ) { + # Insert the account into the database + $user->addToDatabase(); + $user->saveSettings(); + } + + if ( $password ) { + # Try to set the password + try { + $status = $user->changeAuthenticationData( [ + 'username' => $user->getName(), + 'password' => $password, + 'retype' => $password, + ] ); + if ( !$status->isGood() ) { + throw new PasswordError( $status->getWikiText( null, null, 'en' ) ); + } + if ( $exists ) { + $this->output( "Password set.\n" ); + $user->saveSettings(); + } + } catch ( PasswordError $pwe ) { + $this->fatalError( $pwe->getText() ); + } + } + + # Promote user + array_map( [ $user, 'addGroup' ], $promotions ); + + if ( !$exists ) { + # Increment site_stats.ss_users + $ssu = new SiteStatsUpdate( 0, 0, 0, 0, 1 ); + $ssu->doUpdate(); + } + + $this->output( "done.\n" ); + } +} + +$maintClass = CreateAndPromote::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/createCommonPasswordCdb.php b/www/wiki/maintenance/createCommonPasswordCdb.php new file mode 100644 index 00000000..ef5a30de --- /dev/null +++ b/www/wiki/maintenance/createCommonPasswordCdb.php @@ -0,0 +1,118 @@ +addDescription( 'Generate CDB file of common passwords' ); + $this->addOption( 'limit', "Max number of passwords to write", false, true, 'l' ); + $this->addArg( 'inputfile', 'List of passwords (one per line) to use or - for stdin', true ); + $this->addArg( + 'output', + "Location to write CDB file to (Try $IP/serialized/commonpasswords.cdb)", + true + ); + } + + public function execute() { + $limit = (int)$this->getOption( 'limit', PHP_INT_MAX ); + $langEn = Language::factory( 'en' ); + + $infile = $this->getArg( 0 ); + if ( $infile === '-' ) { + $infile = 'php://stdin'; + } + $outfile = $this->getArg( 1 ); + + if ( !is_readable( $infile ) && $infile !== 'php://stdin' ) { + $this->fatalError( "Cannot open input file $infile for reading" ); + } + + $file = fopen( $infile, 'r' ); + if ( $file === false ) { + $this->fatalError( "Cannot read input file $infile" ); + } + + try { + $db = \Cdb\Writer::open( $outfile ); + + $alreadyWritten = []; + $skipped = 0; + for ( $i = 0; ( $i - $skipped ) < $limit; $i++ ) { + if ( feof( $file ) ) { + break; + } + $rawLine = fgets( $file ); + + if ( $rawLine === false ) { + $this->error( "Error reading input file" ); + break; + } + if ( substr( $rawLine, -1 ) !== "\n" && !feof( $file ) ) { + // We're assuming that this just won't happen. + $this->error( "fgets did not return whole line at $i??" ); + } + $line = $langEn->lc( trim( $rawLine ) ); + if ( $line === '' ) { + $this->error( "Line number " . ( $i + 1 ) . " is blank?" ); + $skipped++; + continue; + } + if ( isset( $alreadyWritten[$line] ) ) { + $this->output( "Password '$line' already written (line " . ( $i + 1 ) .")\n" ); + $skipped++; + continue; + } + $alreadyWritten[$line] = true; + $db->set( $line, $i + 1 - $skipped ); + } + // All caps, so cannot conflict with potential password + $db->set( '_TOTALENTRIES', $i - $skipped ); + $db->close(); + + $this->output( "Successfully wrote " . ( $i - $skipped ) . + " (out of $i) passwords to $outfile\n" + ); + } catch ( \Cdb\Exception $e ) { + $this->fatalError( "Error writing cdb file: " . $e->getMessage(), 2 ); + } + } +} + +$maintClass = GenerateCommonPassword::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteArchivedFiles.php b/www/wiki/maintenance/deleteArchivedFiles.php new file mode 100644 index 00000000..d010073b --- /dev/null +++ b/www/wiki/maintenance/deleteArchivedFiles.php @@ -0,0 +1,134 @@ +addDescription( 'Deletes all archived images.' ); + $this->addOption( 'delete', 'Perform the deletion' ); + $this->addOption( 'force', 'Force deletion of rows from filearchive' ); + } + + public function execute() { + if ( !$this->hasOption( 'delete' ) ) { + $this->output( "Use --delete to actually confirm this script\n" ); + return; + } + + # Data should come off the master, wrapped in a transaction + $dbw = $this->getDB( DB_MASTER ); + $this->beginTransaction( $dbw, __METHOD__ ); + $repo = RepoGroup::singleton()->getLocalRepo(); + + # Get "active" revisions from the filearchive table + $this->output( "Searching for and deleting archived files...\n" ); + $res = $dbw->select( + 'filearchive', + [ 'fa_id', 'fa_storage_group', 'fa_storage_key', 'fa_sha1', 'fa_name' ], + '', + __METHOD__ + ); + + $count = 0; + foreach ( $res as $row ) { + $key = $row->fa_storage_key; + if ( !strlen( $key ) ) { + $this->output( "Entry with ID {$row->fa_id} has empty key, skipping\n" ); + continue; + } + + /** @var LocalFile $file */ + $file = $repo->newFile( $row->fa_name ); + try { + $file->lock(); + } catch ( LocalFileLockError $e ) { + $this->error( "Could not acquire lock on '{$row->fa_name}', skipping\n" ); + continue; + } + + $group = $row->fa_storage_group; + $id = $row->fa_id; + $path = $repo->getZonePath( 'deleted' ) . + '/' . $repo->getDeletedHashPath( $key ) . $key; + if ( isset( $row->fa_sha1 ) ) { + $sha1 = $row->fa_sha1; + } else { + // old row, populate from key + $sha1 = LocalRepo::getHashFromKey( $key ); + } + + // Check if the file is used anywhere... + $inuse = $dbw->selectField( + 'oldimage', + '1', + [ + 'oi_sha1' => $sha1, + $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE + ], + __METHOD__, + [ 'FOR UPDATE' ] + ); + + $needForce = true; + if ( !$repo->fileExists( $path ) ) { + $this->output( "Notice - file '$key' not found in group '$group'\n" ); + } elseif ( $inuse ) { + $this->output( "Notice - file '$key' is still in use\n" ); + } elseif ( !$repo->quickPurge( $path ) ) { + $this->output( "Unable to remove file $path, skipping\n" ); + $file->unlock(); + continue; // don't delete even with --force + } else { + $needForce = false; + } + + if ( $needForce ) { + if ( $this->hasOption( 'force' ) ) { + $this->output( "Got --force, deleting DB entry\n" ); + } else { + $file->unlock(); + continue; + } + } + + $count++; + $dbw->delete( 'filearchive', [ 'fa_id' => $id ], __METHOD__ ); + $file->unlock(); + } + + $this->commitTransaction( $dbw, __METHOD__ ); + $this->output( "Done! [$count file(s)]\n" ); + } +} + +$maintClass = DeleteArchivedFiles::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteArchivedRevisions.php b/www/wiki/maintenance/deleteArchivedRevisions.php new file mode 100644 index 00000000..e4419518 --- /dev/null +++ b/www/wiki/maintenance/deleteArchivedRevisions.php @@ -0,0 +1,65 @@ +addDescription( + "Deletes all archived revisions\nThese revisions will no longer be restorable" ); + $this->addOption( 'delete', 'Performs the deletion' ); + } + + public function execute() { + $dbw = $this->getDB( DB_MASTER ); + + if ( !$this->hasOption( 'delete' ) ) { + $count = $dbw->selectField( 'archive', 'COUNT(*)', '', __METHOD__ ); + $this->output( "Found $count revisions to delete.\n" ); + $this->output( "Please run the script again with the --delete option " + . "to really delete the revisions.\n" ); + return; + } + + $this->output( "Deleting archived revisions... " ); + $dbw->delete( 'archive', '*', __METHOD__ ); + $count = $dbw->affectedRows(); + $this->output( "done. $count revisions deleted.\n" ); + + if ( $count ) { + $this->purgeRedundantText( true ); + } + } +} + +$maintClass = DeleteArchivedRevisions::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteAutoPatrolLogs.php b/www/wiki/maintenance/deleteAutoPatrolLogs.php new file mode 100644 index 00000000..c1935a76 --- /dev/null +++ b/www/wiki/maintenance/deleteAutoPatrolLogs.php @@ -0,0 +1,198 @@ +addDescription( 'Remove autopatrol logs in the logging table' ); + $this->addOption( 'dry-run', 'Print debug info instead of actually deleting' ); + $this->addOption( + 'check-old', + 'Check old patrol logs (for deleting old format autopatrols).' . + 'Note that this will not delete rows older than 2011 (MediaWiki 1.18).' + ); + $this->addOption( + 'before', + 'Timestamp to delete only before that time, all MediaWiki timestamp formats are accepted', + false, + true + ); + $this->addOption( + 'from-id', + 'First row (log id) to start updating from', + false, + true + ); + $this->addOption( + 'sleep', + 'Sleep time (in seconds) between every batch', + false, + true + ); + $this->setBatchSize( 1000 ); + } + + public function execute() { + $this->setBatchSize( $this->getOption( 'batch-size', $this->getBatchSize() ) ); + + $sleep = (int)$this->getOption( 'sleep', 10 ); + $fromId = $this->getOption( 'from-id', null ); + $this->countDown( 5 ); + while ( true ) { + if ( $this->hasOption( 'check-old' ) ) { + $rowsData = $this->getRowsOld( $fromId ); + // We reached end of the table + if ( !$rowsData ) { + break; + } + $rows = $rowsData['rows']; + $fromId = $rowsData['lastId']; + + // There is nothing to delete in this batch + if ( !$rows ) { + continue; + } + } else { + $rows = $this->getRows( $fromId ); + if ( !$rows ) { + break; + } + $fromId = end( $rows ); + } + + if ( $this->hasOption( 'dry-run' ) ) { + $this->output( 'These rows will get deleted: ' . implode( ', ', $rows ) . "\n" ); + } else { + $this->deleteRows( $rows ); + $this->output( 'Processed up to row id ' . end( $rows ) . "\n" ); + } + + if ( $sleep > 0 ) { + sleep( $sleep ); + } + } + } + + private function getRows( $fromId ) { + $dbr = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( + DB_REPLICA + ); + $before = $this->getOption( 'before', false ); + + $conds = [ + 'log_type' => 'patrol', + 'log_action' => 'autopatrol', + ]; + + if ( $fromId ) { + $conds[] = 'log_id > ' . $dbr->addQuotes( $fromId ); + } + + if ( $before ) { + $conds[] = 'log_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $before ) ); + } + + return $dbr->selectFieldValues( + 'logging', + 'log_id', + $conds, + __METHOD__, + [ 'LIMIT' => $this->getBatchSize() ] + ); + } + + private function getRowsOld( $fromId ) { + $dbr = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( + DB_REPLICA + ); + $batchSize = $this->getBatchSize(); + $before = $this->getOption( 'before', false ); + + $conds = [ + 'log_type' => 'patrol', + 'log_action' => 'patrol', + ]; + + if ( $fromId ) { + $conds[] = 'log_id > ' . $dbr->addQuotes( $fromId ); + } + + if ( $before ) { + $conds[] = 'log_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $before ) ); + } + + $result = $dbr->select( + 'logging', + [ 'log_id', 'log_params' ], + $conds, + __METHOD__, + [ 'LIMIT' => $batchSize ] + ); + + $last = null; + $autopatrols = []; + foreach ( $result as $row ) { + $last = $row->log_id; + Wikimedia\suppressWarnings(); + $params = unserialize( $row->log_params ); + Wikimedia\restoreWarnings(); + + // Skipping really old rows, before 2011 + if ( !is_array( $params ) || !array_key_exists( '6::auto', $params ) ) { + continue; + } + + $auto = $params['6::auto']; + if ( $auto ) { + $autopatrols[] = $row->log_id; + } + } + + if ( $last === null ) { + return null; + } + + return [ 'rows' => $autopatrols, 'lastId' => $last ]; + } + + private function deleteRows( array $rows ) { + $dbw = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( + DB_MASTER + ); + + $dbw->delete( + 'logging', + [ 'log_id' => $rows ], + __METHOD__ + ); + + MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->waitForReplication(); + } + +} + +$maintClass = DeleteAutoPatrolLogs::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteBatch.php b/www/wiki/maintenance/deleteBatch.php new file mode 100644 index 00000000..0f3c5067 --- /dev/null +++ b/www/wiki/maintenance/deleteBatch.php @@ -0,0 +1,127 @@ +] [-r ] [-i ] [listfile] + * where + * [listfile] is a file where each line contains the title of a page to be + * deleted, standard input is used if listfile is not given. + * is the username + * is the delete reason + * is the number of seconds to sleep for after each delete + * + * 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 Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to delete a batch of pages. + * + * @ingroup Maintenance + */ +class DeleteBatch extends Maintenance { + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Deletes a batch of pages' ); + $this->addOption( 'u', "User to perform deletion", false, true ); + $this->addOption( 'r', "Reason to delete page", false, true ); + $this->addOption( 'i', "Interval to sleep between deletions" ); + $this->addArg( 'listfile', 'File with titles to delete, separated by newlines. ' . + 'If not given, stdin will be used.', false ); + } + + public function execute() { + global $wgUser; + + # Change to current working directory + $oldCwd = getcwd(); + chdir( $oldCwd ); + + # Options processing + $username = $this->getOption( 'u', false ); + $reason = $this->getOption( 'r', '' ); + $interval = $this->getOption( 'i', 0 ); + + if ( $username === false ) { + $user = User::newSystemUser( 'Delete page script', [ 'steal' => true ] ); + } else { + $user = User::newFromName( $username ); + } + if ( !$user ) { + $this->fatalError( "Invalid username" ); + } + $wgUser = $user; + + if ( $this->hasArg() ) { + $file = fopen( $this->getArg(), 'r' ); + } else { + $file = $this->getStdin(); + } + + # Setup + if ( !$file ) { + $this->fatalError( "Unable to read file, exiting" ); + } + + $dbw = $this->getDB( DB_MASTER ); + + # Handle each entry + // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall + for ( $linenum = 1; !feof( $file ); $linenum++ ) { + $line = trim( fgets( $file ) ); + if ( $line == '' ) { + continue; + } + $title = Title::newFromText( $line ); + if ( is_null( $title ) ) { + $this->output( "Invalid title '$line' on line $linenum\n" ); + continue; + } + if ( !$title->exists() ) { + $this->output( "Skipping nonexistent page '$line'\n" ); + continue; + } + + $this->output( $title->getPrefixedText() ); + if ( $title->getNamespace() == NS_FILE ) { + $img = wfFindFile( $title, [ 'ignoreRedirect' => true ] ); + if ( $img && $img->isLocal() && !$img->delete( $reason ) ) { + $this->output( " FAILED to delete associated file... " ); + } + } + $page = WikiPage::factory( $title ); + $error = ''; + $success = $page->doDeleteArticle( $reason, false, 0, true, $error, $user ); + if ( $success ) { + $this->output( " Deleted!\n" ); + } else { + $this->output( " FAILED to delete article\n" ); + } + + if ( $interval ) { + sleep( $interval ); + } + wfWaitForSlaves(); + } + } +} + +$maintClass = DeleteBatch::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteDefaultMessages.php b/www/wiki/maintenance/deleteDefaultMessages.php new file mode 100644 index 00000000..326073f1 --- /dev/null +++ b/www/wiki/maintenance/deleteDefaultMessages.php @@ -0,0 +1,105 @@ +addDescription( 'Deletes all pages in the MediaWiki namespace' . + ' which were last edited by "MediaWiki default"' ); + $this->addOption( 'dry-run', 'Perform a dry run, delete nothing' ); + } + + public function execute() { + global $wgUser; + + $this->output( "Checking existence of old default messages..." ); + $dbr = $this->getDB( DB_REPLICA ); + + $actorQuery = ActorMigration::newMigration() + ->getWhere( $dbr, 'rev_user', User::newFromName( 'MediaWiki default' ) ); + $res = $dbr->select( + [ 'page', 'revision' ] + $actorQuery['tables'], + [ 'page_namespace', 'page_title' ], + [ + 'page_namespace' => NS_MEDIAWIKI, + $actorQuery['conds'], + ], + __METHOD__, + [], + [ 'revision' => [ 'JOIN', 'page_latest=rev_id' ] ] + $actorQuery['joins'] + ); + + if ( $dbr->numRows( $res ) == 0 ) { + // No more messages left + $this->output( "done.\n" ); + return; + } + + $dryrun = $this->hasOption( 'dry-run' ); + if ( $dryrun ) { + foreach ( $res as $row ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->output( "\n* [[$title]]" ); + } + $this->output( "\n\nRun again without --dry-run to delete these pages.\n" ); + return; + } + + // Deletions will be made by $user temporarly added to the bot group + // in order to hide it in RecentChanges. + $user = User::newFromName( 'MediaWiki default' ); + if ( !$user ) { + $this->fatalError( "Invalid username" ); + } + $user->addGroup( 'bot' ); + $wgUser = $user; + + // Handle deletion + $this->output( "\n...deleting old default messages (this may take a long time!)...", 'msg' ); + $dbw = $this->getDB( DB_MASTER ); + + foreach ( $res as $row ) { + wfWaitForSlaves(); + $dbw->ping(); + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $page = WikiPage::factory( $title ); + $error = ''; // Passed by ref + // FIXME: Deletion failures should be reported, not silently ignored. + $page->doDeleteArticle( 'No longer required', false, 0, true, $error, $user ); + } + + $this->output( "done!\n", 'msg' ); + } +} + +$maintClass = DeleteDefaultMessages::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteEqualMessages.php b/www/wiki/maintenance/deleteEqualMessages.php new file mode 100644 index 00000000..cd9ef111 --- /dev/null +++ b/www/wiki/maintenance/deleteEqualMessages.php @@ -0,0 +1,206 @@ +addDescription( 'Deletes all pages in the MediaWiki namespace that are equal to ' + . 'the default message' ); + $this->addOption( 'delete', 'Actually delete the pages (default: dry run)' ); + $this->addOption( 'delete-talk', 'Don\'t leave orphaned talk pages behind during deletion' ); + $this->addOption( 'lang-code', 'Check for subpages of this language code (default: root ' + . 'page against content language). Use value "*" to run for all mwfile language code ' + . 'subpages (including the base pages that override content language).', false, true ); + } + + /** + * @param string|bool $langCode See --lang-code option. + * @param array &$messageInfo + */ + protected function fetchMessageInfo( $langCode, array &$messageInfo ) { + global $wgContLang; + + if ( $langCode ) { + $this->output( "\n... fetching message info for language: $langCode" ); + $nonContLang = true; + } else { + $this->output( "\n... fetching message info for content language" ); + $langCode = $wgContLang->getCode(); + $nonContLang = false; + } + + /* Based on SpecialAllmessages::reallyDoQuery #filter=modified */ + + $l10nCache = Language::getLocalisationCache(); + $messageNames = $l10nCache->getSubitemList( 'en', 'messages' ); + // Normalise message names for NS_MEDIAWIKI page_title + $messageNames = array_map( [ $wgContLang, 'ucfirst' ], $messageNames ); + + $statuses = AllMessagesTablePager::getCustomisedStatuses( + $messageNames, $langCode, $nonContLang ); + // getCustomisedStatuses is stripping the sub page from the page titles, add it back + $titleSuffix = $nonContLang ? "/$langCode" : ''; + + foreach ( $messageNames as $key ) { + $customised = isset( $statuses['pages'][$key] ); + if ( $customised ) { + $actual = wfMessage( $key )->inLanguage( $langCode )->plain(); + $default = wfMessage( $key )->inLanguage( $langCode )->useDatabase( false )->plain(); + + $messageInfo['relevantPages']++; + + if ( + // Exclude messages that are empty by default, such as sitenotice, specialpage + // summaries and accesskeys. + $default !== '' && $default !== '-' && + $actual === $default + ) { + $hasTalk = isset( $statuses['talks'][$key] ); + $messageInfo['results'][] = [ + 'title' => $key . $titleSuffix, + 'hasTalk' => $hasTalk, + ]; + $messageInfo['equalPages']++; + if ( $hasTalk ) { + $messageInfo['equalPagesTalks']++; + } + } + } + } + } + + public function execute() { + $doDelete = $this->hasOption( 'delete' ); + $doDeleteTalk = $this->hasOption( 'delete-talk' ); + $langCode = $this->getOption( 'lang-code' ); + + $messageInfo = [ + 'relevantPages' => 0, + 'equalPages' => 0, + 'equalPagesTalks' => 0, + 'results' => [], + ]; + + $this->output( 'Checking for pages with default message...' ); + + // Load message information + if ( $langCode ) { + $langCodes = Language::fetchLanguageNames( null, 'mwfile' ); + if ( $langCode === '*' ) { + // All valid lang-code subpages in NS_MEDIAWIKI that + // override the messsages in that language + foreach ( $langCodes as $key => $value ) { + $this->fetchMessageInfo( $key, $messageInfo ); + } + // Lastly, the base pages in NS_MEDIAWIKI that override + // messages in content language + $this->fetchMessageInfo( false, $messageInfo ); + } else { + if ( !isset( $langCodes[$langCode] ) ) { + $this->fatalError( 'Invalid language code: ' . $langCode ); + } + $this->fetchMessageInfo( $langCode, $messageInfo ); + } + } else { + $this->fetchMessageInfo( false, $messageInfo ); + } + + if ( $messageInfo['equalPages'] === 0 ) { + // No more equal messages left + $this->output( "\ndone.\n" ); + + return; + } + + $this->output( "\n{$messageInfo['relevantPages']} pages in the MediaWiki namespace " + . "override messages." ); + $this->output( "\n{$messageInfo['equalPages']} pages are equal to the default message " + . "(+ {$messageInfo['equalPagesTalks']} talk pages).\n" ); + + if ( !$doDelete ) { + $list = ''; + foreach ( $messageInfo['results'] as $result ) { + $title = Title::makeTitle( NS_MEDIAWIKI, $result['title'] ); + $list .= "* [[$title]]\n"; + if ( $result['hasTalk'] ) { + $title = Title::makeTitle( NS_MEDIAWIKI_TALK, $result['title'] ); + $list .= "* [[$title]]\n"; + } + } + $this->output( "\nList:\n$list\nRun the script again with --delete to delete these pages" ); + if ( $messageInfo['equalPagesTalks'] !== 0 ) { + $this->output( " (include --delete-talk to also delete the talk pages)" ); + } + $this->output( "\n" ); + + return; + } + + $user = User::newSystemUser( 'MediaWiki default', [ 'steal' => true ] ); + if ( !$user ) { + $this->fatalError( "Invalid username" ); + } + global $wgUser; + $wgUser = $user; + + // Hide deletions from RecentChanges + $user->addGroup( 'bot' ); + + // Handle deletion + $this->output( "\n...deleting equal messages (this may take a long time!)..." ); + $dbw = $this->getDB( DB_MASTER ); + foreach ( $messageInfo['results'] as $result ) { + wfWaitForSlaves(); + $dbw->ping(); + $title = Title::makeTitle( NS_MEDIAWIKI, $result['title'] ); + $this->output( "\n* [[$title]]" ); + $page = WikiPage::factory( $title ); + $error = ''; // Passed by ref + $success = $page->doDeleteArticle( 'No longer required', false, 0, true, $error, $user ); + if ( !$success ) { + $this->output( " (Failed!)" ); + } + if ( $result['hasTalk'] && $doDeleteTalk ) { + $title = Title::makeTitle( NS_MEDIAWIKI_TALK, $result['title'] ); + $this->output( "\n* [[$title]]" ); + $page = WikiPage::factory( $title ); + $error = ''; // Passed by ref + $success = $page->doDeleteArticle( 'Orphaned talk page of no longer required message', + false, 0, true, $error, $user ); + if ( !$success ) { + $this->output( " (Failed!)" ); + } + } + } + $this->output( "\n\ndone!\n" ); + } +} + +$maintClass = DeleteEqualMessages::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteOldRevisions.php b/www/wiki/maintenance/deleteOldRevisions.php new file mode 100644 index 00000000..fc43e220 --- /dev/null +++ b/www/wiki/maintenance/deleteOldRevisions.php @@ -0,0 +1,103 @@ + + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that deletes old (non-current) revisions from the database. + * + * @ingroup Maintenance + */ +class DeleteOldRevisions extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addDescription( 'Delete old (non-current) revisions from the database' ); + $this->addOption( 'delete', 'Actually perform the deletion' ); + $this->addOption( 'page_id', 'List of page ids to work on', false ); + } + + public function execute() { + $this->output( "Delete old revisions\n\n" ); + $this->doDelete( $this->hasOption( 'delete' ), $this->mArgs ); + } + + function doDelete( $delete = false, $args = [] ) { + # Data should come off the master, wrapped in a transaction + $dbw = $this->getDB( DB_MASTER ); + $this->beginTransaction( $dbw, __METHOD__ ); + + $pageConds = []; + $revConds = []; + + # If a list of page_ids was provided, limit results to that set of page_ids + if ( count( $args ) > 0 ) { + $pageConds['page_id'] = $args; + $revConds['rev_page'] = $args; + $this->output( "Limiting to page IDs " . implode( ',', $args ) . "\n" ); + } + + # Get "active" revisions from the page table + $this->output( "Searching for active revisions..." ); + $res = $dbw->select( 'page', 'page_latest', $pageConds, __METHOD__ ); + $latestRevs = []; + foreach ( $res as $row ) { + $latestRevs[] = $row->page_latest; + } + $this->output( "done.\n" ); + + # Get all revisions that aren't in this set + $this->output( "Searching for inactive revisions..." ); + if ( count( $latestRevs ) > 0 ) { + $revConds[] = 'rev_id NOT IN (' . $dbw->makeList( $latestRevs ) . ')'; + } + $res = $dbw->select( 'revision', 'rev_id', $revConds, __METHOD__ ); + $oldRevs = []; + foreach ( $res as $row ) { + $oldRevs[] = $row->rev_id; + } + $this->output( "done.\n" ); + + # Inform the user of what we're going to do + $count = count( $oldRevs ); + $this->output( "$count old revisions found.\n" ); + + # Delete as appropriate + if ( $delete && $count ) { + $this->output( "Deleting..." ); + $dbw->delete( 'revision', [ 'rev_id' => $oldRevs ], __METHOD__ ); + $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $oldRevs ], __METHOD__ ); + $this->output( "done.\n" ); + } + + # This bit's done + # Purge redundant text records + $this->commitTransaction( $dbw, __METHOD__ ); + if ( $delete ) { + $this->purgeRedundantText( true ); + } + } +} + +$maintClass = DeleteOldRevisions::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteOrphanedRevisions.php b/www/wiki/maintenance/deleteOrphanedRevisions.php new file mode 100644 index 00000000..8d3f6b3e --- /dev/null +++ b/www/wiki/maintenance/deleteOrphanedRevisions.php @@ -0,0 +1,102 @@ + + * @todo More efficient cleanup of text records + */ + +require_once __DIR__ . '/Maintenance.php'; + +use Wikimedia\Rdbms\IDatabase; + +/** + * Maintenance script that deletes revisions which refer to a nonexisting page. + * + * @ingroup Maintenance + */ +class DeleteOrphanedRevisions extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addDescription( + 'Maintenance script to delete revisions which refer to a nonexisting page' ); + $this->addOption( 'report', 'Prints out a count of affected revisions but doesn\'t delete them' ); + } + + public function execute() { + $this->output( "Delete Orphaned Revisions\n" ); + + $report = $this->hasOption( 'report' ); + + $dbw = $this->getDB( DB_MASTER ); + $this->beginTransaction( $dbw, __METHOD__ ); + list( $page, $revision ) = $dbw->tableNamesN( 'page', 'revision' ); + + # Find all the orphaned revisions + $this->output( "Checking for orphaned revisions..." ); + $sql = "SELECT rev_id FROM {$revision} LEFT JOIN {$page} ON rev_page = page_id " + . "WHERE page_namespace IS NULL"; + $res = $dbw->query( $sql, 'deleteOrphanedRevisions' ); + + # Stash 'em all up for deletion (if needed) + $revisions = []; + foreach ( $res as $row ) { + $revisions[] = $row->rev_id; + } + $count = count( $revisions ); + $this->output( "found {$count}.\n" ); + + # Nothing to do? + if ( $report || $count == 0 ) { + $this->commitTransaction( $dbw, __METHOD__ ); + exit( 0 ); + } + + # Delete each revision + $this->output( "Deleting..." ); + $this->deleteRevs( $revisions, $dbw ); + $this->output( "done.\n" ); + + # Close the transaction and call the script to purge unused text records + $this->commitTransaction( $dbw, __METHOD__ ); + $this->purgeRedundantText( true ); + } + + /** + * Delete one or more revisions from the database + * Do this inside a transaction + * + * @param array $id Array of revision id values + * @param IDatabase $dbw Master DB handle + */ + private function deleteRevs( $id, &$dbw ) { + if ( !is_array( $id ) ) { + $id = [ $id ]; + } + $dbw->delete( 'revision', [ 'rev_id' => $id ], __METHOD__ ); + + // Delete from ip_changes should a record exist. + $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $id ], __METHOD__ ); + } +} + +$maintClass = DeleteOrphanedRevisions::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/deleteSelfExternals.php b/www/wiki/maintenance/deleteSelfExternals.php new file mode 100644 index 00000000..9849dc50 --- /dev/null +++ b/www/wiki/maintenance/deleteSelfExternals.php @@ -0,0 +1,57 @@ +addDescription( 'Delete self-references to $wgServer from externallinks' ); + $this->setBatchSize( 1000 ); + } + + public function execute() { + global $wgServer; + $this->output( "Deleting self externals from $wgServer\n" ); + $db = $this->getDB( DB_MASTER ); + while ( 1 ) { + $this->commitTransaction( $db, __METHOD__ ); + $q = $db->limitResult( "DELETE /* deleteSelfExternals */ FROM externallinks WHERE el_to" + . $db->buildLike( $wgServer . '/', $db->anyString() ), $this->getBatchSize() ); + $this->output( "Deleting a batch\n" ); + $db->query( $q ); + if ( !$db->affectedRows() ) { + return; + } + } + } +} + +$maintClass = DeleteSelfExternals::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/dev/README b/www/wiki/maintenance/dev/README new file mode 100644 index 00000000..a00f52bb --- /dev/null +++ b/www/wiki/maintenance/dev/README @@ -0,0 +1,7 @@ +maintenance/dev/ scripts can help quickly setup a local MediaWiki for development purposes. + +Wikis setup in this way are NOT meant to be publicly available. They use a development database not acceptable for use in production. Place a sqlite database in an unsafe location a real wiki should never place it in. And use predictable default logins for the initial administrator user. + +Running maintenance/dev/install.sh will download and install a local copy of php 5.6, install a sqlite powered instance of MW for development, and then start up a local webserver to view the wiki. + +After installation you can bring the webserver back up at any time you want with maintenance/dev/start.sh diff --git a/www/wiki/maintenance/dev/includes/php.sh b/www/wiki/maintenance/dev/includes/php.sh new file mode 100644 index 00000000..3c5bef0d --- /dev/null +++ b/www/wiki/maintenance/dev/includes/php.sh @@ -0,0 +1,14 @@ +# Include-able script to determine the location of our php if any +# We search for a environment var called PHP, native php, +# a local copy, home directory location used by installphp.sh +# and previous home directory location +# The binary path is returned in $PHP if any + +for binary in $PHP $(which php || true) "$DEV/php/bin/php" "$HOME/.mediawiki/php/bin/php" "$HOME/.mwphp/bin/php" ]; do + if [ -x "$binary" ]; then + if "$binary" -r 'exit((int)!version_compare(PHP_VERSION, "5.4", ">="));'; then + PHP="$binary" + break + fi + fi +done diff --git a/www/wiki/maintenance/dev/includes/require-php.sh b/www/wiki/maintenance/dev/includes/require-php.sh new file mode 100644 index 00000000..470e6eb8 --- /dev/null +++ b/www/wiki/maintenance/dev/includes/require-php.sh @@ -0,0 +1,8 @@ +# Include-able script to require that we have a known php binary we can execute + +. "$DEV/includes/php.sh" + +if [ "x$PHP" == "x" -o ! -x "$PHP" ]; then + echo "Local copy of PHP is not installed" + exit 1 +fi diff --git a/www/wiki/maintenance/dev/includes/router.php b/www/wiki/maintenance/dev/includes/router.php new file mode 100644 index 00000000..9917a4fa --- /dev/null +++ b/www/wiki/maintenance/dev/includes/router.php @@ -0,0 +1,97 @@ +/dev/null; then + echo " - using wget" + wget -O "$TAR" "$PHPURL" +elif command -v curl &>/dev/null; then + echo " - using curl" + curl "$PHPURL" -L -o "$TAR" +else + echo " - aborting" + echo "Could not find curl or wget." >&2; + exit 1; +fi + +echo "Extracting php $VER" +tar -xzf "$TAR" + +cd "php-$VER/" + +echo "Configuring and installing php $VER in $PREFIX" +./configure --prefix="$PREFIX" +make +make install diff --git a/www/wiki/maintenance/dev/start.sh b/www/wiki/maintenance/dev/start.sh new file mode 100755 index 00000000..dd7363a8 --- /dev/null +++ b/www/wiki/maintenance/dev/start.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [ "x$BASH_SOURCE" == "x" ]; then echo '$BASH_SOURCE not set'; exit 1; fi +DEV=$(cd -P "$(dirname "${BASH_SOURCE[0]}" )" && pwd) + +. "$DEV/includes/require-php.sh" + +PORT=4881 + +echo "Starting up MediaWiki at http://localhost:$PORT/" +echo "" + +cd "$DEV/../../"; # $IP +"$PHP" -S "localhost:$PORT" "$DEV/includes/router.php" diff --git a/www/wiki/maintenance/dictionary/mediawiki.dic b/www/wiki/maintenance/dictionary/mediawiki.dic new file mode 100644 index 00000000..ff06e49d --- /dev/null +++ b/www/wiki/maintenance/dictionary/mediawiki.dic @@ -0,0 +1,4664 @@ +&add +& +&bar +&img +&sim +&url +&wap +ABNF +API +Aacute +Aborted +Abuse +Account +Accum +Acirc +Action +Activity +Agrave +All +Allocations +Ancientpages +Anim +Api +Apitestsysop +Apitestuser +Aring +Article +As +Atilde +Auml +Autopromote +BACKCOMPAT +Backlinks +Blacklist +Block +Blocked +Blocks +Bodytext +Broken +COMPUTERNAME +CRLF +CURLOPT +Campaign +Capture +Categories +Category +Ccedil +Central +Changes +Check +Click +Client +Clientfor +Colorer +Compare +Config +Console +Continue +Contribs +Contributions +Conversiontable +Coordinates +Create +Creation +Cview +DDLMODE +DWIM +DWIMD +Daily +Dbkeyform +Deadendpages +Debugtext +Delete +Deletedrevs +Denied +Dfile +Double +Duplicate +EAGAIN +EBML +ECMA +EDITFILTERMERGED +EINPROGRESS +EINTR +EOCDR +ETAG +Eacute +Ecirc +Edit +Editor +Education +Egrave +Elig +Email +Empty +End +English +Enlist +Euml +Eval +Events +Exists +Expand +Expression +Ext +External +Extracts +Extraneous +FFFD +FOLLOWLOCATION +Failure +Featured +Feed +Feedback +Feedbackv +Feeds +Fewestrevisions +Ffile +File +Filearchive +Filedelete +Files +Filter +Filters +Flag +Flagged +GI +GRAPHEME +Gadget +Gadgets +Geo +Get +Global +Groups +HEA +HTM +Hardblock +Help +Helpful +ID +IPTC +IWBacklinks +IWLinks +Iacute +Icirc +Igrave +Illegal +Image +Images +Implict +Import +Info +Invalidateemail +Isarticle +Item +Iuml +LOCALISATIONCACHE +Lang +Lastmod +Links +Linktags +List +Listredirects +Living +Log +Login +Logout +Logs +Lonelypages +Longpages +Love +Ltitle +MSVC +Mark +Match +Matrix +Members +Mesg +Messages +Metatags +Mobile +Mostcategories +Mostimages +Mostinterwikis +Mostlinked +Mostlinkedcategories +Mostlinkedtemplates +Mostrevisions +Move +Mssql +Mwstore +Myuploads +NEWPAGE +NOTIC +Name +Need +No +Noscript +Not +Notalk +Notice +Notification +Ntilde +Oacute +Ocirc +Ograve +Oldreviewedpages +Open +Options +Oslash +Otilde +Ouml +PAGEEDITDATE +PAGEEDITOR +PAGEEDITTIME +PAGEINTRO +PAGEMINOREDIT +PAGESUMMARY +PARSEHUGE +PARSERFIRSTCALLINIT +PHPTAL +PMID +Page +Pages +Param +Parse +Parsers +Pass +Passpass +Patrol +People +Plugin +Possible +Program +Props +Protect +Protected +Protectexpiry +Protectother +Protectreason +Protectreasonother +Purge +Query +Queued +Random +Rapid +Ratings +Raw +Recent +Redirects +Redis +Referer +Refresh +Regexlike +Replacer +Reset +Resursive +Revert +Review +Revisions +Rollback +Rsd +SEGSIZE +STDERR +SYSDBA +Scaron +Scribunto +Search +Section +Set +Shortpages +Site +Siteinfo +Solr +Stabilize +Stash +Stats +Status +Success +Syntax +TMPDIR +TOOLBOXEND +TRANSLIT +Tagging +Tags +Template +Templates +Textform +Tfile +Throttled +Timestamp +Title +Titles +Token +Tokens +Tracking +Transcode +Triage +UNWATCHURL +Uacute +Ucirc +Ugrave +Unblock +Uncategorizedcategories +Uncategorizedimages +Uncategorizedpages +Uncategorizedtemplates +Undelete +Unusedcategories +Unusedimages +Unusedtemplates +Unwatchedpages +Upload +Urlform +Usage +User +Usercreate +Userdir +Userlang +Userrights +Users +Useruser +Ustart +Uuml +Value +Video +View +Visual +WATCHINGUSERNAME +WEBPVP +Wantedcategories +Wantedfiles +Wantedpages +Wantedtemplates +Warning +Watch +Watchingusers +Watchlist +Wiki +Wikibase +Withoutinterwiki +Wrong +XX +Xml +YYYY +YYYYMMDDHHMMSS +Yacute +Yuml +\ +a +aa +aacute +abbrv +abcdefghijklmnopqrstuvwxyz +abf +aboutpage +aboutsite +abusefilter +abusefiltercheckmatch +abusefilterchecksyntax +abusefilterevalexpression +abusefilters +abusefilterunblockautopromote +abuselog +abusive +ac +acad +accel +acceptbilling +acceptlang +accessdenied +accesskey +accesskeycache +accesskeys +accessors +acchits +account +accountcreator +accum +acirc +aclimit +acprefix +action +actioncomplete +actionhidden +actions +actiontext +actionthrottled +actionthrottledtext +actiontoken +activeusers +activity +acuxvalidate +add +addablegroups +addbegin +addedline +addedwatchtext +addergroup +addergroups +addin +adding +additional +addr +address +addresses +addsection +addstudent +admin +administrator +adnum +adrelid +adsrc +advancedediting +advancedrc +advancedrendering +advancedsearchoptions +advancedwatchlist +aelig +af +afl +aft +afttest +afvf +age +aggregators +agrave +ahandler +ahttp +ai +aifc +aiff +aiprop +airtel +aisort +al +alefsym +algo +algos +all +all's +allcategories +alldata +alle +allexamples +allfileusages +allhidden +allimages +allimit +alllinks +alllogstext +allmessages +allmonths +allowedctypes +allowedonly +allowemail +allowsduplicates +allowusertalk +allpages +allpagesbadtitle +allpagesprefix +allpagesredirect +allpagessubmit +allpartners +allredirects +allrev +alltitles +alltransclusions +allusers +aloption +alprefix +alreadyblocked +alreadydone +alreadyexists +alreadyrolled +alunique +am +analyticsconfig +anchor +anchorclose +anchorencode +and +andconvert +andreescu +andtitle +anon +anoneditwarning +anonnotice +anononly +anonpreviewwarning +anontalk +anontalkpagetext +anontoken +anonuserpage +anonymous +anti +antispoof +antivirus +anymap +ap +apcond +apdir +api +api's +apibase +apihelp +apihighlimits +apis +aplimit +apnamespace +apng +apos +appendnotsupported +appendtext +apprefix +approve +aprops +aqbt +aqct +archivename +aren +args +argsarams +aring +arnfjörð +article +articleexists +articlefeedbackv +articleid +articlelink +articlepage +articlepath +articles +aryeh +asc +ascending +asctime +asdf +aspx +assert +asymp +async +at +atend +atext +atid +atilde +atime +atlimit +atoi +atom +atprefix +atthasdef +attibs +attibute +attlen +attname +attnum +attrdef +attrelid +attrib +attribs +attributename +attrs +atttypid +atunique +au +auml +authplugins +autoaccount +autobiography +autoblock +autoblocked +autoblockedtext +autoblocker +autoblockid +autoblocking +autoblockip +autoblocks +autocad +autocomment +autocomments +autocomplete +autoconfirm +autoconfirmed +autocreate +autocreated +autocreation +autodetection +autoflag +autofocus +autogen +autogenerated +autohide +autoload +autoloader +autoloaders +autoloading +automagically +automatic +autonym +autopatrol +autoplay +autopromote +autopromoted +autopromotion +autoreview +autoreviewer +autoreviewrestore +autosumm +autosummaries +autosummary +axto +azərbaycanca +backends +backlink +backlinks +backlinksubtitle +backported +backslashed +backtraces +bad +badaccess +badarticleerror +badcontinue +baddiff +bademail +badfilename +badformat +badgenerator +badhookmsg +badinterwiki +badip +badipaddress +badkey +badmd +badmime +badminpassword +badminuser +badnamespace +badoption +badparams +badport +badretype +badrevids +badsig +badsiglength +badsyntax +badtag +badtimestamp +badtitle +badtitletext +badtoken +badtype +badupload +baduser +badversion +balancer +balancers +banjar +barebone +barstein +base +basefont +basename +basepagename +basepagenamee +basetimestamp +bashkir +bashpid +bcancel +bceffd +bcmath +bcompress +bcpio +bdop +bdquo +becampus +beinstructor +belarusian +beonline +bereviewer +berror +bestq +besttype +bg +bgcolor +bgzip +bidi +bigdelete +bingbot +binhex +bitdepth +bitfield +bitfields +bitmask +bjarmason +bk +bkey +bkinvalidparammix +bkmissingparam +bkusers +bl +blanking +blanknamespace +blankpage +blegh +bleh +blinvalidparammix +blksize +blmissingparam +block +blockable +blocked +blockedasrange +blockedby +blockedbyid +blockedemailuser +blockedexpiry +blockedfrommail +blockednoreason +blockedreason +blockedtext +blockedtitle +blockemail +blockexpiry +blockid +blockinfo +blockip +blocklink +blocklogentry +blocklogpage +blocklogtext +blockme +blockquote +blockreason +blocks +blocktoken +bloggs +blogs +blogspot +bltitle +bluelink +bluelinks +bmwschema +bmysql +bname +bodycontent +bogo +boldening +bolding +booksources +bool +boolean +bordercolor +borderhack +bot +botedit +boteditletter +bots +bottom +bottomscripts +bpassword +bpatch +bpchar +bport +bprefix +broeck +brokenlibxml +brokenredirects +brokenredirectstext +browsearchive +brvbar +bserver +bservers +bssl +btestpassword +btestuser +btype +bucket +bucketcount +bugfix +bugfixes +buglist +bugzilla +buildpath +buildpathentry +bulgakov +bulkdelcourses +bulkdelorgs +bureaucrat +buser +by +byemail +byid +bytea +bytesleft +bytesread +bytevalue +cacheable +cached +cachedcount +cachedsidebar +cachedspecial +cachedtimestamp +calimit +callargs +campaign +campus +cancelto +cannotdelete +cannotundelete +canonicalised +canonicalization +canonicalize +canonicalizes +canonicalizing +canremember +canreset +cansecurelogin +cantblock +cantcreate +cantdelete +cantedit +cantexecute +canthide +cantimport +cantmove +cantmovefile +cantopenfile +cantoverwrite +cantrollback +cantsend +cantunblock +cantundelete +capitalizeallnouns +captchaid +captchas +captchaword +carriersnoips +cascade +cascadeable +cascadeon +cascadeprotected +cascadeprotectedwarning +cascading +cascadinglevels +cascadingness +categories +categories's +categorieshtml +category +categoryfinder +categoryinfo +categorylinks +categorymembers +categorypage +categoryviewer +catids +catlinks +catmsg +catpage +catrope +cattitles +ccedil +ccme +ccmeonemails +cdab +cdel +cdlink +cedil +ceebc +cellpadding +cellspacing +cellulant +central +centralauth +centralnotice +centralnoticeallocations +centralnoticelogs +centralnoticequerycampaign +cgroup +cgroups +change +change's +changeablegroups +changed +changedby +changedorcreated +changeemail +changelog +changeslist +changing +characters +chardiff +charoff +chars +checkfreq +checkmatrix +checkstatus +checkuser +checkuserlog +chgrp +childs +chillu +chmoding +choicesstring +chrs +chunk +chunked +chunking +ci +cidr +cidrtoobroad +circ +citeseer +ckers +ckey +cl +clamav +clamscan +classname +clcategorie +cldir +cldr +clear +clearable +clearyourcache +clfrom +clickjacking +clicktracking +clientfor +clientpool +cllimit +clober +closed +clto +cm +cminvalidparammix +cmmissingparam +cmnamespace +cmtitle +co +code +codemap +codepoint +codestr +coi +colgroup +collapsable +collectionsaveascommunitypage +collectionsaveasuserpage +colname +colonseparator +colorer +colspan +commafy +commafying +comment +commentedit +commenthidden +comments +commitdiff +commoncssjs +compactpro +compare +compat +complete +cond +condcomment +condeferrable +condeferred +conds +config +confirmdeletetext +confirmed +confirmedittext +confirmemail +confirmrecreate +conflimit +confstr +conkey +conname +conrelid +console +content +contentformat +contenthandler +contentlanguage +contentless +contentmodel +contenttoobig +continue +contribs +contribslink +conttitle +contype +conv +converttitles +convmv +cookieprefix +cooltalk +coord +coordinates +copyrightico +copyrightpage +copyrightwarning +copyuploadbaddomain +copyuploaddisabled +copyvio +copywarn +cors +couldn +counter +countmsg +country +course +courseid +cpio +cprefs +cprotected +crarr +crashbug +create +createaccount +createonly +createpage +createtalk +creationsort +creativecommons +creditspage +crocker +cryptrand +csize +csrf +css +cssclass +csslinks +cta +ctime +ctor +ctype +cu +cul +curation +curdiff +curid +curlink +curren +currentarticle +currentbrowser +currentday +currentdayname +currentdow +currenthour +currentmonth +currentmonthabbrev +currentmonthname +currentmonthnamegen +currentrev +currentrevisionlink +currenttime +currenttimestamp +currentversion +currentweek +currentyear +customcssprotected +customised +customjsprotected +cut +cyber +cygwin +cyrl +d'oh +dadedad +dairiki +danga +danielc +darr +datalen +datapath +dataset +datasets +datasize +datatable +datatype +datedefault +dateformat +dateheader +dateopts +daysago +dbcnt +dbconnect +dberrortext +dbg +dbgfm +dbkey +dbkeys +dbks +dbname +dbrepllag +dbsettings +dbtype +dbversion +ddjvu +de +deadend +deadendpagestext +deadenpages +dealies +debughtml +decline +declined +decls +decr +decrease +default +defaultcontentmodel +defaultmessagetext +defaultmissing +defaultns +defaultoptions +defaultsort +defaultval +deferr +definite +deflimit +defs +deja +delete +deleteall +deletecomment +deleteconfirm +deleted +deletedhistory +deletedline +deletedonly +deletedrevision +deletedrevs +deletedtext +deletedwhileediting +deleteeducation +deleteglobalaccount +deletelogentry +deleteone +deleteotherreason +deletepage +deletereason +deletereasonotherlist +deleterevision +deleteset +deletethispage +deletetoken +deletion +deletionlog +delim +dellogpage +dellogpagetext +delundel +deprecated +deps +depth +dequeue +dequeued +dequeueing +dequeues +derivatives +desc +descending +description +descriptionmsg +descriptionmsgparams +descriptionurl +deserialization +deserialize +dest +detail +details +devangari +devel +df +dflt +dflts +dhtml +diams +didn +diff +diff's +diffchange +diffhist +difflink +diffonly +difftext +diffto +difftocontent +difftotext +dim +dimensions +dir +direction +directionmark +directorycreateerror +directorynotreadableerror +directoryreadonlyerror +dirmark +dirname +disabled +disabledtranscode +disablemail +disablepp +disclaimerpage +diskussion +displayname +displayrc +displaysearchoptions +displaytitle +displaytitles +displaywatchlist +distclean +distro +djava +djob +djvu +djvudump +djvulibre +djvutoxml +djvutxt +djvuxml +djvuzone +dkjsagfjsgashfajsh +dlen +dltk +dmoz +dnsbl +dnsblacklist +dnumber +docm +docroot +doctype +doctypes +docx +dodiff +doesn +domain +domainnames +domainpart +domainparts +domas +doms +dont +dotdotcount +dotm +dotsc +dotsi +dotsm +dotso +dotwise +dotx +doubleclick +doublequote +doxygen +dpos +dr +dropdown +dump +dumpfm +dupfunc +dupl +duplicatefiles +duplicatesoffile +dvips +dwfx +dwhitelist +e +eacute +earth +eauth +ecirc +ecmascript +edit +editbutton +editconflict +editconflicts +editcount +editfont +editform +edithelp +edithelppage +edithelpurl +editingcomment +editinginterface +editingold +editingsection +editinterface +editintro +edititis +editlink +editmyoptions +editmyprivateinfo +editmyusercss +editmyuserjs +editmywatchlist +editnotice +editnotsupported +editondblclick +editor +editownusertalk +editpage +editprotected +editreasons +editredlink +editrestriction +edits +editsection +editsectionhint +editsectiononrightclick +editsemiprotected +editsonly +editthispage +edittime +edittoken +edittools +editurl +editusercss +edituserjs +edoe +egrave +ei +eich +eiinvalidparammix +eimissingparam +eititle +el +elapsedreal +elastica +elemname +elems +elink +eltitle +email +emailable +emailaddress +emailauthenticated +emailauthentication +emailauthenticationclass +emailcapture +emailconfirm +emailconfirmed +emailconfirmlink +emaildisabled +emailling +emaillink +emailnotauthenticated +emailtoken +emailuser +embeddedin +empty +emptyfile +emptynewsection +emptypage +emsenhuber +emsp +en +enabled +enabledonly +enableparser +encapsed +enctype +end +endcode +endcond +endian +endid +endl +endsortkey +endsortkeyprefix +endtime +endverbatim +enhancedchanges +enlist +enotif +enotifminoredits +enotifrevealaddr +enotifusertalkpages +enotifwatchlistpages +enqueueing +enroll +ensp +entirewatchlist +entityid +envcmd +enwiki +eocdr +ep +eparticle +epcampus +epcoordinator +epinstructor +eponline +erevoke +errno +error +errorbox +errormessage +errorpagetitle +errors +errorstr +errortext +errorunknown +errstr +es +escapenoentities +escapeshellarg +esearch +español +española +etag +eu +euml +event +eventid +ex +exampleextension +examples +excludegroup +excludepage +excludeuser +executables +exempt +exiftool +existingwiki +exists +exiv +expandtab +expandtemplates +expandurl +experiment +expertise +expiry +expiryarray +explainconflict +export +exportnowrap +exportxml +expression +exptime +extauth +extendwatchlist +extensionname +extensions +extensiontags +external +externaldberror +externaldiff +externaledit +externaleditor +externalimages +externallinks +externalstore +extet +extiw +extlink +extlinks +extracts +extradata +extrafields +extralanglink +extraq +extratags +exturlusage +extuser +exxaammppllee +fa +facto +failback +failover +failsafe +fallbacks +false +falsy +fancysig +fastcgi +faux +favicon +fclose +fdef +fdff +feature +featured +featuredfeed +feed +feed's +feedback +feedbackid +feedcontributions +feedformat +feeditems +feedlink +feedlinks +feedurl +feedwatchlist +feff +female +fetchfileerror +fffe +ffff +fffff +ffffff +fieldname +fieldset +fieldsets +file +filearchive +filebackend +filecache +filecopyerror +filedelete +filedeleteerror +fileexists +fileextensions +filehidden +filehist +filehistory +fileinfo +filejournal +filekey +filelinks +filemissing +filemover +filemtime +filename +filenames +filenotfound +filepage +filepath +filerenameerror +filerepo +filerepoinfo +filerevert +filerevisions +files +filesize +filesort +filesorts +filesystem's +filesystems +filetoc +filetoobig +filetype +filetypemismatch +fileversions +filter +filterbots +filteriw +filterlanglinks +filterlocal +filterredir +filterwatched +findnext +finfo +firefox +firstname +firstrev +firsttime +fishbowl +fixme +fixup +flac +flag +flagconfig +flagged +flags +flagtype +flatlist +flds +float +flrevs +fmttime +fname +fnof +foldmarker +foldmethod +followpolicy +footericon +footericons +footerlinks +fopen +for +forall +forbidden +forcearticlepath +forcebot +forceditsummary +forceeditsummary +forcelinkupdate +forcerecursivelinkupdate +forcetoc +forcontent +formaction +format +formatmodules +formatted +formatters +formatting +formedness +formenctype +formnovalidate +formtype +forupdate +found +founder +fr +frac +frameborder +frameless +framesets +frasl +fread +freedomdefined +freeform +freenode +frickin +from +fromdb +fromdbmaster +fromid +fromrev +fromrevid +fromtitle +frontends +fseek +fsockopen +fsync +ftp +fullhistory +fullpagename +fullpagenamee +fulluri +fullurl +funcname +functionhooks +functionname +futuresplash +fvalue +ga +gack +gadgetcategories +gadgets +gaid +gaifilterredir +gaifrom +gallerybox +gallerycaption +gallerytext +gapdir +gapfilterredir +gapfrom +gaplimit +gapnamespace +gapprefix +garber +gblblock +gblock +gblrights +gc +gcldir +gcllimit +gender +general +generatexml +generator +geocoordinate +geodata +geosearch +gerrit +geshi +getcookie +getenv +getheader +getimagesize +getlink +getmac +getmarkashelpfulitem +getmypid +getrusage +gettimeofday +gettingstarted +gettoken +getuid +gfdl +ggp +ghostscript +gimpbaseenums +git +gitblit +gitdir +github +global +globalauth +globalblock +globalblocks +globalgroupmembership +globalgrouppermissions +globalgroups +globalsettings +globalunblock +globalusage +globaluserinfo +globe +gmail +gmdate +goodtitle +googlebot +gopher +graymap +grayscale +greant +greymap +group +groupcounts +groupless +groupmember +grouppage +groupperms +groupprms +groups +growinglink +grxml +gs +gtar +gu +guesstimezone +gui +guid +gunblock +guser +gwicke +gzcompress +gzdeflate +gzencode +gzhandler +gzip +gzipped +gzipping +hacky +hansm +hant +hardblocks +hardcode +hardcoding +harr +hash +hashar +hashcheckfailed +hashsearchdisabled +hashtable +hashtables +hasmatch +hasmsg +hasn +hasrelated +headelement +headerpos +headhtml +headitems +headlinks +headscripts +height +hellip +help +helpful +helppage +helptext +helpurl +helpurls +helpwindow +hexdump +hexstring +hidden +hiddencat +hiddencategories +hiddencats +hide +hideanons +hidebots +hidediff +hideliu +hideminor +hidemyself +hidename +hidepatrolled +hideredirects +hiderevision +hideuser +hidpi +highlimit +highmax +highuse +hilfe +hiphop +histfirst +histlast +historyempty +historysubmit +historywarning +hit +hitcount +hits +hlist +hmac +hobby +homelink +hookaborted +horohoe +hostnames +hours +hphp +hplist +hpos +hreflang +hslots +htaccess +htcp +html +htmlelements +htmlescaped +htmlform +htmlish +htmllist +htmlnest +htmlpair +htmlpairs +htmlsingle +htmlsingleallowed +htmlsingleonly +htmlspecialchars +htmltidy +http +httpaccept +httpbl +https +i +ia +iabn +iacute +icirc +icononly +iconv +icubench +icutest +id +idanduser +ids +ie's +ieinternals +ietf +iexcl +ifconfig +iframe +igbinary +iges +ignorewarnings +igrave +ii +iicontinue +iiprop +iiurlparam +iiurlwidth +iker +ilfrom +ilto +im +image +imagecolorallocate +imagegetsize +imageinfo +imageinvalidfilename +imagelimits +imagelinks +imagemagick +imagemaxsize +imagenocrossnamespace +imagepage +imagerepository +imagerotate +images +imagesize +imagetype +imagetypemismatch +imageusage +imagewhitelistenabled +imagick +imgmultigo +imgmultigoto +imgmultipagenext +imgmultipageprev +imgs +imgserv +immobilenamespace +implicitgroups +import +importbadinterwiki +importcantopen +importlogpage +importlogpagetext +importnofile +importtoken +importupload +importuploaderrorpartial +importuploaderrorsize +importuploaderrortemp +in +iname +inbound +includable +include +includecomments +includelocal +includeonly +includexmlnamespace +incr +increase +indefinite +index +indexfield +indexpageids +indexpolicy +indstr +infin +infinite +infiniteblock +info +infoaction +infobox +infoline +infomsg +ingroups +injectjs +inkscape +inlanguagecode +inlined +inno +inputneeded +insb +inser +instantcommons +institution +instructor +int +integer +integeroutofrange +intentionallyblankpage +interlang +interlangs +interlanguage +internal +internaledit +internalerror +interwiki +interwikimap +interwikipage +interwikis +interwikisearchinfo +interwikisource +intnull +intoken +intra +intro +intrw +ints +intval +invalid +invalidaction +invalidations +invalidcategory +invaliddomain +invalidemail +invalidemailaddress +invalidexpiry +invalidip +invalidlang +invalidlevel +invalidmode +invalidoldimage +invalidpage +invalidpageid +invalidparameter +invalidparammix +invalidpath +invalidrange +invalidsection +invalidsessiondata +invalidsha +invalidspecialpage +invalidtags +invalidtime +invalidtitle +invalidtoken +invaliduser +invalue +iorm +ip +ipbblocked +ipblock +ipblocks +ipbnounblockself +ipchain +ipedits +iphash +ipinrange +ipset +ipsets +ipusers +iquest +irc +ircs +isam +isapi +isbot +isconnected +iscur +isin +isip +islocal +ismap +isminor +ismodsince +ismulti +isnew +isroot +isself +isset +istainted +istalk +iswatch +it +item +itemid +itemprop +itemref +itemscope +itemtype +iter +iu +iuinvalidparammix +iumissingparam +iuml +iw +iwbacklinks +iwbl +iwlfrom +iwlinks +iwlprefix +iwltitle +iwprefix +iwtitle +iwurl +ized +javascript +javascripttest +jbartsh +jconds +jdk's +jhtml +jimbo +joaat +jobqueue +jointype +jorsch +journaling +jpeg +jpegtran +jslint +jsmimetype +jsminplus +json +jsonconfig +jsonfm +jsparse +jstext +jsvarurl +justthis +kabardian +kangxi +kashubia +kattouw +kblength +kernowek +key +keygen +keylen +keyname +keynames +keytype +khash +kikongo +kludgy +knownnamespace +konqueror +kpos +kuza +labarga +labelmsg +laggedslavemode +laggy +lang +langbacklinks +langcode +langcodes +langconversion +langlinks +langname +langprop +langs +language +languagelinks +languages +languageselection +languageshtml +laquo +large +larr +last +lastdiff +lastdot +lastedit +lasteditor +lastedittime +lastfile +lastlink +lastmod +lastmodifiedat +lastname +lastrevid +lastvisited +latgalian +laxström +lbase +lbl +lcattrib +lceil +lcomments +lcount +lcrocker +ldquo +le +len +length +leprop +lesque +lettercase +level +lfloor +lg +lgname +lgpassword +lgpl +lgtoken +lguserid +lgusername +libcurl +libel +libgimpbase +libketama +libmemcached +libre +libtidy +ligabue +lighttpd +limit +limitable +line +linenumber +linestart +link +linkarr +linkcolour +linkprefix +linkprefixcharset +linkpurge +links +linkstoimage +linktbl +linktext +linktodiffs +linktrail +linktype +linkupdate +list +listable +listadmins +listbots +listfiles +listgrouprights +listinfo +listingcontinuesabbrev +listoutput +listresult +lists +listtags +listuser +listusers +listusersfrom +livepreview +ll +llfrom +lllang +lltitle +lnumber +local +localday +localdayname +localdow +locale +localhour +localinterwiki +localmonth +localmonthabbrev +localmonthname +localmonthnamegen +localname +localonly +localsettings +localtimezone +localweek +localyear +lock +lockandhid +lockdb +lockdir +locked +lockmanager +log +logaction +logentry +logevent +logevents +logextract +loggedin +logid +login +loginerror +loginfo +loginlanguagelinks +loginlink +loginout +loginprompt +loginreqlink +loginreqpagetext +loginreqtitle +logins +logitem +loglink +loglist +logname +logonly +logopath +logourl +logout +logpage +logtext +logtitle +logtype +longpage +longpageerror +lookie +lookups +loopback +lossless +lossy +lowast +lowercaps +lowercased +lowlimit +lsaquo +lsquo +ltags +ltitle +ltrimmed +lurl +lysator +macr +magicarr +magicfile +magick +magicword +magicwordkey +magicwords +magnus +mahaction +mailerror +mailmypassword +mailnologin +mailparts +mailpassword +mailtext +mailto +mainmodule +mainpage +maint +maintainership +makesafe +male +malloc +manske +manualthumb +mark +markashelpful +markaspatrolledlink +markaspatrolledtext +markbot +markbotedits +markedaspatrollederror +markpatrolled +masse +match +matchcount +mathml +mathtt +matrixes +matroska +max +maxage +maxdim +maxlag +maxlength +maxlifetime +maxqueue +maxresults +maxsize +maxuploadsize +maxwidth +mazeland +mbresponse +mbstring +mccmnc +mckey +mcklmqw +mcrypt +mcvalue +md +mdash +mdot +medialink +mediaqueries +mediatype +mediawarning +mediawiki +mediawiki's +mediawikipage +megapixels +member +memberingroups +members +memc +memcache +memcached +memlimit +memoryp +memsw +merge +mergeable +merged +mergehistory +mergelog +mergelogpagetext +message +messagekey +messagename +messagepattern +messages +messagetype +meta +metacharacters +metachars +metadata +metadataversion +metafile +mhash +mhtml +micrblogging +microdata +microsyntaxes +microtime +middot +migurski +millitime +mime +mimer +mimesearchdisabled +mimetype +min +minangkabau +minh +minification +minified +minifier +minifies +minify +minifying +minimal +minor +minordefault +minoredit +minoreditletter +minsize +misconfigured +misermode +mismatch +misresolved +missing +missingcommentheader +missingcommenttext +missingdata +missingparam +missingpermission +missingresult +missingrev +missingsummary +missingtext +missingtitle +missinguser +mituzas +mixedapproval +mkdir +mms +mobile +mobileformat +mobilelanding +mobileview +modified +modifiedarticleprotection +modify +modsecurity +modsince +module +moduledisabled +modulename +modules +monitor +monobook +monospace +monospaced +month +monthsall +moodbar +moredotdotdot +morelinkstoimage +morethan +mouseup +move +movedarticleprotection +moveddeleted +movedto +movefile +movelogpage +movelogpagetext +movenologintext +movenotallowed +movenotallowedfile +moveonly +moveoverredirect +movepage +moves +movestable +movesubpages +movetalk +movethispage +movetoken +mozilla +mpeg +mpegurl +mpga +mplink +mptitle +msdn +msdownload +msec +msexcel +msgid +msgkey +msgs +msgsize +msgsmall +msgtext +msie +msmetafile +msnbot +mssql +msvideo +msword +mtime +mtype +mullane +multi +multiactions +multibyte +multicast +multipage +multipageimage +multipageimagenavbox +multipart +multiselect +multisource +multithreaded +multival +multivalue +multpages +munge +musso +mustbeloggedin +mustbeposted +mutator +mutators +muxers +mwdumper +mwfile +mwstore +mwsuggest +mwuser +mxircecho +mycontributions +mycontris +myext +myextension +myisam +mykey +mypage +mypreferences +mysqldump +mytalk +mytext +mywatchlist +möller +nabla +name +namehidden +nameinlowercase +namelookup +namemsg +names +namespace +namespacealiases +namespacebanner +namespacee +namespacenotice +namespacenumber +namespaceoptions +namespaceprotected +namespaces +namespacesall +namespaceselector +namespacing +nassert +nbase +nbsp +nbytes +nchanges +ncount +ndash +nearmatch +nedersaksies +nedersaksisch +needreblock +needservers +needtoken +netcdf +netware +never +new +newaddr +newarticletext +newarticletextanon +newer +newerthanrevid +newgroups +newheader +newid +newimages +newlen +newmessagesdifflinkplural +newmessageslinkplural +newname +newnames +newnamespace +newpage +newpageletter +newpages +newpageshidepatrolled +newparams +newpass +newpassword +newpos +newquery +newrevid +news +newsectionheaderdefaultlevel +newsectionlink +newsectionsummary +newset +newsfeed +newsize +newtalk +newtalks +newtalkseparator +newtext +newtimestamp +newtitle +newuser +newuserlogpage +newuserlogpagetext +newusers +newwidth +newwindow +nextdiff +nextid +nextlink +nextn +nextpage +nextredirect +nextrevision +nextval +nfkc +nfkd +nginx +nheight +niklas +nlink +nlinks +nmime +nnnn +nntp +no +noanimatethumb +noanontoken +noapiwrite +noarchivename +noarticle +noarticletext +noarticletextanon +noautopatrol +noblock +nobots +nobucket +nobuffer +nochange +nochanges +noclasses +nocode +nocomment +nocomplete +nocontent +nocontentconvert +nocontinue +noconvertlink +nocookiesfornew +nocopyright +nocourseid +nocreate +nocreatetext +nocredits +nocta +nodata +nodatabase +nodb +nodefault +nodeid +nodeleteablefile +nodeletion +nodelist +nodename +nodirection +nodotdot +noedit +noeditsection +noemail +noemailprefs +noemailtitle +noeventid +noexec +noexpertise +noexpression +nofeed +nofeedbackid +nofile +nofilekey +nofilename +nofilter +noflagtype +noflip +nofollow +nofound +nogallery +nogomatch +nogroup +noheader +noheadings +nohires +noids +noimage +noimageredirect +noimages +noinclude +noindex +noindexing +nointerwikipage +nointerwikiuserrights +noitem +nojs +nolabel +nolang +nolicense +nolimit +nolink +nolinkstoimage +nologging +nologin +nomahaction +nominornewtalk +nomodule +non +noname +nonamespacenumber +nonascii +noncascading +nondefaults +none +nonewsectionlink +nonexistent +nonfile +nonfilenamespace +nonincludable +noninfringement +noninitial +nonlocal +nonote +nonredirects +nonsense +nonunicodebrowser +noobjective +noofexpiries +noofprotections +noop +nooptions +nooverride +nopaction +nopage +nopageid +nopagetext +nopagetitle +noparser +nopathinfo +nopermission +noport +noprefix +noproject +noprop +noprotections +noquestion +noradius +noratelimit +norating +norcid +noread +noreason +noredir +noredirect +norequest +norestrictiontypes +noresult +noreturnto +norev +norevid +noreviewed +normalizedtitle +norole +norollbackdiff +noscale +noschema +noscript +nosearch +nosectiontitle +nosession +noshade +noskipnotif +noslash +nosniff +nosort +nosortdirection +nosource +nospecialpagetext +nost +nosubaction +nosubject +nosubpage +nosubpages +nosuccess +nosuchaction +nosuchactiontext +nosuchdatabase +nosuchlogid +nosuchpageid +nosuchrcid +nosuchrevid +nosuchsection +nosuchsectiontext +nosuchsectiontitle +nosuchspecialpage +nosuchuser +nosuchusershort +nosummary +notacceptable +notag +notaglist +notalk +notallowed +notanarticle +notarget +notcached +notdeleted +note +notempdir +notemplate +notext +nothumb +notif +notificationtimestamp +notificationtimestamps +notin +notitle +notitleconvert +notloggedin +notminor +noto +notoc +notoggle +notoken +notpatrollable +notransform +notreviewable +notrustworthy +notspecialpage +notsuspended +notvisiblerev +notwatched +notwikitext +notype +noudp +noupdates +nouploadmodule +nouser +nouserid +nousername +nouserspecified +novalues +noview +nowatchlist +nowellwritten +nowiki +nowlocal +nowserver +nparsing +ns +nsassociated +nsfrom +nsinvert +nslinks +nslist +nsname +nsnum +nspname +nsselect +nstab +nsub +ntfs +ntilde +ntitle +nuke +null +nullable +numauthors +number +numberheadings +numberingroup +numberof +numberofactiveusers +numberofadmins +numberofarticles +numberofedits +numberoffiles +numberofpages +numberofusers +numberofwatchingusers +numedits +numentries +numericized +numgroups +numtalkauthors +numtalkedits +numwatchers +nwidth +oacute +objectcache +objective +ocirc +ocount +oelig +of +officedocument +offset +offsite +ofname +ogevents +ogghandler +ograve +old +oldaddr +oldcountable +older +olderror +oldfile +oldgroups +oldid +oldimage +oldlen +oldnamespace +oldquery +oldrev +oldrevid +oldreviewedpages +oldshared +oldsig +oldsize +oldtext +oldtitle +oldtitlemsg +oline +oname +onerror +onkeyup +online +onload +onlyauthor +onlyinclude +onlypst +onlyquery +onsubmit +onthisday +ontop +onuser +openbasedir +opendoc +opendocument +opensearch +opensearchdescription +openssl's +openxml +openxmlformats +operamini +oplus +oppositedm +optgroup +optgroups +optionname +options +optionstoken +optionvalue +optstack +or +ordertype +ordf +ordm +org +orghttp +origcategory +ortime +oslash +other +otherlanguages +otherlist +otheroption +otherreason +othertime +otilde +otimes +otitle +ouml +outparam +outputter +outputtype +outreachwiki +over +overridable +override +overwrite +overwroteimage +own +owner +paction +page +pagecannotexist +pagecategories +pagecategorieslink +pageclass +pagecontent +pagecount +pagecss +pagedeleted +pagedlinks +pageid +pageids +pageimages +pageinfo +pagelink +pagelinks +pagemerge +pagename +pagenamee +pagenames +pagenum +pageoffset +pagepropnames +pageprops +pagerestrictions +pages +pageselector +pageset +pagesetmodule +pagesincategory +pagesinnamespace +pageswithprop +pagetextmsg +pagetitle +pagetools +pagetriage +pagetriageaction +pagetriagelist +pagetriagestats +pagetriagetagging +pagetriagetemplate +pageurl +pageview +pango +param +parameters +paraminfo +paramlist +paramname +params +paren +parens +parentid +parenttree +parms +parse +parsedcomment +parseddescription +parsedsummary +parseerror +parseinline +parsemag +parser +parsercache +parserfuncs +parserfunctions +parserhook +parserrender +parsetree +parsevalue +parsoid +partialupload +partname +pass's +passthru +password +passwordfor +passwordreset +passwordtooshort +paste +pastexpiry +pathchar +pathinfo +pathname +patrol +patroldisabled +patrolled +patrollink +patrolmarks +patroltoken +pattern +pcache +pcntl +pcomment +pdbk +pdf's +pendingdelta +perc +perfcached +perfcachedts +perm +perma +permalink +permdenied +permil +permissiondenied +permissionerror +permissionserrors +permissionserrorstext +permissiontype +perp +perrow +pgsql +photoshop +php +php's +phpfm +phps +phpsapi +phpunit +phpversion +phpwiki +phrasewise +phtml +pi +pipermail +pixmap +pkey +pkuk +pl +plain +plainlink +plainlinks +plaintext +plfrom +plink +pllimit +plns +plpgsql +pltitle +pltitles +plusminus +plusmn +pname +png'd +pnmtojpeg +pnmtopng +pointsize +poolcounter +popts +portlet +portlets +posplus +possible +postcomment +postgre +postsep +potd +potm +potx +poweredby +poweredbyico +powersearch +pp +ppam +ppsm +ppsx +pptm +pptx +precaching +precompiled +preemptively +preferences +preferencestoken +prefill +prefilled +prefix +prefixindex +prefixsearch +prefixsearchdisabled +prefs +prefsection +prefsnologintext2 +prefcontrol +preload +preloads +preloadtitle +prepending +prependtext +preprocess +preprocessing +preprocessors +presentationml +presep +pretransfer +prevchar +prevdiff +previd +previewconflict +previewhead +previewheader +previewnote +previewonfirst +previewontop +previewtext +previousrevision +prevlink +prevn +prexpiry +prfiltercascade +prfx +primary +printableversion +printfooter +printurl +privacypage +private +privs +prlevel +probabalistically +probs +proc +processings +procs +prodromou +profession +profileinfo +programmatically +project +projectpage +promotion +prop +properties +property +propname +props +prot +protect +protectcomment +protectedarticle +protectedinterface +protectednamespace +protectedpage +protectedpages +protectedpagetext +protectedpagewarning +protectedtitle +protectedtitles +protection +protections +protectlevel +protectlogpage +protectlogtext +protectthispage +protecttoken +proto +protocol +protocols +protorel +protos +proxied +proxyblocker +proxyblockreason +proxyunbannable +prtype +psir +pst +psttext +psychedelix +pt +ptext +ptool +pubdate +publicsuffix +publishfailed +punycode +purge +purged +qabardjajəbza +qbar +qbsettings +qlow +qmoicj +qp +quasit +query +querycache +querycachetwo +querycur +querydiff +querykey +querymodule +querymodules +querypage +querypages +querystring +querytype +question +queuefull +quickbar +quicksorts +quicktemplate +quicktime +qunit +quux +qvalues +rabdiff +radic +radius +raggett +raii +raimond +random +randompage +randomredirect +randstr +range +rangeblock +rangeblocks +rangedisabled +rangeend +rangestart +raquo +rarr +rarticle +rasterizations +rasterize +rasterized +rasterizer +ratelimited +ratelimits +rating +ratings +raw +rawfm +rawrow +rbspan +rc +rcdays +rceil +rcfeed +rcid +rcids +rclimit +rcoptions +rcpatroldisabled +rctitle +rctoken +rdev +rdfa +rdfrom +rdftype +rdquo +read +readable +readapidenied +readarray +reader +readline +readonlyreason +readonlytext +readonlywarning +readrequired +readrights +realaudio +realllly +realname +realpath +reason +reasonlist +reasonstr +reblock +rebuildtextindex +recache +recached +recaching +recalc +recentchange +recentchanges +recentchangescount +recentchangesdays +recentchangeslinked +recentchangestext +recenteditcount +recentedits +recip +recips +recreate +recurse +recurses +redir +redirect +redirectable +redirectcreated +redirectedfrom +redirections +redirector +redirectpagesub +redirectparams +redirects +redirectsnippet +redirectstofile +redirecttitle +redirectto +redirid +redirlinks +redirs +redis +redlink +redlinks +redocument +redux +reedyboy +reenables +reencode +reference +refetch +refresheducation +refreshlinks +regexes +regexlike +region +registered +registration +registrationdate +reimport +reindexation +reindexed +releasenotes +relevance +relevant +relicense +relimit +relkind +relname +relnamespace +remarticle +remembermypassword +removablegroups +removal +remove +removed +removedwatchtext +removetags +remreviewer +remstudent +renameuser +renaming +renderable +renderesibanner +renderwarning +renormalized +repeating +repl +replaceafter +replacer +replacers +replag +replyto +reporttime +repos +request +requested +requestid +requeue +required +rerender +rerendered +rescnt +researcher +resends +reset +resetkinds +resetlink +resetpass +resized +resolutioninfo +resolutionunit +resolve +resolved +resourceloader +responsecode +restore +restorelink +restoreprefs +restricted +result +resultset +resultsperpage +retrievedfrom +returnto +returntoquery +retval +reupload +revalidate +revalidation +revdel +revdelete +revdelete'd +revdelundel +revert +reverting +revertpage +reverts +revid +revids +review +reviewactivity +reviewed +reviewer +reviewing +revision +revisionasof +revisionday +revisiondelete +revisionid +revisionmonth +revisions +revisiontext +revisiontimestamp +revisionuser +revisionyear +revlink +revwrongpage +rfloor +rgba +richtext +rights +rightscode +rightsinfo +rightslog +rightslogtext +rked +rmdir +rn +rnlimit +robotstxt +roff +role +rollback +rollbacker +rollbacklink +rollbacklinkcount +rollbacktoken +rootpage +rootuserpages +rowcount +rown +rownum +rowsarr +rowset +rowspan +rowspans +rsaquo +rsargs +rsd +rsdf +rsquo +rss +rsvg +ruleset +rulesets +rusyn +rv +rvcontinue +rvdiffto +rvlimit +rvparse +rvprop +rvstart +rvstartid +rvtoken +sabino +safemode +safesubst +sais +sameorigin +samp +sansserif +save +savearticle +savedprefs +saveprefs +saveusergroups +sawfish +sbin +sbquo +scaler +scalers +scaron +score +screensize +scribunto +scriptable +scriptbuilder +scriptpath +scrolltop +sdot +search +search's +searchaction +searcharticle +searchboxes +searchbutton +searcheverything +searchform +searchindex +searchinfo +searchlimit +searchmenu +searchnamespaces +searchoptions +searchresulttext +searchstring +searchtitle +secondary +section +sectionanchor +sectionedit +sectioneditnotsupported +sectionformat +sectionnumber +sectionprop +sections +sectionsnippet +sectionsnotsupported +sectiontitle +securelogin +seiten +selectandother +selectorother +self +selflink +selfmove +semiglobal +semiprotected +semiprotectedlevels +semiprotectedpagewarning +sendemail +sendmail +sentences +serialize +servedby +servername +servertime +serverurl +sess +session +sessionfailure +sessionid +sessionkey +setchange +setcookie +setemail +setext +setglobalaccountstatus +setnewtype +setnotificationtimestamp +setopt +setrename +setrlimit +setstatus +sha +shar +sharding +shared +shareddescriptionfollows +sharedfile +sharedrepo +sharedupload +shellscript +shiftwidth +shockwave +short +shorturl +shouldn +shouting +show +showalldb +showbots +showdeleted +showdiff +showdifflinks +showfilename +showhiddencats +showhideminor +showhooks +showingresults +showinitializer +showjumplinks +showlinkedto +showme +showmeta +shownavigation +shownumberswatching +showpreview +showredirs +showreviewed +showsizediff +showtoc +showtoolbar +showunreviewed +shtml +si +siebrand +sighhhh +sigkill +sigmaf +signup +sigsegv +sigterm +sii +siit +siiurlwidth +simplesearch +singlegroup +singularthey +sinumberingroup +siprop +site +siteadmin +sitecsspreview +sitedir +siteinfo +sitejspreview +sitemap +sitemaps +sitematrix +sitename +sitenotice +siteprop +sitesearch +sitestats +sitestatsupdate +siteuser +sitewide +size +sizediff +sizediffdisabled +sizes +skey +skinclass +skinkey +skinname +skinnameclass +skins +skipcache +skipcaptcha +skipnotif +skname +sktemplate +slideshow +sm +smaxage +smil +smpp +sms's +smscontent +smslogs +smtp +snippet +sodipodi +softredirect +softtabstop +solaris +somecontent +somefeed +someuser +sorani +sorbs +sorbsreason +sort +sortdirection +sortkey +sortkeyprefix +sortkeys +source +soxred +spam +spamdetected +spamprotected +spamprotectionmatch +spamprotectiontext +spamprotectiontitle +spcontent +special +specialpage +specialpagealiases +specialpageattributes +specialpagegroup +specialpages +specialprotected +speedtip +speedy +speex +spekking +spellcheck +spezial +spoofable +spreadsheetml +sprefs +sprintf +sprotected +sql's +sqlite +sqltotal +sr +srchres +srcset +srgs +srprop +srwhat +stabilize +stable +stablesettings +stansvik +starcode +start +startid +startime +startsortkey +startsortkeyprefix +starttime +starttimestamp +starttransfer +stash +stashfailed +stashimageinfo +state +staticredirect +statistics +statline +status +statuskey +stdclass +stdout +steward +stopwords +storedversion +strcasecmp +strcmp +strftime +string +stripos +stripslashes +strlen +strpos +strrpos +strtime +strtok +strtolower +strtotime +strtr +struct +strval +stubthreshold +student +studies +stuffit +stxt +stylename +stylepath +styleversion +subaction +subarray +subcat +subcats +subclassing +subcond +subconds +subdir +subdomain +subdomains +sube +subelement +subelements +subfunction +subfunctions +subimages +subitem +subitems +subject +subjectid +subjectids +subjectpagename +subjectpagenamee +subjectspace +subjectspacee +subkey +subkeys +sublevels +submatch +submodule +submodule's +submodules +subnet +subpage +subpagename +subpagenamee +subpages +subpagestr +subparents +subprocesses +subsql +substr +succ +success +successbox +suckage +suggest +suggestion +suhosin +suhosin's +summ +summary +summarymissed +summaryrequired +supe +superdomain +superglobals +superset +suppress +suppressed +suppressedredirect +suppressionlog +suppressionlogtext +suppressredirect +suppressrevision +svgs +svn +svnroot +sybase +symlinked +syms +sysinfo +sysop +system +systemnachrichten +szdiff +szlig +szymon +t +tabindex +tablealign +tablecell +tablename +tablesorter +tablestack +tabletags +tabletype +tabstop +tag +tagfilter +tagline +taglist +tags +tagset +tagstack +tahoma +tailorings +talk +talkable +talkfrom +talkid +talkids +talkmove +talkmoveoverredirect +talkpage +talkpageheader +talkpagelinktext +talkpagename +talkpagenamee +talkpagetext +talkspace +talkspacee +talkto +tarask +taraškievica +target +tb +tbase +tbody +tboverride +tcount +tcsh +tddate +tdtime +teardown +telnet +temp +tempdir +template +templatelinks +templatepage +templates +templatesused +templatesusedpreview +templatesusedsection +tempname +tempout +test +testclean +testdata +testmailuser +teston +testpass +testrunner +testswarm +testuser +testutf +texi +texinfo +text +textarea +textareas +textares +textbox +textboxsize +texthidden +textid +textlink +textmissing +textoverride +textsf +textsize +textvector +texvc +tfoot +tful +tg +that'll +thead +thelink +theora +thetasym +thinsp +thisisdeleted +thispage +thumbborder +thumbcaption +thumberror +thumbheight +thumbhtml +thumbimage +thumbinner +thumblimits +thumbmime +thumbnail +thumbnailing +thumbnailsize +thumbname +thumbsize +thumbtext +thumburl +thumbwidth +timeago +timeanddate +timecond +timecorrection +timeframe +timekey +timeoffset +timep +timespans +timestamp +timestamps +timestamptz +timezonelegend +timezoneregion +timezoneuseoffset +timezoneuseserverdefault +tino +title +titleblacklist +titleconversion +titleexists +titlemsg +titleprefixeddbkey +titleprotected +titleprotectedwarning +titles +titlesnippet +titletext +titlevector +tl +tllimit +tltemplates +tmpfile +to +toclevel +tocline +tocnumber +tocsection +toctext +toctitle +tofragment +toggle +toid +token +tokenname +tokens +tolang +tongminh +toobig +toofewexpiries +toohigh +toolarray +toolbarparent +toolboxend +toolboxlink +toolong +toolow +tooltiponly +tooshort +top +toparse +topbar +toplevel +toplinks +topojson +toponly +torev +torevid +tornevall +torunblocked +totalcnt +totalcount +totalhits +totalmemory +totaltime +totitle +touched +tplarg +transcludable +transclude +transcluded +transcluding +transclusion +transclusions +transcode +transcodekey +transcoder +transcodereset +transcodestatus +transcoding +translatewiki +transstat +transwiki +troff +true +truespeed +truncatedtext +trustworthy +truteq +truthy +tsearch +tsquery +tuple +tweakblogs +tweakers +txt +txtfm +type +typemustmatch +typeof +typname +tzstring +uacute +uarr +uc +ucfirst +ucirc +udpprofile +ufffd +ugrave +ui +uids +uint +ulimit +ulink +ulinks +uname +unanchored +unapprove +unary +unattached +unauthenticate +unavailable +unblock +unblocklogentry +unblockself +unblocktoken +unbuffered +uncacheable +uncached +uncategorized +unclosable +uncompress +undel +undelete +undeleted +undeletion +undismissable +undismissible +undo +undoafter +undofailure +undorev +unescape +unescaped +unfeature +unfeatured +unflag +ungrouped +unhelpful +unhidden +unhide +unidata +unidecode +unindent +unindexed +uniq +unique +universaleditbutton +unixtime +unknown +unknownerror +unknownnamespace +unlock +unlockdb +unlogged +unmakesafe +unmark +unmerge +unmodified +unpadded +unpatrolled +unpatrolledletter +unprefixed +unprintables +unprotect +unprotectedarticle +unprotection +unprotectthispage +unreadcount +unredacted +unrequest +unrequested +unresolve +unresolved +unreviewed +unreviewedpages +unsanitized +unseed +unserialization +unserialize +unserialized +unserializes +unserializing +unsetting +unstub +unstubbed +unstubbing +unstubs +unsupportednamespace +unsupportedrepo +untaint +untracked +untrustworthiness +unused +unusual +unversioned +unviewable +unviewed +unwatch +unwatched +unwatchedpages +unwatching +unwatchthispage +unwikified +unwritable +upconvert +updateddate +updatedtime +updatelog +upgradedoc +upgrader +upload +upload's +uploaddisabled +uploadedimage +uploadjava +uploadlogpage +uploadlogpagetext +uploadnewversion +uploadnologintext +uploadpage +uploadscripted +uploadsource +uploadstash +uploadvirus +uploadwarning +uppercased +upsih +urandom +url +url's +urlaction +urldecode +urldecoded +urlencode +urlencoded +urlheight +urlparam +urlparm +urlpath +urlvar +urlwidth +ursh +us +usedomain +useemail +uselang +uselivepreview +usemod +usemsgcache +usenewrc +user +useragent +useragents +userblock +usercan +usercontribs +usercreate +usercreated +usercss +usercsspreview +usercssyoucanpreview +userdailycontribs +userdir +userdoesnotexist +usereditcount +useredits +useremail +userexists +usergroup +usergroups +userhidden +userid +userinfo +userinvalidcssjstitle +userips +userjs +userjsprev +userjspreview +userjsyoucanpreview +userlang +userlangattributes +userlink +userlinks +userlogin +userloginlink +userloginprompt +userlogout +usermaildisabled +usermessage +username +usernameless +usernames +userpage +userpages +userpageurl +userprefix +userrights +userrightstoken +users +usersbody +userspace +usertalk +usertalklink +usertext +usertoollinks +useskin +useto +usort +usrmonth +ussd +ussdcontent +ustar +ustoken +utfnormal +uuml +validate +validationbuilder +valign +vals +value +values +vandal +vandalism +variables +variant +variantarticlepath +varlang +varname +vars +varval +vasiliev +vasilvv +vbase +vbscript +vcount +vcsize +venema's +verbosify +version +versioning +versionlink +versionlog +versionrequired +versionrequiredtext +very +vhost +vi +vibber +videoinfo +view +viewcount +viewdeleted +viewhelppage +viewmyprivateinfo +viewmywatchlist +viewport +viewprevnext +viewsource +viewsourcelink +viewsourcetext +viewvc +viewyourtext +visible +visualeditor +viurlwidth +voff +vofp +voicexml +vorbis +vpad +vrml +vslow +vumi +vvcv +vxml +wais +wait +wakeup +walltime +warmup +warning +wasdeleted +wasn +watch +watchcreations +watchdefault +watchdeletion +watched +watchlist +watchlistdays +watchlisthideanons +watchlisthidebots +watchlisthideliu +watchlisthideminor +watchlisthideown +watchlisthidepatrolled +watchlistraw +watchlists +watchlisttoken +watchmoves +watchthis +watchthispage +watchtoken +watchuser +wb +wbmp +wbxml +wddx +wddxfm +weblog +weblogs +webm +webp +webrequest +webserver +weeks +weierp +weight +wellwritten +werdna +wget +what +whatlinkshere +whatwg +wheely +whether +whitelist +whitelisted +whitelistedittext +whitelisting +whois +wicke +width +widthx +wierkosz +wietse +wiki +wiki'd +wiki's +wikia +wikiadmin +wikibase +wikibits +wikibooks +wikidb +wikifarm +wikiid +wikilink +wikilinks +wikilove +wikiloveimagelog +wikimedia +wikimediacommons +wikimediafoundation +wikinews +wikipage +wikipedia +wikipedian +wikipedias +wikiquote +wikis +wikisource +wikisyntax +wikitable +wikitables +wikitech +wikitext +wikiuser +wikiversity +wikivoyage +wiktionary +wincache +wininet +withaccess +withaction +witheditsonly +withlanglinks +withoutlanglinks +wl +wlallrev +wldir +wlend +wlexcludeuser +wllimit +wlowner +wlprop +wltoken +wmf's +wml +wmlc +wmls +wmlsc +wmlscript +wmlscriptc +wordcount +wordprocessingml +wordwg +workalike +worldwind +wouldn +wr +writeapi +writeapidenied +writedisabled +writerequired +writerights +wrongpassword +x +xanalytics +xbitmap +xcancel +xdebug +xdiff +xdomain +xdomains +xff +xhtmldefaultnamespace +xhtmlnamespaces +xiff +xlam +xlsb +xlsm +xlsx +xltm +xltx +xml +xmldoublequote +xmlfm +xmlimport +xmlmeta +xmlns +xmlselect +xor +xpinstall +xpixmap +xpsdocument +xtended +xwindowdump +xxxx +xxxxx +yacute +yaml +yamlfm +yandex +year +yes +youhavenewmessages +youhavenewmessagesfromusers +youhavenewmessagesmanyusers +youhavenewmessagesmulti +yourdiff +yourdomainname +youremail +yourgender +yourinternal +yourlanguage +yourname +yournick +yourpassword +yourrealname +yourtext +yourvariant +yourwiki +yuml +yyyymmddhhiiss +zcmd +zerobanner +zerobar +zerobutton +zeroconfig +zerodontask +zerodot +zeroinfo +zeronet +zeroportal +zfile +zhdaemon +zhengzhu +zhtable +zijdel +zlang +zlib +zoffset +zrma +zwnj +ænglisc +ævar +świerkosz diff --git a/www/wiki/maintenance/doMaintenance.php b/www/wiki/maintenance/doMaintenance.php new file mode 100644 index 00000000..f3fb32ce --- /dev/null +++ b/www/wiki/maintenance/doMaintenance.php @@ -0,0 +1,113 @@ +setup(); + +// We used to call this variable $self, but it was moved +// to $maintenance->mSelf. Keep that here for b/c +$self = $maintenance->getName(); + +// Define how settings are loaded (e.g. LocalSettings.php) +if ( !defined( 'MW_CONFIG_CALLBACK' ) && !defined( 'MW_CONFIG_FILE' ) ) { + define( 'MW_CONFIG_FILE', $maintenance->loadSettings() ); +} + +// Custom setup for Maintenance entry point +if ( !defined( 'MW_SETUP_CALLBACK' ) ) { + function wfMaintenanceSetup() { + // phpcs:ignore MediaWiki.NamingConventions.ValidGlobalName.wgPrefix + global $maintenance, $wgLocalisationCacheConf, $wgCacheDirectory; + if ( $maintenance->getDbType() === Maintenance::DB_NONE ) { + if ( $wgLocalisationCacheConf['storeClass'] === false + && ( $wgLocalisationCacheConf['store'] == 'db' + || ( $wgLocalisationCacheConf['store'] == 'detect' && !$wgCacheDirectory ) ) + ) { + $wgLocalisationCacheConf['storeClass'] = LCStoreNull::class; + } + } + + $maintenance->finalSetup(); + } + define( 'MW_SETUP_CALLBACK', 'wfMaintenanceSetup' ); +} + +require_once "$IP/includes/Setup.php"; + +// Initialize main config instance +$maintenance->setConfig( MediaWikiServices::getInstance()->getMainConfig() ); + +// Sanity-check required extensions are installed +$maintenance->checkRequiredExtensions(); + +// A good time when no DBs have writes pending is around lag checks. +// This avoids having long running scripts just OOM and lose all the updates. +$maintenance->setAgentAndTriggers(); + +// Do the work +$maintenance->execute(); + +// Potentially debug globals +$maintenance->globals(); + +if ( $maintenance->getDbType() !== Maintenance::DB_NONE ) { + // Perform deferred updates. + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lbFactory->commitMasterChanges( $maintClass ); + DeferredUpdates::doUpdates(); +} + +// log profiling info +wfLogProfilingData(); + +if ( isset( $lbFactory ) ) { + // Commit and close up! + $lbFactory->commitMasterChanges( 'doMaintenance' ); + $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT ); +} diff --git a/www/wiki/maintenance/dumpBackup.php b/www/wiki/maintenance/dumpBackup.php new file mode 100644 index 00000000..6bbd86d5 --- /dev/null +++ b/www/wiki/maintenance/dumpBackup.php @@ -0,0 +1,137 @@ + + * 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 + * @ingroup Dump Maintenance + */ + +require_once __DIR__ . '/backup.inc'; + +class DumpBackup extends BackupDumper { + function __construct( $args = null ) { + parent::__construct(); + + $this->addDescription( <<stderr = fopen( "php://stderr", "wt" ); + // Actions + $this->addOption( 'full', 'Dump all revisions of every page' ); + $this->addOption( 'current', 'Dump only the latest revision of every page.' ); + $this->addOption( 'logs', 'Dump all log events' ); + $this->addOption( 'stable', 'Dump stable versions of pages' ); + $this->addOption( 'revrange', 'Dump range of revisions specified by revstart and ' . + 'revend parameters' ); + $this->addOption( 'orderrevs', 'Dump revisions in ascending revision order ' . + '(implies dump of a range of pages)' ); + $this->addOption( 'pagelist', + 'Dump only pages included in the file', false, true ); + // Options + $this->addOption( 'start', 'Start from page_id or log_id', false, true ); + $this->addOption( 'end', 'Stop before page_id or log_id n (exclusive)', false, true ); + $this->addOption( 'revstart', 'Start from rev_id', false, true ); + $this->addOption( 'revend', 'Stop before rev_id n (exclusive)', false, true ); + $this->addOption( 'skip-header', 'Don\'t output the header' ); + $this->addOption( 'skip-footer', 'Don\'t output the footer' ); + $this->addOption( 'stub', 'Don\'t perform old_text lookups; for 2-pass dump' ); + $this->addOption( 'uploads', 'Include upload records without files' ); + $this->addOption( 'include-files', 'Include files within the XML stream' ); + + if ( $args ) { + $this->loadWithArgv( $args ); + $this->processOptions(); + } + } + + function execute() { + $this->processOptions(); + + $textMode = $this->hasOption( 'stub' ) ? WikiExporter::STUB : WikiExporter::TEXT; + + if ( $this->hasOption( 'full' ) ) { + $this->dump( WikiExporter::FULL, $textMode ); + } elseif ( $this->hasOption( 'current' ) ) { + $this->dump( WikiExporter::CURRENT, $textMode ); + } elseif ( $this->hasOption( 'stable' ) ) { + $this->dump( WikiExporter::STABLE, $textMode ); + } elseif ( $this->hasOption( 'logs' ) ) { + $this->dump( WikiExporter::LOGS ); + } elseif ( $this->hasOption( 'revrange' ) ) { + $this->dump( WikiExporter::RANGE, $textMode ); + } else { + $this->fatalError( 'No valid action specified.' ); + } + } + + function processOptions() { + parent::processOptions(); + + // Evaluate options specific to this class + $this->reporting = !$this->hasOption( 'quiet' ); + + if ( $this->hasOption( 'pagelist' ) ) { + $filename = $this->getOption( 'pagelist' ); + $pages = file( $filename ); + if ( $pages === false ) { + $this->fatalError( "Unable to open file {$filename}\n" ); + } + $pages = array_map( 'trim', $pages ); + $this->pages = array_filter( $pages, function ( $x ) { + return $x !== ''; + } ); + } + + if ( $this->hasOption( 'start' ) ) { + $this->startId = intval( $this->getOption( 'start' ) ); + } + + if ( $this->hasOption( 'end' ) ) { + $this->endId = intval( $this->getOption( 'end' ) ); + } + + if ( $this->hasOption( 'revstart' ) ) { + $this->revStartId = intval( $this->getOption( 'revstart' ) ); + } + + if ( $this->hasOption( 'revend' ) ) { + $this->revEndId = intval( $this->getOption( 'revend' ) ); + } + + $this->skipHeader = $this->hasOption( 'skip-header' ); + $this->skipFooter = $this->hasOption( 'skip-footer' ); + $this->dumpUploads = $this->hasOption( 'uploads' ); + $this->dumpUploadFileContents = $this->hasOption( 'include-files' ); + $this->orderRevs = $this->hasOption( 'orderrevs' ); + } +} + +$maintClass = DumpBackup::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/dumpCategoriesAsRdf.php b/www/wiki/maintenance/dumpCategoriesAsRdf.php new file mode 100644 index 00000000..e4bd7564 --- /dev/null +++ b/www/wiki/maintenance/dumpCategoriesAsRdf.php @@ -0,0 +1,184 @@ +addDescription( "Generate RDF dump of categories in a wiki." ); + + $this->setBatchSize( 200 ); + $this->addOption( 'output', "Output file (default is stdout). Will be overwritten.", + false, true ); + $this->addOption( 'format', "Set the dump format.", false, true ); + } + + /** + * Produce row iterator for categories. + * @param IDatabase $dbr Database connection + * @return RecursiveIterator + */ + public function getCategoryIterator( IDatabase $dbr ) { + $it = new BatchRowIterator( + $dbr, + [ 'page', 'page_props', 'category' ], + [ 'page_title' ], + $this->getBatchSize() + ); + $it->addConditions( [ + 'page_namespace' => NS_CATEGORY, + ] ); + $it->setFetchColumns( [ + 'page_title', + 'page_id', + 'pp_propname', + 'cat_pages', + 'cat_subcats', + 'cat_files' + ] ); + $it->addJoinConditions( + [ + 'page_props' => [ + 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] + ], + 'category' => [ + 'LEFT JOIN', [ 'cat_title = page_title' ] + ] + ] + + ); + return $it; + } + + /** + * Get iterator for links for categories. + * @param IDatabase $dbr + * @param array $ids List of page IDs + * @return Traversable + */ + public function getCategoryLinksIterator( IDatabase $dbr, array $ids ) { + $it = new BatchRowIterator( + $dbr, + 'categorylinks', + [ 'cl_from', 'cl_to' ], + $this->getBatchSize() + ); + $it->addConditions( [ + 'cl_type' => 'subcat', + 'cl_from' => $ids + ] ); + $it->setFetchColumns( [ 'cl_from', 'cl_to' ] ); + return new RecursiveIteratorIterator( $it ); + } + + /** + * @param int $timestamp + */ + public function addDumpHeader( $timestamp ) { + global $wgRightsUrl; + $licenseUrl = $wgRightsUrl; + if ( substr( $licenseUrl, 0, 2 ) == '//' ) { + $licenseUrl = 'https:' . $licenseUrl; + } + $this->rdfWriter->about( $this->categoriesRdf->getDumpURI() ) + ->a( 'schema', 'Dataset' ) + ->a( 'owl', 'Ontology' ) + ->say( 'cc', 'license' )->is( $licenseUrl ) + ->say( 'schema', 'softwareVersion' )->value( CategoriesRdf::FORMAT_VERSION ) + ->say( 'schema', 'dateModified' ) + ->value( wfTimestamp( TS_ISO_8601, $timestamp ), 'xsd', 'dateTime' ) + ->say( 'schema', 'isPartOf' )->is( wfExpandUrl( '/', PROTO_CANONICAL ) ) + ->say( 'owl', 'imports' )->is( CategoriesRdf::OWL_URL ); + } + + public function execute() { + $outFile = $this->getOption( 'output', 'php://stdout' ); + + if ( $outFile === '-' ) { + $outFile = 'php://stdout'; + } + + $output = fopen( $outFile, 'w' ); + $this->rdfWriter = $this->createRdfWriter( $this->getOption( 'format', 'ttl' ) ); + $this->categoriesRdf = new CategoriesRdf( $this->rdfWriter ); + + $this->categoriesRdf->setupPrefixes(); + $this->rdfWriter->start(); + + $this->addDumpHeader( time() ); + fwrite( $output, $this->rdfWriter->drain() ); + + $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] ); + + foreach ( $this->getCategoryIterator( $dbr ) as $batch ) { + $pages = []; + foreach ( $batch as $row ) { + $this->categoriesRdf->writeCategoryData( + $row->page_title, + $row->pp_propname === 'hiddencat', + (int)$row->cat_pages - (int)$row->cat_subcats - (int)$row->cat_files, + (int)$row->cat_subcats + ); + $pages[$row->page_id] = $row->page_title; + } + + foreach ( $this->getCategoryLinksIterator( $dbr, array_keys( $pages ) ) as $row ) { + $this->categoriesRdf->writeCategoryLinkData( $pages[$row->cl_from], $row->cl_to ); + } + fwrite( $output, $this->rdfWriter->drain() ); + } + fflush( $output ); + if ( $outFile !== '-' ) { + fclose( $output ); + } + } + + /** + * @param string $format Writer format + * @return RdfWriter + */ + private function createRdfWriter( $format ) { + $factory = new RdfWriterFactory(); + return $factory->getWriter( $factory->getFormatName( $format ) ); + } +} + +$maintClass = DumpCategoriesAsRdf::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/dumpIterator.php b/www/wiki/maintenance/dumpIterator.php new file mode 100644 index 00000000..e9a6bc58 --- /dev/null +++ b/www/wiki/maintenance/dumpIterator.php @@ -0,0 +1,189 @@ +addDescription( 'Does something with a dump' ); + $this->addOption( 'file', 'File with text to run.', false, true ); + $this->addOption( 'dump', 'XML dump to execute all revisions.', false, true ); + $this->addOption( 'from', 'Article from XML dump to start from.', false, true ); + } + + public function execute() { + if ( !( $this->hasOption( 'file' ) ^ $this->hasOption( 'dump' ) ) ) { + $this->fatalError( "You must provide a file or dump" ); + } + + $this->checkOptions(); + + if ( $this->hasOption( 'file' ) ) { + $revision = new WikiRevision( $this->getConfig() ); + + $revision->setText( file_get_contents( $this->getOption( 'file' ) ) ); + $revision->setTitle( Title::newFromText( + rawurldecode( basename( $this->getOption( 'file' ), '.txt' ) ) + ) ); + $this->handleRevision( $revision ); + + return; + } + + $this->startTime = microtime( true ); + + if ( $this->getOption( 'dump' ) == '-' ) { + $source = new ImportStreamSource( $this->getStdin() ); + } else { + $this->fatalError( "Sorry, I don't support dump filenames yet. " + . "Use - and provide it on stdin on the meantime." ); + } + $importer = new WikiImporter( $source, $this->getConfig() ); + + $importer->setRevisionCallback( + [ $this, 'handleRevision' ] ); + $importer->setNoticeCallback( function ( $msg, $params ) { + echo wfMessage( $msg, $params )->text() . "\n"; + } ); + + $this->from = $this->getOption( 'from', null ); + $this->count = 0; + $importer->doImport(); + + $this->conclusions(); + + $delta = microtime( true ) - $this->startTime; + $this->error( "Done {$this->count} revisions in " . round( $delta, 2 ) . " seconds " ); + if ( $delta > 0 ) { + $this->error( round( $this->count / $delta, 2 ) . " pages/sec" ); + } + + # Perform the memory_get_peak_usage() when all the other data has been + # output so there's no damage if it dies. It is only available since + # 5.2.0 (since 5.2.1 if you haven't compiled with --enable-memory-limit) + $this->error( "Memory peak usage of " . memory_get_peak_usage() . " bytes\n" ); + } + + public function finalSetup() { + parent::finalSetup(); + + if ( $this->getDbType() == Maintenance::DB_NONE ) { + global $wgUseDatabaseMessages, $wgLocalisationCacheConf, $wgHooks; + $wgUseDatabaseMessages = false; + $wgLocalisationCacheConf['storeClass'] = LCStoreNull::class; + $wgHooks['InterwikiLoadPrefix'][] = 'DumpIterator::disableInterwikis'; + } + } + + static function disableInterwikis( $prefix, &$data ) { + # Title::newFromText will check on each namespaced article if it's an interwiki. + # We always answer that it is not. + + return false; + } + + /** + * Callback function for each revision, child classes should override + * processRevision instead. + * @param WikiRevision $rev + */ + public function handleRevision( $rev ) { + $title = $rev->getTitle(); + if ( !$title ) { + $this->error( "Got bogus revision with null title!" ); + + return; + } + + $this->count++; + if ( isset( $this->from ) ) { + if ( $this->from != $title ) { + return; + } + $this->output( "Skipped " . ( $this->count - 1 ) . " pages\n" ); + + $this->count = 1; + $this->from = null; + } + + $this->processRevision( $rev ); + } + + /* Stub function for processing additional options */ + public function checkOptions() { + return; + } + + /* Stub function for giving data about what was computed */ + public function conclusions() { + return; + } + + /* Core function which does whatever the maintenance script is designed to do */ + abstract public function processRevision( $rev ); +} + +/** + * Maintenance script that runs a regex in the revisions from a dump. + * + * @ingroup Maintenance + */ +class SearchDump extends DumpIterator { + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Runs a regex in the revisions from a dump' ); + $this->addOption( 'regex', 'Searching regex', true, true ); + } + + public function getDbType() { + return Maintenance::DB_NONE; + } + + /** + * @param Revision $rev + */ + public function processRevision( $rev ) { + if ( preg_match( $this->getOption( 'regex' ), $rev->getContent()->getTextForSearchIndex() ) ) { + $this->output( $rev->getTitle() . " matches at edit from " . $rev->getTimestamp() . "\n" ); + } + } +} + +$maintClass = SearchDump::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/dumpLinks.php b/www/wiki/maintenance/dumpLinks.php new file mode 100644 index 00000000..6904953f --- /dev/null +++ b/www/wiki/maintenance/dumpLinks.php @@ -0,0 +1,79 @@ + + * 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 + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that generates a plaintext link dump. + * + * @ingroup Maintenance + */ +class DumpLinks extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addDescription( 'Quick demo hack to generate a plaintext link dump' ); + } + + public function execute() { + $dbr = $this->getDB( DB_REPLICA ); + $result = $dbr->select( [ 'pagelinks', 'page' ], + [ + 'page_id', + 'page_namespace', + 'page_title', + 'pl_namespace', + 'pl_title' ], + [ 'page_id=pl_from' ], + __METHOD__, + [ 'ORDER BY' => 'page_id' ] ); + + $lastPage = null; + foreach ( $result as $row ) { + if ( $lastPage != $row->page_id ) { + if ( $lastPage !== null ) { + $this->output( "\n" ); + } + $page = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->output( $page->getPrefixedURL() ); + $lastPage = $row->page_id; + } + $link = Title::makeTitle( $row->pl_namespace, $row->pl_title ); + $this->output( " " . $link->getPrefixedURL() ); + } + if ( $lastPage !== null ) { + $this->output( "\n" ); + } + } +} + +$maintClass = DumpLinks::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/dumpTextPass.php b/www/wiki/maintenance/dumpTextPass.php new file mode 100644 index 00000000..59a6b517 --- /dev/null +++ b/www/wiki/maintenance/dumpTextPass.php @@ -0,0 +1,992 @@ + + * 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 + * @ingroup Maintenance + */ + +require_once __DIR__ . '/backup.inc'; +require_once __DIR__ . '/7zip.inc'; +require_once __DIR__ . '/../includes/export/WikiExporter.php'; + +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IMaintainableDatabase; + +/** + * @ingroup Maintenance + */ +class TextPassDumper extends BackupDumper { + /** @var BaseDump */ + public $prefetch = null; + /** @var string|bool */ + private $thisPage; + /** @var string|bool */ + private $thisRev; + + // when we spend more than maxTimeAllowed seconds on this run, we continue + // processing until we write out the next complete page, then save output file(s), + // rename it/them and open new one(s) + public $maxTimeAllowed = 0; // 0 = no limit + + protected $input = "php://stdin"; + protected $history = WikiExporter::FULL; + protected $fetchCount = 0; + protected $prefetchCount = 0; + protected $prefetchCountLast = 0; + protected $fetchCountLast = 0; + + protected $maxFailures = 5; + protected $maxConsecutiveFailedTextRetrievals = 200; + protected $failureTimeout = 5; // Seconds to sleep after db failure + + protected $bufferSize = 524288; // In bytes. Maximum size to read from the stub in on go. + + protected $php = "php"; + protected $spawn = false; + + /** + * @var bool|resource + */ + protected $spawnProc = false; + + /** + * @var bool|resource + */ + protected $spawnWrite = false; + + /** + * @var bool|resource + */ + protected $spawnRead = false; + + /** + * @var bool|resource + */ + protected $spawnErr = false; + + /** + * @var bool|XmlDumpWriter + */ + protected $xmlwriterobj = false; + + protected $timeExceeded = false; + protected $firstPageWritten = false; + protected $lastPageWritten = false; + protected $checkpointJustWritten = false; + protected $checkpointFiles = []; + + /** + * @var IMaintainableDatabase + */ + protected $db; + + /** + * @param array $args For backward compatibility + */ + function __construct( $args = null ) { + parent::__construct(); + + $this->addDescription( <<stderr = fopen( "php://stderr", "wt" ); + + $this->addOption( 'stub', 'To load a compressed stub dump instead of stdin. ' . + 'Specify as --stub=:.', false, true ); + $this->addOption( 'prefetch', 'Use a prior dump file as a text source, to savepressure on the ' . + 'database. (Requires the XMLReader extension). Specify as --prefetch=:', + false, true ); + $this->addOption( 'maxtime', 'Write out checkpoint file after this many minutes (writing' . + 'out complete page, closing xml file properly, and opening new one' . + 'with header). This option requires the checkpointfile option.', false, true ); + $this->addOption( 'checkpointfile', 'Use this string for checkpoint filenames,substituting ' . + 'first pageid written for the first %s (required) and the last pageid written for the ' . + 'second %s if it exists.', false, true, false, true ); // This can be specified multiple times + $this->addOption( 'quiet', 'Don\'t dump status reports to stderr.' ); + $this->addOption( 'current', 'Base ETA on number of pages in database instead of all revisions' ); + $this->addOption( 'spawn', 'Spawn a subprocess for loading text records' ); + $this->addOption( 'buffersize', 'Buffer size in bytes to use for reading the stub. ' . + '(Default: 512KB, Minimum: 4KB)', false, true ); + + if ( $args ) { + $this->loadWithArgv( $args ); + $this->processOptions(); + } + } + + function execute() { + $this->processOptions(); + $this->dump( true ); + } + + function processOptions() { + parent::processOptions(); + + if ( $this->hasOption( 'buffersize' ) ) { + $this->bufferSize = max( intval( $this->getOption( 'buffersize' ) ), 4 * 1024 ); + } + + if ( $this->hasOption( 'prefetch' ) ) { + $url = $this->processFileOpt( $this->getOption( 'prefetch' ) ); + $this->prefetch = new BaseDump( $url ); + } + + if ( $this->hasOption( 'stub' ) ) { + $this->input = $this->processFileOpt( $this->getOption( 'stub' ) ); + } + + if ( $this->hasOption( 'maxtime' ) ) { + $this->maxTimeAllowed = intval( $this->getOption( 'maxtime' ) ) * 60; + } + + if ( $this->hasOption( 'checkpointfile' ) ) { + $this->checkpointFiles = $this->getOption( 'checkpointfile' ); + } + + if ( $this->hasOption( 'current' ) ) { + $this->history = WikiExporter::CURRENT; + } + + if ( $this->hasOption( 'full' ) ) { + $this->history = WikiExporter::FULL; + } + + if ( $this->hasOption( 'spawn' ) ) { + $this->spawn = true; + $val = $this->getOption( 'spawn' ); + if ( $val !== 1 ) { + $this->php = $val; + } + } + } + + /** + * Drop the database connection $this->db and try to get a new one. + * + * This function tries to get a /different/ connection if this is + * possible. Hence, (if this is possible) it switches to a different + * failover upon each call. + * + * This function resets $this->lb and closes all connections on it. + * + * @throws MWException + */ + function rotateDb() { + // Cleaning up old connections + if ( isset( $this->lb ) ) { + $this->lb->closeAll(); + unset( $this->lb ); + } + + if ( $this->forcedDb !== null ) { + $this->db = $this->forcedDb; + + return; + } + + if ( isset( $this->db ) && $this->db->isOpen() ) { + throw new MWException( 'DB is set and has not been closed by the Load Balancer' ); + } + + unset( $this->db ); + + // Trying to set up new connection. + // We do /not/ retry upon failure, but delegate to encapsulating logic, to avoid + // individually retrying at different layers of code. + + try { + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $this->lb = $lbFactory->newMainLB(); + } catch ( Exception $e ) { + throw new MWException( __METHOD__ + . " rotating DB failed to obtain new load balancer (" . $e->getMessage() . ")" ); + } + + try { + $this->db = $this->lb->getConnection( DB_REPLICA, 'dump' ); + } catch ( Exception $e ) { + throw new MWException( __METHOD__ + . " rotating DB failed to obtain new database (" . $e->getMessage() . ")" ); + } + } + + function initProgress( $history = WikiExporter::FULL ) { + parent::initProgress(); + $this->timeOfCheckpoint = $this->startTime; + } + + function dump( $history, $text = WikiExporter::TEXT ) { + // Notice messages will foul up your XML output even if they're + // relatively harmless. + if ( ini_get( 'display_errors' ) ) { + ini_set( 'display_errors', 'stderr' ); + } + + $this->initProgress( $this->history ); + + // We are trying to get an initial database connection to avoid that the + // first try of this request's first call to getText fails. However, if + // obtaining a good DB connection fails it's not a serious issue, as + // getText does retry upon failure and can start without having a working + // DB connection. + try { + $this->rotateDb(); + } catch ( Exception $e ) { + // We do not even count this as failure. Just let eventual + // watchdogs know. + $this->progress( "Getting initial DB connection failed (" . + $e->getMessage() . ")" ); + } + + $this->egress = new ExportProgressFilter( $this->sink, $this ); + + // it would be nice to do it in the constructor, oh well. need egress set + $this->finalOptionCheck(); + + // we only want this so we know how to close a stream :-P + $this->xmlwriterobj = new XmlDumpWriter(); + + $input = fopen( $this->input, "rt" ); + $this->readDump( $input ); + + if ( $this->spawnProc ) { + $this->closeSpawn(); + } + + $this->report( true ); + } + + function processFileOpt( $opt ) { + $split = explode( ':', $opt, 2 ); + $val = $split[0]; + $param = ''; + if ( count( $split ) === 2 ) { + $param = $split[1]; + } + $fileURIs = explode( ';', $param ); + foreach ( $fileURIs as $URI ) { + switch ( $val ) { + case "file": + $newURI = $URI; + break; + case "gzip": + $newURI = "compress.zlib://$URI"; + break; + case "bzip2": + $newURI = "compress.bzip2://$URI"; + break; + case "7zip": + $newURI = "mediawiki.compress.7z://$URI"; + break; + default: + $newURI = $URI; + } + $newFileURIs[] = $newURI; + } + $val = implode( ';', $newFileURIs ); + + return $val; + } + + /** + * Overridden to include prefetch ratio if enabled. + */ + function showReport() { + if ( !$this->prefetch ) { + parent::showReport(); + + return; + } + + if ( $this->reporting ) { + $now = wfTimestamp( TS_DB ); + $nowts = microtime( true ); + $deltaAll = $nowts - $this->startTime; + $deltaPart = $nowts - $this->lastTime; + $this->pageCountPart = $this->pageCount - $this->pageCountLast; + $this->revCountPart = $this->revCount - $this->revCountLast; + + if ( $deltaAll ) { + $portion = $this->revCount / $this->maxCount; + $eta = $this->startTime + $deltaAll / $portion; + $etats = wfTimestamp( TS_DB, intval( $eta ) ); + if ( $this->fetchCount ) { + $fetchRate = 100.0 * $this->prefetchCount / $this->fetchCount; + } else { + $fetchRate = '-'; + } + $pageRate = $this->pageCount / $deltaAll; + $revRate = $this->revCount / $deltaAll; + } else { + $pageRate = '-'; + $revRate = '-'; + $etats = '-'; + $fetchRate = '-'; + } + if ( $deltaPart ) { + if ( $this->fetchCountLast ) { + $fetchRatePart = 100.0 * $this->prefetchCountLast / $this->fetchCountLast; + } else { + $fetchRatePart = '-'; + } + $pageRatePart = $this->pageCountPart / $deltaPart; + $revRatePart = $this->revCountPart / $deltaPart; + } else { + $fetchRatePart = '-'; + $pageRatePart = '-'; + $revRatePart = '-'; + } + $this->progress( sprintf( + "%s: %s (ID %d) %d pages (%0.1f|%0.1f/sec all|curr), " + . "%d revs (%0.1f|%0.1f/sec all|curr), %0.1f%%|%0.1f%% " + . "prefetched (all|curr), ETA %s [max %d]", + $now, wfWikiID(), $this->ID, $this->pageCount, $pageRate, + $pageRatePart, $this->revCount, $revRate, $revRatePart, + $fetchRate, $fetchRatePart, $etats, $this->maxCount + ) ); + $this->lastTime = $nowts; + $this->revCountLast = $this->revCount; + $this->prefetchCountLast = $this->prefetchCount; + $this->fetchCountLast = $this->fetchCount; + } + } + + function setTimeExceeded() { + $this->timeExceeded = true; + } + + function checkIfTimeExceeded() { + if ( $this->maxTimeAllowed + && ( $this->lastTime - $this->timeOfCheckpoint > $this->maxTimeAllowed ) + ) { + return true; + } + + return false; + } + + function finalOptionCheck() { + if ( ( $this->checkpointFiles && !$this->maxTimeAllowed ) + || ( $this->maxTimeAllowed && !$this->checkpointFiles ) + ) { + throw new MWException( "Options checkpointfile and maxtime must be specified together.\n" ); + } + foreach ( $this->checkpointFiles as $checkpointFile ) { + $count = substr_count( $checkpointFile, "%s" ); + if ( $count != 2 ) { + throw new MWException( "Option checkpointfile must contain two '%s' " + . "for substitution of first and last pageids, count is $count instead, " + . "file is $checkpointFile.\n" ); + } + } + + if ( $this->checkpointFiles ) { + $filenameList = (array)$this->egress->getFilenames(); + if ( count( $filenameList ) != count( $this->checkpointFiles ) ) { + throw new MWException( "One checkpointfile must be specified " + . "for each output option, if maxtime is used.\n" ); + } + } + } + + /** + * @throws MWException Failure to parse XML input + * @param string $input + * @return bool + */ + function readDump( $input ) { + $this->buffer = ""; + $this->openElement = false; + $this->atStart = true; + $this->state = ""; + $this->lastName = ""; + $this->thisPage = 0; + $this->thisRev = 0; + $this->thisRevModel = null; + $this->thisRevFormat = null; + + $parser = xml_parser_create( "UTF-8" ); + xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); + + xml_set_element_handler( + $parser, + [ $this, 'startElement' ], + [ $this, 'endElement' ] + ); + xml_set_character_data_handler( $parser, [ $this, 'characterData' ] ); + + $offset = 0; // for context extraction on error reporting + do { + if ( $this->checkIfTimeExceeded() ) { + $this->setTimeExceeded(); + } + $chunk = fread( $input, $this->bufferSize ); + if ( !xml_parse( $parser, $chunk, feof( $input ) ) ) { + wfDebug( "TextDumpPass::readDump encountered XML parsing error\n" ); + + $byte = xml_get_current_byte_index( $parser ); + $msg = wfMessage( 'xml-error-string', + 'XML import parse failure', + xml_get_current_line_number( $parser ), + xml_get_current_column_number( $parser ), + $byte . ( is_null( $chunk ) ? null : ( '; "' . substr( $chunk, $byte - $offset, 16 ) . '"' ) ), + xml_error_string( xml_get_error_code( $parser ) ) )->escaped(); + + xml_parser_free( $parser ); + + throw new MWException( $msg ); + } + $offset += strlen( $chunk ); + } while ( $chunk !== false && !feof( $input ) ); + if ( $this->maxTimeAllowed ) { + $filenameList = (array)$this->egress->getFilenames(); + // we wrote some stuff after last checkpoint that needs renamed + if ( file_exists( $filenameList[0] ) ) { + $newFilenames = []; + # we might have just written the header and footer and had no + # pages or revisions written... perhaps they were all deleted + # there's no pageID 0 so we use that. the caller is responsible + # for deciding what to do with a file containing only the + # siteinfo information and the mw tags. + if ( !$this->firstPageWritten ) { + $firstPageID = str_pad( 0, 9, "0", STR_PAD_LEFT ); + $lastPageID = str_pad( 0, 9, "0", STR_PAD_LEFT ); + } else { + $firstPageID = str_pad( $this->firstPageWritten, 9, "0", STR_PAD_LEFT ); + $lastPageID = str_pad( $this->lastPageWritten, 9, "0", STR_PAD_LEFT ); + } + + $filenameCount = count( $filenameList ); + for ( $i = 0; $i < $filenameCount; $i++ ) { + $checkpointNameFilledIn = sprintf( $this->checkpointFiles[$i], $firstPageID, $lastPageID ); + $fileinfo = pathinfo( $filenameList[$i] ); + $newFilenames[] = $fileinfo['dirname'] . '/' . $checkpointNameFilledIn; + } + $this->egress->closeAndRename( $newFilenames ); + } + } + xml_parser_free( $parser ); + + return true; + } + + /** + * Applies applicable export transformations to $text. + * + * @param string $text + * @param string $model + * @param string|null $format + * + * @return string + */ + private function exportTransform( $text, $model, $format = null ) { + try { + $handler = ContentHandler::getForModelID( $model ); + $text = $handler->exportTransform( $text, $format ); + } + catch ( MWException $ex ) { + $this->progress( + "Unable to apply export transformation for content model '$model': " . + $ex->getMessage() + ); + } + + return $text; + } + + /** + * Tries to get the revision text for a revision id. + * Export transformations are applied if the content model can is given or can be + * determined from the database. + * + * Upon errors, retries (Up to $this->maxFailures tries each call). + * If still no good revision get could be found even after this retrying, "" is returned. + * If no good revision text could be returned for + * $this->maxConsecutiveFailedTextRetrievals consecutive calls to getText, MWException + * is thrown. + * + * @param string $id The revision id to get the text for + * @param string|bool|null $model The content model used to determine + * applicable export transformations. + * If $model is null, it will be determined from the database. + * @param string|null $format The content format used when applying export transformations. + * + * @throws MWException + * @return string The revision text for $id, or "" + */ + function getText( $id, $model = null, $format = null ) { + global $wgContentHandlerUseDB; + + $prefetchNotTried = true; // Whether or not we already tried to get the text via prefetch. + $text = false; // The candidate for a good text. false if no proper value. + $failures = 0; // The number of times, this invocation of getText already failed. + + // The number of times getText failed without yielding a good text in between. + static $consecutiveFailedTextRetrievals = 0; + + $this->fetchCount++; + + // To allow to simply return on success and do not have to worry about book keeping, + // we assume, this fetch works (possible after some retries). Nevertheless, we koop + // the old value, so we can restore it, if problems occur (See after the while loop). + $oldConsecutiveFailedTextRetrievals = $consecutiveFailedTextRetrievals; + $consecutiveFailedTextRetrievals = 0; + + if ( $model === null && $wgContentHandlerUseDB ) { + $row = $this->db->selectRow( + 'revision', + [ 'rev_content_model', 'rev_content_format' ], + [ 'rev_id' => $this->thisRev ], + __METHOD__ + ); + + if ( $row ) { + $model = $row->rev_content_model; + $format = $row->rev_content_format; + } + } + + if ( $model === null || $model === '' ) { + $model = false; + } + + while ( $failures < $this->maxFailures ) { + // As soon as we found a good text for the $id, we will return immediately. + // Hence, if we make it past the try catch block, we know that we did not + // find a good text. + + try { + // Step 1: Get some text (or reuse from previous iteratuon if checking + // for plausibility failed) + + // Trying to get prefetch, if it has not been tried before + if ( $text === false && isset( $this->prefetch ) && $prefetchNotTried ) { + $prefetchNotTried = false; + $tryIsPrefetch = true; + $text = $this->prefetch->prefetch( (int)$this->thisPage, (int)$this->thisRev ); + + if ( $text === null ) { + $text = false; + } + + if ( is_string( $text ) && $model !== false ) { + // Apply export transformation to text coming from an old dump. + // The purpose of this transformation is to convert up from legacy + // formats, which may still be used in the older dump that is used + // for pre-fetching. Applying the transformation again should not + // interfere with content that is already in the correct form. + $text = $this->exportTransform( $text, $model, $format ); + } + } + + if ( $text === false ) { + // Fallback to asking the database + $tryIsPrefetch = false; + if ( $this->spawn ) { + $text = $this->getTextSpawned( $id ); + } else { + $text = $this->getTextDb( $id ); + } + + if ( $text !== false && $model !== false ) { + // Apply export transformation to text coming from the database. + // Prefetched text should already have transformations applied. + $text = $this->exportTransform( $text, $model, $format ); + } + + // No more checks for texts from DB for now. + // If we received something that is not false, + // We treat it as good text, regardless of whether it actually is or is not + if ( $text !== false ) { + return $text; + } + } + + if ( $text === false ) { + throw new MWException( "Generic error while obtaining text for id " . $id ); + } + + // We received a good candidate for the text of $id via some method + + // Step 2: Checking for plausibility and return the text if it is + // plausible + $revID = intval( $this->thisRev ); + if ( !isset( $this->db ) ) { + throw new MWException( "No database available" ); + } + + if ( $model !== CONTENT_MODEL_WIKITEXT ) { + $revLength = strlen( $text ); + } else { + $revLength = $this->db->selectField( 'revision', 'rev_len', [ 'rev_id' => $revID ] ); + } + + if ( strlen( $text ) == $revLength ) { + if ( $tryIsPrefetch ) { + $this->prefetchCount++; + } + + return $text; + } + + $text = false; + throw new MWException( "Received text is unplausible for id " . $id ); + } catch ( Exception $e ) { + $msg = "getting/checking text " . $id . " failed (" . $e->getMessage() . ")"; + if ( $failures + 1 < $this->maxFailures ) { + $msg .= " (Will retry " . ( $this->maxFailures - $failures - 1 ) . " more times)"; + } + $this->progress( $msg ); + } + + // Something went wrong; we did not a text that was plausible :( + $failures++; + + // A failure in a prefetch hit does not warrant resetting db connection etc. + if ( !$tryIsPrefetch ) { + // After backing off for some time, we try to reboot the whole process as + // much as possible to not carry over failures from one part to the other + // parts + sleep( $this->failureTimeout ); + try { + $this->rotateDb(); + if ( $this->spawn ) { + $this->closeSpawn(); + $this->openSpawn(); + } + } catch ( Exception $e ) { + $this->progress( "Rebooting getText infrastructure failed (" . $e->getMessage() . ")" . + " Trying to continue anyways" ); + } + } + } + + // Retirieving a good text for $id failed (at least) maxFailures times. + // We abort for this $id. + + // Restoring the consecutive failures, and maybe aborting, if the dump + // is too broken. + $consecutiveFailedTextRetrievals = $oldConsecutiveFailedTextRetrievals + 1; + if ( $consecutiveFailedTextRetrievals > $this->maxConsecutiveFailedTextRetrievals ) { + throw new MWException( "Graceful storage failure" ); + } + + return ""; + } + + /** + * May throw a database error if, say, the server dies during query. + * @param int $id + * @return bool|string + * @throws MWException + */ + private function getTextDb( $id ) { + global $wgContLang; + if ( !isset( $this->db ) ) { + throw new MWException( __METHOD__ . "No database available" ); + } + $row = $this->db->selectRow( 'text', + [ 'old_text', 'old_flags' ], + [ 'old_id' => $id ], + __METHOD__ ); + $text = Revision::getRevisionText( $row ); + if ( $text === false ) { + return false; + } + $stripped = str_replace( "\r", "", $text ); + $normalized = $wgContLang->normalize( $stripped ); + + return $normalized; + } + + private function getTextSpawned( $id ) { + Wikimedia\suppressWarnings(); + if ( !$this->spawnProc ) { + // First time? + $this->openSpawn(); + } + $text = $this->getTextSpawnedOnce( $id ); + Wikimedia\restoreWarnings(); + + return $text; + } + + function openSpawn() { + global $IP; + + if ( file_exists( "$IP/../multiversion/MWScript.php" ) ) { + $cmd = implode( " ", + array_map( 'wfEscapeShellArg', + [ + $this->php, + "$IP/../multiversion/MWScript.php", + "fetchText.php", + '--wiki', wfWikiID() ] ) ); + } else { + $cmd = implode( " ", + array_map( 'wfEscapeShellArg', + [ + $this->php, + "$IP/maintenance/fetchText.php", + '--wiki', wfWikiID() ] ) ); + } + $spec = [ + 0 => [ "pipe", "r" ], + 1 => [ "pipe", "w" ], + 2 => [ "file", "/dev/null", "a" ] ]; + $pipes = []; + + $this->progress( "Spawning database subprocess: $cmd" ); + $this->spawnProc = proc_open( $cmd, $spec, $pipes ); + if ( !$this->spawnProc ) { + $this->progress( "Subprocess spawn failed." ); + + return false; + } + list( + $this->spawnWrite, // -> stdin + $this->spawnRead, // <- stdout + ) = $pipes; + + return true; + } + + private function closeSpawn() { + Wikimedia\suppressWarnings(); + if ( $this->spawnRead ) { + fclose( $this->spawnRead ); + } + $this->spawnRead = false; + if ( $this->spawnWrite ) { + fclose( $this->spawnWrite ); + } + $this->spawnWrite = false; + if ( $this->spawnErr ) { + fclose( $this->spawnErr ); + } + $this->spawnErr = false; + if ( $this->spawnProc ) { + pclose( $this->spawnProc ); + } + $this->spawnProc = false; + Wikimedia\restoreWarnings(); + } + + private function getTextSpawnedOnce( $id ) { + global $wgContLang; + + $ok = fwrite( $this->spawnWrite, "$id\n" ); + // $this->progress( ">> $id" ); + if ( !$ok ) { + return false; + } + + $ok = fflush( $this->spawnWrite ); + // $this->progress( ">> [flush]" ); + if ( !$ok ) { + return false; + } + + // check that the text id they are sending is the one we asked for + // this avoids out of sync revision text errors we have encountered in the past + $newId = fgets( $this->spawnRead ); + if ( $newId === false ) { + return false; + } + if ( $id != intval( $newId ) ) { + return false; + } + + $len = fgets( $this->spawnRead ); + // $this->progress( "<< " . trim( $len ) ); + if ( $len === false ) { + return false; + } + + $nbytes = intval( $len ); + // actual error, not zero-length text + if ( $nbytes < 0 ) { + return false; + } + + $text = ""; + + // Subprocess may not send everything at once, we have to loop. + while ( $nbytes > strlen( $text ) ) { + $buffer = fread( $this->spawnRead, $nbytes - strlen( $text ) ); + if ( $buffer === false ) { + break; + } + $text .= $buffer; + } + + $gotbytes = strlen( $text ); + if ( $gotbytes != $nbytes ) { + $this->progress( "Expected $nbytes bytes from database subprocess, got $gotbytes " ); + + return false; + } + + // Do normalization in the dump thread... + $stripped = str_replace( "\r", "", $text ); + $normalized = $wgContLang->normalize( $stripped ); + + return $normalized; + } + + function startElement( $parser, $name, $attribs ) { + $this->checkpointJustWritten = false; + + $this->clearOpenElement( null ); + $this->lastName = $name; + + if ( $name == 'revision' ) { + $this->state = $name; + $this->egress->writeOpenPage( null, $this->buffer ); + $this->buffer = ""; + } elseif ( $name == 'page' ) { + $this->state = $name; + if ( $this->atStart ) { + $this->egress->writeOpenStream( $this->buffer ); + $this->buffer = ""; + $this->atStart = false; + } + } + + if ( $name == "text" && isset( $attribs['id'] ) ) { + $id = $attribs['id']; + $model = trim( $this->thisRevModel ); + $format = trim( $this->thisRevFormat ); + + $model = $model === '' ? null : $model; + $format = $format === '' ? null : $format; + + $text = $this->getText( $id, $model, $format ); + $this->openElement = [ $name, [ 'xml:space' => 'preserve' ] ]; + if ( strlen( $text ) > 0 ) { + $this->characterData( $parser, $text ); + } + } else { + $this->openElement = [ $name, $attribs ]; + } + } + + function endElement( $parser, $name ) { + $this->checkpointJustWritten = false; + + if ( $this->openElement ) { + $this->clearOpenElement( "" ); + } else { + $this->buffer .= ""; + } + + if ( $name == 'revision' ) { + $this->egress->writeRevision( null, $this->buffer ); + $this->buffer = ""; + $this->thisRev = ""; + $this->thisRevModel = null; + $this->thisRevFormat = null; + } elseif ( $name == 'page' ) { + if ( !$this->firstPageWritten ) { + $this->firstPageWritten = trim( $this->thisPage ); + } + $this->lastPageWritten = trim( $this->thisPage ); + if ( $this->timeExceeded ) { + $this->egress->writeClosePage( $this->buffer ); + // nasty hack, we can't just write the chardata after the + // page tag, it will include leading blanks from the next line + $this->egress->sink->write( "\n" ); + + $this->buffer = $this->xmlwriterobj->closeStream(); + $this->egress->writeCloseStream( $this->buffer ); + + $this->buffer = ""; + $this->thisPage = ""; + // this could be more than one file if we had more than one output arg + + $filenameList = (array)$this->egress->getFilenames(); + $newFilenames = []; + $firstPageID = str_pad( $this->firstPageWritten, 9, "0", STR_PAD_LEFT ); + $lastPageID = str_pad( $this->lastPageWritten, 9, "0", STR_PAD_LEFT ); + $filenamesCount = count( $filenameList ); + for ( $i = 0; $i < $filenamesCount; $i++ ) { + $checkpointNameFilledIn = sprintf( $this->checkpointFiles[$i], $firstPageID, $lastPageID ); + $fileinfo = pathinfo( $filenameList[$i] ); + $newFilenames[] = $fileinfo['dirname'] . '/' . $checkpointNameFilledIn; + } + $this->egress->closeRenameAndReopen( $newFilenames ); + $this->buffer = $this->xmlwriterobj->openStream(); + $this->timeExceeded = false; + $this->timeOfCheckpoint = $this->lastTime; + $this->firstPageWritten = false; + $this->checkpointJustWritten = true; + } else { + $this->egress->writeClosePage( $this->buffer ); + $this->buffer = ""; + $this->thisPage = ""; + } + } elseif ( $name == 'mediawiki' ) { + $this->egress->writeCloseStream( $this->buffer ); + $this->buffer = ""; + } + } + + function characterData( $parser, $data ) { + $this->clearOpenElement( null ); + if ( $this->lastName == "id" ) { + if ( $this->state == "revision" ) { + $this->thisRev .= $data; + } elseif ( $this->state == "page" ) { + $this->thisPage .= $data; + } + } elseif ( $this->lastName == "model" ) { + $this->thisRevModel .= $data; + } elseif ( $this->lastName == "format" ) { + $this->thisRevFormat .= $data; + } + + // have to skip the newline left over from closepagetag line of + // end of checkpoint files. nasty hack!! + if ( $this->checkpointJustWritten ) { + if ( $data[0] == "\n" ) { + $data = substr( $data, 1 ); + } + $this->checkpointJustWritten = false; + } + $this->buffer .= htmlspecialchars( $data ); + } + + function clearOpenElement( $style ) { + if ( $this->openElement ) { + $this->buffer .= Xml::element( $this->openElement[0], $this->openElement[1], $style ); + $this->openElement = false; + } + } +} + +$maintClass = TextPassDumper::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/dumpUploads.php b/www/wiki/maintenance/dumpUploads.php new file mode 100644 index 00000000..4bfc5746 --- /dev/null +++ b/www/wiki/maintenance/dumpUploads.php @@ -0,0 +1,128 @@ +addDescription( 'Generates list of uploaded files which can be fed to tar or similar. +By default, outputs relative paths against the parent directory of $wgUploadDirectory.' ); + $this->addOption( 'base', 'Set base relative path instead of wiki include root', false, true ); + $this->addOption( 'local', 'List all local files, used or not. No shared files included' ); + $this->addOption( 'used', 'Skip local images that are not used' ); + $this->addOption( 'shared', 'Include images used from shared repository' ); + } + + public function execute() { + global $IP; + $this->mAction = 'fetchLocal'; + $this->mBasePath = $this->getOption( 'base', $IP ); + $this->mShared = false; + $this->mSharedSupplement = false; + + if ( $this->hasOption( 'local' ) ) { + $this->mAction = 'fetchLocal'; + } + + if ( $this->hasOption( 'used' ) ) { + $this->mAction = 'fetchUsed'; + } + + if ( $this->hasOption( 'shared' ) ) { + if ( $this->hasOption( 'used' ) ) { + // Include shared-repo files in the used check + $this->mShared = true; + } else { + // Grab all local *plus* used shared + $this->mSharedSupplement = true; + } + } + $this->{$this->mAction} ( $this->mShared ); + if ( $this->mSharedSupplement ) { + $this->fetchUsed( true ); + } + } + + /** + * Fetch a list of used images from a particular image source. + * + * @param bool $shared True to pass shared-dir settings to hash func + */ + function fetchUsed( $shared ) { + $dbr = $this->getDB( DB_REPLICA ); + $image = $dbr->tableName( 'image' ); + $imagelinks = $dbr->tableName( 'imagelinks' ); + + $sql = "SELECT DISTINCT il_to, img_name + FROM $imagelinks + LEFT OUTER JOIN $image + ON il_to=img_name"; + $result = $dbr->query( $sql ); + + foreach ( $result as $row ) { + $this->outputItem( $row->il_to, $shared ); + } + } + + /** + * Fetch a list of all images from a particular image source. + * + * @param bool $shared True to pass shared-dir settings to hash func + */ + function fetchLocal( $shared ) { + $dbr = $this->getDB( DB_REPLICA ); + $result = $dbr->select( 'image', + [ 'img_name' ], + '', + __METHOD__ ); + + foreach ( $result as $row ) { + $this->outputItem( $row->img_name, $shared ); + } + } + + function outputItem( $name, $shared ) { + $file = wfFindFile( $name ); + if ( $file && $this->filterItem( $file, $shared ) ) { + $filename = $file->getLocalRefPath(); + $rel = wfRelativePath( $filename, $this->mBasePath ); + $this->output( "$rel\n" ); + } else { + wfDebug( __METHOD__ . ": base file? $name\n" ); + } + } + + function filterItem( $file, $shared ) { + return $shared || $file->isLocal(); + } +} + +$maintClass = UploadDumper::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/edit.php b/www/wiki/maintenance/edit.php new file mode 100644 index 00000000..60ed2523 --- /dev/null +++ b/www/wiki/maintenance/edit.php @@ -0,0 +1,107 @@ +addDescription( 'Edit an article from the command line, text is from stdin' ); + $this->addOption( 'user', 'Username', false, true, 'u' ); + $this->addOption( 'summary', 'Edit summary', false, true, 's' ); + $this->addOption( 'minor', 'Minor edit', false, false, 'm' ); + $this->addOption( 'bot', 'Bot edit', false, false, 'b' ); + $this->addOption( 'autosummary', 'Enable autosummary', false, false, 'a' ); + $this->addOption( 'no-rc', 'Do not show the change in recent changes', false, false, 'r' ); + $this->addOption( 'nocreate', 'Don\'t create new pages', false, false ); + $this->addOption( 'createonly', 'Only create new pages', false, false ); + $this->addArg( 'title', 'Title of article to edit' ); + } + + public function execute() { + global $wgUser; + + $userName = $this->getOption( 'user', false ); + $summary = $this->getOption( 'summary', '' ); + $minor = $this->hasOption( 'minor' ); + $bot = $this->hasOption( 'bot' ); + $autoSummary = $this->hasOption( 'autosummary' ); + $noRC = $this->hasOption( 'no-rc' ); + + if ( $userName === false ) { + $wgUser = User::newSystemUser( 'Maintenance script', [ 'steal' => true ] ); + } else { + $wgUser = User::newFromName( $userName ); + } + if ( !$wgUser ) { + $this->fatalError( "Invalid username" ); + } + if ( $wgUser->isAnon() ) { + $wgUser->addToDatabase(); + } + + $title = Title::newFromText( $this->getArg() ); + if ( !$title ) { + $this->fatalError( "Invalid title" ); + } + + if ( $this->hasOption( 'nocreate' ) && !$title->exists() ) { + $this->fatalError( "Page does not exist" ); + } elseif ( $this->hasOption( 'createonly' ) && $title->exists() ) { + $this->fatalError( "Page already exists" ); + } + + $page = WikiPage::factory( $title ); + + # Read the text + $text = $this->getStdin( Maintenance::STDIN_ALL ); + $content = ContentHandler::makeContent( $text, $title ); + + # Do the edit + $this->output( "Saving... " ); + $status = $page->doEditContent( $content, $summary, + ( $minor ? EDIT_MINOR : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ) | + ( $autoSummary ? EDIT_AUTOSUMMARY : 0 ) | + ( $noRC ? EDIT_SUPPRESS_RC : 0 ) ); + if ( $status->isOK() ) { + $this->output( "done\n" ); + $exit = 0; + } else { + $this->output( "failed\n" ); + $exit = 1; + } + if ( !$status->isGood() ) { + $this->output( $status->getWikiText( false, false, 'en' ) . "\n" ); + } + exit( $exit ); + } +} + +$maintClass = EditCLI::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/eraseArchivedFile.php b/www/wiki/maintenance/eraseArchivedFile.php new file mode 100644 index 00000000..ef6d3d8b --- /dev/null +++ b/www/wiki/maintenance/eraseArchivedFile.php @@ -0,0 +1,119 @@ +addDescription( 'Erases traces of deleted files.' ); + $this->addOption( 'delete', 'Perform the deletion' ); + $this->addOption( 'filename', 'File name', false, true ); + $this->addOption( 'filekey', 'File storage key (with extension) or "*"', true, true ); + } + + public function execute() { + if ( !$this->hasOption( 'delete' ) ) { + $this->output( "Use --delete to actually confirm this script\n" ); + } + + $filekey = $this->getOption( 'filekey' ); + $filename = $this->getOption( 'filename' ); + + if ( $filekey === '*' ) { // all versions by name + if ( !strlen( $filename ) ) { + $this->fatalError( "Missing --filename parameter." ); + } + $afile = false; + } else { // specified version + $dbw = $this->getDB( DB_MASTER ); + $fileQuery = ArchivedFile::getQueryInfo(); + $row = $dbw->selectRow( $fileQuery['tables'], $fileQuery['fields'], + [ 'fa_storage_group' => 'deleted', 'fa_storage_key' => $filekey ], + __METHOD__, [], $fileQuery['joins'] ); + if ( !$row ) { + $this->fatalError( "No deleted file exists with key '$filekey'." ); + } + $filename = $row->fa_name; + $afile = ArchivedFile::newFromRow( $row ); + } + + $file = wfLocalFile( $filename ); + if ( $file->exists() ) { + $this->fatalError( "File '$filename' is still a public file, use the delete form.\n" ); + } + + $this->output( "Purging all thumbnails for file '$filename'..." ); + $file->purgeCache(); + $this->output( "done.\n" ); + + if ( $afile instanceof ArchivedFile ) { + $this->scrubVersion( $afile ); + } else { + $this->output( "Finding deleted versions of file '$filename'...\n" ); + $this->scrubAllVersions( $filename ); + $this->output( "Done\n" ); + } + } + + protected function scrubAllVersions( $name ) { + $dbw = $this->getDB( DB_MASTER ); + $fileQuery = ArchivedFile::getQueryInfo(); + $res = $dbw->select( $fileQuery['tables'], $fileQuery['fields'], + [ 'fa_name' => $name, 'fa_storage_group' => 'deleted' ], + __METHOD__, [], $fileQuery['joins'] ); + foreach ( $res as $row ) { + $this->scrubVersion( ArchivedFile::newFromRow( $row ) ); + } + } + + protected function scrubVersion( ArchivedFile $archivedFile ) { + $key = $archivedFile->getStorageKey(); + $name = $archivedFile->getName(); + $ts = $archivedFile->getTimestamp(); + $repo = RepoGroup::singleton()->getLocalRepo(); + $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; + if ( $this->hasOption( 'delete' ) ) { + $status = $repo->getBackend()->delete( [ 'src' => $path ] ); + if ( $status->isOK() ) { + $this->output( "Deleted version '$key' ($ts) of file '$name'\n" ); + } else { + $this->output( "Failed to delete version '$key' ($ts) of file '$name'\n" ); + $this->output( print_r( $status->getErrorsArray(), true ) ); + } + } else { + $this->output( "Would delete version '{$key}' ({$ts}) of file '$name'\n" ); + } + } +} + +$maintClass = EraseArchivedFile::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/eval.php b/www/wiki/maintenance/eval.php new file mode 100644 index 00000000..40d29ef8 --- /dev/null +++ b/www/wiki/maintenance/eval.php @@ -0,0 +1,93 @@ + 0 ) { + LoggerFactory::registerProvider( new ConsoleSpi ); + // Some services hold Logger instances in object properties + MediaWikiServices::resetGlobalInstance(); + } + if ( $d > 1 ) { + wfGetDB( DB_MASTER )->setFlag( DBO_DEBUG ); + wfGetDB( DB_REPLICA )->setFlag( DBO_DEBUG ); + } +} + +$__useReadline = function_exists( 'readline_add_history' ) + && Maintenance::posix_isatty( 0 /*STDIN*/ ); + +if ( $__useReadline ) { + $__historyFile = isset( $_ENV['HOME'] ) ? + "{$_ENV['HOME']}/.mweval_history" : "$IP/maintenance/.mweval_history"; + readline_read_history( $__historyFile ); +} + +$__e = null; // PHP exception +while ( ( $__line = Maintenance::readconsole() ) !== false ) { + if ( $__e && !preg_match( '/^(exit|die);?$/', $__line ) ) { + // Internal state may be corrupted or fatals may occur later due + // to some object not being set. Don't drop out of eval in case + // lines were being pasted in (which would then get dumped to the shell). + // Instead, just absorb the remaning commands. Let "exit" through per DWIM. + echo "Exception was thrown before; please restart eval.php\n"; + continue; + } + if ( $__useReadline ) { + readline_add_history( $__line ); + readline_write_history( $__historyFile ); + } + try { + $__val = eval( $__line . ";" ); + } catch ( Exception $__e ) { + echo "Caught exception " . get_class( $__e ) . + ": {$__e->getMessage()}\n" . $__e->getTraceAsString() . "\n"; + continue; + } + if ( wfIsHHVM() || is_null( $__val ) ) { + echo "\n"; + } elseif ( is_string( $__val ) || is_numeric( $__val ) ) { + echo "$__val\n"; + } else { + var_dump( $__val ); + } +} + +print "\n"; diff --git a/www/wiki/maintenance/exportSites.php b/www/wiki/maintenance/exportSites.php new file mode 100644 index 00000000..736b12b3 --- /dev/null +++ b/www/wiki/maintenance/exportSites.php @@ -0,0 +1,56 @@ +addDescription( 'Exports site definitions the sites table to XML file' ); + + $this->addArg( 'file', 'A file to write the XML to (see docs/sitelist.txt). ' . + 'Use "php://stdout" to write to stdout.', true + ); + + parent::__construct(); + } + + /** + * Do the actual work. All child classes will need to implement this + */ + public function execute() { + $file = $this->getArg( 0 ); + + if ( $file === 'php://output' || $file === 'php://stdout' ) { + $this->mQuiet = true; + } + + $handle = fopen( $file, 'w' ); + + if ( !$handle ) { + $this->fatalError( "Failed to open $file for writing.\n" ); + } + + $exporter = new SiteExporter( $handle ); + + $siteLookup = \MediaWiki\MediaWikiServices::getInstance()->getSiteLookup(); + $exporter->exportSites( $siteLookup->getSites() ); + + fclose( $handle ); + + $this->output( "Exported sites to " . realpath( $file ) . ".\n" ); + } + +} + +$maintClass = ExportSites::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fetchText.php b/www/wiki/maintenance/fetchText.php new file mode 100644 index 00000000..bc4fa31f --- /dev/null +++ b/www/wiki/maintenance/fetchText.php @@ -0,0 +1,96 @@ +addDescription( "Fetch the raw revision blob from an old_id.\n" . + "NOTE: Export transformations are NOT applied. " . + "This is left to backupTextPass.php" + ); + } + + /** + * returns a string containing the following in order: + * textid + * \n + * length of text (-1 on error = failure to retrieve/unserialize/gunzip/etc) + * \n + * text (may be empty) + * + * note that the text string itself is *not* followed by newline + */ + public function execute() { + $db = $this->getDB( DB_REPLICA ); + $stdin = $this->getStdin(); + while ( !feof( $stdin ) ) { + $line = fgets( $stdin ); + if ( $line === false ) { + // We appear to have lost contact... + break; + } + $textId = intval( $line ); + $text = $this->doGetText( $db, $textId ); + if ( $text === false ) { + # actual error, not zero-length text + $textLen = "-1"; + } else { + $textLen = strlen( $text ); + } + $this->output( $textId . "\n" . $textLen . "\n" . $text ); + } + } + + /** + * May throw a database error if, say, the server dies during query. + * @param IDatabase $db + * @param int $id The old_id + * @return string + */ + private function doGetText( $db, $id ) { + $id = intval( $id ); + $row = $db->selectRow( 'text', + [ 'old_text', 'old_flags' ], + [ 'old_id' => $id ], + __METHOD__ ); + $text = Revision::getRevisionText( $row ); + if ( $text === false ) { + return false; + } + + return $text; + } +} + +$maintClass = FetchText::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fileOpPerfTest.php b/www/wiki/maintenance/fileOpPerfTest.php new file mode 100644 index 00000000..a60942fb --- /dev/null +++ b/www/wiki/maintenance/fileOpPerfTest.php @@ -0,0 +1,145 @@ +addDescription( 'Test fileop performance' ); + $this->addOption( 'b1', 'Backend 1', true, true ); + $this->addOption( 'b2', 'Backend 2', false, true ); + $this->addOption( 'srcdir', 'File source directory', true, true ); + $this->addOption( 'maxfiles', 'Max files', false, true ); + $this->addOption( 'quick', 'Avoid operation pre-checks (use doQuickOperations())' ); + $this->addOption( 'parallelize', '"parallelize" flag for doOperations()', false, true ); + } + + public function execute() { + $backend = FileBackendGroup::singleton()->get( $this->getOption( 'b1' ) ); + $this->doPerfTest( $backend ); + + if ( $this->getOption( 'b2' ) ) { + $backend = FileBackendGroup::singleton()->get( $this->getOption( 'b2' ) ); + $this->doPerfTest( $backend ); + } + } + + protected function doPerfTest( FileBackend $backend ) { + $ops1 = []; + $ops2 = []; + $ops3 = []; + $ops4 = []; + $ops5 = []; + + $baseDir = 'mwstore://' . $backend->getName() . '/testing-cont1'; + $backend->prepare( [ 'dir' => $baseDir ] ); + + $dirname = $this->getOption( 'srcdir' ); + $dir = opendir( $dirname ); + if ( !$dir ) { + return; + } + + while ( $dir && ( $file = readdir( $dir ) ) !== false ) { + if ( $file[0] != '.' ) { + $this->output( "Using '$dirname/$file' in operations.\n" ); + $dst = $baseDir . '/' . wfBaseName( $file ); + $ops1[] = [ 'op' => 'store', + 'src' => "$dirname/$file", 'dst' => $dst, 'overwrite' => 1 ]; + $ops2[] = [ 'op' => 'copy', + 'src' => "$dst", 'dst' => "$dst-1", 'overwrite' => 1 ]; + $ops3[] = [ 'op' => 'move', + 'src' => $dst, 'dst' => "$dst-2", 'overwrite' => 1 ]; + $ops4[] = [ 'op' => 'delete', 'src' => "$dst-1" ]; + $ops5[] = [ 'op' => 'delete', 'src' => "$dst-2" ]; + } + if ( count( $ops1 ) >= $this->getOption( 'maxfiles', 20 ) ) { + break; // enough + } + } + closedir( $dir ); + $this->output( "\n" ); + + $method = $this->hasOption( 'quick' ) ? 'doQuickOperations' : 'doOperations'; + + $opts = [ 'force' => 1 ]; + if ( $this->hasOption( 'parallelize' ) ) { + $opts['parallelize'] = ( $this->getOption( 'parallelize' ) === 'true' ); + } + + $start = microtime( true ); + $status = $backend->$method( $ops1, $opts ); + $e = ( microtime( true ) - $start ) * 1000; + if ( $status->getErrorsArray() ) { + print_r( $status->getErrorsArray() ); + exit( 0 ); + } + $this->output( $backend->getName() . ": Stored " . count( $ops1 ) . " files in $e ms.\n" ); + + $start = microtime( true ); + $backend->$method( $ops2, $opts ); + $e = ( microtime( true ) - $start ) * 1000; + if ( $status->getErrorsArray() ) { + print_r( $status->getErrorsArray() ); + exit( 0 ); + } + $this->output( $backend->getName() . ": Copied " . count( $ops2 ) . " files in $e ms.\n" ); + + $start = microtime( true ); + $backend->$method( $ops3, $opts ); + $e = ( microtime( true ) - $start ) * 1000; + if ( $status->getErrorsArray() ) { + print_r( $status->getErrorsArray() ); + exit( 0 ); + } + $this->output( $backend->getName() . ": Moved " . count( $ops3 ) . " files in $e ms.\n" ); + + $start = microtime( true ); + $backend->$method( $ops4, $opts ); + $e = ( microtime( true ) - $start ) * 1000; + if ( $status->getErrorsArray() ) { + print_r( $status->getErrorsArray() ); + exit( 0 ); + } + $this->output( $backend->getName() . ": Deleted " . count( $ops4 ) . " files in $e ms.\n" ); + + $start = microtime( true ); + $backend->$method( $ops5, $opts ); + $e = ( microtime( true ) - $start ) * 1000; + if ( $status->getErrorsArray() ) { + print_r( $status->getErrorsArray() ); + exit( 0 ); + } + $this->output( $backend->getName() . ": Deleted " . count( $ops5 ) . " files in $e ms.\n" ); + } +} + +$maintClass = TestFileOpPerformance::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/findDeprecated.php b/www/wiki/maintenance/findDeprecated.php new file mode 100644 index 00000000..ae2ee421 --- /dev/null +++ b/www/wiki/maintenance/findDeprecated.php @@ -0,0 +1,206 @@ +filename = $this->currentFile; + return $retVal; + } + + public function setCurrentFile( $filename ) { + $this->currentFile = $filename; + } + + public function getCurrentFile() { + return $this->currentFile; + } +} + +/** + * A PHPParser node visitor that finds deprecated functions and methods. + */ +class DeprecatedInterfaceFinder extends FileAwareNodeVisitor { + + private $currentClass = null; + + private $foundNodes = []; + + public function getFoundNodes() { + // Sort results by version, then by filename, then by name. + foreach ( $this->foundNodes as $version => &$nodes ) { + uasort( $nodes, function ( $a, $b ) { + return ( $a['filename'] . $a['name'] ) < ( $b['filename'] . $b['name'] ) ? -1 : 1; + } ); + } + ksort( $this->foundNodes ); + return $this->foundNodes; + } + + /** + * Check whether a function or method includes a call to wfDeprecated(), + * indicating that it is a hard-deprecated interface. + * @param PhpParser\Node $node + * @return bool + */ + public function isHardDeprecated( PhpParser\Node $node ) { + if ( !$node->stmts ) { + return false; + } + foreach ( $node->stmts as $stmt ) { + if ( + $stmt instanceof PhpParser\Node\Expr\FuncCall + && $stmt->name->toString() === 'wfDeprecated' + ) { + return true; + } + return false; + } + } + + public function enterNode( PhpParser\Node $node ) { + $retVal = parent::enterNode( $node ); + + if ( $node instanceof PhpParser\Node\Stmt\ClassLike ) { + $this->currentClass = $node->name; + } + + if ( $node instanceof PhpParser\Node\FunctionLike ) { + $docComment = $node->getDocComment(); + if ( !$docComment ) { + return; + } + if ( !preg_match( '/@deprecated.*(\d+\.\d+)/', $docComment->getText(), $matches ) ) { + return; + } + $version = $matches[1]; + + if ( $node instanceof PhpParser\Node\Stmt\ClassMethod ) { + $name = $this->currentClass . '::' . $node->name; + } else { + $name = $node->name; + } + + $this->foundNodes[ $version ][] = [ + 'filename' => $node->filename, + 'line' => $node->getLine(), + 'name' => $name, + 'hard' => $this->isHardDeprecated( $node ), + ]; + } + + return $retVal; + } +} + +/** + * Maintenance task that recursively scans MediaWiki PHP files for deprecated + * functions and interfaces and produces a report. + */ +class FindDeprecated extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addDescription( 'Find deprecated interfaces' ); + } + + /** + * @return SplFileInfo[] + */ + public function getFiles() { + global $IP; + + $files = new RecursiveDirectoryIterator( $IP . '/includes' ); + $files = new RecursiveIteratorIterator( $files ); + $files = new RegexIterator( $files, '/\.php$/' ); + return iterator_to_array( $files, false ); + } + + public function execute() { + global $IP; + + $files = $this->getFiles(); + $chunkSize = ceil( count( $files ) / 72 ); + + $parser = ( new PhpParser\ParserFactory )->create( PhpParser\ParserFactory::PREFER_PHP7 ); + $traverser = new PhpParser\NodeTraverser; + $finder = new DeprecatedInterfaceFinder; + $traverser->addVisitor( $finder ); + + $fileCount = count( $files ); + + for ( $i = 0; $i < $fileCount; $i++ ) { + $file = $files[$i]; + $code = file_get_contents( $file ); + + if ( strpos( $code, '@deprecated' ) === -1 ) { + continue; + } + + $finder->setCurrentFile( substr( $file->getPathname(), strlen( $IP ) + 1 ) ); + $nodes = $parser->parse( $code ); + $traverser->traverse( $nodes ); + + if ( $i % $chunkSize === 0 ) { + $percentDone = 100 * $i / $fileCount; + fprintf( STDERR, "\r[%-72s] %d%%", str_repeat( '#', $i / $chunkSize ), $percentDone ); + } + } + + fprintf( STDERR, "\r[%'#-72s] 100%%\n", '' ); + + // Colorize output if STDOUT is an interactive terminal. + if ( posix_isatty( STDOUT ) ) { + $versionFmt = "\n* Deprecated since \033[37;1m%s\033[0m:\n"; + $entryFmt = " %s \033[33;1m%s\033[0m (%s:%d)\n"; + } else { + $versionFmt = "\n* Deprecated since %s:\n"; + $entryFmt = " %s %s (%s:%d)\n"; + } + + foreach ( $finder->getFoundNodes() as $version => $nodes ) { + printf( $versionFmt, $version ); + foreach ( $nodes as $node ) { + printf( + $entryFmt, + $node['hard'] ? '+' : '-', + $node['name'], + $node['filename'], + $node['line'] + ); + } + } + printf( "\nlegend:\n -: soft-deprecated\n +: hard-deprecated (via wfDeprecated())\n" ); + } +} + +$maintClass = FindDeprecated::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/findHooks.php b/www/wiki/maintenance/findHooks.php new file mode 100644 index 00000000..ebb1f26c --- /dev/null +++ b/www/wiki/maintenance/findHooks.php @@ -0,0 +1,353 @@ + + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that compares documented and actually present mismatches. + * + * @ingroup Maintenance + */ +class FindHooks extends Maintenance { + const FIND_NON_RECURSIVE = 0; + const FIND_RECURSIVE = 1; + + /* + * Hooks that are ignored + */ + protected static $ignore = [ 'Test' ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Find hooks that are undocumented, missing, or just plain wrong' ); + $this->addOption( 'online', 'Check against MediaWiki.org hook documentation' ); + } + + public function getDbType() { + return Maintenance::DB_NONE; + } + + public function execute() { + global $IP; + + $documentedHooks = $this->getHooksFromDoc( $IP . '/docs/hooks.txt' ); + $potentialHooks = []; + $badHooks = []; + + $recurseDirs = [ + "$IP/includes/", + "$IP/mw-config/", + "$IP/languages/", + "$IP/maintenance/", + // Omit $IP/tests/phpunit as it contains hook tests that shouldn't be documented + "$IP/tests/parser", + "$IP/tests/phpunit/suites", + ]; + $nonRecurseDirs = [ + "$IP/", + ]; + $extraFiles = [ + "$IP/tests/phpunit/MediaWikiTestCase.php", + ]; + + foreach ( $recurseDirs as $dir ) { + $ret = $this->getHooksFromDir( $dir, self::FIND_RECURSIVE ); + $potentialHooks = array_merge( $potentialHooks, $ret['good'] ); + $badHooks = array_merge( $badHooks, $ret['bad'] ); + } + foreach ( $nonRecurseDirs as $dir ) { + $ret = $this->getHooksFromDir( $dir ); + $potentialHooks = array_merge( $potentialHooks, $ret['good'] ); + $badHooks = array_merge( $badHooks, $ret['bad'] ); + } + foreach ( $extraFiles as $file ) { + $potentialHooks = array_merge( $potentialHooks, $this->getHooksFromFile( $file ) ); + $badHooks = array_merge( $badHooks, $this->getBadHooksFromFile( $file ) ); + } + + $documented = array_keys( $documentedHooks ); + $potential = array_keys( $potentialHooks ); + $potential = array_unique( $potential ); + $badHooks = array_diff( array_unique( $badHooks ), self::$ignore ); + $todo = array_diff( $potential, $documented, self::$ignore ); + $deprecated = array_diff( $documented, $potential, self::$ignore ); + + // Check parameter count and references + $badParameterCount = $badParameterReference = []; + foreach ( $potentialHooks as $hook => $args ) { + if ( !isset( $documentedHooks[$hook] ) ) { + // Not documented, but that will also be in $todo + continue; + } + $argsDoc = $documentedHooks[$hook]; + if ( $args === 'unknown' || $argsDoc === 'unknown' ) { + // Could not get parameter information + continue; + } + if ( count( $argsDoc ) !== count( $args ) ) { + $badParameterCount[] = $hook . ': Doc: ' . count( $argsDoc ) . ' vs. Code: ' . count( $args ); + } else { + // Check if & is equal + foreach ( $argsDoc as $index => $argDoc ) { + $arg = $args[$index]; + if ( ( $arg[0] === '&' ) !== ( $argDoc[0] === '&' ) ) { + $badParameterReference[] = $hook . ': References different: Doc: ' . $argDoc . + ' vs. Code: ' . $arg; + } + } + } + } + + // Print the results + $this->printArray( 'Undocumented', $todo ); + $this->printArray( 'Documented and not found', $deprecated ); + $this->printArray( 'Unclear hook calls', $badHooks ); + $this->printArray( 'Different parameter count', $badParameterCount ); + $this->printArray( 'Different parameter reference', $badParameterReference ); + + if ( !$todo && !$deprecated && !$badHooks + && !$badParameterCount && !$badParameterReference + ) { + $this->output( "Looks good!\n" ); + } else { + $this->fatalError( 'The script finished with errors.' ); + } + } + + /** + * Get the hook documentation, either locally or from MediaWiki.org + * @param string $doc + * @return array Array: key => hook name; value => array of arguments or string 'unknown' + */ + private function getHooksFromDoc( $doc ) { + if ( $this->hasOption( 'online' ) ) { + return $this->getHooksFromOnlineDoc(); + } else { + return $this->getHooksFromLocalDoc( $doc ); + } + } + + /** + * Get hooks from a local file (for example docs/hooks.txt) + * @param string $doc Filename to look in + * @return array Array: key => hook name; value => array of arguments or string 'unknown' + */ + private function getHooksFromLocalDoc( $doc ) { + $m = []; + $content = file_get_contents( $doc ); + preg_match_all( + "/\n'(.*?)':.*((?:\n.+)*)/", + $content, + $m, + PREG_SET_ORDER + ); + + // Extract the documented parameter + $hooks = []; + foreach ( $m as $match ) { + $args = []; + if ( isset( $match[2] ) ) { + $n = []; + if ( preg_match_all( "/\n(&?\\$\w+):.+/", $match[2], $n ) ) { + $args = $n[1]; + } + } + $hooks[$match[1]] = $args; + } + return $hooks; + } + + /** + * Get hooks from www.mediawiki.org using the API + * @return array Array: key => hook name; value => string 'unknown' + */ + private function getHooksFromOnlineDoc() { + $allhooks = $this->getHooksFromOnlineDocCategory( 'MediaWiki_hooks' ); + $removed = $this->getHooksFromOnlineDocCategory( 'Removed_hooks' ); + return array_diff_key( $allhooks, $removed ); + } + + /** + * @param string $title + * @return array + */ + private function getHooksFromOnlineDocCategory( $title ) { + $params = [ + 'action' => 'query', + 'list' => 'categorymembers', + 'cmtitle' => "Category:$title", + 'cmlimit' => 500, + 'format' => 'json', + 'continue' => '', + ]; + + $retval = []; + while ( true ) { + $json = Http::get( + wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ), + [], + __METHOD__ + ); + $data = FormatJson::decode( $json, true ); + foreach ( $data['query']['categorymembers'] as $page ) { + if ( preg_match( '/Manual\:Hooks\/([a-zA-Z0-9- :]+)/', $page['title'], $m ) ) { + // parameters are unknown, because that needs parsing of wikitext + $retval[str_replace( ' ', '_', $m[1] )] = 'unknown'; + } + } + if ( !isset( $data['continue'] ) ) { + return $retval; + } + $params = array_replace( $params, $data['continue'] ); + } + } + + /** + * Get hooks from a PHP file + * @param string $filePath Full file path to the PHP file. + * @return array Array: key => hook name; value => array of arguments or string 'unknown' + */ + private function getHooksFromFile( $filePath ) { + $content = file_get_contents( $filePath ); + $m = []; + preg_match_all( + // All functions which runs hooks + '/(?:wfRunHooks|Hooks\:\:run|Hooks\:\:runWithoutAbort)\s*\(\s*' . + // First argument is the hook name as string + '([\'"])(.*?)\1' . + // Comma for second argument + '(?:\s*(,))?' . + // Second argument must start with array to be processed + '(?:\s*(?:array\s*\(|\[)' . + // Matching inside array - allows one deep of brackets + '((?:[^\(\)\[\]]|\((?-1)\)|\[(?-1)\])*)' . + // End + '[\)\]])?/', + $content, + $m, + PREG_SET_ORDER + ); + + // Extract parameter + $hooks = []; + foreach ( $m as $match ) { + $args = []; + if ( isset( $match[4] ) ) { + $n = []; + if ( preg_match_all( '/((?:[^,\(\)]|\([^\(\)]*\))+)/', $match[4], $n ) ) { + $args = array_map( 'trim', $n[1] ); + // remove empty entries from trailing spaces + $args = array_filter( $args ); + } + } elseif ( isset( $match[3] ) ) { + // Found a parameter for Hooks::run, + // but could not extract the hooks argument, + // because there are given by a variable + $args = 'unknown'; + } + $hooks[$match[2]] = $args; + } + + return $hooks; + } + + /** + * Get bad hooks (where the hook name could not be determined) from a PHP file + * @param string $filePath Full filename to the PHP file. + * @return array Array of bad wfRunHooks() lines + */ + private function getBadHooksFromFile( $filePath ) { + $content = file_get_contents( $filePath ); + $m = []; + // We want to skip the "function wfRunHooks()" one. :) + preg_match_all( '/(? hook name; value => array of arguments or string 'unknown' + */ + private function getHooksFromDir( $dir, $recurse = 0 ) { + $good = []; + $bad = []; + + if ( $recurse === self::FIND_RECURSIVE ) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + $iterator = new DirectoryIterator( $dir ); + } + + foreach ( $iterator as $info ) { + // Ignore directories, work only on php files, + if ( $info->isFile() && in_array( $info->getExtension(), [ 'php', 'inc' ] ) + // Skip this file as it contains text that looks like a bad wfRunHooks() call + && $info->getRealPath() !== __FILE__ + ) { + $good = array_merge( $good, $this->getHooksFromFile( $info->getRealPath() ) ); + $bad = array_merge( $bad, $this->getBadHooksFromFile( $info->getRealPath() ) ); + } + } + + return [ 'good' => $good, 'bad' => $bad ]; + } + + /** + * Nicely sort an print an array + * @param string $msg A message to show before the value + * @param array $arr + */ + private function printArray( $msg, $arr ) { + asort( $arr ); + + foreach ( $arr as $v ) { + $this->output( "$msg: $v\n" ); + } + } +} + +$maintClass = FindHooks::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/findMissingFiles.php b/www/wiki/maintenance/findMissingFiles.php new file mode 100644 index 00000000..4997cabb --- /dev/null +++ b/www/wiki/maintenance/findMissingFiles.php @@ -0,0 +1,119 @@ +addDescription( 'Find registered files with no corresponding file.' ); + $this->addOption( 'start', 'Start after this file name', false, true ); + $this->addOption( 'mtimeafter', 'Only include files changed since this time', false, true ); + $this->addOption( 'mtimebefore', 'Only includes files changed before this time', false, true ); + $this->setBatchSize( 300 ); + } + + function execute() { + $lastName = $this->getOption( 'start', '' ); + + $repo = RepoGroup::singleton()->getLocalRepo(); + $dbr = $repo->getReplicaDB(); + $be = $repo->getBackend(); + $batchSize = $this->getBatchSize(); + + $mtime1 = $dbr->timestampOrNull( $this->getOption( 'mtimeafter', null ) ); + $mtime2 = $dbr->timestampOrNull( $this->getOption( 'mtimebefore', null ) ); + + $joinTables = []; + $joinConds = []; + if ( $mtime1 || $mtime2 ) { + $joinTables[] = 'page'; + $joinConds['page'] = [ 'INNER JOIN', + [ 'page_title = img_name', 'page_namespace' => NS_FILE ] ]; + $joinTables[] = 'logging'; + $on = [ 'log_page = page_id', 'log_type' => [ 'upload', 'move', 'delete' ] ]; + if ( $mtime1 ) { + $on[] = "log_timestamp > {$dbr->addQuotes($mtime1)}"; + } + if ( $mtime2 ) { + $on[] = "log_timestamp < {$dbr->addQuotes($mtime2)}"; + } + $joinConds['logging'] = [ 'INNER JOIN', $on ]; + } + + do { + $res = $dbr->select( + array_merge( [ 'image' ], $joinTables ), + [ 'name' => 'img_name' ], + [ "img_name > " . $dbr->addQuotes( $lastName ) ], + __METHOD__, + // DISTINCT causes a pointless filesort + [ 'ORDER BY' => 'name', 'GROUP BY' => 'name', + 'LIMIT' => $batchSize ], + $joinConds + ); + + // Check if any of these files are missing... + $pathsByName = []; + foreach ( $res as $row ) { + $file = $repo->newFile( $row->name ); + $pathsByName[$row->name] = $file->getPath(); + $lastName = $row->name; + } + $be->preloadFileStat( [ 'srcs' => $pathsByName ] ); + foreach ( $pathsByName as $path ) { + if ( $be->fileExists( [ 'src' => $path ] ) === false ) { + $this->output( "$path\n" ); + } + } + + // Find all missing old versions of any of the files in this batch... + if ( count( $pathsByName ) ) { + $ores = $dbr->select( 'oldimage', + [ 'oi_name', 'oi_archive_name' ], + [ 'oi_name' => array_keys( $pathsByName ) ], + __METHOD__ + ); + + $checkPaths = []; + foreach ( $ores as $row ) { + if ( !strlen( $row->oi_archive_name ) ) { + continue; // broken row + } + $file = $repo->newFromArchiveName( $row->oi_name, $row->oi_archive_name ); + $checkPaths[] = $file->getPath(); + } + + foreach ( array_chunk( $checkPaths, $batchSize ) as $paths ) { + $be->preloadFileStat( [ 'srcs' => $paths ] ); + foreach ( $paths as $path ) { + if ( $be->fileExists( [ 'src' => $path ] ) === false ) { + $this->output( "$path\n" ); + } + } + } + } + } while ( $res->numRows() >= $batchSize ); + } +} + +$maintClass = FindMissingFiles::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/findOrphanedFiles.php b/www/wiki/maintenance/findOrphanedFiles.php new file mode 100644 index 00000000..57e04e0e --- /dev/null +++ b/www/wiki/maintenance/findOrphanedFiles.php @@ -0,0 +1,155 @@ +addDescription( "Find unregistered files in the 'public' repo zone." ); + $this->addOption( 'subdir', + 'Only scan files in this subdirectory (e.g. "a/a0")', false, true ); + $this->addOption( 'verbose', "Mention file paths checked" ); + $this->setBatchSize( 500 ); + } + + function execute() { + $subdir = $this->getOption( 'subdir', '' ); + $verbose = $this->hasOption( 'verbose' ); + + $repo = RepoGroup::singleton()->getLocalRepo(); + if ( $repo->hasSha1Storage() ) { + $this->fatalError( "Local repo uses SHA-1 file storage names; aborting." ); + } + + $directory = $repo->getZonePath( 'public' ); + if ( $subdir != '' ) { + $directory .= "/$subdir/"; + } + + if ( $verbose ) { + $this->output( "Scanning files under $directory:\n" ); + } + + $list = $repo->getBackend()->getFileList( [ 'dir' => $directory ] ); + if ( $list === null ) { + $this->fatalError( "Could not get file listing." ); + } + + $pathBatch = []; + foreach ( $list as $path ) { + if ( preg_match( '#^(thumb|deleted)/#', $path ) ) { + continue; // handle ugly nested containers on stock installs + } + + $pathBatch[] = $path; + if ( count( $pathBatch ) >= $this->getBatchSize() ) { + $this->checkFiles( $repo, $pathBatch, $verbose ); + $pathBatch = []; + } + } + $this->checkFiles( $repo, $pathBatch, $verbose ); + } + + protected function checkFiles( LocalRepo $repo, array $paths, $verbose ) { + if ( !count( $paths ) ) { + return; + } + + $dbr = $repo->getReplicaDB(); + + $curNames = []; + $oldNames = []; + $imgIN = []; + $oiWheres = []; + foreach ( $paths as $path ) { + $name = basename( $path ); + if ( preg_match( '#^archive/#', $path ) ) { + if ( $verbose ) { + $this->output( "Checking old file $name\n" ); + } + + $oldNames[] = $name; + list( , $base ) = explode( '!', $name, 2 ); // ! + $oiWheres[] = $dbr->makeList( + [ 'oi_name' => $base, 'oi_archive_name' => $name ], + LIST_AND + ); + } else { + if ( $verbose ) { + $this->output( "Checking current file $name\n" ); + } + + $curNames[] = $name; + $imgIN[] = $name; + } + } + + $res = $dbr->query( + $dbr->unionQueries( + [ + $dbr->selectSQLText( + 'image', + [ 'name' => 'img_name', 'old' => 0 ], + $imgIN ? [ 'img_name' => $imgIN ] : '1=0' + ), + $dbr->selectSQLText( + 'oldimage', + [ 'name' => 'oi_archive_name', 'old' => 1 ], + $oiWheres ? $dbr->makeList( $oiWheres, LIST_OR ) : '1=0' + ) + ], + true // UNION ALL (performance) + ), + __METHOD__ + ); + + $curNamesFound = []; + $oldNamesFound = []; + foreach ( $res as $row ) { + if ( $row->old ) { + $oldNamesFound[] = $row->name; + } else { + $curNamesFound[] = $row->name; + } + } + + foreach ( array_diff( $curNames, $curNamesFound ) as $name ) { + $file = $repo->newFile( $name ); + // Print name and public URL to ease recovery + if ( $file ) { + $this->output( $name . "\n" . $file->getCanonicalUrl() . "\n\n" ); + } else { + $this->error( "Cannot get URL for bad file title '$name'" ); + } + } + + foreach ( array_diff( $oldNames, $oldNamesFound ) as $name ) { + list( , $base ) = explode( '!', $name, 2 ); // ! + $file = $repo->newFromArchiveName( Title::makeTitle( NS_FILE, $base ), $name ); + // Print name and public URL to ease recovery + $this->output( $name . "\n" . $file->getCanonicalUrl() . "\n\n" ); + } + } +} + +$maintClass = FindOrphanedFiles::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fixDefaultJsonContentPages.php b/www/wiki/maintenance/fixDefaultJsonContentPages.php new file mode 100644 index 00000000..cb4eddf5 --- /dev/null +++ b/www/wiki/maintenance/fixDefaultJsonContentPages.php @@ -0,0 +1,128 @@ +addDescription( + 'Fix instances of JSON pages prior to them being the ContentHandler default' ); + $this->setBatchSize( 100 ); + } + + protected function getUpdateKey() { + return __CLASS__; + } + + protected function doDBUpdates() { + if ( !$this->getConfig()->get( 'ContentHandlerUseDB' ) ) { + $this->output( "\$wgContentHandlerUseDB is not enabled, nothing to do.\n" ); + return true; + } + + $dbr = $this->getDB( DB_REPLICA ); + $namespaces = [ + NS_MEDIAWIKI => $dbr->buildLike( $dbr->anyString(), '.json' ), + NS_USER => $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString(), '.json' ), + ]; + foreach ( $namespaces as $ns => $like ) { + $lastPage = 0; + do { + $rows = $dbr->select( + 'page', + [ 'page_id', 'page_title', 'page_namespace', 'page_content_model' ], + [ + 'page_namespace' => $ns, + 'page_title ' . $like, + 'page_id > ' . $dbr->addQuotes( $lastPage ) + ], + __METHOD__, + [ 'ORDER BY' => 'page_id', 'LIMIT' => $this->getBatchSize() ] + ); + foreach ( $rows as $row ) { + $this->handleRow( $row ); + } + } while ( $rows->numRows() >= $this->getBatchSize() ); + } + + return true; + } + + protected function handleRow( stdClass $row ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->output( "Processing {$title} ({$row->page_id})...\n" ); + $rev = Revision::newFromTitle( $title ); + $content = $rev->getContent( Revision::RAW ); + $dbw = $this->getDB( DB_MASTER ); + if ( $content instanceof JsonContent ) { + if ( $content->isValid() ) { + // Yay, actually JSON. We need to just change the + // page_content_model because revision will automatically + // use the default, which is *now* JSON. + $this->output( "Setting page_content_model to json..." ); + $dbw->update( + 'page', + [ 'page_content_model' => CONTENT_MODEL_JSON ], + [ 'page_id' => $row->page_id ], + __METHOD__ + ); + $this->output( "done.\n" ); + wfWaitForSlaves(); + } else { + // Not JSON...force it to wikitext. We need to update the + // revision table so that these revisions are always processed + // as wikitext in the future. page_content_model is already + // set to "wikitext". + $this->output( "Setting rev_content_model to wikitext..." ); + // Grab all the ids for batching + $ids = $dbw->selectFieldValues( + 'revision', + 'rev_id', + [ 'rev_page' => $row->page_id ], + __METHOD__ + ); + foreach ( array_chunk( $ids, 50 ) as $chunk ) { + $dbw->update( + 'revision', + [ 'rev_content_model' => CONTENT_MODEL_WIKITEXT ], + [ 'rev_page' => $row->page_id, 'rev_id' => $chunk ] + ); + wfWaitForSlaves(); + } + $this->output( "done.\n" ); + } + } else { + $this->output( "not a JSON page? Skipping\n" ); + } + } +} + +$maintClass = FixDefaultJsonContentPages::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fixDoubleRedirects.php b/www/wiki/maintenance/fixDoubleRedirects.php new file mode 100644 index 00000000..a76617aa --- /dev/null +++ b/www/wiki/maintenance/fixDoubleRedirects.php @@ -0,0 +1,140 @@ + + * 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 + * @author Ilmari Karonen + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that fixes double redirects. + * + * @ingroup Maintenance + */ +class FixDoubleRedirects extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addDescription( 'Script to fix double redirects' ); + $this->addOption( 'async', 'Don\'t fix anything directly, just queue the jobs' ); + $this->addOption( 'title', 'Fix only redirects pointing to this page', false, true ); + $this->addOption( 'dry-run', 'Perform a dry run, fix nothing' ); + } + + public function execute() { + $async = $this->hasOption( 'async' ); + $dryrun = $this->hasOption( 'dry-run' ); + + if ( $this->hasOption( 'title' ) ) { + $title = Title::newFromText( $this->getOption( 'title' ) ); + if ( !$title || !$title->isRedirect() ) { + $this->fatalError( $title->getPrefixedText() . " is not a redirect!\n" ); + } + } else { + $title = null; + } + + $dbr = $this->getDB( DB_REPLICA ); + + // See also SpecialDoubleRedirects + $tables = [ + 'redirect', + 'pa' => 'page', + 'pb' => 'page', + ]; + $fields = [ + 'pa.page_namespace AS pa_namespace', + 'pa.page_title AS pa_title', + 'pb.page_namespace AS pb_namespace', + 'pb.page_title AS pb_title', + ]; + $conds = [ + 'rd_from = pa.page_id', + 'rd_namespace = pb.page_namespace', + 'rd_title = pb.page_title', + 'rd_interwiki IS NULL OR rd_interwiki = ' . $dbr->addQuotes( '' ), // T42352 + 'pb.page_is_redirect' => 1, + ]; + + if ( $title != null ) { + $conds['pb.page_namespace'] = $title->getNamespace(); + $conds['pb.page_title'] = $title->getDBkey(); + } + // TODO: support batch querying + + $res = $dbr->select( $tables, $fields, $conds, __METHOD__ ); + + if ( !$res->numRows() ) { + $this->output( "No double redirects found.\n" ); + + return; + } + + $jobs = []; + $processedTitles = "\n"; + $n = 0; + foreach ( $res as $row ) { + $titleA = Title::makeTitle( $row->pa_namespace, $row->pa_title ); + $titleB = Title::makeTitle( $row->pb_namespace, $row->pb_title ); + + $processedTitles .= "* [[$titleA]]\n"; + + $job = new DoubleRedirectJob( $titleA, [ + 'reason' => 'maintenance', + 'redirTitle' => $titleB->getPrefixedDBkey() + ] ); + + if ( !$async ) { + $success = ( $dryrun ? true : $job->run() ); + if ( !$success ) { + $this->error( "Error fixing " . $titleA->getPrefixedText() + . ": " . $job->getLastError() . "\n" ); + } + } else { + $jobs[] = $job; + // @todo FIXME: Hardcoded constant 10000 copied from DoubleRedirectJob class + if ( count( $jobs ) > 10000 ) { + $this->queueJobs( $jobs, $dryrun ); + $jobs = []; + } + } + + if ( ++$n % 100 == 0 ) { + $this->output( "$n...\n" ); + } + } + + if ( count( $jobs ) ) { + $this->queueJobs( $jobs, $dryrun ); + } + $this->output( "$n double redirects processed" . $processedTitles . "\n" ); + } + + protected function queueJobs( $jobs, $dryrun = false ) { + $this->output( "Queuing batch of " . count( $jobs ) . " double redirects.\n" ); + JobQueueGroup::singleton()->push( $dryrun ? [] : $jobs ); + } +} + +$maintClass = FixDoubleRedirects::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fixExtLinksProtocolRelative.php b/www/wiki/maintenance/fixExtLinksProtocolRelative.php new file mode 100644 index 00000000..c70b8be7 --- /dev/null +++ b/www/wiki/maintenance/fixExtLinksProtocolRelative.php @@ -0,0 +1,99 @@ +addDescription( + 'Fixes any entries in the externallinks table containing protocol-relative URLs' ); + } + + protected function getUpdateKey() { + return 'fix protocol-relative URLs in externallinks'; + } + + protected function updateSkippedMessage() { + return 'protocol-relative URLs in externallinks table already fixed.'; + } + + protected function doDBUpdates() { + $db = $this->getDB( DB_MASTER ); + if ( !$db->tableExists( 'externallinks' ) ) { + $this->error( "externallinks table does not exist" ); + + return false; + } + $this->output( "Fixing protocol-relative entries in the externallinks table...\n" ); + $res = $db->select( 'externallinks', [ 'el_from', 'el_to', 'el_index' ], + [ 'el_index' . $db->buildLike( '//', $db->anyString() ) ], + __METHOD__ + ); + $count = 0; + foreach ( $res as $row ) { + $count++; + if ( $count % 100 == 0 ) { + $this->output( $count . "\n" ); + wfWaitForSlaves(); + } + $db->insert( 'externallinks', + [ + [ + 'el_from' => $row->el_from, + 'el_to' => $row->el_to, + 'el_index' => "http:{$row->el_index}", + ], + [ + 'el_from' => $row->el_from, + 'el_to' => $row->el_to, + 'el_index' => "https:{$row->el_index}", + ] + ], __METHOD__, [ 'IGNORE' ] + ); + $db->delete( + 'externallinks', + [ + 'el_index' => $row->el_index, + 'el_from' => $row->el_from, + 'el_to' => $row->el_to + ], + __METHOD__ + ); + } + $this->output( "Done, $count rows updated.\n" ); + + return true; + } +} + +$maintClass = FixExtLinksProtocolRelative::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fixTimestamps.php b/www/wiki/maintenance/fixTimestamps.php new file mode 100644 index 00000000..32ff985f --- /dev/null +++ b/www/wiki/maintenance/fixTimestamps.php @@ -0,0 +1,129 @@ +addDescription( '' ); + $this->addArg( 'offset', '' ); + $this->addArg( 'start', 'Starting timestamp' ); + $this->addArg( 'end', 'Ending timestamp' ); + } + + public function execute() { + $offset = $this->getArg( 0 ) * 3600; + $start = $this->getArg( 1 ); + $end = $this->getArg( 2 ); + $grace = 60; // maximum normal clock offset + + # Find bounding revision IDs + $dbw = $this->getDB( DB_MASTER ); + $revisionTable = $dbw->tableName( 'revision' ); + $res = $dbw->query( "SELECT MIN(rev_id) as minrev, MAX(rev_id) as maxrev FROM $revisionTable " . + "WHERE rev_timestamp BETWEEN '{$start}' AND '{$end}'", __METHOD__ ); + $row = $dbw->fetchObject( $res ); + + if ( is_null( $row->minrev ) ) { + $this->fatalError( "No revisions in search period." ); + } + + $minRev = $row->minrev; + $maxRev = $row->maxrev; + + # Select all timestamps and IDs + $sql = "SELECT rev_id, rev_timestamp FROM $revisionTable " . + "WHERE rev_id BETWEEN $minRev AND $maxRev"; + if ( $offset > 0 ) { + $sql .= " ORDER BY rev_id DESC"; + $expectedSign = -1; + } else { + $expectedSign = 1; + } + + $res = $dbw->query( $sql, __METHOD__ ); + + $lastNormal = 0; + $badRevs = []; + $numGoodRevs = 0; + + foreach ( $res as $row ) { + $timestamp = wfTimestamp( TS_UNIX, $row->rev_timestamp ); + $delta = $timestamp - $lastNormal; + $sign = $delta == 0 ? 0 : $delta / abs( $delta ); + if ( $sign == 0 || $sign == $expectedSign ) { + // Monotonic change + $lastNormal = $timestamp; + ++$numGoodRevs; + continue; + } elseif ( abs( $delta ) <= $grace ) { + // Non-monotonic change within grace interval + ++$numGoodRevs; + continue; + } else { + // Non-monotonic change larger than grace interval + $badRevs[] = $row->rev_id; + } + } + + $numBadRevs = count( $badRevs ); + if ( $numBadRevs > $numGoodRevs ) { + $this->fatalError( + "The majority of revisions in the search interval are marked as bad. + + Are you sure the offset ($offset) has the right sign? Positive means the clock + was incorrectly set forward, negative means the clock was incorrectly set back. + + If the offset is right, then increase the search interval until there are enough + good revisions to provide a majority reference." ); + } elseif ( $numBadRevs == 0 ) { + $this->output( "No bad revisions found.\n" ); + exit( 0 ); + } + + $this->output( sprintf( "Fixing %d revisions (%.2f%% of revisions in search interval)\n", + $numBadRevs, $numBadRevs / ( $numGoodRevs + $numBadRevs ) * 100 ) ); + + $fixup = -$offset; + $sql = "UPDATE $revisionTable " . + "SET rev_timestamp=" + . "DATE_FORMAT(DATE_ADD(rev_timestamp, INTERVAL $fixup SECOND), '%Y%m%d%H%i%s') " . + "WHERE rev_id IN (" . $dbw->makeList( $badRevs ) . ')'; + $dbw->query( $sql, __METHOD__ ); + $this->output( "Done\n" ); + } +} + +$maintClass = FixTimestamps::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/fixUserRegistration.php b/www/wiki/maintenance/fixUserRegistration.php new file mode 100644 index 00000000..e1b16829 --- /dev/null +++ b/www/wiki/maintenance/fixUserRegistration.php @@ -0,0 +1,95 @@ +addDescription( 'Fix the user_registration field' ); + $this->setBatchSize( 1000 ); + } + + public function execute() { + $dbw = $this->getDB( DB_MASTER ); + + $lastId = 0; + do { + // Get user IDs which need fixing + $res = $dbw->select( + 'user', + 'user_id', + [ + 'user_id > ' . $dbw->addQuotes( $lastId ), + 'user_registration IS NULL' + ], + __METHOD__, + [ + 'LIMIT' => $this->getBatchSize(), + 'ORDER BY' => 'user_id', + ] + ); + foreach ( $res as $row ) { + $id = $row->user_id; + $lastId = $id; + // Get first edit time + $actorQuery = ActorMigration::newMigration() + ->getWhere( $dbw, 'rev_user', User::newFromId( $id ) ); + $timestamp = $dbw->selectField( + [ 'revision' ] + $actorQuery['tables'], + 'MIN(rev_timestamp)', + $actorQuery['conds'], + __METHOD__, + [], + $actorQuery['joins'] + ); + // Update + if ( $timestamp !== null ) { + $dbw->update( + 'user', + [ 'user_registration' => $timestamp ], + [ 'user_id' => $id ], + __METHOD__ + ); + $user = User::newFromId( $id ); + $user->invalidateCache(); + $this->output( "Set registration for #$id to $timestamp\n" ); + } else { + $this->output( "Could not find registration for #$id NULL\n" ); + } + } + $this->output( "Waiting for replica DBs..." ); + wfWaitForSlaves(); + $this->output( " done.\n" ); + } while ( $res->numRows() >= $this->getBatchSize() ); + } +} + +$maintClass = FixUserRegistration::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/formatInstallDoc.php b/www/wiki/maintenance/formatInstallDoc.php new file mode 100644 index 00000000..dbaeb867 --- /dev/null +++ b/www/wiki/maintenance/formatInstallDoc.php @@ -0,0 +1,76 @@ +addArg( 'path', 'The file name to format', false ); + $this->addOption( 'outfile', 'The output file name', false, true ); + $this->addOption( 'html', 'Use HTML output format. By default, wikitext is used.' ); + } + + function execute() { + if ( $this->hasArg( 0 ) ) { + $fileName = $this->getArg( 0 ); + $inFile = fopen( $fileName, 'r' ); + if ( !$inFile ) { + $this->fatalError( "Unable to open input file \"$fileName\"" ); + } + } else { + $inFile = STDIN; + } + + if ( $this->hasOption( 'outfile' ) ) { + $fileName = $this->getOption( 'outfile' ); + $outFile = fopen( $fileName, 'w' ); + if ( !$outFile ) { + $this->fatalError( "Unable to open output file \"$fileName\"" ); + } + } else { + $outFile = STDOUT; + } + + $inText = stream_get_contents( $inFile ); + $outText = InstallDocFormatter::format( $inText ); + + if ( $this->hasOption( 'html' ) ) { + global $wgParser; + $opt = new ParserOptions; + $title = Title::newFromText( 'Text file' ); + $out = $wgParser->parse( $outText, $title, $opt ); + $outText = "\n" . $out->getText() . "\n\n"; + } + + fwrite( $outFile, $outText ); + } +} + +$maintClass = MaintenanceFormatInstallDoc::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/generateJsonI18n.php b/www/wiki/maintenance/generateJsonI18n.php new file mode 100644 index 00000000..efddfb34 --- /dev/null +++ b/www/wiki/maintenance/generateJsonI18n.php @@ -0,0 +1,196 @@ +addDescription( 'Build JSON messages files from a PHP messages file' ); + + $this->addArg( 'phpfile', 'PHP file defining a $messages array', false ); + $this->addArg( 'jsondir', 'Directory to write JSON files to', false ); + $this->addOption( 'extension', 'Perform default conversion on an extension', + false, true ); + $this->addOption( 'supplementary', 'Find supplementary i18n files in subdirs and convert those', + false, false ); + } + + public function execute() { + global $IP; + + $phpfile = $this->getArg( 0 ); + $jsondir = $this->getArg( 1 ); + $extension = $this->getOption( 'extension' ); + $convertSupplementaryI18nFiles = $this->hasOption( 'supplementary' ); + + if ( $extension ) { + if ( $phpfile ) { + $this->fatalError( "The phpfile is already specified, conflicts with --extension." ); + } + $phpfile = "$IP/extensions/$extension/$extension.i18n.php"; + } + + if ( !$phpfile ) { + $this->error( "I'm here for an argument!" ); + $this->maybeHelp( true ); + // dies. + } + + if ( $convertSupplementaryI18nFiles ) { + if ( is_readable( $phpfile ) ) { + $this->transformI18nFile( $phpfile, $jsondir ); + } else { + // This is non-fatal because we might want to continue searching for + // i18n files in subdirs even if the extension does not include a + // primary i18n.php. + $this->error( "Warning: no primary i18n file was found." ); + } + $this->output( "Searching for supplementary i18n files...\n" ); + $dir_iterator = new RecursiveDirectoryIterator( dirname( $phpfile ) ); + $iterator = new RecursiveIteratorIterator( + $dir_iterator, RecursiveIteratorIterator::LEAVES_ONLY ); + foreach ( $iterator as $path => $fileObject ) { + if ( fnmatch( "*.i18n.php", $fileObject->getFilename() ) ) { + $this->output( "Converting $path.\n" ); + $this->transformI18nFile( $path ); + } + } + } else { + // Just convert the primary i18n file. + $this->transformI18nFile( $phpfile, $jsondir ); + } + } + + public function transformI18nFile( $phpfile, $jsondir = null ) { + if ( !$jsondir ) { + // Assume the json directory should be in the same directory as the + // .i18n.php file. + $jsondir = dirname( $phpfile ) . "/i18n"; + } + if ( !is_dir( $jsondir ) ) { + $this->output( "Creating directory $jsondir.\n" ); + $success = mkdir( $jsondir ); + if ( !$success ) { + $this->fatalError( "Could not create directory $jsondir" ); + } + } + + if ( !is_readable( $phpfile ) ) { + $this->fatalError( "Error reading $phpfile" ); + } + $messages = null; + include $phpfile; + $phpfileContents = file_get_contents( $phpfile ); + + if ( !isset( $messages ) ) { + $this->fatalError( "PHP file $phpfile does not define \$messages array" ); + } + + if ( !$messages ) { + $this->fatalError( "PHP file $phpfile contains an empty \$messages array. " . + "Maybe it was already converted?" ); + } + + if ( !isset( $messages['en'] ) || !is_array( $messages['en'] ) ) { + $this->fatalError( "PHP file $phpfile does not set language codes" ); + } + + foreach ( $messages as $langcode => $langmsgs ) { + $authors = $this->getAuthorsFromComment( $this->findCommentBefore( + "\$messages['$langcode'] =", + $phpfileContents + ) ); + // Make sure the @metadata key is the first key in the output + $langmsgs = array_merge( + [ '@metadata' => [ 'authors' => $authors ] ], + $langmsgs + ); + + $jsonfile = "$jsondir/$langcode.json"; + $success = file_put_contents( + $jsonfile, + FormatJson::encode( $langmsgs, "\t", FormatJson::ALL_OK ) . "\n" + ); + if ( $success === false ) { + $this->fatalError( "FAILED to write $jsonfile" ); + } + $this->output( "$jsonfile\n" ); + } + + $this->output( + "All done. To complete the conversion, please do the following:\n" . + "* Add \$wgMessagesDirs['YourExtension'] = __DIR__ . '/i18n';\n" . + "* Remove \$wgExtensionMessagesFiles['YourExtension']\n" . + "* Delete the old PHP message file\n" . + "This script no longer generates backward compatibility shims! If you need\n" . + "compatibility with MediaWiki 1.22 and older, use the MediaWiki 1.23 version\n" . + "of this script instead, or create a shim manually.\n" + ); + } + + /** + * Find the documentation comment immediately before a given search string + * @param string $needle String to search for + * @param string $haystack String to search in + * @return string Substring of $haystack starting at '/**' ending right before $needle, or empty + */ + protected function findCommentBefore( $needle, $haystack ) { + $needlePos = strpos( $haystack, $needle ); + if ( $needlePos === false ) { + return ''; + } + // Need to pass a negative offset to strrpos() so it'll search backwards from the + // offset + $startPos = strrpos( $haystack, '/**', $needlePos - strlen( $haystack ) ); + if ( $startPos === false ) { + return ''; + } + + return substr( $haystack, $startPos, $needlePos - $startPos ); + } + + /** + * Get an array of author names from a documentation comment containing @author declarations. + * @param string $comment Documentation comment + * @return array Array of author names (strings) + */ + protected function getAuthorsFromComment( $comment ) { + $matches = null; + preg_match_all( '/@author (.*?)$/m', $comment, $matches ); + + return $matches && $matches[1] ? $matches[1] : []; + } +} + +$maintClass = GenerateJsonI18n::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/generateLocalAutoload.php b/www/wiki/maintenance/generateLocalAutoload.php new file mode 100644 index 00000000..189858c5 --- /dev/null +++ b/www/wiki/maintenance/generateLocalAutoload.php @@ -0,0 +1,22 @@ +setExcludePaths( array_values( AutoLoader::getAutoloadNamespaces() ) ); +$generator->initMediaWikiDefault(); + +// Write out the autoload +$fileinfo = $generator->getTargetFileinfo(); +file_put_contents( + $fileinfo['filename'], + $generator->getAutoload( 'maintenance/generateLocalAutoload.php' ) +); diff --git a/www/wiki/maintenance/generateSitemap.php b/www/wiki/maintenance/generateSitemap.php new file mode 100644 index 00000000..5a2c6c7b --- /dev/null +++ b/www/wiki/maintenance/generateSitemap.php @@ -0,0 +1,559 @@ + and + * Brion Vibber + * + * 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 Maintenance + * @see http://www.sitemaps.org/ + * @see http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that generates a sitemap for the site. + * + * @ingroup Maintenance + */ +class GenerateSitemap extends Maintenance { + const GS_MAIN = -2; + const GS_TALK = -1; + + /** + * The maximum amount of urls in a sitemap file + * + * @link http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd + * + * @var int + */ + public $url_limit; + + /** + * The maximum size of a sitemap file + * + * @link http://www.sitemaps.org/faq.php#faq_sitemap_size + * + * @var int + */ + public $size_limit; + + /** + * The path to prepend to the filename + * + * @var string + */ + public $fspath; + + /** + * The URL path to prepend to filenames in the index; + * should resolve to the same directory as $fspath. + * + * @var string + */ + public $urlpath; + + /** + * Whether or not to use compression + * + * @var bool + */ + public $compress; + + /** + * Whether or not to include redirection pages + * + * @var bool + */ + public $skipRedirects; + + /** + * The number of entries to save in each sitemap file + * + * @var array + */ + public $limit = []; + + /** + * Key => value entries of namespaces and their priorities + * + * @var array + */ + public $priorities = []; + + /** + * A one-dimensional array of namespaces in the wiki + * + * @var array + */ + public $namespaces = []; + + /** + * When this sitemap batch was generated + * + * @var string + */ + public $timestamp; + + /** + * A database replica DB object + * + * @var object + */ + public $dbr; + + /** + * A resource pointing to the sitemap index file + * + * @var resource + */ + public $findex; + + /** + * A resource pointing to a sitemap file + * + * @var resource + */ + public $file; + + /** + * Identifier to use in filenames, default $wgDBname + * + * @var string + */ + private $identifier; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Creates a sitemap for the site' ); + $this->addOption( + 'fspath', + 'The file system path to save to, e.g. /tmp/sitemap; defaults to current directory', + false, + true + ); + $this->addOption( + 'urlpath', + 'The URL path corresponding to --fspath, prepended to filenames in the index; ' + . 'defaults to an empty string', + false, + true + ); + $this->addOption( + 'compress', + 'Compress the sitemap files, can take value yes|no, default yes', + false, + true + ); + $this->addOption( 'skip-redirects', 'Do not include redirecting articles in the sitemap' ); + $this->addOption( + 'identifier', + 'What site identifier to use for the wiki, defaults to $wgDBname', + false, + true + ); + } + + /** + * Execute + */ + public function execute() { + $this->setNamespacePriorities(); + $this->url_limit = 50000; + $this->size_limit = pow( 2, 20 ) * 10; + + # Create directory if needed + $fspath = $this->getOption( 'fspath', getcwd() ); + if ( !wfMkdirParents( $fspath, null, __METHOD__ ) ) { + $this->fatalError( "Can not create directory $fspath." ); + } + + $this->fspath = realpath( $fspath ) . DIRECTORY_SEPARATOR; + $this->urlpath = $this->getOption( 'urlpath', "" ); + if ( $this->urlpath !== "" && substr( $this->urlpath, -1 ) !== '/' ) { + $this->urlpath .= '/'; + } + $this->identifier = $this->getOption( 'identifier', wfWikiID() ); + $this->compress = $this->getOption( 'compress', 'yes' ) !== 'no'; + $this->skipRedirects = $this->hasOption( 'skip-redirects' ); + $this->dbr = $this->getDB( DB_REPLICA ); + $this->generateNamespaces(); + $this->timestamp = wfTimestamp( TS_ISO_8601, wfTimestampNow() ); + $this->findex = fopen( "{$this->fspath}sitemap-index-{$this->identifier}.xml", 'wb' ); + $this->main(); + } + + private function setNamespacePriorities() { + global $wgSitemapNamespacesPriorities; + + // Custom main namespaces + $this->priorities[self::GS_MAIN] = '0.5'; + // Custom talk namesspaces + $this->priorities[self::GS_TALK] = '0.1'; + // MediaWiki standard namespaces + $this->priorities[NS_MAIN] = '1.0'; + $this->priorities[NS_TALK] = '0.1'; + $this->priorities[NS_USER] = '0.5'; + $this->priorities[NS_USER_TALK] = '0.1'; + $this->priorities[NS_PROJECT] = '0.5'; + $this->priorities[NS_PROJECT_TALK] = '0.1'; + $this->priorities[NS_FILE] = '0.5'; + $this->priorities[NS_FILE_TALK] = '0.1'; + $this->priorities[NS_MEDIAWIKI] = '0.0'; + $this->priorities[NS_MEDIAWIKI_TALK] = '0.1'; + $this->priorities[NS_TEMPLATE] = '0.0'; + $this->priorities[NS_TEMPLATE_TALK] = '0.1'; + $this->priorities[NS_HELP] = '0.5'; + $this->priorities[NS_HELP_TALK] = '0.1'; + $this->priorities[NS_CATEGORY] = '0.5'; + $this->priorities[NS_CATEGORY_TALK] = '0.1'; + + // Custom priorities + if ( $wgSitemapNamespacesPriorities !== false ) { + /** + * @var $wgSitemapNamespacesPriorities array + */ + foreach ( $wgSitemapNamespacesPriorities as $namespace => $priority ) { + $float = floatval( $priority ); + if ( $float > 1.0 ) { + $priority = '1.0'; + } elseif ( $float < 0.0 ) { + $priority = '0.0'; + } + $this->priorities[$namespace] = $priority; + } + } + } + + /** + * Generate a one-dimensional array of existing namespaces + */ + function generateNamespaces() { + // Only generate for specific namespaces if $wgSitemapNamespaces is an array. + global $wgSitemapNamespaces; + if ( is_array( $wgSitemapNamespaces ) ) { + $this->namespaces = $wgSitemapNamespaces; + + return; + } + + $res = $this->dbr->select( 'page', + [ 'page_namespace' ], + [], + __METHOD__, + [ + 'GROUP BY' => 'page_namespace', + 'ORDER BY' => 'page_namespace', + ] + ); + + foreach ( $res as $row ) { + $this->namespaces[] = $row->page_namespace; + } + } + + /** + * Get the priority of a given namespace + * + * @param int $namespace The namespace to get the priority for + * @return string + */ + function priority( $namespace ) { + return isset( $this->priorities[$namespace] ) + ? $this->priorities[$namespace] + : $this->guessPriority( $namespace ); + } + + /** + * If the namespace isn't listed on the priority list return the + * default priority for the namespace, varies depending on whether it's + * a talkpage or not. + * + * @param int $namespace The namespace to get the priority for + * @return string + */ + function guessPriority( $namespace ) { + return MWNamespace::isSubject( $namespace ) + ? $this->priorities[self::GS_MAIN] + : $this->priorities[self::GS_TALK]; + } + + /** + * Return a database resolution of all the pages in a given namespace + * + * @param int $namespace Limit the query to this namespace + * @return Resource + */ + function getPageRes( $namespace ) { + return $this->dbr->select( 'page', + [ + 'page_namespace', + 'page_title', + 'page_touched', + 'page_is_redirect' + ], + [ 'page_namespace' => $namespace ], + __METHOD__ + ); + } + + /** + * Main loop + */ + public function main() { + global $wgContLang; + + fwrite( $this->findex, $this->openIndex() ); + + foreach ( $this->namespaces as $namespace ) { + $res = $this->getPageRes( $namespace ); + $this->file = false; + $this->generateLimit( $namespace ); + $length = $this->limit[0]; + $i = $smcount = 0; + + $fns = $wgContLang->getFormattedNsText( $namespace ); + $this->output( "$namespace ($fns)\n" ); + $skippedRedirects = 0; // Number of redirects skipped for that namespace + foreach ( $res as $row ) { + if ( $this->skipRedirects && $row->page_is_redirect ) { + $skippedRedirects++; + continue; + } + + if ( $i++ === 0 + || $i === $this->url_limit + 1 + || $length + $this->limit[1] + $this->limit[2] > $this->size_limit + ) { + if ( $this->file !== false ) { + $this->write( $this->file, $this->closeFile() ); + $this->close( $this->file ); + } + $filename = $this->sitemapFilename( $namespace, $smcount++ ); + $this->file = $this->open( $this->fspath . $filename, 'wb' ); + $this->write( $this->file, $this->openFile() ); + fwrite( $this->findex, $this->indexEntry( $filename ) ); + $this->output( "\t$this->fspath$filename\n" ); + $length = $this->limit[0]; + $i = 1; + } + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $date = wfTimestamp( TS_ISO_8601, $row->page_touched ); + $entry = $this->fileEntry( $title->getCanonicalURL(), $date, $this->priority( $namespace ) ); + $length += strlen( $entry ); + $this->write( $this->file, $entry ); + // generate pages for language variants + if ( $wgContLang->hasVariants() ) { + $variants = $wgContLang->getVariants(); + foreach ( $variants as $vCode ) { + if ( $vCode == $wgContLang->getCode() ) { + continue; // we don't want default variant + } + $entry = $this->fileEntry( + $title->getCanonicalURL( '', $vCode ), + $date, + $this->priority( $namespace ) + ); + $length += strlen( $entry ); + $this->write( $this->file, $entry ); + } + } + } + + if ( $this->skipRedirects && $skippedRedirects > 0 ) { + $this->output( " skipped $skippedRedirects redirect(s)\n" ); + } + + if ( $this->file ) { + $this->write( $this->file, $this->closeFile() ); + $this->close( $this->file ); + } + } + fwrite( $this->findex, $this->closeIndex() ); + fclose( $this->findex ); + } + + /** + * gzopen() / fopen() wrapper + * + * @param string $file + * @param string $flags + * @return resource + */ + function open( $file, $flags ) { + $resource = $this->compress ? gzopen( $file, $flags ) : fopen( $file, $flags ); + if ( $resource === false ) { + throw new MWException( __METHOD__ + . " error opening file $file with flags $flags. Check permissions?" ); + } + + return $resource; + } + + /** + * gzwrite() / fwrite() wrapper + * + * @param resource &$handle + * @param string $str + */ + function write( &$handle, $str ) { + if ( $handle === true || $handle === false ) { + throw new MWException( __METHOD__ . " was passed a boolean as a file handle.\n" ); + } + if ( $this->compress ) { + gzwrite( $handle, $str ); + } else { + fwrite( $handle, $str ); + } + } + + /** + * gzclose() / fclose() wrapper + * + * @param resource &$handle + */ + function close( &$handle ) { + if ( $this->compress ) { + gzclose( $handle ); + } else { + fclose( $handle ); + } + } + + /** + * Get a sitemap filename + * + * @param int $namespace + * @param int $count + * @return string + */ + function sitemapFilename( $namespace, $count ) { + $ext = $this->compress ? '.gz' : ''; + + return "sitemap-{$this->identifier}-NS_$namespace-$count.xml$ext"; + } + + /** + * Return the XML required to open an XML file + * + * @return string + */ + function xmlHead() { + return '' . "\n"; + } + + /** + * Return the XML schema being used + * + * @return string + */ + function xmlSchema() { + return 'http://www.sitemaps.org/schemas/sitemap/0.9'; + } + + /** + * Return the XML required to open a sitemap index file + * + * @return string + */ + function openIndex() { + return $this->xmlHead() . '' . "\n"; + } + + /** + * Return the XML for a single sitemap indexfile entry + * + * @param string $filename The filename of the sitemap file + * @return string + */ + function indexEntry( $filename ) { + return "\t\n" . + "\t\t{$this->urlpath}$filename\n" . + "\t\t{$this->timestamp}\n" . + "\t\n"; + } + + /** + * Return the XML required to close a sitemap index file + * + * @return string + */ + function closeIndex() { + return "\n"; + } + + /** + * Return the XML required to open a sitemap file + * + * @return string + */ + function openFile() { + return $this->xmlHead() . '' . "\n"; + } + + /** + * Return the XML for a single sitemap entry + * + * @param string $url An RFC 2396 compliant URL + * @param string $date A ISO 8601 date + * @param string $priority A priority indicator, 0.0 - 1.0 inclusive with a 0.1 stepsize + * @return string + */ + function fileEntry( $url, $date, $priority ) { + return "\t\n" . + // T36666: $url may contain bad characters such as ampersands. + "\t\t" . htmlspecialchars( $url ) . "\n" . + "\t\t$date\n" . + "\t\t$priority\n" . + "\t\n"; + } + + /** + * Return the XML required to close sitemap file + * + * @return string + */ + function closeFile() { + return "\n"; + } + + /** + * Populate $this->limit + * + * @param int $namespace + */ + function generateLimit( $namespace ) { + // T19961: make a title with the longest possible URL in this namespace + $title = Title::makeTitle( $namespace, str_repeat( "\xf0\xa8\xae\x81", 63 ) . "\xe5\x96\x83" ); + + $this->limit = [ + strlen( $this->openFile() ), + strlen( $this->fileEntry( + $title->getCanonicalURL(), + wfTimestamp( TS_ISO_8601, wfTimestamp() ), + $this->priority( $namespace ) + ) ), + strlen( $this->closeFile() ) + ]; + } +} + +$maintClass = GenerateSitemap::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/getConfiguration.php b/www/wiki/maintenance/getConfiguration.php new file mode 100644 index 00000000..de6e87a3 --- /dev/null +++ b/www/wiki/maintenance/getConfiguration.php @@ -0,0 +1,196 @@ + + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Print serialized output of MediaWiki config vars + * + * @ingroup Maintenance + */ +class GetConfiguration extends Maintenance { + + protected $regex = null; + + protected $settings_list = []; + + /** + * List of format output internally supported. + * Each item MUST be lower case. + */ + protected static $outFormats = [ + 'json', + 'php', + 'serialize', + 'vardump', + ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Get serialized MediaWiki site configuration' ); + $this->addOption( 'regex', 'regex to filter variables with', false, true ); + $this->addOption( 'iregex', 'same as --regex but case insensitive', false, true ); + $this->addOption( 'settings', 'Space-separated list of wg* variables', false, true ); + $this->addOption( 'format', implode( ', ', self::$outFormats ), false, true ); + } + + protected function validateParamsAndArgs() { + $error_out = false; + + # Get the format and make sure it is set to a valid default value + $format = strtolower( $this->getOption( 'format', 'PHP' ) ); + + $validFormat = in_array( $format, self::$outFormats ); + if ( !$validFormat ) { + $this->error( "--format set to an unrecognized format" ); + $error_out = true; + } + + if ( $this->getOption( 'regex' ) && $this->getOption( 'iregex' ) ) { + $this->error( "Can only use either --regex or --iregex" ); + $error_out = true; + } + + parent::validateParamsAndArgs(); + + if ( $error_out ) { + # Force help and quit + $this->maybeHelp( true ); + } + } + + /** + * finalSetup() since we need MWException + */ + public function finalSetup() { + parent::finalSetup(); + + $this->regex = $this->getOption( 'regex' ) ?: $this->getOption( 'iregex' ); + if ( $this->regex ) { + $this->regex = '/' . $this->regex . '/'; + if ( $this->hasOption( 'iregex' ) ) { + $this->regex .= 'i'; # case insensitive regex + } + } + + if ( $this->hasOption( 'settings' ) ) { + $this->settings_list = explode( ' ', $this->getOption( 'settings' ) ); + # Values validation + foreach ( $this->settings_list as $name ) { + if ( !preg_match( '/^wg[A-Z]/', $name ) ) { + throw new MWException( "Variable '$name' does start with 'wg'." ); + } elseif ( !array_key_exists( $name, $GLOBALS ) ) { + throw new MWException( "Variable '$name' is not set." ); + } elseif ( !$this->isAllowedVariable( $GLOBALS[$name] ) ) { + throw new MWException( "Variable '$name' includes non-array, non-scalar, items." ); + } + } + } + } + + public function execute() { + // Settings we will display + $res = []; + + # Sane default: dump any wg / wmg variable + if ( !$this->regex && !$this->getOption( 'settings' ) ) { + $this->regex = '/^wm?g/'; + } + + # Filter out globals based on the regex + if ( $this->regex ) { + $res = []; + foreach ( $GLOBALS as $name => $value ) { + if ( preg_match( $this->regex, $name ) ) { + $res[$name] = $value; + } + } + } + + # Explicitly dumps a list of provided global names + if ( $this->settings_list ) { + foreach ( $this->settings_list as $name ) { + $res[$name] = $GLOBALS[$name]; + } + } + + ksort( $res ); + + $out = null; + switch ( strtolower( $this->getOption( 'format' ) ) ) { + case 'serialize': + case 'php': + $out = serialize( $res ); + break; + case 'vardump': + $out = $this->formatVarDump( $res ); + break; + case 'json': + $out = FormatJson::encode( $res ); + break; + default: + throw new MWException( "Invalid serialization format given." ); + } + if ( !is_string( $out ) ) { + throw new MWException( "Failed to serialize the requested settings." ); + } + + if ( $out ) { + $this->output( $out . "\n" ); + } + } + + protected function formatVarDump( $res ) { + $ret = ''; + foreach ( $res as $key => $value ) { + ob_start(); # intercept var_dump() output + print "\${$key} = "; + var_dump( $value ); + # grab var_dump() output and discard it from the output buffer + $ret .= trim( ob_get_clean() ) . ";\n"; + } + + return trim( $ret, "\n" ); + } + + private function isAllowedVariable( $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $k => $v ) { + if ( !$this->isAllowedVariable( $v ) ) { + return false; + } + } + + return true; + } elseif ( is_scalar( $value ) || $value === null ) { + return true; + } + + return false; + } +} + +$maintClass = GetConfiguration::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/getLagTimes.php b/www/wiki/maintenance/getLagTimes.php new file mode 100644 index 00000000..c2c79833 --- /dev/null +++ b/www/wiki/maintenance/getLagTimes.php @@ -0,0 +1,79 @@ +addDescription( 'Dump replication lag times' ); + $this->addOption( 'report', "Report the lag values to StatsD" ); + } + + public function execute() { + $services = MediaWikiServices::getInstance(); + $lbFactory = $services->getDBLoadBalancerFactory(); + $stats = $services->getStatsdDataFactory(); + $lbsByType = [ + 'main' => $lbFactory->getAllMainLBs(), + 'external' => $lbFactory->getAllExternalLBs() + ]; + + foreach ( $lbsByType as $type => $lbs ) { + foreach ( $lbs as $cluster => $lb ) { + if ( $lb->getServerCount() <= 1 ) { + continue; + } + $lags = $lb->getLagTimes(); + foreach ( $lags as $serverIndex => $lag ) { + $host = $lb->getServerName( $serverIndex ); + if ( IP::isValid( $host ) ) { + $ip = $host; + $host = gethostbyaddr( $host ); + } else { + $ip = gethostbyname( $host ); + } + + $starLen = min( intval( $lag ), 40 ); + $stars = str_repeat( '*', $starLen ); + $this->output( sprintf( "%10s %20s %3d %s\n", $ip, $host, $lag, $stars ) ); + + if ( $this->hasOption( 'report' ) ) { + $group = ( $type === 'external' ) ? 'external' : $cluster; + $stats->gauge( "loadbalancer.lag.$group.$host", intval( $lag * 1e3 ) ); + } + } + } + } + } +} + +$maintClass = GetLagTimes::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/getReplicaServer.php b/www/wiki/maintenance/getReplicaServer.php new file mode 100644 index 00000000..43e876ea --- /dev/null +++ b/www/wiki/maintenance/getReplicaServer.php @@ -0,0 +1,55 @@ +addOption( "group", "Query group to check specifically" ); + $this->addDescription( 'Report the hostname of a replica DB server' ); + } + + public function execute() { + global $wgAllDBsAreLocalhost; + if ( $wgAllDBsAreLocalhost ) { + $host = 'localhost'; + } elseif ( $this->hasOption( 'group' ) ) { + $db = $this->getDB( DB_REPLICA, $this->getOption( 'group' ) ); + $host = $db->getServer(); + } else { + $lb = wfGetLB(); + $i = $lb->getReaderIndex(); + $host = $lb->getServerName( $i ); + } + $this->output( "$host\n" ); + } +} + +$maintClass = GetSlaveServer::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/getSlaveServer.php b/www/wiki/maintenance/getSlaveServer.php new file mode 100644 index 00000000..2fa2d7f6 --- /dev/null +++ b/www/wiki/maintenance/getSlaveServer.php @@ -0,0 +1,3 @@ +addDescription( 'Outputs page text to stdout' ); + $this->addOption( 'show-private', 'Show the text even if it\'s not available to the public' ); + $this->addArg( 'title', 'Page title' ); + } + + public function execute() { + $titleText = $this->getArg( 0 ); + $title = Title::newFromText( $titleText ); + if ( !$title ) { + $this->fatalError( "$titleText is not a valid title.\n" ); + } + + $rev = Revision::newFromTitle( $title ); + if ( !$rev ) { + $titleText = $title->getPrefixedText(); + $this->fatalError( "Page $titleText does not exist.\n" ); + } + $content = $rev->getContent( $this->hasOption( 'show-private' ) + ? Revision::RAW + : Revision::FOR_PUBLIC ); + + if ( $content === false ) { + $titleText = $title->getPrefixedText(); + $this->fatalError( "Couldn't extract the text from $titleText.\n" ); + } + $this->output( $content->serialize() ); + } +} + +$maintClass = GetTextMaint::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/hhvm/makeRepo.php b/www/wiki/maintenance/hhvm/makeRepo.php new file mode 100644 index 00000000..cef0dadc --- /dev/null +++ b/www/wiki/maintenance/hhvm/makeRepo.php @@ -0,0 +1,161 @@ +addDescription( 'Compile PHP sources for this MediaWiki instance, ' . + 'and generate an HHVM bytecode file to be used with HHVM\'s ' . + 'RepoAuthoritative mode. The MediaWiki core installation path and ' . + 'all registered extensions are automatically searched for the file ' . + 'extensions *.php, *.inc, *.php5 and *.phtml.' ); + $this->addOption( 'output', 'Output filename', true, true, 'o' ); + $this->addOption( 'input-dir', 'Add an input directory. ' . + 'This can be specified multiple times.', false, true, 'd', true ); + $this->addOption( 'exclude-dir', 'Directory to exclude. ' . + 'This can be specified multiple times.', false, true, false, true ); + $this->addOption( 'extension', 'Extra file extension', false, true, false, true ); + $this->addOption( 'hhvm', 'Location of HHVM binary', false, true ); + $this->addOption( 'base-dir', 'The root of all source files. ' . + 'This must match hhvm.server.source_root in the server\'s configuration file. ' . + 'By default, the MW core install path will be used.', + false, true ); + $this->addOption( 'verbose', 'Log level 0-3', false, true, 'v' ); + } + + private static function startsWith( $subject, $search ) { + return substr( $subject, 0, strlen( $search ) === $search ); + } + + function execute() { + global $wgExtensionCredits, $IP; + + $dirs = [ $IP ]; + + foreach ( $wgExtensionCredits as $type => $extensions ) { + foreach ( $extensions as $extension ) { + if ( isset( $extension['path'] ) + && !self::startsWith( $extension['path'], $IP ) + ) { + $dirs[] = dirname( $extension['path'] ); + } + } + } + + $dirs = array_merge( $dirs, $this->getOption( 'input-dir', [] ) ); + $fileExts = + [ + 'php' => true, + 'inc' => true, + 'php5' => true, + 'phtml' => true + ] + + array_flip( $this->getOption( 'extension', [] ) ); + + $dirs = array_unique( $dirs ); + + $baseDir = $this->getOption( 'base-dir', $IP ); + $excludeDirs = array_map( 'realpath', $this->getOption( 'exclude-dir', [] ) ); + + if ( $baseDir !== '' && substr( $baseDir, -1 ) !== '/' ) { + $baseDir .= '/'; + } + + $unfilteredFiles = [ "$IP/LocalSettings.php" ]; + foreach ( $dirs as $dir ) { + $this->appendDir( $unfilteredFiles, $dir ); + } + + $files = []; + foreach ( $unfilteredFiles as $file ) { + $dotPos = strrpos( $file, '.' ); + $slashPos = strrpos( $file, '/' ); + if ( $dotPos === false || $slashPos === false || $dotPos < $slashPos ) { + continue; + } + $extension = substr( $file, $dotPos + 1 ); + if ( !isset( $fileExts[$extension] ) ) { + continue; + } + $canonical = realpath( $file ); + foreach ( $excludeDirs as $excluded ) { + if ( self::startsWith( $canonical, $excluded ) ) { + continue 2; + } + } + if ( self::startsWith( $file, $baseDir ) ) { + $file = substr( $file, strlen( $baseDir ) ); + } + $files[] = $file; + } + + $files = array_unique( $files ); + + print "Found " . count( $files ) . " files in " . + count( $dirs ) . " directories\n"; + + $tmpDir = wfTempDir() . '/mw-make-repo' . mt_rand( 0, 1 << 31 ); + if ( !mkdir( $tmpDir ) ) { + $this->fatalError( 'Unable to create temporary directory' ); + } + file_put_contents( "$tmpDir/file-list", implode( "\n", $files ) ); + + $hhvm = $this->getOption( 'hhvm', 'hhvm' ); + $verbose = $this->getOption( 'verbose', 3 ); + $cmd = wfEscapeShellArg( + $hhvm, + '--hphp', + '--target', 'hhbc', + '--format', 'binary', + '--force', '1', + '--keep-tempdir', '1', + '--log', $verbose, + '-v', 'AllVolatile=true', + '--input-dir', $baseDir, + '--input-list', "$tmpDir/file-list", + '--output-dir', $tmpDir ); + print "$cmd\n"; + passthru( $cmd, $ret ); + if ( $ret ) { + $this->cleanupTemp( $tmpDir ); + $this->fatalError( "Error: HHVM returned error code $ret" ); + } + if ( !rename( "$tmpDir/hhvm.hhbc", $this->getOption( 'output' ) ) ) { + $this->cleanupTemp( $tmpDir ); + $this->fatalError( "Error: unable to rename output file" ); + } + $this->cleanupTemp( $tmpDir ); + return 0; + } + + private function cleanupTemp( $tmpDir ) { + if ( file_exists( "$tmpDir/hhvm.hhbc" ) ) { + unlink( "$tmpDir/hhvm.hhbc" ); + } + if ( file_exists( "$tmpDir/Stats.js" ) ) { + unlink( "$tmpDir/Stats.js" ); + } + + unlink( "$tmpDir/file-list" ); + rmdir( $tmpDir ); + } + + private function appendDir( &$files, $dir ) { + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $dir, + FilesystemIterator::UNIX_PATHS + ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + foreach ( $iter as $file => $fileInfo ) { + if ( $fileInfo->isFile() ) { + $files[] = $file; + } + } + } +} + +$maintClass = HHVMMakeRepo::class; +require RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/hhvm/run-server b/www/wiki/maintenance/hhvm/run-server new file mode 100755 index 00000000..d84e02f2 --- /dev/null +++ b/www/wiki/maintenance/hhvm/run-server @@ -0,0 +1,28 @@ +#!/usr/bin/hhvm -f + + * 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 + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script that imports XML dump files into the current wiki. + * + * @ingroup Maintenance + */ +class BackupReader extends Maintenance { + public $reportingInterval = 100; + public $pageCount = 0; + public $revCount = 0; + public $dryRun = false; + public $uploads = false; + protected $uploadCount = 0; + public $imageBasePath = false; + public $nsFilter = false; + + function __construct() { + parent::__construct(); + $gz = in_array( 'compress.zlib', stream_get_wrappers() ) + ? 'ok' + : '(disabled; requires PHP zlib module)'; + $bz2 = in_array( 'compress.bzip2', stream_get_wrappers() ) + ? 'ok' + : '(disabled; requires PHP bzip2 module)'; + + $this->addDescription( + << +TEXT + ); + $this->stderr = fopen( "php://stderr", "wt" ); + $this->addOption( 'report', + 'Report position and speed after every n pages processed', false, true ); + $this->addOption( 'namespaces', + 'Import only the pages from namespaces belonging to the list of ' . + 'pipe-separated namespace names or namespace indexes', false, true ); + $this->addOption( 'rootpage', 'Pages will be imported as subpages of the specified page', + false, true ); + $this->addOption( 'dry-run', 'Parse dump without actually importing pages' ); + $this->addOption( 'debug', 'Output extra verbose debug information' ); + $this->addOption( 'uploads', 'Process file upload data if included (experimental)' ); + $this->addOption( + 'no-updates', + 'Disable link table updates. Is faster but leaves the wiki in an inconsistent state' + ); + $this->addOption( 'image-base-path', 'Import files from a specified path', false, true ); + $this->addOption( 'skip-to', 'Start from nth page by skipping first n-1 pages', false, true ); + $this->addOption( 'username-interwiki', 'Use interwiki usernames with this prefix', false, true ); + $this->addOption( 'no-local-users', + 'Treat all usernames as interwiki. ' . + 'The default is to assign edits to local users where they exist.', + false, false + ); + $this->addArg( 'file', 'Dump file to import [else use stdin]', false ); + } + + public function execute() { + if ( wfReadOnly() ) { + $this->fatalError( "Wiki is in read-only mode; you'll need to disable it for import to work." ); + } + + $this->reportingInterval = intval( $this->getOption( 'report', 100 ) ); + if ( !$this->reportingInterval ) { + $this->reportingInterval = 100; // avoid division by zero + } + + $this->dryRun = $this->hasOption( 'dry-run' ); + $this->uploads = $this->hasOption( 'uploads' ); // experimental! + if ( $this->hasOption( 'image-base-path' ) ) { + $this->imageBasePath = $this->getOption( 'image-base-path' ); + } + if ( $this->hasOption( 'namespaces' ) ) { + $this->setNsfilter( explode( '|', $this->getOption( 'namespaces' ) ) ); + } + + if ( $this->hasArg() ) { + $this->importFromFile( $this->getArg() ); + } else { + $this->importFromStdin(); + } + + $this->output( "Done!\n" ); + $this->output( "You might want to run rebuildrecentchanges.php to regenerate RecentChanges,\n" ); + $this->output( "and initSiteStats.php to update page and revision counts\n" ); + } + + function setNsfilter( array $namespaces ) { + if ( count( $namespaces ) == 0 ) { + $this->nsFilter = false; + + return; + } + $this->nsFilter = array_unique( array_map( [ $this, 'getNsIndex' ], $namespaces ) ); + } + + private function getNsIndex( $namespace ) { + global $wgContLang; + $result = $wgContLang->getNsIndex( $namespace ); + if ( $result !== false ) { + return $result; + } + $ns = intval( $namespace ); + if ( strval( $ns ) === $namespace && $wgContLang->getNsText( $ns ) !== false ) { + return $ns; + } + $this->fatalError( "Unknown namespace text / index specified: $namespace" ); + } + + /** + * @param Title|Revision $obj + * @throws MWException + * @return bool + */ + private function skippedNamespace( $obj ) { + $title = null; + if ( $obj instanceof Title ) { + $title = $obj; + } elseif ( $obj instanceof Revision ) { + $title = $obj->getTitle(); + } elseif ( $obj instanceof WikiRevision ) { + $title = $obj->title; + } else { + throw new MWException( "Cannot get namespace of object in " . __METHOD__ ); + } + + if ( is_null( $title ) ) { + // Probably a log entry + return false; + } + + $ns = $title->getNamespace(); + + return is_array( $this->nsFilter ) && !in_array( $ns, $this->nsFilter ); + } + + function reportPage( $page ) { + $this->pageCount++; + } + + /** + * @param Revision $rev + */ + function handleRevision( $rev ) { + $title = $rev->getTitle(); + if ( !$title ) { + $this->progress( "Got bogus revision with null title!" ); + + return; + } + + if ( $this->skippedNamespace( $title ) ) { + return; + } + + $this->revCount++; + $this->report(); + + if ( !$this->dryRun ) { + call_user_func( $this->importCallback, $rev ); + } + } + + /** + * @param Revision $revision + * @return bool + */ + function handleUpload( $revision ) { + if ( $this->uploads ) { + if ( $this->skippedNamespace( $revision ) ) { + return false; + } + $this->uploadCount++; + // $this->report(); + $this->progress( "upload: " . $revision->getFilename() ); + + if ( !$this->dryRun ) { + // bluuuh hack + // call_user_func( $this->uploadCallback, $revision ); + $dbw = $this->getDB( DB_MASTER ); + + return $dbw->deadlockLoop( [ $revision, 'importUpload' ] ); + } + } + + return false; + } + + function handleLogItem( $rev ) { + if ( $this->skippedNamespace( $rev ) ) { + return; + } + $this->revCount++; + $this->report(); + + if ( !$this->dryRun ) { + call_user_func( $this->logItemCallback, $rev ); + } + } + + function report( $final = false ) { + if ( $final xor ( $this->pageCount % $this->reportingInterval == 0 ) ) { + $this->showReport(); + } + } + + function showReport() { + if ( !$this->mQuiet ) { + $delta = microtime( true ) - $this->startTime; + if ( $delta ) { + $rate = sprintf( "%.2f", $this->pageCount / $delta ); + $revrate = sprintf( "%.2f", $this->revCount / $delta ); + } else { + $rate = '-'; + $revrate = '-'; + } + # Logs dumps don't have page tallies + if ( $this->pageCount ) { + $this->progress( "$this->pageCount ($rate pages/sec $revrate revs/sec)" ); + } else { + $this->progress( "$this->revCount ($revrate revs/sec)" ); + } + } + wfWaitForSlaves(); + } + + function progress( $string ) { + fwrite( $this->stderr, $string . "\n" ); + } + + function importFromFile( $filename ) { + if ( preg_match( '/\.gz$/', $filename ) ) { + $filename = 'compress.zlib://' . $filename; + } elseif ( preg_match( '/\.bz2$/', $filename ) ) { + $filename = 'compress.bzip2://' . $filename; + } elseif ( preg_match( '/\.7z$/', $filename ) ) { + $filename = 'mediawiki.compress.7z://' . $filename; + } + + $file = fopen( $filename, 'rt' ); + + return $this->importFromHandle( $file ); + } + + function importFromStdin() { + $file = fopen( 'php://stdin', 'rt' ); + if ( self::posix_isatty( $file ) ) { + $this->maybeHelp( true ); + } + + return $this->importFromHandle( $file ); + } + + function importFromHandle( $handle ) { + $this->startTime = microtime( true ); + + $source = new ImportStreamSource( $handle ); + $importer = new WikiImporter( $source, $this->getConfig() ); + + // Updating statistics require a lot of time so disable it + $importer->disableStatisticsUpdate(); + + if ( $this->hasOption( 'debug' ) ) { + $importer->setDebug( true ); + } + if ( $this->hasOption( 'no-updates' ) ) { + $importer->setNoUpdates( true ); + } + if ( $this->hasOption( 'username-prefix' ) ) { + $importer->setUsernamePrefix( + $this->getOption( 'username-prefix' ), + !$this->hasOption( 'no-local-users' ) + ); + } + if ( $this->hasOption( 'rootpage' ) ) { + $statusRootPage = $importer->setTargetRootPage( $this->getOption( 'rootpage' ) ); + if ( !$statusRootPage->isGood() ) { + // Die here so that it doesn't print "Done!" + $this->fatalError( $statusRootPage->getMessage()->text() ); + return false; + } + } + if ( $this->hasOption( 'skip-to' ) ) { + $nthPage = (int)$this->getOption( 'skip-to' ); + $importer->setPageOffset( $nthPage ); + $this->pageCount = $nthPage - 1; + } + $importer->setPageCallback( [ $this, 'reportPage' ] ); + $importer->setNoticeCallback( function ( $msg, $params ) { + echo wfMessage( $msg, $params )->text() . "\n"; + } ); + $this->importCallback = $importer->setRevisionCallback( + [ $this, 'handleRevision' ] ); + $this->uploadCallback = $importer->setUploadCallback( + [ $this, 'handleUpload' ] ); + $this->logItemCallback = $importer->setLogItemCallback( + [ $this, 'handleLogItem' ] ); + if ( $this->uploads ) { + $importer->setImportUploads( true ); + } + if ( $this->imageBasePath ) { + $importer->setImageBasePath( $this->imageBasePath ); + } + + if ( $this->dryRun ) { + $importer->setPageOutCallback( null ); + } + + return $importer->doImport(); + } +} + +$maintClass = BackupReader::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/importImages.php b/www/wiki/maintenance/importImages.php new file mode 100644 index 00000000..5db1fa89 --- /dev/null +++ b/www/wiki/maintenance/importImages.php @@ -0,0 +1,523 @@ + + * @author Mij + */ + +require_once __DIR__ . '/Maintenance.php'; + +class ImportImages extends Maintenance { + + public function __construct() { + parent::__construct(); + + $this->addDescription( 'Imports images and other media files into the wiki' ); + $this->addArg( 'dir', 'Path to the directory containing images to be imported' ); + + $this->addOption( 'extensions', + 'Comma-separated list of allowable extensions, defaults to $wgFileExtensions', + false, + true + ); + $this->addOption( 'overwrite', + 'Overwrite existing images with the same name (default is to skip them)' ); + $this->addOption( 'limit', + 'Limit the number of images to process. Ignored or skipped images are not counted', + false, + true + ); + $this->addOption( 'from', + "Ignore all files until the one with the given name. Useful for resuming aborted " + . "imports. The name should be the file's canonical database form.", + false, + true + ); + $this->addOption( 'skip-dupes', + 'Skip images that were already uploaded under a different name (check SHA1)' ); + $this->addOption( 'search-recursively', 'Search recursively for files in subdirectories' ); + $this->addOption( 'sleep', + 'Sleep between files. Useful mostly for debugging', + false, + true + ); + $this->addOption( 'user', + "Set username of uploader, default 'Maintenance script'", + false, + true + ); + // This parameter can optionally have an argument. If none specified, getOption() + // returns 1 which is precisely what we need. + $this->addOption( 'check-userblock', 'Check if the user got blocked during import' ); + $this->addOption( 'comment', + "Set file description, default 'Importing file'", + false, + true + ); + $this->addOption( 'comment-file', + 'Set description to the content of this file', + false, + true + ); + $this->addOption( 'comment-ext', + 'Causes the description for each file to be loaded from a file with the same name, but ' + . 'the extension provided. If a global description is also given, it is appended.', + false, + true + ); + $this->addOption( 'summary', + 'Upload summary, description will be used if not provided', + false, + true + ); + $this->addOption( 'license', + 'Use an optional license template', + false, + true + ); + $this->addOption( 'timestamp', + 'Override upload time/date, all MediaWiki timestamp formats are accepted', + false, + true + ); + $this->addOption( 'protect', + 'Specify the protect value (autoconfirmed,sysop)', + false, + true + ); + $this->addOption( 'unprotect', 'Unprotects all uploaded images' ); + $this->addOption( 'source-wiki-url', + 'If specified, take User and Comment data for each imported file from this URL. ' + . 'For example, --source-wiki-url="http://en.wikipedia.org/', + false, + true + ); + $this->addOption( 'dry', "Dry run, don't import anything" ); + } + + public function execute() { + global $wgFileExtensions, $wgUser, $wgRestrictionLevels; + + $processed = $added = $ignored = $skipped = $overwritten = $failed = 0; + + $this->output( "Import Images\n\n" ); + + $dir = $this->getArg( 0 ); + + # Check Protection + if ( $this->hasOption( 'protect' ) && $this->hasOption( 'unprotect' ) ) { + $this->fatalError( "Cannot specify both protect and unprotect. Only 1 is allowed.\n" ); + } + + if ( $this->hasOption( 'protect' ) && trim( $this->getOption( 'protect' ) ) ) { + $this->fatalError( "You must specify a protection option.\n" ); + } + + # Prepare the list of allowed extensions + $extensions = $this->hasOption( 'extensions' ) + ? explode( ',', strtolower( $this->getOption( 'extensions' ) ) ) + : $wgFileExtensions; + + # Search the path provided for candidates for import + $files = $this->findFiles( $dir, $extensions, $this->hasOption( 'search-recursively' ) ); + + # Initialise the user for this operation + $user = $this->hasOption( 'user' ) + ? User::newFromName( $this->getOption( 'user' ) ) + : User::newSystemUser( 'Maintenance script', [ 'steal' => true ] ); + if ( !$user instanceof User ) { + $user = User::newSystemUser( 'Maintenance script', [ 'steal' => true ] ); + } + $wgUser = $user; + + # Get block check. If a value is given, this specified how often the check is performed + $checkUserBlock = (int)$this->getOption( 'check-userblock' ); + + $from = $this->getOption( 'from' ); + $sleep = (int)$this->getOption( 'sleep' ); + $limit = (int)$this->getOption( 'limit' ); + $timestamp = $this->getOption( 'timestamp', false ); + + # Get the upload comment. Provide a default one in case there's no comment given. + $commentFile = $this->getOption( 'comment-file' ); + if ( $commentFile !== null ) { + $comment = file_get_contents( $commentFile ); + if ( $comment === false || $comment === null ) { + $this->fatalError( "failed to read comment file: {$commentFile}\n" ); + } + } else { + $comment = $this->getOption( 'comment', 'Importing file' ); + } + $commentExt = $this->getOption( 'comment-ext' ); + $summary = $this->getOption( 'summary', '' ); + + $license = $this->getOption( 'license', '' ); + + $sourceWikiUrl = $this->getOption( 'source-wiki-url' ); + + # Batch "upload" operation + $count = count( $files ); + if ( $count > 0 ) { + foreach ( $files as $file ) { + if ( $sleep && ( $processed > 0 ) ) { + sleep( $sleep ); + } + + $base = UtfNormal\Validator::cleanUp( wfBaseName( $file ) ); + + # Validate a title + $title = Title::makeTitleSafe( NS_FILE, $base ); + if ( !is_object( $title ) ) { + $this->output( + "{$base} could not be imported; a valid title cannot be produced\n" ); + continue; + } + + if ( $from ) { + if ( $from == $title->getDBkey() ) { + $from = null; + } else { + $ignored++; + continue; + } + } + + if ( $checkUserBlock && ( ( $processed % $checkUserBlock ) == 0 ) ) { + $user->clearInstanceCache( 'name' ); // reload from DB! + if ( $user->isBlocked() ) { + $this->output( $user->getName() . " was blocked! Aborting.\n" ); + break; + } + } + + # Check existence + $image = wfLocalFile( $title ); + if ( $image->exists() ) { + if ( $this->hasOption( 'overwrite' ) ) { + $this->output( "{$base} exists, overwriting..." ); + $svar = 'overwritten'; + } else { + $this->output( "{$base} exists, skipping\n" ); + $skipped++; + continue; + } + } else { + if ( $this->hasOption( 'skip-dupes' ) ) { + $repo = $image->getRepo(); + # XXX: we end up calculating this again when actually uploading. that sucks. + $sha1 = FSFile::getSha1Base36FromPath( $file ); + + $dupes = $repo->findBySha1( $sha1 ); + + if ( $dupes ) { + $this->output( + "{$base} already exists as {$dupes[0]->getName()}, skipping\n" ); + $skipped++; + continue; + } + } + + $this->output( "Importing {$base}..." ); + $svar = 'added'; + } + + if ( $sourceWikiUrl ) { + /* find comment text directly from source wiki, through MW's API */ + $real_comment = $this->getFileCommentFromSourceWiki( $sourceWikiUrl, $base ); + if ( $real_comment === false ) { + $commentText = $comment; + } else { + $commentText = $real_comment; + } + + /* find user directly from source wiki, through MW's API */ + $real_user = $this->getFileUserFromSourceWiki( $sourceWikiUrl, $base ); + if ( $real_user === false ) { + $wgUser = $user; + } else { + $wgUser = User::newFromName( $real_user ); + if ( $wgUser === false ) { + # user does not exist in target wiki + $this->output( + "failed: user '$real_user' does not exist in target wiki." ); + continue; + } + } + } else { + # Find comment text + $commentText = false; + + if ( $commentExt ) { + $f = $this->findAuxFile( $file, $commentExt ); + if ( !$f ) { + $this->output( " No comment file with extension {$commentExt} found " + . "for {$file}, using default comment. " ); + } else { + $commentText = file_get_contents( $f ); + if ( !$commentText ) { + $this->output( + " Failed to load comment file {$f}, using default comment. " ); + } + } + } + + if ( !$commentText ) { + $commentText = $comment; + } + } + + # Import the file + if ( $this->hasOption( 'dry' ) ) { + $this->output( + " publishing {$file} by '{$wgUser->getName()}', comment '$commentText'... " + ); + } else { + $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() ); + $props = $mwProps->getPropsFromPath( $file, true ); + $flags = 0; + $publishOptions = []; + $handler = MediaHandler::getHandler( $props['mime'] ); + if ( $handler ) { + $metadata = Wikimedia\quietCall( 'unserialize', $props['metadata'] ); + + $publishOptions['headers'] = $handler->getContentHeaders( $metadata ); + } else { + $publishOptions['headers'] = []; + } + $archive = $image->publish( $file, $flags, $publishOptions ); + if ( !$archive->isGood() ) { + $this->output( "failed. (" . + $archive->getWikiText( false, false, 'en' ) . + ")\n" ); + $failed++; + continue; + } + } + + $commentText = SpecialUpload::getInitialPageText( $commentText, $license ); + if ( !$this->hasOption( 'summary' ) ) { + $summary = $commentText; + } + + if ( $this->hasOption( 'dry' ) ) { + $this->output( "done.\n" ); + } elseif ( $image->recordUpload2( + $archive->value, + $summary, + $commentText, + $props, + $timestamp + )->isOK() ) { + # We're done! + $this->output( "done.\n" ); + + $doProtect = false; + + $protectLevel = $this->getOption( 'protect' ); + + if ( $protectLevel && in_array( $protectLevel, $wgRestrictionLevels ) ) { + $doProtect = true; + } + if ( $this->hasOption( 'unprotect' ) ) { + $protectLevel = ''; + $doProtect = true; + } + + if ( $doProtect ) { + # Protect the file + $this->output( "\nWaiting for replica DBs...\n" ); + // Wait for replica DBs. + sleep( 2.0 ); # Why this sleep? + wfWaitForSlaves(); + + $this->output( "\nSetting image restrictions ... " ); + + $cascade = false; + $restrictions = []; + foreach ( $title->getRestrictionTypes() as $type ) { + $restrictions[$type] = $protectLevel; + } + + $page = WikiPage::factory( $title ); + $status = $page->doUpdateRestrictions( $restrictions, [], $cascade, '', $user ); + $this->output( ( $status->isOK() ? 'done' : 'failed' ) . "\n" ); + } + } else { + $this->output( "failed. (at recordUpload stage)\n" ); + $svar = 'failed'; + } + + $$svar++; + $processed++; + + if ( $limit && $processed >= $limit ) { + break; + } + } + + # Print out some statistics + $this->output( "\n" ); + foreach ( + [ + 'count' => 'Found', + 'limit' => 'Limit', + 'ignored' => 'Ignored', + 'added' => 'Added', + 'skipped' => 'Skipped', + 'overwritten' => 'Overwritten', + 'failed' => 'Failed' + ] as $var => $desc + ) { + if ( $$var > 0 ) { + $this->output( "{$desc}: {$$var}\n" ); + } + } + } else { + $this->output( "No suitable files could be found for import.\n" ); + } + } + + /** + * Search a directory for files with one of a set of extensions + * + * @param string $dir Path to directory to search + * @param array $exts Array of extensions to search for + * @param bool $recurse Search subdirectories recursively + * @return array|bool Array of filenames on success, or false on failure + */ + private function findFiles( $dir, $exts, $recurse = false ) { + if ( is_dir( $dir ) ) { + $dhl = opendir( $dir ); + if ( $dhl ) { + $files = []; + while ( ( $file = readdir( $dhl ) ) !== false ) { + if ( is_file( $dir . '/' . $file ) ) { + list( /* $name */, $ext ) = $this->splitFilename( $dir . '/' . $file ); + if ( array_search( strtolower( $ext ), $exts ) !== false ) { + $files[] = $dir . '/' . $file; + } + } elseif ( $recurse && is_dir( $dir . '/' . $file ) && $file !== '..' && $file !== '.' ) { + $files = array_merge( $files, $this->findFiles( $dir . '/' . $file, $exts, true ) ); + } + } + + return $files; + } else { + return []; + } + } else { + return []; + } + } + + /** + * Split a filename into filename and extension + * + * @param string $filename + * @return array + */ + private function splitFilename( $filename ) { + $parts = explode( '.', $filename ); + $ext = $parts[count( $parts ) - 1]; + unset( $parts[count( $parts ) - 1] ); + $fname = implode( '.', $parts ); + + return [ $fname, $ext ]; + } + + /** + * Find an auxilliary file with the given extension, matching + * the give base file path. $maxStrip determines how many extensions + * may be stripped from the original file name before appending the + * new extension. For example, with $maxStrip = 1 (the default), + * file files acme.foo.bar.txt and acme.foo.txt would be auxilliary + * files for acme.foo.bar and the extension ".txt". With $maxStrip = 2, + * acme.txt would also be acceptable. + * + * @param string $file Base path + * @param string $auxExtension The extension to be appended to the base path + * @param int $maxStrip The maximum number of extensions to strip from the base path (default: 1) + * @return string|bool + */ + private function findAuxFile( $file, $auxExtension, $maxStrip = 1 ) { + if ( strpos( $auxExtension, '.' ) !== 0 ) { + $auxExtension = '.' . $auxExtension; + } + + $d = dirname( $file ); + $n = basename( $file ); + + while ( $maxStrip >= 0 ) { + $f = $d . '/' . $n . $auxExtension; + + if ( file_exists( $f ) ) { + return $f; + } + + $idx = strrpos( $n, '.' ); + if ( !$idx ) { + break; + } + + $n = substr( $n, 0, $idx ); + $maxStrip -= 1; + } + + return false; + } + + # @todo FIXME: Access the api in a saner way and performing just one query + # (preferably batching files too). + private function getFileCommentFromSourceWiki( $wiki_host, $file ) { + $url = $wiki_host . '/api.php?action=query&format=xml&titles=File:' + . rawurlencode( $file ) . '&prop=imageinfo&&iiprop=comment'; + $body = Http::get( $url, [], __METHOD__ ); + if ( preg_match( '##', $body, $matches ) == 0 ) { + return false; + } + + return html_entity_decode( $matches[1] ); + } + + private function getFileUserFromSourceWiki( $wiki_host, $file ) { + $url = $wiki_host . '/api.php?action=query&format=xml&titles=File:' + . rawurlencode( $file ) . '&prop=imageinfo&&iiprop=user'; + $body = Http::get( $url, [], __METHOD__ ); + if ( preg_match( '##', $body, $matches ) == 0 ) { + return false; + } + + return html_entity_decode( $matches[1] ); + } + +} + +$maintClass = ImportImages::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/importSiteScripts.php b/www/wiki/maintenance/importSiteScripts.php new file mode 100644 index 00000000..e60e7763 --- /dev/null +++ b/www/wiki/maintenance/importSiteScripts.php @@ -0,0 +1,118 @@ +addDescription( 'Import site scripts from a site' ); + $this->addArg( 'api', 'API base url' ); + $this->addArg( 'index', 'index.php base url' ); + $this->addOption( 'username', 'User name of the script importer' ); + } + + public function execute() { + global $wgUser; + + $username = $this->getOption( 'username', false ); + if ( $username === false ) { + $user = User::newSystemUser( 'ScriptImporter', [ 'steal' => true ] ); + } else { + $user = User::newFromName( $username ); + } + $wgUser = $user; + + $baseUrl = $this->getArg( 1 ); + $pageList = $this->fetchScriptList(); + $this->output( 'Importing ' . count( $pageList ) . " pages\n" ); + + foreach ( $pageList as $page ) { + $title = Title::makeTitleSafe( NS_MEDIAWIKI, $page ); + if ( !$title ) { + $this->error( "$page is an invalid title; it will not be imported\n" ); + continue; + } + + $this->output( "Importing $page\n" ); + $url = wfAppendQuery( $baseUrl, [ + 'action' => 'raw', + 'title' => "MediaWiki:{$page}" ] ); + $text = Http::get( $url, [], __METHOD__ ); + + $wikiPage = WikiPage::factory( $title ); + $content = ContentHandler::makeContent( $text, $wikiPage->getTitle() ); + $wikiPage->doEditContent( $content, "Importing from $url", 0, false, $user ); + } + } + + protected function fetchScriptList() { + $data = [ + 'action' => 'query', + 'format' => 'json', + 'list' => 'allpages', + 'apnamespace' => '8', + 'aplimit' => '500', + 'continue' => '', + ]; + $baseUrl = $this->getArg( 0 ); + $pages = []; + + while ( true ) { + $url = wfAppendQuery( $baseUrl, $data ); + $strResult = Http::get( $url, [], __METHOD__ ); + $result = FormatJson::decode( $strResult, true ); + + $page = null; + foreach ( $result['query']['allpages'] as $page ) { + if ( substr( $page['title'], -3 ) === '.js' ) { + strtok( $page['title'], ':' ); + $pages[] = strtok( '' ); + } + } + + if ( $page !== null ) { + $this->output( "Fetched list up to {$page['title']}\n" ); + } + + if ( isset( $result['continue'] ) ) { // >= 1.21 + $data = array_replace( $data, $result['continue'] ); + } elseif ( isset( $result['query-continue']['allpages'] ) ) { // <= 1.20 + $data = array_replace( $data, $result['query-continue']['allpages'] ); + } else { + break; + } + } + + return $pages; + } +} + +$maintClass = ImportSiteScripts::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/importSites.php b/www/wiki/maintenance/importSites.php new file mode 100644 index 00000000..be6cc052 --- /dev/null +++ b/www/wiki/maintenance/importSites.php @@ -0,0 +1,54 @@ +addDescription( 'Imports site definitions from XML into the sites table.' ); + + $this->addArg( 'file', 'An XML file containing site definitions (see docs/sitelist.txt). ' . + 'Use "php://stdin" to read from stdin.', true + ); + + parent::__construct(); + } + + /** + * Do the import. + */ + public function execute() { + $file = $this->getArg( 0 ); + + $siteStore = \MediaWiki\MediaWikiServices::getInstance()->getSiteStore(); + $importer = new SiteImporter( $siteStore ); + $importer->setExceptionCallback( [ $this, 'reportException' ] ); + + $importer->importFromFile( $file ); + + $this->output( "Done.\n" ); + } + + /** + * Outputs a message via the output() method. + * + * @param Exception $ex + */ + public function reportException( Exception $ex ) { + $msg = $ex->getMessage(); + $this->output( "$msg\n" ); + } +} + +$maintClass = ImportSites::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/importTextFiles.php b/www/wiki/maintenance/importTextFiles.php new file mode 100644 index 00000000..c99aa155 --- /dev/null +++ b/www/wiki/maintenance/importTextFiles.php @@ -0,0 +1,208 @@ +addDescription( 'Reads in text files and imports their content to pages of the wiki' ); + $this->addOption( 'user', 'Username to which edits should be attributed. ' . + 'Default: "Maintenance script"', false, true, 'u' ); + $this->addOption( 'summary', 'Specify edit summary for the edits', false, true, 's' ); + $this->addOption( 'use-timestamp', 'Use the modification date of the text file ' . + 'as the timestamp for the edit' ); + $this->addOption( 'overwrite', 'Overwrite existing pages. If --use-timestamp is passed, this ' . + 'will only overwrite pages if the file has been modified since the page was last modified.' ); + $this->addOption( 'prefix', 'A string to place in front of the file name', false, true, 'p' ); + $this->addOption( 'bot', 'Mark edits as bot edits in the recent changes list.' ); + $this->addOption( 'rc', 'Place revisions in RecentChanges.' ); + $this->addArg( 'files', 'Files to import' ); + } + + public function execute() { + $userName = $this->getOption( 'user', false ); + $summary = $this->getOption( 'summary', 'Imported from text file' ); + $useTimestamp = $this->hasOption( 'use-timestamp' ); + $rc = $this->hasOption( 'rc' ); + $bot = $this->hasOption( 'bot' ); + $overwrite = $this->hasOption( 'overwrite' ); + $prefix = $this->getOption( 'prefix', '' ); + + // Get all the arguments. A loop is required since Maintenance doesn't + // support an arbitrary number of arguments. + $files = []; + $i = 0; + while ( $arg = $this->getArg( $i++ ) ) { + if ( file_exists( $arg ) ) { + $files[$arg] = file_get_contents( $arg ); + } else { + // use glob to support the Windows shell, which doesn't automatically + // expand wildcards + $found = false; + foreach ( glob( $arg ) as $filename ) { + $found = true; + $files[$filename] = file_get_contents( $filename ); + } + if ( !$found ) { + $this->fatalError( "Fatal error: The file '$arg' does not exist!" ); + } + } + }; + + $count = count( $files ); + $this->output( "Importing $count pages...\n" ); + + if ( $userName === false ) { + $user = User::newSystemUser( 'Maintenance script', [ 'steal' => true ] ); + } else { + $user = User::newFromName( $userName ); + } + + if ( !$user ) { + $this->fatalError( "Invalid username\n" ); + } + if ( $user->isAnon() ) { + $user->addToDatabase(); + } + + $exit = 0; + + $successCount = 0; + $failCount = 0; + $skipCount = 0; + + foreach ( $files as $file => $text ) { + $pageName = $prefix . pathinfo( $file, PATHINFO_FILENAME ); + $timestamp = $useTimestamp ? wfTimestamp( TS_UNIX, filemtime( $file ) ) : wfTimestampNow(); + + $title = Title::newFromText( $pageName ); + // Have to check for # manually, since it gets interpreted as a fragment + if ( !$title || $title->hasFragment() ) { + $this->error( "Invalid title $pageName. Skipping.\n" ); + $skipCount++; + continue; + } + + $exists = $title->exists(); + $oldRevID = $title->getLatestRevID(); + $oldRev = $oldRevID ? Revision::newFromId( $oldRevID ) : null; + $actualTitle = $title->getPrefixedText(); + + if ( $exists ) { + $touched = wfTimestamp( TS_UNIX, $title->getTouched() ); + if ( !$overwrite ) { + $this->output( "Title $actualTitle already exists. Skipping.\n" ); + $skipCount++; + continue; + } elseif ( $useTimestamp && intval( $touched ) >= intval( $timestamp ) ) { + $this->output( "File for title $actualTitle has not been modified since the " . + "destination page was touched. Skipping.\n" ); + $skipCount++; + continue; + } + } + + $rev = new WikiRevision( MediaWikiServices::getInstance()->getMainConfig() ); + $rev->setText( rtrim( $text ) ); + $rev->setTitle( $title ); + $rev->setUserObj( $user ); + $rev->setComment( $summary ); + $rev->setTimestamp( $timestamp ); + + if ( $exists && $overwrite && $rev->getContent()->equals( $oldRev->getContent() ) ) { + $this->output( "File for title $actualTitle contains no changes from the current " . + "revision. Skipping.\n" ); + $skipCount++; + continue; + } + + $status = $rev->importOldRevision(); + $newId = $title->getLatestRevID(); + + if ( $status ) { + $action = $exists ? 'updated' : 'created'; + $this->output( "Successfully $action $actualTitle\n" ); + $successCount++; + } else { + $action = $exists ? 'update' : 'create'; + $this->output( "Failed to $action $actualTitle\n" ); + $failCount++; + $exit = 1; + } + + // Create the RecentChanges entry if necessary + if ( $rc && $status ) { + if ( $exists ) { + if ( is_object( $oldRev ) ) { + $oldContent = $oldRev->getContent(); + RecentChange::notifyEdit( + $timestamp, + $title, + $rev->getMinor(), + $user, + $summary, + $oldRevID, + $oldRev->getTimestamp(), + $bot, + '', + $oldContent ? $oldContent->getSize() : 0, + $rev->getContent()->getSize(), + $newId, + 1 /* the pages don't need to be patrolled */ + ); + } + } else { + RecentChange::notifyNew( + $timestamp, + $title, + $rev->getMinor(), + $user, + $summary, + $bot, + '', + $rev->getContent()->getSize(), + $newId, + 1 + ); + } + } + } + + $this->output( "Done! $successCount succeeded, $skipCount skipped.\n" ); + if ( $exit ) { + $this->fatalError( "Import failed with $failCount failed pages.\n", $exit ); + } + } +} + +$maintClass = ImportTextFiles::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/initEditCount.php b/www/wiki/maintenance/initEditCount.php new file mode 100644 index 00000000..f7ef7a28 --- /dev/null +++ b/www/wiki/maintenance/initEditCount.php @@ -0,0 +1,191 @@ +addOption( 'quick', 'Force the update to be done in a single query' ); + $this->addOption( 'background', 'Force replication-friendly mode; may be inefficient but + avoids locking tables or lagging replica DBs with large updates; + calculates counts on a replica DB if possible. + +Background mode will be automatically used if multiple servers are listed +in the load balancer, usually indicating a replication environment.' ); + $this->addDescription( 'Batch-recalculate user_editcount fields from the revision table' ); + } + + public function execute() { + global $wgActorTableSchemaMigrationStage; + + $dbw = $this->getDB( DB_MASTER ); + + // Autodetect mode... + if ( $this->hasOption( 'background' ) ) { + $backgroundMode = true; + } elseif ( $this->hasOption( 'quick' ) ) { + $backgroundMode = false; + } else { + $backgroundMode = wfGetLB()->getServerCount() > 1; + } + + $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' ); + + $needSpecialQuery = ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD && + $wgActorTableSchemaMigrationStage !== MIGRATION_NEW ); + if ( $needSpecialQuery ) { + foreach ( $actorQuery['joins'] as &$j ) { + $j[0] = 'JOIN'; // replace LEFT JOIN + } + unset( $j ); + } + + if ( $backgroundMode ) { + $this->output( "Using replication-friendly background mode...\n" ); + + $dbr = $this->getDB( DB_REPLICA ); + $chunkSize = 100; + $lastUser = $dbr->selectField( 'user', 'MAX(user_id)', '', __METHOD__ ); + + $start = microtime( true ); + $migrated = 0; + for ( $min = 0; $min <= $lastUser; $min += $chunkSize ) { + $max = $min + $chunkSize; + + if ( $needSpecialQuery ) { + // Use separate subqueries to collect counts with the old + // and new schemas, to avoid having to do whole-table scans. + $result = $dbr->select( + [ + 'user', + 'rev1' => '(' + . $dbr->selectSQLText( + [ 'revision', 'revision_actor_temp' ], + [ 'rev_user', 'ct' => 'COUNT(*)' ], + [ + "rev_user > $min AND rev_user <= $max", + 'revactor_rev' => null, + ], + __METHOD__, + [ 'GROUP BY' => 'rev_user' ], + [ 'revision_actor_temp' => [ 'LEFT JOIN', 'revactor_rev = rev_id' ] ] + ) . ')', + 'rev2' => '(' + . $dbr->selectSQLText( + [ 'revision' ] + $actorQuery['tables'], + [ 'actor_user', 'ct' => 'COUNT(*)' ], + "actor_user > $min AND actor_user <= $max", + __METHOD__, + [ 'GROUP BY' => 'actor_user' ], + $actorQuery['joins'] + ) . ')', + ], + [ 'user_id', 'user_editcount' => 'COALESCE(rev1.ct,0) + COALESCE(rev2.ct,0)' ], + "user_id > $min AND user_id <= $max", + __METHOD__, + [], + [ + 'rev1' => [ 'LEFT JOIN', 'user_id = rev_user' ], + 'rev2' => [ 'LEFT JOIN', 'user_id = actor_user' ], + ] + ); + } else { + $revUser = $actorQuery['fields']['rev_user']; + $result = $dbr->select( + [ 'user', 'rev' => [ 'revision' ] + $actorQuery['tables'] ], + [ 'user_id', 'user_editcount' => "COUNT($revUser)" ], + "user_id > $min AND user_id <= $max", + __METHOD__, + [ 'GROUP BY' => 'user_id' ], + [ 'rev' => [ 'LEFT JOIN', "user_id = $revUser" ] ] + $actorQuery['joins'] + ); + } + + foreach ( $result as $row ) { + $dbw->update( 'user', + [ 'user_editcount' => $row->user_editcount ], + [ 'user_id' => $row->user_id ], + __METHOD__ ); + ++$migrated; + } + + $delta = microtime( true ) - $start; + $rate = ( $delta == 0.0 ) ? 0.0 : $migrated / $delta; + $this->output( sprintf( "%s %d (%0.1f%%) done in %0.1f secs (%0.3f accounts/sec).\n", + wfWikiID(), + $migrated, + min( $max, $lastUser ) / $lastUser * 100.0, + $delta, + $rate ) ); + + wfWaitForSlaves(); + } + } else { + $this->output( "Using single-query mode...\n" ); + + $user = $dbw->tableName( 'user' ); + if ( $needSpecialQuery ) { + $subquery1 = $dbw->selectSQLText( + [ 'revision', 'revision_actor_temp' ], + [ 'COUNT(*)' ], + [ + 'user_id = rev_user', + 'revactor_rev' => null, + ], + __METHOD__, + [], + [ 'revision_actor_temp' => [ 'LEFT JOIN', 'revactor_rev = rev_id' ] ] + ); + $subquery2 = $dbw->selectSQLText( + [ 'revision' ] + $actorQuery['tables'], + [ 'COUNT(*)' ], + 'user_id = actor_user', + __METHOD__, + [], + $actorQuery['joins'] + ); + $dbw->query( + "UPDATE $user SET user_editcount=($subquery1) + ($subquery2)", + __METHOD__ + ); + } else { + $subquery = $dbw->selectSQLText( + [ 'revision' ] + $actorQuery['tables'], + [ 'COUNT(*)' ], + [ 'user_id = ' . $actorQuery['fields']['rev_user'] ], + __METHOD__, + [], + $actorQuery['joins'] + ); + $dbw->query( "UPDATE $user SET user_editcount=($subquery)", __METHOD__ ); + } + } + + $this->output( "Done!\n" ); + } +} + +$maintClass = InitEditCount::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/initSiteStats.php b/www/wiki/maintenance/initSiteStats.php new file mode 100644 index 00000000..297544dc --- /dev/null +++ b/www/wiki/maintenance/initSiteStats.php @@ -0,0 +1,82 @@ + + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to re-initialise or update the site statistics table + * + * @ingroup Maintenance + */ +class InitSiteStats extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addDescription( 'Re-initialise the site statistics tables' ); + $this->addOption( 'update', 'Update the existing statistics' ); + $this->addOption( 'active', 'Also update active users count' ); + $this->addOption( 'use-master', 'Count using the master database' ); + } + + public function execute() { + $this->output( "Refresh Site Statistics\n\n" ); + $counter = new SiteStatsInit( $this->hasOption( 'use-master' ) ); + + $this->output( "Counting total edits..." ); + $edits = $counter->edits(); + $this->output( "{$edits}\nCounting number of articles..." ); + + $good = $counter->articles(); + $this->output( "{$good}\nCounting total pages..." ); + + $pages = $counter->pages(); + $this->output( "{$pages}\nCounting number of users..." ); + + $users = $counter->users(); + $this->output( "{$users}\nCounting number of images..." ); + + $image = $counter->files(); + $this->output( "{$image}\n" ); + + if ( $this->hasOption( 'update' ) ) { + $this->output( "\nUpdating site statistics..." ); + $counter->refresh(); + $this->output( "done.\n" ); + } else { + $this->output( "\nTo update the site statistics table, run the script " + . "with the --update option.\n" ); + } + + if ( $this->hasOption( 'active' ) ) { + $this->output( "\nCounting and updating active users..." ); + $active = SiteStatsUpdate::cacheUpdate( $this->getDB( DB_MASTER ) ); + $this->output( "{$active}\n" ); + } + + $this->output( "\nDone.\n" ); + } +} + +$maintClass = InitSiteStats::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/initUserPreference.php b/www/wiki/maintenance/initUserPreference.php new file mode 100644 index 00000000..1ab880ce --- /dev/null +++ b/www/wiki/maintenance/initUserPreference.php @@ -0,0 +1,84 @@ +addOption( + 'target', + 'Name of the user preference to initialize', + true, + true, + 't' + ); + $this->addOption( + 'source', + 'Name of the user preference to take the value from', + true, + true, + 's' + ); + $this->setBatchSize( 300 ); + } + + public function execute() { + $target = $this->getOption( 'target' ); + $source = $this->getOption( 'source' ); + $this->output( "Initializing '$target' based on the value of '$source'\n" ); + + $dbr = $this->getDB( DB_REPLICA ); + $dbw = $this->getDB( DB_MASTER ); + + $iterator = new BatchRowIterator( + $dbr, + 'user_properties', + [ 'up_user', 'up_property' ], + $this->getBatchSize() + ); + $iterator->setFetchColumns( [ 'up_user', 'up_value' ] ); + $iterator->addConditions( [ + 'up_property' => $source, + 'up_value IS NOT NULL', + 'up_value != 0', + ] ); + + $processed = 0; + foreach ( $iterator as $batch ) { + foreach ( $batch as $row ) { + $values = [ + 'up_user' => $row->up_user, + 'up_property' => $target, + 'up_value' => $row->up_value, + ]; + $dbw->upsert( + 'user_properties', + $values, + [ 'up_user', 'up_property' ], + $values, + __METHOD__ + ); + + $processed += $dbw->affectedRows(); + } + } + + $this->output( "Processed $processed user(s)\n" ); + $this->output( "Finished!\n" ); + } +} + +$maintClass = InitUserPreference::class; // Tells it to run the class +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/install.php b/www/wiki/maintenance/install.php new file mode 100644 index 00000000..438e9dc4 --- /dev/null +++ b/www/wiki/maintenance/install.php @@ -0,0 +1,175 @@ +addDescription( "CLI-based MediaWiki installation and configuration.\n" . + "Default options are indicated in parentheses." ); + + $this->addArg( 'name', 'The name of the wiki (MediaWiki)', false ); + + $this->addArg( 'admin', 'The username of the wiki administrator.' ); + $this->addOption( 'pass', 'The password for the wiki administrator.', false, true ); + $this->addOption( + 'passfile', + 'An alternative way to provide pass option, as the contents of this file', + false, + true + ); + /* $this->addOption( 'email', 'The email for the wiki administrator', false, true ); */ + $this->addOption( + 'scriptpath', + 'The relative path of the wiki in the web server (/wiki)', + false, + true + ); + + $this->addOption( 'lang', 'The language to use (en)', false, true ); + /* $this->addOption( 'cont-lang', 'The content language (en)', false, true ); */ + + $this->addOption( 'dbtype', 'The type of database (mysql)', false, true ); + $this->addOption( 'dbserver', 'The database host (localhost)', false, true ); + $this->addOption( 'dbport', 'The database port; only for PostgreSQL (5432)', false, true ); + $this->addOption( 'dbname', 'The database name (my_wiki)', false, true ); + $this->addOption( 'dbpath', 'The path for the SQLite DB ($IP/data)', false, true ); + $this->addOption( 'dbprefix', 'Optional database table name prefix', false, true ); + $this->addOption( 'installdbuser', 'The user to use for installing (root)', false, true ); + $this->addOption( 'installdbpass', 'The password for the DB user to install as.', false, true ); + $this->addOption( 'dbuser', 'The user to use for normal operations (wikiuser)', false, true ); + $this->addOption( 'dbpass', 'The password for the DB user for normal operations', false, true ); + $this->addOption( + 'dbpassfile', + 'An alternative way to provide dbpass option, as the contents of this file', + false, + true + ); + $this->addOption( 'confpath', "Path to write LocalSettings.php to ($IP)", false, true ); + $this->addOption( 'dbschema', 'The schema for the MediaWiki DB in ' + . 'PostgreSQL/Microsoft SQL Server (mediawiki)', false, true ); + /* + $this->addOption( 'namespace', 'The project namespace (same as the "name" argument)', + false, true ); + */ + $this->addOption( 'env-checks', "Run environment checks only, don't change anything" ); + + $this->addOption( 'with-extensions', "Detect and include extensions" ); + } + + public function getDbType() { + if ( $this->hasOption( 'env-checks' ) ) { + return Maintenance::DB_NONE; + } + return parent::getDbType(); + } + + function execute() { + global $IP; + + $siteName = $this->getArg( 0, 'MediaWiki' ); // Will not be set if used with --env-checks + $adminName = $this->getArg( 1 ); + $envChecksOnly = $this->hasOption( 'env-checks' ); + + $this->setDbPassOption(); + if ( !$envChecksOnly ) { + $this->setPassOption(); + } + + $installer = InstallerOverrides::getCliInstaller( $siteName, $adminName, $this->mOptions ); + + $status = $installer->doEnvironmentChecks(); + if ( $status->isGood() ) { + $installer->showMessage( 'config-env-good' ); + } else { + $installer->showStatusMessage( $status ); + + return; + } + if ( !$envChecksOnly ) { + $installer->execute(); + $installer->writeConfigurationFile( $this->getOption( 'confpath', $IP ) ); + } + } + + private function setDbPassOption() { + $dbpassfile = $this->getOption( 'dbpassfile' ); + if ( $dbpassfile !== null ) { + if ( $this->getOption( 'dbpass' ) !== null ) { + $this->error( 'WARNING: You have provided the options "dbpass" and "dbpassfile". ' + . 'The content of "dbpassfile" overrides "dbpass".' ); + } + Wikimedia\suppressWarnings(); + $dbpass = file_get_contents( $dbpassfile ); // returns false on failure + Wikimedia\restoreWarnings(); + if ( $dbpass === false ) { + $this->fatalError( "Couldn't open $dbpassfile" ); + } + $this->mOptions['dbpass'] = trim( $dbpass, "\r\n" ); + } + } + + private function setPassOption() { + $passfile = $this->getOption( 'passfile' ); + if ( $passfile !== null ) { + if ( $this->getOption( 'pass' ) !== null ) { + $this->error( 'WARNING: You have provided the options "pass" and "passfile". ' + . 'The content of "passfile" overrides "pass".' ); + } + Wikimedia\suppressWarnings(); + $pass = file_get_contents( $passfile ); // returns false on failure + Wikimedia\restoreWarnings(); + if ( $pass === false ) { + $this->fatalError( "Couldn't open $passfile" ); + } + $this->mOptions['pass'] = trim( $pass, "\r\n" ); + } elseif ( $this->getOption( 'pass' ) === null ) { + $this->fatalError( 'You need to provide the option "pass" or "passfile"' ); + } + } + + function validateParamsAndArgs() { + if ( !$this->hasOption( 'env-checks' ) ) { + parent::validateParamsAndArgs(); + } + } +} + +$maintClass = CommandLineInstaller::class; + +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/interwiki.list b/www/wiki/maintenance/interwiki.list new file mode 100644 index 00000000..5f87e16c --- /dev/null +++ b/www/wiki/maintenance/interwiki.list @@ -0,0 +1,68 @@ +# Based more or less on the public interwiki map from MeatballWiki +# Default interwiki prefixes... +acronym|https://www.acronymfinder.com/~/search/af.aspx?string=exact&Acronym=$1|0| +advogato|http://www.advogato.org/$1|0| +arxiv|https://www.arxiv.org/abs/$1|0| +c2find|http://c2.com/cgi/wiki?FindPage&value=$1|0| +cache|https://www.google.com/search?q=cache:$1|0| +commons|https://commons.wikimedia.org/wiki/$1|0|https://commons.wikimedia.org/w/api.php +dictionary|http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1|0| +doi|https://dx.doi.org/$1|0| +drumcorpswiki|http://www.drumcorpswiki.com/$1|0|http://drumcorpswiki.com/api.php +dwjwiki|http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1|0| +elibre|http://enciclopedia.us.es/index.php/$1|0|http://enciclopedia.us.es/api.php +emacswiki|https://www.emacswiki.org/cgi-bin/wiki.pl?$1|0| +foldoc|https://foldoc.org/?$1|0| +foxwiki|https://fox.wikis.com/wc.dll?Wiki~$1|0| +freebsdman|https://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1|0| +gentoo-wiki|http://gentoo-wiki.com/$1|0| +google|https://www.google.com/search?q=$1|0| +googlegroups|https://groups.google.com/groups?q=$1|0| +hammondwiki|http://www.dairiki.org/HammondWiki/$1|0| +hrwiki|http://www.hrwiki.org/wiki/$1|0|http://www.hrwiki.org/w/api.php +imdb|http://www.imdb.com/find?q=$1&tt=on|0| +kmwiki|https://kmwiki.wikispaces.com/$1|0| +linuxwiki|http://linuxwiki.de/$1|0| +lojban|https://mw.lojban.org/papri/$1|0| +lqwiki|http://wiki.linuxquestions.org/wiki/$1|0| +meatball|http://www.usemod.com/cgi-bin/mb.pl?$1|0| +mediawikiwiki|https://www.mediawiki.org/wiki/$1|0|https://www.mediawiki.org/w/api.php +memoryalpha|http://en.memory-alpha.org/wiki/$1|0|http://en.memory-alpha.org/api.php +metawiki|http://sunir.org/apps/meta.pl?$1|0| +metawikimedia|https://meta.wikimedia.org/wiki/$1|0|https://meta.wikimedia.org/w/api.php +mozillawiki|https://wiki.mozilla.org/$1|0|https://wiki.mozilla.org/api.php +mw|https://www.mediawiki.org/wiki/$1|0|https://www.mediawiki.org/w/api.php +oeis|https://oeis.org/$1|0| +openwiki|http://openwiki.com/ow.asp?$1|0| +pmid|https://www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract|0| +pythoninfo|https://wiki.python.org/moin/$1|0| +rfc|https://tools.ietf.org/html/rfc$1|0| +s23wiki|http://s23.org/wiki/$1|0|http://s23.org/w/api.php +seattlewireless|http://seattlewireless.net/$1|0| +senseislibrary|https://senseis.xmp.net/?$1|0| +shoutwiki|http://www.shoutwiki.com/wiki/$1|0|http://www.shoutwiki.com/w/api.php +squeak|http://wiki.squeak.org/squeak/$1|0| +tmbw|http://www.tmbw.net/wiki/$1|0|http://tmbw.net/wiki/api.php +tmnet|http://www.technomanifestos.net/?$1|0| +theopedia|https://www.theopedia.com/$1|0| +twiki|http://twiki.org/cgi-bin/view/$1|0| +uncyclopedia|https://en.uncyclopedia.co/wiki/$1|0|https://en.uncyclopedia.co/w/api.php +unreal|https://wiki.beyondunreal.com/$1|0|https://wiki.beyondunreal.com/w/api.php +usemod|http://www.usemod.com/cgi-bin/wiki.pl?$1|0| +wiki|http://c2.com/cgi/wiki?$1|0| +wikia|http://www.wikia.com/wiki/$1|0| +wikibooks|https://en.wikibooks.org/wiki/$1|0|https://en.wikibooks.org/w/api.php +wikidata|https://www.wikidata.org/wiki/$1|0|https://www.wikidata.org/w/api.php +wikif1|http://www.wikif1.org/$1|0| +wikihow|https://www.wikihow.com/$1|0|https://www.wikihow.com/api.php +wikinfo|http://wikinfo.co/English/index.php/$1|0| +wikimedia|https://wikimediafoundation.org/wiki/$1|0|https://wikimediafoundation.org/w/api.php +wikinews|https://en.wikinews.org/wiki/$1|0|https://en.wikinews.org/w/api.php +wikipedia|https://en.wikipedia.org/wiki/$1|0|https://en.wikipedia.org/w/api.php +wikiquote|https://en.wikiquote.org/wiki/$1|0|https://en.wikiquote.org/w/api.php +wikisource|https://wikisource.org/wiki/$1|0|https://wikisource.org/w/api.php +wikispecies|https://species.wikimedia.org/wiki/$1|0|https://species.wikimedia.org/w/api.php +wikiversity|https://en.wikiversity.org/wiki/$1|0|https://en.wikiversity.org/w/api.php +wikivoyage|https://en.wikivoyage.org/wiki/$1|0|https://en.wikivoyage.org/w/api.php +wikt|https://en.wiktionary.org/wiki/$1|0|https://en.wiktionary.org/w/api.php +wiktionary|https://en.wiktionary.org/wiki/$1|0|https://en.wiktionary.org/w/api.php diff --git a/www/wiki/maintenance/interwiki.sql b/www/wiki/maintenance/interwiki.sql new file mode 100644 index 00000000..9e6072b7 --- /dev/null +++ b/www/wiki/maintenance/interwiki.sql @@ -0,0 +1,71 @@ +-- Based more or less on the public interwiki map from MeatballWiki +-- Default interwiki prefixes... + +REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local,iw_api) VALUES +('acronym','https://www.acronymfinder.com/~/search/af.aspx?string=exact&Acronym=$1',0,''), +('advogato','http://www.advogato.org/$1',0,''), +('arxiv','https://www.arxiv.org/abs/$1',0,''), +('c2find','http://c2.com/cgi/wiki?FindPage&value=$1',0,''), +('cache','https://www.google.com/search?q=cache:$1',0,''), +('commons','https://commons.wikimedia.org/wiki/$1',0,'https://commons.wikimedia.org/w/api.php'), +('dictionary','http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1',0,''), +('doi','https://dx.doi.org/$1',0,''), +('drumcorpswiki','http://www.drumcorpswiki.com/$1',0,'http://drumcorpswiki.com/api.php'), +('dwjwiki','http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1',0,''), +('elibre','http://enciclopedia.us.es/index.php/$1',0,'http://enciclopedia.us.es/api.php'), +('emacswiki','https://www.emacswiki.org/cgi-bin/wiki.pl?$1',0,''), +('foldoc','https://foldoc.org/?$1',0,''), +('foxwiki','https://fox.wikis.com/wc.dll?Wiki~$1',0,''), +('freebsdman','https://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1',0,''), +('gentoo-wiki','http://gentoo-wiki.com/$1',0,''), +('google','https://www.google.com/search?q=$1',0,''), +('googlegroups','https://groups.google.com/groups?q=$1',0,''), +('hammondwiki','http://www.dairiki.org/HammondWiki/$1',0,''), +('hrwiki','http://www.hrwiki.org/wiki/$1',0,'http://www.hrwiki.org/w/api.php'), +('imdb','http://www.imdb.com/find?q=$1&tt=on',0,''), +('kmwiki','https://kmwiki.wikispaces.com/$1',0,''), +('linuxwiki','http://linuxwiki.de/$1',0,''), +('lojban','https://www.lojban.org/tiki/tiki-index.php?page=$1',0,''), +('lqwiki','http://wiki.linuxquestions.org/wiki/$1',0,''), +('meatball','http://www.usemod.com/cgi-bin/mb.pl?$1',0,''), +('mediawikiwiki','https://www.mediawiki.org/wiki/$1',0,'https://www.mediawiki.org/w/api.php'), +('memoryalpha','http://en.memory-alpha.org/wiki/$1',0,'http://en.memory-alpha.org/api.php'), +('metawiki','http://sunir.org/apps/meta.pl?$1',0,''), +('metawikimedia','https://meta.wikimedia.org/wiki/$1',0,'https://meta.wikimedia.org/w/api.php'), +('mozillawiki','https://wiki.mozilla.org/$1',0,'https://wiki.mozilla.org/api.php'), +('mw','https://www.mediawiki.org/wiki/$1',0,'https://www.mediawiki.org/w/api.php'), +('oeis','https://oeis.org/$1',0,''), +('openwiki','http://openwiki.com/ow.asp?$1',0,''), +('pmid', 'https://www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract',0,''), +('pythoninfo','https://wiki.python.org/moin/$1',0,''), +('rfc','https://tools.ietf.org/html/rfc$1',0,''), +('s23wiki','http://s23.org/wiki/$1',0,'http://s23.org/w/api.php'), +('seattlewireless','http://seattlewireless.net/$1',0,''), +('senseislibrary','https://senseis.xmp.net/?$1',0,''), +('shoutwiki','http://www.shoutwiki.com/wiki/$1',0,'http://www.shoutwiki.com/w/api.php'), +('squeak','http://wiki.squeak.org/squeak/$1',0,''), +('tmbw','http://www.tmbw.net/wiki/$1',0,'http://tmbw.net/wiki/api.php'), +('tmnet','http://www.technomanifestos.net/?$1',0,''), +('theopedia','https://www.theopedia.com/$1',0,''), +('twiki','http://twiki.org/cgi-bin/view/$1',0,''), +('uncyclopedia','https://en.uncyclopedia.co/wiki/$1',0,'https://en.uncyclopedia.co/w/api.php'), +('unreal','https://wiki.beyondunreal.com/$1',0,'https://wiki.beyondunreal.com/w/api.php'), +('usemod','http://www.usemod.com/cgi-bin/wiki.pl?$1',0,''), +('wiki','http://c2.com/cgi/wiki?$1',0,''), +('wikia','http://www.wikia.com/wiki/$1',0,''), +('wikibooks','https://en.wikibooks.org/wiki/$1',0,'https://en.wikibooks.org/w/api.php'), +('wikidata','https://www.wikidata.org/wiki/$1',0,'https://www.wikidata.org/w/api.php'), +('wikif1','http://www.wikif1.org/$1',0,''), +('wikihow','https://www.wikihow.com/$1',0,'https://www.wikihow.com/api.php'), +('wikinfo','http://wikinfo.co/English/index.php/$1',0,''), +('wikimedia','https://wikimediafoundation.org/wiki/$1',0,'https://wikimediafoundation.org/w/api.php'), +('wikinews','https://en.wikinews.org/wiki/$1',0,'https://en.wikinews.org/w/api.php'), +('wikipedia','https://en.wikipedia.org/wiki/$1',0,'https://en.wikipedia.org/w/api.php'), +('wikiquote','https://en.wikiquote.org/wiki/$1',0,'https://en.wikiquote.org/w/api.php'), +('wikisource','https://wikisource.org/wiki/$1',0,'https://wikisource.org/w/api.php'), +('wikispecies','https://species.wikimedia.org/wiki/$1',0,'https://species.wikimedia.org/w/api.php'), +('wikiversity','https://en.wikiversity.org/wiki/$1',0,'https://en.wikiversity.org/w/api.php'), +('wikivoyage','https://en.wikivoyage.org/wiki/$1',0,'https://en.wikivoyage.org/w/api.php'), +('wikt','https://en.wiktionary.org/wiki/$1',0,'https://en.wiktionary.org/w/api.php'), +('wiktionary','https://en.wiktionary.org/wiki/$1',0,'https://en.wiktionary.org/w/api.php') +; diff --git a/www/wiki/maintenance/invalidateUserSessions.php b/www/wiki/maintenance/invalidateUserSessions.php new file mode 100644 index 00000000..8d877a52 --- /dev/null +++ b/www/wiki/maintenance/invalidateUserSessions.php @@ -0,0 +1,94 @@ +addDescription( + 'Invalidate the sessions of certain users on the wiki.' + ); + $this->addOption( 'user', 'Username', false, true, 'u' ); + $this->addOption( 'file', 'File with one username per line', false, true, 'f' ); + $this->setBatchSize( 1000 ); + } + + public function execute() { + $username = $this->getOption( 'user' ); + $file = $this->getOption( 'file' ); + + if ( $username === null && $file === null ) { + $this->fatalError( 'Either --user or --file is required' ); + } elseif ( $username !== null && $file !== null ) { + $this->fatalError( 'Cannot use both --user and --file' ); + } + + if ( $username !== null ) { + $usernames = [ $username ]; + } else { + $usernames = is_readable( $file ) ? + file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ) : false; + if ( $usernames === false ) { + $this->fatalError( "Could not open $file", 2 ); + } + } + + $i = 0; + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $sessionManager = SessionManager::singleton(); + foreach ( $usernames as $username ) { + $i++; + $user = User::newFromName( $username ); + try { + $sessionManager->invalidateSessionsForUser( $user ); + if ( $user->getId() ) { + $this->output( "Invalidated sessions for user $username\n" ); + } else { + # session invalidation might still work if there is a central identity provider + $this->output( "Could not find user $username, tried to invalidate anyway\n" ); + } + } catch ( Exception $e ) { + $this->output( "Failed to invalidate sessions for user $username | " + . str_replace( [ "\r", "\n" ], ' ', $e->getMessage() ) . "\n" ); + } + + if ( $i % $this->getBatchSize() ) { + $lbFactory->waitForReplication(); + } + } + } +} + +$maintClass = InvalidateUserSesssions::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/jsduck/categories.json b/www/wiki/maintenance/jsduck/categories.json new file mode 100644 index 00000000..bebee85f --- /dev/null +++ b/www/wiki/maintenance/jsduck/categories.json @@ -0,0 +1,136 @@ +[ + { + "name": "MediaWiki", + "groups": [ + { + "name": "Base", + "classes": [ + "mw", + "mw.Message", + "mw.loader", + "mw.loader.store", + "mw.html", + "mw.html.Cdata", + "mw.html.Raw", + "mw.hook", + "mw.template", + "mw.errorLogger" + ] + }, + { + "name": "General", + "classes": [ + "mw.Title", + "mw.Uri", + "mw.RegExp", + "mw.String", + "mw.messagePoster.*", + "mw.notification", + "mw.Notification_", + "mw.storage", + "mw.storage.session", + "mw.user", + "mw.util", + "mw.plugin.*", + "mw.cookie", + "mw.experiments", + "mw.viewport", + "mw.htmlform.*", + "mw.visibleTimeout" + ] + }, + { + "name": "Actions", + "classes": ["mw.toolbar"] + }, + { + "name": "API", + "classes": ["mw.Api*", "mw.ForeignApi*"] + }, + { + "name": "Language", + "classes": [ + "mw.language*", + "mw.cldr", + "mw.jqueryMsg" + ] + }, + { + "name": "Interfaces", + "classes": [ + "mw.Feedback*", + "mw.Upload*", + "mw.ForeignUpload", + "mw.ForeignStructuredUpload*", + "mw.GallerySlideshow", + "mw.rcfilters*" + ] + }, + { + "name": "Widgets", + "classes": [ + "mw.widgets*" + ] + }, + { + "name": "Special", + "classes": [ + "mw.special*" + ] + }, + { + "name": "Development", + "classes": [ + "mw.log", + "mw.inspect", + "mw.inspect.reports", + "mw.Debug" + ] + } + ] + }, + { + "name": "jQuery", + "groups": [ + { + "name": "Plugins", + "classes": [ + "jQuery.client", + "jQuery.colorUtil", + "jQuery.plugin.*" + ] + } + ] + }, + { + "name": "Upstream", + "groups": [ + { + "name": "OOjs", + "classes": [ + "OO", + "OO.EmitterList", + "OO.EventEmitter", + "OO.Factory", + "OO.Registry", + "OO.SortedEmitterList" + ] + }, + { + "name": "OOUI", + "classes": [ + "OO.ui", + "OO.ui.*" + ] + }, + { + "name": "jQuery", + "classes": ["jQuery", "jQuery.Event", "jQuery.Callbacks", "jQuery.Promise", "jQuery.Deferred", "jQuery.jqXHR", "QUnit"] + }, + { + "name": "JavaScript", + "classes": ["Array", "Boolean", "Date", "Function", "Number", "Object", "RegExp", "String"] + } + ] + } +] diff --git a/www/wiki/maintenance/jsduck/custom_tags.rb b/www/wiki/maintenance/jsduck/custom_tags.rb new file mode 100644 index 00000000..21cb658d --- /dev/null +++ b/www/wiki/maintenance/jsduck/custom_tags.rb @@ -0,0 +1,102 @@ +# Custom tags for JSDuck 5.x +# See also: +# - https://github.com/senchalabs/jsduck/wiki/Custom-tags +# - https://github.com/senchalabs/jsduck/wiki/Custom-tags/7f5c32e568eab9edc8e3365e935bcb836cb11f1d +require 'jsduck/tag/tag' + +class CommonTag < JsDuck::Tag::Tag + def initialize + @html_position = POS_DOC + 0.1 + @repeatable = true + end + + def parse_doc(scanner, _position) + if @multiline + return { tagname: @tagname, doc: :multiline } + else + text = scanner.match(/.*$/) + return { tagname: @tagname, doc: text } + end + end + + def process_doc(context, tags, _position) + context[@tagname] = tags + end + + def format(context, formatter) + context[@tagname].each do |tag| + tag[:doc] = formatter.format(tag[:doc]) + end + end +end + +class SeeTag < CommonTag + def initialize + @tagname = :see + @pattern = 'see' + super + end + + def format(context, formatter) + position = context[:files][0] + context[@tagname].each do |tag| + tag[:doc] = '
  • ' + render_long_see(tag[:doc], formatter, position) + '
  • ' + end + end + + def to_html(context) + <<-EOHTML +

    Related

    +
      + #{context[@tagname].map { |tag| tag[:doc] }.join("\n")} +
    + EOHTML + end + + def render_long_see(tag, formatter, position) + match = /\A([^\s]+)( .*)?\Z/m.match(tag) + + if match + name = match[1] + doc = match[2] ? ': ' + match[2] : '' + return formatter.format("{@link #{name}} #{doc}") + else + JsDuck::Logger.warn(nil, 'Unexpected @see argument: "' + tag + '"', position) + return tag + end + end +end + +class ContextTag < CommonTag + def initialize + @tagname = :context + @pattern = 'context' + super + end + + def format(context, formatter) + position = context[:files][0] + context[@tagname].each do |tag| + tag[:doc] = render_long_context(tag[:doc], formatter, position) + end + end + + def to_html(context) + <<-EOHTML +

    Context

    + #{context[@tagname].last[:doc]} + EOHTML + end + + def render_long_context(tag, formatter, position) + match = /\A([^\s]+)/m.match(tag) + + if match + name = match[1] + return formatter.format("`context` : {@link #{name}}") + else + JsDuck::Logger.warn(nil, 'Unexpected @context argument: "' + tag + '"', position) + return tag + end + end +end diff --git a/www/wiki/maintenance/jsduck/eg-iframe.html b/www/wiki/maintenance/jsduck/eg-iframe.html new file mode 100644 index 00000000..91e0bc12 --- /dev/null +++ b/www/wiki/maintenance/jsduck/eg-iframe.html @@ -0,0 +1,117 @@ + + + + + MediaWiki Code Example + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/wiki/maintenance/jsduck/external.js b/www/wiki/maintenance/jsduck/external.js new file mode 100644 index 00000000..85b1f921 --- /dev/null +++ b/www/wiki/maintenance/jsduck/external.js @@ -0,0 +1,43 @@ +/** + * Source: + * @class jQuery + */ + +/** + * Source: + * @method ajax + * @static + * @return {jqXHR} + */ + +/** + * Source: + * @class jQuery.Event + */ + +/** + * Source: + * @class jQuery.Callbacks + */ + +/** + * Source: + * @class jQuery.Promise + */ + +/** + * Source: + * @class jQuery.Deferred + * @mixins jQuery.Promise + */ + +/** + * Source: + * @class jQuery.jqXHR + * @alternateClassName jqXHR + */ + +/** + * Source: + * @class QUnit + */ diff --git a/www/wiki/maintenance/jsparse.php b/www/wiki/maintenance/jsparse.php new file mode 100644 index 00000000..661ec986 --- /dev/null +++ b/www/wiki/maintenance/jsparse.php @@ -0,0 +1,77 @@ +addDescription( 'Runs parsing/syntax checks on JavaScript files' ); + $this->addArg( 'file(s)', 'JavaScript file to test', false ); + } + + public function execute() { + if ( $this->hasArg() ) { + $files = $this->mArgs; + } else { + $this->maybeHelp( true ); // @todo fixme this is a lame API :) + exit( 1 ); // it should exit from the above first... + } + + $parser = new JSParser(); + foreach ( $files as $filename ) { + Wikimedia\suppressWarnings(); + $js = file_get_contents( $filename ); + Wikimedia\restoreWarnings(); + if ( $js === false ) { + $this->output( "$filename ERROR: could not read file\n" ); + $this->errs++; + continue; + } + + try { + $parser->parse( $js, $filename, 1 ); + } catch ( Exception $e ) { + $this->errs++; + $this->output( "$filename ERROR: " . $e->getMessage() . "\n" ); + continue; + } + + $this->output( "$filename OK\n" ); + } + + if ( $this->errs > 0 ) { + exit( 1 ); + } + } +} + +$maintClass = JSParseHelper::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/lag.php b/www/wiki/maintenance/lag.php new file mode 100644 index 00000000..f041e15c --- /dev/null +++ b/www/wiki/maintenance/lag.php @@ -0,0 +1,72 @@ +addDescription( 'Shows database lag' ); + $this->addOption( 'r', "Don't exit immediately, but show the lag every 5 seconds" ); + } + + public function execute() { + if ( $this->hasOption( 'r' ) ) { + $lb = wfGetLB(); + echo 'time '; + + $serverCount = $lb->getServerCount(); + for ( $i = 1; $i < $serverCount; $i++ ) { + $hostname = $lb->getServerName( $i ); + printf( "%-12s ", $hostname ); + } + echo "\n"; + + while ( 1 ) { + $lags = $lb->getLagTimes(); + unset( $lags[0] ); + echo gmdate( 'H:i:s' ) . ' '; + foreach ( $lags as $lag ) { + printf( "%-12s ", $lag === false ? 'false' : $lag ); + } + echo "\n"; + sleep( 5 ); + } + } else { + $lb = wfGetLB(); + $lags = $lb->getLagTimes(); + foreach ( $lags as $i => $lag ) { + $name = $lb->getServerName( $i ); + $this->output( sprintf( "%-20s %s\n", $name, $lag === false ? 'false' : $lag ) ); + } + } + } +} + +$maintClass = DatabaseLag::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/language/StatOutputs.php b/www/wiki/maintenance/language/StatOutputs.php new file mode 100644 index 00000000..723ea62c --- /dev/null +++ b/www/wiki/maintenance/language/StatOutputs.php @@ -0,0 +1,146 @@ + + * @author Antoine Musso + */ + +/** A general output object. Need to be overridden */ +class StatsOutput { + function formatPercent( $subset, $total, $revert = false, $accuracy = 2 ) { + Wikimedia\suppressWarnings(); + $return = sprintf( '%.' . $accuracy . 'f%%', 100 * $subset / $total ); + Wikimedia\restoreWarnings(); + + return $return; + } + + # Override the following methods + function heading() { + } + + function footer() { + } + + function blockstart() { + } + + function blockend() { + } + + function element( $in, $heading = false ) { + } +} + +/** Outputs WikiText */ +class WikiStatsOutput extends StatsOutput { + function heading() { + global $wgDummyLanguageCodes; + $version = SpecialVersion::getVersion( 'nodb' ); + echo "'''Statistics are based on:''' " . $version . "\n\n"; + echo "'''Note:''' These statistics can be generated by running " . + "php maintenance/language/transstat.php.\n\n"; + echo "For additional information on specific languages (the message names, the actual " . + "problems, etc.), run php maintenance/language/checkLanguage.php --lang=foo.\n\n"; + echo 'English (en) is excluded because it is the default localization'; + if ( is_array( $wgDummyLanguageCodes ) ) { + $dummyCodes = []; + foreach ( $wgDummyLanguageCodes as $dummyCode => $correctCode ) { + $dummyCodes[] = Language::fetchLanguageName( $dummyCode ) . ' (' . $dummyCode . ')'; + } + echo ', as well as the following languages that are not intended for ' . + 'system message translations, usually because they redirect to other ' . + 'language codes: ' . implode( ', ', $dummyCodes ); + } + echo ".\n\n"; # dot to end sentence + echo '{| class="sortable wikitable" border="2" style="background-color: #F9F9F9; ' . + 'border: 1px #AAAAAA solid; border-collapse: collapse; clear:both; width:100%;"' . "\n"; + } + + function footer() { + echo "|}\n"; + } + + function blockstart() { + echo "|-\n"; + } + + function blockend() { + echo ''; + } + + function element( $in, $heading = false ) { + echo ( $heading ? '!' : '|' ) . "$in\n"; + } + + function formatPercent( $subset, $total, $revert = false, $accuracy = 2 ) { + Wikimedia\suppressWarnings(); + $v = round( 255 * $subset / $total ); + Wikimedia\restoreWarnings(); + + if ( $revert ) { + # Weigh reverse with factor 20 so coloring takes effect more quickly as + # this option is used solely for reporting 'bad' percentages. + $v = $v * 20; + if ( $v > 255 ) { + $v = 255; + } + $v = 255 - $v; + } + if ( $v < 128 ) { + # Red to Yellow + $red = 'FF'; + $green = sprintf( '%02X', 2 * $v ); + } else { + # Yellow to Green + $red = sprintf( '%02X', 2 * ( 255 - $v ) ); + $green = 'FF'; + } + $blue = '00'; + $color = $red . $green . $blue; + + $percent = parent::formatPercent( $subset, $total, $revert, $accuracy ); + + return 'style="background-color:#' . $color . ';"|' . $percent; + } +} + +/** Output text. To be used on a terminal for example. */ +class TextStatsOutput extends StatsOutput { + function element( $in, $heading = false ) { + echo $in . "\t"; + } + + function blockend() { + echo "\n"; + } +} + +/** csv output. Some people love excel */ +class CsvStatsOutput extends StatsOutput { + function element( $in, $heading = false ) { + echo $in . ";"; + } + + function blockend() { + echo "\n"; + } +} diff --git a/www/wiki/maintenance/language/alltrans.php b/www/wiki/maintenance/language/alltrans.php new file mode 100644 index 00000000..684f4d2e --- /dev/null +++ b/www/wiki/maintenance/language/alltrans.php @@ -0,0 +1,47 @@ +addDescription( 'Get all messages as defined by the English language file' ); + } + + public function execute() { + $englishMessages = array_keys( Language::getMessagesFor( 'en' ) ); + foreach ( $englishMessages as $key ) { + $this->output( "$key\n" ); + } + } +} + +$maintClass = AllTrans::class; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/www/wiki/maintenance/language/checkDupeMessages.php b/www/wiki/maintenance/language/checkDupeMessages.php new file mode 100644 index 00000000..92ddc449 --- /dev/null +++ b/www/wiki/maintenance/language/checkDupeMessages.php @@ -0,0 +1,137 @@ + $value ) { + foreach ( $wgMessages[$langCode] as $ckey => $cvalue ) { + if ( !strcmp( $key, $ckey ) ) { + if ( ( !strcmp( $key, $ckey ) ) && ( !strcmp( $value, $cvalue ) ) ) { + if ( !strcmp( $runMode, 'raw' ) ) { + print "$key\n"; + } elseif ( !strcmp( $runMode, 'php' ) ) { + print "'$key' => '',\n"; + } elseif ( !strcmp( $runMode, 'wiki' ) ) { + $uKey = ucfirst( $key ); + print "* MediaWiki:$uKey/$langCode\n"; + } else { + print "* $key\n"; + } + $count++; + } + } + } + } + if ( !strcmp( $runMode, 'php' ) ) { + print "];\n"; + } + if ( !strcmp( $runMode, 'text' ) ) { + if ( $count == 1 ) { + echo "\nThere are $count duplicated message in $langCode, against to $langCodeC.\n"; + } else { + echo "\nThere are $count duplicated messages in $langCode, against to $langCodeC.\n"; + } + } + } else { + if ( !$messageExist ) { + echo "There are no messages defined in $langCode.\n"; + } + if ( !$messageCExist ) { + echo "There are no messages defined in $langCodeC.\n"; + } + } +} diff --git a/www/wiki/maintenance/language/checkExtensions.php b/www/wiki/maintenance/language/checkExtensions.php new file mode 100644 index 00000000..79a4dd98 --- /dev/null +++ b/www/wiki/maintenance/language/checkExtensions.php @@ -0,0 +1,40 @@ +execute(); diff --git a/www/wiki/maintenance/language/checkLanguage.inc b/www/wiki/maintenance/language/checkLanguage.inc new file mode 100644 index 00000000..007ced15 --- /dev/null +++ b/www/wiki/maintenance/language/checkLanguage.inc @@ -0,0 +1,781 @@ +help(); + exit( 1 ); + } + + if ( isset( $options['lang'] ) ) { + $this->code = $options['lang']; + } else { + global $wgLanguageCode; + $this->code = $wgLanguageCode; + } + + if ( isset( $options['level'] ) ) { + $this->level = $options['level']; + } + + $this->doLinks = isset( $options['links'] ); + $this->includeExif = !isset( $options['noexif'] ); + $this->checkAll = isset( $options['all'] ); + + if ( isset( $options['prefix'] ) ) { + $this->linksPrefix = $options['prefix']; + } + + if ( isset( $options['wikilang'] ) ) { + $this->wikiCode = $options['wikilang']; + } + + if ( isset( $options['whitelist'] ) ) { + $this->checks = explode( ',', $options['whitelist'] ); + } elseif ( isset( $options['blacklist'] ) ) { + $this->checks = array_diff( + isset( $options['easy'] ) ? $this->easyChecks() : $this->defaultChecks(), + explode( ',', $options['blacklist'] ) + ); + } elseif ( isset( $options['easy'] ) ) { + $this->checks = $this->easyChecks(); + } else { + $this->checks = $this->defaultChecks(); + } + + if ( isset( $options['output'] ) ) { + $this->output = $options['output']; + } + + $this->L = new Languages( $this->includeExif ); + } + + /** + * Get the default checks. + * @return array A list of the default checks. + */ + protected function defaultChecks() { + return [ + 'untranslated', 'duplicate', 'obsolete', 'variables', 'empty', 'plural', + 'whitespace', 'xhtml', 'chars', 'links', 'unbalanced', 'namespace', + 'projecttalk', 'magic', 'magic-old', 'magic-over', 'magic-case', + 'special', 'special-old', + ]; + } + + /** + * Get the checks which check other things than messages. + * @return array A list of the non-message checks. + */ + protected function nonMessageChecks() { + return [ + 'namespace', 'projecttalk', 'magic', 'magic-old', 'magic-over', + 'magic-case', 'special', 'special-old', + ]; + } + + /** + * Get the checks that can easily be treated by non-speakers of the language. + * @return array A list of the easy checks. + */ + protected function easyChecks() { + return [ + 'duplicate', 'obsolete', 'empty', 'whitespace', 'xhtml', 'chars', 'magic-old', + 'magic-over', 'magic-case', 'special-old', + ]; + } + + /** + * Get all checks. + * @return array An array of all check names mapped to their function names. + */ + protected function getChecks() { + return [ + 'untranslated' => 'getUntranslatedMessages', + 'duplicate' => 'getDuplicateMessages', + 'obsolete' => 'getObsoleteMessages', + 'variables' => 'getMessagesWithMismatchVariables', + 'plural' => 'getMessagesWithoutPlural', + 'empty' => 'getEmptyMessages', + 'whitespace' => 'getMessagesWithWhitespace', + 'xhtml' => 'getNonXHTMLMessages', + 'chars' => 'getMessagesWithWrongChars', + 'links' => 'getMessagesWithDubiousLinks', + 'unbalanced' => 'getMessagesWithUnbalanced', + 'namespace' => 'getUntranslatedNamespaces', + 'projecttalk' => 'getProblematicProjectTalks', + 'magic' => 'getUntranslatedMagicWords', + 'magic-old' => 'getObsoleteMagicWords', + 'magic-over' => 'getOverridingMagicWords', + 'magic-case' => 'getCaseMismatchMagicWords', + 'special' => 'getUntraslatedSpecialPages', + 'special-old' => 'getObsoleteSpecialPages', + ]; + } + + /** + * Get total count for each check non-messages check. + * @return array An array of all check names mapped to a two-element array: + * function name to get the total count and language code or null + * for checked code. + */ + protected function getTotalCount() { + return [ + 'namespace' => [ 'getNamespaceNames', 'en' ], + 'projecttalk' => null, + 'magic' => [ 'getMagicWords', 'en' ], + 'magic-old' => [ 'getMagicWords', null ], + 'magic-over' => [ 'getMagicWords', null ], + 'magic-case' => [ 'getMagicWords', null ], + 'special' => [ 'getSpecialPageAliases', 'en' ], + 'special-old' => [ 'getSpecialPageAliases', null ], + ]; + } + + /** + * Get all check descriptions. + * @return array An array of all check names mapped to their descriptions. + */ + protected function getDescriptions() { + return [ + 'untranslated' => '$1 message(s) of $2 are not translated to $3, but exist in en:', + 'duplicate' => '$1 message(s) of $2 are translated the same in en and $3:', + 'obsolete' => + '$1 message(s) of $2 do not exist in en or are in the ignore list, but exist in $3:', + 'variables' => '$1 message(s) of $2 in $3 don\'t match the variables used in en:', + 'plural' => '$1 message(s) of $2 in $3 don\'t use {{plural}} while en uses:', + 'empty' => '$1 message(s) of $2 in $3 are empty or -:', + 'whitespace' => '$1 message(s) of $2 in $3 have trailing whitespace:', + 'xhtml' => '$1 message(s) of $2 in $3 contain illegal XHTML:', + 'chars' => + '$1 message(s) of $2 in $3 include hidden chars which should not be used in the messages:', + 'links' => '$1 message(s) of $2 in $3 have problematic link(s):', + 'unbalanced' => '$1 message(s) of $2 in $3 have unbalanced {[]}:', + 'namespace' => '$1 namespace name(s) of $2 are not translated to $3, but exist in en:', + 'projecttalk' => + '$1 namespace name(s) and alias(es) in $3 are project talk namespaces without the parameter:', + 'magic' => '$1 magic word(s) of $2 are not translated to $3, but exist in en:', + 'magic-old' => '$1 magic word(s) of $2 do not exist in en, but exist in $3:', + 'magic-over' => '$1 magic word(s) of $2 in $3 do not contain the original en word(s):', + 'magic-case' => + '$1 magic word(s) of $2 in $3 change the case-sensitivity of the original en word:', + 'special' => '$1 special page alias(es) of $2 are not translated to $3, but exist in en:', + 'special-old' => '$1 special page alias(es) of $2 do not exist in en, but exist in $3:', + ]; + } + + /** + * Get help. + * @return string The help string. + */ + protected function help() { + return <<