PluginProbe
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP / trunk
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP vtrunk
3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 3.0.0 2.4.13 2.4.12 2.4.11 2.4.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 All 110 releases
betterlinks / includes / API / Clicks.php

Clicks.php in BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP trunk, at includes/API/Clicks.php

681 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace BetterLinks\API;
3 if ( ! defined( 'ABSPATH' ) ) { exit; }
4
5 use BetterLinks\Traits\ArgumentSchema;
6
7 class Clicks extends Controller {
8
9 use \BetterLinks\Traits\Clicks;
10 use ArgumentSchema;
11
12 /**
13 * Initialize hooks and option name
14 */
15 public function __construct() {
16 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
17 }
18
19 /**
20 * Register the routes for the objects of the controller.
21 */
22 public function register_routes() {
23 $endpoint = '/clicks/';
24 register_rest_route(
25 $this->namespace,
26 $endpoint,
27 array(
28 array(
29 'methods' => \WP_REST_Server::READABLE,
30 'callback' => array( $this, 'get_items' ),
31 'permission_callback' => array( $this, 'get_items_permissions_check' ),
32 'args' => $this->get_clicks_schema(),
33 ),
34 )
35 );
36 register_rest_route(
37 $this->namespace,
38 $endpoint . 'get_graphs/',
39 array(
40 array(
41 'methods' => \WP_REST_Server::READABLE,
42 'callback' => array( $this, 'get_graphs' ),
43 'permission_callback' => array( $this, 'get_items_permissions_check' ),
44 'args' => $this->get_clicks_schema(),
45 ),
46 )
47 );
48
49 register_rest_route(
50 $this->namespace,
51 $endpoint . 'get_countries/',
52 array(
53 array(
54 'methods' => \WP_REST_Server::READABLE,
55 'callback' => array( $this, 'get_countries' ),
56 'permission_callback' => array( $this, 'get_items_permissions_check' ),
57 'args' => $this->get_clicks_schema(),
58 ),
59 )
60 );
61
62 register_rest_route(
63 $this->namespace,
64 $endpoint . 'get_audience/',
65 array(
66 array(
67 'methods' => \WP_REST_Server::READABLE,
68 'callback' => array( $this, 'get_audience' ),
69 'permission_callback' => array( $this, 'get_items_permissions_check' ),
70 'args' => $this->get_clicks_schema(),
71 ),
72 )
73 );
74
75 register_rest_route(
76 $this->namespace,
77 $endpoint . 'get_timing/',
78 array(
79 array(
80 'methods' => \WP_REST_Server::READABLE,
81 'callback' => array( $this, 'get_timing' ),
82 'permission_callback' => array( $this, 'get_items_permissions_check' ),
83 'args' => $this->get_clicks_schema(),
84 ),
85 )
86 );
87
88 register_rest_route(
89 $this->namespace,
90 $endpoint . '(?P<id>[\d]+)',
91 array(
92 'args' => array(
93 'id' => array(
94 'description' => __( 'Unique identifier for the object.' , 'betterlinks' ),
95 'type' => 'integer',
96 ),
97 ),
98 array(
99 'methods' => \WP_REST_Server::READABLE,
100 'callback' => array( $this, 'get_item' ),
101 'permission_callback' => array( $this, 'get_items_permissions_check' ),
102 'args' => $this->get_clicks_schema(),
103 ),
104 )
105 );
106 register_rest_route(
107 $this->namespace,
108 $endpoint . 'tags/' . '(?P<id>[\d]+)',
109 array(
110 'args' => array(
111 'id' => array(
112 'description' => __( 'Unique identifier for the object.' , 'betterlinks' ),
113 'type' => 'integer',
114 ),
115 ),
116 array(
117 'methods' => \WP_REST_Server::READABLE,
118 'callback' => array( $this, 'get_tags_analytics' ),
119 'permission_callback' => array( $this, 'get_items_permissions_check' ),
120 'args' => $this->get_clicks_schema(),
121 ),
122 )
123 );
124
125 register_rest_route(
126 $this->namespace,
127 $endpoint . 'tags/get_graphs/' . '(?P<id>[\d]+)',
128 array(
129 'args' => array(
130 'id' => array(
131 'description' => __( 'Unique identifier for the object.' , 'betterlinks' ),
132 'type' => 'integer',
133 ),
134 ),
135 array(
136 'methods' => \WP_REST_Server::READABLE,
137 'callback' => array( $this, 'get_tags_graph' ),
138 'permission_callback' => array( $this, 'get_items_permissions_check' ),
139 'args' => $this->get_clicks_schema(),
140 ),
141 )
142 );
143
144 register_rest_route(
145 $this->namespace,
146 $endpoint,
147 array(
148 array(
149 'methods' => \WP_REST_Server::CREATABLE,
150 'callback' => array( $this, 'create_item' ),
151 'permission_callback' => array( $this, 'permissions_check' ),
152 'args' => $this->get_clicks_schema(),
153 ),
154 )
155 );
156
157 register_rest_route(
158 $this->namespace,
159 $endpoint,
160 array(
161 array(
162 'methods' => \WP_REST_Server::EDITABLE,
163 'callback' => array( $this, 'update_item' ),
164 'permission_callback' => array( $this, 'permissions_check' ),
165 'args' => $this->get_clicks_schema(),
166 ),
167 )
168 );
169
170 register_rest_route(
171 $this->namespace,
172 $endpoint,
173 array(
174 array(
175 'methods' => \WP_REST_Server::DELETABLE,
176 'callback' => array( $this, 'delete_item' ),
177 'permission_callback' => array( $this, 'permissions_check' ),
178 'args' => $this->get_clicks_schema(),
179 ),
180 )
181 );
182
183 register_rest_route(
184 $this->namespace,
185 $endpoint . 'delete_by_links/',
186 array(
187 array(
188 'methods' => \WP_REST_Server::DELETABLE,
189 'callback' => array( $this, 'delete_links_analytics' ),
190 'permission_callback' => array( $this, 'permissions_check' ),
191 'args' => $this->get_clicks_schema(),
192 ),
193 )
194 );
195
196 do_action( 'betterlinks_register_clicks_routes', $this );
197 }
198
199 /**
200 * Get Analytics Graph Data
201 *
202 * @param WP_REST_Request $request Full data about the request.
203 * @return WP_Error|WP_REST_Response
204 */
205 public function get_graphs( $request ) {
206 $request = $request->get_params();
207 $from = $this->sanitize_date( $request['from'] ) ? $request['from'] : gmdate( 'Y-m-d', strtotime( ' - 30 days' ) );
208 $to = $this->sanitize_date( $request['to'] ) ? $request['to'] : gmdate( 'Y-m-d' );
209
210 $graph_data = $this->get_analytics_graph_data( $from, $to );
211 return new \WP_REST_Response(
212 array(
213 'success' => true,
214 'data' => array(
215 'clicks' => $graph_data,
216 ),
217 )
218 );
219 }
220
221 /**
222 * Get clicks aggregated by country for the range.
223 *
224 * Backs the Geography section: the choropleth map (every country) plus the
225 * Top-countries list. Rows are `{ country_code (ISO alpha-2), country_name,
226 * clicks, unique_clicks }`, already ordered by clicks desc. Countries are
227 * only recorded when extra data tracking is on, so this is simply empty on
228 * setups without it — the client renders a "not tracked" state in that case.
229 *
230 * @param WP_REST_Request $request Full data about the request.
231 * @return WP_Error|WP_REST_Response
232 */
233 public function get_countries( $request ) {
234 $request = $request->get_params();
235 $from = isset( $request['from'] ) && $this->sanitize_date( $request['from'] ) ? $request['from'] : gmdate( 'Y-m-d', strtotime( ' - 30 days' ) );
236 $to = isset( $request['to'] ) && $this->sanitize_date( $request['to'] ) ? $request['to'] : gmdate( 'Y-m-d' );
237
238 $countries = \BetterLinks\Services\CountryDetectionService::get_country_statistics( $from, $to );
239
240 return new \WP_REST_Response(
241 array(
242 'success' => true,
243 'data' => array(
244 'countries' => $countries,
245 ),
246 )
247 );
248 }
249
250 /**
251 * Get clicks bucketed by weekday and hour for the range.
252 *
253 * Backs the Timing heatmap. Rows are `{ dow (0=Mon..6=Sun), hr (0..23),
254 * clicks, unique_clicks }`; empty buckets are omitted and filled client-side.
255 *
256 * @param WP_REST_Request $request Full data about the request.
257 * @return WP_Error|WP_REST_Response
258 */
259 public function get_timing( $request ) {
260 $request = $request->get_params();
261 $from = isset( $request['from'] ) && $this->sanitize_date( $request['from'] ) ? $request['from'] : gmdate( 'Y-m-d', strtotime( ' - 30 days' ) );
262 $to = isset( $request['to'] ) && $this->sanitize_date( $request['to'] ) ? $request['to'] : gmdate( 'Y-m-d' );
263
264 $timing = $this->get_analytics_timing_data( $from, $to );
265
266 return new \WP_REST_Response(
267 array(
268 'success' => true,
269 'data' => array(
270 'timing' => $timing,
271 ),
272 )
273 );
274 }
275
276 /**
277 * Audience composition for the range — human vs bot and new vs returning.
278 *
279 * Backs the two Overview stat cards. Each half carries a `tracked` flag so
280 * the client can tell "no bots seen" from "this data predates bot tracking",
281 * and `bots_blocked` reports the Disable Bot Clicks setting, under which bot
282 * hits are never recorded and the split would read 100% human.
283 *
284 * @param WP_REST_Request $request Full data about the request.
285 * @return WP_Error|WP_REST_Response
286 */
287 public function get_audience( $request ) {
288 $request = $request->get_params();
289 $from = isset( $request['from'] ) && $this->sanitize_date( $request['from'] ) ? $request['from'] : gmdate( 'Y-m-d', strtotime( ' - 30 days' ) );
290 $to = isset( $request['to'] ) && $this->sanitize_date( $request['to'] ) ? $request['to'] : gmdate( 'Y-m-d' );
291
292 $audience = $this->get_analytics_audience_data( $from, $to );
293
294 $options = json_decode( get_option( BETTERLINKS_LINKS_OPTION_NAME ), true );
295 $audience['bots_blocked'] = ! empty( $options['disablebotclicks'] );
296
297 return new \WP_REST_Response(
298 array(
299 'success' => true,
300 'data' => array(
301 'audience' => $audience,
302 ),
303 )
304 );
305 }
306
307 /**
308 * Get analytics graph data by Tag ID
309 *
310 * @param WP_REST_Request $request Full data about the request.
311 * @return WP_Error|WP_REST_Response
312 */
313 public function get_tags_graph( $request ) {
314 $request = $request->get_params();
315 $from = $this->sanitize_date( $request['from']) ? $request['from'] : '';
316 $to = $this->sanitize_date( $request['to']) ? $request['to'] : '';
317 $id = isset( $request['id'] ) ? $request['id'] : '';
318
319 if( empty( $from ) || empty( $to ) ) {
320 return new \WP_REST_Response(
321 array(
322 'success' => false,
323 'message' => __( "Invalid date range provided.", 'betterlinks' ),
324 ),
325 400
326 );
327 }
328
329 $results = $this->get_analytics_graph_data_by_tag( $from, $to, $id );
330
331 return new \WP_REST_Response(
332 array(
333 'success' => true,
334 'data' => $results,
335 ),
336 200
337 );
338 }
339
340 /**
341 * Get unique analytics list by Tag ID
342 *
343 * @param WP_REST_Request $request Full data about the request.
344 * @return WP_Error|WP_REST_Response
345 */
346 public function get_tags_analytics( $request ) {
347 $request = $request->get_params();
348
349 $from = $this->sanitize_date( $request['from'] ) ? $request['from'] : '';
350 $to = $this->sanitize_date( $request['to'] ) ? $request['to'] : '';
351 $id = isset( $request['id'] ) ? $request['id'] : '';
352
353 if( empty( $from ) || empty( $to ) ) {
354 return new \WP_REST_Response(
355 array(
356 'success' => false,
357 'message' => __( "Invalid date range provided.", 'betterlinks' ),
358 ),
359 400
360 );
361 }
362
363 $results = $this->get_analytics_unique_list_by_tag( $from, $to, $id );
364
365 $analytic = get_option( 'betterlinks_analytics_data' );
366 $analytic = $analytic ? json_decode( $analytic, true ) : array();
367
368 return new \WP_REST_Response(
369 array(
370 'success' => true,
371 'data' => array(
372 'list' => $results,
373 'analytic' => $analytic,
374 ),
375 ),
376 200
377 );
378 }
379
380 /**
381 * Get betterlinks
382 *
383 * @param WP_REST_Request $request Full data about the request.
384 * @return WP_Error|WP_REST_Response
385 */
386 public function get_items( $request ) {
387 $request = $request->get_params();
388
389 $from = isset($request['from']) && $this->sanitize_date( $request['from'] ) ? $request['from'] : '';
390 $to = isset($request['to']) && $this->sanitize_date( $request['to'] ) ? $request['to'] : '';
391
392 if( empty( $from ) || empty( $to ) ) {
393 return new \WP_REST_Response(
394 array(
395 'success' => false,
396 'data' => [],
397 'message' => __( "Invalid date range provided.", 'betterlinks' ),
398 ),
399 400
400 );
401 }
402
403 $unique_list = $this->get_analytics_unique_list( $from, $to );
404 $unique_click_count = $this->get_unique_clicks_count($from, $to);
405
406 // $analytic = get_option( 'betterlinks_analytics_data' );
407 $analytic = $this->get_analytics_data($from, $to);
408 $analytic = $analytic ? json_decode( $analytic, true ) : array();
409
410 return new \WP_REST_Response(
411 array(
412 'success' => true,
413 'data' => array(
414 'unique_list' => $unique_list,
415 'unique_count' => $unique_click_count,
416 'analytic' => $analytic,
417 ),
418 ),
419 200
420 );
421 }
422
423
424 /**
425 * Get Individual Clicks
426 *
427 * @param WP_Rest_Request $request
428 * @return WP_Error|WP_Rest_Response
429 */
430 public function get_item( $request ) {
431 $request = $request->get_params();
432
433 $id = ! empty( $request['id'] ) ? sanitize_text_field( $request['id'] ) : null;
434 $from = $this->sanitize_date( $request['from'] ) ? sanitize_text_field( $request['from'] ) : '';
435 $to = $this->sanitize_date( $request['to'] ) ? sanitize_text_field( $request['to'] ) : '';
436
437 if( empty( $from ) || empty( $to ) ) {
438 return new \WP_REST_Response(
439 array(
440 'success' => false,
441 'message' => __( "Invalid date range provided.", 'betterlinks' ),
442 ),
443 400
444 );
445 }
446
447 $results = $this->get_individual_analytics_clicks( $id, $from, $to );
448 $link_details = $this->get_individual_link_details( $id );
449 // The daily series is a plain count of this link's clicks, so free computes
450 // it too; Pro may still replace it through the filter below.
451 $graph_data = $this->get_individual_graph_data( $id, $from, $to );
452 $graph_data = apply_filters( 'betterlinkspro/get_individual_graph_data', $graph_data, $id, $from, $to );
453
454 return new \WP_REST_Response(
455 array(
456 'data' => array(
457 'analytics' => $results,
458 'graph_data' => $graph_data,
459 'link_details' => $link_details,
460 ),
461 'id' => $id,
462 ),
463 200
464 );
465 }
466
467 /**
468 * Create OR Update betterlinks
469 *
470 * @param WP_REST_Request $request Full data about the request.
471 * @return WP_Error|WP_REST_Request
472 */
473 public function create_item( $request ) {
474 return new \WP_REST_Response(
475 array(
476 'success' => false,
477 'data' => array(),
478 ),
479 200
480 );
481 }
482
483 /**
484 * Create OR Update betterlinks
485 *
486 * @param WP_REST_Request $request Full data about the request.
487 * @return WP_Error|WP_REST_Request
488 */
489 public function update_item( $request ) {
490 return new \WP_REST_Response(
491 array(
492 'success' => false,
493 'data' => array(),
494 ),
495 200
496 );
497 }
498
499 /**
500 * Delete betterlinks clicks
501 *
502 * @param WP_REST_Request $request Full data about the request.
503 * @return WP_Error|WP_REST_Request
504 */
505 public function delete_item( $request ) {
506 $params = $request->get_params();
507
508 $click_ids_raw = isset( $params['click_ids'] ) ? $params['click_ids'] : '';
509 $link_id = isset( $params['link_id'] ) ? intval( $params['link_id'] ) : 0;
510
511 // Sanitize and parse click IDs
512 $click_ids = array_filter( array_map( 'intval', explode( ',', sanitize_text_field( $click_ids_raw ) ) ) );
513
514 if ( empty( $click_ids ) || empty( $link_id ) ) {
515 return new \WP_REST_Response(
516 array(
517 'success' => false,
518 'message' => __( 'Invalid click IDs or link ID provided.', 'betterlinks' ),
519 ),
520 400
521 );
522 }
523
524 // Get date range from params
525 $from = isset( $params['from'] ) ? sanitize_text_field( $params['from'] ) : gmdate( 'Y-m-d', strtotime( ' - 30 days' ) );
526 $to = isset( $params['to'] ) ? sanitize_text_field( $params['to'] ) : gmdate( 'Y-m-d' );
527
528 // Delete the clicks from the database - only within the date range
529 global $wpdb;
530 $table_name = $wpdb->prefix . 'betterlinks_clicks';
531
532 foreach ( $click_ids as $click_id ) {
533 $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
534 $table_name,
535 array(
536 'ID' => intval( $click_id ),
537 'link_id' => $link_id,
538 ),
539 array( '%d', '%d' )
540 );
541 }
542
543 // Clear all related transient caches for this link's analytics
544 $transient_key = 'btl_individual_analytics_clicks_' . md5( $from . $to . $link_id );
545 delete_transient( $transient_key );
546
547 // Also clear graph data cache
548 $graph_transient_key = 'btl_individual_graph_data_' . md5( $from . $to . $link_id );
549 delete_transient( $graph_transient_key );
550
551 // Clear all analytics caches to be safe
552 \BetterLinks\Helper::clear_analytics_cache();
553
554 // Update the betterlinks_analytics_data option with fresh data from database
555 \BetterLinks\Helper::update_links_analytics();
556
557 // Clear links cache so that page refresh shows updated analytics
558 delete_transient( BETTERLINKS_CACHE_LINKS_NAME );
559
560 $results = $this->get_individual_analytics_clicks( $link_id, $from, $to );
561 $link_details = $this->get_individual_link_details( $link_id );
562 $graph_data = $this->get_individual_graph_data( $link_id, $from, $to );
563 $graph_data = apply_filters( 'betterlinkspro/get_individual_graph_data', $graph_data, $link_id, $from, $to );
564
565 return new \WP_REST_Response(
566 array(
567 'success' => true,
568 'data' => array(
569 'analytics' => $results,
570 'graph_data' => $graph_data,
571 'link_details' => $link_details,
572 ),
573 'id' => $link_id,
574 ),
575 200
576 );
577 }
578
579 /**
580 * Check if a given request has access to update a setting
581 *
582 * @param WP_REST_Request $request Full data about the request.
583 * @return WP_Error|bool
584 */
585 public function get_items_permissions_check( $request ) {
586 return apply_filters( 'betterlinks/api/analytics_items_permissions_check', current_user_can( 'manage_options' ) );
587 }
588 /**
589 * Delete analytics for multiple links
590 *
591 * @param WP_REST_Request $request Full data about the request.
592 * @return WP_Error|WP_REST_Response
593 */
594 public function delete_links_analytics( $request ) {
595 $params = $request->get_params();
596
597 $link_ids_raw = isset( $params['link_ids'] ) ? $params['link_ids'] : '';
598
599 // Sanitize and parse link IDs
600 $link_ids = array_filter( array_map( 'intval', explode( ',', sanitize_text_field( $link_ids_raw ) ) ) );
601
602 if ( empty( $link_ids ) ) {
603 return new \WP_REST_Response(
604 array(
605 'success' => false,
606 'message' => __( 'Invalid link IDs provided.', 'betterlinks' ),
607 ),
608 400
609 );
610 }
611
612 // Get date range from params
613 $from = isset( $params['from'] ) ? sanitize_text_field( $params['from'] ) : gmdate( 'Y-m-d', strtotime( ' - 30 days' ) );
614 $to = isset( $params['to'] ) ? sanitize_text_field( $params['to'] ) : gmdate( 'Y-m-d' );
615
616 // Delete clicks for the specified links ONLY within the date range
617 global $wpdb;
618 $table_name = $wpdb->prefix . 'betterlinks_clicks';
619
620 // $table_name is {$wpdb->prefix}betterlinks_clicks (wpdb-controlled); values are placeholdered.
621 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
622 foreach ( $link_ids as $link_id ) {
623 $wpdb->query(
624 $wpdb->prepare(
625 "DELETE FROM {$table_name} WHERE link_id = %d AND DATE(created_at) >= %s AND DATE(created_at) <= %s",
626 intval( $link_id ),
627 $from,
628 $to
629 )
630 );
631 }
632 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
633
634 // Clear all related transient caches for all deleted links
635 foreach ( $link_ids as $link_id ) {
636 $transient_key = 'btl_individual_analytics_clicks_' . md5( $from . $to . $link_id );
637 delete_transient( $transient_key );
638
639 $graph_transient_key = 'btl_individual_graph_data_' . md5( $from . $to . $link_id );
640 delete_transient( $graph_transient_key );
641 }
642
643 // Clear all analytics caches
644 \BetterLinks\Helper::clear_analytics_cache();
645
646 // Update the betterlinks_analytics_data option with fresh data from database
647 \BetterLinks\Helper::update_links_analytics();
648
649 // Clear links cache so that page refresh shows updated analytics
650 delete_transient( BETTERLINKS_CACHE_LINKS_NAME );
651
652 // Fetch updated analytics data for the link list
653 $unique_list = $this->get_analytics_unique_list( $from, $to );
654 $unique_click_count = $this->get_unique_clicks_count( $from, $to );
655 $analytic = $this->get_analytics_data( $from, $to );
656 $analytic = $analytic ? json_decode( $analytic, true ) : array();
657
658 return new \WP_REST_Response(
659 array(
660 'success' => true,
661 'data' => array(
662 'unique_list' => $unique_list,
663 'unique_count' => $unique_click_count,
664 'analytic' => $analytic,
665 ),
666 ),
667 200
668 );
669 }
670
671 /**
672 * Check if a given request has access to update a setting
673 *
674 * @param WP_REST_Request $request Full data about the request.
675 * @return WP_Error|bool
676 */
677 public function permissions_check( $request ) {
678 return current_user_can( 'manage_options' );
679 }
680 }
681