jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1343205?usp=email )
Change subject: [FEAT] Add global block checks to Site and User
......................................................................
[FEAT] Add global block checks to Site and User
Local blocks and CentralAuth locks do not expose GlobalBlocking
records. Add is_globally_blocked() for IP addresses, CIDR ranges
and accounts.
Use bgip so IP lookups include covering ranges. Check bgtargets
support before account queries so older APIs cannot silently ignore
the filter. Fetch fresh results and propagate lookup failures.
Recognize MediaWiki's leading-zero IPv4 forms and reject list-valued
targets.
Update welcome.py to skip globally blocked users alongside local
blocks and CentralAuth locks when global account block queries
are supported.
Bug: T394628
Change-Id: Ie0427db17bf0d090bef46ee25b015376b42bb339
---
M pywikibot/page/_user.py
M pywikibot/site/_apisite.py
M pywikibot/site/_extensions.py
M scripts/welcome.py
M tests/user_tests.py
M tests/welcome_tests.py
6 files changed, 208 insertions(+), 5 deletions(-)
Approvals:
jenkins-bot: Verified
Xqt: Looks good to me, approved
diff --git a/pywikibot/page/_user.py b/pywikibot/page/_user.py
index 24bbaa2..5956b8a 100644
--- a/pywikibot/page/_user.py
+++ b/pywikibot/page/_user.py
@@ -316,6 +316,29 @@
"""
return self.site.is_locked(self.username, force)
+ def is_globally_blocked(self) -> bool:
+ """Return whether an active global block matches this user.
+
+ IP lookups include covering range blocks. For a CIDR range, a
+ matching block must cover the entire range. This checks global
+ block records, regardless of local exemptions or disabling.
+ CentralAuth locks are not included. Results are not cached.
+
+ This method cannot detect hidden global autoblocks through an IP
+ lookup because the API excludes them from these results.
+
+ .. version-added:: 11.8
+ .. seealso::
+ - :meth:`is_blocked`
+ - :meth:`is_locked`
+ - :meth:`APISite.is_globally_blocked()
+
<pywikibot.site._extensions.GlobalBlockingMixin.is_globally_blocked>`
+
+ :raises NotImplementedError: The API cannot query account blocks.
+ :raises UnknownExtensionError: GlobalBlocking is not installed.
+ """
+ return self.site.is_globally_blocked(self.username)
+
def isEmailable(self, force: bool = False) -> bool: # noqa: N802
"""Determine whether emails may be sent to this user through MediaWiki.
diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index 6f9deda..6194192 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -53,6 +53,7 @@
EchoMixin,
FlaggedRevsMixin,
GeoDataMixin,
+ GlobalBlockingMixin,
GlobalUsageMixin,
LinterMixin,
PageImagesMixin,
@@ -108,6 +109,7 @@
FlaggedRevsMixin,
GeneratorsMixin,
GeoDataMixin,
+ GlobalBlockingMixin,
GlobalUsageMixin,
LinterMixin,
PageImagesMixin,
diff --git a/pywikibot/site/_extensions.py b/pywikibot/site/_extensions.py
index 7dfa2f9..e681a46 100644
--- a/pywikibot/site/_extensions.py
+++ b/pywikibot/site/_extensions.py
@@ -6,7 +6,9 @@
"""Objects representing API interface to MediaWiki site extensions."""
from __future__ import annotations
+import re
from collections.abc import Generator, Iterable
+from ipaddress import ip_network
from typing import TYPE_CHECKING, Protocol
import pywikibot
@@ -29,6 +31,7 @@
class BaseSiteProtocol(Protocol):
+ _paraminfo: api.ParamInfo
_proofread_levels: dict[int, str]
tokens: dict[str, str]
@@ -337,6 +340,68 @@
yield pywikibot.Page(self, pagedata['title']), pagedata['count']
+class GlobalBlockingMixin:
+
+ """APISite mixin for the GlobalBlocking extension.
+
+ .. version-added:: 11.8
+ """
+
+ @need_extension('GlobalBlocking')
+ def is_globally_blocked(self: BaseSiteProtocol, user: str) -> bool:
+ """Return whether an active global block matches the target.
+
+ IP lookups include covering range blocks. For a CIDR range, a
+ matching block must cover the entire range. Account lookups
+ require an API supporting the ``bgtargets`` parameter.
+
+ This checks global block records, including locally disabled
+ blocks. It does not check exemptions or CentralAuth locks, and
+ does not determine whether an edit is permitted. Results are not
+ cached.
+
+ This method cannot detect hidden global autoblocks through an IP
+ lookup because the API excludes them from these results.
+
+ .. seealso::
+ - :ext:`GlobalBlocking/API#list=globalblocks_(bg)`
+ - :meth:`pywikibot.User.is_globally_blocked`
+ - :meth:`pywikibot.site._apisite.APISite.is_locked`
+
+ :param user: Username, IP address or CIDR range to check
+ :raises ValueError: The target is empty or contains list separators.
+ :raises NotImplementedError: The API cannot query account blocks.
+ :raises UnknownExtensionError: GlobalBlocking is not installed.
+ """
+ user = user.strip()
+ if not user:
+ raise ValueError('The global block target must not be empty.')
+ if '|' in user or '\x1f' in user:
+ raise ValueError('The global block target must not contain '
+ 'list separators.')
+
+ # Recognize leading-zero IPv4 addresses accepted by MediaWiki.
+ address, separator, prefix = user.partition('/')
+ if re.fullmatch(r'[0-9]{1,3}(?:\.[0-9]{1,3}){3}', address):
+ address = '.'.join(str(int(part)) for part in address.split('.'))
+ try:
+ # MediaWiki accepts CIDR ranges with nonzero host bits.
+ ip_network(address + separator + prefix, strict=False)
+ except ValueError:
+ if self._paraminfo.parameter('query+globalblocks',
+ 'targets') is None:
+ raise NotImplementedError(
+ 'This site does not support global account block queries.')
+ parameter = 'bgtargets'
+ else:
+ parameter = 'bgip'
+
+ req = self.simple_request(action='query', list='globalblocks',
+ bgprop='id', bglimit=1,
+ **{parameter: [user]})
+ return bool(req.submit()['query']['globalblocks'])
+
+
class GlobalUsageMixin:
"""APISite mixin for Global Usage extension."""
diff --git a/scripts/welcome.py b/scripts/welcome.py
index 75d57c2..52efe16 100755
--- a/scripts/welcome.py
+++ b/scripts/welcome.py
@@ -192,7 +192,12 @@
import pywikibot
from pywikibot import config, i18n
from pywikibot.bot import SingleSiteBot
-from pywikibot.exceptions import EditConflictError, Error, HiddenKeyError
+from pywikibot.exceptions import (
+ EditConflictError,
+ Error,
+ HiddenKeyError,
+ UnknownExtensionError,
+)
from pywikibot.tools import cached
@@ -826,8 +831,17 @@
.. version-changed:: 7.0
also skip if user is locked globally
+ .. version-changed:: 11.8
+ Also skip globally blocked users if the site supports checking
+ global account blocks.
"""
- if user.is_blocked() or user.is_locked():
+ blocked = user.is_blocked() or user.is_locked()
+ if not blocked:
+ # GlobalBlocking or account block queries may be unavailable.
+ with suppress(UnknownExtensionError, NotImplementedError):
+ blocked = user.is_globally_blocked()
+
+ if blocked:
self.show_status(Msg.SKIP)
pywikibot.info(f'{user.username} has been blocked!')
diff --git a/tests/user_tests.py b/tests/user_tests.py
index 9b6da3c..16d7366 100755
--- a/tests/user_tests.py
+++ b/tests/user_tests.py
@@ -13,7 +13,11 @@
import pywikibot
from pywikibot import Page, Timestamp, User
-from pywikibot.exceptions import AutoblockUserError
+from pywikibot.exceptions import (
+ APIError,
+ AutoblockUserError,
+ UnknownExtensionError,
+)
from tests.aspects import DefaultSiteTestCase, TestCase
@@ -229,6 +233,84 @@
self.assertIsNone(user.get_block_info())
+class TestGlobalBlocks(TestCase):
+
+ """Test global block lookups without changing live blocks."""
+
+ family = 'wikipedia'
+ code = 'en'
+ dry = True
+
+ def test_global_blocks(self) -> None:
+ """Use range-aware IP queries and exact account queries."""
+ cases = (
+ ('192.0.2.1', 'bgip', [{'id': 1}]),
+ ('192.000.002.001', 'bgip', [{'id': 1}]),
+ ('2001:db8::1', 'bgip', []),
+ ('192.0.2.0/24', 'bgip', [{'id': 2}]),
+ ('192.0.2.1/24', 'bgip', [{'id': 2}]),
+ ('Example', 'bgtargets', [{'id': 3}]),
+ )
+ with patch.object(self.site, 'has_extension', return_value=True), \
+ patch.object(self.site._paraminfo, 'parameter',
+ return_value={'name': 'targets'}), \
+ patch.object(self.site, 'simple_request') as request:
+ for target, parameter, blocks in cases:
+ with self.subTest(target=target):
+ request.return_value.submit.return_value = {
+ 'query': {'globalblocks': blocks},
+ }
+ user = User(self.site, target)
+ self.assertIs(user.is_globally_blocked(), bool(blocks))
+ request.assert_called_with(
+ action='query', list='globalblocks', bgprop='id',
+ bglimit=1, **{parameter: [user.username]})
+
+ # A second lookup must observe the changed block status.
+ request.return_value.submit.return_value = {
+ 'query': {'globalblocks': []},
+ }
+ self.assertFalse(user.is_globally_blocked())
+
+ self.assertFalse(self.site.is_globally_blocked(' 192.0.2.1 '))
+ request.assert_called_with(
+ action='query', list='globalblocks', bgprop='id', bglimit=1,
+ bgip=['192.0.2.1'])
+
+ def test_unsupported_account_query(self) -> None:
+ """Do not send an account filter that an older API would ignore."""
+ with patch.object(self.site, 'has_extension', return_value=True), \
+ patch.object(self.site._paraminfo, 'parameter',
+ return_value=None), \
+ patch.object(self.site, 'simple_request') as request:
+ with self.assertRaisesRegex(NotImplementedError,
+ 'global account block queries'):
+ User(self.site, 'Example').is_globally_blocked()
+ request.assert_not_called()
+
+ def test_global_block_errors(self) -> None:
+ """Unavailable or failed queries must not report an unblocked user."""
+ user = User(self.site, '192.0.2.1')
+ with self.assertRaisesRegex(UnknownExtensionError, 'GlobalBlocking'):
+ user.is_globally_blocked()
+
+ with patch.object(self.site, 'has_extension', return_value=True), \
+ patch.object(self.site, 'simple_request') as request:
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
+ self.site.is_globally_blocked('')
+
+ for target in ('Example|Other', '\x1fExample\x1fOther'):
+ with self.subTest(target=target):
+ with self.assertRaisesRegex(ValueError, 'list separators'):
+ self.site.is_globally_blocked(target)
+ request.assert_not_called()
+
+ request.return_value.submit.side_effect = APIError(
+ 'readapidenied', 'Read access denied')
+ with self.assertRaisesRegex(APIError, 'readapidenied'):
+ user.is_globally_blocked()
+
+
class TestUserMethods(DefaultSiteTestCase):
"""Test User methods with bot user."""
diff --git a/tests/welcome_tests.py b/tests/welcome_tests.py
index 1e781f0..f4284b2 100755
--- a/tests/welcome_tests.py
+++ b/tests/welcome_tests.py
@@ -80,12 +80,29 @@
bot.report_bad_account.assert_not_called()
+ def test_skip_page_globally_blocked(self) -> None:
+ """Skip a globally blocked user without a local block or lock."""
+ bot = SimpleNamespace(show_status=MagicMock())
+ user = MagicMock(username='Iyanu')
+ user.is_blocked.return_value = False
+ user.is_locked.return_value = False
+ user.is_globally_blocked.return_value = True
+
+ with patch.object(welcome.pywikibot, 'info') as info:
+ result = welcome.WelcomeBot.skip_page(bot, user)
+
+ self.assertTrue(result)
+ bot.show_status.assert_called_once_with(welcome.Msg.SKIP)
+ info.assert_called_once_with('Iyanu has been blocked!')
+ user.groups.assert_not_called()
+
def test_skip_page_reuses_edit_count(self) -> None:
"""Test that the edit count is retrieved once."""
bot = SimpleNamespace(show_status=MagicMock())
- user = MagicMock(username='Alice')
+ user = MagicMock(username='Iyanu')
user.is_blocked.return_value = False
user.is_locked.return_value = False
+ user.is_globally_blocked.return_value = False
user.groups.return_value = []
user.editCount.return_value = 1
@@ -98,7 +115,7 @@
self.assertTrue(result)
user.editCount.assert_called_once_with()
bot.show_status.assert_called_once_with(welcome.Msg.IGNORE)
- info.assert_called_once_with('Alice has only 1 contributions.')
+ info.assert_called_once_with('Iyanu has only 1 contributions.')
def test_signature_file_closed_on_read_error(self) -> None:
"""Test that the signature file is closed when reading fails."""
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1343205?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ie0427db17bf0d090bef46ee25b015376b42bb339
Gerrit-Change-Number: 1343205
Gerrit-PatchSet: 4
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]