1/**
2 * Copyright (c) 2006-2017, JGraph Ltd
3 * Copyright (c) 2006-2017, Gaudenz Alder
4 */
5TrelloClient = function(editorUi)
6{
7	DrawioClient.call(this, editorUi, 'tauth');
8	Trello.setKey(this.key);
9};
10
11// Extends DrawioClient
12mxUtils.extend(TrelloClient, DrawioClient);
13
14TrelloClient.prototype.key = (window.location.hostname == 'test.draw.io') ?
15	'e73615c79cf7e381aef91c85936e9553' : 'e73615c79cf7e381aef91c85936e9553';
16
17TrelloClient.prototype.baseUrl = 'https://api.trello.com/1/';
18
19TrelloClient.prototype.SEPARATOR = '|$|';
20
21/**
22 * Maximum attachment size of Trello.
23 */
24TrelloClient.prototype.maxFileSize = 10000000 /*10MB*/;
25
26/**
27 * Default extension for new files.
28 */
29TrelloClient.prototype.extension = '.xml'; //TODO export to png
30
31/**
32 * Authorizes the client, used with methods that can be called without a user click and popup blockers will interfere
33 * Show the AuthDialog to work around the popup blockers if the file is opened directly
34 */
35TrelloClient.prototype.authenticate = function(fn, error, force)
36{
37	if (force)
38	{
39		this.logout();
40	}
41
42	var callback = mxUtils.bind(this, function(remember, success)
43	{
44		Trello.authorize(
45		{
46			type: 'popup',
47			name: 'draw.io',
48			scope:
49			{
50				read: 'true',
51			    write: 'true'
52			},
53			expiration: remember ? 'never' : '1hour',
54			success: function()
55			{
56				if (success != null)
57				{
58					success();
59				}
60
61				fn();
62			},
63			error: function()
64			{
65				if (success != null)
66				{
67					success();
68				}
69
70				if (error != null)
71				{
72					error(mxResources.get('loggedOut'));
73				}
74			}
75		});
76	});
77
78	if (this.isAuthorized())
79	{
80		callback(true);
81	}
82	else
83	{
84		this.ui.showAuthDialog(this, true, callback);
85	}
86}
87
88/**
89 *
90 */
91TrelloClient.prototype.getLibrary = function(id, success, error)
92{
93	this.getFile(id, success, error, false, true);
94};
95
96/**
97 *
98 */
99TrelloClient.prototype.getFile = function(id, success, error, denyConvert, asLibrary)
100{
101	//In getFile only, we
102	asLibrary = (asLibrary != null) ? asLibrary : false;
103
104	var callback = mxUtils.bind(this, function()
105	{
106		var ids = id.split(this.SEPARATOR);
107		var acceptResponse = true;
108
109		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
110		{
111			acceptResponse = false;
112			error({code: App.ERROR_TIMEOUT, retry: callback})
113		}), this.ui.timeout);
114
115		Trello.cards.get(ids[0] + '/attachments/' + ids[1], mxUtils.bind(this, function(meta)
116		{
117			window.clearTimeout(timeoutThread);
118
119		    if (acceptResponse)
120		    {
121				var binary = /\.png$/i.test(meta.name);
122				var headers = {
123					Authorization: 'OAuth oauth_consumer_key="' + Trello.key() + '", oauth_token="' + Trello.token() + '"'
124				};
125
126				// TODO Trello doesn't allow CORS requests to load attachments. Confirm that
127				// and make sure that only a proxy technique can work!
128				// Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file
129				if (/\.v(dx|sdx?)$/i.test(meta.name) || /\.gliffy$/i.test(meta.name) ||
130					(!this.ui.useCanvasForExport && binary))
131				{
132					this.ui.convertFile(PROXY_URL + '?url=' + encodeURIComponent(meta.url), meta.name, meta.mimeType,
133						this.extension, success, error, null, headers);
134				}
135				else
136				{
137					acceptResponse = true;
138
139					timeoutThread = window.setTimeout(mxUtils.bind(this, function()
140					{
141						acceptResponse = false;
142						error({code: App.ERROR_TIMEOUT})
143					}), this.ui.timeout);
144
145					this.ui.editor.loadUrl(PROXY_URL + '?url=' + encodeURIComponent(meta.url), mxUtils.bind(this, function(data)
146					{
147						window.clearTimeout(timeoutThread);
148
149					    if (acceptResponse)
150					   	{
151					    	//keep our id which includes the cardId
152					    	meta.compoundId = id;
153
154							var index = (binary) ? data.lastIndexOf(',') : -1;
155
156							if (index > 0)
157							{
158								var xml = this.ui.extractGraphModelFromPng(data);
159
160								if (xml != null && xml.length > 0)
161								{
162									data = xml;
163								}
164								else
165								{
166									// TODO: Import PNG
167								}
168							}
169
170							if (asLibrary)
171							{
172								success(new TrelloLibrary(this.ui, data, meta));
173							}
174							else
175							{
176								success(new TrelloFile(this.ui, data, meta));
177							}
178					    }
179			    	}), mxUtils.bind(this, function(err, req)
180					{
181						window.clearTimeout(timeoutThread);
182
183				    	if (acceptResponse)
184				    	{
185				    		if (req.status == 401)
186				    		{
187				    			this.authenticate(callback, error, true);
188				    		}
189				    		else
190				    		{
191				    			error();
192				    		}
193				    	}
194					}), binary || (meta.mimeType != null &&
195						meta.mimeType.substring(0, 6) == 'image/'), null, null, null, headers);
196				}
197		    }
198		}), mxUtils.bind(this, function(err)
199		{
200			window.clearTimeout(timeoutThread);
201
202		    	if (acceptResponse)
203		    	{
204		    		if (err != null && err.status == 401)
205		    		{
206		    			this.authenticate(callback, error, true);
207		    		}
208		    		else
209		    		{
210		    			error();
211		    		}
212		    	}
213		}));
214	});
215
216	this.authenticate(callback, error);
217};
218
219/**
220 *
221 */
222TrelloClient.prototype.insertLibrary = function(filename, data, success, error, cardId)
223{
224	this.insertFile(filename, data, success, error, true, cardId);
225};
226
227/**
228 *
229 */
230TrelloClient.prototype.insertFile = function(filename, data, success, error, asLibrary, cardId)
231{
232	asLibrary = (asLibrary != null) ? asLibrary : false;
233
234	var callback = mxUtils.bind(this, function()
235	{
236		var fn = mxUtils.bind(this, function(fileData)
237		{
238			this.writeFile(filename, fileData, cardId, mxUtils.bind(this, function(meta)
239			{
240				if (asLibrary)
241				{
242					success(new TrelloLibrary(this.ui, data, meta));
243				}
244				else
245				{
246					success(new TrelloFile(this.ui, data, meta));
247				}
248			}), error);
249		});
250
251		if (this.ui.useCanvasForExport && /(\.png)$/i.test(filename))
252		{
253			this.ui.getEmbeddedPng(mxUtils.bind(this, function(pngData)
254			{
255				fn(this.ui.base64ToBlob(pngData, 'image/png'));
256			}), error, data);
257		}
258		else
259		{
260			fn(data);
261		}
262	});
263
264	this.authenticate(callback, error);
265};
266
267/**
268 *
269 */
270TrelloClient.prototype.saveFile = function(file, success, error)
271{
272	// write the file first (with the same name), then delete the old file
273	// so that nothing is lost if something goes wrong with deleting
274	var ids = file.meta.compoundId.split(this.SEPARATOR);
275
276	var fn = mxUtils.bind(this, function(data)
277	{
278		this.writeFile(file.meta.name, data, ids[0], function(meta)
279		{
280			Trello.del('cards/' + ids[0] + '/attachments/' + ids[1], mxUtils.bind(this, function()
281			{
282				success(meta);
283			}), mxUtils.bind(this, function(err)
284			{
285				if (err != null && err.status == 401)
286	    		{
287					// KNOWN: Does not wait for popup to close for callback
288	    			this.authenticate(callback, error, true);
289	    		}
290	    		else
291	    		{
292	    			error();
293	    		}
294			}));
295		}, error);
296	});
297
298	var callback = mxUtils.bind(this, function()
299	{
300		if (this.ui.useCanvasForExport && /(\.png)$/i.test(file.meta.name))
301		{
302			this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
303			{
304				fn(this.ui.base64ToBlob(data, 'image/png'));
305			}), error, (this.ui.getCurrentFile() != file) ? file.getData() : null);
306		}
307		else
308		{
309			fn(file.getData());
310		}
311	});
312
313	this.authenticate(callback, error);
314};
315
316/**
317 *
318 */
319TrelloClient.prototype.writeFile = function(filename, data, cardId, success, error)
320{
321	if (filename != null && data != null)
322	{
323		if (data.length >= this.maxFileSize)
324		{
325			error({message: mxResources.get('drawingTooLarge') + ' (' +
326				this.ui.formatFileSize(data.length) + ' / 10 MB)'});
327
328			return;
329		}
330
331		var fn = mxUtils.bind(this, function()
332		{
333		  var acceptResponse = true;
334
335		  var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
336		  {
337			acceptResponse = false;
338			error({code: App.ERROR_TIMEOUT, retry: fn});
339		  }), this.ui.timeout);
340
341		  var formData = new FormData();
342		  formData.append('key', Trello.key());
343		  formData.append('token', Trello.token());
344		  formData.append('file', typeof data === 'string' ? new Blob([data]) : data, filename);
345		  formData.append('name', filename);
346
347		  var request = new XMLHttpRequest();
348		  request.responseType = 'json';
349
350		  request.onreadystatechange = mxUtils.bind(this, function()
351		  {
352		    if (request.readyState === 4)
353		    {
354		    	window.clearTimeout(timeoutThread);
355
356		    	if (acceptResponse)
357	    		{
358		    		if (request.status == 200)
359	    			{
360		    			var fileMeta = request.response;
361		    			fileMeta.compoundId = cardId + this.SEPARATOR + fileMeta.id
362		    			success(fileMeta);
363	    			}
364		    		else if (request.status == 401)
365		    		{
366		    			this.authenticate(fn, error, true);
367		    		}
368	    			else
369    				{
370		    			error();
371    				}
372	    		}
373		    }
374		  });
375
376		  request.open('POST', this.baseUrl + 'cards/' + cardId + '/attachments');
377		  request.send(formData);
378		});
379
380		this.authenticate(fn, error);
381	}
382	else
383	{
384		error({message: mxResources.get('unknownError')});
385	}
386};
387
388/**
389 * Checks if the client is authorized and calls the next step.
390 */
391TrelloClient.prototype.pickLibrary = function(fn)
392{
393	this.pickFile(fn);
394};
395
396/**
397 *
398 */
399TrelloClient.prototype.pickFolder = function(fn)
400{
401	this.authenticate(mxUtils.bind(this, function()
402	{
403	  	// show file select
404		this.showTrelloDialog(false, fn);
405	}), mxUtils.bind(this, function(e)
406	{
407		this.ui.showError(mxResources.get('error'), e);
408	}));
409};
410
411/**
412 * Checks if the client is authorized and calls the next step.
413 */
414TrelloClient.prototype.pickFile = function(fn, returnObject)
415{
416	fn = (fn != null) ? fn : mxUtils.bind(this, function(id)
417	{
418		this.ui.loadFile('T' + encodeURIComponent(id));
419	});
420
421	this.authenticate(mxUtils.bind(this, function()
422	{
423	  	// show file select
424		this.showTrelloDialog(true, fn);
425	}), mxUtils.bind(this, function(e)
426	{
427		this.ui.showError(mxResources.get('error'), e, mxResources.get('ok'));
428	}));
429};
430
431
432/**
433 *
434 */
435TrelloClient.prototype.showTrelloDialog = function(showFiles, fn)
436{
437	var cardId = null;
438	var filter = '@me';
439	var linkCounter = 0;
440
441	var content = document.createElement('div');
442	content.style.whiteSpace = 'nowrap';
443	content.style.overflow = 'hidden';
444	content.style.height = '224px';
445
446	var hd = document.createElement('h3');
447	mxUtils.write(hd, showFiles? mxResources.get('selectFile') : mxResources.get('selectCard'));
448	hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
449	content.appendChild(hd);
450
451	var div = document.createElement('div');
452	div.style.whiteSpace = 'nowrap';
453	div.style.overflow = 'auto';
454	div.style.height = '194px';
455	content.appendChild(div);
456
457	var dlg = new CustomDialog(this.ui, content);
458	this.ui.showDialog(dlg.container, 340, 290, true, true);
459
460	dlg.okButton.parentNode.removeChild(dlg.okButton);
461
462	var createLink = mxUtils.bind(this, function(label, fn, preview)
463	{
464		linkCounter++;
465		var div = document.createElement('div');
466		div.style = 'width:100%;text-overflow:ellipsis;overflow:hidden;vertical-align:middle;' +
467			'padding:2px 0 2px 0;background:' + (linkCounter % 2 == 0?
468			((Editor.isDarkMode()) ? '#000' : '#eee') :
469			((Editor.isDarkMode()) ? '' : '#fff'));
470		var link = document.createElement('a');
471		link.style.cursor = 'pointer';
472
473		if (preview != null)
474		{
475			var img = document.createElement('img');
476			img.src = preview.url;
477			img.width = preview.width;
478			img.height= preview.height;
479			img.style= "border: 1px solid black;margin:5px;vertical-align:middle"
480			link.appendChild(img);
481		}
482
483		mxUtils.write(link,  label);
484		mxEvent.addListener(link, 'click', fn);
485
486		div.appendChild(link);
487
488		return div;
489	});
490
491	var error = mxUtils.bind(this, function(err)
492	{
493		this.ui.handleError(err, null, mxUtils.bind(this, function()
494		{
495			this.ui.spinner.stop();
496			this.ui.hideDialog();
497		}));
498	});
499
500	var selectAtt = mxUtils.bind(this, function()
501	{
502		linkCounter = 0;
503		div.innerHTML = '';
504		this.ui.spinner.spin(div, mxResources.get('loading'));
505
506		var callback = mxUtils.bind(this, function()
507		{
508			Trello.cards.get(cardId + '/attachments', {fields: 'id,name,previews'}, mxUtils.bind(this, function(data)
509			{
510				this.ui.spinner.stop();
511				var files = data;
512				div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function()
513				{
514					selectCard();
515				})));
516				mxUtils.br(div);
517
518				if (files == null || files.length == 0)
519				{
520					mxUtils.write(div, mxResources.get('noFiles'));
521				}
522				else
523				{
524					var listFiles = mxUtils.bind(this, function()
525					{
526						for (var i = 0; i < files.length; i++)
527						{
528							(mxUtils.bind(this, function(file)
529							{
530								div.appendChild(createLink(file.name, mxUtils.bind(this, function()
531								{
532									this.ui.hideDialog();
533									fn(cardId + this.SEPARATOR + file.id);
534								}), file.previews != null? file.previews[0] : null));
535							}))(files[i]);
536						}
537					});
538
539					listFiles();
540				}
541			}),
542			mxUtils.bind(this, function(req)
543			{
544	    		if (req.status == 401)
545	    		{
546	    			this.authenticate(callback, error, true);
547	    		}
548	    		else if (error != null)
549	    		{
550	    			error(req);
551	    		}
552			}));
553		});
554
555		callback();
556	});
557
558	// Adds paging for cards (files limited to 1000 by API)
559	var pageSize = 100;
560	var nextPageDiv = null;
561	var scrollFn = null;
562
563	var selectCard = mxUtils.bind(this, function(page)
564	{
565		if (page == null)
566		{
567			linkCounter = 0;
568			div.innerHTML = '';
569			page = 1;
570		}
571
572		this.ui.spinner.spin(div, mxResources.get('loading'));
573
574		if (nextPageDiv != null && nextPageDiv.parentNode != null)
575		{
576			nextPageDiv.parentNode.removeChild(nextPageDiv);
577		}
578
579		nextPageDiv = document.createElement('a');
580		nextPageDiv.style.display = 'block';
581		nextPageDiv.style.cursor = 'pointer';
582		mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
583
584		var nextPage = mxUtils.bind(this, function()
585		{
586			mxEvent.removeListener(div, 'scroll', scrollFn);
587			selectCard(page + 1);
588		});
589
590		mxEvent.addListener(nextPageDiv, 'click', nextPage);
591
592		var callback = mxUtils.bind(this, function()
593		{
594			Trello.get('search', {
595				'query': (mxUtils.trim(filter) == '') ? 'is:open' : filter,
596				'cards_limit': pageSize,
597				'cards_page': page-1
598			},
599			mxUtils.bind(this, function(data)
600			{
601				this.ui.spinner.stop();
602				var cards = (data != null) ? data.cards : null;
603
604				if (cards == null || cards.length == 0)
605				{
606					mxUtils.write(div, mxResources.get('noFiles'));
607				}
608				else
609				{
610					if (page == 1)
611					{
612						div.appendChild(createLink(mxResources.get('filterCards') + '...', mxUtils.bind(this, function()
613						{
614							var dlg = new FilenameDialog(this.ui, filter, mxResources.get('ok'), mxUtils.bind(this, function(value)
615							{
616								if (value != null)
617								{
618									filter = value;
619									selectCard();
620								}
621							}), mxResources.get('filterCards'), null, null, 'http://help.trello.com/article/808-searching-for-cards-all-boards');
622							this.ui.showDialog(dlg.container, 300, 80, true, false);
623							dlg.init();
624						})));
625
626						mxUtils.br(div);
627					}
628
629					for (var i = 0; i < cards.length; i++)
630					{
631						(mxUtils.bind(this, function(card)
632						{
633							div.appendChild(createLink(card.name, mxUtils.bind(this, function()
634							{
635								if (showFiles)
636								{
637									cardId = card.id;
638									selectAtt();
639								}
640								else
641								{
642									this.ui.hideDialog();
643									fn(card.id);
644								}
645							})));
646						}))(cards[i]);
647					}
648
649					if (cards.length == pageSize)
650					{
651						div.appendChild(nextPageDiv);
652
653						scrollFn = function()
654						{
655							if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
656							{
657								nextPage();
658							}
659						};
660
661						mxEvent.addListener(div, 'scroll', scrollFn);
662					}
663				}
664			}),
665			mxUtils.bind(this, function(req)
666			{
667	    		if (req.status == 401)
668	    		{
669	    			this.authenticate(callback, error, true);
670	    		}
671	    		else if (error != null)
672	    		{
673	    			error({message: req.responseText});
674	    		}
675			}));
676		});
677
678		callback();
679	});
680
681	selectCard();
682};
683
684/**
685 * Checks if the client is authorized
686 */
687TrelloClient.prototype.isAuthorized = function()
688{
689	//TODO this may break if Trello client.js is changed
690	try
691	{
692		return localStorage['trello_token'] != null; //Trello.authorized(); doesn't work unless authorize is called first
693	}
694	catch (e)
695	{
696		// ignores access denied
697	}
698
699	return false;
700};
701
702
703/**
704 * Logout and deauthorize the user.
705 */
706TrelloClient.prototype.logout = function()
707{
708	localStorage.removeItem('trello_token');
709	Trello.deauthorize();
710};
711