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

190 lines 5.5 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 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- normalize() sanitize_text_field()s every value it keeps.
110 $raw_input = isset($_POST['focus_keyword']) ? wp_unslash($_POST['focus_keyword']) : '';
111 $new_primary = \ThinkRank\SEO\Focus_Keywords::normalize($raw_input, 1);
112
113 $existing = \ThinkRank\SEO\Focus_Keywords::get($post_id);
114 $secondary = array_slice($existing, 1);
115
116 // Empty input drops the primary and promotes the next keyword; otherwise
117 // the new primary leads, followed by the untouched secondaries.
118 $new_list = empty($new_primary)
119 ? $secondary
120 : array_merge($new_primary, $secondary);
121
122 // Persist via the helper (normalizes + keeps legacy meta in sync, and
123 // preserves any gated overflow on free).
124 $keywords = \ThinkRank\SEO\Focus_Keywords::save($post_id, $new_list);
125
126 // Only the primary keyword is shown and edited in the column.
127 $primary_keyword = $keywords[0] ?? '';
128
129 // Prepare response data
130 $response_data = [
131 'message' => __('Focus keyword updated successfully.', 'thinkrank'),
132 'focus_keyword' => $primary_keyword,
133 'display_value' => !empty($keywords) ? $primary_keyword : __('Not Set', 'thinkrank'),
134 'has_keyword' => !empty($keywords)
135 ];
136
137 // update post modified time
138 self::update_post_modified( $post_id );
139
140 /**
141 * Filter the AJAX response data for focus keyword update
142 *
143 * @param array $response_data Response data
144 * @param int $post_id Post ID
145 * @param string $focus_keyword Updated focus keyword
146 */
147 $response_data = apply_filters('thinkrank_focus_keyword_ajax_response', $response_data, $post_id, $primary_keyword);
148
149 wp_send_json_success($response_data);
150 }
151
152 /**
153 * Force update post_modified and post_modified_gmt timestamps.
154 *
155 * Static so sibling handlers (e.g. Seo_Quick_Edit_Ajax) can reuse it.
156 *
157 * @param int $post_id
158 * @return bool
159 */
160 public static function update_post_modified( $post_id ) {
161
162 if ( ! $post_id || ! get_post( $post_id ) ) {
163 return false;
164 }
165
166 global $wpdb;
167
168 $now = current_time( 'mysql' );
169 $now_gmt = current_time( 'mysql', true );
170
171 // Update database directly
172 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Direct update to set timestamps without triggering save hooks.
173 $updated = $wpdb->update(
174 $wpdb->posts,
175 [
176 'post_modified' => $now,
177 'post_modified_gmt' => $now_gmt,
178 ],
179 [ 'ID' => $post_id ],
180 [ '%s', '%s' ],
181 [ '%d' ]
182 );
183
184 // Clear cache so WordPress picks up new values
185 clean_post_cache( $post_id );
186
187 return ( $updated !== false );
188 }
189 }
190