PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.28.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.28.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.28.0, at includes/admin/class-focus-keyword-ajax.php

191 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 self::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 * Static so sibling handlers (e.g. Seo_Quick_Edit_Ajax) can reuse it.
155 *
156 * @param int $post_id
157 * @return bool
158 */
159 public static function update_post_modified( $post_id ) {
160
161 if ( ! $post_id || ! get_post( $post_id ) ) {
162 return false;
163 }
164
165 global $wpdb;
166
167 $now = current_time( 'mysql' );
168 $now_gmt = current_time( 'mysql', true );
169
170 // Update database directly
171 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Direct update to set timestamps without triggering save hooks.
172 $updated = $wpdb->update(
173 $wpdb->posts,
174 [
175 'post_modified' => $now,
176 'post_modified_gmt' => $now_gmt,
177 ],
178 [ 'ID' => $post_id ],
179 [ '%s', '%s' ],
180 [ '%d' ]
181 );
182
183 // Clear cache so WordPress picks up new values
184 clean_post_cache( $post_id );
185
186 return ( $updated !== false );
187 }
188
189 }
190
191