jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/861895 )
Change subject: [IMPR] Create a cookie file for each account ......................................................................
[IMPR] Create a cookie file for each account
- Add/restore PywikibotCookieJar which overrides load and save method. This enables lazy load of cookies during login when the username is known already. - load() creates the PywikibotCookieJar.filename and tries to load the cookies - save() creates the cookies file and checks the file mode - APISite.login calls http.cookie_jar.load to load cookies for a given user - remove http.SSL_CERT_VERIFY_FAILED_MSG and remove duplicate error message in generate_family_file.py - add a new tools function as_filename() which ensures that a string can be used as filename. - use as_filename to create a filename for the given user - use as_filename with BasePage.title()
Bug: T324000 Change-Id: Ibb717fdb18d5bd9ed6b39e025e5a1dd1afde4eda --- M pywikibot/tools/__init__.py M pywikibot/comms/http.py M pywikibot/scripts/generate_family_file.py M pywikibot/site/_apisite.py M pywikibot/page/_page.py 5 files changed, 129 insertions(+), 33 deletions(-)
Approvals: JJMC89: Looks good to me, approved jenkins-bot: Verified
diff --git a/pywikibot/comms/http.py b/pywikibot/comms/http.py index 429c96b..56518b3 100644 --- a/pywikibot/comms/http.py +++ b/pywikibot/comms/http.py @@ -11,15 +11,19 @@ - Basic HTTP error handling
This module creates and uses its own ``requests.Session`` object. -The session is closed if the module terminates. -If required you can use your own Session object passing it to the -``http.session`` variable:: +The session is closed if the module terminates. If required you can use +your own Session object passing it to the ``http.session`` variable::
from pywikibot.comms import http session = requests.Session() http.session = session
-:py:obj:`flush()` can be called to close the session object. +To enable access via cookies, assign cookie handling class:: + + session.cookies = http.cookie_jar + +.. versionchanged:: 8.0 + Cookies are lazy loaded when logging to site. """ # # (C) Pywikibot team, 2007-2022 @@ -40,7 +44,7 @@ import requests
import pywikibot -from pywikibot import config +from pywikibot import config, tools from pywikibot.backports import Tuple from pywikibot.exceptions import ( FatalServerError, @@ -58,20 +62,48 @@ requests_oauthlib = e
-# The error message for failed SSL certificate verification -# 'certificate verify failed' is a commonly detectable string -SSL_CERT_VERIFY_FAILED_MSG = 'certificate verify failed' +class PywikibotCookieJar(cookiejar.LWPCookieJar):
-cookie_file_path = config.datafilepath('pywikibot.lwp') -file_mode_checker(cookie_file_path, create=True) -cookie_jar = cookiejar.LWPCookieJar(cookie_file_path) -try: - cookie_jar.load(ignore_discard=True) -except cookiejar.LoadError: - debug('Loading cookies failed.') -else: - debug('Loaded cookies from file.') + """CookieJar which create the filename and checks file permissions.
+ .. versionadded:: 8.0 + """ + + def load(self, user: str = '', *args, **kwargs) -> None: + """Loads cookies from a file. + + Insert the account name to the cookie filename, set the + instance`s filename and load the cookies. + + :param user: account name to be part of the cookie filename. + """ + _user = '-' + tools.as_filename(user) if user else '' + self.filename = config.datafilepath(f'pywikibot{_user}.lwp') + + try: + super().load(*args, **kwargs) + except (cookiejar.LoadError, FileNotFoundError): + debug(f'Loading cookies for user {user} failed.') + else: + debug(f'Loaded cookies for user {user} from file.') + + def save(self, *args, **kwargs) -> None: + """Check the file mode and save cookies to a file. + + .. note:: *PywikibotCookieJar* must be loaded previously to set + the filename. + + :raises ValueError: a filename was not supplied; :meth:`load` + must be called first. + """ + if self.filename: + file_mode_checker(self.filename, create=True) + super().save(*args, **kwargs) + + +#: global :class:`PywikibotCookieJar` instance. +cookie_jar = PywikibotCookieJar() +#: global :class:`requests.Session`. session = requests.Session() session.cookies = cookie_jar
@@ -268,7 +300,7 @@ """ # TODO: do some error correcting stuff if isinstance(response, requests.exceptions.SSLError) \ - and SSL_CERT_VERIFY_FAILED_MSG in str(response): + and 'certificate verify failed' in str(response): raise FatalServerError(str(response))
if isinstance(response, requests.ConnectionError): diff --git a/pywikibot/page/_page.py b/pywikibot/page/_page.py index 1b424ed..2e9f734 100644 --- a/pywikibot/page/_page.py +++ b/pywikibot/page/_page.py @@ -30,7 +30,7 @@ from warnings import warn
import pywikibot -from pywikibot import Timestamp, config, date, i18n, textlib +from pywikibot import Timestamp, config, date, i18n, textlib, tools from pywikibot.backports import ( Dict, Generator, @@ -305,14 +305,7 @@ encoded_title = title.encode(self.site.encoding()) title = quote_from_bytes(encoded_title, safe='') if as_filename: - # Replace characters that are not possible in file names on some - # systems, but still are valid in MediaWiki titles: - # Unix: / - # MediaWiki: /:\ - # Windows: /:"?* - # Spaces are possible on most systems, but are bad for URLs. - for forbidden in ':*?/\" ': - title = title.replace(forbidden, '_') + title = tools.as_filename(title) return title
def section(self) -> Optional[str]: diff --git a/pywikibot/scripts/generate_family_file.py b/pywikibot/scripts/generate_family_file.py index e79a650..16e2171 100755 --- a/pywikibot/scripts/generate_family_file.py +++ b/pywikibot/scripts/generate_family_file.py @@ -119,8 +119,6 @@ try: w = self.Wiki(self.base_url, verify=verify) except FatalServerError: - pywikibot.error( - pywikibot.comms.http.SSL_CERT_VERIFY_FAILED_MSG) pywikibot.exception() if not pywikibot.bot.input_yn( 'Retry with disabled ssl certificate validation', diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py index 8032203..b6ccda2 100644 --- a/pywikibot/site/_apisite.py +++ b/pywikibot/site/_apisite.py @@ -25,7 +25,7 @@ removesuffix, ) from pywikibot.backports import OrderedDict as OrderedDictType -from pywikibot.comms.http import get_authentication +from pywikibot.comms import http from pywikibot.data import api from pywikibot.exceptions import ( AbuseFilterDisallowedError, @@ -326,7 +326,7 @@
def is_oauth_token_available(self) -> bool: """Check whether OAuth token is set for this site.""" - auth_token = get_authentication(self.base_url('')) + auth_token = http.get_authentication(self.base_url('')) return auth_token is not None and len(auth_token) == 4
def login( @@ -334,8 +334,10 @@ autocreate: bool = False, user: Optional[str] = None ) -> None: - """ - Log the user in if not already logged in. + """Log the user in if not already logged in. + + .. versionchanged:: 8.0 + lazy load cookies when logging in.
.. seealso:: :api:`Login`
@@ -372,6 +374,10 @@ self._loginstatus = _LoginStatus.IN_PROGRESS if user: self._username = normalize_username(user) + + # load the password for self.username from cookie file + http.cookie_jar.load(self.username(), ignore_discard=True) + try: del self.userinfo # force reload if self.userinfo['name'] == self.user(): diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index f59c46e..bad616f 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -60,6 +60,7 @@
# other tools 'PYTHON_VERSION', + 'as_filename', 'is_ip_address', 'has_module', 'classproperty', @@ -286,6 +287,47 @@ return (_first_upper_exception(first) or first.upper()) + string[1:]
+def as_filename(string: str, repl: str = '_') -> str: + r"""Return a string with characters are valid for filenames. + + Replace characters that are not possible in file names on some + systems, but still are valid in MediaWiki titles: + + - Unix: ``/`` + - MediaWiki: ``/:\`` + - Windows: ``/:\"?*`` + + Spaces are possible on most systems, but are bad for URLs. + + **Example**: + + >>> as_filename('How are you?') + 'How_are_you_' + >>> as_filename('Say: "Hello"') + 'Say___Hello_' + >>> as_filename('foo*bar', '') + 'foobar' + >>> as_filename('foo', 'bar') + Traceback (most recent call last): + ... + ValueError: Invalid repl parameter 'bar' + >>> as_filename('foo', '?') + Traceback (most recent call last): + ... + ValueError: Invalid repl parameter '?' + + .. versionadded:: 8.0 + + :param string: the string to be modified + :param repl: the replacement character + :raises ValueError: Invalid repl parameter + """ + pattern = r':*?/\" ' + if len(repl) > 1 or len(repl) == 1 and repl in pattern: + raise ValueError(f'Invalid repl parameter {repl!r}') + return re.sub(f'[{pattern}]', repl, string) + + def strtobool(val: str) -> bool: """Convert a string representation of truth to True or False.