]> git.sven.stormbind.net Git - sven/vym.git/blob - src/confluence-agent.cpp
New upstream version 2.9.22
[sven/vym.git] / src / confluence-agent.cpp
1 #include "confluence-agent.h"
2
3 #include <QMessageBox>
4 #include <QSslSocket>
5
6 #include <iostream> // FIXME-2 for debugging...
7
8 #include "branchitem.h"
9 #include "confluence-user.h"
10 #include "file.h"
11 #include "mainwindow.h"
12 #include "misc.h"
13 #include "vymmodel.h"
14 #include "warningdialog.h"
15
16 extern Main *mainWindow;
17 extern QDir vymBaseDir;
18 extern QString confluencePassword;
19 extern Settings settings;
20 extern bool debug;
21
22 bool ConfluenceAgent::available() 
23
24     if (!QSslSocket::supportsSsl())
25         return false;
26     if ( settings.value("/atlassian/confluence/username", "").toString().isEmpty())
27         return false;
28
29     if ( settings.value("/atlassian/confluence/url", "").toString().isEmpty())
30         return false;
31
32     return true;
33 }
34
35 ConfluenceAgent::ConfluenceAgent() { 
36     //qDebug() << "Constr. ConfluenceAgent jobType=";
37     init(); 
38 }
39
40 ConfluenceAgent::ConfluenceAgent(BranchItem *bi)
41 {
42     //qDebug() << "Constr. ConfluenceAgent selbi = " << bi;
43
44     if (!bi) {
45         qWarning("Const ConfluenceAgent: bi == nullptr");
46         // This will leave the agent hanging around undeleted...
47         return;
48     }
49
50     init();
51
52     setBranch(bi);
53 }
54
55 ConfluenceAgent::~ConfluenceAgent()
56 {
57     // qDebug() << "Destr ConfluenceAgent." << jobType;
58     if (killTimer)
59         delete killTimer;
60 }
61
62 void ConfluenceAgent::init()
63 {
64     jobType = Undefined;
65     jobStep = -1;
66     abortJob = false;
67
68     killTimer = nullptr;
69
70     networkManager = new QNetworkAccessManager(this);
71
72     modelID = 0;    // invalid ID
73
74     killTimer = new QTimer(this);
75     killTimer->setInterval(15000);
76     killTimer->setSingleShot(true);
77
78     QObject::connect(killTimer, SIGNAL(timeout()), this, SLOT(timeout()));
79
80     apiURL = baseURL + "/rest/api";
81     baseURL = settings.value("/atlassian/confluence/url", "baseURL").toString();
82     
83     // Attachments
84     attachmentsAgent = nullptr;
85     currentUploadAttachmentIndex = -1;
86
87     // Read credentials 
88     authUsingPAT = 
89         settings.value("/atlassian/confluence/authUsingPAT", true).toBool();
90     if (authUsingPAT)
91         personalAccessToken =
92             settings.value("/atlassian/confluence/PAT", "undefined").toString();
93     else {
94         username =
95             settings.value("/atlassian/confluence/username", "user_johnDoe").toString();
96         if (!confluencePassword.isEmpty())
97             password = confluencePassword;
98         else
99             password = 
100                 settings.value("/atlassian/confluence/password", "").toString();
101     }
102
103     if (!authUsingPAT && password.isEmpty()) {
104         // Set global password
105         if (!mainWindow->settingsConfluence()) 
106             abortJob = true;
107     }
108 }
109
110 void ConfluenceAgent::setJobType(JobType jt)
111 {
112     jobType = jt;
113 }
114
115 void ConfluenceAgent::setBranch(BranchItem *bi)
116 {
117     if (!bi) {
118         qWarning() << "ConfluenceAgent::setBranch  bi == nullptr";
119         abortJob = true;
120     } else {
121         branchID = bi->getID();
122         VymModel *model = bi->getModel();
123         modelID = model->getModelID();
124     }
125 }
126
127 void ConfluenceAgent::setModelID(uint id)
128 {
129     modelID = id;
130 }
131
132 void ConfluenceAgent::setPageURL(const QString &u)
133 {
134     pageURL = u;
135 }
136
137 void ConfluenceAgent::setNewPageName(const QString &t)
138 {
139     newPageName = t;
140 }
141
142 void ConfluenceAgent::setUploadPagePath(const QString &fp)
143 {
144     uploadPagePath = fp;
145 }
146
147 void ConfluenceAgent::addUploadAttachmentPath(const QString &fp)
148 {
149     uploadAttachmentPaths << fp;
150 }
151
152 void ConfluenceAgent::startJob()
153 {
154     if (jobStep > 0) {
155         unknownStepWarningFinishJob();
156     } else {
157         jobStep = 0;
158         continueJob();
159     }
160 }
161
162 void ConfluenceAgent::continueJob(int nextStep)
163 {
164     if (abortJob) {
165         finishJob();
166         return;
167     }
168
169     if (nextStep < 0)
170         jobStep++;
171     else
172         jobStep = nextStep;
173
174     VymModel *model;
175
176     // qDebug() << "CA::contJob " << jobType << " Step: " << jobStep;
177
178     switch(jobType) {
179         case CopyPagenameToHeading:
180             if (jobStep == 1) {
181                 startGetPageSourceRequest(pageURL);
182                 return;
183             }
184             if (jobStep == 2) {
185                 startGetPageDetailsRequest();
186                 return;
187             }
188             if (jobStep == 3) {
189                 model = mainWindow->getModel(modelID);
190                 if (model) {
191                     BranchItem *bi = (BranchItem *)(model->findID(branchID));
192
193                     if (bi) {
194                         QString h = spaceKey + ": " + pageObj["title"].toString();
195                         model->setHeading(h, bi);
196                     } else
197                         qWarning() << "CA::continueJob couldn't find branch "
198                                    << branchID;
199                 } else
200                     qWarning() << "CA::continueJob couldn't find model " << modelID;
201                 finishJob();
202                 return;
203             }
204             unknownStepWarningFinishJob();
205             return;
206
207         case CreatePage:
208             if (jobStep == 1) {
209                 if (pageURL.isEmpty()) {
210                     qWarning() << "CA::contJob NewPage: pageURL is empty";
211                     finishJob();
212                     return;
213                 }
214                 if (newPageName.isEmpty()) {
215                     qWarning() << "CA::contJob NewPage: newPageName is empty";
216                     finishJob();
217                     return;
218                 }
219
220                 mainWindow->statusMessage(
221                     QString("Starting to create Confluence page %1").arg(pageURL));
222
223                 // Check if parent page with url already exists and get pageID, spaceKey
224                 startGetPageSourceRequest(pageURL);
225                 return;
226             }
227             if (jobStep == 2) {
228                 // Create new page with parent url
229                 startCreatePageRequest();
230                 return;
231             }
232             if (jobStep == 3) {
233
234                 pageID = pageObj["id"].toString();
235
236                 // Upload attachments?
237                 if (uploadAttachmentPaths.count() > 0) {
238                     attachmentsAgent = new ConfluenceAgent;
239                     attachmentsAgent->setJobType(ConfluenceAgent::UploadAttachments);
240                     attachmentsAgent->pageID = pageID;
241                     attachmentsAgent->uploadAttachmentPaths = uploadAttachmentPaths;
242
243                     connect(attachmentsAgent, &ConfluenceAgent::attachmentsSuccess,
244                         this, &ConfluenceAgent::attachmentsUploadSuccess);
245                     connect(attachmentsAgent, &ConfluenceAgent::attachmentsFailure,
246                         this, &ConfluenceAgent::attachmentsUploadFailure);
247                     attachmentsAgent->startJob();
248                     return;
249                 } else
250                     // Proceed to next step
251                     jobStep = 4;
252             }
253             if (jobStep == 4) {
254                 //qDebug() << "CA::finished  Created page with ID: " << pageObj["id"].toString();
255                 mainWindow->statusMessage(
256                     QString("Created Confluence page %1").arg(pageURL));
257                 finishJob();
258                 return;
259             }
260             unknownStepWarningFinishJob();
261             return;
262
263         case UpdatePage:
264             if (jobStep == 1) {
265                 if (pageURL.isEmpty()) {
266                     qWarning() << "CA::contJob UpdatePage: pageURL is empty";
267                     finishJob();
268                     return;
269                 }
270
271                 mainWindow->statusMessage(
272                     QString("Starting to update Confluence page %1").arg(pageURL));
273
274                 // Check if page with url already exists and get pageID, spaceKey
275                 startGetPageSourceRequest(pageURL);
276                 return;
277             }
278             if (jobStep == 2) {
279                 // Get title, which is required by Confluence to update a page
280                 startGetPageDetailsRequest();
281                 return;
282             }
283             if (jobStep == 3) {
284                 // Upload attachments?
285                 if (uploadAttachmentPaths.count() > 0) {
286                     attachmentsAgent = new ConfluenceAgent;
287                     attachmentsAgent->setJobType(ConfluenceAgent::UploadAttachments);
288                     attachmentsAgent->pageID = pageID;
289                     attachmentsAgent->uploadAttachmentPaths = uploadAttachmentPaths;
290
291                     connect(attachmentsAgent, &ConfluenceAgent::attachmentsSuccess,
292                         this, &ConfluenceAgent::attachmentsUploadSuccess);
293                     connect(attachmentsAgent, &ConfluenceAgent::attachmentsFailure,
294                         this, &ConfluenceAgent::attachmentsUploadFailure);
295                     attachmentsAgent->startJob();
296                     return;
297                 }
298             }
299             if (jobStep == 4) {
300                 // Update page with parent url
301                 if (newPageName.isEmpty())
302                         newPageName = pageObj["title"].toString();
303                 startUpdatePageRequest();
304                 return;
305             }
306             if (jobStep == 5) {
307                 //qDebug() << "CA::finished  Updated page with ID: " << pageObj["id"].toString();
308                 mainWindow->statusMessage(
309                     QString("Updated Confluence page %1").arg(pageURL));
310                 finishJob();
311                 return;
312             }
313             unknownStepWarningFinishJob();
314             return;
315
316         case GetUserInfo:
317             if (jobStep == 1) {
318                 // qDebug() << "CA:: begin getting UserInfo";
319                 startGetUserInfoRequest();
320                 return;
321             }
322             if (jobStep == 2) {
323                 QJsonArray array = pageObj["results"].toArray();
324                 QJsonObject userObj;
325                 QJsonObject u;
326                 ConfluenceUser user;
327                 userList.clear();
328                 for (int i = 0; i < array.size(); ++i) {
329                     userObj = array[i].toObject();
330
331                     u = userObj["user"].toObject();
332                     user.setTitle( userObj["title"].toString());
333                     user.setURL( "https://" + baseURL + "/"
334                             + "display/~" + u["username"].toString());
335                     user.setUserKey( u["userKey"].toString());
336                     user.setUserName( u["username"].toString());
337                     user.setDisplayName( u["displayName"].toString());
338                     userList << user;
339                 }
340                 emit (foundUsers(userList));
341                 finishJob();
342                 return;
343             }
344             unknownStepWarningFinishJob();
345             return;
346
347         case UploadAttachments:
348             if (jobStep == 1) {
349
350                 if (uploadAttachmentPaths.count() <= 0) {
351                     qWarning() << "ConfluenceAgent: No attachments to upload!";
352                     emit(attachmentsFailure());
353                     finishJob();
354                     return;
355                 }
356
357                 // Prepare to upload first attachment in list
358                 currentUploadAttachmentIndex = 0;
359
360                 // Try to get info for attachments
361                 startGetAttachmentsInfoRequest();
362                 return;
363             }
364             if (jobStep == 2) {
365                 // Entry point for looping over list of attachments to upload
366
367                 if (currentUploadAttachmentIndex >= uploadAttachmentPaths.count()) {
368                     // All uploaded, let's finish uploading
369                     emit(attachmentsSuccess());
370                     finishJob();
371                 } else {
372                     currentAttachmentPath = uploadAttachmentPaths.at(currentUploadAttachmentIndex);
373                     currentAttachmentTitle = basename(currentAttachmentPath);
374
375                     // Create attachment with image of map, if required
376                     if (attachmentsTitles.count() == 0 || 
377                         !attachmentsTitles.contains(currentAttachmentTitle)) {
378                         // Create new attachment
379                         startCreateAttachmentRequest();
380                     } else {
381                         // Update existing attachment
382                         startUpdateAttachmentRequest();
383                     }
384                 }
385                 return;
386             }
387             unknownStepWarningFinishJob();
388             return;
389
390         default:
391             qWarning() << "ConfluenceAgent::continueJob   unknown jobType " << jobType;
392     }
393 }
394
395 void ConfluenceAgent::finishJob()
396 {
397     deleteLater();
398 }
399
400 void ConfluenceAgent::unknownStepWarningFinishJob()
401 {
402     qWarning() << "CA::contJob  unknow step in jobType = " 
403         << jobType 
404         << "jobStep = " << jobStep;
405     finishJob();
406 }
407
408 void ConfluenceAgent::getUsers(const QString &usrQuery)
409 {
410     userQuery = usrQuery;
411     if (usrQuery.contains(QRegExp("\\W+"))) {
412         qWarning() << "ConfluenceAgent::getUsers  Forbidden characters in " << usrQuery;
413         return;
414     }
415
416     setJobType(GetUserInfo);
417     startJob();
418 }
419
420 QNetworkRequest ConfluenceAgent::createRequest(const QUrl &url)
421 {
422     QNetworkRequest request = QNetworkRequest(url);
423
424     QString headerData;
425     if (authUsingPAT)
426         headerData = QString("Bearer %1").arg(personalAccessToken);
427     else {
428         QString concatenated = username + ":" + password;
429         QByteArray data = concatenated.toLocal8Bit().toBase64();
430         headerData = "Basic " + data;
431     }
432     request.setRawHeader("Authorization", headerData.toLocal8Bit());
433
434     return request;
435 }
436
437 void ConfluenceAgent::startGetPageSourceRequest(QUrl requestedURL)
438 {
439     //qDebug() << "CA::startGetPageSourceRequest " << requestedURL;
440     if (!requestedURL.toString().startsWith("http"))
441         requestedURL.setPath("https://" + requestedURL.path());
442
443     QUrl url = requestedURL;
444
445     QNetworkRequest request = createRequest(url);
446
447     if (debug)
448         qDebug() << "CA::startGetPageSourceRequest: url = " + request.url().toString();
449
450     killTimer->start();
451
452     connect(networkManager, &QNetworkAccessManager::finished,
453         this, &ConfluenceAgent::pageSourceReceived);
454
455     networkManager->get(request);
456 }
457
458 void ConfluenceAgent::pageSourceReceived(QNetworkReply *reply)
459 {
460     if (debug) qDebug() << "CA::pageSourceReceived";
461
462     killTimer->stop();
463     networkManager->disconnect();
464     reply->deleteLater();
465
466     QByteArray fullReply = reply->readAll();
467     if (!wasRequestSuccessful(reply, "receive page source", fullReply))
468         return;
469
470     // Find pageID
471     QRegExp rx("\\sname=\"ajs-page-id\"\\scontent=\"(\\d*)\"");
472     rx.setMinimal(true);
473
474     if (rx.indexIn(fullReply, 0) != -1) {
475         pageID = rx.cap(1);
476     }
477     else {
478         qWarning()
479             << "ConfluenceAgent::pageSourceReveived Couldn't find page ID";
480         //qWarning() << fullReply;
481         return;
482     }
483
484     // Find spaceKey 
485     rx.setPattern("meta\\s*id=\"confluence-space-key\"\\s* "
486                   "name=\"confluence-space-key\"\\s*content=\"(.*)\"");
487     if (rx.indexIn(fullReply, 0) != -1) {
488         spaceKey = rx.cap(1);
489     }
490     else {
491         qWarning() << "ConfluenceAgent::pageSourceReveived Couldn't find "
492                       "space key in response";
493         qWarning() << fullReply;
494         finishJob();
495         return;
496     }
497
498     const QVariant redirectionTarget =
499         reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
500
501     continueJob();
502 }
503
504 void ConfluenceAgent::startGetPageDetailsRequest()
505 {
506     if (debug) qDebug() << "CA::startGetPageDetailsRequest" << pageID;
507
508     // Authentication in URL  (only SSL!)
509     QString url = "https://" 
510         + baseURL + apiURL 
511         + "/content/" + pageID + "?expand=metadata.labels,version";
512
513     QNetworkRequest request = createRequest(url);
514
515     connect(networkManager, &QNetworkAccessManager::finished,
516         this, &ConfluenceAgent::pageDetailsReceived);
517
518     killTimer->start();
519
520     networkManager->get(request);
521 }
522
523 void ConfluenceAgent::pageDetailsReceived(QNetworkReply *reply)
524 {
525     if (debug) qDebug() << "CA::pageDetailsReceived";
526
527     killTimer->stop();
528     networkManager->disconnect();
529     reply->deleteLater();
530
531     QByteArray fullReply = reply->readAll();
532     if (!wasRequestSuccessful(reply, "receive page details", fullReply))
533         return;
534
535     QJsonDocument jsdoc;
536     jsdoc = QJsonDocument::fromJson(fullReply);
537
538     pageObj = jsdoc.object();
539     // cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
540
541     continueJob();
542 }
543
544 void ConfluenceAgent::startCreatePageRequest()
545 {
546     // qDebug() << "CA::startCreatePageRequest";
547
548     QString url = "https://" + baseURL + apiURL + "/content";
549
550     QNetworkRequest request = createRequest(url);
551     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
552
553     QJsonObject payload;
554     payload["type"] = "page";
555     payload["title"] = newPageName;
556
557     // Build array with ID of parent page
558     QJsonObject ancestorsID;
559     ancestorsID["id"] = pageID;
560     QJsonArray ancestorsArray;
561     ancestorsArray.append(ancestorsID);
562     payload["ancestors"] = ancestorsArray;
563
564     // Build object with space key
565     QJsonObject skey;
566     skey["key"] = spaceKey;
567     payload["space"] = skey;
568
569     // Build body
570     QString body;
571     if (!loadStringFromDisk(uploadPagePath, body))
572     {
573         qWarning() << "ConfluenceAgent: Couldn't read file to upload:" << uploadPagePath;
574         finishJob();
575         return;
576     }
577
578     QJsonObject innerStorageObj
579     {
580         {"value", body},
581         {"representation", "storage"}
582     };
583     QJsonObject outerStorageObj;
584     outerStorageObj["storage"] = innerStorageObj;
585     payload["body"] = outerStorageObj;
586
587     QJsonDocument doc(payload);
588     QByteArray data = doc.toJson();
589
590     connect(networkManager, &QNetworkAccessManager::finished,
591         this, &ConfluenceAgent::pageUploaded);
592
593     killTimer->start();
594
595     networkManager->post(request, data);
596 }
597
598 void ConfluenceAgent::startUpdatePageRequest()
599 {
600     if (debug) qDebug() << "CA::startUpdatePageRequest";
601
602     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID;
603
604     QNetworkRequest request = createRequest(url);
605
606     request.setHeader(
607         QNetworkRequest::ContentTypeHeader,
608         "application/json; charset=utf-8");
609
610     QJsonObject payload;
611     payload["id"] = pageID;
612     payload["type"] = "page";
613     payload["title"] = newPageName;
614
615     // Build version object
616     QJsonObject newVersionObj;
617     QJsonObject oldVersionObj = pageObj["version"].toObject();
618
619     newVersionObj["number"] = oldVersionObj["number"].toInt() + 1;
620     payload["version"] = newVersionObj;
621
622     // Build object with space key
623     QJsonObject skey;
624     skey["key"] = spaceKey;
625     payload["space"] = skey;
626
627     // Build body
628     QString body;
629     if (!loadStringFromDisk(uploadPagePath, body))
630     {
631         qWarning() << "ConfluenceAgent: Couldn't read file to upload:" << uploadPagePath;
632         finishJob();
633         return;
634     }
635
636     QJsonObject innerStorageObj
637     {
638         {"value", body},
639         {"representation", "storage"}
640     };
641     QJsonObject outerStorageObj;
642     outerStorageObj["storage"] = innerStorageObj;
643     payload["body"] = outerStorageObj;
644
645     QJsonDocument doc(payload);
646     QByteArray data = doc.toJson();
647
648     connect(networkManager, &QNetworkAccessManager::finished,
649         this, &ConfluenceAgent::pageUploaded);
650
651     killTimer->start();
652
653     networkManager->put(request, data);
654 }
655
656 void ConfluenceAgent::pageUploaded(QNetworkReply *reply)
657 {
658     if (debug) qDebug() << "CA::pageUploaded";
659
660     killTimer->stop();
661     networkManager->disconnect();
662     reply->deleteLater();
663
664     QByteArray fullReply = reply->readAll();
665     if (!wasRequestSuccessful(reply, "upload page", fullReply))
666         return;
667
668     QJsonDocument jsdoc;
669     jsdoc = QJsonDocument::fromJson(fullReply);
670     pageObj = jsdoc.object();
671     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
672     continueJob();
673 }
674
675 void ConfluenceAgent::startGetUserInfoRequest()
676 {
677     if (debug) qDebug() << "CA::startGetInfoRequest for " << userQuery;
678
679     QString url = "https://" + baseURL + apiURL
680         + "/search?cql=user.fullname~" + userQuery;
681
682     networkManager->disconnect();
683
684     QNetworkRequest request = createRequest(url);
685
686     connect(networkManager, &QNetworkAccessManager::finished,
687         this, &ConfluenceAgent::userInfoReceived);
688
689     killTimer->start();
690
691     networkManager->get(request);
692 }
693
694 void ConfluenceAgent::userInfoReceived(QNetworkReply *reply)
695 {
696     if (debug) qDebug() << "CA::UserInfopageReceived";
697
698     killTimer->stop();
699     networkManager->disconnect();
700     reply->deleteLater();
701
702     QByteArray fullReply = reply->readAll();
703     if (!wasRequestSuccessful(reply, "receive user info", fullReply))
704         return;
705
706     QJsonDocument jsdoc;
707     jsdoc = QJsonDocument::fromJson(fullReply);
708     pageObj = jsdoc.object();
709     continueJob();
710 }
711
712 void ConfluenceAgent::startGetAttachmentsInfoRequest()
713 {
714     if (debug) qDebug() << "CA::startGetAttachmentIdRequest";
715
716     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment";
717
718     QNetworkRequest request = createRequest(url);
719     request.setRawHeader("X-Atlassian-Token", "no-check");
720
721     connect(networkManager, &QNetworkAccessManager::finished,
722         this, &ConfluenceAgent::attachmentsInfoReceived);
723
724     killTimer->start();
725
726     QNetworkReply *reply = networkManager->get(request);
727 }
728
729 void ConfluenceAgent::attachmentsInfoReceived(QNetworkReply *reply)
730 {
731     if (debug) qDebug() << "CA::attachmentsInfoReceived";
732
733     killTimer->stop();
734     networkManager->disconnect();
735     reply->deleteLater();
736
737     QByteArray fullReply = reply->readAll();
738     if (!wasRequestSuccessful(reply, "get attachment info", fullReply))
739         return;
740
741     QJsonDocument jsdoc;
742     jsdoc = QJsonDocument::fromJson(fullReply);
743
744     attachmentObj = jsdoc.object();
745     int attachmentsCount = jsdoc["size"].toInt();
746     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
747     for (int i = 0; i < attachmentsCount; i++) {
748         attachmentsTitles << jsdoc["results"][i]["title"].toString();
749         attachmentsIds    << jsdoc["results"][i]["id"].toString();
750         //qDebug() << " Title: " << attachmentsTitles.last() << 
751         //            " Id: " << attachmentsIds.last();
752     }
753
754     continueJob();
755 }
756
757 void ConfluenceAgent::startCreateAttachmentRequest()
758 {
759     if (debug) qDebug() << "CA::startCreateAttachmentRequest";
760
761     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment";
762
763     QNetworkRequest request = createRequest(url);
764     request.setRawHeader("X-Atlassian-Token", "no-check");
765
766     QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
767
768
769     QHttpPart imagePart;
770     imagePart.setHeader(
771             QNetworkRequest::ContentDispositionHeader,
772
773             // Name must be "file"
774             QVariant(
775                 QString("form-data; name=\"file\"; filename=\"%1\"")
776                     .arg(currentAttachmentTitle)));
777     imagePart.setHeader(
778             QNetworkRequest::ContentTypeHeader,
779             QVariant("image/jpeg"));
780
781     QFile *file = new QFile(currentAttachmentPath);
782     if (!file->open(QIODevice::ReadOnly)) {
783         qWarning() << "Problem opening attachment: " << currentAttachmentPath;
784         QMessageBox::warning(
785             nullptr, tr("Warning"),
786             QString("Could not open attachment file \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
787         finishJob();
788         return;
789     }
790     imagePart.setBodyDevice(file);
791     /*
792     qDebug() << "      title=" << currentAttachmentTitle;
793     qDebug() << "       path=" << currentAttachmentPath;
794     qDebug() << "        url=" << url;
795     qDebug() << "  file size=" << file->size();
796     */
797     multiPart->append(imagePart);
798     file->setParent(multiPart); // delete later with the multiPart
799
800     connect(networkManager, &QNetworkAccessManager::finished,
801         this, &ConfluenceAgent::attachmentCreated);
802
803     killTimer->start();
804
805     QNetworkReply *reply = networkManager->post(request, multiPart);
806
807     multiPart->setParent(reply);
808 }
809
810 void ConfluenceAgent::attachmentCreated(QNetworkReply *reply)
811 {
812     if (debug) qDebug() << "CA::attachmentCreated";
813
814     killTimer->stop();
815     networkManager->disconnect();
816     reply->deleteLater();
817
818     QByteArray fullReply = reply->readAll();
819     if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
820         if (fullReply.contains(
821                     QString("Cannot add a new attachment with same file name as an existing attachment").toLatin1())) {
822             // Replace existing attachment
823             qWarning() << "Attachment with name " << currentAttachmentTitle << " already exists.";
824             qWarning() << "AttachmentID unknown, stopping now"; 
825
826             finishJob();
827             return;
828         }
829         if (!wasRequestSuccessful(reply, "create attachment", fullReply))
830             return;
831     }
832
833     QJsonDocument jsdoc;
834     jsdoc = QJsonDocument::fromJson(fullReply);
835     attachmentObj = jsdoc.object();
836
837     //qDebug() << "CA::attachmentCreated Successful:";
838     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
839     //cout << attachmentObj["results"].toArray().toStdString();
840
841     currentUploadAttachmentIndex++;
842
843     continueJob(2);
844 }
845
846 void ConfluenceAgent::startUpdateAttachmentRequest()
847 {
848     if (debug) qDebug() << "CA::startUpdateAttachmentRequest";
849
850     for (int i = 0; i < attachmentsTitles.count(); i++) {
851         // qDebug() << "     - " << attachmentsTitles.at(i);
852         if (attachmentsTitles.at(i) == currentAttachmentTitle) {
853             currentAttachmentId = attachmentsIds.at(i);
854             break;
855         }
856     }
857
858     if (currentAttachmentId.isEmpty()) {
859         QMessageBox::warning(
860             nullptr, tr("Warning"),
861             QString("Could not find existing attachment \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
862         finishJob();
863         return;
864     }
865
866     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment/" + currentAttachmentId + "/data";
867
868     QNetworkRequest request = createRequest(url);
869     request.setRawHeader("X-Atlassian-Token", "no-check");
870
871     QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
872
873     QHttpPart imagePart;
874     imagePart.setHeader(
875             QNetworkRequest::ContentDispositionHeader,
876
877             // Name must be "file"
878             QVariant(
879                 QString("form-data; name=\"file\"; filename=\"%1\"")
880                     .arg(currentAttachmentTitle)));
881     imagePart.setHeader(
882             QNetworkRequest::ContentTypeHeader,
883             QVariant("image/jpeg"));
884
885     QFile *file = new QFile(currentAttachmentPath);
886     if (!file->open(QIODevice::ReadOnly)) {
887         qWarning() << "Problem opening attachment: " << currentAttachmentPath;
888         QMessageBox::warning(
889             nullptr, tr("Warning"),
890             QString("Could not open attachment file \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
891         finishJob();
892         return;
893     }
894     imagePart.setBodyDevice(file);
895     /*
896     qDebug() << "      title=" << currentAttachmentTitle;
897     qDebug() << "       path=" << currentAttachmentPath;
898     qDebug() << "        url=" << url;
899     qDebug() << "  file size=" << file->size();
900     */
901     multiPart->append(imagePart);
902     file->setParent(multiPart);
903
904     connect(networkManager, &QNetworkAccessManager::finished,
905         this, &ConfluenceAgent::attachmentUpdated);
906
907     killTimer->start();
908
909     QNetworkReply *reply = networkManager->post(request, multiPart);
910
911     multiPart->setParent(reply);
912 }
913
914 void ConfluenceAgent::attachmentUpdated(QNetworkReply *reply)
915 {
916     if (debug) qDebug() << "CA::attachmentUpdated";
917
918     killTimer->stop();
919     networkManager->disconnect();
920     reply->deleteLater();
921
922     QByteArray fullReply = reply->readAll();
923     if (!wasRequestSuccessful(reply, "update attachment", fullReply))
924         return;
925
926     QJsonDocument jsdoc;
927     jsdoc = QJsonDocument::fromJson(fullReply);
928     attachmentObj = jsdoc.object();
929
930     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
931
932     currentUploadAttachmentIndex++;
933
934     continueJob(2);
935 }
936
937 void ConfluenceAgent::attachmentsUploadSuccess() // slot called from attachmentsAgent
938 {
939     continueJob();
940 }
941
942 void ConfluenceAgent::attachmentsUploadFailure() // slot called from attachmentsAgent
943 {
944     qWarning() << "CA::attachmentsUpload failed";
945     finishJob();
946 }
947
948 bool ConfluenceAgent::wasRequestSuccessful(QNetworkReply *reply, const QString &requestDesc, const QByteArray &fullReply)
949 {
950     if (reply->error()) {
951
952         // Additionally print full error on console
953         qWarning() << "         Step: " << requestDesc;
954         qWarning() << "        Error: " << reply->error();
955         qWarning() << "  Errorstring: " <<  reply->errorString();
956
957         qDebug() << "    Request Url: " << reply->url() ;
958         qDebug() << "      Operation: " << reply->operation() ;
959
960         qDebug() << "      readAll: ";
961         QJsonDocument jsdoc;
962         jsdoc = QJsonDocument::fromJson(fullReply);
963         QString fullReplyFormatted = QString(jsdoc.toJson(QJsonDocument::Indented));
964         cout << fullReplyFormatted.toStdString();
965
966         /*
967         qDebug() << "Request headers: ";
968         QList<QByteArray> reqHeaders = reply->rawHeaderList();
969         foreach( QByteArray reqName, reqHeaders )
970         {
971             QByteArray reqValue = reply->rawHeader( reqName );
972             qDebug() << "  " << reqName << ": " << reqValue;
973         }
974         */
975
976         if (reply->error() == QNetworkReply::AuthenticationRequiredError)
977             QMessageBox::warning(
978                 nullptr, tr("Warning"),
979                 tr("Authentication problem when contacting Confluence") + "\n\n" + 
980                 requestDesc);
981         else {
982             QString msg = QString("QNetworkReply error when trying to \"%1\"\n\n").arg(requestDesc);
983             WarningDialog warn;
984             warn.setText(msg + "\n\n" + fullReplyFormatted);
985             warn.showCancelButton(false);
986             warn.exec();
987         }
988
989         finishJob();
990         return false;
991     } else
992         return true;
993 }
994
995 void ConfluenceAgent::timeout()
996 {
997     qWarning() << "ConfluenceAgent timeout!!   jobType = " << jobType;
998 }
999
1000 #ifndef QT_NO_SSL
1001 void ConfluenceAgent::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
1002 {
1003     QString errorString;
1004     foreach (const QSslError &error, errors) {
1005         if (!errorString.isEmpty())
1006             errorString += '\n';
1007         errorString += error.errorString();
1008     }
1009
1010     reply->ignoreSslErrors();
1011     qWarning() << "ConfluenceAgent: One or more SSL errors has occurred: " << errorString;
1012     qWarning() << "Errors ignored.";
1013 }
1014 #endif