Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
595 lines
21 KiB
JavaScript
595 lines
21 KiB
JavaScript
import {derived, writable, get, readable} from "svelte/store";
|
|
import {objectsDiffer} from "./objectsDiffer";
|
|
|
|
// Initial config store.
|
|
export const config = writable( {} );
|
|
|
|
// Whether settings are locked due to background activity such as upgrade.
|
|
export const settingsLocked = writable( false );
|
|
|
|
// Convenience readable store of server's settings, derived from config.
|
|
export const current_settings = derived( config, $config => $config.settings );
|
|
|
|
// Convenience readable store of defined settings keys, derived from config.
|
|
export const defined_settings = derived( config, $config => $config.defined_settings );
|
|
|
|
// Convenience readable store of translated strings, derived from config.
|
|
export const strings = derived( config, $config => $config.strings );
|
|
|
|
// Convenience readable store for nonce, derived from config.
|
|
export const nonce = derived( config, $config => $config.nonce );
|
|
|
|
// Convenience readable store of urls, derived from config.
|
|
export const urls = derived( config, $config => $config.urls );
|
|
|
|
// Convenience readable store of docs, derived from config.
|
|
export const docs = derived( config, $config => $config.docs );
|
|
|
|
// Convenience readable store of api endpoints, derived from config.
|
|
export const endpoints = derived( config, $config => $config.endpoints );
|
|
|
|
// Convenience readable store of diagnostics, derived from config.
|
|
export const diagnostics = derived( config, $config => $config.diagnostics );
|
|
|
|
// Convenience readable store of counts, derived from config.
|
|
export const counts = derived( config, $config => $config.counts );
|
|
|
|
// Convenience readable store of summary counts, derived from config.
|
|
export const summaryCounts = derived( config, $config => $config.summary_counts );
|
|
|
|
// Convenience readable store of offload remaining upsell, derived from config.
|
|
export const offloadRemainingUpsell = derived( config, $config => $config.offload_remaining_upsell );
|
|
|
|
// Convenience readable store of upgrades, derived from config.
|
|
export const upgrades = derived( config, $config => $config.upgrades );
|
|
|
|
// Convenience readable store of whether plugin is set up, derived from config.
|
|
export const is_plugin_setup = derived( config, $config => $config.is_plugin_setup );
|
|
|
|
// Convenience readable store of whether plugin is set up, including with credentials, derived from config.
|
|
export const is_plugin_setup_with_credentials = derived( config, $config => $config.is_plugin_setup_with_credentials );
|
|
|
|
// Convenience readable store of whether storage provider needs access credentials, derived from config.
|
|
export const needs_access_keys = derived( config, $config => $config.needs_access_keys );
|
|
|
|
// Convenience readable store of whether bucket is writable, derived from config.
|
|
export const bucket_writable = derived( config, $config => $config.bucket_writable );
|
|
|
|
// Convenience readable store of settings validation results, derived from config.
|
|
export const settings_validation = derived( config, $config => $config.settings_validation );
|
|
|
|
// Store of inline errors and warnings to be shown next to settings.
|
|
// Format is a map using settings key for keys, values are an array of objects that can be used to instantiate a notification.
|
|
export const settings_notifications = writable( new Map() );
|
|
|
|
// Store of validation errors for settings.
|
|
// Format is a map using settings key for keys, values are strings containing validation error.
|
|
export const validationErrors = writable( new Map() );
|
|
|
|
// Whether settings validations are being run.
|
|
export const revalidatingSettings = writable( false );
|
|
|
|
// Does the app need a page refresh to resolve conflicts?
|
|
export const needs_refresh = writable( false );
|
|
|
|
// Various stores may call the API, and the api object uses some stores.
|
|
// To avoid cyclic dependencies, we therefore co-locate the api object with the stores.
|
|
// We also need to add its functions much later so that JSHint does not complain about using the stores too early.
|
|
export const api = {};
|
|
|
|
/**
|
|
* Creates store of settings.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
function createSettings() {
|
|
const { subscribe, set, update } = writable( [] );
|
|
|
|
return {
|
|
subscribe,
|
|
set,
|
|
async save() {
|
|
const json = await api.put( "settings", get( this ) );
|
|
|
|
if ( json.hasOwnProperty( "saved" ) && true === json.saved ) {
|
|
// Sync settings with what the server has.
|
|
this.updateSettings( json );
|
|
|
|
return json;
|
|
}
|
|
|
|
return { 'saved': false };
|
|
},
|
|
reset() {
|
|
set( { ...get( current_settings ) } );
|
|
},
|
|
async fetch() {
|
|
const json = await api.get( "settings", {} );
|
|
this.updateSettings( json );
|
|
},
|
|
updateSettings( json ) {
|
|
if (
|
|
json.hasOwnProperty( "defined_settings" ) &&
|
|
json.hasOwnProperty( "settings" ) &&
|
|
json.hasOwnProperty( "storage_providers" ) &&
|
|
json.hasOwnProperty( "delivery_providers" ) &&
|
|
json.hasOwnProperty( "is_plugin_setup" ) &&
|
|
json.hasOwnProperty( "is_plugin_setup_with_credentials" ) &&
|
|
json.hasOwnProperty( "needs_access_keys" ) &&
|
|
json.hasOwnProperty( "bucket_writable" ) &&
|
|
json.hasOwnProperty( "urls" )
|
|
) {
|
|
// Update our understanding of what the server's settings are.
|
|
config.update( $config => {
|
|
return {
|
|
...$config,
|
|
defined_settings: json.defined_settings,
|
|
settings: json.settings,
|
|
storage_providers: json.storage_providers,
|
|
delivery_providers: json.delivery_providers,
|
|
is_plugin_setup: json.is_plugin_setup,
|
|
is_plugin_setup_with_credentials: json.is_plugin_setup_with_credentials,
|
|
needs_access_keys: json.needs_access_keys,
|
|
bucket_writable: json.bucket_writable,
|
|
urls: json.urls
|
|
};
|
|
} );
|
|
// Update our local working copy of the settings.
|
|
update( $settings => {
|
|
return { ...json.settings };
|
|
} );
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
export const settings = createSettings();
|
|
|
|
// Have the settings been changed from current server side settings?
|
|
export const settings_changed = derived( [settings, current_settings], objectsDiffer );
|
|
|
|
// Convenience readable store of default storage provider, derived from config.
|
|
export const defaultStorageProvider = derived( config, $config => $config.default_storage_provider );
|
|
|
|
// Convenience readable store of available storage providers.
|
|
export const storage_providers = derived( [config, urls], ( [$config, $urls] ) => {
|
|
for ( const key in $config.storage_providers ) {
|
|
$config.storage_providers[ key ].icon = $urls.assets + "img/icon/provider/storage/" + $config.storage_providers[ key ].provider_key_name + ".svg";
|
|
$config.storage_providers[ key ].link_icon = $urls.assets + "img/icon/provider/storage/" + $config.storage_providers[ key ].provider_key_name + "-link.svg";
|
|
$config.storage_providers[ key ].round_icon = $urls.assets + "img/icon/provider/storage/" + $config.storage_providers[ key ].provider_key_name + "-round.svg";
|
|
}
|
|
|
|
return $config.storage_providers;
|
|
} );
|
|
|
|
// Convenience readable store of storage provider's details.
|
|
export const storage_provider = derived( [settings, storage_providers], ( [$settings, $storage_providers] ) => {
|
|
if ( $settings.hasOwnProperty( "provider" ) && $storage_providers.hasOwnProperty( $settings.provider ) ) {
|
|
return $storage_providers[ $settings.provider ];
|
|
} else {
|
|
return [];
|
|
}
|
|
} );
|
|
|
|
// Convenience readable store of default delivery provider, derived from config.
|
|
export const defaultDeliveryProvider = derived( config, $config => $config.default_delivery_provider );
|
|
|
|
// Convenience readable store of available delivery providers.
|
|
export const delivery_providers = derived( [config, urls, storage_provider], ( [$config, $urls, $storage_provider] ) => {
|
|
for ( const key in $config.delivery_providers ) {
|
|
if ( "storage" === key ) {
|
|
$config.delivery_providers[ key ].icon = $storage_provider.icon;
|
|
$config.delivery_providers[ key ].round_icon = $storage_provider.round_icon;
|
|
$config.delivery_providers[ key ].provider_service_quick_start_url = $storage_provider.provider_service_quick_start_url;
|
|
} else {
|
|
$config.delivery_providers[ key ].icon = $urls.assets + "img/icon/provider/delivery/" + $config.delivery_providers[ key ].provider_key_name + ".svg";
|
|
$config.delivery_providers[ key ].round_icon = $urls.assets + "img/icon/provider/delivery/" + $config.delivery_providers[ key ].provider_key_name + "-round.svg";
|
|
}
|
|
}
|
|
|
|
return $config.delivery_providers;
|
|
} );
|
|
|
|
// Convenience readable store of delivery provider's details.
|
|
export const delivery_provider = derived( [settings, delivery_providers, urls], ( [$settings, $delivery_providers, $urls] ) => {
|
|
if ( $settings.hasOwnProperty( "delivery-provider" ) && $delivery_providers.hasOwnProperty( $settings[ "delivery-provider" ] ) ) {
|
|
return $delivery_providers[ $settings[ "delivery-provider" ] ];
|
|
} else {
|
|
return [];
|
|
}
|
|
} );
|
|
|
|
// Full name for current region.
|
|
export const region_name = derived( [settings, storage_provider, strings], ( [$settings, $storage_provider, $strings] ) => {
|
|
if ( $settings.region && $storage_provider.regions && $storage_provider.regions.hasOwnProperty( $settings.region ) ) {
|
|
return $storage_provider.regions[ $settings.region ];
|
|
} else if ( $settings.region && $storage_provider.regions ) {
|
|
// Region set but not available in list of regions.
|
|
return $strings.unknown;
|
|
} else if ( $storage_provider.default_region && $storage_provider.regions && $storage_provider.regions.hasOwnProperty( $storage_provider.default_region ) ) {
|
|
// Region not set but default available.
|
|
return $storage_provider.regions[ $storage_provider.default_region ];
|
|
} else {
|
|
// Possibly no default region or regions available.
|
|
return $strings.unknown;
|
|
}
|
|
} );
|
|
|
|
// Convenience readable store of whether Block All Public Access is enabled.
|
|
export const bapa = derived( [settings, storage_provider], ( [$settings, $storage_provider] ) => {
|
|
return $storage_provider.block_public_access_supported && $settings.hasOwnProperty( "block-public-access" ) && $settings[ "block-public-access" ];
|
|
} );
|
|
|
|
// Convenience readable store of whether Object Ownership is enforced.
|
|
export const ooe = derived( [settings, storage_provider], ( [$settings, $storage_provider] ) => {
|
|
return $storage_provider.object_ownership_supported && $settings.hasOwnProperty( "object-ownership-enforced" ) && $settings[ "object-ownership-enforced" ];
|
|
} );
|
|
|
|
/**
|
|
* Creates a store of notifications.
|
|
*
|
|
* Example object in the array:
|
|
* {
|
|
* id: "error-message",
|
|
* type: "error", // error | warning | success | primary (default)
|
|
* dismissible: true,
|
|
* flash: true, // Optional, means notification is context specific and will not persist on server, defaults to true.
|
|
* inline: false, // Optional, unlikely to be true, included here for completeness.
|
|
* only_show_on_tab: "media-library", // Optional, blank/missing means on all tabs.
|
|
* heading: "Global Error: Something has gone terribly pear shaped.", // Optional.
|
|
* message: "We're so sorry, but unfortunately we're going to have to delete the year 2020.", // Optional.
|
|
* icon: "notification-error.svg", // Optional icon file name to be shown in front of heading.
|
|
* plainHeading: false, // Optional boolean as to whether a <p> tag should be used instead of <h3> for heading content.
|
|
* extra: "", // Optional extra content to be shown in paragraph below message.
|
|
* links: [], // Optional list of links to be shown at bottom of notice.
|
|
* },
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
function createNotifications() {
|
|
const { subscribe, set, update } = writable( [] );
|
|
|
|
return {
|
|
set,
|
|
subscribe,
|
|
add( notification ) {
|
|
// There's a slight difference between our notification's formatting and what WP uses.
|
|
if ( notification.hasOwnProperty( "type" ) && notification.type === "updated" ) {
|
|
notification.type = "success";
|
|
}
|
|
if ( notification.hasOwnProperty( "type" ) && notification.type === "notice-warning" ) {
|
|
notification.type = "warning";
|
|
}
|
|
if ( notification.hasOwnProperty( "type" ) && notification.type === "notice-info" ) {
|
|
notification.type = "info";
|
|
}
|
|
if (
|
|
notification.hasOwnProperty( "message" ) &&
|
|
(!notification.hasOwnProperty( "heading" ) || notification.heading.trim().length === 0)
|
|
) {
|
|
notification.heading = notification.message;
|
|
notification.plainHeading = true;
|
|
delete notification.message;
|
|
}
|
|
if ( !notification.hasOwnProperty( "flash" ) ) {
|
|
notification.flash = true;
|
|
}
|
|
|
|
// We need some sort of id for indexing and to ensure rendering is efficient.
|
|
if ( !notification.hasOwnProperty( "id" ) ) {
|
|
// Notifications are useless without at least a heading or message, so we can be sure at least one exists.
|
|
const idHeading = notification.hasOwnProperty( "heading" ) ? notification.heading.trim() : "dynamic-heading";
|
|
const idMessage = notification.hasOwnProperty( "message" ) ? notification.message.trim() : "dynamic-message";
|
|
|
|
notification.id = btoa( idHeading + idMessage );
|
|
}
|
|
|
|
// So that rendering is efficient, but updates displayed notifications that re-use keys,
|
|
// we create a render_key based on id and created_at as created_at is churned on re-use.
|
|
const createdAt = notification.hasOwnProperty( "created_at" ) ? notification.created_at : 0;
|
|
notification.render_key = notification.id + "-" + createdAt;
|
|
|
|
update( $notifications => {
|
|
// Maybe update a notification if id already exists.
|
|
let index = -1;
|
|
if ( notification.hasOwnProperty( "id" ) ) {
|
|
index = $notifications.findIndex( _notification => _notification.id === notification.id );
|
|
}
|
|
|
|
if ( index >= 0 ) {
|
|
// If the id exists but has been dismissed, add the replacement notification to the end of the array
|
|
// if given notification is newer, otherwise skip it entirely.
|
|
if ( $notifications[ index ].hasOwnProperty( "dismissed" ) ) {
|
|
if ( $notifications[ index ].dismissed < notification.created_at ) {
|
|
$notifications.push( notification );
|
|
$notifications.splice( index, 1 );
|
|
}
|
|
} else {
|
|
// Update existing.
|
|
$notifications.splice( index, 1, notification );
|
|
}
|
|
} else {
|
|
// Add new.
|
|
$notifications.push( notification );
|
|
}
|
|
|
|
return $notifications.sort( this.sortCompare );
|
|
} );
|
|
},
|
|
sortCompare( a, b ) {
|
|
// Sort by created_at in case an existing notification was updated.
|
|
if ( a.created_at < b.created_at ) {
|
|
return -1;
|
|
}
|
|
|
|
if ( a.created_at > b.created_at ) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
async dismiss( id ) {
|
|
update( $notifications => {
|
|
const index = $notifications.findIndex( notification => notification.id === id );
|
|
|
|
// If the notification still exists, set a "dismissed" tombstone with the created_at value.
|
|
// The cleanup will delete any notifications that have been dismissed and no longer exist
|
|
// in the list of notifications retrieved from the server.
|
|
// The created_at value ensures that if a notification is retrieved from the server that
|
|
// has the same id but later created_at, then it can be added, otherwise it is skipped.
|
|
if ( index >= 0 ) {
|
|
if ( $notifications[ index ].hasOwnProperty( "created_at" ) ) {
|
|
$notifications[ index ].dismissed = $notifications[ index ].created_at;
|
|
} else {
|
|
// Notification likely did not come from server, maybe a local "flash" notification.
|
|
$notifications.splice( index, 1 );
|
|
}
|
|
}
|
|
|
|
return $notifications;
|
|
} );
|
|
|
|
// Tell server to dismiss notification, still ok to try if flash notification, makes sure it is definitely removed.
|
|
await api.delete( "notifications", { id: id, all_tabs: true } );
|
|
},
|
|
/**
|
|
* Delete removes a notification from the UI without telling the server.
|
|
*/
|
|
delete( id ) {
|
|
update( $notifications => {
|
|
const index = $notifications.findIndex( notification => notification.id === id );
|
|
|
|
if ( index >= 0 ) {
|
|
$notifications.splice( index, 1 );
|
|
}
|
|
|
|
return $notifications;
|
|
} );
|
|
},
|
|
cleanup( latest ) {
|
|
update( $notifications => {
|
|
for ( const [index, notification] of $notifications.entries() ) {
|
|
// Only clean up dismissed or server created notices that no longer exist.
|
|
if ( notification.hasOwnProperty( "dismissed" ) || notification.hasOwnProperty( "created_at" ) ) {
|
|
const latestIndex = latest.findIndex( _notification => _notification.id === notification.id );
|
|
|
|
// If server doesn't know about the notification anymore, remove it.
|
|
if ( latestIndex < 0 ) {
|
|
$notifications.splice( index, 1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
return $notifications;
|
|
} );
|
|
}
|
|
};
|
|
}
|
|
|
|
export const notifications = createNotifications();
|
|
|
|
// Controller for periodic fetch of state info.
|
|
let stateFetchInterval;
|
|
let stateFetchIntervalStarted = false;
|
|
let stateFetchIntervalPaused = false;
|
|
|
|
// Store of functions to call before an update of state processes the result into config.
|
|
export const preStateUpdateCallbacks = writable( [] );
|
|
|
|
// Store of functions to call after an update of state processes the result into config.
|
|
export const postStateUpdateCallbacks = writable( [] );
|
|
|
|
/**
|
|
* Store of functions to call when state info is updated, and actual API access methods.
|
|
*
|
|
* Functions are called after the returned state info has been used to update the config store.
|
|
* Therefore, functions should only be added to the store if extra processing is required.
|
|
* The functions should be asynchronous as they are part of the reactive chain and called with await.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
function createState() {
|
|
const { subscribe, set, update } = writable( [] );
|
|
|
|
return {
|
|
subscribe,
|
|
set,
|
|
update,
|
|
async fetch() {
|
|
const json = await api.get( "state", {} );
|
|
|
|
// Abort controller is still a bit hit or miss, so we'll go old skool.
|
|
if ( stateFetchIntervalStarted && !stateFetchIntervalPaused ) {
|
|
this.updateState( json );
|
|
}
|
|
},
|
|
updateState( json ) {
|
|
for ( const callable of get( preStateUpdateCallbacks ) ) {
|
|
callable( json );
|
|
}
|
|
|
|
const dirty = get( settings_changed );
|
|
const previous_settings = { ...get( current_settings ) }; // cloned
|
|
|
|
config.update( $config => {
|
|
return { ...$config, ...json };
|
|
} );
|
|
|
|
// If the settings weren't changed before, they shouldn't be now.
|
|
if ( !dirty && get( settings_changed ) ) {
|
|
settings.reset();
|
|
}
|
|
|
|
// If settings are in middle of being changed when changes come in
|
|
// from server, reset to server version.
|
|
if ( dirty && objectsDiffer( [previous_settings, get( current_settings )] ) ) {
|
|
needs_refresh.update( $needs_refresh => true );
|
|
settings.reset();
|
|
}
|
|
|
|
for ( const callable of get( postStateUpdateCallbacks ) ) {
|
|
callable( json );
|
|
}
|
|
},
|
|
async startPeriodicFetch() {
|
|
stateFetchIntervalStarted = true;
|
|
stateFetchIntervalPaused = false;
|
|
|
|
await this.fetch();
|
|
|
|
stateFetchInterval = setInterval( async () => {
|
|
await this.fetch();
|
|
}, 5000 );
|
|
},
|
|
stopPeriodicFetch() {
|
|
stateFetchIntervalStarted = false;
|
|
stateFetchIntervalPaused = false;
|
|
|
|
clearInterval( stateFetchInterval );
|
|
},
|
|
pausePeriodicFetch() {
|
|
if ( stateFetchIntervalStarted ) {
|
|
stateFetchIntervalPaused = true;
|
|
clearInterval( stateFetchInterval );
|
|
}
|
|
},
|
|
async resumePeriodicFetch() {
|
|
stateFetchIntervalPaused = false;
|
|
|
|
if ( stateFetchIntervalStarted ) {
|
|
await this.startPeriodicFetch();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
export const state = createState();
|
|
|
|
// API functions added here to avoid JSHint errors.
|
|
api.headers = () => {
|
|
return {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-WP-Nonce': get( nonce )
|
|
};
|
|
};
|
|
|
|
api.url = ( endpoint ) => {
|
|
return get( urls ).api + get( endpoints )[ endpoint ];
|
|
};
|
|
|
|
api.get = async ( endpoint, params ) => {
|
|
let url = new URL( api.url( endpoint ) );
|
|
|
|
const searchParams = new URLSearchParams( params );
|
|
|
|
searchParams.forEach( function( value, name ) {
|
|
url.searchParams.set( name, value );
|
|
} );
|
|
|
|
const response = await fetch( url.toString(), {
|
|
method: 'GET',
|
|
headers: api.headers()
|
|
} );
|
|
return response.json().then( json => {
|
|
json = api.check_response( json );
|
|
return json;
|
|
} );
|
|
};
|
|
|
|
api.post = async ( endpoint, body ) => {
|
|
const response = await fetch( api.url( endpoint ), {
|
|
method: 'POST',
|
|
headers: api.headers(),
|
|
body: JSON.stringify( body )
|
|
} );
|
|
return response.json().then( json => {
|
|
json = api.check_response( json );
|
|
return json;
|
|
} );
|
|
};
|
|
|
|
api.put = async ( endpoint, body ) => {
|
|
const response = await fetch( api.url( endpoint ), {
|
|
method: 'PUT',
|
|
headers: api.headers(),
|
|
body: JSON.stringify( body )
|
|
} );
|
|
return response.json().then( json => {
|
|
json = api.check_response( json );
|
|
return json;
|
|
} );
|
|
};
|
|
|
|
api.delete = async ( endpoint, body ) => {
|
|
const response = await fetch( api.url( endpoint ), {
|
|
method: 'DELETE',
|
|
headers: api.headers(),
|
|
body: JSON.stringify( body )
|
|
} );
|
|
return response.json().then( json => {
|
|
json = api.check_response( json );
|
|
return json;
|
|
} );
|
|
};
|
|
|
|
api.check_errors = ( json ) => {
|
|
if ( json.code && json.message ) {
|
|
notifications.add( {
|
|
id: json.code,
|
|
type: 'error',
|
|
dismissible: true,
|
|
heading: get( strings ).api_error_notice_heading,
|
|
message: json.message
|
|
} );
|
|
|
|
// Just in case resultant json is expanded into a store.
|
|
delete json.code;
|
|
delete json.message;
|
|
}
|
|
|
|
return json;
|
|
};
|
|
|
|
api.check_notifications = ( json ) => {
|
|
const _notifications = json.hasOwnProperty( "notifications" ) ? json.notifications : [];
|
|
if ( _notifications ) {
|
|
for ( const notification of _notifications ) {
|
|
notifications.add( notification );
|
|
}
|
|
}
|
|
notifications.cleanup( _notifications );
|
|
|
|
// Just in case resultant json is expanded into a store.
|
|
delete json.notifications;
|
|
|
|
return json;
|
|
};
|
|
|
|
api.check_response = ( json ) => {
|
|
json = api.check_notifications( json );
|
|
json = api.check_errors( json );
|
|
|
|
return json;
|
|
};
|