From 3afef625cc29f5e311ad43d59c8a4b4ef61f842e Mon Sep 17 00:00:00 2001
From: Universal Omega <54654040+Universal-Omega@users.noreply.github.com>
Date: Tue, 7 Jun 2022 21:17:58 -0600
Subject: [PATCH] Create extension
---
.eslintrc.json | 30 +
.gitattributes | 1 +
.github/dependabot.yml | 10 +
.github/workflows/dependencies | 6 +
.github/workflows/globals.php | 67 +
.github/workflows/mediawiki-tests.yml | 338 +
.gitignore | 5 +
.phan/config.php | 24 +
.phpcs.xml | 8 +
.stylelintrc.json | 17 +
ImportDumpAliases.php | 8 +
README.md | 12 +
composer.json | 25 +
extension.json | 156 +
i18n/en.json | 101 +
i18n/qqq.json | 101 +
includes/Hooks/Handlers/Installer.php | 26 +
includes/Hooks/Handlers/Main.php | 116 +
includes/ImportDumpOOUIForm.php | 106 +
includes/ImportDumpRequestManager.php | 541 ++
includes/ImportDumpRequestQueuePager.php | 169 +
includes/ImportDumpRequestViewer.php | 560 ++
.../EchoNewRequestPresentationModel.php | 63 +
.../EchoRequestCommentPresentationModel.php | 58 +
...hoRequestStatusUpdatePresentationModel.php | 56 +
includes/ServiceWiring.php | 22 +
.../SpecialImportDumpRequestQueue.php | 139 +
.../Specials/SpecialRequestImportDump.php | 449 ++
modules/ext.importdump.oouiform.ooui.js | 108 +
modules/ext.importdump.oouiform.ooui.less | 106 +
package-lock.json | 6860 +++++++++++++++++
package.json | 20 +
sql/importdump_request_comments.sql | 10 +
sql/importdump_requests.sql | 16 +
34 files changed, 10334 insertions(+)
create mode 100644 .eslintrc.json
create mode 100644 .gitattributes
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/dependencies
create mode 100644 .github/workflows/globals.php
create mode 100644 .github/workflows/mediawiki-tests.yml
create mode 100644 .gitignore
create mode 100644 .phan/config.php
create mode 100644 .phpcs.xml
create mode 100644 .stylelintrc.json
create mode 100644 ImportDumpAliases.php
create mode 100644 README.md
create mode 100644 composer.json
create mode 100644 extension.json
create mode 100644 i18n/en.json
create mode 100644 i18n/qqq.json
create mode 100644 includes/Hooks/Handlers/Installer.php
create mode 100644 includes/Hooks/Handlers/Main.php
create mode 100644 includes/ImportDumpOOUIForm.php
create mode 100644 includes/ImportDumpRequestManager.php
create mode 100644 includes/ImportDumpRequestQueuePager.php
create mode 100644 includes/ImportDumpRequestViewer.php
create mode 100644 includes/Notifications/EchoNewRequestPresentationModel.php
create mode 100644 includes/Notifications/EchoRequestCommentPresentationModel.php
create mode 100644 includes/Notifications/EchoRequestStatusUpdatePresentationModel.php
create mode 100644 includes/ServiceWiring.php
create mode 100644 includes/Specials/SpecialImportDumpRequestQueue.php
create mode 100644 includes/Specials/SpecialRequestImportDump.php
create mode 100644 modules/ext.importdump.oouiform.ooui.js
create mode 100644 modules/ext.importdump.oouiform.ooui.less
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 sql/importdump_request_comments.sql
create mode 100644 sql/importdump_requests.sql
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..8dd37c4
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,30 @@
+{
+ "root": true,
+ "extends": [
+ "wikimedia/client-es5",
+ "wikimedia/jquery",
+ "wikimedia/mediawiki"
+ ],
+ "globals": {
+ "require": "readonly",
+ "module": "readonly"
+ },
+ "env": {
+ "browser": true,
+ "jquery": true,
+ "es6": true
+ },
+ "ignorePatterns": [ "vendor/**" ],
+ "rules": {
+ "one-var": "off",
+ "no-implicit-globals": "off",
+ "es/no-object-fromentries": "off",
+ "es/no-object-entries": "off",
+ "no-jquery/no-global-selector": "off",
+ "no-jquery/no-sizzle": "off",
+ "max-len": "off"
+ },
+ "parserOptions": {
+ "ecmaVersion": "latest"
+ }
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3cbfe7d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+dependencies linguist-language=yaml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..71a72a2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/dependencies b/.github/workflows/dependencies
new file mode 100644
index 0000000..180e17c
--- /dev/null
+++ b/.github/workflows/dependencies
@@ -0,0 +1,6 @@
+CreateWiki:
+ branch: master
+ repo: https://github.com/miraheze/CreateWiki
+Echo:
+ branch: auto
+ repo: auto
diff --git a/.github/workflows/globals.php b/.github/workflows/globals.php
new file mode 100644
index 0000000..8ae223a
--- /dev/null
+++ b/.github/workflows/globals.php
@@ -0,0 +1,67 @@
+setVariables(
+ "$IP/cache",
+ [
+ ''
+ ],
+ [
+ '127.0.0.1' => ''
+ ]
+);
+
+$wi->config->settings += [
+ 'cwClosed' => [
+ 'default' => false,
+ ],
+ 'cwInactive' => [
+ 'default' => false,
+ ],
+ 'cwPrivate' => [
+ 'default' => false,
+ ],
+];
+
+$wgCreateWikiGlobalWiki = 'wikidb';
+$wgCreateWikiDatabase = 'wikidb';
+$wgCreateWikiCacheDirectory = "$IP/cache";
+
+$wgHooks['MediaWikiServices'][] = 'wfOnMediaWikiServices';
+
+function wfOnMediaWikiServices() {
+ try {
+ $dbw = wfGetDB( DB_PRIMARY );
+
+ $dbw->insert(
+ 'cw_wikis',
+ [
+ 'wiki_dbname' => 'wikidb',
+ 'wiki_dbcluster' => 'c1',
+ 'wiki_sitename' => 'TestWiki',
+ 'wiki_language' => 'en',
+ 'wiki_private' => (int)0,
+ 'wiki_creation' => $dbw->timestamp(),
+ 'wiki_category' => 'uncategorised',
+ 'wiki_closed' => (int)0,
+ 'wiki_deleted' => (int)0,
+ 'wiki_locked' => (int)0,
+ 'wiki_inactive' => (int)0,
+ 'wiki_inactive_exempt' => (int)0,
+ 'wiki_url' => 'http://127.0.0.1:9412'
+ ],
+ __METHOD__,
+ [ 'IGNORE' ]
+ );
+ } catch ( Wikimedia\Rdbms\DBQueryError $e ) {
+ return;
+ }
+}
+
+$wi->readCache();
+$wi->config->extractAllGlobals( $wi->dbname );
+$wgConf = $wi->config;
diff --git a/.github/workflows/mediawiki-tests.yml b/.github/workflows/mediawiki-tests.yml
new file mode 100644
index 0000000..b10d835
--- /dev/null
+++ b/.github/workflows/mediawiki-tests.yml
@@ -0,0 +1,338 @@
+name: Quibble and Phan
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ test:
+ name: "${{ matrix.mw }} | PHP ${{ matrix.php }} (${{ matrix.stage }})"
+
+ strategy:
+ matrix:
+ include:
+ # Latest stable MediaWiki - PHP 7.4 (phan)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: phan
+
+ # Latest MediaWiki master - PHP 7.4 (phan)
+ - mw: 'master'
+ php: 7.4
+ php-docker: 74
+ experimental: true
+ stage: phan
+
+ # Latest stable MediaWiki - PHP 7.4 (phpunit-unit)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: phpunit-unit
+
+ # Latest MediaWiki master - PHP 7.4 (phpunit-unit)
+ - mw: 'master'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: phpunit-unit
+
+ # Latest stable MediaWiki - PHP 7.4 (phpunit)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: phpunit
+
+ # Latest MediaWiki master - PHP 7.4 (phpunit)
+ - mw: 'master'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: phpunit
+
+ # Latest stable MediaWiki - PHP 7.4 (selenium)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: selenium
+
+ # Latest MediaWiki master - PHP 7.4 (selenium)
+ - mw: 'master'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: selenium
+
+ # Latest stable MediaWiki - PHP 7.4 (qunit)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: qunit
+
+ # Latest MediaWiki master - PHP 7.4 (qunit)
+ - mw: 'master'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: qunit
+
+ # Latest stable MediaWiki - PHP 7.4 (npm-test)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: npm-test
+
+ # Latest stable MediaWiki - PHP 7.4 (composer-test)
+ - mw: 'REL1_38'
+ php: 7.4
+ php-docker: 74
+ experimental: false
+ stage: composer-test
+
+ runs-on: ubuntu-latest
+
+ env:
+ DOCKER_REGISTRY: docker-registry.wikimedia.org
+ DOCKER_ORG: releng
+ QUIBBLE_DOCKER_IMAGE: quibble-buster-php${{ matrix.php-docker }}
+ COVERAGE_DOCKER_IMAGE: quibble-buster-php${{ matrix.php-docker }}-coverage
+ PHAN_DOCKER_IMAGE: mediawiki-phan-php${{ matrix.php-docker }}
+
+ steps:
+ - name: Cancel Previous Runs
+ uses: styfle/cancel-workflow-action@0.9.1
+ with:
+ access_token: ${{ github.token }}
+
+ - uses: actions/checkout@v3
+
+ # /home/runner/cache/ Cache
+ # /home/runner/src/ Mediawiki installation
+ # /home/runner/src/extensions/EXTENSION_NAME/ Clone of the extension repository
+ # /home/runner/docker-images/ Docker images which exported with docker-save command
+ # $GITHUB_WORKSPACE/.github/workflows/dependencies Necessary dependencies - YAML syntax
+ # $GITHUB_WORKSPACE/.github/workflows/globals.php Add global configuration options for MediaWiki
+ - name: Set up
+ run: |
+ echo MEDIAWIKI_VERSION="${{ matrix.mw }}" >> $GITHUB_ENV
+
+ if [ "${{ matrix.stage }}" == 'phan' ]; then
+ export DOCKER_IMAGE="${PHAN_DOCKER_IMAGE}"
+ elif [ "${{ matrix.stage }}" == coverage ]; then
+ export DOCKER_IMAGE="${COVERAGE_DOCKER_IMAGE}"
+ else
+ export DOCKER_IMAGE="${QUIBBLE_DOCKER_IMAGE}"
+ fi
+ echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> $GITHUB_ENV
+
+ # Get the latest docker tag (Ref: https://github.com/thcipriani/dockerregistry)
+ DOCKER_LATEST_TAG="$(curl -sL "https://${DOCKER_REGISTRY}/v2/${DOCKER_ORG}/${DOCKER_IMAGE}/tags/list" |
+ python3 -c 'import json;print("\n".join(json.loads(input())["tags"]))' |
+ grep -v latest | sort -Vr | head -1)"
+ echo "DOCKER_LATEST_TAG=${DOCKER_LATEST_TAG}" >> $GITHUB_ENV
+ if [ "${{ matrix.stage }}" == 'phan' ] || [ "${{ matrix.stage }}" == 'coverage' ]; then
+ echo "QUIBBLE_DOCKER_LATEST_TAG=$(curl -sL "https://${DOCKER_REGISTRY}/v2/${DOCKER_ORG}/${QUIBBLE_DOCKER_IMAGE}/tags/list" |
+ python3 -c 'import json;print("\n".join(json.loads(input())["tags"]))' |
+ grep -v latest | sort -Vr | head -1)" >> $GITHUB_ENV
+ fi
+
+ # Resolve dependencies
+ if [ -e .github/workflows/dependencies ]; then
+ cd .github/workflows
+ curl -sL https://raw.githubusercontent.com/wikimedia/integration-config/master/zuul/parameter_functions.py -o pf.py
+ curl -sL https://raw.githubusercontent.com/Universal-Omega/scripts/master/mediawiki/resolve_dependencies.py -o rd.py
+ echo "DEPENDENCIES=$(python3 rd.py)" >> $GITHUB_ENV
+ fi
+
+ - name: Cache docker image
+ uses: actions/cache@v3.0.4
+ with:
+ path: /home/runner/docker-images/${{ env.DOCKER_IMAGE }}
+ key: ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_LATEST_TAG }}
+
+ - name: Load or pull docker image
+ run: |
+ docker load -i /home/runner/docker-images/"${DOCKER_IMAGE}" || \
+ docker pull "${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_IMAGE}:${DOCKER_LATEST_TAG}"
+
+ - name: Cache quibble docker image
+ if: ${{ matrix.stage == 'coverage' || matrix.stage == 'phan' }}
+ uses: actions/cache@v3.0.4
+ with:
+ path: /home/runner/docker-images/${{ env.QUIBBLE_DOCKER_IMAGE }}
+ key: ${{ env.QUIBBLE_DOCKER_IMAGE }}:${{ env.QUIBBLE_DOCKER_LATEST_TAG }}
+ - name: Load or pull quibble docker image
+ if: ${{ matrix.stage == 'coverage' || matrix.stage == 'phan' }}
+ run: |
+ docker load -i /home/runner/docker-images/"${QUIBBLE_DOCKER_IMAGE}" || \
+ docker pull "${DOCKER_REGISTRY}/${DOCKER_ORG}/${QUIBBLE_DOCKER_IMAGE}:${QUIBBLE_DOCKER_LATEST_TAG}"
+
+ - name: Download MediaWiki and extensions
+ run: |
+ cd /home/runner
+ if [ ! -d src ]; then
+ git clone -b "${MEDIAWIKI_VERSION}" --depth 1 https://gerrit.wikimedia.org/r/mediawiki/core src
+ git clone --recurse-submodules -b "${MEDIAWIKI_VERSION}" --depth 1 https://gerrit.wikimedia.org/r/mediawiki/skins/Vector src/skins/Vector
+ for dep in $DEPENDENCIES; do
+ dependency=$(echo $dep | cut -d'|' -f1)
+ repository=$(echo $dep | cut -sd'|' -f2)
+ branch=$(echo $dep | rev | cut -sd'|' -f1 | rev)
+
+ if [ $repository == $branch ]; then
+ repository=""
+ fi
+
+ git clone --recurse-submodules -b "${branch:-${MEDIAWIKI_VERSION}}" --depth 1 "${repository:-https://gerrit.wikimedia.org/r/${dependency}}" src/"$(echo $dependency | cut -d'/' -f2,3)"
+ done
+ fi
+
+ if [ -e "$GITHUB_WORKSPACE"/.github/workflows/globals.php ]; then
+ echo 'require_once __DIR__ . "/../extensions/${{ github.event.repository.name }}/.github/workflows/globals.php";' >> src/includes/DevelopmentSettings.php
+ fi
+
+ git -C src/ log -n 1 --format="%H"
+
+ - name: Cache dependencies (composer and npm)
+ uses: actions/cache@v3.0.4
+ with:
+ path: /home/runner/cache
+ key: ${{ runner.os }}-${{ env.MEDIAWIKI_VERSION }}-${{ hashFiles('**/*.lock') }}
+
+ - name: Setup PHP Action
+ if: ${{ matrix.stage == 'phan' }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: composer:v2
+
+ - name: Composer install
+ if: ${{ matrix.stage == 'phan' }}
+ run: |
+ if [ -e composer.json ]; then
+ composer install --prefer-dist --no-progress --no-interaction
+ fi
+
+ - name: Fix PHPCS violations
+ continue-on-error: true
+ if: ${{ github.event_name == 'pull_request' && matrix.stage == 'composer-test' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ if [ -e composer.json ]; then
+ composer install --prefer-dist --no-progress --no-interaction
+ composer fix
+
+ rm composer.lock
+
+ if ! git diff --exit-code --quiet; then
+ git config --global user.name "github-actions"
+ git config --global user.email "github-actions@users.noreply.github.com"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
+ git checkout -b ${GITHUB_HEAD_REF}
+ git add .
+ git commit -am "CI: lint code to MediaWiki standards" -m "Check commit and GitHub actions for more details"
+ git pull origin ${GITHUB_HEAD_REF} --rebase
+ git push --set-upstream origin ${GITHUB_HEAD_REF}
+ else
+ echo "No changes to commit"
+ fi
+ fi
+
+ - name: Main Test
+ continue-on-error: ${{ matrix.experimental }}
+ run: |
+ cd /home/runner
+ # Move our extension
+ sudo cp -r "${GITHUB_WORKSPACE}" src/extensions/
+ mkdir -p cache cover
+ chmod 777 src cache cover
+ sudo chown -R nobody:nogroup src cache
+ sudo chown $(id -u):$(id -g) src cache
+ # Composer install
+ if [ "${{ matrix.stage }}" == 'phan' ] || [ "${{ matrix.stage }}" == 'coverage' ]; then
+ docker run \
+ -e ZUUL_PROJECT=mediawiki/extensions/"${{ github.event.repository.name }}" \
+ -v "$(pwd)"/cache:/cache \
+ -v "$(pwd)"/src:/workspace/src \
+ "${DOCKER_REGISTRY}/${DOCKER_ORG}/${QUIBBLE_DOCKER_IMAGE}:${QUIBBLE_DOCKER_LATEST_TAG}" \
+ --skip-zuul \
+ --packages-source composer \
+ --skip-install \
+ --skip all \
+ $DEPENDENCIES
+ fi
+
+ if [ "${{ matrix.stage }}" == 'phan' ]; then
+ docker run \
+ -e THING_SUBNAME=extensions/"${{ github.event.repository.name }}" \
+ -v "$(pwd)"/src:/mediawiki \
+ "${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_IMAGE}:${DOCKER_LATEST_TAG}" \
+ --color
+ elif [ "${{ matrix.stage }}" == 'coverage' ] && [ -d src/extensions/"${{ github.event.repository.name }}"/tests/phpunit ]; then
+ docker run \
+ --entrypoint quibble-with-supervisord \
+ -e ZUUL_PROJECT=mediawiki/extensions/"${{ github.event.repository.name }}" \
+ -v "$(pwd)"/cache:/cache \
+ -v "$(pwd)"/src:/workspace/src \
+ -v "$(pwd)"/cover:/workspace/cover \
+ "${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_IMAGE}:${DOCKER_LATEST_TAG}" \
+ --skip-zuul \
+ --skip-deps \
+ -c mwext-phpunit-coverage
+ elif [ "${{ matrix.stage }}" != 'coverage' ]; then
+ docker run \
+ --entrypoint quibble-with-supervisord \
+ -e ZUUL_PROJECT=mediawiki/extensions/"${{ github.event.repository.name }}" \
+ -v "$(pwd)"/cache:/cache \
+ -v "$(pwd)"/src:/workspace/src \
+ "${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_IMAGE}:${DOCKER_LATEST_TAG}" \
+ --skip-zuul \
+ --packages-source composer \
+ --run "${{ matrix.stage }}" \
+ $DEPENDENCIES
+ fi
+
+ - name: Tear down
+ run: |
+ cd /home/runner
+ sudo rm -rf src/extensions/"${{ github.event.repository.name }}"
+ # See https://doc.wikimedia.org/quibble/index.html#remove-localsettings-php-between-runs
+ rm "$(pwd)"/src/LocalSettings.php || true
+ mkdir -p docker-images
+ docker save -o "$(pwd)/docker-images/${DOCKER_IMAGE}" \
+ "${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_IMAGE}:${DOCKER_LATEST_TAG}"
+
+ notify-irc:
+ needs: test
+ runs-on: ubuntu-latest
+ if: ${{ always() && github.repository_owner == 'miraheze' && ( github.ref == 'refs/heads/master' || github.event_name == 'pull_request' ) }}
+ steps:
+ - name: succeeded
+ uses: technote-space/workflow-conclusion-action@v3
+ - uses: rectalogic/notify-irc@v2
+ if: env.WORKFLOW_CONCLUSION == 'success'
+ with:
+ channel: "#miraheze-sre"
+ server: "irc.libera.chat"
+ nickname: miraheze-github
+ message: ${{ github.repository }} - ${{ github.actor }} the build passed.
+ sasl_password: ${{ secrets.IRC_MIRAHEZEBOTS }}
+
+ - name: failed
+ uses: technote-space/workflow-conclusion-action@v3
+ - uses: rectalogic/notify-irc@v2
+ if: env.WORKFLOW_CONCLUSION == 'failure'
+ with:
+ channel: "#miraheze-sre"
+ server: "irc.libera.chat"
+ nickname: miraheze-github
+ message: ${{ github.repository }} - ${{ github.actor }} the build has errored.
+ sasl_password: ${{ secrets.IRC_MIRAHEZEBOTS }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..90151a6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/composer.lock
+/vendor
+/node_modules
+.eslintcache
+.github/*
diff --git a/.phan/config.php b/.phan/config.php
new file mode 100644
index 0000000..46a5fb7
--- /dev/null
+++ b/.phan/config.php
@@ -0,0 +1,24 @@
+
+
+ .
+
+
+
+
+
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000..8a69c4a
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,17 @@
+{
+ "extends": [
+ "stylelint-config-idiomatic-order",
+ "stylelint-config-wikimedia"
+ ],
+ "ignoreFiles": [
+ "vendor/**"
+ ],
+ "rules": {
+ "font-weight-notation": null,
+ "selector-max-id": null,
+ "no-descending-specificity": null,
+ "declaration-no-important": null,
+ "unit-disallowed-list": null,
+ "declaration-property-unit-disallowed-list": null
+ }
+}
diff --git a/ImportDumpAliases.php b/ImportDumpAliases.php
new file mode 100644
index 0000000..329ad27
--- /dev/null
+++ b/ImportDumpAliases.php
@@ -0,0 +1,8 @@
+ [ 'ImportDumpRequestQueue' ],
+ 'RequestImportDump' => [ 'RequestImportDump' ],
+];
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..70e7cfa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# ImportDump
+An extension designed to automate user import requests.
+
+Developed by Universal Omega.
+
+Licensed under the GPLv3 (or later) LICENSE.
+
+# Security Vulnerabilities
+
+If you believe you have found a security vulnerability in any part of our code, please do not post it publicly by using our wikis or bug trackers for that; rather, please read our [security page](https://meta.miraheze.org/wiki/Security) carefully, and follow the instructions.
+
+As a quick overview, you can email security concerns to security@miraheze.org which will open a phabricator task that is hidden from public view. If you'd like, you can instead directly create a security-related task [here](https://phabricator.miraheze.org/maniphest/task/edit/form/2/), but please leave the "Security" project on the issue.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..3accdd0
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "miraheze/importdump",
+ "description": "",
+ "license": "GPL-3.0-or-later",
+ "require-dev": {
+ "mediawiki/mediawiki-codesniffer": "38.0.0",
+ "mediawiki/mediawiki-phan-config": "0.11.1",
+ "mediawiki/minus-x": "1.1.1",
+ "php-parallel-lint/php-console-highlighter": "1.0.0",
+ "php-parallel-lint/php-parallel-lint": "1.3.2"
+ },
+ "scripts": {
+ "fix": [
+ "minus-x fix .",
+ "phpcbf; if [ $? -eq 1 ]; then exit 0; fi"
+ ],
+ "test": [
+ "parallel-lint . --exclude node_modules --exclude vendor",
+ "minus-x check .",
+ "@phpcs"
+ ],
+ "phan": "phan -d . --long-progress-bar",
+ "phpcs": "phpcs -sp --cache"
+ }
+}
diff --git a/extension.json b/extension.json
new file mode 100644
index 0000000..ab90d44
--- /dev/null
+++ b/extension.json
@@ -0,0 +1,156 @@
+{
+ "name": "ImportDump",
+ "author": "Universal Omega",
+ "url": "https://github.com/miraheze/ImportDump",
+ "descriptionmsg": "importdump-desc",
+ "namemsg": "importdump-extensionname",
+ "license-name": "GPL-3.0-or-later",
+ "type": "specialpage",
+ "requires": {
+ "MediaWiki": ">= 1.38.0"
+ },
+ "MessagesDirs": {
+ "ImportDump": [
+ "i18n"
+ ]
+ },
+ "ExtensionMessagesFiles": {
+ "ImportDumpAliases": "ImportDumpAliases.php"
+ },
+ "AutoloadNamespaces": {
+ "Miraheze\\ImportDump\\": "includes/"
+ },
+ "GroupPermissions": {
+ "user": {
+ "request-import-dump": true
+ }
+ },
+ "AvailableRights": [
+ "handle-import-dump-requests",
+ "request-import-dump",
+ "view-private-import-dump-requests"
+ ],
+ "LogActionsHandlers": {
+ "importdump/*": "LogFormatter",
+ "importdumpprivate/*": "LogFormatter"
+ },
+ "LogHeaders": {
+ "importdump": "importdump-log-header",
+ "importdumpprivate": "importdumpprivate-log-header"
+ },
+ "LogNames": {
+ "importdump": "importdump-log-name",
+ "importdumpprivate": "importdumpprivate-log-name"
+ },
+ "LogTypes": [
+ "importdump",
+ "importdumpprivate"
+ ],
+ "LogRestrictions": {
+ "importdumpprivate": "view-private-import-dump-requests"
+ },
+ "SpecialPages": {
+ "ImportDumpRequestQueue": {
+ "class": "Miraheze\\ImportDump\\Specials\\SpecialImportDumpRequestQueue",
+ "services": [
+ "DBLoadBalancerFactory",
+ "ImportDumpRequestManager",
+ "PermissionManager",
+ "UserFactory"
+ ]
+ },
+ "RequestImportDump": {
+ "class": "Miraheze\\ImportDump\\Specials\\SpecialRequestImportDump",
+ "services": [
+ "DBLoadBalancerFactory",
+ "MimeAnalyzer",
+ "RepoGroup",
+ "UserFactory"
+ ]
+ }
+ },
+ "Hooks": {
+ "BeforeCreateEchoEvent": {
+ "handler": "Main"
+ },
+ "GetAllBlockActions": {
+ "handler": "Main"
+ },
+ "LoadExtensionSchemaUpdates": {
+ "handler": "Installer"
+ },
+ "UserGetReservedNames": {
+ "handler": "Main"
+ }
+ },
+ "HookHandlers": {
+ "Installer": {
+ "class": "Miraheze\\ImportDump\\Hooks\\Handlers\\Installer"
+ },
+ "Main": {
+ "class": "Miraheze\\ImportDump\\Hooks\\Handlers\\Main",
+ "services": [
+ "ConfigFactory"
+ ]
+ }
+ },
+ "ResourceModules": {
+ "ext.importdump.oouiform": {
+ "targets": [ "desktop", "mobile" ],
+ "scripts": "ext.importdump.oouiform.ooui.js",
+ "dependencies": [
+ "mediawiki.storage",
+ "oojs-ui-widgets"
+ ]
+ },
+ "ext.importdump.oouiform.styles": {
+ "targets": [ "desktop", "mobile" ],
+ "styles": "ext.importdump.oouiform.ooui.less"
+ }
+ },
+ "ResourceFileModulePaths": {
+ "localBasePath": "modules",
+ "remoteExtPath": "ImportDump/modules"
+ },
+ "config": {
+ "ImportDumpCentralWiki": {
+ "value": "",
+ "description": "If set, only allow users to request import dumps on this wiki."
+ },
+ "ImportDumpHelpUrl": {
+ "value": "",
+ "description": "Full URL. If set, adds a help URL to Special:RequestImportDump."
+ },
+ "ImportDumpInterwikiMap": {
+ "value": [],
+ "description": "A mapping of 'domain => interwiki prefix' for multi-level interwiki prefix generation for --username-prefix."
+ },
+ "ImportDumpScriptCommand": {
+ "value": "php {IP}/maintenance/importDump.php --wiki={wiki} --username-prefix={username-prefix} {file}",
+ "description": "Generated maintenance script command to use."
+ },
+ "ImportDumpUsersNotifiedOnAllRequests": {
+ "value": [],
+ "description": "Array of usernames to send notifications for all requests."
+ }
+ },
+ "ConfigRegistry": {
+ "ImportDump": "GlobalVarConfig::newInstance"
+ },
+ "DefaultUserOptions": {
+ "echo-subscriptions-email-importdump-new-request": true,
+ "echo-subscriptions-email-importdump-request-comment": true,
+ "echo-subscriptions-email-importdump-request-status-update": true,
+ "echo-subscriptions-web-importdump-new-request": false,
+ "echo-subscriptions-web-importdump-request-comment": true,
+ "echo-subscriptions-web-importdump-request-status-update": true
+ },
+ "HiddenPrefs": [
+ "echo-subscriptions-email-importdump-new-request",
+ "echo-subscriptions-web-importdump-new-request"
+ ],
+ "ServiceWiringFiles": [
+ "includes/ServiceWiring.php"
+ ],
+ "manifest_version": 2
+}
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..3b7969a
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,101 @@
+{
+ "@metadata": {
+ "authors": [
+ "Universal Omega"
+ ]
+ },
+ "action-handle-import-dump-requests": "handle user import requests",
+ "action-request-import-dump": "request import dumps",
+ "action-view-private-import-dump-requests": "view private import dump requests",
+ "echo-category-title-importdump-new-request": "New import dump request",
+ "echo-category-title-importdump-request-comment": "Comment added to an import dump request",
+ "echo-category-title-importdump-request-status-update": "Status updated for an import dump request",
+ "echo-pref-tooltip-importdump-new-request": "Notify me when there are any new import dump requests.",
+ "echo-pref-tooltip-importdump-request-comment": "Notify me when there is a comment on an import dump request that I have participated on.",
+ "echo-pref-tooltip-importdump-request-status-update": "Notify me when the status is updated for an import dump request that I have participated on.",
+ "importdump-comment-given": "Comment given by $1:",
+ "importdump-comment-success": "Comment successfully added to this request.",
+ "importdump-desc": "An extension designed to automate user import requests.",
+ "importdump-duplicate-request": "Unable to submit this request. It is to similar to an already open request.",
+ "importdump-edit-success": "Request successfully edited.",
+ "importdump-extensionname": "ImportDump",
+ "importdump-header-comment-withtimestamp": "Comment given by $1 at $2",
+ "importdump-help-source": "Enter the full URL of the source wiki (where you are importing from).",
+ "importdump-help-target": "This must be a valid database name.",
+ "importdump-help-upload": "Only XML files are permitted.",
+ "importdump-info-command": "Command for this import: $1",
+ "importdump-info-groups": "User groups that $1 has on $2: $3",
+ "importdump-info-no-interwiki-prefix": "No interwiki prefix exists on the target wiki ($1), which is pointing to the source wiki ($2), please add one before this request can be approved.",
+ "importdump-info-request-private": "This request is marked as private. It is not publicly viewable.",
+ "importdump-info-requester-globally-blocked": "The requester ($1) has been globally blocked. This request should be declined.",
+ "importdump-info-requester-locally-blocked": "The requester ($1) has been locally blocked on this wiki ($2).",
+ "importdump-info-requester-locked": "The requester ($1) has been locked. This request should be declined.",
+ "importdump-invalid-target": "The provided target is not a valid database name.",
+ "importdump-label-add-comment": "Add Comment",
+ "importdump-label-all": "All",
+ "importdump-label-comment": "Comment:",
+ "importdump-label-complete": "Complete",
+ "importdump-label-declined": "Declined",
+ "importdump-label-edit-request": "Edit Request",
+ "importdump-label-inprogress": "In progress",
+ "importdump-label-lock": "Lock this request from being edited or from comments being added to it?",
+ "importdump-label-pending": "Pending",
+ "importdump-label-private": "Mark this request as private?",
+ "importdump-label-reason": "Reason for Request:",
+ "importdump-label-requested-date": "Requested Date:",
+ "importdump-label-requester": "Requester:",
+ "importdump-label-source": "Source URL:",
+ "importdump-label-status": "Status:",
+ "importdump-label-status-updated-comment": "Optional additional comment to add to this request:",
+ "importdump-label-target": "Target Database Name:",
+ "importdump-label-update-status": "Set the status of this request to:",
+ "importdump-label-upload-file": "Upload File:",
+ "importdump-label-upload-file-url": "Source URL:",
+ "importdump-label-upload-source-file": "Upload file",
+ "importdump-label-upload-source-type": "Upload source:",
+ "importdump-label-upload-source-url": "Upload from URL",
+ "importdump-log-header": "This is a log of all import dump requests and status updates.",
+ "importdump-log-name": "Import dump requests log",
+ "importdump-no-changes": "No changes made.",
+ "importdump-notcentral": "Disabled",
+ "importdump-notcentral-text": "Import dump requests are disabled on this wiki. Please request an import dump from the central wiki.",
+ "importdump-notification-body-new-request": "A new import dump request was received.
Reason given: $1
Requester: $2
Target Database Name: $3",
+ "importdump-notification-header-comment": "Import dump request #$1 received a comment.",
+ "importdump-notification-header-new-request": "New import dump request #$1",
+ "importdump-notification-header-status-update": "Status updated for import dump request #$1",
+ "importdump-notification-visit-request": "Visit request",
+ "importdump-notloggedin": "Sorry, you need to [$1 log in] before you can request an import dump.",
+ "importdump-request-edited": "Request edited by $1.\n\n[Changes made]$2",
+ "importdump-request-edited-reason": "\nReason:\nPrevious value: $1\nNew value: $2",
+ "importdump-request-edited-source": "\nSource:\nPrevious value: $1\nNew value: $2",
+ "importdump-request-edited-target": "\nTarget:\nPrevious value: $1\nNew value: $2",
+ "importdump-request-locked": "This request has been locked. It can not be edited, and no comments can be added to it.",
+ "importdump-request-reopened": "Request reopened by $1.\n\n[Changes made]$2",
+ "importdump-section-comments": "Request Comments",
+ "importdump-section-details": "Request Details",
+ "importdump-section-editing": "Edit Request",
+ "importdump-section-handling": "Handle Request",
+ "importdump-status-updated": "Request $1.",
+ "importdump-status-updated-success": "Status successfully updated for this request.",
+ "importdump-success": "Your import dump request ($1) was successfully submitted. Your request will be reviewed by a system administrator as soon as possible.",
+ "importdump-success-locked": "This request has been successfully locked. It can no longer be edited and comments can not be added to it.",
+ "importdump-success-private": "This request has been successfully marked as private. It is no longer publicly viewable.",
+ "importdump-table-requested-date": "Requested Date",
+ "importdump-table-requester": "Requester",
+ "importdump-table-status": "Status",
+ "importdump-table-target": "Target",
+ "importdump-unknown": "Unknown request.",
+ "importdump-unknown-username-prefix": "unknown",
+ "importdumpprivate-log-header": "This is a log of all import dump requests and status updates for requests with targets that are private wikis.",
+ "importdumpprivate-log-name": "Import dump requests private log",
+ "importdumprequestqueue": "Import Dump Request Queue",
+ "ipb-action-request-import-dump": "Requesting import dumps",
+ "logentry-importdump-request": "$1 requested an import dump for $4 in $5.",
+ "logentry-importdump-statusupdate": "$1 updated status of import dump request $4 to '$5'.",
+ "logentry-importdumpprivate-request": "$1 requested an import dump for private wiki $4 in $5.",
+ "logentry-importdumpprivate-statusupdate": "$1 updated status of private import dump request $4 to '$5'.",
+ "requestimportdump": "Request Import Dump",
+ "right-handle-import-dump-requests": "Handle user import dump requests",
+ "right-request-import-dump": "Request import dumps",
+ "right-view-private-import-dump-requests": "View private import dump requests"
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100644
index 0000000..13aafce
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,101 @@
+{
+ "@metadata": {
+ "authors": [
+ "Universal Omega"
+ ]
+ },
+ "action-handle-import-dump-requests": "{{doc-action|handle-import-dump-requests}}",
+ "action-request-import-dump": "{{doc-action|request-import-dump}}",
+ "action-view-private-import-dump-requests": "{{doc-action|view-private-import-dump-requests}}",
+ "echo-category-title-importdump-new-request": "Echo category title for 'importdump-new-request'.",
+ "echo-category-title-importdump-request-comment": "Echo category title for 'importdump-request-comment'.",
+ "echo-category-title-importdump-request-status-update": "Echo category title for 'importdump-request-status-update'.",
+ "echo-pref-tooltip-importdump-new-request": "Echo preference tooltip for 'importdump-new-request'.",
+ "echo-pref-tooltip-importdump-request-comment": "Echo preference tooltip for 'importdump-request-comment'.",
+ "echo-pref-tooltip-importdump-request-status-update": "Echo preference tooltip for 'importdump-request-status-update'.",
+ "importdump-comment-given": "Displayed if a custom comment is given. $1 is the handler's username.",
+ "importdump-comment-success": "Message displayed when a comment is successfully added to the request.",
+ "importdump-desc": "{{desc|name=ImportDump|url=https://github.com/miraheze/ImportDump}}",
+ "importdump-duplicate-request": "Message displayed when user attempts to submit a request that is to similar to an existing open request.",
+ "importdump-edit-success": "Message displayed when the request is successfully edited.",
+ "importdump-extensionname": "{{name}}",
+ "importdump-header-comment-withtimestamp": "Timestamp for comment.",
+ "importdump-help-source": "Help for the 'source' field.",
+ "importdump-help-target": "Help for the 'target' field.",
+ "importdump-help-upload": "Help for the 'Upload File' and 'Source URL' fields.",
+ "importdump-info-command": "The generated command that is displayed on the 'handling' section of Special:ImportDumpRequestQueue. $1 is the generated command.",
+ "importdump-info-groups": "User groups that the requester has on the target wiki. Displayed on the 'handling' section of Special:ImportDumpRequestQueue. $1 is the requester's username; $2 is the target wiki's database name; $3 is the comma separated list of user groups.",
+ "importdump-info-no-interwiki-prefix": "Message that is displayed on the 'handling' section of Special:ImportDumpRequestQueue when no interwiki prefix is found on the target wiki, which is pointing to the source wiki. $1 is the target wiki; $2 is the source wiki.",
+ "importdump-info-request-private": "Message that is displayed on the 'handling' section of Special:ImportDumpRequestQueue when viewing a request that is private.",
+ "importdump-info-requester-globally-blocked": "Message that is displayed on the 'handling' section of Special:ImportDumpRequestQueue when the requester has been globally blocked. $1 is the requester's username.",
+ "importdump-info-requester-locally-blocked": "Message that is displayed on the 'handling' section of Special:ImportDumpRequestQueue when the requester has been locally blocked on the current wiki. $1 is the requester's username; $2 is the current wiki's ID.",
+ "importdump-info-requester-locked": "Message that is displayed on the 'handling' section of Special:ImportDumpRequestQueue when the requester has been locked. $1 is the requester's username.",
+ "importdump-invalid-target": "Status message displayed when invalid database name was entered for the 'target' field.",
+ "importdump-label-add-comment": "Label for the 'Add Comment' submit button.",
+ "importdump-label-all": "Label for 'All'.",
+ "importdump-label-comment": "Label for 'Comment'.",
+ "importdump-label-complete": "'Complete' status label.",
+ "importdump-label-declined": "'Declined' status label.",
+ "importdump-label-edit-request": "Label for the 'Edit Request' submit button.",
+ "importdump-label-inprogress": "'In progress' status label.",
+ "importdump-label-lock": "Label for the 'lock' checkbox.",
+ "importdump-label-pending": "'Pending' status label.",
+ "importdump-label-private": "Label for the 'private' checkbox.",
+ "importdump-label-reason": "Label for the 'reason' field.",
+ "importdump-label-requested-date": "Label for 'Requested Date'.",
+ "importdump-label-requester": "Label for 'Requester'.",
+ "importdump-label-source": "Label for the 'source' field.",
+ "importdump-label-status": "Label for 'Status'.",
+ "importdump-label-status-updated-comment": "Label for the optional comment field in the 'handling' section of Special:ImportDumpRequestQueue.",
+ "importdump-label-target": "Label for the 'target' field.",
+ "importdump-label-update-status": "Label for the update status select field in the 'handling' section of Special:ImportDumpRequestQueue.",
+ "importdump-label-upload-file": "Label for 'Upload File'.",
+ "importdump-label-upload-file-url": "Label for 'Source URL'.",
+ "importdump-label-upload-source-file": "Label for 'Upload file'.",
+ "importdump-label-upload-source-type": "Label for 'Upload source'.",
+ "importdump-label-upload-source-url": "Label for 'Upload from URL'.",
+ "importdump-log-header": "Header for the ImportDump log.",
+ "importdump-log-name": "{{doc-logpage}}",
+ "importdump-no-changes": "Message displayed if attempting to edit request without making any changes.",
+ "importdump-notcentral": "Heading for the message displayed if there is a configured central wiki, and it is not the current wiki.",
+ "importdump-notcentral-text": "Text for the message displayed if there is a configured central wiki, and it is not the current wiki.",
+ "importdump-notification-body-new-request": "Echo notification body for 'importdump-new-request'. $1 is reason; $2 is the requester's username; $3 is the target wiki's database name.",
+ "importdump-notification-header-comment": "Echo notification header for 'importdump-request-comment'. $1 is the request ID.",
+ "importdump-notification-header-new-request": "Echo notification header for 'importdump-new-request'. $1 is the request ID.",
+ "importdump-notification-header-status-update": "Echo notification header for 'importdump-request-status-update'. $1 is the request ID.",
+ "importdump-notification-visit-request": "Echo notification 'Visit request' label.",
+ "importdump-notloggedin": "Error message if user tries to request an import dump while not being logged in.",
+ "importdump-request-edited": "'Request edited' message. $1 is the actor's username; $2 is the changes that were made.",
+ "importdump-request-edited-reason": "Message displayed if the value of 'reason' was edited. $1 is the previous value; $2 is the new value.",
+ "importdump-request-edited-source": "Message displayed if the value of 'source' was edited. $1 is the previous value; $2 is the new value.",
+ "importdump-request-edited-target": "Message displayed if the value of 'target' was edited. $1 is the previous value; $2 is the new value.",
+ "importdump-request-locked": "Message displayed above the form at Special:ImportDumpRequestQueue if the request is locked.",
+ "importdump-request-reopened": "'Request reopened' message. $1 is the actor's username; $2 is the changes that were made.",
+ "importdump-section-comments": "'Request Comments' section label.",
+ "importdump-section-details": "'Request Details' section label.",
+ "importdump-section-editing": "'Edit Request' section label.",
+ "importdump-section-handling": "'Handle Request' section label.",
+ "importdump-status-updated": "Fixed comment given to requester when the status is updated. $1 is status.",
+ "importdump-status-updated-success": "Message displayed when the status of the request is successfully updated.",
+ "importdump-success": "Message after an import dump request has been successfully submitted.",
+ "importdump-success-locked": "Message displayed when a request has been successfully locked.",
+ "importdump-success-private": "Message displayed when a request has been successfully marked as private.",
+ "importdump-table-requested-date": "'Requested Date' label for the table on Special:ImportDumpRequestQueue.",
+ "importdump-table-requester": "'Requester' label for the table on Special:ImportDumpRequestQueue.",
+ "importdump-table-status": "'Status' label for the table on Special:ImportDumpRequestQueue.",
+ "importdump-table-target": "'Target' label for the table on Special:ImportDumpRequestQueue.",
+ "importdump-unknown": "Message displayed if the request is not found.",
+ "importdump-unknown-username-prefix": "Displayed value for --username-prefix in the generated command, when there is no prefix for the source wiki on the target wiki.",
+ "importdumpprivate-log-name": "{{doc-logpage}}",
+ "importdumpprivate-log-header": "Header for the ImportDump private log.",
+ "importdumprequestqueue": "{{doc-special}}",
+ "ipb-action-request-import-dump": "The label for the action select option on [[Special:Block]] to block a user from requesting import dumps.",
+ "logentry-importdump-request": "{{logentry|[[Special:Log/importdump]]}}",
+ "logentry-importdump-statusupdate": "{{logentry|[[Special:Log/importdump]]}}",
+ "logentry-importdumpprivate-request": "{{logentry|[[Special:Log/importdumpprivate]]}}",
+ "logentry-importdumpprivate-statusupdate": "{{logentry|[[Special:Log/importdumpprivate]]}}",
+ "requestimportdump": "{{doc-special}}",
+ "right-handle-import-dump-requests": "{{doc-right|handle-import-dump-requests}}",
+ "right-request-import-dump": "{{doc-right|request-import-dump}}",
+ "right-view-private-import-dump-requests": "{{doc-right|view-private-import-dump-requests}}"
+}
diff --git a/includes/Hooks/Handlers/Installer.php b/includes/Hooks/Handlers/Installer.php
new file mode 100644
index 0000000..756af42
--- /dev/null
+++ b/includes/Hooks/Handlers/Installer.php
@@ -0,0 +1,26 @@
+addExtensionTable(
+ 'importdump_request_comments',
+ "$dir/importdump_request_comments.sql"
+ );
+
+ $updater->addExtensionTable(
+ 'importdump_requests',
+ "$dir/importdump_requests.sql"
+ );
+ }
+}
diff --git a/includes/Hooks/Handlers/Main.php b/includes/Hooks/Handlers/Main.php
new file mode 100644
index 0000000..e4d7d2e
--- /dev/null
+++ b/includes/Hooks/Handlers/Main.php
@@ -0,0 +1,116 @@
+config = $configFactory->makeConfig( 'ImportDump' );
+ }
+
+ /**
+ * @param array &$reservedUsernames
+ */
+ public function onUserGetReservedNames( &$reservedUsernames ) {
+ $reservedUsernames[] = 'ImportDump Extension';
+ $reservedUsernames[] = 'ImportDump Status Update';
+ }
+
+ /**
+ * @param array &$actions
+ */
+ public function onGetAllBlockActions( &$actions ) {
+ if (
+ $this->config->get( 'ImportDumpCentralWiki' ) &&
+ !WikiMap::isCurrentWikiId( $this->config->get( 'ImportDumpCentralWiki' ) )
+ ) {
+ return;
+ }
+
+ $actions[ 'request-import-dump' ] = 200;
+ }
+
+ /**
+ * @param array &$notifications
+ * @param array &$notificationCategories
+ * @param array &$icons
+ */
+ public function onBeforeCreateEchoEvent( &$notifications, &$notificationCategories, &$icons ) {
+ if (
+ $this->config->get( 'ImportDumpCentralWiki' ) &&
+ !WikiMap::isCurrentWikiId( $this->config->get( 'ImportDumpCentralWiki' ) )
+ ) {
+ return;
+ }
+
+ $notificationCategories['importdump-new-request'] = [
+ 'priority' => 3,
+ 'tooltip' => 'echo-pref-tooltip-importdump-new-request',
+ ];
+
+ $notificationCategories['importdump-request-comment'] = [
+ 'priority' => 3,
+ 'tooltip' => 'echo-pref-tooltip-importdump-request-comment',
+ ];
+
+ $notificationCategories['importdump-request-status-update'] = [
+ 'priority' => 3,
+ 'tooltip' => 'echo-pref-tooltip-importdump-request-status-update',
+ ];
+
+ $notifications['importdump-new-request'] = [
+ EchoAttributeManager::ATTR_LOCATORS => [
+ 'EchoUserLocator::locateEventAgent'
+ ],
+ 'category' => 'importdump-new-request',
+ 'group' => 'positive',
+ 'section' => 'alert',
+ 'canNotifyAgent' => true,
+ 'presentation-model' => EchoNewRequestPresentationModel::class,
+ 'immediate' => true,
+ ];
+
+ $notifications['importdump-request-comment'] = [
+ EchoAttributeManager::ATTR_LOCATORS => [
+ 'EchoUserLocator::locateEventAgent'
+ ],
+ 'category' => 'importdump-request-comment',
+ 'group' => 'positive',
+ 'section' => 'alert',
+ 'canNotifyAgent' => true,
+ 'presentation-model' => EchoRequestCommentPresentationModel::class,
+ 'immediate' => true,
+ ];
+
+ $notifications['importdump-request-status-update'] = [
+ EchoAttributeManager::ATTR_LOCATORS => [
+ 'EchoUserLocator::locateEventAgent'
+ ],
+ 'category' => 'importdump-request-status-update',
+ 'group' => 'positive',
+ 'section' => 'alert',
+ 'canNotifyAgent' => true,
+ 'presentation-model' => EchoRequestStatusUpdatePresentationModel::class,
+ 'immediate' => true,
+ ];
+ }
+}
diff --git a/includes/ImportDumpOOUIForm.php b/includes/ImportDumpOOUIForm.php
new file mode 100644
index 0000000..7df5723
--- /dev/null
+++ b/includes/ImportDumpOOUIForm.php
@@ -0,0 +1,106 @@
+ 'importdump' ], $html );
+
+ return parent::wrapForm( $html );
+ }
+
+ /**
+ * @param string $legend
+ * @param string $section
+ * @param array $attributes
+ * @param bool $isRoot
+ * @return PanelLayout
+ */
+ protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
+ $layout = parent::wrapFieldSetSection( $legend, $section, $attributes, $isRoot );
+
+ $layout->addClasses( [ 'importdump-fieldset-wrapper' ] );
+ $layout->removeClasses( [ 'oo-ui-panelLayout-framed' ] );
+
+ return $layout;
+ }
+
+ /**
+ * @return string
+ */
+ public function getBody() {
+ $tabPanels = [];
+ foreach ( $this->mFieldTree as $key => $val ) {
+ if ( !is_array( $val ) ) {
+ wfDebug( __METHOD__ . " encountered a field not attached to a section: '{$key}'" );
+
+ continue;
+ }
+
+ $label = $this->getLegend( $key );
+
+ $content =
+ $this->getHeaderHtml( $key ) .
+ $this->displaySection(
+ $val,
+ '',
+ "mw-section-{$key}-"
+ ) .
+ $this->getFooterHtml( $key );
+
+ $tabPanels[] = new TabPanelLayout( 'mw-section-' . $key, [
+ 'classes' => [ 'mw-htmlform-autoinfuse-lazy' ],
+ 'label' => $label,
+ 'content' => new FieldsetLayout( [
+ 'classes' => [ 'importdump-section-fieldset' ],
+ 'id' => "mw-section-{$key}",
+ 'label' => $label,
+ 'items' => [
+ new Widget( [
+ 'content' => new HtmlSnippet( $content )
+ ] ),
+ ],
+ ] ),
+ 'expanded' => false,
+ 'framed' => true,
+ ] );
+ }
+
+ $indexLayout = new IndexLayout( [
+ 'infusable' => true,
+ 'expanded' => false,
+ 'autoFocus' => false,
+ 'classes' => [ 'importdump-tabs' ],
+ ] );
+
+ $indexLayout->addTabPanels( $tabPanels );
+
+ $header = $this->formatFormHeader();
+
+ $form = new PanelLayout( [
+ 'framed' => true,
+ 'expanded' => false,
+ 'classes' => [ 'importdump-tabs-wrapper' ],
+ 'content' => $indexLayout
+ ] );
+
+ return $header . $form;
+ }
+}
diff --git a/includes/ImportDumpRequestManager.php b/includes/ImportDumpRequestManager.php
new file mode 100644
index 0000000..c5461ce
--- /dev/null
+++ b/includes/ImportDumpRequestManager.php
@@ -0,0 +1,541 @@
+assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
+
+ $this->config = $config;
+ $this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
+ $this->linkRenderer = $linkRenderer;
+ $this->messageLocalizer = $messageLocalizer;
+ $this->options = $options;
+ $this->userFactory = $userFactory;
+ $this->userGroupManagerFactory = $userGroupManagerFactory;
+ }
+
+ /**
+ * @param int $requestID
+ */
+ public function fromID( int $requestID ) {
+ $this->ID = $requestID;
+
+ $centralWiki = $this->options->get( 'ImportDumpCentralWiki' );
+ if ( $centralWiki ) {
+ $this->dbw = $this->dbLoadBalancerFactory->getMainLB(
+ $centralWiki
+ )->getConnectionRef( DB_PRIMARY, [], $centralWiki );
+ } else {
+ $this->dbw = $this->dbLoadBalancerFactory->getMainLB()->getConnectionRef( DB_PRIMARY );
+ }
+
+ $this->row = $this->dbw->selectRow(
+ 'importdump_requests',
+ '*',
+ [
+ 'request_id' => $requestID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function exists(): bool {
+ return (bool)$this->row;
+ }
+
+ /**
+ * @param string $comment
+ * @param User $user
+ */
+ public function addComment( string $comment, User $user ) {
+ $this->dbw->insert(
+ 'importdump_request_comments',
+ [
+ 'request_id' => $this->ID,
+ 'request_comment_text' => $comment,
+ 'request_comment_timestamp' => $this->dbw->timestamp(),
+ 'request_comment_actor' => $user->getActorId(),
+ ],
+ __METHOD__
+ );
+
+ if ( !in_array( $user->getName(), self::SYSTEM_USERS ) ) {
+ $this->sendNotification( $comment, 'importdump-request-comment', $user );
+ }
+ }
+
+ /**
+ * @param string $comment
+ * @param string $newStatus
+ * @param User $user
+ */
+ public function logStatusUpdate( string $comment, string $newStatus, User $user ) {
+ $requestQueueLink = SpecialPage::getTitleValueFor( 'ImportDumpRequestQueue', (string)$this->ID );
+ $requestLink = $this->linkRenderer->makeLink( $requestQueueLink, "#{$this->ID}" );
+
+ $logEntry = new ManualLogEntry(
+ $this->isPrivate() ? 'importdumpprivate' : 'importdump',
+ 'statusupdate'
+ );
+
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $requestQueueLink );
+
+ if ( $comment ) {
+ $logEntry->setComment( $comment );
+ }
+
+ $logEntry->setParameters(
+ [
+ '4::requestLink' => Message::rawParam( $requestLink ),
+ '5::requestStatus' => strtolower( $this->messageLocalizer->msg(
+ 'importdump-label-' . $newStatus
+ )->inContentLanguage()->text() ),
+ ]
+ );
+
+ $logID = $logEntry->insert( $this->dbw );
+ $logEntry->publish( $logID );
+ }
+
+ /**
+ * @param string $comment
+ * @param string $type
+ * @param User $user
+ */
+ public function sendNotification( string $comment, string $type, User $user ) {
+ $requestLink = SpecialPage::getTitleFor( 'ImportDumpRequestQueue', (string)$this->ID )->getFullURL();
+
+ $involvedUsers = array_values( array_filter(
+ array_diff( $this->getInvolvedUsers(), [ $user ] )
+ ) );
+
+ foreach ( $involvedUsers as $receiver ) {
+ EchoEvent::create( [
+ 'type' => $type,
+ 'extra' => [
+ 'request-id' => $this->ID,
+ 'request-url' => $requestLink,
+ 'comment' => $comment,
+ 'notifyAgent' => true,
+ ],
+ 'agent' => $receiver,
+ ] );
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getComments(): array {
+ $res = $this->dbw->select(
+ 'importdump_request_comments',
+ '*',
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'request_comment_timestamp DESC',
+ ]
+ );
+
+ if ( !$res ) {
+ return [];
+ }
+
+ $comments = [];
+ foreach ( $res as $row ) {
+ $user = $this->userFactory->newFromActorId( $row->request_comment_actor );
+
+ $comments[] = [
+ 'comment' => $row->request_comment_text,
+ 'timestamp' => $row->request_comment_timestamp,
+ 'user' => $user,
+ ];
+ }
+
+ return $comments;
+ }
+
+ /**
+ * @return array
+ */
+ public function getInvolvedUsers(): array {
+ return array_unique( array_column( $this->getComments(), 'user' ) + [ $this->getRequester() ] );
+ }
+
+ /**
+ * @return string
+ */
+ public function getInterwikiPrefix(): string {
+ if ( $this->options->get( 'ImportDumpInterwikiMap' ) ) {
+ $parsedSource = parse_url( $this->getSource() )['host'] ?? '';
+ $domain = explode( '.', $parsedSource )[1] ?? '';
+ $domain .= '.' . ( explode( '.', $parsedSource )[2] ?? '' );
+
+ if ( $domain ) {
+ if ( $this->options->get( 'ImportDumpInterwikiMap' )[$domain] ?? '' ) {
+ $domain = $this->options->get( 'ImportDumpInterwikiMap' )[$domain];
+ $subdomain = explode( '.', $parsedSource )[0] ?? '';
+
+ if ( $subdomain ) {
+ return $domain . ':' . $subdomain;
+ }
+ }
+ }
+ }
+
+ $dbr = $this->dbLoadBalancerFactory->getMainLB(
+ $this->getTarget()
+ )->getConnectionRef( DB_REPLICA, [], $this->getTarget() );
+
+ $row = $dbr->selectRow(
+ 'interwiki',
+ [
+ 'iw_prefix',
+ ],
+ [
+ 'iw_url' . $dbr->buildLike( $this->getSource(), $dbr->anyString() ),
+ ],
+ __METHOD__
+ );
+
+ if ( $row->iw_prefix ?? '' ) {
+ return $row->iw_prefix;
+ }
+
+ if (
+ ExtensionRegistry::getInstance()->isLoaded( 'Interwiki' ) &&
+ $this->config->get( 'InterwikiCentralDB' )
+ ) {
+ $dbr = $this->dbLoadBalancerFactory->getMainLB(
+ $this->config->get( 'InterwikiCentralDB' )
+ )->getConnectionRef( DB_REPLICA, [], $this->config->get( 'InterwikiCentralDB' ) );
+
+ $row = $dbr->selectRow(
+ 'interwiki',
+ [
+ 'iw_prefix',
+ ],
+ [
+ 'iw_url' . $dbr->buildLike( $this->getSource(), $dbr->anyString() ),
+ ],
+ __METHOD__
+ );
+
+ if ( $row->iw_prefix ?? '' ) {
+ return $row->iw_prefix;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ public function getCommand(): string {
+ $blankConfig = new GlobalVarConfig( '' );
+
+ $command = $this->options->get( 'ImportDumpScriptCommand' );
+
+ $userNamePrefix = $this->getInterwikiPrefix() ?:
+ $this->messageLocalizer->msg( 'importdump-unknown-username-prefix' )->text();
+
+ return str_replace( [
+ '{IP}',
+ '{wiki}',
+ '{username-prefix}',
+ '{file}',
+ ], [
+ $blankConfig->get( 'IP' ),
+ $this->getTarget(),
+ $userNamePrefix,
+ $this->getFile(),
+ ], $command );
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getUserGroupsFromTarget() {
+ $userName = $this->getRequester()->getName();
+ $userRightsProxy = UserRightsProxy::newFromName( $this->getTarget(), $userName );
+
+ return $this->userGroupManagerFactory
+ ->getUserGroupManager( $this->getTarget() )
+ ->getUserGroups( $userRightsProxy );
+ }
+
+ /**
+ * @return string
+ */
+ public function getFile(): string {
+ $fileName = $this->getTarget() . '-' . $this->getTimestamp() . '.xml';
+
+ return $this->options->get( 'UploadDirectory' ) . '/ImportDump/' . $fileName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getReason(): string {
+ return $this->row->request_reason;
+ }
+
+ /**
+ * @return User
+ */
+ public function getRequester(): User {
+ return $this->userFactory->newFromActorId( $this->row->request_actor );
+ }
+
+ /**
+ * @return string
+ */
+ public function getSource(): string {
+ return $this->row->request_source;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStatus(): string {
+ return $this->row->request_status;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTarget(): string {
+ return $this->row->request_target;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTimestamp(): string {
+ return $this->row->request_timestamp;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isLocked(): bool {
+ return (bool)$this->row->request_locked;
+ }
+
+ /**
+ * @param bool $forced
+ * @return bool
+ */
+ public function isPrivate( bool $forced = false ): bool {
+ if ( !$forced && $this->row->request_private ) {
+ return true;
+ }
+
+ if (
+ !ExtensionRegistry::getInstance()->isLoaded( 'CreateWiki' ) ||
+ !$this->config->get( 'CreateWikiUsePrivateWikis' )
+ ) {
+ return false;
+ }
+
+ $remoteWiki = new RemoteWiki( $this->getTarget() );
+ return (bool)$remoteWiki->isPrivate();
+ }
+
+ /**
+ * @param string $fname
+ */
+ public function startAtomic( string $fname ) {
+ $this->dbw->startAtomic( $fname );
+ }
+
+ /**
+ * @param int $locked
+ */
+ public function setLocked( int $locked ) {
+ $this->dbw->update(
+ 'importdump_requests',
+ [
+ 'request_locked' => $locked,
+ ],
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param int $private
+ */
+ public function setPrivate( int $private ) {
+ $this->dbw->update(
+ 'importdump_requests',
+ [
+ 'request_private' => $private,
+ ],
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param string $reason
+ */
+ public function setReason( string $reason ) {
+ $this->dbw->update(
+ 'importdump_requests',
+ [
+ 'request_reason' => $reason,
+ ],
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param string $source
+ */
+ public function setSource( string $source ) {
+ $this->dbw->update(
+ 'importdump_requests',
+ [
+ 'request_source' => $source,
+ ],
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param string $status
+ */
+ public function setStatus( string $status ) {
+ $this->dbw->update(
+ 'importdump_requests',
+ [
+ 'request_status' => $status,
+ ],
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param string $target
+ */
+ public function setTarget( string $target ) {
+ $this->dbw->update(
+ 'importdump_requests',
+ [
+ 'request_target' => $target,
+ ],
+ [
+ 'request_id' => $this->ID,
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param string $fname
+ */
+ public function endAtomic( string $fname ) {
+ $this->dbw->endAtomic( $fname );
+ }
+}
diff --git a/includes/ImportDumpRequestQueuePager.php b/includes/ImportDumpRequestQueuePager.php
new file mode 100644
index 0000000..b8053f6
--- /dev/null
+++ b/includes/ImportDumpRequestQueuePager.php
@@ -0,0 +1,169 @@
+get( 'ImportDumpCentralWiki' );
+ if ( $centralWiki ) {
+ $this->mDb = $dbLoadBalancerFactory->getMainLB(
+ $centralWiki
+ )->getConnectionRef( DB_REPLICA, [], $centralWiki );
+ } else {
+ $this->mDb = $dbLoadBalancerFactory->getMainLB()->getConnectionRef( DB_REPLICA );
+ }
+
+ $this->linkRenderer = $linkRenderer;
+ $this->userFactory = $userFactory;
+
+ $this->requester = $requester;
+ $this->status = $status;
+ $this->target = $target;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getFieldNames() {
+ return [
+ 'request_timestamp' => $this->msg( 'importdump-table-requested-date' )->text(),
+ 'request_actor' => $this->msg( 'importdump-table-requester' )->text(),
+ 'request_status' => $this->msg( 'importdump-table-status' )->text(),
+ 'request_target' => $this->msg( 'importdump-table-target' )->text(),
+ ];
+ }
+
+ /**
+ * @param string $name
+ * @param string $value
+ * @return string
+ */
+ public function formatValue( $name, $value ) {
+ $row = $this->mCurrentRow;
+
+ switch ( $name ) {
+ case 'request_timestamp':
+ $language = $this->getLanguage();
+ $formatted = $language->timeanddate( $row->request_timestamp );
+
+ break;
+ case 'request_target':
+ $formatted = $row->request_target;
+
+ break;
+ case 'request_status':
+ $formatted = $this->linkRenderer->makeLink(
+ SpecialPage::getTitleValueFor( 'ImportDumpRequestQueue', $row->request_id ),
+ $this->msg( 'importdump-label-' . $row->request_status )->text()
+ );
+
+ break;
+ case 'request_actor ':
+ $user = $this->userFactory->newFromActorId( $row->request_actor );
+ $formatted = $user->getName();
+
+ break;
+ default:
+ $formatted = "Unable to format $name";
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * @return array
+ */
+ public function getQueryInfo() {
+ $info = [
+ 'tables' => [
+ 'importdump_requests',
+ ],
+ 'fields' => [
+ 'request_actor',
+ 'request_id',
+ 'request_status',
+ 'request_timestamp',
+ 'request_target',
+ ],
+ 'conds' => [],
+ 'joins_conds' => [],
+ ];
+
+ if ( $this->target ) {
+ $info['conds']['request_target'] = $this->target;
+ }
+
+ if ( $this->requester ) {
+ $user = $this->userFactory->newFromName( $this->requester );
+ $info['conds']['request_actor'] = $user->getActorId();
+ }
+
+ if ( $this->status && $this->status != '*' ) {
+ $info['conds']['request_status'] = $this->status;
+ } elseif ( !$this->status ) {
+ $info['conds']['request_status'] = 'pending';
+ }
+
+ return $info;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDefaultSort() {
+ return 'request_id';
+ }
+
+ /**
+ * @param string $name
+ * @return bool
+ */
+ protected function isFieldSortable( $name ) {
+ return $name !== 'request_actor';
+ }
+}
diff --git a/includes/ImportDumpRequestViewer.php b/includes/ImportDumpRequestViewer.php
new file mode 100644
index 0000000..0cbef52
--- /dev/null
+++ b/includes/ImportDumpRequestViewer.php
@@ -0,0 +1,560 @@
+config = $config;
+ $this->context = $context;
+ $this->importDumpRequestManager = $importDumpRequestManager;
+ $this->permissionManager = $permissionManager;
+ }
+
+ /**
+ * @return array
+ */
+ public function getFormDescriptor(): array {
+ $user = $this->context->getUser();
+
+ if (
+ $this->importDumpRequestManager->isPrivate() &&
+ !$this->permissionManager->userHasRight( $user, 'view-private-import-dump-requests' )
+ ) {
+ $this->context->getOutput()->addHTML(
+ Html::errorBox( $this->context->msg( 'importdump-unknown' )->escaped() )
+ );
+
+ return [];
+ }
+
+ if ( $this->importDumpRequestManager->isLocked() ) {
+ $this->context->getOutput()->addHTML(
+ Html::errorBox( $this->context->msg( 'importdump-request-locked' )->escaped() )
+ );
+ }
+
+ $formDescriptor = [
+ 'source' => [
+ 'label-message' => 'importdump-label-source',
+ 'type' => 'url',
+ 'readonly' => true,
+ 'section' => 'details',
+ 'default' => $this->importDumpRequestManager->getSource(),
+ ],
+ 'target' => [
+ 'label-message' => 'importdump-label-target',
+ 'type' => 'text',
+ 'readonly' => true,
+ 'section' => 'details',
+ 'default' => $this->importDumpRequestManager->getTarget(),
+ ],
+ 'requester' => [
+ 'label-message' => 'importdump-label-requester',
+ 'type' => 'info',
+ 'section' => 'details',
+ 'default' => htmlspecialchars( $this->importDumpRequestManager->getRequester()->getName() ) .
+ Linker::userToolLinks(
+ $this->importDumpRequestManager->getRequester()->getId(),
+ $this->importDumpRequestManager->getRequester()->getName()
+ ),
+ 'raw' => true,
+ ],
+ 'requestedDate' => [
+ 'label-message' => 'importdump-label-requested-date',
+ 'type' => 'info',
+ 'section' => 'details',
+ 'default' => $this->context->getLanguage()->timeanddate(
+ $this->importDumpRequestManager->getTimestamp(), true
+ ),
+ ],
+ 'status' => [
+ 'label-message' => 'importdump-label-status',
+ 'type' => 'text',
+ 'readonly' => true,
+ 'section' => 'details',
+ 'default' => $this->context->msg(
+ 'importdump-label-' . $this->importDumpRequestManager->getStatus()
+ )->text(),
+ ],
+ 'reason' => [
+ 'type' => 'textarea',
+ 'rows' => 4,
+ 'readonly' => true,
+ 'label-message' => 'importdump-label-reason',
+ 'default' => $this->importDumpRequestManager->getReason(),
+ 'raw' => true,
+ 'cssclass' => 'importdump-infuse',
+ 'section' => 'details',
+ ],
+ ];
+
+ foreach ( $this->importDumpRequestManager->getComments() as $comment ) {
+ $formDescriptor['comment' . $comment['timestamp'] ] = [
+ 'type' => 'textarea',
+ 'readonly' => true,
+ 'section' => 'comments',
+ 'rows' => 4,
+ 'label-message' => [
+ 'importdump-header-comment-withtimestamp',
+ $comment['user']->getName(),
+ $this->context->getLanguage()->timeanddate( $comment['timestamp'], true ),
+ ],
+ 'default' => $comment['comment'],
+ ];
+ }
+
+ if (
+ $this->permissionManager->userHasRight( $user, 'handle-import-requests' ) ||
+ $user->getActorId() === $this->importDumpRequestManager->getRequester()->getActorId()
+ ) {
+ $formDescriptor += [
+ 'comment' => [
+ 'type' => 'textarea',
+ 'rows' => 4,
+ 'label-message' => 'importdump-label-comment',
+ 'section' => 'comments',
+ 'validation-callback' => [ $this, 'isValidComment' ],
+ 'disabled' => $this->importDumpRequestManager->isLocked(),
+ ],
+ 'submit-comment' => [
+ 'type' => 'submit',
+ 'default' => $this->context->msg( 'importdump-label-add-comment' )->text(),
+ 'section' => 'comments',
+ 'disabled' => $this->importDumpRequestManager->isLocked(),
+ ],
+ 'edit-source' => [
+ 'label-message' => 'importdump-label-source',
+ 'type' => 'url',
+ 'section' => 'editing',
+ 'required' => true,
+ 'default' => $this->importDumpRequestManager->getSource(),
+ 'disabled' => $this->importDumpRequestManager->isLocked(),
+ ],
+ 'edit-target' => [
+ 'label-message' => 'importdump-label-target',
+ 'type' => 'text',
+ 'section' => 'editing',
+ 'required' => true,
+ 'default' => $this->importDumpRequestManager->getTarget(),
+ 'validation-callback' => [ $this, 'isValidDatabase' ],
+ 'disabled' => $this->importDumpRequestManager->isLocked(),
+ ],
+ 'edit-reason' => [
+ 'type' => 'textarea',
+ 'rows' => 4,
+ 'label-message' => 'importdump-label-reason',
+ 'section' => 'editing',
+ 'required' => true,
+ 'default' => $this->importDumpRequestManager->getReason(),
+ 'validation-callback' => [ $this, 'isValidReason' ],
+ 'disabled' => $this->importDumpRequestManager->isLocked(),
+ 'raw' => true,
+ ],
+ 'submit-edit' => [
+ 'type' => 'submit',
+ 'default' => $this->context->msg( 'importdump-label-edit-request' )->text(),
+ 'section' => 'editing',
+ 'disabled' => $this->importDumpRequestManager->isLocked(),
+ ],
+ ];
+ }
+
+ if ( $this->permissionManager->userHasRight( $user, 'handle-import-requests' ) ) {
+ $validRequest = true;
+ $status = $this->importDumpRequestManager->getStatus();
+
+ $info = Html::warningBox(
+ $this->context->msg( 'importdump-info-command' )->plaintextParams(
+ $this->importDumpRequestManager->getCommand()
+ )->escaped()
+ );
+
+ $info .= Html::warningBox(
+ $this->context->msg( 'importdump-info-groups',
+ $this->importDumpRequestManager->getRequester()->getName(),
+ $this->importDumpRequestManager->getTarget(),
+ $this->context->getLanguage()->commaList(
+ $this->importDumpRequestManager->getUserGroupsFromTarget()
+ )
+ )->escaped()
+ );
+
+ if ( $this->importDumpRequestManager->isPrivate() ) {
+ $info .= Html::warningBox(
+ $this->context->msg( 'importdump-info-request-private' )->escaped()
+ );
+ }
+
+ if ( $this->importDumpRequestManager->getRequester()->getBlock() ) {
+ $info .= Html::warningBox(
+ $this->context->msg( 'importdump-info-requester-locally-blocked',
+ $this->importDumpRequestManager->getRequester()->getName(),
+ WikiMap::getCurrentWikiId()
+ )->escaped()
+ );
+ }
+
+ if ( $this->importDumpRequestManager->getRequester()->getGlobalBlock() ) {
+ $info .= Html::errorBox(
+ $this->context->msg( 'importdump-info-requester-globally-blocked',
+ $this->importDumpRequestManager->getRequester()->getName()
+ )->escaped()
+ );
+
+ $validRequest = false;
+ if ( $status === 'pending' || $status === 'inprogress' ) {
+ $status = 'declined';
+ }
+ }
+
+ if ( $this->importDumpRequestManager->getRequester()->isLocked() ) {
+ $info .= Html::errorBox(
+ $this->context->msg( 'importdump-info-requester-locked',
+ $this->importDumpRequestManager->getRequester()->getName()
+ )->escaped()
+ );
+
+ $validRequest = false;
+ if ( $status === 'pending' || $status === 'inprogress' ) {
+ $status = 'declined';
+ }
+ }
+
+ if ( !$this->importDumpRequestManager->getInterwikiPrefix() ) {
+ $info .= Html::errorBox(
+ $this->context->msg( 'importdump-info-no-interwiki-prefix',
+ $this->importDumpRequestManager->getTarget(),
+ $this->importDumpRequestManager->getSource()
+ )->escaped()
+ );
+
+ $validRequest = false;
+ if ( $status === 'pending' || $status === 'inprogress' ) {
+ $status = 'declined';
+ }
+ }
+
+ $formDescriptor += [
+ 'handle-info' => [
+ 'type' => 'info',
+ 'default' => $info,
+ 'raw' => true,
+ 'section' => 'handling',
+ ],
+ 'handle-lock' => [
+ 'type' => 'check',
+ 'label-message' => 'importdump-label-lock',
+ 'default' => $this->importDumpRequestManager->isLocked(),
+ 'section' => 'handling',
+ ],
+ ];
+
+ if ( $this->permissionManager->userHasRight( $user, 'view-private-import-requests' ) ) {
+ $formDescriptor += [
+ 'handle-private' => [
+ 'type' => 'check',
+ 'label-message' => 'importdump-label-private',
+ 'default' => $this->importDumpRequestManager->isPrivate(),
+ 'disabled' => $this->importDumpRequestManager->isPrivate( true ),
+ 'section' => 'handling',
+ ],
+ ];
+ }
+
+ $formDescriptor += [
+ 'handle-status' => [
+ 'type' => 'select',
+ 'label-message' => 'importdump-label-update-status',
+ 'options-messages' => [
+ 'importdump-label-pending' => 'pending',
+ 'importdump-label-inprogress' => 'inprogress',
+ 'importdump-label-complete' => 'complete',
+ 'importdump-label-declined' => 'declined',
+ ],
+ 'default' => $status,
+ 'disabled' => !$validRequest,
+ 'cssclass' => 'importdump-infuse',
+ 'section' => 'handling',
+ ],
+ 'handle-comment' => [
+ 'type' => 'textarea',
+ 'rows' => 4,
+ 'label-message' => 'importdump-label-status-updated-comment',
+ 'section' => 'handling',
+ ],
+ 'submit-handle' => [
+ 'type' => 'submit',
+ 'default' => $this->context->msg( 'htmlform-submit' )->text(),
+ 'section' => 'handling',
+ ],
+ ];
+ }
+
+ return $formDescriptor;
+ }
+
+ /**
+ * @param ?string $comment
+ * @param array $alldata
+ * @return string|bool
+ */
+ public function isValidComment( ?string $comment, array $alldata ) {
+ if ( isset( $alldata['submit-comment'] ) && ( !$comment || ctype_space( $comment ) ) ) {
+ return $this->context->msg( 'htmlform-required' )->escaped();
+ }
+
+ return true;
+ }
+
+ /**
+ * @param ?string $target
+ * @return string|bool
+ */
+ public function isValidDatabase( ?string $target ) {
+ if ( !in_array( $target, $this->config->get( 'LocalDatabases' ) ) ) {
+ return $this->context->msg( 'importdump-invalid-target' )->escaped();
+ }
+
+ return true;
+ }
+
+ /**
+ * @param ?string $reason
+ * @return string|bool
+ */
+ public function isValidReason( ?string $reason ) {
+ if ( !$reason || ctype_space( $reason ) ) {
+ return $this->context->msg( 'htmlform-required' )->escaped();
+ }
+
+ return true;
+ }
+
+ /**
+ * @param int $requestID
+ * @return ?ImportDumpOOUIForm
+ */
+ public function getForm( int $requestID ): ?ImportDumpOOUIForm {
+ $this->importDumpRequestManager->fromID( $requestID );
+ $out = $this->context->getOutput();
+
+ if ( $requestID === 0 || !$this->importDumpRequestManager->exists() ) {
+ $out->addHTML(
+ Html::errorBox( $this->context->msg( 'importdump-unknown' )->escaped() )
+ );
+
+ return null;
+ }
+
+ $out->addModules( [ 'ext.importdump.oouiform' ] );
+ $out->addModuleStyles( [ 'ext.importdump.oouiform.styles' ] );
+ $out->addModuleStyles( [ 'oojs-ui-widgets.styles' ] );
+
+ $formDescriptor = $this->getFormDescriptor();
+ $htmlForm = new ImportDumpOOUIForm( $formDescriptor, $this->context, 'importdump-section' );
+
+ $htmlForm->setId( 'importdump-request-viewer' );
+ $htmlForm->suppressDefaultSubmit();
+ $htmlForm->setSubmitCallback(
+ function ( array $formData, HTMLForm $form ) {
+ return $this->submitForm( $formData, $form );
+ }
+ );
+
+ return $htmlForm;
+ }
+
+ /**
+ * @param array $formData
+ * @param HTMLForm $form
+ */
+ protected function submitForm(
+ array $formData,
+ HTMLForm $form
+ ) {
+ $user = $form->getUser();
+ if ( !$user->isRegistered() ) {
+ throw new UserNotLoggedIn( 'exception-nologin-text', 'exception-nologin' );
+ }
+
+ $out = $form->getContext()->getOutput();
+
+ if ( isset( $formData['submit-comment'] ) ) {
+ $this->importDumpRequestManager->addComment( $formData['comment'], $user );
+ $out->addHTML( Html::successBox( $this->context->msg( 'importdump-comment-success' )->escaped() ) );
+
+ return;
+ }
+
+ if ( isset( $formData['submit-edit'] ) ) {
+ $this->importDumpRequestManager->startAtomic( __METHOD__ );
+
+ $changes = [];
+ if ( $this->importDumpRequestManager->getReason() !== $formData['edit-reason'] ) {
+ $changes[] = $this->context->msg( 'importdump-request-edited-reason' )->plaintextParams(
+ $this->importDumpRequestManager->getReason(),
+ $formData['edit-reason']
+ )->escaped();
+
+ $this->importDumpRequestManager->setReason( $formData['edit-reason'] );
+ }
+
+ if ( $this->importDumpRequestManager->getSource() !== $formData['edit-source'] ) {
+ $changes[] = $this->context->msg( 'importdump-request-edited-source' )->plaintextParams(
+ $this->importDumpRequestManager->getSource(),
+ $formData['edit-source']
+ )->escaped();
+
+ $this->importDumpRequestManager->setSource( $formData['edit-source'] );
+ }
+
+ if ( $this->importDumpRequestManager->getTarget() !== $formData['edit-target'] ) {
+ $changes[] = $this->context->msg(
+ 'importdump-request-edited-target',
+ $this->importDumpRequestManager->getTarget(),
+ $formData['edit-target']
+ )->escaped();
+
+ $this->importDumpRequestManager->setTarget( $formData['edit-target'] );
+ }
+
+ if ( !$changes ) {
+ $this->importDumpRequestManager->endAtomic( __METHOD__ );
+
+ $out->addHTML( Html::errorBox( $this->context->msg( 'importdump-no-changes' )->escaped() ) );
+
+ return;
+ }
+
+ if ( $this->importDumpRequestManager->getStatus() === 'declined' ) {
+ $this->importDumpRequestManager->setStatus( 'pending' );
+
+ $comment = $this->context->msg( 'importdump-request-reopened', $user->getName() )->rawParams(
+ implode( "\n", $changes )
+ )->inContentLanguage()->escaped();
+
+ $this->importDumpRequestManager->logStatusUpdate( $comment, 'pending', $user );
+
+ $this->importDumpRequestManager->addComment( $comment, User::newSystemUser( 'ImportDump Extension' ) );
+
+ $this->importDumpRequestManager->sendNotification(
+ $comment, 'importdump-request-status-update', $user
+ );
+ } else {
+ $comment = $this->context->msg( 'importdump-request-edited', $user->getName() )->rawParams(
+ implode( "\n", $changes )
+ )->inContentLanguage()->escaped();
+
+ $this->importDumpRequestManager->addComment( $comment, User::newSystemUser( 'ImportDump Extension' ) );
+ }
+
+ $this->importDumpRequestManager->endAtomic( __METHOD__ );
+
+ $out->addHTML( Html::successBox( $this->context->msg( 'importdump-edit-success' )->escaped() ) );
+
+ return;
+ }
+
+ if ( isset( $formData['submit-handle'] ) ) {
+ $this->importDumpRequestManager->startAtomic( __METHOD__ );
+ $changes = [];
+
+ if ( $this->importDumpRequestManager->isLocked() !== (bool)$formData['handle-lock'] ) {
+ $this->importDumpRequestManager->setLocked( (int)$formData['handle-lock'] );
+ $changes[] = 'locked';
+ }
+
+ if (
+ isset( $formData['handle-private'] ) &&
+ $this->importDumpRequestManager->isPrivate() !== (bool)$formData['handle-private']
+ ) {
+ $this->importDumpRequestManager->setPrivate( (int)$formData['handle-private'] );
+ $changes[] = 'private';
+ }
+
+ if ( $this->importDumpRequestManager->getStatus() === $formData['handle-status'] ) {
+ $this->importDumpRequestManager->endAtomic( __METHOD__ );
+
+ if ( !$changes ) {
+ $out->addHTML( Html::errorBox( $this->context->msg( 'importdump-no-changes' )->escaped() ) );
+ return;
+ }
+
+ if ( in_array( 'private', $changes ) ) {
+ $out->addHTML( Html::successBox( $this->context->msg( 'importdump-success-private' )->escaped() ) );
+ }
+
+ if ( in_array( 'locked', $changes ) ) {
+ $out->addHTML( Html::successBox( $this->context->msg( 'importdump-success-locked' )->escaped() ) );
+ }
+
+ return;
+ }
+
+ $this->importDumpRequestManager->setStatus( $formData['handle-status'] );
+
+ $statusMessage = $this->context->msg( 'importdump-label-' . $formData['handle-status'] )
+ ->inContentLanguage()
+ ->text();
+
+ $comment = $this->context->msg( 'importdump-status-updated', strtolower( $statusMessage ) )
+ ->inContentLanguage()
+ ->escaped();
+
+ if ( $formData['handle-comment'] ) {
+ $commentUser = User::newSystemUser( 'ImportDump Status Update' );
+
+ $comment .= "\n" . $this->context->msg( 'importdump-comment-given', $user->getName() )
+ ->inContentLanguage()
+ ->escaped();
+
+ $comment .= ' ' . $formData['handle-comment'];
+ }
+
+ $this->importDumpRequestManager->addComment( $comment, $commentUser ?? $user );
+ $this->importDumpRequestManager->logStatusUpdate(
+ $formData['handle-comment'], $formData['handle-status'], $user
+ );
+
+ $this->importDumpRequestManager->sendNotification( $comment, 'importdump-request-status-update', $user );
+
+ $this->importDumpRequestManager->endAtomic( __METHOD__ );
+
+ $out->addHTML( Html::successBox( $this->context->msg( 'importdump-status-updated-success' )->escaped() ) );
+ }
+ }
+}
diff --git a/includes/Notifications/EchoNewRequestPresentationModel.php b/includes/Notifications/EchoNewRequestPresentationModel.php
new file mode 100644
index 0000000..226113e
--- /dev/null
+++ b/includes/Notifications/EchoNewRequestPresentationModel.php
@@ -0,0 +1,63 @@
+msg(
+ 'importdump-notification-header-new-request',
+ $this->event->getExtraParam( 'request-id' )
+ );
+ }
+
+ /**
+ * @return Message
+ */
+ public function getBodyMessage() {
+ $reason = EchoDiscussionParser::getTextSnippet(
+ $this->event->getExtraParam( 'reason' ),
+ $this->language
+ );
+
+ return $this->msg( 'importdump-notification-body-new-request',
+ $reason,
+ $this->event->getExtraParam( 'requester' ),
+ $this->event->getExtraParam( 'target' )
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function getPrimaryLink() {
+ return false;
+ }
+
+ /**
+ * @return array
+ */
+ public function getSecondaryLinks() {
+ $visitLink = [
+ 'url' => $this->event->getExtraParam( 'request-url', 0 ),
+ 'label' => $this->msg( 'importdump-notification-visit-request' )->text(),
+ 'prioritized' => true,
+ ];
+
+ return [ $visitLink ];
+ }
+}
diff --git a/includes/Notifications/EchoRequestCommentPresentationModel.php b/includes/Notifications/EchoRequestCommentPresentationModel.php
new file mode 100644
index 0000000..520756b
--- /dev/null
+++ b/includes/Notifications/EchoRequestCommentPresentationModel.php
@@ -0,0 +1,58 @@
+msg(
+ 'importdump-notification-header-comment',
+ $this->event->getExtraParam( 'request-id' )
+ );
+ }
+
+ /**
+ * @return Message
+ */
+ public function getBodyMessage() {
+ $comment = $this->event->getExtraParam( 'comment' );
+ $text = EchoDiscussionParser::getTextSnippet( $comment, $this->language );
+
+ return new RawMessage( '$1', [ $text ] );
+ }
+
+ /**
+ * @return bool
+ */
+ public function getPrimaryLink() {
+ return false;
+ }
+
+ /**
+ * @return array
+ */
+ public function getSecondaryLinks() {
+ $visitLink = [
+ 'url' => $this->event->getExtraParam( 'request-url', 0 ),
+ 'label' => $this->msg( 'importdump-notification-visit-request' )->text(),
+ 'prioritized' => true,
+ ];
+
+ return [ $visitLink ];
+ }
+}
diff --git a/includes/Notifications/EchoRequestStatusUpdatePresentationModel.php b/includes/Notifications/EchoRequestStatusUpdatePresentationModel.php
new file mode 100644
index 0000000..5ed70dc
--- /dev/null
+++ b/includes/Notifications/EchoRequestStatusUpdatePresentationModel.php
@@ -0,0 +1,56 @@
+msg(
+ 'importdump-notification-header-status-update',
+ $this->event->getExtraParam( 'request-id' )
+ );
+ }
+
+ /**
+ * @return Message
+ */
+ public function getBodyMessage() {
+ $comment = $this->event->getExtraParam( 'comment' );
+
+ return new RawMessage( '$1', [ nl2br( htmlspecialchars( $comment ) ) ] );
+ }
+
+ /**
+ * @return bool
+ */
+ public function getPrimaryLink() {
+ return false;
+ }
+
+ /**
+ * @return array
+ */
+ public function getSecondaryLinks() {
+ $visitLink = [
+ 'url' => $this->event->getExtraParam( 'request-url', 0 ),
+ 'label' => $this->msg( 'importdump-notification-visit-request' )->text(),
+ 'prioritized' => true,
+ ];
+
+ return [ $visitLink ];
+ }
+}
diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php
new file mode 100644
index 0000000..7257f9f
--- /dev/null
+++ b/includes/ServiceWiring.php
@@ -0,0 +1,22 @@
+ static function ( MediaWikiServices $services ): ImportDumpRequestManager {
+ return new ImportDumpRequestManager(
+ $services->getConfigFactory()->makeConfig( 'ImportDump' ),
+ $services->getDBLoadBalancerFactory(),
+ $services->getLinkRenderer(),
+ RequestContext::getMain(),
+ new ServiceOptions(
+ ImportDumpRequestManager::CONSTRUCTOR_OPTIONS,
+ $services->getConfigFactory()->makeConfig( 'ImportDump' )
+ ),
+ $services->getUserFactory(),
+ $services->getUserGroupManagerFactory()
+ );
+ },
+];
diff --git a/includes/Specials/SpecialImportDumpRequestQueue.php b/includes/Specials/SpecialImportDumpRequestQueue.php
new file mode 100644
index 0000000..97dab6e
--- /dev/null
+++ b/includes/Specials/SpecialImportDumpRequestQueue.php
@@ -0,0 +1,139 @@
+dbLoadBalancerFactory = $dbLoadBalancerFactory;
+ $this->importDumpRequestManager = $importDumpRequestManager;
+ $this->permissionManager = $permissionManager;
+ $this->userFactory = $userFactory;
+ }
+
+ /**
+ * @param string $par
+ */
+ public function execute( $par ) {
+ $this->setHeaders();
+
+ if ( $par ) {
+ $this->lookupRequest( $par );
+ return;
+ }
+
+ $this->doPagerStuff();
+ }
+
+ private function doPagerStuff() {
+ $requester = $this->getRequest()->getText( 'requester' );
+ $status = $this->getRequest()->getText( 'status' );
+ $target = $this->getRequest()->getText( 'target' );
+
+ $formDescriptor = [
+ 'target' => [
+ 'type' => 'text',
+ 'name' => 'target',
+ 'label-message' => 'importdump-label-target',
+ 'default' => $target,
+ ],
+ 'requester' => [
+ 'type' => 'user',
+ 'name' => 'requester',
+ 'label-message' => 'importdump-label-requester',
+ 'exist' => true,
+ 'default' => $requester,
+ ],
+ 'status' => [
+ 'type' => 'select',
+ 'name' => 'status',
+ 'label-message' => 'importdump-label-status',
+ 'options-messages' => [
+ 'importdump-label-pending' => 'pending',
+ 'importdump-label-inprogress' => 'inprogress',
+ 'importdump-label-complete' => 'complete',
+ 'importdump-label-declined' => 'declined',
+ 'importdump-label-all' => '*',
+ ],
+ 'default' => $status ?: 'pending',
+ ],
+ ];
+
+ $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
+ $htmlForm->setMethod( 'get' )->prepareForm()->displayForm( false );
+
+ $pager = new ImportDumpRequestQueuePager(
+ $this->getConfig(),
+ $this->getContext(),
+ $this->dbLoadBalancerFactory,
+ $this->getLinkRenderer(),
+ $this->userFactory,
+ $requester,
+ $status,
+ $target
+ );
+
+ $table = $pager->getFullOutput();
+
+ $this->getOutput()->addParserOutputContent( $table );
+ }
+
+ /**
+ * @param string $par
+ */
+ private function lookupRequest( $par ) {
+ $requestViewer = new ImportDumpRequestViewer(
+ $this->getConfig(),
+ $this->getContext(),
+ $this->importDumpRequestManager,
+ $this->permissionManager
+ );
+
+ $htmlForm = $requestViewer->getForm( (int)$par );
+
+ if ( $htmlForm ) {
+ $htmlForm->show();
+ }
+ }
+
+ /**
+ * @return string
+ */
+ protected function getGroupName() {
+ return 'other';
+ }
+}
diff --git a/includes/Specials/SpecialRequestImportDump.php b/includes/Specials/SpecialRequestImportDump.php
new file mode 100644
index 0000000..e43cb88
--- /dev/null
+++ b/includes/Specials/SpecialRequestImportDump.php
@@ -0,0 +1,449 @@
+dbLoadBalancerFactory = $dbLoadBalancerFactory;
+ $this->mimeAnalyzer = $mimeAnalyzer;
+ $this->repoGroup = $repoGroup;
+ $this->userFactory = $userFactory;
+ }
+
+ /**
+ * @param string $par
+ */
+ public function execute( $par ) {
+ $this->setParameter( $par );
+ $this->setHeaders();
+
+ if (
+ $this->getConfig()->get( 'ImportDumpCentralWiki' ) &&
+ !WikiMap::isCurrentWikiId( $this->getConfig()->get( 'ImportDumpCentralWiki' ) )
+ ) {
+ throw new ErrorPageError( 'importdump-notcentral', 'importdump-notcentral-text' );
+ }
+
+ if ( !$this->getUser()->isRegistered() ) {
+ $loginURL = SpecialPage::getTitleFor( 'Userlogin' )
+ ->getFullURL( [
+ 'returnto' => $this->getPageTitle()->getPrefixedText(),
+ ]
+ );
+
+ throw new UserNotLoggedIn( 'importdump-notloggedin', 'exception-nologin', [ $loginURL ] );
+ }
+
+ $this->checkPermissions();
+
+ if ( $this->getConfig()->get( 'ImportDumpHelpUrl' ) ) {
+ $this->getOutput()->addHelpLink( $this->getConfig()->get( 'ImportDumpHelpUrl' ), true );
+ }
+
+ $form = $this->getForm();
+ if ( $form->show() ) {
+ $this->onSuccess();
+ }
+ }
+
+ /**
+ * @return array
+ */
+ protected function getFormFields() {
+ $formDescriptor = [
+ 'source' => [
+ 'type' => 'url',
+ 'label-message' => 'importdump-label-source',
+ 'help-message' => 'importdump-help-source',
+ 'required' => true,
+ ],
+ 'target' => [
+ 'type' => 'text',
+ 'label-message' => 'importdump-label-target',
+ 'help-message' => 'importdump-help-target',
+ 'required' => true,
+ 'validation-callback' => [ $this, 'isValidDatabase' ],
+ ],
+ ];
+
+ if (
+ UploadFromUrl::isEnabled() &&
+ UploadFromUrl::isAllowed( $this->getUser() ) === true
+ ) {
+ $formDescriptor += [
+ 'UploadSourceType' => [
+ 'type' => 'radio',
+ 'label-message' => 'importdump-label-upload-source-type',
+ 'default' => 'File',
+ 'options-messages' => [
+ 'importdump-label-upload-source-file' => 'File',
+ 'importdump-label-upload-source-url' => 'Url',
+ ],
+ ],
+ 'UploadFile' => [
+ 'type' => 'file',
+ 'label-message' => 'importdump-label-upload-file',
+ 'help-message' => 'importdump-help-upload',
+ 'hide-if' => [ '!==', 'wpUploadSourceType', 'File' ],
+ 'required' => true,
+ ],
+ 'UploadFileURL' => [
+ 'type' => 'url',
+ 'label-message' => 'importdump-label-upload-file-url',
+ 'help-message' => 'importdump-help-upload',
+ 'hide-if' => [ '!==', 'wpUploadSourceType', 'Url' ],
+ 'required' => true,
+ ],
+ ];
+ } else {
+ $formDescriptor += [
+ 'UploadFile' => [
+ 'type' => 'file',
+ 'label-message' => 'importdump-label-upload-file',
+ 'help-message' => 'importdump-help-upload',
+ 'required' => true,
+ ],
+ ];
+ }
+
+ $formDescriptor += [
+ 'reason' => [
+ 'type' => 'textarea',
+ 'rows' => 4,
+ 'label-message' => 'importdump-label-reason',
+ 'required' => true,
+ 'validation-callback' => [ $this, 'isValidReason' ],
+ ],
+ ];
+
+ return $formDescriptor;
+ }
+
+ /**
+ * @param array $data
+ * @return Status
+ */
+ public function onSubmit( array $data ) {
+ $token = $this->getRequest()->getVal( 'wpEditToken' );
+ $userToken = $this->getContext()->getCsrfTokenSet();
+
+ if ( !$userToken->matchToken( $token ) ) {
+ return Status::newFatal( 'sessionfailure' );
+ }
+
+ if (
+ $this->getUser()->pingLimiter( 'requestimportdump' ) ||
+ UploadBase::isThrottled( $this->getUser() )
+ ) {
+ return Status::newFatal( 'actionthrottledtext' );
+ }
+
+ $centralWiki = $this->getConfig()->get( 'ImportDumpCentralWiki' );
+ if ( $centralWiki ) {
+ $dbw = $this->dbLoadBalancerFactory->getMainLB(
+ $centralWiki
+ )->getConnectionRef( DB_PRIMARY, [], $centralWiki );
+ } else {
+ $dbw = $this->dbLoadBalancerFactory->getMainLB()->getConnectionRef( DB_PRIMARY );
+ }
+
+ $duplicate = $dbw->selectRow(
+ 'importdump_requests',
+ '*',
+ [
+ 'request_reason' => $data['reason'],
+ 'request_status' => 'pending',
+ ],
+ __METHOD__
+ );
+
+ if ( (bool)$duplicate ) {
+ return Status::newFatal( 'importdump-duplicate-request' );
+ }
+
+ $timestamp = $dbw->timestamp();
+ $fileName = $data['target'] . '-' . $timestamp . '.xml';
+
+ $request = $this->getRequest();
+ $request->setVal( 'wpDestFile', $fileName );
+
+ $uploadBase = UploadBase::createFromRequest( $request, $data['UploadSourceType'] ?? 'File' );
+
+ if ( !$uploadBase->isEnabled() ) {
+ return Status::newFatal( 'uploaddisabled' );
+ }
+
+ $permission = $uploadBase->isAllowed( $this->getUser() );
+ if ( $permission !== true ) {
+ return User::newFatalPermissionDeniedStatus( $permission );
+ }
+
+ $status = $uploadBase->fetchFile();
+ if ( !$status->isOK() ) {
+ return $status;
+ }
+
+ $virus = UploadBase::detectVirus( $uploadBase->getTempPath() );
+ if ( $virus ) {
+ return Status::newFatal( 'uploadvirus', $virus );
+ }
+
+ $mime = $this->mimeAnalyzer->guessMimeType( $uploadBase->getTempPath() );
+ if ( $mime !== 'application/xml' ) {
+ return Status::newFatal( 'filetype-mime-mismatch', 'xml', $mime );
+ }
+
+ $mimeExt = $this->mimeAnalyzer->getExtensionFromMimeTypeOrNull( $mime );
+ if ( $mimeExt !== 'xml' ) {
+ return Status::newFatal(
+ 'filetype-banned-type', $mimeExt ?? 'unknown', 'xml', 1, 1
+ );
+ }
+
+ $status = $uploadBase->tryStashFile( $this->getUser() );
+ if ( !$status->isGood() ) {
+ return $status;
+ }
+
+ $repo = $this->repoGroup->getLocalRepo();
+ $uploadStash = new UploadStash( $repo, $this->getUser() );
+
+ $fileKey = $status->getStatusValue()->getValue()->getFileKey();
+ $file = $uploadStash->getFile( $fileKey );
+
+ $status = $repo->publish(
+ $file->getPath(),
+ '/ImportDump/' . $fileName,
+ '/ImportDump/archive/' . $fileName,
+ FileRepo::DELETE_SOURCE
+ );
+
+ if ( !$status->isOK() ) {
+ return $status;
+ }
+
+ $dbw->insert(
+ 'importdump_requests',
+ [
+ 'request_source' => $data['source'],
+ 'request_target' => $data['target'],
+ 'request_reason' => $data['reason'],
+ 'request_status' => 'pending',
+ 'request_actor' => $this->getUser()->getActorId(),
+ 'request_timestamp' => $timestamp,
+ ],
+ __METHOD__,
+ [ 'IGNORE' ]
+ );
+
+ $requestID = (string)$dbw->insertId();
+ $requestQueueLink = SpecialPage::getTitleValueFor( 'ImportDumpRequestQueue', $requestID );
+
+ $requestLink = $this->getLinkRenderer()->makeLink( $requestQueueLink, "#{$requestID}" );
+
+ $this->getOutput()->addHTML(
+ Html::successBox(
+ $this->msg( 'importdump-success' )->rawParams( $requestLink )->escaped()
+ )
+ );
+
+ $logEntry = new ManualLogEntry( $this->getLogType( $data['target'] ), 'request' );
+
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setTarget( $requestQueueLink );
+ $logEntry->setComment( $data['reason'] );
+
+ $logEntry->setParameters(
+ [
+ '4::requestTarget' => $data['target'],
+ '5::requestLink' => Message::rawParam( $requestLink ),
+ ]
+ );
+
+ $logID = $logEntry->insert( $dbw );
+ $logEntry->publish( $logID );
+
+ if ( $this->getConfig()->get( 'ImportDumpUsersNotifiedOnAllRequests' ) ) {
+ $this->sendNotifications( $data['reason'], $this->getUser()->getName(), $requestID, $data['target'] );
+ }
+
+ return Status::newGood();
+ }
+
+ /**
+ * @param string $target
+ * @return string
+ */
+ public function getLogType( string $target ): string {
+ if (
+ !ExtensionRegistry::getInstance()->isLoaded( 'CreateWiki' ) ||
+ !$this->getConfig()->get( 'CreateWikiUsePrivateWikis' )
+ ) {
+ return 'importdump';
+ }
+
+ $remoteWiki = new RemoteWiki( $target );
+ return $remoteWiki->isPrivate() ? 'importdumpprivate' : 'importdump';
+ }
+
+ /**
+ * @param string $reason
+ * @param string $requester
+ * @param string $requestID
+ * @param string $target
+ */
+ public function sendNotifications( string $reason, string $requester, string $requestID, string $target ) {
+ $notifiedUsers = array_filter(
+ array_map(
+ function ( string $userName ): ?User {
+ return $this->userFactory->newFromName( $userName );
+ }, $this->getConfig()->get( 'ImportDumpUsersNotifiedOnAllRequests' )
+ )
+ );
+
+ $requestLink = SpecialPage::getTitleFor( 'ImportDumpRequestQueue', $requestID )->getFullURL();
+
+ foreach ( $notifiedUsers as $receiver ) {
+ if (
+ !$receiver->isAllowed( 'handle-import-dump-requests' ) ||
+ (
+ $this->getLogType( $target ) === 'importdumpprivate' &&
+ !$receiver->isAllowed( 'view-private-import-dump-requests' )
+ )
+ ) {
+ continue;
+ }
+
+ EchoEvent::create( [
+ 'type' => 'importdump-new-request',
+ 'extra' => [
+ 'request-id' => $requestID,
+ 'request-url' => $requestLink,
+ 'reason' => $reason,
+ 'requester' => $requester,
+ 'target' => $target,
+ 'notifyAgent' => true,
+ ],
+ 'agent' => $receiver,
+ ] );
+ }
+ }
+
+ /**
+ * @param ?string $target
+ * @return string|bool
+ */
+ public function isValidDatabase( ?string $target ) {
+ if ( !in_array( $target, $this->getConfig()->get( 'LocalDatabases' ) ) ) {
+ return $this->msg( 'importdump-invalid-target' )->escaped();
+ }
+
+ return true;
+ }
+
+ /**
+ * @param ?string $reason
+ * @return string|bool
+ */
+ public function isValidReason( ?string $reason ) {
+ if ( !$reason || ctype_space( $reason ) ) {
+ return $this->msg( 'htmlform-required' )->escaped();
+ }
+
+ return true;
+ }
+
+ public function checkPermissions() {
+ parent::checkPermissions();
+
+ $user = $this->getUser();
+ $permissionRequired = UploadBase::isAllowed( $user );
+ if ( $permissionRequired !== true ) {
+ throw new PermissionsError( $permissionRequired );
+ }
+
+ $block = $user->getBlock();
+ if (
+ $block && (
+ $user->isBlockedFromUpload() ||
+ $block->appliesToRight( 'request-import-dump' )
+ )
+ ) {
+ throw new UserBlockedError( $block );
+ }
+
+ $globalBlock = $user->getGlobalBlock();
+ if ( $globalBlock ) {
+ throw new UserBlockedError( $globalBlock );
+ }
+
+ $this->checkReadOnly();
+ if ( !UploadBase::isEnabled() ) {
+ throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ protected function getDisplayFormat() {
+ return 'ooui';
+ }
+
+ /**
+ * @return string
+ */
+ protected function getGroupName() {
+ return 'other';
+ }
+}
diff --git a/modules/ext.importdump.oouiform.ooui.js b/modules/ext.importdump.oouiform.ooui.js
new file mode 100644
index 0000000..a55be1d
--- /dev/null
+++ b/modules/ext.importdump.oouiform.ooui.js
@@ -0,0 +1,108 @@
+( function () {
+ $( function () {
+ var tabs, previousTab, switchingNoHash;
+
+ tabs = OO.ui.infuse( $( '.importdump-tabs' ) );
+
+ tabs.$element.addClass( 'importdump-tabs-infused' );
+
+ function enhancePanel( panel ) {
+ var $infuse = $( panel.$element ).find( '.importdump-infuse' );
+ $infuse.each( function () {
+ try {
+ OO.ui.infuse( this );
+ } catch ( error ) {
+ return;
+ }
+ } );
+
+ if ( !panel.$element.data( 'mw-section-infused' ) ) {
+ panel.$element.removeClass( 'mw-htmlform-autoinfuse-lazy' );
+ mw.hook( 'htmlform.enhance' ).fire( panel.$element );
+ panel.$element.data( 'mw-section-infused', true );
+ }
+ }
+
+ function onTabPanelSet( panel ) {
+ var scrollTop, active;
+
+ if ( switchingNoHash ) {
+ return;
+ }
+ // Handle hash manually to prevent jumping,
+ // therefore save and restore scrollTop to prevent jumping.
+ scrollTop = $( window ).scrollTop();
+ // Changing the hash apparently causes keyboard focus to be lost?
+ // Save and restore it. This makes no sense though.
+ active = document.activeElement;
+ location.hash = '#' + panel.getName();
+ if ( active ) {
+ active.focus();
+ }
+ $( window ).scrollTop( scrollTop );
+ }
+
+ tabs.on( 'set', onTabPanelSet );
+
+ /**
+ * @ignore
+ * @param {string} name The name of a tab
+ * @param {boolean} [noHash] A hash will be set according to the current
+ * open section. Use this flag to suppress this.
+ */
+ function switchImportDumpTab( name, noHash ) {
+ if ( noHash ) {
+ switchingNoHash = true;
+ }
+ tabs.setTabPanel( name );
+ enhancePanel( tabs.getCurrentTabPanel() );
+ if ( noHash ) {
+ switchingNoHash = false;
+ }
+ }
+
+ // Jump to correct section as indicated by the hash.
+ // This function is called onload and onhashchange.
+ function detectHash() {
+ var hash = location.hash,
+ matchedElement, $parentSection;
+ if ( hash.match( /^#mw-section-[\w-]+$/ ) ) {
+ mw.storage.session.remove( 'importdump-prevTab' );
+ switchImportDumpTab( hash.slice( 1 ) );
+ } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+ matchedElement = document.getElementById( hash.slice( 1 ) );
+ $parentSection = $( matchedElement ).closest( '.importdump-section-fieldset' );
+ if ( $parentSection.length ) {
+ mw.storage.session.remove( 'importdump-prevTab' );
+ // Switch to proper tab and scroll to selected item.
+ switchImportDumpTab( $parentSection.attr( 'id' ), true );
+ matchedElement.scrollIntoView();
+ }
+ }
+ }
+
+ $( window ).on( 'hashchange', function () {
+ var hash = location.hash;
+ if ( hash.match( /^#mw-[\w-]+/ ) ) {
+ detectHash();
+ } else if ( hash === '' ) {
+ switchImportDumpTab( $( '[id*=mw-section-]' ).attr( 'id' ), true );
+ }
+ } )
+ // Run the function immediately to select the proper tab on startup.
+ .trigger( 'hashchange' );
+
+ // Restore the active tab after saving
+ previousTab = mw.storage.session.get( 'importdump-prevTab' );
+ if ( previousTab ) {
+ switchImportDumpTab( previousTab, true );
+ // Deleting the key, the tab states should be reset until we press Save
+ mw.storage.session.remove( 'importdump-prevTab' );
+ }
+
+ $( '#importdump-form' ).on( 'submit', function () {
+ var value = tabs.getCurrentTabPanelName();
+ mw.storage.session.set( 'importdump-prevTab', value );
+ } );
+ } );
+}() );
diff --git a/modules/ext.importdump.oouiform.ooui.less b/modules/ext.importdump.oouiform.ooui.less
new file mode 100644
index 0000000..289fb63
--- /dev/null
+++ b/modules/ext.importdump.oouiform.ooui.less
@@ -0,0 +1,106 @@
+#importdump {
+ filter: brightness( 1 );
+}
+
+.importdump-tabs {
+ .importdump-fieldset-wrapper {
+ padding-right: 0;
+ padding-left: 0;
+
+ &:first-child {
+ padding-top: 0;
+ }
+
+ &:last-child {
+ padding-bottom: 0;
+ }
+ }
+}
+
+.importdump-tabs-wrapper.oo-ui-panelLayout-framed,
+.importdump-tabs > .oo-ui-menuLayout-content > .oo-ui-indexLayout-stackLayout > .oo-ui-tabPanelLayout {
+ /* Decrease contrast of `border` slightly as padding/border combination is sufficient
+ * accessibility wise and focus of content is more important here. */
+ border-color: #c8ccd1;
+}
+
+/* JavaScript disabled */
+.client-nojs {
+ // Disable .oo-ui-panelLayout-framed on outer wrapper
+ .importdump-tabs-wrapper {
+ border-width: 0;
+ border-radius: 0;
+ }
+
+ .importdump-tabs {
+ // Hide the tab menu when JS is disabled as we can't use this feature
+ > .oo-ui-menuLayout-menu {
+ display: none;
+ }
+
+ .importdump-section-fieldset {
+ //