1/**
2 * Copyright (c) 2006-2021, JGraph Ltd
3 * Copyright (c) 2006-2021, draw.io AG
4 */
5
6//Add a closure to hide the class private variables without changing the code a lot
7(function ()
8{
9
10var _token = null;
11
12window.NotionClient = function(editorUi)
13{
14	DrawioClient.call(this, editorUi, 'notionAuthInfo');
15};
16
17// Extends DrawioClient
18mxUtils.extend(NotionClient, DrawioClient);
19
20/**
21 *
22 */
23NotionClient.prototype.extension = '.drawio';
24
25NotionClient.prototype.xmlField = 'draw.io XML';
26
27/**
28 *
29 */
30NotionClient.prototype.baseUrl = window.NOTION_API_URL || 'https://app.diagrams.net/notion-api';
31
32
33NotionClient.prototype.getTitle = function (props)
34{
35	var obj, key;
36
37	for (var field in props)
38	{
39		if (props[field].type == 'title')
40		{
41			key = field;
42			obj = props[field];
43			break;
44		}
45	}
46
47	return {title: this.getTitleVal(obj), key: key};
48};
49
50NotionClient.prototype.getTitleVal = function (obj)
51{
52	if (typeof obj.title === 'string')
53	{
54		return obj.title;
55	}
56	else
57	{
58		var title = [];
59
60		for (var i = 0; i < obj.title.length; i++)
61		{
62			title.push(obj.title[i].text.content)
63		}
64
65		return title.join(' ');
66	}
67};
68
69NotionClient.prototype.authenticate = function(success, error, failOnAuth)
70{
71	var acceptAuthResponse = true;
72
73	var errFn = mxUtils.bind(this, function()
74	{
75		if (acceptAuthResponse)
76		{
77			acceptAuthResponse = false;
78			error({message: mxResources.get('accessDenied'), retry: mxUtils.bind(this, function()
79			{
80				this.ui.hideDialog();
81				auth();
82			})});
83		}
84	});
85
86	var auth = mxUtils.bind(this, function()
87	{
88		acceptAuthResponse = true;
89
90		this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, authSuccess)
91		{
92			var tokenDlg = new FilenameDialog(this.ui, '',
93				mxResources.get('ok'), mxUtils.bind(this, function(token)
94				{
95					//check token is valid
96					_token = token;
97
98					//TODO use any simpler request if one becomes available
99					this.executeRequest('/v1/databases', null, 'GET', mxUtils.bind(this, function()
100					{
101						this.executeRequest('/setToken', null, 'GET', mxUtils.bind(this, function()
102						{
103							acceptAuthResponse = false;
104
105							if (remember)
106							{
107								_token = null;
108							}
109
110							if (authSuccess != null)
111							{
112								authSuccess();
113							}
114
115							success();
116						}), errFn, failOnAuth);
117					}), errFn, failOnAuth);
118				}), mxResources.get('notionToken'), function(token)
119				{
120					return token != null && token.length > 0;
121				}, null, 'https://developers.notion.com/docs/getting-started#step-1-create-an-integration');
122
123			this.ui.showDialog(tokenDlg.container, 300, 80, true, true);
124			tokenDlg.init();
125		}), errFn);
126	});
127
128	auth();
129};
130
131/**
132 * Checks if the client is authorized and calls the next step.
133 */
134NotionClient.prototype.executeRequest = function(url, data, method, success, error, failOnAuth)
135{
136	var doExecute = mxUtils.bind(this, function()
137	{
138		var acceptResponse = true;
139
140		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
141		{
142			acceptResponse = false;
143			error({code: App.ERROR_TIMEOUT, retry: doExecute});
144		}), this.ui.timeout);
145
146		var req = new mxXmlRequest(this.baseUrl + url, data, method);
147
148		req.withCredentials = true;
149
150		req.setRequestHeaders = mxUtils.bind(this, function(request, params)
151		{
152			if (_token != null)
153			{
154				request.setRequestHeader('Authorization', 'Bearer ' + _token);
155			}
156
157			request.setRequestHeader('Notion-Version', '2021-05-13');
158			request.setRequestHeader('Content-Type', 'application/json');
159		});
160
161		req.send(mxUtils.bind(this, function(req)
162		{
163			window.clearTimeout(timeoutThread);
164
165			if (acceptResponse)
166			{
167				// 404 (file not found) is a valid response for checkExists
168				if (req.getStatus() >= 200 && req.getStatus() <= 299)
169				{
170					if (this.user == null)
171					{
172						this.setUser(new DrawioUser('notion', null, 'Notion'));
173					}
174
175					success(JSON.parse(req.getText()));
176				}
177				else if (!failOnAuth && (req.getStatus() === 401 || req.getStatus() === 400))
178				{
179					this.setUser(null);
180					failOnAuth = true;
181					//Authorize again using the refresh token
182					this.authenticate(function()
183					{
184						doExecute();
185					}, error, failOnAuth);
186				}
187				else
188				{
189					error(this.parseRequestText(req));
190				}
191			}
192		}), mxUtils.bind(this, function(err)
193		{
194			window.clearTimeout(timeoutThread);
195
196			if (acceptResponse)
197			{
198				error(err);
199			}
200		}));
201	});
202
203	doExecute();
204};
205
206/**
207 *
208 */
209NotionClient.prototype.getLibrary = function(id, success, error)
210{
211	this.getFile(id, success, error, false, true);
212};
213
214/**
215 *
216 */
217NotionClient.prototype.getFile = function(id, success, error, denyConvert, asLibrary)
218{
219	asLibrary = (asLibrary != null) ? asLibrary : false;
220
221	this.executeRequest('/v1/pages/' + encodeURIComponent(id), null, 'GET', mxUtils.bind(this, function(fileInfo)
222	{
223		try
224		{
225			var xmlParts = fileInfo.properties[this.xmlField].rich_text, xml = '';
226			var fileNameObj = this.getTitle(fileInfo.properties);
227
228			for (var i = 0; i < xmlParts.length; i++)
229			{
230				xml += xmlParts[i].text.content;
231			}
232
233			var meta = {id: id, name: fileNameObj.title, nameField: fileNameObj.key};
234
235			if (asLibrary)
236			{
237				success(new NotionLibrary(this.ui, xml, meta));
238			}
239			else
240			{
241				success(new NotionFile(this.ui, xml, meta));
242			}
243		}
244		catch(e)
245		{
246			if (error != null)
247			{
248				error(e);
249			}
250			else
251			{
252				throw e;
253			}
254		}
255	}), error);
256};
257
258/**
259 *
260 */
261NotionClient.prototype.insertLibrary = function(filename, data, success, error, folderObj)
262{
263	this.insertFile(filename, data, success, error, true, folderObj);
264};
265
266/**
267 *
268 */
269NotionClient.prototype.insertFile = function(filename, data, success, error, asLibrary, folderObj)
270{
271	asLibrary = (asLibrary != null) ? asLibrary : false;
272	var folderId, nameField;
273
274	var startSave = mxUtils.bind(this, function()
275	{
276		this.checkExists(folderId, filename, nameField, true, mxUtils.bind(this, function(checked, currentId)
277		{
278			if (checked)
279			{
280				this.writeFile(currentId? '/v1/pages/' + encodeURIComponent(currentId) : '/v1/pages',
281						currentId? null : folderId, filename, nameField, data, currentId? 'PATCH' : 'POST',
282						mxUtils.bind(this, function(fileInfo)
283				{
284					var fileNameObj = this.getTitle(fileInfo.properties);
285					var meta = {id: fileInfo.id, name: fileNameObj.title, nameField: fileNameObj.key};
286
287					if (asLibrary)
288					{
289						success(new NotionLibrary(this.ui, data, meta));
290					}
291					else
292					{
293						success(new NotionFile(this.ui, data, meta));
294					}
295				}), error);
296			}
297			else
298			{
299				error();
300			}
301		}));
302	});
303
304	if (typeof folderObj === 'object')
305	{
306		nameField = this.getTitle(folderObj.schema.properties).key;
307		folderId = folderObj.id;
308
309		if (!folderObj.drawioReady)
310		{
311			folderObj.schema.properties[this.xmlField] = {
312				name: this.xmlField,
313				type: 'rich_text',
314				rich_text: {}
315			};
316
317			this.executeRequest('/v1/databases/' + encodeURIComponent(folderObj.id), JSON.stringify({
318			    title: folderObj.schema.title,
319		        properties: folderObj.schema.properties
320			}), 'PATCH', startSave, error);
321		}
322		else
323		{
324			startSave();
325		}
326	}
327	else
328	{
329		error(); //This shouldn't happen!
330	}
331};
332
333/**
334 *
335 */
336NotionClient.prototype.checkExists = function(parentId, filename, nameField, askReplace, fn)
337{
338	this.executeRequest('/v1/databases/' + encodeURIComponent(parentId) + '/query', JSON.stringify({
339		filter: {
340	        property: nameField,
341			text: {
342			    equals: filename
343			}
344		}
345	}), 'POST', mxUtils.bind(this, function(resp)
346	{
347		if (resp.results.length == 0)
348		{
349			fn(true);
350		}
351		else
352		{
353			if (askReplace)
354			{
355				this.ui.spinner.stop();
356
357				this.ui.confirm(mxResources.get('replaceIt', [filename]), function()
358				{
359					fn(true, resp.results[0].id);
360				}, function()
361				{
362					fn(false);
363				});
364			}
365			else
366			{
367				this.ui.spinner.stop();
368
369				this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function()
370				{
371					fn(false);
372				});
373			}
374		}
375	}), function(req)
376	{
377		fn(false);
378	});
379};
380
381/**
382 *
383 */
384NotionClient.prototype.saveFile = function(file, success, error)
385{
386	try
387	{
388		var data = file.getData();
389
390		this.writeFile('/v1/pages/' + file.getId(), null, file.getTitle(), file.getNameField(),
391				data, 'PATCH', mxUtils.bind(this, function(resp)
392				{
393					success(resp, data);
394				}), error);
395	}
396	catch (e)
397	{
398		error(e);
399	}
400};
401
402/**
403 *
404 */
405NotionClient.prototype.writeFile = function(url, folderId, filename, nameField, data, method, success, error)
406{
407	try
408	{
409		if (url != null && data != null)
410		{
411			//Notion has a limit on rich-text pages of 200KB
412			if (data.length > 200000 /*200KB*/)
413			{
414				error({message: mxResources.get('drawingTooLarge') + ' (' +
415					this.ui.formatFileSize(data.length) + ' / 200 KB)'});
416
417				return;
418			}
419
420			var richTxt = []
421			var parts = Math.ceil(data.length / 2000);
422
423			for (var i = 0; i < parts; i++)
424			{
425				richTxt.push({
426					text: {
427                    	content: data.substr(i * 2000, 2000)
428	                }
429				});
430			}
431
432			var reqBody = {
433				properties: {}
434			};
435
436			reqBody.properties[nameField] = {
437            	title: [{
438					text: {
439						content: filename
440					}
441                }]
442            };
443
444			reqBody.properties[this.xmlField] = {
445            	rich_text: richTxt
446            };
447
448			if (folderId)
449			{
450				reqBody['parent'] = { database_id: folderId };
451			}
452
453			this.executeRequest(url, JSON.stringify(reqBody), method, success, error);
454		}
455		else
456		{
457			error({message: mxResources.get('unknownError')});
458		}
459	}
460	catch (e)
461	{
462		error(e);
463	}
464};
465
466/**
467 *
468 */
469NotionClient.prototype.parseRequestText = function(req)
470{
471	var result = {message: mxResources.get('unknownError')};
472
473	try
474	{
475		result = JSON.parse(req.getText());
476	}
477	catch (e)
478	{
479		// ignore
480	}
481
482	return result;
483};
484
485/**
486 * Checks if the client is authorized and calls the next step.
487 */
488NotionClient.prototype.pickLibrary = function(fn)
489{
490	this.pickFile(fn);
491};
492
493/**
494 * Checks if the client is authorized and calls the next step.
495 */
496NotionClient.prototype.pickFolder = function(fn)
497{
498	this.showNotionDialog(false, fn);
499};
500
501NotionClient.prototype.pickFile = function(fn)
502{
503	fn = (fn != null) ? fn : mxUtils.bind(this, function(id)
504	{
505		this.ui.loadFile('N' + encodeURIComponent(id));
506	});
507
508	this.showNotionDialog(true, fn);
509};
510
511/**
512 *
513 */
514NotionClient.prototype.showNotionDialog = function(showFiles, fn)
515{
516	var itemId, itemName;
517
518	var content = document.createElement('div');
519	content.style.whiteSpace = 'nowrap';
520	content.style.overflow = 'hidden';
521	content.style.height = '304px';
522
523	var hd = document.createElement('h3');
524	mxUtils.write(hd, mxResources.get((showFiles) ? 'officeSelDiag' : 'selectDB'));
525	hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
526	content.appendChild(hd);
527
528	var div = document.createElement('div');
529	div.style.whiteSpace = 'nowrap';
530	div.style.border = '1px solid lightgray';
531	div.style.boxSizing = 'border-box';
532	div.style.padding = '4px';
533	div.style.overflow = 'auto';
534	div.style.lineHeight = '1.2em';
535	div.style.height = '274px';
536	content.appendChild(div);
537
538	var listItem = document.createElement('div');
539	listItem.style.textOverflow = 'ellipsis';
540	listItem.style.boxSizing = 'border-box';
541	listItem.style.overflow = 'hidden';
542	listItem.style.padding = '4px';
543	listItem.style.width = '100%';
544
545	var dlg = new CustomDialog(this.ui, content, mxUtils.bind(this, function()
546	{
547		fn(itemId);
548	}));
549	this.ui.showDialog(dlg.container, 420, 380, true, true);
550
551	if (showFiles)
552	{
553		dlg.okButton.parentNode.removeChild(dlg.okButton);
554	}
555
556	var createLink = mxUtils.bind(this, function(label, exec, padding, underline)
557	{
558		var link = document.createElement('a');
559		link.setAttribute('title', label);
560		link.style.cursor = 'pointer';
561		mxUtils.write(link,  label);
562		mxEvent.addListener(link, 'click', exec);
563
564		if (underline)
565		{
566			link.style.textDecoration = 'underline';
567		}
568
569		if (padding != null)
570		{
571			var temp = listItem.cloneNode();
572			temp.style.padding = padding;
573			temp.appendChild(link);
574
575			link = temp;
576		}
577
578		return link;
579	});
580
581	var updatePathInfo = mxUtils.bind(this, function()
582	{
583		var dbInfo = document.createElement('div');
584		dbInfo.style.marginBottom = '8px';
585
586		dbInfo.appendChild(createLink(itemName, mxUtils.bind(this, function()
587		{
588			itemId = null;
589			selectDB();
590		}), null, true));
591
592		div.appendChild(dbInfo);
593	});
594
595	var error = mxUtils.bind(this, function(err)
596	{
597		// Pass a dummy notFoundMessage to bypass special handling
598		this.ui.handleError(err, null, mxUtils.bind(this, function()
599		{
600			this.ui.spinner.stop();
601
602			if (this.getUser() != null)
603			{
604				itemId = null;
605
606				selectDB();
607			}
608			else
609			{
610				this.ui.hideDialog();
611			}
612		}), null, {});
613	});
614
615	// Adds paging for DBs and diagrams (DB pages)
616	var nextPageDiv = null;
617	var scrollFn = null;
618	var pageSize = 100;
619
620	var selectFile = mxUtils.bind(this, function(nextCursor)
621	{
622		if (nextCursor == null)
623		{
624			div.innerHTML = '';
625		}
626
627		this.ui.spinner.spin(div, mxResources.get('loading'));
628		dlg.okButton.removeAttribute('disabled');
629
630		if (scrollFn != null)
631		{
632			mxEvent.removeListener(div, 'scroll', scrollFn);
633			scrollFn = null;
634		}
635
636		if (nextPageDiv != null && nextPageDiv.parentNode != null)
637		{
638			nextPageDiv.parentNode.removeChild(nextPageDiv);
639		}
640
641		nextPageDiv = document.createElement('a');
642		nextPageDiv.style.display = 'block';
643		nextPageDiv.style.cursor = 'pointer';
644		mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
645
646		var nextPage = mxUtils.bind(this, function()
647		{
648			selectFile(nextCursor);
649		});
650
651		mxEvent.addListener(nextPageDiv, 'click', nextPage);
652
653		var reqBody = {
654			page_size: pageSize
655		};
656
657		if (nextCursor != null)
658		{
659			reqBody.start_cursor = nextCursor;
660		}
661
662		this.executeRequest('/v1/databases/' + encodeURIComponent(itemId) + '/query',
663			JSON.stringify(reqBody), 'POST', mxUtils.bind(this, function(resp)
664		{
665			this.ui.spinner.stop();
666
667			if (nextCursor == null)
668			{
669				updatePathInfo();
670
671				div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function()
672				{
673					itemId = null;
674					selectDB();
675				}), '4px'));
676			}
677
678			var files = resp.results;
679
680			if (files == null || files.length == 0)
681			{
682				mxUtils.write(div, mxResources.get('noFiles'));
683			}
684			else
685			{
686				var gray = true;
687
688				for (var i = 0; i < files.length; i++)
689				{
690					(mxUtils.bind(this, function(file, idx)
691					{
692						var temp = listItem.cloneNode();
693						temp.style.backgroundColor = (gray) ?
694							((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : '';
695						gray = !gray;
696
697						var typeImg = document.createElement('img');
698						typeImg.src = IMAGE_PATH + '/file.png';
699						typeImg.setAttribute('align', 'absmiddle');
700						typeImg.style.marginRight = '4px';
701						typeImg.style.marginTop = '-4px';
702						typeImg.width = 20;
703						temp.appendChild(typeImg);
704
705						temp.appendChild(createLink(this.getTitle(file.properties).title, mxUtils.bind(this, function()
706						{
707							this.ui.hideDialog();
708							fn(file.id);
709						})));
710
711						div.appendChild(temp);
712					}))(files[i], i);
713				}
714
715				if (resp.has_more)
716				{
717					nextCursor = resp.next_cursor;
718
719					div.appendChild(nextPageDiv);
720
721					scrollFn = function()
722					{
723						if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
724						{
725							nextPage();
726						}
727					};
728
729					mxEvent.addListener(div, 'scroll', scrollFn);
730				}
731			}
732		}), error, true);
733	});
734
735	var selectDB = mxUtils.bind(this, function(nextCursor)
736	{
737		if (nextCursor == null)
738		{
739			div.innerHTML = '';
740		}
741
742		dlg.okButton.setAttribute('disabled', 'disabled');
743		this.ui.spinner.spin(div, mxResources.get('loading'));
744
745		if (scrollFn != null)
746		{
747			mxEvent.removeListener(div, 'scroll', scrollFn);
748		}
749
750		if (nextPageDiv != null && nextPageDiv.parentNode != null)
751		{
752			nextPageDiv.parentNode.removeChild(nextPageDiv);
753		}
754
755		nextPageDiv = document.createElement('a');
756		nextPageDiv.style.display = 'block';
757		nextPageDiv.style.cursor = 'pointer';
758		mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
759
760		var nextPage = mxUtils.bind(this, function()
761		{
762			selectDB(nextCursor);
763		});
764
765		mxEvent.addListener(nextPageDiv, 'click', nextPage);
766
767		this.executeRequest('/v1/databases?page_size=' + pageSize + (nextCursor != null? '&start_cursor=' + nextCursor : ''),
768			null, 'GET', mxUtils.bind(this, function(resp)
769		{
770			this.ui.spinner.stop();
771			var dbs = resp.results;
772			var count = 0;
773
774			if (dbs == null || dbs.length == 0)
775			{
776				mxUtils.write(div, mxResources.get('noDBs'));
777			}
778			else
779			{
780				for (var i = 0; i < dbs.length; i++)
781				{
782					var drawioReady = dbs[i].properties[this.xmlField] &&
783								dbs[i].properties[this.xmlField].type == 'rich_text';
784
785					//Filter DBs when opening a file
786					if (showFiles && !drawioReady) continue;
787
788					(mxUtils.bind(this, function(db, idx, drawioReady)
789					{
790						var temp = listItem.cloneNode();
791						temp.style.backgroundColor = (idx % 2 == 0) ?
792							((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : '';
793
794						temp.appendChild(createLink(this.getTitleVal(db), mxUtils.bind(this, function()
795						{
796							itemId = db.id;
797							itemName = this.getTitleVal(db);
798
799							if (showFiles)
800							{
801								selectFile();
802							}
803							else
804							{
805								this.ui.hideDialog();
806								fn({id: itemId, drawioReady: drawioReady, schema: db});
807							}
808						})));
809
810						div.appendChild(temp);
811					}))(dbs[i], i, drawioReady);
812
813					count++;
814				}
815			}
816
817			if (resp.has_more)
818			{
819				nextCursor = resp.next_cursor;
820
821				if (count == 0)
822				{
823					nextPage();
824					return;
825				}
826
827				div.appendChild(nextPageDiv);
828
829				scrollFn = function()
830				{
831					if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
832					{
833						nextPage();
834					}
835				};
836
837				mxEvent.addListener(div, 'scroll', scrollFn);
838			}
839			else if (count == 0 && div.innerHTML == '')
840			{
841				mxUtils.write(div, mxResources.get('noDBs'));
842			}
843		}), error);
844	});
845
846	selectDB();
847};
848
849/**
850 *
851 */
852NotionClient.prototype.logout = function()
853{
854	//Send to server to clear token cookie
855	this.executeRequest('/removeToken', null, 'GET', function(){}, function(){});
856	this.setUser(null);
857	_token = null;
858};
859
860})();