Files
Josep Piguillem 83d1298dda API REST: filtro por idioma en /wp/v2/animals (?lang=es|eu)
- Polylang gratuito no filtra la API REST por idioma: el parámetro «lang» se pasa a la consulta.
- MCP: list_animals descarta además las fichas de otro idioma (funciona aunque WordPress aún no
  esté actualizado).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:03:55 +02:00

522 lines
23 KiB
PHP

<?php
/**
* Public API, in English: REST (/wp-json/wp/v2/animals, /wp-json/wp/v2/species) and GraphQL (/graphql).
*
* Internally the data keeps its original (Spanish) meta keys; this layer maps them to English
* names and values so API consumers get a conventional, stable contract.
*/
defined( 'ABSPATH' ) || exit;
/** Is this request to one of the APIs (REST, GraphQL, OpenAPI schema/docs)? */
function ga_is_api_request() {
static $is = null;
if ( null === $is ) {
$uri = (string) ( $_SERVER['REQUEST_URI'] ?? '' );
$path = (string) wp_parse_url( $uri, PHP_URL_PATH );
$is = (bool) preg_match( '#^/(?:eu/)?(?:wp-json|graphql|rest-api/(?:schema|docs))(?:/|$)#', $path )
|| isset( $_GET['rest_route'] ) || isset( $_GET['graphql'] );
}
return $is;
}
/* The API speaks English (field descriptions, errors, core schema), whatever the site language. */
foreach ( array( 'determine_locale', 'locale' ) as $ga_locale_hook ) {
add_filter( $ga_locale_hook, fn( $locale ) => ga_is_api_request() ? 'en_US' : $locale, 1000 );
}
/** Internal value => API value, per field. */
function ga_api_enums() {
return array(
'estado' => array( 'disponible' => 'available', 'proceso' => 'pending', 'adoptado' => 'adopted' ),
'sexo' => array( 'macho' => 'male', 'hembra' => 'female' ),
'tamano' => array( 'pequeno' => 'small', 'mediano' => 'medium', 'grande' => 'large' ),
'edad_unidad' => array( 'anos' => 'years', 'meses' => 'months' ),
);
}
/** API property => internal field. Shared fields are common to every translation of the animal. */
function ga_api_map() {
return array(
'status' => 'estado',
'featured' => 'destacado',
'sponsorable' => 'apadrinable',
'sex' => 'sexo',
'birthDate' => 'nacimiento',
'age' => 'edad',
'ageUnit' => 'edad_unidad',
'size' => 'tamano',
'arrivalDate' => 'llegada',
'galleryIds' => 'galeria',
'video' => 'video',
'personality' => 'caracter',
'needs' => 'necesidades',
'compatibility' => 'convivencia',
'idealHome' => 'hogar',
);
}
function ga_api_to( $field, $value ) {
return ga_api_enums()[ $field ][ $value ] ?? ( '' === $value ? null : $value );
}
function ga_api_from( $field, $value ) {
$flip = array_flip( ga_api_enums()[ $field ] ?? array() );
return $flip[ $value ] ?? null;
}
/** Animal profile for the APIs (English keys and values, plus readable labels in the animal's language). */
function ga_animal_api_data( $post_id ) {
$lang = function_exists( 'pll_get_post_language' ) ? ( pll_get_post_language( $post_id ) ?: 'es' ) : 'es';
$gallery = array_values( array_filter( array_map( 'absint', explode( ',', ga_get( $post_id, 'galeria' ) ) ) ) );
$text = fn( $key ) => '' === ga_get( $post_id, $key ) ? null : ga_get( $post_id, $key );
$data = array(
'status' => ga_api_to( 'estado', ga_status( $post_id ) ),
'featured' => (bool) ga_get( $post_id, 'destacado' ),
'sponsorable' => (bool) ga_get( $post_id, 'apadrinable' ),
'sex' => ga_api_to( 'sexo', ga_get( $post_id, 'sexo' ) ),
'birthDate' => $text( 'nacimiento' ),
'age' => '' === ga_get( $post_id, 'edad' ) ? null : (float) ga_get( $post_id, 'edad' ),
'ageUnit' => ga_api_to( 'edad_unidad', ga_get( $post_id, 'edad_unidad' ) ),
'size' => ga_api_to( 'tamano', ga_get( $post_id, 'tamano' ) ),
'arrivalDate' => $text( 'llegada' ),
'galleryIds' => $gallery,
'photos' => array_values( array_filter( array_map( fn( $id ) => wp_get_attachment_image_url( $id, 'large' ), ga_gallery_ids( $post_id ) ) ) ),
'video' => $text( 'video' ),
'personality' => $text( 'caracter' ),
'needs' => $text( 'necesidades' ),
'compatibility' => $text( 'convivencia' ),
'idealHome' => $text( 'hogar' ),
'language' => $lang,
);
$data['labels'] = ga_with_lang( $lang, fn() => array(
'status' => ga_label( $post_id, 'estado' ),
'sex' => ga_label( $post_id, 'sexo' ) ?: null,
'age' => ga_age( $post_id ) ?: null,
'size' => ga_label( $post_id, 'tamano' ) ?: null,
) );
return $data;
}
/** Writes an API profile into the animal. Shared fields are copied to its translations. */
function ga_animal_api_update( array $profile, $post_id ) {
$fields = ga_fields();
foreach ( ga_api_map() as $prop => $key ) {
if ( ! array_key_exists( $prop, $profile ) ) {
continue;
}
$raw = $profile[ $prop ];
switch ( $prop ) {
case 'status':
case 'sex':
case 'size':
case 'ageUnit':
$value = null === $raw ? '' : ga_api_from( $key, $raw );
if ( null === $value ) {
return new WP_Error( 'rest_invalid_param', "Invalid value for profile.$prop.", array( 'status' => 400 ) );
}
break;
case 'featured':
case 'sponsorable':
$value = $raw ? '1' : '';
break;
case 'age':
$value = null === $raw ? '' : (string) absint( $raw );
break;
case 'birthDate':
case 'arrivalDate':
$value = (string) $raw;
if ( '' !== $value && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
return new WP_Error( 'rest_invalid_param', "profile.$prop must be YYYY-MM-DD.", array( 'status' => 400 ) );
}
break;
case 'galleryIds':
$value = implode( ',', array_filter( array_map( 'absint', (array) $raw ) ) );
break;
case 'video':
$value = esc_url_raw( (string) $raw );
break;
default:
$value = sanitize_textarea_field( (string) $raw );
}
if ( ! empty( $fields[ $key ]['shared'] ) ) {
ga_update_meta_all_languages( $post_id, "_ga_$key", $value );
} else {
update_post_meta( $post_id, "_ga_$key", $value );
}
}
return true;
}
function ga_lines_of( $text ) {
return array_values( array_filter( array_map( 'trim', explode( "\n", (string) $text ) ) ) );
}
/** Default needs list (same as the theme registry), used when nothing has been saved yet. */
function ga_default_needs( $lang ) {
if ( function_exists( 'ga_customizer_registry' ) ) {
return ga_customizer_registry()['material']['fields']['list'][ $lang ] ?? '';
}
return '';
}
/** JSON schema of the profile (used by REST validation and the OpenAPI docs). */
function ga_animal_api_schema() {
$string_or_null = array( 'type' => array( 'string', 'null' ) );
$enum = fn( $field ) => array( 'type' => array( 'string', 'null' ), 'enum' => array_merge( array_values( ga_api_enums()[ $field ] ), array( null ) ) );
return array(
'description' => 'Adoption profile of the animal. Shared fields are common to all translations; texts are per language.',
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'status' => array( 'type' => 'string', 'enum' => array_values( ga_api_enums()['estado'] ), 'description' => 'Adopted animals are hidden from public listings.' ),
'featured' => array( 'type' => 'boolean', 'description' => 'Shown on the home page.' ),
'sponsorable' => array( 'type' => 'boolean', 'description' => 'Looking for a sponsor.' ),
'sex' => $enum( 'sexo' ),
'birthDate' => $string_or_null + array( 'format' => 'date' ),
'age' => array( 'type' => array( 'number', 'null' ), 'description' => 'Approximate age, used when birthDate is unknown.' ),
'ageUnit' => $enum( 'edad_unidad' ),
'size' => $enum( 'tamano' ),
'arrivalDate' => $string_or_null + array( 'format' => 'date', 'description' => 'Date the animal arrived at the shelter.' ),
'galleryIds' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ), 'description' => 'Attachment IDs of the extra photos (the featured image is the main photo).' ),
'photos' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'format' => 'uri' ), 'readonly' => true, 'description' => 'Main photo followed by the gallery.' ),
'video' => $string_or_null + array( 'format' => 'uri' ),
'personality' => $string_or_null,
'needs' => $string_or_null,
'compatibility' => $string_or_null + array( 'description' => 'Observed behaviour with children and other animals, in context.' ),
'idealHome' => $string_or_null,
'language' => array( 'type' => 'string', 'enum' => array( 'es', 'eu' ), 'readonly' => true ),
'labels' => array( 'type' => 'object', 'readonly' => true, 'description' => 'Human-readable values in the animal\'s language.' ),
),
);
}
add_action( 'rest_api_init', function () {
// Language of the animal and link to its translation (write), e.g. {"language":"eu","translation_of":123}.
register_rest_field( 'animal', 'language', array(
'get_callback' => fn( $post ) => function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post['id'] ) : 'es',
'update_callback' => function ( $value, $post ) {
if ( function_exists( 'pll_set_post_language' ) ) {
pll_set_post_language( $post->ID, $value );
}
return true;
},
'schema' => array( 'type' => 'string', 'enum' => array( 'es', 'eu' ), 'description' => 'Language of this translation.', 'context' => array( 'view', 'edit' ) ),
) );
register_rest_field( 'animal', 'translation_of', array(
'get_callback' => function ( $post ) {
$translations = function_exists( 'pll_get_post_translations' ) ? pll_get_post_translations( $post['id'] ) : array();
unset( $translations[ pll_get_post_language( $post['id'] ) ?: '' ] );
return $translations ? (int) reset( $translations ) : null;
},
'update_callback' => function ( $value, $post ) {
if ( ! $value || ! function_exists( 'pll_save_post_translations' ) || 'animal' !== get_post_type( $value ) ) {
return true;
}
if ( ! current_user_can( 'edit_post', $value ) ) {
return new WP_Error( 'rest_forbidden', 'Cannot edit the linked animal.', array( 'status' => 403 ) );
}
$translations = pll_get_post_translations( $value );
$translations[ pll_get_post_language( $post->ID ) ] = $post->ID;
pll_save_post_translations( $translations );
// The new translation inherits the shared data (status, dates, photos…).
foreach ( ga_fields() as $key => $field ) {
if ( ! empty( $field['shared'] ) && '' === ga_get( $post->ID, $key ) && '' !== ga_get( $value, $key ) ) {
update_post_meta( $post->ID, "_ga_$key", ga_get( $value, $key ) );
}
}
if ( ! has_post_thumbnail( $post->ID ) && has_post_thumbnail( $value ) ) {
set_post_thumbnail( $post->ID, get_post_thumbnail_id( $value ) );
}
return true;
},
'schema' => array( 'type' => array( 'integer', 'null' ), 'description' => 'ID of the same animal in the other language.', 'context' => array( 'view', 'edit' ) ),
) );
// Material needs list (Personalizar → Ayuda material), per language.
register_rest_route( 'bilagun/v1', '/needs', array(
array(
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => fn() => array(
'es' => ga_lines_of( get_theme_mod( 'ga_material_list', ga_default_needs( 'es' ) ) ),
'eu' => ga_lines_of( get_theme_mod( 'ga_material_list_eu', ga_default_needs( 'eu' ) ) ),
),
),
array(
'methods' => 'PUT',
'permission_callback' => fn() => current_user_can( 'edit_others_posts' ),
'args' => array(
'es' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
'eu' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
),
'callback' => function ( WP_REST_Request $req ) {
foreach ( array( 'es' => 'ga_material_list', 'eu' => 'ga_material_list_eu' ) as $lang => $mod ) {
if ( null !== $req->get_param( $lang ) ) {
$items = array_filter( array_map( 'sanitize_text_field', (array) $req->get_param( $lang ) ) );
set_theme_mod( $mod, implode( "\n", $items ) );
}
}
return array(
'es' => ga_lines_of( get_theme_mod( 'ga_material_list', '' ) ),
'eu' => ga_lines_of( get_theme_mod( 'ga_material_list_eu', '' ) ),
);
},
),
) );
register_rest_field( 'animal', 'profile', array(
'get_callback' => fn( $post ) => ga_animal_api_data( $post['id'] ),
'update_callback' => fn( $value, $post ) => ga_animal_api_update( (array) $value, $post->ID ),
'schema' => ga_animal_api_schema(),
) );
} );
add_action( 'graphql_register_types', function () {
foreach ( array(
'AnimalStatusEnum' => array( 'estado', 'Adoption status' ),
'AnimalSexEnum' => array( 'sexo', 'Sex' ),
'AnimalSizeEnum' => array( 'tamano', 'Size (dogs)' ),
'AnimalAgeUnitEnum' => array( 'edad_unidad', 'Unit of the approximate age' ),
) as $type => [ $field, $description ] ) {
$values = array();
foreach ( ga_api_enums()[ $field ] as $api ) {
$values[ strtoupper( $api ) ] = array( 'value' => $api );
}
register_graphql_enum_type( $type, array( 'description' => $description, 'values' => $values ) );
}
register_graphql_object_type( 'AnimalProfileLabels', array(
'description' => 'Human-readable values in the animal\'s language.',
'fields' => array(
'status' => array( 'type' => 'String' ),
'sex' => array( 'type' => 'String' ),
'age' => array( 'type' => 'String' ),
'size' => array( 'type' => 'String' ),
),
) );
register_graphql_object_type( 'AnimalProfile', array(
'description' => 'Adoption profile of the animal.',
'fields' => array(
'status' => array( 'type' => 'AnimalStatusEnum' ),
'featured' => array( 'type' => 'Boolean' ),
'sponsorable' => array( 'type' => 'Boolean' ),
'sex' => array( 'type' => 'AnimalSexEnum' ),
'birthDate' => array( 'type' => 'String' ),
'age' => array( 'type' => 'Float' ),
'ageUnit' => array( 'type' => 'AnimalAgeUnitEnum' ),
'size' => array( 'type' => 'AnimalSizeEnum' ),
'arrivalDate' => array( 'type' => 'String' ),
'photos' => array( 'type' => array( 'list_of' => 'String' ) ),
'video' => array( 'type' => 'String' ),
'personality' => array( 'type' => 'String' ),
'needs' => array( 'type' => 'String' ),
'compatibility' => array( 'type' => 'String' ),
'idealHome' => array( 'type' => 'String' ),
'language' => array( 'type' => 'String' ),
'labels' => array( 'type' => 'AnimalProfileLabels' ),
),
) );
register_graphql_field( 'Animal', 'profile', array(
'type' => 'AnimalProfile',
'resolve' => fn( $post ) => ga_animal_api_data( $post->databaseId ),
) );
register_graphql_field( 'Animal', 'language', array(
'type' => 'String',
'description' => 'Language of this translation (es or eu).',
'resolve' => fn( $post ) => function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post->databaseId ) : 'es',
) );
register_graphql_field( 'RootQueryToAnimalConnectionWhereArgs', 'language', array(
'type' => 'String',
'description' => 'Only animals in this language (es or eu).',
) );
register_graphql_field( 'RootQueryToAnimalConnectionWhereArgs', 'adoptionStatus', array(
'type' => 'AnimalStatusEnum',
'description' => 'Only animals with this adoption status.',
) );
} );
/* REST: «?lang=es|eu» en /wp/v2/animals (Polylang gratuito no filtra la API REST por idioma). */
add_filter( 'rest_animal_collection_params', function ( $params ) {
$params['lang'] = array( 'type' => 'string', 'enum' => array( 'es', 'eu' ), 'description' => 'Only animals in this language.' );
return $params;
} );
add_filter( 'rest_animal_query', function ( $args, $request ) {
$args['lang'] = $request['lang'] ?: '';
return $args;
}, 10, 2 );
/* GraphQL: «where: { language, adoptionStatus }» en la consulta de animales («status» ya es el estado de publicación de WordPress). */
add_filter( 'graphql_post_object_connection_query_args', function ( $args, $source, $input ) {
if ( 'animal' !== ( $args['post_type'] ?? '' ) && array( 'animal' ) !== ( $args['post_type'] ?? '' ) ) {
return $args;
}
$where = $input['where'] ?? array();
$args['lang'] = in_array( $where['language'] ?? '', array( 'es', 'eu' ), true ) ? $where['language'] : '';
if ( ! empty( $where['adoptionStatus'] ) ) {
$internal = ga_api_from( 'estado', $where['adoptionStatus'] );
$values = 'proceso' === $internal ? array( 'proceso', 'reservado' ) : array( $internal );
$args['meta_query'][] = 'disponible' === $internal
? array( 'relation' => 'OR', array( 'key' => '_ga_estado', 'value' => array( 'disponible', 'urgente', 'acogida' ), 'compare' => 'IN' ), array( 'key' => '_ga_estado', 'compare' => 'NOT EXISTS' ) )
: array( 'key' => '_ga_estado', 'value' => $values, 'compare' => 'IN' );
}
return $args;
}, 10, 3 );
/** Converts a WordPress JSON schema into an OpenAPI 3.0 schema (nullable types, readOnly). */
function ga_openapi_schema( $schema ) {
if ( isset( $schema['type'] ) && is_array( $schema['type'] ) ) {
$types = array_values( array_diff( $schema['type'], array( 'null' ) ) );
if ( count( $types ) < count( $schema['type'] ) ) {
$schema['nullable'] = true;
}
$schema['type'] = $types[0] ?? 'string';
}
if ( isset( $schema['enum'] ) ) {
$schema['enum'] = array_values( array_filter( $schema['enum'], fn( $v ) => null !== $v ) );
}
if ( ! empty( $schema['readonly'] ) ) {
$schema['readOnly'] = true;
}
unset( $schema['readonly'], $schema['context'] );
foreach ( array( 'properties' ) as $key ) {
if ( isset( $schema[ $key ] ) ) {
$schema[ $key ] = array_map( 'ga_openapi_schema', $schema[ $key ] );
}
}
if ( isset( $schema['items'] ) ) {
$schema['items'] = ga_openapi_schema( $schema['items'] );
}
return $schema;
}
/*
* OpenAPI schema (/rest-api/schema): the plugin flattens object fields to "string".
* We post-process its output to document «profile» properly (component AnimalProfile).
*/
add_action( 'wp', function () {
if ( 'schema' !== get_query_var( 'swagger_api' ) ) {
return;
}
ob_start( function ( $json ) {
$doc = json_decode( $json, true );
if ( ! is_array( $doc ) || empty( $doc['paths'] ) ) {
return $json;
}
$profile = ga_openapi_schema( ga_animal_api_schema() );
$ref = array( '$ref' => '#/components/schemas/AnimalProfile' );
$doc['components']['schemas']['AnimalProfile'] = $profile;
$doc['components']['schemas']['Animal'] = array(
'type' => 'object',
'properties' => array(
'id' => array( 'type' => 'integer' ),
'slug' => array( 'type' => 'string' ),
'link' => array( 'type' => 'string', 'format' => 'uri' ),
'title' => array( 'type' => 'object', 'properties' => array( 'rendered' => array( 'type' => 'string' ) ) ),
'content' => array( 'type' => 'object', 'properties' => array( 'rendered' => array( 'type' => 'string' ) ) ),
'species' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ) ),
'lang' => array( 'type' => 'string', 'description' => 'Language of this translation (es or eu).' ),
'profile' => $ref,
),
);
foreach ( $doc['paths'] as $path => &$methods ) {
if ( ! preg_match( '#^/wp/v2/animals(/\{id\})?$#', $path, $m ) ) {
continue;
}
foreach ( $methods as $method => &$op ) {
foreach ( $op['requestBody']['content'] ?? array() as $type => $content ) {
if ( isset( $content['schema']['properties']['profile'] ) ) {
$op['requestBody']['content'][ $type ]['schema']['properties']['profile'] = $ref;
}
}
// JSON first: «profile» is a nested object, which form-encoded bodies can't express well.
if ( ! empty( $op['requestBody']['content'] ) && empty( $op['requestBody']['content']['application/json'] ) ) {
$first = reset( $op['requestBody']['content'] );
$op['requestBody']['content'] = array( 'application/json' => $first ) + $op['requestBody']['content'];
}
if ( 'get' === $method ) {
$item = array( '$ref' => '#/components/schemas/Animal' );
$op['responses']['200'] = array(
'description' => 'OK',
'content' => array( 'application/json' => array( 'schema' => empty( $m[1] ) ? array( 'type' => 'array', 'items' => $item ) : $item ) ),
);
}
}
unset( $op );
}
unset( $methods );
return wp_json_encode( $doc );
} );
}, 0 ); // Antes que el plugin, que emite el esquema en «wp».
/*
* Documentación Swagger (/rest-api/docs/).
*
* El plugin WP API SwaggerUI genera bien el esquema OpenAPI (/rest-api/schema), pero su paquete
* en wordpress.org no incluye los archivos de la interfaz (assets/), así que su página sale en blanco.
* Servimos nuestra propia página con Swagger UI oficial (versión fijada y con integridad SRI).
* Solo para usuarios conectados, salvo API_DOCS_PUBLIC=true.
*/
define( 'GA_SWAGGER_UI_VERSION', '5.33.0' );
add_action( 'template_redirect', function () {
if ( 'docs' !== get_query_var( 'swagger_api' ) ) {
return;
}
if ( 'true' !== getenv( 'API_DOCS_PUBLIC' ) && ! is_user_logged_in() ) {
auth_redirect();
}
$cdn = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@' . GA_SWAGGER_UI_VERSION;
$config = array(
'url' => home_url( '/rest-api/schema' ),
'nonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
);
nocache_headers();
header( 'Content-Type: text/html; charset=utf-8' );
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title><?php echo esc_html( get_bloginfo( 'name' ) ); ?> · API</title>
<link rel="stylesheet" href="<?php echo esc_url( $cdn . '/swagger-ui.css' ); ?>" integrity="sha384-Ov4/wv3j2bmct8cDc5X4ngJZohVPzEmc6uDPH8WeljUxO5vtoykvMEfbu9Vh6RaW" crossorigin="anonymous">
<style>body{margin:0;background:#fafafa}.ga-api-bar{padding:10px 20px;background:#401f0c;color:#fff;font:14px system-ui,sans-serif}.ga-api-bar a{color:#e6d9b7}</style>
</head>
<body>
<div class="ga-api-bar">
<?php echo esc_html( get_bloginfo( 'name' ) ); ?> · REST API ·
<a href="<?php echo esc_url( home_url( '/rest-api/schema' ) ); ?>">OpenAPI schema</a> ·
<a href="<?php echo esc_url( home_url( '/graphql' ) ); ?>">GraphQL</a> ·
<a href="<?php echo esc_url( admin_url() ); ?>">Dashboard</a>
</div>
<div id="swagger-ui"></div>
<script src="<?php echo esc_url( $cdn . '/swagger-ui-bundle.js' ); ?>" integrity="sha384-YDALVcy8kj8yltLBVi1vBiBAUqdxvus673gM8XKwiy6aDUJFXivF/KCufekjYbVf" crossorigin="anonymous"></script>
<script>
( function ( config ) {
window.ui = SwaggerUIBundle( {
url: config.url,
dom_id: '#swagger-ui',
deepLinking: true,
docExpansion: 'none',
filter: true,
tryItOutEnabled: false,
// Con la sesión iniciada, «Try it out» se autentica con la cookie + nonce de WordPress.
requestInterceptor: function ( req ) {
if ( config.nonce && req.url.indexOf( '/wp-json/' ) !== -1 ) {
req.headers[ 'X-WP-Nonce' ] = config.nonce;
req.credentials = 'same-origin';
}
return req;
}
} );
} )( <?php echo wp_json_encode( $config ); ?> );
</script>
</body>
</html>
<?php
exit;
}, 1 );