Source code for pywikibot.login

#!/usr/bin/python
"""Library to log the bot in to a wiki account."""
#
# (C) Pywikibot team, 2003-2020
#
# Distributed under the terms of the MIT license.
#
import codecs
import os
import webbrowser

from enum import IntEnum
from typing import Optional
from warnings import warn

import pywikibot

from pywikibot import config, __url__
from pywikibot.comms import http
from pywikibot.exceptions import NoUsername
from pywikibot.tools import (
    deprecated,
    deprecated_args, file_mode_checker, normalize_username, remove_last_args,
)

try:
    import mwoauth
except ImportError as e:
    mwoauth = e


[docs]class OAuthImpossible(ImportError): """OAuth authentication is not possible on your system.""" pass
class _PasswordFileWarning(UserWarning): """The format of password file is incorrect.""" pass _logger = 'wiki.login' # On some wikis you are only allowed to run a bot if there is a link to # the bot's user page in a specific list. # If bots are listed in a template, the templates name must be given as # second parameter, otherwise it must be None botList = { 'wikipedia': { 'simple': ['Wikipedia:Bots', '/links'] }, }
[docs]class LoginStatus(IntEnum): """ Enum for Login statuses. >>> LoginStatus.NOT_ATTEMPTED LoginStatus(-3) >>> LoginStatus.IN_PROGRESS.value -2 >>> LoginStatus.NOT_LOGGED_IN.name NOT_LOGGED_IN >>> int(LoginStatus.AS_USER) 0 >>> LoginStatus(-3).name 'NOT_ATTEMPTED' >>> LoginStatus(0).name 'AS_USER' """ NOT_ATTEMPTED = -3 IN_PROGRESS = -2 NOT_LOGGED_IN = -1 AS_USER = 0 def __repr__(self): """Return internal representation.""" return 'LoginStatus({})'.format(self)
[docs]class LoginManager: """Site login manager."""
[docs] @deprecated_args(username='user', verbose=None, sysop=None) def __init__(self, password: Optional[str] = None, site=None, user: Optional[str] = None): """ Initializer. All parameters default to defaults in user-config. @param site: Site object to log into @type site: BaseSite @param user: username to use. If user is None, the username is loaded from config.usernames. @param password: password to use @raises pywikibot.exceptions.NoUsername: No username is configured for the requested site. """ site = self.site = site or pywikibot.Site() if not user: config_names = config.usernames code_to_usr = config_names[site.family.name] or config_names['*'] try: user = code_to_usr.get(site.code) or code_to_usr['*'] except KeyError: raise NoUsername( 'ERROR: ' 'username for {site.family.name}:{site.code} is undefined.' '\nIf you have a username for that site, ' 'please add a line to user-config.py as follows:\n' "usernames['{site.family.name}']['{site.code}'] = " "'myUsername'" .format(site=site)) self.password = password self.login_name = self.username = user if getattr(config, 'password_file', ''): self.readPassword()
[docs] def check_user_exists(self): """ Check that the username exists on the site. @see: U{https://www.mediawiki.org/wiki/API:Users} @raises pywikibot.exceptions.NoUsername: Username doesn't exist in user list. """ # convert any Special:BotPassword usernames to main account equivalent main_username = self.username if '@' in self.username: warn( 'When using BotPasswords it is recommended that you store ' 'your login credentials in a password_file instead. See ' '{}/BotPasswords for instructions and more information.' .format(__url__)) main_username = self.username.partition('@')[0] try: data = self.site.allusers(start=main_username, total=1) user = next(iter(data)) except pywikibot.data.api.APIError as e: if e.code == 'readapidenied': pywikibot.warning("Could not check user '{}' exists on {}" .format(main_username, self.site)) return raise if user['name'] != main_username: # Report the same error as server error code NotExists raise NoUsername("Username '{}' does not exist on {}" .format(main_username, self.site))
[docs] def botAllowed(self): """ Check whether the bot is listed on a specific page. This allows bots to comply with the policy on the respective wiki. """ code, fam = self.site.code, self.site.family.name if code in botList.get(fam, []): botlist_pagetitle, bot_template_title = botList[fam][code] botlist_page = pywikibot.Page(self.site, botlist_pagetitle) if bot_template_title: for template, params in botlist_page.templatesWithParams(): if (template.title() == bot_template_title and params[0] == self.username): return True else: for linked_page in botlist_page.linkedPages(): if linked_page.title(with_ns=False) == self.username: return True return False # No bot policies on other sites return True
@deprecated('login_to_site', since='20201227', future_warning=True) @remove_last_args(['remember', 'captcha']) def getCookie(self): """ Login to the site. @see: U{https://www.mediawiki.org/wiki/API:Login} @return: cookie data if successful, None otherwise. """ self.login_to_site()
[docs] def login_to_site(self): """Login to the site.""" # THIS IS OVERRIDDEN IN data/api.py raise NotImplementedError
[docs] @remove_last_args(['data']) def storecookiedata(self) -> None: """Store cookie data.""" http.cookie_jar.save(ignore_discard=True)
[docs] def readPassword(self): """ Read passwords from a file. DO NOT FORGET TO REMOVE READ ACCESS FOR OTHER USERS!!! Use chmod 600 password-file. All lines below should be valid Python tuples in the form (code, family, username, password), (family, username, password) or (username, password) to set a default password for an username. The last matching entry will be used, so default usernames should occur above specific usernames. For BotPasswords the password should be given as a BotPassword object. The file must be either encoded in ASCII or UTF-8. Example:: ('my_username', 'my_default_password') ('wikipedia', 'my_wikipedia_user', 'my_wikipedia_pass') ('en', 'wikipedia', 'my_en_wikipedia_user', 'my_en_wikipedia_pass') ('my_username', BotPassword( 'my_BotPassword_suffix', 'my_BotPassword_password')) """ # Set path to password file relative to the user_config # but fall back on absolute path for backwards compatibility password_file = os.path.join(config.base_dir, config.password_file) if not os.path.isfile(password_file): password_file = config.password_file # We fix password file permission first. file_mode_checker(password_file, mode=config.private_files_permission) with codecs.open(password_file, encoding='utf-8') as f: lines = f.readlines() line_nr = len(lines) + 1 for line in reversed(lines): line_nr -= 1 if not line.strip() or line.startswith('#'): continue try: entry = eval(line) except SyntaxError: entry = None if not isinstance(entry, tuple): warn('Invalid tuple in line {0}'.format(line_nr), _PasswordFileWarning) continue if not 2 <= len(entry) <= 4: warn('The length of tuple in line {0} should be 2 to 4 ({1} ' 'given)'.format(line_nr, entry), _PasswordFileWarning) continue code, family, username, password = ( self.site.code, self.site.family.name)[:4 - len(entry)] + entry if (normalize_username(username) == self.username and family == self.site.family.name and code == self.site.code): if isinstance(password, str): self.password = password break if isinstance(password, BotPassword): self.password = password.password self.login_name = password.login_name(self.username) break warn('Invalid password format', _PasswordFileWarning)
_api_error = { 'NotExists': 'does not exist', 'Illegal': 'is invalid', 'readapidenied': 'does not have read permissions', 'Failed': 'does not have read permissions', 'FAIL': 'does not have read permissions', }
[docs] def login(self, retry=False, autocreate=False) -> bool: """ Attempt to log into the server. @see: U{https://www.mediawiki.org/wiki/API:Login} @param retry: infinitely retry if the API returns an unknown error @type retry: bool @param autocreate: if true, allow auto-creation of the account using unified login @type autocreate: bool @raises pywikibot.exceptions.NoUsername: Username is not recognised by the site. """ if not self.password: # First check that the username exists, # to avoid asking for a password that will not work. if not autocreate: self.check_user_exists() # As we don't want the password to appear on the screen, we set # password = True self.password = pywikibot.input( 'Password for user {name} on {site} (no characters will be ' 'shown):'.format(name=self.login_name, site=self.site), password=True) pywikibot.output('Logging in to {site} as {name}' .format(name=self.login_name, site=self.site)) try: self.login_to_site() except pywikibot.data.api.APIError as e: error_code = e.code pywikibot.error('Login failed ({}).'.format(error_code)) if error_code in self._api_error: error_msg = 'Username "{}" {} on {}'.format( self.login_name, self._api_error[error_code], self.site) if error_code in ('Failed', 'FAIL'): error_msg += '\n.{}'.format(e.info) raise NoUsername(error_msg) # TODO: investigate other unhandled API codes (bug T75539) if retry: self.password = None return self.login(retry=True) else: return False self.storecookiedata() pywikibot.log('Should be logged in now') return True
[docs]class BotPassword: """BotPassword object for storage in password file."""
[docs] def __init__(self, suffix: str, password: str): """ Initializer. BotPassword function by using a separate password paired with a suffixed username of the form <username>@<suffix>. @param suffix: Suffix of the login name @param password: bot password @raises _PasswordFileWarning: suffix improperly specified """ if '@' in suffix: warn('The BotPassword entry should only include the suffix', _PasswordFileWarning) self.suffix = suffix self.password = password
[docs] def login_name(self, username: str) -> str: """ Construct the login name from the username and suffix. @param user: username (without suffix) """ return '{0}@{1}'.format(username, self.suffix)
[docs]class OauthLoginManager(LoginManager): """Site login manager using OAuth.""" # NOTE: Currently OauthLoginManager use mwoauth directly to complete OAuth # authentication process
[docs] @deprecated_args(sysop=None) def __init__(self, password: Optional[str] = None, site=None, user: Optional[str] = None): """ Initializer. All parameters default to defaults in user-config. @param site: Site object to log into @type site: BaseSite @param user: consumer key @param password: consumer secret @raises pywikibot.exceptions.NoUsername: No username is configured for the requested site. @raises OAuthImpossible: mwoauth isn't installed """ if isinstance(mwoauth, ImportError): raise OAuthImpossible('mwoauth is not installed: %s.' % mwoauth) assert password is not None and user is not None super().__init__(password=None, site=site, user=None) if self.password: pywikibot.warn('Password exists in password file for {login.site}:' '{login.username}. Password is unnecessary and ' 'should be removed if OAuth enabled.' .format(login=self)) self._consumer_token = (user, password) self._access_token = None
[docs] def login(self, retry=False, force=False): """ Attempt to log into the server. @see: U{https://www.mediawiki.org/wiki/API:Login} @param retry: infinitely retry if exception occurs during authentication. @type retry: bool @param force: force to re-authenticate @type force: bool """ if self.access_token is None or force: pywikibot.output( 'Logging in to {site} via OAuth consumer {key}' .format(key=self.consumer_token[0], site=self.site)) consumer_token = mwoauth.ConsumerToken(*self.consumer_token) handshaker = mwoauth.Handshaker( self.site.base_url(self.site.path()), consumer_token) try: redirect, request_token = handshaker.initiate() pywikibot.stdout('Authenticate via web browser..') webbrowser.open(redirect) pywikibot.stdout('If your web browser does not open ' 'automatically, please point it to: {}' .format(redirect)) request_qs = pywikibot.input('Response query string: ') access_token = handshaker.complete(request_token, request_qs) self._access_token = (access_token.key, access_token.secret) except Exception as e: pywikibot.error(e) if retry: self.login(retry=True, force=force) else: pywikibot.output('Logged in to {site} via consumer {key}' .format(key=self.consumer_token[0], site=self.site))
@property def consumer_token(self): """ Return OAuth consumer key token and secret token. @see: U{https://www.mediawiki.org/wiki/API:Tokens} @rtype: tuple of two str """ return self._consumer_token @property def access_token(self): """ Return OAuth access key token and secret token. @see: U{https://www.mediawiki.org/wiki/API:Tokens} @rtype: tuple of two str """ return self._access_token @property def identity(self) -> Optional[dict]: """Get identifying information about a user via an authorized token.""" if self.access_token is None: pywikibot.error('Access token not set') return None consumer_token = mwoauth.ConsumerToken(*self.consumer_token) access_token = mwoauth.AccessToken(*self.access_token) try: identity = mwoauth.identify(self.site.base_url(self.site.path()), consumer_token, access_token) return identity except Exception as e: pywikibot.error(e) return None