· 4 years ago · May 26, 2021, 07:28 AM
1// ==UserScript==
2 // @name MCM2Wiki
3 // @namespace https://mcm.amazon.com/*
4 // @updateURL https://code.amazon.com/packages/FMAMisc/blobs/mainline/--/scripts/MCM2Wiki.user.js?raw=1
5 // @downloadURL https://code.amazon.com/packages/FMAMisc/blobs/mainline/--/scripts/MCM2Wiki.user.js?raw=1
6 // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js
7 // @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.26/moment-timezone.min.js
8 // @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.26/moment-timezone-with-data-10-year-range.min.js
9 // @description Greasemonkey script for exporting MCM related data to Amazon Deployment Wiki
10 // @author Archit Sanghvi (asanghv@amazon.com)
11 // @version 1.0
12 // @match https://mcm.amazon.com/cms/*
13 // @connect w.amazon.com
14 // @connect maxis-service-prod-iad.amazon.com
15 // @connect kerberos.amazon.com
16 // @connect *
17 // @grant GM_xmlhttpRequest
18 // @grant GM.addStyle
19 // ==/UserScript==
20
21 var resultcontent;
22 var buttonLabel;
23 var appTitle = 'Export to Wiki...';
24 let wikiAddr = "FMA/Projects/asanghv-testing"
25 let wikiResult;
26 let wikiTitle;
27 let title;
28 let mcmFriendlyIdentifier;
29
30
31 GM.addStyle (`
32 .modal-dialog {
33 width: 700px;
34 }
35
36 .modal {
37 position: fixed;
38 width: 100%;
39 height: 100vh;
40 top: 50%;
41 transform: translateY(-50%);
42 }
43
44 `);
45 injectData();
46
47 function injectData(){
48 let buttonHtml = `
49 <br><button id="addToWikiButton" class="label custom-label label-success">Add to Wiki</button>
50 `;
51 let modalHtml = `
52 <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
53 <div class="modal-dialog" role="document">
54 <div class="modal-content">
55 <div class="modal-header">
56 <h2 class="modal-title" id="exampleModalLabel">Fill Data</h2>
57 </div>
58 <div class="modal-body">
59 <form id="wikiForm" action="javascript:0">
60 <div class="form-group">
61 <h3>Enter Label [FORT/Datapath/Precompute/Weblab/G2S2manual/Other]</h3>
62 <input style="height: 30px;" type="text" class="form-control" id="wikiLabel" required>
63 </div>
64 <div class="form-group">
65 <h3>Enter Locale</h3>
66 <input style="height: 30px;" type="text" class="form-control" id="wikiLocale" required placeholder="E.g. IT, UK, DE, FR">
67 </div>
68 <div class="form-group">
69 <h3>Enter feature details</h3>
70 <input style="height: 30px;" type="text" class="form-control" id="wikiFeature" required>
71 </div>
72 <div class="form-group">
73 <h3>Enter SIM ID (Optional)</h3>
74 <input style="height: 30px;" type="text" class="form-control" id="wikiSIMId" placeholder="E.g. FMA-9999">
75 </div>
76 <div class="form-group">
77 <h3>Enter CR ID (Optional)</h3>
78 <input style="height: 30px;" type="text" class="form-control" id="wikiCRId" placeholder="E.g. CR-30009027">
79 </div>
80 <div class="modal-footer">
81 <button type="button" id="closeModal" class="btn btn-secondary" data-dismiss="modal">Close</button>
82 <button type="submit" id="wikiModalId" class="btn btn-primary">Submit</button>
83 </div>
84 </form>
85 </div>
86 </div>
87 </div>
88 </div>
89 `
90 let alertModal=`
91 <div class="modal fade" id="alertModal" role="dialog">
92 <div class="modal-dialog">
93 <!-- Modal content-->
94 <div class="modal-content">
95 <div class="modal-header">
96 <h2 class="modal-title">Entry already exists</h2>
97 </div>
98 <div class="modal-body">
99 <h4><p id="alertMessage"></p></h4>
100 </div>
101 <div class="modal-footer">
102 <button type="button" id="alertClose" class="btn btn-secondary" data-dismiss="modal">Close</button>
103 <button type="submit" id="alertSubmit" class="btn btn-primary">Submit</button>
104 </div>
105 </div>
106 </div>
107 </div>
108 `
109 let errorSpan = `
110 <br><br><h4><span id="errorSpanId" style="font-family:verdana;color:red"></span></h4>
111 `;
112
113 $(".page-header").append(buttonHtml)
114 $(".page-header").append(errorSpan)
115 $(".well").append(modalHtml)
116 $(".well").append(alertModal)
117 buttonLabel = $("#addToWikiButton");
118 }
119 $('#closeModal').click(async function() {
120 resetButton();
121 });
122 $('#alertClose').click(async function() {
123 resetButton();
124 });
125
126 $('#alertSubmit').click(async function() {
127 await updateEntry();
128 });
129
130 $('#addToWikiButton').click(async function() {
131 $("#errorSpanId").text("");
132 buttonLabel.html('Checking Network...');
133 buttonLabel.prop('disabled', true);
134 const isConnected = await checkNetwork();
135 if (!isConnected) {
136 const msg = 'Could not connect to Amazon corp network.<br/><br/>\
137 <i>If you are connected via VPN, try reconnecting. Check your kerberos authentication. If that fails, close and re-open your browser. \
138 [<span style="text-decoration:underline"><a href="https://quip-amazon.com/ac1aAdpLbs1F/Why-I-keep-getting-errors-when-exporting-MCM-to-Deployment-Wiki" target="_blank">More details</a></span>]<br/> \
139 </i>'
140 await Prompts.showAlert(appTitle, msg);
141 return;
142 }
143
144 buttonLabel.html('Checking entry...');
145 const htmlCollection = $('.page-header h1').first();
146 mcmFriendlyIdentifier = htmlCollection[0].childNodes[1].innerText.trim();
147 title = htmlCollection[0].childNodes[2].nodeValue.trim().substring(2);
148 //removing locale/label from title
149 title = removeDataWithBracket(title).trim();
150 if(! await checkIfEntryExists()){
151 buttonLabel.html('Gathering data...');
152 $("#wikiFeature").val(title)
153 $("#exampleModal").modal({backdrop: 'static', keyboard: false})
154 }
155 });
156
157 async function gatherScheduleDate(){
158 keyword = "Scheduled Start (UTC):"
159 indexOfKeyword = $("p").text().indexOf(keyword);
160 scheduleStart = $("p").text().slice(keyword.length +indexOfKeyword, keyword.length +indexOfKeyword+25).trim();
161 scheduleStartArray = scheduleStart.split(" ");
162 startDate = moment(scheduleStartArray[1], "DD-MMM-YYYY")
163 year = startDate.format("YYYY");
164 month = startDate.format("MM");
165 day = startDate.format("DD");
166 time = scheduleStartArray[2].replace(":","");
167 dateInUTC = year + month + day + ":" + time;
168 return dateInUTC;
169 }
170
171 let firstHalf;
172 let footer;
173
174 async function addNewEntryAndGetListOfEntries(newEntry=null){
175 var finalWikiContent = wikiResult;
176 startOFEntriesMessage = "=== Start of Entries ===";
177 endOFEntriesMessage = "=== End of Entries ===";
178 startOFEntriesMessageIndex = finalWikiContent.search(startOFEntriesMessage)+startOFEntriesMessage.length;
179 if(finalWikiContent.search(startOFEntriesMessage)==-1 || finalWikiContent.search(endOFEntriesMessage)==-1){
180 throw "Issue with Deployment Wiki Format. Start or End of entries tag missing in deployment wiki"
181 }
182 if(newEntry){
183 finalWikiContent = wikiResult.substring(0,startOFEntriesMessageIndex) + "\n" + newEntry + wikiResult.substring(startOFEntriesMessageIndex);
184 }
185 firstHalf = finalWikiContent.substring(0,startOFEntriesMessageIndex);
186 lengthOfWikiData = finalWikiContent.length;
187 endOFEntriesMessageIndex = finalWikiContent.search(endOFEntriesMessage);
188 footer = finalWikiContent.substring(endOFEntriesMessageIndex,lengthOfWikiData);
189 return finalWikiContent.substring(startOFEntriesMessageIndex,endOFEntriesMessageIndex).trim();
190 }
191
192 async function convertStringEntriesToList(entries){
193 listOfEntries = entries.split("===");
194 listOfEntries = listOfEntries.filter(word => word.trim()!="");
195 listOfEntries = listOfEntries.map(Function.prototype.call, String.prototype.trim);
196 return listOfEntries;
197 }
198
199 async function sortEntries(listOfEntries){
200 dictData = {}
201 for(let i=0;i<listOfEntries.length;i=i+2){
202 dictData[listOfEntries[i]]= "=== "+ listOfEntries[i] + " ===" + "\n\n" + listOfEntries[i+1] + "\n\n";
203 }
204 var keys = Object.keys(dictData);
205 keys.sort().reverse();
206 dataEntry = "";
207 for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
208 var key = keys[i];
209 var value = dictData[key];
210 dataEntry += value;
211 }
212 return dataEntry;
213 }
214
215 $('#wikiModalId').click(async function() {
216 if($('#wikiForm')[0].checkValidity()) {
217 $('#exampleModal').modal('toggle');
218 label = $("#wikiLabel").val()
219 feature = $("#wikiFeature").val()
220 locale = $("#wikiLocale").val()
221 simId = $("#wikiSIMId").val()
222 crId = $("#wikiCRId").val()
223 technician = $(".translatable-req-or-tech")[1].innerText.match(/\((.*)\)/).pop()
224 buttonLabel.html('Receiving Wiki data...');
225 let wikiResponse = await getWikiData();
226 try{
227 if(wikiResponse){
228 scheduleStartDate = await gatherScheduleDate();
229 /**
230 * Creating new Entry
231 */
232 var head = "=== [UTC] " + scheduleStartDate + " [" + label + "] " + locale + " " + title + " ==="
233 var line1 = `* **Release Owner:** [[`+ technician +`@>>url:https://phonetool.amazon.com/users/`+technician+`||rel="nofollow"]] \n`
234 var line2 = `* **MCM** [[`+mcmFriendlyIdentifier+`>>https://mcm.amazon.com/cms/`+mcmFriendlyIdentifier+`]] \n`
235 var line3 = `* **Feature** ` + feature + "\n"
236 var entry = head + "\n\n" + line1 + line2 + line3
237 var line4;
238 var line5;
239 if(simId){
240 line4 = `** **SIM** [[`+simId+`>>url:https://issues.amazon.com/issues/`+simId+`]] \n`
241 entry += line4
242 }
243 if(crId){
244 line5 = `** **CR** [[`+crId+`>>url:https://code.amazon.com/reviews/`+crId+`]] \n`
245 entry += line5
246 }
247 entries = await addNewEntryAndGetListOfEntries(entry);
248 listOfEntries = await convertStringEntriesToList(entries);
249 dataEntry = await sortEntries(listOfEntries);
250 finalData = firstHalf + "\n" + dataEntry + "\n" + footer;
251 await pushToWiki(wikiAddr, finalData);
252 }
253 else{
254 resetButton();
255 }
256 } catch (ex) {
257 $("#errorSpanId").text(`Error while adding entry [MCM2Wiki]: ${ex}`);
258 resetButton();
259 return false;
260 }
261 }
262 });
263
264 async function updateEntry(){
265 try{
266 $('#alertModal').modal('toggle');
267 newScheduleStartDate = await gatherScheduleDate();
268 entries = await addNewEntryAndGetListOfEntries();
269 listOfEntries = await convertStringEntriesToList(entries);
270 for(i=1;i<listOfEntries.length;i=i+2){
271 mcmId = listOfEntries[i].match(/MCM-[0-9]*/)
272 if(mcmId == mcmFriendlyIdentifier){
273 listOfEntries[i-1] = listOfEntries[i-1].replace(/[0-9]*:[0-9]*/,newScheduleStartDate)
274 }
275 }
276 dataEntry = await sortEntries(listOfEntries);
277 finalData = firstHalf + "\n" + dataEntry + "\n" + footer;
278 await pushToWiki(wikiAddr, finalData);
279 }
280 catch (ex) {
281 $("#errorSpanId").text(`Error while adding entry [MCM2Wiki]: ${ex}`);
282 resetButton();
283 return false;
284 }
285 }
286
287 async function checkIfEntryExists(){
288 let wikiResponse = await getWikiData()
289 if(wikiResponse){
290 entryCount = await mcmEntryCount();
291 if(entryCount>1){
292 $("#alertSubmit").hide()
293 $("#alertMessage").html("Multiple entries exists for this MCM. Please update manually if you are trying to update entry.");
294 $('#alertModal').modal({backdrop: 'static', keyboard: false});
295 return true;
296 }
297 else if(entryCount==1){
298 $("#alertMessage").html("Entry for this MCM already exists. Click on submit button to update schedule start date.");
299 $('#alertModal').modal("show");
300 return true;
301 }
302 else{
303 return false;
304 }
305 }
306 else{
307 resetButton();
308 }
309 }
310
311 async function mcmEntryCount(){
312 var regex = new RegExp(mcmFriendlyIdentifier, "g");
313 listOfMCMs = wikiResult.match(regex)
314 if(!listOfMCMs){
315 return 0;
316 }
317 return listOfMCMs.length/2;
318 }
319
320
321 async function checkNetwork () {
322 log.info('Checking network connection..');
323
324 let kerberosResult;
325 try {
326 kerberosResult = await Utils.getKerberosUser(); // check kerberos via kerberos endpoint
327 if (!kerberosResult) {
328 throw `Kerberos test result: ${kerberosResult}`;
329 }
330 } catch (ex) {
331 $("#errorSpanId").text(`Network connection error [kerberos]: ${ex}`);
332 resetButton();
333 return false;
334 }
335
336 let testWikiResult;
337 try {
338 testWikiResult = await WikiClient.testConnection(); // check wiki API connection
339 if (!testWikiResult) {
340 throw `Wiki API test result: ${testWikiResult}`;
341 }
342 } catch (ex) {
343 $("#errorSpanId").text(`Network connection error [wiki-api]: ${ex}`);
344 resetButton();
345 return false;
346 }
347
348 return true;
349 }
350
351 /**
352 * Common dialogs for prompting the user for information of confirmation.
353 */
354 const Prompts = {
355 /**
356 * Shows a modal alert dialog with an OK button.
357 * @param {string} title dialog title
358 * @param {string} text dialog message to be displayed
359 * @param {*} width (optional) dialog width in pixels. Set to null for automatic.
360 * @param {*} height (optional) dialog height in pixels. Set to null for automatic.
361 */
362 async showAlert (title, text, width, height) {
363 const buttons = { OK: true };
364 return await this.showDialog(title, text, buttons, width, height);
365 },
366
367 /**
368 * Shows a modal dialog with customized buttons and return values.
369 * @param {string} title dialog title
370 * @param {string} text dialog message to be displayed
371 * @param {*} buttons object that describes the button label (key) and its return value (value)
372 * @param {*} width (optional) dialog width in pixels. Set to null for automatic.
373 * @param {*} height (optional) dialog height in pixels. Set to null for automatic.
374 */
375 showDialog (title, text, buttons, width, height) {
376 return new Promise((res, rej) => {
377 // make sure every button will close the dialog and return its given value
378 const dialog_buttons = {};
379 Object.keys(buttons).forEach(function(key) {
380 const value = buttons[key];
381 dialog_buttons[key] = function() {
382 $(this).dialog('close');
383 res(value); // returns button value
384 }
385 });
386 // open the dialog
387 $(`<div style="word-wrap: normal; white-space: normal"><p>${text.replace(/\n/g, '<br/>')}</p></div>`).dialog({
388 closeOnEscape: false,
389 open: function() {
390 $(this) // the element being dialogged
391 .parent() // get the dialog widget element
392 .find(".ui-dialog-titlebar-close") // find the close button for this dialog
393 .hide(); // hide it
394 },
395 title: title,
396 modal: true,
397 width: width ? width : 'auto',
398 height: height ? height : 'auto',
399 create: function( event, ui ) {
400 // maxWidth won't work so we set in the css
401 if (!width) {
402 $(this).css("maxWidth", "1200px");
403 }
404 },
405 buttons: dialog_buttons
406 });
407 });
408 },
409 }
410
411 /**
412 * Helper methods.
413 */
414 const Utils = {
415
416 /**
417 * Encondes parameters as an encoded URI.
418 * @param {*} data binary content to be encoded
419 */
420 getEncodedQueryData: function (data) {
421 if (!data) {
422 return '';
423 }
424 const ret = [];
425 for (let d in data) {
426 const value = data[d];
427 if (value == null || value == undefined) {
428 continue;
429 }
430 ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
431 }
432 return ret.join('&');
433 },
434
435 sleep: function (milliseconds) {
436 return new Promise(resolve => setTimeout(resolve, milliseconds))
437 },
438
439 getKerberosUser: function () {
440 const SUCCESS_REGEX = /Success: You are logged on as ([a-zA-Z0-9._%+-]+)\./;
441 const kerberos_call = (res, rej) => {
442 GM_xmlhttpRequest({
443 method: "GET",
444 url: "https://kerberos.amazon.com/",
445 timeout: 5000,
446 ontimeout: () => rej('Get user timeout'),
447 onload: function(response) {
448 if (response.status == 200) {
449 const responseHTML = new DOMParser().parseFromString(response.responseText, 'text/html');
450 for (const element of responseHTML.querySelectorAll('h2')) {
451 const match = element.innerHTML.match(SUCCESS_REGEX);
452 if (match && match[1]) {
453 log.info(`Kerberos response: authenticated=true, login=${match[1]}`);
454 res(match[1]);
455 return;
456 }
457 }
458 log.debugError("Kerberos response: not athenticated");
459 rej("You are not authenticated to kerberos");
460 return;
461 }
462 log.debugError(`Kerberos response error: ${response.status}`);
463 rej("Error occurred while retrieving kerberos status");
464 },
465 onerror: function(response) {
466 log.debugError("Kerberos uknown error: " + response.responseText);
467 rej("Error occurred while retrieving kerberos status");
468 }
469 });
470 };
471
472 return Utils.retry(() => new Promise(kerberos_call), 2, 1000);
473 },
474
475 /**
476 * Retries the given function until it succeeds given a number of retries and an interval between them. They are set
477 * by default to retry 5 times with 1sec in between. There's also a flag to make the cooldown time exponential
478 *
479 * @param {Function} fn - Returns a promise
480 * @param {Number} retriesLeft - Number of retries. If -1 will keep retrying
481 * @param {Number} waitInterval - Millis between retries. If exponential set to true will be doubled each retry
482 * @param {Boolean} isExponentialWait - Flag for exponential back-off mode
483 * @return {Promise<*>}
484 */
485 retry: async function (fn, retriesLeft = 5, waitInterval = 1000, isExponentialWait = false) {
486 try {
487 const val = await fn();
488 return val;
489 } catch (error) {
490 if (retriesLeft) {
491 log.error(error);
492 log.info(`Retrying execution (${retriesLeft - 1} attempts left)...`);
493 await this.sleep(waitInterval);
494 const nextWaitInterval = isExponentialWait ? waitInterval * 2 : waitInterval;
495 return this.retry(fn, retriesLeft - 1, nextWaitInterval, isExponentialWait);
496 } else {
497 log.error('Max retries reached');
498 throw error;
499 }
500 }
501 },
502
503 };
504
505 const log = {
506 logEntries: [],
507 MAX_LENGTH: 500,
508
509 /**
510 * Prints the message only in the console.
511 * @param {string} message
512 */
513 debug: function (message) {
514 console.debug(message);
515 },
516
517 /**
518 * Prints the error message only in the console.
519 * @param {string} message
520 */
521 debugError: function (message) {
522 console.error(message);
523 },
524
525 info: function (message) {
526 const entry = String(message).slice(0, this.MAX_LENGHT);
527 this.logEntries.push(`${this.now()} [INFO] ${entry}`);
528 console.info(message);
529 },
530
531 warn: function (message) {
532 const entry = String(message).slice(0, this.MAX_LENGHT);
533 this.logEntries.push(`${this.now()} [WARN] ${entry}`);
534 console.warn(message);
535 },
536
537 error: function (message) {
538 const entry = String(message).slice(0, this.MAX_LENGHT);
539 this.logEntries.push(`${this.now()} [ERROR] ${entry}`);
540 console.error(message);
541 },
542
543 clear: function () {
544 this.logEntries = [];
545 },
546
547 entries: function () {
548 return this.logEntries;
549 },
550
551 now: function () {
552 return new Date().toISOString();
553 }
554 };
555
556 class WikiExport {
557 constructor() {
558 this.wikiClient = new WikiClient();
559 this.DEFAULT_SYNTAX = 'xwiki';
560 }
561
562 /**
563 * Saves XHTML content to a Wiki page, including tags.
564 *
565 * @param {string} wikiAddr wiki url address (or relative address)
566 * @param {string} htmlContent xhtml page content
567 * @param {*} timestamp date and time of current document revision
568 * @param {string} title wiki page title
569 * @param {*} tags list of tags of the page
570 */
571 async savePageXHTML (wikiAddr, htmlContent, timestamp, title) {
572 log.info('[wiki-export] start save page as XHTML...');
573 await this.savePage(wikiAddr, htmlContent, timestamp, title);
574 }
575
576 async savePage (wikiAddr, htmlContent, timestamp, title) {
577 // create page content
578 const version = GM_info.script.version;
579 const comment = `Updated with MCM2Wiki Tampermonkey script version ${version} (timestamp=${timestamp})`;
580 const xmlContent = this.wikiClient.createXmlPageDocument(title, htmlContent, this.DEFAULT_SYNTAX, comment);
581
582 // save wiki content
583 await this.wikiClient.putXmlContent(wikiAddr, xmlContent, 'home');
584 }
585 }
586
587 class WikiClient {
588 constructor() {
589 // Wiki API settings
590 this.TIMEOUT = 10000;
591 this.MAX_ATTEMPTS = 3;
592 this.READ_MIN_WAIT = 350; // minimum wait time between read calls (wiki client API only allows 3 TPS for read calls)
593 this.WRITE_MIN_WAIT = 1000; // minimum wait time between write calls (wiki client API only allows 1 TPS for write calls)
594 this.lastCall = null;
595 }
596
597 /**
598 * Extracts the destination wiki relative address from a user input or full wiki URL (enconded or not).
599 *
600 * @param {*} wikiAddr destination wiki address (full or relative, encoded or not)
601 */
602 static extractWikiRelativeAddr (wikiAddr) {
603 if (!wikiAddr) {
604 return null;
605 }
606 const match = decodeURI(wikiAddr)
607 .replace(/\s+/g, '')
608 .replace(/\\/g, '/')
609 .replace(/[\[\]\{\}]/g, '')
610 .replace(/%/g, '')
611 .match(WikiClient.URL_REGEX);
612
613 if (match == null || match[1] == null) {
614 log.info('invalid page match: ' + match);
615 return null;
616 }
617 const page = encodeURI(match[1])
618 .trim()
619 .replace(/^User:(.*)/, 'Users/$1')
620 .replace(/\/+/g, '/')
621 .replace(/\/$/, '');
622 return page;
623 }
624
625 createXmlPageDocument (title, content, syntax, comment) {
626 log.info('[wiki-client] creating page...');
627
628 return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
629 '<page xmlns="http://www.xwiki.org">' +
630 ' <title><![CDATA[' + title + ']]></title>' +
631 ' <content><![CDATA[' + content + ']]></content>' +
632 ' <comment>' + comment + '</comment>' +
633 ' <syntax>' + WikiClient.SYNTAX[syntax] + '</syntax>' +
634 '</page>';
635 }
636
637 /**
638 * Returns the Wiki API endpoint for Page requests as an URI. The 'type' parameter defines the different pages
639 * for the Wiki API endpoint.
640 *
641 * @param {string} wikiAddr wiki url address (or relative address)
642 * @param {*} params additional url parameters as an object
643 * @param {string} type the page type, such as home, tags, history, comments, or attachments
644 */
645 getRestEndpoint (wikiAddr, params, type) {
646 const page = WikiClient.extractWikiRelativeAddr(wikiAddr);
647 if (page == null) {
648 return null; // invalid wiki address
649 }
650 const pathParts = page.includes('/') ? page.split('/') : [page];
651 // base page target
652 let targetPath = WikiClient.API_URL + pathParts.join('/spaces/');
653 // other page types
654 if (type === 'home') {
655 targetPath += '/pages/WebHome';
656 } else if (type === 'tags') {
657 targetPath += '/pages/WebHome/tags';
658 } else if (type === 'history') {
659 targetPath += '/pages/WebHome/history';
660 } else if (type === 'comments') {
661 targetPath += '/pages/WebHome/comments';
662 } else if (type === 'attachments') {
663 targetPath += '/pages/WebHome/attachments';
664 }
665 // encode parameters in url
666 if (params) {
667 targetPath += '?' + Utils.getEncodedQueryData(params);
668 }
669 log.info('[wiki-client] wiki endpoint: ' + targetPath);
670 return targetPath;
671 }
672
673 /**
674 * Private method to execute an Wiki API call, respecting its TPS limits read/write call per second limit.
675 *
676 * @param {*} promise executor method
677 */
678 async execute (call, minWait) {
679 const elapsed = this.lastCall ? (new Date() - this.lastCall) : minWait;
680 const wait = minWait - elapsed;
681
682 if (wait > 0) {
683 log.info(`[wiki-client] throttling wiki client call: waiting ${wait} ms`);
684 await Utils.sleep(wait);
685 }
686
687 // execute the API call
688 const res = await new Promise(call);
689 this.lastCall = new Date();
690 return res;
691 }
692
693 static async testConnection() {
694 const client = new WikiClient();
695 const xml = await client.getWikiPageXml('FMA/Projects/asanghv-testing', null, WikiClient.TEST_TIMEOUT);
696 if(!!xml){
697 wikiResult = xml.getElementsByTagName("content")[0].childNodes[0].nodeValue
698 wikiTitle = xml.getElementsByTagName("title")[0].childNodes[0].nodeValue
699 }
700 return !!xml; // valid XML means we can connect to Wiki API
701 }
702
703 getWikiPageXml (wikiAddr, type, timeout) {
704 const get_call = (res, rej) => {
705 const typeLabel = type || "root";
706 log.info(`[wiki-client] start get xml (${typeLabel})...`);
707 const requestUrl = this.getRestEndpoint(wikiAddr, null, "home");
708 GM_xmlhttpRequest({
709 method: "GET",
710 url: requestUrl,
711 headers: {
712 'Content-Type': 'application/xml',
713 'User-Agent': 'MCM2Wiki/' + GM_info.script.version
714 },
715 timeout: timeout || this.TIMEOUT,
716 ontimeout: () => rej(`Timeout while retrieving page XML`),
717 onload: function (response) {
718 if (response.finalUrl != requestUrl) {
719 $("#errorSpanId").text(`[wiki-client] redirected to ` + response.finalUrl);
720 resetButton();
721 rej("Error occurred while retrieving page XML [redirected]");
722 } else if (response.status != 200) {
723 $("#errorSpanId").text(`[wiki-client] get xml error: ` + response.responseText);
724 resetButton();
725 rej("Error occurred while retrieving page XML");
726 } else {
727 log.info('[wiki-client] get xml result: ' + response.status);
728 const parser = new DOMParser();
729 const xmlDoc = parser.parseFromString(response.responseText, "text/xml");
730 res(parser.parseFromString(response.responseText, "text/xml"));
731 }
732 },
733 onerror: function (response) {
734 $("#errorSpanId").text(`[wiki-client] get xml error: ` + response.responseText);
735 resetButton();
736 rej("Error occurred while retrieving page XML");
737 }
738 });
739 };
740
741 return Utils.retry(async () => this.execute(get_call, this.READ_MIN_WAIT), this.MAX_ATTEMPTS);
742 }
743
744 /**
745 * Creates or saves a wiki page using Wiki API.
746 *
747 * - HTTP 201 CREATED if page doesn't exist
748 * - HTTP 202 ACCEPTED if page exists
749 * - HTTP 400 BAD REQUEST if validation of page path failed
750 * - HTTP 401 UNAUTHORIZED if permission check failed
751 *
752 * @param {*} wikiAddr the page path
753 * @param {*} content the page content
754 * @param {*} type the page type, such as home, tags, history, comments, or attachments
755 */
756 putXmlContent (wikiAddr, content, type) {
757 const pageType = type || "root";
758 const writingWhat = type == 'tags' ? 'tags to the page' : 'wiki page content';
759 const providedWhat = type == 'tags' ? 'one of the tags' : 'the page address';
760 const put_call = (res, rej) => {
761 log.info(`[wiki-client] start PUT xml of ${pageType}...`);
762 const requestUrl = this.getRestEndpoint(wikiAddr, null, type);
763 GM_xmlhttpRequest({
764 method: "PUT",
765 url: requestUrl,
766 data: content,
767 headers: {
768 'Content-Type': 'application/xml',
769 'User-Agent': 'MCM2Wiki/' + GM_info.script.version
770 },
771 timeout: (3 * this.TIMEOUT),
772 ontimeout: () => rej(`Put wiki ${type} timeout`),
773 onload: function (response) {
774 if (response.finalUrl != requestUrl) {
775 log.debug('[wiki-client] redirected to ' + response.finalUrl);
776 }
777 if (response.status == 201 || response.status == 202) {
778 log.info('[wiki-client] put xml result: ' + response.status);
779 res();
780 } else if (response.status == 400) {
781 $("#errorSpanId").text(`[wiki-client] put xml error: [400 - bad request]`);
782 resetButton();
783 rej(`Error occurred while writing ${writingWhat}: ${providedWhat} you have provided contains an invalid character`);
784 } else if (response.status == 401) {
785 $("#errorSpanId").text(`[wiki-client] put xml error: [401 - unauthorized]`);
786 resetButton();
787 rej(`Error occurred while writing ${writingWhat}: you don't have permission to change the page`);
788 } else {
789 $("#errorSpanId").text(`[wiki-client] put xml error: [${response.status} - unknown]`);
790 resetButton();
791 rej(`Error occurred while writing ${writingWhat} with cause "${response.status} - unknown"`);
792 }
793 },
794 onerror: function (response) {
795 $("#errorSpanId").text(`[wiki-client] put xml error: ` + response.responseText);
796 resetButton();
797 rej(`Error occurred while writing ${writingWhat} with cause "${response.responseText}"`);
798 }
799 });
800 };
801
802 return Utils.retry(async () => this.execute(put_call, this.WRITE_MIN_WAIT), this.MAX_ATTEMPTS);
803 }
804 }
805
806 WikiClient.API_URL = 'https://w.amazon.com:8443/rest/wikis/xwiki/spaces/';
807 WikiClient.TEST_TIMEOUT = 7500;
808 WikiClient.VIEW_URL = 'https://w.amazon.com/bin/view/';
809 WikiClient.URL_REGEX = /^(?:(?:https?:\/\/)?w\.amazon\.com\/(?:bin\/view|index\.php)\/|w\?)?([^#?\s]*[^#?\s\/])\/?/;
810 WikiClient.WIKI_TAG = "MCM2Wiki";
811 WikiClient.SYNTAX = {
812 'xwiki': 'xwiki/2.1'
813 };
814
815 async function getWikiData(){
816 let result;
817 try {
818 result = await WikiClient.testConnection(); // check wiki API connection
819 if (!result) {
820 throw `Wiki API test result: ${wikiResult}`;
821 }
822 return true
823 } catch (ex) {
824 $("#errorSpanId").text(`Network connection error [wiki-api]: ${ex}`);
825 resetButton();
826 return false;
827 }
828 }
829
830 async function pushToWiki(wikiAddr, finalWikiContent){
831 try {
832 buttonLabel.html('Pushing Entry to Wiki...');
833 wikiExport = new WikiExport();
834 let timestamp = Date.now();
835 await wikiExport.savePageXHTML(wikiAddr, finalWikiContent, timestamp, wikiTitle);
836 const url = `${WikiClient.VIEW_URL}${wikiAddr}`;
837 const wikiLink = `<span style="text-decoration:underline; font-style:italic;"><a href="${url}" target="_blank">${url}</a></span>`;
838 const msg = `MCM details successfuly exported to Deployment Wiki</br></br>${wikiLink}`;
839 await Prompts.showAlert(appTitle, msg);
840 resetButton();
841 return true;
842
843 } catch (ex) {
844 $("#errorSpanId").text(`Network connection error [wiki-api]: ${ex}`);
845 resetButton();
846 return false;
847 }
848 }
849
850 function removeDataWithBracket(string){
851 return string.replace(/ *\[[^\]]*]/g, '');
852 }
853
854 function resetButton(){
855 buttonLabel.prop('disabled', false);
856 buttonLabel.html('Add to Wiki');
857 }