Commit da59c41c authored by Cédric Bellegarde's avatar Cédric Bellegarde

Add button to force sync to Firefox Sync

parent 3f7427dd
Pipeline #83641 failed with stage
in 7 minutes and 38 seconds
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="image1">
......@@ -7,11 +7,6 @@
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">view-refresh-symbolic</property>
</object>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
......@@ -142,48 +137,6 @@
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="sync_stack">
<property name="can_focus">False</property>
<property name="margin_start">1</property>
<property name="margin_end">1</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<child>
<object class="GtkButton" id="reload_bookmarks">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Sync bookmarks</property>
<property name="image">image2</property>
<property name="relief">none</property>
<signal name="clicked" handler="_on_sync_button_clicked" swapped="no"/>
<style>
<class name="small-padding"/>
</style>
</object>
<packing>
<property name="name">sync</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
</object>
<packing>
<property name="name">spinner</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="import_button">
<property name="visible">True</property>
......@@ -205,6 +158,9 @@
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">1</property>
......
......@@ -19,6 +19,21 @@
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
</object>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-down-symbolic</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-up-symbolic</property>
</object>
<object class="GtkWindow" id="settings_dialog">
<property name="width_request">700</property>
<property name="can_focus">False</property>
......@@ -974,6 +989,7 @@ on all your devices running Eolie or Firefox</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">start</property>
<signal name="clicked" handler="_on_sync_button_clicked" swapped="no"/>
<style>
<class name="suggested-action"/>
......@@ -1045,7 +1061,48 @@ on all your devices running Eolie or Firefox</property>
</packing>
</child>
<child>
<placeholder/>
<object class="GtkBox" id="sync_buttons">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="pull_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Pull from Firefox sync</property>
<property name="image">image1</property>
<signal name="clicked" handler="_on_pull_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="push_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Push to Firefox sync</property>
<property name="image">image2</property>
<signal name="clicked" handler="_on_push_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
</object>
<packing>
......@@ -1072,9 +1129,4 @@ on all your devices running Eolie or Firefox</property>
<widget name="appearance"/>
</widgets>
</object>
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
</object>
</interface>
......@@ -334,19 +334,6 @@
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.settings</property>
<property name="text" translatable="yes">_Preferences</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">9</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
......@@ -396,5 +383,43 @@
<property name="top_attach">12</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="hexpand">True</property>
<property name="action_name">app.settings</property>
<property name="text" translatable="yes">_Preferences</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_right">5</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">9</property>
</packing>
</child>
</object>
</interface>
......@@ -78,13 +78,6 @@ class Application(Gtk.Application):
application_id="org.gnome.Eolie",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.set_property("register-session", True)
# Fix proxy for python
proxy = GLib.environ_getenv(GLib.get_environ(), "all_proxy")
if proxy is not None and proxy.startswith("socks://"):
proxy = proxy.replace("socks://", "socks4://")
from os import environ
environ["all_proxy"] = proxy
environ["ALL_PROXY"] = proxy
# Ideally, we will be able to delete this once Flatpak has a solution
# for SSL certificate management inside of applications.
if GLib.file_test("/app", GLib.FileTest.EXISTS):
......@@ -218,8 +211,7 @@ class Application(Gtk.Application):
self.history.clear_to(int(atime))
if self.sync_worker is not None:
if self.sync_worker.syncing:
self.sync_worker.stop()
self.sync_worker.stop()
self.sync_worker.save_pendings()
if vacuum:
task_helper = TaskHelper()
......@@ -323,7 +315,7 @@ class Application(Gtk.Application):
from eolie.firefox_sync import SyncWorker
if SyncWorker.check_modules():
self.sync_worker = SyncWorker()
self.sync_worker.sync_loop()
self.sync_worker.pull_loop()
else:
self.sync_worker = None
cssProviderFile = Gio.File.new_for_uri(
......
......@@ -414,22 +414,31 @@ class DatabaseBookmarks:
ORDER BY title COLLATE LOCALIZED")
return list(result)
def get_bookmarks(self, tag_id):
def get_bookmarks(self, tag_id=None):
"""
Get all bookmarks
@param tag id as int
@return [(id, title, uri)]
"""
with SqlCursor(self) as sql:
result = sql.execute("\
SELECT bookmarks.rowid,\
bookmarks.uri,\
bookmarks.title\
FROM bookmarks, bookmarks_tags\
WHERE bookmarks.rowid=bookmarks_tags.bookmark_id\
AND bookmarks_tags.tag_id=?\
AND bookmarks.guid != bookmarks.uri\
ORDER BY bookmarks.popularity DESC", (tag_id,))
if tag_id is None:
result = sql.execute("\
SELECT bookmarks.rowid,\
bookmarks.uri,\
bookmarks.title\
FROM bookmarks\
ORDER BY bookmarks.popularity DESC")
else:
result = sql.execute("\
SELECT bookmarks.rowid,\
bookmarks.uri,\
bookmarks.title\
FROM bookmarks, bookmarks_tags\
WHERE bookmarks.rowid=\
bookmarks_tags.bookmark_id\
AND bookmarks_tags.tag_id=?\
AND bookmarks.guid != bookmarks.uri\
ORDER BY bookmarks.popularity DESC", (tag_id,))
return list(result)
def get_populars(self, limit):
......
......@@ -11,7 +11,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from gi.repository import Gio, GLib
from gi.repository import Gio, GLib, GObject
from pickle import dump, load
from hashlib import sha256
......@@ -30,11 +30,15 @@ TOKENSERVER_URL = "https://token.services.mozilla.com/"
FXA_SERVER_URL = "https://api.accounts.mozilla.com"
class SyncWorker:
class SyncWorker(GObject.Object):
"""
Manage sync with firefox server, will start syncing on init
"""
__gsignals__ = {
'syncing': (GObject.SignalFlags.RUN_FIRST, None, (bool,))
}
def check_modules():
"""
True if deps are installed
......@@ -53,7 +57,7 @@ class SyncWorker:
"""
Init worker
"""
self.__syncing = False
GObject.Object.__init__(self)
self.__sync_cancellable = Gio.Cancellable()
self.__username = ""
self.__password = ""
......@@ -66,10 +70,13 @@ class SyncWorker:
self.__mz = None
self.__state_lock = True
self.__session = None
self.__syncing = False
self.__syncing_pendings = False
try:
self.__pending_records = load(open(EOLIE_DATA_PATH +
"/firefox_sync_pendings.bin",
"rb"))
self.__sync_pendings()
except:
self.__pending_records = {"history": [],
"passwords": [],
......@@ -132,24 +139,35 @@ class SyncWorker:
"""
self.__helper.get_sync(self.__set_credentials)
def sync_loop(self):
def pull_loop(self):
"""
Start syncing every hours
Start pulling every hours
"""
def loop():
self.sync()
self.pull()
return True
self.sync()
self.pull()
GLib.timeout_add_seconds(3600, loop)
def sync(self):
def pull(self, force=False):
"""
Start pulling from Firefox sync
@param force as bool
"""
if Gio.NetworkMonitor.get_default().get_network_available() and\
self.__username:
task_helper = TaskHelper()
task_helper.run(self.__pull, force)
def push(self):
"""
Start syncing
Start pushing to Firefox sync
Will push all data
"""
if Gio.NetworkMonitor.get_default().get_network_available() and\
self.__username and not self.syncing:
self.__username:
task_helper = TaskHelper()
task_helper.run(self.__sync)
task_helper.run(self.__push,)
def push_history(self, history_id):
"""
......@@ -221,7 +239,6 @@ class SyncWorker:
self.__username = ""
self.__password = ""
self.__session = None
self.__sync_cancellable = Gio.Cancellable()
self.__helper.clear_sync(None)
def stop(self, force=False):
......@@ -230,6 +247,7 @@ class SyncWorker:
@param force as bool
"""
self.__sync_cancellable.cancel()
self.__sync_cancellable = Gio.Cancellable()
if force:
self.__session = None
......@@ -246,7 +264,7 @@ class SyncWorker:
@property
def syncing(self):
"""
True if sync is running
True if syncing
@return bool
"""
return self.__syncing
......@@ -327,26 +345,33 @@ class SyncWorker:
"""
Sync pendings record
"""
if Gio.NetworkMonitor.get_default().get_network_available() and\
self.__username and not self.syncing:
self.__syncing = True
self.__check_worker()
bulk_keys = self.__get_session_bulk_keys()
for key in self.__pending_records.keys():
while self.__pending_records[key]:
try:
record = self.__pending_records[key].pop(0)
Logger.sync_debug("syncing %s", record)
self.__firefox_sync.add(record, key, bulk_keys)
except:
self.__pending_records[key].append(record)
self.__syncing = False
self.__update_state()
try:
if Gio.NetworkMonitor.get_default().get_network_available() and\
self.__username and not self.__syncing_pendings:
self.__syncing_pendings = True
Logger.debug("Elements to push to Firefox sync: %s",
self.__pending_records)
self.__check_worker()
bulk_keys = self.__get_session_bulk_keys()
for key in self.__pending_records.keys():
while self.__pending_records[key]:
try:
record = self.__pending_records[key].pop(0)
Logger.sync_debug("syncing %s", record)
self.__firefox_sync.add(record, key, bulk_keys)
except:
self.__pending_records[key].append(record)
self.__update_state()
self.__syncing_pendings = False
except Exception as e:
Logger.error("SyncWorker::__sync_pendings(): %s", e)
self.__syncing_pendings = False
def __push_history(self, history_id):
def __push_history(self, history_id, sync=True):
"""
Push history item
@param history is as int
@param sync as bool
"""
try:
record = {}
......@@ -360,14 +385,16 @@ class SyncWorker:
record["visits"].append({"date": atime * 1000000,
"type": 1})
self.__pending_records["history"].append(record)
self.__sync_pendings()
if sync:
self.__sync_pendings()
except Exception as e:
Logger.error("SyncWorker::__push_history(): %s", e)
def __push_bookmark(self, bookmark_id):
def __push_bookmark(self, bookmark_id, sync=True):
"""
Push bookmark
@param bookmark_id as in
@param bookmark_id as int
@param sync as bool
"""
try:
parent_guid = App().bookmarks.get_parent_guid(bookmark_id)
......@@ -400,12 +427,13 @@ class SyncWorker:
record["title"] = parent_name
record["children"] = children
self.__pending_records["bookmarks"].append(record)
self.__sync_pendings()
if sync:
self.__sync_pendings()
except Exception as e:
Logger.error("SyncWorker::__push_bookmark(): %s", e)
def __push_password(self, user_form_name, user_form_value, pass_form_name,
pass_form_value, uri, form_uri, uuid):
pass_form_value, uri, form_uri, uuid, sync=True):
"""
Push password
@param user_form_name as str
......@@ -414,6 +442,7 @@ class SyncWorker:
@param pass_form_value as str
@param uri as str
@param uuid as str
@param sync as bool
"""
try:
record = {}
......@@ -429,14 +458,16 @@ class SyncWorker:
record["timeCreated"] = mtime
record["timePasswordChanged"] = mtime
self.__pending_records["passwords"].append(record)
self.__sync_pendings()
if sync:
self.__sync_pendings()
except Exception as e:
Logger.error("SyncWorker::__push_password(): %s", e)
def __remove_from_history(self, guid):
def __remove_from_history(self, guid, sync=True):
"""
Remove from history
@param guid as str
@param sync as bool
"""
try:
record = {}
......@@ -444,14 +475,16 @@ class SyncWorker:
record["type"] = "item"
record["deleted"] = True
self.__pending_records["history"].append(record)
self.__sync_pendings()
if sync:
self.__sync_pendings()
except Exception as e:
Logger.sync_debug("SyncWorker::__remove_from_history(): %s", e)
def __remove_from_bookmarks(self, guid):
def __remove_from_bookmarks(self, guid, sync=True):
"""
Remove from history
@param guid as str
@param sync as bool
"""
try:
record = {}
......@@ -459,32 +492,42 @@ class SyncWorker:
record["type"] = "bookmark"
record["deleted"] = True
self.__pending_records["bookmarks"].append(record)
self.__sync_pendings()
if sync:
self.__sync_pendings()
except Exception as e:
Logger.sync_debug("SyncWorker::__remove_from_bookmarks(): %s", e)
def __remove_from_passwords(self, uuid):
def __remove_from_passwords(self, uuid, sync=True):
"""
Remove password from passwords collection
@param uuid as str
@param sync as bool
"""
try:
record = {}
record["id"] = uuid
record["deleted"] = True
self.__pending_records["passwords"].append(record)
self.__sync_pendings()
if sync:
self.__sync_pendings()
except Exception as e:
Logger.sync_debug("SyncWorker::__remove_from_passwords(): %s", e)
def __sync(self):
def __pull(self, force):
"""
Sync Eolie objects (bookmarks, history, ...) with Firefox Sync
Pull bookmarks, history, ... from Firefox Sync
@param force as bool
"""
Logger.sync_debug("Start syncing")
if self.__syncing:
return
Logger.sync_debug("Start pulling")
GLib.idle_add(self.emit, "syncing", True)
self.__syncing = True
self.__sync_cancellable.cancel()
self.__sync_cancellable = Gio.Cancellable()
try:
if force:
raise
self.__mtimes = load(open(EOLIE_DATA_PATH + "/firefox_sync.bin",
"rb"))
except:
......@@ -539,9 +582,51 @@ class SyncWorker:
except:
pass
self.__update_state()
Logger.sync_debug("Stop syncing")
Logger.sync_debug("Stop pulling")
except Exception as e:
Logger.error("SyncWorker::__sync(): %s", e)
Logger.error("SyncWorker::__pull(): %s", e)
GLib.idle_add(self.emit, "syncing", False)
self.__syncing = False
def __push(self):
"""
Push bookmarks, history, ... to Firefox Sync
@param force as bool
"""
if self.__syncing:
return
Logger.sync_debug("Start pushing")
GLib.idle_add(self.emit, "syncing", True)
self.__syncing = True
self.__sync_cancellable.cancel()
self.__sync_cancellable = Gio.Cancellable()
try:
self.__check_worker()
########################
# Passwords Management #
########################
self.__helper.get_all(self.__on_helper_get_all)
self.__check_worker()
######################
# History Management #
######################
for history_id in App().history.get_from_atime(0):
self.__push_history(history_id, False)
self.__check_worker()
########################
# Bookmarks Management #
########################
for (bookmark_id, title, uri) in App().bookmarks.get_bookmarks():
self.__push_bookmark(bookmark_id, False)
self.__check_worker()
self.__sync_pendings()
Logger.sync_debug("Stop pushing")
except Exception as e:
Logger.error("SyncWorker::__push(): %s", e)
GLib.idle_add(self.emit, "syncing", False)
self.__syncing = False
def __pull_bookmarks(self, bulk_keys):
......@@ -738,7 +823,7 @@ class SyncWorker:
self.__token = record["token"]
self.__uid = record["uid"]
self.__keyB = b64decode(record["keyB"])
self.sync()
self.pull()
except Exception as e:
Logger.error("SyncWorker::__set_credentials(): %s" % e)
......@@ -753,6 +838,33 @@ class SyncWorker:
elif not self.__token:
raise StopIteration("SyncWorker: missing token")
def __on_helper_get_all(self, attributes, password, uri, index, count):
"""
Push password
@param attributes as {}
@param password as str
@param uri as None
@param index as int
@param count as int
"""
if attributes is None:
return
try:
self.__check_worker()
user_form_name = attributes["login"]
user_form_value = attributes["userform"]
pass_form_name = attributes["passform"]
pass_form_value = password
uri = attributes["hostname"]
form_uri = attributes["formSubmitURL"]
uuid = attributes["uuid"]
task_helper = TaskHelper()
task_helper.run(self.__push_password,
user_form_name, user_form_value, pass_form_name,
pass_form_value, uri, form_uri, uuid, False)
except Exception as e:
Logger.error("SyncWorker::__on_helper_get_all(): %s" % e)
class FirefoxSync(object):
"""
......
......@@ -38,6 +38,7 @@ class ToolbarMenu(Gtk.PopoverMenu):
self.__toolbar = toolbar
builder = Gtk.Builder()
builder.add_from_resource("/org/gnome/Eolie/ToolbarMenu.ui")
self.__spinner = builder.get_object("spinner")
if not toolbar.home_button.is_visible():
builder.get_object("toolbar_items").show()
fullscreen_button = builder.get_object("fullscreen_button")
......@@ -102,6 +103,9 @@ class ToolbarMenu(Gtk.PopoverMenu):
quit_action.connect("activate", lambda x, y: App().quit())
App().add_action(quit_action)
self.connect("map", self.__on_map)
self.connect("unmap", self.__on_unmap)
#######################
# PROTECTED #
#######################
......@@ -227,3 +231,31 @@ class ToolbarMenu(Gtk.PopoverMenu):
"""
action.set_state(value)
App().settings.set_value("show-sidebar", GLib.Variant("b", value))
def __on_worker_syncing(self, worker, status):
"""
Update sync status
@param worker as SyncWorker
@param status as bool
"""
if status:
self.__spinner.start()
else:
self.__spinner.stop()
def __on_map(self, widget):
"""
Connect signals and update sync status
"""
if App().sync_worker is not None:
App().sync_worker.connect("syncing", self.__on_worker_syncing)
if App().sync_worker.syncing:
self.__spinner.start()
def __on_unmap(self, widget):
"""
Disconnect signals and update sync status