-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcore.js
906 lines (837 loc) · 27.7 KB
/
core.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
/* eslint no-unused-vars: [ "error", { "args": "none" } ] */
// Session has abstract methods with parameters only used in subclasses
/**
* A request parameter value that can potentially be put in a list:
* a title, user name, namespace number, etc.
*
* @typedef ListableParam
* @type {string|number}
*/
/**
* A request parameter value that cannot be put in a list:
* a boolean toggle, a Blob or File (POST requests only),
* or null or undefined as a default fallback for not sending a parameter at all.
*
* @typedef UnlistableParam
* @type {boolean|Blob|File|null|undefined}
*/
/**
* A single request parameter value.
*
* @typedef SingleParam
* @type {ListableParam|UnlistableParam}
*/
/**
* A request parameter value that is a list of values
* (several titles, namespace numbers, etc.),
* which may potentially be combined with other lists in a single request
* (if specified as a set) or not (if specified as an array).
*
* @typedef ListParam
* @type {Array<ListableParam>|Set<ListableParam>}
*/
/**
* A request parameter value of any kind.
*
* @typedef Param
* @type {ListParam|SingleParam}
*/
/**
* Request parameters for {@link Session#request} and related methods.
* Each parameter may be a string, number, boolean, null, or undefined,
* or an array or set of strings or numbers.
* Parameters with values false, null, or undefined are completely removed
* when the request is sent out.
* In POST requests, a parameter may also be a Blob or File.
*
* @typedef Params
* @type {Object<string, Param>}
*/
/**
* Request options for {@link Session#request} and related methods.
* The actual effective options are merged from
* the builtin {@link DEFAULT_OPTIONS},
* the default options passed into the {@link Session} constructor,
* and the options given with a particular request call.
*
* @typedef Options
* @type {Object}
* @property {string} [method] The method, either GET (default) or POST.
* @property {string|null} [tokenType] Include a token parameter of this type,
* automatically getting it from the API if necessary.
* The most common token type is 'csrf' (some actions use a different type);
* you will also want to set the method option to POST.
* @property {string} [tokenName] The name of the token parameter.
* Only used if the tokenType option is not null.
* Defaults to 'token', but some modules need a different name
* (e.g. action=login needs 'lgtoken').
* @property {string} [userAgent] The User-Agent header to send.
* (Usually specified as a default option in the constructor.)
* @property {number} [maxRetriesSeconds] The maximum duration for automatic retries,
* i.e. a time interval (in seconds) during which the request will be automatically repeated
* according to the Retry-After response header if it is present.
* Defaults to 65 seconds; set to 0 to disable automatic retries.
* (Can also be a fractional number for sub-second precision.)
* @property {number} [retryAfterMaxlagSeconds] Default Retry-After header value
* in case of a maxlag error. Only used when the response is missing the header.
* Since MediaWiki usually sends this header for maxlag errors, this option is rarely used.
* Defaults to five seconds, which is the recommended maxlag value for bots.
* @property {number} [retryAfterReadonlySeconds] Default Retry-After header value
* in case of a readonly error. Only used when the response is missing the header.
* MediaWiki does not usually send this header for readonly errors,
* so this option is more important than the retryAfterMaxlagSeconds option.
* The default of 30 seconds is thought to be appropriate for Wikimedia wikis;
* for third-party wikis, higher values may be useful
* (remember to also increase the maxRetriesSeconds option accordingly).
* @property {Function} [warn] A handler for warnings from this API request.
* Called with a single instance of a subclass of Error, such as {@link ApiWarnings}.
* The default is console.warn (interactive CLI applications may wish to change this).
* @property {boolean} [dropTruncatedResultWarning]
* Whether to drop warnings about truncated results instead of passing them to the warn handler.
* Occasionally, an API result may not fit into a single network response;
* in such cases, the API will add a warning about the result being truncated,
* as well as continuation parameters that will result in the remaining information
* being included in the next request, if continuation is followed.
* If you follow continuation and are prepared to merge truncated responses back together,
* you don’t need to see this warning and can use this option to suppress it.
* This option defaults to false in {@link Session#request} (i.e. treat the warning like any other),
* but to true in {@link Session#requestAndContinueReducingBatch}.
* @property {string} [authorization] Value for the Authorization request header.
* This option can be used to authenticate requests using OAuth 2.0.
* For an owner-only client / consumer, where you have an access token,
* you can set this option to `Bearer ${ accessToken }` directly.
* Otherwise, use the m3api-oauth2 extension package.
* @property {Object.<string, ErrorHandler>} [errorHandlers] Internal option.
* Define handlers for API errors, which can retry the request if appropriate.
* This option is only part of the internal interface, not of the stable, public interface.
* @property {number} [retryUntil] Internal option.
* Retry until the given timestamp (in terms of the performance.now() clock).
* Takes precedence over the maxRetriesSeconds option.
* This option is only part of the internal interface, not of the stable, public interface.
*/
/**
* An error handler callback, which can be registered in the errorHandlers option.
*
* The callback is called if an API request results in an error
* and the callback has been registered for that error code.
* It may retry the request or perform any other action.
*
* @callback ErrorHandler
* @param {Session} session The session to which the request belongs.
* @param {Params} params The request parameters.
* @param {Options} options The request options.
* The retryUntil option is always set here,
* and the error handler should not retry the request if this timestamp has already passed.
* @param {InternalResponse} internalResponse The full response sent by the server.
* @param {Object} error The specific error returned to the API that matched this handler.
* @return {Object|null|Promise<Object|null>} A synchronous or asynchronous result.
* If the handler returns an object (or a promise resolving to an object),
* that object is used as the result of the API request;
* this can be used to retry the request
* (the handler makes another request to the session with the same params and options,
* and returns its result).
* If the handler returns null (or a promise resolving to null),
* the error could not be handled;
* m3api will call error handlers for the remaining errors (if any)
* and eventually throw ApiErrors if none of them returned an object either.
*/
/**
* The internal representation of a full server response,
* returned by {@link Session#internalGet} and {@link Session#internalPost}.
*
* @protected
* @typedef InternalResponse
* @type {Object}
* @property {number} status The HTTP status code (e.g. 200 OK).
* @property {Object} headers The response headers.
* Header names must be all-lowercase.
* (Set-Cookie is not expected to be included.)
* @property {Object} body JSON-decoded response body.
*/
/**
* Default options for requests across all sessions.
*
* Packages extending m3api’s capabilities (“extension packages”)
* may add their own options here,
* conventionally prefixed with the package name and a slash.
* For example, a package named 'abc' may add options 'abc/x' and 'abc/y',
* while a package named '@abc/def' may add '@abc/def/x' and '@abc/def/y'.
* Extension packages are encouraged to use a single options object
* for their own options as well as ones that are passed through to m3api,
* rather than e.g. separate options or individual parameters;
* both kinds of options can then have per-session and global defaults.
*
* Changing or removing any default options here is strongly discouraged,
* and may result in unpredictable behavior.
*
* @type {Options}
*/
const DEFAULT_OPTIONS = {
method: 'GET',
tokenType: null,
tokenName: 'token',
maxRetriesSeconds: 65,
retryAfterMaxlagSeconds: 5,
retryAfterReadonlySeconds: 30,
warn: console.warn,
dropTruncatedResultWarning: false,
authorization: null,
errorHandlers: {
maxlag: ( session, params, options, internalResponse, error ) => {
if ( 'retry-after' in internalResponse.headers ) {
return null; // header takes precedence over option
}
const { retryAfterMaxlagSeconds } = {
...DEFAULT_OPTIONS,
...session.defaultOptions,
...options,
};
return retryIfBefore( session, params, options, retryAfterMaxlagSeconds );
},
readonly: ( session, params, options, internalResponse, error ) => {
if ( 'retry-after' in internalResponse.headers ) {
return null; // header takes precedence over option
}
const { retryAfterReadonlySeconds } = {
...DEFAULT_OPTIONS,
...session.defaultOptions,
...options,
};
return retryIfBefore( session, params, options, retryAfterReadonlySeconds );
},
badtoken: ( session, params, options, internalResponse, error ) => {
const { tokenType } = {
...DEFAULT_OPTIONS,
...session.defaultOptions,
...options,
};
if ( tokenType === null ) {
return null; // bad token was supplied manually, nothing for us to do
}
session.tokens.clear();
return retryIfBefore( session, params, options, 0 /* no delay */ );
},
},
};
const DEFAULT_USER_AGENT = 'm3api/0.8.4 (https://www.npmjs.com/package/m3api)';
const TRUNCATED_RESULT = /^This result was truncated because it would otherwise ?be larger than the limit of .* bytes\.?$/;
/**
* @private
* @param {Object} params
* @return {Array.<Object>} [urlParams, bodyParams]
*/
function splitPostParameters( params ) {
const urlParams = {};
const bodyParams = {};
for ( const [ key, value ] of Object.entries( params ) ) {
if ( key === 'action' || key === 'origin' || key === 'crossorigin' ) {
urlParams[ key ] = value;
} else {
bodyParams[ key ] = value;
}
}
return [ urlParams, bodyParams ];
}
/**
* @private
* @param {Session} session
* @param {Params} params
* @param {Options} options
* @param {number} retryAfterSeconds
* @return {Promise<Object>|null}
*/
function retryIfBefore( session, params, options, retryAfterSeconds ) {
const retryAfterMillis = 1000 * retryAfterSeconds;
if ( performance.now() + retryAfterMillis <= options.retryUntil ) {
return new Promise( ( resolve ) => {
setTimeout( resolve, retryAfterMillis );
} ).then( () => session.request( params, options ) );
} else {
return null;
}
}
/**
* Return whether the given warning is *not* a truncatedresult warning.
*
* @private
* @param {Object} warning
* @return {boolean}
*/
function notTruncatedResultWarning( warning ) {
return warning.code ?
warning.code !== 'truncatedresult' :
!TRUNCATED_RESULT.test( warning.warnings || warning[ '*' ] );
}
/**
* Return the errors of a response (if any).
*
* @private
* @param {Object} response
* @return {Array.<Object>}
*/
function responseErrors( response ) {
if ( 'error' in response ) {
return [ response.error ];
}
if ( 'errors' in response ) {
return response.errors;
}
return [];
}
/**
* Return the warnings of a response (if any).
*
* @private
* @param {Object} response
* @return {Array.<Object>}
*/
function responseWarnings( response ) {
let warnings = response.warnings;
if ( !warnings ) {
return [];
}
if ( !Array.isArray( warnings ) ) {
const bcWarnings = Object.entries( warnings );
if ( bcWarnings[ 0 ][ 0 ] === 'main' ) {
// move to end of list
bcWarnings.push( bcWarnings.shift() );
}
warnings = [];
for ( const [ module, warning ] of bcWarnings ) {
warning.module = module;
warnings.push( warning );
}
}
return warnings;
}
/**
* An Error wrapping one or more API errors.
*/
class ApiErrors extends Error {
/**
* @param {Object[]} errors The error objects from the API.
* Must be nonempty, and each error must contain at least a code.
* Other error members depend on the errorformat of the request.
* @param {...*} params Any other params for the Error constructor.
* (Not including the message: the first error code is used for that.)
*/
constructor( errors, ...params ) {
super( errors[ 0 ].code, ...params );
if ( Error.captureStackTrace ) {
Error.captureStackTrace( this, ApiErrors );
}
this.name = 'ApiErrors';
/**
* The error objects from the API.
*
* @member {Object[]}
*/
this.errors = errors;
}
}
/**
* An Error wrapping one or more API warnings.
*/
class ApiWarnings extends Error {
/**
* @param {Object[]} warnings The warning objects from the API.
* Must be nonempty; the warning members depend on the errorformat of the request.
* @param {...*} params Any other params for the Error constructor.
* (Not including the message: the first warning is used for that.)
*/
constructor( warnings, ...params ) {
super(
warnings[ 0 ].code || warnings[ 0 ].warnings || warnings[ 0 ][ '*' ],
...params,
);
if ( Error.captureStackTrace ) {
Error.captureStackTrace( this, ApiWarnings );
}
this.name = 'ApiWarnings';
/**
* The warning objects from the API.
*
* @member {Object[]}
*/
this.warnings = warnings;
}
}
/**
* Decorate the given warn handler so that warnings about truncated results are dropped.
*
* Most of the time, you should use the dropTruncatedResultWarning request option
* instead of using this function directly.
*
* @param {Function} warn The original warn function.
* @return {Function} A new function that, when called,
* will call the original warn functions,
* but with all truncated result warnings dropped;
* when there are no other warnings, the original function is not called.
*/
function makeWarnDroppingTruncatedResultWarning( warn ) {
return function ( error ) {
if ( error instanceof ApiWarnings ) {
const warnings = error.warnings.filter( notTruncatedResultWarning );
if ( warnings.length > 0 ) {
return warn( warnings.length === error.warnings.length ?
error :
new ApiWarnings( warnings ) );
}
} else {
return warn( error );
}
};
}
/**
* An Error used as a warning when a request with no custom user agent is made.
*/
class DefaultUserAgentWarning extends Error {
/**
* @param {...*} params Any additional params for the Error constructor,
* not including the message (which is hard-coded).
*/
constructor( ...params ) {
super(
'm3api: Sending request with default User-Agent. ' +
'You should set the userAgent request option, ' +
'either as a default option for the session (third constructor argument) ' +
'or as a custom option for each request (second request argument). ' +
'See w.wiki/9mMA for the User-Agent policy.',
...params,
);
if ( Error.captureStackTrace ) {
Error.captureStackTrace( this, DefaultUserAgentWarning );
}
this.name = 'DefaultUserAgentWarning';
}
}
/**
* A session to make API requests.
*/
class Session {
/**
* @param {string} apiUrl The URL to the api.php endpoint,
* such as {@link https://en.wikipedia.org/w/api.php}.
* Can also be just the domain, such as en.wikipedia.org.
* @param {Params} [defaultParams] Parameters to include in every API request.
* You are strongly encouraged to specify formatversion: 2 here;
* other useful global parameters include uselang, errorformat, maxlag.
* @param {Options} [defaultOptions] Options to set for each request.
* You are strongly encouraged to specify a userAgent according to the
* {@link https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:User-Agent_policy User-Agent policy}.
*/
constructor( apiUrl, defaultParams = {}, defaultOptions = {} ) {
if ( !apiUrl.includes( '/' ) ) {
apiUrl = `https://${ apiUrl }/w/api.php`;
}
/**
* The URL to the api.php endpoint.
* Must not be reassigned.
*
* @member {string}
*/
this.apiUrl = apiUrl;
/**
* Parameters to include in every API request.
* Can be modified after construction,
* e.g. to add assert=user after logging in.
*
* @member {Object}
*/
this.defaultParams = defaultParams;
/**
* Options to set for each request.
* Can be modified after construction.
*
* @member {Options}
*/
this.defaultOptions = defaultOptions;
/**
* Saved/cached tokens.
* Can be modified after construction,
* particularly to call `clear()` after logging in or out;
* apart from that, however,
* using the tokenType/tokenName options or {@link Session#getToken}
* is generally more convenient.
*
* @member {Map}
*/
this.tokens = new Map();
}
/**
* Make an API request.
*
* @param {Params} params The parameters.
* Default parameters from the constructor are added to these,
* with per-request parameters overriding default parameters in case of collision.
* @param {Options} [options] Other options for the request.
* The per-request options extend and override the options passed into the constructor,
* which in turn extend and override the builtin {@link DEFAULT_OPTIONS}.
* @return {Object}
* @throws {ApiErrors}
*/
async request( params, options = {} ) {
const {
method,
tokenType,
tokenName,
maxRetriesSeconds,
warn,
dropTruncatedResultWarning,
retryUntil = performance.now() + maxRetriesSeconds * 1000,
} = {
...DEFAULT_OPTIONS,
...this.defaultOptions,
...options,
};
const retryOptions = { ...options, retryUntil };
let tokenParams = null;
if ( tokenType !== null ) {
tokenParams = { [ tokenName ]: await this.getToken( tokenType, retryOptions ) };
}
const allParams = this.transformParams( {
...this.defaultParams,
...tokenParams,
...params,
format: 'json',
} );
const requestHeaders = this.getRequestHeaders( options );
let result;
if ( method === 'GET' ) {
result = this.internalGet( this.apiUrl, allParams, requestHeaders );
} else if ( method === 'POST' ) {
const [ urlParams, bodyParams ] = splitPostParameters( allParams );
result = this.internalPost( this.apiUrl, urlParams, bodyParams, requestHeaders );
} else {
throw new Error( `Unknown request method: ${ method }` );
}
const internalResponse = await result;
const {
status,
headers: responseHeaders,
body,
} = internalResponse;
if ( status !== 200 && !( 'mediawiki-api-error' in responseHeaders ) ) {
throw new Error( `API request returned non-200 HTTP status code: ${ status }` );
}
if ( 'retry-after' in responseHeaders ) {
const retryAfterSeconds = parseInt( responseHeaders[ 'retry-after' ] );
const retryResult = await retryIfBefore(
this, params, retryOptions, retryAfterSeconds );
if ( retryResult !== null ) {
return retryResult;
}
}
const errors = responseErrors( body );
for ( const error of errors ) {
const { [ error.code ]: handler } = {
...DEFAULT_OPTIONS.errorHandlers,
...this.defaultOptions.errorHandlers,
...options.errorHandlers,
};
if ( handler ) {
const handlerResult = await handler(
this, params, retryOptions, internalResponse, error );
if ( handlerResult !== null ) {
return handlerResult;
}
}
}
if ( errors.length > 0 ) {
throw new ApiErrors( errors );
}
const warnings = responseWarnings( body );
if ( warnings.length > 0 ) {
const actualWarn = dropTruncatedResultWarning ?
makeWarnDroppingTruncatedResultWarning( warn ) :
warn;
actualWarn( new ApiWarnings( warnings ) );
}
return body;
}
/**
* Make a series of API requests, following API continuation.
*
* @param {Params} params Same as for {@link Session#request}.
* Continuation parameters will be added automatically.
* @param {Options} [options] Same as for {@link Session#request}.
* @yield {Object}
* @throws {ApiErrors}
*/
async * requestAndContinue( params, options = {} ) {
let continueParams = { continue: undefined };
do {
const response = await this.request( {
...params,
...continueParams,
}, options );
continueParams = response.continue && { ...response.continue };
yield response;
} while ( continueParams !== undefined );
}
/**
* Make a series of API requests, following API continuation,
* accumulating responses and yielding one result per batch.
*
* This works conceptually similar to Array.reduce(), but repeatedly,
* with each batch of responses corresponding to one array.
* At the beginning of each batch, an initial value is generated,
* and then for each response in the batch,
* a reducer is called with the current value and that response.
* (The current value starts out as the initial value;
* afterwards, it’s the reducer’s return value for the previous response.)
* At the end of each batch, the current value is yielded,
* and the process starts over with a new initial value.
*
* The reducer will typically extract some kind of pages or other entries from the response,
* add them to the current value, possibly merging them with existing entries there,
* and then return the updated value.
* The initial callback defaults to producing empty objects,
* but other values are also possible: sets or maps may be useful.
*
* @param {Params} params Same as for {@link Session#request}.
* @param {Options} options Same as for {@link Session#request}. (But not optional here!)
* The dropTruncatedResultWarning option defaults to true here,
* since continuation will produce the rest of the truncated result automatically.
* @param {Function} reducer A callback like for Array.reduce().
* Called with two arguments, the current value and the current response.
* @param {Function} [initial] A callback producing initial values.
* Called with no arguments. Defaults to producing empty objects.
* @yield {*} The last reducer return value for each batch.
* Typically, the initial and reducer callbacks will have the same return type,
* which will then also be the return type of this function, such as Object, Map, or Set.
*/
async * requestAndContinueReducingBatch( params, options, reducer, initial = () => ( {} ) ) {
options = {
dropTruncatedResultWarning: true,
...options,
};
let accumulator = initial();
for await ( const response of this.requestAndContinue( params, options ) ) {
const complete = responseBoolean( response.batchcomplete );
accumulator = reducer( accumulator, response );
if ( complete ) {
yield accumulator;
accumulator = initial();
}
}
}
/**
* Get a token of the specified type.
*
* Though this method is public, it should generally not be used directly:
* call {@link Session#request} with the tokenType/tokenName options instead.
*
* @param {string} type
* @param {Options} options Options for the request to get the token.
* @return {string}
*/
async getToken( type, options ) {
if ( !this.tokens.has( type ) ) {
const params = {
action: 'query',
meta: set( 'tokens' ),
type: set( type ),
};
options = {
...options,
method: 'GET',
tokenType: null,
dropTruncatedResultWarning: true,
};
for await ( const response of this.requestAndContinue( params, options ) ) {
try {
const token = response.query.tokens[ type + 'token' ];
if ( typeof token === 'string' ) {
this.tokens.set( type, token );
break;
}
// if token not found in this response, follow continuation
} catch ( _ ) {
}
}
}
return this.tokens.get( type );
}
/**
* Get the effective request headers for these options.
*
* @protected
* @param {Options} options
* @return {Object}
*/
getRequestHeaders( options ) {
const requestHeaders = {
'user-agent': this.getUserAgent( options ),
};
const { authorization } = {
...DEFAULT_OPTIONS,
...this.defaultOptions,
...options,
};
if ( authorization ) {
requestHeaders.authorization = authorization;
}
return requestHeaders;
}
/**
* Get the effective user agent string for these options.
*
* @protected
* @param {Options} options
* @return {string}
*/
getUserAgent( options ) {
const {
userAgent,
warn,
} = {
...DEFAULT_OPTIONS,
...this.defaultOptions,
...options,
};
if ( userAgent ) {
return `${ userAgent } ${ DEFAULT_USER_AGENT }`;
} else {
if ( !this.warnedDefaultUserAgent ) {
warn( new DefaultUserAgentWarning() );
/** @private */
this.warnedDefaultUserAgent = true;
}
return DEFAULT_USER_AGENT;
}
}
/**
* @private
* @param {Params} params
* @return {Object}
*/
transformParams( params ) {
const transformedParams = {};
for ( const [ key, value ] of Object.entries( params ) ) {
const transformedParamValue = this.transformParamValue( value );
if ( transformedParamValue !== undefined ) {
transformedParams[ key ] = transformedParamValue;
}
}
return transformedParams;
}
/**
* @private
* @param {Param} value
* @return {string|undefined}
*/
transformParamValue( value ) {
if ( value instanceof Set ) {
value = [ ...value ];
}
if ( Array.isArray( value ) ) {
return this.transformParamArray( value );
} else {
return this.transformParamSingle( value );
}
}
/**
* @private
* @param {Array<ListableParam>} value
* @return {string}
*/
transformParamArray( value ) {
if ( value.some( ( element ) => String.prototype.includes.call( element, '|' ) ) ) {
return '\x1f' + value.join( '\x1f' );
} else {
return value.join( '|' );
}
}
/**
* @private
* @param {*} value
* @return {*} string|undefined for string|number|boolean|null|undefined value,
* the value unmodified otherwise
*/
transformParamSingle( value ) {
if ( typeof value === 'number' ) {
return String( value );
}
if ( value === true ) {
return '';
}
if ( value === false || value === null || value === undefined ) {
return undefined;
}
return value;
}
/**
* Actually make a GET request.
*
* @abstract
* @protected
* @param {string} apiUrl
* @param {Object} params
* @param {Object} headers Header names must be all-lowercase.
* @return {Promise<InternalResponse>}
*/
internalGet( apiUrl, params, headers ) {
throw new Error( 'Abstract method internalGet not implemented!' );
}
/**
* Actually make a POST request.
*
* @abstract
* @protected
* @param {string} apiUrl
* @param {Object} urlParams
* @param {Object} bodyParams
* @param {Object} headers Header names must be all-lowercase.
* @return {Promise<InternalResponse>}
*/
internalPost( apiUrl, urlParams, bodyParams, headers ) {
throw new Error( 'Abstract method internalPost not implemented!' );
}
}
/**
* Convenience function to get a boolean from an API response value.
*
* Works for formatversion=1 booleans
* (absent means false, empty string means true)
* as well as formatversion=2 booleans
* (absent or false means false, true means true).
* Mostly useful in library code,
* when you don’t know the formatversion of the response.
* (If you control the request parameters, just use formatversion=2.)
*
* @param {boolean|''|undefined} value A value from an API response
* (e.g. response.query.general.rtl).
* @return {boolean}
*/
function responseBoolean( value ) {
return ( value && '' ) === '';
}
/**
* Convenience function to create a Set.
*
* The two invocations
*
* new Set( [ 'a', 'b' ] )
* set( 'a', 'b' )
*
* are equivalent, but the second one is shorter and easier to type.
*
* @param {...*} elements
* @return {Set}
*/
function set( ...elements ) {
return new Set( elements );
}
// note: exports that are useful to end-users / applications
// should be re-exported from browser.js and node.js
export {
DEFAULT_OPTIONS,
ApiErrors,
ApiWarnings,
DefaultUserAgentWarning,
Session,
makeWarnDroppingTruncatedResultWarning,
responseBoolean,
set,
};