]> git.sven.stormbind.net Git - sven/vym.git/blobdiff - src/confluence-agent.cpp
New upstream version 2.9.22
[sven/vym.git] / src / confluence-agent.cpp
diff --git a/src/confluence-agent.cpp b/src/confluence-agent.cpp
new file mode 100644 (file)
index 0000000..1825198
--- /dev/null
@@ -0,0 +1,1014 @@
+#include "confluence-agent.h"
+
+#include <QMessageBox>
+#include <QSslSocket>
+
+#include <iostream> // FIXME-2 for debugging...
+
+#include "branchitem.h"
+#include "confluence-user.h"
+#include "file.h"
+#include "mainwindow.h"
+#include "misc.h"
+#include "vymmodel.h"
+#include "warningdialog.h"
+
+extern Main *mainWindow;
+extern QDir vymBaseDir;
+extern QString confluencePassword;
+extern Settings settings;
+extern bool debug;
+
+bool ConfluenceAgent::available() 
+{ 
+    if (!QSslSocket::supportsSsl())
+        return false;
+    if ( settings.value("/atlassian/confluence/username", "").toString().isEmpty())
+        return false;
+
+    if ( settings.value("/atlassian/confluence/url", "").toString().isEmpty())
+        return false;
+
+    return true;
+}
+
+ConfluenceAgent::ConfluenceAgent() { 
+    //qDebug() << "Constr. ConfluenceAgent jobType=";
+    init(); 
+}
+
+ConfluenceAgent::ConfluenceAgent(BranchItem *bi)
+{
+    //qDebug() << "Constr. ConfluenceAgent selbi = " << bi;
+
+    if (!bi) {
+        qWarning("Const ConfluenceAgent: bi == nullptr");
+        // This will leave the agent hanging around undeleted...
+        return;
+    }
+
+    init();
+
+    setBranch(bi);
+}
+
+ConfluenceAgent::~ConfluenceAgent()
+{
+    // qDebug() << "Destr ConfluenceAgent." << jobType;
+    if (killTimer)
+        delete killTimer;
+}
+
+void ConfluenceAgent::init()
+{
+    jobType = Undefined;
+    jobStep = -1;
+    abortJob = false;
+
+    killTimer = nullptr;
+
+    networkManager = new QNetworkAccessManager(this);
+
+    modelID = 0;    // invalid ID
+
+    killTimer = new QTimer(this);
+    killTimer->setInterval(15000);
+    killTimer->setSingleShot(true);
+
+    QObject::connect(killTimer, SIGNAL(timeout()), this, SLOT(timeout()));
+
+    apiURL = baseURL + "/rest/api";
+    baseURL = settings.value("/atlassian/confluence/url", "baseURL").toString();
+    
+    // Attachments
+    attachmentsAgent = nullptr;
+    currentUploadAttachmentIndex = -1;
+
+    // Read credentials 
+    authUsingPAT = 
+        settings.value("/atlassian/confluence/authUsingPAT", true).toBool();
+    if (authUsingPAT)
+        personalAccessToken =
+            settings.value("/atlassian/confluence/PAT", "undefined").toString();
+    else {
+        username =
+            settings.value("/atlassian/confluence/username", "user_johnDoe").toString();
+        if (!confluencePassword.isEmpty())
+            password = confluencePassword;
+        else
+            password = 
+                settings.value("/atlassian/confluence/password", "").toString();
+    }
+
+    if (!authUsingPAT && password.isEmpty()) {
+        // Set global password
+        if (!mainWindow->settingsConfluence()) 
+            abortJob = true;
+    }
+}
+
+void ConfluenceAgent::setJobType(JobType jt)
+{
+    jobType = jt;
+}
+
+void ConfluenceAgent::setBranch(BranchItem *bi)
+{
+    if (!bi) {
+        qWarning() << "ConfluenceAgent::setBranch  bi == nullptr";
+        abortJob = true;
+    } else {
+        branchID = bi->getID();
+        VymModel *model = bi->getModel();
+        modelID = model->getModelID();
+    }
+}
+
+void ConfluenceAgent::setModelID(uint id)
+{
+    modelID = id;
+}
+
+void ConfluenceAgent::setPageURL(const QString &u)
+{
+    pageURL = u;
+}
+
+void ConfluenceAgent::setNewPageName(const QString &t)
+{
+    newPageName = t;
+}
+
+void ConfluenceAgent::setUploadPagePath(const QString &fp)
+{
+    uploadPagePath = fp;
+}
+
+void ConfluenceAgent::addUploadAttachmentPath(const QString &fp)
+{
+    uploadAttachmentPaths << fp;
+}
+
+void ConfluenceAgent::startJob()
+{
+    if (jobStep > 0) {
+        unknownStepWarningFinishJob();
+    } else {
+        jobStep = 0;
+        continueJob();
+    }
+}
+
+void ConfluenceAgent::continueJob(int nextStep)
+{
+    if (abortJob) {
+        finishJob();
+        return;
+    }
+
+    if (nextStep < 0)
+        jobStep++;
+    else
+        jobStep = nextStep;
+
+    VymModel *model;
+
+    // qDebug() << "CA::contJob " << jobType << " Step: " << jobStep;
+
+    switch(jobType) {
+        case CopyPagenameToHeading:
+            if (jobStep == 1) {
+                startGetPageSourceRequest(pageURL);
+                return;
+            }
+            if (jobStep == 2) {
+                startGetPageDetailsRequest();
+                return;
+            }
+            if (jobStep == 3) {
+                model = mainWindow->getModel(modelID);
+                if (model) {
+                    BranchItem *bi = (BranchItem *)(model->findID(branchID));
+
+                    if (bi) {
+                        QString h = spaceKey + ": " + pageObj["title"].toString();
+                        model->setHeading(h, bi);
+                    } else
+                        qWarning() << "CA::continueJob couldn't find branch "
+                                   << branchID;
+                } else
+                    qWarning() << "CA::continueJob couldn't find model " << modelID;
+                finishJob();
+                return;
+            }
+            unknownStepWarningFinishJob();
+            return;
+
+        case CreatePage:
+            if (jobStep == 1) {
+                if (pageURL.isEmpty()) {
+                    qWarning() << "CA::contJob NewPage: pageURL is empty";
+                    finishJob();
+                    return;
+                }
+                if (newPageName.isEmpty()) {
+                    qWarning() << "CA::contJob NewPage: newPageName is empty";
+                    finishJob();
+                    return;
+                }
+
+                mainWindow->statusMessage(
+                    QString("Starting to create Confluence page %1").arg(pageURL));
+
+                // Check if parent page with url already exists and get pageID, spaceKey
+                startGetPageSourceRequest(pageURL);
+                return;
+            }
+            if (jobStep == 2) {
+                // Create new page with parent url
+                startCreatePageRequest();
+                return;
+            }
+            if (jobStep == 3) {
+
+                pageID = pageObj["id"].toString();
+
+                // Upload attachments?
+                if (uploadAttachmentPaths.count() > 0) {
+                    attachmentsAgent = new ConfluenceAgent;
+                    attachmentsAgent->setJobType(ConfluenceAgent::UploadAttachments);
+                    attachmentsAgent->pageID = pageID;
+                    attachmentsAgent->uploadAttachmentPaths = uploadAttachmentPaths;
+
+                    connect(attachmentsAgent, &ConfluenceAgent::attachmentsSuccess,
+                        this, &ConfluenceAgent::attachmentsUploadSuccess);
+                    connect(attachmentsAgent, &ConfluenceAgent::attachmentsFailure,
+                        this, &ConfluenceAgent::attachmentsUploadFailure);
+                    attachmentsAgent->startJob();
+                    return;
+                } else
+                    // Proceed to next step
+                    jobStep = 4;
+            }
+            if (jobStep == 4) {
+                //qDebug() << "CA::finished  Created page with ID: " << pageObj["id"].toString();
+                mainWindow->statusMessage(
+                    QString("Created Confluence page %1").arg(pageURL));
+                finishJob();
+                return;
+            }
+            unknownStepWarningFinishJob();
+            return;
+
+        case UpdatePage:
+            if (jobStep == 1) {
+                if (pageURL.isEmpty()) {
+                    qWarning() << "CA::contJob UpdatePage: pageURL is empty";
+                    finishJob();
+                    return;
+                }
+
+                mainWindow->statusMessage(
+                    QString("Starting to update Confluence page %1").arg(pageURL));
+
+                // Check if page with url already exists and get pageID, spaceKey
+                startGetPageSourceRequest(pageURL);
+                return;
+            }
+            if (jobStep == 2) {
+                // Get title, which is required by Confluence to update a page
+                startGetPageDetailsRequest();
+                return;
+            }
+            if (jobStep == 3) {
+                // Upload attachments?
+                if (uploadAttachmentPaths.count() > 0) {
+                    attachmentsAgent = new ConfluenceAgent;
+                    attachmentsAgent->setJobType(ConfluenceAgent::UploadAttachments);
+                    attachmentsAgent->pageID = pageID;
+                    attachmentsAgent->uploadAttachmentPaths = uploadAttachmentPaths;
+
+                    connect(attachmentsAgent, &ConfluenceAgent::attachmentsSuccess,
+                        this, &ConfluenceAgent::attachmentsUploadSuccess);
+                    connect(attachmentsAgent, &ConfluenceAgent::attachmentsFailure,
+                        this, &ConfluenceAgent::attachmentsUploadFailure);
+                    attachmentsAgent->startJob();
+                    return;
+                }
+            }
+            if (jobStep == 4) {
+                // Update page with parent url
+                if (newPageName.isEmpty())
+                        newPageName = pageObj["title"].toString();
+                startUpdatePageRequest();
+                return;
+            }
+            if (jobStep == 5) {
+                //qDebug() << "CA::finished  Updated page with ID: " << pageObj["id"].toString();
+                mainWindow->statusMessage(
+                    QString("Updated Confluence page %1").arg(pageURL));
+                finishJob();
+                return;
+            }
+            unknownStepWarningFinishJob();
+            return;
+
+        case GetUserInfo:
+            if (jobStep == 1) {
+                // qDebug() << "CA:: begin getting UserInfo";
+                startGetUserInfoRequest();
+                return;
+            }
+            if (jobStep == 2) {
+                QJsonArray array = pageObj["results"].toArray();
+                QJsonObject userObj;
+                QJsonObject u;
+                ConfluenceUser user;
+                userList.clear();
+                for (int i = 0; i < array.size(); ++i) {
+                    userObj = array[i].toObject();
+
+                    u = userObj["user"].toObject();
+                    user.setTitle( userObj["title"].toString());
+                    user.setURL( "https://" + baseURL + "/"
+                            + "display/~" + u["username"].toString());
+                    user.setUserKey( u["userKey"].toString());
+                    user.setUserName( u["username"].toString());
+                    user.setDisplayName( u["displayName"].toString());
+                    userList << user;
+                }
+                emit (foundUsers(userList));
+                finishJob();
+                return;
+            }
+            unknownStepWarningFinishJob();
+            return;
+
+        case UploadAttachments:
+            if (jobStep == 1) {
+
+                if (uploadAttachmentPaths.count() <= 0) {
+                    qWarning() << "ConfluenceAgent: No attachments to upload!";
+                    emit(attachmentsFailure());
+                    finishJob();
+                    return;
+                }
+
+                // Prepare to upload first attachment in list
+                currentUploadAttachmentIndex = 0;
+
+                // Try to get info for attachments
+                startGetAttachmentsInfoRequest();
+                return;
+            }
+            if (jobStep == 2) {
+                // Entry point for looping over list of attachments to upload
+
+                if (currentUploadAttachmentIndex >= uploadAttachmentPaths.count()) {
+                    // All uploaded, let's finish uploading
+                    emit(attachmentsSuccess());
+                    finishJob();
+                } else {
+                    currentAttachmentPath = uploadAttachmentPaths.at(currentUploadAttachmentIndex);
+                    currentAttachmentTitle = basename(currentAttachmentPath);
+
+                    // Create attachment with image of map, if required
+                    if (attachmentsTitles.count() == 0 || 
+                        !attachmentsTitles.contains(currentAttachmentTitle)) {
+                        // Create new attachment
+                        startCreateAttachmentRequest();
+                    } else {
+                        // Update existing attachment
+                        startUpdateAttachmentRequest();
+                    }
+                }
+                return;
+            }
+            unknownStepWarningFinishJob();
+            return;
+
+        default:
+            qWarning() << "ConfluenceAgent::continueJob   unknown jobType " << jobType;
+    }
+}
+
+void ConfluenceAgent::finishJob()
+{
+    deleteLater();
+}
+
+void ConfluenceAgent::unknownStepWarningFinishJob()
+{
+    qWarning() << "CA::contJob  unknow step in jobType = " 
+        << jobType 
+        << "jobStep = " << jobStep;
+    finishJob();
+}
+
+void ConfluenceAgent::getUsers(const QString &usrQuery)
+{
+    userQuery = usrQuery;
+    if (usrQuery.contains(QRegExp("\\W+"))) {
+        qWarning() << "ConfluenceAgent::getUsers  Forbidden characters in " << usrQuery;
+        return;
+    }
+
+    setJobType(GetUserInfo);
+    startJob();
+}
+
+QNetworkRequest ConfluenceAgent::createRequest(const QUrl &url)
+{
+    QNetworkRequest request = QNetworkRequest(url);
+
+    QString headerData;
+    if (authUsingPAT)
+        headerData = QString("Bearer %1").arg(personalAccessToken);
+    else {
+        QString concatenated = username + ":" + password;
+        QByteArray data = concatenated.toLocal8Bit().toBase64();
+        headerData = "Basic " + data;
+    }
+    request.setRawHeader("Authorization", headerData.toLocal8Bit());
+
+    return request;
+}
+
+void ConfluenceAgent::startGetPageSourceRequest(QUrl requestedURL)
+{
+    //qDebug() << "CA::startGetPageSourceRequest " << requestedURL;
+    if (!requestedURL.toString().startsWith("http"))
+        requestedURL.setPath("https://" + requestedURL.path());
+
+    QUrl url = requestedURL;
+
+    QNetworkRequest request = createRequest(url);
+
+    if (debug)
+        qDebug() << "CA::startGetPageSourceRequest: url = " + request.url().toString();
+
+    killTimer->start();
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::pageSourceReceived);
+
+    networkManager->get(request);
+}
+
+void ConfluenceAgent::pageSourceReceived(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::pageSourceReceived";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (!wasRequestSuccessful(reply, "receive page source", fullReply))
+        return;
+
+    // Find pageID
+    QRegExp rx("\\sname=\"ajs-page-id\"\\scontent=\"(\\d*)\"");
+    rx.setMinimal(true);
+
+    if (rx.indexIn(fullReply, 0) != -1) {
+        pageID = rx.cap(1);
+    }
+    else {
+        qWarning()
+            << "ConfluenceAgent::pageSourceReveived Couldn't find page ID";
+        //qWarning() << fullReply;
+        return;
+    }
+
+    // Find spaceKey 
+    rx.setPattern("meta\\s*id=\"confluence-space-key\"\\s* "
+                  "name=\"confluence-space-key\"\\s*content=\"(.*)\"");
+    if (rx.indexIn(fullReply, 0) != -1) {
+        spaceKey = rx.cap(1);
+    }
+    else {
+        qWarning() << "ConfluenceAgent::pageSourceReveived Couldn't find "
+                      "space key in response";
+        qWarning() << fullReply;
+        finishJob();
+        return;
+    }
+
+    const QVariant redirectionTarget =
+        reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
+
+    continueJob();
+}
+
+void ConfluenceAgent::startGetPageDetailsRequest()
+{
+    if (debug) qDebug() << "CA::startGetPageDetailsRequest" << pageID;
+
+    // Authentication in URL  (only SSL!)
+    QString url = "https://" 
+        + baseURL + apiURL 
+        + "/content/" + pageID + "?expand=metadata.labels,version";
+
+    QNetworkRequest request = createRequest(url);
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::pageDetailsReceived);
+
+    killTimer->start();
+
+    networkManager->get(request);
+}
+
+void ConfluenceAgent::pageDetailsReceived(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::pageDetailsReceived";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (!wasRequestSuccessful(reply, "receive page details", fullReply))
+        return;
+
+    QJsonDocument jsdoc;
+    jsdoc = QJsonDocument::fromJson(fullReply);
+
+    pageObj = jsdoc.object();
+    // cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
+
+    continueJob();
+}
+
+void ConfluenceAgent::startCreatePageRequest()
+{
+    // qDebug() << "CA::startCreatePageRequest";
+
+    QString url = "https://" + baseURL + apiURL + "/content";
+
+    QNetworkRequest request = createRequest(url);
+    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+    QJsonObject payload;
+    payload["type"] = "page";
+    payload["title"] = newPageName;
+
+    // Build array with ID of parent page
+    QJsonObject ancestorsID;
+    ancestorsID["id"] = pageID;
+    QJsonArray ancestorsArray;
+    ancestorsArray.append(ancestorsID);
+    payload["ancestors"] = ancestorsArray;
+
+    // Build object with space key
+    QJsonObject skey;
+    skey["key"] = spaceKey;
+    payload["space"] = skey;
+
+    // Build body
+    QString body;
+    if (!loadStringFromDisk(uploadPagePath, body))
+    {
+        qWarning() << "ConfluenceAgent: Couldn't read file to upload:" << uploadPagePath;
+        finishJob();
+        return;
+    }
+
+    QJsonObject innerStorageObj
+    {
+        {"value", body},
+        {"representation", "storage"}
+    };
+    QJsonObject outerStorageObj;
+    outerStorageObj["storage"] = innerStorageObj;
+    payload["body"] = outerStorageObj;
+
+    QJsonDocument doc(payload);
+    QByteArray data = doc.toJson();
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::pageUploaded);
+
+    killTimer->start();
+
+    networkManager->post(request, data);
+}
+
+void ConfluenceAgent::startUpdatePageRequest()
+{
+    if (debug) qDebug() << "CA::startUpdatePageRequest";
+
+    QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID;
+
+    QNetworkRequest request = createRequest(url);
+
+    request.setHeader(
+        QNetworkRequest::ContentTypeHeader,
+        "application/json; charset=utf-8");
+
+    QJsonObject payload;
+    payload["id"] = pageID;
+    payload["type"] = "page";
+    payload["title"] = newPageName;
+
+    // Build version object
+    QJsonObject newVersionObj;
+    QJsonObject oldVersionObj = pageObj["version"].toObject();
+
+    newVersionObj["number"] = oldVersionObj["number"].toInt() + 1;
+    payload["version"] = newVersionObj;
+
+    // Build object with space key
+    QJsonObject skey;
+    skey["key"] = spaceKey;
+    payload["space"] = skey;
+
+    // Build body
+    QString body;
+    if (!loadStringFromDisk(uploadPagePath, body))
+    {
+        qWarning() << "ConfluenceAgent: Couldn't read file to upload:" << uploadPagePath;
+        finishJob();
+        return;
+    }
+
+    QJsonObject innerStorageObj
+    {
+        {"value", body},
+        {"representation", "storage"}
+    };
+    QJsonObject outerStorageObj;
+    outerStorageObj["storage"] = innerStorageObj;
+    payload["body"] = outerStorageObj;
+
+    QJsonDocument doc(payload);
+    QByteArray data = doc.toJson();
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::pageUploaded);
+
+    killTimer->start();
+
+    networkManager->put(request, data);
+}
+
+void ConfluenceAgent::pageUploaded(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::pageUploaded";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (!wasRequestSuccessful(reply, "upload page", fullReply))
+        return;
+
+    QJsonDocument jsdoc;
+    jsdoc = QJsonDocument::fromJson(fullReply);
+    pageObj = jsdoc.object();
+    //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
+    continueJob();
+}
+
+void ConfluenceAgent::startGetUserInfoRequest()
+{
+    if (debug) qDebug() << "CA::startGetInfoRequest for " << userQuery;
+
+    QString url = "https://" + baseURL + apiURL
+        + "/search?cql=user.fullname~" + userQuery;
+
+    networkManager->disconnect();
+
+    QNetworkRequest request = createRequest(url);
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::userInfoReceived);
+
+    killTimer->start();
+
+    networkManager->get(request);
+}
+
+void ConfluenceAgent::userInfoReceived(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::UserInfopageReceived";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (!wasRequestSuccessful(reply, "receive user info", fullReply))
+        return;
+
+    QJsonDocument jsdoc;
+    jsdoc = QJsonDocument::fromJson(fullReply);
+    pageObj = jsdoc.object();
+    continueJob();
+}
+
+void ConfluenceAgent::startGetAttachmentsInfoRequest()
+{
+    if (debug) qDebug() << "CA::startGetAttachmentIdRequest";
+
+    QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment";
+
+    QNetworkRequest request = createRequest(url);
+    request.setRawHeader("X-Atlassian-Token", "no-check");
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::attachmentsInfoReceived);
+
+    killTimer->start();
+
+    QNetworkReply *reply = networkManager->get(request);
+}
+
+void ConfluenceAgent::attachmentsInfoReceived(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::attachmentsInfoReceived";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (!wasRequestSuccessful(reply, "get attachment info", fullReply))
+        return;
+
+    QJsonDocument jsdoc;
+    jsdoc = QJsonDocument::fromJson(fullReply);
+
+    attachmentObj = jsdoc.object();
+    int attachmentsCount = jsdoc["size"].toInt();
+    //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
+    for (int i = 0; i < attachmentsCount; i++) {
+        attachmentsTitles << jsdoc["results"][i]["title"].toString();
+        attachmentsIds    << jsdoc["results"][i]["id"].toString();
+        //qDebug() << " Title: " << attachmentsTitles.last() << 
+        //            " Id: " << attachmentsIds.last();
+    }
+
+    continueJob();
+}
+
+void ConfluenceAgent::startCreateAttachmentRequest()
+{
+    if (debug) qDebug() << "CA::startCreateAttachmentRequest";
+
+    QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment";
+
+    QNetworkRequest request = createRequest(url);
+    request.setRawHeader("X-Atlassian-Token", "no-check");
+
+    QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
+
+
+    QHttpPart imagePart;
+    imagePart.setHeader(
+            QNetworkRequest::ContentDispositionHeader,
+
+            // Name must be "file"
+            QVariant(
+                QString("form-data; name=\"file\"; filename=\"%1\"")
+                    .arg(currentAttachmentTitle)));
+    imagePart.setHeader(
+            QNetworkRequest::ContentTypeHeader,
+            QVariant("image/jpeg"));
+
+    QFile *file = new QFile(currentAttachmentPath);
+    if (!file->open(QIODevice::ReadOnly)) {
+        qWarning() << "Problem opening attachment: " << currentAttachmentPath;
+        QMessageBox::warning(
+            nullptr, tr("Warning"),
+            QString("Could not open attachment file \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
+        finishJob();
+        return;
+    }
+    imagePart.setBodyDevice(file);
+    /*
+    qDebug() << "      title=" << currentAttachmentTitle;
+    qDebug() << "       path=" << currentAttachmentPath;
+    qDebug() << "        url=" << url;
+    qDebug() << "  file size=" << file->size();
+    */
+    multiPart->append(imagePart);
+    file->setParent(multiPart); // delete later with the multiPart
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::attachmentCreated);
+
+    killTimer->start();
+
+    QNetworkReply *reply = networkManager->post(request, multiPart);
+
+    multiPart->setParent(reply);
+}
+
+void ConfluenceAgent::attachmentCreated(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::attachmentCreated";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
+        if (fullReply.contains(
+                    QString("Cannot add a new attachment with same file name as an existing attachment").toLatin1())) {
+            // Replace existing attachment
+            qWarning() << "Attachment with name " << currentAttachmentTitle << " already exists.";
+            qWarning() << "AttachmentID unknown, stopping now"; 
+
+            finishJob();
+            return;
+        }
+        if (!wasRequestSuccessful(reply, "create attachment", fullReply))
+            return;
+    }
+
+    QJsonDocument jsdoc;
+    jsdoc = QJsonDocument::fromJson(fullReply);
+    attachmentObj = jsdoc.object();
+
+    //qDebug() << "CA::attachmentCreated Successful:";
+    //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
+    //cout << attachmentObj["results"].toArray().toStdString();
+
+    currentUploadAttachmentIndex++;
+
+    continueJob(2);
+}
+
+void ConfluenceAgent::startUpdateAttachmentRequest()
+{
+    if (debug) qDebug() << "CA::startUpdateAttachmentRequest";
+
+    for (int i = 0; i < attachmentsTitles.count(); i++) {
+        // qDebug() << "     - " << attachmentsTitles.at(i);
+        if (attachmentsTitles.at(i) == currentAttachmentTitle) {
+            currentAttachmentId = attachmentsIds.at(i);
+            break;
+        }
+    }
+
+    if (currentAttachmentId.isEmpty()) {
+        QMessageBox::warning(
+            nullptr, tr("Warning"),
+            QString("Could not find existing attachment \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
+        finishJob();
+        return;
+    }
+
+    QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment/" + currentAttachmentId + "/data";
+
+    QNetworkRequest request = createRequest(url);
+    request.setRawHeader("X-Atlassian-Token", "no-check");
+
+    QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
+
+    QHttpPart imagePart;
+    imagePart.setHeader(
+            QNetworkRequest::ContentDispositionHeader,
+
+            // Name must be "file"
+            QVariant(
+                QString("form-data; name=\"file\"; filename=\"%1\"")
+                    .arg(currentAttachmentTitle)));
+    imagePart.setHeader(
+            QNetworkRequest::ContentTypeHeader,
+            QVariant("image/jpeg"));
+
+    QFile *file = new QFile(currentAttachmentPath);
+    if (!file->open(QIODevice::ReadOnly)) {
+        qWarning() << "Problem opening attachment: " << currentAttachmentPath;
+        QMessageBox::warning(
+            nullptr, tr("Warning"),
+            QString("Could not open attachment file \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
+        finishJob();
+        return;
+    }
+    imagePart.setBodyDevice(file);
+    /*
+    qDebug() << "      title=" << currentAttachmentTitle;
+    qDebug() << "       path=" << currentAttachmentPath;
+    qDebug() << "        url=" << url;
+    qDebug() << "  file size=" << file->size();
+    */
+    multiPart->append(imagePart);
+    file->setParent(multiPart);
+
+    connect(networkManager, &QNetworkAccessManager::finished,
+        this, &ConfluenceAgent::attachmentUpdated);
+
+    killTimer->start();
+
+    QNetworkReply *reply = networkManager->post(request, multiPart);
+
+    multiPart->setParent(reply);
+}
+
+void ConfluenceAgent::attachmentUpdated(QNetworkReply *reply)
+{
+    if (debug) qDebug() << "CA::attachmentUpdated";
+
+    killTimer->stop();
+    networkManager->disconnect();
+    reply->deleteLater();
+
+    QByteArray fullReply = reply->readAll();
+    if (!wasRequestSuccessful(reply, "update attachment", fullReply))
+        return;
+
+    QJsonDocument jsdoc;
+    jsdoc = QJsonDocument::fromJson(fullReply);
+    attachmentObj = jsdoc.object();
+
+    //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
+
+    currentUploadAttachmentIndex++;
+
+    continueJob(2);
+}
+
+void ConfluenceAgent::attachmentsUploadSuccess() // slot called from attachmentsAgent
+{
+    continueJob();
+}
+
+void ConfluenceAgent::attachmentsUploadFailure() // slot called from attachmentsAgent
+{
+    qWarning() << "CA::attachmentsUpload failed";
+    finishJob();
+}
+
+bool ConfluenceAgent::wasRequestSuccessful(QNetworkReply *reply, const QString &requestDesc, const QByteArray &fullReply)
+{
+    if (reply->error()) {
+
+        // Additionally print full error on console
+        qWarning() << "         Step: " << requestDesc;
+        qWarning() << "        Error: " << reply->error();
+        qWarning() << "  Errorstring: " <<  reply->errorString();
+
+        qDebug() << "    Request Url: " << reply->url() ;
+        qDebug() << "      Operation: " << reply->operation() ;
+
+        qDebug() << "      readAll: ";
+        QJsonDocument jsdoc;
+        jsdoc = QJsonDocument::fromJson(fullReply);
+        QString fullReplyFormatted = QString(jsdoc.toJson(QJsonDocument::Indented));
+        cout << fullReplyFormatted.toStdString();
+
+        /*
+        qDebug() << "Request headers: ";
+       QList<QByteArray> reqHeaders = reply->rawHeaderList();
+       foreach( QByteArray reqName, reqHeaders )
+        {
+            QByteArray reqValue = reply->rawHeader( reqName );
+            qDebug() << "  " << reqName << ": " << reqValue;
+       }
+        */
+
+        if (reply->error() == QNetworkReply::AuthenticationRequiredError)
+            QMessageBox::warning(
+                nullptr, tr("Warning"),
+                tr("Authentication problem when contacting Confluence") + "\n\n" + 
+                requestDesc);
+        else {
+            QString msg = QString("QNetworkReply error when trying to \"%1\"\n\n").arg(requestDesc);
+            WarningDialog warn;
+            warn.setText(msg + "\n\n" + fullReplyFormatted);
+            warn.showCancelButton(false);
+            warn.exec();
+        }
+
+        finishJob();
+        return false;
+    } else
+        return true;
+}
+
+void ConfluenceAgent::timeout()
+{
+    qWarning() << "ConfluenceAgent timeout!!   jobType = " << jobType;
+}
+
+#ifndef QT_NO_SSL
+void ConfluenceAgent::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
+{
+    QString errorString;
+    foreach (const QSslError &error, errors) {
+        if (!errorString.isEmpty())
+            errorString += '\n';
+        errorString += error.errorString();
+    }
+
+    reply->ignoreSslErrors();
+    qWarning() << "ConfluenceAgent: One or more SSL errors has occurred: " << errorString;
+    qWarning() << "Errors ignored.";
+}
+#endif