PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.26.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.26.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / admin / class-focus-keyword-ajax.php

class-focus-keyword-ajax.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.26.0, at includes/admin/class-focus-keyword-ajax.php

189 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Focus Keyword AJAX Handler
4 *
5 * Handles AJAX requests for inline editing of focus keywords in post list
6 *
7 * @package ThinkRank\Admin
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\Admin;
14
15 // Prevent direct access
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20 /**
21 * Focus Keyword AJAX Handler Class
22 *
23 * Single Responsibility: Handle AJAX operations for focus keyword updates
24 *
25 * @since 1.0.0
26 */
27 class Focus_Keyword_Ajax {
28
29 /**
30 * AJAX action name
31 *
32 * @var string
33 */
34 private const AJAX_ACTION = 'thinkrank_update_focus_keyword';
35
36 /**
37 * Nonce action name
38 *
39 * @var string
40 */
41 private const NONCE_ACTION = 'thinkrank_focus_keyword_nonce';
42
43 /**
44 * Initialize AJAX handler
45 *
46 * @return void
47 */
48 public function init(): void {
49 add_action('wp_ajax_' . self::AJAX_ACTION, [$this, 'handle_update_focus_keyword']);
50 }
51
52 /**
53 * Get nonce action name
54 *
55 * @return string
56 */
57 public static function get_nonce_action(): string {
58 return self::NONCE_ACTION;
59 }
60
61 /**
62 * Get AJAX action name
63 *
64 * @return string
65 */
66 public static function get_ajax_action(): string {
67 return self::AJAX_ACTION;
68 }
69
70 /**
71 * Handle AJAX request to update focus keyword
72 *
73 * @return void
74 */
75 public function handle_update_focus_keyword(): void {
76 // Verify nonce
77 if (!check_ajax_referer(self::NONCE_ACTION, 'nonce', false)) {
78 wp_send_json_error([
79 'message' => __('Security check failed. Please refresh the page and try again.', 'thinkrank')
80 ], 403);
81 }
82
83 // Check user capabilities
84 if (!current_user_can('edit_posts')) {
85 wp_send_json_error([
86 'message' => __('You do not have permission to perform this action.', 'thinkrank')
87 ], 403);
88 }
89
90 // Get and validate post ID
91 $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
92
93 if (!$post_id) {
94 wp_send_json_error([
95 'message' => __('Invalid post ID.', 'thinkrank')
96 ], 400);
97 }
98
99 // Check if user can edit this specific post
100 if (!current_user_can('edit_post', $post_id)) {
101 wp_send_json_error([
102 'message' => __('You do not have permission to edit this post.', 'thinkrank')
103 ], 403);
104 }
105
106 // The SEO Overview column edits only the PRIMARY focus keyword. Take the
107 // new primary from the input (first value if a list is pasted) and keep
108 // the existing secondary keywords so inline editing never drops them.
109 $raw_input = isset($_POST['focus_keyword']) ? wp_unslash($_POST['focus_keyword']) : '';
110 $new_primary = \ThinkRank\SEO\Focus_Keywords::normalize($raw_input, 1);
111
112 $existing = \ThinkRank\SEO\Focus_Keywords::get($post_id);
113 $secondary = array_slice($existing, 1);
114
115 // Empty input drops the primary and promotes the next keyword; otherwise
116 // the new primary leads, followed by the untouched secondaries.
117 $new_list = empty($new_primary)
118 ? $secondary
119 : array_merge($new_primary, $secondary);
120
121 // Persist via the helper (normalizes + keeps legacy meta in sync, and
122 // preserves any gated overflow on free).
123 $keywords = \ThinkRank\SEO\Focus_Keywords::save($post_id, $new_list);
124
125 // Only the primary keyword is shown and edited in the column.
126 $primary_keyword = $keywords[0] ?? '';
127
128 // Prepare response data
129 $response_data = [
130 'message' => __('Focus keyword updated successfully.', 'thinkrank'),
131 'focus_keyword' => $primary_keyword,
132 'display_value' => !empty($keywords) ? $primary_keyword : __('Not Set', 'thinkrank'),
133 'has_keyword' => !empty($keywords)
134 ];
135
136 // update post modified time
137 $this->update_post_modified( $post_id );
138
139 /**
140 * Filter the AJAX response data for focus keyword update
141 *
142 * @param array $response_data Response data
143 * @param int $post_id Post ID
144 * @param string $focus_keyword Updated focus keyword
145 */
146 $response_data = apply_filters('thinkrank_focus_keyword_ajax_response', $response_data, $post_id, $primary_keyword);
147
148 wp_send_json_success($response_data);
149 }
150
151 /**
152 * Force update post_modified and post_modified_gmt timestamps.
153 *
154 * @param int $post_id
155 * @return bool
156 */
157 function update_post_modified( $post_id ) {
158
159 if ( ! $post_id || ! get_post( $post_id ) ) {
160 return false;
161 }
162
163 global $wpdb;
164
165 $now = current_time( 'mysql' );
166 $now_gmt = current_time( 'mysql', true );
167
168 // Update database directly
169 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Direct update to set timestamps without triggering save hooks.
170 $updated = $wpdb->update(
171 $wpdb->posts,
172 [
173 'post_modified' => $now,
174 'post_modified_gmt' => $now_gmt,
175 ],
176 [ 'ID' => $post_id ],
177 [ '%s', '%s' ],
178 [ '%d' ]
179 );
180
181 // Clear cache so WordPress picks up new values
182 clean_post_cache( $post_id );
183
184 return ( $updated !== false );
185 }
186
187 }
188
189