@@ -13,7 +13,7 @@ import {
1313 BulkImportResource ,
1414 ClickCountResource , ICapabilities , IHashSettings ,
1515 LoadFolderChildrenResource ,
16- OrderFolderResource
16+ OrderFolderResource , THashFunction
1717} from '../interfaces/Resource'
1818import Ordering from '../interfaces/Ordering'
1919import {
@@ -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