Stored fields whose type is not available. */ private static $unavailable = []; /** * @var bool */ private static $noticeHooked = false; /** * Register the fields of a page on admin_init. Fields of a type that is not registered (an add-on that is * not active) are skipped; the page still works and an administrator sees a notice. * * @param iterable $fields Stored fields. * @param array|null $page The page the fields are on; then only fields the user may edit are registered * (Access::fieldMode()). */ public static function hook(iterable $fields, ?array $page = null): void { foreach ($fields as $field) { // The type is looked up on admin_init, after add-ons had their chance to register it. add_action('admin_init', function () use ($field, $page) { if ($page !== null && Access::fieldMode($page, $field, wp_get_current_user()) !== 'edit') { return; } self::registerField($field); }); add_action('admin_enqueue_scripts', function () use ($field) { $type = FieldRegistry::instance()->get($field['type'] ?? ''); if ($type !== null && $type->usesAssets()) { $type->enqueue($field); } }); } if (!self::$noticeHooked) { self::$noticeHooked = true; add_action('admin_notices', [self::class, 'printNotice']); } } /** * @param array $field A stored field. */ public static function registerField(array $field): void { $type = FieldRegistry::instance()->get($field['type'] ?? ''); if ($type === null) { self::$unavailable[] = ['type' => (string) ($field['type'] ?? ''), 'name' => (string) ($field['field_name'] ?? '')]; return; } $page = $field['page_name']; $sectionId = $type->sectionId($field); if (!$type->hasOption()) { // Layout: the section itself prints the field. add_settings_section($sectionId, '', function () use ($type, $field) { $type->render($field, null); }, $page); return; } $name = $field['field_name']; $class = isset($field['class_attribute']) ? $field['class_attribute'] : ''; register_setting($page, $name, [ 'type' => $type->optionType(), 'group' => $page, 'sanitize_callback' => function ($value) use ($field) { return self::sanitizeValue($field, $value); }, ]); add_settings_section($sectionId, '', '', $page); $args = ['class' => $class]; if ($type->labelFor()) { $args = ['label_for' => $name, 'class' => $class]; } add_settings_field( $name, isset($field['field_label']) ? stripslashes($field['field_label']) : '', function () use ($type, $field, $name) { $type->render($field, get_option($name)); }, $page, $sectionId, $args ); } /** * What an option of a field becomes when it is saved: the field type sanitizes it, unless a filter on * `aop_sanitize_option_` decides. The result then passes `aop_sanitized_option` and * `aop_sanitized_option_`, which can change it without having to sanitize it again. The options page * and the import both come through here. * * @param array $field A stored field of a type that is available and has an option. * @param mixed $value * * @return mixed */ public static function sanitizeValue(array $field, $value) { $name = $field['field_name']; if (has_filter(Plugin::PREFIX_ . 'sanitize_option_' . $name)) { $sanitized = apply_filters(Plugin::PREFIX_ . 'sanitize_option_' . $name, $value); } else { $sanitized = FieldRegistry::instance()->get($field['type'])->sanitizeValue($value, $field); } /** * Change the value of any field after it was sanitized, just before it is stored. * * @since 1.0.0 * * @param mixed $sanitized The value as the field type (or `aop_sanitize_option_`) made it. * @param array $field The stored field: type, field_name and its settings. * @param mixed $value The value as it was posted (or imported). Not safe: never store it as is. */ $sanitized = apply_filters(Plugin::PREFIX_ . 'sanitized_option', $sanitized, $field, $value); /** * The same, for the option of one field: `aop_sanitized_option_