Exemplo n.º 1
0
class CampaignAssistant(gui_utilities.GladeGObject):
	"""
	Display an assistant which walks the user through creating a new campaign or
	configuring an existing campaign. If no *campaign_id* is specified a new
	campaign will be created.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			resources.CompanyEditorGrid(
				gui_utilities.GladeProxyDestination(
					widget='alignment_company',
					method='add'
				)
			),
			'calendar_campaign_expiration',
			'checkbutton_alert_subscribe',
			'checkbutton_expire_campaign',
			'checkbutton_reject_after_credentials',
			'combobox_campaign_type',
			'combobox_company_existing',
			'entry_campaign_name',
			'entry_campaign_description',
			'entry_test_validation_text',
			'entry_validation_regex_username',
			'entry_validation_regex_password',
			'entry_validation_regex_mfa_token',
			'frame_campaign_expiration',
			'frame_company_existing',
			'frame_company_new',
			'image_intro_title',
			'label_confirm_body',
			'label_confirm_title',
			'label_intro_body',
			'label_intro_title',
			'label_validation_regex_username',
			'label_validation_regex_password',
			'label_validation_regex_mfa_token',
			'radiobutton_company_existing',
			'radiobutton_company_new',
			'radiobutton_company_none',
			'togglebutton_expiration_time'
		),
		top_level=(
			'ClockHourAdjustment',
			'ClockMinuteAdjustment'
		)
	)
	top_gobject = 'assistant'
	objects_persist = False
	def __init__(self, application, campaign_id=None):
		"""
		:param application: The application instance which this object belongs to.
		:type application: :py:class:`~king_phisher.client.application.KingPhisherClientApplication`
		:param campaign_id: The ID of the campaign to edit.
		"""
		super(CampaignAssistant, self).__init__(application)
		self.campaign_id = campaign_id
		self._close_ready = True
		self._page_titles = {}
		for page_n in range(self.assistant.get_n_pages()):
			page = self.assistant.get_nth_page(page_n)
			page_title = self.assistant.get_page_title(page)
			if page_title:
				self._page_titles[page_title] = page_n

		self._expiration_time = managers.TimeSelectorButtonManager(self.application, self.gobjects['togglebutton_expiration_time'])
		self._set_comboboxes()
		self._set_defaults()

		if not self.config['server_config']['server.require_id']:
			self.gobjects['checkbutton_reject_after_credentials'].set_sensitive(False)
			self.gobjects['checkbutton_reject_after_credentials'].set_property('active', False)

		confirm_preamble = 'Verify all settings are correct in the previous sections'
		if campaign_id:
			# re-configuring an existing campaign
			self.gobjects['label_confirm_body'].set_text(confirm_preamble + ', then hit "Apply" to update the King Phisher campaign with the new settings.')
			self.gobjects['label_intro_body'].set_text('This assistant will walk you through reconfiguring the selected King Phisher campaign.')
			self.gobjects['label_intro_title'].set_text('Configure Campaign')
		else:
			# creating a new campaign
			self.gobjects['label_confirm_body'].set_text(confirm_preamble + ', then hit "Apply" to create the new King Phisher campaign.')
			self.gobjects['label_intro_body'].set_text('This assistant will walk you through creating and configuring a new King Phisher campaign.')
			self.gobjects['label_intro_title'].set_text('New Campaign')

	@property
	def campaign_name(self):
		"""
		The string value of the configured campaign name. This may be set even
		when the campaign was not created, which would be the case if the user
		closed the window.
		"""
		return self.gobjects['entry_campaign_name'].get_text()

	@property
	def is_editing_campaign(self):
		return self.campaign_id is not None

	@property
	def is_new_campaign(self):
		return self.campaign_id is None

	def _set_comboboxes(self):
		"""Set up all the comboboxes and load the data for their models."""
		renderer = resources.renderer_text_desc
		rpc = self.application.rpc
		for tag_name, tag_table in (('campaign_type', 'campaign_types'), ('company_existing', 'companies'), ('company_industry', 'industries')):
			combobox = self.gobjects['combobox_' + tag_name]
			model = combobox.get_model()
			if model is None:
				combobox.pack_start(renderer, True)
				combobox.add_attribute(renderer, 'text', 2)
			combobox.set_model(rpc.get_tag_model(tag_table, model=model))

	def _set_defaults(self):
		"""
		Set any default values for widgets. Also load settings from the existing
		campaign if one was specified.
		"""
		calendar = self.gobjects['calendar_campaign_expiration']
		default_day = datetime.datetime.today() + datetime.timedelta(days=31)
		gui_utilities.gtk_calendar_set_pydate(calendar, default_day)

		if self.campaign_id is None:
			return
		campaign = self.application.get_graphql_campaign()

		# set entries
		self.gobjects['entry_campaign_name'].set_text(campaign['name'])
		self.gobjects['entry_validation_regex_username'].set_text(campaign['credentialRegexUsername'] or '')
		self.gobjects['entry_validation_regex_password'].set_text(campaign['credentialRegexPassword'] or '')
		self.gobjects['entry_validation_regex_mfa_token'].set_text(campaign['credentialRegexMfaToken'] or '')

		if campaign['description'] is not None:
			self.gobjects['entry_campaign_description'].set_text(campaign['description'])
		if campaign['campaignType'] is not None:
			combobox = self.gobjects['combobox_campaign_type']
			model = combobox.get_model()
			model_iter = gui_utilities.gtk_list_store_search(model, campaign['campaignType']['id'], column=0)
			if model_iter is not None:
				combobox.set_active_iter(model_iter)

		self.gobjects['checkbutton_alert_subscribe'].set_property('active', self.application.rpc('campaign/alerts/is_subscribed', self.campaign_id))
		self.gobjects['checkbutton_reject_after_credentials'].set_property('active', bool(campaign['maxCredentials']))

		if campaign['company'] is not None:
			self.gobjects['radiobutton_company_existing'].set_active(True)
			combobox = self.gobjects['combobox_company_existing']
			model = combobox.get_model()
			model_iter = gui_utilities.gtk_list_store_search(model, campaign['company']['id'], column=0)
			if model_iter is not None:
				combobox.set_active_iter(model_iter)

		if campaign['expiration'] is not None:
			expiration = utilities.datetime_utc_to_local(campaign['expiration'])
			self.gobjects['checkbutton_expire_campaign'].set_active(True)
			self._expiration_time.time = expiration.time()
			gui_utilities.gtk_calendar_set_pydate(self.gobjects['calendar_campaign_expiration'], expiration.date())

	def _get_tag_from_combobox(self, combobox, db_table):
		model = combobox.get_model()
		model_iter = combobox.get_active_iter()
		if model_iter is not None:
			return model.get_value(model_iter, 0)
		campaign_type = combobox.get_child().get_text().strip()
		if not campaign_type:
			return
		model_iter = gui_utilities.gtk_list_store_search(model, campaign_type, column=1)
		if model_iter is None:
			return self.application.rpc('db/table/insert', db_table, 'name', campaign_type)
		return model.get_value(model_iter, 0)

	def _get_company_existing_id(self):
		combobox_company = self.gobjects['combobox_company_existing']
		model = combobox_company.get_model()
		model_iter = combobox_company.get_active_iter()
		if model is None or model_iter is None:
			return
		return model.get_value(model_iter, 0)

	def _get_company_new_id(self):
		name = self.gobjects['entry_company_name'].get_text()
		name = name.strip()
		# check if this company name already exists in the model
		model = self.gobjects['combobox_company_existing'].get_model()
		model_iter = gui_utilities.gtk_list_store_search(model, name, column=1)
		if model_iter is not None:
			return model.get_value(model_iter, 0)
		# check if this company name already exists remotely
		remote_company = self._get_graphql_company(name)
		if remote_company:
			return remote_company['id']

		company_id = self.application.rpc(
			'db/table/insert',
			'companies',
			('name', 'description', 'industry_id', 'url_main', 'url_email', 'url_remote_access'),
			(
				name,
				self.get_entry_value('company_description'),
				self._get_tag_from_combobox(self.gobjects['combobox_company_industry'], 'industries'),
				self.get_entry_value('company_url_main'),
				self.get_entry_value('company_url_email'),
				self.get_entry_value('company_url_remote_access')
			)
		)
		self.gobjects['radiobutton_company_existing'].set_active(True)
		return company_id

	def _get_graphql_company(self, company_name):
		results = self.application.rpc.graphql("""\
		query getCompany($name: String!) {
			db {
				company(name: $name) {
					id
				}
			}
		}""", {'name': company_name})
		return results['db']['company']

	def _do_regex_validation(self, test_text, entry):
		try:
			regex = re.compile(entry.get_text())
		except re.error:
			entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
			return
		result = True
		if regex.pattern and test_text:
			result = regex.match(test_text) is not None
		entry.set_property('secondary-icon-stock', 'gtk-yes' if result else 'gtk-no')

	def signal_assistant_apply(self, _):
		self._close_ready = False
		# have to do it this way because the next page will be selected when the apply signal is complete
		set_current_page = lambda page_name: self.assistant.set_current_page(max(0, self._page_titles[page_name] - 1))

		# get and validate the campaign name
		campaign_name = self.gobjects['entry_campaign_name'].get_text()
		campaign_name = campaign_name.strip()
		if not campaign_name:
			gui_utilities.show_dialog_error('Invalid Campaign Name', self.parent, 'A unique and valid campaign name must be specified.')
			set_current_page('Basic Settings')
			return True

		properties = {}

		# validate the credential validation regular expressions
		for field in ('username', 'password', 'mfa_token'):
			regex = self.gobjects['entry_validation_regex_' + field].get_text()
			if regex:
				try:
					re.compile(regex)
				except re.error:
					label = self.gobjects['label_validation_regex_' + field].get_text()
					gui_utilities.show_dialog_error('Invalid Regex', self.parent, "The '{0}' regular expression is invalid.".format(label))
					return True
			else:
				regex = None  # keep empty strings out of the database
			properties['credential_regex_' + field] = regex

		# validate the company
		company_id = None
		if self.gobjects['radiobutton_company_existing'].get_active():
			company_id = self._get_company_existing_id()
			if company_id is None:
				gui_utilities.show_dialog_error('Invalid Company', self.parent, 'A valid existing company must be specified.')
				set_current_page('Company')
				return True
		elif self.gobjects['radiobutton_company_new'].get_active():
			company_id = self._get_company_new_id()
			if company_id is None:
				gui_utilities.show_dialog_error('Invalid Company', self.parent, 'The new company settings are invalid.')
				set_current_page('Company')
				return True

		# get and validate the campaign expiration
		expiration = None
		if self.gobjects['checkbutton_expire_campaign'].get_property('active'):
			expiration = datetime.datetime.combine(
				gui_utilities.gtk_calendar_get_pydate(self.gobjects['calendar_campaign_expiration']),
				self._expiration_time.time
			)
			expiration = utilities.datetime_local_to_utc(expiration)
			if self.is_new_campaign and expiration <= datetime.datetime.now():
				gui_utilities.show_dialog_error('Invalid Campaign Expiration', self.parent, 'The expiration date is set in the past.')
				set_current_page('Expiration')
				return True

		# point of no return
		campaign_description = self.get_entry_value('campaign_description')
		if self.campaign_id:
			properties['name'] = self.campaign_name
			properties['description'] = campaign_description
			cid = self.campaign_id
		else:
			try:
				cid = self.application.rpc('campaign/new', campaign_name, description=campaign_description)
			except advancedhttpserver.RPCError as error:
				if not error.is_remote_exception:
					raise error
				if not error.remote_exception['name'] == 'exceptions.ValueError':
					raise error
				error_message = error.remote_exception.get('message', 'an unknown error occurred').capitalize() + '.'
				gui_utilities.show_dialog_error('Failed To Create Campaign', self.parent, error_message)
				set_current_page('Basic Settings')
				return True
			self.application.emit('campaign-created', cid)

		properties['campaign_type_id'] = self._get_tag_from_combobox(self.gobjects['combobox_campaign_type'], 'campaign_types')
		properties['company_id'] = company_id
		properties['expiration'] = expiration
		properties['max_credentials'] = (1 if self.gobjects['checkbutton_reject_after_credentials'].get_property('active') else None)
		self.application.rpc('db/table/set', 'campaigns', cid, tuple(properties.keys()), tuple(properties.values()))

		should_subscribe = self.gobjects['checkbutton_alert_subscribe'].get_property('active')
		if should_subscribe != self.application.rpc('campaign/alerts/is_subscribed', cid):
			if should_subscribe:
				self.application.rpc('campaign/alerts/subscribe', cid)
			else:
				self.application.rpc('campaign/alerts/unsubscribe', cid)

		self.application.emit('campaign-changed', cid)
		self._close_ready = True
		return

	def signal_assistant_cancel(self, assistant):
		assistant.destroy()

	def signal_assistant_close(self, assistant):
		if self._close_ready:
			assistant.destroy()
		self._close_ready = True

	def signal_assistant_prepare(self, _, page):
		page_title = self.assistant.get_page_title(page)
		if page_title == 'Company':
			combobox = self.gobjects['combobox_company_existing']
			model = combobox.get_model()
			company_name = self.get_entry_value('company_name')
			if company_name:
				model_iter = gui_utilities.gtk_list_store_search(model, company_name, column=1)
				if model_iter is not None:
					combobox.set_active_iter(model_iter)
					self.gobjects['radiobutton_company_existing'].set_active(True)

	def signal_calendar_prev(self, calendar):
		today = datetime.date.today()
		calendar_day = gui_utilities.gtk_calendar_get_pydate(calendar)
		if calendar_day >= today:
			return
		gui_utilities.gtk_calendar_set_pydate(calendar, today)

	def signal_checkbutton_expire_campaign_toggled(self, _):
		active = self.gobjects['checkbutton_expire_campaign'].get_property('active')
		self.gobjects['frame_campaign_expiration'].set_sensitive(active)

	def signal_entry_changed_test_validation_text(self, field):
		test_text = field.get_text()
		for field in ('username', 'password', 'mfa_token'):
			self._do_regex_validation(test_text, self.gobjects['entry_validation_regex_' + field])

	def signal_entry_changed_validation_regex(self, entry):
		self._do_regex_validation(self.gobjects['entry_test_validation_text'].get_text(), entry)

	def signal_radiobutton_toggled(self, radiobutton):
		if not radiobutton.get_active():
			return
		if radiobutton == self.gobjects['radiobutton_company_existing']:
			self.gobjects['frame_company_existing'].set_sensitive(True)
			self.gobjects['frame_company_new'].set_sensitive(False)
		elif radiobutton == self.gobjects['radiobutton_company_new']:
			self.gobjects['frame_company_existing'].set_sensitive(False)
			self.gobjects['frame_company_new'].set_sensitive(True)
		elif radiobutton == self.gobjects['radiobutton_company_none']:
			self.gobjects['frame_company_existing'].set_sensitive(False)
			self.gobjects['frame_company_new'].set_sensitive(False)

	def interact(self):
		self.assistant.show_all()
Exemplo n.º 2
0
class CompanyEditorDialog(gui_utilities.GladeGObject):
    """
	Display a dialog which can be used to edit the various fields associated
	with a company object.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=(resources.CompanyEditorGrid(
            gui_utilities.GladeProxyDestination(
                widget='box_company',
                method='pack_start',
                kwargs=dict(expand=True, fill=True, padding=0))),
                  'combobox_company_existing'))
    top_gobject = 'dialog'
    objects_persist = False

    def __init__(self, *args, **kwargs):
        super(CompanyEditorDialog, self).__init__(*args, **kwargs)
        self._company_info_changed = False
        self._last_company_id = None
        self._set_comboboxes()
        self.gobjects['combobox_company_industry'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_industry'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_name'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_description'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_url_main'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_url_email'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_url_remote_access'].connect(
            'changed', self.signal_editable_changed)

    def _set_comboboxes(self):
        """Set up all the comboboxes and load the data for their models."""
        renderer = resources.renderer_text_desc
        rpc = self.application.rpc
        for tag_name, tag_table in (('company_existing', 'companies'),
                                    ('company_industry', 'industries')):
            combobox = self.gobjects['combobox_' + tag_name]
            model = combobox.get_model()
            if model is None:
                combobox.pack_start(renderer, True)
                combobox.add_attribute(renderer, 'text', 2)
            combobox.set_model(rpc.get_tag_model(tag_table, model=model))

    def _get_company_info(self, company_id):
        company = self.application.rpc.graphql(
            """\
		query getCompany($id: String!) {
			db {
				company(id: $id) {
					id
					description
					name
					industryId
					urlMain
					urlEmail
					urlRemoteAccess
				}
			}
		}""", {'id': company_id})['db']['company']
        combobox = self.gobjects['combobox_company_industry']
        if company['industryId'] is None:
            combobox.set_active_iter(None)
            combobox.get_child().set_text('')
        else:
            combobox.set_active_iter(
                gui_utilities.gtk_list_store_search(combobox.get_model(),
                                                    company['industryId']))
        self.gobjects['entry_company_name'].set_text(company['name'])
        self.gobjects['entry_company_description'].set_text(
            company['description'] or '')
        self.gobjects['entry_company_url_main'].set_text(company['urlMain']
                                                         or '')
        self.gobjects['entry_company_url_email'].set_text(company['urlEmail']
                                                          or '')
        self.gobjects['entry_company_url_remote_access'].set_text(
            company['urlRemoteAccess'] or '')

    def _get_tag_from_combobox(self, combobox, db_table):
        model = combobox.get_model()
        model_iter = combobox.get_active_iter()
        if model_iter is not None:
            return model.get_value(model_iter, 0)
        campaign_type = combobox.get_child().get_text().strip()
        if not campaign_type:
            return
        model_iter = gui_utilities.gtk_list_store_search(model,
                                                         campaign_type,
                                                         column=1)
        if model_iter is None:
            return self.application.rpc('db/table/insert', db_table, 'name',
                                        campaign_type)
        return model.get_value(model_iter, 0)

    def _set_company_info(self, company_id):
        company_name = self.get_entry_value('company_name')
        company_description = self.get_entry_value('company_description')
        self.application.rpc.remote_table_row_set(
            'companies', company_id, {
                'name':
                company_name,
                'description':
                company_description,
                'industry_id':
                self._get_tag_from_combobox(
                    self.gobjects['combobox_company_industry'], 'industries'),
                'url_main':
                self.get_entry_value('company_url_main'),
                'url_email':
                self.get_entry_value('company_url_email'),
                'url_remote_access':
                self.get_entry_value('company_url_remote_access')
            })
        model = self.gobjects['combobox_company_existing'].get_model()
        model_iter = gui_utilities.gtk_list_store_search(model, company_id)
        model[model_iter][1] = company_name
        model[model_iter][2] = company_description

    def _should_set_company_info(self):
        if self._last_company_id is None:
            return False
        if not self._company_info_changed:
            return False
        return gui_utilities.show_dialog_yes_no(
            'Update Company Info?', self.parent,
            'Do you want to save the changes to the company information?')

    def interact(self):
        self.dialog.show_all()
        response = self.dialog.run()
        if response == Gtk.ResponseType.APPLY and self._should_set_company_info(
        ):
            self._set_company_info(self._last_company_id)
        self.dialog.destroy()

    def signal_combobox_company_changed(self, _):
        if self._should_set_company_info():
            self._set_company_info(self._last_company_id)

        combobox = self.gobjects['combobox_company_existing']
        model = combobox.get_model()
        company_id = model.get_value(combobox.get_active_iter(), 0)
        self._last_company_id = company_id
        self._get_company_info(company_id)
        self._company_info_changed = False

    def signal_editable_changed(self, _):
        if self._last_company_id is None:
            return
        self._company_info_changed = True
Exemplo n.º 3
0
class CampaignAssistant(gui_utilities.GladeGObject):
    """
	Display an assistant which walks the user through creating a new campaign or
	configuring an existing campaign. If no *campaign_id* is specified a new
	campaign will be created.
	"""
    dependencies = gui_utilities.GladeDependencies(children=(
        resources.CompanyEditorGrid(
            gui_utilities.GladeProxyDestination('add',
                                                widget='alignment_company')),
        'button_select_kpm_dest_folder',
        'button_select_kpm_file',
        'button_url_ssl_issue_certificate',
        'calendar_campaign_expiration',
        'checkbutton_alert_subscribe',
        'checkbutton_expire_campaign',
        'checkbutton_reject_after_credentials',
        'combobox_campaign_type',
        'combobox_company_existing',
        'combobox_url_hostname',
        'combobox_url_path',
        'combobox_url_scheme',
        'entry_campaign_description',
        'entry_campaign_name',
        'entry_kpm_dest_folder',
        'entry_kpm_file',
        'entry_test_validation_text',
        'entry_validation_regex_mfa_token',
        'entry_validation_regex_password',
        'entry_validation_regex_username',
        'frame_campaign_expiration',
        'frame_company_existing',
        'frame_company_new',
        'image_intro_title',
        'image_url_ssl_status',
        'label_confirm_body',
        'label_confirm_title',
        'label_intro_body',
        'label_intro_title',
        'label_url_for_scheme',
        'label_url_for_hostname',
        'label_url_for_path',
        'label_url_info_authors',
        'label_url_info_created',
        'label_url_info_description',
        'label_url_info_title',
        'label_url_info_for_authors',
        'label_url_preview',
        'label_url_ssl_for_status',
        'label_url_ssl_status',
        'label_validation_regex_mfa_token',
        'label_validation_regex_password',
        'label_validation_regex_username',
        'listbox_url_info_classifiers',
        'listbox_url_info_references',
        'radiobutton_company_existing',
        'radiobutton_company_new',
        'radiobutton_company_none',
        'revealer_url_ssl_settings',
        'togglebutton_expiration_time',
    ),
                                                   top_level=(
                                                       'ClockHourAdjustment',
                                                       'ClockMinuteAdjustment',
                                                       'StockExecuteImage',
                                                   ))
    top_gobject = 'assistant'
    objects_persist = False

    def __init__(self, application, campaign_id=None):
        """
		:param application: The application instance which this object belongs to.
		:type application: :py:class:`~king_phisher.client.application.KingPhisherClientApplication`
		:param campaign_id: The ID of the campaign to edit.
		"""
        super(CampaignAssistant, self).__init__(application)
        self.campaign_id = campaign_id
        self._close_ready = True
        self._page_titles = {}
        for page_n in range(self.assistant.get_n_pages()):
            page = self.assistant.get_nth_page(page_n)
            page_title = self.assistant.get_page_title(page)
            if page_title:
                self._page_titles[page_title] = page_n

        campaign_edges = self.application.rpc.graphql(
            '{ db { campaigns { edges { node { id name } } } } }',
        )['db']['campaigns']['edges']
        self._campaign_names = dict((edge['node']['name'], edge['node']['id'])
                                    for edge in campaign_edges)
        self._cache_hostname = {}
        self._cache_site_template = {}
        self._can_issue_certs = False
        self._ssl_status = {}

        self._expiration_time = managers.TimeSelectorButtonManager(
            self.application, self.gobjects['togglebutton_expiration_time'])
        self._set_comboboxes()
        self._set_defaults()
        self.application.rpc.async_graphql(
            '{ ssl { status { enabled hasLetsencrypt hasSni } } }',
            on_success=self.__async_rpc_cb_ssl_status)
        _homogenous_label_width((self.gobjects['label_url_for_scheme'],
                                 self.gobjects['label_url_ssl_for_status'],
                                 self.gobjects['label_url_info_for_authors']))

        if not self.config['server_config']['server.require_id']:
            self.gobjects[
                'checkbutton_reject_after_credentials'].set_sensitive(False)
            self.gobjects['checkbutton_reject_after_credentials'].set_property(
                'active', False)

        confirm_preamble = 'Verify all settings are correct in the previous sections'
        if campaign_id:
            # re-configuring an existing campaign
            self.gobjects['label_confirm_body'].set_text(
                confirm_preamble +
                ', then hit "Apply" to update the King Phisher campaign with the new settings.'
            )
            self.gobjects['label_intro_body'].set_text(
                'This assistant will walk you through reconfiguring the selected King Phisher campaign.'
            )
            self.gobjects['label_intro_title'].set_text('Configure Campaign')
            self._set_webserver_url(self.config['mailer.webserver_url'])
        else:
            # creating a new campaign
            self.gobjects['label_confirm_body'].set_text(
                confirm_preamble +
                ', then hit "Apply" to create the new King Phisher campaign.')
            self.gobjects['label_intro_body'].set_text(
                'This assistant will walk you through creating and configuring a new King Phisher campaign.'
            )
            self.gobjects['label_intro_title'].set_text('New Campaign')

    def __async_rpc_cb_issue_cert_error(self, error, message=None):
        self._set_page_complete(True, page='Web Server URL')
        self.gobjects['button_url_ssl_issue_certificate'].set_sensitive(True)
        _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-dialog-warning')
        label = self.gobjects['label_url_ssl_status']
        label.set_text(
            'An error occurred while requesting a certificate for the specified hostname'
        )
        gui_utilities.show_dialog_error(
            'Operation Error', self.application.get_active_window(), message
            or "Unknown error: {!r}".format(error))

    def __async_rpc_cb_issue_cert_success(self, result):
        self._set_page_complete(True, page='Web Server URL')
        if not result['success']:
            return self.__async_rpc_cb_issue_cert_error(
                None, result['message'])
        _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-yes')
        label = self.gobjects['label_url_ssl_status']
        label.set_text(
            'A certificate for the specified hostname has been issued and loaded'
        )

    def __async_rpc_cb_populate_url_hostname_combobox(self, hostnames):
        hostnames = sorted(hostnames)
        model = self.gobjects['combobox_url_hostname'].get_model()
        for hostname in hostnames:
            model.append((hostname, ))

    def __async_rpc_cb_populate_url_info(self, hostname, path, results):
        self._cache_site_template[(hostname, path)] = results
        template = results['siteTemplate']
        if template is None:
            return
        self.gobjects['label_url_info_created'].set_text(
            utilities.format_datetime(
                utilities.datetime_utc_to_local(template['created'])))
        metadata = template['metadata']
        if metadata is None:
            return
        self.gobjects['label_url_info_title'].set_text(metadata['title'])
        self.gobjects['label_url_info_authors'].set_text('\n'.join(
            metadata['authors']))
        self.gobjects['label_url_info_description'].set_text(
            metadata['description'])
        if metadata['referenceUrls']:
            gui_utilities.gtk_listbox_populate_urls(
                self.gobjects['listbox_url_info_references'],
                metadata['referenceUrls'],
                signals={'activate-link': self.signal_label_activate_link})
        gui_utilities.gtk_listbox_populate_labels(
            self.gobjects['listbox_url_info_classifiers'],
            metadata['classifiers'])

    def __async_rpc_cb_populate_url_scheme_combobox(self, addresses):
        addresses = sorted(addresses, key=lambda address: address['port'])
        combobox_url_scheme = self.gobjects['combobox_url_scheme']
        model = combobox_url_scheme.get_model()
        for address in addresses:
            if address['ssl']:
                scheme_name = 'https'
                description = '' if address['port'] == 443 else 'port: ' + str(
                    address['port'])
            else:
                scheme_name = 'http'
                description = '' if address['port'] == 80 else 'port: ' + str(
                    address['port'])
            # use the scheme and port to make a row UID
            model.append(
                _ModelURLScheme(id=scheme_name + '/' + str(address['port']),
                                name=scheme_name,
                                description=description,
                                port=address['port']))
        if gui_utilities.gtk_list_store_search(model, 'https/443'):
            combobox_url_scheme.set_active_id('https/443')
        elif gui_utilities.gtk_list_store_search(model, 'http/80'):
            combobox_url_scheme.set_active_id('http/80')

    def __async_rpc_cb_changed_url_hostname(self, hostname, results):
        self._cache_hostname[hostname] = results
        templates = results['siteTemplates']
        combobox = self.gobjects['combobox_url_path']
        combobox.set_property('button-sensitivity', templates['total'] > 0)
        model = combobox.get_model()
        model.clear()
        for template in templates['edges']:
            template = template['node']
            path = utilities.make_webrelpath(template['path'])
            if path and not path.endswith(webpath.sep):
                path += webpath.sep
            for page in template['metadata']['pages']:
                model.append((path + utilities.make_webrelpath(page), path))
        # this is going to trigger a changed signal and the cascade effect will update the URL information and preview
        combobox.set_active_id(
            utilities.make_webrelpath(
                gui_utilities.gtk_combobox_get_entry_text(combobox)))

        sni_hostname = results['ssl']['sniHostname']
        label = self.gobjects['label_url_ssl_status']
        if sni_hostname is None:
            _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-no')
            label.set_text(
                'There is no certificate available for the specified hostname')
            if self._can_issue_certs:
                self.gobjects[
                    'button_url_ssl_issue_certificate'].set_sensitive(True)
        else:
            _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-yes')
            label.set_text(
                'A certificate for the specified hostname is available')

    def __async_rpc_cb_ssl_status(self, results):
        self._ssl_status = results['ssl']['status']
        self._can_issue_certs = all(results['ssl']['status'].values())

    @property
    def campaign_name(self):
        """
		The string value of the configured campaign name. This may be set even
		when the campaign was not created, which would be the case if the user
		closed the window.
		"""
        return self.gobjects['entry_campaign_name'].get_text()

    @property
    def is_editing_campaign(self):
        return self.campaign_id is not None

    @property
    def is_new_campaign(self):
        return self.campaign_id is None

    def _do_regex_validation(self, test_text, entry):
        try:
            regex = re.compile(entry.get_text())
        except re.error:
            entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
            return
        result = True
        if regex.pattern and test_text:
            result = regex.match(test_text) is not None
        entry.set_property('secondary-icon-stock',
                           'gtk-yes' if result else 'gtk-no')

    def _get_campaign_by_name(self, name):
        campaign = self.application.rpc.graphql(
            """\
			query getCampaignByName($name: String!)
			{ db { campaign(name: $name) { id } } }
		""",
            query_vars={'name': name})['db']['campaign']
        return campaign

    def _get_company_existing_id(self):
        combobox = self.gobjects['combobox_company_existing']
        model = combobox.get_model()
        model_iter = combobox.get_active_iter()
        if model is None:
            return
        if model_iter is None:
            text = combobox.get_child().get_text().strip()
            if text:
                model_iter = gui_utilities.gtk_list_store_search(model,
                                                                 text,
                                                                 column=1)
        if model_iter is None:
            return None
        return model.get_value(model_iter, 0)

    def _get_company_new_id(self):
        name = self.gobjects['entry_company_name'].get_text()
        name = name.strip()
        # check if this company name already exists in the model
        model = self.gobjects['combobox_company_existing'].get_model()
        model_iter = gui_utilities.gtk_list_store_search(model, name, column=1)
        if model_iter is not None:
            return model.get_value(model_iter, 0)
        # check if this company name already exists remotely
        remote_company = self._get_graphql_company(name)
        if remote_company:
            return remote_company['id']

        company_id = self.application.rpc(
            'db/table/insert', 'companies',
            ('name', 'description', 'industry_id', 'url_main', 'url_email',
             'url_remote_access'),
            (name, self.get_entry_value('company_description'),
             self._get_tag_from_combobox(
                 self.gobjects['combobox_company_industry'],
                 'industries'), self.get_entry_value('company_url_main'),
             self.get_entry_value('company_url_email'),
             self.get_entry_value('company_url_remote_access')))
        self.gobjects['radiobutton_company_existing'].set_active(True)
        return company_id

    def _get_graphql_company(self, company_name):
        results = self.application.rpc.graphql(
            """\
		query getCompany($name: String!) {
			db {
				company(name: $name) {
					id
				}
			}
		}""", {'name': company_name})
        return results['db']['company']

    def _get_kpm_path(self):
        file_path = self.gobjects['entry_kpm_file'].get_text()
        dir_path = self.gobjects['entry_kpm_dest_folder'].get_text()
        if not dir_path and not file_path:
            return _KPMPaths(None, None, True)
        if not _kpm_file_path_is_valid(file_path):
            return _KPMPaths(None, None, False)
        if not (dir_path and os.path.isdir(dir_path)
                and os.access(dir_path, os.R_OK | os.W_OK)):
            return _KPMPaths(None, None, False)
        return _KPMPaths(file_path, dir_path, True)

    def _get_tag_from_combobox(self, combobox, db_table):
        model = combobox.get_model()
        model_iter = combobox.get_active_iter()
        if model_iter is not None:
            return model.get_value(model_iter, 0)
        text = combobox.get_child().get_text().strip()
        if not text:
            return
        model_iter = gui_utilities.gtk_list_store_search(model, text, column=1)
        if model_iter is None:
            return self.application.rpc('db/table/insert', db_table, 'name',
                                        text)
        return model.get_value(model_iter, 0)

    def _get_webserver_url(self):
        return self.gobjects['label_url_preview'].get_text()

    @property
    def _server_uses_ssl(self):
        return any(
            address['ssl']
            for address in self.config['server_config']['server.addresses'])

    def _set_comboboxes(self):
        """Set up all the comboboxes and load the data for their models."""
        renderer = resources.renderer_text_desc
        rpc = self.application.rpc
        for tag_name, tag_table in (('campaign_type', 'campaign_types'),
                                    ('company_existing', 'companies'),
                                    ('company_industry', 'industries')):
            combobox = self.gobjects['combobox_' + tag_name]
            model = combobox.get_model()
            if model is None:
                combobox.pack_start(renderer, True)
                combobox.add_attribute(renderer, 'text', 2)
            combobox.set_model(rpc.get_tag_model(tag_table, model=model))
            gui_utilities.gtk_combobox_set_entry_completion(combobox)
        # setup the URL scheme combobox asynchronously
        model = Gtk.ListStore(str, str, str, int)
        combobox = self.gobjects['combobox_url_scheme']
        combobox.set_model(model)
        combobox.pack_start(renderer, True)
        combobox.add_attribute(renderer, 'text', 2)
        rpc.async_call(
            'config/get', ('server.addresses', ),
            on_success=self.__async_rpc_cb_populate_url_scheme_combobox,
            when_idle=True)
        # setup the URL hostname combobox asynchronously
        model = Gtk.ListStore(str)
        combobox = self.gobjects['combobox_url_hostname']
        combobox.set_model(model)
        gui_utilities.gtk_combobox_set_entry_completion(combobox)
        rpc.async_call(
            'hostnames/get',
            on_success=self.__async_rpc_cb_populate_url_hostname_combobox,
            when_idle=True)
        # setup the URL path combobox model, but don't populate it until a hostname is selected
        model = Gtk.ListStore(str, str)
        combobox = self.gobjects['combobox_url_path']
        combobox.set_model(model)
        gui_utilities.gtk_combobox_set_entry_completion(combobox)

    def _set_defaults(self):
        """
		Set any default values for widgets. Also load settings from the existing
		campaign if one was specified.
		"""
        calendar = self.gobjects['calendar_campaign_expiration']
        default_day = datetime.datetime.today() + datetime.timedelta(days=31)
        gui_utilities.gtk_calendar_set_pydate(calendar, default_day)

        if self.is_new_campaign:
            return
        campaign = self.application.get_graphql_campaign()

        # set entries
        self.gobjects['entry_campaign_name'].set_text(campaign['name'])
        self.gobjects['entry_validation_regex_username'].set_text(
            campaign['credentialRegexUsername'] or '')
        self.gobjects['entry_validation_regex_password'].set_text(
            campaign['credentialRegexPassword'] or '')
        self.gobjects['entry_validation_regex_mfa_token'].set_text(
            campaign['credentialRegexMfaToken'] or '')

        if campaign['description'] is not None:
            self.gobjects['entry_campaign_description'].set_text(
                campaign['description'])
        if campaign['campaignType'] is not None:
            combobox = self.gobjects['combobox_campaign_type']
            model = combobox.get_model()
            model_iter = gui_utilities.gtk_list_store_search(
                model, campaign['campaignType']['id'], column=0)
            if model_iter is not None:
                combobox.set_active_iter(model_iter)

        self.gobjects['checkbutton_alert_subscribe'].set_property(
            'active',
            self.application.rpc('campaign/alerts/is_subscribed',
                                 self.campaign_id))
        self.gobjects['checkbutton_reject_after_credentials'].set_property(
            'active', bool(campaign['maxCredentials']))

        if campaign['company'] is not None:
            self.gobjects['radiobutton_company_existing'].set_active(True)
            combobox = self.gobjects['combobox_company_existing']
            model = combobox.get_model()
            model_iter = gui_utilities.gtk_list_store_search(
                model, campaign['company']['id'], column=0)
            if model_iter is not None:
                combobox.set_active_iter(model_iter)

        if campaign['expiration'] is not None:
            expiration = utilities.datetime_utc_to_local(
                campaign['expiration'])
            self.gobjects['checkbutton_expire_campaign'].set_active(True)
            self._expiration_time.time = expiration.time()
            gui_utilities.gtk_calendar_set_pydate(
                self.gobjects['calendar_campaign_expiration'],
                expiration.date())

    def _set_webserver_url(self, webserver_url):
        webserver_url = urllib.parse.urlparse(webserver_url.strip())
        if webserver_url.scheme == 'http':
            self.gobjects['combobox_url_scheme'].set_active_id(
                'http/' + str(webserver_url.port or 80))
        elif webserver_url.scheme == 'https':
            self.gobjects['combobox_url_scheme'].set_active_id(
                'https/' + str(webserver_url.port or 443))
        if webserver_url.hostname:
            self.gobjects['combobox_url_hostname'].get_child().set_text(
                webserver_url.hostname)
        if webserver_url.path:
            self.gobjects['combobox_url_path'].get_child().set_text(
                utilities.make_webrelpath(webserver_url.path))

    def _set_page_complete(self, complete, page=None):
        if page is None:
            page = self.assistant.get_nth_page(
                self.assistant.get_current_page())
        elif isinstance(page, str):
            page = self.assistant.get_nth_page(self._page_titles[page])
        elif isinstance(page, int):
            page = self.assistant.get_nth_page(page)
        self.assistant.set_page_complete(page, complete)

    def signal_assistant_apply(self, _):
        self._close_ready = False
        # have to do it this way because the next page will be selected when the apply signal is complete
        set_current_page = lambda page_name: self.assistant.set_current_page(
            max(0, self._page_titles[page_name] - 1))

        # get and validate the campaign name
        campaign_name = self.gobjects['entry_campaign_name'].get_text()
        campaign_name = campaign_name.strip()
        if not campaign_name:
            gui_utilities.show_dialog_error(
                'Invalid Campaign Name', self.parent,
                'A valid campaign name must be specified.')
            set_current_page('Basic Settings')
            return True
        campaign = self._get_campaign_by_name(campaign_name)
        if campaign and campaign['id'] != self.campaign_id:
            gui_utilities.show_dialog_error(
                'Invalid Campaign Name', self.parent,
                'A unique campaign name must be specified.')
            set_current_page('Basic Settings')
            return True

        properties = {}
        # validate the credential validation regular expressions
        for field in ('username', 'password', 'mfa_token'):
            regex = self.gobjects['entry_validation_regex_' + field].get_text()
            if regex:
                try:
                    re.compile(regex)
                except re.error:
                    label = self.gobjects['label_validation_regex_' +
                                          field].get_text()
                    gui_utilities.show_dialog_error(
                        'Invalid Regex', self.parent,
                        "The '{0}' regular expression is invalid.".format(
                            label))
                    return True
            else:
                regex = None  # keep empty strings out of the database
            properties['credential_regex_' + field] = regex

        # validate the company
        company_id = None
        if self.gobjects['radiobutton_company_existing'].get_active():
            company_id = self._get_company_existing_id()
            if company_id is None:
                gui_utilities.show_dialog_error(
                    'Invalid Company', self.parent,
                    'A valid existing company must be specified.')
                set_current_page('Company')
                return True
        elif self.gobjects['radiobutton_company_new'].get_active():
            company_id = self._get_company_new_id()
            if company_id is None:
                gui_utilities.show_dialog_error(
                    'Invalid Company', self.parent,
                    'The new company settings are invalid.')
                set_current_page('Company')
                return True

        # get and validate the campaign expiration
        expiration = None
        if self.gobjects['checkbutton_expire_campaign'].get_property('active'):
            expiration = datetime.datetime.combine(
                gui_utilities.gtk_calendar_get_pydate(
                    self.gobjects['calendar_campaign_expiration']),
                self._expiration_time.time)
            expiration = utilities.datetime_local_to_utc(expiration)
            if self.is_new_campaign and expiration <= datetime.datetime.now():
                gui_utilities.show_dialog_error(
                    'Invalid Campaign Expiration', self.parent,
                    'The expiration date is set in the past.')
                set_current_page('Expiration')
                return True

        # point of no return
        campaign_description = self.get_entry_value('campaign_description')
        if self.campaign_id:
            properties['name'] = self.campaign_name
            properties['description'] = campaign_description
            cid = self.campaign_id
        else:
            try:
                cid = self.application.rpc('campaign/new',
                                           campaign_name,
                                           description=campaign_description)
                properties['name'] = campaign_name
            except advancedhttpserver.RPCError as error:
                if not error.is_remote_exception:
                    raise error
                if not error.remote_exception[
                        'name'] == 'exceptions.ValueError':
                    raise error
                error_message = error.remote_exception.get(
                    'message', 'an unknown error occurred').capitalize() + '.'
                gui_utilities.show_dialog_error('Failed To Create Campaign',
                                                self.parent, error_message)
                set_current_page('Basic Settings')
                return True
            self.application.emit('campaign-created', cid)

        properties['campaign_type_id'] = self._get_tag_from_combobox(
            self.gobjects['combobox_campaign_type'], 'campaign_types')
        properties['company_id'] = company_id
        properties['expiration'] = expiration
        properties['max_credentials'] = (
            1 if self.gobjects['checkbutton_reject_after_credentials'].
            get_property('active') else None)
        self.application.rpc('db/table/set', 'campaigns', cid,
                             tuple(properties.keys()),
                             tuple(properties.values()))

        should_subscribe = self.gobjects[
            'checkbutton_alert_subscribe'].get_property('active')
        if should_subscribe != self.application.rpc(
                'campaign/alerts/is_subscribed', cid):
            if should_subscribe:
                self.application.rpc('campaign/alerts/subscribe', cid)
            else:
                self.application.rpc('campaign/alerts/unsubscribe', cid)

        self.application.emit('campaign-changed', cid)

        old_cid = self.config.get('campaign_id')
        self.config['campaign_id'] = cid
        self.config['campaign_name'] = properties['name']

        _kpm_pathing = self._get_kpm_path()
        if all(_kpm_pathing):
            if not self.application.main_tabs['mailer'].emit(
                    'message-data-import', _kpm_pathing.kpm_file,
                    _kpm_pathing.destination_folder):
                gui_utilities.show_dialog_info(
                    'Failure', self.parent,
                    'Failed to import the message configuration.')
            else:
                gui_utilities.show_dialog_info(
                    'Success', self.parent,
                    'Successfully imported the message configuration.')

        if self._ssl_status['hasSni']:
            combobox_url_scheme = self.gobjects['combobox_url_scheme']
            active = combobox_url_scheme.get_active()
            if active != -1:
                url_scheme = _ModelURLScheme(
                    *combobox_url_scheme.get_model()[active])
                if url_scheme.name == 'https':
                    hostname = gui_utilities.gtk_combobox_get_entry_text(
                        self.gobjects['combobox_url_hostname'])
                    if not self.application.rpc('ssl/sni_hostnames/load',
                                                hostname):
                        gui_utilities.show_dialog_error(
                            'Failure', self.parent,
                            'Failed to load an SSL certificate for the specified hostname.'
                        )

        self.config['mailer.webserver_url'] = self._get_webserver_url()
        self.application.emit('campaign-set', old_cid, cid)
        self._close_ready = True
        return

    def signal_assistant_cancel(self, assistant):
        assistant.destroy()

    def signal_assistant_close(self, assistant):
        if self._close_ready:
            assistant.destroy()
        self._close_ready = True

    def signal_assistant_prepare(self, _, page):
        page_title = self.assistant.get_page_title(page)
        if page_title == 'Company':
            combobox = self.gobjects['combobox_company_existing']
            model = combobox.get_model()
            company_name = self.get_entry_value('company_name')
            if company_name:
                model_iter = gui_utilities.gtk_list_store_search(model,
                                                                 company_name,
                                                                 column=1)
                if model_iter is not None:
                    combobox.set_active_iter(model_iter)
                    self.gobjects['radiobutton_company_existing'].set_active(
                        True)

    def signal_button_clicked_issue_certificate(self, button):
        button.set_sensitive(False)
        self._set_page_complete(False, page='Web Server URL')
        label = self.gobjects['label_url_ssl_status']
        label.set_text(
            'A certificate for the specified hostname is being requested')
        hostname = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_hostname'])
        self.application.rpc.async_call(
            'ssl/letsencrypt/issue', (hostname, ),
            on_error=self.__async_rpc_cb_issue_cert_error,
            on_success=self.__async_rpc_cb_issue_cert_success,
            when_idle=True)

    def signal_calendar_prev(self, calendar):
        today = datetime.date.today()
        calendar_day = gui_utilities.gtk_calendar_get_pydate(calendar)
        if calendar_day >= today:
            return
        gui_utilities.gtk_calendar_set_pydate(calendar, today)

    def signal_checkbutton_expire_campaign_toggled(self, _):
        active = self.gobjects['checkbutton_expire_campaign'].get_property(
            'active')
        self.gobjects['frame_campaign_expiration'].set_sensitive(active)

    @gui_utilities.delayed_signal()
    def signal_combobox_changed_set_url_information(self, _):
        for label in ('info_title', 'info_authors', 'info_created',
                      'info_description'):
            self.gobjects['label_url_' + label].set_text('')
        hostname = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_hostname'])
        if not hostname:
            return
        combobox_url_path = self.gobjects['combobox_url_path']
        path = gui_utilities.gtk_combobox_get_active_cell(combobox_url_path,
                                                          column=1)
        if path is None:
            model = combobox_url_path.get_model()
            text = utilities.make_webrelpath(
                gui_utilities.gtk_combobox_get_entry_text(combobox_url_path))
            row_iter = gui_utilities.gtk_list_store_search(model, text)
            if row_iter:
                path = model[row_iter][1]
        gui_utilities.gtk_widget_destroy_children(
            self.gobjects['listbox_url_info_classifiers'])
        gui_utilities.gtk_widget_destroy_children(
            self.gobjects['listbox_url_info_references'])
        cached_result = self._cache_site_template.get((hostname, path))
        if cached_result:
            self.__async_rpc_cb_populate_url_info(hostname, path,
                                                  cached_result)
            return
        self.application.rpc.async_graphql(
            """
			query getSiteTemplate($hostname: String, $path: String) {
			  siteTemplate(hostname: $hostname, path: $path) {
				created path metadata { title authors description referenceUrls classifiers pages }
			  }
			}
			""",
            query_vars={
                'hostname': hostname,
                'path': path
            },
            on_success=self.__async_rpc_cb_populate_url_info,
            cb_args=(hostname, path),
            when_idle=True)

    def signal_combobox_changed_set_url_preview(self, _):
        label = self.gobjects['label_url_preview']
        label.set_text('')
        combobox_url_scheme = self.gobjects['combobox_url_scheme']
        active = combobox_url_scheme.get_active()
        if active == -1:
            return
        url_scheme = _ModelURLScheme(*combobox_url_scheme.get_model()[active])
        authority = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_hostname'])
        path = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_path'])
        if url_scheme and authority:
            path = utilities.make_webrelpath(path)
            if (url_scheme.name == 'http'
                    and url_scheme.port != 80) or (url_scheme.name == 'https'
                                                   and url_scheme.port != 443):
                authority += ':' + str(url_scheme.port)
            label.set_text("{}://{}/{}".format(url_scheme.name, authority,
                                               path))

    @gui_utilities.delayed_signal()
    def signal_combobox_changed_url_hostname(self, combobox):
        self.gobjects['button_url_ssl_issue_certificate'].set_sensitive(False)
        hostname = gui_utilities.gtk_combobox_get_entry_text(combobox)
        if not hostname:
            return
        cached_result = self._cache_hostname.get(hostname)
        if cached_result:
            self.__async_rpc_cb_changed_url_hostname(hostname, cached_result)
            return
        self.application.rpc.async_graphql(
            """
			query getHostname($hostname: String) {
			  siteTemplates(hostname: $hostname) {
				total edges { node { hostname path metadata { pages } } }
			  }
			  ssl { sniHostname(hostname: $hostname) { enabled } }
			}
			""",
            query_vars={'hostname': hostname},
            on_success=self.__async_rpc_cb_changed_url_hostname,
            cb_args=(hostname, ),
            when_idle=True)

    def signal_combobox_changed_url_scheme(self, combobox):
        active = combobox.get_active()
        if active == -1:
            return
        url_scheme = _ModelURLScheme(*combobox.get_model()[active])
        revealer = self.gobjects['revealer_url_ssl_settings']
        revealer.set_reveal_child(url_scheme.name == 'https')

    def signal_entry_changed_campaign_name(self, entry):
        campaign_name = entry.get_text().strip()
        if not campaign_name:
            entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
        if self.is_new_campaign:
            if self._campaign_names.get(campaign_name) is not None:
                entry.set_property('secondary-icon-stock',
                                   'gtk-dialog-warning')
            else:
                entry.set_property('secondary-icon-stock', None)
        elif self.is_editing_campaign:
            other_cid = self._campaign_names.get(campaign_name)
            if other_cid is not None and other_cid != self.campaign_id:
                entry.set_property('secondary-icon-stock',
                                   'gtk-dialog-warning')
            else:
                entry.set_property('secondary-icon-stock', None)

    def signal_entry_changed_test_validation_text(self, field):
        test_text = field.get_text()
        for field in ('username', 'password', 'mfa_token'):
            self._do_regex_validation(
                test_text, self.gobjects['entry_validation_regex_' + field])

    def signal_entry_changed_validation_regex(self, entry):
        self._do_regex_validation(
            self.gobjects['entry_test_validation_text'].get_text(), entry)

    def signal_label_activate_link(self, _, uri):
        utilities.open_uri(uri)

    def signal_kpm_select_clicked(self, _):
        dialog = extras.FileChooserDialog('Import Message Configuration',
                                          self.parent)
        dialog.quick_add_filter('King Phisher Message Files', '*.kpm')
        dialog.quick_add_filter('All Files', '*')
        response = dialog.run_quick_open()
        dialog.destroy()
        if not response:
            return False
        target_path = response['target_path']
        self.gobjects['entry_kpm_file'].set_text(target_path)
        self._set_page_complete(self._get_kpm_path().is_valid)

        if not _kpm_file_path_is_valid(target_path):
            return
        # open the KPM for reading to extract the target URL for the assistant,
        # ignore the directory to allow the user to optionally only import the URL
        kpm = archive.ArchiveFile(target_path, 'r')
        if not kpm.has_file('message_config.json'):
            self.logger.warning(
                'the kpm archive is missing the message_config.json file')
            return
        message_config = kpm.get_json('message_config.json')
        webserver_url = message_config.get('webserver_url')
        if not webserver_url:
            return
        self._set_webserver_url(webserver_url)

    def signal_kpm_dest_folder_clicked(self, _):
        dialog = extras.FileChooserDialog('Destination Directory', self.parent)
        response = dialog.run_quick_select_directory()
        dialog.destroy()
        if not response:
            return False
        self.gobjects['entry_kpm_dest_folder'].set_text(
            response['target_path'])
        self._set_page_complete(self._get_kpm_path().is_valid)

    def signal_kpm_entry_clear(self, entry_widget):
        entry_widget.set_text('')
        self._set_page_complete(self._get_kpm_path().is_valid)

    def signal_radiobutton_toggled(self, radiobutton):
        if not radiobutton.get_active():
            return
        if radiobutton == self.gobjects['radiobutton_company_existing']:
            self.gobjects['frame_company_existing'].set_sensitive(True)
            self.gobjects['frame_company_new'].set_sensitive(False)
        elif radiobutton == self.gobjects['radiobutton_company_new']:
            self.gobjects['frame_company_existing'].set_sensitive(False)
            self.gobjects['frame_company_new'].set_sensitive(True)
        elif radiobutton == self.gobjects['radiobutton_company_none']:
            self.gobjects['frame_company_existing'].set_sensitive(False)
            self.gobjects['frame_company_new'].set_sensitive(False)

    def interact(self):
        self.assistant.show_all()
Exemplo n.º 4
0
class CampaignAssistant(gui_utilities.GladeGObject):
	"""
	Display an assistant which walks the user through creating a new campaign or
	configuring an existing campaign. If no *campaign_id* is specified a new
	campaign will be created.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			resources.CompanyEditorGrid(
				gui_utilities.GladeProxyDestination(
					'add',
					widget='alignment_company'
				)
			),
			'button_select_kpm_dest_folder',
			'button_select_kpm_file',
			'calendar_campaign_expiration',
			'checkbutton_alert_subscribe',
			'checkbutton_expire_campaign',
			'checkbutton_reject_after_credentials',
			'combobox_campaign_type',
			'combobox_company_existing',
			'entry_campaign_description',
			'entry_campaign_name',
			'entry_hostname_filter',
			'entry_kpm_dest_folder',
			'entry_kpm_file',
			'entry_test_validation_text',
			'entry_validation_regex_mfa_token',
			'entry_validation_regex_password',
			'entry_validation_regex_username',
			'expander_url_info',
			'frame_campaign_expiration',
			'frame_company_existing',
			'frame_company_new',
			'image_intro_title',
			'label_confirm_body',
			'label_confirm_title',
			'label_intro_body',
			'label_intro_title',
			'label_url_info_authors',
			'label_url_info_created',
			'label_url_info_description',
			'label_url_info_for_classifiers',
			'label_url_info_url',
			'label_validation_regex_mfa_token',
			'label_validation_regex_password',
			'label_validation_regex_username',
			'listbox_url_info_classifiers',
			'paned_url_info',
			'radiobutton_company_existing',
			'radiobutton_company_new',
			'radiobutton_company_none',
			'togglebutton_expiration_time',
			'treeselection_url_selector',
			'treeview_url_selector',
		),
		top_level=(
			'ClockHourAdjustment',
			'ClockMinuteAdjustment'
		)
	)
	top_gobject = 'assistant'
	objects_persist = False
	def __init__(self, application, campaign_id=None):
		"""
		:param application: The application instance which this object belongs to.
		:type application: :py:class:`~king_phisher.client.application.KingPhisherClientApplication`
		:param campaign_id: The ID of the campaign to edit.
		"""
		super(CampaignAssistant, self).__init__(application)
		self.campaign_id = campaign_id
		self._close_ready = True
		self._page_titles = {}
		for page_n in range(self.assistant.get_n_pages()):
			page = self.assistant.get_nth_page(page_n)
			page_title = self.assistant.get_page_title(page)
			if page_title:
				self._page_titles[page_title] = page_n

		self._expiration_time = managers.TimeSelectorButtonManager(self.application, self.gobjects['togglebutton_expiration_time'])
		self._set_comboboxes()
		self._set_defaults()

		if not self.config['server_config']['server.require_id']:
			self.gobjects['checkbutton_reject_after_credentials'].set_sensitive(False)
			self.gobjects['checkbutton_reject_after_credentials'].set_property('active', False)

		confirm_preamble = 'Verify all settings are correct in the previous sections'
		if campaign_id:
			# re-configuring an existing campaign
			self.gobjects['label_confirm_body'].set_text(confirm_preamble + ', then hit "Apply" to update the King Phisher campaign with the new settings.')
			self.gobjects['label_intro_body'].set_text('This assistant will walk you through reconfiguring the selected King Phisher campaign.')
			self.gobjects['label_intro_title'].set_text('Configure Campaign')
		else:
			# creating a new campaign
			self.gobjects['label_confirm_body'].set_text(confirm_preamble + ', then hit "Apply" to create the new King Phisher campaign.')
			self.gobjects['label_intro_body'].set_text('This assistant will walk you through creating and configuring a new King Phisher campaign.')
			self.gobjects['label_intro_title'].set_text('New Campaign')

		self._url_thread = None
		domain_completion = Gtk.EntryCompletion()
		self._hostname_list_store = Gtk.ListStore(str)
		domain_completion.set_model(self._hostname_list_store)
		domain_completion.set_text_column(0)
		self.gobjects['entry_hostname_filter'].set_completion(domain_completion)

		tvm = managers.TreeViewManager(self.gobjects['treeview_url_selector'])
		tvm.set_column_titles(
			['Hostname', 'Landing Page', 'URL'],
			renderers=[
				Gtk.CellRendererText(),
				Gtk.CellRendererText(),
				Gtk.CellRendererText()
			]
		)
		self._url_model = Gtk.ListStore(str, str, str, object, object, str, str)
		self._url_model.set_sort_column_id(0, Gtk.SortType.ASCENDING)
		self.gobjects['treeview_url_selector'].set_model(self._url_model)
		self._url_information = {
			'created': None,
			'data': None
		}
		self._load_url_treeview_tsafe(refresh=True)
		paned = self.gobjects['paned_url_info']
		self._paned_offset = paned.get_allocation().height - paned.get_position()

	@property
	def campaign_name(self):
		"""
		The string value of the configured campaign name. This may be set even
		when the campaign was not created, which would be the case if the user
		closed the window.
		"""
		return self.gobjects['entry_campaign_name'].get_text()

	@property
	def is_editing_campaign(self):
		return self.campaign_id is not None

	@property
	def is_new_campaign(self):
		return self.campaign_id is None

	def _update_completion_status(self):
		self.assistant.set_page_complete(self.assistant.get_nth_page(self.assistant.get_current_page()), self._get_kpm_path().is_valid)

	@property
	def _url_thread_is_ready(self):
		return self._url_thread is None or not self._url_thread.is_alive()

	@property
	def _server_uses_ssl(self):
		return any(address['ssl'] for address in self.config['server_config']['server.addresses'])

	def signal_kpm_select_clicked(self, _):
		dialog = extras.FileChooserDialog('Import Message Configuration', self.parent)
		dialog.quick_add_filter('King Phisher Message Files', '*.kpm')
		dialog.quick_add_filter('All Files', '*')
		response = dialog.run_quick_open()
		dialog.destroy()
		if not response:
			return False
		self.gobjects['entry_kpm_file'].set_text(response['target_path'])
		self._update_completion_status()

	def signal_kpm_dest_folder_clicked(self, _):
		dialog = extras.FileChooserDialog('Destination Directory', self.parent)
		response = dialog.run_quick_select_directory()
		dialog.destroy()
		if not response:
			return False
		self.gobjects['entry_kpm_dest_folder'].set_text(response['target_path'])
		self._update_completion_status()

	def signal_kpm_entry_clear(self, entry_widget):
		entry_widget.set_text('')
		self._update_completion_status()

	def _set_comboboxes(self):
		"""Set up all the comboboxes and load the data for their models."""
		renderer = resources.renderer_text_desc
		rpc = self.application.rpc
		for tag_name, tag_table in (('campaign_type', 'campaign_types'), ('company_existing', 'companies'), ('company_industry', 'industries')):
			combobox = self.gobjects['combobox_' + tag_name]
			model = combobox.get_model()
			if model is None:
				combobox.pack_start(renderer, True)
				combobox.add_attribute(renderer, 'text', 2)
			combobox.set_model(rpc.get_tag_model(tag_table, model=model))

	def _load_url_treeview_tsafe(self, hostname=None, refresh=False):
		if refresh or not self._url_information['created']:
			self._url_information['data'] = self.application.rpc.graphql_find_file('get_site_templates.graphql')
			self._url_information['created'] = datetime.datetime.utcnow()
		url_information = self._url_information['data']
		if not url_information:
			return

		rows = []
		domains = []
		for edge in url_information['siteTemplates']['edges']:
			template = edge['node']
			for page in template['metadata']['pages']:
				if hostname and template['hostname'] and not template['hostname'].startswith(hostname):
					continue
				page = page.strip('/')
				resource = '/' + '/'.join((template.get('path', '').strip('/'), page)).lstrip('/')
				domains.append(template['hostname'])
				rows.append(_ModelNamedRow(
					hostname=template['hostname'],
					page=page,
					url=self._build_url(template['hostname'], resource, 'http'),
					classifiers=template['metadata']['classifiers'],
					authors=template['metadata']['authors'],
					description=template['metadata']['description'].strip('\n'),
					created=utilities.format_datetime(utilities.datetime_utc_to_local(template['created']))
				))

				if self._server_uses_ssl:
					rows.append(_ModelNamedRow(
						hostname=template['hostname'],
						page=page,
						url=self._build_url(template['hostname'], resource, 'https'),
						classifiers=template['metadata']['classifiers'],
						authors=template['metadata']['authors'],
						description=template['metadata']['description'].strip('\n'),
						created=utilities.format_datetime(utilities.datetime_utc_to_local(template['created']))
					))

		gui_utilities.glib_idle_add_once(self.gobjects['treeselection_url_selector'].unselect_all)
		gui_utilities.glib_idle_add_store_extend(self._url_model, rows, clear=True)
		# make domain list unique in case multiple pages are advertised for the domains
		domains = [[domain] for domain in set(domains)]
		gui_utilities.glib_idle_add_store_extend(self._hostname_list_store, domains, clear=True)

	def _build_url(self, hostname, page, scheme):
		if not hostname:
			for address in self.config['server_config']['server.addresses']:
				ip = ipaddress.ip_address(address['host'])
				if not ip.is_unspecified and (ip.is_global or ip.is_private):
					hostname = address['host']
					break
			else:
				hostname = 'localhost'
		return urllib.parse.urljoin(scheme + '://' + hostname, page)

	def signal_url_entry_change(self, gtk_entry):
		gtk_entry_text = gtk_entry.get_text()
		if not self._url_information['created'] or datetime.datetime.utcnow() - self._url_information['created'] > datetime.timedelta(minutes=5):
			self._load_url_treeview_tsafe(hostname=gtk_entry_text, refresh=True)
		else:
			self._load_url_treeview_tsafe(hostname=gtk_entry_text, refresh=False)

	def _set_defaults(self):
		"""
		Set any default values for widgets. Also load settings from the existing
		campaign if one was specified.
		"""
		calendar = self.gobjects['calendar_campaign_expiration']
		default_day = datetime.datetime.today() + datetime.timedelta(days=31)
		gui_utilities.gtk_calendar_set_pydate(calendar, default_day)

		if self.campaign_id is None:
			return
		campaign = self.application.get_graphql_campaign()

		# set entries
		self.gobjects['entry_campaign_name'].set_text(campaign['name'])
		self.gobjects['entry_validation_regex_username'].set_text(campaign['credentialRegexUsername'] or '')
		self.gobjects['entry_validation_regex_password'].set_text(campaign['credentialRegexPassword'] or '')
		self.gobjects['entry_validation_regex_mfa_token'].set_text(campaign['credentialRegexMfaToken'] or '')

		if campaign['description'] is not None:
			self.gobjects['entry_campaign_description'].set_text(campaign['description'])
		if campaign['campaignType'] is not None:
			combobox = self.gobjects['combobox_campaign_type']
			model = combobox.get_model()
			model_iter = gui_utilities.gtk_list_store_search(model, campaign['campaignType']['id'], column=0)
			if model_iter is not None:
				combobox.set_active_iter(model_iter)

		self.gobjects['checkbutton_alert_subscribe'].set_property('active', self.application.rpc('campaign/alerts/is_subscribed', self.campaign_id))
		self.gobjects['checkbutton_reject_after_credentials'].set_property('active', bool(campaign['maxCredentials']))

		if campaign['company'] is not None:
			self.gobjects['radiobutton_company_existing'].set_active(True)
			combobox = self.gobjects['combobox_company_existing']
			model = combobox.get_model()
			model_iter = gui_utilities.gtk_list_store_search(model, campaign['company']['id'], column=0)
			if model_iter is not None:
				combobox.set_active_iter(model_iter)

		if campaign['expiration'] is not None:
			expiration = utilities.datetime_utc_to_local(campaign['expiration'])
			self.gobjects['checkbutton_expire_campaign'].set_active(True)
			self._expiration_time.time = expiration.time()
			gui_utilities.gtk_calendar_set_pydate(self.gobjects['calendar_campaign_expiration'], expiration.date())

	def _get_tag_from_combobox(self, combobox, db_table):
		model = combobox.get_model()
		model_iter = combobox.get_active_iter()
		if model_iter is not None:
			return model.get_value(model_iter, 0)
		campaign_type = combobox.get_child().get_text().strip()
		if not campaign_type:
			return
		model_iter = gui_utilities.gtk_list_store_search(model, campaign_type, column=1)
		if model_iter is None:
			return self.application.rpc('db/table/insert', db_table, 'name', campaign_type)
		return model.get_value(model_iter, 0)

	def _get_company_existing_id(self):
		combobox_company = self.gobjects['combobox_company_existing']
		model = combobox_company.get_model()
		model_iter = combobox_company.get_active_iter()
		if model is None or model_iter is None:
			return
		return model.get_value(model_iter, 0)

	def _get_company_new_id(self):
		name = self.gobjects['entry_company_name'].get_text()
		name = name.strip()
		# check if this company name already exists in the model
		model = self.gobjects['combobox_company_existing'].get_model()
		model_iter = gui_utilities.gtk_list_store_search(model, name, column=1)
		if model_iter is not None:
			return model.get_value(model_iter, 0)
		# check if this company name already exists remotely
		remote_company = self._get_graphql_company(name)
		if remote_company:
			return remote_company['id']

		company_id = self.application.rpc(
			'db/table/insert',
			'companies',
			('name', 'description', 'industry_id', 'url_main', 'url_email', 'url_remote_access'),
			(
				name,
				self.get_entry_value('company_description'),
				self._get_tag_from_combobox(self.gobjects['combobox_company_industry'], 'industries'),
				self.get_entry_value('company_url_main'),
				self.get_entry_value('company_url_email'),
				self.get_entry_value('company_url_remote_access')
			)
		)
		self.gobjects['radiobutton_company_existing'].set_active(True)
		return company_id

	def _get_graphql_company(self, company_name):
		results = self.application.rpc.graphql("""\
		query getCompany($name: String!) {
			db {
				company(name: $name) {
					id
				}
			}
		}""", {'name': company_name})
		return results['db']['company']

	def _get_kpm_path(self):
		file_path = self.gobjects['entry_kpm_file'].get_text()
		dir_path = self.gobjects['entry_kpm_dest_folder'].get_text()
		if not dir_path and not file_path:
			return _KPMPaths(None, None, True)
		if not (file_path and os.path.isfile(file_path) and os.access(file_path, os.R_OK)):
			return _KPMPaths(None, None, False)
		if not (dir_path and os.path.isdir(dir_path) and os.access(dir_path, os.R_OK | os.W_OK)):
			return _KPMPaths(None, None, False)
		return _KPMPaths(file_path, dir_path, True)

	def _set_info_url_details(self, model_row):
		named_row = _ModelNamedRow(*model_row)
		self.gobjects['label_url_info_url'].set_text(named_row.url or '')
		self.gobjects['label_url_info_authors'].set_text('\n'.join(named_row.authors))
		self.gobjects['label_url_info_created'].set_text(named_row.created or '')
		self.gobjects['label_url_info_description'].set_text(named_row.description or '')

		if named_row.classifiers:
			self.gobjects['label_url_info_for_classifiers'].set_property('visible', True)
			gui_utilities.gtk_listbox_populate_labels(
				self.gobjects['listbox_url_info_classifiers'],
				named_row.classifiers
			)
		else:
			self.gobjects['label_url_info_for_classifiers'].set_property('visible', False)

	def _do_regex_validation(self, test_text, entry):
		try:
			regex = re.compile(entry.get_text())
		except re.error:
			entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
			return
		result = True
		if regex.pattern and test_text:
			result = regex.match(test_text) is not None
		entry.set_property('secondary-icon-stock', 'gtk-yes' if result else 'gtk-no')

	def signal_assistant_apply(self, _):
		self._close_ready = False
		# have to do it this way because the next page will be selected when the apply signal is complete
		set_current_page = lambda page_name: self.assistant.set_current_page(max(0, self._page_titles[page_name] - 1))

		# get and validate the campaign name
		campaign_name = self.gobjects['entry_campaign_name'].get_text()
		campaign_name = campaign_name.strip()
		if not campaign_name:
			gui_utilities.show_dialog_error('Invalid Campaign Name', self.parent, 'A unique and valid campaign name must be specified.')
			set_current_page('Basic Settings')
			return True

		properties = {}
		# validate the credential validation regular expressions
		for field in ('username', 'password', 'mfa_token'):
			regex = self.gobjects['entry_validation_regex_' + field].get_text()
			if regex:
				try:
					re.compile(regex)
				except re.error:
					label = self.gobjects['label_validation_regex_' + field].get_text()
					gui_utilities.show_dialog_error('Invalid Regex', self.parent, "The '{0}' regular expression is invalid.".format(label))
					return True
			else:
				regex = None  # keep empty strings out of the database
			properties['credential_regex_' + field] = regex

		# validate the company
		company_id = None
		if self.gobjects['radiobutton_company_existing'].get_active():
			company_id = self._get_company_existing_id()
			if company_id is None:
				gui_utilities.show_dialog_error('Invalid Company', self.parent, 'A valid existing company must be specified.')
				set_current_page('Company')
				return True
		elif self.gobjects['radiobutton_company_new'].get_active():
			company_id = self._get_company_new_id()
			if company_id is None:
				gui_utilities.show_dialog_error('Invalid Company', self.parent, 'The new company settings are invalid.')
				set_current_page('Company')
				return True

		# get and validate the campaign expiration
		expiration = None
		if self.gobjects['checkbutton_expire_campaign'].get_property('active'):
			expiration = datetime.datetime.combine(
				gui_utilities.gtk_calendar_get_pydate(self.gobjects['calendar_campaign_expiration']),
				self._expiration_time.time
			)
			expiration = utilities.datetime_local_to_utc(expiration)
			if self.is_new_campaign and expiration <= datetime.datetime.now():
				gui_utilities.show_dialog_error('Invalid Campaign Expiration', self.parent, 'The expiration date is set in the past.')
				set_current_page('Expiration')
				return True

		# point of no return
		campaign_description = self.get_entry_value('campaign_description')
		if self.campaign_id:
			properties['name'] = self.campaign_name
			properties['description'] = campaign_description
			cid = self.campaign_id
		else:
			try:
				cid = self.application.rpc('campaign/new', campaign_name, description=campaign_description)
				properties['name'] = campaign_name
			except advancedhttpserver.RPCError as error:
				if not error.is_remote_exception:
					raise error
				if not error.remote_exception['name'] == 'exceptions.ValueError':
					raise error
				error_message = error.remote_exception.get('message', 'an unknown error occurred').capitalize() + '.'
				gui_utilities.show_dialog_error('Failed To Create Campaign', self.parent, error_message)
				set_current_page('Basic Settings')
				return True
			self.application.emit('campaign-created', cid)

		properties['campaign_type_id'] = self._get_tag_from_combobox(self.gobjects['combobox_campaign_type'], 'campaign_types')
		properties['company_id'] = company_id
		properties['expiration'] = expiration
		properties['max_credentials'] = (1 if self.gobjects['checkbutton_reject_after_credentials'].get_property('active') else None)
		self.application.rpc('db/table/set', 'campaigns', cid, tuple(properties.keys()), tuple(properties.values()))

		should_subscribe = self.gobjects['checkbutton_alert_subscribe'].get_property('active')
		if should_subscribe != self.application.rpc('campaign/alerts/is_subscribed', cid):
			if should_subscribe:
				self.application.rpc('campaign/alerts/subscribe', cid)
			else:
				self.application.rpc('campaign/alerts/unsubscribe', cid)

		self.application.emit('campaign-changed', cid)

		old_cid = self.config.get('campaign_id')
		self.config['campaign_id'] = cid
		self.config['campaign_name'] = properties['name']

		_kpm_pathing = self._get_kpm_path()
		if all(_kpm_pathing):
			if not self.application.main_tabs['mailer'].emit('message-data-import', _kpm_pathing.kpm_file, _kpm_pathing.destination_folder):
				gui_utilities.show_dialog_info('Failure', self.parent, 'Failed to import the message configuration.')
			else:
				gui_utilities.show_dialog_info('Success', self.parent, 'Successfully imported the message configuration.')

		url_model, url_iter = self.gobjects['treeselection_url_selector'].get_selected()
		if url_iter:
			selected_row = []
			for column_n in range(0, url_model.get_n_columns()):
				selected_row.append(url_model.get_value(url_iter, column_n))
			selected_row = _ModelNamedRow(*selected_row)
			self.config['mailer.webserver_url'] = selected_row.url

		self.application.emit('campaign-set', old_cid, cid)
		self._close_ready = True
		return

	def signal_assistant_cancel(self, assistant):
		assistant.destroy()

	def signal_assistant_close(self, assistant):
		if self._close_ready:
			assistant.destroy()
		self._close_ready = True

	def signal_assistant_prepare(self, _, page):
		page_title = self.assistant.get_page_title(page)
		if page_title == 'Company':
			combobox = self.gobjects['combobox_company_existing']
			model = combobox.get_model()
			company_name = self.get_entry_value('company_name')
			if company_name:
				model_iter = gui_utilities.gtk_list_store_search(model, company_name, column=1)
				if model_iter is not None:
					combobox.set_active_iter(model_iter)
					self.gobjects['radiobutton_company_existing'].set_active(True)

	def signal_calendar_prev(self, calendar):
		today = datetime.date.today()
		calendar_day = gui_utilities.gtk_calendar_get_pydate(calendar)
		if calendar_day >= today:
			return
		gui_utilities.gtk_calendar_set_pydate(calendar, today)

	def signal_checkbutton_expire_campaign_toggled(self, _):
		active = self.gobjects['checkbutton_expire_campaign'].get_property('active')
		self.gobjects['frame_campaign_expiration'].set_sensitive(active)

	def signal_entry_changed_test_validation_text(self, field):
		test_text = field.get_text()
		for field in ('username', 'password', 'mfa_token'):
			self._do_regex_validation(test_text, self.gobjects['entry_validation_regex_' + field])

	def signal_entry_changed_validation_regex(self, entry):
		self._do_regex_validation(self.gobjects['entry_test_validation_text'].get_text(), entry)

	def signal_expander_activate(self, expander):
		paned = self.gobjects['paned_url_info']
		if expander.get_property('expanded'):  # collapsing
			paned.set_position(paned.get_allocation().height + self._paned_offset)

	def signal_paned_button_press_event(self, paned, event):
		return not self.gobjects['expander_url_info'].get_property('expanded')

	def signal_radiobutton_toggled(self, radiobutton):
		if not radiobutton.get_active():
			return
		if radiobutton == self.gobjects['radiobutton_company_existing']:
			self.gobjects['frame_company_existing'].set_sensitive(True)
			self.gobjects['frame_company_new'].set_sensitive(False)
		elif radiobutton == self.gobjects['radiobutton_company_new']:
			self.gobjects['frame_company_existing'].set_sensitive(False)
			self.gobjects['frame_company_new'].set_sensitive(True)
		elif radiobutton == self.gobjects['radiobutton_company_none']:
			self.gobjects['frame_company_existing'].set_sensitive(False)
			self.gobjects['frame_company_new'].set_sensitive(False)

	def signal_treeview_row_activated(self, treeview, path, column):
		model_row = self._url_model[path]
		self._set_info_url_details(model_row)

	def interact(self):
		self.assistant.show_all()