/** * WPpiloot Connector 1.1.0 (snippet-versie) - https://wppiloot.nl * Laat WPpiloot ACF-velden lezen en aanpassen, ook als "Show in REST API" uit staat. * WordPress-rechten blijven gelden. Staat de plugin ook aan, dan laadt maar één van beide. */ if (! defined('WPPILOOT_CONNECTOR_VERSION')) { define('WPPILOOT_CONNECTOR_SOURCE', 'snippet'); define('WPPILOOT_CONNECTOR_VERSION', '1.1.0'); /** * Eindpunten (namespace wppiloot/v1), alleen voor ingelogde gebruikers * (applicatiewachtwoord): * GET /status versie, ACF aanwezig? * GET /acf/overview optiepagina's en berichttypes met ACF-velden * GET /acf/fields?target=... velddefinities + huidige waarden * POST /acf/fields {target, values: {veldnaam: waarde}} * target = "post:123" (elk berichttype) of "options:". */ add_action('rest_api_init', function () { $ns = 'wppiloot/v1'; register_rest_route($ns, '/status', [ 'methods' => 'GET', 'permission_callback' => 'is_user_logged_in', 'callback' => function () { return [ 'version' => WPPILOOT_CONNECTOR_VERSION, 'acf' => function_exists('acf_get_field_groups'), 'acf_version' => defined('ACF_VERSION') ? ACF_VERSION : null, 'acf_pro' => defined('ACF_PRO') && ACF_PRO, 'source' => defined('WPPILOOT_CONNECTOR_SOURCE') ? WPPILOOT_CONNECTOR_SOURCE : 'plugin', ]; }, ]); register_rest_route($ns, '/acf/overview', [ 'methods' => 'GET', 'permission_callback' => 'is_user_logged_in', 'callback' => 'wppiloot_acf_overview', ]); register_rest_route($ns, '/acf/fields', [ [ 'methods' => 'GET', 'permission_callback' => 'wppiloot_acf_can_edit', 'callback' => 'wppiloot_acf_get', 'args' => ['target' => ['required' => true, 'type' => 'string']], ], [ 'methods' => 'POST', 'permission_callback' => 'wppiloot_acf_can_edit', 'callback' => 'wppiloot_acf_update', 'args' => [ 'target' => ['required' => true, 'type' => 'string'], 'values' => ['required' => true, 'type' => 'object'], ], ], ]); }); /** * Zet "post:123" of "options:slug" om naar wat ACF verwacht. * * @return array{kind: string, acf_id: int|string, post?: WP_Post, page?: array}|WP_Error */ function wppiloot_acf_target($target) { if (! function_exists('acf_get_field_groups')) { return new WP_Error('wppiloot_no_acf', 'Advanced Custom Fields is niet actief op deze site.', ['status' => 400]); } if (preg_match('/^post:(\d+)$/', (string) $target, $m)) { $post = get_post((int) $m[1]); if (! $post || $post->post_type === 'revision') { return new WP_Error('wppiloot_not_found', 'Dit bericht bestaat niet.', ['status' => 404]); } return ['kind' => 'post', 'acf_id' => $post->ID, 'post' => $post]; } if (preg_match('/^options:([A-Za-z0-9_-]+)$/', (string) $target, $m)) { $page = function_exists('acf_get_options_page') ? acf_get_options_page($m[1]) : null; if (! $page) { return new WP_Error('wppiloot_not_found', 'Deze optiepagina bestaat niet.', ['status' => 404]); } return ['kind' => 'options', 'acf_id' => $page['post_id'] ?: 'options', 'page' => $page]; } return new WP_Error('wppiloot_bad_target', 'target moet "post:" of "options:" zijn.', ['status' => 400]); } /** Dezelfde rechten als in de WordPress-backend. */ function wppiloot_acf_can_edit(WP_REST_Request $request) { if (! is_user_logged_in()) { return false; } $t = wppiloot_acf_target($request['target']); if (is_wp_error($t)) { return $t; } return $t['kind'] === 'post' ? current_user_can('edit_post', $t['post']->ID) : current_user_can($t['page']['capability'] ?: 'edit_posts'); } /** Veldgroepen die bij dit doel horen (zoals ACF ze in de editor toont). */ function wppiloot_acf_groups(array $t) { if ($t['kind'] === 'post') { $args = ['post_id' => $t['post']->ID, 'post_type' => $t['post']->post_type]; $template = get_page_template_slug($t['post']); if ($template) { $args['page_template'] = $template; } } else { $args = ['options_page' => $t['page']['menu_slug']]; } return array_values(array_filter(acf_get_field_groups($args), function ($g) { return ! empty($g['active']); })); } /** Beknopte definitie van een veld, voor het taalmodel. */ function wppiloot_acf_describe(array $field) { $d = [ 'name' => $field['name'], 'label' => $field['label'], 'type' => $field['type'], ]; if (! empty($field['instructions'])) { $d['instructions'] = wp_strip_all_tags($field['instructions']); } if (! empty($field['required'])) { $d['required'] = true; } if (! empty($field['choices']) && is_array($field['choices'])) { $d['choices'] = $field['choices']; } if (! empty($field['multiple'])) { $d['multiple'] = true; } if (! empty($field['max'])) { $d['max'] = $field['max']; } if (in_array($field['type'], ['image', 'file', 'gallery'], true)) { $d['value_format'] = 'media-id (gallery: lijst van media-id\'s)'; } elseif (in_array($field['type'], ['post_object', 'relationship', 'page_link'], true)) { $d['value_format'] = 'bericht-id (of lijst van id\'s)'; } elseif ($field['type'] === 'taxonomy') { $d['value_format'] = 'term-id (of lijst van id\'s)'; } elseif ($field['type'] === 'link') { $d['value_format'] = '{"title","url","target"}'; } elseif ($field['type'] === 'true_false') { $d['value_format'] = '0 of 1'; } if (! empty($field['sub_fields'])) { $d['sub_fields'] = array_map('wppiloot_acf_describe', $field['sub_fields']); } if (! empty($field['layouts'])) { $d['layouts'] = array_values(array_map(function ($l) { return [ 'name' => $l['name'], 'label' => $l['label'], 'sub_fields' => array_map('wppiloot_acf_describe', $l['sub_fields'] ?? []), ]; }, $field['layouts'])); } return $d; } /** * Ruwe waarde (zoals opgeslagen, zonder opmaak) met veldnamen in plaats van * veldsleutels in repeaters, groepen en flexibele inhoud. Zo komt het weer * terug in update_field(). */ function wppiloot_acf_export(array $field, $value) { switch ($field['type']) { case 'repeater': if (! is_array($value)) { return []; } return array_values(array_map(function ($row) use ($field) { return wppiloot_acf_export_row($field['sub_fields'] ?? [], (array) $row); }, $value)); case 'group': return wppiloot_acf_export_row($field['sub_fields'] ?? [], (array) $value); case 'flexible_content': if (! is_array($value)) { return []; } $layouts = []; foreach ($field['layouts'] ?? [] as $l) { $layouts[$l['name']] = $l; } return array_values(array_map(function ($row) use ($layouts) { $row = (array) $row; $name = $row['acf_fc_layout'] ?? ''; $out = ['acf_fc_layout' => $name]; if (isset($layouts[$name])) { $out += wppiloot_acf_export_row($layouts[$name]['sub_fields'] ?? [], $row); } return $out; }, $value)); default: return $value; } } function wppiloot_acf_export_row(array $sub_fields, array $row) { $out = []; foreach ($sub_fields as $sub) { $raw = $row[$sub['key']] ?? $row[$sub['name']] ?? null; $out[$sub['name']] = wppiloot_acf_export($sub, $raw); } return $out; } function wppiloot_acf_get(WP_REST_Request $request) { $t = wppiloot_acf_target($request['target']); if (is_wp_error($t)) { return $t; } $groups = []; foreach (wppiloot_acf_groups($t) as $group) { $fields = []; foreach (acf_get_fields($group) ?: [] as $field) { if (in_array($field['type'], ['tab', 'message', 'accordion'], true) || $field['name'] === '') { continue; } $fields[] = wppiloot_acf_describe($field) + [ 'value' => wppiloot_acf_export($field, get_field($field['key'], $t['acf_id'], false)), ]; } $groups[] = ['title' => $group['title'], 'fields' => $fields]; } return [ 'target' => $request['target'], 'title' => $t['kind'] === 'post' ? get_the_title($t['post']) : $t['page']['page_title'], 'post_type' => $t['kind'] === 'post' ? $t['post']->post_type : null, 'groups' => $groups, ]; } /** Alle velden (op het hoogste niveau) die bij het doel horen, op naam. */ function wppiloot_acf_field_map(array $t) { $map = []; foreach (wppiloot_acf_groups($t) as $group) { foreach (acf_get_fields($group) ?: [] as $field) { if ($field['name'] !== '' && ! in_array($field['type'], ['tab', 'message', 'accordion'], true)) { $map[$field['name']] = $field; } } } return $map; } /** Tekst zonder unfiltered_html-recht door wp_kses_post, net als in de editor. */ function wppiloot_acf_clean($value) { if (is_array($value)) { return array_map('wppiloot_acf_clean', $value); } if (is_string($value) && ! current_user_can('unfiltered_html')) { return wp_kses_post($value); } return $value; } function wppiloot_acf_update(WP_REST_Request $request) { $t = wppiloot_acf_target($request['target']); if (is_wp_error($t)) { return $t; } $values = (array) $request['values']; if (! $values) { return new WP_Error('wppiloot_empty', 'Geen waarden meegegeven.', ['status' => 400]); } $map = wppiloot_acf_field_map($t); $unknown = array_diff(array_keys($values), array_keys($map)); if ($unknown) { return new WP_Error('wppiloot_unknown_field', 'Onbekende velden voor dit doel: '.implode(', ', $unknown).'. Bestaande velden: '.implode(', ', array_keys($map)).'.', ['status' => 400]); } foreach ($values as $name => $value) { $field = $map[$name]; if (in_array($field['type'], ['select', 'radio', 'button_group', 'checkbox'], true) && ! empty($field['choices']) && $value !== '' && $value !== null) { foreach ((array) $value as $v) { if (! array_key_exists((string) $v, $field['choices'])) { return new WP_Error('wppiloot_bad_choice', "Waarde \"{$v}\" is niet toegestaan voor {$name}. Kies uit: ".implode(', ', array_keys($field['choices'])).'.', ['status' => 400]); } } } } $updated = []; foreach ($values as $name => $value) { $field = $map[$name]; update_field($field['key'], wppiloot_acf_clean($value), $t['acf_id']); $updated[$name] = wppiloot_acf_export($field, get_field($field['key'], $t['acf_id'], false)); } if ($t['kind'] === 'post') { // Wijzigingsdatum bijwerken (caches, sitemaps), zonder de inhoud aan te raken. wp_update_post(['ID' => $t['post']->ID]); } return ['target' => $request['target'], 'updated' => $updated]; } function wppiloot_acf_overview() { if (! function_exists('acf_get_field_groups')) { return new WP_Error('wppiloot_no_acf', 'Advanced Custom Fields is niet actief op deze site.', ['status' => 400]); } $options = []; foreach (function_exists('acf_get_options_pages') ? (acf_get_options_pages() ?: []) : [] as $page) { if (current_user_can($page['capability'] ?: 'edit_posts')) { $options[] = ['target' => 'options:'.$page['menu_slug'], 'title' => $page['page_title']]; } } $post_types = []; foreach (acf_get_field_groups() as $group) { if (empty($group['active'])) { continue; } foreach ($group['location'] as $rules) { foreach ($rules as $rule) { if ($rule['param'] === 'post_type' && $rule['operator'] === '==') { $post_types[$rule['value']][] = $group['title']; } elseif ($rule['param'] === 'page_template' && $rule['operator'] === '==') { $post_types['page'][] = $group['title'].' (sjabloon '.$rule['value'].')'; } } } } $types = []; foreach ($post_types as $slug => $titles) { $obj = get_post_type_object($slug); if ($obj && current_user_can($obj->cap->edit_posts)) { $types[] = [ 'post_type' => $slug, 'label' => $obj->labels->name, 'rest_base' => $obj->show_in_rest ? ($obj->rest_base ?: $slug) : null, 'field_groups' => array_values(array_unique($titles)), ]; } } return ['options_pages' => $options, 'post_types' => $types]; } }