application/settings.c

Sun, 27 Oct 2024 18:24:37 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 27 Oct 2024 18:24:37 +0100
changeset 60
ee4e4742391e
child 61
eb63af2f2bdd
permissions
-rw-r--r--

add settings window and implement repository editing

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2024 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "settings.h"

#include <cx/list.h>
#include <libidav/utils.h>


#define SETTINGS_STATE_REPOLIST_SELECTED 1

#define SETTINGS_STATE_REPO_ENCRYPTION 20


static void repolist_activate(UiEvent *event, void *userdata) {
    UiListSelection *selection = event->eventdata;
    SettingsWindow *settings = event->window;
    settings_edit_repository(settings, selection->rows[0]);
}

static void repolist_selection(UiEvent *event, void *userdata) {
    UiListSelection *selection = event->eventdata;
    SettingsWindow *settings = event->window;
    if(selection->count > 0) {
        ui_set_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
        settings->selected_repo = selection->rows[0];
    } else {
        ui_unset_group(event->obj->ctx, SETTINGS_STATE_REPOLIST_SELECTED);
    }
}

static void repolist_edit(UiEvent *event, void *userdata) {
    SettingsWindow *settings = event->window;
    if(settings->selected_repo >= 0) {
        settings_edit_repository(settings, settings->selected_repo);
    }
}

static void repolist_remove(UiEvent *event, void *userdata) {
    // TODO
}

static void editrepo_go_back(UiEvent *event, void *userdata) {
    SettingsWindow *settings = event->window;
    settings_store_repository(settings);
    ui_set(settings->repo_tabview, 0);
}


void settings_ok(UiEvent *event, void *userdata) {
    SettingsWindow *settings = event->window;
    settings->config_saved = TRUE;
    // save any changed settings
    settings_store_repository(settings);
    
    set_config(settings->config); // TODO: free old config
    if(store_config()) {
        ui_dialog(event->obj, .title = "Error", .content = "Cannot store settings", .closebutton_label = "OK");
    }
    application_update_repolist(get_application());
    ui_close(event->obj);
}

void settings_cancel(UiEvent *event, void *userdata) {
    SettingsWindow *settings = event->window;
    if(settings->config_saved) {
        // function was called as context closefunc by ui_close
        // in settings_ok
        // don't free anything
        return;
    }
    dav_config_free(settings->config);
    if(userdata) {
        ui_close(event->obj);
    }
}

void settings_window_open() {
    DavConfig *config = load_config_file();
    if(!config) {
        return;
    }
    
    UiObject *obj = ui_simple_window("Settings", NULL);
    ui_context_closefunc(obj->ctx, settings_cancel, NULL);
    SettingsWindow *wdata = ui_malloc(obj->ctx, sizeof(SettingsWindow));
    wdata->config = config;
    obj->window = wdata;
    settings_init(obj, wdata);
    
    ui_tabview(obj) {
        ui_tab(obj, "General") {
            ui_grid(obj, .margin = 10) {
                ui_label(obj, .label = "TODO");
            }
        }
        
        ui_tab(obj, "Repositories") {
            
            ui_tabview(obj, .value = wdata->repo_tabview, .margin = 10, .tabview = UI_TABVIEW_INVISIBLE) {
                ui_tab(obj, "list") {
                    ui_grid(obj, .margin = 10, .columnspacing = 10, .rowspacing = 10) {
                        ui_hbox(obj, .spacing = 10) {
                            ui_button(obj, .label = "Add");
                            ui_button(obj, .label = "Edit", .onclick = repolist_edit, .groups = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
                            ui_button(obj, .label = "Remove", .groups = UI_GROUPS(SETTINGS_STATE_REPOLIST_SELECTED));
                        }
                        ui_newline(obj);
                        
                        UiModel* model = ui_model(obj->ctx, UI_STRING, "Name", UI_STRING, "URL", UI_STRING, "User", UI_STRING, "Encrypted", -1);
                        model->getvalue = (ui_getvaluefunc) settings_repolist_getvalue;
                        ui_table(obj,
                                .model = model,
                                .list = wdata->repos,
                                .multiselection = FALSE,
                                .onactivate = repolist_activate,
                                .onselection = repolist_selection,
                                .vexpand = TRUE, .hexpand = TRUE, .colspan = 3);
                    }
                }
                
                ui_tab(obj, "repo") {
                    ui_vbox(obj, .margin = 10, .spacing = 10) {
                        ui_hbox(obj, .fill = UI_OFF, .spacing = 4) {
                            ui_button(obj, .icon = UI_ICON_GO_BACK, .onclick = editrepo_go_back);
                            ui_label(obj, .label = "Repository List");
                        }
                        
                        ui_scrolledwindow(obj, .hexpand = TRUE, .vexpand = TRUE, .subcontainer = UI_CONTAINER_NO_SUB) {
                            ui_grid(obj, .margin = 10, .columnspacing = 10, .rowspacing = 10) {
                                ui_llabel(obj, .label = "Name");
                                ui_hbox0(obj) {
                                    ui_textfield(obj, .value = wdata->repo_name, .width = 15);
                                }
                                ui_newline(obj);
                                ui_llabel(obj, .label = "URL");
                                ui_textfield(obj, .value = wdata->repo_url, .hexpand = TRUE);
                                ui_newline(obj);
                                
                                ui_llabel(obj, .label = "Credentials", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
                                ui_newline(obj);
                                ui_hbox(obj, .spacing = 4, .colspan = 2) {
                                    ui_combobox(obj, .list = wdata->repo_credentials);
                                    ui_hbox0(obj) {
                                        ui_button(obj, .label = "New Credentials");
                                    }
                                }
                                ui_newline(obj);
                                ui_expander(obj, .spacing = 10, .colspan = 2, .label = "Unencrypted User/Password", .margin = 10) {
                                    ui_llabel(obj, .label = "Store the credentials unencrypted in the repository and not in the secret store", .style = UI_LABEL_STYLE_DIM);
                                    ui_grid(obj, .rowspacing = 10, .columnspacing = 10, .fill = UI_OFF) {
                                        ui_llabel(obj, .label = "User");
                                        ui_textfield(obj, .value = wdata->repo_user, .width = 15);
                                        ui_newline(obj);
                                        
                                        ui_llabel(obj, .label = "Password");
                                        ui_passwordfield(obj, .value = wdata->repo_password, .width = 15);
                                    }
                                }
                                ui_newline(obj);
                                
                                ui_llabel(obj, .label = "Encryption", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
                                ui_newline(obj);
                                
                                ui_checkbox(obj, .label = "Enable client-side encryption", .value = wdata->repo_encryption, .colspan = 2, .enable_group = SETTINGS_STATE_REPO_ENCRYPTION);
                                ui_newline(obj);
                                ui_llabel(obj, .label = "Default key");
                                ui_hbox(obj, .spacing = 4) {
                                    ui_combobox(obj, .list = wdata->repo_keys, .groups = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
                                    ui_button(obj, .label = "Generate Key", .groups = UI_GROUPS(SETTINGS_STATE_REPO_ENCRYPTION));
                                }
                                ui_newline(obj);
                                
                                ui_llabel(obj, .label = "TLS", .style = UI_LABEL_STYLE_TITLE, .colspan = 2);
                                ui_newline(obj);
                                
                                ui_llabel(obj, .label = "Cert Path");
                                ui_hbox0(obj) {
                                    ui_textfield(obj, .value = wdata->repo_cacert, .width = 15);
                                }
                                ui_newline(obj);
                                
                                ui_llabel(obj, .label = "TLS Version");
                                ui_hbox0(obj) {
                                    ui_combobox(obj, .list = wdata->repo_tls_versions);
                                }
                                ui_newline(obj);
                                ui_checkbox(obj, .label = "Disable TLS verification", .value = wdata->repo_disable_verification, .colspan = 2);
                            }
                        }
                    }
                }
            }
            
            
        }
        
        ui_tab(obj, "Sync Directories") {
            ui_grid(obj, .margin = 10) {
                ui_label(obj, .label = "TODO");
            }
        }
        
        ui_tab(obj, "Credentials") {
            ui_grid(obj, .margin = 10) {
                ui_label(obj, .label = "TODO");
            }
        }
        
        ui_tab(obj, "Keys") {
            ui_grid(obj, .margin = 10) {
                ui_label(obj, .label = "TODO");
            }
        }
        
        ui_tab(obj, "Properties") {
            ui_grid(obj, .margin = 10) {
                ui_label(obj, .label = "TODO");
            }
        }
    }
    
    ui_hbox(obj, .fill = UI_OFF, .margin = 10) {
        ui_button(obj, .label = "Cancel", .onclick = settings_cancel, .onclickdata = "close");
        ui_label(obj, .fill = UI_ON);
        ui_button(obj, .label = "Save", .onclick = settings_ok);
    }
    
    
    ui_show(obj);
}

void settings_init(UiObject *obj, SettingsWindow *settings) {
    settings->repos = ui_list_new(obj->ctx, NULL);
    settings->repo_tabview = ui_int_new(obj->ctx, NULL);
    
    settings->repo_name = ui_string_new(obj->ctx, NULL);
    settings->repo_url = ui_string_new(obj->ctx, NULL);
    settings->repo_user = ui_string_new(obj->ctx, NULL);
    settings->repo_password = ui_string_new(obj->ctx, NULL);
    settings->repo_cacert = ui_string_new(obj->ctx, NULL);
    settings->repo_credentials = ui_list_new(obj->ctx, NULL);
    settings->repo_keys = ui_list_new(obj->ctx, NULL);
    settings->repo_tls_versions = ui_list_new(obj->ctx, NULL);
    settings->repo_encryption = ui_int_new(obj->ctx, NULL);
    settings->repo_disable_verification = ui_int_new(obj->ctx, NULL);
    
    ui_list_append(settings->repo_tls_versions, "Default");
    ui_list_append(settings->repo_tls_versions, "TLSv1.3");
    ui_list_append(settings->repo_tls_versions, "TLSv1.2");
    ui_list_append(settings->repo_tls_versions, "TLSv1.1");
    ui_list_append(settings->repo_tls_versions, "TLSv1.0");
    
    // load some list values, that can be reused
    settings_update_repolist(settings);
    settings_reload_keys(settings);
    settings_reload_credentials(settings);
    
    settings->selected_repo = -1;
}

#define SETTINGS_SET_STRING(str, setting) if(setting.value.ptr) ui_set(str, setting.value.ptr);

void settings_edit_repository(SettingsWindow *settings, int repo_index) {
    DavCfgRepository *repo = ui_list_get(settings->repos, repo_index);
    if(!repo) {
        fprintf(stderr, "Error: cannot get repository at index %d\n", repo_index);
        return;
    }
    settings->selected_repo = repo_index;
    
    // load plain string values
    SETTINGS_SET_STRING(settings->repo_name, repo->name);
    SETTINGS_SET_STRING(settings->repo_url, repo->url);
    SETTINGS_SET_STRING(settings->repo_cacert, repo->cert);
    SETTINGS_SET_STRING(settings->repo_user, repo->user);
    // load decrypted password
    if(repo->password.value.ptr) {
        char *decoded_pw = util_base64decode(repo->password.value.ptr);
        ui_set(settings->repo_password, decoded_pw);
        size_t decoded_pw_len = strlen(decoded_pw);
        memset(decoded_pw, 0, decoded_pw_len);
        free(decoded_pw);
    }
    
    // select credentials dropdown value
    CxList *cred = settings->repo_credentials->data;
    cred->collection.cmpfunc = (cx_compare_func)strcmp;
    ssize_t cred_index = repo->stored_user.value.ptr ? cxListFind(cred, repo->stored_user.value.ptr) : 0;
    if(cred_index > 0) {
        ui_list_setselection(settings->repo_credentials, cred_index);
    } else {
        ui_list_setselection(settings->repo_credentials, 0);
    }
    
    // load encryption value and default key value
    ui_set(settings->repo_encryption, repo->full_encryption.value);
    CxList *keys = settings->repo_keys->data;
    keys->collection.cmpfunc = (cx_compare_func)strcmp;
    ssize_t key_index = repo->default_key.value.ptr ? cxListFind(keys, repo->default_key.value.ptr) : 0;
    if(key_index > 0) {
        ui_list_setselection(settings->repo_keys, key_index);
    } else {
        ui_list_setselection(settings->repo_keys, 0);
    }
    
    // select tls version from dropdown menu
    CxList *tlsVersions = settings->repo_tls_versions->data;
    tlsVersions->collection.cmpfunc = (cx_compare_func)strcmp;
    const char *tls_str = dav_tlsversion2str(repo->ssl_version.value);
    if(!tls_str) tls_str = "";
    ssize_t tlsv_index = cxListFind(tlsVersions, tls_str);
    if(tlsv_index > 0) {
        ui_list_setselection(settings->repo_tls_versions, tlsv_index);
    } else {
        ui_list_setselection(settings->repo_tls_versions, 0);
    }
    
    if(!repo->verification.value) {
        ui_set(settings->repo_disable_verification, TRUE);
    }
    
    
    // switch to editing tab
    ui_set(settings->repo_tabview, 1);
}

/*
 * sets a config value
 * if new_value is null, the xml node is removed
 */
static void cfg_string_set_value_or_remove(DavConfig *config, CfgString *str, xmlNode *parent, cxstring new_value, const char *nodename) {
    if(new_value.length == 0) {
        new_value.ptr = NULL;
    }
    dav_cfg_string_set_value(config, str, parent, new_value, nodename);
    if(!new_value.ptr) {
        dav_cfg_string_remove(str);
    }
}

/*
 * return the selected list value, if it is not the default value at index 0
 */
static cxstring default_list_get_value(UiList *list) {
    cxstring ret = { NULL, 0 };
    UiListSelection sel = ui_list_getselection(list);
    if(sel.count > 0) {
        int index = sel.rows[0];
        if(index > 0) {
            ret = cx_str(ui_list_get(list, index));
        }        
        free(sel.rows);
    }
    return ret;
}

void settings_store_repository(SettingsWindow *settings) {
    DavConfig *config = settings->config;
    DavCfgRepository *repo = ui_list_get(settings->repos, settings->selected_repo);
    if(!repo) {
        fprintf(stderr, "Error: cannot get repository at index %d\n", settings->selected_repo);
        return;
    }
    
    // always store name/url nodes
    dav_cfg_string_set_value(config, &repo->name, repo->node, cx_str(ui_get(settings->repo_name)), "name");
    dav_cfg_string_set_value(config, &repo->url, repo->node, cx_str(ui_get(settings->repo_url)), "url");
    // store user of remove node, if no user is configured
    cfg_string_set_value_or_remove(config, &repo->user, repo->node, cx_str(ui_get(settings->repo_user)), "user");
    // store cert or remove node
    cfg_string_set_value_or_remove(config, &repo->cert, repo->node, cx_str(ui_get(settings->repo_cacert)), "cert");
    
    
    // store password or remove node, if no password is configured
    char *pw = ui_get(settings->repo_password);
    size_t pwlen = strlen(pw);
    if(pwlen > 0) {
        char *pwenc = util_base64encode(pw, pwlen);
        memset(pw, 0, pwlen);
        dav_cfg_string_set_value(config, &repo->password, repo->node, cx_str(pwenc), "password");
        free(pwenc);
    } else {
        // set password 0 NULL and remove password config node
        cfg_string_set_value_or_remove(config, &repo->password, repo->node, cx_strn(NULL, 0), "password");
    }
    
    // get the selected credentials and set "stored-user"
    // remove node if not needed
    cxstring stored_user = default_list_get_value(settings->repo_credentials);
    cfg_string_set_value_or_remove(config, &repo->stored_user, repo->node, stored_user, "stored-user");
    
    // adjust full-encryption node if necessary
    int encryption = ui_get(settings->repo_encryption);
    if(encryption || repo->full_encryption.node) {
        dav_cfg_bool_set_value(config, &repo->full_encryption, repo->node, encryption, "full-encryption");
    }
    
    // set default-key
    cxstring key = default_list_get_value(settings->repo_keys);
    cfg_string_set_value_or_remove(config, &repo->default_key, repo->node, key, "default-key");
    
    // if disable_verification is enabled, set repo verification to false
    // otherwise remove the node, because verification is enabled by default
    int disable_verification = ui_get(settings->repo_disable_verification);
    if(disable_verification) {
        dav_cfg_bool_set_value(config, &repo->verification, repo->node, !disable_verification, "verification");
    } else {
        dav_cfg_bool_remove(&repo->verification);
    }
    
    // set tls version if configured
    cxstring tlsversion_str = default_list_get_value(settings->repo_tls_versions);
    int tlsversion = dav_str2ssl_version(tlsversion_str.ptr);
    if(tlsversion >= 0) {
        dav_cfg_int_set_value(config, &repo->ssl_version, repo->node, tlsversion, "ssl-version");
    } else {
        dav_cfg_int_remove(&repo->ssl_version);
    }
    
    settings->selected_repo = -1;
}

void settings_update_repolist(SettingsWindow *settings) {
    DavConfig *config = settings->config;

    ui_list_clear(settings->repos);

    for (DavCfgRepository *repo = config->repositories; repo; repo = repo->next) {
        ui_list_append(settings->repos, repo);
    }
}

void* settings_repolist_getvalue(DavCfgRepository *repo, int col) {
    switch(col) {
        case 0: {
            return repo->name.value.ptr;
        }
        case 1: {
            return repo->url.value.ptr;
        }
        case 2: {
            return repo->user.value.ptr;
        }
        case 3: {
            return repo->full_encryption.value ? "yes" : "no";
        }
    }
    return NULL;
}

void settings_reload_keys(SettingsWindow *settings) {
    DavConfig *config = settings->config;
    DavCfgKey *key = config->keys; 
    ui_list_clear(settings->repo_keys);
    ui_list_append(settings->repo_keys, "-");
    while(key) {
        if(key->name.value.ptr) {
            ui_list_append(settings->repo_keys, key->name.value.ptr);
        }
        key = key->next;
    }
    if(settings->repo_keys->update) {
        ui_list_update(settings->repo_keys);
    }
}

const char* dav_tlsversion2str(int value) {
    if(value == CURL_SSLVERSION_TLSv1) {
        return "TLSv1";
    } else if(value == CURL_SSLVERSION_SSLv2) {
        return "SSLv2";
    } else if(value == CURL_SSLVERSION_SSLv3) {
        return "SSLv3";
    }
#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7034
    else if(value == CURL_SSLVERSION_TLSv1_0) {
        return "TLSv1.0";
    } else if(value == CURL_SSLVERSION_TLSv1_1) {
        return "TLSv1.1";
    } else if(value == CURL_SSLVERSION_TLSv1_2) {
        return "TLSv1.2";
    }
#endif
#if LIBCURL_VERSION_MAJOR * 1000 + LIBCURL_VERSION_MINOR >= 7052
    else if(value == CURL_SSLVERSION_TLSv1_3) {
        return "TLSv1.3";
    }
#endif
    return NULL;
}

void settings_reload_credentials(SettingsWindow *settings) {
    PwdStore *pwd = get_pwdstore();
    if(!pwd) {
        return;
    }
    
    ui_list_clear(settings->repo_credentials);
    ui_list_append(settings->repo_credentials, "-");
    
    CxIterator i = cxListIterator(pwd->noloc);
    cx_foreach(PwdIndexEntry*, entry, i) {
        ui_list_append(settings->repo_credentials, entry->id);
    }
    
    if(settings->repo_credentials->update) {
        ui_list_update(settings->repo_credentials);
    }
}

mercurial