jenkins-bot submitted this change.

View Change


Approvals: JJMC89: Looks good to me, approved jenkins-bot: Verified
[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(-)

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.


To view, visit change 861895. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ibb717fdb18d5bd9ed6b39e025e5a1dd1afde4eda
Gerrit-Change-Number: 861895
Gerrit-PatchSet: 9
Gerrit-Owner: Xqt <info@gno.de>
Gerrit-Reviewer: BinĂ¡ris <wikiposta@gmail.com>
Gerrit-Reviewer: Dalba <dalba.wiki@gmail.com>
Gerrit-Reviewer: JJMC89 <JJMC89.Wikimedia@gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged