]> git.sven.stormbind.net Git - sven/vym.git/blob - src/confluence-agent.cpp
Replace Pierre as the maintainer
[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                 // cout << QJsonDocument(pageObj).toJson(QJsonDocument::Indented).toStdString();
256                 model = mainWindow->getModel(modelID);
257                 if (model) {
258                     pageURL = QString("https://%1/pages/viewpage.action?pageId=%2")
259                         .arg(baseURL).arg(pageObj["id"].toString());
260                     QString command = QString("vym.currentMap().exportMap(\"ConfluenceUpdatePage\",\"%1\")")
261                         .arg(pageURL);
262                     QString dest = QString("Page title: \"%1\"\nUrl: \"%2\"")
263                         .arg(pageObj["title"].toString()).arg(pageURL);
264                     QString desc = tr("Update existing confluence page");
265                     model->setExportLastCommand(command);
266                     model->setExportLastDestination(dest);
267                     model->setExportLastDescription(desc);
268                     mainWindow->updateActions();
269                 }
270
271                 mainWindow->statusMessage(
272                     QString("Created Confluence page %1").arg(pageURL));
273
274
275                 finishJob();
276                 return;
277             }
278             unknownStepWarningFinishJob();
279             return;
280
281         case UpdatePage:
282             if (jobStep == 1) {
283                 if (pageURL.isEmpty()) {
284                     qWarning() << "CA::contJob UpdatePage: pageURL is empty";
285                     finishJob();
286                     return;
287                 }
288
289                 mainWindow->statusMessage(
290                     QString("Starting to update Confluence page %1").arg(pageURL));
291
292                 // Check if page with url already exists and get pageID, spaceKey
293                 startGetPageSourceRequest(pageURL);
294                 return;
295             }
296             if (jobStep == 2) {
297                 // Get title, which is required by Confluence to update a page
298                 startGetPageDetailsRequest();
299                 return;
300             }
301             if (jobStep == 3) {
302                 // Upload attachments?
303                 if (uploadAttachmentPaths.count() > 0) {
304                     attachmentsAgent = new ConfluenceAgent;
305                     attachmentsAgent->setJobType(ConfluenceAgent::UploadAttachments);
306                     attachmentsAgent->pageID = pageID;
307                     attachmentsAgent->uploadAttachmentPaths = uploadAttachmentPaths;
308
309                     connect(attachmentsAgent, &ConfluenceAgent::attachmentsSuccess,
310                         this, &ConfluenceAgent::attachmentsUploadSuccess);
311                     connect(attachmentsAgent, &ConfluenceAgent::attachmentsFailure,
312                         this, &ConfluenceAgent::attachmentsUploadFailure);
313                     attachmentsAgent->startJob();
314                     return;
315                 }
316                 jobStep++;
317             }
318             if (jobStep == 4) {
319                 // Update page with parent url
320                 if (newPageName.isEmpty())
321                         newPageName = pageObj["title"].toString();
322                 startUpdatePageRequest();
323                 return;
324             }
325             if (jobStep == 5) {
326                 //qDebug() << "CA::finished  Updated page with ID: " << pageObj["id"].toString();
327                 mainWindow->statusMessage(
328                     QString("Updated Confluence page %1").arg(pageURL));
329
330                 model = mainWindow->getModel(modelID);
331                 if (model) {
332                     pageURL = QString("https://%1/pages/viewpage.action?pageId=%2")
333                         .arg(baseURL).arg(pageObj["id"].toString());
334                     QString command = QString("vym.currentMap().exportMap(\"ConfluenceUpdatePage\",\"%1\")")
335                         .arg(pageURL);
336                     QString dest = QString("Page title: \"%1\"\nUrl: \"%2\"").arg(pageObj["title"].toString())
337                         .arg(pageURL);
338                     QString desc = tr("Update existing confluence page");
339                     model->setExportLastCommand(command);
340                     model->setExportLastDestination(dest);
341                     model->setExportLastDescription(desc);
342                     mainWindow->updateActions();
343                 }
344                 finishJob();
345                 return;
346             }
347             unknownStepWarningFinishJob();
348             return;
349
350         case GetUserInfo:
351             if (jobStep == 1) {
352                 // qDebug() << "CA:: begin getting UserInfo";
353                 startGetUserInfoRequest();
354                 return;
355             }
356             if (jobStep == 2) {
357                 QJsonArray array = pageObj["results"].toArray();
358                 QJsonObject userObj;
359                 QJsonObject u;
360                 ConfluenceUser user;
361                 userList.clear();
362                 for (int i = 0; i < array.size(); ++i) {
363                     userObj = array[i].toObject();
364
365                     u = userObj["user"].toObject();
366                     user.setTitle( userObj["title"].toString());
367                     user.setURL( "https://" + baseURL + "/"
368                             + "display/~" + u["username"].toString());
369                     user.setUserKey( u["userKey"].toString());
370                     user.setUserName( u["username"].toString());
371                     user.setDisplayName( u["displayName"].toString());
372                     userList << user;
373                 }
374                 emit (foundUsers(userList));
375                 finishJob();
376                 return;
377             }
378             unknownStepWarningFinishJob();
379             return;
380
381         case UploadAttachments:
382             if (jobStep == 1) {
383
384                 if (uploadAttachmentPaths.count() <= 0) {
385                     qWarning() << "ConfluenceAgent: No attachments to upload!";
386                     emit(attachmentsFailure());
387                     finishJob();
388                     return;
389                 }
390
391                 // Prepare to upload first attachment in list
392                 currentUploadAttachmentIndex = 0;
393
394                 // Try to get info for attachments
395                 startGetAttachmentsInfoRequest();
396                 return;
397             }
398             if (jobStep == 2) {
399                 // Entry point for looping over list of attachments to upload
400
401                 if (currentUploadAttachmentIndex >= uploadAttachmentPaths.count()) {
402                     // All uploaded, let's finish uploading
403                     emit(attachmentsSuccess());
404                     finishJob();
405                 } else {
406                     currentAttachmentPath = uploadAttachmentPaths.at(currentUploadAttachmentIndex);
407                     currentAttachmentTitle = basename(currentAttachmentPath);
408
409                     // Create attachment with image of map, if required
410                     if (attachmentsTitles.count() == 0 || 
411                         !attachmentsTitles.contains(currentAttachmentTitle)) {
412                         // Create new attachment
413                         startCreateAttachmentRequest();
414                     } else {
415                         // Update existing attachment
416                         startUpdateAttachmentRequest();
417                     }
418                 }
419                 return;
420             }
421             unknownStepWarningFinishJob();
422             return;
423
424         default:
425             qWarning() << "ConfluenceAgent::continueJob   unknown jobType " << jobType;
426     }
427 }
428
429 void ConfluenceAgent::finishJob()
430 {
431     deleteLater();
432 }
433
434 void ConfluenceAgent::unknownStepWarningFinishJob()
435 {
436     qWarning() << "CA::contJob  unknown step in jobType = " 
437         << jobType 
438         << "jobStep = " << jobStep;
439     finishJob();
440 }
441
442 void ConfluenceAgent::getUsers(const QString &usrQuery)
443 {
444     userQuery = usrQuery;
445     if (usrQuery.contains(QRegExp("\\W+"))) {
446         qWarning() << "ConfluenceAgent::getUsers  Forbidden characters in " << usrQuery;
447         return;
448     }
449
450     setJobType(GetUserInfo);
451     startJob();
452 }
453
454 QNetworkRequest ConfluenceAgent::createRequest(const QUrl &url)
455 {
456     QNetworkRequest request = QNetworkRequest(url);
457
458     QString headerData;
459     if (authUsingPAT)
460         headerData = QString("Bearer %1").arg(personalAccessToken);
461     else {
462         QString concatenated = username + ":" + password;
463         QByteArray data = concatenated.toLocal8Bit().toBase64();
464         headerData = "Basic " + data;
465     }
466     request.setRawHeader("Authorization", headerData.toLocal8Bit());
467
468     return request;
469 }
470
471 void ConfluenceAgent::startGetPageSourceRequest(QUrl requestedURL)
472 {
473     //qDebug() << "CA::startGetPageSourceRequest " << requestedURL;
474     if (!requestedURL.toString().startsWith("http"))
475         requestedURL.setPath("https://" + requestedURL.path());
476
477     QUrl url = requestedURL;
478
479     QNetworkRequest request = createRequest(url);
480
481     if (debug)
482         qDebug() << "CA::startGetPageSourceRequest: url = " + request.url().toString();
483
484     killTimer->start();
485
486     connect(networkManager, &QNetworkAccessManager::finished,
487         this, &ConfluenceAgent::pageSourceReceived);
488
489     networkManager->get(request);
490 }
491
492 void ConfluenceAgent::pageSourceReceived(QNetworkReply *reply)
493 {
494     if (debug) qDebug() << "CA::pageSourceReceived";
495
496     killTimer->stop();
497     networkManager->disconnect();
498     reply->deleteLater();
499
500     QByteArray fullReply = reply->readAll();
501     if (!wasRequestSuccessful(reply, "receive page source", fullReply))
502         return;
503
504     // Find pageID
505     QRegExp rx("\\sname=\"ajs-page-id\"\\scontent=\"(\\d*)\"");
506     rx.setMinimal(true);
507
508     if (rx.indexIn(fullReply, 0) != -1) {
509         pageID = rx.cap(1);
510     }
511     else {
512         qWarning()
513             << "ConfluenceAgent::pageSourceReveived Couldn't find page ID";
514         //qWarning() << fullReply;
515         return;
516     }
517
518     // Find spaceKey 
519     rx.setPattern("meta\\s*id=\"confluence-space-key\"\\s* "
520                   "name=\"confluence-space-key\"\\s*content=\"(.*)\"");
521     if (rx.indexIn(fullReply, 0) != -1) {
522         spaceKey = rx.cap(1);
523     }
524     else {
525         qWarning() << "ConfluenceAgent::pageSourceReveived Couldn't find "
526                       "space key in response";
527         qWarning() << fullReply;
528         finishJob();
529         return;
530     }
531
532     const QVariant redirectionTarget =
533         reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
534
535     continueJob();
536 }
537
538 void ConfluenceAgent::startGetPageDetailsRequest()
539 {
540     if (debug) qDebug() << "CA::startGetPageDetailsRequest" << pageID;
541
542     // Authentication in URL  (only SSL!)
543     QString url = "https://" 
544         + baseURL + apiURL 
545         + "/content/" + pageID + "?expand=metadata.labels,version";
546
547     QNetworkRequest request = createRequest(url);
548
549     connect(networkManager, &QNetworkAccessManager::finished,
550         this, &ConfluenceAgent::pageDetailsReceived);
551
552     killTimer->start();
553
554     networkManager->get(request);
555 }
556
557 void ConfluenceAgent::pageDetailsReceived(QNetworkReply *reply)
558 {
559     if (debug) qDebug() << "CA::pageDetailsReceived";
560
561     killTimer->stop();
562     networkManager->disconnect();
563     reply->deleteLater();
564
565     QByteArray fullReply = reply->readAll();
566     if (!wasRequestSuccessful(reply, "receive page details", fullReply))
567         return;
568
569     QJsonDocument jsdoc;
570     jsdoc = QJsonDocument::fromJson(fullReply);
571
572     pageObj = jsdoc.object();
573     // cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
574
575     continueJob();
576 }
577
578 void ConfluenceAgent::startCreatePageRequest()
579 {
580     // qDebug() << "CA::startCreatePageRequest";
581
582     QString url = "https://" + baseURL + apiURL + "/content";
583
584     QNetworkRequest request = createRequest(url);
585     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
586
587     QJsonObject payload;
588     payload["type"] = "page";
589     payload["title"] = newPageName;
590
591     // Build array with ID of parent page
592     QJsonObject ancestorsID;
593     ancestorsID["id"] = pageID;
594     QJsonArray ancestorsArray;
595     ancestorsArray.append(ancestorsID);
596     payload["ancestors"] = ancestorsArray;
597
598     // Build object with space key
599     QJsonObject skey;
600     skey["key"] = spaceKey;
601     payload["space"] = skey;
602
603     // Build body
604     QString body;
605     if (!loadStringFromDisk(uploadPagePath, body))
606     {
607         qWarning() << "ConfluenceAgent: Couldn't read file to upload:" << uploadPagePath;
608         finishJob();
609         return;
610     }
611
612     QJsonObject innerStorageObj
613     {
614         {"value", body},
615         {"representation", "storage"}
616     };
617     QJsonObject outerStorageObj;
618     outerStorageObj["storage"] = innerStorageObj;
619     payload["body"] = outerStorageObj;
620
621     QJsonDocument doc(payload);
622     QByteArray data = doc.toJson();
623
624     connect(networkManager, &QNetworkAccessManager::finished,
625         this, &ConfluenceAgent::pageUploaded);
626
627     killTimer->start();
628
629     networkManager->post(request, data);
630 }
631
632 void ConfluenceAgent::startUpdatePageRequest()
633 {
634     if (debug) qDebug() << "CA::startUpdatePageRequest";
635
636     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID;
637
638     QNetworkRequest request = createRequest(url);
639
640     request.setHeader(
641         QNetworkRequest::ContentTypeHeader,
642         "application/json; charset=utf-8");
643
644     QJsonObject payload;
645     payload["id"] = pageID;
646     payload["type"] = "page";
647     payload["title"] = newPageName;
648
649     // Build version object
650     QJsonObject newVersionObj;
651     QJsonObject oldVersionObj = pageObj["version"].toObject();
652
653     newVersionObj["number"] = oldVersionObj["number"].toInt() + 1;
654     payload["version"] = newVersionObj;
655
656     // Build object with space key
657     QJsonObject skey;
658     skey["key"] = spaceKey;
659     payload["space"] = skey;
660
661     // Build body
662     QString body;
663     if (!loadStringFromDisk(uploadPagePath, body))
664     {
665         qWarning() << "ConfluenceAgent: Couldn't read file to upload:" << uploadPagePath;
666         finishJob();
667         return;
668     }
669
670     QJsonObject innerStorageObj
671     {
672         {"value", body},
673         {"representation", "storage"}
674     };
675     QJsonObject outerStorageObj;
676     outerStorageObj["storage"] = innerStorageObj;
677     payload["body"] = outerStorageObj;
678
679     QJsonDocument doc(payload);
680     QByteArray data = doc.toJson();
681
682     connect(networkManager, &QNetworkAccessManager::finished,
683         this, &ConfluenceAgent::pageUploaded);
684
685     killTimer->start();
686
687     networkManager->put(request, data);
688 }
689
690 void ConfluenceAgent::pageUploaded(QNetworkReply *reply)
691 {
692     if (debug) qDebug() << "CA::pageUploaded";
693
694     killTimer->stop();
695     networkManager->disconnect();
696     reply->deleteLater();
697
698     QByteArray fullReply = reply->readAll();
699     if (!wasRequestSuccessful(reply, "upload page", fullReply))
700         return;
701
702     QJsonDocument jsdoc;
703     jsdoc = QJsonDocument::fromJson(fullReply);
704     pageObj = jsdoc.object();
705     // cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
706     continueJob();
707 }
708
709 void ConfluenceAgent::startGetUserInfoRequest()
710 {
711     if (debug) qDebug() << "CA::startGetInfoRequest for " << userQuery;
712
713     QString url = "https://" + baseURL + apiURL
714         + "/search?cql=user.fullname~" + userQuery;
715
716     networkManager->disconnect();
717
718     QNetworkRequest request = createRequest(url);
719
720     connect(networkManager, &QNetworkAccessManager::finished,
721         this, &ConfluenceAgent::userInfoReceived);
722
723     killTimer->start();
724
725     networkManager->get(request);
726 }
727
728 void ConfluenceAgent::userInfoReceived(QNetworkReply *reply)
729 {
730     if (debug) qDebug() << "CA::UserInfopageReceived";
731
732     killTimer->stop();
733     networkManager->disconnect();
734     reply->deleteLater();
735
736     QByteArray fullReply = reply->readAll();
737     if (!wasRequestSuccessful(reply, "receive user info", fullReply))
738         return;
739
740     QJsonDocument jsdoc;
741     jsdoc = QJsonDocument::fromJson(fullReply);
742     pageObj = jsdoc.object();
743     continueJob();
744 }
745
746 void ConfluenceAgent::startGetAttachmentsInfoRequest()
747 {
748     if (debug) qDebug() << "CA::startGetAttachmentIdRequest";
749
750     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment";
751
752     QNetworkRequest request = createRequest(url);
753     request.setRawHeader("X-Atlassian-Token", "no-check");
754
755     connect(networkManager, &QNetworkAccessManager::finished,
756         this, &ConfluenceAgent::attachmentsInfoReceived);
757
758     killTimer->start();
759
760     QNetworkReply *reply = networkManager->get(request);
761 }
762
763 void ConfluenceAgent::attachmentsInfoReceived(QNetworkReply *reply)
764 {
765     if (debug) qDebug() << "CA::attachmentsInfoReceived";
766
767     killTimer->stop();
768     networkManager->disconnect();
769     reply->deleteLater();
770
771     QByteArray fullReply = reply->readAll();
772     if (!wasRequestSuccessful(reply, "get attachment info", fullReply))
773         return;
774
775     QJsonDocument jsdoc;
776     jsdoc = QJsonDocument::fromJson(fullReply);
777
778     attachmentObj = jsdoc.object();
779     int attachmentsCount = jsdoc["size"].toInt();
780     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
781     for (int i = 0; i < attachmentsCount; i++) {
782         attachmentsTitles << jsdoc["results"][i]["title"].toString();
783         attachmentsIds    << jsdoc["results"][i]["id"].toString();
784         //qDebug() << " Title: " << attachmentsTitles.last() << 
785         //            " Id: " << attachmentsIds.last();
786     }
787
788     continueJob();
789 }
790
791 void ConfluenceAgent::startCreateAttachmentRequest()
792 {
793     if (debug) qDebug() << "CA::startCreateAttachmentRequest";
794
795     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment";
796
797     QNetworkRequest request = createRequest(url);
798     request.setRawHeader("X-Atlassian-Token", "no-check");
799
800     QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
801
802
803     QHttpPart imagePart;
804     imagePart.setHeader(
805             QNetworkRequest::ContentDispositionHeader,
806
807             // Name must be "file"
808             QVariant(
809                 QString("form-data; name=\"file\"; filename=\"%1\"")
810                     .arg(currentAttachmentTitle)));
811     imagePart.setHeader(
812             QNetworkRequest::ContentTypeHeader,
813             QVariant("image/jpeg"));
814
815     QFile *file = new QFile(currentAttachmentPath);
816     if (!file->open(QIODevice::ReadOnly)) {
817         qWarning() << "Problem opening attachment: " << currentAttachmentPath;
818         QMessageBox::warning(
819             nullptr, tr("Warning"),
820             QString("Could not open attachment file \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
821         finishJob();
822         return;
823     }
824     imagePart.setBodyDevice(file);
825     /*
826     qDebug() << "      title=" << currentAttachmentTitle;
827     qDebug() << "       path=" << currentAttachmentPath;
828     qDebug() << "        url=" << url;
829     qDebug() << "  file size=" << file->size();
830     */
831     multiPart->append(imagePart);
832     file->setParent(multiPart); // delete later with the multiPart
833
834     connect(networkManager, &QNetworkAccessManager::finished,
835         this, &ConfluenceAgent::attachmentCreated);
836
837     killTimer->start();
838
839     QNetworkReply *reply = networkManager->post(request, multiPart);
840
841     multiPart->setParent(reply);
842 }
843
844 void ConfluenceAgent::attachmentCreated(QNetworkReply *reply)
845 {
846     if (debug) qDebug() << "CA::attachmentCreated";
847
848     killTimer->stop();
849     networkManager->disconnect();
850     reply->deleteLater();
851
852     QByteArray fullReply = reply->readAll();
853     if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) {
854         if (fullReply.contains(
855                     QString("Cannot add a new attachment with same file name as an existing attachment").toLatin1())) {
856             // Replace existing attachment
857             qWarning() << "Attachment with name " << currentAttachmentTitle << " already exists.";
858             qWarning() << "AttachmentID unknown, stopping now"; 
859
860             finishJob();
861             return;
862         }
863         if (!wasRequestSuccessful(reply, "create attachment", fullReply))
864             return;
865     }
866
867     QJsonDocument jsdoc;
868     jsdoc = QJsonDocument::fromJson(fullReply);
869     attachmentObj = jsdoc.object();
870
871     //qDebug() << "CA::attachmentCreated Successful:";
872     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
873     //cout << attachmentObj["results"].toArray().toStdString();
874
875     currentUploadAttachmentIndex++;
876
877     continueJob(2);
878 }
879
880 void ConfluenceAgent::startUpdateAttachmentRequest()
881 {
882     if (debug) qDebug() << "CA::startUpdateAttachmentRequest";
883
884     for (int i = 0; i < attachmentsTitles.count(); i++) {
885         // qDebug() << "     - " << attachmentsTitles.at(i);
886         if (attachmentsTitles.at(i) == currentAttachmentTitle) {
887             currentAttachmentId = attachmentsIds.at(i);
888             break;
889         }
890     }
891
892     if (currentAttachmentId.isEmpty()) {
893         QMessageBox::warning(
894             nullptr, tr("Warning"),
895             QString("Could not find existing attachment \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
896         finishJob();
897         return;
898     }
899
900     QString url = "https://" + baseURL + apiURL + "/content" + "/" + pageID + "/child/attachment/" + currentAttachmentId + "/data";
901
902     QNetworkRequest request = createRequest(url);
903     request.setRawHeader("X-Atlassian-Token", "no-check");
904
905     QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
906
907     QHttpPart imagePart;
908     imagePart.setHeader(
909             QNetworkRequest::ContentDispositionHeader,
910
911             // Name must be "file"
912             QVariant(
913                 QString("form-data; name=\"file\"; filename=\"%1\"")
914                     .arg(currentAttachmentTitle)));
915     imagePart.setHeader(
916             QNetworkRequest::ContentTypeHeader,
917             QVariant("image/jpeg"));
918
919     QFile *file = new QFile(currentAttachmentPath);
920     if (!file->open(QIODevice::ReadOnly)) {
921         qWarning() << "Problem opening attachment: " << currentAttachmentPath;
922         QMessageBox::warning(
923             nullptr, tr("Warning"),
924             QString("Could not open attachment file \"%1\" in page with ID: %2").arg(currentAttachmentTitle).arg(pageID));
925         finishJob();
926         return;
927     }
928     imagePart.setBodyDevice(file);
929     /*
930     qDebug() << "      title=" << currentAttachmentTitle;
931     qDebug() << "       path=" << currentAttachmentPath;
932     qDebug() << "        url=" << url;
933     qDebug() << "  file size=" << file->size();
934     */
935     multiPart->append(imagePart);
936     file->setParent(multiPart);
937
938     connect(networkManager, &QNetworkAccessManager::finished,
939         this, &ConfluenceAgent::attachmentUpdated);
940
941     killTimer->start();
942
943     QNetworkReply *reply = networkManager->post(request, multiPart);
944
945     multiPart->setParent(reply);
946 }
947
948 void ConfluenceAgent::attachmentUpdated(QNetworkReply *reply)
949 {
950     if (debug) qDebug() << "CA::attachmentUpdated";
951
952     killTimer->stop();
953     networkManager->disconnect();
954     reply->deleteLater();
955
956     QByteArray fullReply = reply->readAll();
957     if (!wasRequestSuccessful(reply, "update attachment", fullReply))
958         return;
959
960     QJsonDocument jsdoc;
961     jsdoc = QJsonDocument::fromJson(fullReply);
962     attachmentObj = jsdoc.object();
963
964     //cout << jsdoc.toJson(QJsonDocument::Indented).toStdString();
965
966     currentUploadAttachmentIndex++;
967
968     continueJob(2);
969 }
970
971 void ConfluenceAgent::attachmentsUploadSuccess() // slot called from attachmentsAgent
972 {
973     continueJob();
974 }
975
976 void ConfluenceAgent::attachmentsUploadFailure() // slot called from attachmentsAgent
977 {
978     qWarning() << "CA::attachmentsUpload failed";
979     finishJob();
980 }
981
982 bool ConfluenceAgent::wasRequestSuccessful(QNetworkReply *reply, const QString &requestDesc, const QByteArray &fullReply)
983 {
984     if (reply->error()) {
985
986         // Additionally print full error on console
987         qWarning() << "         Step: " << requestDesc;
988         qWarning() << "        Error: " << reply->error();
989         qWarning() << "  Errorstring: " <<  reply->errorString();
990
991         qDebug() << "    Request Url: " << reply->url() ;
992         qDebug() << "      Operation: " << reply->operation() ;
993
994         qDebug() << "      readAll: ";
995         QJsonDocument jsdoc;
996         jsdoc = QJsonDocument::fromJson(fullReply);
997         QString fullReplyFormatted = QString(jsdoc.toJson(QJsonDocument::Indented));
998         cout << fullReplyFormatted.toStdString();
999
1000         /*
1001         qDebug() << "Request headers: ";
1002         QList<QByteArray> reqHeaders = reply->rawHeaderList();
1003         foreach( QByteArray reqName, reqHeaders )
1004         {
1005             QByteArray reqValue = reply->rawHeader( reqName );
1006             qDebug() << "  " << reqName << ": " << reqValue;
1007         }
1008         */
1009
1010         if (reply->error() == QNetworkReply::AuthenticationRequiredError)
1011             QMessageBox::warning(
1012                 nullptr, tr("Warning"),
1013                 tr("Authentication problem when contacting Confluence") + "\n\n" + 
1014                 requestDesc);
1015         else {
1016             QString msg = QString("QNetworkReply error when trying to \"%1\"\n\n").arg(requestDesc);
1017             WarningDialog warn;
1018             warn.setText(msg + "\n\n" + fullReplyFormatted);
1019             warn.showCancelButton(false);
1020             warn.exec();
1021         }
1022
1023         finishJob();
1024         return false;
1025     } else
1026         return true;
1027 }
1028
1029 void ConfluenceAgent::timeout()
1030 {
1031     qWarning() << "ConfluenceAgent timeout!!   jobType = " << jobType;
1032 }
1033
1034 #ifndef QT_NO_SSL
1035 void ConfluenceAgent::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
1036 {
1037     QString errorString;
1038     foreach (const QSslError &error, errors) {
1039         if (!errorString.isEmpty())
1040             errorString += '\n';
1041         errorString += error.errorString();
1042     }
1043
1044     reply->ignoreSslErrors();
1045     qWarning() << "ConfluenceAgent: One or more SSL errors has occurred: " << errorString;
1046     qWarning() << "Errors ignored.";
1047 }
1048 #endif