PluginProbe
Arvow AI SEO Writer / trunk
Arvow AI SEO Writer vtrunk
1.5.4 trunk 1.0.0 1.0.1 1.1.0 1.1.1 1.2.0 1.3.0 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3
journalist-ai / journalist-ai.php

journalist-ai.php in Arvow AI SEO Writer trunk, at journalist-ai.php

494 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @link https://arvow.com/
4 * @since 1.1.0
5 * @package Arvow
6 *
7 * @wordpress-plugin
8 * Plugin Name: Arvow AI SEO Writer
9 * Description: Arvow - AI SEO writer for WordPress.
10 * Version: 1.5.4
11 * Author: Arvow
12 * Author URI: https://arvow.com/
13 * License: GPL-2.0 or later
14 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
15 */
16
17 if (!defined('WPINC')) {
18 die;
19 }
20
21 if (!class_exists('JournalistAI')) {
22 class JournalistAI
23 {
24 const REST_VERSION = 1;
25 const SECRET_OPTION = 'journalistai_secret';
26
27 public function __construct()
28 {
29 add_action('rest_api_init', [$this, 'register_rest_route']);
30
31 if (is_admin()) {
32 add_filter('plugin_action_links_' . plugin_basename(__FILE__), [$this, 'add_settings_link']);
33 add_action('admin_menu', [$this, 'add_plugin_page']);
34 // New explicit handlers
35 add_action('admin_post_journalistai_update_secret', [$this, 'handle_update_secret']);
36 add_action('admin_post_journalistai_connect', [$this, 'handle_connect']);
37 }
38 }
39
40 public function register_rest_route(): void
41 {
42 register_rest_route('journalistai/v' . self::REST_VERSION, '/webhook', [
43 'methods' => 'POST',
44 'callback' => [$this, 'handle_webhook'],
45 'permission_callback' => '__return_true',
46 ]);
47 }
48
49 public function handle_webhook($request): WP_REST_Response
50 {
51 $event = $request->get_param('event');
52 $received_secret_header = $request->get_header('x-secret');
53 $received_secret_param = $request->get_param('secret');
54 $stored_secret = get_option(self::SECRET_OPTION, '');
55
56 if (
57 !is_string($stored_secret)
58 || $stored_secret === ''
59 || (
60 !$this->secrets_match($stored_secret, $received_secret_header)
61 && !$this->secrets_match($stored_secret, $received_secret_param)
62 )
63 ) {
64 return new WP_REST_Response('Unauthorized', 401);
65 }
66
67 switch ($event) {
68 case 'integration_created':
69 return new WP_REST_Response('Integration successful', 200);
70
71 case 'get_data':
72 return new WP_REST_Response([
73 'api_version' => self::REST_VERSION,
74 'authors' => $this->get_available_authors(),
75 'categories' => $this->get_available_categories(),
76 'tags' => $this->get_available_tags()
77 ], 200);
78
79 case 'create_post':
80 $title = $request->get_param('payload')['title'];
81 $content = $request->get_param('payload')['content'];
82 $thumbnail = $request->get_param('payload')['thumbnail'] ?? null;
83 $thumbnail_alt = $request->get_param('payload')['thumbnail_alt'] ?? '';
84 $status = $request->get_param('payload')['status'] ?? 'publish';
85 $metadescription = $request->get_param('payload')['metadescription'] ?? null;
86 $keyword = $request->get_param('payload')['keyword'] ?? null;
87 $elementor = $request->get_param('payload')['elementor'] ?? false;
88 $elementor_json = $request->get_param('payload')['elementor_json'] ?? null;
89 $post_type = $request->get_param('payload')['post_type'] ?? 'post';
90 $use_keyword_slug = $request->get_param('payload')['use_keyword_slug'] ?? false;
91 $author = $request->get_param('payload')['author'] ?? null;
92 $categories = $request->get_param('payload')['categories'] ?? [];
93 $tags = $request->get_param('payload')['tags'] ?? [];
94
95 if (empty($title) || empty($content)) {
96 return new WP_REST_Response('Invalid payload: title and content are required', 400);
97 }
98
99 // Validate status
100 $valid_statuses = ['publish', 'draft', 'pending', 'private'];
101 if (!in_array($status, $valid_statuses)) {
102 $status = 'publish'; // Default to publish if invalid status
103 }
104
105 $post_data = array(
106 'post_title' => sanitize_text_field($title),
107 'post_content' => wp_kses_post($content),
108 'post_status' => $status,
109 'post_type' => $post_type
110 );
111
112 // Set author if provided
113 if (!empty($author)) {
114 $post_data['post_author'] = (int)$author;
115 }
116
117 // Set categories if provided
118 if (!empty($categories) && is_array($categories)) {
119 $post_data['post_category'] = array_map('intval', $categories);
120 }
121
122 // Set tags if provided
123 if (!empty($tags) && is_array($tags)) {
124 $post_data['tags_input'] = array_map('intval', $tags);
125 }
126
127 // Generate slug from keyword if use_keyword_slug is enabled
128 if ($use_keyword_slug && !empty($keyword)) {
129 $post_data['post_name'] = sanitize_title($keyword);
130 }
131
132 $post_id = wp_insert_post($post_data);
133
134 if (is_wp_error($post_id)) {
135 return new WP_REST_Response('Failed to create post: ' . $post_id->get_error_message(), 500);
136 }
137
138 // Handle thumbnail if provided
139 if (!empty($thumbnail)) {
140 require_once(ABSPATH . 'wp-admin/includes/media.php');
141 require_once(ABSPATH . 'wp-admin/includes/file.php');
142 require_once(ABSPATH . 'wp-admin/includes/image.php');
143
144 // Download and attach the image
145 $tmp = download_url($thumbnail);
146 if (is_wp_error($tmp)) {
147 // Use proper WordPress logging
148 if (WP_DEBUG) {
149 error_log('Arvow: Failed to download image: ' . esc_html($tmp->get_error_message()));
150 }
151 } else {
152 $file_array = array(
153 'name' => basename($thumbnail),
154 'tmp_name' => $tmp
155 );
156
157 $attachment_id = media_handle_sideload($file_array, $post_id);
158 if (is_wp_error($attachment_id)) {
159 if (WP_DEBUG) {
160 error_log('Arvow: Failed to process image: ' . esc_html($attachment_id->get_error_message()));
161 }
162 } else {
163 set_post_thumbnail($post_id, $attachment_id);
164
165 // Set alt text if provided
166 if (!empty($thumbnail_alt)) {
167 update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($thumbnail_alt));
168 }
169 }
170
171 // Clean up temp file using WordPress function
172 wp_delete_file($tmp);
173 }
174 }
175
176 // Set Elementor data if enabled and available
177 if ($elementor && !empty($elementor_json)) {
178 $this->set_elementor_data($post_id, $elementor_json);
179 }
180
181 // Set SEO metadata for popular SEO plugins
182 $this->set_seo_metadata($post_id, $title, $metadescription, $keyword);
183
184 $post_url = get_permalink($post_id);
185 return new WP_REST_Response(array(
186 'message' => 'Post created successfully',
187 'post_id' => $post_id,
188 'url' => $post_url
189 ), 200);
190
191 default:
192 return new WP_REST_Response('Event not recognized', 400);
193 }
194 }
195
196 /**
197 * Compares a stored secret with a client-supplied credential.
198 */
199 private function secrets_match(string $stored_secret, $received_secret): bool
200 {
201 return is_string($received_secret) && hash_equals($stored_secret, $received_secret);
202 }
203
204 /**
205 * Ensures the webhook has a secret before it can receive requests.
206 */
207 public static function activate(): void
208 {
209 $existing_secret = get_option(self::SECRET_OPTION, '');
210
211 if (!is_string($existing_secret) || $existing_secret === '') {
212 update_option(self::SECRET_OPTION, wp_generate_password(32, false));
213 }
214 }
215
216 /**
217 * Updates the stored secret from the settings form
218 */
219 public function handle_update_secret(): void
220 {
221 check_admin_referer('journalistai_update_secret_action', 'journalistai_update_secret_nonce');
222
223 $provided_secret = isset($_POST['secret']) ? sanitize_text_field(wp_unslash($_POST['secret'])) : '';
224 if (empty($provided_secret) || strlen($provided_secret) < 8) {
225 add_settings_error(
226 'journalistai_secret',
227 'journalistai_secret_short',
228 'Secret must be at least 8 characters long.',
229 'error'
230 );
231 set_transient('settings_errors', get_settings_errors(), 30);
232 wp_safe_redirect(admin_url('options-general.php?page=journalist-ai-setting-admin&settings-updated=false'));
233 exit;
234 }
235
236 update_option(self::SECRET_OPTION, $provided_secret);
237 add_settings_error('journalistai', 'settings_updated', 'Secret updated.', 'updated');
238 set_transient('settings_errors', get_settings_errors(), 30);
239 wp_safe_redirect(admin_url('options-general.php?page=journalist-ai-setting-admin&settings-updated=true'));
240 exit;
241 }
242
243 /**
244 * Connects the site to Arvow using the current stored secret
245 */
246 public function handle_connect(): void
247 {
248 check_admin_referer('journalistai_connect_action', 'journalistai_connect_nonce');
249 $existing_secret = get_option(self::SECRET_OPTION);
250 $secret = $existing_secret;
251 if (empty($existing_secret)) {
252 // generate if none existed
253 $secret = wp_generate_password(32, false);
254 update_option(self::SECRET_OPTION, $secret);
255 }
256
257 $webhook_url = esc_url_raw(rest_url('journalistai/v' . self::REST_VERSION . '/webhook'));
258 $redirect_url = add_query_arg(
259 array(
260 'webhook_url' => urlencode($webhook_url),
261 'secret' => urlencode($secret)
262 ),
263 'https://api.arvow.com/wp-plugin/authorize'
264 );
265
266 wp_redirect($redirect_url);
267 exit;
268 }
269
270 public function add_settings_link($links): array
271 {
272 $settings_link = '<a href="' . esc_url(admin_url('options-general.php?page=journalist-ai-setting-admin')) . '">Settings</a>';
273 array_unshift($links, $settings_link);
274 return $links;
275 }
276
277 public function add_plugin_page(): void
278 {
279 add_options_page(
280 'Arvow Settings',
281 'Arvow',
282 'manage_options',
283 'journalist-ai-setting-admin',
284 [$this, 'create_admin_page']
285 );
286 }
287
288 public function create_admin_page(): void
289 {
290 // Ensure there is a secret generated to pre-fill the form
291 $existing = get_option(self::SECRET_OPTION);
292 if (empty($existing)) {
293 $generated = wp_generate_password(32, false);
294 update_option(self::SECRET_OPTION, $generated);
295 }
296 $template_path = plugin_dir_path(__FILE__) . 'admin-settings.php';
297 if (file_exists($template_path)) {
298 include $template_path;
299 }
300 }
301
302 /**
303 * Set SEO metadata for popular SEO plugins
304 */
305 private function set_seo_metadata($post_id, $title, $metadescription, $keyword): void
306 {
307 if (empty($metadescription) && empty($keyword)) {
308 return; // Nothing to set
309 }
310
311 // Ensure plugin functions are available
312 if (!function_exists('is_plugin_active')) {
313 require_once(ABSPATH . 'wp-admin/includes/plugin.php');
314 }
315
316 // Check which SEO plugins are active and set appropriate meta fields
317
318 // Yoast SEO
319 if (is_plugin_active('wordpress-seo/wp-seo.php') || is_plugin_active('wordpress-seo-premium/wp-seo-premium.php')) {
320 if (!empty($title)) {
321 update_post_meta($post_id, '_yoast_wpseo_title', sanitize_text_field($title));
322 }
323 if (!empty($metadescription)) {
324 update_post_meta($post_id, '_yoast_wpseo_metadesc', sanitize_text_field($metadescription));
325 }
326 if (!empty($keyword)) {
327 update_post_meta($post_id, '_yoast_wpseo_focuskw', sanitize_text_field($keyword));
328 }
329 }
330
331 // All in One SEO Pack
332 if (is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php') || is_plugin_active('all-in-one-seo-pack-pro/all_in_one_seo_pack.php')) {
333 if (!empty($title)) {
334 update_post_meta($post_id, '_aioseo_title', sanitize_text_field($title));
335 update_post_meta($post_id, '_aioseo_og_title', sanitize_text_field($title));
336 }
337 if (!empty($metadescription)) {
338 update_post_meta($post_id, '_aioseo_description', sanitize_text_field($metadescription));
339 update_post_meta($post_id, '_aioseo_og_description', sanitize_text_field($metadescription));
340 }
341 if (!empty($keyword)) {
342 update_post_meta($post_id, '_aioseo_keywords', sanitize_text_field($keyword));
343 }
344 }
345
346 // Rank Math SEO
347 if (is_plugin_active('seo-by-rank-math/rank-math.php')) {
348 if (!empty($title)) {
349 update_post_meta($post_id, 'rank_math_title', sanitize_text_field($title));
350 }
351 if (!empty($metadescription)) {
352 update_post_meta($post_id, 'rank_math_description', sanitize_text_field($metadescription));
353 }
354 if (!empty($keyword)) {
355 update_post_meta($post_id, 'rank_math_focus_keyword', sanitize_text_field($keyword));
356 }
357 }
358
359 // SEOPress
360 if (is_plugin_active('wp-seopress/seopress.php')) {
361 if (!empty($title)) {
362 update_post_meta($post_id, '_seopress_titles_title', sanitize_text_field($title));
363 update_post_meta($post_id, '_seopress_social_fb_title', sanitize_text_field($title));
364 update_post_meta($post_id, '_seopress_social_twitter_title', sanitize_text_field($title));
365 }
366 if (!empty($metadescription)) {
367 update_post_meta($post_id, '_seopress_titles_desc', sanitize_text_field($metadescription));
368 update_post_meta($post_id, '_seopress_social_fb_desc', sanitize_text_field($metadescription));
369 update_post_meta($post_id, '_seopress_social_twitter_desc', sanitize_text_field($metadescription));
370 }
371 if (!empty($keyword)) {
372 update_post_meta($post_id, '_seopress_analysis_target_kw', sanitize_text_field($keyword));
373 }
374 }
375
376 // The SEO Framework
377 if (is_plugin_active('autodescription/autodescription.php')) {
378 if (!empty($title)) {
379 update_post_meta($post_id, '_genesis_title', sanitize_text_field($title));
380 update_post_meta($post_id, '_open_graph_title', sanitize_text_field($title));
381 update_post_meta($post_id, '_twitter_title', sanitize_text_field($title));
382 }
383 if (!empty($metadescription)) {
384 update_post_meta($post_id, '_genesis_description', sanitize_text_field($metadescription));
385 update_post_meta($post_id, '_open_graph_description', sanitize_text_field($metadescription));
386 update_post_meta($post_id, '_twitter_description', sanitize_text_field($metadescription));
387 }
388 }
389
390 // Squirrly SEO
391 if (is_plugin_active('squirrly-seo/squirrly.php')) {
392 if (!empty($title)) {
393 update_post_meta($post_id, '_sq_title', sanitize_text_field($title));
394 }
395 if (!empty($metadescription)) {
396 update_post_meta($post_id, '_sq_description', sanitize_text_field($metadescription));
397 }
398 if (!empty($keyword)) {
399 update_post_meta($post_id, '_sq_keywords', sanitize_text_field($keyword));
400 }
401 }
402 }
403
404 /**
405 * Set Elementor data for posts when Elementor is enabled
406 */
407 private function set_elementor_data($post_id, $elementor_json): void
408 {
409 // Ensure plugin functions are available
410 if (!function_exists('is_plugin_active')) {
411 require_once(ABSPATH . 'wp-admin/includes/plugin.php');
412 }
413
414 // Only proceed if Elementor plugin is active
415 if (is_plugin_active('elementor/elementor.php')) {
416 // Set required Elementor meta fields
417 update_post_meta($post_id, '_elementor_edit_mode', 'builder');
418 update_post_meta($post_id, '_elementor_template_type', 'wp-post');
419 update_post_meta($post_id, '_elementor_data', $elementor_json);
420
421 // Get and set Elementor version if available
422 if (function_exists('get_plugin_data')) {
423 $elementor_plugin_file = WP_PLUGIN_DIR . '/elementor/elementor.php';
424 if (file_exists($elementor_plugin_file)) {
425 $elementor_data = get_plugin_data($elementor_plugin_file);
426 if (isset($elementor_data['Version'])) {
427 update_post_meta($post_id, '_elementor_version', sanitize_text_field($elementor_data['Version']));
428 }
429 }
430 }
431 }
432 }
433
434 /**
435 * Get available authors for the site
436 */
437 private function get_available_authors(): array
438 {
439 if (!function_exists('get_users')) {
440 return [];
441 }
442
443 $users = get_users([
444 'role__in' => ['administrator', 'author', 'editor'],
445 'fields' => ['ID', 'display_name']
446 ]);
447
448 return array_map(function($user) {
449 return [
450 'id' => (int)$user->ID,
451 'name' => $user->display_name
452 ];
453 }, $users);
454 }
455
456 /**
457 * Get available categories for the site
458 */
459 private function get_available_categories(): array
460 {
461 $categories = get_categories([
462 'hide_empty' => false
463 ]);
464
465 return array_map(function($category) {
466 return [
467 'id' => (int)$category->term_id,
468 'name' => $category->name
469 ];
470 }, $categories);
471 }
472
473 /**
474 * Get available tags for the site
475 */
476 private function get_available_tags(): array
477 {
478 $tags = get_tags([
479 'hide_empty' => false
480 ]);
481
482 return array_map(function($tag) {
483 return [
484 'id' => (int)$tag->term_id,
485 'name' => $tag->name
486 ];
487 }, $tags);
488 }
489 }
490
491 register_activation_hook(__FILE__, [JournalistAI::class, 'activate']);
492 new JournalistAI();
493 }
494