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

6,164 lines 260.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: BotWriter – AI Writer & SEO Content Generator
4 Plugin URI: https://www.wpbotwriter.com
5 Description: Plugin for automatically generating posts using artificial intelligence. Create content from scratch with AI and generate custom images. Optimize content for SEO, including tags, titles, and image descriptions. Advanced features like ChatGPT, automatic content creation, image generation, SEO optimization, and AI training make this plugin a complete tool for writers and content creators.
6 Version: 3.4.4
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.4');
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-2.5-flash-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 $text_model_defaults = [
4347 'openai' => 'gpt-5.4-mini',
4348 'anthropic' => 'claude-sonnet-4-6',
4349 'google' => 'gemini-2.5-flash',
4350 'mistral' => 'mistral-large-latest',
4351 'groq' => 'llama-3.3-70b-versatile',
4352 'openrouter' => 'anthropic/claude-sonnet-4.6',
4353 ];
4354 $data['text_model'] = get_option("botwriter_{$text_provider}_model", $text_model_defaults[$text_provider] ?? 'gpt-5.4-mini');
4355
4356 // Get current image model based on provider
4357 $image_provider = $data['image_provider'];
4358 $image_model_default = null;
4359 $image_model_option = null;
4360 $image_model_raw = null;
4361
4362 if ($image_provider === 'stockphoto') {
4363 $data['image_model'] = $data['stockphoto_preferred'];
4364 } elseif ($image_provider === 'none') {
4365 $data['image_model'] = 'none';
4366 } else {
4367 $image_model_default = function_exists('botwriter_get_provider_default_image_model')
4368 ? (string) botwriter_get_provider_default_image_model($image_provider)
4369 : '';
4370 if ($image_model_default === '') {
4371 $image_model_default = 'gpt-image-1';
4372 }
4373
4374 $image_model_option = function_exists('botwriter_get_image_model_option_name')
4375 ? botwriter_get_image_model_option_name($image_provider)
4376 : ($image_provider === 'gemini' ? 'botwriter_gemini_image_model' : "botwriter_{$image_provider}_model");
4377
4378 $image_model_raw = (string) get_option($image_model_option, $image_model_default);
4379
4380 if (function_exists('botwriter_get_current_image_model_by_provider')) {
4381 $data['image_model'] = botwriter_get_current_image_model_by_provider($image_provider);
4382 } else {
4383 $data['image_model'] = $image_model_raw;
4384 }
4385 }
4386
4387 // Also send the specific field name the server expects
4388 if ($image_provider === 'gemini') {
4389 $data['gemini_image_model'] = $data['image_model'];
4390
4391 $gemini_catalog_models = array();
4392 $image_model_raw_in_catalog = null;
4393 if (function_exists('botwriter_get_provider_image_models_flat')) {
4394 $gemini_catalog_models = array_keys(botwriter_get_provider_image_models_flat('gemini'));
4395 if (!empty($gemini_catalog_models)) {
4396 $image_model_raw_in_catalog = in_array(strtolower((string) $image_model_raw), $gemini_catalog_models, true);
4397 }
4398 }
4399
4400 botwriter_log('Image model trace before phase 1 send', array(
4401 'log_id' => $data['id'] ?? null,
4402 'task_id' => $data['id_task'] ?? null,
4403 'image_provider' => $image_provider,
4404 'image_model_option' => $image_model_option,
4405 'image_model_default' => $image_model_default,
4406 'image_model_raw' => $image_model_raw,
4407 'image_model_resolved' => $data['image_model'] ?? null,
4408 'image_model_changed' => ((string) ($image_model_raw ?? '') !== (string) ($data['image_model'] ?? '')),
4409 'image_model_raw_in_catalog' => $image_model_raw_in_catalog,
4410 'gemini_catalog_models' => $gemini_catalog_models,
4411 ));
4412 }
4413
4414 // Send all API keys (decrypted) - server will use the ones needed
4415 $data['openai_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_openai_api_key'));
4416 $data['anthropic_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_anthropic_api_key'));
4417 $data['google_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_google_api_key'));
4418 $data['mistral_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_mistral_api_key'));
4419 $data['groq_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_groq_api_key'));
4420 $data['openrouter_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_openrouter_api_key'));
4421 $data['fal_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_fal_api_key'));
4422 $data['replicate_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_replicate_api_key'));
4423 $data['stability_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_stability_api_key'));
4424 $data['cloudflare_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_cloudflare_api_key'));
4425 $data['cloudflare_account_id'] = get_option('botwriter_cloudflare_account_id');
4426
4427 // Legacy field (kept for backward compatibility)
4428 $data["openai_model"]=get_option('botwriter_openai_model', 'gpt-4o-mini');
4429
4430 $current_time = gmdate('Y-m-d H:i:s', current_time('timestamp'));
4431 $data["last_execution_time"]=$current_time;
4432
4433 $last_execution_time = $data["last_execution_time"];
4434
4435
4436 $category_ids=array_map('intval', explode(',', $data['category_id']));
4437 $titles = botwriter_get_logs_titles($data['id_task']);
4438 if ($titles === false) {
4439 $data['titles'] = '';
4440 } else {
4441 $data['titles'] = implode(' | ', $titles);
4442 }
4443
4444
4445 // add the links of the posts where it has been copied
4446 $links = botwriter_get_logs_links($data['id_task']);
4447 if ($links === false) {
4448 $data['links'] = '';
4449 } else {
4450 $data['links'] = implode(',', $links);
4451 }
4452
4453 // post_lenght
4454 if ($data['post_length'] === 'custom') {
4455 $data['post_length'] = $data['custom_post_length'];
4456 }
4457
4458 // =====================================================
4459 // CLIENT-SIDE RSS FETCH (pre-fetch before prompt build)
4460 // =====================================================
4461 $website_type = $data['website_type'] ?? '';
4462 if ($website_type === 'rss') {
4463 $rss_result = botwriter_fetch_rss_content(
4464 $data['rss_source'] ?? '',
4465 $data['links'] ?? ''
4466 );
4467
4468 if ( ! $rss_result['success'] ) {
4469 botwriter_log('Client RSS fetch failed', [
4470 'log_id' => $data['id'] ?? null,
4471 'task_id' => $data['id_task'] ?? null,
4472 'error' => $rss_result['error'] ?? 'Unknown error',
4473 ]);
4474 $data['task_status'] = 'error';
4475 $data['error'] = $rss_result['error'] ?? 'RSS fetch failed';
4476 $data['intentosfase1'] = isset($data['intentosfase1']) ? (int) $data['intentosfase1'] + 1 : 1;
4477 botwriter_logs_register($data, $data['id']);
4478 return false;
4479 }
4480
4481 // Populate data with the pre-fetched article so the prompt builder can use it
4482 $data['source_title'] = $rss_result['source_title'];
4483 $data['source_content'] = $rss_result['source_content'];
4484 $data['link_post_original'] = $rss_result['link_original'];
4485 $data['source_prefetched'] = '1';
4486
4487 // Reserve the source link in the log row BEFORE the phase 1 dispatch.
4488 // This protects against a second cron tick picking the same article
4489 // while this dispatch is still in flight (HTTP latency, retries, etc.).
4490 if ( ! empty( $data['id'] ) ) {
4491 botwriter_logs_register( $data, $data['id'] );
4492 }
4493
4494 botwriter_log('Client RSS article ready', [
4495 'log_id' => $data['id'] ?? null,
4496 'task_id' => $data['id_task'] ?? null,
4497 'article_link' => $rss_result['link_original'],
4498 ]);
4499 }
4500 // =====================================================
4501
4502 // =====================================================
4503 // CLIENT-SIDE WORDPRESS FETCH (pre-fetch before prompt build)
4504 // =====================================================
4505 if ($website_type === 'wordpress') {
4506 $wp_result = botwriter_fetch_wordpress_content(
4507 $data['domain_name'] ?? '',
4508 $data['website_category_id'] ?? '',
4509 $data['links'] ?? ''
4510 );
4511
4512 if ( ! $wp_result['success'] ) {
4513 botwriter_log('Client WordPress fetch failed', [
4514 'log_id' => $data['id'] ?? null,
4515 'task_id' => $data['id_task'] ?? null,
4516 'error' => $wp_result['error'] ?? 'Unknown error',
4517 ]);
4518 $data['task_status'] = 'error';
4519 $data['error'] = $wp_result['error'] ?? 'WordPress fetch failed';
4520 $data['intentosfase1'] = isset($data['intentosfase1']) ? (int) $data['intentosfase1'] + 1 : 1;
4521 botwriter_logs_register($data, $data['id']);
4522 return false;
4523 }
4524
4525 $data['source_title'] = $wp_result['source_title'];
4526 $data['source_content'] = $wp_result['source_content'];
4527 $data['link_post_original'] = $wp_result['link_original'];
4528 $data['source_prefetched'] = '1';
4529
4530 // Reserve the source link in the log row BEFORE the phase 1 dispatch
4531 // (see RSS branch above for rationale).
4532 if ( ! empty( $data['id'] ) ) {
4533 botwriter_logs_register( $data, $data['id'] );
4534 }
4535
4536 botwriter_log('Client WordPress article ready', [
4537 'log_id' => $data['id'] ?? null,
4538 'task_id' => $data['id_task'] ?? null,
4539 'article_link' => $wp_result['link_original'],
4540 ]);
4541 }
4542 // =====================================================
4543
4544 // Build prompt from template (except for super1 which is handled by server)
4545 if ($website_type !== 'super1') {
4546 $data['client_prompt'] = botwriter_build_client_prompt($data);
4547 }
4548
4549 botwriter_log('SEND1: after prompt build', [
4550 'log_id' => $data['id'] ?? null,
4551 'website_type' => $website_type,
4552 'has_client_prompt' => !empty($data['client_prompt']),
4553 'client_prompt_len' => strlen($data['client_prompt'] ?? ''),
4554 'source_prefetched' => $data['source_prefetched'] ?? '0',
4555 'has_source_title' => !empty($data['source_title']),
4556 'has_source_content' => !empty($data['source_content']),
4557 'link_post_original' => $data['link_post_original'] ?? '',
4558 ]);
4559
4560 botwriter_log('Dispatching phase 1 request', [
4561 'log_id' => $data['id'] ?? null,
4562 'task_id' => $data['id_task'] ?? null,
4563 'website_type' => $data['website_type'] ?? null,
4564 'status' => $data['task_status'] ?? null,
4565 'attempt' => $data['intentosfase1'] ?? null,
4566 'text_provider' => $data['text_provider'] ?? null,
4567 'text_model' => $data['text_model'] ?? null,
4568 'image_provider' => $data['image_provider'] ?? null,
4569 'image_model' => $data['image_model'] ?? null,
4570 'gemini_image_model' => $data['gemini_image_model'] ?? null,
4571 ]);
4572
4573 $data["error"]= "";
4574
4575 $ssl_verify = get_option('botwriter_sslverify');
4576
4577
4578 if ($ssl_verify === 'no') {
4579 $ssl_verify = false;
4580 } else {
4581 $ssl_verify = true;
4582 }
4583
4584
4585
4586 $response = botwriter_post_compat_with_token_recovery($remote_url, $data, $ssl_verify, 45);
4587
4588 $data["intentosfase1"]++;
4589 botwriter_logs_register($data, $data["id"]);
4590
4591 $last_execution_time=$data["last_execution_time"];
4592
4593
4594
4595 if (is_wp_error($response)) {
4596 $error_message = $response->get_error_message();
4597 botwriter_log('Phase 1 request error', [
4598 'log_id' => $data['id'] ?? null,
4599 'task_id' => $data['id_task'] ?? null,
4600 'website_type' => $data['website_type'] ?? null,
4601 'error' => $error_message,
4602 ]);
4603 $data["task_status"]="error";
4604 botwriter_logs_register($data, $data["id"]);
4605 return false;
4606
4607 } else {
4608
4609 if ($response['response']['code'] === 200) {
4610
4611 $body = wp_remote_retrieve_body($response);
4612 $result = json_decode($body, true);
4613
4614 //error_log("Data received: " . print_r($body, true));
4615
4616 if (!empty($result['site_token'])) {
4617 update_option('botwriter_site_token', sanitize_text_field((string) $result['site_token']));
4618 }
4619
4620 // STOPFORMANY: server reports too many consecutive errors
4621 if (isset($result['error']) && strpos($result['error'], 'STOPFORMANY') === 0) {
4622 update_option('botwriter_stopformany', true);
4623 $data['task_status'] = 'error';
4624 $data['error'] = $result['error'];
4625 $data['intentosfase1'] = 8; // stop retries
4626 botwriter_logs_register($data, $data['id']);
4627 botwriter_log('STOPFORMANY activated from phase 1', [
4628 'log_id' => $data['id'] ?? null,
4629 'task_id' => $data['id_task'] ?? null,
4630 'error' => $result['error'],
4631 ]);
4632 return false;
4633 }
4634
4635 if (isset($result['id_task_server']) && $result['id_task_server'] !== 0) { // ok
4636 $data["id_task_server"]=$result['id_task_server'];
4637 $data["task_status"]='inqueue';
4638
4639 // Capture site_token from server response (auto-provisioning)
4640 if (!empty($result['site_token'])) {
4641 update_option('botwriter_site_token', sanitize_text_field($result['site_token']));
4642 }
4643
4644 botwriter_logs_register($data, $data["id"]);
4645 botwriter_log('Phase 1 request accepted', [
4646 'log_id' => $data['id'] ?? null,
4647 'task_id' => $data['id_task'] ?? null,
4648 'id_task_server' => $result['id_task_server'],
4649 ]);
4650 return $result['id_task_server'];
4651 } else { // error — id_task_server is 0 or missing
4652 $data["task_status"]="error";
4653 // Use full error_message (may include reset link) when available
4654 $data["error"] = !empty($result['error_message']) ? $result['error_message'] : ($result['error'] ?? '');
4655
4656 // Generic terminal flag — server says stop retrying
4657 if (!empty($result['terminal'])) {
4658 $data['intentosfase1'] = 8;
4659 }
4660 // Show server-provided admin notice
4661 if (!empty($result['error_message']) && (int)($result['error_level'] ?? 0) === 1) {
4662 botwriter_announcements_add(
4663 __('Service notice', 'botwriter'),
4664 wp_kses_post($result['error_message'])
4665 );
4666 }
4667
4668 // Handle known server errors (same logic as Phase 2)
4669 if ($data["error"] == "Maximum monthly posts limit reached") {
4670 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>");
4671 $data["intentosfase1"] = 8;
4672 }
4673 if ($data["error"] == "Payment date exceeded") {
4674 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>");
4675 $data["intentosfase1"] = 8;
4676 }
4677 if ($data["error"] == "API Key error") {
4678 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>");
4679 $data["intentosfase1"] = 8;
4680 }
4681
4682 botwriter_logs_register($data, $data["id"]);
4683 botwriter_log('Phase 1 request rejected', [
4684 'log_id' => $data['id'] ?? null,
4685 'task_id' => $data['id_task'] ?? null,
4686 'error' => $data['error'],
4687 'response_length' => isset($body) ? strlen($body) : null,
4688 ]);
4689 return false;
4690 }
4691 } else { // error
4692 $data["task_status"]="error";
4693 botwriter_logs_register($data, $data["id"]);
4694 botwriter_log('Phase 1 non-200 response', [
4695 'log_id' => $data['id'] ?? null,
4696 'task_id' => $data['id_task'] ?? null,
4697 'status_code' => $response['response']['code'],
4698 ]);
4699 return false;
4700 }
4701 }
4702
4703
4704 }
4705
4706 // Function to send data to the server pass2
4707 function botwriter_send2_data_to_server($data) {
4708 Global $wpdb;
4709
4710 $data['api_key'] = get_option('botwriter_api_key');
4711 $data["user_domainname"] = esc_url(get_site_url());
4712 $data['site_token'] = get_option('botwriter_site_token', '');
4713
4714 $remote_url = BOTWRITER_API_URL . 'redis_api_finish.php';
4715
4716 $ssl_verify = get_option('botwriter_sslverify');
4717 if ($ssl_verify === 'no') {
4718 $ssl_verify = false;
4719 } else {
4720 $ssl_verify = true;
4721 }
4722
4723 botwriter_log('Dispatching phase 2 request', [
4724 'log_id' => $data['id'] ?? null,
4725 'task_id' => $data['id_task'] ?? null,
4726 'website_type' => $data['website_type'] ?? null,
4727 'id_task_server' => $data['id_task_server'] ?? null,
4728 ]);
4729
4730 $response = botwriter_post_compat_with_token_recovery($remote_url, $data, $ssl_verify, 45);
4731
4732
4733 if (is_wp_error($response)) {
4734 $error_message = $response->get_error_message();
4735 botwriter_log('Phase 2 request error', [
4736 'log_id' => $data['id'] ?? null,
4737 'task_id' => $data['id_task'] ?? null,
4738 'website_type' => $data['website_type'] ?? null,
4739 'error' => $error_message,
4740 ]);
4741 $data["error"]= "Error sending data " . $error_message;
4742 return false;
4743 } else {
4744
4745 if ($response['response']['code'] === 200) {
4746
4747 $body = wp_remote_retrieve_body($response);
4748 $result = json_decode($body, true);
4749
4750 if (!empty($result['site_token'])) {
4751 update_option('botwriter_site_token', sanitize_text_field((string) $result['site_token']));
4752 }
4753
4754 //echo 'Data recive: <pre>' . print_r($result, true) . '</pre>';
4755
4756 // results errors
4757 if (isset($result["task_status"]) && $result["task_status"] == "error") {
4758 $data["task_status"]="error";
4759 // Use full error_message (may include reset link) when available
4760 $data["error"] = !empty($result['error_message']) ? $result['error_message'] : ($result['error'] ?? '');
4761 botwriter_log('Phase 2 reported error', [
4762 'log_id' => $data['id'] ?? null,
4763 'task_id' => $data['id_task'] ?? null,
4764 'id_task_server' => $data['id_task_server'] ?? null,
4765 'error' => $data['error'],
4766 ]);
4767
4768 // Generic terminal flag — server says stop retrying
4769 if (!empty($result['terminal'])) {
4770 $data['intentosfase1'] = 8;
4771 }
4772 // Show server-provided admin notice
4773 if (!empty($result['error_message']) && (int)($result['error_level'] ?? 0) === 1) {
4774 botwriter_announcements_add(
4775 __('Service notice', 'botwriter'),
4776 wp_kses_post($result['error_message'])
4777 );
4778 }
4779
4780 if ($data["error"]=="Maximum monthly posts limit reached") {
4781 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>");
4782 $data["intentosfase1"]=8;
4783 }
4784 if ($data["error"]=="Payment date exceeded") {
4785 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>");
4786 $data["intentosfase1"]=8;
4787 }
4788 if ($data["error"]=="API Key error") {
4789 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>");
4790 $data["intentosfase1"]=8;
4791 }
4792 // STOPFORMANY: server reports too many consecutive errors
4793 if (isset($result['error']) && strpos($result['error'], 'STOPFORMANY') === 0) {
4794 update_option('botwriter_stopformany', true);
4795 $data['intentosfase1'] = 8;
4796 botwriter_log('STOPFORMANY activated from phase 2', [
4797 'log_id' => $data['id'] ?? null,
4798 'task_id' => $data['id_task'] ?? null,
4799 'error' => $result['error'],
4800 ]);
4801 }
4802
4803 botwriter_logs_register($data, $data["id"]);
4804 return false;
4805 }
4806
4807 // result completed
4808 botwriter_log('Phase 2 response payload', [
4809 'log_id' => $data['id'] ?? null,
4810 'task_id' => $data['id_task'] ?? null,
4811 'id_task_server' => $data['id_task_server'] ?? null,
4812 'task_status' => $result['task_status'] ?? null,
4813 ]);
4814 if (isset($result["task_status"]) && $result["task_status"] == "completed") {
4815 // Preserve image_attribution from server response (not stored in DB)
4816 $image_attribution = isset($result['image_attribution']) ? $result['image_attribution'] : null;
4817
4818 botwriter_logs_register($result, $data["id"]);
4819 botwriter_log('Phase 2 completed event', [
4820 'log_id' => $data['id'] ?? null,
4821 'task_id' => $data['id_task'] ?? null,
4822 'id_task_server' => $data['id_task_server'] ?? null,
4823 ]);
4824
4825 $result=botwriter_logs_get($data["id"]); // merge the result with the log
4826
4827 // Re-attach image_attribution (not persisted in logs table)
4828 if ($image_attribution) {
4829 $result['image_attribution'] = $image_attribution;
4830 }
4831
4832 // ── Guard: prevent duplicate post creation ──
4833 // If this log already has a published post, skip generation.
4834 if (!empty($result['id_post_published']) && intval($result['id_post_published']) > 0) {
4835 botwriter_log('Phase 2 skipped — post already published', [
4836 'log_id' => $data['id'],
4837 'post_id' => $result['id_post_published'],
4838 ]);
4839 return $result;
4840 }
4841
4842 if ($result["website_type"] == "super1") {
4843 botwriter_super1_log_to_bd($result,$data["id"]);
4844
4845 } else {
4846 $post_id=botwriter_generate_post($result);
4847 $result["id_post_published"]=$post_id;
4848 }
4849
4850 botwriter_logs_register($result, $data["id"]);
4851 if ($result["website_type"] == "super2") {
4852 //update tabla super poner el task_Status en completed
4853 $wpdb->update($wpdb->prefix . 'botwriter_super', ['task_status' => 'completed'], ['id_log' => $data["id"]]);
4854 }
4855
4856
4857 return $result;
4858 }
4859
4860 // other results, inqueue, pending, etc
4861 $now=current_time('timestamp');
4862 $last_execution_time = strtotime($data["last_execution_time"]);
4863 $diff = $now - $last_execution_time;
4864 if ($diff > 60 * 5) { // 5 minutes
4865 $data["task_status"]="error";
4866 $data["error"]="Error in server";
4867 botwriter_logs_register($data, $data["id"]);
4868 botwriter_log('Phase 2 timeout detected', [
4869 'log_id' => $data['id'] ?? null,
4870 'task_id' => $data['id_task'] ?? null,
4871 ]);
4872 }
4873 return false;
4874
4875
4876
4877
4878
4879
4880 } else { // error
4881 // update log
4882 $data["task_status"]="error";
4883 botwriter_logs_register($data, $data["id"]);
4884 botwriter_log('Phase 2 non-200 response', [
4885 'log_id' => $data['id'] ?? null,
4886 'task_id' => $data['id_task'] ?? null,
4887 'status_code' => $response['response']['code'],
4888 ]);
4889 return false;
4890 }
4891 }
4892
4893
4894
4895 }
4896
4897
4898
4899
4900
4901 function botwriter_cambiar_status_ajax() {
4902 // Verify user has permission
4903 if (!current_user_can('manage_options')) {
4904 wp_send_json_error(['message' => 'Permission denied']);
4905 }
4906
4907 check_ajax_referer('botwriter_cambiar_status_nonce', 'nonce');
4908
4909 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
4910 $nuevo_status = isset($_POST['status']) ? intval($_POST['status']) : 0;
4911
4912
4913
4914 if ($id > 0) {
4915 global $wpdb;
4916 $table_name = $wpdb->prefix . 'botwriter_tasks';
4917
4918 $result = $wpdb->update(
4919 $table_name,
4920 ['status' => $nuevo_status],
4921 ['id' => $id],
4922 ['%d'],
4923 ['%d']
4924 );
4925
4926 if ($result !== false) {
4927 wp_send_json_success(['message' => 'Estado actualizado correctamente']);
4928 } else {
4929 wp_send_json_error(['message' => 'Error al actualizar el estado']);
4930 }
4931 } else {
4932 wp_send_json_error(['message' => 'ID inválido']);
4933 }
4934
4935 wp_die();
4936 }
4937 add_action('wp_ajax_botwriter_cambiar_status', 'botwriter_cambiar_status_ajax');
4938
4939 /**
4940 * AJAX handler to check/preview an RSS feed from the admin UI.
4941 * Reads the feed client-side using botwriter_fetch_rss_content().
4942 */
4943 function botwriter_check_rss_ajax() {
4944 if ( ! current_user_can( 'manage_options' ) ) {
4945 wp_send_json_error( [ 'error' => 'Permission denied' ] );
4946 }
4947
4948 check_ajax_referer( 'botwriter_check_rss_nonce', 'nonce' );
4949
4950 $url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
4951 if ( empty( $url ) ) {
4952 wp_send_json_error( [ 'error' => 'RSS URL is required.' ] );
4953 }
4954
4955 $result = botwriter_fetch_rss_content( $url );
4956 if ( $result['success'] ) {
4957 wp_send_json_success( [
4958 'title' => $result['source_title'],
4959 'description' => $result['source_content'],
4960 'link' => $result['link_original'],
4961 ] );
4962 } else {
4963 wp_send_json_error( [ 'error' => $result['error'] ] );
4964 }
4965 }
4966 add_action( 'wp_ajax_botwriter_check_rss', 'botwriter_check_rss_ajax' );
4967
4968 /**
4969 * AJAX handler to fetch categories from a remote WordPress site.
4970 * Reads categories client-side using botwriter_fetch_wordpress_categories().
4971 */
4972 function botwriter_get_wordpress_categories_ajax() {
4973 if ( ! current_user_can( 'manage_options' ) ) {
4974 wp_send_json_error( [ 'error' => 'Permission denied' ] );
4975 }
4976
4977 check_ajax_referer( 'botwriter_wp_categories_nonce', 'nonce' );
4978
4979 $domain = isset( $_POST['website_domainname'] ) ? esc_url_raw( wp_unslash( $_POST['website_domainname'] ) ) : '';
4980 if ( empty( $domain ) ) {
4981 wp_send_json_error( [ 'error' => 'WordPress domain is required.' ] );
4982 }
4983
4984 $result = botwriter_fetch_wordpress_categories( $domain );
4985 if ( $result['success'] ) {
4986 wp_send_json_success( $result['categories'] );
4987 } else {
4988 wp_send_json_error( [ 'error' => $result['error'] ] );
4989 }
4990 }
4991 add_action( 'wp_ajax_botwriter_get_wp_categories', 'botwriter_get_wordpress_categories_ajax' );
4992
4993 // AJAX handler for deleting a log entry
4994 add_action('wp_ajax_botwriter_delete_log', 'botwriter_delete_log_ajax');
4995 function botwriter_delete_log_ajax() {
4996 // Verify user has permission
4997 if (!current_user_can('manage_options')) {
4998 wp_send_json_error(['message' => 'Permission denied']);
4999 }
5000
5001 // Verify nonce
5002 check_ajax_referer('botwriter_logs_delete_nonce', 'nonce');
5003
5004 $log_id = isset($_POST['log_id']) ? intval($_POST['log_id']) : 0;
5005
5006 if ($log_id > 0) {
5007 global $wpdb;
5008 $table_name = $wpdb->prefix . 'botwriter_logs';
5009
5010 $result = $wpdb->delete($table_name, array('id' => $log_id), array('%d'));
5011
5012 if ($result !== false) {
5013 wp_send_json_success(['message' => __('Log deleted successfully', 'botwriter')]);
5014 } else {
5015 wp_send_json_error(['message' => __('Error deleting log', 'botwriter')]);
5016 }
5017 } else {
5018 wp_send_json_error(['message' => __('Invalid log ID', 'botwriter')]);
5019 }
5020
5021 wp_die();
5022 }
5023
5024 // AJAX handler for bulk deleting log entries
5025 add_action('wp_ajax_botwriter_bulk_delete_logs', 'botwriter_bulk_delete_logs_ajax');
5026 function botwriter_bulk_delete_logs_ajax() {
5027 if (!current_user_can('manage_options')) {
5028 wp_send_json_error(['message' => 'Permission denied']);
5029 }
5030
5031 check_ajax_referer('botwriter_logs_delete_nonce', 'nonce');
5032
5033 $log_ids = isset($_POST['log_ids']) ? array_map('intval', $_POST['log_ids']) : array();
5034 $log_ids = array_filter($log_ids, function($id) { return $id > 0; });
5035
5036 if (empty($log_ids)) {
5037 wp_send_json_error(['message' => __('No logs selected', 'botwriter')]);
5038 }
5039
5040 global $wpdb;
5041 $table_name = $wpdb->prefix . 'botwriter_logs';
5042 $placeholders = implode(',', array_fill(0, count($log_ids), '%d'));
5043 $table_name = esc_sql($table_name);
5044 $query = $wpdb->prepare("DELETE FROM {$table_name} WHERE id IN ({$placeholders})", $log_ids);
5045 // 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.
5046 $deleted = $wpdb->query($query);
5047
5048 if ($deleted !== false) {
5049 wp_send_json_success(['message' => sprintf(
5050 /* translators: %d: number of deleted log entries. */
5051 __('%d log(s) deleted successfully', 'botwriter'),
5052 $deleted
5053 )]);
5054 } else {
5055 wp_send_json_error(['message' => __('Error deleting logs', 'botwriter')]);
5056 }
5057
5058 wp_die();
5059 }
5060
5061 // AJAX handler for getting taxonomies and terms for a post type
5062 add_action('wp_ajax_botwriter_get_taxonomies', 'botwriter_get_taxonomies_ajax');
5063 function botwriter_get_taxonomies_ajax() {
5064 // Verify user has permission
5065 if (!current_user_can('manage_options')) {
5066 wp_send_json_error(['message' => 'Permission denied']);
5067 }
5068
5069 // Verify nonce
5070 check_ajax_referer('botwriter_taxonomies_nonce', 'nonce');
5071
5072 $post_type = isset($_POST['post_type']) ? sanitize_text_field(wp_unslash($_POST['post_type'])) : 'post';
5073
5074 // Get taxonomies for this post type
5075 $taxonomies = get_object_taxonomies($post_type, 'objects');
5076
5077 $result = array();
5078 // Skip tag taxonomies since AI generates tags automatically
5079 $skip_taxonomies = array('post_tag', 'product_tag');
5080
5081 foreach ($taxonomies as $taxonomy) {
5082 // Skip non-public taxonomies
5083 if (!$taxonomy->public) {
5084 continue;
5085 }
5086
5087 // Skip tag taxonomies (AI generates tags)
5088 if (in_array($taxonomy->name, $skip_taxonomies, true)) {
5089 continue;
5090 }
5091
5092 // Get terms for this taxonomy
5093 $terms = get_terms(array(
5094 'taxonomy' => $taxonomy->name,
5095 'hide_empty' => false,
5096 'orderby' => 'name',
5097 'order' => 'ASC',
5098 ));
5099
5100 $terms_data = array();
5101 if (!is_wp_error($terms)) {
5102 foreach ($terms as $term) {
5103 $terms_data[] = array(
5104 'id' => $term->term_id,
5105 'name' => $term->name,
5106 'slug' => $term->slug,
5107 'parent' => $term->parent,
5108 );
5109 }
5110 }
5111
5112 $result[] = array(
5113 'name' => $taxonomy->name,
5114 'label' => $taxonomy->label,
5115 'hierarchical' => $taxonomy->hierarchical,
5116 'terms' => $terms_data,
5117 );
5118 }
5119
5120 wp_send_json_success($result);
5121 }
5122
5123
5124 function botwriter_attach_image_to_post($post_id, $image_url, $post_title, $translated_image_slug = '', $image_attribution = null) {
5125 if (!$image_url || !$post_id || empty(trim($image_url))) {
5126 botwriter_log('Image attachment skipped', [
5127 'post_id' => $post_id,
5128 'image_url' => $image_url ?: 'empty',
5129 'reason' => 'Invalid post ID or empty image URL'
5130 ]);
5131 return 'Invalid post ID or image URL';
5132 }
5133
5134 if ($image_url && $post_id) {
5135 //$image_data = file_get_contents($image_url);
5136 $image_data = wp_remote_retrieve_body(wp_remote_get($image_url));
5137 $upload_dir = wp_upload_dir();
5138 // Use translated image slug if available, otherwise fall back to title
5139 if (!empty($translated_image_slug)) {
5140 $base_name = sanitize_file_name($translated_image_slug);
5141 } else {
5142 $base_name = sanitize_file_name(remove_accents($post_title));
5143 }
5144 if (empty($base_name)) {
5145 $base_name = 'botwriter-post-' . $post_id;
5146 }
5147 /**
5148 * Filter the filename used for BotWriter generated images.
5149 *
5150 * @param string $base_name Base filename without extension.
5151 * @param int $post_id Post ID.
5152 * @param string $post_title Post title.
5153 */
5154 $base_name = apply_filters('botwriter_image_filename', $base_name, $post_id, $post_title);
5155 $filename = $base_name . '.jpg';
5156 $filename = wp_unique_filename($upload_dir['path'], $filename);
5157
5158 if (wp_mkdir_p($upload_dir['path'])) {
5159 $file = $upload_dir['path'] . '/' . $filename;
5160 } else {
5161 $file = $upload_dir['basedir'] . '/' . $filename;
5162 }
5163
5164 global $wp_filesystem;
5165 if ( ! function_exists( 'WP_Filesystem' ) ) {
5166 require_once ABSPATH . 'wp-admin/includes/file.php';
5167 }
5168 WP_Filesystem();
5169 if ( $wp_filesystem->put_contents( $file, $image_data, FS_CHMOD_FILE ) ) {
5170 //error_log('Imagen guardada exitosamente en: ' . $file);
5171 } else {
5172 //error_log('Error al guardar la imagen en: ' . $file);
5173 }
5174
5175 $wp_filetype = wp_check_filetype($filename, null);
5176 $attachment = [
5177 'post_mime_type' => $wp_filetype['type'],
5178 'post_title' => sanitize_file_name($filename),
5179 'post_content' => '',
5180 'post_status' => 'inherit',
5181 ];
5182
5183 $attach_id = wp_insert_attachment($attachment, $file, $post_id);
5184 require_once(ABSPATH . 'wp-admin/includes/image.php');
5185
5186 // Apply post-processing if enabled
5187 $postprocess_enabled = get_option('botwriter_image_postprocess_enabled', '0');
5188 if ($postprocess_enabled === '1') {
5189 $processed_file = botwriter_process_image($file);
5190 if ($processed_file && $processed_file !== $file) {
5191 // Update file reference if format changed
5192 $file = $processed_file;
5193 // Update attachment with new file info
5194 $wp_filetype = wp_check_filetype(basename($file), null);
5195 wp_update_post([
5196 'ID' => $attach_id,
5197 'post_mime_type' => $wp_filetype['type'],
5198 ]);
5199 update_attached_file($attach_id, $file);
5200 }
5201 }
5202
5203 $attach_data = wp_generate_attachment_metadata($attach_id, $file);
5204 wp_update_attachment_metadata($attach_id, $attach_data);
5205 set_post_thumbnail($post_id, $attach_id);
5206
5207 // Deterministic featured-image ALT: fill empty ALT from post title.
5208 if (get_option('botwriter_seo_featured_image_alt_enabled', '1') === '1') {
5209 $current_alt = trim((string) get_post_meta($attach_id, '_wp_attachment_image_alt', true));
5210 if ($current_alt === '') {
5211 $fallback_alt = sanitize_text_field($post_title);
5212 if ($fallback_alt === '') {
5213 $fallback_alt = sanitize_text_field((string) get_the_title($post_id));
5214 }
5215 if ($fallback_alt !== '') {
5216 update_post_meta($attach_id, '_wp_attachment_image_alt', $fallback_alt);
5217 }
5218 }
5219 }
5220
5221 // Apply stock photo attribution as image caption.
5222 if (!empty($image_attribution) && is_array($image_attribution)) {
5223 $attribution_mode = get_option('botwriter_stockphoto_attribution', 'caption');
5224 if ($attribution_mode !== 'disabled') {
5225 $author = sanitize_text_field($image_attribution['author'] ?? '');
5226 $source = sanitize_text_field($image_attribution['source'] ?? '');
5227
5228 // Build caption: "Photo by Author on Source"
5229 $caption = '';
5230 if ($author && $source) {
5231 $caption = sprintf(
5232 /* translators: 1: photographer name, 2: image source name */
5233 esc_html__('Photo by %1$s on %2$s', 'botwriter'),
5234 $author,
5235 $source
5236 );
5237 } elseif ($source) {
5238 $caption = sprintf(
5239 /* translators: %s: image source name. */
5240 esc_html__('Photo from %s', 'botwriter'),
5241 $source
5242 );
5243 }
5244
5245 if ($caption && $attribution_mode === 'caption') {
5246 wp_update_post(array(
5247 'ID' => $attach_id,
5248 'post_excerpt' => $caption,
5249 ));
5250 }
5251
5252 // Store full attribution data as post meta for later use
5253 update_post_meta($attach_id, '_botwriter_image_attribution', $image_attribution);
5254 }
5255 }
5256
5257
5258 }
5259 }
5260
5261 /**
5262 * Process image for optimization (resize, compress, convert format)
5263 * Uses WordPress wp_get_image_editor for maximum hosting compatibility.
5264 *
5265 * @param string $file_path Full path to the image file.
5266 * @return string|false New file path if processed, original path if no changes, false on error.
5267 */
5268 function botwriter_process_image($file_path) {
5269 if (!file_exists($file_path)) {
5270 botwriter_log('Image post-processing: File not found', ['path' => $file_path]);
5271 return false;
5272 }
5273
5274 // Get settings
5275 $output_format = get_option('botwriter_image_output_format', 'webp');
5276 $max_width = intval(get_option('botwriter_image_max_width', 1200));
5277 $compression = intval(get_option('botwriter_image_compression', 85));
5278 $max_filesize = intval(get_option('botwriter_image_max_filesize', 120)) * 1024; // Convert KB to bytes
5279
5280 // Log settings at start
5281 $original_size = filesize($file_path);
5282 botwriter_log('Image post-processing: Starting', [
5283 'file' => basename($file_path),
5284 'original_size_kb' => round($original_size / 1024, 1),
5285 'settings' => [
5286 'output_format' => $output_format,
5287 'max_width' => $max_width,
5288 'compression' => $compression,
5289 'max_filesize_kb' => $max_filesize / 1024
5290 ]
5291 ]);
5292
5293 // Get WordPress image editor
5294 $editor = wp_get_image_editor($file_path);
5295 if (is_wp_error($editor)) {
5296 botwriter_log('Image post-processing: Failed to load editor', [
5297 'path' => $file_path,
5298 'error' => $editor->get_error_message()
5299 ]);
5300 return $file_path; // Return original on error
5301 }
5302
5303 // Log which editor is being used
5304 botwriter_log('Image post-processing: Editor loaded', [
5305 'editor_class' => get_class($editor)
5306 ]);
5307
5308 $size = $editor->get_size();
5309 $modified = false;
5310
5311 botwriter_log('Image post-processing: Original dimensions', [
5312 'width' => $size['width'],
5313 'height' => $size['height']
5314 ]);
5315
5316 // Resize if wider than max width
5317 if ($max_width > 0 && $size['width'] > $max_width) {
5318 $new_height = intval($size['height'] * ($max_width / $size['width']));
5319 $result = $editor->resize($max_width, $new_height, false);
5320 if (!is_wp_error($result)) {
5321 $modified = true;
5322 botwriter_log('Image post-processing: Resized', [
5323 'from' => $size['width'] . 'x' . $size['height'],
5324 'to' => $max_width . 'x' . $new_height
5325 ]);
5326 }
5327 }
5328
5329 // Set quality
5330 $editor->set_quality($compression);
5331
5332 // Determine output file path and mime type
5333 $path_info = pathinfo($file_path);
5334 $new_extension = $path_info['extension'];
5335 $mime_type = null;
5336
5337 if ($output_format !== 'original') {
5338 switch ($output_format) {
5339 case 'webp':
5340 // Check if WebP is supported via GD or Imagick
5341 $webp_supported = function_exists('imagewebp');
5342 if (!$webp_supported && extension_loaded('imagick') && class_exists('Imagick')) {
5343 // Check Imagick WebP support dynamically to avoid static analysis errors
5344 $imagick_formats = call_user_func(['Imagick', 'queryFormats'], 'WEBP');
5345 $webp_supported = !empty($imagick_formats);
5346 }
5347 if ($webp_supported) {
5348 $new_extension = 'webp';
5349 $mime_type = 'image/webp';
5350 } else {
5351 // Fallback to JPEG if WebP not supported
5352 $new_extension = 'jpg';
5353 $mime_type = 'image/jpeg';
5354 botwriter_log('Image post-processing: WebP not supported, falling back to JPEG');
5355 }
5356 break;
5357 case 'jpeg':
5358 $new_extension = 'jpg';
5359 $mime_type = 'image/jpeg';
5360 break;
5361 case 'png':
5362 $new_extension = 'png';
5363 $mime_type = 'image/png';
5364 break;
5365 }
5366 }
5367
5368 // Build new file path
5369 $new_file_path = $path_info['dirname'] . '/' . $path_info['filename'] . '.' . $new_extension;
5370
5371 botwriter_log('Image post-processing: Format decision', [
5372 'original_extension' => $path_info['extension'],
5373 'new_extension' => $new_extension,
5374 'mime_type' => $mime_type,
5375 'new_file_path' => basename($new_file_path)
5376 ]);
5377
5378 // Save the image
5379 $save_args = [];
5380 if ($mime_type) {
5381 $save_args['mime_type'] = $mime_type;
5382 }
5383
5384 $saved = $editor->save($new_file_path, $mime_type);
5385
5386 if (is_wp_error($saved)) {
5387 botwriter_log('Image post-processing: Failed to save', [
5388 'path' => $new_file_path,
5389 'error' => $saved->get_error_message()
5390 ]);
5391 return $file_path; // Return original on error
5392 }
5393
5394 $new_file_path = $saved['path'];
5395
5396 botwriter_log('Image post-processing: Initial save complete', [
5397 'saved_file' => basename($new_file_path),
5398 'size_kb' => round(filesize($new_file_path) / 1024, 1)
5399 ]);
5400
5401 // If max filesize is set and file is too large, reduce quality iteratively
5402 if ($max_filesize > 0) {
5403 $current_size = filesize($new_file_path);
5404 $quality = $compression;
5405 $attempts = 0;
5406 $max_attempts = 5;
5407
5408 botwriter_log('Image post-processing: Checking filesize target', [
5409 'current_kb' => round($current_size / 1024, 1),
5410 'target_kb' => $max_filesize / 1024,
5411 'needs_compression' => $current_size > $max_filesize
5412 ]);
5413
5414 while ($current_size > $max_filesize && $quality > 40 && $attempts < $max_attempts) {
5415 $quality -= 10;
5416 $attempts++;
5417
5418 botwriter_log('Image post-processing: Compression attempt', [
5419 'attempt' => $attempts,
5420 'quality' => $quality
5421 ]);
5422
5423 // Reload and resave with lower quality
5424 $editor = wp_get_image_editor($new_file_path);
5425 if (!is_wp_error($editor)) {
5426 $editor->set_quality($quality);
5427 $saved = $editor->save($new_file_path, $mime_type);
5428 if (!is_wp_error($saved)) {
5429 $current_size = filesize($saved['path']);
5430 $new_file_path = $saved['path'];
5431 }
5432 }
5433 }
5434
5435 if ($attempts > 0) {
5436 botwriter_log('Image post-processing: Compressed for filesize target', [
5437 'target_kb' => $max_filesize / 1024,
5438 'final_kb' => round($current_size / 1024, 1),
5439 'final_quality' => $quality,
5440 'attempts' => $attempts
5441 ]);
5442 }
5443 }
5444
5445 // Delete original file if format changed
5446 if ($new_file_path !== $file_path && file_exists($file_path) && file_exists($new_file_path)) {
5447 wp_delete_file($file_path);
5448 botwriter_log('Image post-processing: Converted format', [
5449 'from' => $path_info['extension'],
5450 'to' => $new_extension,
5451 'new_size_kb' => round(filesize($new_file_path) / 1024, 1)
5452 ]);
5453 }
5454
5455 // Final summary log
5456 $final_size = filesize($new_file_path);
5457 botwriter_log('Image post-processing: Complete', [
5458 'original_file' => basename($file_path),
5459 'final_file' => basename($new_file_path),
5460 'original_size_kb' => round($original_size / 1024, 1),
5461 'final_size_kb' => round($final_size / 1024, 1),
5462 'size_reduction_percent' => round((1 - ($final_size / $original_size)) * 100, 1)
5463 ]);
5464
5465 return $new_file_path;
5466 }
5467
5468
5469
5470 add_action('plugins_loaded', 'botwriter_check_update');
5471 function botwriter_check_update() {
5472 // Ensure install date exists for legacy installs
5473 if (get_option('botwriter_install_date') === false) {
5474 update_option('botwriter_install_date', current_time('timestamp'));
5475 }
5476 // Use constant instead of get_plugin_data() to obtain current plugin version
5477 $plugin_version = BOTWRITER_VERSION;
5478
5479 // Get the previously installed version
5480 $version_instalada = get_option('botwriter_version');
5481
5482 if ($version_instalada != $plugin_version) {
5483 botwriter_create_table();
5484
5485 // Insert default templates if none exist (for updates from older versions)
5486 botwriter_insert_all_default_templates();
5487
5488 // Migration: reset 'custom' provider to defaults (removed in this version)
5489 if (get_option('botwriter_text_provider') === 'custom') {
5490 update_option('botwriter_text_provider', 'openai');
5491 }
5492 if (get_option('botwriter_image_provider') === 'custom') {
5493 update_option('botwriter_image_provider', 'stockphoto');
5494 }
5495 // Clean up custom provider options
5496 delete_option('botwriter_custom_text_url');
5497 delete_option('botwriter_custom_text_api_key');
5498 delete_option('botwriter_custom_text_model');
5499 delete_option('botwriter_custom_text_timeout');
5500 delete_option('botwriter_custom_image_url');
5501 delete_option('botwriter_custom_image_type');
5502 delete_option('botwriter_custom_image_model');
5503 delete_option('botwriter_custom_image_timeout');
5504
5505 // Drop direct mode table if it exists
5506 global $wpdb;
5507 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}botwriter_direct_tasks");
5508
5509 update_option('botwriter_version', $plugin_version); // Update version in database
5510 }
5511 }
5512
5513
5514
5515 // new super functions
5516 add_action('wp_ajax_botwriter_actualizar_articulo', 'botwriter_actualizar_articulo_callback');
5517
5518 function botwriter_actualizar_articulo_callback() {
5519 // Verify user has permission
5520 if (!current_user_can('manage_options')) {
5521 wp_send_json_error('Permission denied');
5522 }
5523
5524 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5525 global $wpdb;
5526 $table_name = $wpdb->prefix . 'botwriter_super';
5527
5528 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
5529 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
5530 $content = isset($_POST['content']) ? sanitize_textarea_field(wp_unslash($_POST['content'])) : '';
5531
5532 $result = $wpdb->update(
5533 $table_name,
5534 array('title' => $title, 'content' => $content),
5535 array('id' => $id)
5536 );
5537
5538 if ($result !== false) {
5539 wp_send_json_success('Artículo actualizado correctamente.');
5540 } else {
5541 wp_send_json_error('Error al actualizar el artículo.');
5542 }
5543
5544 wp_die();
5545 }
5546
5547 add_action('wp_ajax_botwriter_eliminar_articulo', 'botwriter_eliminar_articulo_callback');
5548
5549 function botwriter_eliminar_articulo_callback() {
5550 // Verify user has permission
5551 if (!current_user_can('manage_options')) {
5552 wp_send_json_error('Permission denied');
5553 }
5554
5555 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5556 global $wpdb;
5557 $table_name = $wpdb->prefix . 'botwriter_super';
5558
5559 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
5560
5561 $result = $wpdb->delete($table_name, array('id' => $id));
5562 // consultar cuantos registros con id_task=0
5563 $result = $wpdb->get_results("SELECT * FROM $table_name WHERE id_task='0'");
5564 $num_rows = count($result);
5565 if ($num_rows == 0) {
5566 // borramos en la tabla logs la tarea super1 si no hay articulos
5567 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
5568 $wpdb->delete($table_name_logs, array('website_type' => 'super1'));
5569 }
5570
5571
5572
5573 if ($result !== false) {
5574 wp_send_json_success('Artículo eliminado correctamente.');
5575 } else {
5576 wp_send_json_error('Error al eliminar el artículo.');
5577 }
5578
5579 wp_die();
5580 }
5581
5582 add_action('wp_ajax_botwriter_check_super1', 'botwriter_check_super1_callback');
5583
5584 function botwriter_check_super1_callback() {
5585 // Verify user has permission
5586 if (!current_user_can('manage_options')) {
5587 wp_send_json_error('Permission denied');
5588 }
5589
5590 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5591 $estado_super1=botwriter_super1_check_task_finish();
5592 if ($estado_super1=="completed") {
5593 $response = botwriter_super1_view_articles_html();
5594 wp_send_json_success($response);
5595 return;
5596 }
5597
5598 if ($estado_super1=="error") {
5599 // borramos la tarea super1
5600 global $wpdb;
5601 $table_name = $wpdb->prefix . 'botwriter_super';
5602 $wpdb->delete($table_name, array('id_task' => 0));
5603 wp_send_json_error("error");
5604 return;
5605 }
5606 wp_send_json_error('inqueue');
5607
5608
5609 }
5610
5611
5612 add_action('wp_ajax_botwriter_create_super1', 'botwriter_create_super1_callback');
5613
5614 function botwriter_create_super1_callback() {
5615 // Verify user has permission
5616 if (!current_user_can('manage_options')) {
5617 wp_send_json_error('Permission denied');
5618 }
5619
5620 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
5621 if (!isset($_POST['prompt']) || !isset($_POST['numarticles'])) {
5622 botwriter_log('Super1 creation request missing parameters');
5623 wp_send_json_error('Missing required parameters.');
5624 wp_die();
5625 }
5626
5627 $prompt = sanitize_text_field(wp_unslash($_POST['prompt']));
5628 $numarticles = intval(wp_unslash($_POST['numarticles']));
5629 if ($prompt=="Custom") {
5630 $category_id = isset($_POST['category_id']) ? sanitize_text_field(wp_unslash($_POST['category_id'])) : '0';
5631 }
5632
5633 botwriter_log('Super1 creation request received', [
5634 'prompt' => $prompt,
5635 'num_articles' => $numarticles,
5636 'category_id' => isset($category_id) ? $category_id : null,
5637 ]);
5638
5639 $title_prompt = $prompt;
5640
5641 if ($prompt=="Custom") {
5642 $content_prompt = isset($_POST['custom_prompt']) ? sanitize_text_field(wp_unslash($_POST['custom_prompt'])) : '';
5643 } else {
5644 $info_blog = botwriter_get_info_blog();
5645 $json_info_blog = json_encode($info_blog, JSON_PRETTY_PRINT);
5646 $content_prompt = $json_info_blog;
5647 }
5648 $task_name = "Super1 Task " . current_time('Y-m-d H:i:s');
5649 $log_id = botwriter_super1_create_task($task_name, $title_prompt,$content_prompt, $numarticles, $category_id);
5650
5651 botwriter_log('Super1 task queued', [
5652 'log_id' => $log_id,
5653 'title_prompt' => $title_prompt,
5654 'num_articles' => $numarticles,
5655 ]);
5656
5657 wp_send_json_success("Task created successfully: " . $title_prompt . " " . $content_prompt . " " . $numarticles . " " . $category_id);
5658
5659
5660 }
5661
5662
5663 add_action('wp_ajax_botwriter_eliminar_super1', 'botwriter_eliminar_super1_y_logs0');
5664
5665 function botwriter_eliminar_super1_y_logs0() {
5666 // Verify user has permission
5667 if (!current_user_can('manage_options')) {
5668 wp_send_json_error('Permission denied');
5669 }
5670
5671 check_ajax_referer('botwriter_super_nonce');
5672 global $wpdb;
5673 $table_name = $wpdb->prefix . 'botwriter_super';
5674 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
5675
5676 $result = $wpdb->delete($table_name, array('id_task' => 0));
5677 $result = $wpdb->delete($table_name_logs, array('website_type' => 'super1'));
5678
5679 if ($result !== false) {
5680 wp_send_json_success('Super1 task deleted successfully.');
5681 } else {
5682 wp_send_json_error('Error');
5683 }
5684 wp_die();
5685 }
5686
5687 add_action('wp_ajax_botwriter_create_super1_manual', 'botwriter_create_super1_manual_callback');
5688
5689 function botwriter_create_super1_manual_callback() {
5690 // Verify user has permission
5691 if (!current_user_can('manage_options')) {
5692 wp_send_json_error('Permission denied');
5693 }
5694
5695 check_ajax_referer('botwriter_super_nonce');
5696
5697 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.
5698 wp_send_json_error('No titles provided.');
5699 wp_die();
5700 }
5701
5702 $raw_titles = sanitize_textarea_field(wp_unslash($_POST['manual_titles']));
5703 $global_prompt = isset($_POST['global_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['global_prompt'])) : '';
5704 $category_id = isset($_POST['category_id']) ? intval(wp_unslash($_POST['category_id'])) : 0;
5705
5706 // Split titles by newline and filter empty lines
5707 $titles = array_filter(array_map('trim', explode("\n", $raw_titles)), function($t) {
5708 return !empty($t);
5709 });
5710
5711 if (empty($titles)) {
5712 wp_send_json_error('No valid titles found.');
5713 wp_die();
5714 }
5715
5716 // Limit to 100 titles max
5717 if (count($titles) > 100) {
5718 $titles = array_slice($titles, 0, 100);
5719 }
5720
5721 botwriter_log('Manual Super1 creation request', [
5722 'num_titles' => count($titles),
5723 'category_id' => $category_id,
5724 'has_global_prompt' => !empty($global_prompt),
5725 ]);
5726
5727 global $wpdb;
5728
5729 // 1. Create a completed super1 log entry (bypassing AI generation)
5730 $log_data = array(
5731 'task_name' => 'Manual Super1 Task ' . current_time('Y-m-d H:i:s'),
5732 'task_status' => 'completed',
5733 'website_type' => 'super1',
5734 'title_prompt' => 'Manual',
5735 'content_prompt' => $global_prompt,
5736 'post_count' => count($titles),
5737 'category_id' => $category_id,
5738 );
5739 $log_id = botwriter_logs_register($log_data);
5740
5741 if (!$log_id) {
5742 wp_send_json_error('Error creating log entry.');
5743 wp_die();
5744 }
5745
5746 // 2. Insert each title into wp_botwriter_super
5747 $table_name = $wpdb->prefix . 'botwriter_super';
5748 $category_name = '';
5749 if ($category_id > 0) {
5750 $category_name = get_cat_name($category_id);
5751 }
5752
5753 foreach ($titles as $title) {
5754 $data = array(
5755 'title' => sanitize_text_field($title),
5756 'content' => $global_prompt,
5757 'category_name' => $category_name,
5758 'category_id' => $category_id,
5759 'id_log' => $log_id,
5760 'id_task' => 0, // draft, will be assigned on save
5761 'task_status' => '',
5762 );
5763 $wpdb->insert($table_name, $data);
5764 }
5765
5766 botwriter_log('Manual Super1 titles inserted', [
5767 'log_id' => $log_id,
5768 'count' => count($titles),
5769 ]);
5770
5771 // 3. Return the articles HTML for immediate review
5772 $html = botwriter_super1_view_articles_html(0);
5773 wp_send_json_success($html);
5774 }
5775
5776
5777 // ========================================
5778 // CONTENT REWRITER AJAX HANDLERS
5779 // ========================================
5780 add_action('wp_ajax_botwriter_rewriter_fetch', 'botwriter_rewriter_fetch_ajax');
5781 add_action('wp_ajax_botwriter_rewriter_create_task', 'botwriter_rewriter_create_task_ajax');
5782
5783 /**
5784 * AJAX: Fetch and extract content from URLs
5785 */
5786 function botwriter_rewriter_fetch_ajax() {
5787 check_ajax_referer('botwriter_rewriter_nonce');
5788
5789 if (!current_user_can('manage_options')) {
5790 wp_send_json_error(__('Permission denied.', 'botwriter'));
5791 }
5792
5793 $urls = isset($_POST['urls']) ? array_map('esc_url_raw', (array) wp_unslash($_POST['urls'])) : array();
5794
5795 if (empty($urls)) {
5796 wp_send_json_error(__('No URLs provided.', 'botwriter'));
5797 }
5798
5799 if (count($urls) > 20) {
5800 wp_send_json_error(__('Maximum 20 URLs allowed.', 'botwriter'));
5801 }
5802
5803 $articles = array();
5804 $errors = array();
5805
5806 foreach ($urls as $url) {
5807 if (empty($url)) {
5808 continue;
5809 }
5810
5811 $result = botwriter_rewriter_extract_content($url);
5812
5813 if (is_wp_error($result)) {
5814 $errors[] = array(
5815 'url' => $url,
5816 'error' => $result->get_error_message(),
5817 );
5818 } else {
5819 $articles[] = $result;
5820 }
5821 }
5822
5823 wp_send_json_success(array(
5824 'articles' => $articles,
5825 'errors' => $errors,
5826 ));
5827 }
5828
5829 /**
5830 * AJAX: Create a Super Task with the articles to rewrite.
5831 *
5832 * Inserts the task as an active super2 task and stores each article
5833 * in the botwriter_super table. The cron loop picks them up via
5834 * botwriter_super_prepare_event() just like normal Super Tasks.
5835 */
5836 function botwriter_rewriter_create_task_ajax() {
5837 check_ajax_referer('botwriter_rewriter_nonce');
5838
5839 if (!current_user_can('manage_options')) {
5840 wp_send_json_error(__('Permission denied.', 'botwriter'));
5841 }
5842
5843 // wp_unslash only — sanitize_text_field corrupts JSON (strips tags/newlines)
5844 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON is decoded first; each title/content field is sanitized below before use.
5845 $articles_json = isset($_POST['articles']) ? wp_unslash($_POST['articles']) : '';
5846 $rewrite_prompt = isset($_POST['rewrite_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['rewrite_prompt'])) : '';
5847 $category_id = isset($_POST['category_id']) ? absint(wp_unslash($_POST['category_id'])) : 0;
5848
5849 // Task properties from Step 3 form
5850 $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : 'draft';
5851 $post_language = isset($_POST['post_language']) ? sanitize_text_field(wp_unslash($_POST['post_language'])) : substr(get_locale(), 0, 2);
5852 $author_selection = isset($_POST['author_selection']) ? sanitize_text_field(wp_unslash($_POST['author_selection'])) : strval(get_current_user_id());
5853 $post_length = isset($_POST['post_length']) ? sanitize_text_field(wp_unslash($_POST['post_length'])) : '800';
5854 $custom_post_length = isset($_POST['custom_post_length']) ? sanitize_text_field(wp_unslash($_POST['custom_post_length'])) : '';
5855 $template_id = isset($_POST['template_id']) && !empty($_POST['template_id']) ? absint(wp_unslash($_POST['template_id'])) : null;
5856 $disable_ai_images = isset($_POST['disable_ai_images']) ? absint(wp_unslash($_POST['disable_ai_images'])) : 0;
5857 $days = isset($_POST['days']) ? sanitize_text_field(wp_unslash($_POST['days'])) : 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday';
5858 $times_per_day = isset($_POST['times_per_day']) ? absint(wp_unslash($_POST['times_per_day'])) : 1;
5859 $task_name_custom = isset($_POST['task_name']) ? sanitize_text_field(wp_unslash($_POST['task_name'])) : '';
5860
5861 // Validate post_status
5862 if (!in_array($post_status, array('draft', 'publish'), true)) {
5863 $post_status = 'draft';
5864 }
5865
5866 $articles = json_decode($articles_json, true);
5867 if (!is_array($articles) || empty($articles)) {
5868 wp_send_json_error(__('No articles provided.', 'botwriter'));
5869 }
5870
5871 global $wpdb;
5872 $tasks_table = $wpdb->prefix . 'botwriter_tasks';
5873 $super_table = $wpdb->prefix . 'botwriter_super';
5874
5875 // Build default rewrite prompt if user didn't provide one
5876 if (empty($rewrite_prompt)) {
5877 $rewrite_prompt = 'Rewrite this article completely in your own words while preserving all key information. Make it original, engaging, and well-structured.';
5878 }
5879
5880 // Count valid articles (filter empties before inserting)
5881 $valid_articles = array();
5882 foreach ($articles as $article) {
5883 $title = sanitize_text_field($article['title'] ?? '');
5884 $content = wp_kses_post($article['content'] ?? '');
5885 if (!empty($title) || !empty($content)) {
5886 $valid_articles[] = array('title' => $title, 'content' => $content);
5887 }
5888 }
5889
5890 if (empty($valid_articles)) {
5891 wp_send_json_error(__('All articles are empty.', 'botwriter'));
5892 }
5893
5894 $task_name = !empty($task_name_custom) ? $task_name_custom : 'Rewriter: ' . count($valid_articles) . ' articles - ' . wp_date('M j, Y H:i');
5895
5896 // Use the days and times_per_day from the form
5897 $wpdb->insert($tasks_table, array(
5898 'task_name' => $task_name,
5899 'post_status' => $post_status,
5900 'writer' => 'ai_cerebro',
5901 'narration' => 'Descriptive',
5902 'custom_style' => '',
5903 'post_language' => $post_language,
5904 'post_length' => $post_length,
5905 'custom_post_length' => $custom_post_length,
5906 'days' => $days,
5907 'times_per_day' => $times_per_day > 0 ? $times_per_day : 1,
5908 'status' => 1,
5909 'website_type' => 'super2',
5910 'task_type' => 'rewriter',
5911 'domain_name' => get_site_url(),
5912 'category_id' => $category_id > 0 ? strval($category_id) : '',
5913 'title_prompt' => '',
5914 'content_prompt' => $rewrite_prompt,
5915 'tags_prompt' => '',
5916 'image_prompt' => '',
5917 'aigenerated_title' => '',
5918 'aigenerated_content' => '',
5919 'aigenerated_tags' => '',
5920 'aigenerated_image' => '',
5921 'ai_keywords' => '',
5922 'author_selection' => $author_selection,
5923 'disable_ai_images' => $disable_ai_images,
5924 'template_id' => $template_id,
5925 ));
5926
5927 $task_id = $wpdb->insert_id;
5928
5929 if (!$task_id) {
5930 wp_send_json_error(__('Failed to create task.', 'botwriter'));
5931 }
5932
5933 // Insert each article into the super table.
5934 // Only the article content goes here — rewrite instructions stay in the task's content_prompt.
5935 // super_prepare_event() preserves the rewrite_prompt, and
5936 // botwriter_build_client_prompt() outputs it BEFORE the ENDARTICLE-wrapped content.
5937 $inserted = 0;
5938 foreach ($valid_articles as $art) {
5939 $wpdb->insert($super_table, array(
5940 'id_task' => $task_id,
5941 'id_log' => 0,
5942 'title' => $art['title'],
5943 'content' => $art['content'],
5944 ));
5945 // task_status left NULL — super_prepare_event picks rows with NULL/empty task_status
5946 $inserted++;
5947 }
5948
5949 botwriter_log('Content Rewriter: Task created', [
5950 'task_id' => $task_id,
5951 'article_count' => $inserted,
5952 ]);
5953
5954 wp_send_json_success(array(
5955 'task_id' => $task_id,
5956 'count' => $inserted,
5957 'edit_url' => wp_nonce_url(admin_url('admin.php?page=botwriter_super_page&id=' . $task_id), 'botwriter_tasks_action'),
5958 ));
5959 }
5960
5961 // ========================================
5962 // SITE REWRITER AJAX HANDLERS
5963 // ========================================
5964 add_action('wp_ajax_botwriter_siterewriter_crawl', 'botwriter_siterewriter_crawl_ajax');
5965 add_action('wp_ajax_botwriter_siterewriter_fetch', 'botwriter_siterewriter_fetch_ajax');
5966 add_action('wp_ajax_botwriter_siterewriter_create_task', 'botwriter_siterewriter_create_task_ajax');
5967
5968 /**
5969 * AJAX: Crawl a single page — returns title + internal links.
5970 * The JS manages the BFS queue for live/progressive UI updates.
5971 */
5972 function botwriter_siterewriter_crawl_ajax() {
5973 check_ajax_referer('botwriter_siterewriter_nonce');
5974
5975 if (!current_user_can('manage_options')) {
5976 wp_send_json_error(__('Permission denied.', 'botwriter'));
5977 }
5978
5979 $url = isset($_POST['url']) ? esc_url_raw(wp_unslash($_POST['url'])) : '';
5980 $base_domain = isset($_POST['base_domain']) ? sanitize_text_field(wp_unslash($_POST['base_domain'])) : '';
5981
5982 if (empty($url) || empty($base_domain)) {
5983 wp_send_json_error(__('Missing URL or domain.', 'botwriter'));
5984 }
5985
5986 $result = botwriter_siterewriter_crawl_page($url, $base_domain);
5987
5988 if (is_wp_error($result)) {
5989 wp_send_json_error($result->get_error_message());
5990 }
5991
5992 wp_send_json_success($result);
5993 }
5994
5995 /**
5996 * AJAX: Fetch and extract content from selected URLs.
5997 * Reuses botwriter_rewriter_extract_content() for full content extraction.
5998 */
5999 function botwriter_siterewriter_fetch_ajax() {
6000 check_ajax_referer('botwriter_siterewriter_nonce');
6001
6002 if (!current_user_can('manage_options')) {
6003 wp_send_json_error(__('Permission denied.', 'botwriter'));
6004 }
6005
6006 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Long-running image batch operation; safe inside an admin AJAX handler.
6007 @set_time_limit(0);
6008
6009 $urls = isset($_POST['urls']) ? array_map('esc_url_raw', (array) wp_unslash($_POST['urls'])) : array();
6010
6011 if (empty($urls)) {
6012 wp_send_json_error(__('No URLs provided.', 'botwriter'));
6013 }
6014
6015 $articles = array();
6016 $errors = array();
6017
6018 foreach ($urls as $url) {
6019 if (empty($url)) continue;
6020
6021 $result = botwriter_rewriter_extract_content($url);
6022
6023 if (is_wp_error($result)) {
6024 $errors[] = array(
6025 'url' => $url,
6026 'error' => $result->get_error_message(),
6027 );
6028 } else {
6029 $articles[] = $result;
6030 }
6031 }
6032
6033 wp_send_json_success(array(
6034 'articles' => $articles,
6035 'errors' => $errors,
6036 ));
6037 }
6038
6039 /**
6040 * AJAX: Create a Super Task with the articles to rewrite.
6041 * Identical flow to Content Rewriter but with task_type = 'siterewriter'.
6042 */
6043 function botwriter_siterewriter_create_task_ajax() {
6044 check_ajax_referer('botwriter_siterewriter_nonce');
6045
6046 if (!current_user_can('manage_options')) {
6047 wp_send_json_error(__('Permission denied.', 'botwriter'));
6048 }
6049
6050 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON is decoded first; each title/content field is sanitized below before use.
6051 $articles_json = isset($_POST['articles']) ? wp_unslash($_POST['articles']) : '';
6052 $rewrite_prompt = isset($_POST['rewrite_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['rewrite_prompt'])) : '';
6053 $category_id = isset($_POST['category_id']) ? absint(wp_unslash($_POST['category_id'])) : 0;
6054
6055 $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : 'draft';
6056 $post_language = isset($_POST['post_language']) ? sanitize_text_field(wp_unslash($_POST['post_language'])) : substr(get_locale(), 0, 2);
6057 $author_selection = isset($_POST['author_selection']) ? sanitize_text_field(wp_unslash($_POST['author_selection'])) : strval(get_current_user_id());
6058 $post_length = isset($_POST['post_length']) ? sanitize_text_field(wp_unslash($_POST['post_length'])) : '800';
6059 $custom_post_length = isset($_POST['custom_post_length'])? sanitize_text_field(wp_unslash($_POST['custom_post_length'])): '';
6060 $template_id = isset($_POST['template_id']) && !empty($_POST['template_id']) ? absint(wp_unslash($_POST['template_id'])) : null;
6061 $disable_ai_images = isset($_POST['disable_ai_images']) ? absint(wp_unslash($_POST['disable_ai_images'])) : 0;
6062 $days = isset($_POST['days']) ? sanitize_text_field(wp_unslash($_POST['days'])) : 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday';
6063 $times_per_day = isset($_POST['times_per_day']) ? absint(wp_unslash($_POST['times_per_day'])) : 1;
6064 $task_name_custom = isset($_POST['task_name']) ? sanitize_text_field(wp_unslash($_POST['task_name'])) : '';
6065
6066 if (!in_array($post_status, array('draft', 'publish'), true)) {
6067 $post_status = 'draft';
6068 }
6069
6070 $articles = json_decode($articles_json, true);
6071 if (!is_array($articles) || empty($articles)) {
6072 wp_send_json_error(__('No articles provided.', 'botwriter'));
6073 }
6074
6075 global $wpdb;
6076 $tasks_table = $wpdb->prefix . 'botwriter_tasks';
6077 $super_table = $wpdb->prefix . 'botwriter_super';
6078
6079 if (empty($rewrite_prompt)) {
6080 $rewrite_prompt = 'Rewrite this article completely in your own words while preserving all key information. Make it original, engaging, and well-structured.';
6081 }
6082
6083 $valid_articles = array();
6084 foreach ($articles as $article) {
6085 $title = sanitize_text_field($article['title'] ?? '');
6086 $content = wp_kses_post($article['content'] ?? '');
6087 if (!empty($title) || !empty($content)) {
6088 $valid_articles[] = array('title' => $title, 'content' => $content);
6089 }
6090 }
6091
6092 if (empty($valid_articles)) {
6093 wp_send_json_error(__('All articles are empty.', 'botwriter'));
6094 }
6095
6096 $task_name = !empty($task_name_custom)
6097 ? $task_name_custom
6098 : 'Site Rewriter: ' . count($valid_articles) . ' articles - ' . wp_date('M j, Y H:i');
6099
6100 $wpdb->insert($tasks_table, array(
6101 'task_name' => $task_name,
6102 'post_status' => $post_status,
6103 'writer' => 'ai_cerebro',
6104 'narration' => 'Descriptive',
6105 'custom_style' => '',
6106 'post_language' => $post_language,
6107 'post_length' => $post_length,
6108 'custom_post_length' => $custom_post_length,
6109 'days' => $days,
6110 'times_per_day' => $times_per_day > 0 ? $times_per_day : 1,
6111 'status' => 1,
6112 'website_type' => 'super2',
6113 'task_type' => 'siterewriter',
6114 'domain_name' => get_site_url(),
6115 'category_id' => $category_id > 0 ? strval($category_id) : '',
6116 'title_prompt' => '',
6117 'content_prompt' => $rewrite_prompt,
6118 'tags_prompt' => '',
6119 'image_prompt' => '',
6120 'aigenerated_title' => '',
6121 'aigenerated_content' => '',
6122 'aigenerated_tags' => '',
6123 'aigenerated_image' => '',
6124 'ai_keywords' => '',
6125 'author_selection' => $author_selection,
6126 'disable_ai_images' => $disable_ai_images,
6127 'template_id' => $template_id,
6128 ));
6129
6130 $task_id = $wpdb->insert_id;
6131
6132 if (!$task_id) {
6133 wp_send_json_error(__('Failed to create task.', 'botwriter'));
6134 }
6135
6136 // Insert each article into the super table.
6137 // Only the article content goes here — rewrite instructions stay in the task's content_prompt.
6138 // super_prepare_event() preserves the rewrite_prompt, and
6139 // botwriter_build_client_prompt() outputs it BEFORE the ENDARTICLE-wrapped content.
6140 $inserted = 0;
6141 foreach ($valid_articles as $art) {
6142 $wpdb->insert($super_table, array(
6143 'id_task' => $task_id,
6144 'id_log' => 0,
6145 'title' => $art['title'],
6146 'content' => $art['content'],
6147 ));
6148 $inserted++;
6149 }
6150
6151 botwriter_log('Site Rewriter: Task created', [
6152 'task_id' => $task_id,
6153 'article_count' => $inserted,
6154 ]);
6155
6156 wp_send_json_success(array(
6157 'task_id' => $task_id,
6158 'count' => $inserted,
6159 'edit_url' => wp_nonce_url(admin_url('admin.php?page=botwriter_super_page&id=' . $task_id), 'botwriter_tasks_action'),
6160 ));
6161 }
6162
6163 ?>
6164