source: src/mlx/gui/cef.py@ 1186:beb5a473fd54

python3
Last change on this file since 1186:beb5a473fd54 was 1186:beb5a473fd54, checked in by István Váradi <ivaradi@…>, 39 hours ago

SimBrief MFA login is supported

File size: 18.3 KB
RevLine 
[919]1from .common import *
[648]2
[682]3from mlx.util import secondaryInstallation
4
5from cefpython3 import cefpython
6
[648]7import platform
8import json
[682]9import time
[648]10import os
11import re
[919]12import _thread
[682]13import threading
14import tempfile
15import traceback
[945]16import ctypes
[919]17import urllib.request, urllib.error, urllib.parse
[805]18from lxml import etree
[919]19from io import StringIO
[805]20import lxml.html
[1096]21import shutil
[648]22
23#------------------------------------------------------------------------------
24
25## @package mlx.gui.cef
26#
27# Some helper stuff related to the Chrome Embedded Framework
28
29#------------------------------------------------------------------------------
30
31# Indicate if we should quit
32_toQuit = False
33
[805]34# The SimBrief handler
35_simBriefHandler = None
[682]36
[1097]37# The cache directory
38cacheDir = os.path.join(GLib.get_user_cache_dir(), "mlxcef")
39
[682]40#------------------------------------------------------------------------------
41
[805]42SIMBRIEF_PROGRESS_SEARCHING_BROWSER = 1
43SIMBRIEF_PROGRESS_LOADING_FORM = 2
44SIMBRIEF_PROGRESS_FILLING_FORM = 3
45SIMBRIEF_PROGRESS_WAITING_LOGIN = 4
46SIMBRIEF_PROGRESS_LOGGING_IN = 5
47SIMBRIEF_PROGRESS_WAITING_RESULT = 6
[682]48
[805]49SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING = 7
50SIMBRIEF_PROGRESS_DONE = 1000
[682]51
[805]52SIMBRIEF_RESULT_NONE = 0
53SIMBRIEF_RESULT_OK = 1
54SIMBRIEF_RESULT_ERROR_OTHER = 2
55SIMBRIEF_RESULT_ERROR_NO_FORM = 11
56SIMBRIEF_RESULT_ERROR_NO_POPUP = 12
57SIMBRIEF_RESULT_ERROR_LOGIN_FAILED = 13
[682]58
[1186]59#-----------------------------------------------------------------------------
60
61class SimBriefMFACodeDialog(Gtk.Dialog):
62 """A dialog window to ask for the SimBrief (Navigraph) MFA code."""
63 def __init__(self, gui):
64 """Construct the dialog."""
65 super(SimBriefMFACodeDialog, self).__init__(WINDOW_TITLE_BASE + " - " +
66 xstr("simbrief_mfa_code_title"),
67 gui.mainWindow,
68 Gtk.DialogFlags.MODAL)
69 self.add_button(xstr("button_cancel"), Gtk.ResponseType.CANCEL)
70 self.add_button(xstr("button_ok"), Gtk.ResponseType.OK)
71
72 contentArea = self.get_content_area()
73
74 contentAlignment = Gtk.Alignment(xalign = 0.5, yalign = 0.5,
75 xscale = 0.0, yscale = 0.0)
76 contentAlignment.set_padding(padding_top = 4, padding_bottom = 16,
77 padding_left = 8, padding_right = 8)
78
79 contentArea.pack_start(contentAlignment, False, False, 0)
80
81 contentVBox = Gtk.VBox()
82 contentAlignment.add(contentVBox)
83
84 label = Gtk.Label(xstr("simbrief_mfa_code_needed"))
85 label.set_line_wrap(True)
86 label.set_justify(Gtk.Justification.CENTER)
87 label.set_alignment(0.5, 0.0)
88
89 contentVBox.pack_start(label, False, False, 0)
90
91 tableAlignment = Gtk.Alignment(xalign = 0.5, yalign = 0.5,
92 xscale = 0.0, yscale = 0.0)
93 tableAlignment.set_padding(padding_top = 24, padding_bottom = 0,
94 padding_left = 0, padding_right = 0)
95
96 table = Gtk.Table(1, 2)
97 table.set_row_spacings(4)
98 table.set_col_spacings(16)
99 table.set_homogeneous(False)
100
101 tableAlignment.add(table)
102 contentVBox.pack_start(tableAlignment, True, True, 0)
103
104 label = Gtk.Label(xstr("simbrief_mfa_code"))
105 label.set_use_underline(True)
106 label.set_alignment(0.0, 0.5)
107 table.attach(label, 0, 1, 0, 1)
108
109 self._mfaCode = Gtk.Entry()
110 self._mfaCode.set_width_chars(16)
111 self._mfaCode.set_tooltip_text(xstr("simbrief_mfa_code_tooltip"))
112 table.attach(self._mfaCode, 1, 2, 0, 1)
113 label.set_mnemonic_widget(self._mfaCode)
114
115
116 @property
117 def mfaCode(self):
118 """Get the MFA code entered."""
119 return self._mfaCode.get_text()
120
121 def run(self):
122 """Run the dialog."""
123 self.show_all()
124
125 response = super(SimBriefMFACodeDialog, self).run()
126
127 self.hide()
128
129 return response
130
[648]131#------------------------------------------------------------------------------
132
[805]133class SimBriefHandler(object):
134 """An object to store the state of a SimBrief query."""
[1084]135 _formURLBase = MAVA_BASE_URL + "/simbrief_form.php"
136
137 _resultURLBase = MAVA_BASE_URL + "/simbrief_briefing.php"
138
139 @staticmethod
140 def getFormURL(plan):
141 """Get the form URL for the given plan."""
142 return SimBriefHandler._formURLBase + "?" + \
143 urllib.parse.urlencode(plan)
[692]144
[805]145 _querySettings = {
146 'navlog': True,
147 'etops': True,
148 'stepclimbs': True,
149 'tlr': True,
150 'notams': True,
151 'firnot': True,
152 'maps': 'Simple',
153 };
[682]154
155
[1186]156 def __init__(self, gui):
[805]157 """Construct the handler."""
158 self._browser = None
159 self._plan = None
160 self._getCredentials = None
161 self._getCredentialsCount = 0
162 self._updateProgressFn = None
163 self._htmlFilePath = None
164 self._lastProgress = SIMBRIEF_PROGRESS_SEARCHING_BROWSER
165 self._timeoutID = None
[1186]166 self._gui = gui
[682]167
[805]168 def initialize(self):
[695]169 """Create and initialize the browser used for Simbrief."""
170 windowInfo = cefpython.WindowInfo()
171 windowInfo.SetAsOffscreen(int(0))
172
[805]173 self._browser = \
[1084]174 cefpython.CreateBrowserSync(windowInfo, browserSettings = {})
[805]175 self._browser.SetClientHandler(OffscreenRenderHandler())
176 self._browser.SetFocus(True)
177 self._browser.SetClientCallback("OnLoadEnd", self._onLoadEnd)
[1083]178 self._browser.SetClientCallback("OnLoadError", self._onLoadError)
179 self._browser.SetClientCallback("OnBeforeBrowse",
180 lambda browser, frame, request,
181 user_gesture, is_redirect: False)
[805]182
183 def call(self, plan, getCredentials, updateProgress, htmlFilePath):
[685]184 """Call SimBrief with the given plan."""
[995]185 self._timeoutID = GObject.timeout_add(120*1000, self._timedOut)
[805]186
187 self._plan = plan
188 self._getCredentials = getCredentials
189 self._getCredentialsCount = 0
190 self._updateProgressFn = updateProgress
191 self._htmlFilePath = htmlFilePath
192
[1084]193 self._browser.LoadUrl(SimBriefHandler.getFormURL(plan))
[805]194 self._updateProgress(SIMBRIEF_PROGRESS_LOADING_FORM,
195 SIMBRIEF_RESULT_NONE, None)
196
[1083]197 def finalize(self):
198 """Close the browser and release it."""
[1099]199 if self._browser is not None:
200 self._browser.CloseBrowser(True)
201 self._browser = None
[1083]202
[943]203 def _onLoadEnd(self, browser, frame, http_code):
[805]204 """Called when a page has been loaded in the SimBrief browser."""
205 url = frame.GetUrl()
[943]206 print("gui.cef.SimBriefHandler._onLoadEnd", http_code, url)
207 if http_code>=300:
[805]208 self._updateProgress(self._lastProgress,
209 SIMBRIEF_RESULT_ERROR_OTHER, None)
[1084]210 elif url.startswith(SimBriefHandler._formURLBase):
211 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
[805]212 SIMBRIEF_RESULT_NONE, None)
[1084]213 elif url.startswith("https://www.simbrief.com/system/login.api.sso.php"):
214 js = "document.getElementsByClassName(\"login_option navigraph\")[0].click();"
[805]215 frame.ExecuteJavascript(js)
[1084]216 elif url.startswith("https://identity.api.navigraph.com/login?"):
[805]217 (user, password) = self._getCredentials(self._getCredentialsCount)
218 if user is None or password is None:
219 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
220 SIMBRIEF_RESULT_ERROR_LOGIN_FAILED, None)
221 return
[682]222
[805]223 self._getCredentialsCount += 1
[1084]224
225 js = "form=document.getElementsByName(\"form\")[0];"
226 js +="form.username.value=\"" + user + "\";"
227 js +="form.password.value=\"" + password + "\";"
228 js +="form.submit();"
[805]229 frame.ExecuteJavascript(js)
[1186]230 elif url.startswith("https://identity.api.navigraph.com//mfaEmail"):
231 dialog = SimBriefMFACodeDialog(self._gui)
232 response = dialog.run()
233
234 if response!=Gtk.ResponseType.OK:
235 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
236 SIMBRIEF_RESULT_ERROR_LOGIN_FAILED, None)
237 return
238
239 mfaCode = dialog.mfaCode
240 js = "form=document.getElementById(\"form1\");"
241 js +="form.txtcdvl.value=\"" + mfaCode + "\";"
242 js +="form.btnApprove.click();"
243 frame.ExecuteJavascript(js)
244 elif url.startswith("https://identity.api.navigraph.com/mfaFailed"):
245 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_LOGIN,
246 SIMBRIEF_RESULT_ERROR_LOGIN_FAILED, None)
[1084]247 elif url.startswith("https://www.simbrief.com/ofp/ofp.loader.api.php"):
[805]248 self._updateProgress(SIMBRIEF_PROGRESS_WAITING_RESULT,
249 SIMBRIEF_RESULT_NONE, None)
[1084]250 elif url.startswith(SimBriefHandler._resultURLBase):
[805]251 self._updateProgress(SIMBRIEF_PROGRESS_RETRIEVING_BRIEFING,
[1084]252 SIMBRIEF_RESULT_OK, None)
[805]253
[1083]254 def _onLoadError(self, browser, frame, error_code, error_text_out,
255 failed_url):
256 """Called when loading of an URL fails."""
257 print("gui.cef.SimBriefHandler._onLoadError", browser, frame, error_code, error_text_out, failed_url)
[1095]258 if error_code==-3 and \
259 failed_url.startswith("https://identity.api.navigraph.com/connect/authorize"):
260 self._browser.LoadUrl(SimBriefHandler.getFormURL(self._plan))
261 self._updateProgress(SIMBRIEF_PROGRESS_LOADING_FORM,
262 SIMBRIEF_RESULT_NONE, None)
263 else:
264 self._updateProgress(self._lastProgress,
265 SIMBRIEF_RESULT_ERROR_OTHER, None)
[805]266
267 def _updateProgress(self, progress, results, flightInfo):
268 """Update the progress."""
269 self._lastProgress = progress
270 if results!=SIMBRIEF_RESULT_NONE:
[944]271 if self._timeoutID is not None:
[995]272 GObject.source_remove(self._timeoutID)
[805]273 self._plan = None
274
[944]275 if self._updateProgressFn is not None:
276 self._updateProgressFn(progress, results, flightInfo)
[805]277
278 def _timedOut(self):
279 """Called when the timeout occurs."""
280 if self._lastProgress==SIMBRIEF_PROGRESS_LOADING_FORM:
281 result = SIMBRIEF_RESULT_ERROR_NO_FORM
282 elif self._lastProgress==SIMBRIEF_PROGRESS_WAITING_LOGIN:
283 result = SIMBRIEF_RESULT_ERROR_NO_POPUP
284 else:
285 result = SIMBRIEF_RESULT_ERROR_OTHER
[685]286
[805]287 self._updateProgress(self._lastProgress, result, None)
288
289 return False
[685]290
[682]291#------------------------------------------------------------------------------
292
[1186]293def initialize(initializedCallback, gui):
[648]294 """Initialize the Chrome Embedded Framework."""
[805]295 global _toQuit, _simBriefHandler
[648]296 _toQuit = False
297
[995]298 GObject.threads_init()
[648]299
[1186]300 _simBriefHandler = SimBriefHandler(gui)
[1111]301 GObject.timeout_add(100, _initializeCEF, [], initializedCallback)
[682]302
303#------------------------------------------------------------------------------
304
305def _initializeCEF(args, initializedCallback):
306 """Perform the actual initialization of CEF using the given arguments."""
[919]307 print("Initializing CEF with args:", args)
[682]308
[648]309 settings = {
310 "debug": True, # cefpython debug messages in console and in log_file
311 "log_severity": cefpython.LOGSEVERITY_VERBOSE, # LOGSEVERITY_VERBOSE
312 "log_file": "", # Set to "" to disable
313 "release_dcheck_enabled": True, # Enable only when debugging
314 # This directories must be set on Linux
315 "locales_dir_path": os.path.join(cefpython.GetModuleDirectory(), "locales"),
316 "resources_dir_path": cefpython.GetModuleDirectory(),
317 "browser_subprocess_path": "%s/%s" % \
318 (cefpython.GetModuleDirectory(), "subprocess"),
[1083]319 "windowless_rendering_enabled": True,
[1097]320 "cache_path": cacheDir,
321 "persist_session_cookies": True,
322 "persist_user_preferences": True
[648]323 }
324
[682]325 switches={}
326 for arg in args:
327 if arg.startswith("--"):
328 if arg != "--enable-logging":
329 assignIndex = arg.find("=")
330 if assignIndex<0:
331 switches[arg[2:]] = ""
332 else:
333 switches[arg[2:assignIndex]] = arg[assignIndex+1:]
334 else:
[919]335 print("Unhandled switch", arg)
[682]336
337 cefpython.Initialize(settings, switches)
[648]338
[995]339 GObject.timeout_add(10, _handleTimeout)
[648]340
[919]341 print("Initialized, executing callback...")
[682]342 initializedCallback()
[1111]343
344 if os.name != "nt":
345 Gtk.main_quit()
346
[1091]347 return False
[682]348
[648]349#------------------------------------------------------------------------------
350
[1108]351def messageLoop():
352 """Run the CEF message loop"""
353 cefpython.MessageLoop()
354
355#------------------------------------------------------------------------------
356
[648]357def getContainer():
358 """Get a container object suitable for running a browser instance
359 within."""
[1083]360 container = Gtk.DrawingArea()
361 container.set_property("can-focus", True)
362 container.connect("size-allocate", _handleSizeAllocate)
[648]363 container.show()
364
365 return container
366
367#------------------------------------------------------------------------------
368
369def startInContainer(container, url, browserSettings = {}):
370 """Start a browser instance in the given container with the given URL."""
371 if os.name=="nt":
[997]372 Gdk.threads_enter()
[945]373 ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
374 ctypes.pythonapi.PyCapsule_GetPointer.argtypes = \
375 [ctypes.py_object]
376 gpointer = ctypes.pythonapi.PyCapsule_GetPointer(
377 container.get_property("window").__gpointer__, None)
378 libgdk = ctypes.CDLL("libgdk-3-0.dll")
379 windowID = libgdk.gdk_win32_window_get_handle(gpointer)
380 container.windowID = windowID
[997]381 Gdk.threads_leave()
[1083]382 windowRect = [0, 0, 1, 1]
[648]383 else:
[924]384 container.set_visual(container.get_screen().lookup_visual(0x21))
385 windowID = container.get_window().get_xid()
[1083]386 windowRect = None
[648]387
388 windowInfo = cefpython.WindowInfo()
[805]389 if windowID is not None:
[1083]390 windowInfo.SetAsChild(windowID, windowRect = windowRect)
[648]391
[1083]392 browser = cefpython.CreateBrowserSync(windowInfo,
393 browserSettings = browserSettings,
394 navigateUrl = url)
395 container.browser = browser
396
397 return browser
[648]398
399#------------------------------------------------------------------------------
400
[685]401class OffscreenRenderHandler(object):
[943]402 def GetRootScreenRect(self, browser, rect_out):
[685]403 #print "GetRootScreenRect"
[1083]404 rect_out += [0, 0, 1920, 1080]
[685]405 return True
406
[943]407 def GetViewRect(self, browser, rect_out):
[685]408 #print "GetViewRect"
[1083]409 rect_out += [0, 40, 1920, 1040]
[685]410 return True
411
412 def GetScreenPoint(self, browser, viewX, viewY, screenCoordinates):
413 #print "GetScreenPoint", viewX, viewY
414 rect += [viewX, viewY]
415 return True
416
417 def GetScreenInfo(self, browser, screenInfo):
418 #print "GetScreenInfo"
419 pass
420
421 def OnPopupShow(self, browser, show):
422 #print "OnPopupShow", show
423 pass
424
425 def OnPopupSize(self, browser, rect):
426 #print "OnPopupSize", rect
427 pass
428
[943]429 def OnPaint(self, browser, element_type, dirty_rects, paint_buffer, width, height):
[685]430 #print "OnPaint", paintElementType, dirtyRects, buffer, width, height
431 pass
432
433 def OnCursorChange(self, browser, cursor):
434 #print "OnCursorChange", cursor
435 pass
436
437 def OnScrollOffsetChanged(self, browser):
438 #print "OnScrollOffsetChange"
439 pass
440
[1083]441 def OnBeforePopup(self, browser, frame, target_url, target_frame_name,
442 target_disposition, user_gesture,
443 popup_features, window_info_out, client,
444 browser_settings_out, no_javascript_access_out,
445 **kwargs):
[685]446 wInfo = cefpython.WindowInfo()
447 wInfo.SetAsOffscreen(int(0))
448
[1083]449 window_info_out.append(wInfo)
[685]450
451 return False
452
453#------------------------------------------------------------------------------
454
455def initializeSimBrief():
456 """Initialize the (hidden) browser window for SimBrief."""
[805]457 _simBriefHandler.initialize()
[685]458
459#------------------------------------------------------------------------------
460
461def callSimBrief(plan, getCredentials, updateProgress, htmlFilePath):
[805]462 """Call SimBrief with the given plan.
463
464 The callbacks will be called in the main GUI thread."""
465 _simBriefHandler.call(plan, getCredentials, updateProgress, htmlFilePath)
[685]466
467#------------------------------------------------------------------------------
468
[1097]469def finalizeSimBrief():
470 """Finallize the (hidden) browser window for SimBrief."""
471 if _simBriefHandler is not None:
472 _simBriefHandler.finalize()
473
474#------------------------------------------------------------------------------
475
[1108]476def quitMessageLoop():
477 """Quit the CEF message loop"""
478 cefpython.QuitMessageLoop()
479
480#------------------------------------------------------------------------------
481
[648]482def finalize():
483 """Finalize the Chrome Embedded Framework."""
[805]484 global _toQuit
485 _toQuit = True
[1083]486
[1097]487 if os.name!="nt":
488 cefpython.Shutdown()
489
490#------------------------------------------------------------------------------
[1083]491
[1097]492def clearCache():
493 """Clear the CEF cache directory."""
494 try:
495 shutil.rmtree(cacheDir)
496 print("gui.cef.clearCache: the cache directory is removed")
497 except Exception as e:
498 print("gui.cef.clearCache: cache directory removal failed:", e)
[648]499
500#------------------------------------------------------------------------------
501
502def _handleTimeout():
503 """Handle the timeout by running the CEF message loop."""
504 if _toQuit:
505 return False
506 else:
507 cefpython.MessageLoopWork()
508 return True
509
510#------------------------------------------------------------------------------
511
512def _handleSizeAllocate(widget, sizeAlloc):
[945]513 """Handle the size-allocate event on Windows."""
[1083]514 if os.name=="nt":
515 if widget is not None and hasattr(widget, "windowID"):
516 cefpython.WindowUtils.OnSize(widget.windowID, 0, 0, 0)
517 else:
518 if widget is not None and hasattr(widget, "browser") and \
519 widget.browser is not None:
520 widget.browser.SetBounds(sizeAlloc.x, sizeAlloc.y,
521 sizeAlloc.width, sizeAlloc.height)
Note: See TracBrowser for help on using the repository browser.