Source code for pywikibot.login

"""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 import (
    deprecated_args, file_mode_checker, normalize_username, remove_last_args,

    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 >>> 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 = = site or pywikibot.Site() if not user: config_names = config.usernames code_to_usr = config_names[] or config_names['*'] try: user = code_to_usr.get(site.code) or code_to_usr['*'] except KeyError: raise NoUsername( 'ERROR: ' 'username for {}:{site.code} is undefined.' '\nIf you have a username for that site, ' 'please add a line to as follows:\n' "usernames['{}']['{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{} @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 =, total=1) user = next(iter(data)) except as e: if e.code == 'readapidenied': pywikibot.warning("Could not check user '{}' exists on {}" .format(main_username, 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,
[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 =, if code in botList.get(fam, []): botlist_pagetitle, bot_template_title = botList[fam][code] botlist_page = pywikibot.Page(, 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{} @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/ raise NotImplementedError
[docs] @remove_last_args(['data']) def storecookiedata(self) -> None: """Store cookie data."""
[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, 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 = (,[:4 - len(entry)] + entry if (normalize_username(username) == self.username and family == and 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{} @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,, password=True) pywikibot.output('Logging in to {site} as {name}' .format(name=self.login_name, try: self.login_to_site() except 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], if error_code in ('Failed', 'FAIL'): error_msg += '\n.{}'.format( 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.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{} @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], consumer_token = mwoauth.ConsumerToken(*self.consumer_token) handshaker = mwoauth.Handshaker(, consumer_token) try: redirect, request_token = handshaker.initiate() pywikibot.stdout('Authenticate via web browser..') 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],
@property def consumer_token(self): """ Return OAuth consumer key token and secret token. @see: U{} @rtype: tuple of two str """ return self._consumer_token @property def access_token(self): """ Return OAuth access key token and secret token. @see: U{} @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(, consumer_token, access_token) return identity except Exception as e: pywikibot.error(e) return None