1# Copyright (c) 2017-2021 Cedric Bellegarde <cedric.bellegarde@adishatz.org> 2# This program is free software: you can redistribute it and/or modify 3# it under the terms of the GNU General Public License as published by 4# the Free Software Foundation, either version 3 of the License, or 5# (at your option) any later version. 6# This program is distributed in the hope that it will be useful, 7# but WITHOUT ANY WARRANTY; without even the implied warranty of 8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9# GNU General Public License for more details. 10# You should have received a copy of the GNU General Public License 11# along with this program. If not, see <http://www.gnu.org/licenses/>. 12 13from gi.repository import GLib, Gtk, Gio, WebKit2, Gdk 14 15from gettext import gettext as _ 16from urllib.parse import urlparse 17from time import time 18 19from eolie.define import App, LoadingType, LoadingState 20from eolie.utils import get_ftp_cmd, emit_signal 21from eolie.logger import Logger 22 23 24class WebViewNavigation: 25 """ 26 Implement WebView navigation (uri, title, readable, ...) 27 Should be inherited by a WebView 28 """ 29 30 __MIMES = ["text/html", "text/xml", "application/xhtml+xml", 31 "x-scheme-handler/http", "x-scheme-handler/https", 32 "multipart/related", "application/x-mimearchive", 33 "application/x-extension-html"] 34 35 def __init__(self): 36 """ 37 Init navigation 38 """ 39 self.__loaded_uri = None 40 self.__insecure_content_detected = False 41 self.connect("decide-policy", self.__on_decide_policy) 42 self.connect("insecure-content-detected", 43 self.__on_insecure_content_detected) 44 self.connect("run-as-modal", self.__on_run_as_modal) 45 self.connect("permission_request", self.__on_permission_request) 46 47 def load_uri(self, uri): 48 """ 49 Load uri 50 @param uri as str 51 """ 52 parsed = urlparse(uri) 53 # If not an URI, start a search 54 is_uri = parsed.scheme in ["about", "http", 55 "https", "file", "populars"] 56 if not is_uri and\ 57 not uri.startswith("/") and\ 58 App().search.is_search(uri): 59 uri = App().search.get_search_uri(uri) 60 parsed = urlparse(uri) 61 if uri == "about:blank": 62 WebKit2.WebView.load_plain_text(self, "") 63 # We are not a ftp browser, fall back to env 64 elif parsed.scheme == "ftp": 65 argv = [get_ftp_cmd(), uri, None] 66 GLib.spawn_sync(None, argv, None, 67 GLib.SpawnFlags.SEARCH_PATH, None) 68 else: 69 if uri.startswith("/"): 70 uri = "file://" + uri 71 elif parsed.scheme == "javascript": 72 # To bypass popup blocker 73 self._last_click_time = time() 74 uri = GLib.uri_unescape_string(uri, None) 75 self.run_javascript(uri.replace("javascript:", ""), None, None) 76 elif parsed.scheme not in ["http", "https", "file", 77 "populars", "accept"]: 78 uri = "http://" + uri 79 # Reset bad tls certificate 80 if parsed.scheme != "accept": 81 self.reset_bad_tls() 82 self.__insecure_content_detected = False 83 self.stop_loading() 84 self.set_uri(uri) 85 self.__loaded_uri = self.uri 86 GLib.idle_add(WebKit2.WebView.load_uri, self, uri) 87 88 @property 89 def loaded_uri(self): 90 """ 91 Get loaded uri 92 @return str/None 93 """ 94 return self.__loaded_uri 95 96####################### 97# PROTECTED # 98####################### 99 def _set_user_agent(self, uri): 100 """ 101 Set user agent for uri 102 @param uri as str 103 """ 104 user_agent = App().websettings.get("user_agent", uri) 105 settings = self.get_settings() 106 if user_agent: 107 settings.set_user_agent(user_agent) 108 else: 109 settings.set_user_agent_with_application_details("Eolie", 110 None) 111 112 def _on_load_changed(self, webview, event): 113 """ 114 Update internals 115 @param webview as WebView 116 @param event as WebKit2.LoadEvent 117 """ 118 parsed = urlparse(webview.uri) 119 if event == WebKit2.LoadEvent.STARTED: 120 self._loading_state = LoadingState.LOADING 121 elif event == WebKit2.LoadEvent.COMMITTED: 122 if parsed.scheme in ["http", "https"]: 123 emit_signal(self, "title-changed", webview.uri) 124 self.update_zoom_level() 125 self.update_sound_policy() 126 elif event == WebKit2.LoadEvent.FINISHED: 127 if self._loading_state not in [LoadingState.STOPPED, 128 LoadingState.ERROR]: 129 self._loading_state = LoadingState.NONE 130 App().history.set_page_state(self.uri) 131 self.__update_bookmark_metadata(self.uri) 132 self.update_spell_checking(self.uri) 133 if App().show_tls: 134 try: 135 from OpenSSL import crypto 136 from datetime import datetime 137 (valid, tls, errors) = webview.get_tls_info() 138 if tls is not None: 139 Logger.info("***************************************" 140 "***************************************") 141 cert_pem = tls.get_property("certificate-pem") 142 cert = crypto.load_certificate(crypto.FILETYPE_PEM, 143 cert_pem) 144 subject = cert.get_subject() 145 Logger.info("CN: %s", subject.CN) 146 start_bytes = cert.get_notBefore() 147 end_bytes = cert.get_notAfter() 148 start = datetime.strptime(start_bytes.decode("utf-8"), 149 "%Y%m%d%H%M%SZ") 150 end = datetime.strptime(end_bytes.decode("utf-8"), 151 "%Y%m%d%H%M%SZ") 152 Logger.info("Valid from %s to %s", (start, end)) 153 Logger.info("Serial number: %s", 154 cert.get_serial_number()) 155 Logger.info(cert_pem) 156 Logger.info("***************************************" 157 "***************************************") 158 except Exception as e: 159 Logger.info("Please install OpenSSL python support: %s", e) 160 161####################### 162# PRIVATE # 163####################### 164 def __update_bookmark_metadata(self, uri): 165 """ 166 Update bookmark access time/popularity 167 @param uri as str 168 """ 169 if App().bookmarks.get_id(uri) is not None: 170 App().bookmarks.set_access_time(uri, round(time(), 2)) 171 App().bookmarks.set_more_popular(uri) 172 173 def __on_run_as_modal(self, webview): 174 Logger.info("WebView::__on_run_as_modal(): TODO") 175 176 def __on_insecure_content_detected(self, webview, event): 177 """ 178 @param webview as WebView 179 @param event as WebKit2.InsecureContentEvent 180 """ 181 self.__insecure_content_detected = True 182 183 def __on_permission_request(self, webview, request): 184 """ 185 Handle Webkit permissions 186 @param webview as WebKit2.WebView 187 @param request as WebKit2.PermissionRequest 188 """ 189 def on_allow_notifications(parsed): 190 values = list(App().settings.get_value("notification-domains")) 191 values.append("%s;%s" % (parsed.scheme, parsed.netloc)) 192 App().settings.set_value("notification-domains", 193 GLib.Variant("as", values)) 194 request.allow() 195 196 if isinstance(request, WebKit2.GeolocationPermissionRequest): 197 if self.is_ephemeral: 198 request.deny() 199 else: 200 uri = webview.uri 201 self.window.toolbar.title.show_geolocation(uri, request) 202 elif isinstance(request, WebKit2.NotificationPermissionRequest): 203 parsed = urlparse(webview.uri) 204 from eolie.container_notification import ContainerNotification 205 notification = ContainerNotification( 206 _("Allow notifications for %s") % parsed.netloc, 207 [_("Allow")], 208 [lambda: on_allow_notifications(parsed)]) 209 self.window.container.overlay.add_overlay(notification) 210 notification.show() 211 notification.set_reveal_child(True) 212 return True 213 214 def __on_decide_policy(self, webview, decision, decision_type): 215 """ 216 Navigation policy 217 @param webview as WebKit2.WebView 218 @param decision as WebKit2.NavigationPolicyDecision 219 @param decision_type as WebKit2.PolicyDecisionType 220 @return bool 221 """ 222 # Always accept response 223 if decision_type == WebKit2.PolicyDecisionType.RESPONSE: 224 response = decision.get_response() 225 mime_type = response.props.mime_type 226 uri = response.get_uri() 227 parsed = urlparse(uri) 228 if mime_type in self.__MIMES: 229 decision.use() 230 return False 231 elif parsed.scheme == "file": 232 f = Gio.File.new_for_uri(uri) 233 info = f.query_info("standard::type", 234 Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, 235 None) 236 if info.get_file_type() == Gio.FileType.REGULAR: 237 try: 238 Gtk.show_uri_on_window(self.window, 239 uri, 240 Gtk.get_current_event_time()) 241 except Exception as e: 242 Logger.error("""WebViewNavigation:: 243 __on_decide_policy(): %s""", e) 244 decision.ignore() 245 return True 246 else: 247 decision.use() 248 return False 249 elif self.can_show_mime_type(mime_type): 250 decision.use() 251 return False 252 else: 253 decision.download() 254 return True 255 256 navigation_action = decision.get_navigation_action() 257 navigation_uri = navigation_action.get_request().get_uri() 258 mouse_button = navigation_action.get_mouse_button() 259 parsed_navigation = urlparse(navigation_uri) 260 self.clear_text_entry() 261 if parsed_navigation.scheme not in ["http", "https", "file", "about", 262 "populars", "accept"]: 263 try: 264 Gtk.show_uri_on_window(self.window, 265 navigation_uri, 266 Gtk.get_current_event_time()) 267 except Exception as e: 268 Logger.error("WebViewNavigation::__on_decide_policy(): %s", e) 269 decision.ignore() 270 elif mouse_button == 0: 271 # Prevent opening empty pages 272 if navigation_uri == "about:blank": 273 self.reset_last_click_event() 274 decision.use() 275 return False 276 elif decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION: 277 decision.use() 278 return False 279 else: 280 decision.use() 281 return False 282 elif mouse_button == 1: 283 modifiers = navigation_action.get_modifiers() 284 if decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION: 285 decision.use() 286 return False 287 elif modifiers == Gdk.ModifierType.CONTROL_MASK: 288 self.new_page(navigation_uri, LoadingType.BACKGROUND) 289 decision.ignore() 290 return True 291 elif modifiers == Gdk.ModifierType.SHIFT_MASK: 292 self.new_page(navigation_uri, LoadingType.POPOVER) 293 decision.ignore() 294 return True 295 else: 296 self.__loaded_uri = navigation_uri.rstrip("/") 297 emit_signal(self, "title-changed", navigation_uri) 298 decision.use() 299 return False 300 else: 301 self.new_page(navigation_uri, LoadingType.BACKGROUND) 302 decision.ignore() 303 return True 304