Changeset 3695885
- Timestamp:
- 09/14/2026 08:33:35 PM (7 weeks ago)
- Location:
- groundhogg
- Files:
-
- 20 added
- 4 deleted
- 66 edited
- 1 copied
-
tags/4.8.1 (copied) (copied from groundhogg/trunk )
-
tags/4.8.1/README.txt (modified) (2 diffs)
-
tags/4.8.1/groundhogg.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/abilities.php (modified) (7 diffs)
-
tags/4.8.1/includes/abilities/ability.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/broadcasts/cancel-broadcast.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/broadcasts/get-broadcast.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/broadcasts/list-broadcasts.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/broadcasts/send-email-broadcast.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/campaigns/list-campaigns.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/contacts/add-contact-note.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/contacts/create-contact.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/contacts/get-contact.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/contacts/list-contact-notes.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/contacts/list-custom-fields.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/contacts/list-owners.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/contacts/list-saved-searches.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/contacts/search-contacts.php (modified) (6 diffs)
-
tags/4.8.1/includes/abilities/contacts/update-contact.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/db/describe-table.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/db/query-table.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/emails/create-email-template.php (added)
-
tags/4.8.1/includes/abilities/emails/create-email.php (deleted)
-
tags/4.8.1/includes/abilities/emails/get-email-template.php (added)
-
tags/4.8.1/includes/abilities/emails/list-email-templates.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/emails/list-replacement-codes.php (added)
-
tags/4.8.1/includes/abilities/emails/list-sender-profiles.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/emails/send-composed-email.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/emails/send-email-template.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/emails/update-email-template.php (added)
-
tags/4.8.1/includes/abilities/emails/update-email.php (deleted)
-
tags/4.8.1/includes/abilities/funnels/activate-flow.php (added)
-
tags/4.8.1/includes/abilities/funnels/add-to-flow.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/funnels/create-flow.php (added)
-
tags/4.8.1/includes/abilities/funnels/deactivate-flow.php (added)
-
tags/4.8.1/includes/abilities/funnels/list-flows.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/funnels/list-step-types.php (added)
-
tags/4.8.1/includes/abilities/reports/get-reports.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/reports/list-report-types.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/schemas/contact-schema.php (modified) (4 diffs)
-
tags/4.8.1/includes/abilities/schemas/email-schema.php (modified) (2 diffs)
-
tags/4.8.1/includes/abilities/schemas/segment-schema.php (modified) (6 diffs)
-
tags/4.8.1/includes/abilities/schemas/step-type-schema.php (added)
-
tags/4.8.1/includes/abilities/tags/list-tags.php (modified) (1 diff)
-
tags/4.8.1/includes/abilities/traits/extensible-schema.php (added)
-
tags/4.8.1/includes/abilities/utils/upload-media.php (modified) (3 diffs)
-
trunk/README.txt (modified) (2 diffs)
-
trunk/groundhogg.php (modified) (2 diffs)
-
trunk/includes/abilities/abilities.php (modified) (7 diffs)
-
trunk/includes/abilities/ability.php (modified) (1 diff)
-
trunk/includes/abilities/broadcasts/cancel-broadcast.php (modified) (1 diff)
-
trunk/includes/abilities/broadcasts/get-broadcast.php (modified) (1 diff)
-
trunk/includes/abilities/broadcasts/list-broadcasts.php (modified) (1 diff)
-
trunk/includes/abilities/broadcasts/send-email-broadcast.php (modified) (2 diffs)
-
trunk/includes/abilities/campaigns/list-campaigns.php (modified) (2 diffs)
-
trunk/includes/abilities/contacts/add-contact-note.php (modified) (1 diff)
-
trunk/includes/abilities/contacts/create-contact.php (modified) (2 diffs)
-
trunk/includes/abilities/contacts/get-contact.php (modified) (2 diffs)
-
trunk/includes/abilities/contacts/list-contact-notes.php (modified) (1 diff)
-
trunk/includes/abilities/contacts/list-custom-fields.php (modified) (1 diff)
-
trunk/includes/abilities/contacts/list-owners.php (modified) (1 diff)
-
trunk/includes/abilities/contacts/list-saved-searches.php (modified) (1 diff)
-
trunk/includes/abilities/contacts/search-contacts.php (modified) (6 diffs)
-
trunk/includes/abilities/contacts/update-contact.php (modified) (2 diffs)
-
trunk/includes/abilities/db/describe-table.php (modified) (1 diff)
-
trunk/includes/abilities/db/query-table.php (modified) (1 diff)
-
trunk/includes/abilities/emails/create-email-template.php (added)
-
trunk/includes/abilities/emails/create-email.php (deleted)
-
trunk/includes/abilities/emails/get-email-template.php (added)
-
trunk/includes/abilities/emails/list-email-templates.php (modified) (1 diff)
-
trunk/includes/abilities/emails/list-replacement-codes.php (added)
-
trunk/includes/abilities/emails/list-sender-profiles.php (modified) (1 diff)
-
trunk/includes/abilities/emails/send-composed-email.php (modified) (1 diff)
-
trunk/includes/abilities/emails/send-email-template.php (modified) (1 diff)
-
trunk/includes/abilities/emails/update-email-template.php (added)
-
trunk/includes/abilities/emails/update-email.php (deleted)
-
trunk/includes/abilities/funnels/activate-flow.php (added)
-
trunk/includes/abilities/funnels/add-to-flow.php (modified) (2 diffs)
-
trunk/includes/abilities/funnels/create-flow.php (added)
-
trunk/includes/abilities/funnels/deactivate-flow.php (added)
-
trunk/includes/abilities/funnels/list-flows.php (modified) (1 diff)
-
trunk/includes/abilities/funnels/list-step-types.php (added)
-
trunk/includes/abilities/reports/get-reports.php (modified) (1 diff)
-
trunk/includes/abilities/reports/list-report-types.php (modified) (1 diff)
-
trunk/includes/abilities/schemas/contact-schema.php (modified) (4 diffs)
-
trunk/includes/abilities/schemas/email-schema.php (modified) (2 diffs)
-
trunk/includes/abilities/schemas/segment-schema.php (modified) (6 diffs)
-
trunk/includes/abilities/schemas/step-type-schema.php (added)
-
trunk/includes/abilities/tags/list-tags.php (modified) (1 diff)
-
trunk/includes/abilities/traits/extensible-schema.php (added)
-
trunk/includes/abilities/utils/upload-media.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
groundhogg/tags/4.8.1/README.txt
r3695315 r3695885 7 7 Tested up to: 7.1 8 8 Requires PHP: 7.4 9 Stable tag: 4.8 9 Stable tag: 4.8.1 10 10 License: GPLv3 11 11 License URI: https://www.gnu.org/licenses/gpl.md … … 391 391 392 392 == Changelog == 393 394 = 4.8.1 (2026-09-14) = 395 * ADDED "Create Flow" and "Activate Flow" abilities, so an AI agent can build a flow (funnel) from scratch, including branching steps, and turn it on. A new "List Step Types" ability lists the buildable step types and their settings so an agent can discover what's available before building. 396 * ADDED "List Replacement Codes" ability for discovering the merge tags available for use in emails. 397 * ADDED Add-ons can now register their own ability categories and abilities alongside Groundhogg's, and extend the contact and segment schemas with their own fields, so the WordPress Abilities API integration introduced in 4.8 isn't limited to what Groundhogg itself provides. 398 * TWEAKED Email-related abilities are now named consistently around "email template" (`create-email-template`, `update-email-template`, `get-email-template`) instead of a mix of "email" and "email-template". 399 * TWEAKED Ability descriptions now link out to relevant documentation for easier reference when working with an agent. 393 400 394 401 = 4.8 (2026-09-10) = -
groundhogg/tags/4.8.1/groundhogg.php
r3695315 r3695885 4 4 * Plugin URI: https://groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash 5 5 * Description: CRM and marketing automation for WordPress 6 * Version: 4.8 6 * Version: 4.8.1 7 7 * Author: Groundhogg Inc. 8 8 * Author URI: https://groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash … … 27 27 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly 28 28 29 define( 'GROUNDHOGG_VERSION', '4.8 ' );30 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4. 7.2' );29 define( 'GROUNDHOGG_VERSION', '4.8.1' ); 30 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.8' ); 31 31 32 32 define( 'GROUNDHOGG__FILE__', __FILE__ ); -
groundhogg/tags/4.8.1/includes/abilities/abilities.php
r3695315 r3695885 19 19 use Groundhogg\Abilities\Db\Describe_Table; 20 20 use Groundhogg\Abilities\Db\Query_Table; 21 use Groundhogg\Abilities\Funnels\Activate_Flow; 21 22 use Groundhogg\Abilities\Funnels\Add_To_Flow; 23 use Groundhogg\Abilities\Funnels\Create_Flow; 24 use Groundhogg\Abilities\Funnels\Deactivate_Flow; 22 25 use Groundhogg\Abilities\Funnels\List_Flows; 23 use Groundhogg\Abilities\Emails\Create_Email; 26 use Groundhogg\Abilities\Funnels\List_Step_Types; 27 use Groundhogg\Abilities\Emails\Create_Email_Template; 28 use Groundhogg\Abilities\Emails\Get_Email_Template; 24 29 use Groundhogg\Abilities\Emails\List_Email_Templates; 30 use Groundhogg\Abilities\Emails\List_Replacement_Codes; 25 31 use Groundhogg\Abilities\Emails\List_Sender_Profiles; 26 32 use Groundhogg\Abilities\Emails\Send_Composed_Email; 27 33 use Groundhogg\Abilities\Emails\Send_Email_Template; 28 use Groundhogg\Abilities\Emails\Update_Email ;34 use Groundhogg\Abilities\Emails\Update_Email_Template; 29 35 use Groundhogg\Abilities\Reports\Get_Reports; 30 36 use Groundhogg\Abilities\Reports\List_Report_Types; … … 32 38 use Groundhogg\Abilities\Utils\Upload_Media; 33 39 40 /** 41 * Registers Groundhogg's own abilities/categories, and acts as the registry add-ons use to 42 * register their own alongside them - Abilities::add_category() and Abilities::add_ability(). 43 * 44 * Both must be called before the WP Abilities API actually registers anything - the 45 * 'wp_abilities_api_categories_init'/'wp_abilities_api_init' hooks below, which normally fire on 46 * 'init'. Groundhogg's own bootstrap runs on 'plugins_loaded' at priority 0 (see Plugin::init()), 47 * so any add-on's own 'plugins_loaded' (default priority 10) or 'init' callback is early enough. 48 * Registering from that far out just queues the category/class; if a call ever comes in after the 49 * relevant hook has already fired, it's applied immediately instead of being silently dropped. 50 * 51 * Example - a 3rd-party add-on registering its own category and ability: 52 * 53 * add_action( 'plugins_loaded', function () { 54 * 55 * Abilities::add_category( 'my-addon', [ 56 * 'label' => __( 'My Add-on', 'my-addon' ), 57 * 'description' => __( 'Abilities provided by My Add-on.', 'my-addon' ), 58 * ] ); 59 * 60 * Abilities::add_ability( My_Addon\Abilities\Do_The_Thing::class ); 61 * } ); 62 * 63 * Do_The_Thing must extend Groundhogg\Abilities\Ability just like Groundhogg's own abilities - 64 * it's instantiated the same way, which is what actually calls wp_register_ability(). 65 */ 34 66 class Abilities { 67 68 /** 69 * Ability classes registered by add-ons via self::add_ability(), instantiated alongside 70 * Groundhogg's own in register_abilities(). 71 * 72 * @var string[] 73 */ 74 protected static array $extra_abilities = []; 75 76 /** 77 * Ability categories registered by add-ons via self::add_category(), registered alongside 78 * Groundhogg's own in register_categories(). 79 * 80 * @var array<string, array> 81 */ 82 protected static array $extra_categories = []; 35 83 36 84 public function __construct() { … … 51 99 } 52 100 101 /** 102 * Register an additional ability category, so a 3rd-party add-on's abilities can be grouped 103 * under their own category instead of one of Groundhogg's. See the class docblock for a full 104 * example and the timing requirement. 105 * 106 * @param string $slug the category slug, e.g. "my-addon" 107 * @param array $args ['label' => string, 'description' => string], same shape 108 * wp_register_ability_category() itself takes 109 * 110 * @return void 111 */ 112 public static function add_category( string $slug, array $args ) { 113 114 self::$extra_categories[ $slug ] = $args; 115 116 // The categories hook already fired - register it right away rather than dropping it. 117 if ( did_action( 'wp_abilities_api_categories_init' ) && function_exists( 'wp_register_ability_category' ) ) { 118 wp_register_ability_category( $slug, $args ); 119 } 120 } 121 122 /** 123 * Register an additional ability class, instantiated the same way Groundhogg's own abilities 124 * are - which is what actually calls wp_register_ability(). See the class docblock for a full 125 * example and the timing requirement. 126 * 127 * @param string $class fully-qualified class name, extending Groundhogg\Abilities\Ability 128 * 129 * @return void 130 */ 131 public static function add_ability( string $class ) { 132 133 self::$extra_abilities[] = $class; 134 135 // The abilities hook already fired - register it right away rather than dropping it. 136 if ( did_action( 'wp_abilities_api_init' ) ) { 137 new $class(); 138 } 139 } 140 53 141 public function register_categories() { 54 142 … … 97 185 'description' => __( 'General-purpose utilities that support the other categories but aren\'t specific to any one of them.', 'groundhogg' ), 98 186 ] ); 187 188 foreach ( self::$extra_categories as $slug => $args ) { 189 wp_register_ability_category( $slug, $args ); 190 } 99 191 } 100 192 101 193 public function register_abilities() { 102 194 103 foreach( [195 $abilities = array_merge( [ 104 196 Get_Contact::class, 105 197 Create_Contact::class, … … 114 206 List_Contact_Notes::class, 115 207 List_Email_Templates::class, 208 Get_Email_Template::class, 116 209 List_Sender_Profiles::class, 117 Create_Email::class, 118 Update_Email::class, 210 Create_Email_Template::class, 211 Update_Email_Template::class, 212 List_Replacement_Codes::class, 119 213 Send_Composed_Email::class, 120 214 Send_Email_Template::class, … … 124 218 Cancel_Broadcast::class, 125 219 List_Flows::class, 220 List_Step_Types::class, 221 Create_Flow::class, 222 Activate_Flow::class, 223 Deactivate_Flow::class, 126 224 Add_To_Flow::class, 127 225 List_Report_Types::class, … … 130 228 Query_Table::class, 131 229 Upload_Media::class, 132 ] as $ability ){ 133 134 $ability = new $ability(); 135 136 } 137 230 ], self::$extra_abilities ); 231 232 foreach ( $abilities as $ability ) { 233 new $ability(); 234 } 138 235 } 139 236 } -
groundhogg/tags/4.8.1/includes/abilities/ability.php
r3695315 r3695885 5 5 abstract class Ability { 6 6 7 protected const stringNAME = '';8 protected const stringCATEGORY = '';9 protected const stringCAPABILITY = '';7 protected const NAME = ''; 8 protected const CATEGORY = ''; 9 protected const CAPABILITY = ''; 10 10 11 protected const boolPUBLIC = true;12 protected const boolREADONLY = false;13 protected const boolDESTRUCTIVE = false;14 protected const boolIDEMPOTENT = false;11 protected const PUBLIC = true; 12 protected const READONLY = false; 13 protected const DESTRUCTIVE = false; 14 protected const IDEMPOTENT = false; 15 15 16 16 /** -
groundhogg/tags/4.8.1/includes/abilities/broadcasts/cancel-broadcast.php
r3695315 r3695885 23 23 class Cancel_Broadcast extends Ability { 24 24 25 protected const stringNAME = 'groundhogg/cancel-broadcast';26 protected const stringCATEGORY = 'groundhogg-broadcasts';27 protected const stringCAPABILITY = 'cancel_broadcasts';25 protected const NAME = 'groundhogg/cancel-broadcast'; 26 protected const CATEGORY = 'groundhogg-broadcasts'; 27 protected const CAPABILITY = 'cancel_broadcasts'; 28 28 29 protected const boolREADONLY = false;30 protected const boolDESTRUCTIVE = true;31 protected const boolIDEMPOTENT = true;29 protected const READONLY = false; 30 protected const DESTRUCTIVE = true; 31 protected const IDEMPOTENT = true; 32 32 33 33 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/broadcasts/get-broadcast.php
r3695315 r3695885 16 16 class Get_Broadcast extends Ability { 17 17 18 protected const stringNAME = 'groundhogg/get-broadcast';19 protected const stringCATEGORY = 'groundhogg-broadcasts';20 protected const stringCAPABILITY = 'view_broadcasts';18 protected const NAME = 'groundhogg/get-broadcast'; 19 protected const CATEGORY = 'groundhogg-broadcasts'; 20 protected const CAPABILITY = 'view_broadcasts'; 21 21 22 protected const boolREADONLY = true;23 protected const boolIDEMPOTENT = true;22 protected const READONLY = true; 23 protected const IDEMPOTENT = true; 24 24 25 25 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/broadcasts/list-broadcasts.php
r3695315 r3695885 37 37 class List_Broadcasts extends Ability { 38 38 39 protected const stringNAME = 'groundhogg/list-broadcasts';40 protected const stringCATEGORY = 'groundhogg-broadcasts';41 protected const stringCAPABILITY = 'view_broadcasts';42 43 protected const boolREADONLY = true;44 protected const boolIDEMPOTENT = true;39 protected const NAME = 'groundhogg/list-broadcasts'; 40 protected const CATEGORY = 'groundhogg-broadcasts'; 41 protected const CAPABILITY = 'view_broadcasts'; 42 43 protected const READONLY = true; 44 protected const IDEMPOTENT = true; 45 45 46 46 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/broadcasts/send-email-broadcast.php
r3695315 r3695885 41 41 class Send_Email_Broadcast extends Ability { 42 42 43 protected const stringNAME = 'groundhogg/send-email-broadcast';44 protected const stringCATEGORY = 'groundhogg-broadcasts';45 protected const stringCAPABILITY = 'schedule_broadcasts';46 47 protected const boolREADONLY = false;48 protected const boolDESTRUCTIVE = true;49 protected const boolIDEMPOTENT = false;43 protected const NAME = 'groundhogg/send-email-broadcast'; 44 protected const CATEGORY = 'groundhogg-broadcasts'; 45 protected const CAPABILITY = 'schedule_broadcasts'; 46 47 protected const READONLY = false; 48 protected const DESTRUCTIVE = true; 49 protected const IDEMPOTENT = false; 50 50 51 51 protected function get_args(): array { … … 195 195 } 196 196 197 // The plain array form, not to_contact_query()'s live query object - 198 // this gets stored (Broadcast::schedule()'s 'query' arg below) and, for 199 // a "dynamic" segment_type, re-run later against whatever then matches; 200 // a live query object (and anything a filter attached to it) can't 201 // survive that round trip. 197 202 $query = Segment_Schema::to_query( $input ); 198 203 -
groundhogg/tags/4.8.1/includes/abilities/campaigns/list-campaigns.php
r3695315 r3695885 9 9 /** 10 10 * Lists the campaigns defined in Groundhogg (Contacts > Campaigns in the admin), so a caller 11 * can discover valid campaign ids/names to pass as create-email 's/update-email's campaigns param,12 * or to filter flows/broadcasts by campaign, without guessing.11 * can discover valid campaign ids/names to pass as create-email-template's/update-email-template's 12 * campaigns param, or to filter flows/broadcasts/emails by campaign, without guessing. 13 13 */ 14 14 class List_Campaigns extends Ability { 15 15 16 protected const stringNAME = 'groundhogg/list-campaigns';17 protected const stringCATEGORY = 'groundhogg-campaigns';18 protected const stringCAPABILITY = 'manage_campaigns';16 protected const NAME = 'groundhogg/list-campaigns'; 17 protected const CATEGORY = 'groundhogg-campaigns'; 18 protected const CAPABILITY = 'manage_campaigns'; 19 19 20 protected const boolREADONLY = true;21 protected const boolIDEMPOTENT = true;20 protected const READONLY = true; 21 protected const IDEMPOTENT = true; 22 22 23 23 protected function get_args(): array { … … 25 25 return [ 26 26 'label' => __( 'List Campaigns', 'groundhogg' ), 27 'description' => __( 'List the campaigns defined in Groundhogg. Use the returned id or name with groundhogg/create-email \'s or groundhogg/update-email\'s campaigns param.', 'groundhogg' ),27 'description' => __( 'List the campaigns defined in Groundhogg. Use the returned id or name with groundhogg/create-email-template\'s or groundhogg/update-email-template\'s campaigns param.', 'groundhogg' ), 28 28 29 29 'input_schema' => [ -
groundhogg/tags/4.8.1/includes/abilities/contacts/add-contact-note.php
r3695315 r3695885 20 20 class Add_Contact_Note extends Ability { 21 21 22 protected const stringNAME = 'groundhogg/add-contact-note';23 protected const stringCATEGORY = 'groundhogg-contacts';24 protected const stringCAPABILITY = 'add_notes';22 protected const NAME = 'groundhogg/add-contact-note'; 23 protected const CATEGORY = 'groundhogg-contacts'; 24 protected const CAPABILITY = 'add_notes'; 25 25 26 protected const boolREADONLY = false;27 protected const boolDESTRUCTIVE = false;28 protected const boolIDEMPOTENT = false;26 protected const READONLY = false; 27 protected const DESTRUCTIVE = false; 28 protected const IDEMPOTENT = false; 29 29 30 30 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/contacts/create-contact.php
r3695315 r3695885 34 34 use Has_Owner_Validation; 35 35 36 protected const stringNAME = 'groundhogg/create-contact';37 protected const stringCATEGORY = 'groundhogg-contacts';38 protected const stringCAPABILITY = 'add_contacts';39 40 protected const boolREADONLY = false;41 protected const boolDESTRUCTIVE = false;42 protected const boolIDEMPOTENT = true;36 protected const NAME = 'groundhogg/create-contact'; 37 protected const CATEGORY = 'groundhogg-contacts'; 38 protected const CAPABILITY = 'add_contacts'; 39 40 protected const READONLY = false; 41 protected const DESTRUCTIVE = false; 42 protected const IDEMPOTENT = true; 43 43 44 44 protected function get_args(): array { … … 88 88 'items' => [ 89 89 'type' => 'string', 90 'enum' => [ 'tags', 'meta' ],90 'enum' => Contact_Schema::expand_options(), 91 91 ], 92 92 'default' => [ 'tags' ], 93 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta .', 'groundhogg' ),93 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta, plus any sections an installed add-on has registered.', 'groundhogg' ), 94 94 ], 95 95 ], -
groundhogg/tags/4.8.1/includes/abilities/contacts/get-contact.php
r3695315 r3695885 10 10 class Get_Contact extends Ability { 11 11 12 protected const stringNAME = 'groundhogg/get-contact';13 protected const stringCATEGORY = 'groundhogg-contacts';14 protected const stringCAPABILITY = 'view_contacts';12 protected const NAME = 'groundhogg/get-contact'; 13 protected const CATEGORY = 'groundhogg-contacts'; 14 protected const CAPABILITY = 'view_contacts'; 15 15 16 protected const boolREADONLY = true;17 protected const boolIDEMPOTENT = true;16 protected const READONLY = true; 17 protected const IDEMPOTENT = true; 18 18 19 19 protected function get_args(): array { … … 46 46 'items' => [ 47 47 'type' => 'string', 48 'enum' => [ 'tags', 'meta' ],48 'enum' => Contact_Schema::expand_options(), 49 49 ], 50 50 'default' => [ 'tags' ], 51 'description' => __( 'Optional extra sections to expand beyond the standard contact fields. Available: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them) .', 'groundhogg' ),51 'description' => __( 'Optional extra sections to expand beyond the standard contact fields. Available: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them), plus any sections an installed add-on has registered.', 'groundhogg' ), 52 52 ], 53 53 ], -
groundhogg/tags/4.8.1/includes/abilities/contacts/list-contact-notes.php
r3695315 r3695885 26 26 class List_Contact_Notes extends Ability { 27 27 28 protected const stringNAME = 'groundhogg/list-contact-notes';29 protected const stringCATEGORY = 'groundhogg-contacts';30 protected const stringCAPABILITY = 'view_notes';28 protected const NAME = 'groundhogg/list-contact-notes'; 29 protected const CATEGORY = 'groundhogg-contacts'; 30 protected const CAPABILITY = 'view_notes'; 31 31 32 protected const boolREADONLY = true;33 protected const boolIDEMPOTENT = true;32 protected const READONLY = true; 33 protected const IDEMPOTENT = true; 34 34 35 35 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/contacts/list-custom-fields.php
r3695315 r3695885 15 15 class List_Custom_Fields extends Ability { 16 16 17 protected const stringNAME = 'groundhogg/list-custom-fields';18 protected const stringCATEGORY = 'groundhogg-contacts';19 protected const stringCAPABILITY = 'edit_contacts';17 protected const NAME = 'groundhogg/list-custom-fields'; 18 protected const CATEGORY = 'groundhogg-contacts'; 19 protected const CAPABILITY = 'edit_contacts'; 20 20 21 protected const boolREADONLY = true;22 protected const boolIDEMPOTENT = true;21 protected const READONLY = true; 22 protected const IDEMPOTENT = true; 23 23 24 24 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/contacts/list-owners.php
r3695315 r3695885 14 14 class List_Owners extends Ability { 15 15 16 protected const stringNAME = 'groundhogg/list-owners';17 protected const stringCATEGORY = 'groundhogg-contacts';18 protected const stringCAPABILITY = 'view_contacts';16 protected const NAME = 'groundhogg/list-owners'; 17 protected const CATEGORY = 'groundhogg-contacts'; 18 protected const CAPABILITY = 'view_contacts'; 19 19 20 protected const boolREADONLY = true;21 protected const boolIDEMPOTENT = true;20 protected const READONLY = true; 21 protected const IDEMPOTENT = true; 22 22 23 23 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/contacts/list-saved-searches.php
r3695315 r3695885 13 13 class List_Saved_Searches extends Ability { 14 14 15 protected const stringNAME = 'groundhogg/list-saved-searches';16 protected const stringCATEGORY = 'groundhogg-contacts';17 protected const stringCAPABILITY = 'view_contacts';15 protected const NAME = 'groundhogg/list-saved-searches'; 16 protected const CATEGORY = 'groundhogg-contacts'; 17 protected const CAPABILITY = 'view_contacts'; 18 18 19 protected const boolREADONLY = true;20 protected const boolIDEMPOTENT = true;19 protected const READONLY = true; 20 protected const IDEMPOTENT = true; 21 21 22 22 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/contacts/search-contacts.php
r3695315 r3695885 6 6 use Groundhogg\Abilities\Schemas\Contact_Schema; 7 7 use Groundhogg\Abilities\Schemas\Segment_Schema; 8 use Groundhogg\Contact_Query;9 8 use Throwable; 10 9 use WP_Error; … … 12 11 class Search_Contacts extends Ability { 13 12 14 protected const stringNAME = 'groundhogg/search-contacts';15 protected const stringCATEGORY = 'groundhogg-contacts';16 protected const stringCAPABILITY = 'view_contacts';13 protected const NAME = 'groundhogg/search-contacts'; 14 protected const CATEGORY = 'groundhogg-contacts'; 15 protected const CAPABILITY = 'view_contacts'; 17 16 18 protected const boolREADONLY = true;19 protected const boolIDEMPOTENT = true;17 protected const READONLY = true; 18 protected const IDEMPOTENT = true; 20 19 21 20 protected function get_args(): array { … … 33 32 'items' => [ 34 33 'type' => 'string', 35 'enum' => [ 'tags', 'meta' ], 34 // 'sql' is search-contacts' own debugging addition, not 35 // part of Contact_Schema::expand_options() (it's about 36 // the query as a whole, not a per-contact section, and 37 // wouldn't make sense on get-contact/create-contact/ 38 // update-contact's single-row fetches). 39 'enum' => array_merge( Contact_Schema::expand_options(), [ 'sql' ] ), 36 40 ], 37 41 'default' => [ 'tags' ], 38 'description' => __( 'Optional extra sections to expand on each contact, beyond the standard fields. Available: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them).', 'groundhogg' ),42 'description' => __( 'Optional extra sections to expand, beyond the standard fields. Per-contact: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them), plus any sections an installed add-on has registered. Query-level (not per-contact): "sql" adds a top-level `sql` field with the raw SQL Contact_Query built for this search - useful for debugging why a search matched (or didn\'t match) what was expected. Note it reflects the modern query builder\'s attempt specifically; if that attempt threw and Contact_Query silently fell back to its legacy query engine, this SQL will NOT be what actually ran.', 'groundhogg' ), 39 43 ], 40 44 'limit' => [ … … 65 69 'items' => Contact_Schema::get_schema(), 66 70 ], 71 'sql' => [ 72 'type' => 'string', 73 'description' => __( 'Only present when "sql" is passed in expand. The raw SQL Contact_Query built for this search - see the `expand` input\'s description for a caveat about legacy-query fallback.', 'groundhogg' ), 74 ], 67 75 ], 68 76 ], … … 75 83 $offset = ! empty( $input['offset'] ) ? absint( $input['offset'] ) : 0; 76 84 77 $query_vars = Segment_Schema::to_query( $input ); 85 // Executes immediately below, so a live query object (to_contact_query()) 86 // rather than the plain array to_query() gives - lets an add-on hook 87 // groundhogg/segment_schema/contact_query and manipulate the query 88 // directly (joins, raw where conditions), and lets pagination be applied 89 // here by just chaining onto it. 90 $contact_query = Segment_Schema::to_contact_query( $input ); 78 91 79 if ( is_wp_error( $ query_vars) ) {80 return $ query_vars;92 if ( is_wp_error( $contact_query ) ) { 93 return $contact_query; 81 94 } 82 95 83 $query_vars['number'] = $limit; 84 $query_vars['offset'] = $offset; 85 $query_vars['found_rows'] = true; 96 $contact_query->setLimit( $limit )->setOffset( $offset )->setFoundRows( true ); 86 97 87 98 try { 88 $contact_query = new Contact_Query(); 89 $results = $contact_query->query( $query_vars ); 99 $results = $contact_query->query(); 90 100 } catch ( Throwable $e ) { 91 101 // Contact_Query's modern query building already falls back to a legacy query on … … 101 111 }, $results ); 102 112 103 return[113 $output = [ 104 114 'total_items' => $contact_query->found_items, 105 115 'contacts' => $contacts, 106 116 ]; 117 118 if ( in_array( 'sql', $expand, true ) ) { 119 // Called after query() rather than before: get_sql()'s own 120 // maybe_setup_query() call is a no-op by this point (already run), so 121 // this reflects the exact query state query() actually built - not a 122 // fresh, possibly-different build. See the `expand` input's 123 // description for the legacy-fallback caveat. 124 $output['sql'] = $contact_query->get_sql(); 125 } 126 127 return $output; 107 128 } 108 129 } -
groundhogg/tags/4.8.1/includes/abilities/contacts/update-contact.php
r3695315 r3695885 29 29 use Has_Owner_Validation; 30 30 31 protected const stringNAME = 'groundhogg/update-contact';32 protected const stringCATEGORY = 'groundhogg-contacts';33 protected const stringCAPABILITY = 'edit_contacts';34 35 protected const boolREADONLY = false;36 protected const boolDESTRUCTIVE = false;37 protected const boolIDEMPOTENT = true;31 protected const NAME = 'groundhogg/update-contact'; 32 protected const CATEGORY = 'groundhogg-contacts'; 33 protected const CAPABILITY = 'edit_contacts'; 34 35 protected const READONLY = false; 36 protected const DESTRUCTIVE = false; 37 protected const IDEMPOTENT = true; 38 38 39 39 protected function get_args(): array { … … 93 93 'items' => [ 94 94 'type' => 'string', 95 'enum' => [ 'tags', 'meta' ],95 'enum' => Contact_Schema::expand_options(), 96 96 ], 97 97 'default' => [ 'tags' ], 98 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta .', 'groundhogg' ),98 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta, plus any sections an installed add-on has registered.', 'groundhogg' ), 99 99 ], 100 100 ], -
groundhogg/tags/4.8.1/includes/abilities/db/describe-table.php
r3695315 r3695885 15 15 class Describe_Table extends Ability { 16 16 17 protected const stringNAME = 'groundhogg/describe-table';18 protected const stringCATEGORY = 'groundhogg-db';19 protected const stringCAPABILITY = 'manage_options';17 protected const NAME = 'groundhogg/describe-table'; 18 protected const CATEGORY = 'groundhogg-db'; 19 protected const CAPABILITY = 'manage_options'; 20 20 21 protected const boolREADONLY = true;22 protected const boolIDEMPOTENT = true;21 protected const READONLY = true; 22 protected const IDEMPOTENT = true; 23 23 24 24 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/db/query-table.php
r3695315 r3695885 40 40 class Query_Table extends Ability { 41 41 42 protected const stringNAME = 'groundhogg/query-table';43 protected const stringCATEGORY = 'groundhogg-db';44 protected const stringCAPABILITY = 'manage_options';45 46 protected const boolREADONLY = true;47 protected const boolIDEMPOTENT = true;42 protected const NAME = 'groundhogg/query-table'; 43 protected const CATEGORY = 'groundhogg-db'; 44 protected const CAPABILITY = 'manage_options'; 45 46 protected const READONLY = true; 47 protected const IDEMPOTENT = true; 48 48 49 49 /** -
groundhogg/tags/4.8.1/includes/abilities/emails/list-email-templates.php
r3695315 r3695885 19 19 * is almost never needed just to pick which email to send, so it is only 20 20 * returned when explicitly requested via `expand`. When expanding content, keep 21 * `limit` small. 21 * `limit` small, or use groundhogg/get-email-template to fetch one email's full 22 * body directly once you have its id. 22 23 * 23 24 * `campaigns` filters via the generic object_relationships-backed 'related' 24 25 * query var (DB::query()) - an email is the primary/parent side of its 25 26 * relationship to a campaign, same direction Email::get_related_objects('campaign') 26 * reads (see groundhogg/create-email's and groundhogg/update-email's campaigns param). 27 * reads (see groundhogg/create-email-template's and groundhogg/update-email-template's 28 * campaigns param). 27 29 */ 28 30 class List_Email_Templates extends Ability { 29 31 30 protected const stringNAME = 'groundhogg/list-email-templates';31 protected const stringCATEGORY = 'groundhogg-email';32 protected const stringCAPABILITY = 'view_emails';32 protected const NAME = 'groundhogg/list-email-templates'; 33 protected const CATEGORY = 'groundhogg-email'; 34 protected const CAPABILITY = 'view_emails'; 33 35 34 protected const boolREADONLY = true;35 protected const boolIDEMPOTENT = true;36 protected const READONLY = true; 37 protected const IDEMPOTENT = true; 36 38 37 39 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/emails/list-sender-profiles.php
r3695315 r3695885 22 22 class List_Sender_Profiles extends Ability { 23 23 24 protected const stringNAME = 'groundhogg/list-sender-profiles';25 protected const stringCATEGORY = 'groundhogg-email';26 protected const stringCAPABILITY = 'send_emails';24 protected const NAME = 'groundhogg/list-sender-profiles'; 25 protected const CATEGORY = 'groundhogg-email'; 26 protected const CAPABILITY = 'send_emails'; 27 27 28 protected const boolREADONLY = true;29 protected const boolIDEMPOTENT = true;28 protected const READONLY = true; 29 protected const IDEMPOTENT = true; 30 30 31 31 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/emails/send-composed-email.php
r3695315 r3695885 43 43 class Send_Composed_Email extends Ability { 44 44 45 protected const stringNAME = 'groundhogg/send-composed-email';46 protected const stringCATEGORY = 'groundhogg-email';47 protected const stringCAPABILITY = 'send_emails';48 49 protected const boolREADONLY = false;50 protected const boolDESTRUCTIVE = true;51 protected const boolIDEMPOTENT = false;45 protected const NAME = 'groundhogg/send-composed-email'; 46 protected const CATEGORY = 'groundhogg-email'; 47 protected const CAPABILITY = 'send_emails'; 48 49 protected const READONLY = false; 50 protected const DESTRUCTIVE = true; 51 protected const IDEMPOTENT = false; 52 52 53 53 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/emails/send-email-template.php
r3695315 r3695885 36 36 class Send_Email_Template extends Ability { 37 37 38 protected const stringNAME = 'groundhogg/send-email-template';39 protected const stringCATEGORY = 'groundhogg-email';40 protected const stringCAPABILITY = 'send_emails';41 42 protected const boolREADONLY = false;43 protected const boolDESTRUCTIVE = true;44 protected const boolIDEMPOTENT = false;38 protected const NAME = 'groundhogg/send-email-template'; 39 protected const CATEGORY = 'groundhogg-email'; 40 protected const CAPABILITY = 'send_emails'; 41 42 protected const READONLY = false; 43 protected const DESTRUCTIVE = true; 44 protected const IDEMPOTENT = false; 45 45 46 46 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/funnels/add-to-flow.php
r3695315 r3695885 34 34 class Add_To_Flow extends Ability { 35 35 36 protected const stringNAME = 'groundhogg/add-to-flow';37 protected const stringCATEGORY = 'groundhogg-funnels';38 protected const stringCAPABILITY = 'start_flows';39 40 protected const boolREADONLY = false;41 protected const boolDESTRUCTIVE = true;42 protected const boolIDEMPOTENT = false;36 protected const NAME = 'groundhogg/add-to-flow'; 37 protected const CATEGORY = 'groundhogg-funnels'; 38 protected const CAPABILITY = 'start_flows'; 39 40 protected const READONLY = false; 41 protected const DESTRUCTIVE = true; 42 protected const IDEMPOTENT = false; 43 43 44 44 protected function get_args(): array { … … 173 173 } 174 174 175 // The plain array form, not to_contact_query()'s live query object - 176 // this gets stored and re-run in background batches (Background_Tasks:: 177 // add_contacts_to_funnel() below); a live query object (and anything a 178 // filter attached to it) can't survive that round trip. 175 179 $query = Segment_Schema::to_query( $input ); 176 180 -
groundhogg/tags/4.8.1/includes/abilities/funnels/list-flows.php
r3695315 r3695885 21 21 class List_Flows extends Ability { 22 22 23 protected const stringNAME = 'groundhogg/list-flows';24 protected const stringCATEGORY = 'groundhogg-funnels';25 protected const stringCAPABILITY = 'view_funnels';23 protected const NAME = 'groundhogg/list-flows'; 24 protected const CATEGORY = 'groundhogg-funnels'; 25 protected const CAPABILITY = 'view_funnels'; 26 26 27 protected const boolREADONLY = true;28 protected const boolIDEMPOTENT = true;27 protected const READONLY = true; 28 protected const IDEMPOTENT = true; 29 29 30 30 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/reports/get-reports.php
r3695315 r3695885 37 37 class Get_Reports extends Ability { 38 38 39 protected const stringNAME = 'groundhogg/get-reports';40 protected const stringCATEGORY = 'groundhogg-reports';41 protected const stringCAPABILITY = 'view_reports';42 43 protected const boolREADONLY = true;44 protected const boolIDEMPOTENT = true;39 protected const NAME = 'groundhogg/get-reports'; 40 protected const CATEGORY = 'groundhogg-reports'; 41 protected const CAPABILITY = 'view_reports'; 42 43 protected const READONLY = true; 44 protected const IDEMPOTENT = true; 45 45 46 46 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/reports/list-report-types.php
r3695315 r3695885 16 16 class List_Report_Types extends Ability { 17 17 18 protected const stringNAME = 'groundhogg/list-report-types';19 protected const stringCATEGORY = 'groundhogg-reports';20 protected const stringCAPABILITY = 'view_reports';18 protected const NAME = 'groundhogg/list-report-types'; 19 protected const CATEGORY = 'groundhogg-reports'; 20 protected const CAPABILITY = 'view_reports'; 21 21 22 protected const boolREADONLY = true;23 protected const boolIDEMPOTENT = true;22 protected const READONLY = true; 23 protected const IDEMPOTENT = true; 24 24 25 25 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/schemas/contact-schema.php
r3695315 r3695885 5 5 use DateTime; 6 6 use Exception; 7 use Groundhogg\Abilities\Traits\Extensible_Schema; 7 8 use Groundhogg\Contact; 8 9 use Groundhogg\Preferences; … … 24 25 * per install. Use the groundhogg/list-custom-fields ability to look up what each meta 25 26 * key actually means (label, type, group). 27 * 28 * Extensible via the Extensible_Schema trait, so an add-on can expose additional 29 * read-only fields on groundhogg/get-contact and groundhogg/search-contacts 30 * results without modifying this class or either of those abilities - one call 31 * registers a new bounded-but-optional property (schema, expand option, and the 32 * callback that computes it), the same "only when asked for" treatment 33 * 'tags'/'meta' get above: 34 * 35 * Contact_Schema::extend( 36 * 'ltv', 37 * __( 'Lifetime value: total spend across all WooCommerce orders.', 'my-plugin' ), 38 * function ( Contact $contact ) { 39 * return (float) wc_get_customer_total_spent( $contact->get_user_id() ); 40 * }, 41 * 'number' 42 * ); 43 * 44 * Call this once (e.g. on `init`, after confirming both Groundhogg and the 45 * dependency it needs are active) - before any ability builds its input/output 46 * schema. See Extensible_Schema's own docblock for what extend() can't express 47 * and the three underlying filters (`groundhogg/contact_schema/properties`, 48 * `groundhogg/contact_schema/transform`, `groundhogg/contact_schema/expand_options`) 49 * available for those cases. 26 50 */ 27 51 class Contact_Schema extends Schema { 28 52 53 use Extensible_Schema; 54 55 protected static function extension_hook_prefix(): string { 56 return 'contact_schema'; 57 } 58 59 protected static function builtin_property_keys(): array { 60 return [ 'id', 'email', 'first_name', 'last_name', 'date_created', 'optin_status', 'tags', 'meta' ]; 61 } 62 29 63 public static function get_schema(): array { 64 65 $properties = [ 66 'id' => [ 67 'type' => 'integer', 68 ], 69 'email' => [ 70 'type' => 'string', 71 'format' => 'email', 72 ], 73 'first_name' => [ 74 'type' => 'string', 75 ], 76 'last_name' => [ 77 'type' => 'string', 78 ], 79 'date_created' => [ 80 'type' => 'string', 81 'format' => 'date-time', 82 'description' => __( 'When the contact was created.', 'groundhogg' ), 83 ], 84 'optin_status' => [ 85 'type' => 'object', 86 'description' => __( 'Whether/how the contact can be marketed to.', 'groundhogg' ), 87 'properties' => [ 88 'value' => [ 89 'type' => 'integer', 90 'description' => __( 'The raw Groundhogg\Preferences status constant.', 'groundhogg' ), 91 ], 92 'label' => [ 93 'type' => 'string', 94 ], 95 ], 96 ], 97 'tags' => [ 98 'type' => 'array', 99 'description' => __( 'Only present when "tags" is passed in include.', 'groundhogg' ), 100 'items' => Tag_Schema::get_schema(), 101 ], 102 'meta' => [ 103 'type' => 'object', 104 'description' => __( 'Only present when "meta" is passed in include. Raw meta key => value pairs, including custom fields. See groundhogg/list-custom-fields to map keys to labels.', 'groundhogg' ), 105 'additionalProperties' => true, 106 ], 107 ]; 30 108 31 109 return [ 32 110 'type' => 'object', 33 'properties' => [ 34 'id' => [ 35 'type' => 'integer', 36 ], 37 'email' => [ 38 'type' => 'string', 39 'format' => 'email', 40 ], 41 'first_name' => [ 42 'type' => 'string', 43 ], 44 'last_name' => [ 45 'type' => 'string', 46 ], 47 'date_created' => [ 48 'type' => 'string', 49 'format' => 'date-time', 50 'description' => __( 'When the contact was created.', 'groundhogg' ), 51 ], 52 'optin_status' => [ 53 'type' => 'object', 54 'description' => __( 'Whether/how the contact can be marketed to.', 'groundhogg' ), 55 'properties' => [ 56 'value' => [ 57 'type' => 'integer', 58 'description' => __( 'The raw Groundhogg\Preferences status constant.', 'groundhogg' ), 59 ], 60 'label' => [ 61 'type' => 'string', 62 ], 63 ], 64 ], 65 'tags' => [ 66 'type' => 'array', 67 'description' => __( 'Only present when "tags" is passed in include.', 'groundhogg' ), 68 'items' => Tag_Schema::get_schema(), 69 ], 70 'meta' => [ 71 'type' => 'object', 72 'description' => __( 'Only present when "meta" is passed in include. Raw meta key => value pairs, including custom fields. See groundhogg/list-custom-fields to map keys to labels.', 'groundhogg' ), 73 'additionalProperties' => true, 74 ], 75 ], 111 'properties' => self::extend_properties( $properties ), 76 112 ]; 113 } 114 115 /** 116 * The valid values for groundhogg/get-contact's and groundhogg/search-contacts' 117 * own `expand` input parameter (their `enum`, not just this class's $include). 118 * extend()'d keys are included automatically - see the class docblock for 119 * the escape hatch when an add-on isn't going through extend(). 120 * 121 * @return string[] 122 */ 123 public static function expand_options(): array { 124 return self::extend_expand_options( [ 'tags', 'meta' ] ); 77 125 } 78 126 … … 82 130 * 83 131 * @param Contact|int|string|object $object 84 * @param array $include Optional sections to include: 'tags', 'meta'. 132 * @param array $include Optional sections to include: 'tags', 'meta', 133 * plus anything an add-on has registered via 134 * Contact_Schema::extend() (see the class docblock). 85 135 * 86 136 * @return array … … 124 174 } 125 175 126 return $data;176 return self::extend_transform( $data, $object, $include ); 127 177 } 128 178 -
groundhogg/tags/4.8.1/includes/abilities/schemas/email-schema.php
r3695315 r3695885 55 55 'type' => 'string', 56 56 'enum' => [ 'html', 'blocks', 'legacy_blocks', 'legacy_plain' ], 57 'description' => __( 'Which editor this email\'s content was built with, and which one wp-admin will open it in. "html": raw HTML (from the meta "type" flag, or content starting with "<!DOCTYPE"). "blocks": the current block editor (from the meta "blocks" flag) - content is HTML annotated with block comments ("<!--text:uuid {...}-->...<!--/text:uuid-->", etc.) that get parsed back into editable blocks. "legacy_blocks"/"legacy_plain": older formats inferred from the content itself. groundhogg/create-email \'s `editor` input controls which of "html"/"blocks" it produces.', 'groundhogg' ),57 'description' => __( 'Which editor this email\'s content was built with, and which one wp-admin will open it in. "html": raw HTML (from the meta "type" flag, or content starting with "<!DOCTYPE"). "blocks": the current block editor (from the meta "blocks" flag) - content is HTML annotated with block comments ("<!--text:uuid {...}-->...<!--/text:uuid-->", etc.) that get parsed back into editable blocks. "legacy_blocks"/"legacy_plain": older formats inferred from the content itself. groundhogg/create-email-template\'s `editor` input controls which of "html"/"blocks" it produces.', 'groundhogg' ), 58 58 ], 59 59 'template_settings' => [ 60 60 'type' => 'object', 61 'description' => __( 'The outer page layout/background this email renders inside of - distinct from the block/HTML content itself. Mirrors the "Template Settings" panel in wp-admin\'s email editor. Set via groundhogg/create-email \'s or groundhogg/update-email\'s `template_settings` input.', 'groundhogg' ),61 'description' => __( 'The outer page layout/background this email renders inside of - distinct from the block/HTML content itself. Mirrors the "Template Settings" panel in wp-admin\'s email editor. Set via groundhogg/create-email-template\'s or groundhogg/update-email-template\'s `template_settings` input.', 'groundhogg' ), 62 62 'properties' => self::template_settings_properties(), 63 63 ], … … 100 100 * The `template_settings` object's properties - shared by the output 101 101 * schema above and the `template_settings` input on 102 * groundhogg/create-email and groundhogg/update-email, so the property 103 * names, enums, and descriptions can't drift out of sync between them. 102 * groundhogg/create-email-template and groundhogg/update-email-template, so 103 * the property names, enums, and descriptions can't drift out of sync 104 * between them. 104 105 * 105 106 * @param bool $with_defaults Include `default` values for "layout" and 106 * "direction" - appropriate for create-email ,107 * "direction" - appropriate for create-email-template, 107 108 * where a brand-new email needs a sensible 108 109 * starting point. Leave false for the output 109 * schema and for update-email , where an absent110 * property should be left untouched rather110 * schema and for update-email-template, where an 111 * absent property should be left untouched rather 111 112 * than implying it would reset to a default. 112 113 * -
groundhogg/tags/4.8.1/includes/abilities/schemas/segment-schema.php
r3695315 r3695885 4 4 5 5 use Groundhogg\Abilities\Traits\Has_Optin_Status; 6 use Groundhogg\Contact_Query; 6 7 use WP_Error; 7 8 use function Groundhogg\parse_tag_list; … … 25 26 * describes *input*, not an output shape, so it has no get_schema()/transform(). 26 27 * 27 * // in get_args(): 28 * 'properties' => array_merge( Segment_Schema::properties(), [ ...ability-specific... ] ), 29 * 30 * // in __invoke(): 28 * Three ways to turn a validated $input into something queryable, depending on 29 * what the calling ability actually does with it: 30 * 31 * // Executes immediately (groundhogg/search-contacts): a live Contact_Query, 32 * // so the caller can keep chaining (setLimit(), setOrderby(), ...) and an 33 * // add-on can manipulate the query object directly - see 34 * // groundhogg/segment_schema/contact_query below. 35 * $contact_query = Segment_Schema::to_contact_query( $input ); 36 * if ( is_wp_error( $contact_query ) ) { return $contact_query; } 37 * $results = $contact_query->setLimit( $limit )->setFoundRows( true )->query(); 38 * 39 * // Stored and (re-)run later (groundhogg/send-email-broadcast's dynamic 40 * // segments, groundhogg/add-to-flow's background batches): a plain, 41 * // serializable array of query vars - a live query object (and whatever a 42 * // filter attached to it via addJoin()/where()) can't survive that round 43 * // trip. 31 44 * $query = Segment_Schema::to_query( $input ); 32 45 * if ( is_wp_error( $query ) ) { return $query; } 33 * // ... hand $query to Contact_Query / Broadcast::schedule() / ... 46 * // ... hand $query to Broadcast::schedule() / Background_Tasks / ... 47 * 48 * // A branch/logic step's own condition setting (e.g. if_else's 49 * // `include_filters`) - Groundhogg's Filters DSL directly, not a 50 * // Contact_Query-vars array at all. 51 * $filters = Segment_Schema::to_filters( $input ); 52 * if ( is_wp_error( $filters ) ) { return $filters; } 53 * // ... use as an if_else/branch-logic step's include_filters setting 54 * 55 * // in get_args(), for all three: 56 * 'properties' => array_merge( Segment_Schema::properties(), [ ...ability-specific... ] ), 57 * 58 * Extensible, so an add-on can add its own audience-filtering params to every 59 * ability built on this schema (groundhogg/search-contacts, 60 * groundhogg/send-email-broadcast, groundhogg/add-to-flow, ...) without modifying 61 * this class or any of them individually. 62 * 63 * The easy way in is Segment_Schema::extend() - one call registers a new input 64 * property (its schema) and a callback that turns it into Groundhogg's Filters 65 * DSL (the same condition format Contact_Query's own `include_filters` query var 66 * and branch-logic step settings like if_else's `include_filters` consume - see 67 * `Groundhogg\DB\Query\Filters` and Contact_Query's `filter_*` methods for the 68 * full vocabulary of condition `type`s). Returning filter conditions rather than 69 * mutating a query-vars array directly means one extend() call works for BOTH 70 * to_query()/to_contact_query() (folded into `include_filters`) AND to_filters() 71 * (used for branch/logic conditions) - not two things to hook separately: 72 * 73 * Segment_Schema::extend( 74 * 'woocommerce', 75 * [ 76 * 'type' => 'object', 77 * 'additionalProperties' => false, 78 * 'properties' => [ 79 * 'min_orders' => [ 80 * 'type' => 'integer', 81 * 'description' => __( 'Only contacts with at least this many WooCommerce orders.', 'my-plugin' ), 82 * ], 83 * ], 84 * ], 85 * function ( array $input ): array { 86 * if ( empty( $input['woocommerce']['min_orders'] ) ) { 87 * return []; 88 * } 89 * // Requires a filter type actually registered with Contact_Query's 90 * // Filters instance (via groundhogg/contact_query/filters/register) - 91 * // extend() only carries conditions through, it doesn't invent new 92 * // filter types on its own. 93 * return [ [ 94 * 'type' => 'woocommerce_orders', 95 * 'compare' => 'greater_than_or_equal_to', 96 * 'value' => absint( $input['woocommerce']['min_orders'] ), 97 * ] ]; 98 * }, 99 * true // this alone counts as naming a deliberate audience - see has_audience() 100 * ); 101 * 102 * Call this once (e.g. on `init`, after confirming both Groundhogg and the 103 * dependency it needs are active) - before any ability builds its input schema. 104 * 105 * For query logic no Filters condition type can express at all (a join, a raw 106 * where condition, a custom select), reach for to_contact_query()'s live 107 * Contact_Query object instead, via the `groundhogg/segment_schema/contact_query` 108 * action below - only usable by abilities that execute the query themselves 109 * rather than storing it (see to_contact_query()'s own docblock). 110 * 111 * Everything extend() does is built on filters, also available directly for 112 * anything extend() can't express (e.g. contributing to more than one property 113 * from a single registration): 114 * 115 * - `groundhogg/segment_schema/properties` filters the input properties (properties(), 116 * below). 117 * - `groundhogg/segment_schema/query` filters the plain Contact_Query-vars array 118 * built from $input (to_query(), below) - runs after extend()'d conditions are 119 * already folded into `include_filters`. 120 * - `groundhogg/segment_schema/filters` filters the Filters-DSL array built from 121 * $input (to_filters(), below) - same timing, after extend()'d conditions are 122 * already merged in. 123 * - `groundhogg/segment_schema/audience_keys` adds to the keys has_audience() treats 124 * as deliberately naming a group of contacts, so an ability that must not act on 125 * the whole database by accident still recognizes a request that only supplies 126 * an add-on's own param as targeted, not "everyone". 34 127 */ 35 128 class Segment_Schema { … … 57 150 58 151 /** 59 * The input_schema `properties` for a contact segment. Every condition is 60 * ANDed together. Spread these into an ability's own `properties`. 152 * Add-on-registered input properties, keyed by property name. Populated by 153 * extend(). 154 * 155 * @var array<string, array{schema: array, callback: callable, audience: bool}> 156 */ 157 private static array $extensions = []; 158 159 /** 160 * Register an additional audience-filtering input property - see the class 161 * docblock for the full picture. One call adds the property to the input 162 * schema (properties(), below) and wires the callback that turns it into 163 * Filters-DSL conditions, merged into `include_filters` for to_query()/ 164 * to_contact_query() and into the result of to_filters() alike - instead of 165 * hooking groundhogg/segment_schema/properties and (one or both of) 166 * groundhogg/segment_schema/query / groundhogg/segment_schema/filters by hand. 167 * 168 * @param string $key The property name, e.g. "woocommerce". Must 169 * not collide with a built-in name or an 170 * existing extend()'d one - either is refused 171 * with _doing_it_wrong() rather than silently 172 * overwriting something else already relying 173 * on that key. 174 * @param array $schema The full JSON schema fragment for this 175 * property (type, description, enum, 176 * properties, items, ...), used as-is. Unlike 177 * Contact_Schema::extend()'s simpler 178 * description+type shorthand, a segment 179 * filter's shape varies too much (an object 180 * with its own sub-properties, an enum, a 181 * plain scalar, ...) to usefully paper over. 182 * @param callable $callback function( array $input ): array|WP_Error. 183 * Receives the full $input and returns a flat 184 * list of Filters-DSL condition arrays (each 185 * `['type' => ..., ...]`) contributed by this 186 * property - empty array if $key isn't present 187 * in $input - or a WP_Error to reject the 188 * input outright, same as to_query()'s own 189 * tags_include handling. The `type` used must 190 * be a filter type actually registered with 191 * Contact_Query's Filters instance (see the 192 * class docblock) - extend() carries 193 * conditions through, it doesn't register new 194 * filter types itself. Called on every 195 * to_query()/to_contact_query()/to_filters(), 196 * regardless of whether $key is actually 197 * present - check $input[ $key ] yourself. 198 * @param bool $is_audience_key Whether supplying this property alone (with 199 * no other segment param) should count as 200 * deliberately naming a group of contacts - 201 * see has_audience(). Default false. 202 * 203 * @return void 204 */ 205 public static function extend( string $key, array $schema, callable $callback, bool $is_audience_key = false ) { 206 207 if ( array_key_exists( $key, self::base_properties() ) || isset( self::$extensions[ $key ] ) ) { 208 _doing_it_wrong( 209 __METHOD__, 210 sprintf( 'A Segment_Schema property named "%s" already exists.', $key ), 211 '4.8' 212 ); 213 214 return; 215 } 216 217 self::$extensions[ $key ] = [ 218 'schema' => $schema, 219 'callback' => $callback, 220 'audience' => $is_audience_key, 221 ]; 222 } 223 224 /** 225 * This schema's own built-in input properties, before add-on extensions or 226 * the groundhogg/segment_schema/properties filter. Split out from 227 * properties() so extend() can check a proposed key against it without 228 * re-running the filter (and without properties() needing to special-case 229 * its own extensions when checking for collisions). 61 230 * 62 231 * @return array 63 232 */ 64 public static function properties(): array { 233 private static function base_properties(): array { 234 65 235 return [ 66 236 'search' => [ … … 179 349 180 350 /** 351 * The input_schema `properties` for a contact segment. Every condition is 352 * ANDed together. Spread these into an ability's own `properties`. 353 * 354 * @return array 355 */ 356 public static function properties(): array { 357 358 $properties = self::base_properties(); 359 360 foreach ( self::$extensions as $key => $extension ) { 361 $properties[ $key ] = $extension['schema']; 362 } 363 364 // See the class docblock for how an add-on adds its own properties here. 365 return apply_filters( 'groundhogg/segment_schema/properties', $properties ); 366 } 367 368 /** 181 369 * Whether $input names a deliberate group of contacts (vs. "everyone" or just 182 370 * refinements). An ability that must not act on the whole database by … … 189 377 public static function has_audience( array $input ): bool { 190 378 191 foreach ( self::AUDIENCE_KEYS as $key ) { 379 $audience_keys = self::AUDIENCE_KEYS; 380 381 foreach ( self::$extensions as $key => $extension ) { 382 if ( $extension['audience'] ) { 383 $audience_keys[] = $key; 384 } 385 } 386 387 // See the class docblock for how an add-on adds its own key here. 388 $audience_keys = apply_filters( 'groundhogg/segment_schema/audience_keys', $audience_keys ); 389 390 foreach ( $audience_keys as $key ) { 192 391 if ( ! empty( $input[ $key ] ) ) { 193 392 return true; … … 349 548 } 350 549 351 return $query; 550 $extension_conditions = self::extension_filter_conditions( $input ); 551 552 if ( is_wp_error( $extension_conditions ) ) { 553 return $extension_conditions; 554 } 555 556 if ( ! empty( $extension_conditions ) ) { 557 // Merged into the same single AND-group `include_filters` already uses 558 // elsewhere in this method (see to_filters() for the built-in fields' 559 // equivalent) - Contact_Query natively understands `include_filters` 560 // alongside the native query vars above (db.php's `case 'include_filters'`), 561 // so this doesn't conflict with anything already in $query. 562 $query['include_filters'][0] = array_merge( 563 $query['include_filters'][0] ?? [], 564 $extension_conditions 565 ); 566 } 567 568 // See the class docblock for how an add-on translates its own input 569 // properties into query vars here. Only reached on success - the early 570 // WP_Error returns above (e.g. unknown tags, an extend()'d callback 571 // rejecting its own input) bypass this filter, same as any other hard 572 // validation failure. 573 return apply_filters( 'groundhogg/segment_schema/query', $query, $input ); 574 } 575 576 /** 577 * Run every extend()'d property's callback against $input and collect the 578 * Filters-DSL conditions they contribute, for merging into either 579 * to_query()'s `include_filters` or to_filters()'s result. 580 * 581 * @param array $input 582 * 583 * @return array|WP_Error A flat list of condition arrays (possibly empty), or 584 * the first WP_Error a callback returns. 585 */ 586 private static function extension_filter_conditions( array $input ) { 587 588 $conditions = []; 589 590 foreach ( self::$extensions as $key => $extension ) { 591 592 $result = call_user_func( $extension['callback'], $input ); 593 594 if ( is_wp_error( $result ) ) { 595 return $result; 596 } 597 598 if ( ! empty( $result ) ) { 599 array_push( $conditions, ...array_values( $result ) ); 600 } 601 } 602 603 return $conditions; 604 } 605 606 /** 607 * Maps a `meta` condition's `compare` (this class's own vocabulary, matching 608 * Where::compare() - see META_COMPARISONS) to the equivalent 609 * Groundhogg\DB\Query\Filters::string() compare keyword, for to_filters(). 610 * The two vocabularies grew independently and mostly agree, except: 611 * "in"/"not_in" here must become "any_of"/"none_of" for Filters::string() - 612 * passed through unmapped, Filters::string() silently compares against only 613 * the first array value instead of the whole list, since it only special-cases 614 * exactly "any_of"/"none_of" before falling back to a scalar comparison. 615 * "like"/"not_like" have no exact Filters::string() equivalent and are mapped 616 * to the closest substring match ("contains"/"not_contains"). 617 */ 618 private const META_COMPARE_TO_FILTER_COMPARE = [ 619 'equals' => 'equals', 620 'not_equals' => 'not_equals', 621 'less_than' => 'less_than', 622 'greater_than' => 'greater_than', 623 'less_than_or_equal_to' => 'less_than_or_equal_to', 624 'greater_than_or_equal_to' => 'greater_than_or_equal_to', 625 'in' => 'any_of', 626 'not_in' => 'none_of', 627 'like' => 'contains', 628 'not_like' => 'not_contains', 629 'contains' => 'contains', 630 'not_contains' => 'not_contains', 631 'starts_with' => 'starts_with', 632 'ends_with' => 'ends_with', 633 'empty' => 'empty', 634 'not_empty' => 'not_empty', 635 ]; 636 637 /** 638 * Build Groundhogg's Filters DSL (Groundhogg\DB\Query\Filters::sanitize()'s 639 * expected shape - an OR-of-AND condition list, e.g. 640 * `[[ {type:'tags', ...}, {type:'owner', ...} ]]`) from a validated segment 641 * $input. This is the format branch/logic step settings consume directly 642 * (e.g. if_else's `include_filters`/`exclude_filters`) - unlike to_query()/ 643 * to_contact_query(), the result here is NOT a Contact_Query-vars array, and 644 * running it requires a Contact_Query (or a step type) that understands the 645 * Filters DSL, not a raw query. 646 * 647 * Every translated condition lands in a single AND-group (`[[ ...all of 648 * them... ]]`), matching Segment_Schema's own "every condition ANDed 649 * together" semantics - to_query()'s notion of ANDing everything, expressed 650 * here as one AND-group with no OR alternatives. 651 * 652 * `meta_relation: OR` is expressed via the `sub_query` filter type 653 * (Contact_Query::filter_sub_query()), which takes its own nested 654 * `include_filters` (a full OR-of-AND array) and applies it inside a 655 * sub-Where group - exactly the primitive needed to nest an OR *inside* the 656 * single outer AND-group everything else lands in, without restructuring the 657 * whole condition list or duplicating every other condition into each OR 658 * branch. See the `meta` handling below. 659 * 660 * One known gap: `search` has no Filters equivalent (Contact_Query's own 661 * `search` handling is a query-var-level heuristic - email vs. full name - 662 * not a Filters condition) and is silently ignored here. Use 663 * to_query()/to_contact_query() if `search` needs to actually apply. 664 * 665 * @param array $input 666 * 667 * @return array|WP_Error 668 */ 669 public static function to_filters( array $input ) { 670 671 $conditions = []; 672 673 if ( ! empty( $input['include'] ) ) { 674 $conditions[] = [ 675 'type' => 'contact_id', 676 'compare' => 'any_of', 677 'value' => wp_parse_id_list( $input['include'] ), 678 ]; 679 } 680 681 if ( ! empty( $input['exclude'] ) ) { 682 $conditions[] = [ 683 'type' => 'contact_id', 684 'compare' => 'none_of', 685 'value' => wp_parse_id_list( $input['exclude'] ), 686 ]; 687 } 688 689 if ( ! empty( $input['tags_include'] ) ) { 690 691 // $create = false: resolving an audience must never mint a tag. 692 $resolved = parse_tag_list( $input['tags_include'], 'ID', false ); 693 694 if ( empty( $resolved ) ) { 695 return new WP_Error( 696 'groundhogg_unknown_tags', 697 __( 'None of the tags in tags_include match an existing tag.', 'groundhogg' ) 698 ); 699 } 700 701 $conditions[] = [ 702 'type' => 'tags', 703 'tags' => $resolved, 704 'compare' => 'includes', 705 'compare2' => ! empty( $input['tags_include_needs_all'] ) ? 'all' : 'any', 706 ]; 707 } 708 709 if ( ! empty( $input['tags_exclude'] ) ) { 710 711 $resolved = parse_tag_list( $input['tags_exclude'], 'ID', false ); 712 713 if ( ! empty( $resolved ) ) { 714 $conditions[] = [ 715 'type' => 'tags', 716 'tags' => $resolved, 717 'compare' => 'excludes', 718 'compare2' => ! empty( $input['tags_exclude_needs_all'] ) ? 'all' : 'any', 719 ]; 720 } 721 } 722 723 if ( ! empty( $input['optin_status'] ) && is_array( $input['optin_status'] ) ) { 724 $conditions[] = [ 725 'type' => 'optin_status', 726 'value' => self::resolve_optin_status_list( $input['optin_status'] ), 727 'compare' => 'in', 728 ]; 729 } 730 731 if ( ! empty( $input['optin_status_exclude'] ) && is_array( $input['optin_status_exclude'] ) ) { 732 $conditions[] = [ 733 'type' => 'optin_status', 734 'value' => self::resolve_optin_status_list( $input['optin_status_exclude'] ), 735 'compare' => 'not_in', 736 ]; 737 } 738 739 if ( isset( $input['marketable'] ) ) { 740 $conditions[] = [ 741 'type' => 'is_marketable', 742 'marketable' => $input['marketable'] ? 'yes' : 'no', 743 ]; 744 } 745 746 if ( ! empty( $input['owner'] ) ) { 747 $conditions[] = [ 748 'type' => 'owner', 749 'value' => wp_parse_id_list( $input['owner'] ), 750 'compare' => 'in', 751 ]; 752 } 753 754 if ( ! empty( $input['users_include'] ) ) { 755 $conditions[] = [ 756 'type' => 'user_id', 757 'value' => wp_parse_id_list( $input['users_include'] ), 758 'compare' => 'any_of', 759 ]; 760 } 761 762 if ( ! empty( $input['users_exclude'] ) ) { 763 $conditions[] = [ 764 'type' => 'user_id', 765 'value' => wp_parse_id_list( $input['users_exclude'] ), 766 'compare' => 'none_of', 767 ]; 768 } 769 770 if ( isset( $input['has_user'] ) ) { 771 if ( $input['has_user'] ) { 772 $conditions[] = [ 'type' => 'is_user' ]; 773 } else { 774 // No negated "is_user" filter type exists - approximate "no linked 775 // user" as user_id being exactly the unset value. 776 $conditions[] = [ 'type' => 'user_id', 'compare' => 'equals', 'value' => 0 ]; 777 } 778 } 779 780 if ( ! empty( $input['created_after'] ) && ! empty( $input['created_before'] ) ) { 781 $conditions[] = [ 782 'type' => 'date_created', 783 'date_range' => 'between', 784 'after' => sanitize_text_field( $input['created_after'] ), 785 'before' => sanitize_text_field( $input['created_before'] ), 786 ]; 787 } else if ( ! empty( $input['created_after'] ) ) { 788 $conditions[] = [ 789 'type' => 'date_created', 790 'date_range' => 'after', 791 'after' => sanitize_text_field( $input['created_after'] ), 792 ]; 793 } else if ( ! empty( $input['created_before'] ) ) { 794 $conditions[] = [ 795 'type' => 'date_created', 796 'date_range' => 'before', 797 'before' => sanitize_text_field( $input['created_before'] ), 798 ]; 799 } 800 801 if ( ! empty( $input['saved_search'] ) ) { 802 $conditions[] = [ 803 'type' => 'saved_search', 804 'search' => sanitize_text_field( $input['saved_search'] ), 805 'compare' => 'in', 806 ]; 807 } 808 809 if ( ! empty( $input['meta'] ) && is_array( $input['meta'] ) ) { 810 811 $meta_conditions = []; 812 813 foreach ( $input['meta'] as $condition ) { 814 815 $condition = (array) $condition; 816 $key = isset( $condition['key'] ) ? sanitize_key( $condition['key'] ) : ''; 817 818 if ( ! $key ) { 819 continue; 820 } 821 822 $compare = in_array( $condition['compare'] ?? 'equals', self::META_COMPARISONS, true ) 823 ? $condition['compare'] 824 : 'equals'; 825 826 $meta_conditions[] = [ 827 'type' => 'meta', 828 'meta' => $key, 829 'compare' => self::META_COMPARE_TO_FILTER_COMPARE[ $compare ] ?? 'equals', 830 'value' => $condition['value'] ?? '', 831 ]; 832 } 833 834 $is_or = ! empty( $input['meta_relation'] ) && strtoupper( $input['meta_relation'] ) === 'OR'; 835 836 if ( $is_or && count( $meta_conditions ) > 1 ) { 837 // Nest as one OR-of-AND sub-group inside the outer AND-group, via 838 // the sub_query filter type - see the class docblock for why this 839 // is the only way to express an OR here without restructuring 840 // everything else. Each meta condition becomes its own single-item 841 // AND-group, ORed together. 842 $conditions[] = [ 843 'type' => 'sub_query', 844 'include_filters' => array_map( static fn( $condition ) => [ $condition ], $meta_conditions ), 845 ]; 846 } else { 847 // Either explicitly ANDed, or only one condition (where AND/OR are 848 // equivalent) - no need for the sub_query wrapper, just AND them 849 // into the outer group directly like everything else. 850 array_push( $conditions, ...$meta_conditions ); 851 } 852 } 853 854 $extension_conditions = self::extension_filter_conditions( $input ); 855 856 if ( is_wp_error( $extension_conditions ) ) { 857 return $extension_conditions; 858 } 859 860 array_push( $conditions, ...$extension_conditions ); 861 862 // See the class docblock for how an add-on adds its own conditions here. 863 return apply_filters( 'groundhogg/segment_schema/filters', empty( $conditions ) ? [] : [ $conditions ], $input ); 864 } 865 866 /** 867 * Build a live Contact_Query from a validated segment $input - same 868 * conditions as to_query(), but as a query object the caller can keep 869 * building on (setLimit(), setOrderby(), ...) and an add-on can manipulate 870 * directly via `groundhogg/segment_schema/contact_query` for anything a 871 * plain query-var array can't express (a join, a raw where condition). 872 * See the class docblock for when to reach for this vs. to_query(). 873 * 874 * Does not run the query - the caller decides when (query(), get_results(), 875 * count(), ...) and with what pagination/ordering on top. 876 * 877 * @param array $input 878 * @param int $flags Passed through to Contact_Query's constructor (e.g. 879 * Contact_Query::IS_SUB_QUERY). 880 * 881 * @return Contact_Query|WP_Error 882 */ 883 public static function to_contact_query( array $input, int $flags = 0 ) { 884 885 $query_vars = self::to_query( $input ); 886 887 if ( is_wp_error( $query_vars ) ) { 888 return $query_vars; 889 } 890 891 $contact_query = new Contact_Query( $query_vars, $flags ); 892 893 /** 894 * Fires with the live Contact_Query built from a segment $input, so an 895 * add-on can manipulate it directly - see the class docblock's 896 * WooCommerce example. The query hasn't run yet, so anything applied 897 * here (addJoin(), where(), setSelect(), ...) takes effect normally. 898 * 899 * @param Contact_Query $contact_query 900 * @param array $input 901 */ 902 do_action( 'groundhogg/segment_schema/contact_query', $contact_query, $input ); 903 904 return $contact_query; 352 905 } 353 906 -
groundhogg/tags/4.8.1/includes/abilities/tags/list-tags.php
r3695315 r3695885 14 14 class List_Tags extends Ability { 15 15 16 protected const stringNAME = 'groundhogg/list-tags';17 protected const stringCATEGORY = 'groundhogg-tags';18 protected const stringCAPABILITY = 'manage_tags';16 protected const NAME = 'groundhogg/list-tags'; 17 protected const CATEGORY = 'groundhogg-tags'; 18 protected const CAPABILITY = 'manage_tags'; 19 19 20 protected const boolREADONLY = true;21 protected const boolIDEMPOTENT = true;20 protected const READONLY = true; 21 protected const IDEMPOTENT = true; 22 22 23 23 protected function get_args(): array { -
groundhogg/tags/4.8.1/includes/abilities/utils/upload-media.php
r3695315 r3695885 9 9 * Uploads a file into the WordPress Media Library and returns its ID, URL, 10 10 * and relative path - the missing piece for building an email (via 11 * groundhogg/create-email or groundhogg/update-email) that includes a real12 * i mage rather than a placeholder: upload the image first, then use the13 * returned `url` as an `image` block's `src` (see the groundhogg-block-email14 * skill) or a plain `<img>` src in HTML content.11 * groundhogg/create-email-template or groundhogg/update-email-template) that 12 * includes a real image rather than a placeholder: upload the image first, 13 * then use the returned `url` as an `image` block's `src` (see the 14 * groundhogg-block-email skill) or a plain `<img>` src in HTML content. 15 15 * 16 16 * `data` is base64, not a raw file upload - abilities are called with a JSON … … 37 37 class Upload_Media extends Ability { 38 38 39 protected const stringNAME = 'groundhogg/upload-media';40 protected const stringCATEGORY = 'groundhogg-utils';41 protected const stringCAPABILITY = 'upload_files';39 protected const NAME = 'groundhogg/upload-media'; 40 protected const CATEGORY = 'groundhogg-utils'; 41 protected const CAPABILITY = 'upload_files'; 42 42 43 protected const boolREADONLY = false;44 protected const boolDESTRUCTIVE = false;45 protected const boolIDEMPOTENT = false;43 protected const READONLY = false; 44 protected const DESTRUCTIVE = false; 45 protected const IDEMPOTENT = false; 46 46 47 47 protected function get_args(): array { … … 49 49 return [ 50 50 'label' => __( 'Upload Media', 'groundhogg' ), 51 'description' => __( 'Upload a file (e.g. an image) into the WordPress Media Library from base64 data. Returns the attachment\'s id, URL, and relative path, for use as an image src elsewhere - e.g. an image block in groundhogg/create-email \'s content.', 'groundhogg' ),51 'description' => __( 'Upload a file (e.g. an image) into the WordPress Media Library from base64 data. Returns the attachment\'s id, URL, and relative path, for use as an image src elsewhere - e.g. an image block in groundhogg/create-email-template\'s content.', 'groundhogg' ), 52 52 53 53 'input_schema' => [ -
groundhogg/trunk/README.txt
r3695315 r3695885 7 7 Tested up to: 7.1 8 8 Requires PHP: 7.4 9 Stable tag: 4.8 9 Stable tag: 4.8.1 10 10 License: GPLv3 11 11 License URI: https://www.gnu.org/licenses/gpl.md … … 391 391 392 392 == Changelog == 393 394 = 4.8.1 (2026-09-14) = 395 * ADDED "Create Flow" and "Activate Flow" abilities, so an AI agent can build a flow (funnel) from scratch, including branching steps, and turn it on. A new "List Step Types" ability lists the buildable step types and their settings so an agent can discover what's available before building. 396 * ADDED "List Replacement Codes" ability for discovering the merge tags available for use in emails. 397 * ADDED Add-ons can now register their own ability categories and abilities alongside Groundhogg's, and extend the contact and segment schemas with their own fields, so the WordPress Abilities API integration introduced in 4.8 isn't limited to what Groundhogg itself provides. 398 * TWEAKED Email-related abilities are now named consistently around "email template" (`create-email-template`, `update-email-template`, `get-email-template`) instead of a mix of "email" and "email-template". 399 * TWEAKED Ability descriptions now link out to relevant documentation for easier reference when working with an agent. 393 400 394 401 = 4.8 (2026-09-10) = -
groundhogg/trunk/groundhogg.php
r3695315 r3695885 4 4 * Plugin URI: https://groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash 5 5 * Description: CRM and marketing automation for WordPress 6 * Version: 4.8 6 * Version: 4.8.1 7 7 * Author: Groundhogg Inc. 8 8 * Author URI: https://groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash … … 27 27 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly 28 28 29 define( 'GROUNDHOGG_VERSION', '4.8 ' );30 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4. 7.2' );29 define( 'GROUNDHOGG_VERSION', '4.8.1' ); 30 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.8' ); 31 31 32 32 define( 'GROUNDHOGG__FILE__', __FILE__ ); -
groundhogg/trunk/includes/abilities/abilities.php
r3695315 r3695885 19 19 use Groundhogg\Abilities\Db\Describe_Table; 20 20 use Groundhogg\Abilities\Db\Query_Table; 21 use Groundhogg\Abilities\Funnels\Activate_Flow; 21 22 use Groundhogg\Abilities\Funnels\Add_To_Flow; 23 use Groundhogg\Abilities\Funnels\Create_Flow; 24 use Groundhogg\Abilities\Funnels\Deactivate_Flow; 22 25 use Groundhogg\Abilities\Funnels\List_Flows; 23 use Groundhogg\Abilities\Emails\Create_Email; 26 use Groundhogg\Abilities\Funnels\List_Step_Types; 27 use Groundhogg\Abilities\Emails\Create_Email_Template; 28 use Groundhogg\Abilities\Emails\Get_Email_Template; 24 29 use Groundhogg\Abilities\Emails\List_Email_Templates; 30 use Groundhogg\Abilities\Emails\List_Replacement_Codes; 25 31 use Groundhogg\Abilities\Emails\List_Sender_Profiles; 26 32 use Groundhogg\Abilities\Emails\Send_Composed_Email; 27 33 use Groundhogg\Abilities\Emails\Send_Email_Template; 28 use Groundhogg\Abilities\Emails\Update_Email ;34 use Groundhogg\Abilities\Emails\Update_Email_Template; 29 35 use Groundhogg\Abilities\Reports\Get_Reports; 30 36 use Groundhogg\Abilities\Reports\List_Report_Types; … … 32 38 use Groundhogg\Abilities\Utils\Upload_Media; 33 39 40 /** 41 * Registers Groundhogg's own abilities/categories, and acts as the registry add-ons use to 42 * register their own alongside them - Abilities::add_category() and Abilities::add_ability(). 43 * 44 * Both must be called before the WP Abilities API actually registers anything - the 45 * 'wp_abilities_api_categories_init'/'wp_abilities_api_init' hooks below, which normally fire on 46 * 'init'. Groundhogg's own bootstrap runs on 'plugins_loaded' at priority 0 (see Plugin::init()), 47 * so any add-on's own 'plugins_loaded' (default priority 10) or 'init' callback is early enough. 48 * Registering from that far out just queues the category/class; if a call ever comes in after the 49 * relevant hook has already fired, it's applied immediately instead of being silently dropped. 50 * 51 * Example - a 3rd-party add-on registering its own category and ability: 52 * 53 * add_action( 'plugins_loaded', function () { 54 * 55 * Abilities::add_category( 'my-addon', [ 56 * 'label' => __( 'My Add-on', 'my-addon' ), 57 * 'description' => __( 'Abilities provided by My Add-on.', 'my-addon' ), 58 * ] ); 59 * 60 * Abilities::add_ability( My_Addon\Abilities\Do_The_Thing::class ); 61 * } ); 62 * 63 * Do_The_Thing must extend Groundhogg\Abilities\Ability just like Groundhogg's own abilities - 64 * it's instantiated the same way, which is what actually calls wp_register_ability(). 65 */ 34 66 class Abilities { 67 68 /** 69 * Ability classes registered by add-ons via self::add_ability(), instantiated alongside 70 * Groundhogg's own in register_abilities(). 71 * 72 * @var string[] 73 */ 74 protected static array $extra_abilities = []; 75 76 /** 77 * Ability categories registered by add-ons via self::add_category(), registered alongside 78 * Groundhogg's own in register_categories(). 79 * 80 * @var array<string, array> 81 */ 82 protected static array $extra_categories = []; 35 83 36 84 public function __construct() { … … 51 99 } 52 100 101 /** 102 * Register an additional ability category, so a 3rd-party add-on's abilities can be grouped 103 * under their own category instead of one of Groundhogg's. See the class docblock for a full 104 * example and the timing requirement. 105 * 106 * @param string $slug the category slug, e.g. "my-addon" 107 * @param array $args ['label' => string, 'description' => string], same shape 108 * wp_register_ability_category() itself takes 109 * 110 * @return void 111 */ 112 public static function add_category( string $slug, array $args ) { 113 114 self::$extra_categories[ $slug ] = $args; 115 116 // The categories hook already fired - register it right away rather than dropping it. 117 if ( did_action( 'wp_abilities_api_categories_init' ) && function_exists( 'wp_register_ability_category' ) ) { 118 wp_register_ability_category( $slug, $args ); 119 } 120 } 121 122 /** 123 * Register an additional ability class, instantiated the same way Groundhogg's own abilities 124 * are - which is what actually calls wp_register_ability(). See the class docblock for a full 125 * example and the timing requirement. 126 * 127 * @param string $class fully-qualified class name, extending Groundhogg\Abilities\Ability 128 * 129 * @return void 130 */ 131 public static function add_ability( string $class ) { 132 133 self::$extra_abilities[] = $class; 134 135 // The abilities hook already fired - register it right away rather than dropping it. 136 if ( did_action( 'wp_abilities_api_init' ) ) { 137 new $class(); 138 } 139 } 140 53 141 public function register_categories() { 54 142 … … 97 185 'description' => __( 'General-purpose utilities that support the other categories but aren\'t specific to any one of them.', 'groundhogg' ), 98 186 ] ); 187 188 foreach ( self::$extra_categories as $slug => $args ) { 189 wp_register_ability_category( $slug, $args ); 190 } 99 191 } 100 192 101 193 public function register_abilities() { 102 194 103 foreach( [195 $abilities = array_merge( [ 104 196 Get_Contact::class, 105 197 Create_Contact::class, … … 114 206 List_Contact_Notes::class, 115 207 List_Email_Templates::class, 208 Get_Email_Template::class, 116 209 List_Sender_Profiles::class, 117 Create_Email::class, 118 Update_Email::class, 210 Create_Email_Template::class, 211 Update_Email_Template::class, 212 List_Replacement_Codes::class, 119 213 Send_Composed_Email::class, 120 214 Send_Email_Template::class, … … 124 218 Cancel_Broadcast::class, 125 219 List_Flows::class, 220 List_Step_Types::class, 221 Create_Flow::class, 222 Activate_Flow::class, 223 Deactivate_Flow::class, 126 224 Add_To_Flow::class, 127 225 List_Report_Types::class, … … 130 228 Query_Table::class, 131 229 Upload_Media::class, 132 ] as $ability ){ 133 134 $ability = new $ability(); 135 136 } 137 230 ], self::$extra_abilities ); 231 232 foreach ( $abilities as $ability ) { 233 new $ability(); 234 } 138 235 } 139 236 } -
groundhogg/trunk/includes/abilities/ability.php
r3695315 r3695885 5 5 abstract class Ability { 6 6 7 protected const stringNAME = '';8 protected const stringCATEGORY = '';9 protected const stringCAPABILITY = '';7 protected const NAME = ''; 8 protected const CATEGORY = ''; 9 protected const CAPABILITY = ''; 10 10 11 protected const boolPUBLIC = true;12 protected const boolREADONLY = false;13 protected const boolDESTRUCTIVE = false;14 protected const boolIDEMPOTENT = false;11 protected const PUBLIC = true; 12 protected const READONLY = false; 13 protected const DESTRUCTIVE = false; 14 protected const IDEMPOTENT = false; 15 15 16 16 /** -
groundhogg/trunk/includes/abilities/broadcasts/cancel-broadcast.php
r3695315 r3695885 23 23 class Cancel_Broadcast extends Ability { 24 24 25 protected const stringNAME = 'groundhogg/cancel-broadcast';26 protected const stringCATEGORY = 'groundhogg-broadcasts';27 protected const stringCAPABILITY = 'cancel_broadcasts';25 protected const NAME = 'groundhogg/cancel-broadcast'; 26 protected const CATEGORY = 'groundhogg-broadcasts'; 27 protected const CAPABILITY = 'cancel_broadcasts'; 28 28 29 protected const boolREADONLY = false;30 protected const boolDESTRUCTIVE = true;31 protected const boolIDEMPOTENT = true;29 protected const READONLY = false; 30 protected const DESTRUCTIVE = true; 31 protected const IDEMPOTENT = true; 32 32 33 33 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/broadcasts/get-broadcast.php
r3695315 r3695885 16 16 class Get_Broadcast extends Ability { 17 17 18 protected const stringNAME = 'groundhogg/get-broadcast';19 protected const stringCATEGORY = 'groundhogg-broadcasts';20 protected const stringCAPABILITY = 'view_broadcasts';18 protected const NAME = 'groundhogg/get-broadcast'; 19 protected const CATEGORY = 'groundhogg-broadcasts'; 20 protected const CAPABILITY = 'view_broadcasts'; 21 21 22 protected const boolREADONLY = true;23 protected const boolIDEMPOTENT = true;22 protected const READONLY = true; 23 protected const IDEMPOTENT = true; 24 24 25 25 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/broadcasts/list-broadcasts.php
r3695315 r3695885 37 37 class List_Broadcasts extends Ability { 38 38 39 protected const stringNAME = 'groundhogg/list-broadcasts';40 protected const stringCATEGORY = 'groundhogg-broadcasts';41 protected const stringCAPABILITY = 'view_broadcasts';42 43 protected const boolREADONLY = true;44 protected const boolIDEMPOTENT = true;39 protected const NAME = 'groundhogg/list-broadcasts'; 40 protected const CATEGORY = 'groundhogg-broadcasts'; 41 protected const CAPABILITY = 'view_broadcasts'; 42 43 protected const READONLY = true; 44 protected const IDEMPOTENT = true; 45 45 46 46 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/broadcasts/send-email-broadcast.php
r3695315 r3695885 41 41 class Send_Email_Broadcast extends Ability { 42 42 43 protected const stringNAME = 'groundhogg/send-email-broadcast';44 protected const stringCATEGORY = 'groundhogg-broadcasts';45 protected const stringCAPABILITY = 'schedule_broadcasts';46 47 protected const boolREADONLY = false;48 protected const boolDESTRUCTIVE = true;49 protected const boolIDEMPOTENT = false;43 protected const NAME = 'groundhogg/send-email-broadcast'; 44 protected const CATEGORY = 'groundhogg-broadcasts'; 45 protected const CAPABILITY = 'schedule_broadcasts'; 46 47 protected const READONLY = false; 48 protected const DESTRUCTIVE = true; 49 protected const IDEMPOTENT = false; 50 50 51 51 protected function get_args(): array { … … 195 195 } 196 196 197 // The plain array form, not to_contact_query()'s live query object - 198 // this gets stored (Broadcast::schedule()'s 'query' arg below) and, for 199 // a "dynamic" segment_type, re-run later against whatever then matches; 200 // a live query object (and anything a filter attached to it) can't 201 // survive that round trip. 197 202 $query = Segment_Schema::to_query( $input ); 198 203 -
groundhogg/trunk/includes/abilities/campaigns/list-campaigns.php
r3695315 r3695885 9 9 /** 10 10 * Lists the campaigns defined in Groundhogg (Contacts > Campaigns in the admin), so a caller 11 * can discover valid campaign ids/names to pass as create-email 's/update-email's campaigns param,12 * or to filter flows/broadcasts by campaign, without guessing.11 * can discover valid campaign ids/names to pass as create-email-template's/update-email-template's 12 * campaigns param, or to filter flows/broadcasts/emails by campaign, without guessing. 13 13 */ 14 14 class List_Campaigns extends Ability { 15 15 16 protected const stringNAME = 'groundhogg/list-campaigns';17 protected const stringCATEGORY = 'groundhogg-campaigns';18 protected const stringCAPABILITY = 'manage_campaigns';16 protected const NAME = 'groundhogg/list-campaigns'; 17 protected const CATEGORY = 'groundhogg-campaigns'; 18 protected const CAPABILITY = 'manage_campaigns'; 19 19 20 protected const boolREADONLY = true;21 protected const boolIDEMPOTENT = true;20 protected const READONLY = true; 21 protected const IDEMPOTENT = true; 22 22 23 23 protected function get_args(): array { … … 25 25 return [ 26 26 'label' => __( 'List Campaigns', 'groundhogg' ), 27 'description' => __( 'List the campaigns defined in Groundhogg. Use the returned id or name with groundhogg/create-email \'s or groundhogg/update-email\'s campaigns param.', 'groundhogg' ),27 'description' => __( 'List the campaigns defined in Groundhogg. Use the returned id or name with groundhogg/create-email-template\'s or groundhogg/update-email-template\'s campaigns param.', 'groundhogg' ), 28 28 29 29 'input_schema' => [ -
groundhogg/trunk/includes/abilities/contacts/add-contact-note.php
r3695315 r3695885 20 20 class Add_Contact_Note extends Ability { 21 21 22 protected const stringNAME = 'groundhogg/add-contact-note';23 protected const stringCATEGORY = 'groundhogg-contacts';24 protected const stringCAPABILITY = 'add_notes';22 protected const NAME = 'groundhogg/add-contact-note'; 23 protected const CATEGORY = 'groundhogg-contacts'; 24 protected const CAPABILITY = 'add_notes'; 25 25 26 protected const boolREADONLY = false;27 protected const boolDESTRUCTIVE = false;28 protected const boolIDEMPOTENT = false;26 protected const READONLY = false; 27 protected const DESTRUCTIVE = false; 28 protected const IDEMPOTENT = false; 29 29 30 30 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/contacts/create-contact.php
r3695315 r3695885 34 34 use Has_Owner_Validation; 35 35 36 protected const stringNAME = 'groundhogg/create-contact';37 protected const stringCATEGORY = 'groundhogg-contacts';38 protected const stringCAPABILITY = 'add_contacts';39 40 protected const boolREADONLY = false;41 protected const boolDESTRUCTIVE = false;42 protected const boolIDEMPOTENT = true;36 protected const NAME = 'groundhogg/create-contact'; 37 protected const CATEGORY = 'groundhogg-contacts'; 38 protected const CAPABILITY = 'add_contacts'; 39 40 protected const READONLY = false; 41 protected const DESTRUCTIVE = false; 42 protected const IDEMPOTENT = true; 43 43 44 44 protected function get_args(): array { … … 88 88 'items' => [ 89 89 'type' => 'string', 90 'enum' => [ 'tags', 'meta' ],90 'enum' => Contact_Schema::expand_options(), 91 91 ], 92 92 'default' => [ 'tags' ], 93 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta .', 'groundhogg' ),93 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta, plus any sections an installed add-on has registered.', 'groundhogg' ), 94 94 ], 95 95 ], -
groundhogg/trunk/includes/abilities/contacts/get-contact.php
r3695315 r3695885 10 10 class Get_Contact extends Ability { 11 11 12 protected const stringNAME = 'groundhogg/get-contact';13 protected const stringCATEGORY = 'groundhogg-contacts';14 protected const stringCAPABILITY = 'view_contacts';12 protected const NAME = 'groundhogg/get-contact'; 13 protected const CATEGORY = 'groundhogg-contacts'; 14 protected const CAPABILITY = 'view_contacts'; 15 15 16 protected const boolREADONLY = true;17 protected const boolIDEMPOTENT = true;16 protected const READONLY = true; 17 protected const IDEMPOTENT = true; 18 18 19 19 protected function get_args(): array { … … 46 46 'items' => [ 47 47 'type' => 'string', 48 'enum' => [ 'tags', 'meta' ],48 'enum' => Contact_Schema::expand_options(), 49 49 ], 50 50 'default' => [ 'tags' ], 51 'description' => __( 'Optional extra sections to expand beyond the standard contact fields. Available: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them) .', 'groundhogg' ),51 'description' => __( 'Optional extra sections to expand beyond the standard contact fields. Available: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them), plus any sections an installed add-on has registered.', 'groundhogg' ), 52 52 ], 53 53 ], -
groundhogg/trunk/includes/abilities/contacts/list-contact-notes.php
r3695315 r3695885 26 26 class List_Contact_Notes extends Ability { 27 27 28 protected const stringNAME = 'groundhogg/list-contact-notes';29 protected const stringCATEGORY = 'groundhogg-contacts';30 protected const stringCAPABILITY = 'view_notes';28 protected const NAME = 'groundhogg/list-contact-notes'; 29 protected const CATEGORY = 'groundhogg-contacts'; 30 protected const CAPABILITY = 'view_notes'; 31 31 32 protected const boolREADONLY = true;33 protected const boolIDEMPOTENT = true;32 protected const READONLY = true; 33 protected const IDEMPOTENT = true; 34 34 35 35 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/contacts/list-custom-fields.php
r3695315 r3695885 15 15 class List_Custom_Fields extends Ability { 16 16 17 protected const stringNAME = 'groundhogg/list-custom-fields';18 protected const stringCATEGORY = 'groundhogg-contacts';19 protected const stringCAPABILITY = 'edit_contacts';17 protected const NAME = 'groundhogg/list-custom-fields'; 18 protected const CATEGORY = 'groundhogg-contacts'; 19 protected const CAPABILITY = 'edit_contacts'; 20 20 21 protected const boolREADONLY = true;22 protected const boolIDEMPOTENT = true;21 protected const READONLY = true; 22 protected const IDEMPOTENT = true; 23 23 24 24 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/contacts/list-owners.php
r3695315 r3695885 14 14 class List_Owners extends Ability { 15 15 16 protected const stringNAME = 'groundhogg/list-owners';17 protected const stringCATEGORY = 'groundhogg-contacts';18 protected const stringCAPABILITY = 'view_contacts';16 protected const NAME = 'groundhogg/list-owners'; 17 protected const CATEGORY = 'groundhogg-contacts'; 18 protected const CAPABILITY = 'view_contacts'; 19 19 20 protected const boolREADONLY = true;21 protected const boolIDEMPOTENT = true;20 protected const READONLY = true; 21 protected const IDEMPOTENT = true; 22 22 23 23 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/contacts/list-saved-searches.php
r3695315 r3695885 13 13 class List_Saved_Searches extends Ability { 14 14 15 protected const stringNAME = 'groundhogg/list-saved-searches';16 protected const stringCATEGORY = 'groundhogg-contacts';17 protected const stringCAPABILITY = 'view_contacts';15 protected const NAME = 'groundhogg/list-saved-searches'; 16 protected const CATEGORY = 'groundhogg-contacts'; 17 protected const CAPABILITY = 'view_contacts'; 18 18 19 protected const boolREADONLY = true;20 protected const boolIDEMPOTENT = true;19 protected const READONLY = true; 20 protected const IDEMPOTENT = true; 21 21 22 22 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/contacts/search-contacts.php
r3695315 r3695885 6 6 use Groundhogg\Abilities\Schemas\Contact_Schema; 7 7 use Groundhogg\Abilities\Schemas\Segment_Schema; 8 use Groundhogg\Contact_Query;9 8 use Throwable; 10 9 use WP_Error; … … 12 11 class Search_Contacts extends Ability { 13 12 14 protected const stringNAME = 'groundhogg/search-contacts';15 protected const stringCATEGORY = 'groundhogg-contacts';16 protected const stringCAPABILITY = 'view_contacts';13 protected const NAME = 'groundhogg/search-contacts'; 14 protected const CATEGORY = 'groundhogg-contacts'; 15 protected const CAPABILITY = 'view_contacts'; 17 16 18 protected const boolREADONLY = true;19 protected const boolIDEMPOTENT = true;17 protected const READONLY = true; 18 protected const IDEMPOTENT = true; 20 19 21 20 protected function get_args(): array { … … 33 32 'items' => [ 34 33 'type' => 'string', 35 'enum' => [ 'tags', 'meta' ], 34 // 'sql' is search-contacts' own debugging addition, not 35 // part of Contact_Schema::expand_options() (it's about 36 // the query as a whole, not a per-contact section, and 37 // wouldn't make sense on get-contact/create-contact/ 38 // update-contact's single-row fetches). 39 'enum' => array_merge( Contact_Schema::expand_options(), [ 'sql' ] ), 36 40 ], 37 41 'default' => [ 'tags' ], 38 'description' => __( 'Optional extra sections to expand on each contact, beyond the standard fields. Available: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them).', 'groundhogg' ),42 'description' => __( 'Optional extra sections to expand, beyond the standard fields. Per-contact: tags, meta (raw custom field/meta values - see groundhogg/list-custom-fields to interpret them), plus any sections an installed add-on has registered. Query-level (not per-contact): "sql" adds a top-level `sql` field with the raw SQL Contact_Query built for this search - useful for debugging why a search matched (or didn\'t match) what was expected. Note it reflects the modern query builder\'s attempt specifically; if that attempt threw and Contact_Query silently fell back to its legacy query engine, this SQL will NOT be what actually ran.', 'groundhogg' ), 39 43 ], 40 44 'limit' => [ … … 65 69 'items' => Contact_Schema::get_schema(), 66 70 ], 71 'sql' => [ 72 'type' => 'string', 73 'description' => __( 'Only present when "sql" is passed in expand. The raw SQL Contact_Query built for this search - see the `expand` input\'s description for a caveat about legacy-query fallback.', 'groundhogg' ), 74 ], 67 75 ], 68 76 ], … … 75 83 $offset = ! empty( $input['offset'] ) ? absint( $input['offset'] ) : 0; 76 84 77 $query_vars = Segment_Schema::to_query( $input ); 85 // Executes immediately below, so a live query object (to_contact_query()) 86 // rather than the plain array to_query() gives - lets an add-on hook 87 // groundhogg/segment_schema/contact_query and manipulate the query 88 // directly (joins, raw where conditions), and lets pagination be applied 89 // here by just chaining onto it. 90 $contact_query = Segment_Schema::to_contact_query( $input ); 78 91 79 if ( is_wp_error( $ query_vars) ) {80 return $ query_vars;92 if ( is_wp_error( $contact_query ) ) { 93 return $contact_query; 81 94 } 82 95 83 $query_vars['number'] = $limit; 84 $query_vars['offset'] = $offset; 85 $query_vars['found_rows'] = true; 96 $contact_query->setLimit( $limit )->setOffset( $offset )->setFoundRows( true ); 86 97 87 98 try { 88 $contact_query = new Contact_Query(); 89 $results = $contact_query->query( $query_vars ); 99 $results = $contact_query->query(); 90 100 } catch ( Throwable $e ) { 91 101 // Contact_Query's modern query building already falls back to a legacy query on … … 101 111 }, $results ); 102 112 103 return[113 $output = [ 104 114 'total_items' => $contact_query->found_items, 105 115 'contacts' => $contacts, 106 116 ]; 117 118 if ( in_array( 'sql', $expand, true ) ) { 119 // Called after query() rather than before: get_sql()'s own 120 // maybe_setup_query() call is a no-op by this point (already run), so 121 // this reflects the exact query state query() actually built - not a 122 // fresh, possibly-different build. See the `expand` input's 123 // description for the legacy-fallback caveat. 124 $output['sql'] = $contact_query->get_sql(); 125 } 126 127 return $output; 107 128 } 108 129 } -
groundhogg/trunk/includes/abilities/contacts/update-contact.php
r3695315 r3695885 29 29 use Has_Owner_Validation; 30 30 31 protected const stringNAME = 'groundhogg/update-contact';32 protected const stringCATEGORY = 'groundhogg-contacts';33 protected const stringCAPABILITY = 'edit_contacts';34 35 protected const boolREADONLY = false;36 protected const boolDESTRUCTIVE = false;37 protected const boolIDEMPOTENT = true;31 protected const NAME = 'groundhogg/update-contact'; 32 protected const CATEGORY = 'groundhogg-contacts'; 33 protected const CAPABILITY = 'edit_contacts'; 34 35 protected const READONLY = false; 36 protected const DESTRUCTIVE = false; 37 protected const IDEMPOTENT = true; 38 38 39 39 protected function get_args(): array { … … 93 93 'items' => [ 94 94 'type' => 'string', 95 'enum' => [ 'tags', 'meta' ],95 'enum' => Contact_Schema::expand_options(), 96 96 ], 97 97 'default' => [ 'tags' ], 98 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta .', 'groundhogg' ),98 'description' => __( 'Optional extra sections to expand on the returned contact. Available: tags, meta, plus any sections an installed add-on has registered.', 'groundhogg' ), 99 99 ], 100 100 ], -
groundhogg/trunk/includes/abilities/db/describe-table.php
r3695315 r3695885 15 15 class Describe_Table extends Ability { 16 16 17 protected const stringNAME = 'groundhogg/describe-table';18 protected const stringCATEGORY = 'groundhogg-db';19 protected const stringCAPABILITY = 'manage_options';17 protected const NAME = 'groundhogg/describe-table'; 18 protected const CATEGORY = 'groundhogg-db'; 19 protected const CAPABILITY = 'manage_options'; 20 20 21 protected const boolREADONLY = true;22 protected const boolIDEMPOTENT = true;21 protected const READONLY = true; 22 protected const IDEMPOTENT = true; 23 23 24 24 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/db/query-table.php
r3695315 r3695885 40 40 class Query_Table extends Ability { 41 41 42 protected const stringNAME = 'groundhogg/query-table';43 protected const stringCATEGORY = 'groundhogg-db';44 protected const stringCAPABILITY = 'manage_options';45 46 protected const boolREADONLY = true;47 protected const boolIDEMPOTENT = true;42 protected const NAME = 'groundhogg/query-table'; 43 protected const CATEGORY = 'groundhogg-db'; 44 protected const CAPABILITY = 'manage_options'; 45 46 protected const READONLY = true; 47 protected const IDEMPOTENT = true; 48 48 49 49 /** -
groundhogg/trunk/includes/abilities/emails/list-email-templates.php
r3695315 r3695885 19 19 * is almost never needed just to pick which email to send, so it is only 20 20 * returned when explicitly requested via `expand`. When expanding content, keep 21 * `limit` small. 21 * `limit` small, or use groundhogg/get-email-template to fetch one email's full 22 * body directly once you have its id. 22 23 * 23 24 * `campaigns` filters via the generic object_relationships-backed 'related' 24 25 * query var (DB::query()) - an email is the primary/parent side of its 25 26 * relationship to a campaign, same direction Email::get_related_objects('campaign') 26 * reads (see groundhogg/create-email's and groundhogg/update-email's campaigns param). 27 * reads (see groundhogg/create-email-template's and groundhogg/update-email-template's 28 * campaigns param). 27 29 */ 28 30 class List_Email_Templates extends Ability { 29 31 30 protected const stringNAME = 'groundhogg/list-email-templates';31 protected const stringCATEGORY = 'groundhogg-email';32 protected const stringCAPABILITY = 'view_emails';32 protected const NAME = 'groundhogg/list-email-templates'; 33 protected const CATEGORY = 'groundhogg-email'; 34 protected const CAPABILITY = 'view_emails'; 33 35 34 protected const boolREADONLY = true;35 protected const boolIDEMPOTENT = true;36 protected const READONLY = true; 37 protected const IDEMPOTENT = true; 36 38 37 39 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/emails/list-sender-profiles.php
r3695315 r3695885 22 22 class List_Sender_Profiles extends Ability { 23 23 24 protected const stringNAME = 'groundhogg/list-sender-profiles';25 protected const stringCATEGORY = 'groundhogg-email';26 protected const stringCAPABILITY = 'send_emails';24 protected const NAME = 'groundhogg/list-sender-profiles'; 25 protected const CATEGORY = 'groundhogg-email'; 26 protected const CAPABILITY = 'send_emails'; 27 27 28 protected const boolREADONLY = true;29 protected const boolIDEMPOTENT = true;28 protected const READONLY = true; 29 protected const IDEMPOTENT = true; 30 30 31 31 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/emails/send-composed-email.php
r3695315 r3695885 43 43 class Send_Composed_Email extends Ability { 44 44 45 protected const stringNAME = 'groundhogg/send-composed-email';46 protected const stringCATEGORY = 'groundhogg-email';47 protected const stringCAPABILITY = 'send_emails';48 49 protected const boolREADONLY = false;50 protected const boolDESTRUCTIVE = true;51 protected const boolIDEMPOTENT = false;45 protected const NAME = 'groundhogg/send-composed-email'; 46 protected const CATEGORY = 'groundhogg-email'; 47 protected const CAPABILITY = 'send_emails'; 48 49 protected const READONLY = false; 50 protected const DESTRUCTIVE = true; 51 protected const IDEMPOTENT = false; 52 52 53 53 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/emails/send-email-template.php
r3695315 r3695885 36 36 class Send_Email_Template extends Ability { 37 37 38 protected const stringNAME = 'groundhogg/send-email-template';39 protected const stringCATEGORY = 'groundhogg-email';40 protected const stringCAPABILITY = 'send_emails';41 42 protected const boolREADONLY = false;43 protected const boolDESTRUCTIVE = true;44 protected const boolIDEMPOTENT = false;38 protected const NAME = 'groundhogg/send-email-template'; 39 protected const CATEGORY = 'groundhogg-email'; 40 protected const CAPABILITY = 'send_emails'; 41 42 protected const READONLY = false; 43 protected const DESTRUCTIVE = true; 44 protected const IDEMPOTENT = false; 45 45 46 46 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/funnels/add-to-flow.php
r3695315 r3695885 34 34 class Add_To_Flow extends Ability { 35 35 36 protected const stringNAME = 'groundhogg/add-to-flow';37 protected const stringCATEGORY = 'groundhogg-funnels';38 protected const stringCAPABILITY = 'start_flows';39 40 protected const boolREADONLY = false;41 protected const boolDESTRUCTIVE = true;42 protected const boolIDEMPOTENT = false;36 protected const NAME = 'groundhogg/add-to-flow'; 37 protected const CATEGORY = 'groundhogg-funnels'; 38 protected const CAPABILITY = 'start_flows'; 39 40 protected const READONLY = false; 41 protected const DESTRUCTIVE = true; 42 protected const IDEMPOTENT = false; 43 43 44 44 protected function get_args(): array { … … 173 173 } 174 174 175 // The plain array form, not to_contact_query()'s live query object - 176 // this gets stored and re-run in background batches (Background_Tasks:: 177 // add_contacts_to_funnel() below); a live query object (and anything a 178 // filter attached to it) can't survive that round trip. 175 179 $query = Segment_Schema::to_query( $input ); 176 180 -
groundhogg/trunk/includes/abilities/funnels/list-flows.php
r3695315 r3695885 21 21 class List_Flows extends Ability { 22 22 23 protected const stringNAME = 'groundhogg/list-flows';24 protected const stringCATEGORY = 'groundhogg-funnels';25 protected const stringCAPABILITY = 'view_funnels';23 protected const NAME = 'groundhogg/list-flows'; 24 protected const CATEGORY = 'groundhogg-funnels'; 25 protected const CAPABILITY = 'view_funnels'; 26 26 27 protected const boolREADONLY = true;28 protected const boolIDEMPOTENT = true;27 protected const READONLY = true; 28 protected const IDEMPOTENT = true; 29 29 30 30 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/reports/get-reports.php
r3695315 r3695885 37 37 class Get_Reports extends Ability { 38 38 39 protected const stringNAME = 'groundhogg/get-reports';40 protected const stringCATEGORY = 'groundhogg-reports';41 protected const stringCAPABILITY = 'view_reports';42 43 protected const boolREADONLY = true;44 protected const boolIDEMPOTENT = true;39 protected const NAME = 'groundhogg/get-reports'; 40 protected const CATEGORY = 'groundhogg-reports'; 41 protected const CAPABILITY = 'view_reports'; 42 43 protected const READONLY = true; 44 protected const IDEMPOTENT = true; 45 45 46 46 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/reports/list-report-types.php
r3695315 r3695885 16 16 class List_Report_Types extends Ability { 17 17 18 protected const stringNAME = 'groundhogg/list-report-types';19 protected const stringCATEGORY = 'groundhogg-reports';20 protected const stringCAPABILITY = 'view_reports';18 protected const NAME = 'groundhogg/list-report-types'; 19 protected const CATEGORY = 'groundhogg-reports'; 20 protected const CAPABILITY = 'view_reports'; 21 21 22 protected const boolREADONLY = true;23 protected const boolIDEMPOTENT = true;22 protected const READONLY = true; 23 protected const IDEMPOTENT = true; 24 24 25 25 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/schemas/contact-schema.php
r3695315 r3695885 5 5 use DateTime; 6 6 use Exception; 7 use Groundhogg\Abilities\Traits\Extensible_Schema; 7 8 use Groundhogg\Contact; 8 9 use Groundhogg\Preferences; … … 24 25 * per install. Use the groundhogg/list-custom-fields ability to look up what each meta 25 26 * key actually means (label, type, group). 27 * 28 * Extensible via the Extensible_Schema trait, so an add-on can expose additional 29 * read-only fields on groundhogg/get-contact and groundhogg/search-contacts 30 * results without modifying this class or either of those abilities - one call 31 * registers a new bounded-but-optional property (schema, expand option, and the 32 * callback that computes it), the same "only when asked for" treatment 33 * 'tags'/'meta' get above: 34 * 35 * Contact_Schema::extend( 36 * 'ltv', 37 * __( 'Lifetime value: total spend across all WooCommerce orders.', 'my-plugin' ), 38 * function ( Contact $contact ) { 39 * return (float) wc_get_customer_total_spent( $contact->get_user_id() ); 40 * }, 41 * 'number' 42 * ); 43 * 44 * Call this once (e.g. on `init`, after confirming both Groundhogg and the 45 * dependency it needs are active) - before any ability builds its input/output 46 * schema. See Extensible_Schema's own docblock for what extend() can't express 47 * and the three underlying filters (`groundhogg/contact_schema/properties`, 48 * `groundhogg/contact_schema/transform`, `groundhogg/contact_schema/expand_options`) 49 * available for those cases. 26 50 */ 27 51 class Contact_Schema extends Schema { 28 52 53 use Extensible_Schema; 54 55 protected static function extension_hook_prefix(): string { 56 return 'contact_schema'; 57 } 58 59 protected static function builtin_property_keys(): array { 60 return [ 'id', 'email', 'first_name', 'last_name', 'date_created', 'optin_status', 'tags', 'meta' ]; 61 } 62 29 63 public static function get_schema(): array { 64 65 $properties = [ 66 'id' => [ 67 'type' => 'integer', 68 ], 69 'email' => [ 70 'type' => 'string', 71 'format' => 'email', 72 ], 73 'first_name' => [ 74 'type' => 'string', 75 ], 76 'last_name' => [ 77 'type' => 'string', 78 ], 79 'date_created' => [ 80 'type' => 'string', 81 'format' => 'date-time', 82 'description' => __( 'When the contact was created.', 'groundhogg' ), 83 ], 84 'optin_status' => [ 85 'type' => 'object', 86 'description' => __( 'Whether/how the contact can be marketed to.', 'groundhogg' ), 87 'properties' => [ 88 'value' => [ 89 'type' => 'integer', 90 'description' => __( 'The raw Groundhogg\Preferences status constant.', 'groundhogg' ), 91 ], 92 'label' => [ 93 'type' => 'string', 94 ], 95 ], 96 ], 97 'tags' => [ 98 'type' => 'array', 99 'description' => __( 'Only present when "tags" is passed in include.', 'groundhogg' ), 100 'items' => Tag_Schema::get_schema(), 101 ], 102 'meta' => [ 103 'type' => 'object', 104 'description' => __( 'Only present when "meta" is passed in include. Raw meta key => value pairs, including custom fields. See groundhogg/list-custom-fields to map keys to labels.', 'groundhogg' ), 105 'additionalProperties' => true, 106 ], 107 ]; 30 108 31 109 return [ 32 110 'type' => 'object', 33 'properties' => [ 34 'id' => [ 35 'type' => 'integer', 36 ], 37 'email' => [ 38 'type' => 'string', 39 'format' => 'email', 40 ], 41 'first_name' => [ 42 'type' => 'string', 43 ], 44 'last_name' => [ 45 'type' => 'string', 46 ], 47 'date_created' => [ 48 'type' => 'string', 49 'format' => 'date-time', 50 'description' => __( 'When the contact was created.', 'groundhogg' ), 51 ], 52 'optin_status' => [ 53 'type' => 'object', 54 'description' => __( 'Whether/how the contact can be marketed to.', 'groundhogg' ), 55 'properties' => [ 56 'value' => [ 57 'type' => 'integer', 58 'description' => __( 'The raw Groundhogg\Preferences status constant.', 'groundhogg' ), 59 ], 60 'label' => [ 61 'type' => 'string', 62 ], 63 ], 64 ], 65 'tags' => [ 66 'type' => 'array', 67 'description' => __( 'Only present when "tags" is passed in include.', 'groundhogg' ), 68 'items' => Tag_Schema::get_schema(), 69 ], 70 'meta' => [ 71 'type' => 'object', 72 'description' => __( 'Only present when "meta" is passed in include. Raw meta key => value pairs, including custom fields. See groundhogg/list-custom-fields to map keys to labels.', 'groundhogg' ), 73 'additionalProperties' => true, 74 ], 75 ], 111 'properties' => self::extend_properties( $properties ), 76 112 ]; 113 } 114 115 /** 116 * The valid values for groundhogg/get-contact's and groundhogg/search-contacts' 117 * own `expand` input parameter (their `enum`, not just this class's $include). 118 * extend()'d keys are included automatically - see the class docblock for 119 * the escape hatch when an add-on isn't going through extend(). 120 * 121 * @return string[] 122 */ 123 public static function expand_options(): array { 124 return self::extend_expand_options( [ 'tags', 'meta' ] ); 77 125 } 78 126 … … 82 130 * 83 131 * @param Contact|int|string|object $object 84 * @param array $include Optional sections to include: 'tags', 'meta'. 132 * @param array $include Optional sections to include: 'tags', 'meta', 133 * plus anything an add-on has registered via 134 * Contact_Schema::extend() (see the class docblock). 85 135 * 86 136 * @return array … … 124 174 } 125 175 126 return $data;176 return self::extend_transform( $data, $object, $include ); 127 177 } 128 178 -
groundhogg/trunk/includes/abilities/schemas/email-schema.php
r3695315 r3695885 55 55 'type' => 'string', 56 56 'enum' => [ 'html', 'blocks', 'legacy_blocks', 'legacy_plain' ], 57 'description' => __( 'Which editor this email\'s content was built with, and which one wp-admin will open it in. "html": raw HTML (from the meta "type" flag, or content starting with "<!DOCTYPE"). "blocks": the current block editor (from the meta "blocks" flag) - content is HTML annotated with block comments ("<!--text:uuid {...}-->...<!--/text:uuid-->", etc.) that get parsed back into editable blocks. "legacy_blocks"/"legacy_plain": older formats inferred from the content itself. groundhogg/create-email \'s `editor` input controls which of "html"/"blocks" it produces.', 'groundhogg' ),57 'description' => __( 'Which editor this email\'s content was built with, and which one wp-admin will open it in. "html": raw HTML (from the meta "type" flag, or content starting with "<!DOCTYPE"). "blocks": the current block editor (from the meta "blocks" flag) - content is HTML annotated with block comments ("<!--text:uuid {...}-->...<!--/text:uuid-->", etc.) that get parsed back into editable blocks. "legacy_blocks"/"legacy_plain": older formats inferred from the content itself. groundhogg/create-email-template\'s `editor` input controls which of "html"/"blocks" it produces.', 'groundhogg' ), 58 58 ], 59 59 'template_settings' => [ 60 60 'type' => 'object', 61 'description' => __( 'The outer page layout/background this email renders inside of - distinct from the block/HTML content itself. Mirrors the "Template Settings" panel in wp-admin\'s email editor. Set via groundhogg/create-email \'s or groundhogg/update-email\'s `template_settings` input.', 'groundhogg' ),61 'description' => __( 'The outer page layout/background this email renders inside of - distinct from the block/HTML content itself. Mirrors the "Template Settings" panel in wp-admin\'s email editor. Set via groundhogg/create-email-template\'s or groundhogg/update-email-template\'s `template_settings` input.', 'groundhogg' ), 62 62 'properties' => self::template_settings_properties(), 63 63 ], … … 100 100 * The `template_settings` object's properties - shared by the output 101 101 * schema above and the `template_settings` input on 102 * groundhogg/create-email and groundhogg/update-email, so the property 103 * names, enums, and descriptions can't drift out of sync between them. 102 * groundhogg/create-email-template and groundhogg/update-email-template, so 103 * the property names, enums, and descriptions can't drift out of sync 104 * between them. 104 105 * 105 106 * @param bool $with_defaults Include `default` values for "layout" and 106 * "direction" - appropriate for create-email ,107 * "direction" - appropriate for create-email-template, 107 108 * where a brand-new email needs a sensible 108 109 * starting point. Leave false for the output 109 * schema and for update-email , where an absent110 * property should be left untouched rather110 * schema and for update-email-template, where an 111 * absent property should be left untouched rather 111 112 * than implying it would reset to a default. 112 113 * -
groundhogg/trunk/includes/abilities/schemas/segment-schema.php
r3695315 r3695885 4 4 5 5 use Groundhogg\Abilities\Traits\Has_Optin_Status; 6 use Groundhogg\Contact_Query; 6 7 use WP_Error; 7 8 use function Groundhogg\parse_tag_list; … … 25 26 * describes *input*, not an output shape, so it has no get_schema()/transform(). 26 27 * 27 * // in get_args(): 28 * 'properties' => array_merge( Segment_Schema::properties(), [ ...ability-specific... ] ), 29 * 30 * // in __invoke(): 28 * Three ways to turn a validated $input into something queryable, depending on 29 * what the calling ability actually does with it: 30 * 31 * // Executes immediately (groundhogg/search-contacts): a live Contact_Query, 32 * // so the caller can keep chaining (setLimit(), setOrderby(), ...) and an 33 * // add-on can manipulate the query object directly - see 34 * // groundhogg/segment_schema/contact_query below. 35 * $contact_query = Segment_Schema::to_contact_query( $input ); 36 * if ( is_wp_error( $contact_query ) ) { return $contact_query; } 37 * $results = $contact_query->setLimit( $limit )->setFoundRows( true )->query(); 38 * 39 * // Stored and (re-)run later (groundhogg/send-email-broadcast's dynamic 40 * // segments, groundhogg/add-to-flow's background batches): a plain, 41 * // serializable array of query vars - a live query object (and whatever a 42 * // filter attached to it via addJoin()/where()) can't survive that round 43 * // trip. 31 44 * $query = Segment_Schema::to_query( $input ); 32 45 * if ( is_wp_error( $query ) ) { return $query; } 33 * // ... hand $query to Contact_Query / Broadcast::schedule() / ... 46 * // ... hand $query to Broadcast::schedule() / Background_Tasks / ... 47 * 48 * // A branch/logic step's own condition setting (e.g. if_else's 49 * // `include_filters`) - Groundhogg's Filters DSL directly, not a 50 * // Contact_Query-vars array at all. 51 * $filters = Segment_Schema::to_filters( $input ); 52 * if ( is_wp_error( $filters ) ) { return $filters; } 53 * // ... use as an if_else/branch-logic step's include_filters setting 54 * 55 * // in get_args(), for all three: 56 * 'properties' => array_merge( Segment_Schema::properties(), [ ...ability-specific... ] ), 57 * 58 * Extensible, so an add-on can add its own audience-filtering params to every 59 * ability built on this schema (groundhogg/search-contacts, 60 * groundhogg/send-email-broadcast, groundhogg/add-to-flow, ...) without modifying 61 * this class or any of them individually. 62 * 63 * The easy way in is Segment_Schema::extend() - one call registers a new input 64 * property (its schema) and a callback that turns it into Groundhogg's Filters 65 * DSL (the same condition format Contact_Query's own `include_filters` query var 66 * and branch-logic step settings like if_else's `include_filters` consume - see 67 * `Groundhogg\DB\Query\Filters` and Contact_Query's `filter_*` methods for the 68 * full vocabulary of condition `type`s). Returning filter conditions rather than 69 * mutating a query-vars array directly means one extend() call works for BOTH 70 * to_query()/to_contact_query() (folded into `include_filters`) AND to_filters() 71 * (used for branch/logic conditions) - not two things to hook separately: 72 * 73 * Segment_Schema::extend( 74 * 'woocommerce', 75 * [ 76 * 'type' => 'object', 77 * 'additionalProperties' => false, 78 * 'properties' => [ 79 * 'min_orders' => [ 80 * 'type' => 'integer', 81 * 'description' => __( 'Only contacts with at least this many WooCommerce orders.', 'my-plugin' ), 82 * ], 83 * ], 84 * ], 85 * function ( array $input ): array { 86 * if ( empty( $input['woocommerce']['min_orders'] ) ) { 87 * return []; 88 * } 89 * // Requires a filter type actually registered with Contact_Query's 90 * // Filters instance (via groundhogg/contact_query/filters/register) - 91 * // extend() only carries conditions through, it doesn't invent new 92 * // filter types on its own. 93 * return [ [ 94 * 'type' => 'woocommerce_orders', 95 * 'compare' => 'greater_than_or_equal_to', 96 * 'value' => absint( $input['woocommerce']['min_orders'] ), 97 * ] ]; 98 * }, 99 * true // this alone counts as naming a deliberate audience - see has_audience() 100 * ); 101 * 102 * Call this once (e.g. on `init`, after confirming both Groundhogg and the 103 * dependency it needs are active) - before any ability builds its input schema. 104 * 105 * For query logic no Filters condition type can express at all (a join, a raw 106 * where condition, a custom select), reach for to_contact_query()'s live 107 * Contact_Query object instead, via the `groundhogg/segment_schema/contact_query` 108 * action below - only usable by abilities that execute the query themselves 109 * rather than storing it (see to_contact_query()'s own docblock). 110 * 111 * Everything extend() does is built on filters, also available directly for 112 * anything extend() can't express (e.g. contributing to more than one property 113 * from a single registration): 114 * 115 * - `groundhogg/segment_schema/properties` filters the input properties (properties(), 116 * below). 117 * - `groundhogg/segment_schema/query` filters the plain Contact_Query-vars array 118 * built from $input (to_query(), below) - runs after extend()'d conditions are 119 * already folded into `include_filters`. 120 * - `groundhogg/segment_schema/filters` filters the Filters-DSL array built from 121 * $input (to_filters(), below) - same timing, after extend()'d conditions are 122 * already merged in. 123 * - `groundhogg/segment_schema/audience_keys` adds to the keys has_audience() treats 124 * as deliberately naming a group of contacts, so an ability that must not act on 125 * the whole database by accident still recognizes a request that only supplies 126 * an add-on's own param as targeted, not "everyone". 34 127 */ 35 128 class Segment_Schema { … … 57 150 58 151 /** 59 * The input_schema `properties` for a contact segment. Every condition is 60 * ANDed together. Spread these into an ability's own `properties`. 152 * Add-on-registered input properties, keyed by property name. Populated by 153 * extend(). 154 * 155 * @var array<string, array{schema: array, callback: callable, audience: bool}> 156 */ 157 private static array $extensions = []; 158 159 /** 160 * Register an additional audience-filtering input property - see the class 161 * docblock for the full picture. One call adds the property to the input 162 * schema (properties(), below) and wires the callback that turns it into 163 * Filters-DSL conditions, merged into `include_filters` for to_query()/ 164 * to_contact_query() and into the result of to_filters() alike - instead of 165 * hooking groundhogg/segment_schema/properties and (one or both of) 166 * groundhogg/segment_schema/query / groundhogg/segment_schema/filters by hand. 167 * 168 * @param string $key The property name, e.g. "woocommerce". Must 169 * not collide with a built-in name or an 170 * existing extend()'d one - either is refused 171 * with _doing_it_wrong() rather than silently 172 * overwriting something else already relying 173 * on that key. 174 * @param array $schema The full JSON schema fragment for this 175 * property (type, description, enum, 176 * properties, items, ...), used as-is. Unlike 177 * Contact_Schema::extend()'s simpler 178 * description+type shorthand, a segment 179 * filter's shape varies too much (an object 180 * with its own sub-properties, an enum, a 181 * plain scalar, ...) to usefully paper over. 182 * @param callable $callback function( array $input ): array|WP_Error. 183 * Receives the full $input and returns a flat 184 * list of Filters-DSL condition arrays (each 185 * `['type' => ..., ...]`) contributed by this 186 * property - empty array if $key isn't present 187 * in $input - or a WP_Error to reject the 188 * input outright, same as to_query()'s own 189 * tags_include handling. The `type` used must 190 * be a filter type actually registered with 191 * Contact_Query's Filters instance (see the 192 * class docblock) - extend() carries 193 * conditions through, it doesn't register new 194 * filter types itself. Called on every 195 * to_query()/to_contact_query()/to_filters(), 196 * regardless of whether $key is actually 197 * present - check $input[ $key ] yourself. 198 * @param bool $is_audience_key Whether supplying this property alone (with 199 * no other segment param) should count as 200 * deliberately naming a group of contacts - 201 * see has_audience(). Default false. 202 * 203 * @return void 204 */ 205 public static function extend( string $key, array $schema, callable $callback, bool $is_audience_key = false ) { 206 207 if ( array_key_exists( $key, self::base_properties() ) || isset( self::$extensions[ $key ] ) ) { 208 _doing_it_wrong( 209 __METHOD__, 210 sprintf( 'A Segment_Schema property named "%s" already exists.', $key ), 211 '4.8' 212 ); 213 214 return; 215 } 216 217 self::$extensions[ $key ] = [ 218 'schema' => $schema, 219 'callback' => $callback, 220 'audience' => $is_audience_key, 221 ]; 222 } 223 224 /** 225 * This schema's own built-in input properties, before add-on extensions or 226 * the groundhogg/segment_schema/properties filter. Split out from 227 * properties() so extend() can check a proposed key against it without 228 * re-running the filter (and without properties() needing to special-case 229 * its own extensions when checking for collisions). 61 230 * 62 231 * @return array 63 232 */ 64 public static function properties(): array { 233 private static function base_properties(): array { 234 65 235 return [ 66 236 'search' => [ … … 179 349 180 350 /** 351 * The input_schema `properties` for a contact segment. Every condition is 352 * ANDed together. Spread these into an ability's own `properties`. 353 * 354 * @return array 355 */ 356 public static function properties(): array { 357 358 $properties = self::base_properties(); 359 360 foreach ( self::$extensions as $key => $extension ) { 361 $properties[ $key ] = $extension['schema']; 362 } 363 364 // See the class docblock for how an add-on adds its own properties here. 365 return apply_filters( 'groundhogg/segment_schema/properties', $properties ); 366 } 367 368 /** 181 369 * Whether $input names a deliberate group of contacts (vs. "everyone" or just 182 370 * refinements). An ability that must not act on the whole database by … … 189 377 public static function has_audience( array $input ): bool { 190 378 191 foreach ( self::AUDIENCE_KEYS as $key ) { 379 $audience_keys = self::AUDIENCE_KEYS; 380 381 foreach ( self::$extensions as $key => $extension ) { 382 if ( $extension['audience'] ) { 383 $audience_keys[] = $key; 384 } 385 } 386 387 // See the class docblock for how an add-on adds its own key here. 388 $audience_keys = apply_filters( 'groundhogg/segment_schema/audience_keys', $audience_keys ); 389 390 foreach ( $audience_keys as $key ) { 192 391 if ( ! empty( $input[ $key ] ) ) { 193 392 return true; … … 349 548 } 350 549 351 return $query; 550 $extension_conditions = self::extension_filter_conditions( $input ); 551 552 if ( is_wp_error( $extension_conditions ) ) { 553 return $extension_conditions; 554 } 555 556 if ( ! empty( $extension_conditions ) ) { 557 // Merged into the same single AND-group `include_filters` already uses 558 // elsewhere in this method (see to_filters() for the built-in fields' 559 // equivalent) - Contact_Query natively understands `include_filters` 560 // alongside the native query vars above (db.php's `case 'include_filters'`), 561 // so this doesn't conflict with anything already in $query. 562 $query['include_filters'][0] = array_merge( 563 $query['include_filters'][0] ?? [], 564 $extension_conditions 565 ); 566 } 567 568 // See the class docblock for how an add-on translates its own input 569 // properties into query vars here. Only reached on success - the early 570 // WP_Error returns above (e.g. unknown tags, an extend()'d callback 571 // rejecting its own input) bypass this filter, same as any other hard 572 // validation failure. 573 return apply_filters( 'groundhogg/segment_schema/query', $query, $input ); 574 } 575 576 /** 577 * Run every extend()'d property's callback against $input and collect the 578 * Filters-DSL conditions they contribute, for merging into either 579 * to_query()'s `include_filters` or to_filters()'s result. 580 * 581 * @param array $input 582 * 583 * @return array|WP_Error A flat list of condition arrays (possibly empty), or 584 * the first WP_Error a callback returns. 585 */ 586 private static function extension_filter_conditions( array $input ) { 587 588 $conditions = []; 589 590 foreach ( self::$extensions as $key => $extension ) { 591 592 $result = call_user_func( $extension['callback'], $input ); 593 594 if ( is_wp_error( $result ) ) { 595 return $result; 596 } 597 598 if ( ! empty( $result ) ) { 599 array_push( $conditions, ...array_values( $result ) ); 600 } 601 } 602 603 return $conditions; 604 } 605 606 /** 607 * Maps a `meta` condition's `compare` (this class's own vocabulary, matching 608 * Where::compare() - see META_COMPARISONS) to the equivalent 609 * Groundhogg\DB\Query\Filters::string() compare keyword, for to_filters(). 610 * The two vocabularies grew independently and mostly agree, except: 611 * "in"/"not_in" here must become "any_of"/"none_of" for Filters::string() - 612 * passed through unmapped, Filters::string() silently compares against only 613 * the first array value instead of the whole list, since it only special-cases 614 * exactly "any_of"/"none_of" before falling back to a scalar comparison. 615 * "like"/"not_like" have no exact Filters::string() equivalent and are mapped 616 * to the closest substring match ("contains"/"not_contains"). 617 */ 618 private const META_COMPARE_TO_FILTER_COMPARE = [ 619 'equals' => 'equals', 620 'not_equals' => 'not_equals', 621 'less_than' => 'less_than', 622 'greater_than' => 'greater_than', 623 'less_than_or_equal_to' => 'less_than_or_equal_to', 624 'greater_than_or_equal_to' => 'greater_than_or_equal_to', 625 'in' => 'any_of', 626 'not_in' => 'none_of', 627 'like' => 'contains', 628 'not_like' => 'not_contains', 629 'contains' => 'contains', 630 'not_contains' => 'not_contains', 631 'starts_with' => 'starts_with', 632 'ends_with' => 'ends_with', 633 'empty' => 'empty', 634 'not_empty' => 'not_empty', 635 ]; 636 637 /** 638 * Build Groundhogg's Filters DSL (Groundhogg\DB\Query\Filters::sanitize()'s 639 * expected shape - an OR-of-AND condition list, e.g. 640 * `[[ {type:'tags', ...}, {type:'owner', ...} ]]`) from a validated segment 641 * $input. This is the format branch/logic step settings consume directly 642 * (e.g. if_else's `include_filters`/`exclude_filters`) - unlike to_query()/ 643 * to_contact_query(), the result here is NOT a Contact_Query-vars array, and 644 * running it requires a Contact_Query (or a step type) that understands the 645 * Filters DSL, not a raw query. 646 * 647 * Every translated condition lands in a single AND-group (`[[ ...all of 648 * them... ]]`), matching Segment_Schema's own "every condition ANDed 649 * together" semantics - to_query()'s notion of ANDing everything, expressed 650 * here as one AND-group with no OR alternatives. 651 * 652 * `meta_relation: OR` is expressed via the `sub_query` filter type 653 * (Contact_Query::filter_sub_query()), which takes its own nested 654 * `include_filters` (a full OR-of-AND array) and applies it inside a 655 * sub-Where group - exactly the primitive needed to nest an OR *inside* the 656 * single outer AND-group everything else lands in, without restructuring the 657 * whole condition list or duplicating every other condition into each OR 658 * branch. See the `meta` handling below. 659 * 660 * One known gap: `search` has no Filters equivalent (Contact_Query's own 661 * `search` handling is a query-var-level heuristic - email vs. full name - 662 * not a Filters condition) and is silently ignored here. Use 663 * to_query()/to_contact_query() if `search` needs to actually apply. 664 * 665 * @param array $input 666 * 667 * @return array|WP_Error 668 */ 669 public static function to_filters( array $input ) { 670 671 $conditions = []; 672 673 if ( ! empty( $input['include'] ) ) { 674 $conditions[] = [ 675 'type' => 'contact_id', 676 'compare' => 'any_of', 677 'value' => wp_parse_id_list( $input['include'] ), 678 ]; 679 } 680 681 if ( ! empty( $input['exclude'] ) ) { 682 $conditions[] = [ 683 'type' => 'contact_id', 684 'compare' => 'none_of', 685 'value' => wp_parse_id_list( $input['exclude'] ), 686 ]; 687 } 688 689 if ( ! empty( $input['tags_include'] ) ) { 690 691 // $create = false: resolving an audience must never mint a tag. 692 $resolved = parse_tag_list( $input['tags_include'], 'ID', false ); 693 694 if ( empty( $resolved ) ) { 695 return new WP_Error( 696 'groundhogg_unknown_tags', 697 __( 'None of the tags in tags_include match an existing tag.', 'groundhogg' ) 698 ); 699 } 700 701 $conditions[] = [ 702 'type' => 'tags', 703 'tags' => $resolved, 704 'compare' => 'includes', 705 'compare2' => ! empty( $input['tags_include_needs_all'] ) ? 'all' : 'any', 706 ]; 707 } 708 709 if ( ! empty( $input['tags_exclude'] ) ) { 710 711 $resolved = parse_tag_list( $input['tags_exclude'], 'ID', false ); 712 713 if ( ! empty( $resolved ) ) { 714 $conditions[] = [ 715 'type' => 'tags', 716 'tags' => $resolved, 717 'compare' => 'excludes', 718 'compare2' => ! empty( $input['tags_exclude_needs_all'] ) ? 'all' : 'any', 719 ]; 720 } 721 } 722 723 if ( ! empty( $input['optin_status'] ) && is_array( $input['optin_status'] ) ) { 724 $conditions[] = [ 725 'type' => 'optin_status', 726 'value' => self::resolve_optin_status_list( $input['optin_status'] ), 727 'compare' => 'in', 728 ]; 729 } 730 731 if ( ! empty( $input['optin_status_exclude'] ) && is_array( $input['optin_status_exclude'] ) ) { 732 $conditions[] = [ 733 'type' => 'optin_status', 734 'value' => self::resolve_optin_status_list( $input['optin_status_exclude'] ), 735 'compare' => 'not_in', 736 ]; 737 } 738 739 if ( isset( $input['marketable'] ) ) { 740 $conditions[] = [ 741 'type' => 'is_marketable', 742 'marketable' => $input['marketable'] ? 'yes' : 'no', 743 ]; 744 } 745 746 if ( ! empty( $input['owner'] ) ) { 747 $conditions[] = [ 748 'type' => 'owner', 749 'value' => wp_parse_id_list( $input['owner'] ), 750 'compare' => 'in', 751 ]; 752 } 753 754 if ( ! empty( $input['users_include'] ) ) { 755 $conditions[] = [ 756 'type' => 'user_id', 757 'value' => wp_parse_id_list( $input['users_include'] ), 758 'compare' => 'any_of', 759 ]; 760 } 761 762 if ( ! empty( $input['users_exclude'] ) ) { 763 $conditions[] = [ 764 'type' => 'user_id', 765 'value' => wp_parse_id_list( $input['users_exclude'] ), 766 'compare' => 'none_of', 767 ]; 768 } 769 770 if ( isset( $input['has_user'] ) ) { 771 if ( $input['has_user'] ) { 772 $conditions[] = [ 'type' => 'is_user' ]; 773 } else { 774 // No negated "is_user" filter type exists - approximate "no linked 775 // user" as user_id being exactly the unset value. 776 $conditions[] = [ 'type' => 'user_id', 'compare' => 'equals', 'value' => 0 ]; 777 } 778 } 779 780 if ( ! empty( $input['created_after'] ) && ! empty( $input['created_before'] ) ) { 781 $conditions[] = [ 782 'type' => 'date_created', 783 'date_range' => 'between', 784 'after' => sanitize_text_field( $input['created_after'] ), 785 'before' => sanitize_text_field( $input['created_before'] ), 786 ]; 787 } else if ( ! empty( $input['created_after'] ) ) { 788 $conditions[] = [ 789 'type' => 'date_created', 790 'date_range' => 'after', 791 'after' => sanitize_text_field( $input['created_after'] ), 792 ]; 793 } else if ( ! empty( $input['created_before'] ) ) { 794 $conditions[] = [ 795 'type' => 'date_created', 796 'date_range' => 'before', 797 'before' => sanitize_text_field( $input['created_before'] ), 798 ]; 799 } 800 801 if ( ! empty( $input['saved_search'] ) ) { 802 $conditions[] = [ 803 'type' => 'saved_search', 804 'search' => sanitize_text_field( $input['saved_search'] ), 805 'compare' => 'in', 806 ]; 807 } 808 809 if ( ! empty( $input['meta'] ) && is_array( $input['meta'] ) ) { 810 811 $meta_conditions = []; 812 813 foreach ( $input['meta'] as $condition ) { 814 815 $condition = (array) $condition; 816 $key = isset( $condition['key'] ) ? sanitize_key( $condition['key'] ) : ''; 817 818 if ( ! $key ) { 819 continue; 820 } 821 822 $compare = in_array( $condition['compare'] ?? 'equals', self::META_COMPARISONS, true ) 823 ? $condition['compare'] 824 : 'equals'; 825 826 $meta_conditions[] = [ 827 'type' => 'meta', 828 'meta' => $key, 829 'compare' => self::META_COMPARE_TO_FILTER_COMPARE[ $compare ] ?? 'equals', 830 'value' => $condition['value'] ?? '', 831 ]; 832 } 833 834 $is_or = ! empty( $input['meta_relation'] ) && strtoupper( $input['meta_relation'] ) === 'OR'; 835 836 if ( $is_or && count( $meta_conditions ) > 1 ) { 837 // Nest as one OR-of-AND sub-group inside the outer AND-group, via 838 // the sub_query filter type - see the class docblock for why this 839 // is the only way to express an OR here without restructuring 840 // everything else. Each meta condition becomes its own single-item 841 // AND-group, ORed together. 842 $conditions[] = [ 843 'type' => 'sub_query', 844 'include_filters' => array_map( static fn( $condition ) => [ $condition ], $meta_conditions ), 845 ]; 846 } else { 847 // Either explicitly ANDed, or only one condition (where AND/OR are 848 // equivalent) - no need for the sub_query wrapper, just AND them 849 // into the outer group directly like everything else. 850 array_push( $conditions, ...$meta_conditions ); 851 } 852 } 853 854 $extension_conditions = self::extension_filter_conditions( $input ); 855 856 if ( is_wp_error( $extension_conditions ) ) { 857 return $extension_conditions; 858 } 859 860 array_push( $conditions, ...$extension_conditions ); 861 862 // See the class docblock for how an add-on adds its own conditions here. 863 return apply_filters( 'groundhogg/segment_schema/filters', empty( $conditions ) ? [] : [ $conditions ], $input ); 864 } 865 866 /** 867 * Build a live Contact_Query from a validated segment $input - same 868 * conditions as to_query(), but as a query object the caller can keep 869 * building on (setLimit(), setOrderby(), ...) and an add-on can manipulate 870 * directly via `groundhogg/segment_schema/contact_query` for anything a 871 * plain query-var array can't express (a join, a raw where condition). 872 * See the class docblock for when to reach for this vs. to_query(). 873 * 874 * Does not run the query - the caller decides when (query(), get_results(), 875 * count(), ...) and with what pagination/ordering on top. 876 * 877 * @param array $input 878 * @param int $flags Passed through to Contact_Query's constructor (e.g. 879 * Contact_Query::IS_SUB_QUERY). 880 * 881 * @return Contact_Query|WP_Error 882 */ 883 public static function to_contact_query( array $input, int $flags = 0 ) { 884 885 $query_vars = self::to_query( $input ); 886 887 if ( is_wp_error( $query_vars ) ) { 888 return $query_vars; 889 } 890 891 $contact_query = new Contact_Query( $query_vars, $flags ); 892 893 /** 894 * Fires with the live Contact_Query built from a segment $input, so an 895 * add-on can manipulate it directly - see the class docblock's 896 * WooCommerce example. The query hasn't run yet, so anything applied 897 * here (addJoin(), where(), setSelect(), ...) takes effect normally. 898 * 899 * @param Contact_Query $contact_query 900 * @param array $input 901 */ 902 do_action( 'groundhogg/segment_schema/contact_query', $contact_query, $input ); 903 904 return $contact_query; 352 905 } 353 906 -
groundhogg/trunk/includes/abilities/tags/list-tags.php
r3695315 r3695885 14 14 class List_Tags extends Ability { 15 15 16 protected const stringNAME = 'groundhogg/list-tags';17 protected const stringCATEGORY = 'groundhogg-tags';18 protected const stringCAPABILITY = 'manage_tags';16 protected const NAME = 'groundhogg/list-tags'; 17 protected const CATEGORY = 'groundhogg-tags'; 18 protected const CAPABILITY = 'manage_tags'; 19 19 20 protected const boolREADONLY = true;21 protected const boolIDEMPOTENT = true;20 protected const READONLY = true; 21 protected const IDEMPOTENT = true; 22 22 23 23 protected function get_args(): array { -
groundhogg/trunk/includes/abilities/utils/upload-media.php
r3695315 r3695885 9 9 * Uploads a file into the WordPress Media Library and returns its ID, URL, 10 10 * and relative path - the missing piece for building an email (via 11 * groundhogg/create-email or groundhogg/update-email) that includes a real12 * i mage rather than a placeholder: upload the image first, then use the13 * returned `url` as an `image` block's `src` (see the groundhogg-block-email14 * skill) or a plain `<img>` src in HTML content.11 * groundhogg/create-email-template or groundhogg/update-email-template) that 12 * includes a real image rather than a placeholder: upload the image first, 13 * then use the returned `url` as an `image` block's `src` (see the 14 * groundhogg-block-email skill) or a plain `<img>` src in HTML content. 15 15 * 16 16 * `data` is base64, not a raw file upload - abilities are called with a JSON … … 37 37 class Upload_Media extends Ability { 38 38 39 protected const stringNAME = 'groundhogg/upload-media';40 protected const stringCATEGORY = 'groundhogg-utils';41 protected const stringCAPABILITY = 'upload_files';39 protected const NAME = 'groundhogg/upload-media'; 40 protected const CATEGORY = 'groundhogg-utils'; 41 protected const CAPABILITY = 'upload_files'; 42 42 43 protected const boolREADONLY = false;44 protected const boolDESTRUCTIVE = false;45 protected const boolIDEMPOTENT = false;43 protected const READONLY = false; 44 protected const DESTRUCTIVE = false; 45 protected const IDEMPOTENT = false; 46 46 47 47 protected function get_args(): array { … … 49 49 return [ 50 50 'label' => __( 'Upload Media', 'groundhogg' ), 51 'description' => __( 'Upload a file (e.g. an image) into the WordPress Media Library from base64 data. Returns the attachment\'s id, URL, and relative path, for use as an image src elsewhere - e.g. an image block in groundhogg/create-email \'s content.', 'groundhogg' ),51 'description' => __( 'Upload a file (e.g. an image) into the WordPress Media Library from base64 data. Returns the attachment\'s id, URL, and relative path, for use as an image src elsewhere - e.g. an image block in groundhogg/create-email-template\'s content.', 'groundhogg' ), 52 52 53 53 'input_schema' => [
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)