diff --git a/w3af/core/controllers/chrome/devtools/exceptions.py b/w3af/core/controllers/chrome/devtools/exceptions.py
index 38ac0e3561..45214b34c6 100644
--- a/w3af/core/controllers/chrome/devtools/exceptions.py
+++ b/w3af/core/controllers/chrome/devtools/exceptions.py
@@ -27,3 +27,11 @@ class ChromeInterfaceException(Exception):
class ChromeInterfaceTimeout(Exception):
pass
+
+
+class ChromeScriptRuntimeException(Exception):
+ def __init__(self, message, function_called=None, *args):
+ if function_called:
+ message = "function: {}, exception: {}".format(function_called, message)
+ super(ChromeScriptRuntimeException, self).__init__(message, *args)
+ pass
diff --git a/w3af/core/controllers/chrome/instrumented/frame_manager.py b/w3af/core/controllers/chrome/instrumented/frame_manager.py
index 14f660559a..f96f46e8c6 100644
--- a/w3af/core/controllers/chrome/instrumented/frame_manager.py
+++ b/w3af/core/controllers/chrome/instrumented/frame_manager.py
@@ -166,7 +166,7 @@ def _on_frame_navigated(self, message):
# URL all the child frames are removed from Chrome, we should remove
# them from our code too to mirror state
if frame:
- for child_frame_id, child_frame in frame.child_frames:
+ for child_frame_id, child_frame in frame.child_frames.items():
child_frame.detach(self)
frame.set_navigated()
diff --git a/w3af/core/controllers/chrome/instrumented/main.py b/w3af/core/controllers/chrome/instrumented/main.py
index 41262e49ba..9b3672aa95 100644
--- a/w3af/core/controllers/chrome/instrumented/main.py
+++ b/w3af/core/controllers/chrome/instrumented/main.py
@@ -23,6 +23,7 @@
import json
import w3af.core.controllers.output_manager as om
+from w3af.core.controllers.chrome.devtools.exceptions import ChromeScriptRuntimeException
from w3af.core.data.parsers.doc.url import URL
from w3af.core.controllers.chrome.instrumented.instrumented_base import InstrumentedChromeBase
@@ -297,11 +298,20 @@ def dispatch_js_event(self, selector, event_type):
return True
- def get_login_forms(self):
+ def get_login_forms(self, exact_css_selectors):
"""
+ :param dict exact_css_selectors: Optional parameter containing css selectors
+ for part of form like username input or login button.
:return: Yield LoginForm instances
"""
- result = self.js_runtime_evaluate('window._DOMAnalyzer.getLoginForms()')
+ func = (
+ 'window._DOMAnalyzer.getLoginForms("{}", "{}")'
+ )
+ func = func.format(
+ exact_css_selectors.get('username_input', '').replace('"', '\\"'),
+ exact_css_selectors.get('login_button', '').replace('"', '\\"'),
+ )
+ result = self.js_runtime_evaluate(func)
if result is None:
raise EventTimeout('The event execution timed out')
@@ -316,11 +326,20 @@ def get_login_forms(self):
yield login_form
- def get_login_forms_without_form_tags(self):
+ def get_login_forms_without_form_tags(self, exact_css_selectors):
"""
+ :param dict exact_css_selectors: Optional parameter containing css selectors
+ for part of form like username input or login button.
:return: Yield LoginForm instances
"""
- result = self.js_runtime_evaluate('window._DOMAnalyzer.getLoginFormsWithoutFormTags()')
+ func = (
+ 'window._DOMAnalyzer.getLoginFormsWithoutFormTags("{}", "{}")'
+ )
+ func = func.format(
+ exact_css_selectors.get('username_input', '').replace('"', '\\"'),
+ exact_css_selectors.get('login_button', '').replace('"', '\\"'),
+ )
+ result = self.js_runtime_evaluate(func)
if result is None:
raise EventTimeout('The event execution timed out')
@@ -406,9 +425,9 @@ def focus(self, selector):
if result is None:
return None
- node_ids = result.get('result', {}).get('nodeIds', None)
+ node_ids = result.get('result', {}).get('nodeIds')
- if node_ids is None:
+ if not node_ids:
msg = ('The call to chrome.focus() failed.'
' CSS selector "%s" returned no nodes (did: %s)')
args = (selector, self.debugging_id)
@@ -589,19 +608,13 @@ def js_runtime_evaluate(self, expression, timeout=5):
timeout=timeout)
# This is a rare case where the DOM is not present
- if result is None:
- return None
-
- if 'result' not in result:
- return None
-
- if 'result' not in result['result']:
- return None
-
- if 'value' not in result['result']['result']:
- return None
-
- return result['result']['result']['value']
+ runtime_exception = result.get('result', {}).get('exceptionDetails')
+ if runtime_exception:
+ raise ChromeScriptRuntimeException(
+ runtime_exception,
+ function_called=expression
+ )
+ return result.get('result', {}).get('result', {}).get('value', None)
def get_js_variable_value(self, variable_name):
"""
diff --git a/w3af/core/controllers/chrome/js/dom_analyzer.js b/w3af/core/controllers/chrome/js/dom_analyzer.js
index b8077b60a3..9b113de676 100644
--- a/w3af/core/controllers/chrome/js/dom_analyzer.js
+++ b/w3af/core/controllers/chrome/js/dom_analyzer.js
@@ -330,7 +330,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
if( !_DOMAnalyzer.eventIsValidForTagName( tag_name, type ) ) return false;
let selector = OptimalSelect.getSingleSelector(element);
-
+
// node_type is https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants
_DOMAnalyzer.event_listeners.push({"tag_name": tag_name,
"node_type": element.nodeType,
@@ -865,6 +865,48 @@ var _DOMAnalyzer = _DOMAnalyzer || {
return false;
},
+ /**
+ * This is naive function which takes parentElement (the login form) and
+ * tries to find username input field within it.
+ * @param {Node} parentElement - parent element to scope to document.querySelectorAll()
+ * @param {String} exactSelector - optional CSS selector. If provided prevents
+ * using standard selectors
+ * @returns {NodeList} - result of querySelectorAll()
+ */
+ _getUsernameInput(parentElement, exactSelector = '') {
+ if (exactSelector) {
+ return document.querySelectorAll(exactSelector, parentElement);
+ }
+ result = document.querySelectorAll("input[type='email']", parentElement);
+ if (!result.length) {
+ result = document.querySelectorAll("input[type='text']", parentElement);
+ }
+ return result;
+ },
+
+ /**
+ * This is naive function which takes parentElement (the login form) and tries
+ * to find submit button within it.
+ * @param {Node} parentElement - parent element to scope to document.querySelectorAll()
+ * @param {String} exactSelector - optional CSS selector. If provided prevents
+ * using standard selectors
+ * @returns {NodeList} - result of querySelectorAll()
+ */
+ _getSubmitButton(parentElement, exactSelector = '') {
+ if (exactSelector) {
+ return document.querySelectorAll(exactSelector, parentElement);
+ }
+ result = document.querySelectorAll("input[type='submit']", parentElement);
+ if (!result.length) {
+ result = document.querySelectorAll("button[type='submit']", parentElement);
+ }
+ // Maybe it's just normal button without type="submit"...
+ if (!result.length) {
+ result = document.querySelectorAll('button', parentElement);
+ }
+ return result;
+ },
+
/**
* Return the CSS selector for the login forms which exist in the DOM.
*
@@ -874,8 +916,12 @@ var _DOMAnalyzer = _DOMAnalyzer || {
* - , and
* -
*
+ * @param {String} usernameCssSelector - CSS selector for username input. If
+ * provided we won't try to find username input automatically.
+ * @param {String} submitButtonCssSelector - CSS selector for submit button. If
+ * provided we won't try to find submit button autmatically.
*/
- getLoginForms: function () {
+ getLoginForms: function (usernameCssSelector = '', submitButtonCssSelector = '') {
let login_forms = [];
// First we identify the forms with a password field using a descendant Selector
@@ -898,7 +944,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
let form = forms[0];
// Finally we confirm that the form has a type=text input
- let text_fields = document.querySelectorAll("input[type='text']", form)
+ let text_fields = this._getUsernameInput(form, usernameCssSelector);
// Zero text fields is most likely a password-only login form
// Two text fields or more is most likely a registration from
@@ -906,7 +952,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
if (text_fields.length !== 1) continue;
// And if there is a submit button I want that selector too
- let submit_fields = document.querySelectorAll("input[type='submit']", form)
+ let submit_fields = this._getSubmitButton(form, submitButtonCssSelector);
let submit_selector = null;
if (submit_fields.length !== 0) {
@@ -936,8 +982,12 @@ var _DOMAnalyzer = _DOMAnalyzer || {
* - , and
* -
*
+ * @param {String} usernameCssSelector - CSS selector for username input. If
+ * provided we won't try to find username input automatically.
+ * @param {String} submitButtonCssSelector - CSS selector for submit button. If
+ * provided we won't try to find submit button autmatically.
*/
- getLoginFormsWithoutFormTags: function () {
+ getLoginFormsWithoutFormTags: function (usernameCssSelector = '', submitButtonCssSelector = '') {
let login_forms = [];
// First we identify the password fields
@@ -962,7 +1012,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
// go up one more level, and so one.
//
// Find if this parent has a type=text input
- let text_fields = document.querySelectorAll("input[type='text']", parent)
+ let text_fields = this._getUsernameInput(parent, usernameCssSelector);
// Zero text fields is most likely a password-only login form
// Two text fields or more is most likely a registration from
@@ -974,7 +1024,7 @@ var _DOMAnalyzer = _DOMAnalyzer || {
}
// And if there is a submit button I want that selector too
- let submit_fields = document.querySelectorAll("input[type='submit']", parent)
+ let submit_fields = this._getSubmitButton(parent, submitButtonCssSelector)
let submit_selector = null;
if (submit_fields.length !== 0) {
@@ -999,6 +1049,12 @@ var _DOMAnalyzer = _DOMAnalyzer || {
return JSON.stringify(login_forms);
},
+ clickOnSelector(exactSelector) {
+ let element = document.querySelector(exactSelector);
+ element.click();
+ return 'success'
+ },
+
sliceAndSerialize: function (filtered_event_listeners, start, count) {
return JSON.stringify(filtered_event_listeners.slice(start, start + count));
},
@@ -1142,4 +1198,4 @@ var _DOMAnalyzer = _DOMAnalyzer || {
};
-_DOMAnalyzer.initialize();
\ No newline at end of file
+_DOMAnalyzer.initialize();
diff --git a/w3af/core/controllers/chrome/login/find_form/main.py b/w3af/core/controllers/chrome/login/find_form/main.py
index 2ee45ad7f5..2e42e13c57 100644
--- a/w3af/core/controllers/chrome/login/find_form/main.py
+++ b/w3af/core/controllers/chrome/login/find_form/main.py
@@ -36,16 +36,24 @@ def __init__(self, chrome, debugging_id):
self.chrome = chrome
self.debugging_id = debugging_id
- def find_forms(self):
+ def find_forms(self, css_selectors=None):
"""
+ :param dict css_selectors: optional dict of css selectors used to find
+ elements of form (like username input or login button)
:return: Yield forms as they are found by each strategy
"""
+ if css_selectors:
+ msg = 'Form finder uses the CSS selectors: "%s" (did: %s)'
+ args = (css_selectors, self.debugging_id)
+ om.out.debug(msg % args)
+
identified_forms = []
for strategy_klass in self.STRATEGIES:
- strategy = strategy_klass(self.chrome, self.debugging_id)
+ strategy = strategy_klass(self.chrome, self.debugging_id, css_selectors)
try:
+ strategy.prepare()
for form in strategy.find_forms():
if form in identified_forms:
continue
@@ -55,6 +63,6 @@ def find_forms(self):
except Exception as e:
msg = 'Form finder strategy %s raised exception: "%s" (did: %s)'
args = (strategy.get_name(),
- e,
+ repr(e),
self.debugging_id)
om.out.debug(msg % args)
diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py
new file mode 100644
index 0000000000..6c635adc44
--- /dev/null
+++ b/w3af/core/controllers/chrome/login/find_form/strategies/base_find_form_strategy.py
@@ -0,0 +1,35 @@
+from w3af.core.controllers.chrome.instrumented.exceptions import EventTimeout
+
+
+class BaseFindFormStrategy:
+ def __init__(self, chrome, debugging_id, exact_css_selectors=None):
+ """
+ :param InstrumentedChrome chrome:
+ :param String debugging_id:
+ :param dict exact_css_selectors: Optional parameter containing css selectors
+ for part of form like username input or login button.
+ """
+ self.chrome = chrome
+ self.debugging_id = debugging_id
+ self.exact_css_selectors = exact_css_selectors or {}
+
+ def prepare(self):
+ """
+ :raises EventTimeout:
+ Hook called before find_forms()
+ """
+ form_activator_selector = self.exact_css_selectors.get('form_activator')
+ if form_activator_selector:
+ func = 'window._DOMAnalyzer.clickOnSelector("{}")'.format(
+ form_activator_selector.replace('"', '\\"')
+ )
+ result = self.chrome.js_runtime_evaluate(func)
+ if result is None:
+ raise EventTimeout('The event execution timed out')
+
+ def find_forms(self):
+ raise NotImplementedError
+
+ @staticmethod
+ def get_name():
+ return 'BaseFindFormStrategy'
diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py b/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py
index bf47ba4a17..ec6da6aab0 100644
--- a/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py
+++ b/w3af/core/controllers/chrome/login/find_form/strategies/form_tag.py
@@ -19,12 +19,11 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""
+from w3af.core.controllers.chrome.login.find_form.strategies.base_find_form_strategy import \
+ BaseFindFormStrategy
-class FormTagStrategy(object):
- def __init__(self, chrome, debugging_id):
- self.chrome = chrome
- self.debugging_id = debugging_id
+class FormTagStrategy(BaseFindFormStrategy):
def find_forms(self):
"""
@@ -37,7 +36,7 @@ def _simple_form_with_username_password_submit(self):
"""
:return: Yield forms that have username, password and submit inputs
"""
- for login_form in self.chrome.get_login_forms():
+ for login_form in self.chrome.get_login_forms(self.exact_css_selectors):
yield login_form
@staticmethod
diff --git a/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py b/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py
index 1f64780502..4dbf7c654a 100644
--- a/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py
+++ b/w3af/core/controllers/chrome/login/find_form/strategies/password_and_parent.py
@@ -19,12 +19,11 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""
+from w3af.core.controllers.chrome.login.find_form.strategies.base_find_form_strategy import \
+ BaseFindFormStrategy
-class PasswordAndParentStrategy(object):
- def __init__(self, chrome, debugging_id):
- self.chrome = chrome
- self.debugging_id = debugging_id
+class PasswordAndParentStrategy(BaseFindFormStrategy):
def find_forms(self):
"""
@@ -32,8 +31,9 @@ def find_forms(self):
:return: Yield forms which are identified by the strategy algorithm
"""
- for login_form in self.chrome.get_login_forms_without_form_tags():
+ for login_form in self.chrome.get_login_forms_without_form_tags(self.exact_css_selectors):
yield login_form
- def get_name(self):
+ @staticmethod
+ def get_name():
return 'PasswordAndParent'
diff --git a/w3af/core/controllers/chrome/login/submit_form/main.py b/w3af/core/controllers/chrome/login/submit_form/main.py
index b3954a5b92..a4726663c8 100644
--- a/w3af/core/controllers/chrome/login/submit_form/main.py
+++ b/w3af/core/controllers/chrome/login/submit_form/main.py
@@ -19,6 +19,8 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""
+import traceback
+
from w3af.core.controllers import output_manager as om
from w3af.core.controllers.chrome.login.submit_form.strategies.press_enter import PressEnterStrategy
@@ -31,7 +33,7 @@ class FormSubmitter(object):
STRATEGIES = [
PressEnterStrategy,
PressTabEnterStrategy,
- #FormInputSubmitStrategy
+ # FormInputSubmitStrategy
]
def __init__(self, chrome, form, login_form_url, username, password, debugging_id):
@@ -91,3 +93,4 @@ def _handle_exception(self, strategy, e):
e,
self.debugging_id)
om.out.debug(msg % args)
+ om.out.error(traceback.format_exc())
diff --git a/w3af/core/data/options/option_list.py b/w3af/core/data/options/option_list.py
index 74f3c4820d..ff4a1d5207 100644
--- a/w3af/core/data/options/option_list.py
+++ b/w3af/core/data/options/option_list.py
@@ -35,6 +35,18 @@ def add(self, option):
self._internal_opt_list.append(option)
append = add
+ def pop(self, option):
+ """
+ DANGEROUS!!
+ You will probably want to deepcopy the OptionList instance before
+ modifying it with this method to don't block the user from accessing options
+ again.
+ """
+ if not isinstance(option, int):
+ option_names = [item.get_name() for item in self._internal_opt_list]
+ option = option_names.index(option)
+ return self._internal_opt_list.pop(option)
+
def __len__(self):
return len(self._internal_opt_list)
diff --git a/w3af/plugins/auth/autocomplete_js.py b/w3af/plugins/auth/autocomplete_js.py
index 6b1d39e2cd..488a1a5ee9 100644
--- a/w3af/plugins/auth/autocomplete_js.py
+++ b/w3af/plugins/auth/autocomplete_js.py
@@ -20,7 +20,10 @@
"""
import Queue
+from copy import deepcopy
+from w3af.core.data.options.opt_factory import opt_factory
+from w3af.core.data.options.option_types import STRING
from w3af.core.data.request.fuzzable_request import FuzzableRequest
from w3af.core.controllers.chrome.instrumented.main import InstrumentedChrome
from w3af.core.controllers.chrome.login.find_form.main import FormFinder
@@ -36,6 +39,11 @@ class autocomplete_js(autocomplete):
def __init__(self):
autocomplete.__init__(self)
+ # default values for autocomplete_js options
+ self.username_field_css_selector = ''
+ self.login_button_css_selector = ''
+ self.login_form_activator_css_selector = ''
+
self._login_form = None
self._http_traffic_queue = None
@@ -81,6 +89,7 @@ def login(self, debugging_id=None):
return True
def _handle_authentication_success(self):
+ self._login_result_log.append(True)
#
# Logging
#
@@ -129,7 +138,12 @@ def _login_using_existing_form(self, chrome):
:param chrome: The chrome instance to use during login
:return: True if login was successful
"""
- raise NotImplementedError
+ form_submit_strategy = self._find_form_submit_strategy(chrome, self._login_form)
+ if form_submit_strategy is None:
+ return False
+ self._login_form.set_submit_strategy(form_submit_strategy)
+ self._log_debug('Identified valid login form: %s' % self._login_form)
+ return True
def _login_and_save_form(self, chrome):
"""
@@ -207,8 +221,13 @@ def _find_all_login_forms(self, chrome):
* Use the FormFinder class to yield all existing forms
"""
form_finder = FormFinder(chrome, self._debugging_id)
+ css_selectors = {
+ 'username_input': self.username_field_css_selector,
+ 'login_button': self.login_button_css_selector,
+ 'form_activator': self.login_form_activator_css_selector,
+ }
- for form in form_finder.find_forms():
+ for form in form_finder.find_forms(css_selectors):
msg = 'Found potential login form: %s'
args = (form,)
@@ -239,7 +258,10 @@ def _find_form_submit_strategy(self, chrome, form):
for form_submit_strategy in form_submitter.submit_form():
- if not self.has_active_session(debugging_id=self._debugging_id):
+ if not self.has_active_session(debugging_id=self._debugging_id, chrome=chrome):
+ msg = '%s is invalid form submit strategy for %s'
+ args = (form_submit_strategy.get_name(), form)
+ self._log_debug(msg % args)
# No need to set the state of the chrome browser back to the
# login page, that is performed inside the FormSubmitter
continue
@@ -256,22 +278,89 @@ def _find_form_submit_strategy(self, chrome, form):
return None
- def has_active_session(self, debugging_id=None):
+ def has_active_session(self, debugging_id=None, chrome=None):
"""
Check user session with chrome
+ :param str debugging_id: string representing debugging id.
+ :param InstrumentedChrome chrome: chrome instance passed from outer scope
+ to reuse. EDGE CASE EXAMPLE:
+ Sometimes we don't want to create new chrome instance. For example
+ when we login for the first time to webapp and in _find_form_submit_strategy()
+ we just pressed enter in login form. Browser may take some actions under
+ the hood like sending XHR to backend API and after receiving response
+ setting API token at localStorage. Before token will be saved to localStorage
+ it may exist only in webapp's code, so using the same chrome will prevent
+ us from performing check without credentials.
"""
has_active_session = False
+ is_new_chrome_instance_created = False
self._set_debugging_id(debugging_id)
- chrome = self._get_chrome_instance(load_url=False)
+ if not chrome or not chrome.chrome_conn:
+ chrome = self._get_chrome_instance(load_url=False)
+ is_new_chrome_instance_created = True
try:
chrome.load_url(self.check_url)
chrome.wait_for_load()
has_active_session = self.check_string in chrome.get_dom()
finally:
- chrome.terminate()
+ if is_new_chrome_instance_created:
+ chrome.terminate()
return has_active_session
+ def get_options(self):
+ """
+ :returns OptionList: list of option objects for plugin
+ """
+ option_list = super(autocomplete_js, self).get_options()
+ autocomplete_js_options = [
+ (
+ 'username_field_css_selector',
+ self.username_field_css_selector,
+ STRING,
+ "(Optional) Exact CSS selector which will be used to retrieve "
+ "the username input field. When provided the scanner is not going"
+ " to try to detect the input field in an automated way"
+ ),
+ (
+ 'login_button_css_selector',
+ self.login_button_css_selector,
+ STRING,
+ "(Optional) Exact CSS selector which will be used to retrieve "
+ "the login button field. When provided the scanner is not going "
+ "to try to detect the login button in an automated way"
+ ),
+ (
+ 'login_form_activator_css_selector',
+ self.login_form_activator_css_selector,
+ STRING,
+ "(Optional) Exact CSS selector for the element which needs to be "
+ "clicked to show login form."
+ )
+ ]
+ for option in autocomplete_js_options:
+ option_list.add(opt_factory(
+ option[0],
+ option[1],
+ option[3],
+ option[2],
+ help=option[3],
+ ))
+ return option_list
+
+ def set_options(self, options_list):
+ options_list_copy = deepcopy(options_list) # we don't want to touch real option_list
+ self.username_field_css_selector = options_list_copy.pop(
+ 'username_field_css_selector'
+ ).get_value()
+ self.login_button_css_selector = options_list_copy.pop(
+ 'login_button_css_selector'
+ ).get_value()
+ self.login_form_activator_css_selector = options_list_copy.pop(
+ 'login_form_activator_css_selector'
+ ).get_value()
+ super(autocomplete_js, self).set_options(options_list_copy)
+
def get_long_desc(self):
"""
:return: A DETAILED description of the plugin functions and features.
@@ -283,7 +372,15 @@ def get_long_desc(self):
The plugin loads the `login_form_url` to obtain the login form, automatically
identifies the inputs where the `username` and `password` should be entered,
- and then submits the form by clicking on the login button.
+ and then submits the form by clicking on the login button. You can specify
+ the exact CSS selectors (like ".login > input #password") in
+ `username_filed_css_selector` and `login_button_css_selector` to force
+ plugin to use those selectors in case when it can't find username field
+ or login button automatically.
+
+ If the page requires to click on something to show the login form you
+ can set `login_form_activator_css_selector` and scanner will use it
+ find and click on element
The following configurable parameters exist:
- username
@@ -291,4 +388,7 @@ def get_long_desc(self):
- login_form_url
- check_url
- check_string
+ - username_field_css_selector
+ - login_button_css_selector
+ - login_form_activator_css_selector
"""