= (int) $required; } /** * One of a feature's human-readable strings, resolved at the moment it is drawn. * * @since 1.0.0 * * @param mixed $value A string, or something that can produce one. * @return string */ function wpbr_feature_text($value) { if (is_callable($value)) return (string) call_user_func($value); return is_scalar($value) ? (string) $value : ''; } /** * Register a feature an add-on provides. * * Free calls wpbr_feature_enabled() to decide whether to render a feature's own * surface or a placeholder. It never calls into the add-on directly, and it * never checks whether a particular plugin is active: a feature is present * because something registered it, not because of who that something was. * * @param string $id Stable identifier, e.g. 'woocommerce'. Never translated. * @param array $args { * @type string $title Shown wherever the feature is named. Translated by the caller. * @type string $description One sentence, for a placeholder. Translated by the caller. * @type int $api The WPBR_EXTENSION_API this add-on was written against. * @type callable $render Optional. The feature's default surface; the same * thing as $surfaces['default']. * @type array $surfaces Optional. Map of slot name => callable, naming the * slots in the free UI this feature fills. The slots * are listed in docs/EXTENDING.md; a name that is not * one of them is simply never rendered, because a * later release may add it. * } * @return bool True if registered; false if refused, in which case the refusal * is recorded so an administrator is told rather than left guessing. */ function wpbr_register_feature($id, array $args = array()) { $id = sanitize_key($id); if ($id === '') return false; $required = isset($args['api']) ? (int) $args['api'] : 1; if (!wpbr_extension_api_supports($required)) { // An add-on from the future. Refusing is the safe answer: it expects // guarantees this version does not make, and half-working is worse than // visibly absent. // // But refusing silently is worse than either. The customer installed and // paid for something and nothing happened; the fix - update the free // plugin - is one they would never guess. So the refusal is recorded and // wpbr_admin_warnings() tells them. $refused = wpbr_refused_features(); $refused[$id] = array( 'id' => $id, 'title' => isset($args['title']) ? $args['title'] : $id, 'required' => $required, ); $GLOBALS['wpbr_refused_features'] = $refused; return false; } // 'render' is the default surface under another name, so an add-on that only // has one place to draw does not have to know the slot vocabulary. $surfaces = array(); if (isset($args['surfaces']) && is_array($args['surfaces'])) { foreach ($args['surfaces'] as $slot => $callback) { $slot = sanitize_key($slot); if ($slot === '') continue; // Declaring a slot with no callable is meaningful, not an error: it is // how a lapsed licence says "this is mine, but I cannot draw it today", // which is what turns a hole in the screen into a placeholder. $surfaces[$slot] = is_callable($callback) ? $callback : null; } } if (isset($args['render']) && is_callable($args['render'])) { $surfaces['default'] = $args['render']; } $features = wpbr_registered_features(); $features[$id] = array( 'id' => $id, // A CALLABLE IS ACCEPTED HERE FOR THE SAME REASON IT IS ON A REFERENCE // PROVIDER'S LABEL. Features are registered on `wpbr_extensions_init`, // which Free fires from `plugins_loaded` — and translating there makes // WordPress 6.7+ log `_load_textdomain_just_in_time` on every request, // which is noise in somebody else's error log and reads as carelessness // in a plugin review. A plain string is still accepted, because an // add-on that does not need translating should not have to write a // function to say so. Resolve with `wpbr_feature_text()`. 'title' => isset($args['title']) ? $args['title'] : $id, 'description' => isset($args['description']) ? $args['description'] : '', 'api' => $required, 'render' => isset($surfaces['default']) ? $surfaces['default'] : null, 'surfaces' => $surfaces, ); $GLOBALS['wpbr_features'] = $features; return true; } /** * Every registered feature, keyed by id. * * Held in a global rather than an option: registration happens on every request, * from whatever add-on is active right now. Persisting it would mean a * deactivated add-on's features outliving it. */ function wpbr_registered_features() { if (!isset($GLOBALS['wpbr_features']) || !is_array($GLOBALS['wpbr_features'])) { $GLOBALS['wpbr_features'] = array(); } return $GLOBALS['wpbr_features']; } /** * Add-ons refused because they need a newer contract than this release honours. * * Held beside the registry and just as transient: the next request asks the * add-ons again, and an add-on that stopped asking stops being refused. */ function wpbr_refused_features() { if (!isset($GLOBALS['wpbr_refused_features']) || !is_array($GLOBALS['wpbr_refused_features'])) { $GLOBALS['wpbr_refused_features'] = array(); } return $GLOBALS['wpbr_refused_features']; } /** Whether a feature is available on this install right now. */ function wpbr_feature_enabled($id) { $features = wpbr_registered_features(); return isset($features[sanitize_key($id)]); } /** One registered feature, or null. */ function wpbr_feature($id) { $features = wpbr_registered_features(); $id = sanitize_key($id); return isset($features[$id]) ? $features[$id] : null; } /** * Render a feature's surface, or say plainly that it is not installed. * * The placeholder is the point. A screen that simply omits what it cannot do * leaves the reader wondering whether they are looking in the wrong place; one * that names the feature and says it is not installed answers the question. * * @param string $id Feature id. * @param string $fallback Optional sentence describing what is missing. Translated by the caller. */ function wpbr_render_feature($id, $fallback = '') { $feature = wpbr_feature($id); if ($feature && $feature['render']) { call_user_func($feature['render']); return; } $text = $fallback; if ($text === '' && $feature) { $text = wpbr_feature_text($feature['description']); } if ($text === '') { return; } echo '
' . esc_html($text) . '
'; echo '' . esc_html__('Make matching smarter over time with Pro', 'wpbr-bulk-replace') . '
'; echo '' . esc_html__('Corrections you make here are used for this batch. WP Bulk Replace Pro remembers them, manages your abbreviations and ignore rules, and reuses that knowledge on every batch after this one.', 'wpbr-bulk-replace') . '
'; echo ''; echo '