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