PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / trunk
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 66 releases
better-payment / includes / API / CampaignAPI.php

CampaignAPI.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More trunk, at includes/API/CampaignAPI.php

391 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\API;
4
5 use Better_Payment\Lite\Campaign\CampaignStats;
6 use Better_Payment\Lite\Campaign\MetaBox;
7 use Better_Payment\Lite\Campaign\Templates\TemplateManager;
8 use Better_Payment\Lite\Admin\DB;
9 use Better_Payment\Lite\Campaign\Services\RendererService;
10 use WP_Error;
11 use WP_REST_Controller;
12 use WP_REST_Request;
13 use WP_REST_Response;
14 use WP_REST_Server;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * REST API routes for bp_campaign posts and campaign stats.
22 * Namespace: better-payment/v1
23 */
24 class CampaignAPI extends WP_REST_Controller {
25
26 protected $namespace = 'better-payment/v1';
27
28 public function __construct() {
29 add_action( 'rest_api_init', [ $this, 'register_routes' ] );
30 }
31
32 public function register_routes() {
33 // List campaigns
34 register_rest_route( $this->namespace, '/campaigns', [
35 [
36 'methods' => WP_REST_Server::READABLE,
37 'callback' => [ $this, 'get_campaigns' ],
38 'permission_callback' => [ $this, 'check_admin_permissions' ],
39 ],
40 [
41 'methods' => WP_REST_Server::CREATABLE,
42 'callback' => [ $this, 'create_campaign' ],
43 'permission_callback' => [ $this, 'check_admin_permissions' ],
44 ],
45 ] );
46
47 // Restore campaign from trash
48 register_rest_route( $this->namespace, '/campaigns/(?P<id>[\d]+)/restore', [
49 'methods' => WP_REST_Server::CREATABLE,
50 'callback' => [ $this, 'restore_campaign' ],
51 'permission_callback' => [ $this, 'check_admin_permissions' ],
52 'args' => [ 'id' => [ 'type' => 'integer' ] ],
53 ] );
54
55 // Single campaign
56 register_rest_route( $this->namespace, '/campaigns/(?P<id>[\d]+)', [
57 [
58 'methods' => WP_REST_Server::READABLE,
59 'callback' => [ $this, 'get_campaign' ],
60 'permission_callback' => [ $this, 'check_admin_permissions' ],
61 'args' => [ 'id' => [ 'type' => 'integer' ] ],
62 ],
63 [
64 'methods' => WP_REST_Server::EDITABLE,
65 'callback' => [ $this, 'update_campaign' ],
66 'permission_callback' => [ $this, 'check_admin_permissions' ],
67 'args' => [ 'id' => [ 'type' => 'integer' ] ],
68 ],
69 [
70 'methods' => WP_REST_Server::DELETABLE,
71 'callback' => [ $this, 'delete_campaign' ],
72 'permission_callback' => [ $this, 'check_admin_permissions' ],
73 'args' => [ 'id' => [ 'type' => 'integer' ] ],
74 ],
75 ] );
76
77 // Campaign stats (public — used for frontend display)
78 register_rest_route( $this->namespace, '/campaigns/(?P<id>[\d]+)/stats', [
79 'methods' => WP_REST_Server::READABLE,
80 'callback' => [ $this, 'get_campaign_stats' ],
81 'permission_callback' => '__return_true',
82 'args' => [ 'id' => [ 'type' => 'integer' ] ],
83 ] );
84
85 // Available templates — public (builder needs these before user is authenticated)
86 register_rest_route( $this->namespace, '/campaigns/templates', [
87 'methods' => WP_REST_Server::READABLE,
88 'callback' => [ $this, 'get_templates' ],
89 'permission_callback' => '__return_true',
90 ] );
91
92 // Builder live-preview — renders layout+meta as a full HTML document (no saved post required)
93 register_rest_route( $this->namespace, '/campaigns/preview', [
94 'methods' => WP_REST_Server::CREATABLE,
95 'callback' => [ $this, 'preview_campaign_builder' ],
96 'permission_callback' => [ $this, 'check_admin_permissions' ],
97 ] );
98
99 // Preview — renders layout JSON as HTML without saving
100 register_rest_route( $this->namespace, '/campaigns/(?P<id>[\d]+)/preview', [
101 'methods' => WP_REST_Server::CREATABLE,
102 'callback' => [ $this, 'preview_campaign' ],
103 'permission_callback' => [ $this, 'check_admin_permissions' ],
104 'args' => [ 'id' => [ 'type' => 'integer' ] ],
105 ] );
106
107 // Template preview — renders a template definition as HTML for the picker iframe
108 register_rest_route( $this->namespace, '/templates/(?P<key>[a-z0-9\-]+)/preview', [
109 'methods' => WP_REST_Server::READABLE,
110 'callback' => [ $this, 'preview_template' ],
111 'permission_callback' => [ $this, 'check_admin_permissions' ],
112 ] );
113 }
114
115 // ------------------------------------------------------------------ handlers
116
117 public function get_campaigns( WP_REST_Request $request ): WP_REST_Response {
118 $status = sanitize_text_field( $request->get_param( 'status' ) ?: 'all' );
119 $search = sanitize_text_field( $request->get_param( 's' ) ?: '' );
120 $orderby = in_array( $request->get_param( 'orderby' ), [ 'title', 'date' ], true ) ? $request->get_param( 'orderby' ) : 'date';
121 $order = strtoupper( $request->get_param( 'order' ) ?: 'DESC' ) === 'ASC' ? 'ASC' : 'DESC';
122 $per_page = max( 1, (int) ( $request->get_param( 'per_page' ) ?: 20 ) );
123 $page = max( 1, (int) ( $request->get_param( 'page' ) ?: 1 ) );
124
125 if ( $status === 'trash' ) {
126 $post_statuses = [ 'trash' ];
127 } elseif ( in_array( $status, [ 'publish', 'draft' ], true ) ) {
128 $post_statuses = [ $status ];
129 } else {
130 $post_statuses = [ 'publish', 'draft' ];
131 }
132
133 $args = [
134 'post_type' => 'bp_campaign',
135 'post_status' => $post_statuses,
136 'posts_per_page' => $per_page,
137 'paged' => $page,
138 'orderby' => $orderby,
139 'order' => $order,
140 's' => $search,
141 ];
142
143 $query = new \WP_Query( $args );
144 $data = array_map( [ $this, 'prepare_campaign_response' ], $query->posts );
145
146 $post_counts = wp_count_posts( 'bp_campaign' );
147 $counts = [
148 'all' => (int) ( $post_counts->publish ?? 0 ) + (int) ( $post_counts->draft ?? 0 ),
149 'publish' => (int) ( $post_counts->publish ?? 0 ),
150 'draft' => (int) ( $post_counts->draft ?? 0 ),
151 'trash' => (int) ( $post_counts->trash ?? 0 ),
152 ];
153
154 return new WP_REST_Response( [
155 'campaigns' => $data,
156 'total' => (int) $query->found_posts,
157 'pages' => (int) $query->max_num_pages,
158 'page' => $page,
159 'counts' => $counts,
160 ], 200 );
161 }
162
163 public function create_campaign( WP_REST_Request $request ): WP_REST_Response {
164 if ( ! $this->valid_nonce( $request ) ) {
165 return new WP_REST_Response( [ 'message' => 'Invalid nonce' ], 403 );
166 }
167
168 $title = sanitize_text_field( $request->get_param( 'title' ) ?: __( 'New Campaign', 'better-payment' ) );
169 $post_status = sanitize_text_field( $request->get_param( 'post_status' ) ?: 'draft' );
170
171 $post_id = wp_insert_post( [
172 'post_type' => 'bp_campaign',
173 'post_title' => $title,
174 'post_status' => $post_status,
175 ] );
176
177 if ( is_wp_error( $post_id ) ) {
178 return new WP_REST_Response( [ 'message' => $post_id->get_error_message() ], 500 );
179 }
180
181 MetaBox::save_from_array( $post_id, $request->get_params() );
182
183 return new WP_REST_Response( $this->prepare_campaign_response( get_post( $post_id ) ), 201 );
184 }
185
186 public function get_campaign( WP_REST_Request $request ): WP_REST_Response {
187 $post = $this->get_validated_post( $request['id'] );
188 if ( is_wp_error( $post ) ) {
189 return new WP_REST_Response( [ 'message' => $post->get_error_message() ], 404 );
190 }
191
192 return new WP_REST_Response( $this->prepare_campaign_response( $post ), 200 );
193 }
194
195 public function update_campaign( WP_REST_Request $request ): WP_REST_Response {
196 if ( ! $this->valid_nonce( $request ) ) {
197 return new WP_REST_Response( [ 'message' => 'Invalid nonce' ], 403 );
198 }
199
200 $post = $this->get_validated_post( $request['id'] );
201 if ( is_wp_error( $post ) ) {
202 return new WP_REST_Response( [ 'message' => $post->get_error_message() ], 404 );
203 }
204
205 $post_id = $post->ID;
206
207 // Update title / status if provided
208 $update = [ 'ID' => $post_id ];
209
210 if ( $request->get_param( 'title' ) ) {
211 $update['post_title'] = sanitize_text_field( $request->get_param( 'title' ) );
212 }
213
214 if ( $request->get_param( 'post_status' ) ) {
215 $update['post_status'] = sanitize_text_field( $request->get_param( 'post_status' ) );
216 }
217
218 if ( count( $update ) > 1 ) {
219 wp_update_post( $update );
220 }
221
222 MetaBox::save_from_array( $post_id, $request->get_params() );
223
224 // Bust stats cache in case goal changed.
225 CampaignStats::bust_cache( $post_id );
226
227 return new WP_REST_Response( $this->prepare_campaign_response( get_post( $post_id ) ), 200 );
228 }
229
230 public function delete_campaign( WP_REST_Request $request ): WP_REST_Response {
231 if ( ! $this->valid_nonce( $request ) ) {
232 return new WP_REST_Response( [ 'message' => 'Invalid nonce' ], 403 );
233 }
234
235 if ( ! current_user_can( 'manage_options' ) ) {
236 return new WP_REST_Response( [ 'message' => 'Forbidden' ], 403 );
237 }
238
239 $post = $this->get_validated_post( $request['id'] );
240 if ( is_wp_error( $post ) ) {
241 return new WP_REST_Response( [ 'message' => $post->get_error_message() ], 404 );
242 }
243
244 $force = filter_var( $request->get_param( 'force' ), FILTER_VALIDATE_BOOLEAN );
245
246 if ( $force ) {
247 wp_delete_post( $post->ID, true );
248 } else {
249 wp_trash_post( $post->ID );
250 }
251
252 return new WP_REST_Response( [ 'deleted' => true ], 200 );
253 }
254
255 public function restore_campaign( WP_REST_Request $request ): WP_REST_Response {
256 if ( ! $this->valid_nonce( $request ) ) {
257 return new WP_REST_Response( [ 'message' => 'Invalid nonce' ], 403 );
258 }
259
260 $post = get_post( (int) $request['id'] );
261 if ( ! $post || $post->post_type !== 'bp_campaign' ) {
262 return new WP_REST_Response( [ 'message' => 'Campaign not found' ], 404 );
263 }
264
265 wp_untrash_post( $post->ID );
266
267 return new WP_REST_Response( [ 'restored' => true ], 200 );
268 }
269
270 public function get_campaign_stats( WP_REST_Request $request ): WP_REST_Response {
271 $not_found = new WP_REST_Response( [ 'message' => 'Not found' ], 404 );
272
273 $post = $this->get_validated_post( $request['id'] );
274 if ( is_wp_error( $post ) ) {
275 return $not_found;
276 }
277
278 if ( $post->post_status !== 'publish' && ! current_user_can( 'manage_options' ) ) {
279 return $not_found;
280 }
281
282 $stats = CampaignStats::get_stats( $post->ID );
283
284 return new WP_REST_Response( $stats, 200 );
285 }
286
287 public function get_templates(): WP_REST_Response {
288 // The builder's template list — picker-facing, so it must not serve the
289 // retired designs get_all() still holds for the renderer.
290 return new WP_REST_Response( array_values( TemplateManager::get_for_picker() ), 200 );
291 }
292
293 public function preview_campaign( WP_REST_Request $request ): WP_REST_Response {
294 $post = $this->get_validated_post( $request['id'] );
295 if ( is_wp_error( $post ) ) {
296 return new WP_REST_Response( [ 'message' => $post->get_error_message() ], 404 );
297 }
298
299 $layout = $request->get_param( 'layout' );
300 if ( ! is_array( $layout ) ) {
301 return new WP_REST_Response( [ 'message' => 'Invalid layout' ], 400 );
302 }
303
304 $html = RendererService::render_campaign( $post->ID, true, $layout );
305
306 return new WP_REST_Response( [ 'html' => $html ], 200 );
307 }
308
309 public function preview_campaign_builder( WP_REST_Request $request ): WP_REST_Response {
310 $layout = $request->get_param( 'layout' );
311 $meta = $request->get_param( 'meta' ) ?: [];
312 $campaign_id = (int) ( $request->get_param( 'campaign_id' ) ?: 0 );
313
314 if ( ! is_array( $layout ) ) {
315 return new WP_REST_Response( [ 'message' => 'Invalid layout' ], 400 );
316 }
317
318 if ( ! is_array( $meta ) ) {
319 $meta = [];
320 }
321
322 $html = RendererService::build_preview_document( $layout, $meta, $campaign_id );
323 return new WP_REST_Response( [ 'html' => $html ], 200 );
324 }
325
326 public function preview_template( WP_REST_Request $request ): WP_REST_Response {
327 $key = sanitize_key( $request['key'] );
328
329 // ?document=1 → a standalone HTML document for the picker's Preview
330 // lightbox iframe (stylesheets, theme class and colours included), rather
331 // than the bare fragment the original callers expect. It returns '' for a
332 // template with no layout of its own — a locked Pro card — and the client
333 // falls back to that template's screenshot.
334 if ( $request->get_param( 'document' ) ) {
335 $document = RendererService::build_template_preview_document( $key );
336 if ( ! $document ) {
337 return new WP_REST_Response( [ 'message' => 'Template not previewable' ], 404 );
338 }
339 return new WP_REST_Response( [ 'html' => $document ], 200 );
340 }
341
342 $html = RendererService::render_template_preview( $key );
343 if ( ! $html ) {
344 return new WP_REST_Response( [ 'message' => 'Template not found' ], 404 );
345 }
346 return new WP_REST_Response( [ 'html' => $html ], 200 );
347 }
348
349 // ------------------------------------------------------------------ helpers
350
351 public function check_admin_permissions() {
352 if ( ! current_user_can( 'manage_options' ) ) {
353 return new WP_Error( 'unauthorized', 'Unauthorized', [ 'status' => 401 ] );
354 }
355 return true;
356 }
357
358 private function valid_nonce( WP_REST_Request $request ): bool {
359 $nonce = $request->get_header( 'x_wp_nonce' );
360 return (bool) wp_verify_nonce( $nonce, 'wp_rest' );
361 }
362
363 private function get_validated_post( int $id ) {
364 $post = get_post( $id );
365 if ( ! $post || $post->post_type !== 'bp_campaign' ) {
366 return new WP_Error( 'not_found', __( 'Campaign not found', 'better-payment' ), [ 'status' => 404 ] );
367 }
368 return $post;
369 }
370
371 private function prepare_campaign_response( \WP_Post $post ): array {
372 $meta = MetaBox::get_all( $post->ID );
373 $stats = CampaignStats::get_stats( $post->ID );
374 $code = DB::get_settings( 'better_payment_settings_general_general_currency' );
375 $currency = ( is_string( $code ) && $code !== '' ) ? $code : 'USD';
376
377 return [
378 'id' => $post->ID,
379 'title' => $post->post_title,
380 'post_status' => $post->post_status,
381 'post_date' => $post->post_date,
382 'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'large' ) ?: '',
383 'edit_url' => admin_url( 'admin.php?page=bp-campaign-builder&campaign_id=' . $post->ID ),
384 'view_url' => $post->post_status === 'publish' ? ( get_permalink( $post->ID ) ?: '' ) : '',
385 'currency' => $currency,
386 'meta' => $meta,
387 'stats' => $stats,
388 ];
389 }
390 }
391