Skip to content

Commit fa70aa3

Browse files
authored
Merge pull request #2031 from floccusaddon/feat/capabilities
feat(NextcloudBookmarks): Use capabilities for feature detection + JWT ticket authentication
2 parents 01d95bd + 2e3554e commit fa70aa3

File tree

2 files changed

+102
-38
lines changed

2 files changed

+102
-38
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"version": "5.6.0",
44
"description": "Sync your bookmarks privately across browsers and devices",
55
"scripts": {
6-
"build": "NODE_OPTIONS=--max-old-space-size=6000 gulp",
7-
"build-win": "SET NODE_OPTIONS=--max-old-space-size=6000 & gulp",
8-
"build-release": "NODE_OPTIONS=--max-old-space-size=6000 gulp release",
9-
"build-release-win": "SET NODE_OPTIONS=--max-old-space-size=6000 & gulp release",
10-
"watch": "NODE_OPTIONS=--max-old-space-size=6000 gulp watch",
11-
"watch-win": "SET NODE_OPTIONS=--max-old-space-size=6000 & gulp watch",
6+
"build": "NODE_OPTIONS=--max-old-space-size=8000 gulp",
7+
"build-win": "SET NODE_OPTIONS=--max-old-space-size=8000 & gulp",
8+
"build-release": "NODE_OPTIONS=--max-old-space-size=8000 gulp release",
9+
"build-release-win": "SET NODE_OPTIONS=--max-old-space-size=8000 & gulp release",
10+
"watch": "NODE_OPTIONS=--max-old-space-size=8000 gulp watch",
11+
"watch-win": "SET NODE_OPTIONS=--max-old-space-size=8000 & gulp watch",
1212
"test": "node --unhandled-rejections=strict test/selenium-runner.js",
1313
"lint": "eslint --ext .js,.vue src",
1414
"lint:fix": "eslint --ext .js,.vue src --fix"

src/lib/adapters/NextcloudBookmarks.ts

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
BulkImportResource,
1414
ClickCountResource, ICapabilities, IHashSettings,
1515
LoadFolderChildrenResource,
16-
OrderFolderResource
16+
OrderFolderResource, THashFunction
1717
} from '../interfaces/Resource'
1818
import Ordering from '../interfaces/Ordering'
1919
import {
@@ -65,7 +65,6 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
6565
private server: NextcloudBookmarksConfig
6666
private fetchQueue: PQueue<{ concurrency: 12 }>
6767
private bookmarkLock: AsyncLock
68-
public hasFeatureBulkImport:boolean = null
6968
private list: Bookmark<typeof ItemLocation.SERVER>[]
7069
private tree: Folder<typeof ItemLocation.SERVER>
7170
private abortController: AbortController
@@ -78,6 +77,9 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
7877
private locked = false
7978
private hasFeatureJavascriptLinks: boolean = null
8079
private hashSettings: IHashSettings
80+
private capabilities: any
81+
private ticket: string
82+
private ticketTimestamp: number
8183

8284
constructor(server: NextcloudBookmarksConfig) {
8385
this.server = server
@@ -102,6 +104,8 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
102104

103105
setData(data:NextcloudBookmarksConfig):void {
104106
this.server = { ...data }
107+
this.ticket = null
108+
this.ticketTimestamp = 0
105109
}
106110

107111
getData():NextcloudBookmarksConfig {
@@ -151,6 +155,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
151155
this.canceled = false
152156
this.ended = false
153157

158+
this.capabilities = await this.getNextcloudCapabilities()
154159
await this.checkFeatureJavascriptLinks()
155160

156161
this.abortController = new AbortController()
@@ -317,12 +322,13 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
317322
}
318323

319324
async _getFolderHash(folderId:string|number):Promise<string> {
320-
if (this.hashSettings.hashFn !== 'sha256') {
321-
throw new Error('Unsupported hash function: ' + this.hashSettings.hashFn + ' - Nextcloud Bookmarks only supports sha256')
325+
const hashFn = {'sha256': 'sha256', 'murmur3': 'murmur3a', 'xxhash3': 'xxh32'}[this.hashSettings.hashFn]
326+
if (this.capabilities && this.capabilities.bookmarks && this.capabilities.bookmarks['hash-function'] && !this.capabilities.bookmarks['hash-function'].includes[hashFn]) {
327+
throw new Error('Selected hash function is not supported by server')
322328
}
323329
return this.sendRequest(
324330
'GET',
325-
`index.php/apps/bookmarks/public/rest/v2/folder/${folderId}/hash`
331+
`index.php/apps/bookmarks/public/rest/v2/folder/${folderId}/hash?hashFn=${hashFn}`
326332
)
327333
.catch(() => {
328334
return { data: '0' } // fallback
@@ -431,9 +437,6 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
431437
}
432438

433439
async bulkImportFolder(parentId:string|number, folder:Folder<typeof ItemLocation.SERVER>):Promise<Folder<typeof ItemLocation.SERVER>> {
434-
if (this.hasFeatureBulkImport === false) {
435-
throw new Error('Current server does not support bulk import')
436-
}
437440
if (folder.count() > 75) {
438441
throw new Error('Refusing to bulk import more than 75 bookmarks')
439442
}
@@ -456,18 +459,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
456459
const body = new FormData()
457460
body.append('bm_import', blob, 'upload.html')
458461

459-
let json
460-
try {
461-
json = await this.sendRequest(
462-
'POST',
463-
`index.php/apps/bookmarks/public/rest/v2/folder/${parentId}/import`,
464-
'multipart/form-data',
465-
body
466-
)
467-
} catch (e) {
468-
this.hasFeatureBulkImport = false
469-
throw e
470-
}
462+
const json = await this.sendRequest(
463+
'POST',
464+
`index.php/apps/bookmarks/public/rest/v2/folder/${parentId}/import`,
465+
'multipart/form-data',
466+
body
467+
)
471468

472469
const recurseChildren = (children, id, title, parentId) => {
473470
return new Folder({
@@ -771,7 +768,19 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
771768
})
772769
}
773770

771+
async getNextcloudCapabilities(): Promise<any> {
772+
const data = await this.sendOCSRequest(
773+
'GET',
774+
`/ocs/v2.php/cloud/capabilities?format=json`,
775+
)
776+
return data.capabilities
777+
}
778+
774779
async checkFeatureJavascriptLinks(): Promise<void> {
780+
if (this.capabilities && this.capabilities.bookmarks && typeof this.capabilities.bookmarks['javascript-bookmarks'] !== 'undefined') {
781+
this.hasFeatureJavascriptLinks = this.capabilities.bookmarks['javascript-bookmarks']
782+
return
783+
}
775784
try {
776785
const json = await this.sendRequest(
777786
'GET',
@@ -794,11 +803,34 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
794803
}
795804
}
796805

797-
async sendRequest(verb:string, relUrl:string, type:string = null, body:any = null, returnRawResponse = false):Promise<any> {
806+
async sendOCSRequest(verb: string, relUrl: string, type: string = null, body: any = null) {
807+
const res = await this.sendRequest(verb, relUrl, type, body, true, {
808+
'OCS-APIRequest': 'true',
809+
})
810+
811+
if (res.status === 401 || res.status === 403) {
812+
throw new AuthenticationError()
813+
}
814+
if (res.status === 503 || res.status >= 400) {
815+
const url = this.normalizeServerURL(this.server.url) + relUrl
816+
Logger.log(`${verb} ${url}: Server responded with ${res.status}: ` + (await res.text()).substring(0, 250))
817+
throw new HttpError(res.status, verb)
818+
}
819+
let json
820+
try {
821+
json = await res.json()
822+
} catch (e) {
823+
throw new ParseResponseError(e.message)
824+
}
825+
return json.ocs.data
826+
}
827+
828+
async sendRequest(verb:string, relUrl:string, type:string = null, originalBody:any = null, returnRawResponse = false, headers = {}):Promise<any> {
798829
const url = this.normalizeServerURL(this.server.url) + relUrl
799830
let res
800831
let timedOut = false
801832

833+
let body = originalBody
802834
if (type && type.includes('application/json')) {
803835
body = JSON.stringify(body)
804836
} else if (type && type.includes('application/x-www-form-urlencoded')) {
@@ -812,12 +844,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
812844
Logger.log(`QUEUING ${verb} ${url}`)
813845

814846
if (Capacitor.getPlatform() !== 'web') {
815-
return this.sendRequestNative(verb, url, type, body, returnRawResponse)
847+
return this.sendRequestNative(verb, url, type, body, returnRawResponse, headers)
816848
}
817849

818-
const authString = Base64.encode(
819-
this.server.username + ':' + this.server.password
820-
)
850+
const authString = !this.ticket || this.ticketTimestamp + 60 * 60 * 1000 < Date.now()
851+
? 'Basic ' + Base64.encode(this.server.username + ':' + this.server.password)
852+
: 'Bearer ' + this.ticket
821853

822854
try {
823855
res = await this.fetchQueue.add(() => {
@@ -828,7 +860,8 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
828860
credentials: this.server.includeCredentials ? 'include' : 'omit',
829861
headers: {
830862
...(type && type !== 'multipart/form-data' && { 'Content-type': type }),
831-
Authorization: 'Basic ' + authString,
863+
Authorization: authString,
864+
...headers
832865
},
833866
signal: this.abortSignal,
834867
...(body && !['get', 'head'].includes(verb.toLowerCase()) && { body }),
@@ -854,6 +887,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
854887
throw new RedirectError()
855888
}
856889

890+
if ((res.status === 401 || res.status === 403 || res.status === 404) && authString.startsWith('Bearer')) {
891+
this.ticket = null
892+
this.ticketTimestamp = 0
893+
return this.sendRequest(verb, relUrl, type, originalBody, returnRawResponse, headers)
894+
}
895+
857896
if (returnRawResponse) {
858897
return res
859898
}
@@ -875,6 +914,11 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
875914
throw new Error('Nextcloud API error for request ' + verb + ' ' + relUrl + ' : \n' + JSON.stringify(json))
876915
}
877916

917+
if (json.ticket) {
918+
this.ticket = json.ticket
919+
this.ticketTimestamp = Date.now()
920+
}
921+
878922
return json
879923
}
880924

@@ -918,12 +962,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
918962
return res.status === 200
919963
}
920964

921-
private async sendRequestNative(verb: string, url: string, type: string, body: any, returnRawResponse: boolean) {
965+
private async sendRequestNative(verb: string, url: string, type: string, body: any, returnRawResponse: boolean, headers = {}) {
922966
let res
923967
let timedOut = false
924-
const authString = Base64.encode(
925-
this.server.username + ':' + this.server.password
926-
)
968+
const authString = !this.ticket || this.ticketTimestamp + 60 * 60 * 1000 < Date.now()
969+
? 'Basic ' + Base64.encode(this.server.username + ':' + this.server.password)
970+
: 'Bearer ' + this.ticket
927971
try {
928972
res = await this.fetchQueue.add(() => {
929973
Logger.log(`FETCHING ${verb} ${url}`)
@@ -934,7 +978,8 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
934978
disableRedirects: !this.server.allowRedirects,
935979
headers: {
936980
...(type && type !== 'multipart/form-data' && { 'Content-type': type }),
937-
Authorization: 'Basic ' + authString,
981+
Authorization: authString,
982+
...headers,
938983
},
939984
responseType: 'json',
940985
...(body && !['get', 'head'].includes(verb.toLowerCase()) && { data: body }),
@@ -959,6 +1004,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
9591004
throw new RedirectError()
9601005
}
9611006

1007+
if ((res.status === 401 || res.status === 403 || res.status === 404) && authString.startsWith('Bearer')) {
1008+
this.ticket = null
1009+
this.ticketTimestamp = 0
1010+
return this.sendRequestNative(verb, url, type, body, returnRawResponse, headers)
1011+
}
1012+
9621013
if (returnRawResponse) {
9631014
return res
9641015
}
@@ -974,6 +1025,11 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
9741025
throw new Error('Nextcloud API error for request ' + verb + ' ' + url + ' : \n' + JSON.stringify(json))
9751026
}
9761027

1028+
if (json.ticket) {
1029+
this.ticket = json.ticket
1030+
this.ticketTimestamp = Date.now()
1031+
}
1032+
9771033
return json
9781034
}
9791035

@@ -982,9 +1038,17 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
9821038
}
9831039

9841040
async getCapabilities(): Promise<ICapabilities> {
1041+
let hashFn : THashFunction[] = ['sha256']
1042+
if (this.capabilities && this.capabilities.bookmarks && typeof this.capabilities.bookmarks['hash-functions'] !== 'undefined') {
1043+
hashFn = this.capabilities.bookmarks['hash-functions'].map(hashFn => ({
1044+
'sha256': 'sha256',
1045+
'xxh32': 'xxhash3',
1046+
'murmur3a': 'murmur3',
1047+
}[hashFn]))
1048+
}
9851049
return {
9861050
preserveOrder: true,
987-
hashFn: ['sha256'],
1051+
hashFn,
9881052
}
9891053
}
9901054

0 commit comments

Comments
 (0)