application/davcontroller.c

Wed, 27 Nov 2024 16:48:59 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 27 Nov 2024 16:48:59 +0100
changeset 93
5ee236155955
parent 91
7ee944cf53de
child 95
e92c72705da4
permissions
-rw-r--r--

use repo encryption and SSL settings, resolves #516

/*
* 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 "davcontroller.h"
#include "window.h"

#include <cx/printf.h>

#include "config.h"
#include "upload.h"
#include "download.h"

#include "system.h"
#include "common/context.h"

#include <libidav/config.h>
#include <libidav/utils.h>

DavBrowser* davbrowser_create(UiObject *toplevel) {
    DavBrowser *doc = ui_document_new(sizeof(DavBrowser));
    UiContext *ctx = ui_document_context(doc);
    doc->ctx = ctx;

    doc->navigation_stack = cxLinkedListCreateSimple(CX_STORE_POINTERS);
    doc->navstack_enabled = true;
    doc->navstack_pos = 0;

    doc->dav_queue = ui_threadpool_create(1);
    
    doc->res_open_inprogress = cxHashMapCreate(ctx->allocator, CX_STORE_POINTERS, 4);

    doc->path = ui_string_new(ctx, "path");
    doc->resources = ui_list_new(ctx, "reslist");

    return doc;
}


void davbrowser_set_collection(UiObject *ui, DavBrowser *browser, DavResource *collection) {
    if (browser->current) {
        dav_resource_free_all(browser->current);
    }
    ui_list_clear(browser->resources);

    browser->current = collection;
    browser->res_counter++;
    for (DavResource *res = collection->children; res; res = res->next) {
        ui_list_append(browser->resources, res);
    }

    browser->resources->update(browser->resources, 0);
    
    ui_set_group(ui->ctx, APP_STATE_BROWSER_SESSION);
}

// ------------------------------ davbrowser_connect2repo ------------------------------

typedef struct DavConnect2Repo {
    UiObject *ui;
    DavBrowser *browser;
    
    DavCfgRepository *repo;
    char *path;
    
    UiString *password;
} DavConnect2Repo;

static void dialog_secretstore_decrypt(UiEvent *event, void *data) {
    DavConnect2Repo *c2r = event->window;
    
    if(event->intval == 4) {
        char *pw = ui_get(c2r->password);
        
        PwdStore *secrets = get_pwdstore();
        pwdstore_setpassword(secrets, pw);
        if(pwdstore_decrypt(secrets)) {
            ui_dialog(c2r->ui, .title = "Error", .content = "Cannot decrypt Secret Store", .closebutton_label = "OK");
        } else {
            davbrowser_connect2repo(c2r->ui, c2r->browser, c2r->repo, c2r->path);
        }
    }
    free(c2r->path);
    if(!c2r->repo->node) {
        dav_repository_free(get_config(), c2r->repo);
    }
    
    ui_close(event->obj);
}

int davbrowser_connect2repo(UiObject *ui, DavBrowser *browser, DavCfgRepository *repo, const char *path) {
    char *user = NULL;
    char *password = NULL;
    char *password_free = NULL;
    if (repo->user.value.ptr && repo->password.value.ptr) {
        user = repo->user.value.ptr;
        cxmutstr decodedpw = dav_repository_get_decodedpassword(repo);
        password = decodedpw.ptr;
        password_free = decodedpw.ptr;
    } else {
        PwdStore *secrets = get_pwdstore();
        const char *credentials = NULL;
        if(repo->stored_user.value.ptr) {
            if(pwdstore_has_id(secrets, repo->stored_user.value.ptr)) {
                credentials = repo->stored_user.value.ptr;
            }
        } else {
            credentials = get_location_credentials(repo, path);
        }
        
        if(credentials) {
            if(!secrets->isdecrypted) {
                UiObject *obj = ui_dialog_window(ui,
                        .title = "Authentication", 
                        .lbutton1 = "Cancel",
                        .rbutton4 = "Connect",
                        .default_button = 4,
                        .show_closebutton = UI_OFF,
                        .onclick = dialog_secretstore_decrypt);
                
                DavConnect2Repo *c2r = ui_malloc(obj->ctx, sizeof(DavConnect2Repo));
                c2r->ui = ui;
                c2r->browser = browser;
                c2r->repo = repo;
                c2r->path = path ? strdup(path) : NULL;
                c2r->password = ui_string_new(obj->ctx, NULL);
                obj->window = c2r;
                
                ui_grid(obj, .margin = 20, .columnspacing = 12, .rowspacing = 16) {
                    ui_llabel(obj, .label = "Decrypt Secret Store", .colspan = 2);
                    ui_newline(obj);

                    ui_llabel(obj, .label = "Password");
                    ui_passwordfield(obj, .value = c2r->password, .hexpand = TRUE);
                }
                
                ui_show(obj);
                
                return 1;
            }
            
            if(!get_stored_credentials(credentials, &user, &password)) {
                fprintf(stderr, "Error: failed to get user/password for credentials %s\n", credentials);
            }
        }
    }
    
    DavSession *sn = dav_session_new(application_dav_context(), repo->url.value.ptr);
    if (user && password) {
        dav_session_set_auth(sn, user, password);
    }
    free(password_free);
    
    sn->flags = dav_repository_get_flags(repo);
    sn->key = dav_context_get_key(application_dav_context(), repo->default_key.value.ptr);
    curl_easy_setopt(sn->handle, CURLOPT_SSLVERSION, repo->ssl_version);
    if(repo->cert.value.ptr) {
        curl_easy_setopt(sn->handle, CURLOPT_CAINFO, repo->cert.value.ptr);
    }
    if(!repo->verification.value) {
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYPEER, 0);
        curl_easy_setopt(sn->handle, CURLOPT_SSL_VERIFYHOST, 0);
    }
    
    
    browser->sn = sn;
    if (repo->name.value.length > 0) {
        browser->repo_base = cx_strdup(cx_strn(repo->name.value.ptr, repo->name.value.length)).ptr;
    } else {
        browser->repo_base = cx_strdup(cx_strn(repo->url.value.ptr, repo->url.value.length)).ptr;
    }

    char *dav_path = util_concat_path(browser->repo_base, path);
    ui_set(browser->path, dav_path);
    free(dav_path);
    
    SessionAuthData *auth = cxMalloc(sn->mp->allocator, sizeof(SessionAuthData));
    auth->obj = ui;
    auth->cond = ui_condvar_create();
    auth->sn = sn;
    auth->user = repo->user.value.ptr ? cx_strdup_a(sn->mp->allocator, cx_strcast(repo->user.value)).ptr : NULL;
    auth->password = NULL;
    dav_session_set_authcallback(sn, jobthr_davbrowser_auth, auth);
    
    davbrowser_query_path(ui, browser, path);
    
    return 0;
}

// ------------------------------ davbrowser_auth ------------------------------

static int davbrowser_auth_dialog(void *data) {
    SessionAuthData *auth = data;
    auth_dialog(auth);
    return 0;
}

void davbrowser_auth_set_user_pwd(SessionAuthData *auth, const char *user, const char *password) {
    dav_session_free(auth->sn, auth->user);
    dav_session_free(auth->sn, auth->password);
    auth->user = user ? dav_session_strdup(auth->sn, user) : NULL;
    auth->password = password ? dav_session_strdup(auth->sn, password) : NULL;
}

int jobthr_davbrowser_auth(DavSession *sn, void *data) {
    SessionAuthData *auth = data;
    
    ui_call_mainthread(davbrowser_auth_dialog, auth);
    ui_condvar_wait(auth->cond);
    
    if(auth->cond->intdata) {
        dav_session_set_auth(sn, auth->user, auth->password);
    }
    dav_session_free(auth->sn, auth->password);
    auth->password = NULL;
    
    return 0;
}


// ------------------------------ davbrowser_query_path ------------------------------

typedef struct DavBrowserQueryPath {
    UiThreadpool *pool;
    DavBrowser *browser;
    char *path;
    DavResource *result;
} DavBrowserQueryPath;

static int browser_query_path(void *data) {
    DavBrowserQueryPath *query = data;
    DavSession *sn = query->browser->sn;

    DavResource *res = dav_query(sn, "select `idav:crypto-name`,`idav:crypto-key`,D:lockdiscovery from %s with depth = 1 order by iscollection desc, name", query->path);
    query->result = res;

    return 0;
}

static void browser_query_finished(UiEvent *event, void *data) {
    DavBrowserQueryPath *query = data;
    DavBrowser *browser = event->document;

    if (query->pool == browser->dav_queue) {
        if (query->result) {
            davbrowser_set_collection(event->obj, browser, query->result);
        } else {
            cxmutstr error = cx_asprintf("Error %d", query->browser->sn->error);
            ui_dialog(event->obj, .title = "Error", .content = error.ptr, .closebutton_label = "OK");
        }

        window_progress(event->window, 0);
    } else {
        // operation aborted
        if (query->result) {
            dav_resource_free_all(query->result);
        }
    }

    free(query->path);
    free(query);
}

void davbrowser_query_path(UiObject *ui, DavBrowser *browser, const char *path) {
    if (!browser->sn) {
        // TODO: error
        return;
    }

    // for comparison, we need the current base_url/repo_name + path
    size_t len = path ? strlen(path) : 0;
    if (len == 1 && *path == '/') {
        path = "";
    }
    
    // check if the new path is a prefix of the current path
    // if not, we have to set the pathbar string to the new path
    char *full_path = util_concat_path(browser->repo_base, path);
    char *full_path_col = util_concat_path(full_path, "/");
    char *current_path = ui_get(browser->path);
    cxstring cpath = cx_str(current_path);
    cxstring newc = cx_str(full_path_col);
    if (!cx_strprefix(cpath, newc)) {
        ui_set(browser->path, full_path);
    }
    free(full_path);
    free(full_path_col);

    DavBrowserQueryPath *query = malloc(sizeof(DavBrowserQueryPath));
    query->pool = browser->dav_queue;
    query->browser = browser;
    query->path = strdup(path);
    query->result = NULL;
    ui_threadpool_job(browser->dav_queue, ui, browser_query_path, query, browser_query_finished, query);

    window_progress(ui->window, 1);

    davbrowser_add2navstack(browser, browser->repo_base, path);
}

void davbrowser_query_url(UiObject *ui, DavBrowser *browser, const char *url) {
    if (browser->repo_base) {
        cxstring base = cx_str(browser->repo_base);
        cxstring newurl = cx_str(url);

        if (cx_strprefix(newurl, base)) {
            const char *path = url + base.length;
            davbrowser_query_path(ui, browser, path);
            return;
        }
    }

    char *path = NULL;
    DavCfgRepository *repo = dav_config_url2repo(get_config(), url, &path);
    
    int ret = davbrowser_connect2repo(ui, browser, repo, path);
    free(path);
    if(ret) {
        return;
    }

    if (!repo->node) {
        dav_repository_free(get_config(), repo);
    }
}

void davbrowser_open_resource(UiObject *ui, DavBrowser *browser, DavResource *res, const char *contenttype) {
    DavResourceViewType type = DAV_RESOURCE_VIEW_PROPERTIES;
    if(!contenttype) {
        contenttype = res->contenttype;
    }
    
    if(res->iscollection) {
        // default type
    } else if(contenttype) {
        cxstring ctype = cx_str(contenttype);
        if(cx_strprefix(ctype, CX_STR("text/"))) {
            type = DAV_RESOURCE_VIEW_TEXT;
        } else if(cx_strprefix(ctype, CX_STR("image/"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strprefix(ctype, CX_STR("application/"))) {
            if(cx_strsuffix(ctype, CX_STR("json"))) {
                type = DAV_RESOURCE_VIEW_TEXT;
            } else if(cx_strsuffix(ctype, CX_STR("/xml"))) {
                type = DAV_RESOURCE_VIEW_TEXT;
            } else if(cx_strsuffix(ctype, CX_STR("+xml"))) {
                type = DAV_RESOURCE_VIEW_TEXT;
            } else if(cx_strsuffix(ctype, CX_STR("/xml"))) {
                type = DAV_RESOURCE_VIEW_TEXT;
            }
        }
    } else {
        cxstring path = cx_str(res->path);
        if(cx_strsuffix(path, CX_STR(".png"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".jpg"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".jpeg"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".tif"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".tiff"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".webp"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".bmp"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".gif"))) {
            type = DAV_RESOURCE_VIEW_IMAGE;
        } else if(cx_strsuffix(path, CX_STR(".txt"))) {
            type = DAV_RESOURCE_VIEW_TEXT;
        } else if(cx_strsuffix(path, CX_STR(".md"))) {
            type = DAV_RESOURCE_VIEW_TEXT;
        } else if(cx_strsuffix(path, CX_STR(".xml"))) {
            type = DAV_RESOURCE_VIEW_TEXT;
        }
    }
    
    resourceviewer_new(browser, res->path, type);
}

void davbrowser_add2navstack(DavBrowser *browser, const char *base, const char *path) {
    if (browser->navstack_enabled) {
        for (int i = 0; i < browser->navstack_pos; i++) {
            char *nav_url = cxListAt(browser->navigation_stack, 0);
            cxListRemove(browser->navigation_stack, 0);
            free(nav_url);
        }
        browser->navstack_pos = 0;

        char *nav_url = util_concat_path(base, path);
        cxListInsert(browser->navigation_stack, 0, nav_url);

        if (cxListSize(browser->navigation_stack) > DAVBROWSER_MAX_NAVLIST) {
            char *nav = cxListAt(browser->navigation_stack, cxListSize(browser->navigation_stack) - 1);
            free(nav);
            cxListRemove(browser->navigation_stack, cxListSize(browser->navigation_stack) - 1);
        }
    }
}

void davbrowser_navigation_parent(UiObject *ui, DavBrowser *browser) {
    if(browser->current) {
        char *parent = util_parent_path(browser->current->path);
        if(strlen(parent) > 0) {
            davbrowser_query_path(ui, browser, parent);
        }
        free(parent);
    }
}

void davbrowser_navigation_back(UiObject *ui, DavBrowser *browser) {
    if (browser->navstack_pos+1 < cxListSize(browser->navigation_stack)) {
        browser->navstack_pos++;
        char *nav_url = cxListAt(browser->navigation_stack, browser->navstack_pos);
        browser->navstack_enabled = false;
        davbrowser_query_url(ui, browser, nav_url);
        browser->navstack_enabled = true;
        ui_set(browser->path, nav_url);
    }
}

void davbrowser_navigation_forward(UiObject *ui, DavBrowser *browser) {
    if (browser->navstack_pos > 0) {
        browser->navstack_pos--;
        char *nav_url = cxListAt(browser->navigation_stack, browser->navstack_pos);
        browser->navstack_enabled = false;
        davbrowser_query_url(ui, browser, nav_url);
        browser->navstack_enabled = true;
        ui_set(browser->path, nav_url);
    }
}


void davbrowser_upload_files(UiObject *ui, DavBrowser *browser, UiFileList files) {
    if (!browser->sn) {
        return; // TODO: error msg
    }
    
    cxmutstr wtitle = cx_asprintf("Upload to: %s", ui_get(browser->path));
    UiObject *dialog = ui_simple_window(wtitle.ptr, NULL);
    free(wtitle.ptr);

    DavFileUpload *upload = dav_upload_create(browser, dialog, files);
    transfer_window_init(dialog, action_upload_cancel);
    dav_upload_start(upload);
}

void davbrowser_download(UiObject *ui, DavBrowser *browser, DavResource *reslist, const char *local_path) {
    cxmutstr wtitle = cx_asprintf("Download to: %s", local_path);
    UiObject *dialog = ui_simple_window(wtitle.ptr, NULL);
    free(wtitle.ptr);
    
    DavFileDownload *download = dav_download_create(browser, dialog, reslist, local_path);
    transfer_window_init(dialog, action_download_cancel);
    dav_download_start(download);
}


// ------------------------------------- Path Operation (DELETE, MKCOL) -------------------------------------

enum DavPathOpType {
    DAV_PATH_OP_DELETE = 0,
    DAV_PATH_OP_CREATE
};

typedef struct DavPathOp {
    UiObject *ui;
    DavBrowser *browser;

    // operation type (delete, mkcol, ...)
    enum DavPathOpType op;
    // clone of the browser's DavSession
    DavSession *sn;
    // path array
    char **path;
    // browser->resources indices
    size_t *list_indices;
    // number of path/list_indices elements
    size_t nelm;
    // path is collection
    DavBool iscollection;

    // browser->current ptr when the PathOp started
    // used in combination with collection_ctn to check if the browser list changed
    DavResource *collection;
    int64_t collection_ctn;
} DavPathOp;

typedef struct DavPathOpResult {
    UiObject *ui;
    DavBrowser *browser;
    DavResource *collection;
    int64_t collection_ctn;
    enum DavPathOpType op;
    DavBool iscollection;

    char *path;
    int res_index;
    int result;
    char *errormsg;
    
    time_t result_lastmodified;
    uint64_t result_contentlength;
    char *result_contenttype;
} DavPathOpResult;

typedef struct DavRenameOp {
    UiObject *ui;
    DavBrowser *browser;
    
    // clone of the browser's DavSession
    DavSession *sn;
    char *path;
    char *newname;
    int result;
    char *errormsg;
    
    // browser->resources index
    size_t index;
    
    // browser->current ptr when the PathOp started
    // used in combination with collection_ctn to check if the browser list changed
    DavResource *collection;
    int64_t collection_ctn;
} DavRenameOp;

static int uithr_pathop_delete_error(void *data) {
    DavPathOpResult *result = data;

    cxmutstr msg = cx_asprintf("Cannot delete resource %s", result->path);
    ui_dialog(result->ui, .title = "Error", .content = msg.ptr, .button1_label = "OK");
    free(msg.ptr);

    if (result->errormsg) {
        free(result->errormsg);
    }
    free(result->path);
    free(result);
    return 0;
}

static int uithr_pathop_delete_sucess(void *data) {
    DavPathOpResult *result = data;

    if (result->browser->current == result->collection && result->browser->res_counter == result->collection_ctn) {
        ui_list_remove(result->browser->resources, result->res_index);
        result->browser->resources->update(result->browser->resources, 0);
    }

    free(result->path);
    free(result);
    return 0;
}

static int uithr_pathop_create_resource_error(void *data) {
    DavPathOpResult *result = data;

    cxmutstr msg = cx_asprintf("Cannot create %s %s", result->iscollection ? "collection" : "resource", result->path);
    ui_dialog(result->ui, .title = "Error", .content = msg.ptr, .button1_label = "OK");
    free(msg.ptr);

    if (result->errormsg) {
        free(result->errormsg);
    }
    free(result->path);
    free(result);
    return 0;
}

static int uithr_pathop_create_resource_sucess(void *data) {
    DavPathOpResult *result = data;

    if (result->browser->current == result->collection && result->browser->res_counter == result->collection_ctn) {
        DavResource *res = dav_resource_new(result->browser->sn, result->path);
        res->iscollection = result->iscollection;
        res->lastmodified = result->result_lastmodified;
        res->contentlength = result->result_contentlength;
        res->contenttype = result->result_contenttype ? dav_session_strdup(res->session, result->result_contenttype) : NULL;
        // TODO: add the resource at the correct position or sort the list after append
        ui_list_append(result->browser->resources, res);
        result->browser->resources->update(result->browser->resources, 0);
    }

    free(result->path);
    free(result->result_contenttype);
    free(result);
    return 0;
}

static int jobthr_path_op(void *data) {
    DavPathOp *op = data;

    for (int i = op->nelm-1; i >= 0; i--) {
        if (op->path[i]) {
            DavResource *res = dav_resource_new(op->sn, op->path[i]);

            DavPathOpResult *result = malloc(sizeof(DavPathOpResult));
            result->ui = op->ui;
            result->browser = op->browser;
            result->collection = op->collection;
            result->collection_ctn = op->collection_ctn;
            result->op = op->op;
            result->path = strdup(res->path);
            result->result = 0;
            result->res_index = op->list_indices[i];
            result->errormsg = NULL;
            result->iscollection = op->iscollection;
            result->result_lastmodified = 0;
            result->result_contentlength = 0;
            result->result_contenttype = NULL;
    
            if (op->op == DAV_PATH_OP_DELETE) {
                ui_threadfunc result_callback = uithr_pathop_delete_sucess;
                if (dav_delete(res)) {
                    result->errormsg = op->sn->errorstr ? strdup(op->sn->errorstr) : NULL;
                    result_callback = uithr_pathop_delete_error;
                }
                ui_call_mainthread(result_callback, result);
            } else if (op->op == DAV_PATH_OP_CREATE) {
                res->iscollection = op->iscollection;
                ui_threadfunc result_callback = uithr_pathop_create_resource_sucess;
                if (dav_create(res)) {
                    result->errormsg = op->sn->errorstr ? strdup(op->sn->errorstr) : NULL;
                    result_callback = uithr_pathop_create_resource_error;
                } else {
                    // try to load some basic resource properties 
                    // we don't care about the result, if it fails,
                    // we just don't have the new properties
                    dav_load_prop(res, NULL, 0);
                    result->result_lastmodified = res->lastmodified;
                    result->result_contentlength = res->contentlength;
                    result->result_contenttype = res->contenttype ? strdup(res->contenttype) : NULL;
                }
                ui_call_mainthread(result_callback, result);
            } 

            dav_resource_free(res);
            free(op->path[i]);
        }
    }

    dav_session_destroy(op->sn);
    free(op->path);
    free(op->list_indices);
    free(op);

    return 0;
}

void davbrowser_delete(UiObject *ui, DavBrowser *browser, UiListSelection selection) {
    DavPathOp *op = malloc(sizeof(DavPathOp));
    op->ui = ui;
    op->browser = browser;
    op->op = DAV_PATH_OP_DELETE;
    op->sn = dav_session_clone(browser->sn);
    op->path = calloc(selection.count, sizeof(char*));
    op->list_indices = calloc(selection.count, sizeof(size_t));
    op->nelm = selection.count;

    op->collection = browser->current;
    op->collection_ctn = browser->res_counter;

    for (int i = 0; i < selection.count; i++) {
        DavResource *res = ui_list_get(browser->resources, selection.rows[i]);
        if (res) {
            op->path[i] = strdup(res->path);
            op->list_indices[i] = selection.rows[i];
        }
    }

    ui_job(ui, jobthr_path_op, op, NULL, NULL);
}

void davbrowser_create_resource(UiObject *ui, DavBrowser *browser, const char *name, DavBool iscollection) {
    DavPathOp *op = malloc(sizeof(DavPathOp));
    op->ui = ui;
    op->browser = browser;
    op->op = DAV_PATH_OP_CREATE;
    op->sn = dav_session_clone(browser->sn);
    op->path = calloc(1, sizeof(char*));
    op->list_indices = calloc(1, sizeof(size_t));
    op->nelm = 1;
    op->iscollection = iscollection;

    op->path[0] = util_concat_path(browser->current->path, name);

    op->collection = browser->current;
    op->collection_ctn = browser->res_counter;

    ui_job(ui, jobthr_path_op, op, NULL, NULL);
}

void davbrowser_mkcol(UiObject *ui, DavBrowser *browser, const char *name) {
    davbrowser_create_resource(ui, browser, name, TRUE);
}

void davbrowser_newfile(UiObject *ui, DavBrowser *browser, const char *name) {
    davbrowser_create_resource(ui, browser, name, FALSE);
}




static int jobthr_rename(void *data) {
    DavRenameOp *op = data;
    
    DavResource *res = dav_get(op->sn, op->path, NULL);
    if(!res) {
        fprintf(stderr, "Error: Cannot get resource %s\n", op->path);
        op->result = 1;
        return 0;
    }
    
    char *cryptoname = dav_get_string_property_ns(res, DAV_NS, "crypto-name");
    char *cryptokey = dav_get_string_property_ns(res, DAV_NS, "crypto-key");
    if(cryptoname && cryptokey) {
        // encrypted resource, the name is stored in the crypto-name property
        // these properties are only loaded if encryption is enabled for
        // this session
        DavKey *key = dav_context_get_key(op->sn->context, cryptokey);
        if(!key) {
            cxmutstr error = cx_asprintf_a(op->sn->mp->allocator, "Cannot rename resource: crypto key %s not found.", cryptokey);
            op->errormsg = error.ptr;
            op->result = 1;
            return 0;
        }
        
        // check if a resource with this name already exists
        char *parent = util_parent_path(res->path);
        char *newpath = util_concat_path(parent, op->newname);
        DavResource *testres = dav_resource_new(op->sn, newpath);
        if(dav_exists(testres)) {
            cxmutstr error = cx_asprintf_a(op->sn->mp->allocator, "A resource with the name %s already exists.", op->newname);
            op->errormsg = error.ptr;
            op->result = 1;
        } else {
            char *crname = aes_encrypt(op->newname, strlen(op->newname), key);
            dav_set_string_property_ns(res, DAV_NS, "crypto-name", crname);
            free(crname);
            if(dav_store(res)) {
                op->result = 1;
            }
        }
        free(parent);
        free(newpath);
    } else {
        // rename the resource by changing the url mapping with MOVE  
        char *parent = util_parent_path(res->href);
        char *new_href = util_concat_path(parent, op->newname);
        char *dest = util_get_url(op->sn, new_href);
        free(parent);
        free(new_href);
        if(dav_moveto(res, dest, false)) {
            op->result = 1;
        }
        free(dest);
    }
    
    if(op->result && !op->errormsg) {
        cxmutstr error = cx_asprintf_a(op->sn->mp->allocator, "Error: %d", op->sn->error);
        op->errormsg = error.ptr;
    }
    
    return 0;
}

static void uithr_rename_finished(UiEvent *event, void *data) {
    DavRenameOp *op = data;
    
    if(!op->result) {
        // update name in the browser list
        if (op->browser->current == op->collection && op->browser->res_counter == op->collection_ctn) {
            DavResource *res = ui_list_get(op->browser->resources, op->index);
            
            char *parent = util_parent_path(res->path);
            char *newpath = util_concat_path(parent, op->newname);
            dav_session_free(res->session, res->path);
            dav_session_free(res->session, res->name);
            res->path = dav_session_strdup(res->session, newpath);
            res->name = dav_session_strdup(res->session, op->newname);
            op->browser->resources->update(op->browser->resources, 0);
            free(parent);
            free(newpath);
        }
    } else {
        // error
        ui_dialog(op->ui, .title = "Error", .content = op->errormsg, .closebutton_label = "OK");
    }
    dav_session_destroy(op->sn);
}

static void action_resource_rename(UiEvent *event, void *data) {
    DavRenameOp *op = data;
    if(event->intval == 1) {
        char *newname = event->eventdata;
        if(!newname || strlen(newname) == 0) {
            ui_dialog(op->ui, .title = "Error", .content = "No name specified", .closebutton_label = "OK");
            dav_session_destroy(op->sn);
            return;
        }
        
        char *s = strchr(newname, '/');
        if(s) {
            ui_dialog(op->ui, .title = "Error", .content = "Character '/' is not allowed", .closebutton_label = "OK");
            dav_session_destroy(op->sn);
            return;
        }
        
        op->newname = dav_session_strdup(op->sn, newname);
        
        ui_job(op->ui, jobthr_rename, op, uithr_rename_finished, op);
        return;
    }
    dav_session_destroy(op->sn);
}

void davbrowser_rename(UiObject *ui, DavBrowser *browser, UiListSelection selection) {
    DavSession *sn = dav_session_clone(browser->sn);
    
    DavResource *res = ui_list_get(browser->resources, selection.rows[0]);
    
    DavRenameOp *rename = dav_session_malloc(sn, sizeof(DavRenameOp));
    memset(rename, 0, sizeof(DavRenameOp));
    rename->browser = browser;
    rename->ui = ui;
    rename->sn = sn;
    rename->path = dav_session_strdup(sn, res->path);
    rename->index = selection.rows[0];
    
    rename->collection = browser->current;
    rename->collection_ctn = browser->res_counter;
    
    ui_dialog(ui,
            .title = "Rename",
            .content = res->name,
            .input = TRUE,
            .input_value = res->name,
            .result = action_resource_rename,
            .resultdata = rename,
            .button1_label = "Rename",
            .closebutton_label = "Cancel");
}



DavResourceViewer* dav_resourceviewer_create(DavSession *sn, const char *path, DavResourceViewType type) {
    DavResourceViewer *doc = ui_document_new(sizeof(DavResourceViewer));
    UiContext *ctx = ui_document_context(doc);
    doc->ctx = ctx;
    
    doc->sn = dav_session_clone(sn);
    doc->path = strdup(path);
    doc->type = type;
    
    doc->tabview = ui_int_new(ctx, "tabview");
    doc->loading = ui_int_new(ctx, "loading");
    doc->message = ui_string_new(ctx, "message");
    
    doc->text = ui_text_new(ctx, "text");
    doc->image = ui_generic_new(ctx, "image");
    
    doc->properties = ui_list_new(ctx, "properties");
    
    doc->info_url = ui_string_new(ctx, "info_url");
    doc->info_name = ui_string_new(ctx, "info_name");
    doc->info_type = ui_string_new(ctx, "info_type");
    doc->info_encrypted = ui_string_new(ctx, "info_encrypted");
    doc->info_etag = ui_string_new(ctx, "info_etag");
    doc->info_size = ui_string_new(ctx, "info_size");
    
    return doc;
}

static char* gen_tmp_download_filename(const char *name) {
    char *dir = ui_getappdir();
    
    unsigned char rd[8];
    memset(rd, 0, 8);
    dav_rand_bytes(rd, 8);   
    char *hex = util_hexstr(rd, 8);
    cxmutstr tmp = cx_asprintf("%sdownload-%s-%s", dir, hex, name);
    return tmp.ptr;
}

static int jobthr_resourceviewer_load(void *data) {
    DavResourceViewer *doc = data;
     
    DavResource *res = dav_resource_new(doc->sn, doc->path);
    doc->error = dav_load(res);
    if(!doc->error) {
        doc->current = res;
        
        if(res->contentlength < DAV_RESOURCEVIEWER_PREVIEW_MAX_SIZE) {
            if(doc->type == DAV_RESOURCE_VIEW_TEXT) {
                doc->text_content = cxBufferCreate(NULL, res->contentlength, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS);
                int err = dav_get_content(res, doc->text_content, (dav_write_func)cxBufferWrite);
                cxBufferPut(doc->text_content, 0);
                if(err) {
                    doc->error = err;
                    doc->message_str = "Cannot load content"; // TODO: better message
                }
            } else if(doc->type == DAV_RESOURCE_VIEW_IMAGE) {
                char *tmp = gen_tmp_download_filename(res->name);
                FILE *f = sys_fopen(tmp, "wb");
                if(f) {
                    int err = dav_get_content(res, f, (dav_write_func)fwrite);
                    if(!err) {
                        doc->tmp_file = tmp;
                    } else {
                        free(tmp);
                    }
                    fclose(f);
                } else {
                    free(tmp);
                }
            }
        } else {
            // TODO: add file too large message
        }
    } else {
        doc->message_str = "Cannot load properties"; // TODO: better message
        dav_resource_free(res);
    }
    
    return 0;
}

static void resourceviewer_set_info(DavResourceViewer *doc) {
    DavResource *res = doc->current;
    if(!res) {
        return;
    }
    
    char *url = util_concat_path(res->session->base_url, res->href);
    ui_set(doc->info_url, url);
    free(url);

    ui_set(doc->info_name, res->name);

    if(res->iscollection) {
        ui_set(doc->info_type, "Collection");
    } else {
        if(res->contenttype) {
            cxmutstr type = cx_asprintf("Resource (%s)", res->contenttype);
            ui_set(doc->info_type, type.ptr);
            free(type.ptr);
        } else {
            ui_set(doc->info_type, "Resource");
        }
    }

    char *keyprop = dav_get_string_property_ns(
        res,
        DAV_NS,
        "crypto-key");
    if(keyprop) {
        cxmutstr info_encrypted = cx_asprintf("Yes   Key: %s", keyprop);
        ui_set(doc->info_encrypted, info_encrypted.ptr);
        free(info_encrypted.ptr);
    } else {
        ui_set(doc->info_encrypted, "No");
    }

    char *etag = dav_get_string_property_ns(
        res,
        "DAV:",
        "getetag");
    ui_set(doc->info_etag, etag);

    if(res->contentlength > 0) {
        char *sz = util_size_str(FALSE, res->contentlength);
        cxmutstr size_str = cx_asprintf("%s (%" PRIu64 " bytes)", sz, res->contentlength);
        ui_set(doc->info_size, size_str.ptr);
        free(size_str.ptr);
    } else {
        ui_set(doc->info_size, "0");
    }
}

static void resourceviewer_update_proplist(DavResourceViewer *doc) {
    DavResource *res = doc->current;
    if(!res) {
        return;
    }
    
    size_t count = 0;
    DavPropName *properties = dav_get_property_names(res, &count);
    for(int i=0;i<count;i++) {
        DavPropertyList *prop = ui_malloc(doc->ctx, sizeof(DavPropertyList));
        prop->ns = properties[i].ns ? ui_strdup(doc->ctx, properties[i].ns) : NULL;
        prop->name = ui_strdup(doc->ctx, properties[i].name);

        DavXmlNode *xval = dav_get_property_ns(res, prop->ns, prop->name);
        if(xval) {
            if(dav_xml_isstring(xval)) {
                char *value = dav_xml_getstring(xval);
                if(value) {
                    prop->value_simplified = NULL;
                    prop->value_full = ui_strdup(doc->ctx, value);
                }
            } else {
                DavXmlNode *x = xval->type == DAV_XML_ELEMENT ? xval : dav_xml_nextelm(xval);
                cxmutstr value = cx_asprintf_a(ui_allocator(doc->ctx), "<%s>...</%s>", x->name, x->name);
                prop->value_simplified = value.ptr;
            }
        }

        ui_list_append(doc->properties, prop);
    }
    doc->properties->update(doc->properties, 0);
}

static void resourceviewer_load_finished(UiEvent *event, void *data) {
    DavResourceViewer *doc = data;
    
    if(doc->window_closed) {
        dav_resourceviewer_destroy(doc);
        return;
    }
    
    resourceviewer_set_info(doc);
    resourceviewer_update_proplist(doc);
    
    if(doc->type == DAV_RESOURCE_VIEW_TEXT) {
        ui_set(doc->text, doc->text_content->space);
    } else if(doc->type == DAV_RESOURCE_VIEW_IMAGE) {
        ui_image_load_file(doc->image, doc->tmp_file);
    }
      
    ui_set(doc->tabview, 1);
    
    doc->loaded = TRUE;
}

void dav_resourceviewer_load(UiObject *ui, DavResourceViewer *res) {
    ui_set(res->loading, 1);
    ui_set(res->message, "Loading...");
    ui_set(res->tabview, 0);
    
    ui_job(ui, jobthr_resourceviewer_load, res, resourceviewer_load_finished, res);
}


typedef struct ResourceViewerUploadFile {
    UiObject *ui;
    DavSession *sn;
    char *path;
    cxmutstr text;
    int error;
} ResourceViewerUploadFile;

static int jobthr_upload_text(void *data) {
    ResourceViewerUploadFile *upload = data;
    
    DavResource *res = dav_resource_new(upload->sn, upload->path);
    dav_set_content_data(res, upload->text.ptr, upload->text.length);
    upload->error = dav_store(res);
    dav_resource_free(res);
    
    return 0;
}

static void uithr_upload_text_finished(UiEvent *event, void *data) {
    ResourceViewerUploadFile *upload = data;
    
    if(upload->error) {
        cxmutstr errormsg = cx_asprintf("Upload failed: %d", upload->sn->error); // TODO: add full error message
        ui_dialog(event->obj, .title = "Error", .content = errormsg.ptr, .closebutton_label = "OK");
        free(errormsg.ptr);
        ui_set_group(event->obj->ctx, RESOURCEVIEWER_STATE_MODIFIED);
    }
    
    dav_session_destroy(upload->sn);
    free(upload->path);
    free(upload->text.ptr);
    free(upload);
}

void dav_resourceviewer_save(UiObject *ui, DavResourceViewer *res) {
    if(res->type == DAV_RESOURCE_VIEW_TEXT) {
        DavSession *newsn = dav_session_clone(res->current->session);
        ResourceViewerUploadFile *upload = malloc(sizeof(ResourceViewerUploadFile));
        upload->ui = ui;
        upload->sn = newsn;
        upload->path = strdup(res->current->path);
        char *text = ui_get(res->text);
        size_t textlen = strlen(text);
        upload->text = cx_strdup(cx_strn(text, textlen));
        ui_job(ui, jobthr_upload_text, upload, uithr_upload_text_finished, upload);
        ui_unset_group(ui->ctx, RESOURCEVIEWER_STATE_MODIFIED);
    }
}

void dav_resourceviewer_destroy(DavResourceViewer *res) {
    
}


uint64_t dav_transfer_speed(TransferProgress *progress, time_t current) {
    size_t bytes = progress->transferred_bytes - progress->speedtest_bytes;
    time_t t = current - progress->speedtest_start;
    
    progress->speedtest_start = current;
    progress->speedtest_bytes = progress->transferred_bytes;
    
    return bytes/t;
}

mercurial