PluginProbe
Gutenberg / 14.7.0
Gutenberg v14.7.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
gutenberg / build / api-fetch / index.js

index.js in Gutenberg 14.7.0, at build/api-fetch/index.js

784 lines 21.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /******/ (function() { // webpackBootstrap
2 /******/ "use strict";
3 /******/ // The require scope
4 /******/ var __webpack_require__ = {};
5 /******/
6 /************************************************************************/
7 /******/ /* webpack/runtime/define property getters */
8 /******/ !function() {
9 /******/ // define getter functions for harmony exports
10 /******/ __webpack_require__.d = function(exports, definition) {
11 /******/ for(var key in definition) {
12 /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
13 /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
14 /******/ }
15 /******/ }
16 /******/ };
17 /******/ }();
18 /******/
19 /******/ /* webpack/runtime/hasOwnProperty shorthand */
20 /******/ !function() {
21 /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
22 /******/ }();
23 /******/
24 /************************************************************************/
25 var __webpack_exports__ = {};
26
27 // EXPORTS
28 __webpack_require__.d(__webpack_exports__, {
29 "default": function() { return /* binding */ build_module; }
30 });
31
32 ;// CONCATENATED MODULE: external ["wp","i18n"]
33 var external_wp_i18n_namespaceObject = window["wp"]["i18n"];
34 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/nonce.js
35 /**
36 * @param {string} nonce
37 * @return {import('../types').APIFetchMiddleware & { nonce: string }} A middleware to enhance a request with a nonce.
38 */
39 function createNonceMiddleware(nonce) {
40 /**
41 * @type {import('../types').APIFetchMiddleware & { nonce: string }}
42 */
43 const middleware = (options, next) => {
44 const {
45 headers = {}
46 } = options; // If an 'X-WP-Nonce' header (or any case-insensitive variation
47 // thereof) was specified, no need to add a nonce header.
48
49 for (const headerName in headers) {
50 if (headerName.toLowerCase() === 'x-wp-nonce' && headers[headerName] === middleware.nonce) {
51 return next(options);
52 }
53 }
54
55 return next({ ...options,
56 headers: { ...headers,
57 'X-WP-Nonce': middleware.nonce
58 }
59 });
60 };
61
62 middleware.nonce = nonce;
63 return middleware;
64 }
65
66 /* harmony default export */ var nonce = (createNonceMiddleware);
67
68 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/namespace-endpoint.js
69 /**
70 * @type {import('../types').APIFetchMiddleware}
71 */
72 const namespaceAndEndpointMiddleware = (options, next) => {
73 let path = options.path;
74 let namespaceTrimmed, endpointTrimmed;
75
76 if (typeof options.namespace === 'string' && typeof options.endpoint === 'string') {
77 namespaceTrimmed = options.namespace.replace(/^\/|\/$/g, '');
78 endpointTrimmed = options.endpoint.replace(/^\//, '');
79
80 if (endpointTrimmed) {
81 path = namespaceTrimmed + '/' + endpointTrimmed;
82 } else {
83 path = namespaceTrimmed;
84 }
85 }
86
87 delete options.namespace;
88 delete options.endpoint;
89 return next({ ...options,
90 path
91 });
92 };
93
94 /* harmony default export */ var namespace_endpoint = (namespaceAndEndpointMiddleware);
95
96 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/root-url.js
97 /**
98 * Internal dependencies
99 */
100
101 /**
102 * @param {string} rootURL
103 * @return {import('../types').APIFetchMiddleware} Root URL middleware.
104 */
105
106 const createRootURLMiddleware = rootURL => (options, next) => {
107 return namespace_endpoint(options, optionsWithPath => {
108 let url = optionsWithPath.url;
109 let path = optionsWithPath.path;
110 let apiRoot;
111
112 if (typeof path === 'string') {
113 apiRoot = rootURL;
114
115 if (-1 !== rootURL.indexOf('?')) {
116 path = path.replace('?', '&');
117 }
118
119 path = path.replace(/^\//, ''); // API root may already include query parameter prefix if site is
120 // configured to use plain permalinks.
121
122 if ('string' === typeof apiRoot && -1 !== apiRoot.indexOf('?')) {
123 path = path.replace('?', '&');
124 }
125
126 url = apiRoot + path;
127 }
128
129 return next({ ...optionsWithPath,
130 url
131 });
132 });
133 };
134
135 /* harmony default export */ var root_url = (createRootURLMiddleware);
136
137 ;// CONCATENATED MODULE: external ["wp","url"]
138 var external_wp_url_namespaceObject = window["wp"]["url"];
139 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/preloading.js
140 /**
141 * WordPress dependencies
142 */
143
144 /**
145 * @param {Record<string, any>} preloadedData
146 * @return {import('../types').APIFetchMiddleware} Preloading middleware.
147 */
148
149 function createPreloadingMiddleware(preloadedData) {
150 const cache = Object.fromEntries(Object.entries(preloadedData).map(_ref => {
151 let [path, data] = _ref;
152 return [(0,external_wp_url_namespaceObject.normalizePath)(path), data];
153 }));
154 return (options, next) => {
155 const {
156 parse = true
157 } = options;
158 /** @type {string | void} */
159
160 let rawPath = options.path;
161
162 if (!rawPath && options.url) {
163 const {
164 rest_route: pathFromQuery,
165 ...queryArgs
166 } = (0,external_wp_url_namespaceObject.getQueryArgs)(options.url);
167
168 if (typeof pathFromQuery === 'string') {
169 rawPath = (0,external_wp_url_namespaceObject.addQueryArgs)(pathFromQuery, queryArgs);
170 }
171 }
172
173 if (typeof rawPath !== 'string') {
174 return next(options);
175 }
176
177 const method = options.method || 'GET';
178 const path = (0,external_wp_url_namespaceObject.normalizePath)(rawPath);
179
180 if ('GET' === method && cache[path]) {
181 const cacheData = cache[path]; // Unsetting the cache key ensures that the data is only used a single time.
182
183 delete cache[path];
184 return prepareResponse(cacheData, !!parse);
185 } else if ('OPTIONS' === method && cache[method] && cache[method][path]) {
186 const cacheData = cache[method][path]; // Unsetting the cache key ensures that the data is only used a single time.
187
188 delete cache[method][path];
189 return prepareResponse(cacheData, !!parse);
190 }
191
192 return next(options);
193 };
194 }
195 /**
196 * This is a helper function that sends a success response.
197 *
198 * @param {Record<string, any>} responseData
199 * @param {boolean} parse
200 * @return {Promise<any>} Promise with the response.
201 */
202
203
204 function prepareResponse(responseData, parse) {
205 return Promise.resolve(parse ? responseData.body : new window.Response(JSON.stringify(responseData.body), {
206 status: 200,
207 statusText: 'OK',
208 headers: responseData.headers
209 }));
210 }
211
212 /* harmony default export */ var preloading = (createPreloadingMiddleware);
213
214 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/fetch-all-middleware.js
215 /**
216 * WordPress dependencies
217 */
218
219 /**
220 * Internal dependencies
221 */
222
223
224 /**
225 * Apply query arguments to both URL and Path, whichever is present.
226 *
227 * @param {import('../types').APIFetchOptions} props
228 * @param {Record<string, string | number>} queryArgs
229 * @return {import('../types').APIFetchOptions} The request with the modified query args
230 */
231
232 const modifyQuery = (_ref, queryArgs) => {
233 let {
234 path,
235 url,
236 ...options
237 } = _ref;
238 return { ...options,
239 url: url && (0,external_wp_url_namespaceObject.addQueryArgs)(url, queryArgs),
240 path: path && (0,external_wp_url_namespaceObject.addQueryArgs)(path, queryArgs)
241 };
242 };
243 /**
244 * Duplicates parsing functionality from apiFetch.
245 *
246 * @param {Response} response
247 * @return {Promise<any>} Parsed response json.
248 */
249
250
251 const parseResponse = response => response.json ? response.json() : Promise.reject(response);
252 /**
253 * @param {string | null} linkHeader
254 * @return {{ next?: string }} The parsed link header.
255 */
256
257
258 const parseLinkHeader = linkHeader => {
259 if (!linkHeader) {
260 return {};
261 }
262
263 const match = linkHeader.match(/<([^>]+)>; rel="next"/);
264 return match ? {
265 next: match[1]
266 } : {};
267 };
268 /**
269 * @param {Response} response
270 * @return {string | undefined} The next page URL.
271 */
272
273
274 const getNextPageUrl = response => {
275 const {
276 next
277 } = parseLinkHeader(response.headers.get('link'));
278 return next;
279 };
280 /**
281 * @param {import('../types').APIFetchOptions} options
282 * @return {boolean} True if the request contains an unbounded query.
283 */
284
285
286 const requestContainsUnboundedQuery = options => {
287 const pathIsUnbounded = !!options.path && options.path.indexOf('per_page=-1') !== -1;
288 const urlIsUnbounded = !!options.url && options.url.indexOf('per_page=-1') !== -1;
289 return pathIsUnbounded || urlIsUnbounded;
290 };
291 /**
292 * The REST API enforces an upper limit on the per_page option. To handle large
293 * collections, apiFetch consumers can pass `per_page=-1`; this middleware will
294 * then recursively assemble a full response array from all available pages.
295 *
296 * @type {import('../types').APIFetchMiddleware}
297 */
298
299
300 const fetchAllMiddleware = async (options, next) => {
301 if (options.parse === false) {
302 // If a consumer has opted out of parsing, do not apply middleware.
303 return next(options);
304 }
305
306 if (!requestContainsUnboundedQuery(options)) {
307 // If neither url nor path is requesting all items, do not apply middleware.
308 return next(options);
309 } // Retrieve requested page of results.
310
311
312 const response = await build_module({ ...modifyQuery(options, {
313 per_page: 100
314 }),
315 // Ensure headers are returned for page 1.
316 parse: false
317 });
318 const results = await parseResponse(response);
319
320 if (!Array.isArray(results)) {
321 // We have no reliable way of merging non-array results.
322 return results;
323 }
324
325 let nextPage = getNextPageUrl(response);
326
327 if (!nextPage) {
328 // There are no further pages to request.
329 return results;
330 } // Iteratively fetch all remaining pages until no "next" header is found.
331
332
333 let mergedResults =
334 /** @type {any[]} */
335 [].concat(results);
336
337 while (nextPage) {
338 const nextResponse = await build_module({ ...options,
339 // Ensure the URL for the next page is used instead of any provided path.
340 path: undefined,
341 url: nextPage,
342 // Ensure we still get headers so we can identify the next page.
343 parse: false
344 });
345 const nextResults = await parseResponse(nextResponse);
346 mergedResults = mergedResults.concat(nextResults);
347 nextPage = getNextPageUrl(nextResponse);
348 }
349
350 return mergedResults;
351 };
352
353 /* harmony default export */ var fetch_all_middleware = (fetchAllMiddleware);
354
355 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/http-v1.js
356 /**
357 * Set of HTTP methods which are eligible to be overridden.
358 *
359 * @type {Set<string>}
360 */
361 const OVERRIDE_METHODS = new Set(['PATCH', 'PUT', 'DELETE']);
362 /**
363 * Default request method.
364 *
365 * "A request has an associated method (a method). Unless stated otherwise it
366 * is `GET`."
367 *
368 * @see https://fetch.spec.whatwg.org/#requests
369 *
370 * @type {string}
371 */
372
373 const DEFAULT_METHOD = 'GET';
374 /**
375 * API Fetch middleware which overrides the request method for HTTP v1
376 * compatibility leveraging the REST API X-HTTP-Method-Override header.
377 *
378 * @type {import('../types').APIFetchMiddleware}
379 */
380
381 const httpV1Middleware = (options, next) => {
382 const {
383 method = DEFAULT_METHOD
384 } = options;
385
386 if (OVERRIDE_METHODS.has(method.toUpperCase())) {
387 options = { ...options,
388 headers: { ...options.headers,
389 'X-HTTP-Method-Override': method,
390 'Content-Type': 'application/json'
391 },
392 method: 'POST'
393 };
394 }
395
396 return next(options);
397 };
398
399 /* harmony default export */ var http_v1 = (httpV1Middleware);
400
401 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/user-locale.js
402 /**
403 * WordPress dependencies
404 */
405
406 /**
407 * @type {import('../types').APIFetchMiddleware}
408 */
409
410 const userLocaleMiddleware = (options, next) => {
411 if (typeof options.url === 'string' && !(0,external_wp_url_namespaceObject.hasQueryArg)(options.url, '_locale')) {
412 options.url = (0,external_wp_url_namespaceObject.addQueryArgs)(options.url, {
413 _locale: 'user'
414 });
415 }
416
417 if (typeof options.path === 'string' && !(0,external_wp_url_namespaceObject.hasQueryArg)(options.path, '_locale')) {
418 options.path = (0,external_wp_url_namespaceObject.addQueryArgs)(options.path, {
419 _locale: 'user'
420 });
421 }
422
423 return next(options);
424 };
425
426 /* harmony default export */ var user_locale = (userLocaleMiddleware);
427
428 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/utils/response.js
429 /**
430 * WordPress dependencies
431 */
432
433 /**
434 * Parses the apiFetch response.
435 *
436 * @param {Response} response
437 * @param {boolean} shouldParseResponse
438 *
439 * @return {Promise<any> | null | Response} Parsed response.
440 */
441
442 const response_parseResponse = function (response) {
443 let shouldParseResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
444
445 if (shouldParseResponse) {
446 if (response.status === 204) {
447 return null;
448 }
449
450 return response.json ? response.json() : Promise.reject(response);
451 }
452
453 return response;
454 };
455 /**
456 * Calls the `json` function on the Response, throwing an error if the response
457 * doesn't have a json function or if parsing the json itself fails.
458 *
459 * @param {Response} response
460 * @return {Promise<any>} Parsed response.
461 */
462
463
464 const parseJsonAndNormalizeError = response => {
465 const invalidJsonError = {
466 code: 'invalid_json',
467 message: (0,external_wp_i18n_namespaceObject.__)('The response is not a valid JSON response.')
468 };
469
470 if (!response || !response.json) {
471 throw invalidJsonError;
472 }
473
474 return response.json().catch(() => {
475 throw invalidJsonError;
476 });
477 };
478 /**
479 * Parses the apiFetch response properly and normalize response errors.
480 *
481 * @param {Response} response
482 * @param {boolean} shouldParseResponse
483 *
484 * @return {Promise<any>} Parsed response.
485 */
486
487
488 const parseResponseAndNormalizeError = function (response) {
489 let shouldParseResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
490 return Promise.resolve(response_parseResponse(response, shouldParseResponse)).catch(res => parseAndThrowError(res, shouldParseResponse));
491 };
492 /**
493 * Parses a response, throwing an error if parsing the response fails.
494 *
495 * @param {Response} response
496 * @param {boolean} shouldParseResponse
497 * @return {Promise<any>} Parsed response.
498 */
499
500 function parseAndThrowError(response) {
501 let shouldParseResponse = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
502
503 if (!shouldParseResponse) {
504 throw response;
505 }
506
507 return parseJsonAndNormalizeError(response).then(error => {
508 const unknownError = {
509 code: 'unknown_error',
510 message: (0,external_wp_i18n_namespaceObject.__)('An unknown error occurred.')
511 };
512 throw error || unknownError;
513 });
514 }
515
516 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/middlewares/media-upload.js
517 /**
518 * WordPress dependencies
519 */
520
521 /**
522 * Internal dependencies
523 */
524
525
526 /**
527 * @param {import('../types').APIFetchOptions} options
528 * @return {boolean} True if the request is for media upload.
529 */
530
531 function isMediaUploadRequest(options) {
532 const isCreateMethod = !!options.method && options.method === 'POST';
533 const isMediaEndpoint = !!options.path && options.path.indexOf('/wp/v2/media') !== -1 || !!options.url && options.url.indexOf('/wp/v2/media') !== -1;
534 return isMediaEndpoint && isCreateMethod;
535 }
536 /**
537 * Middleware handling media upload failures and retries.
538 *
539 * @type {import('../types').APIFetchMiddleware}
540 */
541
542
543 const mediaUploadMiddleware = (options, next) => {
544 if (!isMediaUploadRequest(options)) {
545 return next(options);
546 }
547
548 let retries = 0;
549 const maxRetries = 5;
550 /**
551 * @param {string} attachmentId
552 * @return {Promise<any>} Processed post response.
553 */
554
555 const postProcess = attachmentId => {
556 retries++;
557 return next({
558 path: `/wp/v2/media/${attachmentId}/post-process`,
559 method: 'POST',
560 data: {
561 action: 'create-image-subsizes'
562 },
563 parse: false
564 }).catch(() => {
565 if (retries < maxRetries) {
566 return postProcess(attachmentId);
567 }
568
569 next({
570 path: `/wp/v2/media/${attachmentId}?force=true`,
571 method: 'DELETE'
572 });
573 return Promise.reject();
574 });
575 };
576
577 return next({ ...options,
578 parse: false
579 }).catch(response => {
580 const attachmentId = response.headers.get('x-wp-upload-attachment-id');
581
582 if (response.status >= 500 && response.status < 600 && attachmentId) {
583 return postProcess(attachmentId).catch(() => {
584 if (options.parse !== false) {
585 return Promise.reject({
586 code: 'post_process',
587 message: (0,external_wp_i18n_namespaceObject.__)('Media upload failed. If this is a photo or a large image, please scale it down and try again.')
588 });
589 }
590
591 return Promise.reject(response);
592 });
593 }
594
595 return parseAndThrowError(response, options.parse);
596 }).then(response => parseResponseAndNormalizeError(response, options.parse));
597 };
598
599 /* harmony default export */ var media_upload = (mediaUploadMiddleware);
600
601 ;// CONCATENATED MODULE: ./packages/api-fetch/build-module/index.js
602 /**
603 * WordPress dependencies
604 */
605
606 /**
607 * Internal dependencies
608 */
609
610
611
612
613
614
615
616
617
618
619 /**
620 * Default set of header values which should be sent with every request unless
621 * explicitly provided through apiFetch options.
622 *
623 * @type {Record<string, string>}
624 */
625
626 const DEFAULT_HEADERS = {
627 // The backend uses the Accept header as a condition for considering an
628 // incoming request as a REST request.
629 //
630 // See: https://core.trac.wordpress.org/ticket/44534
631 Accept: 'application/json, */*;q=0.1'
632 };
633 /**
634 * Default set of fetch option values which should be sent with every request
635 * unless explicitly provided through apiFetch options.
636 *
637 * @type {Object}
638 */
639
640 const DEFAULT_OPTIONS = {
641 credentials: 'include'
642 };
643 /** @typedef {import('./types').APIFetchMiddleware} APIFetchMiddleware */
644
645 /** @typedef {import('./types').APIFetchOptions} APIFetchOptions */
646
647 /**
648 * @type {import('./types').APIFetchMiddleware[]}
649 */
650
651 const middlewares = [user_locale, namespace_endpoint, http_v1, fetch_all_middleware];
652 /**
653 * Register a middleware
654 *
655 * @param {import('./types').APIFetchMiddleware} middleware
656 */
657
658 function registerMiddleware(middleware) {
659 middlewares.unshift(middleware);
660 }
661 /**
662 * Checks the status of a response, throwing the Response as an error if
663 * it is outside the 200 range.
664 *
665 * @param {Response} response
666 * @return {Response} The response if the status is in the 200 range.
667 */
668
669
670 const checkStatus = response => {
671 if (response.status >= 200 && response.status < 300) {
672 return response;
673 }
674
675 throw response;
676 };
677 /** @typedef {(options: import('./types').APIFetchOptions) => Promise<any>} FetchHandler*/
678
679 /**
680 * @type {FetchHandler}
681 */
682
683
684 const defaultFetchHandler = nextOptions => {
685 const {
686 url,
687 path,
688 data,
689 parse = true,
690 ...remainingOptions
691 } = nextOptions;
692 let {
693 body,
694 headers
695 } = nextOptions; // Merge explicitly-provided headers with default values.
696
697 headers = { ...DEFAULT_HEADERS,
698 ...headers
699 }; // The `data` property is a shorthand for sending a JSON body.
700
701 if (data) {
702 body = JSON.stringify(data);
703 headers['Content-Type'] = 'application/json';
704 }
705
706 const responsePromise = window.fetch( // Fall back to explicitly passing `window.location` which is the behavior if `undefined` is passed.
707 url || path || window.location.href, { ...DEFAULT_OPTIONS,
708 ...remainingOptions,
709 body,
710 headers
711 });
712 return responsePromise.then(value => Promise.resolve(value).then(checkStatus).catch(response => parseAndThrowError(response, parse)).then(response => parseResponseAndNormalizeError(response, parse)), err => {
713 // Re-throw AbortError for the users to handle it themselves.
714 if (err && err.name === 'AbortError') {
715 throw err;
716 } // Otherwise, there is most likely no network connection.
717 // Unfortunately the message might depend on the browser.
718
719
720 throw {
721 code: 'fetch_error',
722 message: (0,external_wp_i18n_namespaceObject.__)('You are probably offline.')
723 };
724 });
725 };
726 /** @type {FetchHandler} */
727
728
729 let fetchHandler = defaultFetchHandler;
730 /**
731 * Defines a custom fetch handler for making the requests that will override
732 * the default one using window.fetch
733 *
734 * @param {FetchHandler} newFetchHandler The new fetch handler
735 */
736
737 function setFetchHandler(newFetchHandler) {
738 fetchHandler = newFetchHandler;
739 }
740 /**
741 * @template T
742 * @param {import('./types').APIFetchOptions} options
743 * @return {Promise<T>} A promise representing the request processed via the registered middlewares.
744 */
745
746
747 function apiFetch(options) {
748 // creates a nested function chain that calls all middlewares and finally the `fetchHandler`,
749 // converting `middlewares = [ m1, m2, m3 ]` into:
750 // ```
751 // opts1 => m1( opts1, opts2 => m2( opts2, opts3 => m3( opts3, fetchHandler ) ) );
752 // ```
753 const enhancedHandler = middlewares.reduceRight((
754 /** @type {FetchHandler} */
755 next, middleware) => {
756 return workingOptions => middleware(workingOptions, next);
757 }, fetchHandler);
758 return enhancedHandler(options).catch(error => {
759 if (error.code !== 'rest_cookie_invalid_nonce') {
760 return Promise.reject(error);
761 } // If the nonce is invalid, refresh it and try again.
762
763
764 return window // @ts-ignore
765 .fetch(apiFetch.nonceEndpoint).then(checkStatus).then(data => data.text()).then(text => {
766 // @ts-ignore
767 apiFetch.nonceMiddleware.nonce = text;
768 return apiFetch(options);
769 });
770 });
771 }
772
773 apiFetch.use = registerMiddleware;
774 apiFetch.setFetchHandler = setFetchHandler;
775 apiFetch.createNonceMiddleware = nonce;
776 apiFetch.createPreloadingMiddleware = preloading;
777 apiFetch.createRootURLMiddleware = root_url;
778 apiFetch.fetchAllMiddleware = fetch_all_middleware;
779 apiFetch.mediaUploadMiddleware = media_upload;
780 /* harmony default export */ var build_module = (apiFetch);
781
782 (window.wp = window.wp || {}).apiFetch = __webpack_exports__["default"];
783 /******/ })()
784 ;