Source code for pywikibot.login

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Library to log the bot in to a wiki account."""
#
# (C) Rob W.W. Hooft, 2003
# (C) Pywikibot team, 2003-2019
#
# Distributed under the terms of the MIT license.
#
from __future__ import absolute_import, division, unicode_literals

import codecs
import os
import webbrowser

from pywikibot.tools import file_mode_checker

from warnings import warn

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

import pywikibot

from pywikibot import config, __url__
from pywikibot.exceptions import NoUsername
from pywikibot.tools import (deprecated_args, remove_last_args,
                             normalize_username, UnicodeType)


[docs]class OAuthImpossible(ImportError): """OAuth authentication is not possible on your system."""
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 LoginManager(object): """Site login manager."""
[docs] @deprecated_args(username='user', verbose=None, sysop=False) def __init__(self, password=None, site=None, user=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. @type user: basestring @param password: password to use @type password: basestring @raises 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 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 %s exists on %s' % (main_username, self.site)) return else: raise if user['name'] != main_username: # Report the same error as server error code NotExists raise NoUsername("Username '%s' does not exist on %s" % (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. """ if self.site.family.name in botList \ and self.site.code in botList[self.site.family.name]: botlist_pagetitle, bot_template_title = botList[ self.site.family.name][self.site.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 else: # No bot policies on other sites return True
[docs] @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. """ # THIS IS OVERRIDDEN IN data/api.py return None
[docs] def storecookiedata(self, data): """ Store cookie data. The argument data is the raw data, as returned by getCookie(). Returns nothing. """ # THIS IS OVERRIDDEN IN data/api.py filename = config.datafilepath('pywikibot.lwp') pywikibot.debug('Storing cookies to %s' % filename, _logger) with open(filename, 'w') as f: f.write(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 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 type(entry) is not 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, UnicodeType): self.password = password break elif isinstance(password, BotPassword): self.password = password.password self.login_name = password.login_name(self.username) break else: 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', }
[docs] def login(self, retry=False, autocreate=False): """ 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 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)s on %(site)s (no characters will ' 'be shown):' % {'name': self.login_name, 'site': self.site}, password=True) pywikibot.output('Logging in to %(site)s as %(name)s' % {'name': self.login_name, 'site': self.site}) try: cookiedata = self.getCookie() 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 == 'Failed': 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(cookiedata) pywikibot.log('Should be logged in now') return True
[docs]class BotPassword(object): """BotPassword object for storage in password file."""
[docs] def __init__(self, suffix, password): """ 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 @type suffix: basestring @param password: bot password @type password: basestring @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): """ Construct the login name from the username and suffix. @param user: username (without suffix) @type user: basestring @rtype: basestring """ 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=False) def __init__(self, password=None, site=None, user=None): """ Initializer. All parameters default to defaults in user-config. @param site: Site object to log into @type site: BaseSite @param user: consumer key @type user: basestring @param password: consumer secret @type password: basestring @raises 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(OauthLoginManager, self).__init__(None, False, site, None) if self.password: pywikibot.warn('Password exists in password file for %s:%s.' 'Password is unnecessary and should be removed ' 'when OAuth enabled.' % (self.site, self.username)) 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)s via OAuth consumer %(key)s' % {'key': self.consumer_token[0], 'site': self.site}) consumer_token = mwoauth.ConsumerToken(self.consumer_token[0], self.consumer_token[1]) 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: %s' % 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)s via consumer %(key)s' % {'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): """ Get identifying information about a user via an authorized token. @rtype: None or dict """ if self.access_token is None: pywikibot.error('Access token not set') return None consumer_token = mwoauth.ConsumerToken(self.consumer_token[0], self.consumer_token[1]) access_token = mwoauth.AccessToken(self.access_token[0], self.access_token[1]) 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