PluginProbe
BotWriter – AI Writer & SEO Content Generator / trunk
BotWriter – AI Writer & SEO Content Generator vtrunk
3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.4 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 3.3.0 3.2.8 trunk 1.3.0 1.3.1 1.3.2 1.3.3 All 51 releases
botwriter / botwriter.php

botwriter.php in BotWriter – AI Writer & SEO Content Generator trunk, at botwriter.php

6,281 lines 265.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: BotWriter – AI Writer & SEO Content Generator
4 Plugin URI: https://www.wpbotwriter.com
5 Description: Plugin for automatically generating posts using artificial intelligence. Create content from scratch with AI and generate custom images. Optimize content for SEO, including tags, titles, and image descriptions. Advanced features like ChatGPT, automatic content creation, image generation, SEO optimization, and AI training make this plugin a complete tool for writers and content creators.
6 Version: 3.4.10
7 Author: estebandezafra
8 Requires PHP: 7.0
9 License: GPL v2 or later
10 License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 Text Domain: botwriter
12 Domain Path: /languages
13 */
14
15 // Prevent direct access to the file
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20
21
22
23 if (!defined('BOTWRITER_VERSION')) {
24 define('BOTWRITER_VERSION', '3.4.10');
25 }
26
27 // Plugin directory path (with trailing slash)
28 if (!defined('BOTWRITER_PLUGIN_DIR')) {
29 define('BOTWRITER_PLUGIN_DIR', plugin_dir_path(__FILE__));
30 }
31
32 define('BOTWRITER_URL', plugin_dir_url(__FILE__));
33
34 define('BOTWRITER_API_URL', "https://api.wpbotwriter.com/");
35
36
37
38 // Debugging constant for development
39 if (!defined('BOTWRITER_DEBUG')) {
40 define('BOTWRITER_DEBUG', false);
41 }
42
43 /**
44 * Pair a new installation with the service before its first generation call.
45 * Existing installations with a stored token do not perform any extra request.
46 *
47 * @return string The stored/confirmed token, or an empty string on failure.
48 */
49 function botwriter_ensure_site_token() {
50 static $pairing = false;
51
52 $stored_token = (string) get_option('botwriter_site_token', '');
53 if ($stored_token !== '' || $pairing) {
54 return $stored_token;
55 }
56
57 $pairing = true;
58 $domain = preg_replace('#^https?://#', '', home_url());
59 $domain = rtrim((string) $domain, '/');
60 $candidate = '';
61
62 for ($step = 0; $step < 2; $step++) {
63 $response = wp_remote_post(rtrim(BOTWRITER_API_URL, '/') . '/validate', array(
64 'timeout' => 15,
65 'sslverify' => true,
66 'headers' => array('Content-Type' => 'application/json'),
67 'body' => wp_json_encode(array(
68 'domain' => $domain,
69 'api_key' => (string) get_option('botwriter_api_key', ''),
70 'site_token' => $candidate,
71 )),
72 ));
73
74 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
75 break;
76 }
77
78 $data = json_decode(wp_remote_retrieve_body($response), true);
79 if (!is_array($data) || empty($data['valid']) || empty($data['site_token'])) {
80 break;
81 }
82
83 $candidate = sanitize_text_field((string) $data['site_token']);
84 }
85
86 if ($candidate !== '') {
87 update_option('botwriter_site_token', $candidate);
88 }
89
90 $pairing = false;
91 return $candidate;
92 }
93
94
95 if (!function_exists('botwriter_is_seo_module_enabled')) {
96 function botwriter_is_seo_module_enabled() {
97 return get_option('botwriter_seo_module_enabled', '1') === '1';
98 }
99 }
100
101
102 /**
103 * Returns absolute filesystem path to the plugin debug log file
104 * (wp-content/uploads/botwriter-debug.log) or false if uploads dir
105 * is not writable.
106 */
107 if (!function_exists('botwriter_debug_log_path')) {
108 function botwriter_debug_log_path() {
109 $uploads = wp_upload_dir();
110 if (!empty($uploads['error'])) {
111 return false;
112 }
113 $base = isset($uploads['basedir']) ? $uploads['basedir'] : '';
114 if (!$base) {
115 return false;
116 }
117 return trailingslashit($base) . 'botwriter-debug.log';
118 }
119 }
120
121 /**
122 * Whether the UI-toggle for debug logging to file is enabled.
123 */
124 if (!function_exists('botwriter_debug_log_enabled')) {
125 function botwriter_debug_log_enabled() {
126 return get_option('botwriter_debug_logging', '0') === '1';
127 }
128 }
129
130
131 if (!function_exists('botwriter_log')) {
132 function botwriter_log($message, array $context = []) {
133 $botwriter_debug = defined('BOTWRITER_DEBUG') && BOTWRITER_DEBUG === true;
134 $wp_debug_log = defined('WP_DEBUG') && WP_DEBUG === true && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG;
135 $file_log = botwriter_debug_log_enabled();
136 if (!$botwriter_debug && !$wp_debug_log && !$file_log) {
137 return;
138 }
139
140 if (!is_string($message)) {
141 $encoded = wp_json_encode($message);
142 if ($encoded !== false) {
143 $message = $encoded;
144 } else {
145 // Fallback to safe string/serialization without using print_r
146 if (is_scalar($message)) {
147 $message = (string) $message;
148 } else {
149 $message = maybe_serialize($message);
150 }
151 }
152 }
153
154 if (!empty($context)) {
155 $encoded_context = wp_json_encode($context);
156 if ($encoded_context !== false) {
157 $message .= ' ' . $encoded_context;
158 }
159 }
160
161 $line = '[BotWriter] ' . $message;
162
163 if ($botwriter_debug || $wp_debug_log) {
164 error_log($line);
165 }
166
167 if ($file_log) {
168 $path = botwriter_debug_log_path();
169 if ($path) {
170 // Cap file size at 5 MB by truncating-then-rotating in place.
171 if (file_exists($path) && filesize($path) > 5 * 1024 * 1024) {
172 @file_put_contents($path, '');
173 }
174 $stamp = gmdate('Y-m-d H:i:s');
175 @file_put_contents(
176 $path,
177 '[' . $stamp . ' UTC] ' . $line . "\n",
178 FILE_APPEND | LOCK_EX
179 );
180 }
181 }
182 }
183 }
184
185
186 /**
187 * Add plugin action links (Settings link next to Deactivate)
188 */
189 add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'botwriter_plugin_action_links');
190 function botwriter_plugin_action_links($links) {
191 $settings_link = '<a href="' . admin_url('admin.php?page=botwriter_settings') . '">' . __('Settings', 'botwriter') . '</a>';
192 array_unshift($links, $settings_link);
193 return $links;
194 }
195
196 /**
197 * Add plugin row meta links (Website, FAQ, Support below plugin description)
198 */
199 add_filter('plugin_row_meta', 'botwriter_plugin_row_meta', 10, 2);
200 function botwriter_plugin_row_meta($links, $file) {
201 if (plugin_basename(__FILE__) === $file) {
202 $row_meta = array(
203 'website' => '<a href="https://www.wpbotwriter.com" target="_blank" rel="noopener">' . __('Website', 'botwriter') . '</a>',
204 'faq' => '<a href="https://wpbotwriter.com/faq.html" target="_blank" rel="noopener">' . __('FAQ', 'botwriter') . '</a>',
205 'support' => '<a href="https://wordpress.org/support/plugin/botwriter/" target="_blank" rel="noopener">' . __('Support', 'botwriter') . '</a>',
206 );
207 return array_merge($links, $row_meta);
208 }
209 return $links;
210 }
211
212
213 require plugin_dir_path( __FILE__ ) . 'includes/posts.php';
214 require plugin_dir_path( __FILE__ ) . 'includes/functions.php';
215 require plugin_dir_path( __FILE__ ) . 'includes/settings.php';
216 require plugin_dir_path( __FILE__ ) . 'includes/logs.php';
217 require plugin_dir_path( __FILE__ ) . 'includes/dedup.php';
218 require plugin_dir_path( __FILE__ ) . 'includes/announcements.php';
219 require plugin_dir_path( __FILE__ ) . 'includes/super.php';
220 require plugin_dir_path( __FILE__ ) . 'includes/addnew.php';
221 require plugin_dir_path( __FILE__ ) . 'includes/quickpost.php';
222 require plugin_dir_path( __FILE__ ) . 'includes/rewriter.php';
223 require plugin_dir_path( __FILE__ ) . 'includes/siterewriter.php';
224 require plugin_dir_path( __FILE__ ) . 'includes/templates.php';
225 require plugin_dir_path( __FILE__ ) . 'includes/default-templates.php';
226 if (botwriter_is_seo_module_enabled()) {
227 require plugin_dir_path( __FILE__ ) . 'includes/seo/seo.php';
228 }
229
230 // WooCommerce AI Content Optimizer (loads only when WooCommerce is active)
231 require plugin_dir_path( __FILE__ ) . 'includes/woocommerce-ai/class-bw-woo-ai.php';
232 add_action( 'plugins_loaded', function () {
233 $bw_woo_ai = new BotWriter_Woo_AI();
234 $bw_woo_ai->init();
235 } );
236
237 /**
238 * Determine whether the editor assistant supports the current post type.
239 *
240 * @param string $post_type Post type slug.
241 * @return bool
242 */
243 function botwriter_editor_assistant_supports_post_type($post_type) {
244 $supported_post_types = apply_filters('botwriter_editor_assistant_post_types', array('post', 'page'));
245 return in_array((string) $post_type, array_map('strval', (array) $supported_post_types), true);
246 }
247
248 /**
249 * Check if current user can edit posts for the given post type.
250 *
251 * @param string $post_type Post type slug.
252 * @return bool
253 */
254 function botwriter_editor_assistant_can_edit_post_type($post_type) {
255 $post_type_object = get_post_type_object((string) $post_type);
256 if ($post_type_object && !empty($post_type_object->cap->edit_posts)) {
257 return current_user_can($post_type_object->cap->edit_posts);
258 }
259
260 return current_user_can('edit_posts') || current_user_can('edit_pages');
261 }
262
263 /**
264 * Decide if editor assistant assets should be loaded for a screen.
265 *
266 * @param WP_Screen|null $screen Current admin screen.
267 * @return bool
268 */
269 function botwriter_should_enqueue_editor_assistant($screen) {
270 if (!$screen || (string) $screen->base !== 'post') {
271 return false;
272 }
273
274 $post_type = (string) ($screen->post_type ?? '');
275 if ($post_type === '' || !botwriter_editor_assistant_supports_post_type($post_type)) {
276 return false;
277 }
278
279 if (get_option('botwriter_editor_assistant_enabled', '1') !== '1') {
280 return false;
281 }
282
283 return botwriter_editor_assistant_can_edit_post_type($post_type);
284 }
285
286
287 // Enqueque JS Files
288 function botwriter_enqueue_scripts() {
289 $my_plugin_dir = plugin_dir_url(__FILE__);
290 $screen = get_current_screen();
291 $slug = $screen->id;
292
293
294
295 wp_register_script( 'bootstrapjs',$my_plugin_dir.'/assets/js/bootstrap.min.js' , array('jquery'), false, true );
296 wp_enqueue_script( 'bootstrapjs' );
297
298
299 wp_register_script( 'botwriter_bootstrap_bundle',$my_plugin_dir.'/assets/js/bootstrap.bundle.min.js' , array('jquery'), false, true );
300 wp_enqueue_script( 'botwriter_bootstrap_bundle' );
301
302
303 wp_register_script( 'botwriter_botwriter',$my_plugin_dir.'/assets/js/botwriter.js' , array('jquery'), false, true );
304 wp_enqueue_script( 'botwriter_botwriter' );
305 wp_localize_script('botwriter_botwriter', 'botwriter_ajax', array(
306 'ajax_url' => admin_url('admin-ajax.php'),
307 'nonce' => wp_create_nonce('botwriter_super_nonce'),
308 'rss_nonce' => wp_create_nonce('botwriter_check_rss_nonce'),
309 'wp_categories_nonce' => wp_create_nonce('botwriter_wp_categories_nonce'),
310 ));
311
312 wp_enqueue_script('botwriter-admin-ajax-status', $my_plugin_dir.'/assets/js/admin-ajax-status.js', ['jquery'], null, true);
313 wp_localize_script('botwriter-admin-ajax-status', 'botwriter_ajax_object', [
314 'ajax_url' => admin_url('admin-ajax.php'),
315 'nonce' => wp_create_nonce('botwriter_cambiar_status_nonce')
316 ]);
317
318
319 wp_enqueue_script('botwriter-dismiss-script', $my_plugin_dir . '/assets/js/botwriter_dismiss.js', array('jquery'), null, true);
320 wp_localize_script('botwriter-dismiss-script','botwriterData',
321 array(
322 'nonce' => wp_create_nonce('botwriter_dismiss_nonce'),
323 'ajaxurl' => admin_url('admin-ajax.php')
324 )
325 );
326
327
328 if ($slug=="botwriter_page_botwriter_automatic_post_new" || $slug === 'botwriter_page_botwriter_super_page' || $slug === 'botwriter_page_botwriter_write_now' || $slug === 'botwriter_page_botwriter_rewriter_page' || $slug === 'botwriter_page_botwriter_siterewriter_page') {
329 wp_register_script('botwriter_automatic_posts', $my_plugin_dir . 'assets/js/posts.js', array('jquery'), false, true);
330 wp_enqueue_script('botwriter_automatic_posts');
331 wp_localize_script('botwriter_automatic_posts', 'botwriter_posts_ajax', array(
332 'ajax_url' => admin_url('admin-ajax.php'),
333 'taxonomies_nonce' => wp_create_nonce('botwriter_taxonomies_nonce'),
334 ));
335 }
336
337
338 if ($slug==="botwriter_page_botwriter_logs") {
339 wp_register_script('botwriter_logs', $my_plugin_dir . 'assets/js/logs.js', array('jquery'), false, true);
340 wp_enqueue_script('botwriter_logs');
341 wp_localize_script('botwriter_logs', 'botwriter_logs_vars', array(
342 'ajax_url' => admin_url('admin-ajax.php'),
343 'nonce' => wp_create_nonce('botwriter_logs_delete_nonce'),
344 'confirm_delete' => __('Are you sure you want to delete this log entry? This action cannot be undone.', 'botwriter'),
345 'confirm_bulk_delete' => __('Are you sure you want to delete the selected log entries? This action cannot be undone.', 'botwriter'),
346 'error_delete' => __('Error deleting log. Please try again.', 'botwriter'),
347 ));
348
349 // Reuse the featured image regeneration modal inside BotWriter logs.
350 wp_register_script('botwriter_post_image_regeneration', $my_plugin_dir . 'assets/js/post-image-regeneration.js', array('jquery'), BOTWRITER_VERSION, true);
351 wp_enqueue_script('botwriter_post_image_regeneration');
352 wp_localize_script('botwriter_post_image_regeneration', 'botwriter_post_image_regeneration', array(
353 'ajax_url' => admin_url('admin-ajax.php'),
354 'nonce' => wp_create_nonce('botwriter_regenerate_image_nonce'),
355 'i18n' => array(
356 'link_text' => __('Regenerate', 'botwriter'),
357 'modal_title' => __('BotWriter Image Regeneration', 'botwriter'),
358 'modal_subtitle' => __('Generate and preview a featured image before applying it.', 'botwriter'),
359 'provider' => __('Current provider', 'botwriter'),
360 'model' => __('Current model', 'botwriter'),
361 'prompt_label' => __('Image Prompt', 'botwriter'),
362 'cleanup_label' => __('Previous featured image', 'botwriter'),
363 'cleanup_keep' => __('Keep in media library', 'botwriter'),
364 'cleanup_delete' => __('Delete permanently (if not used elsewhere)', 'botwriter'),
365 'current_image' => __('Current featured image', 'botwriter'),
366 'no_current_image' => __('This post has no featured image yet.', 'botwriter'),
367 'btn_regenerate' => __('Regenerate', 'botwriter'),
368 'btn_accept' => __('Accept', 'botwriter'),
369 'btn_close' => __('Close', 'botwriter'),
370 'loading_context' => __('Loading data...', 'botwriter'),
371 'generating' => __('Generating image preview...', 'botwriter'),
372 'applying' => __('Applying featured image...', 'botwriter'),
373 'missing_log' => __('No saved image prompt was found for this post. Please write your prompt manually.', 'botwriter'),
374 'provider_none' => __('Image provider is currently set to "none" in settings. Select an image provider first.', 'botwriter'),
375 'invalid_post' => __('No valid published post is linked to this log entry.', 'botwriter'),
376 'empty_prompt' => __('Please enter an image prompt before regenerating.', 'botwriter'),
377 'working' => __('Regenerating image...', 'botwriter'),
378 'generic_error' => __('Could not regenerate the image. Please try again.', 'botwriter'),
379 ),
380 ));
381 }
382
383
384
385 if ($slug === 'botwriter_page_botwriter_super_page') {
386 wp_register_script('botwriter_super', $my_plugin_dir . 'assets/js/super.js', array('jquery'), false, true);
387 wp_enqueue_script('botwriter_super');
388 wp_localize_script('botwriter_super', 'botwriter_super_ajax', array(
389 'ajax_url' => admin_url('admin-ajax.php'),
390 'nonce' => wp_create_nonce('botwriter_super_nonce')
391 ));
392 }
393
394 if ($slug === 'botwriter_page_botwriter_rewriter_page') {
395 wp_register_script('botwriter_rewriter', $my_plugin_dir . 'assets/js/rewriter.js', array('jquery'), false, true);
396 wp_enqueue_script('botwriter_rewriter');
397 wp_localize_script('botwriter_rewriter', 'botwriter_rewriter_ajax', array(
398 'ajax_url' => admin_url('admin-ajax.php'),
399 'nonce' => wp_create_nonce('botwriter_rewriter_nonce'),
400 'logs_url' => admin_url('admin.php?page=botwriter_logs'),
401 ));
402 }
403
404 if ($slug === 'botwriter_page_botwriter_siterewriter_page') {
405 wp_register_script('botwriter_siterewriter', $my_plugin_dir . 'assets/js/siterewriter.js', array('jquery'), false, true);
406 wp_enqueue_script('botwriter_siterewriter');
407 wp_localize_script('botwriter_siterewriter', 'botwriter_siterewriter_ajax', array(
408 'ajax_url' => admin_url('admin-ajax.php'),
409 'nonce' => wp_create_nonce('botwriter_siterewriter_nonce'),
410 'logs_url' => admin_url('admin.php?page=botwriter_logs'),
411 ));
412 }
413
414 if ($slug === 'botwriter_page_botwriter_settings') {
415 wp_register_script('botwriter_settings', $my_plugin_dir . 'assets/js/botwriter-settings.js', array('jquery'), false, true);
416 wp_enqueue_script('botwriter_settings');
417 wp_localize_script('botwriter_settings', 'botwriter_settings', array(
418 'ajax_url' => admin_url('admin-ajax.php'),
419 'nonce' => wp_create_nonce('botwriter_settings_nonce'),
420 'i18n' => array(
421 'saving' => __('Saving...', 'botwriter'),
422 'saved' => __('Saved', 'botwriter'),
423 'error' => __('Error', 'botwriter'),
424 'connection_error' => __('Connection error', 'botwriter'),
425 'connection_failed' => __('Connection failed', 'botwriter'),
426 'hide' => __('Hide', 'botwriter'),
427 'show' => __('Show', 'botwriter'),
428 'active' => __('Active', 'botwriter'),
429 'enter_api_key' => __('Please enter an API key first.', 'botwriter'),
430 'enter_api_url' => __('Please enter an API URL', 'botwriter'),
431 'testing' => __('Testing...', 'botwriter'),
432 'models_found' => __('Models found:', 'botwriter'),
433 'configure_openai_key' => __('Configure OpenAI API key in Text AI tab first.', 'botwriter'),
434 'confirm_reset_models' => __('Are you sure you want to reset all model lists to factory defaults?', 'botwriter'),
435 )
436 ));
437
438 wp_register_script('botwriter_debug_log', $my_plugin_dir . 'assets/js/debug-log.js', array('jquery', 'botwriter_settings'), BOTWRITER_VERSION, true);
439 wp_enqueue_script('botwriter_debug_log');
440 wp_localize_script('botwriter_debug_log', 'botwriter_debug_log', array(
441 'ajax_url' => admin_url('admin-ajax.php'),
442 'nonce' => wp_create_nonce('botwriter_settings_nonce'),
443 'i18n' => array(
444 'loading' => __('Loading...', 'botwriter'),
445 'size' => __('Size:', 'botwriter'),
446 'showing_last' => __('showing last 512 KB', 'botwriter'),
447 'logging_off' => __('logging is OFF', 'botwriter'),
448 'error' => __('Error.', 'botwriter'),
449 'request_failed' => __('Request failed.', 'botwriter'),
450 'confirm_clear' => __('Clear the debug log file?', 'botwriter'),
451 'clearing' => __('Clearing...', 'botwriter'),
452 'cleared' => __('Log cleared.', 'botwriter'),
453 ),
454 ));
455 }
456
457 if ($slug === 'botwriter_page_botwriter_write_now') {
458 wp_register_script('botwriter_quickpost', $my_plugin_dir . 'assets/js/quickpost.js', array('jquery'), false, true);
459 wp_enqueue_script('botwriter_quickpost');
460 wp_localize_script('botwriter_quickpost', 'botwriter_quickpost_ajax', array(
461 'ajax_url' => admin_url('admin-ajax.php'),
462 'nonce' => wp_create_nonce('botwriter_quickpost_nonce')
463 ));
464 }
465
466 // Regenerate featured image modal (post editor)
467 if ($screen && $screen->base === 'post' && current_user_can('manage_options')) {
468 wp_register_script('botwriter_post_image_regeneration', $my_plugin_dir . 'assets/js/post-image-regeneration.js', array('jquery'), BOTWRITER_VERSION, true);
469 wp_enqueue_script('botwriter_post_image_regeneration');
470 wp_localize_script('botwriter_post_image_regeneration', 'botwriter_post_image_regeneration', array(
471 'ajax_url' => admin_url('admin-ajax.php'),
472 'nonce' => wp_create_nonce('botwriter_regenerate_image_nonce'),
473 'i18n' => array(
474 'link_text' => __('Regenerate', 'botwriter'),
475 'modal_title' => __('BotWriter Image Regeneration', 'botwriter'),
476 'modal_subtitle' => __('Generate and preview a featured image before applying it.', 'botwriter'),
477 'provider' => __('Current provider', 'botwriter'),
478 'model' => __('Current model', 'botwriter'),
479 'prompt_label' => __('Image Prompt', 'botwriter'),
480 'cleanup_label' => __('Previous featured image', 'botwriter'),
481 'cleanup_keep' => __('Keep in media library', 'botwriter'),
482 'cleanup_delete' => __('Delete permanently (if not used elsewhere)', 'botwriter'),
483 'current_image' => __('Current featured image', 'botwriter'),
484 'no_current_image' => __('This post has no featured image yet.', 'botwriter'),
485 'btn_regenerate' => __('Regenerate', 'botwriter'),
486 'btn_accept' => __('Accept', 'botwriter'),
487 'btn_close' => __('Close', 'botwriter'),
488 'loading_context'=> __('Loading data...', 'botwriter'),
489 'generating' => __('Generating image preview...', 'botwriter'),
490 'applying' => __('Applying featured image...', 'botwriter'),
491 'missing_log' => __('No saved image prompt was found for this post. Please write your prompt manually.', 'botwriter'),
492 'provider_none' => __('Image provider is currently set to "none" in settings. Select an image provider first.', 'botwriter'),
493 'invalid_post' => __('No valid published post is linked to this log entry.', 'botwriter'),
494 'empty_prompt' => __('Please enter an image prompt before regenerating.', 'botwriter'),
495 'working' => __('Regenerating image...', 'botwriter'),
496 'generic_error'=> __('Could not regenerate the image. Please try again.', 'botwriter'),
497 ),
498 ));
499 }
500
501 // Floating AI assistant widget (post/page editor)
502 if (botwriter_should_enqueue_editor_assistant($screen)) {
503 wp_register_script('botwriter_editor_assistant', $my_plugin_dir . 'assets/js/editor-ai-assistant.js', array('jquery'), BOTWRITER_VERSION, true);
504 wp_enqueue_script('botwriter_editor_assistant');
505 wp_localize_script('botwriter_editor_assistant', 'botwriter_editor_ai', array(
506 'ajax_url' => admin_url('admin-ajax.php'),
507 'nonce' => wp_create_nonce('botwriter_editor_assistant_nonce'),
508 'robot_image' => $my_plugin_dir . 'assets/images/robot.png',
509 'robot_face_image' => $my_plugin_dir . 'assets/images/robot_face.png',
510 'settings' => array(
511 'skip_heading_links' => '1',
512 'seo_module_enabled' => botwriter_is_seo_module_enabled() ? '1' : '0',
513 ),
514 'i18n' => array(
515 'widget_title' => __('BotWriter Copilot', 'botwriter'),
516 'tab_prompt' => __('Prompt', 'botwriter'),
517 'tab_seo' => __('SEO', 'botwriter'),
518 'intro' => __('Select what to update', 'botwriter'),
519 'seo_intro' => __('Review the current post SEO checks.', 'botwriter'),
520 'seo_subtab_analysis' => __('SEO analysis', 'botwriter'),
521 'seo_subtab_readability' => __('Readability', 'botwriter'),
522 'seo_loading' => __('Loading SEO report...', 'botwriter'),
523 'seo_missing_post' => __('Save the post first to view SEO reports.', 'botwriter'),
524 'seo_error' => __('Could not load SEO report.', 'botwriter'),
525 'seo_empty' => __('No SEO checks available.', 'botwriter'),
526 'target_text' => __('Text', 'botwriter'),
527 'target_title' => __('Title', 'botwriter'),
528 'target_tags' => __('Tags', 'botwriter'),
529 'target_excerpt' => __('Excerpt', 'botwriter'),
530 'target_seo_meta' => __('SEO Meta', 'botwriter'),
531 'target_internal_links' => __('Internal Links', 'botwriter'),
532 'suggestions_title' => __('Suggestions', 'botwriter'),
533 'prompt_placeholder' => __('Describe exactly what you want to improve...', 'botwriter'),
534 'links_prompt_placeholder' => __('What kind of internal links do you want (educational, conversion, cluster, etc.)?', 'botwriter'),
535 'keyphrases_label' => __('Keyphrases (up to 5, comma-separated)', 'botwriter'),
536 'keyphrases_placeholder' => __('e.g. internal linking, seo writing, topic clusters', 'botwriter'),
537 'links_title' => __('Suggested internal links', 'botwriter'),
538 'insert_link' => __('Insert', 'botwriter'),
539 'inserted_link' => __('Inserted', 'botwriter'),
540 'open_link' => __('Open', 'botwriter'),
541 'links_ready' => __('Suggestions are ready. Insert the links you want, then Keep or Undo.', 'botwriter'),
542 'links_mode_ai' => __('AI mode: suggestions ranked by semantic relevance and anchor fit.', 'botwriter'),
543 'links_mode_noai' => __('Deterministic mode: suggestions ranked using taxonomy and keyword overlap (no AI call).', 'botwriter'),
544 'links_empty' => __('No relevant internal links were found yet.', 'botwriter'),
545 'link_inserted' => __('Internal link inserted. Review and choose Keep or Undo.', 'botwriter'),
546 'link_already_exists' => __('This URL is already linked in the content.', 'botwriter'),
547 'same_response_notice' => __('AI returned the same text. No changes were applied.', 'botwriter'),
548 'sending' => __('Thinking', 'botwriter'),
549 'keep' => __('Keep', 'botwriter'),
550 'undo' => __('Undo', 'botwriter'),
551 'confirm_label' => __('Apply this AI change?', 'botwriter'),
552 'missing_prompt' => __('Write a prompt or choose a suggestion first.', 'botwriter'),
553 'generic_error' => __('Could not generate a response. Please try again.', 'botwriter'),
554 'empty_response' => __('The assistant returned an empty response.', 'botwriter'),
555 'updated_notice' => __('Updated. Review and choose Keep or Undo.', 'botwriter'),
556 'reverted_notice' => __('Change reverted.', 'botwriter'),
557 'kept_notice' => __('Change kept. Save or update the post when ready.', 'botwriter'),
558 ),
559 ));
560 }
561
562
563
564 }
565 add_action('admin_enqueue_scripts','botwriter_enqueue_scripts');
566
567 /**
568 * Retrieve the latest BotWriter log associated with a published post.
569 *
570 * @param int $post_id Post ID.
571 * @return array|null
572 */
573 function botwriter_get_latest_log_by_post_id($post_id) {
574 global $wpdb;
575
576 $post_id = intval($post_id);
577 if ($post_id <= 0) {
578 return null;
579 }
580
581 $table_name = $wpdb->prefix . 'botwriter_logs';
582 $log = $wpdb->get_row(
583 $wpdb->prepare(
584 "SELECT * FROM {$table_name} WHERE id_post_published = %d ORDER BY id DESC LIMIT 1",
585 $post_id
586 ),
587 ARRAY_A
588 );
589
590 botwriter_log('Image prompt lookup: latest log by post', array(
591 'post_id' => $post_id,
592 'found' => is_array($log),
593 'log_id' => is_array($log) ? intval($log['id'] ?? 0) : 0,
594 'id_post_published' => is_array($log) ? intval($log['id_post_published'] ?? 0) : 0,
595 'image_prompt_len' => is_array($log) ? strlen(trim((string) ($log['image_prompt'] ?? ''))) : 0,
596 ));
597
598 return is_array($log) ? $log : null;
599 }
600
601 /**
602 * Retrieve the latest BotWriter log for a post that has a non-empty image_prompt.
603 *
604 * @param int $post_id Post ID.
605 * @return array|null
606 */
607 function botwriter_get_latest_log_with_image_prompt_by_post_id($post_id) {
608 global $wpdb;
609
610 $post_id = intval($post_id);
611 if ($post_id <= 0) {
612 return null;
613 }
614
615 $table_name = $wpdb->prefix . 'botwriter_logs';
616 $log = $wpdb->get_row(
617 $wpdb->prepare(
618 "SELECT * FROM {$table_name} WHERE id_post_published = %d AND image_prompt IS NOT NULL AND TRIM(image_prompt) <> '' ORDER BY id DESC LIMIT 1",
619 $post_id
620 ),
621 ARRAY_A
622 );
623
624 botwriter_log('Image prompt lookup: latest log WITH prompt by post', array(
625 'post_id' => $post_id,
626 'found' => is_array($log),
627 'log_id' => is_array($log) ? intval($log['id'] ?? 0) : 0,
628 'id_post_published' => is_array($log) ? intval($log['id_post_published'] ?? 0) : 0,
629 'image_prompt_len' => is_array($log) ? strlen(trim((string) ($log['image_prompt'] ?? ''))) : 0,
630 ));
631
632 return is_array($log) ? $log : null;
633 }
634
635 /**
636 * Resolve current image model from provider settings.
637 *
638 * @param string $provider Provider slug.
639 * @return string
640 */
641 function botwriter_get_current_image_model_by_provider($provider) {
642 $provider = sanitize_key((string) $provider);
643
644 if ($provider === 'stockphoto') {
645 $preferred = sanitize_key((string) get_option('botwriter_stockphoto_preferred', 'random'));
646 $allowed_preferred = array('pixabay', 'pexels', 'unsplash', 'openverse', 'random');
647 if (!in_array($preferred, $allowed_preferred, true)) {
648 $preferred = 'random';
649 }
650
651 return $preferred;
652 }
653 if ($provider === 'none') {
654 return 'none';
655 }
656
657 $default_model = function_exists('botwriter_get_provider_default_image_model')
658 ? (string) botwriter_get_provider_default_image_model($provider)
659 : '';
660 if ($default_model === '') {
661 $fallback_defaults = array(
662 'dalle' => 'gpt-image-1',
663 'gemini' => 'gemini-3.1-flash-lite-image',
664 'fal' => 'fal-ai/flux-pro/v1.1',
665 'replicate' => 'black-forest-labs/flux-1.1-pro',
666 'stability' => 'sd3.5-large-turbo',
667 'cloudflare' => 'flux-1-schnell',
668 );
669 $default_model = (string) ($fallback_defaults[$provider] ?? 'gpt-image-1');
670 }
671
672 $option_name = function_exists('botwriter_get_image_model_option_name')
673 ? botwriter_get_image_model_option_name($provider)
674 : ($provider === 'gemini' ? 'botwriter_gemini_image_model' : "botwriter_{$provider}_model");
675
676 $model = (string) get_option($option_name, $default_model);
677
678 if (function_exists('botwriter_normalize_image_model')) {
679 $normalized_model = (string) botwriter_normalize_image_model($provider, $model);
680
681 // Persist normalized aliases (for example legacy Gemini 2.0 IDs)
682 // so the settings UI reflects the real value used in dispatch.
683 if ($normalized_model !== '' && $normalized_model !== $model) {
684 update_option($option_name, $normalized_model);
685 botwriter_log('Image model option auto-normalized', array(
686 'provider' => $provider,
687 'option_name' => $option_name,
688 'raw_model' => $model,
689 'normalized_model' => $normalized_model,
690 ));
691 }
692
693 return $normalized_model;
694 }
695
696 return $model;
697 }
698
699 /**
700 * Normalize image size values to the canonical semantic set.
701 *
702 * Legacy values such as square_hd or landscape_16_9 may still exist in
703 * older installations/options. The direct /images endpoint expects only
704 * landscape|square|portrait.
705 *
706 * @param string $size Raw size value.
707 * @return string
708 */
709 function botwriter_normalize_ai_image_size($size) {
710 $normalized = strtolower(trim((string) $size));
711
712 if ($normalized === '' || $normalized === 'square' || $normalized === 'square_hd'
713 || $normalized === '1:1' || $normalized === '1024x1024') {
714 return 'square';
715 }
716
717 if ($normalized === 'landscape' || $normalized === 'landscape_4_3' || $normalized === 'landscape_16_9'
718 || $normalized === '4:3' || $normalized === '16:9' || $normalized === '1536x1024'
719 || $normalized === '1792x1024') {
720 return 'landscape';
721 }
722
723 if ($normalized === 'portrait' || $normalized === 'portrait_4_3' || $normalized === 'portrait_16_9'
724 || $normalized === '3:4' || $normalized === '9:16' || $normalized === '1024x1536'
725 || $normalized === '1024x1792') {
726 return 'portrait';
727 }
728
729 return 'square';
730 }
731
732 /**
733 * Return current image settings used for regeneration.
734 * Uses active plugin settings at execution time.
735 *
736 * @return array
737 */
738 function botwriter_get_current_image_generation_settings() {
739 $provider = (string) get_option('botwriter_image_provider', 'stockphoto');
740 $model = botwriter_get_current_image_model_by_provider($provider);
741 $size = botwriter_normalize_ai_image_size((string) get_option('botwriter_ai_image_size', 'square'));
742 $stockphoto_preferred = sanitize_key((string) get_option('botwriter_stockphoto_preferred', 'random'));
743 $allowed_preferred = array('pixabay', 'pexels', 'unsplash', 'openverse', 'random');
744 if (!in_array($stockphoto_preferred, $allowed_preferred, true)) {
745 $stockphoto_preferred = 'random';
746 }
747
748 return array(
749 'provider' => $provider,
750 'model' => $model,
751 'size' => $size,
752 'quality' => (string) get_option('botwriter_ai_image_quality', 'medium'),
753 'style' => (string) get_option('botwriter_ai_image_style', 'realistic'),
754 'style_custom' => (string) get_option('botwriter_ai_image_style_custom', ''),
755 'stockphoto_preferred' => $stockphoto_preferred,
756 'stockphoto_selection' => (string) get_option('botwriter_stockphoto_selection', 'random_top10'),
757 'stockphoto_attribution' => (string) get_option('botwriter_stockphoto_attribution', 'caption'),
758 );
759 }
760
761 /**
762 * Return post meta keys used to persist image prompts.
763 *
764 * @return array
765 */
766 function botwriter_get_image_prompt_meta_keys() {
767 return array(
768 'ai' => 'botwriter_image_prompt',
769 'stock' => 'botwriter_stockphoto_prompt',
770 'last' => 'botwriter_image_prompt_last',
771 'provider' => 'botwriter_image_prompt_last_provider',
772 );
773 }
774
775 /**
776 * Persist image prompt metadata on the post itself.
777 *
778 * @param int $post_id Post ID.
779 * @param string $prompt Prompt text.
780 * @param string $provider Provider used for generation.
781 * @return bool
782 */
783 function botwriter_save_post_image_prompt_meta($post_id, $prompt, $provider = '') {
784 $post_id = intval($post_id);
785 if ($post_id <= 0) {
786 botwriter_log('Image prompt meta save skipped: invalid post_id', array('post_id' => $post_id));
787 return false;
788 }
789
790 $prompt = trim(sanitize_textarea_field((string) $prompt));
791 if ($prompt === '') {
792 botwriter_log('Image prompt meta save skipped: empty prompt', array(
793 'post_id' => $post_id,
794 'provider' => $provider,
795 ));
796 return false;
797 }
798
799 $provider = sanitize_key((string) $provider);
800 $keys = botwriter_get_image_prompt_meta_keys();
801
802 update_post_meta($post_id, $keys['last'], $prompt);
803
804 if ($provider === 'stockphoto') {
805 update_post_meta($post_id, $keys['stock'], $prompt);
806 } else {
807 update_post_meta($post_id, $keys['ai'], $prompt);
808 }
809
810 if ($provider !== '') {
811 update_post_meta($post_id, $keys['provider'], $provider);
812 }
813
814 botwriter_log('Image prompt meta saved', array(
815 'post_id' => $post_id,
816 'provider' => $provider,
817 'prompt_len' => strlen($prompt),
818 'saved_ai_meta' => ($provider !== 'stockphoto'),
819 'saved_stock_meta' => ($provider === 'stockphoto'),
820 'meta_key_last' => $keys['last'],
821 ));
822
823 return true;
824 }
825
826 /**
827 * Resolve the best prompt saved on post meta for current provider context.
828 *
829 * @param int $post_id Post ID.
830 * @param string $provider Current provider.
831 * @return array
832 */
833 function botwriter_get_post_image_prompt_from_meta($post_id, $provider = '') {
834 $post_id = intval($post_id);
835 $provider = sanitize_key((string) $provider);
836 $keys = botwriter_get_image_prompt_meta_keys();
837
838 $ai_prompt = trim((string) get_post_meta($post_id, $keys['ai'], true));
839 $stock_prompt = trim((string) get_post_meta($post_id, $keys['stock'], true));
840 $last_prompt = trim((string) get_post_meta($post_id, $keys['last'], true));
841
842 botwriter_log('Image prompt lookup: post meta snapshot', array(
843 'post_id' => $post_id,
844 'provider' => $provider,
845 'ai_len' => strlen($ai_prompt),
846 'stock_len' => strlen($stock_prompt),
847 'last_len' => strlen($last_prompt),
848 'meta_ai_key' => $keys['ai'],
849 'meta_stock_key' => $keys['stock'],
850 'meta_last_key' => $keys['last'],
851 ));
852
853 if ($provider === 'stockphoto') {
854 if ($stock_prompt !== '') {
855 botwriter_log('Image prompt lookup: selected meta_stock', array('post_id' => $post_id, 'len' => strlen($stock_prompt)));
856 return array('prompt' => $stock_prompt, 'source' => 'meta_stock');
857 }
858 if ($ai_prompt !== '') {
859 botwriter_log('Image prompt lookup: selected meta_ai for stock provider', array('post_id' => $post_id, 'len' => strlen($ai_prompt)));
860 return array('prompt' => $ai_prompt, 'source' => 'meta_ai');
861 }
862 } else {
863 if ($ai_prompt !== '') {
864 botwriter_log('Image prompt lookup: selected meta_ai', array('post_id' => $post_id, 'len' => strlen($ai_prompt)));
865 return array('prompt' => $ai_prompt, 'source' => 'meta_ai');
866 }
867 }
868
869 if ($last_prompt !== '') {
870 botwriter_log('Image prompt lookup: selected meta_last', array('post_id' => $post_id, 'len' => strlen($last_prompt)));
871 return array('prompt' => $last_prompt, 'source' => 'meta_last');
872 }
873
874 if ($stock_prompt !== '') {
875 botwriter_log('Image prompt lookup: selected stock fallback', array('post_id' => $post_id, 'len' => strlen($stock_prompt)));
876 return array('prompt' => $stock_prompt, 'source' => 'meta_stock');
877 }
878
879 botwriter_log('Image prompt lookup: no prompt found in post meta', array('post_id' => $post_id));
880
881 return array('prompt' => '', 'source' => 'none');
882 }
883
884 /**
885 * Resolve the best available image prompt from generated post data.
886 *
887 * @param array $data Data used to generate/publish the post.
888 * @return string
889 */
890 function botwriter_resolve_image_prompt_from_post_data($data) {
891 if (!is_array($data)) {
892 botwriter_log('Image prompt resolve from post data: invalid payload');
893 return '';
894 }
895
896 $explicit_prompt = isset($data['image_prompt']) ? trim(sanitize_textarea_field((string) $data['image_prompt'])) : '';
897 if ($explicit_prompt !== '') {
898 botwriter_log('Image prompt resolve from post data: using explicit image_prompt', array(
899 'len' => strlen($explicit_prompt),
900 'has_image_provider' => isset($data['image_provider']),
901 ));
902 return $explicit_prompt;
903 }
904
905 // Edge/legacy fallback: when image_prompt is not returned, the title is the usual fallback basis.
906 $title_based_prompt = isset($data['aigenerated_title']) ? trim(sanitize_text_field((string) $data['aigenerated_title'])) : '';
907 if ($title_based_prompt !== '') {
908 botwriter_log('Image prompt resolve from post data: fallback to generated title', array(
909 'title_len' => strlen($title_based_prompt),
910 'has_image_prompt_key' => array_key_exists('image_prompt', $data),
911 ));
912 return $title_based_prompt;
913 }
914
915 botwriter_log('Image prompt resolve from post data: fallback to hardcoded default');
916 return 'Blog post illustration';
917 }
918
919 /**
920 * Build UI context for image regeneration modal.
921 *
922 * @param int $post_id Post ID.
923 * @return array
924 */
925 function botwriter_get_image_regeneration_context($post_id) {
926 $post_id = intval($post_id);
927 $settings = botwriter_get_current_image_generation_settings();
928 $provider = (string) ($settings['provider'] ?? '');
929
930 $prompt_context = botwriter_get_post_image_prompt_from_meta($post_id, $provider);
931 $prefill_prompt = (string) ($prompt_context['prompt'] ?? '');
932 $prompt_source = (string) ($prompt_context['source'] ?? 'none');
933
934 botwriter_log('Image regeneration context: after meta lookup', array(
935 'post_id' => $post_id,
936 'provider' => $provider,
937 'prompt_source' => $prompt_source,
938 'prompt_len' => strlen($prefill_prompt),
939 ));
940
941 $latest_log = null;
942 if ($prefill_prompt === '') {
943 // Prefer the newest log that actually contains an image prompt.
944 $latest_log = botwriter_get_latest_log_with_image_prompt_by_post_id($post_id);
945
946 // Backward-compat fallback: if no prompt-carrying log exists, inspect latest log anyway.
947 if (!is_array($latest_log)) {
948 $latest_log = botwriter_get_latest_log_by_post_id($post_id);
949 }
950
951 $prefill_prompt = is_array($latest_log) ? trim((string) ($latest_log['image_prompt'] ?? '')) : '';
952 if ($prefill_prompt !== '') {
953 $prompt_source = 'log';
954 // Legacy migration: if prompt exists in log but not in post meta, persist it now.
955 botwriter_save_post_image_prompt_meta($post_id, $prefill_prompt, $provider);
956 botwriter_log('Image regeneration context: prompt recovered from log and migrated to meta', array(
957 'post_id' => $post_id,
958 'log_id' => intval($latest_log['id'] ?? 0),
959 'prompt_len' => strlen($prefill_prompt),
960 ));
961 }
962 }
963
964 if ($prefill_prompt === '') {
965 $title_prompt = trim((string) get_the_title($post_id));
966 if ($title_prompt !== '') {
967 $prefill_prompt = $title_prompt;
968 $prompt_source = 'post_title';
969 botwriter_log('Image regeneration context: fallback to post title', array(
970 'post_id' => $post_id,
971 'title_len' => strlen($title_prompt),
972 ));
973 }
974 }
975
976 $has_meta_prompt = in_array($prompt_source, array('meta_ai', 'meta_stock', 'meta_last'), true);
977
978 $current_image_src = '';
979 $current_attachment_id = intval(get_post_thumbnail_id($post_id));
980 if ($current_attachment_id > 0) {
981 $current_image = wp_get_attachment_image_src($current_attachment_id, 'medium_large');
982 if (is_array($current_image) && !empty($current_image[0])) {
983 $current_image_src = esc_url_raw($current_image[0]);
984 }
985 }
986
987 botwriter_log('Image regeneration context resolved', array(
988 'post_id' => $post_id,
989 'provider' => $provider,
990 'prompt_source' => $prompt_source,
991 'prompt_len' => strlen($prefill_prompt),
992 'has_meta_prompt' => $has_meta_prompt,
993 'has_current_image' => ($current_image_src !== ''),
994 'has_log' => is_array($latest_log),
995 'log_id' => is_array($latest_log) ? intval($latest_log['id'] ?? 0) : 0,
996 ));
997
998 return array(
999 'post_id' => $post_id,
1000 'prompt' => $prefill_prompt,
1001 'has_log' => is_array($latest_log),
1002 'has_meta_prompt' => $has_meta_prompt,
1003 'has_prompt' => ($prefill_prompt !== ''),
1004 'prompt_source' => $prompt_source,
1005 'provider' => $provider,
1006 'model' => (string) ($settings['model'] ?? ''),
1007 'provider_disabled' => ($provider === 'none'),
1008 'current_attachment_id' => $current_attachment_id,
1009 'current_image_src' => $current_image_src,
1010 );
1011 }
1012
1013 /**
1014 * Generate an image URL using the direct backend endpoint (/images) with current settings.
1015 *
1016 * @param string $prompt Prompt text.
1017 * @return array
1018 */
1019 function botwriter_generate_image_with_current_settings($prompt) {
1020 $prompt = trim((string) $prompt);
1021 if ($prompt === '') {
1022 return array('success' => false, 'message' => __('Image prompt is required.', 'botwriter'));
1023 }
1024
1025 $settings = botwriter_get_current_image_generation_settings();
1026 $provider = (string) $settings['provider'];
1027 $model = (string) $settings['model'];
1028
1029 if ($provider === 'none') {
1030 return array('success' => false, 'message' => __('Image provider is disabled in settings.', 'botwriter'));
1031 }
1032
1033 botwriter_ensure_site_token();
1034
1035 $style_value = (string) ($settings['style_custom'] ?: $settings['style']);
1036 if ($style_value === 'realistic' || $style_value === 'none') {
1037 $style_value = '';
1038 }
1039
1040 $payload = array(
1041 'prompt' => $prompt,
1042 'domain' => esc_url_raw(get_site_url()),
1043 'api_key' => get_option('botwriter_api_key'),
1044 'site_token' => get_option('botwriter_site_token', ''),
1045 // Image regenerations are UX actions and should not consume license quota.
1046 'no_count' => true,
1047 'provider' => $provider,
1048 'model' => $model,
1049 'size' => (string) $settings['size'],
1050 'quality' => (string) $settings['quality'],
1051 'style' => $style_value,
1052 'stockphoto_preferred' => (string) $settings['stockphoto_preferred'],
1053 'stockphoto_selection' => (string) $settings['stockphoto_selection'],
1054 'stockphoto_attribution' => (string) $settings['stockphoto_attribution'],
1055 // Forward client keys (edge endpoint overlays provider keys from this payload)
1056 'openai_api_key' => botwriter_decrypt_api_key(get_option('botwriter_openai_api_key')),
1057 'google_api_key' => botwriter_decrypt_api_key(get_option('botwriter_google_api_key')),
1058 'fal_api_key' => botwriter_decrypt_api_key(get_option('botwriter_fal_api_key')),
1059 'replicate_api_key' => botwriter_decrypt_api_key(get_option('botwriter_replicate_api_key')),
1060 'stability_api_key' => botwriter_decrypt_api_key(get_option('botwriter_stability_api_key')),
1061 'cloudflare_api_key' => botwriter_decrypt_api_key(get_option('botwriter_cloudflare_api_key')),
1062 'cloudflare_account_id' => get_option('botwriter_cloudflare_account_id'),
1063 );
1064
1065 $ssl_verify = get_option('botwriter_sslverify');
1066 $ssl_verify = ($ssl_verify !== 'no');
1067
1068 $remote_url = BOTWRITER_API_URL . 'images';
1069 $response = wp_remote_post($remote_url, array(
1070 'method' => 'POST',
1071 'headers' => array(
1072 'Content-Type' => 'application/json',
1073 ),
1074 'body' => wp_json_encode($payload),
1075 'timeout' => 120,
1076 'sslverify' => $ssl_verify,
1077 ));
1078
1079 if (is_wp_error($response)) {
1080 return array('success' => false, 'message' => $response->get_error_message());
1081 }
1082
1083 $status_code = wp_remote_retrieve_response_code($response);
1084 $body_raw = wp_remote_retrieve_body($response);
1085 $result = json_decode($body_raw, true);
1086
1087 if (is_array($result) && !empty($result['site_token'])) {
1088 update_option('botwriter_site_token', sanitize_text_field((string) $result['site_token']));
1089 }
1090
1091 if ($status_code !== 200 || !is_array($result) || ($result['status'] ?? '') !== 'success' || empty($result['download_url'])) {
1092 $error_message = '';
1093 if (is_array($result)) {
1094 $error_message = (string) ($result['error'] ?? $result['message'] ?? '');
1095 }
1096 if ($error_message === '') {
1097 $error_message = __('Image generation failed on server.', 'botwriter');
1098 }
1099 return array('success' => false, 'message' => $error_message);
1100 }
1101
1102 $download_url = (string) $result['download_url'];
1103 $image_url = $download_url;
1104 if (strpos($download_url, 'http://') !== 0 && strpos($download_url, 'https://') !== 0) {
1105 $image_url = rtrim(BOTWRITER_API_URL, '/') . '/' . ltrim($download_url, '/');
1106 }
1107
1108 return array(
1109 'success' => true,
1110 'image_url' => $image_url,
1111 'provider' => $provider,
1112 'model' => $model,
1113 );
1114 }
1115
1116 /**
1117 * AJAX: fetch context for image regeneration modal.
1118 */
1119 function botwriter_get_post_image_regeneration_context_ajax() {
1120 if (!current_user_can('manage_options')) {
1121 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
1122 }
1123
1124 check_ajax_referer('botwriter_regenerate_image_nonce', 'nonce');
1125
1126 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1127 if ($post_id <= 0 || !get_post($post_id)) {
1128 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
1129 }
1130
1131 if (!current_user_can('edit_post', $post_id)) {
1132 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
1133 }
1134
1135 $context = botwriter_get_image_regeneration_context($post_id);
1136 botwriter_log('AJAX context response for image regeneration', array(
1137 'post_id' => $post_id,
1138 'prompt_source' => $context['prompt_source'] ?? 'unknown',
1139 'prompt_len' => strlen((string) ($context['prompt'] ?? '')),
1140 'has_meta_prompt' => !empty($context['has_meta_prompt']),
1141 'has_log' => !empty($context['has_log']),
1142 ));
1143 wp_send_json_success($context);
1144 }
1145 add_action('wp_ajax_botwriter_get_post_image_regeneration_context', 'botwriter_get_post_image_regeneration_context_ajax');
1146
1147 /**
1148 * AJAX: generate preview image only (does not apply to post yet).
1149 */
1150 function botwriter_generate_post_image_preview_ajax() {
1151 if (!current_user_can('manage_options')) {
1152 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
1153 }
1154
1155 check_ajax_referer('botwriter_regenerate_image_nonce', 'nonce');
1156
1157 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1158 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
1159
1160 if ($post_id <= 0 || !get_post($post_id)) {
1161 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
1162 }
1163
1164 if (!current_user_can('edit_post', $post_id)) {
1165 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
1166 }
1167
1168 $generated = botwriter_generate_image_with_current_settings($prompt);
1169 if (empty($generated['success'])) {
1170 wp_send_json_error(array('message' => (string) ($generated['message'] ?? __('Image generation failed.', 'botwriter'))));
1171 }
1172
1173 wp_send_json_success(array(
1174 'post_id' => $post_id,
1175 'prompt' => trim((string) $prompt),
1176 'image_url' => (string) $generated['image_url'],
1177 'provider' => (string) $generated['provider'],
1178 'model' => (string) $generated['model'],
1179 ));
1180 }
1181 add_action('wp_ajax_botwriter_generate_post_image_preview', 'botwriter_generate_post_image_preview_ajax');
1182
1183 /**
1184 * Check if an attachment is used as featured image by posts other than the current one.
1185 *
1186 * @param int $attachment_id Attachment ID.
1187 * @param int $exclude_post_id Post ID to exclude.
1188 * @return bool
1189 */
1190 function botwriter_is_attachment_featured_elsewhere($attachment_id, $exclude_post_id = 0) {
1191 global $wpdb;
1192
1193 $attachment_id = intval($attachment_id);
1194 $exclude_post_id = intval($exclude_post_id);
1195
1196 if ($attachment_id <= 0) {
1197 return false;
1198 }
1199
1200 $query = "SELECT COUNT(1) FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id' AND meta_value = %d";
1201 $params = array($attachment_id);
1202
1203 if ($exclude_post_id > 0) {
1204 $query .= " AND post_id <> %d";
1205 $params[] = $exclude_post_id;
1206 }
1207
1208 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Query is built dynamically and prepared with placeholders for the only user-supplied values.
1209 $count = $wpdb->get_var($wpdb->prepare($query, $params));
1210 return intval($count) > 0;
1211 }
1212
1213 /**
1214 * Register a lightweight image-only log entry for future prompt prefill.
1215 *
1216 * @param int $post_id Post ID.
1217 * @param string $prompt Prompt used.
1218 * @param string $image_url Generated image URL.
1219 * @param string $provider Current provider.
1220 * @param string $model Current model.
1221 * @param array|null $source_log Existing latest log for this post.
1222 * @return int|false
1223 */
1224 function botwriter_register_only_image_log($post_id, $prompt, $image_url, $provider, $model, $source_log = null) {
1225 $post = get_post($post_id);
1226 if (!($post instanceof WP_Post)) {
1227 return false;
1228 }
1229
1230 $base = array(
1231 'id_task' => 0,
1232 'id_task_server' => 0,
1233 'post_status' => $post->post_status ?: 'draft',
1234 'task_name' => sprintf(
1235 /* translators: %d: post ID. */
1236 __('Image regeneration for post #%d', 'botwriter'),
1237 intval($post_id)
1238 ),
1239 'task_type' => 'only_image',
1240 'writer' => 'orion',
1241 'narration' => 'Descriptive',
1242 'custom_style' => '',
1243 'post_language' => substr(get_locale(), 0, 2),
1244 'post_length' => '800',
1245 'link_post_original' => get_permalink($post_id),
1246 'id_post_published' => intval($post_id),
1247 'task_status' => 'completed',
1248 'error' => '',
1249 'website_name' => '',
1250 'website_type' => 'ai',
1251 'domain_name' => esc_url_raw(get_site_url()),
1252 'post_type' => $post->post_type ?: 'post',
1253 'category_id' => '',
1254 'taxonomy_data' => '',
1255 'website_category_id' => '',
1256 'aigenerated_title' => get_the_title($post_id),
1257 'aigenerated_content' => '',
1258 'aigenerated_tags' => '',
1259 'aigenerated_image' => $image_url,
1260 'post_count' => '1',
1261 'post_order' => '',
1262 'title_prompt' => '',
1263 'content_prompt' => '',
1264 'tags_prompt' => '',
1265 'image_prompt' => $prompt,
1266 'image_generating_status' => 'completed',
1267 'author_selection' => strval($post->post_author ?: get_current_user_id()),
1268 'news_time_published' => '',
1269 'news_language' => '',
1270 'news_country' => '',
1271 'news_keyword' => '',
1272 'news_source' => '',
1273 'rss_source' => '',
1274 'ai_keywords' => '',
1275 'disable_ai_images' => 0,
1276 'template_id' => null,
1277 'intentosfase1' => 0,
1278 'last_execution_time' => current_time('mysql'),
1279 );
1280
1281 // Reuse as much context as possible from latest known log.
1282 if (is_array($source_log) && !empty($source_log)) {
1283 $inherit_keys = array(
1284 'id_task',
1285 'post_status',
1286 'task_name',
1287 'writer',
1288 'narration',
1289 'custom_style',
1290 'post_language',
1291 'post_length',
1292 'website_name',
1293 'website_type',
1294 'domain_name',
1295 'post_type',
1296 'category_id',
1297 'taxonomy_data',
1298 'website_category_id',
1299 'title_prompt',
1300 'content_prompt',
1301 'tags_prompt',
1302 'author_selection',
1303 'ai_keywords',
1304 'template_id',
1305 );
1306
1307 foreach ($inherit_keys as $key) {
1308 if (array_key_exists($key, $source_log) && $source_log[$key] !== null && $source_log[$key] !== '') {
1309 $base[$key] = $source_log[$key];
1310 }
1311 }
1312 }
1313
1314 // Ensure this log is identifiable as image-only and references current settings context.
1315 $base['task_type'] = 'only_image';
1316 $base['task_status'] = 'completed';
1317 $base['id_post_published'] = intval($post_id);
1318 $base['image_prompt'] = $prompt;
1319 $base['aigenerated_image'] = $image_url;
1320 $base['error'] = '';
1321 $base['last_execution_time'] = current_time('mysql');
1322 $base['task_name'] = sprintf(
1323 /* translators: 1: image provider name, 2: image model name, 3: post ID. */
1324 __('Only image regeneration (%1$s / %2$s) - Post #%3$d', 'botwriter'),
1325 $provider,
1326 $model,
1327 intval($post_id)
1328 );
1329
1330 return botwriter_logs_register($base);
1331 }
1332
1333 /**
1334 * AJAX: apply a regenerated image URL as featured image for an existing post.
1335 * If no image_url is provided, it can generate one using current settings (legacy fallback).
1336 */
1337 function botwriter_apply_post_regenerated_image_ajax() {
1338 if (!current_user_can('manage_options')) {
1339 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
1340 }
1341
1342 check_ajax_referer('botwriter_regenerate_image_nonce', 'nonce');
1343
1344 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1345 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
1346 $image_url = isset($_POST['image_url']) ? esc_url_raw(wp_unslash($_POST['image_url'])) : '';
1347 $cleanup_policy = isset($_POST['cleanup_policy']) ? sanitize_key(wp_unslash($_POST['cleanup_policy'])) : 'keep_old';
1348 $provider = isset($_POST['provider']) ? sanitize_key(wp_unslash($_POST['provider'])) : '';
1349 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
1350
1351 if ($post_id <= 0 || !get_post($post_id)) {
1352 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
1353 }
1354
1355 if (!current_user_can('edit_post', $post_id)) {
1356 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
1357 }
1358
1359 if (trim($prompt) === '') {
1360 wp_send_json_error(array('message' => __('Image prompt is required.', 'botwriter')));
1361 }
1362
1363 if (!in_array($cleanup_policy, array('keep_old', 'delete_old'), true)) {
1364 $cleanup_policy = 'keep_old';
1365 }
1366
1367 if ($image_url === '') {
1368 // Legacy fallback: if called without image_url, generate directly now.
1369 $generated = botwriter_generate_image_with_current_settings($prompt);
1370 if (empty($generated['success'])) {
1371 wp_send_json_error(array('message' => (string) ($generated['message'] ?? __('Image generation failed.', 'botwriter'))));
1372 }
1373 $image_url = (string) $generated['image_url'];
1374 $provider = (string) $generated['provider'];
1375 $model = (string) $generated['model'];
1376 }
1377
1378 if (strpos($image_url, 'http://') !== 0 && strpos($image_url, 'https://') !== 0) {
1379 wp_send_json_error(array('message' => __('Invalid image URL.', 'botwriter')));
1380 }
1381
1382 if ($provider === '' || $model === '') {
1383 $settings = botwriter_get_current_image_generation_settings();
1384 if ($provider === '') {
1385 $provider = (string) ($settings['provider'] ?? 'dalle');
1386 }
1387 if ($model === '') {
1388 $model = (string) ($settings['model'] ?? 'gpt-image-1');
1389 }
1390 }
1391
1392 $old_thumbnail_id = get_post_thumbnail_id($post_id);
1393 $post_title = get_the_title($post_id);
1394
1395 botwriter_attach_image_to_post($post_id, $image_url, $post_title);
1396 $new_thumbnail_id = get_post_thumbnail_id($post_id);
1397
1398 if (empty($new_thumbnail_id)) {
1399 wp_send_json_error(array('message' => __('Image was generated but could not be attached as featured image.', 'botwriter')));
1400 }
1401
1402 $deleted_old = false;
1403 $delete_note = '';
1404 if ($cleanup_policy === 'delete_old' && !empty($old_thumbnail_id) && intval($old_thumbnail_id) !== intval($new_thumbnail_id)) {
1405 if (botwriter_is_attachment_featured_elsewhere(intval($old_thumbnail_id), $post_id)) {
1406 $delete_note = __('Previous featured image was not deleted because it is used by other posts.', 'botwriter');
1407 } else {
1408 $deleted_old = (bool) wp_delete_attachment(intval($old_thumbnail_id), true);
1409 if (!$deleted_old) {
1410 $delete_note = __('Previous featured image could not be deleted automatically.', 'botwriter');
1411 }
1412 }
1413 }
1414
1415 $latest_log = botwriter_get_latest_log_by_post_id($post_id);
1416 botwriter_log('Apply regenerated image: persisting prompt to log/meta', array(
1417 'post_id' => $post_id,
1418 'provider' => $provider,
1419 'model' => $model,
1420 'prompt_len' => strlen((string) $prompt),
1421 'latest_log_id' => is_array($latest_log) ? intval($latest_log['id'] ?? 0) : 0,
1422 ));
1423 $log_id = botwriter_register_only_image_log($post_id, $prompt, $image_url, $provider, $model, $latest_log);
1424 botwriter_save_post_image_prompt_meta($post_id, $prompt, $provider);
1425
1426 $thumb_src = wp_get_attachment_image_src($new_thumbnail_id, 'medium');
1427 $featured_src = is_array($thumb_src) ? $thumb_src[0] : '';
1428
1429 wp_send_json_success(array(
1430 'message' => __('Featured image regenerated successfully.', 'botwriter'),
1431 'post_id' => $post_id,
1432 'image_url' => $image_url,
1433 'featured_image_src' => $featured_src,
1434 'attachment_id' => intval($new_thumbnail_id),
1435 'provider' => $provider,
1436 'model' => $model,
1437 'deleted_old' => $deleted_old,
1438 'delete_note' => $delete_note,
1439 'log_id' => $log_id ?: 0,
1440 ));
1441 }
1442 add_action('wp_ajax_botwriter_apply_post_regenerated_image', 'botwriter_apply_post_regenerated_image_ajax');
1443 // Backward-compatible alias for previous one-step endpoint name.
1444 add_action('wp_ajax_botwriter_regenerate_post_image', 'botwriter_apply_post_regenerated_image_ajax');
1445
1446 /**
1447 * Build an instruction prompt for the post editor assistant.
1448 *
1449 * @param string $target Selected field target.
1450 * @param string $user_prompt User instruction.
1451 * @param array $context Current post context.
1452 * @return string
1453 */
1454 function botwriter_build_editor_assistant_prompt($target, $user_prompt, $context) {
1455 $rules = array(
1456 'text' => 'Return only the improved post body as valid HTML. Do not include title, tags, excerpt, SEO meta, or explanations.',
1457 'title' => 'Return only one improved post title as plain text. No quotes, bullets, or commentary.',
1458 'tags' => 'Return only a comma-separated list of tags. No hashtags, no numbering, and no extra text.',
1459 'excerpt' => 'Return only one short excerpt (max 160 characters) as plain text.',
1460 'seo_meta' => 'Return only one SEO meta description (max 160 characters) as plain text.',
1461 );
1462
1463 $context_payload = array(
1464 'title' => (string) ($context['title'] ?? ''),
1465 'content' => (string) ($context['content'] ?? ''),
1466 'tags' => (string) ($context['tags'] ?? ''),
1467 'excerpt' => (string) ($context['excerpt'] ?? ''),
1468 'seo_meta' => (string) ($context['seo_meta'] ?? ''),
1469 );
1470
1471 $context_json = wp_json_encode($context_payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
1472 if (!is_string($context_json) || $context_json === '') {
1473 $context_json = '{}';
1474 }
1475
1476 $target_rule = isset($rules[$target]) ? $rules[$target] : $rules['text'];
1477
1478 return "You are BotWriter inline editor assistant for WordPress.\n"
1479 . "The user is editing a post right now.\n"
1480 . "Selected field: {$target}\n"
1481 . "Output format rule: {$target_rule}\n"
1482 . "Keep the same language as the source content unless the user asks otherwise.\n"
1483 . "Never use markdown code fences.\n\n"
1484 . "User instruction:\n{$user_prompt}\n\n"
1485 . "Current post context JSON:\n{$context_json}";
1486 }
1487
1488 /**
1489 * Call the Cloudflare Worker for editor assistant generation.
1490 *
1491 * Uses a dedicated /editor endpoint and falls back to /woo only when the
1492 * new endpoint is not available yet.
1493 *
1494 * @param string $provider Provider key (openai, anthropic, google, etc.).
1495 * @param string $api_key Provider API key.
1496 * @param string $model Model name.
1497 * @param string $prompt Prompt text.
1498 * @param int $max_tokens Max output tokens.
1499 * @param float $temperature Temperature.
1500 * @return string|WP_Error
1501 */
1502 function botwriter_call_editor_worker($provider, $api_key, $model, $prompt, $max_tokens = 2048, $temperature = 0.35) {
1503 $ssl_verify = get_option('botwriter_sslverify', 'yes') === 'yes';
1504 botwriter_ensure_site_token();
1505
1506 $provider_map = array(
1507 'google' => 'gemini',
1508 );
1509 $worker_provider = isset($provider_map[$provider]) ? $provider_map[$provider] : $provider;
1510
1511 $key_field_map = array(
1512 'openai' => 'openai_api_key',
1513 'anthropic' => 'anthropic_api_key',
1514 'google' => 'google_api_key',
1515 'mistral' => 'mistral_api_key',
1516 'groq' => 'groq_api_key',
1517 'openrouter' => 'openrouter_api_key',
1518 );
1519
1520 $domain = preg_replace('#^https?://#', '', home_url());
1521 $domain = rtrim((string) $domain, '/');
1522
1523 $payload = array(
1524 'prompt' => $prompt,
1525 'domain' => $domain,
1526 'provider' => $worker_provider,
1527 'model' => $model,
1528 'max_tokens' => intval($max_tokens),
1529 'temperature' => floatval($temperature),
1530 'site_token' => get_option('botwriter_site_token', ''),
1531 // Keep editor assistant out of quota checks for now.
1532 'no_count' => true,
1533 'assistant' => 'post_editor',
1534 );
1535
1536 if (!empty($api_key) && isset($key_field_map[$provider])) {
1537 $payload[$key_field_map[$provider]] = $api_key;
1538 }
1539
1540 $base_url = rtrim(BOTWRITER_API_URL, '/');
1541 $endpoints = array(
1542 $base_url . '/editor',
1543 $base_url . '/woo',
1544 );
1545
1546 $endpoint_total = count($endpoints);
1547 $token_retry_attempted = false;
1548 foreach ($endpoints as $index => $remote_url) {
1549 $response = wp_remote_post($remote_url, array(
1550 'timeout' => 90,
1551 'sslverify' => $ssl_verify,
1552 'headers' => array('Content-Type' => 'application/json'),
1553 'body' => wp_json_encode($payload),
1554 ));
1555
1556 if (is_wp_error($response)) {
1557 if ($index === $endpoint_total - 1) {
1558 return new WP_Error('editor_worker_network', $response->get_error_message(), array(
1559 'provider' => (string) $provider,
1560 'worker_provider' => (string) $worker_provider,
1561 'model' => (string) $model,
1562 'endpoint' => (string) $remote_url,
1563 'transport_code' => (string) $response->get_error_code(),
1564 ));
1565 }
1566 continue;
1567 }
1568
1569 $http_code = wp_remote_retrieve_response_code($response);
1570 $body = wp_remote_retrieve_body($response);
1571 $data = json_decode($body, true);
1572
1573 // If the new route is not deployed yet, retry once with /woo.
1574 if ($http_code === 404 && $index === 0) {
1575 continue;
1576 }
1577
1578 if (is_array($data) && !empty($data['site_token'])) {
1579 update_option('botwriter_site_token', sanitize_text_field((string) $data['site_token']));
1580 }
1581
1582 if (is_array($data) && !empty($data['warning']) && function_exists('botwriter_announcements_add')) {
1583 botwriter_announcements_add(
1584 __('Service notice', 'botwriter'),
1585 (string) $data['warning']
1586 );
1587 }
1588
1589 if ($http_code !== 200 || (isset($data['status']) && $data['status'] === 'error')) {
1590 $worker_error_code = is_array($data) ? (string) ($data['error_code'] ?? '') : '';
1591 $worker_error = is_array($data) ? (string) ($data['error'] ?? '') : '';
1592 $worker_error_message = is_array($data) ? (string) ($data['error_message'] ?? $data['message'] ?? '') : '';
1593 $token_issue = in_array($worker_error_code, array('invalid_site_token', 'token_required'), true)
1594 || in_array($worker_error, array('invalid_site_token', 'token_required'), true)
1595 || stripos($worker_error_message, 'site token') !== false
1596 || stripos($worker_error_message, 'requires authentication') !== false;
1597
1598 if ($token_issue && !$token_retry_attempted && !empty($payload['site_token'])) {
1599 $token_retry_attempted = true;
1600 delete_option('botwriter_site_token');
1601
1602 $retry_payload = $payload;
1603 $retry_payload['site_token'] = '';
1604
1605 botwriter_log('Editor assistant site token mismatch detected. Retrying request with empty site_token.', array(
1606 'error_code' => $worker_error_code,
1607 'error' => $worker_error,
1608 'endpoint' => $remote_url,
1609 ));
1610
1611 $retry_response = wp_remote_post($remote_url, array(
1612 'timeout' => 90,
1613 'sslverify' => $ssl_verify,
1614 'headers' => array('Content-Type' => 'application/json'),
1615 'body' => wp_json_encode($retry_payload),
1616 ));
1617
1618 if (!is_wp_error($retry_response)) {
1619 $http_code = wp_remote_retrieve_response_code($retry_response);
1620 $retry_body = wp_remote_retrieve_body($retry_response);
1621 $retry_data = json_decode($retry_body, true);
1622
1623 if (is_array($retry_data)) {
1624 $data = $retry_data;
1625 if (!empty($retry_data['site_token'])) {
1626 update_option('botwriter_site_token', sanitize_text_field((string) $retry_data['site_token']));
1627 }
1628 }
1629 }
1630 }
1631
1632 if ($http_code !== 200 || (isset($data['status']) && $data['status'] === 'error')) {
1633 $error_message = '';
1634 if (is_array($data)) {
1635 $error_message = (string) ($data['error_message'] ?? $data['error'] ?? $data['message'] ?? '');
1636 }
1637 if ($error_message === '') {
1638 $error_message = "HTTP {$http_code}";
1639 }
1640 return new WP_Error('editor_worker_error', $error_message, array(
1641 'provider' => (string) $provider,
1642 'worker_provider' => (string) $worker_provider,
1643 'model' => (string) $model,
1644 'endpoint' => (string) $remote_url,
1645 'http_code' => (int) $http_code,
1646 'worker_error_code' => is_array($data) ? (string) ($data['error_code'] ?? '') : '',
1647 ));
1648 }
1649 }
1650
1651 $content = is_array($data) ? (string) ($data['content'] ?? '') : '';
1652 if ($content === '') {
1653 return new WP_Error('editor_worker_empty', __('AI returned an empty response.', 'botwriter'));
1654 }
1655
1656 return $content;
1657 }
1658
1659 return new WP_Error('editor_worker_unavailable', __('Editor assistant service is currently unavailable.', 'botwriter'));
1660 }
1661
1662 // SEO module relocated to includes/seo/ — see botwriter_seo_register_admin_menu and botwriter_seo_auto_internal_links_postprocess.
1663
1664 /**
1665 * Strip wrapping quotes added by model responses.
1666 *
1667 * @param string $text Input text.
1668 * @return string
1669 */
1670 function botwriter_editor_strip_wrapping_quotes($text) {
1671 $text = trim((string) $text);
1672
1673 for ($i = 0; $i < 3; $i++) {
1674 $next = preg_replace("/^[\"'`\\x{201C}\\x{201D}\\x{00AB}\\x{00BB}\\x{2018}\\x{2019}]+|[\"'`\\x{201C}\\x{201D}\\x{00AB}\\x{00BB}\\x{2018}\\x{2019}]+$/u", '', $text);
1675 $next = trim((string) $next);
1676 if ($next === $text) {
1677 break;
1678 }
1679 $text = $next;
1680 }
1681
1682 return $text;
1683 }
1684
1685 /**
1686 * AJAX: generate post editor assistant output for selected field.
1687 */
1688 function botwriter_editor_assistant_generate_ajax() {
1689 check_ajax_referer('botwriter_editor_assistant_nonce', 'nonce');
1690
1691 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1692 if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
1693 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
1694 }
1695 if ($post_id <= 0 && !current_user_can('edit_posts') && !current_user_can('edit_pages')) {
1696 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
1697 }
1698
1699 $allowed_targets = array('text', 'title', 'tags', 'excerpt', 'seo_meta', 'internal_links');
1700 $target = isset($_POST['target']) ? sanitize_key(wp_unslash($_POST['target'])) : 'text';
1701 if (!in_array($target, $allowed_targets, true)) {
1702 $target = 'text';
1703 }
1704
1705 $user_prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
1706 if ($user_prompt === '') {
1707 wp_send_json_error(array('message' => __('Prompt is required.', 'botwriter')));
1708 }
1709
1710 $context = array(
1711 'title' => isset($_POST['context_title']) ? sanitize_text_field(wp_unslash($_POST['context_title'])) : '',
1712 'content' => isset($_POST['context_content']) ? wp_kses_post(wp_unslash($_POST['context_content'])) : '',
1713 'tags' => isset($_POST['context_tags']) ? sanitize_text_field(wp_unslash($_POST['context_tags'])) : '',
1714 'excerpt' => isset($_POST['context_excerpt']) ? sanitize_textarea_field(wp_unslash($_POST['context_excerpt'])) : '',
1715 'seo_meta' => isset($_POST['context_seo_meta']) ? sanitize_textarea_field(wp_unslash($_POST['context_seo_meta'])) : '',
1716 );
1717
1718 $context_limits = array(
1719 'title' => 300,
1720 'content' => 30000,
1721 'tags' => 1000,
1722 'excerpt' => 500,
1723 'seo_meta' => 500,
1724 );
1725 foreach ($context_limits as $field => $max_length) {
1726 $value = (string) ($context[$field] ?? '');
1727 if (function_exists('mb_strlen') && function_exists('mb_substr')) {
1728 if (mb_strlen($value) > $max_length) {
1729 $context[$field] = mb_substr($value, 0, $max_length);
1730 }
1731 } elseif (strlen($value) > $max_length) {
1732 $context[$field] = substr($value, 0, $max_length);
1733 }
1734 }
1735
1736 $keyphrases_raw = isset($_POST['context_keyphrases']) ? sanitize_text_field(wp_unslash($_POST['context_keyphrases'])) : '';
1737 $keyphrases = botwriter_editor_parse_keyphrases($keyphrases_raw);
1738
1739 $provider = sanitize_key((string) get_option('botwriter_text_provider', 'openai'));
1740 $model = function_exists('botwriter_get_current_text_model')
1741 ? (string) botwriter_get_current_text_model()
1742 : (string) get_option('botwriter_openai_model', 'gpt-5.4-mini');
1743 $api_key = function_exists('botwriter_get_provider_api_key')
1744 ? (string) botwriter_get_provider_api_key($provider)
1745 : '';
1746
1747 // SEO settings tab controls automatic publish post-processing, not the editor widget.
1748 $internal_links_ai_enabled = true;
1749 $internal_links_noai_enabled = true;
1750
1751 if ($target === 'internal_links') {
1752 $candidates = botwriter_editor_get_internal_link_candidates($post_id, $context, 26);
1753 if (empty($candidates)) {
1754 wp_send_json_success(array(
1755 'target' => $target,
1756 'suggestions' => array(),
1757 'provider' => $provider,
1758 'model' => $model,
1759 'candidate_count' => 0,
1760 'strategy' => 'no_candidates',
1761 ));
1762 }
1763
1764 $use_ai_strategy = $internal_links_ai_enabled && $api_key !== '';
1765 if (!$use_ai_strategy) {
1766 if (!$internal_links_noai_enabled) {
1767 if ($api_key === '') {
1768 wp_send_json_error(array('message' => __('Please configure the API key for your selected text provider, or enable deterministic internal-link mode in SEO settings.', 'botwriter')));
1769 }
1770
1771 wp_send_json_error(array('message' => __('Internal-link generation is disabled. Enable AI mode or deterministic mode in SEO settings.', 'botwriter')));
1772 }
1773
1774 $suggestions = botwriter_editor_build_internal_links_noai_suggestions($candidates, $context, $keyphrases, 8);
1775 wp_send_json_success(array(
1776 'target' => $target,
1777 'suggestions' => $suggestions,
1778 'provider' => $provider,
1779 'model' => $model,
1780 'candidate_count' => count($candidates),
1781 'keyphrases' => $keyphrases,
1782 'strategy' => 'no_ai',
1783 ));
1784 }
1785
1786 $links_prompt = botwriter_build_editor_internal_links_prompt($user_prompt, $context, $keyphrases, $candidates);
1787 $generated_links = botwriter_call_editor_worker($provider, $api_key, $model, $links_prompt, 2400, 0.2);
1788
1789 if (is_wp_error($generated_links)) {
1790 $error_message = $generated_links->get_error_message();
1791 if ($error_message === '') {
1792 $error_message = __('Could not generate internal link suggestions.', 'botwriter');
1793 }
1794 wp_send_json_error(array('message' => $error_message));
1795 }
1796
1797 $suggestions = botwriter_parse_editor_internal_links_response((string) $generated_links, $candidates);
1798
1799 wp_send_json_success(array(
1800 'target' => $target,
1801 'suggestions' => $suggestions,
1802 'provider' => $provider,
1803 'model' => $model,
1804 'candidate_count' => count($candidates),
1805 'keyphrases' => $keyphrases,
1806 'strategy' => 'ai',
1807 ));
1808 }
1809
1810 if ($api_key === '') {
1811 wp_send_json_error(array('message' => __('Please configure the API key for your selected text provider in BotWriter settings.', 'botwriter')));
1812 }
1813
1814 $max_tokens = ($target === 'text') ? 4096 : 700;
1815 $assistant_prompt = botwriter_build_editor_assistant_prompt($target, $user_prompt, $context);
1816 $generated = botwriter_call_editor_worker($provider, $api_key, $model, $assistant_prompt, $max_tokens, 0.35);
1817
1818 if (is_wp_error($generated)) {
1819 $error_message = $generated->get_error_message();
1820 if ($error_message === '') {
1821 $error_message = __('Could not generate a response.', 'botwriter');
1822 }
1823 wp_send_json_error(array('message' => $error_message));
1824 }
1825
1826 $content = trim((string) $generated);
1827 $content = preg_replace('/^```(?:[a-zA-Z0-9_-]+)?\s*/', '', $content);
1828 $content = preg_replace('/\s*```$/', '', $content);
1829 $content = trim((string) $content);
1830
1831 if ($content === '') {
1832 wp_send_json_error(array('message' => __('AI returned an empty response.', 'botwriter')));
1833 }
1834
1835 if ($target === 'title') {
1836 $content = sanitize_text_field($content);
1837 $content = botwriter_editor_strip_wrapping_quotes($content);
1838 } elseif ($target === 'tags') {
1839 $parts = preg_split('/[\r\n,]+/', $content);
1840 $parts = is_array($parts) ? $parts : array();
1841 $tags = array();
1842 foreach ($parts as $part) {
1843 $tag = trim(sanitize_text_field((string) $part));
1844 if ($tag !== '') {
1845 $tags[] = $tag;
1846 }
1847 }
1848 $tags = array_values(array_unique($tags));
1849 $content = implode(', ', $tags);
1850 } elseif ($target === 'excerpt' || $target === 'seo_meta') {
1851 $content = botwriter_editor_strip_wrapping_quotes($content);
1852 if (function_exists('botwriter_sanitize_meta_description')) {
1853 $content = botwriter_sanitize_meta_description($content);
1854 } else {
1855 $content = sanitize_textarea_field($content);
1856 }
1857 } else {
1858 $content = botwriter_editor_strip_wrapping_quotes($content);
1859 $content = str_replace(array("\\r\\n", "\\n", "\\r"), "\n", $content);
1860 $content = preg_replace('/(\r?\n){3,}/', "\n\n", $content);
1861 $content = wp_kses_post($content);
1862 }
1863
1864 wp_send_json_success(array(
1865 'target' => $target,
1866 'content' => $content,
1867 'provider' => $provider,
1868 'model' => $model,
1869 ));
1870 }
1871 add_action('wp_ajax_botwriter_editor_ai_generate', 'botwriter_editor_assistant_generate_ajax');
1872
1873 /**
1874 * Render SEO checks for editor widget tab.
1875 *
1876 * @param array $seo_report SEO report array.
1877 * @return string
1878 */
1879 function botwriter_editor_render_seo_checks_html($seo_report) {
1880 $seo_report = is_array($seo_report) ? $seo_report : array();
1881 $seo_score = (int) ($seo_report['score'] ?? 0);
1882 $seo_grade = (string) ($seo_report['grade'] ?? 'n/a');
1883 $grade_label = function_exists('botwriter_seo_grade_label')
1884 ? botwriter_seo_grade_label($seo_grade)
1885 : ucfirst($seo_grade);
1886
1887 $seo_counts = array('good' => 0, 'warn' => 0, 'bad' => 0);
1888 foreach ((array) ($seo_report['checks'] ?? array()) as $check) {
1889 $status = function_exists('botwriter_seo_check_status')
1890 ? botwriter_seo_check_status($check)
1891 : (!empty($check['passed']) ? 'good' : 'bad');
1892 $seo_counts[$status] = ($seo_counts[$status] ?? 0) + 1;
1893 }
1894
1895 ob_start();
1896 ?>
1897 <div class="bw-editor-ai-seo-score-box bw-grade-<?php echo esc_attr($seo_grade); ?>">
1898 <div class="bw-editor-ai-seo-score-main"><?php echo (int) $seo_score; ?></div>
1899 <div class="bw-editor-ai-seo-score-label"><?php echo esc_html($grade_label); ?></div>
1900 </div>
1901 <div class="bw-summary-row">
1902 <span class="bw-pill good"><span class="dashicons dashicons-yes-alt"></span> <?php echo (int) ($seo_counts['good'] ?? 0); ?> <?php esc_html_e('passed', 'botwriter'); ?></span>
1903 <span class="bw-pill warn"><span class="dashicons dashicons-warning"></span> <?php echo (int) ($seo_counts['warn'] ?? 0); ?> <?php esc_html_e('to improve', 'botwriter'); ?></span>
1904 <span class="bw-pill bad"><span class="dashicons dashicons-dismiss"></span> <?php echo (int) ($seo_counts['bad'] ?? 0); ?> <?php esc_html_e('issues', 'botwriter'); ?></span>
1905 </div>
1906 <ul class="bw-report-checks">
1907 <?php foreach ((array) ($seo_report['checks'] ?? array()) as $check) :
1908 $status = function_exists('botwriter_seo_check_status')
1909 ? botwriter_seo_check_status($check)
1910 : (!empty($check['passed']) ? 'good' : 'bad');
1911 $icon = function_exists('botwriter_seo_status_icon')
1912 ? botwriter_seo_status_icon($status)
1913 : ($status === 'good' ? 'dashicons-yes-alt' : ($status === 'warn' ? 'dashicons-warning' : 'dashicons-dismiss'));
1914 ?>
1915 <li class="bw-check bw-status-<?php echo esc_attr($status); ?>">
1916 <span class="dashicons <?php echo esc_attr($icon); ?> bw-check-icon"></span>
1917 <div class="bw-check-body">
1918 <div class="bw-check-label"><?php echo esc_html((string) ($check['label'] ?? '')); ?></div>
1919 <?php if (!empty($check['hint'])) : ?>
1920 <div class="bw-check-hint"><?php echo esc_html((string) $check['hint']); ?></div>
1921 <?php endif; ?>
1922 </div>
1923 <?php if ((int) ($check['weight'] ?? 0) > 0) : ?>
1924 <span class="bw-weight" title="<?php esc_attr_e('Weight', 'botwriter'); ?>"><?php echo (int) ($check['weight'] ?? 0); ?></span>
1925 <?php endif; ?>
1926 </li>
1927 <?php endforeach; ?>
1928 </ul>
1929 <?php
1930 return (string) ob_get_clean();
1931 }
1932
1933 /**
1934 * Render readability checks for editor widget tab.
1935 *
1936 * @param array $readability_report Readability report array.
1937 * @return string
1938 */
1939 function botwriter_editor_render_readability_checks_html($readability_report) {
1940 $readability_report = is_array($readability_report) ? $readability_report : array();
1941 $read_score = (int) ($readability_report['score'] ?? 0);
1942 $read_grade = (string) ($readability_report['grade'] ?? 'n/a');
1943 $grade_label = function_exists('botwriter_seo_grade_label')
1944 ? botwriter_seo_grade_label($read_grade)
1945 : ucfirst($read_grade);
1946
1947 $read_counts = array('good' => 0, 'warn' => 0, 'bad' => 0);
1948 foreach ((array) ($readability_report['checks'] ?? array()) as $check) {
1949 $status = (string) ($check['status'] ?? 'bad');
1950 $read_counts[$status] = ($read_counts[$status] ?? 0) + 1;
1951 }
1952
1953 ob_start();
1954 ?>
1955 <div class="bw-editor-ai-seo-score-box bw-grade-<?php echo esc_attr($read_grade); ?>">
1956 <div class="bw-editor-ai-seo-score-main"><?php echo (int) $read_score; ?></div>
1957 <div class="bw-editor-ai-seo-score-label"><?php echo esc_html($grade_label); ?></div>
1958 </div>
1959 <div class="bw-summary-row">
1960 <span class="bw-pill good"><span class="dashicons dashicons-yes-alt"></span> <?php echo (int) ($read_counts['good'] ?? 0); ?> <?php esc_html_e('great', 'botwriter'); ?></span>
1961 <span class="bw-pill warn"><span class="dashicons dashicons-warning"></span> <?php echo (int) ($read_counts['warn'] ?? 0); ?> <?php esc_html_e('ok', 'botwriter'); ?></span>
1962 <span class="bw-pill bad"><span class="dashicons dashicons-dismiss"></span> <?php echo (int) ($read_counts['bad'] ?? 0); ?> <?php esc_html_e('hard', 'botwriter'); ?></span>
1963 </div>
1964 <ul class="bw-report-checks">
1965 <?php foreach ((array) ($readability_report['checks'] ?? array()) as $check) :
1966 $status = (string) ($check['status'] ?? 'bad');
1967 $icon = function_exists('botwriter_seo_status_icon')
1968 ? botwriter_seo_status_icon($status)
1969 : ($status === 'good' ? 'dashicons-yes-alt' : ($status === 'warn' ? 'dashicons-warning' : 'dashicons-dismiss'));
1970 ?>
1971 <li class="bw-check bw-status-<?php echo esc_attr($status); ?>">
1972 <span class="dashicons <?php echo esc_attr($icon); ?> bw-check-icon"></span>
1973 <div class="bw-check-body">
1974 <div class="bw-check-label">
1975 <?php echo esc_html((string) ($check['label'] ?? '')); ?>
1976 <?php if (!empty($check['value'])) : ?>
1977 <span class="bw-tag"><?php echo esc_html((string) $check['value']); ?></span>
1978 <?php endif; ?>
1979 </div>
1980 <?php if (!empty($check['hint'])) : ?>
1981 <div class="bw-check-hint"><?php echo esc_html((string) $check['hint']); ?></div>
1982 <?php endif; ?>
1983 </div>
1984 <span class="bw-weight" title="<?php esc_attr_e('Weight', 'botwriter'); ?>"><?php echo (int) ($check['weight'] ?? 0); ?></span>
1985 </li>
1986 <?php endforeach; ?>
1987 </ul>
1988 <?php
1989 return (string) ob_get_clean();
1990 }
1991
1992 /**
1993 * AJAX: return SEO and readability report sections for editor widget.
1994 */
1995 function botwriter_editor_assistant_get_seo_report_ajax() {
1996 check_ajax_referer('botwriter_editor_assistant_nonce', 'nonce');
1997
1998 $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
1999 if ($post_id <= 0) {
2000 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
2001 }
2002
2003 if (!current_user_can('edit_post', $post_id)) {
2004 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
2005 }
2006
2007 if (!function_exists('botwriter_seo_compute_score') || !function_exists('botwriter_seo_compute_readability')) {
2008 wp_send_json_error(array('message' => __('SEO module is not available.', 'botwriter')));
2009 }
2010
2011 $seo_report = botwriter_seo_compute_score($post_id);
2012 $readability_report = botwriter_seo_compute_readability($post_id);
2013
2014 wp_send_json_success(array(
2015 'seo_html' => botwriter_editor_render_seo_checks_html($seo_report),
2016 'readability_html' => botwriter_editor_render_readability_checks_html($readability_report),
2017 ));
2018 }
2019 add_action('wp_ajax_botwriter_editor_ai_get_seo_report', 'botwriter_editor_assistant_get_seo_report_ajax');
2020
2021
2022
2023 if (!function_exists('deactivate_plugins')) {
2024 require_once ABSPATH . 'wp-admin/includes/plugin.php';
2025 }
2026
2027
2028 function botwriter_enqueue_styles(){
2029 $my_plugin_dir = plugin_dir_url(__FILE__);
2030 $screen = get_current_screen();
2031
2032 $slug = $screen->id;
2033
2034 // Keep submenu cleanup styles available across the whole admin so folded flyouts stay filtered too.
2035 wp_register_style('botwriter_admin_menu', $my_plugin_dir . 'assets/css/admin-menu.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/admin-menu.css'));
2036 wp_enqueue_style('botwriter_admin_menu');
2037
2038 // Welcome banner CSS - load on ALL admin pages if not dismissed
2039 // (because admin_notices shows on all pages)
2040 $welcome_dismissed = get_option('botwriter_welcome_dismissed', false);
2041 if (!$welcome_dismissed) {
2042 wp_register_style('botwriter_welcome_banner', $my_plugin_dir . 'assets/css/welcome-banner.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/welcome-banner.css'));
2043 wp_enqueue_style('botwriter_welcome_banner');
2044 }
2045
2046 if (botwriter_should_enqueue_editor_assistant($screen)) {
2047 wp_register_style('botwriter_editor_assistant', $my_plugin_dir . 'assets/css/editor-ai-assistant.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/editor-ai-assistant.css'));
2048 wp_enqueue_style('botwriter_editor_assistant');
2049 }
2050
2051 // Only enqueue other styles for BotWriter admin screens
2052
2053 if (strpos((string)$slug, 'botwriter') !== false) {
2054
2055 // Register and enqueue styles with dynamic versioning for better caching
2056 wp_register_style('botwriter_bootstrap', $my_plugin_dir . 'assets/css/bootstrap.min.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/bootstrap.min.css'));
2057 wp_enqueue_style('botwriter_bootstrap');
2058
2059 wp_register_style('botwriter_jquery_ui', $my_plugin_dir . 'assets/css/jquery-ui.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/jquery-ui.css'));
2060 wp_enqueue_style('botwriter_jquery_ui');
2061
2062 wp_register_style('botwriter_loader', $my_plugin_dir . 'assets/css/loader.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/loader.css'));
2063 wp_enqueue_style('botwriter_loader');
2064
2065 wp_register_style('botwriter_style', $my_plugin_dir . 'assets/css/style.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/style.css'));
2066 wp_enqueue_style('botwriter_style');
2067
2068 // Settings page specific styles
2069 if (strpos((string)$slug, 'botwriter_settings') !== false) {
2070 wp_register_style('botwriter_settings', $my_plugin_dir . 'assets/css/settings.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/settings.css'));
2071 wp_enqueue_style('botwriter_settings');
2072 }
2073
2074 if ($slug === 'botwriter_page_botwriter_siterewriter_page') {
2075 wp_register_style('botwriter_siterewriter', $my_plugin_dir . 'assets/css/siterewriter.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/siterewriter.css'));
2076 wp_enqueue_style('botwriter_siterewriter');
2077 }
2078 }
2079 }
2080
2081 add_action('admin_enqueue_scripts', 'botwriter_enqueue_styles');
2082
2083
2084
2085
2086 // Hook to add the admin menu
2087 add_action('admin_menu', function() {
2088 add_menu_page(
2089 __('BotWriter', 'botwriter'),
2090 __('BotWriter', 'botwriter'),
2091 'manage_options',
2092 'botwriter_menu',
2093 'botwriter_admin_page',
2094 plugin_dir_url(__FILE__) . '/assets/images/icono25.png',
2095 90
2096 );
2097
2098 add_submenu_page('botwriter_menu',
2099 __('Write now', 'botwriter'),
2100 __('Write now', 'botwriter'),
2101 'manage_options',
2102 'botwriter_write_now',
2103 'botwriter_quick_post_page_handler'
2104 );
2105
2106 add_submenu_page('botwriter_menu',
2107 __('New task', 'botwriter'),
2108 __('New task', 'botwriter'),
2109 'manage_options',
2110 'botwriter_addnew_page',
2111 'botwriter_addnew_page_handler'
2112 );
2113
2114 // Register under parent to avoid deprecations (null parent). We'll hide it from the submenu below.
2115 add_submenu_page('botwriter_menu',
2116 __('Super Task AI', 'botwriter'),
2117 __('Super Task AI', 'botwriter'),
2118 'manage_options',
2119 'botwriter_super_page',
2120 'botwriter_super_page_handler'
2121 );
2122
2123 add_submenu_page('botwriter_menu',
2124 __('Tasks AI', 'botwriter'),
2125 __('Tasks AI', 'botwriter'),
2126 'manage_options',
2127 'botwriter_automatic_posts',
2128 'botwriter_automatic_posts_page'
2129 );
2130
2131 add_submenu_page('botwriter_menu',
2132 __('Content Rewriter', 'botwriter'),
2133 __('Content Rewriter', 'botwriter'),
2134 'manage_options',
2135 'botwriter_rewriter_page',
2136 'botwriter_rewriter_page_handler'
2137 );
2138
2139 add_submenu_page('botwriter_menu',
2140 __('Site Rewriter', 'botwriter'),
2141 __('Site Rewriter', 'botwriter'),
2142 'manage_options',
2143 'botwriter_siterewriter_page',
2144 'botwriter_siterewriter_page_handler'
2145 );
2146
2147 // for development
2148 /*
2149 add_submenu_page('botwriter_menu',
2150 __('Test Call', 'botwriter'),
2151 __('Test Call', 'botwriter'),
2152 'manage_options',
2153 'botwriter_prueba',
2154 'botwriter_prueba'
2155 );
2156 */
2157
2158
2159 // Register the edit/detail page under the parent, then hide it programmatically to avoid null parent deprecations
2160 $hook = add_submenu_page('botwriter_menu',
2161 __('Add New Task', 'botwriter'),
2162 __('Add New Task', 'botwriter'),
2163 'manage_options',
2164 'botwriter_automatic_post_new',
2165 'botwriter_form_page_handler'
2166 );
2167
2168 add_submenu_page('botwriter_menu',
2169 __('Settings', 'botwriter'),
2170 __('Settings', 'botwriter'),
2171 'manage_options',
2172 'botwriter_settings',
2173 'botwriter_settings_page_handler'
2174 );
2175
2176 add_submenu_page('botwriter_menu',
2177 __('Templates', 'botwriter'),
2178 __('Templates', 'botwriter'),
2179 'manage_options',
2180 'botwriter_templates',
2181 'botwriter_templates_page_handler'
2182 );
2183
2184 add_submenu_page('botwriter_menu',
2185 __('Logs', 'botwriter'),
2186 get_option('botwriter_stopformany', false)
2187 ? __('Logs', 'botwriter') . ' <span class="update-plugins count-1" style="background:#d63638;"><span class="plugin-count">!</span></span>'
2188 : __('Logs', 'botwriter'),
2189 'manage_options',
2190 'botwriter_logs',
2191 'botwriter_logs_page_handler'
2192 );
2193 });
2194
2195 add_filter('submenu_file', function($submenu_file) {
2196 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing parameter used to highlight hidden submenu pages.
2197 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
2198 if (in_array($page, array('botwriter_rewriter_page', 'botwriter_siterewriter_page'), true)) {
2199 return 'botwriter_addnew_page';
2200 }
2201 return $submenu_file;
2202 });
2203
2204 // CSS for hiding duplicate submenu entries is now in assets/css/admin-menu.css
2205 // and enqueued via botwriter_enqueue_styles()
2206
2207
2208
2209
2210
2211 function botwriter_prueba() {
2212 if (!current_user_can('manage_options')) {
2213 return;
2214 }
2215
2216 ?>
2217
2218 <h1>Prueba...</h1>
2219 <div>
2220 Llamando a la funcion que ejecuta las tareas
2221 </div>
2222
2223 <?php
2224 botwriter_scheduled_events_execute_tasks();
2225
2226 }
2227
2228
2229
2230
2231
2232
2233 // First screen of the plugin
2234 function botwriter_admin_page() {
2235 if (!current_user_can('manage_options')) {
2236 return;
2237 }
2238
2239 // Check if any API key is configured
2240 $has_api_key = false;
2241 $text_providers = [
2242 'botwriter_openai_api_key',
2243 'botwriter_anthropic_api_key',
2244 'botwriter_google_api_key',
2245 'botwriter_mistral_api_key',
2246 'botwriter_groq_api_key',
2247 'botwriter_openrouter_api_key'
2248 ];
2249 foreach ($text_providers as $provider_key) {
2250 $key_value = get_option($provider_key);
2251 if (!empty($key_value) && function_exists('botwriter_decrypt_api_key')) {
2252 $decrypted = botwriter_decrypt_api_key($key_value);
2253 if (!empty($decrypted)) {
2254 $has_api_key = true;
2255 break;
2256 }
2257 }
2258 }
2259
2260 $settings_url = admin_url('admin.php?page=botwriter_settings');
2261 $addnew_url = admin_url('admin.php?page=botwriter_addnew_page');
2262 $tasks_url = admin_url('admin.php?page=botwriter_automatic_posts');
2263 $logs_url = admin_url('admin.php?page=botwriter_logs');
2264 ?>
2265 <div class="wrap">
2266 <div style="max-width: 900px; margin: 0 auto;">
2267
2268 <!-- Header -->
2269 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);">
2270 <h1 style="margin: 0 0 10px 0; font-size: 28px; font-weight: 600;">
2271 <span class="dashicons dashicons-superhero" style="margin-right: 10px; font-size: 28px; width: 28px; height: 28px;"></span><?php echo esc_html__('BotWriter', 'botwriter'); ?>
2272 </h1>
2273 <p style="margin: 0; font-size: 16px; opacity: 0.95;">
2274 <?php echo esc_html__('AI-Powered Content Creation for WordPress', 'botwriter'); ?>
2275 </p>
2276 </div>
2277
2278 <!-- Quick Start Alert -->
2279 <?php if (!$has_api_key): ?>
2280 <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px 20px; border-radius: 0 8px 8px 0; margin-bottom: 25px;">
2281 <strong style="color: #856404;"><span class="dashicons dashicons-lightbulb" style="font-size: 18px; width: 18px; height: 18px; vertical-align: text-bottom;"></span> <?php echo esc_html__('Quick Start:', 'botwriter'); ?></strong>
2282 <span style="color: #856404;">
2283 <?php echo esc_html__('Configure your AI provider API key to get started.', 'botwriter'); ?>
2284 <a href="<?php echo esc_url($settings_url); ?>" style="color: #856404; font-weight: 600;"><?php echo esc_html__('Go to Settings', 'botwriter'); ?> &rarr;</a>
2285 </span>
2286 </div>
2287 <?php endif; ?>
2288
2289 <!-- Description -->
2290 <div style="background: white; padding: 25px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
2291 <p style="font-size: 15px; line-height: 1.7; color: #444; margin: 0;">
2292 <?php echo esc_html__('BotWriter automates content creation using the latest AI models. Connect your preferred AI provider, configure your content sources, and let BotWriter generate SEO-optimized articles with AI-generated images, completely hands-free.', 'botwriter'); ?>
2293 </p>
2294 </div>
2295
2296 <!-- Features Grid -->
2297 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 25px;">
2298
2299 <!-- Text AI Card -->
2300 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
2301 <div style="margin-bottom: 12px;"><span class="dashicons dashicons-edit" style="font-size: 24px; width: 24px; height: 24px;"></span></div>
2302 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('Multi-Provider Text AI', 'botwriter'); ?></h3>
2303 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
2304 <?php echo esc_html__('Choose from OpenAI (GPT-4o), Anthropic (Claude), Google (Gemini), Mistral, Groq, or OpenRouter. Use your own API keys.', 'botwriter'); ?>
2305 </p>
2306 </div>
2307
2308 <!-- Image AI Card -->
2309 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
2310 <div style="margin-bottom: 12px;"><span class="dashicons dashicons-format-image" style="font-size: 24px; width: 24px; height: 24px;"></span></div>
2311 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('AI Image Generation', 'botwriter'); ?></h3>
2312 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
2313 <?php echo esc_html__('Generate featured images with DALL-E, Stable Diffusion, Flux, Recraft, and more via Replicate, Stability AI, or Fal.ai.', 'botwriter'); ?>
2314 </p>
2315 </div>
2316
2317 <!-- Content Sources Card -->
2318 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
2319 <div style="margin-bottom: 12px;"><span class="dashicons dashicons-rss" style="font-size: 24px; width: 24px; height: 24px;"></span></div>
2320 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('Multiple Content Sources', 'botwriter'); ?></h3>
2321 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
2322 <?php echo esc_html__('Import and rewrite content from any WordPress site, RSS feed, or news API. Prevent duplicates automatically.', 'botwriter'); ?>
2323 </p>
2324 </div>
2325
2326 <!-- Automation Card -->
2327 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
2328 <div style="margin-bottom: 12px;"><span class="dashicons dashicons-admin-generic" style="font-size: 24px; width: 24px; height: 24px;"></span></div>
2329 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('Full Automation', 'botwriter'); ?></h3>
2330 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
2331 <?php echo esc_html__('Schedule unlimited tasks, set publishing frequency, and let BotWriter work 24/7. Monitor everything from the Logs.', 'botwriter'); ?>
2332 </p>
2333 </div>
2334
2335 </div>
2336
2337 <!-- Getting Started Steps -->
2338 <div style="background: white; padding: 25px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
2339 <h2 style="margin: 0 0 20px 0; font-size: 18px; color: #333;">
2340 <span class="dashicons dashicons-controls-play" style="font-size: 20px; width: 20px; height: 20px; vertical-align: text-bottom;"></span> <?php echo esc_html__('Getting Started', 'botwriter'); ?>
2341 </h2>
2342
2343 <div style="display: flex; flex-direction: column; gap: 15px;">
2344
2345 <div style="display: flex; align-items: flex-start; gap: 15px;">
2346 <div style="background: <?php echo $has_api_key ? '#28a745' : '#667eea'; ?>; color: white; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0;">
2347 <?php echo $has_api_key ? '&#10003;' : '1'; ?>
2348 </div>
2349 <div>
2350 <strong style="color: #333;"><?php echo esc_html__('Configure your AI Provider', 'botwriter'); ?></strong>
2351 <p style="margin: 5px 0 0 0; color: #666; font-size: 13px;">
2352 <?php echo esc_html__('Add your API key from OpenAI, Anthropic (Claude), Google (Gemini), Mistral, Groq, or OpenRouter.', 'botwriter'); ?>
2353 <a href="<?php echo esc_url($settings_url); ?>"><?php echo esc_html__('Settings', 'botwriter'); ?> &rarr;</a>
2354 </p>
2355 </div>
2356 </div>
2357
2358 <div style="display: flex; align-items: flex-start; gap: 15px;">
2359 <div style="background: #667eea; color: white; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0;">2</div>
2360 <div>
2361 <strong style="color: #333;"><?php echo esc_html__('Create Your First Task', 'botwriter'); ?></strong>
2362 <p style="margin: 5px 0 0 0; color: #666; font-size: 13px;">
2363 <?php echo esc_html__('Define your content source, AI prompts, categories, and publishing schedule.', 'botwriter'); ?>
2364 <a href="<?php echo esc_url($addnew_url); ?>"><?php echo esc_html__('Add New', 'botwriter'); ?> &rarr;</a>
2365 </p>
2366 </div>
2367 </div>
2368
2369 <div style="display: flex; align-items: flex-start; gap: 15px;">
2370 <div style="background: #667eea; color: white; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0;">3</div>
2371 <div>
2372 <strong style="color: #333;"><?php echo esc_html__('Activate and Monitor', 'botwriter'); ?></strong>
2373 <p style="margin: 5px 0 0 0; color: #666; font-size: 13px;">
2374 <?php echo esc_html__('Enable your tasks and watch BotWriter generate posts automatically. Check the Logs for status updates.', 'botwriter'); ?>
2375 <a href="<?php echo esc_url($logs_url); ?>"><?php echo esc_html__('Logs', 'botwriter'); ?> &rarr;</a>
2376 </p>
2377 </div>
2378 </div>
2379
2380 </div>
2381 </div>
2382
2383 <!-- Quick Links -->
2384 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px;">
2385 <a href="<?php echo esc_url($settings_url); ?>" class="botwriter-quick-link" style="background: white; padding: 15px; border-radius: 8px; text-align: center; text-decoration: none; color: #333; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s, box-shadow 0.2s;">
2386 <div style="margin-bottom: 6px;"><span class="dashicons dashicons-admin-generic" style="font-size: 20px; width: 20px; height: 20px;"></span></div>
2387 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Settings', 'botwriter'); ?></div>
2388 </a>
2389 <a href="<?php echo esc_url($addnew_url); ?>" class="botwriter-quick-link" style="background: white; padding: 15px; border-radius: 8px; text-align: center; text-decoration: none; color: #333; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s, box-shadow 0.2s;">
2390 <div style="margin-bottom: 6px;"><span class="dashicons dashicons-plus-alt2" style="font-size: 20px; width: 20px; height: 20px;"></span></div>
2391 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Add New', 'botwriter'); ?></div>
2392 </a>
2393 <a href="<?php echo esc_url($tasks_url); ?>" class="botwriter-quick-link" style="background: white; padding: 15px; border-radius: 8px; text-align: center; text-decoration: none; color: #333; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s, box-shadow 0.2s;">
2394 <div style="margin-bottom: 6px;"><span class="dashicons dashicons-list-view" style="font-size: 20px; width: 20px; height: 20px;"></span></div>
2395 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Tasks', 'botwriter'); ?></div>
2396 </a>
2397 <a href="<?php echo esc_url($logs_url); ?>" class="botwriter-quick-link" style="background: white; padding: 15px; border-radius: 8px; text-align: center; text-decoration: none; color: #333; box-shadow: 0 2px 8px rgba(0,0,0,0.08); transition: transform 0.2s, box-shadow 0.2s;">
2398 <div style="margin-bottom: 6px;"><span class="dashicons dashicons-chart-bar" style="font-size: 20px; width: 20px; height: 20px;"></span></div>
2399 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Logs', 'botwriter'); ?></div>
2400 </a>
2401 <a href="https://wpbotwriter.com/faq.html" target="_blank" class="botwriter-quick-link botwriter-quick-link-highlight" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 8px; text-align: center; text-decoration: none; color: white; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); transition: transform 0.2s, box-shadow 0.2s;">
2402 <div style="margin-bottom: 6px;"><span class="dashicons dashicons-editor-help" style="font-size: 20px; width: 20px; height: 20px;"></span></div>
2403 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Help & FAQ', 'botwriter'); ?></div>
2404 </a>
2405 </div>
2406
2407 <!-- Footer -->
2408 <div style="text-align: center; margin-top: 30px; padding: 15px; color: #888; font-size: 12px;">
2409 <?php echo esc_html__('BotWriter', 'botwriter'); ?> v<?php echo esc_html(BOTWRITER_VERSION); ?> &mdash; 100% Free
2410 <br>
2411 <a href="https://www.wpbotwriter.com" target="_blank" style="color: #667eea; text-decoration: none;"><?php echo esc_html__('Website', 'botwriter'); ?></a>
2412 &nbsp;&bull;&nbsp;
2413 <a href="https://wpbotwriter.com/faq.html" target="_blank" style="color: #667eea; text-decoration: none;">FAQ</a>
2414 &nbsp;&bull;&nbsp;
2415 <a href="https://wordpress.org/support/plugin/botwriter/" target="_blank" style="color: #667eea; text-decoration: none;"><?php echo esc_html__('Support', 'botwriter'); ?></a>
2416 </div>
2417
2418 </div>
2419 </div>
2420
2421 <?php
2422 }
2423
2424
2425
2426 // Hook that runs on plugin activation
2427 register_activation_hook(__FILE__, 'botwriter_plugin_activate');
2428 function botwriter_plugin_activate() {
2429 // Store first install date if missing
2430 if (get_option('botwriter_install_date') === false) {
2431 update_option('botwriter_install_date', current_time('timestamp'));
2432 }
2433 botwriter_activate_apikey_and_defaults();
2434 botwriter_create_table();
2435 // Create the first supertask if it doesn't exist
2436 /*
2437 if (!botwriter_super1_check_task_exist()) {
2438 botwriter_super1_create_first_task();
2439 }
2440 */
2441 }
2442
2443
2444 function botwriter_activate_apikey_and_defaults() {
2445 if (get_option('botwriter_paused_tasks') === false) {
2446 update_option('botwriter_paused_tasks', "2");
2447 }
2448
2449 if (get_option('botwriter_email') === false) {
2450 update_option('botwriter_email', get_option('admin_email'));
2451 }
2452
2453 if (get_option('botwriter_cron_active') === false) {
2454 update_option('botwriter_cron_active', '1');
2455 }
2456
2457 if (get_option('botwriter_image_provider') === false) {
2458 update_option('botwriter_image_provider', 'stockphoto');
2459 }
2460
2461 if (get_option('botwriter_stockphoto_preferred') === false) {
2462 update_option('botwriter_stockphoto_preferred', 'random');
2463 }
2464
2465 if (get_option('botwriter_stockphoto_selection') === false) {
2466 update_option('botwriter_stockphoto_selection', 'random_top10');
2467 }
2468
2469 if (get_option('botwriter_stockphoto_attribution') === false) {
2470 update_option('botwriter_stockphoto_attribution', 'caption');
2471 }
2472
2473 if (get_option('botwriter_ai_image_size') === false) {
2474 update_option('botwriter_ai_image_size', 'square');
2475 }
2476
2477 if (get_option('botwriter_sslverify') !== 'yes') {
2478 update_option('botwriter_sslverify', 'yes');
2479 }
2480
2481 if (get_option('botwriter_openai_model') === false) {
2482 update_option('botwriter_openai_model', 'gpt-5.4-mini');
2483 }
2484 if (get_option('botwriter_ai_image_quality') === false) {
2485 update_option('botwriter_ai_image_quality', 'medium');
2486 }
2487
2488 if (get_option('botwriter_seo_featured_image_alt_enabled') === false) {
2489 update_option('botwriter_seo_featured_image_alt_enabled', '1');
2490 }
2491
2492 if (get_option('botwriter_seo_publish_focus_keyword_enabled') === false) {
2493 update_option('botwriter_seo_publish_focus_keyword_enabled', '0');
2494 }
2495
2496 if (get_option('botwriter_seo_publish_faq_enabled') === false) {
2497 update_option('botwriter_seo_publish_faq_enabled', '0');
2498 }
2499
2500 if (get_option('botwriter_seo_publish_faq_mode') === false) {
2501 update_option('botwriter_seo_publish_faq_mode', 'visible_schema');
2502 }
2503
2504 if (get_option('botwriter_seo_social_meta_enabled') === false) {
2505 update_option('botwriter_seo_social_meta_enabled', '0');
2506 }
2507 }
2508
2509
2510 // Compatibility check for different WordPress versions
2511 add_action('plugins_loaded', 'botwriter_compatibility_check');
2512
2513 function botwriter_compatibility_check() {
2514 global $wp_version;
2515
2516 if (version_compare($wp_version, '4.0', '<')) {
2517 deactivate_plugins(plugin_basename(__FILE__));
2518
2519 wp_die(esc_html__('This plugin requires WordPress 4.0 or higher', 'botwriter'));
2520 }
2521 }
2522
2523
2524 // Ensure DB schema migrations run even for sites that didn't re-activate the plugin
2525 add_action('plugins_loaded', 'botwriter_maybe_add_task_type_col', 20);
2526 function botwriter_maybe_add_task_type_col() {
2527 global $wpdb;
2528 $tasks_table_name = $wpdb->prefix . 'botwriter_tasks';
2529 // Bail if table is missing
2530 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tasks_table_name));
2531 if ($table_exists !== $tasks_table_name) {
2532 return;
2533 }
2534 // Add task_type if missing
2535 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'task_type'));
2536 if (!$col) {
2537 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `task_type` VARCHAR(50) NULL AFTER `website_type`");
2538 }
2539 }
2540
2541
2542
2543 // funciones extra
2544 if (!class_exists('WP_List_Table')) {
2545 require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
2546 }
2547
2548
2549 function botwriter_create_table() {
2550 global $wpdb;
2551 try {
2552
2553 $tasks_table_name = $wpdb->prefix . 'botwriter_tasks';
2554 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tasks_table_name)) !== $tasks_table_name) {
2555 $charset_collate = $wpdb->get_charset_collate();
2556
2557 $tasks_sql = "CREATE TABLE $tasks_table_name (
2558 `id` int(11) NOT NULL AUTO_INCREMENT,
2559 `post_status` VARCHAR(20) NOT NULL,
2560 `task_name` VARCHAR(255) NOT NULL,
2561 `writer` VARCHAR(255) NOT NULL,
2562 `narration` VARCHAR(255),
2563 `custom_style` VARCHAR(255),
2564 `post_language` VARCHAR(255) NOT NULL,
2565 `post_length` VARCHAR(255) NOT NULL,
2566 `custom_post_length` VARCHAR(255) NOT NULL,
2567 `days` VARCHAR(255) NOT NULL,
2568 `times_per_day` INT NOT NULL,
2569 `execution_count` INT DEFAULT 0,
2570 `last_execution_date` DATE DEFAULT NULL,
2571 `last_execution_time` TIMESTAMP DEFAULT 0,
2572 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
2573 `status` int DEFAULT 1,
2574 `website_name` VARCHAR(255),
2575 `website_type` VARCHAR(255),
2576 `task_type` VARCHAR(50) DEFAULT NULL,
2577 `domain_name` VARCHAR(255) NOT NULL,
2578 `post_type` VARCHAR(50) DEFAULT 'post',
2579 `category_id` VARCHAR(255),
2580 `taxonomy_data` TEXT,
2581 `website_category_id` VARCHAR(255),
2582 `website_category_name` VARCHAR(255),
2583 `aigenerated_title` TEXT NOT NULL,
2584 `aigenerated_content` TEXT NOT NULL,
2585 `aigenerated_tags` TEXT NOT NULL,
2586 `aigenerated_image` TEXT NOT NULL,
2587 `post_count` VARCHAR(255),
2588 `post_order` VARCHAR(255),
2589 `title_prompt` TEXT NOT NULL,
2590 `content_prompt` TEXT NOT NULL,
2591 `tags_prompt` TEXT NOT NULL,
2592 `image_prompt` TEXT NOT NULL,
2593 `image_generating_status` VARCHAR(255),
2594 `author_selection` VARCHAR(255),
2595 `news_time_published` VARCHAR(255),
2596 `news_language` VARCHAR(255),
2597 `news_country` VARCHAR(255),
2598 `news_keyword` VARCHAR(255),
2599 `news_source` VARCHAR(255),
2600 `rss_source` VARCHAR(255),
2601 `ai_keywords` TEXT NOT NULL,
2602 `disable_ai_images` TINYINT(1) DEFAULT 0,
2603 `template_id` INT(11) DEFAULT NULL,
2604 PRIMARY KEY (`id`)
2605 ) $charset_collate;";
2606
2607 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
2608 dbDelta($tasks_sql);
2609 } else {
2610 // Ensure new column task_type exists for legacy installs
2611 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'task_type'));
2612 if (!$col) {
2613 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `task_type` VARCHAR(50) NULL AFTER `website_type`");
2614 }
2615
2616 // Ensure new column disable_ai_images exists for legacy installs
2617 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'disable_ai_images'));
2618 if (!$col) {
2619 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `disable_ai_images` TINYINT(1) DEFAULT 0");
2620 }
2621
2622 // Ensure new column template_id exists for legacy installs
2623 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'template_id'));
2624 if (!$col) {
2625 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `template_id` INT(11) DEFAULT NULL");
2626 }
2627
2628 // Ensure new column post_type exists for legacy installs
2629 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'post_type'));
2630 if (!$col) {
2631 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `post_type` VARCHAR(50) DEFAULT 'post' AFTER `domain_name`");
2632 }
2633
2634 // Ensure new column taxonomy_data exists for legacy installs
2635 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'taxonomy_data'));
2636 if (!$col) {
2637 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `taxonomy_data` TEXT AFTER `category_id`");
2638 }
2639 }
2640
2641 // Table botwriter_logs
2642 $logs_table_name = $wpdb->prefix . 'botwriter_logs';
2643 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $logs_table_name)) !== $logs_table_name) {
2644 $charset_collate = $wpdb->get_charset_collate();
2645
2646 $logs_sql = "CREATE TABLE $logs_table_name (
2647 `id` int(11) NOT NULL AUTO_INCREMENT,
2648 `id_task` int(11) NOT NULL,
2649 `id_task_server` int(11) NOT NULL,
2650 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
2651 `last_execution_time` TIMESTAMP DEFAULT 0,
2652 `intentosfase1` int(11) NOT NULL DEFAULT 0,
2653 `intentosfase2` int(11) NOT NULL DEFAULT 0,
2654 `task_status` VARCHAR(255),
2655 `task_type` VARCHAR(50) DEFAULT NULL,
2656 `error` TEXT,
2657 `link_post_original` TEXT,
2658 `id_post_published` int(11) default 0,
2659 `post_status` VARCHAR(20) NOT NULL,
2660 `task_name` VARCHAR(255) NOT NULL,
2661 `writer` VARCHAR(255) NOT NULL,
2662 `narration` VARCHAR(255),
2663 `custom_style` VARCHAR(255),
2664 `post_language` VARCHAR(255) NOT NULL,
2665 `post_length` VARCHAR(255) NOT NULL,
2666 `custom_post_length` VARCHAR(255) NOT NULL,
2667 `website_name` VARCHAR(255),
2668 `website_type` VARCHAR(255),
2669 `domain_name` VARCHAR(255) NOT NULL,
2670 `post_type` VARCHAR(50) DEFAULT 'post',
2671 `category_id` VARCHAR(255),
2672 `taxonomy_data` TEXT,
2673 `website_category_id` VARCHAR(255),
2674 `aigenerated_title` TEXT NOT NULL,
2675 `aigenerated_content` TEXT NOT NULL,
2676 `aigenerated_tags` TEXT NOT NULL,
2677 `aigenerated_image` TEXT NOT NULL,
2678 `post_count` VARCHAR(255),
2679 `post_order` VARCHAR(255),
2680 `title_prompt` TEXT NOT NULL,
2681 `content_prompt` TEXT NOT NULL,
2682 `tags_prompt` TEXT NOT NULL,
2683 `image_prompt` TEXT NOT NULL,
2684 `image_generating_status` VARCHAR(255),
2685 `author_selection` VARCHAR(255),
2686 `news_time_published` VARCHAR(255),
2687 `news_language` VARCHAR(255),
2688 `news_country` VARCHAR(255),
2689 `news_keyword` VARCHAR(255),
2690 `news_source` VARCHAR(255),
2691 `rss_source` VARCHAR(255),
2692 `ai_keywords` TEXT NOT NULL,
2693 `disable_ai_images` TINYINT(1) DEFAULT 0,
2694 `template_id` INT(11) DEFAULT NULL,
2695 PRIMARY KEY (`id`)
2696 ) $charset_collate;";
2697
2698 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
2699 dbDelta($logs_sql);
2700 } else {
2701 // Ensure new column disable_ai_images exists in logs table for legacy installs
2702 $logs_table_name = $wpdb->prefix . 'botwriter_logs';
2703 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'disable_ai_images'));
2704 if (!$col) {
2705 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `disable_ai_images` TINYINT(1) DEFAULT 0");
2706 }
2707
2708 // Ensure new column template_id exists in logs table for legacy installs
2709 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'template_id'));
2710 if (!$col) {
2711 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `template_id` INT(11) DEFAULT NULL");
2712 }
2713
2714 // Ensure new column task_type exists in logs table for legacy installs (for writenow exclusion)
2715 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'task_type'));
2716 if (!$col) {
2717 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `task_type` VARCHAR(50) DEFAULT NULL AFTER `task_status`");
2718 }
2719
2720 // Ensure new column post_type exists in logs table for legacy installs
2721 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'post_type'));
2722 if (!$col) {
2723 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `post_type` VARCHAR(50) DEFAULT 'post' AFTER `domain_name`");
2724 }
2725
2726 // Ensure new column taxonomy_data exists in logs table for legacy installs
2727 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'taxonomy_data'));
2728 if (!$col) {
2729 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `taxonomy_data` TEXT AFTER `category_id`");
2730 }
2731 }
2732
2733
2734
2735 $tasks_table_name = $wpdb->prefix . 'botwriter_super';
2736 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tasks_table_name)) !== $tasks_table_name) {
2737 $charset_collate = $wpdb->get_charset_collate();
2738
2739 $tasks_sql = "CREATE TABLE $tasks_table_name (
2740 `id` int(11) NOT NULL AUTO_INCREMENT,
2741 `id_task` int(11) NOT NULL,
2742 `id_log` int(11) NOT NULL,
2743 `title` VARCHAR(255) NOT NULL,
2744 `content` TEXT NOT NULL,
2745 `category_id` VARCHAR(255),
2746 `category_name` VARCHAR(255),
2747 `task_status` VARCHAR(255),
2748 PRIMARY KEY (`id`)
2749 ) $charset_collate;";
2750
2751 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
2752 dbDelta($tasks_sql);
2753 }
2754
2755 // Table botwriter_templates for prompt templates
2756 $templates_table_name = $wpdb->prefix . 'botwriter_templates';
2757 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $templates_table_name)) !== $templates_table_name) {
2758 $charset_collate = $wpdb->get_charset_collate();
2759
2760 $templates_sql = "CREATE TABLE $templates_table_name (
2761 `id` int(11) NOT NULL AUTO_INCREMENT,
2762 `name` VARCHAR(255) NOT NULL,
2763 `content` LONGTEXT NOT NULL,
2764 `is_default` TINYINT(1) DEFAULT 0,
2765 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
2766 PRIMARY KEY (`id`)
2767 ) $charset_collate;";
2768
2769 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
2770 dbDelta($templates_sql);
2771
2772 // Insert all default templates
2773 botwriter_insert_all_default_templates();
2774 }
2775
2776 } catch (Exception $e) {
2777
2778 //error_log("Error creating botwriter tables: " . $e->getMessage());
2779 }
2780 }
2781
2782 /**
2783 * Insert the default prompt template (legacy function - now uses default-templates.php)
2784 * @deprecated Use botwriter_insert_all_default_templates() instead
2785 */
2786 function botwriter_insert_default_template() {
2787 // Now handled by botwriter_insert_all_default_templates() in default-templates.php
2788 botwriter_insert_all_default_templates();
2789 }
2790
2791 /**
2792 * Get the default template content with all placeholders
2793 * Uses the content from default-templates.php if available
2794 */
2795 function botwriter_get_default_template_content() {
2796 // Try to get from the default templates array
2797 if (function_exists('botwriter_get_default_template_by_name')) {
2798 $default = botwriter_get_default_template_by_name('Default Template');
2799 if ($default && !empty($default['content'])) {
2800 return $default['content'];
2801 }
2802 }
2803
2804 // Fallback hardcoded template
2805 $template = 'Write an article for a blog, follow these instructions:
2806
2807 -The article must be HTML, with proper opening and closing H2-H4 tags for headings, and <p> for paragraphs.
2808 -The length should be approximately {{post_length}} words.
2809 -The article language must be: {{post_language}}.
2810 -Narrative style: {{writer_style}}
2811 -The topic must be related to some of the following keywords: {{prompt_or_keywords}}
2812
2813 -IMPORTANT: Do not title or label the last paragraph with Conclusion, Final Thoughts, Summary, or any similar term. The last paragraph should integrate naturally into the article, without any heading or subheading. It should subtly close the article by reinforcing the main message or idea, offering a final reflection, or leaving the reader with a powerful takeaway, but without explicitly indicating it is the end.';
2814
2815 return $template;
2816 }
2817
2818 /**
2819 * Get template by ID or default template
2820 */
2821 function botwriter_get_template($template_id = null) {
2822 global $wpdb;
2823 $table_name = $wpdb->prefix . 'botwriter_templates';
2824
2825 if ($template_id) {
2826 $template = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $template_id), ARRAY_A);
2827 } else {
2828 $template = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE is_default = %d", 1), ARRAY_A);
2829 }
2830
2831 if (!$template) {
2832 // Return hardcoded default if no template in DB
2833 return [
2834 'id' => 0,
2835 'name' => 'Default Template',
2836 'content' => botwriter_get_default_template_content(),
2837 'is_default' => 1
2838 ];
2839 }
2840
2841 return $template;
2842 }
2843
2844 /**
2845 * Get all templates
2846 */
2847 function botwriter_get_all_templates() {
2848 global $wpdb;
2849 $table_name = $wpdb->prefix . 'botwriter_templates';
2850
2851 if (function_exists('botwriter_ensure_default_templates_exist')) {
2852 botwriter_ensure_default_templates_exist();
2853 }
2854
2855 return $wpdb->get_results("SELECT * FROM $table_name ORDER BY name DESC", ARRAY_A);
2856 }
2857
2858 /**
2859 * Save a template (insert or update)
2860 */
2861 function botwriter_save_template($data) {
2862 global $wpdb;
2863 $table_name = $wpdb->prefix . 'botwriter_templates';
2864
2865 if (!empty($data['id'])) {
2866 // Update
2867 return $wpdb->update($table_name, [
2868 'name' => sanitize_text_field($data['name']),
2869 'content' => wp_kses_post($data['content'])
2870 ], ['id' => intval($data['id'])]);
2871 } else {
2872 // Insert
2873 return $wpdb->insert($table_name, [
2874 'name' => sanitize_text_field($data['name']),
2875 'content' => wp_kses_post($data['content']),
2876 'is_default' => 0
2877 ]);
2878 }
2879 }
2880
2881 /**
2882 * Set a template as default (unset other defaults)
2883 */
2884 function botwriter_set_default_template($template_id) {
2885 global $wpdb;
2886 $table_name = $wpdb->prefix . 'botwriter_templates';
2887
2888 // Check if template exists
2889 $template = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $template_id));
2890 if (!$template) {
2891 return false;
2892 }
2893
2894 // Remove default from all templates
2895 $wpdb->update($table_name, ['is_default' => 0], ['is_default' => 1]);
2896
2897 // Set new default
2898 return $wpdb->update($table_name, ['is_default' => 1], ['id' => intval($template_id)]);
2899 }
2900
2901 /**
2902 * Delete a template (cannot delete default)
2903 */
2904 function botwriter_delete_template($template_id) {
2905 global $wpdb;
2906 $table_name = $wpdb->prefix . 'botwriter_templates';
2907
2908 // Prevent deleting default template
2909 $is_default = $wpdb->get_var($wpdb->prepare("SELECT is_default FROM $table_name WHERE id = %d", $template_id));
2910 if ($is_default == 1) {
2911 return false;
2912 }
2913
2914 return $wpdb->delete($table_name, ['id' => intval($template_id)]);
2915 }
2916
2917 /**
2918 * Build prompt from template by replacing placeholders with actual values
2919 * Uses Mustache-like syntax: {{variable}} for simple values, {{#section}}...{{/section}} for conditionals
2920 */
2921 function botwriter_build_prompt_from_template($template_content, $data) {
2922 global $botwriter_languages;
2923
2924 // Map writer styles
2925 $writer_styles = [
2926 'ai_cerebro' => '',
2927 'orion' => '',
2928 'cloe' => 'ironic critic, sarcastic and witty',
2929 'lucida' => 'analytical critic, precise and direct',
2930 'max' => 'Passionate and descriptive',
2931 'gael' => 'Reflective, introspective and poetic',
2932 ];
2933
2934 // Prepare data for template — NOTE: this is the legacy function,
2935 // main flow uses botwriter_build_client_prompt() below.
2936 $writer = strtolower($data['writer'] ?? '');
2937 $writer_style = '';
2938
2939 if ($writer === 'custom') {
2940 $narration = strtolower($data['narration'] ?? '');
2941 if ($narration === 'custom') {
2942 $writer_style = $data['custom_style'] ?? '';
2943 } else {
2944 $writer_style = $narration;
2945 }
2946 } elseif (isset($writer_styles[$writer])) {
2947 $writer_style = $writer_styles[$writer];
2948 }
2949
2950 // Get language name from code
2951 $post_language_code = $data['post_language'] ?? 'en';
2952 $post_language = $botwriter_languages[$post_language_code] ?? 'English';
2953
2954 // Post length
2955 $post_length = $data['post_length'] ?? '800';
2956 if (!is_numeric($post_length)) {
2957 $post_length = 800;
2958 }
2959 $post_length = min(intval($post_length), 4000);
2960
2961 // Build replacements array
2962 // Note: source_content, existing_titles, title_prompt, content_prompt are handled server-side
2963 $replacements = [
2964 'post_length' => $post_length,
2965 'post_language' => $post_language,
2966 'writer_style' => $writer_style,
2967 'prompt_or_keywords' => $data['ai_keywords'] ?? '',
2968 ];
2969
2970 $prompt = $template_content;
2971
2972 // Replace variables: {{variable}}
2973 foreach ($replacements as $key => $value) {
2974 $prompt = str_replace('{{' . $key . '}}', $value, $prompt);
2975 }
2976
2977 // Clean up short instruction lines left empty after variable replacement
2978 // e.g. "-Narrative style: " or "-Topic: " when the value is empty
2979 // Never remove lines containing ENDARTICLE or other source markers
2980 $lines = explode("\n", $prompt);
2981 $cleaned_lines = [];
2982 foreach ($lines as $line) {
2983 $trimmed = trim($line);
2984 if (preg_match('/^-[^:]+:\s*$/', $trimmed) && strpos($trimmed, 'ENDARTICLE') === false && strlen($trimmed) < 40) {
2985 continue;
2986 }
2987 $cleaned_lines[] = $line;
2988 }
2989 $prompt = implode("\n", $cleaned_lines);
2990
2991 // Clean up multiple blank lines
2992 $prompt = preg_replace('/\n{3,}/', "\n\n", $prompt);
2993 $prompt = trim($prompt);
2994
2995 return $prompt;
2996 }
2997
2998 /**
2999 * Build the complete prompt on the client side using the template system
3000 * For types that require external content (wordpress, rss, news),
3001 * the server will add the source content to the prompt
3002 */
3003 function botwriter_build_client_prompt($data) {
3004 global $botwriter_languages;
3005
3006 // Get the template - use task-specific template if set, otherwise default
3007 $template_id = isset($data['template_id']) && !empty($data['template_id']) ? intval($data['template_id']) : null;
3008 $template = botwriter_get_template($template_id);
3009 $template_content = $template['content'];
3010
3011 // Map writer styles
3012 $writer_styles = [
3013 'ai_cerebro' => '',
3014 'orion' => '',
3015 'cloe' => 'ironic critic, sarcastic and witty',
3016 'lucida' => 'analytical critic, precise and direct',
3017 'max' => 'Passionate and descriptive',
3018 'gael' => 'Reflective, introspective and poetic',
3019 ];
3020
3021 // Prepare writer style
3022 $writer = strtolower($data['writer'] ?? '');
3023 $writer_style = '';
3024
3025 if ($writer === 'custom') {
3026 $narration = strtolower($data['narration'] ?? '');
3027 if ($narration === 'custom') {
3028 $writer_style = $data['custom_style'] ?? '';
3029 } else {
3030 $writer_style = $narration;
3031 }
3032 } elseif (isset($writer_styles[$writer])) {
3033 $writer_style = $writer_styles[$writer];
3034 }
3035
3036 // Get language name from code
3037 $post_language_code = $data['post_language'] ?? 'en';
3038 $post_language = $botwriter_languages[$post_language_code] ?? 'English';
3039
3040 // Post length
3041 $post_length = $data['post_length'] ?? '800';
3042 if (!is_numeric($post_length)) {
3043 $post_length = 800;
3044 }
3045 $post_length = min(intval($post_length), 4000);
3046
3047 // Determine what content to include based on website_type
3048 $website_type = $data['website_type'] ?? '';
3049 $source_title = '';
3050 $source_content = '';
3051 $ai_keywords = '';
3052 $existing_titles = '';
3053 $title_prompt = '';
3054 $content_prompt = '';
3055
3056 switch ($website_type) {
3057 case 'ai':
3058 case '':
3059 // AI mode: use keywords and avoid existing titles
3060 $ai_keywords = $data['ai_keywords'] ?? '';
3061 $existing_titles = $data['titles'] ?? '';
3062 break;
3063
3064 case 'super2':
3065 // Super2: use title_prompt and content_prompt from outline
3066 $title_prompt = $data['title_prompt'] ?? '';
3067 $content_prompt = $data['content_prompt'] ?? '';
3068 break;
3069
3070 case 'rss':
3071 // RSS content is now pre-fetched on the client side
3072 // Data is populated by botwriter_send1_data_to_server before calling this function
3073 $source_title = $data['source_title'] ?? '';
3074 $source_content = $data['source_content'] ?? '';
3075 break;
3076
3077 case 'wordpress':
3078 // WordPress content is now pre-fetched on the client side
3079 // Data is populated by botwriter_send1_data_to_server before calling this function
3080 $source_title = $data['source_title'] ?? '';
3081 $source_content = $data['source_content'] ?? '';
3082 break;
3083
3084 case 'news':
3085 // News still requires server-side content fetching
3086 // We leave source_title and source_content empty, server will fill them
3087 break;
3088 }
3089
3090 // Build replacements array
3091 $replacements = [
3092 'post_length' => $post_length,
3093 'post_language' => $post_language,
3094 'writer_style' => $writer_style,
3095 'source_title' => $source_title,
3096 'source_content' => $source_content,
3097 'ai_keywords' => $ai_keywords,
3098 'prompt_or_keywords' => $ai_keywords, // Alias for templates
3099 'existing_titles' => $existing_titles,
3100 'title_prompt' => $title_prompt,
3101 'content_prompt' => $content_prompt,
3102 ];
3103
3104 $prompt = $template_content;
3105
3106 botwriter_log('PROMPT BUILD: before source embed', [
3107 'website_type' => $website_type,
3108 'source_title_empty' => empty($source_title),
3109 'source_title' => mb_substr($source_title, 0, 100),
3110 'source_content_len' => strlen($source_content),
3111 'template_len' => strlen($template_content),
3112 ]);
3113
3114 // For RSS/WordPress: embed source content directly (already pre-fetched on client)
3115 if (in_array($website_type, ['rss', 'wordpress']) && !empty($source_title)) {
3116 $prompt .= "\n\n-Based on this news article (I indicate the end with the word ENDARTICLE):\n\n" . $source_title . "\n" . $source_content . "\n\nENDARTICLE:\n";
3117 botwriter_log('PROMPT BUILD: ENDARTICLE block appended', [
3118 'prompt_len_after' => strlen($prompt),
3119 ]);
3120 } else {
3121 botwriter_log('PROMPT BUILD: ENDARTICLE block NOT appended', [
3122 'reason' => !in_array($website_type, ['rss', 'wordpress']) ? 'type not rss/wordpress' : 'source_title is empty',
3123 ]);
3124 }
3125 // For Super2: embed title and content instructions from the outline
3126 if ($website_type === 'super2') {
3127 // Rewrite instructions go BEFORE the ENDARTICLE block (rewriter / siterewriter tasks)
3128 $rewrite_prompt = $data['rewrite_prompt'] ?? '';
3129 if (!empty($rewrite_prompt)) {
3130 $prompt .= "\n-" . $rewrite_prompt;
3131 }
3132 if (!empty($title_prompt)) {
3133 $prompt .= "\n-The article title must be: " . $title_prompt;
3134 }
3135 if (!empty($content_prompt)) {
3136 $prompt .= "\n\n-Based on the following content (I indicate the end with the word ENDARTICLE):\n\n" . $content_prompt . "\n\nENDARTICLE\n";
3137 }
3138 botwriter_log('PROMPT BUILD: super2 title/content appended', [
3139 'title_prompt' => mb_substr($title_prompt, 0, 100),
3140 'content_prompt_len' => strlen($content_prompt),
3141 'has_rewrite_prompt' => !empty($rewrite_prompt),
3142 ]);
3143 }
3144 // For News: server still needs to fetch content
3145 if ($website_type === 'news') {
3146 $prompt .= "\n\n{{SERVER_SOURCE_CONTENT}}";
3147 }
3148
3149 // Replace variables: {{variable}}
3150 foreach ($replacements as $key => $value) {
3151 $prompt = str_replace('{{' . $key . '}}', $value, $prompt);
3152 }
3153
3154 $prompt_before_clean = $prompt;
3155 botwriter_log('PROMPT BUILD: BEFORE cleanup', [
3156 'prompt_len' => strlen($prompt),
3157 'contains_ENDARTICLE' => (strpos($prompt, 'ENDARTICLE') !== false),
3158 'contains_Based_on' => (strpos($prompt, 'Based on this') !== false),
3159 'first_300_chars' => mb_substr($prompt, 0, 300),
3160 'last_300_chars' => mb_substr($prompt, -300),
3161 ]);
3162
3163 // Clean up lines that have empty placeholders (lines ending with : or empty after replacement)
3164 $lines = explode("\n", $prompt);
3165 $cleaned_lines = [];
3166 $removed_lines = [];
3167 foreach ($lines as $line) {
3168 $trimmed = trim($line);
3169 // Skip short instruction lines left empty after variable replacement
3170 // e.g. "-Narrative style: " or "-Topic: " — never remove ENDARTICLE marker
3171 if (preg_match('/^-[^:]+:\s*$/', $trimmed) && strpos($trimmed, 'ENDARTICLE') === false && strlen($trimmed) < 40) {
3172 $removed_lines[] = $trimmed . ' (len=' . strlen($trimmed) . ')';
3173 continue;
3174 }
3175 $cleaned_lines[] = $line;
3176 }
3177 $prompt = implode("\n", $cleaned_lines);
3178
3179 if (!empty($removed_lines)) {
3180 botwriter_log('PROMPT BUILD: lines REMOVED by cleanup', [
3181 'count' => count($removed_lines),
3182 'lines' => $removed_lines,
3183 ]);
3184 }
3185
3186 // Clean up multiple blank lines
3187 $prompt = preg_replace('/\n{3,}/', "\n\n", $prompt);
3188 $prompt = trim($prompt);
3189
3190 botwriter_log('PROMPT BUILD: AFTER cleanup (final)', [
3191 'prompt_len' => strlen($prompt),
3192 'contains_ENDARTICLE' => (strpos($prompt, 'ENDARTICLE') !== false),
3193 'contains_Based_on' => (strpos($prompt, 'Based on this') !== false),
3194 'first_300_chars' => mb_substr($prompt, 0, 300),
3195 'last_300_chars' => mb_substr($prompt, -300),
3196 ]);
3197
3198 return $prompt;
3199 }
3200
3201
3202
3203 // Extending class
3204 class botwriter_tasks_Table extends WP_List_Table
3205 {
3206 // Define table columns
3207 function get_columns()
3208 {
3209 $columns = array(
3210 'cb' => '<input type="checkbox" />',
3211 'writer' => __('Writer', 'botwriter'),
3212 'task_name' => __('Task Name', 'botwriter'),
3213 'days' => __('Days', 'botwriter'),
3214 'times_per_day' => __('Times per Day', 'botwriter'),
3215 'type' => __('Type', 'botwriter'),
3216 'status' => __('Status', 'botwriter')
3217
3218 );
3219 return $columns;
3220 }
3221
3222
3223 // define $table_data property
3224 private $table_data;
3225
3226 // Bind table with columns, data and all
3227 function prepare_items()
3228 {
3229 //data
3230 if ( isset( $_POST['s'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'botwriter_nonce' ) ) {
3231 $search_query = sanitize_text_field(wp_unslash($_POST['s']));
3232 $this->table_data = $this->get_table_data($search_query);
3233 } else {
3234 $this->table_data = $this->get_table_data();
3235 }
3236
3237
3238 $columns = $this->get_columns();
3239 $hidden = ( is_array(get_user_meta( get_current_user_id(), 'managetoplevel_page_list_tablecolumnshidden', true)) ) ? get_user_meta( get_current_user_id(), 'managetoplevel_page_list_tablecolumnshidden', true) : array();
3240 $sortable = $this->get_sortable_columns();
3241 $primary = 'name';
3242 $this->_column_headers = array($columns, $hidden, $sortable, $primary);
3243 $this->process_bulk_action();
3244 $this->table_data = $this->get_table_data();
3245
3246 usort($this->table_data, array($this, 'usort_reorder'));
3247
3248 /* pagination */
3249 $per_page = $this->get_items_per_page('elements_per_page', 10);
3250 $current_page = $this->get_pagenum();
3251 $total_items = count($this->table_data);
3252
3253 $this->table_data = array_slice($this->table_data, (($current_page - 1) * $per_page), $per_page);
3254
3255 $this->set_pagination_args(array(
3256 'total_items' => $total_items, // total number of items
3257 'per_page' => $per_page, // items to show on a page
3258 'total_pages' => ceil( $total_items / $per_page ) // use ceil to round up
3259 ));
3260
3261 $this->items = $this->table_data;
3262 }
3263
3264
3265
3266 function column_task_name($item){
3267 $slug='botwriter_automatic_post_new';
3268 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page slug used to build edit links.
3269 $page = isset($_REQUEST['page']) ? sanitize_text_field(wp_unslash($_REQUEST['page'])) : '';
3270
3271 if ($item["website_type"] == 'super2') {
3272 $slug='botwriter_super_page';
3273 $url_edit= wp_nonce_url('?page=' . $slug . '&id=' . $item['id'], "botwriter_tasks_action");
3274 } else {
3275 $url_edit= wp_nonce_url('?page=' . $slug . '&id=' . $item['id'], "botwriter_tasks_action");
3276 }
3277
3278 $url_delete= wp_nonce_url('?page=' . $page . '&action=delete&id=' . $item['id'], "botwriter_tasks_action");
3279
3280 $actions = array(
3281 'edit' => sprintf('<a href="%s">%s</a>', $url_edit, __('Edit', 'botwriter')),
3282 'delete' => sprintf('<a href="%s">%s</a>', $url_delete, __('Delete', 'botwriter')),
3283 );
3284
3285 $id=$item['id'];
3286 return sprintf('%s %s',
3287 "<a class='row-title' href='?page=$slug&id=$id&_wpnonce=" . wp_create_nonce('botwriter_tasks_action') . "'>" . $item['task_name'] . "</a>",
3288 $this->row_actions($actions)
3289 );
3290 }
3291
3292 function column_writer($item){
3293 $dir_images_writers = plugin_dir_url(__FILE__) . 'assets/images/writers/';
3294 $writer=$item['writer'];
3295 $writer = strtolower($writer);
3296
3297 $slug='botwriter_automatic_post_new';
3298 $id=$item['id'];
3299 $link="<a class='row-title' href='?page=$slug&id=$id'>";
3300 $img= '<img src="' . esc_url($dir_images_writers . $writer . '.jpeg') . '" alt="' . esc_attr($writer) . '" class="writer-photo">';
3301 return $link . $img . '</a>';
3302
3303 }
3304
3305
3306
3307 function column_status($item){
3308
3309 $status = $item['status'];
3310 $status_opuesto = $status ? 0 : 1;
3311 $icono = $status ? 'dashicons-yes' : 'dashicons-dismiss';
3312 $texto_status = $status ? 'Desactivate' : 'Activate';
3313
3314 return sprintf(
3315 '<a href="#" class="icono-status dashicons %s" data-id="%d" data-status="%d" title="%s"></a>',
3316 $icono,
3317 $item['id'],
3318 $status_opuesto,
3319 $texto_status
3320 );
3321
3322
3323 }
3324
3325 /*
3326 function column_category_id($item) {
3327 $categories = get_categories();
3328 $category_name = '';
3329 foreach ($categories as $category) {
3330 $aux_cateforias=explode(',',$item['category_id']);
3331 if (in_array($category->term_id, $aux_cateforias)) {
3332 $category_name .= $category->name . ', ';
3333 }
3334
3335 }
3336 $category_name = rtrim($category_name, ', ');
3337 return $category_name;
3338 }
3339 */
3340
3341
3342
3343
3344
3345 // To show bulk action dropdown
3346 function get_bulk_actions()
3347 {
3348 $actions = array(
3349 'delete_all' => __('Delete', 'botwriter'),
3350
3351 );
3352 return $actions;
3353 }
3354
3355 function process_bulk_action()
3356 {
3357 // Verify user has permission
3358 if (!current_user_can('manage_options')) {
3359 return;
3360 }
3361
3362 // Verify nonce
3363 if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])), "botwriter_tasks_action")) {
3364 return;
3365 }
3366
3367 global $wpdb;
3368
3369 $table = esc_sql($wpdb->prefix . 'botwriter_tasks');
3370
3371 if ('delete_all' === $this->current_action() || ('delete' === $this->current_action() && isset($_REQUEST['id']))) {
3372 $request_id = isset($_REQUEST['id']) ? array_map('absint', (array) wp_unslash($_REQUEST['id'])) : array();
3373
3374 if (!empty($request_id)) {
3375 // Prepare the DELETE query with proper escaping
3376 $placeholders = implode(',', array_fill(0, count($request_id), '%d'));
3377 $query = $wpdb->prepare("DELETE FROM {$table} WHERE id IN({$placeholders})", $request_id);
3378 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is generated internally and placeholders are prepared from sanitized IDs.
3379 $wpdb->query($query);
3380 }
3381 }
3382 }
3383
3384
3385
3386 // Get table data
3387 private function get_table_data( $search = '' ) {
3388 global $wpdb;
3389
3390 $table = esc_sql($wpdb->prefix . 'botwriter_tasks');
3391
3392
3393 if ( ! empty( $search ) ) {
3394 $prepared_search = $wpdb->esc_like( $search );
3395 $prepared_search = '%' . $wpdb->esc_like( $search ) . '%';
3396
3397 $query = $wpdb->prepare(
3398 "SELECT * FROM {$table} WHERE name LIKE %s AND (task_type IS NULL OR task_type <> %s)",
3399 $prepared_search,
3400 'writenow'
3401 );
3402 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is generated internally and escaped for identifier use.
3403 return $wpdb->get_results($query, ARRAY_A);
3404 } else {
3405 $query = $wpdb->prepare(
3406 "SELECT * FROM {$table} WHERE (task_type IS NULL OR task_type <> %s)",
3407 'writenow'
3408 );
3409 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is generated internally and escaped for identifier use.
3410 return $wpdb->get_results($query, ARRAY_A);
3411 }
3412 }
3413
3414 function column_default($item, $column_name)
3415 {
3416
3417 switch ($column_name) {
3418 case 'id':
3419 case 'website_type':
3420 case 'website_name':
3421 case 'task_name':
3422 case 'category_id':
3423 case 'website_category_id':
3424 default:
3425 return $item[$column_name];
3426 }
3427 }
3428
3429 // Render the combined Type column: website_type + task_type
3430 function column_type($item) {
3431 $parts = array();
3432 if (!empty($item['website_type'])) {
3433 $parts[] = sanitize_text_field($item['website_type']);
3434 }
3435 if (!empty($item['task_type'])) {
3436 $parts[] = sanitize_text_field($item['task_type']);
3437 }
3438 $label = !empty($parts) ? implode(' / ', $parts) : __('', 'botwriter');
3439 return esc_html($label);
3440 }
3441
3442 function column_cb($item){
3443 return sprintf(
3444 '<input type="checkbox" name="id[]" value="%s" />',
3445 $item['id']
3446 );
3447 }
3448
3449 public function get_sortable_columns(){
3450 $sortable_columns = array(
3451 'task_name' => array('task_name', false),
3452 'days' => array('days', false),
3453 'id' => array('id', true)
3454 );
3455 return $sortable_columns;
3456 }
3457
3458 // Sorting function
3459 function usort_reorder($a, $b)
3460 {
3461 // If no sort, default to task_name
3462 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sort param for list table.
3463 $sanitized_orderby = isset($_GET['orderby']) ? sanitize_text_field(wp_unslash($_GET['orderby'])) : '';
3464
3465 $orderby = (!empty($sanitized_orderby)) ? $sanitized_orderby : 'task_name';
3466
3467 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sort param for list table.
3468 $order = isset($_GET['order']) ? sanitize_text_field(wp_unslash($_GET['order'])) : 'asc';
3469
3470 // filtrar order solo asd o desc
3471 $order = in_array($order, array('asc', 'desc')) ? $order : 'asc';
3472 // filter orderby only allowed columns
3473 $orderby = in_array($orderby, array('task_name', 'days', 'id')) ? $orderby : 'task_name';
3474
3475
3476
3477
3478 // Determine sort order
3479 $result = strcmp($a[$orderby], $b[$orderby]);
3480
3481 // Send final sort direction to usort
3482 return ($order === 'asc') ? $result : -$result;
3483 }
3484
3485 } // end class botwriter_tasks_Table
3486
3487
3488
3489
3490
3491
3492 function botwriter_validate_website($item,$is_manual = false)
3493 {
3494
3495 $messages = array();
3496
3497
3498 if (empty($item['task_name'])) $messages[] = __('Task Name is required', 'botwriter');
3499 if (empty($item['website_type'])) $messages[] = __('Website Type is required', 'botwriter');
3500
3501 // Category/taxonomy validation: require category_id for 'post' type, or taxonomy_data for other types
3502 $post_type = isset($item['post_type']) ? $item['post_type'] : 'post';
3503 if ($post_type === 'post') {
3504 if (empty($item['category_id']) && empty($item['taxonomy_data'])) {
3505 $messages[] = __('Category is required', 'botwriter');
3506 }
3507 }
3508 // For other post types, taxonomy selection is optional
3509
3510
3511 if($item['website_type'] == 'wordpress'){
3512 if (empty($item['domain_name'])) {
3513 $messages[] = __('Domain Name is required', 'botwriter');
3514 }
3515 if( !botwriter_isValidDomain(sanitize_text_field($item['domain_name'])) ){
3516 $messages[] = __('Domain name should be valid.', 'botwriter');
3517 }
3518 }
3519
3520 if ($item['website_type'] == 'rss') {
3521 if (empty($item['rss_source'])) {
3522 $messages[] = __('RSS Source is required', 'botwriter');
3523 }
3524 }
3525
3526 if ($item['website_type'] == 'news') {
3527 if (empty($item['news_keyword'])) {
3528 $messages[] = __('News keyword is required', 'botwriter');
3529 }
3530 }
3531
3532 if (empty($messages)) return true;
3533 return implode('<br />', $messages);
3534 }
3535
3536
3537 function botwriter_isValidDomain($domain) {
3538 // WordPress wp_http_validate_url
3539 $valid_url = wp_http_validate_url( $domain);
3540
3541 if (!is_wp_error($valid_url)) {
3542 return true;
3543 } else {
3544 return false;
3545 }
3546 }
3547
3548
3549 function botwriter_is_site_working($site_url, $site_type) {
3550 $response = false;
3551
3552 if ($site_type === 'wordpress') {
3553 // Check if the WordPress REST API is accessible
3554 $api_url = rtrim($site_url, '/') . '/wp-json/wp/v2/posts';
3555 $headers = @get_headers($api_url);
3556 if ($headers && strpos((string)$headers[0], '200') !== false) {
3557 $response = true;
3558 }
3559 } elseif ($site_type === 'rss') {
3560 // Check if the RSS feed is accessible
3561 $rss = @simplexml_load_file($site_url);
3562 if ($rss) {
3563 $response = true;
3564 }
3565 }
3566
3567 return $response;
3568 }
3569
3570
3571 //wp-cron:
3572
3573
3574 // Add a custom schedule for cron jobs
3575
3576 function botwriter_add_custom_cron_schedule($schedules) {
3577 if (!isset($schedules['every_30'])) {
3578 $schedules['every_30'] = array(
3579 'interval' => 30, // 30 seconds
3580 'display' => __('Every thirty seconds', 'botwriter')
3581 );
3582 }
3583 return $schedules;
3584 }
3585 add_filter('cron_schedules', 'botwriter_add_custom_cron_schedule');
3586
3587 // Ensure cron is scheduled on admin load (in case activation hook didn't run)
3588 function botwriter_ensure_cron_scheduled() {
3589 if (get_option('botwriter_cron_active') === '0') {
3590 return;
3591 }
3592
3593 if (!wp_next_scheduled('botwriter_scheduled_events_plugin_cron')) {
3594 $scheduled = wp_schedule_event(time() + 30, 'every_30', 'botwriter_scheduled_events_plugin_cron');
3595 botwriter_log('Cron scheduled (admin init)', [
3596 'scheduled' => $scheduled ? 'yes' : 'no',
3597 ]);
3598 }
3599 }
3600 add_action('admin_init', 'botwriter_ensure_cron_scheduled');
3601
3602 // Schedule the cron job during plugin activation
3603 function botwriter_scheduled_events_plugin_activate() {
3604 if (get_option('botwriter_cron_active')=="0") {
3605 return;
3606 }
3607 if (!wp_next_scheduled('botwriter_scheduled_events_plugin_cron')) {
3608 wp_schedule_event(time(), 'every_30', 'botwriter_scheduled_events_plugin_cron');
3609 }
3610 }
3611 register_activation_hook(__FILE__, 'botwriter_scheduled_events_plugin_activate');
3612
3613 // Register the cron task
3614 add_action('botwriter_scheduled_events_plugin_cron', 'botwriter_scheduled_events_execute_tasks');
3615
3616
3617
3618
3619 // Clear the cron job upon plugin deactivation
3620 function botwriter_scheduled_events_plugin_deactivate() {
3621 wp_clear_scheduled_hook('botwriter_scheduled_events_plugin_cron');
3622 }
3623 register_deactivation_hook(__FILE__, 'botwriter_scheduled_events_plugin_deactivate');
3624
3625
3626
3627
3628 function botwriter_scheduled_events_execute_tasks() {
3629 global $wpdb;
3630 $table_name_tasks = $wpdb->prefix . 'botwriter_tasks';
3631 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
3632 $table_name_super = $wpdb->prefix . 'botwriter_super';
3633
3634 // ── Prevent overlapping cron runs (race condition guard) ──
3635 // Use a transient lock so two cron ticks cannot run simultaneously.
3636 // Lock expires after 120 seconds as a safety net.
3637 $lock_key = 'botwriter_cron_lock';
3638 if (get_transient($lock_key)) {
3639 botwriter_log('CRON SKIPPED — another cron run is still in progress');
3640 return;
3641 }
3642 set_transient($lock_key, time(), 120);
3643
3644 // Check if cron is active
3645 $cron_active = get_option('botwriter_cron_active');
3646 botwriter_log('=== CRON START ===', [
3647 'cron_active_option' => $cron_active,
3648 'timestamp' => current_time('Y-m-d H:i:s'),
3649 ]);
3650
3651 if ($cron_active !== '1') {
3652 botwriter_log('CRON DISABLED - exiting', ['cron_active' => $cron_active]);
3653 delete_transient($lock_key);
3654 return;
3655 }
3656
3657 // STOPFORMANY: refuse to dispatch new tasks while the flag is active
3658 if (get_option('botwriter_stopformany', false)) {
3659 botwriter_log('CRON BLOCKED by STOPFORMANY — too many consecutive errors on server');
3660 delete_transient($lock_key);
3661 return;
3662 }
3663
3664 // Get the current day (English name) and date based on WordPress local time
3665 // Use DateTime with wp_timezone() to respect site timezone and keep English day name
3666 try {
3667 $dt = new DateTime('now', wp_timezone());
3668 $current_day_en = $dt->format('l');
3669 } catch (Exception $e) {
3670 // Fallback, still English but GMT-based
3671 $current_day_en = gmdate('l');
3672 }
3673 $current_date = current_time('Y-m-d');
3674
3675 botwriter_log('Cron dispatcher triggered', [
3676 'day' => $current_day_en,
3677 'date' => $current_date,
3678 'timezone' => wp_timezone_string(),
3679 ]);
3680
3681 // PHASE 2
3682 botwriter_execute_events_pass2();
3683
3684 //PHASE 1 Execute each event if it meets the conditions
3685 // Get tasks scheduled for today and status=1
3686 // Exclude one-off Write now tasks from cron to avoid duplicate logs
3687 $tasks = (array) $wpdb->get_results(
3688 $wpdb->prepare(
3689 "SELECT * FROM {$table_name_tasks} WHERE days LIKE %s AND status = %d AND (task_type IS NULL OR task_type <> %s)",
3690 '%' . $wpdb->esc_like($current_day_en) . '%',
3691 1,
3692 'writenow'
3693 ),
3694 ARRAY_A
3695 );
3696 botwriter_log('Tasks evaluated for cron tick', [
3697 'count' => count($tasks),
3698 'query_day' => $current_day_en,
3699 ]);
3700
3701 // Log all tasks found for debugging
3702 if (count($tasks) === 0) {
3703 // Check total active tasks to see if it's a day mismatch
3704 $all_active = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name_tasks} WHERE status = 1");
3705 botwriter_log('NO TASKS FOUND for today', [
3706 'current_day' => $current_day_en,
3707 'total_active_tasks' => $all_active,
3708 ]);
3709 }
3710
3711 foreach ($tasks as $task) {
3712 botwriter_log('Evaluating task', [
3713 'task_id' => $task['id'],
3714 'task_name' => $task['task_name'],
3715 'days' => $task['days'],
3716 'times_per_day' => $task['times_per_day'],
3717 'execution_count' => $task['execution_count'],
3718 'last_execution_date' => $task['last_execution_date'],
3719 'last_execution_time' => $task['last_execution_time'],
3720 'website_type' => $task['website_type'],
3721 ]);
3722
3723 // Skip Write now tasks defensively (in case of legacy rows)
3724 if (!empty($task['task_type']) && $task['task_type'] === 'writenow') {
3725 botwriter_log('Skipping writenow task', ['task_id' => $task['id']]);
3726 continue;
3727 }
3728
3729 // Reset execution count daily
3730 if ($task["last_execution_date"] !== $current_date) {
3731 $wpdb->update($table_name_tasks, ['execution_count' => 0, 'last_execution_date' => $current_date], ['id' => $task["id"]]);
3732 $task["execution_count"] = 0;
3733 botwriter_log('Reset execution count for new day', ['task_id' => $task['id']]);
3734 }
3735
3736 // Check if the task is a supertask and if it exists
3737 $super_exists = true;
3738 if ($task["website_type"] == 'super2') { //supertask
3739 $super = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name_super WHERE id_task = %d AND (task_status IS NULL OR task_status = '')", $task["id"]), ARRAY_A);
3740 if (!$super) {
3741 $super_exists = false;
3742 botwriter_log('Super2 task skipped, no pending outline', [
3743 'task_id' => $task['id'],
3744 ]);
3745 }
3746 }
3747
3748 // Check if the task can still be executed based on its daily limit
3749 if ($task["execution_count"] < $task["times_per_day"] && $super_exists) {
3750 $last_execution_time = $task["last_execution_time"];
3751 $now = current_time('timestamp');
3752 $diff = $now - strtotime($last_execution_time);
3753 $pause = get_option('botwriter_paused_tasks');
3754 $pause = is_numeric($pause) ? intval($pause) : 2;
3755
3756 botwriter_log('Checking pause time', [
3757 'task_id' => $task['id'],
3758 'last_execution_time' => $last_execution_time,
3759 'now' => current_time('Y-m-d H:i:s'),
3760 'diff_seconds' => $diff,
3761 'pause_minutes' => $pause,
3762 'required_seconds' => 60 * $pause,
3763 'can_execute' => ($diff > 60 * $pause) ? 'YES' : 'NO',
3764 ]);
3765
3766 if ($diff > 60 * $pause) { //
3767
3768 $event = $task;
3769 $event["task_status"] = "pending";
3770 $event["id_task"] = $task["id"];
3771 $event["intentosfase1"] = 0;
3772 $id_log = botwriter_logs_register($event); // create log in db
3773 $event["id"] = $id_log;
3774 if ($task["website_type"] == 'super2') { // supertask
3775 botwriter_log('Preparing super2 dispatch', [
3776 'task_id' => $task['id'],
3777 'log_id' => $id_log,
3778 ]);
3779 $prepared_event = botwriter_super_prepare_event($event);
3780 if ($prepared_event === false) {
3781 botwriter_log('Super2 preparation returned false', [
3782 'task_id' => $task['id'],
3783 'log_id' => $id_log,
3784 ]);
3785 continue;
3786 }
3787 $event = $prepared_event;
3788 $id_log = botwriter_logs_register($event, $id_log); // actualizamos log con los datos de super2
3789 }
3790 botwriter_log('Queueing phase 1 send', [
3791 'task_id' => $task['id'],
3792 'log_id' => $id_log,
3793 'website_type' => $task['website_type'],
3794 ]);
3795 // Update execution count BEFORE the HTTP call to prevent
3796 // overlapping cron ticks from creating duplicate logs.
3797 $current_time = current_time('Y-m-d H:i:s');
3798 $wpdb->update($table_name_tasks, ['execution_count' => $task["execution_count"] + 1, 'last_execution_time' => $current_time], ['id' => $task["id"]]);
3799 botwriter_send1_data_to_server((array) $event);
3800 } else {
3801 botwriter_log('Task paused - waiting for pause interval', [
3802 'task_id' => $task['id'],
3803 'diff_seconds' => $diff,
3804 'required_seconds' => 60 * $pause,
3805 'remaining_seconds' => (60 * $pause) - $diff,
3806 ]);
3807 }
3808 } else {
3809 if ($super_exists) {
3810 botwriter_log('Task skipped due to daily limit reached', [
3811 'task_id' => $task['id'],
3812 'execution_count' => $task['execution_count'],
3813 'times_per_day' => $task['times_per_day'],
3814 ]);
3815 }
3816 }
3817 } // end tasks
3818
3819 botwriter_log('=== CRON END ===', ['timestamp' => current_time('Y-m-d H:i:s')]);
3820
3821 // ── Release cron lock ──
3822 delete_transient('botwriter_cron_lock');
3823 }
3824
3825 function botwriter_execute_events_pass2(){
3826
3827 // check if the task is still in queue or finished
3828 global $wpdb;
3829 $table_name_tasks = $wpdb->prefix . 'botwriter_tasks';
3830 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
3831 // INQUEUE
3832 $events2 = (array) $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table_name_logs} WHERE task_status=%s", 'inqueue'));
3833 botwriter_log('Phase 2 queue check', ['inqueue_count' => count($events2)]);
3834 foreach ($events2 as $event) {
3835 $event = (array) $event;
3836
3837 // ── Atomically mark as 'polling' to prevent overlapping cron ticks ──
3838 // Only update if the status is still 'inqueue'; if another cron tick
3839 // already changed it, affected_rows will be 0 and we skip this log.
3840 $affected = $wpdb->query($wpdb->prepare(
3841 "UPDATE {$table_name_logs} SET task_status = 'polling' WHERE id = %d AND task_status = 'inqueue'",
3842 $event['id']
3843 ));
3844 if ($affected === 0) {
3845 botwriter_log('Phase 2 skipped — already being polled', ['log_id' => $event['id']]);
3846 continue;
3847 }
3848
3849 // Execute the event (send2 will set final status: completed/error/inqueue)
3850 $result = botwriter_send2_data_to_server( (array) $event);
3851
3852 // If send2 did NOT update the log status (returned false without changing it),
3853 // restore to 'inqueue' so the next tick can retry.
3854 if ($result === false) {
3855 $current_status = $wpdb->get_var($wpdb->prepare(
3856 "SELECT task_status FROM {$table_name_logs} WHERE id = %d",
3857 $event['id']
3858 ));
3859 if ($current_status === 'polling') {
3860 $wpdb->update($table_name_logs, ['task_status' => 'inqueue'], ['id' => $event['id']]);
3861 }
3862 }
3863 } // end INQUEUE
3864
3865
3866 //IN ERROR, depending on the attempt, it is resent later or marked as finished
3867 // Exclude 'writenow' tasks from automatic retries - they should only be retried manually
3868 $events1 = (array) $wpdb->get_results(
3869 $wpdb->prepare(
3870 "SELECT l.*, t.id AS task_exists, t.status AS task_enabled
3871 FROM {$table_name_logs} l
3872 LEFT JOIN {$table_name_tasks} t ON t.id = l.id_task
3873 WHERE l.task_status = %s
3874 AND l.intentosfase1 < %d
3875 AND (l.task_type IS NULL OR l.task_type <> 'writenow')",
3876 'error',
3877 8
3878 ),
3879 ARRAY_A
3880 );
3881
3882 $retryable_events = array();
3883 $closed_missing_task = 0;
3884 $closed_disabled_task = 0;
3885
3886 foreach ($events1 as $event_row) {
3887 $event_row = (array) $event_row;
3888 $task_exists = !empty($event_row['task_exists']);
3889 $task_enabled = isset($event_row['task_enabled']) ? (int) $event_row['task_enabled'] : 0;
3890
3891 if (!$task_exists || $task_enabled !== 1) {
3892 $reason = !$task_exists
3893 ? 'Retry stopped: linked task was deleted'
3894 : 'Retry stopped: linked task is disabled';
3895
3896 $prev_error = isset($event_row['error']) ? trim((string) $event_row['error']) : '';
3897 $new_error = $prev_error !== '' ? ($prev_error . ' | ' . $reason) : $reason;
3898
3899 $wpdb->update(
3900 $table_name_logs,
3901 array(
3902 'intentosfase1' => 8,
3903 'error' => $new_error,
3904 ),
3905 array('id' => (int) $event_row['id'])
3906 );
3907
3908 if (!$task_exists) {
3909 $closed_missing_task++;
3910 } else {
3911 $closed_disabled_task++;
3912 }
3913 continue;
3914 }
3915
3916 $retryable_events[] = $event_row;
3917 }
3918
3919 botwriter_log('Phase 1 retries fetched', [
3920 'error_count' => count($retryable_events),
3921 'closed_missing_task' => $closed_missing_task,
3922 'closed_disabled_task' => $closed_disabled_task,
3923 ]);
3924
3925 $intento_tiempo = array(0=>0,1=>0,2=>5,3=>10,4=>30,5=>60,6=>120,7=>240,8=>480); // minutos
3926 foreach ($retryable_events as $event) {
3927 $event = (array) $event;
3928 // Execute the event if the time has passed
3929 $intentosfase1 = $event["intentosfase1"];
3930 $tiempo = $intento_tiempo[$intentosfase1+1];
3931 $retry_reference = !empty($event['last_execution_time']) ? $event['last_execution_time'] : $event['created_at'];
3932 $retry_reference_ts = strtotime($retry_reference);
3933 if ($retry_reference_ts === false) {
3934 $retry_reference_ts = strtotime($event['created_at']);
3935 }
3936 $now = current_time('timestamp');
3937
3938 $diff = $now - $retry_reference_ts;
3939 if ($diff > $tiempo * 60) {
3940 botwriter_log('Retrying phase 1 request', [
3941 'log_id' => $event['id'],
3942 'task_id' => $event['id_task'],
3943 'attempt' => $intentosfase1 + 1,
3944 ]);
3945 botwriter_send1_data_to_server( (array) $event);
3946 }
3947
3948 } // END LOGS IN ERROR
3949
3950
3951 }
3952
3953
3954 function botwriter_generate_post($data){
3955 $data = botwriter_normalize_generated_post_payload($data);
3956
3957 // Determine post type (default to 'post' for backward compatibility)
3958 $post_type = isset($data['post_type']) && !empty($data['post_type']) ? $data['post_type'] : 'post';
3959
3960 // Build post data array
3961 $post_data = array(
3962 'post_title' => $data['aigenerated_title'],
3963 'post_content' => $data['aigenerated_content'],
3964 'post_status' => $data['post_status'],
3965 'post_author' => $data['author_selection'],
3966 'post_type' => $post_type,
3967 );
3968
3969 // For 'post' type with category_id (backward compatibility)
3970 if ($post_type === 'post' && !empty($data['category_id'])) {
3971 $post_data['post_category'] = array_map('intval', explode(',', $data['category_id']));
3972 }
3973
3974 // Create the post
3975 $post_id = wp_insert_post($post_data);
3976
3977 if ($post_id === 0) {
3978 //error_log('Error creating post');
3979 return false;
3980 }
3981
3982 // Assign taxonomy terms from taxonomy_data (if present)
3983 if (!empty($data['taxonomy_data'])) {
3984 $taxonomy_data = json_decode($data['taxonomy_data'], true);
3985 if (is_array($taxonomy_data)) {
3986 foreach ($taxonomy_data as $taxonomy_name => $term_ids) {
3987 if (!empty($term_ids) && taxonomy_exists($taxonomy_name)) {
3988 $term_ids = array_map('intval', (array)$term_ids);
3989 wp_set_object_terms($post_id, $term_ids, $taxonomy_name);
3990 }
3991 }
3992 }
3993 }
3994
3995 // Add tags to the post (unless disabled in settings)
3996 $tags_disabled = get_option('botwriter_tags_disabled', '0');
3997 if ($tags_disabled !== '1' && !empty($data['aigenerated_tags'])) {
3998 $tags = explode(',', $data['aigenerated_tags']);
3999 wp_set_post_tags($post_id, $tags);
4000 }
4001
4002 // SEO Slug Translation: translate post slug, tag slugs, and get image slug
4003 $translated_image_slug = '';
4004 if (function_exists('botwriter_apply_translated_slugs')) {
4005 $translated_image_slug = botwriter_apply_translated_slugs(
4006 $post_id,
4007 $data['aigenerated_title'],
4008 $data['aigenerated_tags'] ?? ''
4009 );
4010 }
4011
4012 // Add image to the post only if image URL is provided and images are not disabled for this task
4013 $task_disable_images = isset($data['disable_ai_images']) ? intval($data['disable_ai_images']) : 0;
4014 if (!empty($data['aigenerated_image']) && $task_disable_images !== 1) {
4015 // Pass attribution data for stock photos
4016 $image_attribution = isset($data['image_attribution']) ? $data['image_attribution'] : null;
4017 botwriter_attach_image_to_post($post_id, $data['aigenerated_image'], $data['aigenerated_title'], $translated_image_slug, $image_attribution);
4018
4019 // Handle stock photo attribution in post content (footer mode)
4020 if (!empty($image_attribution) && is_array($image_attribution)) {
4021 $attribution_mode = get_option('botwriter_stockphoto_attribution', 'caption');
4022 if ($attribution_mode === 'content_footer') {
4023 $author = sanitize_text_field($image_attribution['author'] ?? '');
4024 $source = sanitize_text_field($image_attribution['source'] ?? '');
4025 $source_url = esc_url($image_attribution['source_url'] ?? '');
4026 $author_url = esc_url($image_attribution['author_url'] ?? '');
4027
4028 if ($author || $source) {
4029 $credit_parts = array();
4030 if ($author) {
4031 $credit_parts[] = $author_url
4032 ? sprintf('<a href="%s" rel="nofollow noopener" target="_blank">%s</a>', $author_url, esc_html($author))
4033 : esc_html($author);
4034 }
4035 if ($source) {
4036 $credit_parts[] = $source_url
4037 ? sprintf('<a href="%s" rel="nofollow noopener" target="_blank">%s</a>', $source_url, esc_html($source))
4038 : esc_html($source);
4039 }
4040 $credit_html = '<p class="botwriter-image-attribution"><small>'
4041 . sprintf(
4042 /* translators: %s: attribution credit (author / source) */
4043 esc_html__('Photo by %s', 'botwriter'),
4044 implode(' / ', $credit_parts)
4045 )
4046 . '</small></p>';
4047
4048 wp_update_post(array(
4049 'ID' => $post_id,
4050 'post_content' => get_post_field('post_content', $post_id) . "\n" . $credit_html,
4051 ));
4052 }
4053 }
4054 }
4055 } else {
4056 $skip_reason = '';
4057 if (empty($data['aigenerated_image'])) {
4058 $skip_reason = 'No image URL provided';
4059 } elseif ($task_disable_images === 1) {
4060 $skip_reason = 'AI images disabled for this task';
4061 }
4062
4063 botwriter_log('Image attachment skipped during post creation', [
4064 'post_id' => $post_id,
4065 'post_title' => $data['aigenerated_title'],
4066 'image_url' => $data['aigenerated_image'] ?? 'not provided',
4067 'reason' => $skip_reason
4068 ]);
4069 }
4070
4071 // Automatic SEO post-processing: insert internal links directly on publish.
4072 $seo_internal_links_result = array(
4073 'updated' => false,
4074 'inserted' => 0,
4075 'strategy' => 'disabled',
4076 );
4077 if (function_exists('botwriter_seo_auto_internal_links_postprocess')) {
4078 $seo_publish_context = array(
4079 'title' => (string) ($data['aigenerated_title'] ?? ''),
4080 'content' => (string) get_post_field('post_content', $post_id),
4081 'tags' => (string) ($data['aigenerated_tags'] ?? ''),
4082 'excerpt' => (string) get_post_field('post_excerpt', $post_id),
4083 );
4084
4085 botwriter_log('SEO publish post-processing start', array(
4086 'post_id' => $post_id,
4087 'title_len' => strlen((string) $seo_publish_context['title']),
4088 'content_len' => strlen((string) $seo_publish_context['content']),
4089 'tags_len' => strlen((string) $seo_publish_context['tags']),
4090 'excerpt_len' => strlen((string) $seo_publish_context['excerpt']),
4091 'content_preview' => botwriter_seo_debug_preview((string) $seo_publish_context['content'], 360),
4092 ));
4093
4094 $seo_internal_links_result = botwriter_seo_auto_internal_links_postprocess($post_id, array(
4095 'title' => (string) $seo_publish_context['title'],
4096 'content' => (string) $seo_publish_context['content'],
4097 'tags' => (string) $seo_publish_context['tags'],
4098 'excerpt' => (string) $seo_publish_context['excerpt'],
4099 ));
4100 }
4101
4102 // Generate SEO meta description using AI (if enabled in SEO settings)
4103 if ( function_exists( 'botwriter_generate_seo_meta' ) && function_exists( 'botwriter_is_seo_ai_meta_enabled' ) && botwriter_is_seo_ai_meta_enabled() ) {
4104 $post_language = $data['post_language'] ?? '';
4105 $meta_source_content = (string) get_post_field('post_content', $post_id);
4106 $meta_description = botwriter_generate_seo_meta(
4107 $data['aigenerated_title'],
4108 $meta_source_content,
4109 $post_language
4110 );
4111 if ( $meta_description ) {
4112 botwriter_apply_seo_meta( $post_id, $meta_description );
4113 }
4114 }
4115
4116 $seo_focus_keyword_result = array(
4117 'enabled' => false,
4118 'generated' => false,
4119 'length' => 0,
4120 );
4121 if (
4122 function_exists('botwriter_generate_seo_focus_keyword')
4123 && function_exists('botwriter_is_seo_publish_focus_keyword_enabled')
4124 && botwriter_is_seo_publish_focus_keyword_enabled()
4125 ) {
4126 $seo_focus_keyword_result['enabled'] = true;
4127 $post_language = $data['post_language'] ?? '';
4128 $focus_source_content = (string) get_post_field('post_content', $post_id);
4129 $focus_keyword = botwriter_generate_seo_focus_keyword(
4130 (string) ($data['aigenerated_title'] ?? ''),
4131 $focus_source_content,
4132 $post_language
4133 );
4134
4135 if (!empty($focus_keyword) && function_exists('botwriter_apply_seo_focus_keyword')) {
4136 botwriter_apply_seo_focus_keyword($post_id, (string) $focus_keyword);
4137 $seo_focus_keyword_result['generated'] = true;
4138 $seo_focus_keyword_result['length'] = function_exists('mb_strlen')
4139 ? mb_strlen((string) $focus_keyword)
4140 : strlen((string) $focus_keyword);
4141 }
4142 }
4143
4144 $seo_faq_result = array(
4145 'enabled' => false,
4146 'generated' => false,
4147 'mode' => 'disabled',
4148 'visible' => null,
4149 );
4150 if (get_option('botwriter_seo_publish_faq_enabled', '0') === '1') {
4151 $seo_faq_result['enabled'] = true;
4152 $faq_mode = sanitize_key((string) get_option('botwriter_seo_publish_faq_mode', 'visible_schema'));
4153 if ($faq_mode !== 'visible_schema' && $faq_mode !== 'schema_only') {
4154 $faq_mode = 'visible_schema';
4155 }
4156
4157 $faq_visible = $faq_mode === 'schema_only' ? 0 : 1;
4158 $seo_faq_result['mode'] = $faq_mode;
4159 $seo_faq_result['visible'] = $faq_visible;
4160
4161 if (function_exists('botwriter_seo_generate_faq_for_post')) {
4162 $faq_generated = (bool) botwriter_seo_generate_faq_for_post($post_id);
4163 $seo_faq_result['generated'] = $faq_generated;
4164 if ($faq_generated) {
4165 update_post_meta($post_id, '_botwriter_seo_faq_visible', $faq_visible);
4166 if (function_exists('botwriter_seo_compute_score')) {
4167 botwriter_seo_compute_score($post_id, true);
4168 }
4169 }
4170 } else {
4171 $seo_faq_result['mode'] = 'function_missing';
4172 }
4173 }
4174
4175 botwriter_log('SEO publish post-processing summary', array(
4176 'post_id' => $post_id,
4177 'internal_links_strategy' => (string) ($seo_internal_links_result['strategy'] ?? 'unknown'),
4178 'internal_links_inserted' => intval($seo_internal_links_result['inserted'] ?? 0),
4179 'internal_links_updated' => !empty($seo_internal_links_result['updated']) ? 1 : 0,
4180 'focus_keyword_enabled' => !empty($seo_focus_keyword_result['enabled']) ? 1 : 0,
4181 'focus_keyword_generated' => !empty($seo_focus_keyword_result['generated']) ? 1 : 0,
4182 'focus_keyword_length' => intval($seo_focus_keyword_result['length'] ?? 0),
4183 'faq_enabled' => !empty($seo_faq_result['enabled']) ? 1 : 0,
4184 'faq_generated' => !empty($seo_faq_result['generated']) ? 1 : 0,
4185 'faq_mode' => (string) ($seo_faq_result['mode'] ?? 'disabled'),
4186 'faq_visible' => isset($seo_faq_result['visible']) ? intval($seo_faq_result['visible']) : -1,
4187 ));
4188
4189 // Persist image prompt in post meta so regeneration never depends on logs.
4190 $saved_image_prompt = botwriter_resolve_image_prompt_from_post_data($data);
4191 botwriter_log('Post creation: image prompt resolution before meta save', array(
4192 'post_id' => $post_id,
4193 'resolved_prompt_len' => strlen((string) $saved_image_prompt),
4194 'incoming_image_prompt_len' => strlen(trim((string) ($data['image_prompt'] ?? ''))),
4195 'incoming_image_provider' => isset($data['image_provider']) ? sanitize_key((string) $data['image_provider']) : '',
4196 'has_image_attribution' => !empty($data['image_attribution']),
4197 ));
4198 if ($saved_image_prompt !== '') {
4199 $saved_provider = isset($data['image_provider']) ? sanitize_key((string) $data['image_provider']) : '';
4200
4201 if ($saved_provider === '' && !empty($data['image_attribution'])) {
4202 $saved_provider = 'stockphoto';
4203 }
4204
4205 if ($saved_provider === '') {
4206 $settings = botwriter_get_current_image_generation_settings();
4207 $saved_provider = (string) ($settings['provider'] ?? '');
4208 }
4209
4210 botwriter_log('Post creation: saving image prompt meta', array(
4211 'post_id' => $post_id,
4212 'provider' => $saved_provider,
4213 'prompt_len' => strlen((string) $saved_image_prompt),
4214 ));
4215
4216 botwriter_save_post_image_prompt_meta($post_id, $saved_image_prompt, $saved_provider);
4217 }
4218
4219 return $post_id;
4220 }
4221
4222 /**
4223 * Parse a JSON-like AI payload from a raw content string.
4224 * Handles markdown fences and typographic quotes used by some providers.
4225 *
4226 * @param mixed $raw_content
4227 * @return array|null
4228 */
4229 function botwriter_parse_generated_payload_from_content($raw_content) {
4230 if (!is_string($raw_content)) {
4231 return null;
4232 }
4233
4234 $clean = trim($raw_content);
4235 if ($clean === '') {
4236 return null;
4237 }
4238
4239 // Normalize BOM + typographic quotes before parsing.
4240 $clean = preg_replace('/^\xEF\xBB\xBF/u', '', $clean);
4241 $clean = preg_replace('/[\x{201C}\x{201D}\x{201E}\x{201F}]/u', '"', $clean);
4242 $clean = preg_replace('/[\x{2018}\x{2019}\x{201A}\x{201B}]/u', "'", $clean);
4243
4244 // Strip markdown code fences.
4245 if (preg_match('/^```(?:json|JSON)?\s*\n?(.*?)\n?```$/su', $clean, $matches)) {
4246 $clean = trim($matches[1]);
4247 } elseif (preg_match('/```(?:json|JSON)?\s*\n?(.*?)\n?```/su', $clean, $matches)) {
4248 $clean = trim($matches[1]);
4249 } elseif (preg_match('/^`{1,3}(?:json|JSON)?\s*\n?([\s\S]+)$/u', $clean, $matches)) {
4250 $inner = trim($matches[1]);
4251 $clean = preg_replace('/`{1,3}\s*$/u', '', $inner);
4252 $clean = trim((string) $clean);
4253 }
4254
4255 // Some providers prepend a stray "json" token before the object.
4256 $clean = preg_replace('/^json\s*(?=\{|\[)/i', '', $clean);
4257
4258 // Remove decorative wrappers around the payload.
4259 $clean = trim($clean, " \t\n\r\0\x0B`'\"");
4260
4261 $parsed = json_decode($clean, true);
4262
4263 if (!is_array($parsed) && preg_match('/\{[\s\S]*\}/u', $clean, $json_match)) {
4264 $parsed = json_decode($json_match[0], true);
4265 }
4266
4267 if (!is_array($parsed) || !isset($parsed['aigenerated_content'])) {
4268 return null;
4269 }
4270
4271 return $parsed;
4272 }
4273
4274 /**
4275 * Ensure the post payload uses parsed fields when content accidentally contains
4276 * wrapped JSON returned by the AI model.
4277 *
4278 * @param mixed $data
4279 * @return mixed
4280 */
4281 function botwriter_normalize_generated_post_payload($data) {
4282 if (!is_array($data)) {
4283 return $data;
4284 }
4285
4286 $raw_content = isset($data['aigenerated_content']) ? (string) $data['aigenerated_content'] : '';
4287 $parsed_payload = botwriter_parse_generated_payload_from_content($raw_content);
4288
4289 if (!is_array($parsed_payload)) {
4290 return $data;
4291 }
4292
4293 $normalized = $data;
4294 $changed = false;
4295
4296 $field_map = array('aigenerated_title', 'aigenerated_content', 'aigenerated_tags', 'image_prompt', 'image_keywords');
4297 foreach ($field_map as $field) {
4298 if (!array_key_exists($field, $parsed_payload)) {
4299 continue;
4300 }
4301
4302 $new_value = $parsed_payload[$field];
4303 if ($field === 'aigenerated_tags' && is_array($new_value)) {
4304 $new_value = implode(', ', $new_value);
4305 }
4306 $new_value = is_string($new_value) ? trim($new_value) : '';
4307
4308 if ($new_value === '') {
4309 continue;
4310 }
4311
4312 $current_value = isset($normalized[$field]) ? (string) $normalized[$field] : '';
4313 if ($current_value !== $new_value) {
4314 $normalized[$field] = $new_value;
4315 $changed = true;
4316 }
4317 }
4318
4319 if ($changed) {
4320 botwriter_log('Post payload normalized from wrapped JSON response', array(
4321 'title_len' => strlen((string) ($normalized['aigenerated_title'] ?? '')),
4322 'content_len' => strlen((string) ($normalized['aigenerated_content'] ?? '')),
4323 'tags_len' => strlen((string) ($normalized['aigenerated_tags'] ?? '')),
4324 'image_prompt_len' => strlen((string) ($normalized['image_prompt'] ?? '')),
4325 ));
4326 }
4327
4328 return $normalized;
4329 }
4330
4331
4332
4333
4334 /**
4335 * Send a legacy compat request with one automatic site_token recovery retry.
4336 *
4337 * When the backend reports token mismatch, clear local token and retry once.
4338 * This self-heals cloned/reinstalled sites where the stored token is stale.
4339 *
4340 * @param string $remote_url Endpoint URL.
4341 * @param array $data Request body.
4342 * @param bool $ssl_verify SSL verify flag.
4343 * @param int $timeout Request timeout in seconds.
4344 * @return array|WP_Error
4345 */
4346 function botwriter_post_compat_with_token_recovery($remote_url, $data, $ssl_verify, $timeout = 45) {
4347 $request_args = array(
4348 'method' => 'POST',
4349 'body' => $data,
4350 'timeout' => $timeout,
4351 'headers' => array(),
4352 'sslverify' => (bool) $ssl_verify,
4353 );
4354
4355 $response = wp_remote_post($remote_url, $request_args);
4356 if (is_wp_error($response)) {
4357 return $response;
4358 }
4359
4360 $http_code = wp_remote_retrieve_response_code($response);
4361 if ((int) $http_code !== 200) {
4362 return $response;
4363 }
4364
4365 $body = wp_remote_retrieve_body($response);
4366 $result = json_decode($body, true);
4367 if (!is_array($result)) {
4368 return $response;
4369 }
4370
4371 $error_code = (string) ($result['error'] ?? '');
4372 $error_message = (string) ($result['error_message'] ?? '');
4373 $token_issue = in_array($error_code, array('invalid_site_token', 'token_required'), true)
4374 || stripos($error_message, 'site token mismatch') !== false
4375 || stripos($error_message, 'requires authentication') !== false;
4376
4377 if (!$token_issue) {
4378 return $response;
4379 }
4380
4381 $current_token = (string) get_option('botwriter_site_token', '');
4382 if ($current_token === '') {
4383 return $response;
4384 }
4385
4386 delete_option('botwriter_site_token');
4387 $retry_data = $data;
4388 $retry_data['site_token'] = '';
4389
4390 botwriter_log('Site token mismatch detected. Retrying request with empty site_token.', array(
4391 'error' => $error_code,
4392 'message' => $error_message,
4393 ));
4394
4395 return wp_remote_post($remote_url, array(
4396 'method' => 'POST',
4397 'body' => $retry_data,
4398 'timeout' => $timeout,
4399 'headers' => array(),
4400 'sslverify' => (bool) $ssl_verify,
4401 ));
4402 }
4403
4404 // Function to send data to the server pass1
4405 function botwriter_send1_data_to_server($data) {
4406
4407 Global $botwriter_version;
4408 $remote_url = BOTWRITER_API_URL . 'redis_api_cola.php';
4409 botwriter_ensure_site_token();
4410
4411 // Use constant to avoid get_plugin_data() and early translation loading
4412 $botwriter_version = BOTWRITER_VERSION;
4413
4414 // settings
4415 $data['version'] = $botwriter_version;
4416 $data['api_key'] = get_option('botwriter_api_key'); // la api_key del programa
4417 $data["user_domainname"] = esc_url(get_site_url());
4418 $data['site_token'] = get_option('botwriter_site_token', '');
4419
4420 $data["ai_image_size"] = botwriter_normalize_ai_image_size((string) get_option('botwriter_ai_image_size', 'square'));
4421 $data["ai_image_quality"]=get_option('botwriter_ai_image_quality');
4422 $data["ai_image_style"]=get_option('botwriter_ai_image_style', 'realistic');
4423 $data["ai_image_style_custom"]=get_option('botwriter_ai_image_style_custom', '');
4424
4425 // Use task-specific setting for disable_ai_images (already in $data from task/log)
4426 // If not present, default to 0 (images enabled)
4427 if (!isset($data["disable_ai_images"])) {
4428 $data["disable_ai_images"] = 0;
4429 }
4430
4431 // If image generation fails, publish the post without image instead of erroring
4432 $data['image_error_continue'] = get_option('botwriter_image_error_continue', '0');
4433
4434 // Provider selections
4435 $data['text_provider'] = get_option('botwriter_text_provider', 'openai');
4436 $data['image_provider'] = get_option('botwriter_image_provider', 'stockphoto');
4437
4438 // Global provider "none" always means no AI images, regardless of legacy task flags.
4439 if ($data['image_provider'] === 'none') {
4440 $data['disable_ai_images'] = 1;
4441 }
4442
4443 // Stock photo settings (sent always, used only when image_provider=stockphoto)
4444 $data['stockphoto_preferred'] = botwriter_get_current_image_model_by_provider('stockphoto');
4445 $data['stockphoto_selection'] = get_option('botwriter_stockphoto_selection', 'random_top10');
4446 $data['stockphoto_attribution'] = get_option('botwriter_stockphoto_attribution', 'caption');
4447
4448 // Get current text model based on provider
4449 $text_provider = $data['text_provider'];
4450 if (function_exists('botwriter_get_provider_text_model')) {
4451 $data['text_model'] = botwriter_get_provider_text_model($text_provider);
4452 } else {
4453 $text_model_defaults = [
4454 'openai' => 'gpt-5.4-mini',
4455 'anthropic' => 'claude-sonnet-4-6',
4456 'google' => 'gemini-3.5-flash',
4457 'mistral' => 'mistral-large-latest',
4458 'groq' => 'llama-3.3-70b-versatile',
4459 'openrouter' => 'anthropic/claude-sonnet-4.6',
4460 ];
4461 $data['text_model'] = get_option("botwriter_{$text_provider}_model", $text_model_defaults[$text_provider] ?? 'gpt-5.4-mini');
4462 }
4463
4464 // Get current image model based on provider
4465 $image_provider = $data['image_provider'];
4466 $image_model_default = null;
4467 $image_model_option = null;
4468 $image_model_raw = null;
4469
4470 if ($image_provider === 'stockphoto') {
4471 $data['image_model'] = $data['stockphoto_preferred'];
4472 } elseif ($image_provider === 'none') {
4473 $data['image_model'] = 'none';
4474 } else {
4475 $image_model_default = function_exists('botwriter_get_provider_default_image_model')
4476 ? (string) botwriter_get_provider_default_image_model($image_provider)
4477 : '';
4478 if ($image_model_default === '') {
4479 $image_model_default = 'gpt-image-1';
4480 }
4481
4482 $image_model_option = function_exists('botwriter_get_image_model_option_name')
4483 ? botwriter_get_image_model_option_name($image_provider)
4484 : ($image_provider === 'gemini' ? 'botwriter_gemini_image_model' : "botwriter_{$image_provider}_model");
4485
4486 $image_model_raw = (string) get_option($image_model_option, $image_model_default);
4487
4488 if (function_exists('botwriter_get_current_image_model_by_provider')) {
4489 $data['image_model'] = botwriter_get_current_image_model_by_provider($image_provider);
4490 } else {
4491 $data['image_model'] = $image_model_raw;
4492 }
4493 }
4494
4495 // Also send the specific field name the server expects
4496 if ($image_provider === 'gemini') {
4497 $data['gemini_image_model'] = $data['image_model'];
4498
4499 $gemini_catalog_models = array();
4500 $image_model_raw_in_catalog = null;
4501 if (function_exists('botwriter_get_provider_image_models_flat')) {
4502 $gemini_catalog_models = array_keys(botwriter_get_provider_image_models_flat('gemini'));
4503 if (!empty($gemini_catalog_models)) {
4504 $image_model_raw_in_catalog = in_array(strtolower((string) $image_model_raw), $gemini_catalog_models, true);
4505 }
4506 }
4507
4508 botwriter_log('Image model trace before phase 1 send', array(
4509 'log_id' => $data['id'] ?? null,
4510 'task_id' => $data['id_task'] ?? null,
4511 'image_provider' => $image_provider,
4512 'image_model_option' => $image_model_option,
4513 'image_model_default' => $image_model_default,
4514 'image_model_raw' => $image_model_raw,
4515 'image_model_resolved' => $data['image_model'] ?? null,
4516 'image_model_changed' => ((string) ($image_model_raw ?? '') !== (string) ($data['image_model'] ?? '')),
4517 'image_model_raw_in_catalog' => $image_model_raw_in_catalog,
4518 'gemini_catalog_models' => $gemini_catalog_models,
4519 ));
4520 }
4521
4522 // Send all API keys (decrypted) - server will use the ones needed
4523 $data['openai_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_openai_api_key'));
4524 $data['anthropic_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_anthropic_api_key'));
4525 $data['google_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_google_api_key'));
4526 $data['mistral_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_mistral_api_key'));
4527 $data['groq_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_groq_api_key'));
4528 $data['openrouter_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_openrouter_api_key'));
4529 $data['fal_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_fal_api_key'));
4530 $data['replicate_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_replicate_api_key'));
4531 $data['stability_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_stability_api_key'));
4532 $data['cloudflare_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_cloudflare_api_key'));
4533 $data['cloudflare_account_id'] = get_option('botwriter_cloudflare_account_id');
4534
4535 // Legacy field (kept for backward compatibility)
4536 $data["openai_model"]=get_option('botwriter_openai_model', 'gpt-4o-mini');
4537
4538 $current_time = gmdate('Y-m-d H:i:s', current_time('timestamp'));
4539 $data["last_execution_time"]=$current_time;
4540
4541 $last_execution_time = $data["last_execution_time"];
4542
4543
4544 $category_ids=array_map('intval', explode(',', $data['category_id']));
4545 $titles = botwriter_get_logs_titles($data['id_task']);
4546 if ($titles === false) {
4547 $data['titles'] = '';
4548 } else {
4549 $data['titles'] = implode(' | ', $titles);
4550 }
4551
4552
4553 // add the links of the posts where it has been copied
4554 $links = botwriter_get_logs_links($data['id_task']);
4555 if ($links === false) {
4556 $data['links'] = '';
4557 } else {
4558 $data['links'] = implode(',', $links);
4559 }
4560
4561 // post_lenght
4562 if ($data['post_length'] === 'custom') {
4563 $data['post_length'] = $data['custom_post_length'];
4564 }
4565
4566 // =====================================================
4567 // CLIENT-SIDE RSS FETCH (pre-fetch before prompt build)
4568 // =====================================================
4569 $website_type = $data['website_type'] ?? '';
4570 if ($website_type === 'rss') {
4571 $rss_result = botwriter_fetch_rss_content(
4572 $data['rss_source'] ?? '',
4573 $data['links'] ?? ''
4574 );
4575
4576 if ( ! $rss_result['success'] ) {
4577 botwriter_log('Client RSS fetch failed', [
4578 'log_id' => $data['id'] ?? null,
4579 'task_id' => $data['id_task'] ?? null,
4580 'error' => $rss_result['error'] ?? 'Unknown error',
4581 ]);
4582 $data['task_status'] = 'error';
4583 $data['error'] = $rss_result['error'] ?? 'RSS fetch failed';
4584 $data['intentosfase1'] = isset($data['intentosfase1']) ? (int) $data['intentosfase1'] + 1 : 1;
4585 botwriter_logs_register($data, $data['id']);
4586 return false;
4587 }
4588
4589 // Populate data with the pre-fetched article so the prompt builder can use it
4590 $data['source_title'] = $rss_result['source_title'];
4591 $data['source_content'] = $rss_result['source_content'];
4592 $data['link_post_original'] = $rss_result['link_original'];
4593 $data['source_prefetched'] = '1';
4594
4595 // Reserve the source link in the log row BEFORE the phase 1 dispatch.
4596 // This protects against a second cron tick picking the same article
4597 // while this dispatch is still in flight (HTTP latency, retries, etc.).
4598 if ( ! empty( $data['id'] ) ) {
4599 botwriter_logs_register( $data, $data['id'] );
4600 }
4601
4602 botwriter_log('Client RSS article ready', [
4603 'log_id' => $data['id'] ?? null,
4604 'task_id' => $data['id_task'] ?? null,
4605 'article_link' => $rss_result['link_original'],
4606 ]);
4607 }
4608 // =====================================================
4609
4610 // =====================================================
4611 // CLIENT-SIDE WORDPRESS FETCH (pre-fetch before prompt build)
4612 // =====================================================
4613 if ($website_type === 'wordpress') {
4614 $wp_result = botwriter_fetch_wordpress_content(
4615 $data['domain_name'] ?? '',
4616 $data['website_category_id'] ?? '',
4617 $data['links'] ?? ''
4618 );
4619
4620 if ( ! $wp_result['success'] ) {
4621 botwriter_log('Client WordPress fetch failed', [
4622 'log_id' => $data['id'] ?? null,
4623 'task_id' => $data['id_task'] ?? null,
4624 'error' => $wp_result['error'] ?? 'Unknown error',
4625 ]);
4626 $data['task_status'] = 'error';
4627 $data['error'] = $wp_result['error'] ?? 'WordPress fetch failed';
4628 $data['intentosfase1'] = isset($data['intentosfase1']) ? (int) $data['intentosfase1'] + 1 : 1;
4629 botwriter_logs_register($data, $data['id']);
4630 return false;
4631 }
4632
4633 $data['source_title'] = $wp_result['source_title'];
4634 $data['source_content'] = $wp_result['source_content'];
4635 $data['link_post_original'] = $wp_result['link_original'];
4636 $data['source_prefetched'] = '1';
4637
4638 // Reserve the source link in the log row BEFORE the phase 1 dispatch
4639 // (see RSS branch above for rationale).
4640 if ( ! empty( $data['id'] ) ) {
4641 botwriter_logs_register( $data, $data['id'] );
4642 }
4643
4644 botwriter_log('Client WordPress article ready', [
4645 'log_id' => $data['id'] ?? null,
4646 'task_id' => $data['id_task'] ?? null,
4647 'article_link' => $wp_result['link_original'],
4648 ]);
4649 }
4650 // =====================================================
4651
4652 // Build prompt from template (except for super1 which is handled by server)
4653 if ($website_type !== 'super1') {
4654 $data['client_prompt'] = botwriter_build_client_prompt($data);
4655 }
4656
4657 botwriter_log('SEND1: after prompt build', [
4658 'log_id' => $data['id'] ?? null,
4659 'website_type' => $website_type,
4660 'has_client_prompt' => !empty($data['client_prompt']),
4661 'client_prompt_len' => strlen($data['client_prompt'] ?? ''),
4662 'source_prefetched' => $data['source_prefetched'] ?? '0',
4663 'has_source_title' => !empty($data['source_title']),
4664 'has_source_content' => !empty($data['source_content']),
4665 'link_post_original' => $data['link_post_original'] ?? '',
4666 ]);
4667
4668 botwriter_log('Dispatching phase 1 request', [
4669 'log_id' => $data['id'] ?? null,
4670 'task_id' => $data['id_task'] ?? null,
4671 'website_type' => $data['website_type'] ?? null,
4672 'status' => $data['task_status'] ?? null,
4673 'attempt' => $data['intentosfase1'] ?? null,
4674 'text_provider' => $data['text_provider'] ?? null,
4675 'text_model' => $data['text_model'] ?? null,
4676 'image_provider' => $data['image_provider'] ?? null,
4677 'image_model' => $data['image_model'] ?? null,
4678 'gemini_image_model' => $data['gemini_image_model'] ?? null,
4679 ]);
4680
4681 $data["error"]= "";
4682
4683 $ssl_verify = get_option('botwriter_sslverify');
4684
4685
4686 if ($ssl_verify === 'no') {
4687 $ssl_verify = false;
4688 } else {
4689 $ssl_verify = true;
4690 }
4691
4692
4693
4694 $response = botwriter_post_compat_with_token_recovery($remote_url, $data, $ssl_verify, 45);
4695
4696 $data["intentosfase1"]++;
4697 botwriter_logs_register($data, $data["id"]);
4698
4699 $last_execution_time=$data["last_execution_time"];
4700
4701
4702
4703 if (is_wp_error($response)) {
4704 $error_message = $response->get_error_message();
4705 botwriter_log('Phase 1 request error', [
4706 'log_id' => $data['id'] ?? null,
4707 'task_id' => $data['id_task'] ?? null,
4708 'website_type' => $data['website_type'] ?? null,
4709 'error' => $error_message,
4710 ]);
4711 $data["task_status"]="error";
4712 botwriter_logs_register($data, $data["id"]);
4713 return false;
4714
4715 } else {
4716
4717 if ($response['response']['code'] === 200) {
4718
4719 $body = wp_remote_retrieve_body($response);
4720 $result = json_decode($body, true);
4721
4722 //error_log("Data received: " . print_r($body, true));
4723
4724 if (!empty($result['site_token'])) {
4725 update_option('botwriter_site_token', sanitize_text_field((string) $result['site_token']));
4726 }
4727
4728 // STOPFORMANY: server reports too many consecutive errors
4729 if (isset($result['error']) && strpos($result['error'], 'STOPFORMANY') === 0) {
4730 update_option('botwriter_stopformany', true);
4731 $data['task_status'] = 'error';
4732 $data['error'] = $result['error'];
4733 $data['intentosfase1'] = 8; // stop retries
4734 botwriter_logs_register($data, $data['id']);
4735 botwriter_log('STOPFORMANY activated from phase 1', [
4736 'log_id' => $data['id'] ?? null,
4737 'task_id' => $data['id_task'] ?? null,
4738 'error' => $result['error'],
4739 ]);
4740 return false;
4741 }
4742
4743 if (isset($result['id_task_server']) && $result['id_task_server'] !== 0) { // ok
4744 $data["id_task_server"]=$result['id_task_server'];
4745 $data["task_status"]='inqueue';
4746
4747 // Capture site_token from server response (auto-provisioning)
4748 if (!empty($result['site_token'])) {
4749 update_option('botwriter_site_token', sanitize_text_field($result['site_token']));
4750 }
4751
4752 botwriter_logs_register($data, $data["id"]);
4753 botwriter_log('Phase 1 request accepted', [
4754 'log_id' => $data['id'] ?? null,
4755 'task_id' => $data['id_task'] ?? null,
4756 'id_task_server' => $result['id_task_server'],
4757 ]);
4758 return $result['id_task_server'];
4759 } else { // error — id_task_server is 0 or missing
4760 $data["task_status"]="error";
4761 // Use full error_message (may include reset link) when available
4762 $data["error"] = !empty($result['error_message']) ? $result['error_message'] : ($result['error'] ?? '');
4763
4764 // Generic terminal flag — server says stop retrying
4765 if (!empty($result['terminal'])) {
4766 $data['intentosfase1'] = 8;
4767 }
4768 // Show server-provided admin notice
4769 if (!empty($result['error_message']) && (int)($result['error_level'] ?? 0) === 1) {
4770 botwriter_announcements_add(
4771 __('Service notice', 'botwriter'),
4772 wp_kses_post($result['error_message'])
4773 );
4774 }
4775
4776 // Handle known server errors (same logic as Phase 2)
4777 if ($data["error"] == "Maximum monthly posts limit reached") {
4778 botwriter_announcements_add("Maximum monthly posts limit reached", "You have reached the maximum monthly posts limit. Please upgrade your plan to continue using the plugin. <a href='https://wpbotwriter.com' target='_blank'>Go to upgrade</a>");
4779 $data["intentosfase1"] = 8;
4780 }
4781 if ($data["error"] == "Payment date exceeded") {
4782 botwriter_announcements_add("Payment date exceeded", "Your subscription payment date has exceeded. Please renew your subscription to continue using the plugin. <a href='https://wpbotwriter.com' target='_blank'>Go to renew</a>");
4783 $data["intentosfase1"] = 8;
4784 }
4785 if ($data["error"] == "API Key error") {
4786 botwriter_announcements_add("API Key error", "Your API Key is invalid. Please check your API Key in the plugin settings. <a href='admin.php?page=botwriter_settings'>Go to Settings</a>");
4787 $data["intentosfase1"] = 8;
4788 }
4789
4790 botwriter_logs_register($data, $data["id"]);
4791 botwriter_log('Phase 1 request rejected', [
4792 'log_id' => $data['id'] ?? null,
4793 'task_id' => $data['id_task'] ?? null,
4794 'error' => $data['error'],
4795 'response_length' => isset($body) ? strlen($body) : null,
4796 ]);
4797 return false;
4798 }
4799 } else { // error
4800 $data["task_status"]="error";
4801 botwriter_logs_register($data, $data["id"]);
4802 botwriter_log('Phase 1 non-200 response', [
4803 'log_id' => $data['id'] ?? null,
4804 'task_id' => $data['id_task'] ?? null,
4805 'status_code' => $response['response']['code'],
4806 ]);
4807 return false;
4808 }
4809 }
4810
4811
4812 }
4813
4814 // Function to send data to the server pass2
4815 function botwriter_send2_data_to_server($data) {
4816 Global $wpdb;
4817
4818 $data['api_key'] = get_option('botwriter_api_key');
4819 $data["user_domainname"] = esc_url(get_site_url());
4820 $data['site_token'] = get_option('botwriter_site_token', '');
4821
4822 $remote_url = BOTWRITER_API_URL . 'redis_api_finish.php';
4823
4824 $ssl_verify = get_option('botwriter_sslverify');
4825 if ($ssl_verify === 'no') {
4826 $ssl_verify = false;
4827 } else {
4828 $ssl_verify = true;
4829 }
4830
4831 botwriter_log('Dispatching phase 2 request', [
4832 'log_id' => $data['id'] ?? null,
4833 'task_id' => $data['id_task'] ?? null,
4834 'website_type' => $data['website_type'] ?? null,
4835 'id_task_server' => $data['id_task_server'] ?? null,
4836 ]);
4837
4838 $response = botwriter_post_compat_with_token_recovery($remote_url, $data, $ssl_verify, 45);
4839
4840
4841 if (is_wp_error($response)) {
4842 $error_message = $response->get_error_message();
4843 botwriter_log('Phase 2 request error', [
4844 'log_id' => $data['id'] ?? null,
4845 'task_id' => $data['id_task'] ?? null,
4846 'website_type' => $data['website_type'] ?? null,
4847 'error' => $error_message,
4848 ]);
4849 $data["error"]= "Error sending data " . $error_message;
4850 return false;
4851 } else {
4852
4853 if ($response['response']['code'] === 200) {
4854
4855 $body = wp_remote_retrieve_body($response);
4856 $result = json_decode($body, true);
4857
4858 if (!empty($result['site_token'])) {
4859 update_option('botwriter_site_token', sanitize_text_field((string) $result['site_token']));
4860 }
4861
4862 //echo 'Data recive: <pre>' . print_r($result, true) . '</pre>';
4863
4864 // results errors
4865 if (isset($result["task_status"]) && $result["task_status"] == "error") {
4866 $data["task_status"]="error";
4867 // Use full error_message (may include reset link) when available
4868 $data["error"] = !empty($result['error_message']) ? $result['error_message'] : ($result['error'] ?? '');
4869 botwriter_log('Phase 2 reported error', [
4870 'log_id' => $data['id'] ?? null,
4871 'task_id' => $data['id_task'] ?? null,
4872 'id_task_server' => $data['id_task_server'] ?? null,
4873 'error' => $data['error'],
4874 ]);
4875
4876 // Generic terminal flag — server says stop retrying
4877 if (!empty($result['terminal'])) {
4878 $data['intentosfase1'] = 8;
4879 }
4880 // Show server-provided admin notice
4881 if (!empty($result['error_message']) && (int)($result['error_level'] ?? 0) === 1) {
4882 botwriter_announcements_add(
4883 __('Service notice', 'botwriter'),
4884 wp_kses_post($result['error_message'])
4885 );
4886 }
4887
4888 if ($data["error"]=="Maximum monthly posts limit reached") {
4889 botwriter_announcements_add("Maximum monthly posts limit reached", "You have reached the maximum monthly posts limit. Please upgrade your plan to continue using the plugin. <a href='https://wpbotwriter.com' target='_blank'>Go to upgrade</a>");
4890 $data["intentosfase1"]=8;
4891 }
4892 if ($data["error"]=="Payment date exceeded") {
4893 botwriter_announcements_add("Payment date exceeded", "Your subscription payment date has exceeded. Please renew your subscription to continue using the plugin. <a href='https://wpbotwriter.com' target='_blank'>Go to renew</a>");
4894 $data["intentosfase1"]=8;
4895 }
4896 if ($data["error"]=="API Key error") {
4897 botwriter_announcements_add("API Key error", "Your API Key is invalid. Please check your API Key in the plugin settings. <a href='admin.php?page=botwriter_settings'>Go to Settings</a>");
4898 $data["intentosfase1"]=8;
4899 }
4900 // STOPFORMANY: server reports too many consecutive errors
4901 if (isset($result['error']) && strpos($result['error'], 'STOPFORMANY') === 0) {
4902 update_option('botwriter_stopformany', true);
4903 $data['intentosfase1'] = 8;
4904 botwriter_log('STOPFORMANY activated from phase 2', [
4905 'log_id' => $data['id'] ?? null,
4906 'task_id' => $data['id_task'] ?? null,
4907 'error' => $result['error'],
4908 ]);
4909 }
4910
4911 botwriter_logs_register($data, $data["id"]);
4912 return false;
4913 }
4914
4915 // result completed
4916 botwriter_log('Phase 2 response payload', [
4917 'log_id' => $data['id'] ?? null,
4918 'task_id' => $data['id_task'] ?? null,
4919 'id_task_server' => $data['id_task_server'] ?? null,
4920 'task_status' => $result['task_status'] ?? null,
4921 ]);
4922 if (isset($result["task_status"]) && $result["task_status"] == "completed") {
4923 // Preserve image_attribution from server response (not stored in DB)
4924 $image_attribution = isset($result['image_attribution']) ? $result['image_attribution'] : null;
4925
4926 botwriter_logs_register($result, $data["id"]);
4927 botwriter_log('Phase 2 completed event', [
4928 'log_id' => $data['id'] ?? null,
4929 'task_id' => $data['id_task'] ?? null,
4930 'id_task_server' => $data['id_task_server'] ?? null,
4931 ]);
4932
4933 $result=botwriter_logs_get($data["id"]); // merge the result with the log
4934
4935 // Re-attach image_attribution (not persisted in logs table)
4936 if ($image_attribution) {
4937 $result['image_attribution'] = $image_attribution;
4938 }
4939
4940 // ── Guard: prevent duplicate post creation ──
4941 // If this log already has a published post, skip generation.
4942 if (!empty($result['id_post_published']) && intval($result['id_post_published']) > 0) {
4943 botwriter_log('Phase 2 skipped — post already published', [
4944 'log_id' => $data['id'],
4945 'post_id' => $result['id_post_published'],
4946 ]);
4947 return $result;
4948 }
4949
4950 if ($result["website_type"] == "super1") {
4951 botwriter_super1_log_to_bd($result,$data["id"]);
4952
4953 } else {
4954 $post_id=botwriter_generate_post($result);
4955 $result["id_post_published"]=$post_id;
4956 }
4957
4958 botwriter_logs_register($result, $data["id"]);
4959 if ($result["website_type"] == "super2") {
4960 //update tabla super poner el task_Status en completed
4961 $wpdb->update($wpdb->prefix . 'botwriter_super', ['task_status' => 'completed'], ['id_log' => $data["id"]]);
4962 }
4963
4964
4965 return $result;
4966 }
4967
4968 // other results, inqueue, pending, etc
4969 $now=current_time('timestamp');
4970 $last_execution_time = strtotime($data["last_execution_time"]);
4971 $diff = $now - $last_execution_time;
4972 if ($diff > 60 * 5) { // 5 minutes
4973 $data["task_status"]="error";
4974 $data["error"]="Error in server";
4975 botwriter_logs_register($data, $data["id"]);
4976 botwriter_log('Phase 2 timeout detected', [
4977 'log_id' => $data['id'] ?? null,
4978 'task_id' => $data['id_task'] ?? null,
4979 ]);
4980 }
4981 return false;
4982
4983
4984
4985
4986
4987
4988 } else { // error
4989 // update log
4990 $data["task_status"]="error";
4991 botwriter_logs_register($data, $data["id"]);
4992 botwriter_log('Phase 2 non-200 response', [
4993 'log_id' => $data['id'] ?? null,
4994 'task_id' => $data['id_task'] ?? null,
4995 'status_code' => $response['response']['code'],
4996 ]);
4997 return false;
4998 }
4999 }
5000
5001
5002
5003 }
5004
5005
5006
5007
5008
5009 function botwriter_cambiar_status_ajax() {
5010 // Verify user has permission
5011 if (!current_user_can('manage_options')) {
5012 wp_send_json_error(['message' => 'Permission denied']);
5013 }
5014
5015 check_ajax_referer('botwriter_cambiar_status_nonce', 'nonce');
5016
5017 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
5018 $nuevo_status = isset($_POST['status']) ? intval($_POST['status']) : 0;
5019
5020
5021
5022 if ($id > 0) {
5023 global $wpdb;
5024 $table_name = $wpdb->prefix . 'botwriter_tasks';
5025
5026 $result = $wpdb->update(
5027 $table_name,
5028 ['status' => $nuevo_status],
5029 ['id' => $id],
5030 ['%d'],
5031 ['%d']
5032 );
5033
5034 if ($result !== false) {
5035 wp_send_json_success(['message' => 'Estado actualizado correctamente']);
5036 } else {
5037 wp_send_json_error(['message' => 'Error al actualizar el estado']);
5038 }
5039 } else {
5040 wp_send_json_error(['message' => 'ID inválido']);
5041 }
5042
5043 wp_die();
5044 }
5045 add_action('wp_ajax_botwriter_cambiar_status', 'botwriter_cambiar_status_ajax');
5046
5047 /**
5048 * AJAX handler to check/preview an RSS feed from the admin UI.
5049 * Reads the feed client-side using botwriter_fetch_rss_content().
5050 */
5051 function botwriter_check_rss_ajax() {
5052 if ( ! current_user_can( 'manage_options' ) ) {
5053 wp_send_json_error( [ 'error' => 'Permission denied' ] );
5054 }
5055
5056 check_ajax_referer( 'botwriter_check_rss_nonce', 'nonce' );
5057
5058 $url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
5059 if ( empty( $url ) ) {
5060 wp_send_json_error( [ 'error' => 'RSS URL is required.' ] );
5061 }
5062
5063 $result = botwriter_fetch_rss_content( $url );
5064 if ( $result['success'] ) {
5065 wp_send_json_success( [
5066 'title' => $result['source_title'],
5067 'description' => $result['source_content'],
5068 'link' => $result['link_original'],
5069 ] );
5070 } else {
5071 wp_send_json_error( [ 'error' => $result['error'] ] );
5072 }
5073 }
5074 add_action( 'wp_ajax_botwriter_check_rss', 'botwriter_check_rss_ajax' );
5075
5076 /**
5077 * AJAX handler to fetch categories from a remote WordPress site.
5078 * Reads categories client-side using botwriter_fetch_wordpress_categories().
5079 */
5080 function botwriter_get_wordpress_categories_ajax() {
5081 if ( ! current_user_can( 'manage_options' ) ) {
5082 wp_send_json_error( [ 'error' => 'Permission denied' ] );
5083 }
5084
5085 check_ajax_referer( 'botwriter_wp_categories_nonce', 'nonce' );
5086
5087 $domain = isset( $_POST['website_domainname'] ) ? esc_url_raw( wp_unslash( $_POST['website_domainname'] ) ) : '';
5088 if ( empty( $domain ) ) {
5089 wp_send_json_error( [ 'error' => 'WordPress domain is required.' ] );
5090 }
5091
5092 $result = botwriter_fetch_wordpress_categories( $domain );
5093 if ( $result['success'] ) {
5094 wp_send_json_success( $result['categories'] );
5095 } else {
5096 wp_send_json_error( [ 'error' => $result['error'] ] );
5097 }
5098 }
5099 add_action( 'wp_ajax_botwriter_get_wp_categories', 'botwriter_get_wordpress_categories_ajax' );
5100
5101 // AJAX handler for deleting a log entry
5102 add_action('wp_ajax_botwriter_delete_log', 'botwriter_delete_log_ajax');
5103 function botwriter_delete_log_ajax() {
5104 // Verify user has permission
5105 if (!current_user_can('manage_options')) {
5106 wp_send_json_error(['message' => 'Permission denied']);
5107 }
5108
5109 // Verify nonce
5110 check_ajax_referer('botwriter_logs_delete_nonce', 'nonce');
5111
5112 $log_id = isset($_POST['log_id']) ? intval($_POST['log_id']) : 0;
5113
5114 if ($log_id > 0) {
5115 global $wpdb;
5116 $table_name = $wpdb->prefix . 'botwriter_logs';
5117
5118 $result = $wpdb->delete($table_name, array('id' => $log_id), array('%d'));
5119
5120 if ($result !== false) {
5121 wp_send_json_success(['message' => __('Log deleted successfully', 'botwriter')]);
5122 } else {
5123 wp_send_json_error(['message' => __('Error deleting log', 'botwriter')]);
5124 }
5125 } else {
5126 wp_send_json_error(['message' => __('Invalid log ID', 'botwriter')]);
5127 }
5128
5129 wp_die();
5130 }
5131
5132 // AJAX handler for bulk deleting log entries
5133 add_action('wp_ajax_botwriter_bulk_delete_logs', 'botwriter_bulk_delete_logs_ajax');
5134 function botwriter_bulk_delete_logs_ajax() {
5135 if (!current_user_can('manage_options')) {
5136 wp_send_json_error(['message' => 'Permission denied']);
5137 }
5138
5139 check_ajax_referer('botwriter_logs_delete_nonce', 'nonce');
5140
5141 $log_ids = isset($_POST['log_ids']) ? array_map('intval', $_POST['log_ids']) : array();
5142 $log_ids = array_filter($log_ids, function($id) { return $id > 0; });
5143
5144 if (empty($log_ids)) {
5145 wp_send_json_error(['message' => __('No logs selected', 'botwriter')]);
5146 }
5147
5148 global $wpdb;
5149 $table_name = $wpdb->prefix . 'botwriter_logs';
5150 $placeholders = implode(',', array_fill(0, count($log_ids), '%d'));
5151 $table_name = esc_sql($table_name);
5152 $query = $wpdb->prepare("DELETE FROM {$table_name} WHERE id IN ({$placeholders})", $log_ids);
5153 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is generated internally and placeholders are prepared from sanitized IDs.
5154 $deleted = $wpdb->query($query);
5155
5156 if ($deleted !== false) {
5157 wp_send_json_success(['message' => sprintf(
5158 /* translators: %d: number of deleted log entries. */
5159 __('%d log(s) deleted successfully', 'botwriter'),
5160 $deleted
5161 )]);
5162 } else {
5163 wp_send_json_error(['message' => __('Error deleting logs', 'botwriter')]);
5164 }
5165
5166 wp_die();
5167 }
5168
5169 // AJAX handler for getting taxonomies and terms for a post type
5170 add_action('wp_ajax_botwriter_get_taxonomies', 'botwriter_get_taxonomies_ajax');
5171 function botwriter_get_taxonomies_ajax() {
5172 // Verify user has permission
5173 if (!current_user_can('manage_options')) {
5174 wp_send_json_error(['message' => 'Permission denied']);
5175 }
5176
5177 // Verify nonce
5178 check_ajax_referer('botwriter_taxonomies_nonce', 'nonce');
5179
5180 $post_type = isset($_POST['post_type']) ? sanitize_text_field(wp_unslash($_POST['post_type'])) : 'post';
5181
5182 // Get taxonomies for this post type
5183 $taxonomies = get_object_taxonomies($post_type, 'objects');
5184
5185 $result = array();
5186 // Skip tag taxonomies since AI generates tags automatically
5187 $skip_taxonomies = array('post_tag', 'product_tag');
5188
5189 foreach ($taxonomies as $taxonomy) {
5190 // Skip non-public taxonomies
5191 if (!$taxonomy->public) {
5192 continue;
5193 }
5194
5195 // Skip tag taxonomies (AI generates tags)
5196 if (in_array($taxonomy->name, $skip_taxonomies, true)) {
5197 continue;
5198 }
5199
5200 // Get terms for this taxonomy
5201 $terms = get_terms(array(
5202 'taxonomy' => $taxonomy->name,
5203 'hide_empty' => false,
5204 'orderby' => 'name',
5205 'order' => 'ASC',
5206 ));
5207
5208 $terms_data = array();
5209 if (!is_wp_error($terms)) {
5210 foreach ($terms as $term) {
5211 $terms_data[] = array(
5212 'id' => $term->term_id,
5213 'name' => $term->name,
5214 'slug' => $term->slug,
5215 'parent' => $term->parent,
5216 );
5217 }
5218 }
5219
5220 $result[] = array(
5221 'name' => $taxonomy->name,
5222 'label' => $taxonomy->label,
5223 'hierarchical' => $taxonomy->hierarchical,
5224 'terms' => $terms_data,
5225 );
5226 }
5227
5228 wp_send_json_success($result);
5229 }
5230
5231
5232 function botwriter_attach_image_to_post($post_id, $image_url, $post_title, $translated_image_slug = '', $image_attribution = null) {
5233 if (!$image_url || !$post_id || empty(trim($image_url))) {
5234 botwriter_log('Image attachment skipped', [
5235 'post_id' => $post_id,
5236 'image_url' => $image_url ?: 'empty',
5237 'reason' => 'Invalid post ID or empty image URL'
5238 ]);
5239 return 'Invalid post ID or image URL';
5240 }
5241
5242 if ($image_url && $post_id) {
5243 //$image_data = file_get_contents($image_url);
5244 $image_data = wp_remote_retrieve_body(wp_remote_get($image_url));
5245 $upload_dir = wp_upload_dir();
5246 // Use translated image slug if available, otherwise fall back to title
5247 if (!empty($translated_image_slug)) {
5248 $base_name = sanitize_file_name($translated_image_slug);
5249 } else {
5250 $base_name = sanitize_file_name(remove_accents($post_title));
5251 }
5252 if (empty($base_name)) {
5253 $base_name = 'botwriter-post-' . $post_id;
5254 }
5255 /**
5256 * Filter the filename used for BotWriter generated images.
5257 *
5258 * @param string $base_name Base filename without extension.
5259 * @param int $post_id Post ID.
5260 * @param string $post_title Post title.
5261 */
5262 $base_name = apply_filters('botwriter_image_filename', $base_name, $post_id, $post_title);
5263 $filename = $base_name . '.jpg';
5264 $filename = wp_unique_filename($upload_dir['path'], $filename);
5265
5266 if (wp_mkdir_p($upload_dir['path'])) {
5267 $file = $upload_dir['path'] . '/' . $filename;
5268 } else {
5269 $file = $upload_dir['basedir'] . '/' . $filename;
5270 }
5271
5272 global $wp_filesystem;
5273 if ( ! function_exists( 'WP_Filesystem' ) ) {
5274 require_once ABSPATH . 'wp-admin/includes/file.php';
5275 }
5276 WP_Filesystem();
5277 if ( $wp_filesystem->put_contents( $file, $image_data, FS_CHMOD_FILE ) ) {
5278 //error_log('Imagen guardada exitosamente en: ' . $file);
5279 } else {
5280 //error_log('Error al guardar la imagen en: ' . $file);
5281 }
5282
5283 $wp_filetype = wp_check_filetype($filename, null);
5284 $attachment = [
5285 'post_mime_type' => $wp_filetype['type'],
5286 'post_title' => sanitize_file_name($filename),
5287 'post_content' => '',
5288 'post_status' => 'inherit',
5289 ];
5290
5291 $attach_id = wp_insert_attachment($attachment, $file, $post_id);
5292 require_once(ABSPATH . 'wp-admin/includes/image.php');
5293
5294 // Apply post-processing if enabled
5295 $postprocess_enabled = get_option('botwriter_image_postprocess_enabled', '0');
5296 if ($postprocess_enabled === '1') {
5297 $processed_file = botwriter_process_image($file);
5298 if ($processed_file && $processed_file !== $file) {
5299 // Update file reference if format changed
5300 $file = $processed_file;
5301 // Update attachment with new file info
5302 $wp_filetype = wp_check_filetype(basename($file), null);
5303 wp_update_post([
5304 'ID' => $attach_id,
5305 'post_mime_type' => $wp_filetype['type'],
5306 ]);
5307 update_attached_file($attach_id, $file);
5308 }
5309 }
5310
5311 $attach_data = wp_generate_attachment_metadata($attach_id, $file);
5312 wp_update_attachment_metadata($attach_id, $attach_data);
5313 set_post_thumbnail($post_id, $attach_id);
5314
5315 // Deterministic featured-image ALT: fill empty ALT from post title.
5316 if (get_option('botwriter_seo_featured_image_alt_enabled', '1') === '1') {
5317 $current_alt = trim((string) get_post_meta($attach_id, '_wp_attachment_image_alt', true));
5318 if ($current_alt === '') {
5319 $fallback_alt = sanitize_text_field($post_title);
5320 if ($fallback_alt === '') {
5321 $fallback_alt = sanitize_text_field((string) get_the_title($post_id));
5322 }
5323 if ($fallback_alt !== '') {
5324 update_post_meta($attach_id, '_wp_attachment_image_alt', $fallback_alt);
5325 }
5326 }
5327 }
5328
5329 // Apply stock photo attribution as image caption.
5330 if (!empty($image_attribution) && is_array($image_attribution)) {
5331 $attribution_mode = get_option('botwriter_stockphoto_attribution', 'caption');
5332 if ($attribution_mode !== 'disabled') {
5333 $author = sanitize_text_field($image_attribution['author'] ?? '');
5334 $source = sanitize_text_field($image_attribution['source'] ?? '');
5335
5336 // Build caption: "Photo by Author on Source"
5337 $caption = '';
5338 if ($author && $source) {
5339 $caption = sprintf(
5340 /* translators: 1: photographer name, 2: image source name */
5341 esc_html__('Photo by %1$s on %2$s', 'botwriter'),
5342 $author,
5343 $source
5344 );
5345 } elseif ($source) {
5346 $caption = sprintf(
5347 /* translators: %s: image source name. */
5348 esc_html__('Photo from %s', 'botwriter'),
5349 $source
5350 );
5351 }
5352
5353 if ($caption && $attribution_mode === 'caption') {
5354 wp_update_post(array(
5355 'ID' => $attach_id,
5356 'post_excerpt' => $caption,
5357 ));
5358 }
5359
5360 // Store full attribution data as post meta for later use
5361 update_post_meta($attach_id, '_botwriter_image_attribution', $image_attribution);
5362 }
5363 }
5364
5365
5366 }
5367 }
5368
5369 /**
5370 * Process image for optimization (resize, compress, convert format)
5371 * Uses WordPress wp_get_image_editor for maximum hosting compatibility.
5372 *
5373 * @param string $file_path Full path to the image file.
5374 * @return string|false New file path if processed, original path if no changes, false on error.
5375 */
5376 function botwriter_process_image($file_path) {
5377 if (!file_exists($file_path)) {
5378 botwriter_log('Image post-processing: File not found', ['path' => $file_path]);
5379 return false;
5380 }
5381
5382 // Get settings
5383 $output_format = get_option('botwriter_image_output_format', 'webp');
5384 $max_width = intval(get_option('botwriter_image_max_width', 1200));
5385 $compression = intval(get_option('botwriter_image_compression', 85));
5386 $max_filesize = intval(get_option('botwriter_image_max_filesize', 120)) * 1024; // Convert KB to bytes
5387
5388 // Log settings at start
5389 $original_size = filesize($file_path);
5390 botwriter_log('Image post-processing: Starting', [
5391 'file' => basename($file_path),
5392 'original_size_kb' => round($original_size / 1024, 1),
5393 'settings' => [
5394 'output_format' => $output_format,
5395 'max_width' => $max_width,
5396 'compression' => $compression,
5397 'max_filesize_kb' => $max_filesize / 1024
5398 ]
5399 ]);
5400
5401 // Get WordPress image editor
5402 $editor = wp_get_image_editor($file_path);
5403 if (is_wp_error($editor)) {
5404 botwriter_log('Image post-processing: Failed to load editor', [
5405 'path' => $file_path,
5406 'error' => $editor->get_error_message()
5407 ]);
5408 return $file_path; // Return original on error
5409 }
5410
5411 // Log which editor is being used
5412 botwriter_log('Image post-processing: Editor loaded', [
5413 'editor_class' => get_class($editor)
5414 ]);
5415
5416 $size = $editor->get_size();
5417 $modified = false;
5418
5419 botwriter_log('Image post-processing: Original dimensions', [
5420 'width' => $size['width'],
5421 'height' => $size['height']
5422 ]);
5423
5424 // Resize if wider than max width
5425 if ($max_width > 0 && $size['width'] > $max_width) {
5426 $new_height = intval($size['height'] * ($max_width / $size['width']));
5427 $result = $editor->resize($max_width, $new_height, false);
5428 if (!is_wp_error($result)) {
5429 $modified = true;
5430 botwriter_log('Image post-processing: Resized', [
5431 'from' => $size['width'] . 'x' . $size['height'],
5432 'to' => $max_width . 'x' . $new_height
5433 ]);
5434 }
5435 }
5436
5437 // Set quality
5438 $editor->set_quality($compression);
5439
5440 // Determine output file path and mime type
5441 $path_info = pathinfo($file_path);
5442 $new_extension = $path_info['extension'];
5443 $mime_type = null;
5444
5445 if ($output_format !== 'original') {
5446 switch ($output_format) {
5447 case 'webp':
5448 // Check if WebP is supported via GD or Imagick
5449 $webp_supported = function_exists('imagewebp');
5450 if (!$webp_supported && extension_loaded('imagick') && class_exists('Imagick')) {
5451 // Check Imagick WebP support dynamically to avoid static analysis errors
5452 $imagick_formats = call_user_func(['Imagick', 'queryFormats'], 'WEBP');
5453 $webp_supported = !empty($imagick_formats);
5454 }
5455 if ($webp_supported) {
5456 $new_extension = 'webp';
5457 $mime_type = 'image/webp';
5458 } else {
5459 // Fallback to JPEG if WebP not supported
5460 $new_extension = 'jpg';
5461 $mime_type = 'image/jpeg';
5462 botwriter_log('Image post-processing: WebP not supported, falling back to JPEG');
5463 }
5464 break;
5465 case 'jpeg':
5466 $new_extension = 'jpg';
5467 $mime_type = 'image/jpeg';
5468 break;
5469 case 'png':
5470 $new_extension = 'png';
5471 $mime_type = 'image/png';
5472 break;
5473 }
5474 }
5475
5476 // Build new file path
5477 $new_file_path = $path_info['dirname'] . '/' . $path_info['filename'] . '.' . $new_extension;
5478
5479 botwriter_log('Image post-processing: Format decision', [
5480 'original_extension' => $path_info['extension'],
5481 'new_extension' => $new_extension,
5482 'mime_type' => $mime_type,
5483 'new_file_path' => basename($new_file_path)
5484 ]);
5485
5486 // Save the image
5487 $save_args = [];
5488 if ($mime_type) {
5489 $save_args['mime_type'] = $mime_type;
5490 }
5491
5492 $saved = $editor->save($new_file_path, $mime_type);
5493
5494 if (is_wp_error($saved)) {
5495 botwriter_log('Image post-processing: Failed to save', [
5496 'path' => $new_file_path,
5497 'error' => $saved->get_error_message()
5498 ]);
5499 return $file_path; // Return original on error
5500 }
5501
5502 $new_file_path = $saved['path'];
5503
5504 botwriter_log('Image post-processing: Initial save complete', [
5505 'saved_file' => basename($new_file_path),
5506 'size_kb' => round(filesize($new_file_path) / 1024, 1)
5507 ]);
5508
5509 // If max filesize is set and file is too large, reduce quality iteratively
5510 if ($max_filesize > 0) {
5511 $current_size = filesize($new_file_path);
5512 $quality = $compression;
5513 $attempts = 0;
5514 $max_attempts = 5;
5515
5516 botwriter_log('Image post-processing: Checking filesize target', [
5517 'current_kb' => round($current_size / 1024, 1),
5518 'target_kb' => $max_filesize / 1024,
5519 'needs_compression' => $current_size > $max_filesize
5520 ]);
5521
5522 while ($current_size > $max_filesize && $quality > 40 && $attempts < $max_attempts) {
5523 $quality -= 10;
5524 $attempts++;
5525
5526 botwriter_log('Image post-processing: Compression attempt', [
5527 'attempt' => $attempts,
5528 'quality' => $quality
5529 ]);
5530
5531 // Reload and resave with lower quality
5532 $editor = wp_get_image_editor($new_file_path);
5533 if (!is_wp_error($editor)) {
5534 $editor->set_quality($quality);
5535 $saved = $editor->save($new_file_path, $mime_type);
5536 if (!is_wp_error($saved)) {
5537 $current_size = filesize($saved['path']);
5538 $new_file_path = $saved['path'];
5539 }
5540 }
5541 }
5542
5543 if ($attempts > 0) {
5544 botwriter_log('Image post-processing: Compressed for filesize target', [
5545 'target_kb' => $max_filesize / 1024,
5546 'final_kb' => round($current_size / 1024, 1),
5547 'final_quality' => $quality,
5548 'attempts' => $attempts
5549 ]);
5550 }
5551 }
5552
5553 // Delete original file if format changed
5554 if ($new_file_path !== $file_path && file_exists($file_path) && file_exists($new_file_path)) {
5555 wp_delete_file($file_path);
5556 botwriter_log('Image post-processing: Converted format', [
5557 'from' => $path_info['extension'],
5558 'to' => $new_extension,
5559 'new_size_kb' => round(filesize($new_file_path) / 1024, 1)
5560 ]);
5561 }
5562
5563 // Final summary log
5564 $final_size = filesize($new_file_path);
5565 botwriter_log('Image post-processing: Complete', [
5566 'original_file' => basename($file_path),
5567 'final_file' => basename($new_file_path),
5568 'original_size_kb' => round($original_size / 1024, 1),
5569 'final_size_kb' => round($final_size / 1024, 1),
5570 'size_reduction_percent' => round((1 - ($final_size / $original_size)) * 100, 1)
5571 ]);
5572
5573 return $new_file_path;
5574 }
5575
5576
5577
5578 add_action('plugins_loaded', 'botwriter_check_update');
5579 function botwriter_check_update() {
5580 // Ensure install date exists for legacy installs
5581 if (get_option('botwriter_install_date') === false) {
5582 update_option('botwriter_install_date', current_time('timestamp'));
5583 }
5584 // Use constant instead of get_plugin_data() to obtain current plugin version
5585 $plugin_version = BOTWRITER_VERSION;
5586
5587 // Get the previously installed version
5588 $version_instalada = get_option('botwriter_version');
5589
5590 if ($version_instalada != $plugin_version) {
5591 botwriter_create_table();
5592
5593 // Insert default templates if none exist (for updates from older versions)
5594 botwriter_insert_all_default_templates();
5595
5596 // Migration: reset 'custom' provider to defaults (removed in this version)
5597 if (get_option('botwriter_text_provider') === 'custom') {
5598 update_option('botwriter_text_provider', 'openai');
5599 }
5600 if (get_option('botwriter_image_provider') === 'custom') {
5601 update_option('botwriter_image_provider', 'stockphoto');
5602 }
5603 // Clean up custom provider options
5604 delete_option('botwriter_custom_text_url');
5605 delete_option('botwriter_custom_text_api_key');
5606 delete_option('botwriter_custom_text_model');
5607 delete_option('botwriter_custom_text_timeout');
5608 delete_option('botwriter_custom_image_url');
5609 delete_option('botwriter_custom_image_type');
5610 delete_option('botwriter_custom_image_model');
5611 delete_option('botwriter_custom_image_timeout');
5612
5613 // Drop direct mode table if it exists
5614 global $wpdb;
5615 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}botwriter_direct_tasks");
5616
5617 // Normalize legacy text model options and curated model catalogs.
5618 if (function_exists('botwriter_get_provider_text_model')) {
5619 botwriter_get_provider_text_model('google');
5620 botwriter_get_provider_text_model('openrouter');
5621 }
5622 if (function_exists('botwriter_get_models_data')) {
5623 botwriter_get_models_data();
5624 }
5625
5626 update_option('botwriter_version', $plugin_version); // Update version in database
5627 }
5628 }
5629
5630
5631
5632 // new super functions
5633 add_action('wp_ajax_botwriter_actualizar_articulo', 'botwriter_actualizar_articulo_callback');
5634
5635 function botwriter_actualizar_articulo_callback() {
5636 // Verify user has permission
5637 if (!current_user_can('manage_options')) {
5638 wp_send_json_error('Permission denied');
5639 }
5640
5641 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5642 global $wpdb;
5643 $table_name = $wpdb->prefix . 'botwriter_super';
5644
5645 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
5646 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
5647 $content = isset($_POST['content']) ? sanitize_textarea_field(wp_unslash($_POST['content'])) : '';
5648
5649 $result = $wpdb->update(
5650 $table_name,
5651 array('title' => $title, 'content' => $content),
5652 array('id' => $id)
5653 );
5654
5655 if ($result !== false) {
5656 wp_send_json_success('Artículo actualizado correctamente.');
5657 } else {
5658 wp_send_json_error('Error al actualizar el artículo.');
5659 }
5660
5661 wp_die();
5662 }
5663
5664 add_action('wp_ajax_botwriter_eliminar_articulo', 'botwriter_eliminar_articulo_callback');
5665
5666 function botwriter_eliminar_articulo_callback() {
5667 // Verify user has permission
5668 if (!current_user_can('manage_options')) {
5669 wp_send_json_error('Permission denied');
5670 }
5671
5672 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5673 global $wpdb;
5674 $table_name = $wpdb->prefix . 'botwriter_super';
5675
5676 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
5677
5678 $result = $wpdb->delete($table_name, array('id' => $id));
5679 // consultar cuantos registros con id_task=0
5680 $result = $wpdb->get_results("SELECT * FROM $table_name WHERE id_task='0'");
5681 $num_rows = count($result);
5682 if ($num_rows == 0) {
5683 // borramos en la tabla logs la tarea super1 si no hay articulos
5684 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
5685 $wpdb->delete($table_name_logs, array('website_type' => 'super1'));
5686 }
5687
5688
5689
5690 if ($result !== false) {
5691 wp_send_json_success('Artículo eliminado correctamente.');
5692 } else {
5693 wp_send_json_error('Error al eliminar el artículo.');
5694 }
5695
5696 wp_die();
5697 }
5698
5699 add_action('wp_ajax_botwriter_check_super1', 'botwriter_check_super1_callback');
5700
5701 function botwriter_check_super1_callback() {
5702 // Verify user has permission
5703 if (!current_user_can('manage_options')) {
5704 wp_send_json_error('Permission denied');
5705 }
5706
5707 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5708 $estado_super1=botwriter_super1_check_task_finish();
5709 if ($estado_super1=="completed") {
5710 $response = botwriter_super1_view_articles_html();
5711 wp_send_json_success($response);
5712 return;
5713 }
5714
5715 if ($estado_super1=="error") {
5716 // borramos la tarea super1
5717 global $wpdb;
5718 $table_name = $wpdb->prefix . 'botwriter_super';
5719 $wpdb->delete($table_name, array('id_task' => 0));
5720 wp_send_json_error("error");
5721 return;
5722 }
5723 wp_send_json_error('inqueue');
5724
5725
5726 }
5727
5728
5729 add_action('wp_ajax_botwriter_create_super1', 'botwriter_create_super1_callback');
5730
5731 function botwriter_create_super1_callback() {
5732 // Verify user has permission
5733 if (!current_user_can('manage_options')) {
5734 wp_send_json_error('Permission denied');
5735 }
5736
5737 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5738 if (!isset($_POST['prompt']) || !isset($_POST['numarticles'])) {
5739 botwriter_log('Super1 creation request missing parameters');
5740 wp_send_json_error('Missing required parameters.');
5741 wp_die();
5742 }
5743
5744 $prompt = sanitize_text_field(wp_unslash($_POST['prompt']));
5745 $numarticles = intval(wp_unslash($_POST['numarticles']));
5746 if ($prompt=="Custom") {
5747 $category_id = isset($_POST['category_id']) ? sanitize_text_field(wp_unslash($_POST['category_id'])) : '0';
5748 }
5749
5750 botwriter_log('Super1 creation request received', [
5751 'prompt' => $prompt,
5752 'num_articles' => $numarticles,
5753 'category_id' => isset($category_id) ? $category_id : null,
5754 ]);
5755
5756 $title_prompt = $prompt;
5757
5758 if ($prompt=="Custom") {
5759 $content_prompt = isset($_POST['custom_prompt']) ? sanitize_text_field(wp_unslash($_POST['custom_prompt'])) : '';
5760 } else {
5761 $info_blog = botwriter_get_info_blog();
5762 $json_info_blog = json_encode($info_blog, JSON_PRETTY_PRINT);
5763 $content_prompt = $json_info_blog;
5764 }
5765 $task_name = "Super1 Task " . current_time('Y-m-d H:i:s');
5766 $log_id = botwriter_super1_create_task($task_name, $title_prompt,$content_prompt, $numarticles, $category_id);
5767
5768 botwriter_log('Super1 task queued', [
5769 'log_id' => $log_id,
5770 'title_prompt' => $title_prompt,
5771 'num_articles' => $numarticles,
5772 ]);
5773
5774 wp_send_json_success("Task created successfully: " . $title_prompt . " " . $content_prompt . " " . $numarticles . " " . $category_id);
5775
5776
5777 }
5778
5779
5780 add_action('wp_ajax_botwriter_eliminar_super1', 'botwriter_eliminar_super1_y_logs0');
5781
5782 function botwriter_eliminar_super1_y_logs0() {
5783 // Verify user has permission
5784 if (!current_user_can('manage_options')) {
5785 wp_send_json_error('Permission denied');
5786 }
5787
5788 check_ajax_referer('botwriter_super_nonce');
5789 global $wpdb;
5790 $table_name = $wpdb->prefix . 'botwriter_super';
5791 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
5792
5793 $result = $wpdb->delete($table_name, array('id_task' => 0));
5794 $result = $wpdb->delete($table_name_logs, array('website_type' => 'super1'));
5795
5796 if ($result !== false) {
5797 wp_send_json_success('Super1 task deleted successfully.');
5798 } else {
5799 wp_send_json_error('Error');
5800 }
5801 wp_die();
5802 }
5803
5804 add_action('wp_ajax_botwriter_create_super1_manual', 'botwriter_create_super1_manual_callback');
5805
5806 function botwriter_create_super1_manual_callback() {
5807 // Verify user has permission
5808 if (!current_user_can('manage_options')) {
5809 wp_send_json_error('Permission denied');
5810 }
5811
5812 check_ajax_referer('botwriter_super_nonce');
5813
5814 if (!isset($_POST['manual_titles']) || empty(trim(wp_unslash($_POST['manual_titles'])))) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only emptiness check; sanitized below before use.
5815 wp_send_json_error('No titles provided.');
5816 wp_die();
5817 }
5818
5819 $raw_titles = sanitize_textarea_field(wp_unslash($_POST['manual_titles']));
5820 $global_prompt = isset($_POST['global_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['global_prompt'])) : '';
5821 $category_id = isset($_POST['category_id']) ? intval(wp_unslash($_POST['category_id'])) : 0;
5822
5823 // Split titles by newline and filter empty lines
5824 $titles = array_filter(array_map('trim', explode("\n", $raw_titles)), function($t) {
5825 return !empty($t);
5826 });
5827
5828 if (empty($titles)) {
5829 wp_send_json_error('No valid titles found.');
5830 wp_die();
5831 }
5832
5833 // Limit to 100 titles max
5834 if (count($titles) > 100) {
5835 $titles = array_slice($titles, 0, 100);
5836 }
5837
5838 botwriter_log('Manual Super1 creation request', [
5839 'num_titles' => count($titles),
5840 'category_id' => $category_id,
5841 'has_global_prompt' => !empty($global_prompt),
5842 ]);
5843
5844 global $wpdb;
5845
5846 // 1. Create a completed super1 log entry (bypassing AI generation)
5847 $log_data = array(
5848 'task_name' => 'Manual Super1 Task ' . current_time('Y-m-d H:i:s'),
5849 'task_status' => 'completed',
5850 'website_type' => 'super1',
5851 'title_prompt' => 'Manual',
5852 'content_prompt' => $global_prompt,
5853 'post_count' => count($titles),
5854 'category_id' => $category_id,
5855 );
5856 $log_id = botwriter_logs_register($log_data);
5857
5858 if (!$log_id) {
5859 wp_send_json_error('Error creating log entry.');
5860 wp_die();
5861 }
5862
5863 // 2. Insert each title into wp_botwriter_super
5864 $table_name = $wpdb->prefix . 'botwriter_super';
5865 $category_name = '';
5866 if ($category_id > 0) {
5867 $category_name = get_cat_name($category_id);
5868 }
5869
5870 foreach ($titles as $title) {
5871 $data = array(
5872 'title' => sanitize_text_field($title),
5873 'content' => $global_prompt,
5874 'category_name' => $category_name,
5875 'category_id' => $category_id,
5876 'id_log' => $log_id,
5877 'id_task' => 0, // draft, will be assigned on save
5878 'task_status' => '',
5879 );
5880 $wpdb->insert($table_name, $data);
5881 }
5882
5883 botwriter_log('Manual Super1 titles inserted', [
5884 'log_id' => $log_id,
5885 'count' => count($titles),
5886 ]);
5887
5888 // 3. Return the articles HTML for immediate review
5889 $html = botwriter_super1_view_articles_html(0);
5890 wp_send_json_success($html);
5891 }
5892
5893
5894 // ========================================
5895 // CONTENT REWRITER AJAX HANDLERS
5896 // ========================================
5897 add_action('wp_ajax_botwriter_rewriter_fetch', 'botwriter_rewriter_fetch_ajax');
5898 add_action('wp_ajax_botwriter_rewriter_create_task', 'botwriter_rewriter_create_task_ajax');
5899
5900 /**
5901 * AJAX: Fetch and extract content from URLs
5902 */
5903 function botwriter_rewriter_fetch_ajax() {
5904 check_ajax_referer('botwriter_rewriter_nonce');
5905
5906 if (!current_user_can('manage_options')) {
5907 wp_send_json_error(__('Permission denied.', 'botwriter'));
5908 }
5909
5910 $urls = isset($_POST['urls']) ? array_map('esc_url_raw', (array) wp_unslash($_POST['urls'])) : array();
5911
5912 if (empty($urls)) {
5913 wp_send_json_error(__('No URLs provided.', 'botwriter'));
5914 }
5915
5916 if (count($urls) > 20) {
5917 wp_send_json_error(__('Maximum 20 URLs allowed.', 'botwriter'));
5918 }
5919
5920 $articles = array();
5921 $errors = array();
5922
5923 foreach ($urls as $url) {
5924 if (empty($url)) {
5925 continue;
5926 }
5927
5928 $result = botwriter_rewriter_extract_content($url);
5929
5930 if (is_wp_error($result)) {
5931 $errors[] = array(
5932 'url' => $url,
5933 'error' => $result->get_error_message(),
5934 );
5935 } else {
5936 $articles[] = $result;
5937 }
5938 }
5939
5940 wp_send_json_success(array(
5941 'articles' => $articles,
5942 'errors' => $errors,
5943 ));
5944 }
5945
5946 /**
5947 * AJAX: Create a Super Task with the articles to rewrite.
5948 *
5949 * Inserts the task as an active super2 task and stores each article
5950 * in the botwriter_super table. The cron loop picks them up via
5951 * botwriter_super_prepare_event() just like normal Super Tasks.
5952 */
5953 function botwriter_rewriter_create_task_ajax() {
5954 check_ajax_referer('botwriter_rewriter_nonce');
5955
5956 if (!current_user_can('manage_options')) {
5957 wp_send_json_error(__('Permission denied.', 'botwriter'));
5958 }
5959
5960 // wp_unslash only — sanitize_text_field corrupts JSON (strips tags/newlines)
5961 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON is decoded first; each title/content field is sanitized below before use.
5962 $articles_json = isset($_POST['articles']) ? wp_unslash($_POST['articles']) : '';
5963 $rewrite_prompt = isset($_POST['rewrite_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['rewrite_prompt'])) : '';
5964 $category_id = isset($_POST['category_id']) ? absint(wp_unslash($_POST['category_id'])) : 0;
5965
5966 // Task properties from Step 3 form
5967 $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : 'draft';
5968 $post_language = isset($_POST['post_language']) ? sanitize_text_field(wp_unslash($_POST['post_language'])) : substr(get_locale(), 0, 2);
5969 $author_selection = isset($_POST['author_selection']) ? sanitize_text_field(wp_unslash($_POST['author_selection'])) : strval(get_current_user_id());
5970 $post_length = isset($_POST['post_length']) ? sanitize_text_field(wp_unslash($_POST['post_length'])) : '800';
5971 $custom_post_length = isset($_POST['custom_post_length']) ? sanitize_text_field(wp_unslash($_POST['custom_post_length'])) : '';
5972 $template_id = isset($_POST['template_id']) && !empty($_POST['template_id']) ? absint(wp_unslash($_POST['template_id'])) : null;
5973 $disable_ai_images = isset($_POST['disable_ai_images']) ? absint(wp_unslash($_POST['disable_ai_images'])) : 0;
5974 $days = isset($_POST['days']) ? sanitize_text_field(wp_unslash($_POST['days'])) : 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday';
5975 $times_per_day = isset($_POST['times_per_day']) ? absint(wp_unslash($_POST['times_per_day'])) : 1;
5976 $task_name_custom = isset($_POST['task_name']) ? sanitize_text_field(wp_unslash($_POST['task_name'])) : '';
5977
5978 // Validate post_status
5979 if (!in_array($post_status, array('draft', 'publish'), true)) {
5980 $post_status = 'draft';
5981 }
5982
5983 $articles = json_decode($articles_json, true);
5984 if (!is_array($articles) || empty($articles)) {
5985 wp_send_json_error(__('No articles provided.', 'botwriter'));
5986 }
5987
5988 global $wpdb;
5989 $tasks_table = $wpdb->prefix . 'botwriter_tasks';
5990 $super_table = $wpdb->prefix . 'botwriter_super';
5991
5992 // Build default rewrite prompt if user didn't provide one
5993 if (empty($rewrite_prompt)) {
5994 $rewrite_prompt = 'Rewrite this article completely in your own words while preserving all key information. Make it original, engaging, and well-structured.';
5995 }
5996
5997 // Count valid articles (filter empties before inserting)
5998 $valid_articles = array();
5999 foreach ($articles as $article) {
6000 $title = sanitize_text_field($article['title'] ?? '');
6001 $content = wp_kses_post($article['content'] ?? '');
6002 if (!empty($title) || !empty($content)) {
6003 $valid_articles[] = array('title' => $title, 'content' => $content);
6004 }
6005 }
6006
6007 if (empty($valid_articles)) {
6008 wp_send_json_error(__('All articles are empty.', 'botwriter'));
6009 }
6010
6011 $task_name = !empty($task_name_custom) ? $task_name_custom : 'Rewriter: ' . count($valid_articles) . ' articles - ' . wp_date('M j, Y H:i');
6012
6013 // Use the days and times_per_day from the form
6014 $wpdb->insert($tasks_table, array(
6015 'task_name' => $task_name,
6016 'post_status' => $post_status,
6017 'writer' => 'ai_cerebro',
6018 'narration' => 'Descriptive',
6019 'custom_style' => '',
6020 'post_language' => $post_language,
6021 'post_length' => $post_length,
6022 'custom_post_length' => $custom_post_length,
6023 'days' => $days,
6024 'times_per_day' => $times_per_day > 0 ? $times_per_day : 1,
6025 'status' => 1,
6026 'website_type' => 'super2',
6027 'task_type' => 'rewriter',
6028 'domain_name' => get_site_url(),
6029 'category_id' => $category_id > 0 ? strval($category_id) : '',
6030 'title_prompt' => '',
6031 'content_prompt' => $rewrite_prompt,
6032 'tags_prompt' => '',
6033 'image_prompt' => '',
6034 'aigenerated_title' => '',
6035 'aigenerated_content' => '',
6036 'aigenerated_tags' => '',
6037 'aigenerated_image' => '',
6038 'ai_keywords' => '',
6039 'author_selection' => $author_selection,
6040 'disable_ai_images' => $disable_ai_images,
6041 'template_id' => $template_id,
6042 ));
6043
6044 $task_id = $wpdb->insert_id;
6045
6046 if (!$task_id) {
6047 wp_send_json_error(__('Failed to create task.', 'botwriter'));
6048 }
6049
6050 // Insert each article into the super table.
6051 // Only the article content goes here — rewrite instructions stay in the task's content_prompt.
6052 // super_prepare_event() preserves the rewrite_prompt, and
6053 // botwriter_build_client_prompt() outputs it BEFORE the ENDARTICLE-wrapped content.
6054 $inserted = 0;
6055 foreach ($valid_articles as $art) {
6056 $wpdb->insert($super_table, array(
6057 'id_task' => $task_id,
6058 'id_log' => 0,
6059 'title' => $art['title'],
6060 'content' => $art['content'],
6061 ));
6062 // task_status left NULL — super_prepare_event picks rows with NULL/empty task_status
6063 $inserted++;
6064 }
6065
6066 botwriter_log('Content Rewriter: Task created', [
6067 'task_id' => $task_id,
6068 'article_count' => $inserted,
6069 ]);
6070
6071 wp_send_json_success(array(
6072 'task_id' => $task_id,
6073 'count' => $inserted,
6074 'edit_url' => wp_nonce_url(admin_url('admin.php?page=botwriter_super_page&id=' . $task_id), 'botwriter_tasks_action'),
6075 ));
6076 }
6077
6078 // ========================================
6079 // SITE REWRITER AJAX HANDLERS
6080 // ========================================
6081 add_action('wp_ajax_botwriter_siterewriter_crawl', 'botwriter_siterewriter_crawl_ajax');
6082 add_action('wp_ajax_botwriter_siterewriter_fetch', 'botwriter_siterewriter_fetch_ajax');
6083 add_action('wp_ajax_botwriter_siterewriter_create_task', 'botwriter_siterewriter_create_task_ajax');
6084
6085 /**
6086 * AJAX: Crawl a single page — returns title + internal links.
6087 * The JS manages the BFS queue for live/progressive UI updates.
6088 */
6089 function botwriter_siterewriter_crawl_ajax() {
6090 check_ajax_referer('botwriter_siterewriter_nonce');
6091
6092 if (!current_user_can('manage_options')) {
6093 wp_send_json_error(__('Permission denied.', 'botwriter'));
6094 }
6095
6096 $url = isset($_POST['url']) ? esc_url_raw(wp_unslash($_POST['url'])) : '';
6097 $base_domain = isset($_POST['base_domain']) ? sanitize_text_field(wp_unslash($_POST['base_domain'])) : '';
6098
6099 if (empty($url) || empty($base_domain)) {
6100 wp_send_json_error(__('Missing URL or domain.', 'botwriter'));
6101 }
6102
6103 $result = botwriter_siterewriter_crawl_page($url, $base_domain);
6104
6105 if (is_wp_error($result)) {
6106 wp_send_json_error($result->get_error_message());
6107 }
6108
6109 wp_send_json_success($result);
6110 }
6111
6112 /**
6113 * AJAX: Fetch and extract content from selected URLs.
6114 * Reuses botwriter_rewriter_extract_content() for full content extraction.
6115 */
6116 function botwriter_siterewriter_fetch_ajax() {
6117 check_ajax_referer('botwriter_siterewriter_nonce');
6118
6119 if (!current_user_can('manage_options')) {
6120 wp_send_json_error(__('Permission denied.', 'botwriter'));
6121 }
6122
6123 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Long-running image batch operation; safe inside an admin AJAX handler.
6124 @set_time_limit(0);
6125
6126 $urls = isset($_POST['urls']) ? array_map('esc_url_raw', (array) wp_unslash($_POST['urls'])) : array();
6127
6128 if (empty($urls)) {
6129 wp_send_json_error(__('No URLs provided.', 'botwriter'));
6130 }
6131
6132 $articles = array();
6133 $errors = array();
6134
6135 foreach ($urls as $url) {
6136 if (empty($url)) continue;
6137
6138 $result = botwriter_rewriter_extract_content($url);
6139
6140 if (is_wp_error($result)) {
6141 $errors[] = array(
6142 'url' => $url,
6143 'error' => $result->get_error_message(),
6144 );
6145 } else {
6146 $articles[] = $result;
6147 }
6148 }
6149
6150 wp_send_json_success(array(
6151 'articles' => $articles,
6152 'errors' => $errors,
6153 ));
6154 }
6155
6156 /**
6157 * AJAX: Create a Super Task with the articles to rewrite.
6158 * Identical flow to Content Rewriter but with task_type = 'siterewriter'.
6159 */
6160 function botwriter_siterewriter_create_task_ajax() {
6161 check_ajax_referer('botwriter_siterewriter_nonce');
6162
6163 if (!current_user_can('manage_options')) {
6164 wp_send_json_error(__('Permission denied.', 'botwriter'));
6165 }
6166
6167 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON is decoded first; each title/content field is sanitized below before use.
6168 $articles_json = isset($_POST['articles']) ? wp_unslash($_POST['articles']) : '';
6169 $rewrite_prompt = isset($_POST['rewrite_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['rewrite_prompt'])) : '';
6170 $category_id = isset($_POST['category_id']) ? absint(wp_unslash($_POST['category_id'])) : 0;
6171
6172 $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : 'draft';
6173 $post_language = isset($_POST['post_language']) ? sanitize_text_field(wp_unslash($_POST['post_language'])) : substr(get_locale(), 0, 2);
6174 $author_selection = isset($_POST['author_selection']) ? sanitize_text_field(wp_unslash($_POST['author_selection'])) : strval(get_current_user_id());
6175 $post_length = isset($_POST['post_length']) ? sanitize_text_field(wp_unslash($_POST['post_length'])) : '800';
6176 $custom_post_length = isset($_POST['custom_post_length'])? sanitize_text_field(wp_unslash($_POST['custom_post_length'])): '';
6177 $template_id = isset($_POST['template_id']) && !empty($_POST['template_id']) ? absint(wp_unslash($_POST['template_id'])) : null;
6178 $disable_ai_images = isset($_POST['disable_ai_images']) ? absint(wp_unslash($_POST['disable_ai_images'])) : 0;
6179 $days = isset($_POST['days']) ? sanitize_text_field(wp_unslash($_POST['days'])) : 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday';
6180 $times_per_day = isset($_POST['times_per_day']) ? absint(wp_unslash($_POST['times_per_day'])) : 1;
6181 $task_name_custom = isset($_POST['task_name']) ? sanitize_text_field(wp_unslash($_POST['task_name'])) : '';
6182
6183 if (!in_array($post_status, array('draft', 'publish'), true)) {
6184 $post_status = 'draft';
6185 }
6186
6187 $articles = json_decode($articles_json, true);
6188 if (!is_array($articles) || empty($articles)) {
6189 wp_send_json_error(__('No articles provided.', 'botwriter'));
6190 }
6191
6192 global $wpdb;
6193 $tasks_table = $wpdb->prefix . 'botwriter_tasks';
6194 $super_table = $wpdb->prefix . 'botwriter_super';
6195
6196 if (empty($rewrite_prompt)) {
6197 $rewrite_prompt = 'Rewrite this article completely in your own words while preserving all key information. Make it original, engaging, and well-structured.';
6198 }
6199
6200 $valid_articles = array();
6201 foreach ($articles as $article) {
6202 $title = sanitize_text_field($article['title'] ?? '');
6203 $content = wp_kses_post($article['content'] ?? '');
6204 if (!empty($title) || !empty($content)) {
6205 $valid_articles[] = array('title' => $title, 'content' => $content);
6206 }
6207 }
6208
6209 if (empty($valid_articles)) {
6210 wp_send_json_error(__('All articles are empty.', 'botwriter'));
6211 }
6212
6213 $task_name = !empty($task_name_custom)
6214 ? $task_name_custom
6215 : 'Site Rewriter: ' . count($valid_articles) . ' articles - ' . wp_date('M j, Y H:i');
6216
6217 $wpdb->insert($tasks_table, array(
6218 'task_name' => $task_name,
6219 'post_status' => $post_status,
6220 'writer' => 'ai_cerebro',
6221 'narration' => 'Descriptive',
6222 'custom_style' => '',
6223 'post_language' => $post_language,
6224 'post_length' => $post_length,
6225 'custom_post_length' => $custom_post_length,
6226 'days' => $days,
6227 'times_per_day' => $times_per_day > 0 ? $times_per_day : 1,
6228 'status' => 1,
6229 'website_type' => 'super2',
6230 'task_type' => 'siterewriter',
6231 'domain_name' => get_site_url(),
6232 'category_id' => $category_id > 0 ? strval($category_id) : '',
6233 'title_prompt' => '',
6234 'content_prompt' => $rewrite_prompt,
6235 'tags_prompt' => '',
6236 'image_prompt' => '',
6237 'aigenerated_title' => '',
6238 'aigenerated_content' => '',
6239 'aigenerated_tags' => '',
6240 'aigenerated_image' => '',
6241 'ai_keywords' => '',
6242 'author_selection' => $author_selection,
6243 'disable_ai_images' => $disable_ai_images,
6244 'template_id' => $template_id,
6245 ));
6246
6247 $task_id = $wpdb->insert_id;
6248
6249 if (!$task_id) {
6250 wp_send_json_error(__('Failed to create task.', 'botwriter'));
6251 }
6252
6253 // Insert each article into the super table.
6254 // Only the article content goes here — rewrite instructions stay in the task's content_prompt.
6255 // super_prepare_event() preserves the rewrite_prompt, and
6256 // botwriter_build_client_prompt() outputs it BEFORE the ENDARTICLE-wrapped content.
6257 $inserted = 0;
6258 foreach ($valid_articles as $art) {
6259 $wpdb->insert($super_table, array(
6260 'id_task' => $task_id,
6261 'id_log' => 0,
6262 'title' => $art['title'],
6263 'content' => $art['content'],
6264 ));
6265 $inserted++;
6266 }
6267
6268 botwriter_log('Site Rewriter: Task created', [
6269 'task_id' => $task_id,
6270 'article_count' => $inserted,
6271 ]);
6272
6273 wp_send_json_success(array(
6274 'task_id' => $task_id,
6275 'count' => $inserted,
6276 'edit_url' => wp_nonce_url(admin_url('admin.php?page=botwriter_super_page&id=' . $task_id), 'botwriter_tasks_action'),
6277 ));
6278 }
6279
6280 ?>
6281