diff --git a/package.json b/package.json index ede2e10dcc..0cc7215166 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "js-yaml": "^4.1.0", "mdx-mermaid": "^1.2.2", "mermaid": "^8.14.0", - "nodemw": "^0.16.0", + "nodemw": "^0.17.0", "plugin-image-zoom": "flexanalytics/plugin-image-zoom", "prism-react-renderer": "^1.3.1", "raw-loader": "^4.0.2", @@ -87,4 +87,4 @@ "typescript": "^4.6.3", "unist-util-inspect": "6.0.0" } -} \ No newline at end of file +} diff --git a/scripts/utils.js b/scripts/utils.js index b537731096..aa3ef6c712 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -55,6 +55,42 @@ const getGetMigratedPageIds = (logger) => (client) => async (pageTitle) => { return articleData; }; +const getGetPageProtectionState = (logger) => (client) => async (pageTitle) => { + logger.debug(`===> Fetching protection state for ${pageTitle}`); + const options = { + inprop: [ + 'protection', + ], + }; + return new Promise((resolve, reject) => { + client.getArticleInfo([pageTitle], options, (err, [data]) => { + if (err) { + reject(err); + } + resolve({ + pageTitle, + data, + }); + }); + }); +}; + +const getProtectPage = (logger) => (client) => async (pageTitle, protections, options) => { + logger.debug(`===> Setting protection state for ${pageTitle} to ${protections}`); + + return new Promise((resolve, reject) => { + client.protect(pageTitle, protections, options, (err, data) => { + if (err) { + reject(err); + } + resolve({ + pageTitle, + data, + }); + }); + }); +}; + const getClient = (remoteHost) => new Bot({ protocol: 'https', server: remoteHost, @@ -129,10 +165,116 @@ const getObsoletePagePath = () => getNormalizedPath('data', 'obsoletePages.json' */ const getMigrationPagePath = () => getNormalizedPath('data', 'migratedPages.yml'); +const getUpdateMigratedPagesProtection = (logger) => (client) => async () => { + const migratedPageData = yaml.load(await readFile(getMigrationPagePath(), 'utf8')); + + const getPageProtectionState = (pageTitle) => getGetPageProtectionState(logger)(client)(pageTitle); + const protectPage = (pageTitle, protections, reason) => getProtectPage(logger)(client)( + pageTitle, + protections, + { + // This is a bot. It does not need to subscribe. + watchlist: 'nochange', + reason, + }, + ); + + /** + * Get the intended protection for the current page, modifying the 'edit' level to sysop. + * Other protections will be preserved. + * If an existing protection exists for edit=sysop, then it will only be modified if the expiry must be updated. + * If no change are required, none will be made. + * + * @param {object[]} current + * @returns {object} + */ + const getTargetProtection = (current) => { + // There are synonyms for 'infinite' which can be returned by the API. + const infiniteNames = [ + 'never', + 'infinite', + 'indefinite', + 'infinity', + ]; + + const protectionValue = { + type: 'edit', + level: 'sysop', + expiry: 'infinite', + }; + + let isProtected = false; + let changed = false; + if (!Array.isArray(current)) { + return { + changed: true, + protection: [protectionValue], + }; + } + + const protection = current.map((protectionItem) => { + if (protectionItem.type !== 'edit') { + return protectionItem; + } + + if (protectionItem.level !== 'sysop') { + protectionItem.level = 'sysop'; + changed = true; + } + + if (infiniteNames.indexOf(protectionItem.expiry) === -1) { + protectionItem.expiry = 'infinite'; + changed = true; + } + + isProtected = true; + + return protectionItem; + }); + + if (!isProtected) { + changed = true; + protection.push(protectionValue); + } + + return { + protection, + changed, + }; + }; + + for (const [legacyPage] of Object.entries(migratedPageData)) { + logger.debug(`=> Checking ${legacyPage}`); + + const [ + protectionState, + ] = await Promise.all([ + getPageProtectionState(legacyPage), + ]); + + const protectionReason = 'Developer Docs Migration'; + const { protection, changed } = getTargetProtection(protectionState.data.protection); + if (changed) { + logger.info(`==> Updating page protection for ${legacyPage}`); + protectPage(legacyPage, protection, protectionReason) + .then(() => { + logger.debug(`===> Updated ${legacyPage}`); + }) + .catch((...err) => { + logger.error(err); + }); + } else { + logger.debug(`===> No need to update protection for ${legacyPage}`); + } + } +}; + /** * Update the migrated page docs in Wikimedia. */ const getUpdateMigratedPages = (logger) => (client) => async () => { + const migratedPageData = yaml.load(await readFile(getMigrationPagePath(), 'utf8')); + const getDocIdList = (newDocIds) => { if (Array.isArray(newDocIds)) { return newDocIds.map((newDoc) => newDoc.slug); @@ -143,21 +285,18 @@ const getUpdateMigratedPages = (logger) => (client) => async () => { const getCurrentMigratedIdsForPage = (pageTitle) => getGetMigratedPageIds(logger)(client)(pageTitle); - const migratedPageData = yaml.load(await readFile(getMigrationPagePath(), 'utf8')); - for (const [legacyPage, newPages] of Object.entries(migratedPageData)) { - logger.info(`=> Checking ${legacyPage}`); + logger.debug(`=> Checking ${legacyPage}`); const newDocIds = getDocIdList(newPages); - const migratedDocData = (await getCurrentMigratedIdsForPage(legacyPage)); + const migratedDocData = await getCurrentMigratedIdsForPage(legacyPage); const docIds = migratedDocData.newDocIds.sort(); if (JSON.stringify(docIds) === JSON.stringify(newDocIds)) { - logger.info(`==> No changes (${docIds.join(', ')})`); + logger.debug(`==> No changes for ${legacyPage} (${docIds.join(', ')})`); } else { - logger.info(`==> Updating ${legacyPage}`); - logger.info(`===> Current docIds are: ${docIds.join(', ')}`); - logger.info(`===> Setting docIds of ${newDocIds.join(', ')}`); + logger.debug(`===> Current docIds for ${legacyPage} are: ${docIds.join(', ')}`); + logger.info(`===> Setting docIds for ${legacyPage} to: ${newDocIds.join(', ')}`); const newTemplates = newDocIds.map((newDocId) => `{{Template:Migrated|newDocId=${newDocId}}}\n`).join(''); const newContent = newTemplates + migratedDocData.data.replaceAll( @@ -173,7 +312,7 @@ const getUpdateMigratedPages = (logger) => (client) => async () => { if (err) { throw err; } - logger.info(`===> Updated ${legacyPage} to ${data.newrevid}`); + logger.debug(`===> Updated ${legacyPage} to ${data.newrevid}`); }, ); } @@ -218,5 +357,6 @@ module.exports = { getObsoletePagePath, getNormalizedPath, getUpdateMigratedPages, + getUpdateMigratedPagesProtection, guessSlug, }; diff --git a/scripts/wikimedia-sync.js b/scripts/wikimedia-sync.js index c2375d666f..fe4045aeb0 100755 --- a/scripts/wikimedia-sync.js +++ b/scripts/wikimedia-sync.js @@ -27,6 +27,7 @@ const { getClient, getObsoletePagePath, getUpdateMigratedPages, + getUpdateMigratedPagesProtection, } = require('./utils'); program @@ -50,6 +51,7 @@ program } const updateMigratedPages = getUpdateMigratedPages(logger)(client); + const updateMigratedPagesProtection = getUpdateMigratedPagesProtection(logger)(client); logger.info('Logging in'); try { @@ -61,7 +63,7 @@ program } logger.info('Starting update of migrated pages in remote site'); - await updateMigratedPages(); + await Promise.all([updateMigratedPages(), updateMigratedPagesProtection()]); logger.info('Run completed'); }); diff --git a/yarn.lock b/yarn.lock index cde635749f..778ba99dc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4278,9 +4278,9 @@ color-name@^1.0.0, color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-string@^1.6.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" - integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -9259,10 +9259,10 @@ node-version-compare@^1.0.1: resolved "https://registry.yarnpkg.com/node-version-compare/-/node-version-compare-1.0.3.tgz#ca6d2005e67822fb4dfa259e08f1f6cfaabe2e81" integrity sha512-unO5GpBAh5YqeGULMLpmDT94oanSDMwtZB8KHTKCH/qrGv8bHN0mlDj9xQDAicCYXv2OLnzdi67lidCrcVotVw== -nodemw@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/nodemw/-/nodemw-0.16.0.tgz#d9b430929258ec27be5959c46e1022dad57a91a1" - integrity sha512-C21O4Bxp1MAbmRvwKzGv4UHWX1qE48dC1OAGrXcnOjPpoJa7efq8DEwEuebgYD1pTWeupn+ozuOaGVT+L/h/4w== +nodemw@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/nodemw/-/nodemw-0.17.0.tgz#e7c2dbb11c692e36eca1103913e98e2d1afccf98" + integrity sha512-Cz5WnBU6dJlhgTOxX3lVJWsVux6ZA1uKBUvcxDbP9vkfN0ppu3wCo2K+7OvfNT7v/moJLyJOm63jyKrWtBTYzA== dependencies: ansicolors "0.3.x" async "^3.2.0" @@ -12143,9 +12143,9 @@ unbox-primitive@^1.0.1: which-boxed-primitive "^1.0.2" underscore@^1.9.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.2.tgz#276cea1e8b9722a8dbed0100a407dda572125881" - integrity sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g== + version "1.13.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.3.tgz#54bc95f7648c5557897e5e968d0f76bc062c34ee" + integrity sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA== unherit@^1.0.4: version "1.1.3"