Skip to content

Commit 50b7376

Browse files
authored
Merge pull request #1859 from xwp/feature/XWPENG-13-abilities-api
Register Stream operations as WordPress Abilities
2 parents 807224e + c44aca1 commit 50b7376

57 files changed

Lines changed: 5646 additions & 22 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lint-and-test.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,22 @@ jobs:
127127
run: npm run build
128128

129129
- name: Pull custom Docker images
130-
run: docker compose pull wordpress
130+
# `--ignore-pull-failures` so the first push of this branch (before
131+
# docker-images.yml has published ghcr.io/xwp/stream-mkcert) still
132+
# passes CI. Docker Compose then falls back to the local build:
133+
# context for any image it couldn't pull, keeping the workflow
134+
# green while a master push backfills the registry.
135+
run: docker compose pull --ignore-pull-failures wordpress mkcert
136+
137+
- name: Rebuild WordPress image on top of the pulled base
138+
# The published ghcr.io/xwp/stream-wordpress image is updated only by
139+
# docker-images.yml on master pushes. When this branch changes
140+
# local/docker/wordpress/Dockerfile (e.g. to enable mod_ssl), the
141+
# pulled image lags behind the branch's intent and Apache rejects
142+
# :443 with ERR_CONNECTION_CLOSED. Build locally so the branch's
143+
# Dockerfile actually applies; Docker layer caching keeps this cheap
144+
# when the Dockerfile hasn't changed since the last published build.
145+
run: docker compose build wordpress
131146

132147
- name: Start WordPress
133148
run: npm run start
@@ -144,8 +159,8 @@ jobs:
144159
run: |
145160
php -r '
146161
require "vendor/autoload.php";
147-
( new XWP\Wait_For\Tcp_Connection( "127.0.0.1", 80 ) )->connect( 60 );
148-
echo "WordPress is accepting connections on 127.0.0.1:80.\n";
162+
( new XWP\Wait_For\Tcp_Connection( "127.0.0.1", 443 ) )->connect( 60 );
163+
echo "WordPress is accepting HTTPS connections on 127.0.0.1:443.\n";
149164
'
150165
151166
- name: Install WordPress multisite

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ debug.log
1414
package.lock
1515
.phpunit.result.cache
1616

17+
# Locally-generated TLS cert + key for the dev environment HTTPS vhost.
18+
# Created by local/scripts/setup-https.sh via mkcert.
19+
/local/certs/
20+
1721
# Generated test data
1822
/artifacts/
1923
tests/data/tmp/*
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
/**
3+
* Ability: stream/create-alert — create a Stream alert rule.
4+
*
5+
* @package WP_Stream
6+
*/
7+
8+
namespace WP_Stream;
9+
10+
/**
11+
* Class - Ability_Create_Alert
12+
*/
13+
class Ability_Create_Alert extends Ability {
14+
15+
/**
16+
* {@inheritDoc}
17+
*/
18+
public function get_name() {
19+
return 'stream/create-alert';
20+
}
21+
22+
/**
23+
* {@inheritDoc}
24+
*/
25+
public function get_label() {
26+
return __( 'Create Stream Alert', 'stream' );
27+
}
28+
29+
/**
30+
* {@inheritDoc}
31+
*/
32+
public function get_description() {
33+
return __( 'Create a new Stream alert rule. Alerts notify configured channels when records matching the trigger filters are logged.', 'stream' );
34+
}
35+
36+
/**
37+
* {@inheritDoc}
38+
*/
39+
public function get_annotations() {
40+
return array(
41+
'readonly' => false,
42+
'instructions' => __( 'Create an alert that fires whenever a record matches the supplied filters. Validate the connector/context/action with stream/get-connectors first, and confirm with the user before creating the alert because it changes site behavior.', 'stream' ),
43+
);
44+
}
45+
46+
/**
47+
* {@inheritDoc}
48+
*/
49+
public function get_input_schema() {
50+
return array(
51+
'type' => 'object',
52+
'additionalProperties' => false,
53+
'required' => array( 'alert_type', 'trigger_author', 'trigger_context', 'trigger_action' ),
54+
'properties' => array(
55+
'alert_type' => array(
56+
'type' => 'string',
57+
'description' => 'Notifier slug. Built-in types are none, highlight, email, ifttt, slack. Other slugs may be registered by extensions.',
58+
),
59+
'trigger_author' => array(
60+
'type' => 'string',
61+
'description' => 'User ID or role slug to match. Use "any" to match all authors.',
62+
),
63+
'trigger_context' => array(
64+
'type' => 'string',
65+
'description' => 'Connector or "connector-context" slug. Use "any" to match all contexts.',
66+
),
67+
'trigger_action' => array(
68+
'type' => 'string',
69+
'description' => 'Action slug to match (e.g. "updated"). Use "any" to match all actions.',
70+
),
71+
'alert_meta' => array(
72+
'type' => 'object',
73+
'description' => 'Additional notifier-specific configuration (e.g. email recipients, slack webhook).',
74+
'additionalProperties' => true,
75+
),
76+
'status' => array(
77+
'type' => 'string',
78+
'description' => 'Initial alert status.',
79+
'enum' => array( Alerts::STATUS_ENABLED, Alerts::STATUS_DISABLED ),
80+
'default' => Alerts::STATUS_ENABLED,
81+
),
82+
),
83+
);
84+
}
85+
86+
/**
87+
* {@inheritDoc}
88+
*/
89+
public function get_output_schema() {
90+
return array(
91+
'type' => 'object',
92+
'additionalProperties' => false,
93+
'properties' => array(
94+
'id' => array( 'type' => 'integer' ),
95+
'status' => array(
96+
'type' => 'string',
97+
'enum' => array( Alerts::STATUS_ENABLED, Alerts::STATUS_DISABLED ),
98+
),
99+
'title' => array( 'type' => 'string' ),
100+
'alert_type' => array( 'type' => array( 'string', 'null' ) ),
101+
'alert_meta' => array(
102+
'type' => 'object',
103+
'additionalProperties' => true,
104+
),
105+
),
106+
);
107+
}
108+
109+
/**
110+
* {@inheritDoc}
111+
*
112+
* @param mixed $input Validated input matching get_input_schema(), or null.
113+
*/
114+
public function execute( $input = null ) {
115+
$status = isset( $input['status'] ) ? $input['status'] : Alerts::STATUS_ENABLED;
116+
117+
// Validate alert_type against the registered notifier slugs. The schema
118+
// can't enum these because alert types are extensible via the
119+
// wp_stream_alert_types filter -- a hardcoded enum would lock out
120+
// 3rd-party notifiers. Validate at execute() time instead.
121+
$registered_types = isset( $this->plugin->alerts->alert_types )
122+
? array_keys( (array) $this->plugin->alerts->alert_types )
123+
: array();
124+
if ( ! empty( $registered_types ) && ! in_array( $input['alert_type'], $registered_types, true ) ) {
125+
return new \WP_Error(
126+
'stream_unknown_alert_type',
127+
sprintf(
128+
/* translators: 1: alert_type slug supplied by caller, 2: comma-separated list of registered alert type slugs */
129+
__( 'Unknown alert_type "%1$s". Registered types: %2$s.', 'stream' ),
130+
(string) $input['alert_type'],
131+
implode( ', ', $registered_types )
132+
),
133+
array( 'status' => 400 )
134+
);
135+
}
136+
137+
// Mirror the admin form's connector-context split so Alert::get_title()
138+
// and Alert_Trigger_Context::check_record() see the same data shape
139+
// they see when an admin creates the alert through the UI.
140+
$trigger_context_raw = (string) $input['trigger_context'];
141+
if ( false !== strpos( $trigger_context_raw, '-' ) ) {
142+
list( $trigger_connector, $trigger_context ) = explode( '-', $trigger_context_raw, 2 );
143+
} else {
144+
$trigger_connector = $trigger_context_raw;
145+
$trigger_context = '';
146+
}
147+
148+
$extra_meta = isset( $input['alert_meta'] ) ? (array) $input['alert_meta'] : array();
149+
$alert_meta = array_merge(
150+
$extra_meta,
151+
array(
152+
'trigger_author' => $input['trigger_author'],
153+
'trigger_connector' => $trigger_connector,
154+
'trigger_context' => $trigger_context,
155+
'trigger_action' => $input['trigger_action'],
156+
)
157+
);
158+
159+
// Delegate the actual insert + meta save to Alert::save() so the
160+
// admin UI and the Abilities API stay on a single code path.
161+
$alert = new Alert(
162+
(object) array(
163+
'alert_type' => $input['alert_type'],
164+
'alert_meta' => $alert_meta,
165+
'status' => $status,
166+
),
167+
$this->plugin
168+
);
169+
170+
$post_id = $alert->save();
171+
172+
if ( is_wp_error( $post_id ) ) {
173+
return $post_id;
174+
}
175+
176+
if ( empty( $post_id ) ) {
177+
return new \WP_Error(
178+
'stream_alert_create_failed',
179+
__( 'Failed to create alert.', 'stream' ),
180+
array( 'status' => 500 )
181+
);
182+
}
183+
184+
// Normalize alert_meta to stdClass when empty so the response satisfies
185+
// the declared object output schema. Reachable in theory if a future
186+
// Alert::save() change ever leaves alert_meta unwritten -- today the
187+
// call always persists the merged trigger keys, but the coerce is cheap
188+
// defense and keeps the JSON output consistent with get-alerts.
189+
$persisted_meta = get_post_meta( $post_id, 'alert_meta', true );
190+
$alert_meta = is_array( $persisted_meta ) && ! empty( $persisted_meta )
191+
? $persisted_meta
192+
: new \stdClass();
193+
194+
return array(
195+
'id' => (int) $post_id,
196+
'status' => (string) get_post_status( $post_id ),
197+
'title' => (string) get_the_title( $post_id ),
198+
'alert_type' => get_post_meta( $post_id, 'alert_type', true ),
199+
'alert_meta' => $alert_meta,
200+
);
201+
}
202+
}

0 commit comments

Comments
 (0)