PluginProbe
BotWriter – AI Writer & SEO Content Generator / 3.4.6
BotWriter – AI Writer & SEO Content Generator v3.4.6
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 3.4.6, at botwriter.php

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