diff --git a/earwigbot/exceptions.py b/earwigbot/exceptions.py index 3016f74..e92935e 100644 --- a/earwigbot/exceptions.py +++ b/earwigbot/exceptions.py @@ -36,13 +36,13 @@ This module contains all exceptions used by EarwigBot:: | +-- SQLError +-- NoServiceError +-- LoginError + +-- PermissionsError +-- NamespaceNotFoundError +-- PageNotFoundError +-- InvalidPageError +-- RedirectError +-- UserNotFoundError +-- EditError - | +-- PermissionsError | +-- EditConflictError | +-- NoContentError | +-- ContentTooBigError @@ -120,6 +120,19 @@ class LoginError(WikiToolsetError): Raised by :py:meth:`Site._login `. """ +class PermissionsError(WikiToolsetError): + """A permissions error ocurred. + + We tried to do something we don't have permission to, like trying to delete + a page as a non-admin, or trying to edit a page without login information + and AssertEdit enabled. This will also be raised if we have been blocked + from editing. + + Raised by :py:meth:`Page.edit `, + :py:meth:`Page.add_section `, and + other API methods depending on settings. + """ + class NamespaceNotFoundError(WikiToolsetError): """A requested namespace name or namespace ID does not exist. @@ -164,18 +177,6 @@ class EditError(WikiToolsetError): :py:meth:`Page.add_section `. """ -class PermissionsError(EditError): - """A permissions error ocurred while editing. - - We tried to do something we don't have permission to, like trying to delete - a page as a non-admin, or trying to edit a page without login information - and AssertEdit enabled. This will also be raised if we have been blocked - from editing. - - Raised by :py:meth:`Page.edit ` and - :py:meth:`Page.add_section `. - """ - class EditConflictError(EditError): """We gotten an edit conflict or a (rarer) delete/recreate conflict. diff --git a/earwigbot/wiki/page.py b/earwigbot/wiki/page.py index af047dd..eacb221 100644 --- a/earwigbot/wiki/page.py +++ b/earwigbot/wiki/page.py @@ -308,6 +308,7 @@ class Page(CopyvioMixIn): section, captcha_id, captcha_word) else: # Make sure we have the right token: params["token"] = self._token + self._token = None # Token now invalid # Try the API query, catching most errors with our handler: try: @@ -324,13 +325,8 @@ class Page(CopyvioMixIn): self._exists = self.PAGE_UNKNOWN return - # If we're here, then the edit failed. If it's because of AssertEdit, - # handle that. Otherwise, die - something odd is going on: - try: - assertion = result["edit"]["assert"] - except KeyError: - raise exceptions.EditError(result["edit"]) - self._handle_assert_edit(assertion, params, tries) + # Otherwise, there was some kind of problem. Throw an exception: + raise exceptions.EditError(result["edit"]) def _build_edit_params(self, text, summary, minor, bot, force, section, captcha_id, captcha_word): @@ -371,95 +367,27 @@ class Page(CopyvioMixIn): is protected), or we'll try to fix it (for example, if we can't edit due to being logged out, we'll try to log in). """ - if error.code in ["noedit", "cantcreate", "protectedtitle", - "noimageredirect"]: + perms = ["noedit", "noedit-anon", "cantcreate", "cantcreate-anon", + "protectedtitle", "noimageredirect", "noimageredirect-anon", + "blocked"] + if error.code in perms: raise exceptions.PermissionsError(error.info) - - elif error.code in ["noedit-anon", "cantcreate-anon", - "noimageredirect-anon"]: - if not all(self.site._login_info): - # Insufficient login info: - raise exceptions.PermissionsError(error.info) - if tries == 0: - # We have login info; try to login: - self.site._login(self.site._login_info) - self._token = None # Need a new token; old one is invalid now - return self._edit(params=params, tries=1) - else: - # We already tried to log in and failed! - e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." - raise exceptions.LoginError(e) - elif error.code in ["editconflict", "pagedeleted", "articleexists"]: # These attributes are now invalidated: self._content = None self._basetimestamp = None self._exists = self.PAGE_UNKNOWN raise exceptions.EditConflictError(error.info) - elif error.code in ["emptypage", "emptynewsection"]: raise exceptions.NoContentError(error.info) - - elif error.code == "blocked": - if tries > 0 or not all(self.site._login_info): - raise exceptions.PermissionsError(error.info) - else: - # Perhaps we are blocked from being logged-out? Try to log in: - self.site._login(self.site._login_info) - self._token = None # Need a new token; old one is invalid now - return self._edit(params=params, tries=1) - elif error.code == "contenttoobig": raise exceptions.ContentTooBigError(error.info) - elif error.code == "spamdetected": raise exceptions.SpamDetectedError(error.info) - elif error.code == "filtered": raise exceptions.FilteredError(error.info) - raise exceptions.EditError(": ".join((error.code, error.info))) - def _handle_assert_edit(self, assertion, params, tries): - """If we can't edit due to a failed AssertEdit assertion, handle that. - - If the assertion was 'user' and we have valid login information, try to - log in. Otherwise, raise PermissionsError with details. - """ - if assertion == "user": - if not all(self.site._login_info): - # Insufficient login info: - e = "AssertEdit: user assertion failed, and no login info was provided." - raise exceptions.PermissionsError(e) - if tries == 0: - # We have login info; try to login: - self.site._login(self.site._login_info) - self._token = None # Need a new token; old one is invalid now - return self._edit(params=params, tries=1) - else: - # We already tried to log in and failed! - e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." - raise exceptions.LoginError(e) - - elif assertion == "bot": - if not all(self.site._login_info): - # Insufficient login info: - e = "AssertEdit: bot assertion failed, and no login info was provided." - raise exceptions.PermissionsError(e) - if tries == 0: - # Try to log in if we got logged out: - self.site._login(self.site._login_info) - self._token = None # Need a new token; old one is invalid now - return self._edit(params=params, tries=1) - else: - # We already tried to log in, so we don't have a bot flag: - e = "AssertEdit: bot assertion failed: we don't have a bot flag!" - raise exceptions.PermissionsError(e) - - # Unknown assertion, maybe "true", "false", or "exists": - e = "AssertEdit: assertion '{0}' failed.".format(assertion) - raise exceptions.PermissionsError(e) - @property def site(self): """The page's corresponding Site object.""" diff --git a/earwigbot/wiki/site.py b/earwigbot/wiki/site.py index 31f5f39..1947f13 100644 --- a/earwigbot/wiki/site.py +++ b/earwigbot/wiki/site.py @@ -209,11 +209,13 @@ class Site(object): args.append(key + "=" + val) return "&".join(args) - def _api_query(self, params, tries=0, wait=5, ignore_maxlag=False): + def _api_query(self, params, tries=0, wait=5, ignore_maxlag=False, + ae_retry=True): """Do an API query with *params* as a dict of parameters. See the documentation for :py:meth:`api_query` for full implementation - details. + details. *tries*, *wait*, and *ignore_maxlag* are for maxlag; + *ae_retry* is for AssertEdit. """ since_last_query = time() - self._last_query_time # Throttling support if since_last_query < self._wait_between_queries: @@ -247,7 +249,7 @@ class Site(object): gzipper = GzipFile(fileobj=stream) result = gzipper.read() - return self._handle_api_query_result(result, params, tries, wait) + return self._handle_api_result(result, params, tries, wait, ae_retry) def _build_api_query(self, params, ignore_maxlag): """Given API query params, return the URL to query and POST data.""" @@ -257,7 +259,8 @@ class Site(object): url = ''.join((self.url, self._script_path, "/api.php")) params["format"] = "json" # This is the only format we understand - if self._assert_edit: # If requested, ensure that we're logged in + if self._assert_edit and params.get("action") != "login": + # If requested, ensure that we're logged in params["assert"] = self._assert_edit if self._maxlag and not ignore_maxlag: # If requested, don't overload the servers: @@ -266,7 +269,7 @@ class Site(object): data = self._urlencode_utf8(params) return url, data - def _handle_api_query_result(self, result, params, tries, wait): + def _handle_api_result(self, result, params, tries, wait, ae_retry): """Given the result of an API query, attempt to return useful data.""" try: res = loads(result) # Try to parse as a JSON object @@ -277,8 +280,8 @@ class Site(object): try: code = res["error"]["code"] info = res["error"]["info"] - except (TypeError, KeyError): # Having these keys indicates a problem - return res # All is well; return the decoded JSON + except (TypeError, KeyError): # If there's no error code/info, return + return res if code == "maxlag": # We've been throttled by the server if tries >= self._max_retries: @@ -288,7 +291,22 @@ class Site(object): msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' self._logger.info(msg.format(info, wait, tries, self._max_retries)) sleep(wait) - return self._api_query(params, tries=tries, wait=wait*2) + return self._api_query(params, tries, wait * 2, ae_retry=ae_retry) + elif code in ["assertuserfailed", "assertbotfailed"]: # AssertEdit + if ae_retry and all(self._login_info): + # Try to log in if we got logged out: + self._login(self._login_info) + if "token" in params: # Fetch a new one; this is invalid now + tparams = {"action": "tokens", "type": params["action"]} + params["token"] = self._api_query(tparams, ae_retry=False) + return self._api_query(params, tries, wait, ae_retry=False) + if not all(self._login_info): + e = "Assertion failed, and no login info was provided." + elif code == "assertbotfailed": + e = "Bot assertion failed: we don't have a bot flag!" + else: + e = "User assertion failed due to an unknown issue. Cookie problem?" + raise exceptions.PermissionsError("AssertEdit: " + e) else: # Some unknown error occurred e = 'API query failed: got error "{0}"; server says: "{1}".' error = exceptions.APIError(e.format(code, info))