You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	API: Added more API calls
This commit is contained in:
		| @@ -22,7 +22,6 @@ trap finish EXIT | ||||
|  | ||||
| cd "$ROOT_DIR" | ||||
| npm test tests-build/ArrayUtils.js | ||||
| npm test tests-build/encryption.js | ||||
| npm test tests-build/EnexToMd.js | ||||
| npm test tests-build/HtmlToMd.js | ||||
| npm test tests-build/markdownUtils.js | ||||
| @@ -32,5 +31,7 @@ npm test tests-build/models_Tag.js | ||||
| npm test tests-build/models_Setting.js | ||||
| npm test tests-build/services_InteropService.js | ||||
| npm test tests-build/services_ResourceService.js | ||||
| npm test tests-build/synchronizer.js | ||||
| npm test tests-build/urlUtils.js | ||||
| npm test tests-build/urlUtils.js | ||||
| npm test tests-build/encryption.js | ||||
| npm test tests-build/services_rest_Api.js | ||||
| npm test tests-build/synchronizer.js | ||||
| @@ -6,6 +6,7 @@ const markdownUtils = require('lib/markdownUtils.js'); | ||||
| const Api = require('lib/services/rest/Api'); | ||||
| const Folder = require('lib/models/Folder'); | ||||
| const Note = require('lib/models/Note'); | ||||
| const Tag = require('lib/models/Tag'); | ||||
| const Resource = require('lib/models/Resource'); | ||||
|  | ||||
| jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; | ||||
| @@ -45,6 +46,60 @@ describe('services_rest_Api', function() { | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should update folders', async (done) => { | ||||
| 		let f1 = await Folder.save({ title: "mon carnet" }); | ||||
| 		const response = await api.route('PUT', 'folders/' + f1.id, null, JSON.stringify({ | ||||
| 			title: 'modifié', | ||||
| 		})); | ||||
|  | ||||
| 		let f1b = await Folder.load(f1.id); | ||||
| 		expect(f1b.title).toBe('modifié'); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should delete folders', async (done) => { | ||||
| 		let f1 = await Folder.save({ title: "mon carnet" }); | ||||
| 		await api.route('DELETE', 'folders/' + f1.id); | ||||
|  | ||||
| 		let f1b = await Folder.load(f1.id); | ||||
| 		expect(!f1b).toBe(true); | ||||
| 		 | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should create folders', async (done) => { | ||||
| 		const response = await api.route('POST', 'folders', null, JSON.stringify({ | ||||
| 			title: 'from api', | ||||
| 		})); | ||||
|  | ||||
| 		expect(!!response.id).toBe(true); | ||||
|  | ||||
| 		let f = await Folder.all(); | ||||
| 		expect(f.length).toBe(1); | ||||
| 		expect(f[0].title).toBe('from api'); | ||||
| 		 | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should get one folder', async (done) => { | ||||
| 		let f1 = await Folder.save({ title: "mon carnet" }); | ||||
| 		const response = await api.route('GET', 'folders/' + f1.id); | ||||
| 		expect(response.id).toBe(f1.id); | ||||
|  | ||||
| 		const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'folders/doesntexist')); | ||||
| 		expect(hasThrown).toBe(true); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should fail on invalid paths', async (done) => { | ||||
| 		const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'schtroumpf')); | ||||
| 		expect(hasThrown).toBe(true); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should get notes', async (done) => { | ||||
| 		let response = null; | ||||
| 		const f1 = await Folder.save({ title: "mon carnet" }); | ||||
| @@ -159,4 +214,44 @@ describe('services_rest_Api', function() { | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should add tags to notes', async (done) => { | ||||
| 		const tag = await Tag.save({ title: "mon étiquette" }); | ||||
| 		const note = await Note.save({ title: "ma note" }); | ||||
|  | ||||
| 		const response = await api.route('POST', 'tags/' + tag.id + '/notes', null, JSON.stringify({ | ||||
| 			id: note.id, | ||||
| 		})); | ||||
|  | ||||
| 		const noteIds = await Tag.noteIds(tag.id);	 | ||||
| 		expect(noteIds[0]).toBe(note.id); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should remove tags from notes', async (done) => { | ||||
| 		const tag = await Tag.save({ title: "mon étiquette" }); | ||||
| 		const note = await Note.save({ title: "ma note" }); | ||||
| 		await Tag.addNote(tag.id, note.id); | ||||
|  | ||||
| 		const response = await api.route('DELETE', 'tags/' + tag.id + '/notes/' + note.id); | ||||
|  | ||||
| 		const noteIds = await Tag.noteIds(tag.id);	 | ||||
| 		expect(noteIds.length).toBe(0); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should list all tag notes', async (done) => { | ||||
| 		const tag = await Tag.save({ title: "mon étiquette" }); | ||||
| 		const note1 = await Note.save({ title: "ma note un" }); | ||||
| 		const note2 = await Note.save({ title: "ma note deux" }); | ||||
| 		await Tag.addNote(tag.id, note1.id); | ||||
| 		await Tag.addNote(tag.id, note2.id); | ||||
|  | ||||
| 		const response = await api.route('GET', 'tags/' + tag.id + '/notes'); | ||||
| 		expect(response.length).toBe(2); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										119
									
								
								ElectronClient/app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										119
									
								
								ElectronClient/app/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -40,18 +40,31 @@ | ||||
|       "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" | ||||
|     }, | ||||
|     "acorn": { | ||||
|       "version": "5.7.2", | ||||
|       "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz", | ||||
|       "integrity": "sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw==" | ||||
|       "version": "5.7.3", | ||||
|       "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", | ||||
|       "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" | ||||
|     }, | ||||
|     "acorn-globals": { | ||||
|       "version": "4.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", | ||||
|       "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", | ||||
|       "version": "4.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", | ||||
|       "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", | ||||
|       "requires": { | ||||
|         "acorn": "^5.0.0" | ||||
|         "acorn": "^6.0.1", | ||||
|         "acorn-walk": "^6.0.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "acorn": { | ||||
|           "version": "6.0.2", | ||||
|           "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz", | ||||
|           "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "acorn-walk": { | ||||
|       "version": "6.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz", | ||||
|       "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg==" | ||||
|     }, | ||||
|     "ajv": { | ||||
|       "version": "6.5.0", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.0.tgz", | ||||
| @@ -914,9 +927,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "browser-process-hrtime": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", | ||||
|       "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=" | ||||
|       "version": "0.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", | ||||
|       "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" | ||||
|     }, | ||||
|     "buffer-from": { | ||||
|       "version": "1.1.0", | ||||
| @@ -1783,6 +1796,11 @@ | ||||
|       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||
|       "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" | ||||
|     }, | ||||
|     "depd": { | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", | ||||
|       "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" | ||||
|     }, | ||||
|     "detect-indent": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", | ||||
| @@ -3519,6 +3537,18 @@ | ||||
|       "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", | ||||
|       "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" | ||||
|     }, | ||||
|     "http-errors": { | ||||
|       "version": "1.7.1", | ||||
|       "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz", | ||||
|       "integrity": "sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==", | ||||
|       "requires": { | ||||
|         "depd": "~1.1.2", | ||||
|         "inherits": "2.0.3", | ||||
|         "setprototypeof": "1.1.0", | ||||
|         "statuses": ">= 1.5.0 < 2", | ||||
|         "toidentifier": "1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "http-signature": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", | ||||
| @@ -3909,9 +3939,9 @@ | ||||
|       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" | ||||
|     }, | ||||
|     "joplin-turndown": { | ||||
|       "version": "4.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.8.tgz", | ||||
|       "integrity": "sha512-RPZJSZEplVPL3UiJNkaKsFAG8bCGofsKIiH24s8/4qcy1xYnEufvg++rHm7rxi/0VCtpSkRBlWHSs1/srJZvoA==", | ||||
|       "version": "4.0.9", | ||||
|       "resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.9.tgz", | ||||
|       "integrity": "sha512-8MOxX4t5Ai22muHhXPMGNoKc/AB7gSo0eUvNh6dyd6b3vcSiMIRZE8UHpMjS9ruJQ+8e+8TtJXc0nfbexeHwrA==", | ||||
|       "requires": { | ||||
|         "jsdom": "^11.9.0" | ||||
|       } | ||||
| @@ -4483,6 +4513,32 @@ | ||||
|       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", | ||||
|       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" | ||||
|     }, | ||||
|     "multiparty": { | ||||
|       "version": "4.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.1.tgz", | ||||
|       "integrity": "sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA==", | ||||
|       "requires": { | ||||
|         "fd-slicer": "1.1.0", | ||||
|         "http-errors": "~1.7.0", | ||||
|         "safe-buffer": "5.1.2", | ||||
|         "uid-safe": "2.1.5" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "fd-slicer": { | ||||
|           "version": "1.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", | ||||
|           "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", | ||||
|           "requires": { | ||||
|             "pend": "~1.2.0" | ||||
|           } | ||||
|         }, | ||||
|         "safe-buffer": { | ||||
|           "version": "5.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", | ||||
|           "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "nan": { | ||||
|       "version": "2.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", | ||||
| @@ -4888,8 +4944,7 @@ | ||||
|     "pend": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", | ||||
|       "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", | ||||
|       "dev": true | ||||
|       "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" | ||||
|     }, | ||||
|     "performance-now": { | ||||
|       "version": "2.1.0", | ||||
| @@ -5059,6 +5114,11 @@ | ||||
|       "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", | ||||
|       "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" | ||||
|     }, | ||||
|     "random-bytes": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", | ||||
|       "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" | ||||
|     }, | ||||
|     "randomatic": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", | ||||
| @@ -5539,6 +5599,11 @@ | ||||
|       "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", | ||||
|       "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" | ||||
|     }, | ||||
|     "setprototypeof": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", | ||||
|       "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" | ||||
|     }, | ||||
|     "shebang-command": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", | ||||
| @@ -6538,6 +6603,11 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "statuses": { | ||||
|       "version": "1.5.0", | ||||
|       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", | ||||
|       "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" | ||||
|     }, | ||||
|     "stealthy-require": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", | ||||
| @@ -6816,6 +6886,11 @@ | ||||
|         "repeat-string": "^1.6.1" | ||||
|       } | ||||
|     }, | ||||
|     "toidentifier": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", | ||||
|       "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" | ||||
|     }, | ||||
|     "tough-cookie": { | ||||
|       "version": "2.3.4", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", | ||||
| @@ -6898,6 +6973,14 @@ | ||||
|       "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", | ||||
|       "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==" | ||||
|     }, | ||||
|     "uid-safe": { | ||||
|       "version": "2.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", | ||||
|       "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", | ||||
|       "requires": { | ||||
|         "random-bytes": "~1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "union-value": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", | ||||
| @@ -7182,9 +7265,9 @@ | ||||
|       "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" | ||||
|     }, | ||||
|     "whatwg-mimetype": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", | ||||
|       "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==" | ||||
|       "version": "2.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz", | ||||
|       "integrity": "sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw==" | ||||
|     }, | ||||
|     "whatwg-url": { | ||||
|       "version": "6.5.0", | ||||
|   | ||||
| @@ -105,6 +105,7 @@ | ||||
|     "mermaid": "^8.0.0-rc.8", | ||||
|     "mime": "^2.3.1", | ||||
|     "moment": "^2.22.2", | ||||
|     "multiparty": "^4.2.1", | ||||
|     "node-fetch": "^1.7.3", | ||||
|     "node-notifier": "^5.2.1", | ||||
|     "promise": "^8.0.1", | ||||
|   | ||||
| @@ -494,6 +494,12 @@ class BaseApplication { | ||||
| 			setLocale(Setting.value('locale')); | ||||
| 		} | ||||
|  | ||||
| 		if (!Setting.value('api.token')) { | ||||
| 			EncryptionService.instance().randomHexString(64).then((token) => { | ||||
| 				Setting.setValue('api.token', token); | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		time.setDateFormat(Setting.value('dateFormat')); | ||||
| 		time.setTimeFormat(Setting.value('timeFormat')); | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const { Logger } = require('lib/logger.js'); | ||||
| const randomClipperPort = require('lib/randomClipperPort'); | ||||
| const enableServerDestroy = require('server-destroy'); | ||||
| const Api = require('lib/services/rest/Api'); | ||||
| const multiparty = require('multiparty'); | ||||
|  | ||||
| class ClipperServer { | ||||
|  | ||||
| @@ -13,7 +14,9 @@ class ClipperServer { | ||||
| 		this.startState_ = 'idle'; | ||||
| 		this.server_ = null; | ||||
| 		this.port_ = null; | ||||
| 		this.api_ = new Api(); | ||||
| 		this.api_ = new Api(() => { | ||||
| 			return Setting.value('api.token'); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	static instance() { | ||||
| @@ -122,32 +125,50 @@ class ClipperServer { | ||||
|  | ||||
| 			const url = urlParser.parse(request.url, true); | ||||
|  | ||||
| 			const execRequest = async (request, body = '') => { | ||||
| 			const execRequest = async (request, body = '', files = []) => { | ||||
| 				try { | ||||
| 					const response = await this.api_.route(request.method, url.pathname, url.query, body); | ||||
| 					const response = await this.api_.route(request.method, url.pathname, url.query, body, files); | ||||
| 					writeResponse(200, response); | ||||
| 				} catch (error) { | ||||
| 					console.error(error); | ||||
| 					writeResponse(error.httpCode ? error.httpCode : 500, error.message); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			const contentType = request.headers['content-type'] ? request.headers['content-type'] : ''; | ||||
|  | ||||
| 			if (request.method === 'OPTIONS') { | ||||
| 				writeCorsHeaders(200); | ||||
| 				response.end(); | ||||
| 			} else { | ||||
| 				if (request.method === 'POST') { | ||||
| 					let body = ''; | ||||
| 				if (contentType.indexOf('multipart/form-data') === 0) { | ||||
| 				    const form = new multiparty.Form(); | ||||
|  | ||||
| 					request.on('data', (data) => { | ||||
| 						body += data; | ||||
| 					}); | ||||
|  | ||||
| 					request.on('end', async () => { | ||||
| 						execRequest(request, body); | ||||
| 					}); | ||||
| 				    form.parse(request, function(error, fields, files) { | ||||
| 				    	if (error) { | ||||
| 							writeResponse(error.httpCode ? error.httpCode : 500, error.message); | ||||
| 							return; | ||||
| 						} else { | ||||
| 							execRequest( | ||||
| 								request, | ||||
| 								fields && fields.props && fields.props.length ? fields.props[0] : '', | ||||
| 								files && files.data ? files.data : [] | ||||
| 							); | ||||
| 						} | ||||
| 				    }); | ||||
| 				} else { | ||||
| 					execRequest(request); | ||||
| 					if (request.method === 'POST') { | ||||
| 						let body = ''; | ||||
|  | ||||
| 						request.on('data', (data) => { | ||||
| 							body += data; | ||||
| 						}); | ||||
|  | ||||
| 						request.on('end', async () => { | ||||
| 							execRequest(request, body); | ||||
| 						}); | ||||
| 					} else { | ||||
| 						execRequest(request); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|   | ||||
| @@ -154,6 +154,8 @@ class Setting extends BaseModel { | ||||
|  | ||||
| 			'net.customCertificates': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0 }, public: true, appTypes: ['desktop', 'cli'], label: () => _('Custom TLS certificates'), description: () => _('Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on "Check synchronisation configuration".') }, | ||||
| 			'net.ignoreTlsErrors': { value: false, type: Setting.TYPE_BOOL, show: (settings) => { return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0 }, public: true, appTypes: ['desktop', 'cli'], label: () => _('Ignore TLS certificate errors') }, | ||||
| 		 | ||||
| 			'api.token': { value: null, type: Setting.TYPE_STRING, public: false }, | ||||
| 		}; | ||||
|  | ||||
| 		return this.metadata_; | ||||
|   | ||||
| @@ -213,6 +213,11 @@ class EncryptionService { | ||||
| 		sjcl.random.addEntropy(hexSeed, 1024, 'shim.randomBytes'); | ||||
| 	} | ||||
|  | ||||
| 	async randomHexString(byteCount) { | ||||
| 		const bytes = await shim.randomBytes(byteCount); | ||||
| 		return bytes.map((a) => { return hexPad(a.toString(16), 2); }).join(''); | ||||
| 	} | ||||
|  | ||||
| 	async generateMasterKey(password) { | ||||
| 		const bytes = await shim.randomBytes(256); | ||||
| 		const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join(''); | ||||
|   | ||||
| @@ -2,6 +2,8 @@ const { ltrimSlashes } = require('lib/path-utils.js'); | ||||
| const Folder = require('lib/models/Folder'); | ||||
| const Note = require('lib/models/Note'); | ||||
| const Tag = require('lib/models/Tag'); | ||||
| const BaseItem = require('lib/models/BaseItem'); | ||||
| const BaseModel = require('lib/BaseModel'); | ||||
| const Setting = require('lib/models/Setting'); | ||||
| const markdownUtils = require('lib/markdownUtils'); | ||||
| const mimeUtils = require('lib/mime-utils.js').mime; | ||||
| @@ -48,6 +50,14 @@ class ErrorForbidden extends ApiError { | ||||
|  | ||||
| } | ||||
|  | ||||
| class ErrorBadRequest extends ApiError { | ||||
|  | ||||
| 	constructor(message = 'Bad Request') { | ||||
| 		super(message, 400); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| class Api { | ||||
|  | ||||
| 	constructor(token = null) { | ||||
| @@ -56,25 +66,68 @@ class Api { | ||||
| 	} | ||||
|  | ||||
| 	get token() { | ||||
| 		return this.token_; | ||||
| 		return typeof this.token_ === 'function' ? this.token_() : this.token_; | ||||
| 	} | ||||
|  | ||||
| 	async route(method, path, query = null, body = null) { | ||||
| 	parsePath(path) { | ||||
| 		path = ltrimSlashes(path); | ||||
| 		if (!path) throw new ErrorNotFound(); // Nothing at the root yet | ||||
| 		if (!path) return { callName: '', params: [] }; | ||||
|  | ||||
| 		const pathParts = path.split('/'); | ||||
| 		const callSuffix = pathParts.splice(0,1)[0]; | ||||
| 		const callName = 'action_' + callSuffix; | ||||
| 		if (!this[callName]) throw new ErrorNotFound(); | ||||
| 		let callName = 'action_' + callSuffix; | ||||
| 		return { | ||||
| 			callName: callName, | ||||
| 			params: pathParts, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	async route(method, path, query = null, body = null, files = null) { | ||||
| 		if (!files) files = []; | ||||
|  | ||||
| 		const parsedPath = this.parsePath(path); | ||||
| 		if (!parsedPath.callName) throw new ErrorNotFound(); // Nothing at the root yet | ||||
| 		 | ||||
| 		const request = { | ||||
| 			method: method, | ||||
| 			path: ltrimSlashes(path), | ||||
| 			query: query ? query : {}, | ||||
| 			body: body, | ||||
| 			bodyJson_: null, | ||||
| 			bodyJson: function(disallowedProperties = null) { | ||||
| 				if (!this.bodyJson_) this.bodyJson_ = JSON.parse(this.body); | ||||
|  | ||||
| 				if (disallowedProperties) { | ||||
| 					const filteredBody = Object.assign({}, this.bodyJson_); | ||||
| 					for (let i = 0; i < disallowedProperties.length; i++) { | ||||
| 						const n = disallowedProperties[i]; | ||||
| 						delete filteredBody[n]; | ||||
| 					} | ||||
| 					return filteredBody; | ||||
| 				} | ||||
|  | ||||
| 				return this.bodyJson_; | ||||
| 			}, | ||||
| 			files: files, | ||||
| 		} | ||||
|  | ||||
| 		let id = null; | ||||
| 		let link = null; | ||||
| 		let params = parsedPath.params; | ||||
|  | ||||
| 		if (params.length >= 1) { | ||||
| 			id = params[0]; | ||||
| 			params.splice(0, 1); | ||||
| 			if (params.length >= 1) { | ||||
| 				link = params[0]; | ||||
| 				params.splice(0, 1); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		request.params = params; | ||||
|  | ||||
| 		try { | ||||
| 			return this[callName]({ | ||||
| 				method: method, | ||||
| 				query: query ? query : {}, | ||||
| 				body: body, | ||||
| 				params: pathParts, | ||||
| 			}); | ||||
| 			return this[parsedPath.callName](request, id, link); | ||||
| 		} catch (error) { | ||||
| 			if (!error.httpCode) error.httpCode = 500; | ||||
| 			throw error; | ||||
| @@ -89,6 +142,10 @@ class Api { | ||||
| 		return this.logger_; | ||||
| 	} | ||||
|  | ||||
| 	get readonlyProperties() { | ||||
| 		return ['id', 'created_time', 'updated_time', 'encryption_blob_encrypted', 'encryption_applied', 'encryption_cipher_text']; | ||||
| 	} | ||||
|  | ||||
| 	fields_(request, defaultFields) { | ||||
| 		const query = request.query; | ||||
| 		if (!query || !query.fields) return defaultFields; | ||||
| @@ -97,39 +154,139 @@ class Api { | ||||
| 	} | ||||
|  | ||||
| 	checkToken_(request) { | ||||
| 		// For now, whitelist some calls to allow the web clipper to work | ||||
| 		// without an extra auth step | ||||
| 		const whiteList = [ | ||||
| 			[ 'GET', 'ping' ], | ||||
| 			[ 'GET', 'tags' ], | ||||
| 			[ 'GET', 'folders' ], | ||||
| 			[ 'POST', 'notes' ], | ||||
| 		]; | ||||
|  | ||||
| 		for (let i = 0; i < whiteList.length; i++) { | ||||
| 			if (whiteList[i][0] === request.method && whiteList[i][1] === request.path) return; | ||||
| 		} | ||||
|  | ||||
| 		if (!this.token) return; | ||||
| 		if (!request.query || !request.query.token) throw new ErrorForbidden('Missing "token" parameter'); | ||||
| 		if (request.query.token !== this.token) throw new ErrorForbidden('Invalid "token" parameter'); | ||||
| 	} | ||||
|  | ||||
| 	async action_ping(request) { | ||||
| 	async defaultAction_(modelType, request, id = null, link = null) { | ||||
| 		this.checkToken_(request); | ||||
|  | ||||
| 		if (link) throw new ErrorNotFound(); // Default action doesn't support links at all for now | ||||
|  | ||||
| 		const ModelClass = BaseItem.getClassByItemType(modelType); | ||||
|  | ||||
| 		const getOneModel = async () => { | ||||
| 			const model = await ModelClass.load(id); | ||||
| 			if (!model) throw new ErrorNotFound();			 | ||||
| 			return model; | ||||
| 		} | ||||
|  | ||||
| 		if (request.method === 'GET') { | ||||
| 			if (id) { | ||||
| 				return getOneModel(); | ||||
| 			} else { | ||||
| 				const options = {}; | ||||
| 				const fields = this.fields_(request, []); | ||||
| 				if (fields.length) options.fields = fields; | ||||
| 				return await ModelClass.all(options); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (request.method === 'PUT' && id) { | ||||
| 			const model = await getOneModel(); | ||||
| 			let newModel = Object.assign({}, model, request.bodyJson(this.readonlyProperties)); | ||||
| 			newModel = await ModelClass.save(newModel, { userSideValidation: true }); | ||||
| 			return newModel; | ||||
| 		} | ||||
|  | ||||
| 		if (request.method === 'DELETE' && id) { | ||||
| 			const model = await getOneModel(); | ||||
| 			await ModelClass.delete(model.id); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (request.method === 'POST') { | ||||
| 			const model = request.bodyJson(this.readonlyProperties); | ||||
| 			const result = await ModelClass.save(model, { userSideValidation: true }); | ||||
| 			return result; | ||||
| 		} | ||||
|  | ||||
| 		throw new ErrorMethodNotAllowed(); | ||||
| 	} | ||||
|  | ||||
| 	async action_ping(request, id = null, link = null) { | ||||
| 		if (request.method === 'GET') { | ||||
| 			return 'JoplinClipperServer'; | ||||
| 		} | ||||
|  | ||||
| 		throw new ErrorMethodNotAllowed(); | ||||
| 	} | ||||
|  | ||||
| 	async action_folders(request) { | ||||
| 		if (request.method === 'GET') { | ||||
| 	async action_folders(request, id = null, link = null) { | ||||
| 		if (request.method === 'GET' && !id) { | ||||
| 			return await Folder.allAsTree({ fields: this.fields_(request, ['id', 'parent_id', 'title']) }); | ||||
| 		} | ||||
|  | ||||
| 		throw new ErrorMethodNotAllowed(); | ||||
| 		return this.defaultAction_(BaseModel.TYPE_FOLDER, request, id, link); | ||||
| 	} | ||||
|  | ||||
| 	async action_tags(request) { | ||||
| 		if (request.method === 'GET') { | ||||
| 			return await Tag.all({ fields: this.fields_(request, ['id', 'title']) }) | ||||
| 	async action_tags(request, id = null, link = null) { | ||||
| 		if (link === 'notes') { | ||||
| 			const tag = await Tag.load(id); | ||||
| 			if (!tag) throw new ErrorNotFound(); | ||||
|  | ||||
| 			if (request.method === 'POST') { | ||||
| 				const note = request.bodyJson(); | ||||
| 				if (!note || !note.id) throw new ErrorBadRequest('Missing note ID'); | ||||
| 				return await Tag.addNote(tag.id, note.id); | ||||
| 			} | ||||
|  | ||||
| 			if (request.method === 'DELETE') { | ||||
| 				const noteId = request.params.length ? request.params[0] : null; | ||||
| 				if (!noteId) throw new ErrorBadRequest('Missing note ID'); | ||||
| 				await Tag.removeNote(tag.id, noteId); | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if (request.method === 'GET') { | ||||
| 				return await Tag.noteIds(tag.id); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		throw new ErrorMethodNotAllowed(); | ||||
| 		return this.defaultAction_(BaseModel.TYPE_TAG, request, id, link); | ||||
| 	} | ||||
|  | ||||
| 	async action_notes(request) { | ||||
| 	async action_master_keys(request, id = null, link = null) { | ||||
| 		return this.defaultAction_(BaseModel.TYPE_MASTER_KEY, request, id, link); | ||||
| 	} | ||||
|  | ||||
| 	async action_resources(request, id = null, link = null) { | ||||
| 		// fieldName: "data" | ||||
| 		// headers: Object | ||||
| 		// originalFilename: "test.jpg" | ||||
| 		// path: "C:\Users\Laurent\AppData\Local\Temp\BW77wkpP23iIGUstd0kDuXXC.jpg" | ||||
| 		// size: 164394 | ||||
|  | ||||
| 		if (request.method === 'POST') { | ||||
| 			if (!request.files.length) throw new ErrorBadRequest('Resource cannot be created without a file'); | ||||
| 			const filePath = request.files[0].path; | ||||
| 			const resource = await shim.createResourceFromPath(filePath); | ||||
| 			const newResource = Object.assign({}, resource, request.bodyJson(this.readonlyProperties)); | ||||
| 			return await Resource.save(newResource); | ||||
| 		} | ||||
|  | ||||
| 		return this.defaultAction_(BaseModel.TYPE_RESOURCE, request, id, link); | ||||
| 	} | ||||
|  | ||||
| 	async action_notes(request, id = null, link = null) { | ||||
| 		if (request.method === 'GET') { | ||||
| 			this.checkToken_(request); | ||||
|  | ||||
| 			const noteId = request.params.length ? request.params[0] : null; | ||||
| 			const noteId = id; | ||||
| 			const parentId = request.query.parent_id ? request.query.parent_id : null; | ||||
| 			const fields = this.fields_(request, []); // previews() already returns default fields | ||||
| 			const options = {}; | ||||
| @@ -177,7 +334,7 @@ class Api { | ||||
| 			return note; | ||||
| 		} | ||||
|  | ||||
| 		throw new ErrorMethodNotAllowed(); | ||||
| 		return this.defaultAction_(BaseModel.TYPE_NOTE, request, id, link); | ||||
| 	} | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -143,9 +143,7 @@ function shimInit() { | ||||
| 			await fs.copy(filePath, targetPath, { overwrite: true }); | ||||
| 		} | ||||
|  | ||||
| 		await Resource.save(resource, { isNew: true }); | ||||
|  | ||||
| 		return resource; | ||||
| 		return await Resource.save(resource, { isNew: true }); | ||||
| 	} | ||||
|  | ||||
| 	shim.attachFileToNote = async function(note, filePath, position = null) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user