Shared GUI for Bridge and Import/Export

This commit is contained in:
Jakub
2020-05-27 15:58:50 +02:00
committed by Michal Horejsek
parent b598779c0f
commit 49316a935c
96 changed files with 11469 additions and 209 deletions

View File

@ -0,0 +1,6 @@
clean:
rm -f moc.cpp
rm -f moc.go
rm -f moc.h
rm -f moc_cgo*.go
rm -f moc_moc.h

View File

@ -0,0 +1,236 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
import (
"fmt"
"github.com/therecipe/qt/core"
)
// AccountInfo is an element of model. It contains all data for one account and
// it's aliases
type AccountInfo struct {
core.QObject
_ string `property:"account"`
_ string `property:"userID"`
_ string `property:"status"`
_ string `property:"hostname"`
_ string `property:"password"`
_ string `property:"security"`
_ int `property:"portSMTP"`
_ int `property:"portIMAP"`
_ string `property:"aliases"`
_ bool `property:"isExpanded"`
_ bool `property:"isCombinedAddressMode"`
}
// Constants for AccountsModel property map
const (
Account = int(core.Qt__UserRole) + 1<<iota
UserID
Status
Hostname
Password
Security
PortIMAP
PortSMTP
Aliases
IsExpanded
IsCombinedAddressMode
)
// Registration of new metatype before creating instance
// NOTE: check it is run once per program. write a log
func init() {
AccountInfo_QRegisterMetaType()
}
// AccountModel for providing container of accounts information to QML.
// QML ListView connects the model from Go and it shows item (accounts) information.
// Copied and edited from `github.com/therecipe/qt/internal/examples/sailfish/listview`.
type AccountsModel struct {
core.QAbstractListModel
// QtObject Constructor
_ func() `constructor:"init"`
// List of item properties.
// All available item properties are inside the map.
_ map[int]*core.QByteArray `property:"roles"`
// The data storage.
// The slice with all accounts. It is not accessed directly but using `data(index,role)`.
_ []*AccountInfo `property:"accounts"`
// Method for adding account.
_ func(*AccountInfo) `slot:"addAccount"`
// Method for retrieving account.
_ func(row int) *AccountInfo `slot:"get"`
// Method for login/logout the account.
_ func(row int) `slot:"toggleIsAvailable"`
// Method for removing account from list.
_ func(row int) `slot:"removeAccount"`
_ int `property:"count"`
}
// init is called by C constructor. It creates the map for item properties and
// connects the methods.
func (s *AccountsModel) init() {
s.SetRoles(map[int]*core.QByteArray{
Account: NewQByteArrayFromString("account"),
UserID: NewQByteArrayFromString("userID"),
Status: NewQByteArrayFromString("status"),
Hostname: NewQByteArrayFromString("hostname"),
Password: NewQByteArrayFromString("password"),
Security: NewQByteArrayFromString("security"),
PortIMAP: NewQByteArrayFromString("portIMAP"),
PortSMTP: NewQByteArrayFromString("portSMTP"),
Aliases: NewQByteArrayFromString("aliases"),
IsExpanded: NewQByteArrayFromString("isExpanded"),
IsCombinedAddressMode: NewQByteArrayFromString("isCombinedAddressMode"),
})
// Basic QAbstractListModel methods.
s.ConnectData(s.data)
s.ConnectRowCount(s.rowCount)
s.ConnectColumnCount(s.columnCount)
s.ConnectRoleNames(s.roleNames)
// Custom AccountModel methods.
s.ConnectGet(s.get)
s.ConnectAddAccount(s.addAccount)
s.ConnectToggleIsAvailable(s.toggleIsAvailable)
s.ConnectRemoveAccount(s.removeAccount)
}
// get returns account info pointer or create new empy if index is out of
// range.
func (s *AccountsModel) get(index int) *AccountInfo {
if index < 0 || index >= len(s.Accounts()) {
return NewAccountInfo(nil)
}
return s.Accounts()[index]
}
// data return value for index and property
func (s *AccountsModel) data(index *core.QModelIndex, property int) *core.QVariant {
if !index.IsValid() {
return core.NewQVariant()
}
if index.Row() >= len(s.Accounts()) {
return core.NewQVariant()
}
var accountInfo = s.Accounts()[index.Row()]
switch property {
case Account:
return NewQVariantString(accountInfo.Account())
case UserID:
return NewQVariantString(accountInfo.UserID())
case Status:
return NewQVariantString(accountInfo.Status())
case Hostname:
return NewQVariantString(accountInfo.Hostname())
case Password:
return NewQVariantString(accountInfo.Password())
case Security:
return NewQVariantString(accountInfo.Security())
case PortIMAP:
return NewQVariantInt(accountInfo.PortIMAP())
case PortSMTP:
return NewQVariantInt(accountInfo.PortSMTP())
case Aliases:
return NewQVariantString(accountInfo.Aliases())
case IsExpanded:
return NewQVariantBool(accountInfo.IsExpanded())
case IsCombinedAddressMode:
return NewQVariantBool(accountInfo.IsCombinedAddressMode())
default:
return core.NewQVariant()
}
}
// rowCount returns the dimension of model: number of rows is equivalent to number of items in list.
func (s *AccountsModel) rowCount(parent *core.QModelIndex) int {
return len(s.Accounts())
}
// columnCount returns the dimension of model: AccountsModel has only one column.
func (s *AccountsModel) columnCount(parent *core.QModelIndex) int {
return 1
}
// roleNames returns the names of available item properties.
func (s *AccountsModel) roleNames() map[int]*core.QByteArray {
return s.Roles()
}
// addAccount is connected to the addAccount slot.
func (s *AccountsModel) addAccount(accountInfo *AccountInfo) {
s.BeginInsertRows(core.NewQModelIndex(), len(s.Accounts()), len(s.Accounts()))
s.SetAccounts(append(s.Accounts(), accountInfo))
s.SetCount(len(s.Accounts()))
s.EndInsertRows()
}
// toggleIsAvailable is connected to toggleIsAvailable slot.
func (s *AccountsModel) toggleIsAvailable(row int) {
var accountInfo = s.Accounts()[row]
currentStatus := accountInfo.Status()
if currentStatus == "active" {
accountInfo.SetStatus("disabled")
} else if currentStatus == "disabled" {
accountInfo.SetStatus("active")
} else {
accountInfo.SetStatus("error")
}
var pIndex = s.Index(row, 0, core.NewQModelIndex())
s.DataChanged(pIndex, pIndex, []int{Status})
}
// removeAccount is connected to removeAccount slot.
func (s *AccountsModel) removeAccount(row int) {
s.BeginRemoveRows(core.NewQModelIndex(), row, row)
s.SetAccounts(append(s.Accounts()[:row], s.Accounts()[row+1:]...))
s.SetCount(len(s.Accounts()))
s.EndRemoveRows()
}
// Clear removes all items in model.
func (s *AccountsModel) Clear() {
s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Accounts()))
s.SetAccounts(s.Accounts()[0:0])
s.SetCount(len(s.Accounts()))
s.EndRemoveRows()
}
// Dump prints the content of account models to console.
func (s *AccountsModel) Dump() {
fmt.Printf("Dimensions rows %d cols %d\n", s.rowCount(nil), s.columnCount(nil))
for iAcc := 0; iAcc < s.rowCount(nil); iAcc++ {
var accountInfo = s.Accounts()[iAcc]
fmt.Printf(" %d. %s\n", iAcc, accountInfo.Account())
}
}

View File

@ -0,0 +1,259 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
import (
"fmt"
"strings"
"sync"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// QMLer sends signals to GUI
type QMLer interface {
ProcessFinished()
NotifyHasNoKeychain()
SetConnectionStatus(bool)
SetIsRestarting(bool)
SetAddAccountWarning(string, int)
NotifyBubble(int, string)
EmitEvent(string, string)
Quit()
CanNotReachAPI() string
WrongMailboxPassword() string
}
// Accounts holds functionality of users
type Accounts struct {
Model *AccountsModel
qml QMLer
um types.UserManager
prefs *config.Preferences
authClient pmapi.Client
auth *pmapi.Auth
LatestUserID string
accountMutex sync.Mutex
}
// SetupAccounts will create Model and set QMLer and UserManager
func (a *Accounts) SetupAccounts(qml QMLer, um types.UserManager) {
a.Model = NewAccountsModel(nil)
a.qml = qml
a.um = um
}
// LoadAccounts refreshes the current account list in GUI
func (a *Accounts) LoadAccounts() {
a.accountMutex.Lock()
defer a.accountMutex.Unlock()
a.Model.Clear()
users := a.um.GetUsers()
// If there are no active accounts.
if len(users) == 0 {
log.Info("No active accounts")
return
}
for _, user := range users {
accInfo := NewAccountInfo(nil)
username := user.Username()
if username == "" {
username = user.ID()
}
accInfo.SetAccount(username)
// Set status.
if user.IsConnected() {
accInfo.SetStatus("connected")
} else {
accInfo.SetStatus("disconnected")
}
// Set login info.
accInfo.SetUserID(user.ID())
accInfo.SetHostname(bridge.Host)
accInfo.SetPassword(user.GetBridgePassword())
if a.prefs != nil {
accInfo.SetPortIMAP(a.prefs.GetInt(preferences.IMAPPortKey))
accInfo.SetPortSMTP(a.prefs.GetInt(preferences.SMTPPortKey))
}
// Set aliases.
accInfo.SetAliases(strings.Join(user.GetAddresses(), ";"))
accInfo.SetIsExpanded(user.ID() == a.LatestUserID)
accInfo.SetIsCombinedAddressMode(user.IsCombinedAddressMode())
a.Model.addAccount(accInfo)
}
// Updated can clear.
a.LatestUserID = ""
}
// ClearCache signal to remove all DB files
func (a *Accounts) ClearCache() {
defer a.qml.ProcessFinished()
if err := a.um.ClearData(); err != nil {
log.Error("While clearing cache: ", err)
}
// Clearing data removes everything (db, preferences, ...)
// so everything has to be stopped and started again.
a.qml.SetIsRestarting(true)
a.qml.Quit()
}
// ClearKeychain signal remove all accounts from keychains
func (a *Accounts) ClearKeychain() {
defer a.qml.ProcessFinished()
for _, user := range a.um.GetUsers() {
if err := a.um.DeleteUser(user.ID(), false); err != nil {
log.Error("While deleting user: ", err)
if err == keychain.ErrNoKeychainInstalled { // Probably not needed anymore.
a.qml.NotifyHasNoKeychain()
}
}
}
}
// LogoutAccount signal to remove account
func (a *Accounts) LogoutAccount(iAccount int) {
defer a.qml.ProcessFinished()
userID := a.Model.get(iAccount).UserID()
user, err := a.um.GetUser(userID)
if err != nil {
log.Error("While logging out ", userID, ": ", err)
return
}
if err := user.Logout(); err != nil {
log.Error("While logging out ", userID, ": ", err)
}
}
func (a *Accounts) showLoginError(err error, scope string) bool {
if err == nil {
a.qml.SetConnectionStatus(true) // If we are here connection is ok.
return false
}
log.Warnf("%s: %v", scope, err)
if err == pmapi.ErrAPINotReachable {
a.qml.SetConnectionStatus(false)
SendNotification(a.qml, TabAccount, a.qml.CanNotReachAPI())
a.qml.ProcessFinished()
return true
}
a.qml.SetConnectionStatus(true) // If we are here connection is ok.
if err == pmapi.ErrUpgradeApplication {
a.qml.EmitEvent(events.UpgradeApplicationEvent, "")
return true
}
a.qml.SetAddAccountWarning(err.Error(), -1)
return true
}
// Login signal returns:
// -1: when error occurred
// 0: when no 2FA and no MBOX
// 1: when has 2FA
// 2: when has no 2FA but have MBOX
func (a *Accounts) Login(login, password string) int {
var err error
a.authClient, a.auth, err = a.um.Login(login, password)
if a.showLoginError(err, "login") {
return -1
}
if a.auth.HasTwoFactor() {
return 1
}
if a.auth.HasMailboxPassword() {
return 2
}
return 0 // No 2FA, no mailbox password.
}
// Auth2FA returns:
// -1 : error (use SetAddAccountWarning to show message)
// 0 : single password mode
// 1 : two password mode
func (a *Accounts) Auth2FA(twoFacAuth string) int {
var err error
if a.auth == nil || a.authClient == nil {
err = fmt.Errorf("missing authentication in auth2FA %p %p", a.auth, a.authClient)
} else {
_, err = a.authClient.Auth2FA(twoFacAuth, a.auth)
}
if a.showLoginError(err, "auth2FA") {
return -1
}
if a.auth.HasMailboxPassword() {
return 1 // Ask for mailbox password.
}
return 0 // One password.
}
// AddAccount signal to add an account. It should close login modal
// ProcessFinished if ok.
func (a *Accounts) AddAccount(mailboxPassword string) int {
if a.auth == nil || a.authClient == nil {
log.Errorf("Missing authentication in addAccount %p %p", a.auth, a.authClient)
a.qml.SetAddAccountWarning(a.qml.WrongMailboxPassword(), -2)
return -1
}
user, err := a.um.FinishLogin(a.authClient, a.auth, mailboxPassword)
if err != nil {
log.WithError(err).Error("Login was unsuccessful")
a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2)
return -1
}
a.LatestUserID = user.ID()
a.qml.EmitEvent(events.UserRefreshEvent, user.ID())
a.qml.ProcessFinished()
return 0
}
// DeleteAccount by index in Model
func (a *Accounts) DeleteAccount(iAccount int, removePreferences bool) {
defer a.qml.ProcessFinished()
userID := a.Model.get(iAccount).UserID()
if err := a.um.DeleteUser(userID, removePreferences); err != nil {
log.Warn("deleteUser: cannot remove user: ", err)
if err == keychain.ErrNoKeychainInstalled {
a.qml.NotifyHasNoKeychain()
return
}
SendNotification(a.qml, TabSettings, err.Error())
return
}
}

View File

@ -0,0 +1,30 @@
// +build !nogui
#include "common.h"
#include "_cgo_export.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QVector>
#include <QtGlobal>
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_UNUSED( type )
Q_UNUSED( context )
QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE");
logMsgPacked(
const_cast<char*>( (localMsg.constData()) +10 ),
localMsg.size()-10
);
//printf("Handler: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function);
}
void InstallMessageHandler() {
qInstallMessageHandler(messageHandler);
}
void RegisterTypes() {
qRegisterMetaType<QVector<int> >();
}

View File

@ -0,0 +1,130 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
//#include "common.h"
import "C"
import (
"bufio"
"os"
"time"
"github.com/sirupsen/logrus"
"github.com/therecipe/qt/core"
)
var log = logrus.WithField("pkg", "frontend/qt-common")
var logQML = logrus.WithField("pkg", "frontend/qml")
// RegisterTypes for vector of ints
func RegisterTypes() { // need to fix test message
C.RegisterTypes()
}
func installMessageHandler() {
C.InstallMessageHandler()
}
//export logMsgPacked
func logMsgPacked(data *C.char, len C.int) {
logQML.Warn(C.GoStringN(data, len))
}
// QtSetupCoreAndControls hanldes global setup of Qt.
// Should be called once per program. Probably once per thread is fine.
func QtSetupCoreAndControls(programName, programVersion string) {
installMessageHandler()
// Core setup.
core.QCoreApplication_SetApplicationName(programName)
core.QCoreApplication_SetApplicationVersion(programVersion)
// High DPI scaling for windows.
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
// Software OpenGL: to avoid dedicated GPU.
core.QCoreApplication_SetAttribute(core.Qt__AA_UseSoftwareOpenGL, true)
// Basic style for QuickControls2 objects.
//quickcontrols2.QQuickStyle_SetStyle("material")
}
// NewQByteArrayFromString is wrapper for new QByteArray from string
func NewQByteArrayFromString(name string) *core.QByteArray {
return core.NewQByteArray2(name, -1)
}
// NewQVariantString is wrapper for QVariant alocator String
func NewQVariantString(data string) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantStringArray is wrapper for QVariant alocator String Array
func NewQVariantStringArray(data []string) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantBool is wrapper for QVariant alocator Bool
func NewQVariantBool(data bool) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantInt is wrapper for QVariant alocator Int
func NewQVariantInt(data int) *core.QVariant {
return core.NewQVariant1(data)
}
// NewQVariantLong is wrapper for QVariant alocator Int64
func NewQVariantLong(data int64) *core.QVariant {
return core.NewQVariant1(data)
}
// Pause used to show GUI tests
func Pause() {
time.Sleep(500 * time.Millisecond)
}
// Longer pause used to diplay GUI tests
func PauseLong() {
time.Sleep(3 * time.Second)
}
func ParsePMAPIError(err error, code int) error {
/*
if err == pmapi.ErrAPINotReachable {
code = ErrNoInternet
}
return errors.NewFromError(code, err)
*/
return nil
}
// FIXME: Not working in test...
func WaitForEnter() {
log.Print("Press 'Enter' to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
}
type Listener interface {
Add(string, chan<- string)
}
func MakeAndRegisterEvent(eventListener Listener, event string) <-chan string {
ch := make(chan string)
eventListener.Add(event, ch)
return ch
}

View File

@ -0,0 +1,21 @@
#pragma once
#ifndef GO_LOG_H
#define GO_LOG_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif // C++
void InstallMessageHandler();
void RegisterTypes();
;
#ifdef __cplusplus
}
#endif // C++
#endif // LOG

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !nogui
package qtcommon
// Positions of notification bubble
const (
TabAccount = 0
TabSettings = 1
TabHelp = 2
TabQuit = 4
TabUpdates = 100
TabAddAccount = -1
)
// Notifier show bubble notification at postion marked by int
type Notifier interface {
NotifyBubble(int, string)
}
// SendNotification unifies notification in GUI
func SendNotification(qml Notifier, tabIndex int, msg string) {
qml.NotifyBubble(tabIndex, msg)
}

View File

@ -0,0 +1,81 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package qtcommon
import (
"io/ioutil"
"os"
"path/filepath"
)
// PathStatus maps folder properties to flag
type PathStatus int
// Definition of PathStatus flags
const (
PathOK PathStatus = 1 << iota
PathEmptyPath
PathWrongPath
PathNotADir
PathWrongPermissions
PathDirEmpty
)
// CheckPathStatus return PathStatus flag as int
func CheckPathStatus(path string) int {
stat := PathStatus(0)
// path is not empty
if path == "" {
stat |= PathEmptyPath
return int(stat)
}
// is dir
fi, err := os.Lstat(path)
if err != nil {
stat |= PathWrongPath
return int(stat)
}
if fi.IsDir() {
// can open
files, err := ioutil.ReadDir(path)
if err != nil {
stat |= PathWrongPermissions
return int(stat)
}
// empty folder
if len(files) == 0 {
stat |= PathDirEmpty
}
// can write
tmpFile := filepath.Join(path, "tmp")
for err == nil {
tmpFile += "tmp"
_, err = os.Lstat(tmpFile)
}
err = os.Mkdir(tmpFile, 0777)
if err != nil {
stat |= PathWrongPermissions
return int(stat)
}
os.Remove(tmpFile)
} else {
stat |= PathNotADir
}
stat |= PathOK
return int(stat)
}