|
| 1 | +import flask_login |
| 2 | +from flask import Blueprint, render_template, request, redirect, url_for, flash |
| 3 | +from flask_babel import gettext |
| 4 | + |
| 5 | +from changedetectionio.store import ChangeDetectionStore |
| 6 | +from changedetectionio.auth_decorator import login_optionally_required |
| 7 | + |
| 8 | + |
| 9 | +def construct_blueprint(datastore: ChangeDetectionStore): |
| 10 | + settings_browser_profile_blueprint = Blueprint( |
| 11 | + 'settings_browsers', |
| 12 | + __name__, |
| 13 | + template_folder="templates" |
| 14 | + ) |
| 15 | + |
| 16 | + def _render_index(browser_profile_form=None, editing_machine_name=None): |
| 17 | + from changedetectionio import forms |
| 18 | + from changedetectionio import content_fetchers as cf |
| 19 | + from changedetectionio.model.browser_profile import BrowserProfile, RESERVED_MACHINE_NAMES |
| 20 | + |
| 21 | + # Only browser-capable fetchers are valid profile types |
| 22 | + fetcher_choices = cf.available_browser_fetchers() |
| 23 | + if browser_profile_form is None: |
| 24 | + browser_profile_form = forms.BrowserProfileForm() |
| 25 | + browser_profile_form.fetch_backend.choices = fetcher_choices |
| 26 | + |
| 27 | + fetcher_supports_screenshots = {name: True for name, _ in fetcher_choices} |
| 28 | + fetcher_requires_connection_url = {name: True for name, cls in cf.FETCHERS.items() |
| 29 | + if getattr(cls, 'requires_connection_url', False)} |
| 30 | + |
| 31 | + # Table shows default built-in profiles first, then user-created profiles |
| 32 | + store_profiles = datastore.data['settings']['application'].get('browser_profiles', {}) |
| 33 | + user_profiles = dict(cf.DEFAULT_BROWSER_PROFILES) |
| 34 | + for machine_name, raw in store_profiles.items(): |
| 35 | + try: |
| 36 | + user_profiles[machine_name] = BrowserProfile(**raw) if isinstance(raw, dict) else raw |
| 37 | + except Exception: |
| 38 | + pass |
| 39 | + |
| 40 | + current_default = datastore.data['settings']['application'].get('browser_profile') or 'direct_http_requests' |
| 41 | + |
| 42 | + return render_template( |
| 43 | + "browser_profiles.html", |
| 44 | + browser_profiles=user_profiles, |
| 45 | + browser_profile_form=browser_profile_form, |
| 46 | + reserved_browser_profile_names=RESERVED_MACHINE_NAMES, |
| 47 | + fetcher_choices=fetcher_choices, |
| 48 | + fetcher_supports_screenshots=fetcher_supports_screenshots, |
| 49 | + fetcher_requires_connection_url=fetcher_requires_connection_url, |
| 50 | + current_default_profile=current_default, |
| 51 | + editing_machine_name=editing_machine_name, |
| 52 | + ) |
| 53 | + |
| 54 | + @settings_browser_profile_blueprint.route("", methods=['GET']) |
| 55 | + @login_optionally_required |
| 56 | + def index(): |
| 57 | + return _render_index() |
| 58 | + |
| 59 | + @settings_browser_profile_blueprint.route("/<string:machine_name>/edit", methods=['GET']) |
| 60 | + @login_optionally_required |
| 61 | + def edit(machine_name): |
| 62 | + from changedetectionio import forms |
| 63 | + from changedetectionio.model.browser_profile import BrowserProfile, RESERVED_MACHINE_NAMES |
| 64 | + |
| 65 | + if machine_name in RESERVED_MACHINE_NAMES: |
| 66 | + flash(gettext("Built-in browser profiles cannot be edited."), 'error') |
| 67 | + return redirect(url_for('settings.settings_browsers.index')) |
| 68 | + |
| 69 | + store_profiles = datastore.data['settings']['application'].get('browser_profiles', {}) |
| 70 | + raw = store_profiles.get(machine_name) |
| 71 | + if raw is None: |
| 72 | + flash(gettext("Browser profile not found."), 'error') |
| 73 | + return redirect(url_for('settings.settings_browsers.index')) |
| 74 | + |
| 75 | + profile = BrowserProfile(**raw) if isinstance(raw, dict) else raw |
| 76 | + form = forms.BrowserProfileForm(data=profile.model_dump()) |
| 77 | + return _render_index(browser_profile_form=form, editing_machine_name=machine_name) |
| 78 | + |
| 79 | + @settings_browser_profile_blueprint.route("/save", methods=['POST']) |
| 80 | + @login_optionally_required |
| 81 | + def save(): |
| 82 | + from changedetectionio import forms |
| 83 | + from changedetectionio import content_fetchers as cf |
| 84 | + from changedetectionio.model.browser_profile import BrowserProfile, RESERVED_MACHINE_NAMES |
| 85 | + |
| 86 | + fetcher_choices = [(name, desc) for name, desc in cf.available_fetchers()] |
| 87 | + browser_profile_form = forms.BrowserProfileForm(formdata=request.form) |
| 88 | + browser_profile_form.fetch_backend.choices = fetcher_choices |
| 89 | + |
| 90 | + if not browser_profile_form.validate(): |
| 91 | + flash(gettext("Browser profile error: {}").format( |
| 92 | + '; '.join(str(e) for errs in browser_profile_form.errors.values() for e in errs) |
| 93 | + ), 'error') |
| 94 | + return redirect(url_for('settings.settings_browsers.index')) |
| 95 | + |
| 96 | + name = browser_profile_form.name.data.strip() |
| 97 | + machine_name = BrowserProfile.machine_name_from_str(name) |
| 98 | + |
| 99 | + if machine_name in RESERVED_MACHINE_NAMES: |
| 100 | + flash(gettext("Cannot use reserved profile name '{}'. Please choose a different name.").format(name), 'error') |
| 101 | + return redirect(url_for('settings.settings_browsers.index')) |
| 102 | + |
| 103 | + original_machine_name = request.form.get('original_machine_name', '').strip() |
| 104 | + store_profiles = datastore.data['settings']['application'].setdefault('browser_profiles', {}) |
| 105 | + |
| 106 | + if machine_name != original_machine_name and machine_name in store_profiles: |
| 107 | + flash(gettext("A browser profile named '{}' already exists.").format(name), 'error') |
| 108 | + return redirect(url_for('settings.settings_browsers.index')) |
| 109 | + |
| 110 | + profile_data = { |
| 111 | + 'name': name, |
| 112 | + 'fetch_backend': browser_profile_form.fetch_backend.data, |
| 113 | + 'browser_connection_url': browser_profile_form.browser_connection_url.data or None, |
| 114 | + 'viewport_width': browser_profile_form.viewport_width.data or 1280, |
| 115 | + 'viewport_height': browser_profile_form.viewport_height.data or 1000, |
| 116 | + 'block_images': bool(browser_profile_form.block_images.data), |
| 117 | + 'block_fonts': bool(browser_profile_form.block_fonts.data), |
| 118 | + 'ignore_https_errors': bool(browser_profile_form.ignore_https_errors.data), |
| 119 | + 'user_agent': browser_profile_form.user_agent.data or None, |
| 120 | + 'locale': browser_profile_form.locale.data or None, |
| 121 | + 'custom_headers': browser_profile_form.custom_headers.data or '', |
| 122 | + 'is_builtin': False, |
| 123 | + } |
| 124 | + |
| 125 | + try: |
| 126 | + BrowserProfile(**profile_data) |
| 127 | + except Exception as e: |
| 128 | + flash(gettext("Browser profile validation error: {}").format(str(e)), 'error') |
| 129 | + return redirect(url_for('settings.settings_browsers.index')) |
| 130 | + |
| 131 | + # Handle rename: remove old key, cascade-update watches and tags |
| 132 | + if original_machine_name and original_machine_name != machine_name and original_machine_name in store_profiles: |
| 133 | + del store_profiles[original_machine_name] |
| 134 | + for watch in datastore.data['watching'].values(): |
| 135 | + if watch.get('browser_profile') == original_machine_name: |
| 136 | + watch['browser_profile'] = machine_name |
| 137 | + for tag in datastore.data.get('settings', {}).get('application', {}).get('tags', {}).values(): |
| 138 | + if tag.get('browser_profile') == original_machine_name: |
| 139 | + tag['browser_profile'] = machine_name |
| 140 | + |
| 141 | + store_profiles[machine_name] = profile_data |
| 142 | + datastore.commit() |
| 143 | + flash(gettext("Browser profile '{}' saved.").format(name), 'notice') |
| 144 | + return redirect(url_for('settings.settings_browsers.index')) |
| 145 | + |
| 146 | + @settings_browser_profile_blueprint.route("/<string:machine_name>/delete", methods=['GET']) |
| 147 | + @login_optionally_required |
| 148 | + def delete(machine_name): |
| 149 | + from changedetectionio.model.browser_profile import RESERVED_MACHINE_NAMES |
| 150 | + |
| 151 | + if machine_name in RESERVED_MACHINE_NAMES: |
| 152 | + flash(gettext("Built-in browser profiles cannot be deleted."), 'error') |
| 153 | + return redirect(url_for('settings.settings_browsers.index')) |
| 154 | + |
| 155 | + store_profiles = datastore.data['settings']['application'].get('browser_profiles', {}) |
| 156 | + if machine_name not in store_profiles: |
| 157 | + flash(gettext("Browser profile not found."), 'error') |
| 158 | + return redirect(url_for('settings.settings_browsers.index')) |
| 159 | + |
| 160 | + raw = store_profiles[machine_name] |
| 161 | + profile_name = raw.get('name', machine_name) if isinstance(raw, dict) else machine_name |
| 162 | + |
| 163 | + for watch in datastore.data['watching'].values(): |
| 164 | + if watch.get('browser_profile') == machine_name: |
| 165 | + watch['browser_profile'] = None |
| 166 | + |
| 167 | + for tag in datastore.data.get('settings', {}).get('application', {}).get('tags', {}).values(): |
| 168 | + if tag.get('browser_profile') == machine_name: |
| 169 | + tag['browser_profile'] = None |
| 170 | + |
| 171 | + if datastore.data['settings']['application'].get('browser_profile') == machine_name: |
| 172 | + datastore.data['settings']['application']['browser_profile'] = None |
| 173 | + |
| 174 | + del store_profiles[machine_name] |
| 175 | + datastore.commit() |
| 176 | + flash(gettext("Browser profile '{}' deleted.").format(profile_name), 'notice') |
| 177 | + return redirect(url_for('settings.settings_browsers.index')) |
| 178 | + |
| 179 | + @settings_browser_profile_blueprint.route("/set-default", methods=['POST']) |
| 180 | + @login_optionally_required |
| 181 | + def set_default(): |
| 182 | + from changedetectionio import content_fetchers as cf |
| 183 | + |
| 184 | + machine_name = request.form.get('machine_name', '').strip() |
| 185 | + if not machine_name: |
| 186 | + flash(gettext("No profile specified."), 'error') |
| 187 | + return redirect(url_for('settings.settings_browsers.index')) |
| 188 | + |
| 189 | + from changedetectionio.model.browser_profile import get_profile |
| 190 | + store_profiles = datastore.data['settings']['application'].get('browser_profiles', {}) |
| 191 | + if get_profile(machine_name, store_profiles) is None: |
| 192 | + flash(gettext("Unknown browser profile '{}'.").format(machine_name), 'error') |
| 193 | + return redirect(url_for('settings.settings_browsers.index')) |
| 194 | + |
| 195 | + datastore.data['settings']['application']['browser_profile'] = machine_name |
| 196 | + datastore.commit() |
| 197 | + flash(gettext("Default browser profile set to '{}'.").format(machine_name), 'notice') |
| 198 | + return redirect(url_for('settings.settings_browsers.index')) |
| 199 | + |
| 200 | + return settings_browser_profile_blueprint |
0 commit comments