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

4,832 lines 199.2 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 & 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.2.8
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.2.8');
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
45
46 if (!function_exists('botwriter_log')) {
47 function botwriter_log($message, array $context = []) {
48 $botwriter_debug = defined('BOTWRITER_DEBUG') && BOTWRITER_DEBUG === true;
49 $wp_debug_log = defined('WP_DEBUG') && WP_DEBUG === true && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG;
50 if (!$botwriter_debug && !$wp_debug_log) {
51 return;
52 }
53
54 if (!is_string($message)) {
55 $encoded = wp_json_encode($message);
56 if ($encoded !== false) {
57 $message = $encoded;
58 } else {
59 // Fallback to safe string/serialization without using print_r
60 if (is_scalar($message)) {
61 $message = (string) $message;
62 } else {
63 $message = maybe_serialize($message);
64 }
65 }
66 }
67
68 if (!empty($context)) {
69 $encoded_context = wp_json_encode($context);
70 if ($encoded_context !== false) {
71 $message .= ' ' . $encoded_context;
72 }
73 }
74
75 error_log('[BotWriter] ' . $message);
76 }
77 }
78
79
80 /**
81 * Add plugin action links (Settings link next to Deactivate)
82 */
83 add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'botwriter_plugin_action_links');
84 function botwriter_plugin_action_links($links) {
85 $settings_link = '<a href="' . admin_url('admin.php?page=botwriter_settings') . '">' . __('Settings', 'botwriter') . '</a>';
86 array_unshift($links, $settings_link);
87 return $links;
88 }
89
90 /**
91 * Add plugin row meta links (Website, FAQ, Support below plugin description)
92 */
93 add_filter('plugin_row_meta', 'botwriter_plugin_row_meta', 10, 2);
94 function botwriter_plugin_row_meta($links, $file) {
95 if (plugin_basename(__FILE__) === $file) {
96 $row_meta = array(
97 'website' => '<a href="https://www.wpbotwriter.com" target="_blank" rel="noopener">' . __('Website', 'botwriter') . '</a>',
98 'faq' => '<a href="https://www.wpbotwriter.com/faq.html" target="_blank" rel="noopener">' . __('FAQ', 'botwriter') . '</a>',
99 'support' => '<a href="https://wordpress.org/support/plugin/botwriter/" target="_blank" rel="noopener">' . __('Support', 'botwriter') . '</a>',
100 );
101 return array_merge($links, $row_meta);
102 }
103 return $links;
104 }
105
106
107 require plugin_dir_path( __FILE__ ) . 'includes/posts.php';
108 require plugin_dir_path( __FILE__ ) . 'includes/functions.php';
109 require plugin_dir_path( __FILE__ ) . 'includes/settings.php';
110 require plugin_dir_path( __FILE__ ) . 'includes/logs.php';
111 require plugin_dir_path( __FILE__ ) . 'includes/announcements.php';
112 require plugin_dir_path( __FILE__ ) . 'includes/super.php';
113 require plugin_dir_path( __FILE__ ) . 'includes/addnew.php';
114 require plugin_dir_path( __FILE__ ) . 'includes/quickpost.php';
115 require plugin_dir_path( __FILE__ ) . 'includes/rewriter.php';
116 require plugin_dir_path( __FILE__ ) . 'includes/siterewriter.php';
117 require plugin_dir_path( __FILE__ ) . 'includes/templates.php';
118 require plugin_dir_path( __FILE__ ) . 'includes/default-templates.php';
119
120 // WooCommerce AI Content Optimizer (loads only when WooCommerce is active)
121 require plugin_dir_path( __FILE__ ) . 'includes/woocommerce-ai/class-bw-woo-ai.php';
122 add_action( 'plugins_loaded', function () {
123 $bw_woo_ai = new BW_Woo_AI();
124 $bw_woo_ai->init();
125 } );
126
127
128 // Enqueque JS Files
129 function botwriter_enqueue_scripts() {
130 $my_plugin_dir = plugin_dir_url(__FILE__);
131 $screen = get_current_screen();
132 $slug = $screen->id;
133
134
135
136 wp_register_script( 'bootstrapjs',$my_plugin_dir.'/assets/js/bootstrap.min.js' , array('jquery'), false, true );
137 wp_enqueue_script( 'bootstrapjs' );
138
139
140 wp_register_script( 'botwriter_bootstrap_bundle',$my_plugin_dir.'/assets/js/bootstrap.bundle.min.js' , array('jquery'), false, true );
141 wp_enqueue_script( 'botwriter_bootstrap_bundle' );
142
143
144 wp_register_script( 'botwriter_botwriter',$my_plugin_dir.'/assets/js/botwriter.js' , array('jquery'), false, true );
145 wp_enqueue_script( 'botwriter_botwriter' );
146 wp_localize_script('botwriter_botwriter', 'botwriter_ajax', array(
147 'ajax_url' => admin_url('admin-ajax.php'),
148 'nonce' => wp_create_nonce('botwriter_super_nonce'),
149 'rss_nonce' => wp_create_nonce('botwriter_check_rss_nonce'),
150 'wp_categories_nonce' => wp_create_nonce('botwriter_wp_categories_nonce'),
151 ));
152
153 wp_enqueue_script('botwriter-admin-ajax-status', $my_plugin_dir.'/assets/js/admin-ajax-status.js', ['jquery'], null, true);
154 wp_localize_script('botwriter-admin-ajax-status', 'botwriter_ajax_object', [
155 'ajax_url' => admin_url('admin-ajax.php'),
156 'nonce' => wp_create_nonce('botwriter_cambiar_status_nonce')
157 ]);
158
159
160 wp_enqueue_script('botwriter-dismiss-script', $my_plugin_dir . '/assets/js/botwriter_dismiss.js', array('jquery'), null, true);
161 wp_localize_script('botwriter-dismiss-script','botwriterData',
162 array(
163 'nonce' => wp_create_nonce('botwriter_dismiss_nonce'),
164 'ajaxurl' => admin_url('admin-ajax.php')
165 )
166 );
167
168
169 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') {
170 wp_register_script('botwriter_automatic_posts', $my_plugin_dir . 'assets/js/posts.js', array('jquery'), false, true);
171 wp_enqueue_script('botwriter_automatic_posts');
172 wp_localize_script('botwriter_automatic_posts', 'botwriter_posts_ajax', array(
173 'ajax_url' => admin_url('admin-ajax.php'),
174 'taxonomies_nonce' => wp_create_nonce('botwriter_taxonomies_nonce'),
175 ));
176 }
177
178
179 if ($slug==="botwriter_page_botwriter_logs") {
180 wp_register_script('botwriter_logs', $my_plugin_dir . 'assets/js/logs.js', array('jquery'), false, true);
181 wp_enqueue_script('botwriter_logs');
182 wp_localize_script('botwriter_logs', 'botwriter_logs_vars', array(
183 'ajax_url' => admin_url('admin-ajax.php'),
184 'nonce' => wp_create_nonce('botwriter_logs_delete_nonce'),
185 'confirm_delete' => __('Are you sure you want to delete this log entry? This action cannot be undone.', 'botwriter'),
186 'confirm_bulk_delete' => __('Are you sure you want to delete the selected log entries? This action cannot be undone.', 'botwriter'),
187 'error_delete' => __('Error deleting log. Please try again.', 'botwriter'),
188 ));
189 }
190
191
192
193 if ($slug === 'botwriter_page_botwriter_super_page') {
194 wp_register_script('botwriter_super', $my_plugin_dir . 'assets/js/super.js', array('jquery'), false, true);
195 wp_enqueue_script('botwriter_super');
196 wp_localize_script('botwriter_super', 'botwriter_super_ajax', array(
197 'ajax_url' => admin_url('admin-ajax.php'),
198 'nonce' => wp_create_nonce('botwriter_super_nonce')
199 ));
200 }
201
202 if ($slug === 'botwriter_page_botwriter_rewriter_page') {
203 wp_register_script('botwriter_rewriter', $my_plugin_dir . 'assets/js/rewriter.js', array('jquery'), false, true);
204 wp_enqueue_script('botwriter_rewriter');
205 wp_localize_script('botwriter_rewriter', 'botwriter_rewriter_ajax', array(
206 'ajax_url' => admin_url('admin-ajax.php'),
207 'nonce' => wp_create_nonce('botwriter_rewriter_nonce'),
208 'logs_url' => admin_url('admin.php?page=botwriter_logs'),
209 ));
210 }
211
212 if ($slug === 'botwriter_page_botwriter_siterewriter_page') {
213 wp_register_script('botwriter_siterewriter', $my_plugin_dir . 'assets/js/siterewriter.js', array('jquery'), false, true);
214 wp_enqueue_script('botwriter_siterewriter');
215 wp_localize_script('botwriter_siterewriter', 'botwriter_siterewriter_ajax', array(
216 'ajax_url' => admin_url('admin-ajax.php'),
217 'nonce' => wp_create_nonce('botwriter_siterewriter_nonce'),
218 'logs_url' => admin_url('admin.php?page=botwriter_logs'),
219 ));
220 }
221
222 if ($slug === 'botwriter_page_botwriter_settings') {
223 wp_register_script('botwriter_settings', $my_plugin_dir . 'assets/js/botwriter-settings.js', array('jquery'), false, true);
224 wp_enqueue_script('botwriter_settings');
225 wp_localize_script('botwriter_settings', 'botwriter_settings', array(
226 'ajax_url' => admin_url('admin-ajax.php'),
227 'nonce' => wp_create_nonce('botwriter_settings_nonce'),
228 'i18n' => array(
229 'saving' => __('Saving...', 'botwriter'),
230 'saved' => __('Saved', 'botwriter'),
231 'error' => __('Error', 'botwriter'),
232 'connection_error' => __('Connection error', 'botwriter'),
233 'connection_failed' => __('Connection failed', 'botwriter'),
234 'hide' => __('Hide', 'botwriter'),
235 'show' => __('Show', 'botwriter'),
236 'active' => __('Active', 'botwriter'),
237 'enter_api_key' => __('Please enter an API key first.', 'botwriter'),
238 'enter_api_url' => __('Please enter an API URL', 'botwriter'),
239 'testing' => __('Testing...', 'botwriter'),
240 'models_found' => __('Models found:', 'botwriter'),
241 'configure_openai_key' => __('Configure OpenAI API key in Text AI tab first.', 'botwriter'),
242 'confirm_reset_models' => __('Are you sure you want to reset all model lists to factory defaults?', 'botwriter'),
243 )
244 ));
245 }
246
247 if ($slug === 'botwriter_page_botwriter_write_now') {
248 wp_register_script('botwriter_quickpost', $my_plugin_dir . 'assets/js/quickpost.js', array('jquery'), false, true);
249 wp_enqueue_script('botwriter_quickpost');
250 wp_localize_script('botwriter_quickpost', 'botwriter_quickpost_ajax', array(
251 'ajax_url' => admin_url('admin-ajax.php'),
252 'nonce' => wp_create_nonce('botwriter_quickpost_nonce')
253 ));
254 }
255
256 // Regenerate featured image modal (post editor)
257 if ($screen && $screen->base === 'post' && current_user_can('manage_options')) {
258 wp_register_script('botwriter_post_image_regeneration', $my_plugin_dir . 'assets/js/post-image-regeneration.js', array('jquery'), BOTWRITER_VERSION, true);
259 wp_enqueue_script('botwriter_post_image_regeneration');
260 wp_localize_script('botwriter_post_image_regeneration', 'botwriter_post_image_regeneration', array(
261 'ajax_url' => admin_url('admin-ajax.php'),
262 'nonce' => wp_create_nonce('botwriter_regenerate_image_nonce'),
263 'i18n' => array(
264 'link_text' => __('Regenerate', 'botwriter'),
265 'modal_title' => __('BotWriter Image Regeneration', 'botwriter'),
266 'modal_subtitle' => __('Generate and preview a featured image before applying it.', 'botwriter'),
267 'provider' => __('Current provider', 'botwriter'),
268 'model' => __('Current model', 'botwriter'),
269 'prompt_label' => __('Image Prompt', 'botwriter'),
270 'cleanup_label' => __('Previous featured image', 'botwriter'),
271 'cleanup_keep' => __('Keep in media library', 'botwriter'),
272 'cleanup_delete' => __('Delete permanently (if not used elsewhere)', 'botwriter'),
273 'btn_regenerate' => __('Regenerate', 'botwriter'),
274 'btn_accept' => __('Accept', 'botwriter'),
275 'btn_close' => __('Close', 'botwriter'),
276 'loading_context'=> __('Loading data...', 'botwriter'),
277 'generating' => __('Generating image preview...', 'botwriter'),
278 'applying' => __('Applying featured image...', 'botwriter'),
279 'missing_log' => __('No saved image prompt was found for this post. Please write your prompt manually.', 'botwriter'),
280 'provider_none' => __('Image provider is currently set to "none" in settings. Select an image provider first.', 'botwriter'),
281 'empty_prompt' => __('Please enter an image prompt before regenerating.', 'botwriter'),
282 'working' => __('Regenerating image...', 'botwriter'),
283 'generic_error'=> __('Could not regenerate the image. Please try again.', 'botwriter'),
284 ),
285 ));
286 }
287
288
289
290 }
291 add_action('admin_enqueue_scripts','botwriter_enqueue_scripts');
292
293 /**
294 * Retrieve the latest BotWriter log associated with a published post.
295 *
296 * @param int $post_id Post ID.
297 * @return array|null
298 */
299 function botwriter_get_latest_log_by_post_id($post_id) {
300 global $wpdb;
301
302 $post_id = intval($post_id);
303 if ($post_id <= 0) {
304 return null;
305 }
306
307 $table_name = $wpdb->prefix . 'botwriter_logs';
308 $log = $wpdb->get_row(
309 $wpdb->prepare(
310 "SELECT * FROM {$table_name} WHERE id_post_published = %d ORDER BY id DESC LIMIT 1",
311 $post_id
312 ),
313 ARRAY_A
314 );
315
316 botwriter_log('Image prompt lookup: latest log by post', array(
317 'post_id' => $post_id,
318 'found' => is_array($log),
319 'log_id' => is_array($log) ? intval($log['id'] ?? 0) : 0,
320 'id_post_published' => is_array($log) ? intval($log['id_post_published'] ?? 0) : 0,
321 'image_prompt_len' => is_array($log) ? strlen(trim((string) ($log['image_prompt'] ?? ''))) : 0,
322 ));
323
324 return is_array($log) ? $log : null;
325 }
326
327 /**
328 * Retrieve the latest BotWriter log for a post that has a non-empty image_prompt.
329 *
330 * @param int $post_id Post ID.
331 * @return array|null
332 */
333 function botwriter_get_latest_log_with_image_prompt_by_post_id($post_id) {
334 global $wpdb;
335
336 $post_id = intval($post_id);
337 if ($post_id <= 0) {
338 return null;
339 }
340
341 $table_name = $wpdb->prefix . 'botwriter_logs';
342 $log = $wpdb->get_row(
343 $wpdb->prepare(
344 "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",
345 $post_id
346 ),
347 ARRAY_A
348 );
349
350 botwriter_log('Image prompt lookup: latest log WITH prompt by post', array(
351 'post_id' => $post_id,
352 'found' => is_array($log),
353 'log_id' => is_array($log) ? intval($log['id'] ?? 0) : 0,
354 'id_post_published' => is_array($log) ? intval($log['id_post_published'] ?? 0) : 0,
355 'image_prompt_len' => is_array($log) ? strlen(trim((string) ($log['image_prompt'] ?? ''))) : 0,
356 ));
357
358 return is_array($log) ? $log : null;
359 }
360
361 /**
362 * Resolve current image model from provider settings.
363 *
364 * @param string $provider Provider slug.
365 * @return string
366 */
367 function botwriter_get_current_image_model_by_provider($provider) {
368 $provider = sanitize_key((string) $provider);
369
370 $image_model_defaults = array(
371 'dalle' => 'gpt-image-1',
372 'gemini' => 'gemini-2.5-flash-image',
373 'fal' => 'fal-ai/flux-pro/v1.1',
374 'replicate' => 'black-forest-labs/flux-1.1-pro',
375 'stability' => 'sd3.5-large-turbo',
376 'cloudflare' => 'flux-1-schnell',
377 'stockphoto' => 'stockphoto',
378 );
379
380 if ($provider === 'gemini') {
381 return (string) get_option('botwriter_gemini_image_model', $image_model_defaults[$provider]);
382 }
383
384 return (string) get_option("botwriter_{$provider}_model", $image_model_defaults[$provider] ?? 'gpt-image-1');
385 }
386
387 /**
388 * Return current image settings used for regeneration.
389 * Uses active plugin settings at execution time.
390 *
391 * @return array
392 */
393 function botwriter_get_current_image_generation_settings() {
394 $provider = (string) get_option('botwriter_image_provider', 'dalle');
395 $model = botwriter_get_current_image_model_by_provider($provider);
396
397 return array(
398 'provider' => $provider,
399 'model' => $model,
400 'size' => (string) get_option('botwriter_ai_image_size', 'square'),
401 'quality' => (string) get_option('botwriter_ai_image_quality', 'medium'),
402 'style' => (string) get_option('botwriter_ai_image_style', 'realistic'),
403 'style_custom' => (string) get_option('botwriter_ai_image_style_custom', ''),
404 'stockphoto_preferred' => (string) get_option('botwriter_stockphoto_preferred', 'pixabay'),
405 'stockphoto_selection' => (string) get_option('botwriter_stockphoto_selection', 'random_top10'),
406 'stockphoto_attribution' => (string) get_option('botwriter_stockphoto_attribution', 'caption'),
407 );
408 }
409
410 /**
411 * Return post meta keys used to persist image prompts.
412 *
413 * @return array
414 */
415 function botwriter_get_image_prompt_meta_keys() {
416 return array(
417 'ai' => 'botwriter_image_prompt',
418 'stock' => 'botwriter_stockphoto_prompt',
419 'last' => 'botwriter_image_prompt_last',
420 'provider' => 'botwriter_image_prompt_last_provider',
421 );
422 }
423
424 /**
425 * Persist image prompt metadata on the post itself.
426 *
427 * @param int $post_id Post ID.
428 * @param string $prompt Prompt text.
429 * @param string $provider Provider used for generation.
430 * @return bool
431 */
432 function botwriter_save_post_image_prompt_meta($post_id, $prompt, $provider = '') {
433 $post_id = intval($post_id);
434 if ($post_id <= 0) {
435 botwriter_log('Image prompt meta save skipped: invalid post_id', array('post_id' => $post_id));
436 return false;
437 }
438
439 $prompt = trim(sanitize_textarea_field((string) $prompt));
440 if ($prompt === '') {
441 botwriter_log('Image prompt meta save skipped: empty prompt', array(
442 'post_id' => $post_id,
443 'provider' => $provider,
444 ));
445 return false;
446 }
447
448 $provider = sanitize_key((string) $provider);
449 $keys = botwriter_get_image_prompt_meta_keys();
450
451 update_post_meta($post_id, $keys['last'], $prompt);
452
453 if ($provider === 'stockphoto') {
454 update_post_meta($post_id, $keys['stock'], $prompt);
455 } else {
456 update_post_meta($post_id, $keys['ai'], $prompt);
457 }
458
459 if ($provider !== '') {
460 update_post_meta($post_id, $keys['provider'], $provider);
461 }
462
463 botwriter_log('Image prompt meta saved', array(
464 'post_id' => $post_id,
465 'provider' => $provider,
466 'prompt_len' => strlen($prompt),
467 'saved_ai_meta' => ($provider !== 'stockphoto'),
468 'saved_stock_meta' => ($provider === 'stockphoto'),
469 'meta_key_last' => $keys['last'],
470 ));
471
472 return true;
473 }
474
475 /**
476 * Resolve the best prompt saved on post meta for current provider context.
477 *
478 * @param int $post_id Post ID.
479 * @param string $provider Current provider.
480 * @return array
481 */
482 function botwriter_get_post_image_prompt_from_meta($post_id, $provider = '') {
483 $post_id = intval($post_id);
484 $provider = sanitize_key((string) $provider);
485 $keys = botwriter_get_image_prompt_meta_keys();
486
487 $ai_prompt = trim((string) get_post_meta($post_id, $keys['ai'], true));
488 $stock_prompt = trim((string) get_post_meta($post_id, $keys['stock'], true));
489 $last_prompt = trim((string) get_post_meta($post_id, $keys['last'], true));
490
491 botwriter_log('Image prompt lookup: post meta snapshot', array(
492 'post_id' => $post_id,
493 'provider' => $provider,
494 'ai_len' => strlen($ai_prompt),
495 'stock_len' => strlen($stock_prompt),
496 'last_len' => strlen($last_prompt),
497 'meta_ai_key' => $keys['ai'],
498 'meta_stock_key' => $keys['stock'],
499 'meta_last_key' => $keys['last'],
500 ));
501
502 if ($provider === 'stockphoto') {
503 if ($stock_prompt !== '') {
504 botwriter_log('Image prompt lookup: selected meta_stock', array('post_id' => $post_id, 'len' => strlen($stock_prompt)));
505 return array('prompt' => $stock_prompt, 'source' => 'meta_stock');
506 }
507 if ($ai_prompt !== '') {
508 botwriter_log('Image prompt lookup: selected meta_ai for stock provider', array('post_id' => $post_id, 'len' => strlen($ai_prompt)));
509 return array('prompt' => $ai_prompt, 'source' => 'meta_ai');
510 }
511 } else {
512 if ($ai_prompt !== '') {
513 botwriter_log('Image prompt lookup: selected meta_ai', array('post_id' => $post_id, 'len' => strlen($ai_prompt)));
514 return array('prompt' => $ai_prompt, 'source' => 'meta_ai');
515 }
516 }
517
518 if ($last_prompt !== '') {
519 botwriter_log('Image prompt lookup: selected meta_last', array('post_id' => $post_id, 'len' => strlen($last_prompt)));
520 return array('prompt' => $last_prompt, 'source' => 'meta_last');
521 }
522
523 if ($stock_prompt !== '') {
524 botwriter_log('Image prompt lookup: selected stock fallback', array('post_id' => $post_id, 'len' => strlen($stock_prompt)));
525 return array('prompt' => $stock_prompt, 'source' => 'meta_stock');
526 }
527
528 botwriter_log('Image prompt lookup: no prompt found in post meta', array('post_id' => $post_id));
529
530 return array('prompt' => '', 'source' => 'none');
531 }
532
533 /**
534 * Resolve the best available image prompt from generated post data.
535 *
536 * @param array $data Data used to generate/publish the post.
537 * @return string
538 */
539 function botwriter_resolve_image_prompt_from_post_data($data) {
540 if (!is_array($data)) {
541 botwriter_log('Image prompt resolve from post data: invalid payload');
542 return '';
543 }
544
545 $explicit_prompt = isset($data['image_prompt']) ? trim(sanitize_textarea_field((string) $data['image_prompt'])) : '';
546 if ($explicit_prompt !== '') {
547 botwriter_log('Image prompt resolve from post data: using explicit image_prompt', array(
548 'len' => strlen($explicit_prompt),
549 'has_image_provider' => isset($data['image_provider']),
550 ));
551 return $explicit_prompt;
552 }
553
554 // Edge/legacy fallback: when image_prompt is not returned, the title is the usual fallback basis.
555 $title_based_prompt = isset($data['aigenerated_title']) ? trim(sanitize_text_field((string) $data['aigenerated_title'])) : '';
556 if ($title_based_prompt !== '') {
557 botwriter_log('Image prompt resolve from post data: fallback to generated title', array(
558 'title_len' => strlen($title_based_prompt),
559 'has_image_prompt_key' => array_key_exists('image_prompt', $data),
560 ));
561 return $title_based_prompt;
562 }
563
564 botwriter_log('Image prompt resolve from post data: fallback to hardcoded default');
565 return 'Blog post illustration';
566 }
567
568 /**
569 * Build UI context for image regeneration modal.
570 *
571 * @param int $post_id Post ID.
572 * @return array
573 */
574 function botwriter_get_image_regeneration_context($post_id) {
575 $post_id = intval($post_id);
576 $settings = botwriter_get_current_image_generation_settings();
577 $provider = (string) ($settings['provider'] ?? '');
578
579 $prompt_context = botwriter_get_post_image_prompt_from_meta($post_id, $provider);
580 $prefill_prompt = (string) ($prompt_context['prompt'] ?? '');
581 $prompt_source = (string) ($prompt_context['source'] ?? 'none');
582
583 botwriter_log('Image regeneration context: after meta lookup', array(
584 'post_id' => $post_id,
585 'provider' => $provider,
586 'prompt_source' => $prompt_source,
587 'prompt_len' => strlen($prefill_prompt),
588 ));
589
590 $latest_log = null;
591 if ($prefill_prompt === '') {
592 // Prefer the newest log that actually contains an image prompt.
593 $latest_log = botwriter_get_latest_log_with_image_prompt_by_post_id($post_id);
594
595 // Backward-compat fallback: if no prompt-carrying log exists, inspect latest log anyway.
596 if (!is_array($latest_log)) {
597 $latest_log = botwriter_get_latest_log_by_post_id($post_id);
598 }
599
600 $prefill_prompt = is_array($latest_log) ? trim((string) ($latest_log['image_prompt'] ?? '')) : '';
601 if ($prefill_prompt !== '') {
602 $prompt_source = 'log';
603 // Legacy migration: if prompt exists in log but not in post meta, persist it now.
604 botwriter_save_post_image_prompt_meta($post_id, $prefill_prompt, $provider);
605 botwriter_log('Image regeneration context: prompt recovered from log and migrated to meta', array(
606 'post_id' => $post_id,
607 'log_id' => intval($latest_log['id'] ?? 0),
608 'prompt_len' => strlen($prefill_prompt),
609 ));
610 }
611 }
612
613 if ($prefill_prompt === '') {
614 $title_prompt = trim((string) get_the_title($post_id));
615 if ($title_prompt !== '') {
616 $prefill_prompt = $title_prompt;
617 $prompt_source = 'post_title';
618 botwriter_log('Image regeneration context: fallback to post title', array(
619 'post_id' => $post_id,
620 'title_len' => strlen($title_prompt),
621 ));
622 }
623 }
624
625 $has_meta_prompt = in_array($prompt_source, array('meta_ai', 'meta_stock', 'meta_last'), true);
626
627 botwriter_log('Image regeneration context resolved', array(
628 'post_id' => $post_id,
629 'provider' => $provider,
630 'prompt_source' => $prompt_source,
631 'prompt_len' => strlen($prefill_prompt),
632 'has_meta_prompt' => $has_meta_prompt,
633 'has_log' => is_array($latest_log),
634 'log_id' => is_array($latest_log) ? intval($latest_log['id'] ?? 0) : 0,
635 ));
636
637 return array(
638 'post_id' => $post_id,
639 'prompt' => $prefill_prompt,
640 'has_log' => is_array($latest_log),
641 'has_meta_prompt' => $has_meta_prompt,
642 'has_prompt' => ($prefill_prompt !== ''),
643 'prompt_source' => $prompt_source,
644 'provider' => $provider,
645 'model' => (string) ($settings['model'] ?? ''),
646 'provider_disabled' => ($provider === 'none'),
647 );
648 }
649
650 /**
651 * Generate an image URL using the direct backend endpoint (/images) with current settings.
652 *
653 * @param string $prompt Prompt text.
654 * @return array
655 */
656 function botwriter_generate_image_with_current_settings($prompt) {
657 $prompt = trim((string) $prompt);
658 if ($prompt === '') {
659 return array('success' => false, 'message' => __('Image prompt is required.', 'botwriter'));
660 }
661
662 $settings = botwriter_get_current_image_generation_settings();
663 $provider = (string) $settings['provider'];
664 $model = (string) $settings['model'];
665
666 if ($provider === 'none') {
667 return array('success' => false, 'message' => __('Image provider is disabled in settings.', 'botwriter'));
668 }
669
670 $style_value = (string) ($settings['style_custom'] ?: $settings['style']);
671 if ($style_value === 'realistic' || $style_value === 'none') {
672 $style_value = '';
673 }
674
675 $payload = array(
676 'prompt' => $prompt,
677 'domain' => esc_url_raw(get_site_url()),
678 'api_key' => get_option('botwriter_api_key'),
679 'site_token' => get_option('botwriter_site_token', ''),
680 // Image regenerations are UX actions and should not consume license quota.
681 'no_count' => true,
682 'provider' => $provider,
683 'model' => $model,
684 'size' => (string) $settings['size'],
685 'quality' => (string) $settings['quality'],
686 'style' => $style_value,
687 'stockphoto_preferred' => (string) $settings['stockphoto_preferred'],
688 'stockphoto_selection' => (string) $settings['stockphoto_selection'],
689 'stockphoto_attribution' => (string) $settings['stockphoto_attribution'],
690 // Forward client keys (edge endpoint overlays provider keys from this payload)
691 'openai_api_key' => botwriter_decrypt_api_key(get_option('botwriter_openai_api_key')),
692 'google_api_key' => botwriter_decrypt_api_key(get_option('botwriter_google_api_key')),
693 'fal_api_key' => botwriter_decrypt_api_key(get_option('botwriter_fal_api_key')),
694 'replicate_api_key' => botwriter_decrypt_api_key(get_option('botwriter_replicate_api_key')),
695 'stability_api_key' => botwriter_decrypt_api_key(get_option('botwriter_stability_api_key')),
696 'cloudflare_api_key' => botwriter_decrypt_api_key(get_option('botwriter_cloudflare_api_key')),
697 'cloudflare_account_id' => get_option('botwriter_cloudflare_account_id'),
698 );
699
700 $ssl_verify = get_option('botwriter_sslverify');
701 $ssl_verify = ($ssl_verify !== 'no');
702
703 $remote_url = BOTWRITER_API_URL . 'images';
704 $response = wp_remote_post($remote_url, array(
705 'method' => 'POST',
706 'headers' => array(
707 'Content-Type' => 'application/json',
708 ),
709 'body' => wp_json_encode($payload),
710 'timeout' => 120,
711 'sslverify' => $ssl_verify,
712 ));
713
714 if (is_wp_error($response)) {
715 return array('success' => false, 'message' => $response->get_error_message());
716 }
717
718 $status_code = wp_remote_retrieve_response_code($response);
719 $body_raw = wp_remote_retrieve_body($response);
720 $result = json_decode($body_raw, true);
721
722 if ($status_code !== 200 || !is_array($result) || ($result['status'] ?? '') !== 'success' || empty($result['download_url'])) {
723 $error_message = '';
724 if (is_array($result)) {
725 $error_message = (string) ($result['error'] ?? $result['message'] ?? '');
726 }
727 if ($error_message === '') {
728 $error_message = __('Image generation failed on server.', 'botwriter');
729 }
730 return array('success' => false, 'message' => $error_message);
731 }
732
733 $download_url = (string) $result['download_url'];
734 $image_url = $download_url;
735 if (strpos($download_url, 'http://') !== 0 && strpos($download_url, 'https://') !== 0) {
736 $image_url = rtrim(BOTWRITER_API_URL, '/') . '/' . ltrim($download_url, '/');
737 }
738
739 return array(
740 'success' => true,
741 'image_url' => $image_url,
742 'provider' => $provider,
743 'model' => $model,
744 );
745 }
746
747 /**
748 * AJAX: fetch context for image regeneration modal.
749 */
750 function botwriter_get_post_image_regeneration_context_ajax() {
751 if (!current_user_can('manage_options')) {
752 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
753 }
754
755 check_ajax_referer('botwriter_regenerate_image_nonce', 'nonce');
756
757 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
758 if ($post_id <= 0 || !get_post($post_id)) {
759 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
760 }
761
762 if (!current_user_can('edit_post', $post_id)) {
763 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
764 }
765
766 $context = botwriter_get_image_regeneration_context($post_id);
767 botwriter_log('AJAX context response for image regeneration', array(
768 'post_id' => $post_id,
769 'prompt_source' => $context['prompt_source'] ?? 'unknown',
770 'prompt_len' => strlen((string) ($context['prompt'] ?? '')),
771 'has_meta_prompt' => !empty($context['has_meta_prompt']),
772 'has_log' => !empty($context['has_log']),
773 ));
774 wp_send_json_success($context);
775 }
776 add_action('wp_ajax_botwriter_get_post_image_regeneration_context', 'botwriter_get_post_image_regeneration_context_ajax');
777
778 /**
779 * AJAX: generate preview image only (does not apply to post yet).
780 */
781 function botwriter_generate_post_image_preview_ajax() {
782 if (!current_user_can('manage_options')) {
783 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
784 }
785
786 check_ajax_referer('botwriter_regenerate_image_nonce', 'nonce');
787
788 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
789 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
790
791 if ($post_id <= 0 || !get_post($post_id)) {
792 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
793 }
794
795 if (!current_user_can('edit_post', $post_id)) {
796 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
797 }
798
799 $generated = botwriter_generate_image_with_current_settings($prompt);
800 if (empty($generated['success'])) {
801 wp_send_json_error(array('message' => (string) ($generated['message'] ?? __('Image generation failed.', 'botwriter'))));
802 }
803
804 wp_send_json_success(array(
805 'post_id' => $post_id,
806 'prompt' => trim((string) $prompt),
807 'image_url' => (string) $generated['image_url'],
808 'provider' => (string) $generated['provider'],
809 'model' => (string) $generated['model'],
810 ));
811 }
812 add_action('wp_ajax_botwriter_generate_post_image_preview', 'botwriter_generate_post_image_preview_ajax');
813
814 /**
815 * Check if an attachment is used as featured image by posts other than the current one.
816 *
817 * @param int $attachment_id Attachment ID.
818 * @param int $exclude_post_id Post ID to exclude.
819 * @return bool
820 */
821 function botwriter_is_attachment_featured_elsewhere($attachment_id, $exclude_post_id = 0) {
822 global $wpdb;
823
824 $attachment_id = intval($attachment_id);
825 $exclude_post_id = intval($exclude_post_id);
826
827 if ($attachment_id <= 0) {
828 return false;
829 }
830
831 $query = "SELECT COUNT(1) FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id' AND meta_value = %d";
832 $params = array($attachment_id);
833
834 if ($exclude_post_id > 0) {
835 $query .= " AND post_id <> %d";
836 $params[] = $exclude_post_id;
837 }
838
839 $count = $wpdb->get_var($wpdb->prepare($query, $params));
840 return intval($count) > 0;
841 }
842
843 /**
844 * Register a lightweight image-only log entry for future prompt prefill.
845 *
846 * @param int $post_id Post ID.
847 * @param string $prompt Prompt used.
848 * @param string $image_url Generated image URL.
849 * @param string $provider Current provider.
850 * @param string $model Current model.
851 * @param array|null $source_log Existing latest log for this post.
852 * @return int|false
853 */
854 function botwriter_register_only_image_log($post_id, $prompt, $image_url, $provider, $model, $source_log = null) {
855 $post = get_post($post_id);
856 if (!($post instanceof WP_Post)) {
857 return false;
858 }
859
860 $base = array(
861 'id_task' => 0,
862 'id_task_server' => 0,
863 'post_status' => $post->post_status ?: 'draft',
864 'task_name' => sprintf(__('Image regeneration for post #%d', 'botwriter'), intval($post_id)),
865 'task_type' => 'only_image',
866 'writer' => 'orion',
867 'narration' => 'Descriptive',
868 'custom_style' => '',
869 'post_language' => substr(get_locale(), 0, 2),
870 'post_length' => '800',
871 'link_post_original' => get_permalink($post_id),
872 'id_post_published' => intval($post_id),
873 'task_status' => 'completed',
874 'error' => '',
875 'website_name' => '',
876 'website_type' => 'ai',
877 'domain_name' => esc_url_raw(get_site_url()),
878 'post_type' => $post->post_type ?: 'post',
879 'category_id' => '',
880 'taxonomy_data' => '',
881 'website_category_id' => '',
882 'aigenerated_title' => get_the_title($post_id),
883 'aigenerated_content' => '',
884 'aigenerated_tags' => '',
885 'aigenerated_image' => $image_url,
886 'post_count' => '1',
887 'post_order' => '',
888 'title_prompt' => '',
889 'content_prompt' => '',
890 'tags_prompt' => '',
891 'image_prompt' => $prompt,
892 'image_generating_status' => 'completed',
893 'author_selection' => strval($post->post_author ?: get_current_user_id()),
894 'news_time_published' => '',
895 'news_language' => '',
896 'news_country' => '',
897 'news_keyword' => '',
898 'news_source' => '',
899 'rss_source' => '',
900 'ai_keywords' => '',
901 'disable_ai_images' => 0,
902 'template_id' => null,
903 'intentosfase1' => 0,
904 'last_execution_time' => current_time('mysql'),
905 );
906
907 // Reuse as much context as possible from latest known log.
908 if (is_array($source_log) && !empty($source_log)) {
909 $inherit_keys = array(
910 'id_task',
911 'post_status',
912 'task_name',
913 'writer',
914 'narration',
915 'custom_style',
916 'post_language',
917 'post_length',
918 'website_name',
919 'website_type',
920 'domain_name',
921 'post_type',
922 'category_id',
923 'taxonomy_data',
924 'website_category_id',
925 'title_prompt',
926 'content_prompt',
927 'tags_prompt',
928 'author_selection',
929 'ai_keywords',
930 'template_id',
931 );
932
933 foreach ($inherit_keys as $key) {
934 if (array_key_exists($key, $source_log) && $source_log[$key] !== null && $source_log[$key] !== '') {
935 $base[$key] = $source_log[$key];
936 }
937 }
938 }
939
940 // Ensure this log is identifiable as image-only and references current settings context.
941 $base['task_type'] = 'only_image';
942 $base['task_status'] = 'completed';
943 $base['id_post_published'] = intval($post_id);
944 $base['image_prompt'] = $prompt;
945 $base['aigenerated_image'] = $image_url;
946 $base['error'] = '';
947 $base['last_execution_time'] = current_time('mysql');
948 $base['task_name'] = sprintf(
949 __('Only image regeneration (%1$s / %2$s) - Post #%3$d', 'botwriter'),
950 $provider,
951 $model,
952 intval($post_id)
953 );
954
955 return botwriter_logs_register($base);
956 }
957
958 /**
959 * AJAX: apply a regenerated image URL as featured image for an existing post.
960 * If no image_url is provided, it can generate one using current settings (legacy fallback).
961 */
962 function botwriter_apply_post_regenerated_image_ajax() {
963 if (!current_user_can('manage_options')) {
964 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
965 }
966
967 check_ajax_referer('botwriter_regenerate_image_nonce', 'nonce');
968
969 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
970 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
971 $image_url = isset($_POST['image_url']) ? esc_url_raw(wp_unslash($_POST['image_url'])) : '';
972 $cleanup_policy = isset($_POST['cleanup_policy']) ? sanitize_key(wp_unslash($_POST['cleanup_policy'])) : 'keep_old';
973 $provider = isset($_POST['provider']) ? sanitize_key(wp_unslash($_POST['provider'])) : '';
974 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
975
976 if ($post_id <= 0 || !get_post($post_id)) {
977 wp_send_json_error(array('message' => __('Invalid post.', 'botwriter')));
978 }
979
980 if (!current_user_can('edit_post', $post_id)) {
981 wp_send_json_error(array('message' => __('You cannot edit this post.', 'botwriter')));
982 }
983
984 if (trim($prompt) === '') {
985 wp_send_json_error(array('message' => __('Image prompt is required.', 'botwriter')));
986 }
987
988 if (!in_array($cleanup_policy, array('keep_old', 'delete_old'), true)) {
989 $cleanup_policy = 'keep_old';
990 }
991
992 if ($image_url === '') {
993 // Legacy fallback: if called without image_url, generate directly now.
994 $generated = botwriter_generate_image_with_current_settings($prompt);
995 if (empty($generated['success'])) {
996 wp_send_json_error(array('message' => (string) ($generated['message'] ?? __('Image generation failed.', 'botwriter'))));
997 }
998 $image_url = (string) $generated['image_url'];
999 $provider = (string) $generated['provider'];
1000 $model = (string) $generated['model'];
1001 }
1002
1003 if (strpos($image_url, 'http://') !== 0 && strpos($image_url, 'https://') !== 0) {
1004 wp_send_json_error(array('message' => __('Invalid image URL.', 'botwriter')));
1005 }
1006
1007 if ($provider === '' || $model === '') {
1008 $settings = botwriter_get_current_image_generation_settings();
1009 if ($provider === '') {
1010 $provider = (string) ($settings['provider'] ?? 'dalle');
1011 }
1012 if ($model === '') {
1013 $model = (string) ($settings['model'] ?? 'gpt-image-1');
1014 }
1015 }
1016
1017 $old_thumbnail_id = get_post_thumbnail_id($post_id);
1018 $post_title = get_the_title($post_id);
1019
1020 botwriter_attach_image_to_post($post_id, $image_url, $post_title);
1021 $new_thumbnail_id = get_post_thumbnail_id($post_id);
1022
1023 if (empty($new_thumbnail_id)) {
1024 wp_send_json_error(array('message' => __('Image was generated but could not be attached as featured image.', 'botwriter')));
1025 }
1026
1027 $deleted_old = false;
1028 $delete_note = '';
1029 if ($cleanup_policy === 'delete_old' && !empty($old_thumbnail_id) && intval($old_thumbnail_id) !== intval($new_thumbnail_id)) {
1030 if (botwriter_is_attachment_featured_elsewhere(intval($old_thumbnail_id), $post_id)) {
1031 $delete_note = __('Previous featured image was not deleted because it is used by other posts.', 'botwriter');
1032 } else {
1033 $deleted_old = (bool) wp_delete_attachment(intval($old_thumbnail_id), true);
1034 if (!$deleted_old) {
1035 $delete_note = __('Previous featured image could not be deleted automatically.', 'botwriter');
1036 }
1037 }
1038 }
1039
1040 $latest_log = botwriter_get_latest_log_by_post_id($post_id);
1041 botwriter_log('Apply regenerated image: persisting prompt to log/meta', array(
1042 'post_id' => $post_id,
1043 'provider' => $provider,
1044 'model' => $model,
1045 'prompt_len' => strlen((string) $prompt),
1046 'latest_log_id' => is_array($latest_log) ? intval($latest_log['id'] ?? 0) : 0,
1047 ));
1048 $log_id = botwriter_register_only_image_log($post_id, $prompt, $image_url, $provider, $model, $latest_log);
1049 botwriter_save_post_image_prompt_meta($post_id, $prompt, $provider);
1050
1051 $thumb_src = wp_get_attachment_image_src($new_thumbnail_id, 'medium');
1052 $featured_src = is_array($thumb_src) ? $thumb_src[0] : '';
1053
1054 wp_send_json_success(array(
1055 'message' => __('Featured image regenerated successfully.', 'botwriter'),
1056 'post_id' => $post_id,
1057 'image_url' => $image_url,
1058 'featured_image_src' => $featured_src,
1059 'attachment_id' => intval($new_thumbnail_id),
1060 'provider' => $provider,
1061 'model' => $model,
1062 'deleted_old' => $deleted_old,
1063 'delete_note' => $delete_note,
1064 'log_id' => $log_id ?: 0,
1065 ));
1066 }
1067 add_action('wp_ajax_botwriter_apply_post_regenerated_image', 'botwriter_apply_post_regenerated_image_ajax');
1068 // Backward-compatible alias for previous one-step endpoint name.
1069 add_action('wp_ajax_botwriter_regenerate_post_image', 'botwriter_apply_post_regenerated_image_ajax');
1070
1071
1072
1073 if (!function_exists('deactivate_plugins')) {
1074 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1075 }
1076
1077
1078 function botwriter_enqueue_styles(){
1079 $my_plugin_dir = plugin_dir_url(__FILE__);
1080 $screen = get_current_screen();
1081
1082 $slug = $screen->id;
1083
1084 // Welcome banner CSS - load on ALL admin pages if not dismissed
1085 // (because admin_notices shows on all pages)
1086 $welcome_dismissed = get_option('botwriter_welcome_dismissed', false);
1087 if (!$welcome_dismissed) {
1088 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'));
1089 wp_enqueue_style('botwriter_welcome_banner');
1090 }
1091
1092 // Only enqueue other styles for BotWriter admin screens
1093
1094 if (strpos((string)$slug, 'botwriter') !== false) {
1095
1096 // Register and enqueue styles with dynamic versioning for better caching
1097 wp_register_style('botwriter_bootstrap', $my_plugin_dir . 'assets/css/bootstrap.min.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/bootstrap.min.css'));
1098 wp_enqueue_style('botwriter_bootstrap');
1099
1100 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'));
1101 wp_enqueue_style('botwriter_jquery_ui');
1102
1103 wp_register_style('botwriter_loader', $my_plugin_dir . 'assets/css/loader.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/loader.css'));
1104 wp_enqueue_style('botwriter_loader');
1105
1106 wp_register_style('botwriter_style', $my_plugin_dir . 'assets/css/style.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/style.css'));
1107 wp_enqueue_style('botwriter_style');
1108
1109 // Admin menu styles (hide duplicate submenus)
1110 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'));
1111 wp_enqueue_style('botwriter_admin_menu');
1112
1113 // Settings page specific styles
1114 if (strpos((string)$slug, 'botwriter_settings') !== false) {
1115 wp_register_style('botwriter_settings', $my_plugin_dir . 'assets/css/settings.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/settings.css'));
1116 wp_enqueue_style('botwriter_settings');
1117 }
1118
1119 if ($slug === 'botwriter_page_botwriter_siterewriter_page') {
1120 wp_register_style('botwriter_siterewriter', $my_plugin_dir . 'assets/css/siterewriter.css', array(), filemtime(plugin_dir_path(__FILE__) . 'assets/css/siterewriter.css'));
1121 wp_enqueue_style('botwriter_siterewriter');
1122 }
1123 }
1124 }
1125
1126 add_action('admin_enqueue_scripts', 'botwriter_enqueue_styles');
1127
1128
1129
1130
1131 // Hook to add the admin menu
1132 add_action('admin_menu', function() {
1133 add_menu_page(
1134 __('BotWriter', 'botwriter'),
1135 __('BotWriter', 'botwriter'),
1136 'manage_options',
1137 'botwriter_menu',
1138 'botwriter_admin_page',
1139 plugin_dir_url(__FILE__) . '/assets/images/icono25.png',
1140 90
1141 );
1142
1143 add_submenu_page('botwriter_menu',
1144 __('Write now', 'botwriter'),
1145 __('Write now', 'botwriter'),
1146 'manage_options',
1147 'botwriter_write_now',
1148 'botwriter_quick_post_page_handler'
1149 );
1150
1151 add_submenu_page('botwriter_menu',
1152 __('New task', 'botwriter'),
1153 __('New task', 'botwriter'),
1154 'manage_options',
1155 'botwriter_addnew_page',
1156 'botwriter_addnew_page_handler'
1157 );
1158
1159 // Register under parent to avoid deprecations (null parent). We'll hide it from the submenu below.
1160 add_submenu_page('botwriter_menu',
1161 __('Super Task AI', 'botwriter'),
1162 __('Super Task AI', 'botwriter'),
1163 'manage_options',
1164 'botwriter_super_page',
1165 'botwriter_super_page_handler'
1166 );
1167
1168 add_submenu_page('botwriter_menu',
1169 __('Tasks AI', 'botwriter'),
1170 __('Tasks AI', 'botwriter'),
1171 'manage_options',
1172 'botwriter_automatic_posts',
1173 'botwriter_automatic_posts_page'
1174 );
1175
1176 add_submenu_page('botwriter_menu',
1177 __('Content Rewriter', 'botwriter'),
1178 __('Content Rewriter', 'botwriter'),
1179 'manage_options',
1180 'botwriter_rewriter_page',
1181 'botwriter_rewriter_page_handler'
1182 );
1183
1184 add_submenu_page('botwriter_menu',
1185 __('Site Rewriter', 'botwriter'),
1186 __('Site Rewriter', 'botwriter'),
1187 'manage_options',
1188 'botwriter_siterewriter_page',
1189 'botwriter_siterewriter_page_handler'
1190 );
1191
1192 // for development
1193 /*
1194 add_submenu_page('botwriter_menu',
1195 __('Test Call', 'botwriter'),
1196 __('Test Call', 'botwriter'),
1197 'manage_options',
1198 'botwriter_prueba',
1199 'botwriter_prueba'
1200 );
1201 */
1202
1203
1204 // Register the edit/detail page under the parent, then hide it programmatically to avoid null parent deprecations
1205 $hook = add_submenu_page('botwriter_menu',
1206 __('Add New Task', 'botwriter'),
1207 __('Add New Task', 'botwriter'),
1208 'manage_options',
1209 'botwriter_automatic_post_new',
1210 'botwriter_form_page_handler'
1211 );
1212
1213 add_submenu_page('botwriter_menu',
1214 __('Settings', 'botwriter'),
1215 __('Settings', 'botwriter'),
1216 'manage_options',
1217 'botwriter_settings',
1218 'botwriter_settings_page_handler'
1219 );
1220
1221 add_submenu_page('botwriter_menu',
1222 __('Templates', 'botwriter'),
1223 __('Templates', 'botwriter'),
1224 'manage_options',
1225 'botwriter_templates',
1226 'botwriter_templates_page_handler'
1227 );
1228
1229 add_submenu_page('botwriter_menu',
1230 __('Logs', 'botwriter'),
1231 get_option('botwriter_stopformany', false)
1232 ? __('Logs', 'botwriter') . ' <span class="update-plugins count-1" style="background:#d63638;"><span class="plugin-count">!</span></span>'
1233 : __('Logs', 'botwriter'),
1234 'manage_options',
1235 'botwriter_logs',
1236 'botwriter_logs_page_handler'
1237 );
1238 });
1239
1240 // CSS for hiding duplicate submenu entries is now in assets/css/admin-menu.css
1241 // and enqueued via botwriter_enqueue_styles()
1242
1243
1244
1245
1246
1247 function botwriter_prueba() {
1248 if (!current_user_can('manage_options')) {
1249 return;
1250 }
1251
1252 ?>
1253
1254 <h1>Prueba...</h1>
1255 <div>
1256 Llamando a la funcion que ejecuta las tareas
1257 </div>
1258
1259 <?php
1260 botwriter_scheduled_events_execute_tasks();
1261
1262 }
1263
1264
1265
1266
1267
1268
1269 // First screen of the plugin
1270 function botwriter_admin_page() {
1271 if (!current_user_can('manage_options')) {
1272 return;
1273 }
1274
1275 // Check if any API key is configured
1276 $has_api_key = false;
1277 $text_providers = [
1278 'botwriter_openai_api_key',
1279 'botwriter_anthropic_api_key',
1280 'botwriter_google_api_key',
1281 'botwriter_mistral_api_key',
1282 'botwriter_groq_api_key',
1283 'botwriter_openrouter_api_key'
1284 ];
1285 foreach ($text_providers as $provider_key) {
1286 $key_value = get_option($provider_key);
1287 if (!empty($key_value) && function_exists('botwriter_decrypt_api_key')) {
1288 $decrypted = botwriter_decrypt_api_key($key_value);
1289 if (!empty($decrypted)) {
1290 $has_api_key = true;
1291 break;
1292 }
1293 }
1294 }
1295
1296 $settings_url = admin_url('admin.php?page=botwriter_settings');
1297 $addnew_url = admin_url('admin.php?page=botwriter_addnew_page');
1298 $tasks_url = admin_url('admin.php?page=botwriter_automatic_posts');
1299 $logs_url = admin_url('admin.php?page=botwriter_logs');
1300 ?>
1301 <div class="wrap">
1302 <div style="max-width: 900px; margin: 0 auto;">
1303
1304 <!-- Header -->
1305 <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);">
1306 <h1 style="margin: 0 0 10px 0; font-size: 28px; font-weight: 600;">
1307 <span style="margin-right: 10px;">🤖</span><?php echo esc_html__('BotWriter', 'botwriter'); ?>
1308 </h1>
1309 <p style="margin: 0; font-size: 16px; opacity: 0.95;">
1310 <?php echo esc_html__('AI-Powered Content Creation for WordPress', 'botwriter'); ?>
1311 </p>
1312 </div>
1313
1314 <!-- Quick Start Alert -->
1315 <?php if (!$has_api_key): ?>
1316 <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px 20px; border-radius: 0 8px 8px 0; margin-bottom: 25px;">
1317 <strong style="color: #856404;"> <?php echo esc_html__('Quick Start:', 'botwriter'); ?></strong>
1318 <span style="color: #856404;">
1319 <?php echo esc_html__('Configure your AI provider API key to get started.', 'botwriter'); ?>
1320 <a href="<?php echo esc_url($settings_url); ?>" style="color: #856404; font-weight: 600;"><?php echo esc_html__('Go to Settings →', 'botwriter'); ?></a>
1321 </span>
1322 </div>
1323 <?php endif; ?>
1324
1325 <!-- Description -->
1326 <div style="background: white; padding: 25px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
1327 <p style="font-size: 15px; line-height: 1.7; color: #444; margin: 0;">
1328 <?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'); ?>
1329 </p>
1330 </div>
1331
1332 <!-- Features Grid -->
1333 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 25px;">
1334
1335 <!-- Text AI Card -->
1336 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
1337 <div style="font-size: 24px; margin-bottom: 12px;">✍️</div>
1338 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('Multi-Provider Text AI', 'botwriter'); ?></h3>
1339 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
1340 <?php echo esc_html__('Choose from OpenAI (GPT-4o), Anthropic (Claude), Google (Gemini), Mistral, Groq, or OpenRouter. Use your own API keys.', 'botwriter'); ?>
1341 </p>
1342 </div>
1343
1344 <!-- Image AI Card -->
1345 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
1346 <div style="font-size: 24px; margin-bottom: 12px;">🎨</div>
1347 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('AI Image Generation', 'botwriter'); ?></h3>
1348 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
1349 <?php echo esc_html__('Generate featured images with DALL-E, Stable Diffusion, Flux, Recraft, and more via Replicate, Stability AI, or Fal.ai.', 'botwriter'); ?>
1350 </p>
1351 </div>
1352
1353 <!-- Content Sources Card -->
1354 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
1355 <div style="font-size: 24px; margin-bottom: 12px;">📡</div>
1356 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('Multiple Content Sources', 'botwriter'); ?></h3>
1357 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
1358 <?php echo esc_html__('Import and rewrite content from any WordPress site, RSS feed, or news API. Prevent duplicates automatically.', 'botwriter'); ?>
1359 </p>
1360 </div>
1361
1362 <!-- Automation Card -->
1363 <div style="background: white; padding: 22px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
1364 <div style="font-size: 24px; margin-bottom: 12px;">⚙️</div>
1365 <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;"><?php echo esc_html__('Full Automation', 'botwriter'); ?></h3>
1366 <p style="margin: 0; color: #666; font-size: 13px; line-height: 1.6;">
1367 <?php echo esc_html__('Schedule unlimited tasks, set publishing frequency, and let BotWriter work 24/7. Monitor everything from the Logs.', 'botwriter'); ?>
1368 </p>
1369 </div>
1370
1371 </div>
1372
1373 <!-- Getting Started Steps -->
1374 <div style="background: white; padding: 25px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
1375 <h2 style="margin: 0 0 20px 0; font-size: 18px; color: #333;">
1376 🚀 <?php echo esc_html__('Getting Started', 'botwriter'); ?>
1377 </h2>
1378
1379 <div style="display: flex; flex-direction: column; gap: 15px;">
1380
1381 <div style="display: flex; align-items: flex-start; gap: 15px;">
1382 <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;">
1383 <?php echo $has_api_key ? '' : '1'; ?>
1384 </div>
1385 <div>
1386 <strong style="color: #333;"><?php echo esc_html__('Configure your AI Provider', 'botwriter'); ?></strong>
1387 <p style="margin: 5px 0 0 0; color: #666; font-size: 13px;">
1388 <?php echo esc_html__('Add your API key from OpenAI, Anthropic (Claude), Google (Gemini), Mistral, Groq, or OpenRouter.', 'botwriter'); ?>
1389 <a href="<?php echo esc_url($settings_url); ?>"><?php echo esc_html__('Settings →', 'botwriter'); ?></a>
1390 </p>
1391 </div>
1392 </div>
1393
1394 <div style="display: flex; align-items: flex-start; gap: 15px;">
1395 <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>
1396 <div>
1397 <strong style="color: #333;"><?php echo esc_html__('Create Your First Task', 'botwriter'); ?></strong>
1398 <p style="margin: 5px 0 0 0; color: #666; font-size: 13px;">
1399 <?php echo esc_html__('Define your content source, AI prompts, categories, and publishing schedule.', 'botwriter'); ?>
1400 <a href="<?php echo esc_url($addnew_url); ?>"><?php echo esc_html__('Add New →', 'botwriter'); ?></a>
1401 </p>
1402 </div>
1403 </div>
1404
1405 <div style="display: flex; align-items: flex-start; gap: 15px;">
1406 <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>
1407 <div>
1408 <strong style="color: #333;"><?php echo esc_html__('Activate and Monitor', 'botwriter'); ?></strong>
1409 <p style="margin: 5px 0 0 0; color: #666; font-size: 13px;">
1410 <?php echo esc_html__('Enable your tasks and watch BotWriter generate posts automatically. Check the Logs for status updates.', 'botwriter'); ?>
1411 <a href="<?php echo esc_url($logs_url); ?>"><?php echo esc_html__('Logs →', 'botwriter'); ?></a>
1412 </p>
1413 </div>
1414 </div>
1415
1416 </div>
1417 </div>
1418
1419 <!-- Quick Links -->
1420 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px;">
1421 <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;">
1422 <div style="font-size: 20px; margin-bottom: 6px;">⚙️</div>
1423 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Settings', 'botwriter'); ?></div>
1424 </a>
1425 <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;">
1426 <div style="font-size: 20px; margin-bottom: 6px;"></div>
1427 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Add New', 'botwriter'); ?></div>
1428 </a>
1429 <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;">
1430 <div style="font-size: 20px; margin-bottom: 6px;">📋</div>
1431 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Tasks', 'botwriter'); ?></div>
1432 </a>
1433 <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;">
1434 <div style="font-size: 20px; margin-bottom: 6px;">📊</div>
1435 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Logs', 'botwriter'); ?></div>
1436 </a>
1437 <a href="https://wpbotwriter.com/faq-frequently-asked-questions/" 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;">
1438 <div style="font-size: 20px; margin-bottom: 6px;"></div>
1439 <div style="font-size: 13px; font-weight: 500;"><?php echo esc_html__('Help & FAQ', 'botwriter'); ?></div>
1440 </a>
1441 </div>
1442
1443 <!-- Footer -->
1444 <div style="text-align: center; margin-top: 30px; padding: 15px; color: #888; font-size: 12px;">
1445 <?php echo esc_html__('BotWriter', 'botwriter'); ?> v<?php echo esc_html(BOTWRITER_VERSION); ?> 100% Free
1446 <br>
1447 <a href="https://www.wpbotwriter.com" target="_blank" style="color: #667eea; text-decoration: none;"><?php echo esc_html__('Website', 'botwriter'); ?></a>
1448 &nbsp;&nbsp;
1449 <a href="https://www.wpbotwriter.com/faq.html" target="_blank" style="color: #667eea; text-decoration: none;">FAQ</a>
1450 &nbsp;&nbsp;
1451 <a href="https://wordpress.org/support/plugin/botwriter/" target="_blank" style="color: #667eea; text-decoration: none;"><?php echo esc_html__('Support', 'botwriter'); ?></a>
1452 </div>
1453
1454 </div>
1455 </div>
1456
1457 <?php
1458 }
1459
1460
1461
1462 // Hook that runs on plugin activation
1463 register_activation_hook(__FILE__, 'botwriter_plugin_activate');
1464 function botwriter_plugin_activate() {
1465 // Store first install date if missing
1466 if (get_option('botwriter_install_date') === false) {
1467 update_option('botwriter_install_date', current_time('timestamp'));
1468 }
1469 botwriter_activate_apikey_and_defaults();
1470 botwriter_create_table();
1471 // Create the first supertask if it doesn't exist
1472 /*
1473 if (!botwriter_super1_check_task_exist()) {
1474 botwriter_super1_create_first_task();
1475 }
1476 */
1477 }
1478
1479
1480 function botwriter_activate_apikey_and_defaults() {
1481 if (get_option('botwriter_paused_tasks') === false) {
1482 update_option('botwriter_paused_tasks', "2");
1483 }
1484
1485 if (get_option('botwriter_email') === false) {
1486 update_option('botwriter_email', get_option('admin_email'));
1487 }
1488
1489 if (get_option('botwriter_cron_active') === false) {
1490 update_option('botwriter_cron_active', '1');
1491 }
1492
1493 if (get_option('botwriter_ai_image_size') === false) {
1494 update_option('botwriter_ai_image_size', 'square');
1495 }
1496
1497 if (get_option('botwriter_sslverify') === false) {
1498 update_option('botwriter_sslverify', 'yes');
1499 }
1500
1501 if (get_option('botwriter_openai_model') === false) {
1502 update_option('botwriter_openai_model', 'gpt-5.4-mini');
1503 }
1504 if (get_option('botwriter_ai_image_quality') === false) {
1505 update_option('botwriter_ai_image_quality', 'medium');
1506 }
1507 }
1508
1509
1510 // Compatibility check for different WordPress versions
1511 add_action('plugins_loaded', 'botwriter_compatibility_check');
1512
1513 function botwriter_compatibility_check() {
1514 global $wp_version;
1515
1516 if (version_compare($wp_version, '4.0', '<')) {
1517 deactivate_plugins(plugin_basename(__FILE__));
1518
1519 wp_die(esc_html__('This plugin requires WordPress 4.0 or higher', 'botwriter'));
1520 }
1521 }
1522
1523
1524 // Ensure DB schema migrations run even for sites that didn't re-activate the plugin
1525 add_action('plugins_loaded', 'botwriter_maybe_add_task_type_col', 20);
1526 function botwriter_maybe_add_task_type_col() {
1527 global $wpdb;
1528 $tasks_table_name = $wpdb->prefix . 'botwriter_tasks';
1529 // Bail if table is missing
1530 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tasks_table_name));
1531 if ($table_exists !== $tasks_table_name) {
1532 return;
1533 }
1534 // Add task_type if missing
1535 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'task_type'));
1536 if (!$col) {
1537 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `task_type` VARCHAR(50) NULL AFTER `website_type`");
1538 }
1539 }
1540
1541
1542
1543 // funciones extra
1544 if (!class_exists('WP_List_Table')) {
1545 require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
1546 }
1547
1548
1549 function botwriter_create_table() {
1550 global $wpdb;
1551 try {
1552
1553 $tasks_table_name = $wpdb->prefix . 'botwriter_tasks';
1554 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tasks_table_name)) !== $tasks_table_name) {
1555 $charset_collate = $wpdb->get_charset_collate();
1556
1557 $tasks_sql = "CREATE TABLE $tasks_table_name (
1558 `id` int(11) NOT NULL AUTO_INCREMENT,
1559 `post_status` VARCHAR(20) NOT NULL,
1560 `task_name` VARCHAR(255) NOT NULL,
1561 `writer` VARCHAR(255) NOT NULL,
1562 `narration` VARCHAR(255),
1563 `custom_style` VARCHAR(255),
1564 `post_language` VARCHAR(255) NOT NULL,
1565 `post_length` VARCHAR(255) NOT NULL,
1566 `custom_post_length` VARCHAR(255) NOT NULL,
1567 `days` VARCHAR(255) NOT NULL,
1568 `times_per_day` INT NOT NULL,
1569 `execution_count` INT DEFAULT 0,
1570 `last_execution_date` DATE DEFAULT NULL,
1571 `last_execution_time` TIMESTAMP DEFAULT 0,
1572 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1573 `status` int DEFAULT 1,
1574 `website_name` VARCHAR(255),
1575 `website_type` VARCHAR(255),
1576 `task_type` VARCHAR(50) DEFAULT NULL,
1577 `domain_name` VARCHAR(255) NOT NULL,
1578 `post_type` VARCHAR(50) DEFAULT 'post',
1579 `category_id` VARCHAR(255),
1580 `taxonomy_data` TEXT,
1581 `website_category_id` VARCHAR(255),
1582 `website_category_name` VARCHAR(255),
1583 `aigenerated_title` TEXT NOT NULL,
1584 `aigenerated_content` TEXT NOT NULL,
1585 `aigenerated_tags` TEXT NOT NULL,
1586 `aigenerated_image` TEXT NOT NULL,
1587 `post_count` VARCHAR(255),
1588 `post_order` VARCHAR(255),
1589 `title_prompt` TEXT NOT NULL,
1590 `content_prompt` TEXT NOT NULL,
1591 `tags_prompt` TEXT NOT NULL,
1592 `image_prompt` TEXT NOT NULL,
1593 `image_generating_status` VARCHAR(255),
1594 `author_selection` VARCHAR(255),
1595 `news_time_published` VARCHAR(255),
1596 `news_language` VARCHAR(255),
1597 `news_country` VARCHAR(255),
1598 `news_keyword` VARCHAR(255),
1599 `news_source` VARCHAR(255),
1600 `rss_source` VARCHAR(255),
1601 `ai_keywords` TEXT NOT NULL,
1602 `disable_ai_images` TINYINT(1) DEFAULT 0,
1603 `template_id` INT(11) DEFAULT NULL,
1604 PRIMARY KEY (`id`)
1605 ) $charset_collate;";
1606
1607 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1608 dbDelta($tasks_sql);
1609 } else {
1610 // Ensure new column task_type exists for legacy installs
1611 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'task_type'));
1612 if (!$col) {
1613 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `task_type` VARCHAR(50) NULL AFTER `website_type`");
1614 }
1615
1616 // Ensure new column disable_ai_images exists for legacy installs
1617 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'disable_ai_images'));
1618 if (!$col) {
1619 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `disable_ai_images` TINYINT(1) DEFAULT 0");
1620 }
1621
1622 // Ensure new column template_id exists for legacy installs
1623 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'template_id'));
1624 if (!$col) {
1625 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `template_id` INT(11) DEFAULT NULL");
1626 }
1627
1628 // Ensure new column post_type exists for legacy installs
1629 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'post_type'));
1630 if (!$col) {
1631 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `post_type` VARCHAR(50) DEFAULT 'post' AFTER `domain_name`");
1632 }
1633
1634 // Ensure new column taxonomy_data exists for legacy installs
1635 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $tasks_table_name LIKE %s", 'taxonomy_data'));
1636 if (!$col) {
1637 $wpdb->query("ALTER TABLE $tasks_table_name ADD COLUMN `taxonomy_data` TEXT AFTER `category_id`");
1638 }
1639 }
1640
1641 // Table botwriter_logs
1642 $logs_table_name = $wpdb->prefix . 'botwriter_logs';
1643 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $logs_table_name)) !== $logs_table_name) {
1644 $charset_collate = $wpdb->get_charset_collate();
1645
1646 $logs_sql = "CREATE TABLE $logs_table_name (
1647 `id` int(11) NOT NULL AUTO_INCREMENT,
1648 `id_task` int(11) NOT NULL,
1649 `id_task_server` int(11) NOT NULL,
1650 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1651 `last_execution_time` TIMESTAMP DEFAULT 0,
1652 `intentosfase1` int(11) NOT NULL DEFAULT 0,
1653 `intentosfase2` int(11) NOT NULL DEFAULT 0,
1654 `task_status` VARCHAR(255),
1655 `task_type` VARCHAR(50) DEFAULT NULL,
1656 `error` TEXT,
1657 `link_post_original` TEXT,
1658 `id_post_published` int(11) default 0,
1659 `post_status` VARCHAR(20) NOT NULL,
1660 `task_name` VARCHAR(255) NOT NULL,
1661 `writer` VARCHAR(255) NOT NULL,
1662 `narration` VARCHAR(255),
1663 `custom_style` VARCHAR(255),
1664 `post_language` VARCHAR(255) NOT NULL,
1665 `post_length` VARCHAR(255) NOT NULL,
1666 `custom_post_length` VARCHAR(255) NOT NULL,
1667 `website_name` VARCHAR(255),
1668 `website_type` VARCHAR(255),
1669 `domain_name` VARCHAR(255) NOT NULL,
1670 `post_type` VARCHAR(50) DEFAULT 'post',
1671 `category_id` VARCHAR(255),
1672 `taxonomy_data` TEXT,
1673 `website_category_id` VARCHAR(255),
1674 `aigenerated_title` TEXT NOT NULL,
1675 `aigenerated_content` TEXT NOT NULL,
1676 `aigenerated_tags` TEXT NOT NULL,
1677 `aigenerated_image` TEXT NOT NULL,
1678 `post_count` VARCHAR(255),
1679 `post_order` VARCHAR(255),
1680 `title_prompt` TEXT NOT NULL,
1681 `content_prompt` TEXT NOT NULL,
1682 `tags_prompt` TEXT NOT NULL,
1683 `image_prompt` TEXT NOT NULL,
1684 `image_generating_status` VARCHAR(255),
1685 `author_selection` VARCHAR(255),
1686 `news_time_published` VARCHAR(255),
1687 `news_language` VARCHAR(255),
1688 `news_country` VARCHAR(255),
1689 `news_keyword` VARCHAR(255),
1690 `news_source` VARCHAR(255),
1691 `rss_source` VARCHAR(255),
1692 `ai_keywords` TEXT NOT NULL,
1693 `disable_ai_images` TINYINT(1) DEFAULT 0,
1694 `template_id` INT(11) DEFAULT NULL,
1695 PRIMARY KEY (`id`)
1696 ) $charset_collate;";
1697
1698 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1699 dbDelta($logs_sql);
1700 } else {
1701 // Ensure new column disable_ai_images exists in logs table for legacy installs
1702 $logs_table_name = $wpdb->prefix . 'botwriter_logs';
1703 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'disable_ai_images'));
1704 if (!$col) {
1705 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `disable_ai_images` TINYINT(1) DEFAULT 0");
1706 }
1707
1708 // Ensure new column template_id exists in logs table for legacy installs
1709 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'template_id'));
1710 if (!$col) {
1711 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `template_id` INT(11) DEFAULT NULL");
1712 }
1713
1714 // Ensure new column task_type exists in logs table for legacy installs (for writenow exclusion)
1715 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'task_type'));
1716 if (!$col) {
1717 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `task_type` VARCHAR(50) DEFAULT NULL AFTER `task_status`");
1718 }
1719
1720 // Ensure new column post_type exists in logs table for legacy installs
1721 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'post_type'));
1722 if (!$col) {
1723 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `post_type` VARCHAR(50) DEFAULT 'post' AFTER `domain_name`");
1724 }
1725
1726 // Ensure new column taxonomy_data exists in logs table for legacy installs
1727 $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $logs_table_name LIKE %s", 'taxonomy_data'));
1728 if (!$col) {
1729 $wpdb->query("ALTER TABLE $logs_table_name ADD COLUMN `taxonomy_data` TEXT AFTER `category_id`");
1730 }
1731 }
1732
1733
1734
1735 $tasks_table_name = $wpdb->prefix . 'botwriter_super';
1736 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tasks_table_name)) !== $tasks_table_name) {
1737 $charset_collate = $wpdb->get_charset_collate();
1738
1739 $tasks_sql = "CREATE TABLE $tasks_table_name (
1740 `id` int(11) NOT NULL AUTO_INCREMENT,
1741 `id_task` int(11) NOT NULL,
1742 `id_log` int(11) NOT NULL,
1743 `title` VARCHAR(255) NOT NULL,
1744 `content` TEXT NOT NULL,
1745 `category_id` VARCHAR(255),
1746 `category_name` VARCHAR(255),
1747 `task_status` VARCHAR(255),
1748 PRIMARY KEY (`id`)
1749 ) $charset_collate;";
1750
1751 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1752 dbDelta($tasks_sql);
1753 }
1754
1755 // Table botwriter_templates for prompt templates
1756 $templates_table_name = $wpdb->prefix . 'botwriter_templates';
1757 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $templates_table_name)) !== $templates_table_name) {
1758 $charset_collate = $wpdb->get_charset_collate();
1759
1760 $templates_sql = "CREATE TABLE $templates_table_name (
1761 `id` int(11) NOT NULL AUTO_INCREMENT,
1762 `name` VARCHAR(255) NOT NULL,
1763 `content` LONGTEXT NOT NULL,
1764 `is_default` TINYINT(1) DEFAULT 0,
1765 `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1766 PRIMARY KEY (`id`)
1767 ) $charset_collate;";
1768
1769 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1770 dbDelta($templates_sql);
1771
1772 // Insert all default templates
1773 botwriter_insert_all_default_templates();
1774 }
1775
1776 } catch (Exception $e) {
1777
1778 //error_log("Error creating botwriter tables: " . $e->getMessage());
1779 }
1780 }
1781
1782 /**
1783 * Insert the default prompt template (legacy function - now uses default-templates.php)
1784 * @deprecated Use botwriter_insert_all_default_templates() instead
1785 */
1786 function botwriter_insert_default_template() {
1787 // Now handled by botwriter_insert_all_default_templates() in default-templates.php
1788 botwriter_insert_all_default_templates();
1789 }
1790
1791 /**
1792 * Get the default template content with all placeholders
1793 * Uses the content from default-templates.php if available
1794 */
1795 function botwriter_get_default_template_content() {
1796 // Try to get from the default templates array
1797 if (function_exists('botwriter_get_default_template_by_name')) {
1798 $default = botwriter_get_default_template_by_name('Default Template');
1799 if ($default && !empty($default['content'])) {
1800 return $default['content'];
1801 }
1802 }
1803
1804 // Fallback hardcoded template
1805 $template = 'Write an article for a blog, follow these instructions:
1806
1807 -The article must be HTML, with proper opening and closing H2-H4 tags for headings, and <p> for paragraphs.
1808 -The length should be approximately {{post_length}} words.
1809 -The article language must be: {{post_language}}.
1810 -Narrative style: {{writer_style}}
1811 -The topic must be related to some of the following keywords: {{prompt_or_keywords}}
1812
1813 -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.';
1814
1815 return $template;
1816 }
1817
1818 /**
1819 * Get template by ID or default template
1820 */
1821 function botwriter_get_template($template_id = null) {
1822 global $wpdb;
1823 $table_name = $wpdb->prefix . 'botwriter_templates';
1824
1825 if ($template_id) {
1826 $template = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $template_id), ARRAY_A);
1827 } else {
1828 $template = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE is_default = %d", 1), ARRAY_A);
1829 }
1830
1831 if (!$template) {
1832 // Return hardcoded default if no template in DB
1833 return [
1834 'id' => 0,
1835 'name' => 'Default Template',
1836 'content' => botwriter_get_default_template_content(),
1837 'is_default' => 1
1838 ];
1839 }
1840
1841 return $template;
1842 }
1843
1844 /**
1845 * Get all templates
1846 */
1847 function botwriter_get_all_templates() {
1848 global $wpdb;
1849 $table_name = $wpdb->prefix . 'botwriter_templates';
1850 return $wpdb->get_results("SELECT * FROM $table_name ORDER BY name DESC", ARRAY_A);
1851 }
1852
1853 /**
1854 * Save a template (insert or update)
1855 */
1856 function botwriter_save_template($data) {
1857 global $wpdb;
1858 $table_name = $wpdb->prefix . 'botwriter_templates';
1859
1860 if (!empty($data['id'])) {
1861 // Update
1862 return $wpdb->update($table_name, [
1863 'name' => sanitize_text_field($data['name']),
1864 'content' => wp_kses_post($data['content'])
1865 ], ['id' => intval($data['id'])]);
1866 } else {
1867 // Insert
1868 return $wpdb->insert($table_name, [
1869 'name' => sanitize_text_field($data['name']),
1870 'content' => wp_kses_post($data['content']),
1871 'is_default' => 0
1872 ]);
1873 }
1874 }
1875
1876 /**
1877 * Set a template as default (unset other defaults)
1878 */
1879 function botwriter_set_default_template($template_id) {
1880 global $wpdb;
1881 $table_name = $wpdb->prefix . 'botwriter_templates';
1882
1883 // Check if template exists
1884 $template = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $template_id));
1885 if (!$template) {
1886 return false;
1887 }
1888
1889 // Remove default from all templates
1890 $wpdb->update($table_name, ['is_default' => 0], ['is_default' => 1]);
1891
1892 // Set new default
1893 return $wpdb->update($table_name, ['is_default' => 1], ['id' => intval($template_id)]);
1894 }
1895
1896 /**
1897 * Delete a template (cannot delete default)
1898 */
1899 function botwriter_delete_template($template_id) {
1900 global $wpdb;
1901 $table_name = $wpdb->prefix . 'botwriter_templates';
1902
1903 // Prevent deleting default template
1904 $is_default = $wpdb->get_var($wpdb->prepare("SELECT is_default FROM $table_name WHERE id = %d", $template_id));
1905 if ($is_default == 1) {
1906 return false;
1907 }
1908
1909 return $wpdb->delete($table_name, ['id' => intval($template_id)]);
1910 }
1911
1912 /**
1913 * Build prompt from template by replacing placeholders with actual values
1914 * Uses Mustache-like syntax: {{variable}} for simple values, {{#section}}...{{/section}} for conditionals
1915 */
1916 function botwriter_build_prompt_from_template($template_content, $data) {
1917 global $botwriter_languages;
1918
1919 // Map writer styles
1920 $writer_styles = [
1921 'ai_cerebro' => '',
1922 'orion' => '',
1923 'cloe' => 'ironic critic, sarcastic and witty',
1924 'lucida' => 'analytical critic, precise and direct',
1925 'max' => 'Passionate and descriptive',
1926 'gael' => 'Reflective, introspective and poetic',
1927 ];
1928
1929 // Prepare data for template — NOTE: this is the legacy function,
1930 // main flow uses botwriter_build_client_prompt() below.
1931 $writer = strtolower($data['writer'] ?? '');
1932 $writer_style = '';
1933
1934 if ($writer === 'custom') {
1935 $narration = strtolower($data['narration'] ?? '');
1936 if ($narration === 'custom') {
1937 $writer_style = $data['custom_style'] ?? '';
1938 } else {
1939 $writer_style = $narration;
1940 }
1941 } elseif (isset($writer_styles[$writer])) {
1942 $writer_style = $writer_styles[$writer];
1943 }
1944
1945 // Get language name from code
1946 $post_language_code = $data['post_language'] ?? 'en';
1947 $post_language = $botwriter_languages[$post_language_code] ?? 'English';
1948
1949 // Post length
1950 $post_length = $data['post_length'] ?? '800';
1951 if (!is_numeric($post_length)) {
1952 $post_length = 800;
1953 }
1954 $post_length = min(intval($post_length), 4000);
1955
1956 // Build replacements array
1957 // Note: source_content, existing_titles, title_prompt, content_prompt are handled server-side
1958 $replacements = [
1959 'post_length' => $post_length,
1960 'post_language' => $post_language,
1961 'writer_style' => $writer_style,
1962 'prompt_or_keywords' => $data['ai_keywords'] ?? '',
1963 ];
1964
1965 $prompt = $template_content;
1966
1967 // Replace variables: {{variable}}
1968 foreach ($replacements as $key => $value) {
1969 $prompt = str_replace('{{' . $key . '}}', $value, $prompt);
1970 }
1971
1972 // Clean up short instruction lines left empty after variable replacement
1973 // e.g. "-Narrative style: " or "-Topic: " when the value is empty
1974 // Never remove lines containing ENDARTICLE or other source markers
1975 $lines = explode("\n", $prompt);
1976 $cleaned_lines = [];
1977 foreach ($lines as $line) {
1978 $trimmed = trim($line);
1979 if (preg_match('/^-[^:]+:\s*$/', $trimmed) && strpos($trimmed, 'ENDARTICLE') === false && strlen($trimmed) < 40) {
1980 continue;
1981 }
1982 $cleaned_lines[] = $line;
1983 }
1984 $prompt = implode("\n", $cleaned_lines);
1985
1986 // Clean up multiple blank lines
1987 $prompt = preg_replace('/\n{3,}/', "\n\n", $prompt);
1988 $prompt = trim($prompt);
1989
1990 return $prompt;
1991 }
1992
1993 /**
1994 * Build the complete prompt on the client side using the template system
1995 * For types that require external content (wordpress, rss, news),
1996 * the server will add the source content to the prompt
1997 */
1998 function botwriter_build_client_prompt($data) {
1999 global $botwriter_languages;
2000
2001 // Get the template - use task-specific template if set, otherwise default
2002 $template_id = isset($data['template_id']) && !empty($data['template_id']) ? intval($data['template_id']) : null;
2003 $template = botwriter_get_template($template_id);
2004 $template_content = $template['content'];
2005
2006 // Map writer styles
2007 $writer_styles = [
2008 'ai_cerebro' => '',
2009 'orion' => '',
2010 'cloe' => 'ironic critic, sarcastic and witty',
2011 'lucida' => 'analytical critic, precise and direct',
2012 'max' => 'Passionate and descriptive',
2013 'gael' => 'Reflective, introspective and poetic',
2014 ];
2015
2016 // Prepare writer style
2017 $writer = strtolower($data['writer'] ?? '');
2018 $writer_style = '';
2019
2020 if ($writer === 'custom') {
2021 $narration = strtolower($data['narration'] ?? '');
2022 if ($narration === 'custom') {
2023 $writer_style = $data['custom_style'] ?? '';
2024 } else {
2025 $writer_style = $narration;
2026 }
2027 } elseif (isset($writer_styles[$writer])) {
2028 $writer_style = $writer_styles[$writer];
2029 }
2030
2031 // Get language name from code
2032 $post_language_code = $data['post_language'] ?? 'en';
2033 $post_language = $botwriter_languages[$post_language_code] ?? 'English';
2034
2035 // Post length
2036 $post_length = $data['post_length'] ?? '800';
2037 if (!is_numeric($post_length)) {
2038 $post_length = 800;
2039 }
2040 $post_length = min(intval($post_length), 4000);
2041
2042 // Determine what content to include based on website_type
2043 $website_type = $data['website_type'] ?? '';
2044 $source_title = '';
2045 $source_content = '';
2046 $ai_keywords = '';
2047 $existing_titles = '';
2048 $title_prompt = '';
2049 $content_prompt = '';
2050
2051 switch ($website_type) {
2052 case 'ai':
2053 case '':
2054 // AI mode: use keywords and avoid existing titles
2055 $ai_keywords = $data['ai_keywords'] ?? '';
2056 $existing_titles = $data['titles'] ?? '';
2057 break;
2058
2059 case 'super2':
2060 // Super2: use title_prompt and content_prompt from outline
2061 $title_prompt = $data['title_prompt'] ?? '';
2062 $content_prompt = $data['content_prompt'] ?? '';
2063 break;
2064
2065 case 'rss':
2066 // RSS content is now pre-fetched on the client side
2067 // Data is populated by botwriter_send1_data_to_server before calling this function
2068 $source_title = $data['source_title'] ?? '';
2069 $source_content = $data['source_content'] ?? '';
2070 break;
2071
2072 case 'wordpress':
2073 // WordPress content is now pre-fetched on the client side
2074 // Data is populated by botwriter_send1_data_to_server before calling this function
2075 $source_title = $data['source_title'] ?? '';
2076 $source_content = $data['source_content'] ?? '';
2077 break;
2078
2079 case 'news':
2080 // News still requires server-side content fetching
2081 // We leave source_title and source_content empty, server will fill them
2082 break;
2083 }
2084
2085 // Build replacements array
2086 $replacements = [
2087 'post_length' => $post_length,
2088 'post_language' => $post_language,
2089 'writer_style' => $writer_style,
2090 'source_title' => $source_title,
2091 'source_content' => $source_content,
2092 'ai_keywords' => $ai_keywords,
2093 'prompt_or_keywords' => $ai_keywords, // Alias for templates
2094 'existing_titles' => $existing_titles,
2095 'title_prompt' => $title_prompt,
2096 'content_prompt' => $content_prompt,
2097 ];
2098
2099 $prompt = $template_content;
2100
2101 botwriter_log('PROMPT BUILD: before source embed', [
2102 'website_type' => $website_type,
2103 'source_title_empty' => empty($source_title),
2104 'source_title' => mb_substr($source_title, 0, 100),
2105 'source_content_len' => strlen($source_content),
2106 'template_len' => strlen($template_content),
2107 ]);
2108
2109 // For RSS/WordPress: embed source content directly (already pre-fetched on client)
2110 if (in_array($website_type, ['rss', 'wordpress']) && !empty($source_title)) {
2111 $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";
2112 botwriter_log('PROMPT BUILD: ENDARTICLE block appended', [
2113 'prompt_len_after' => strlen($prompt),
2114 ]);
2115 } else {
2116 botwriter_log('PROMPT BUILD: ENDARTICLE block NOT appended', [
2117 'reason' => !in_array($website_type, ['rss', 'wordpress']) ? 'type not rss/wordpress' : 'source_title is empty',
2118 ]);
2119 }
2120 // For Super2: embed title and content instructions from the outline
2121 if ($website_type === 'super2') {
2122 // Rewrite instructions go BEFORE the ENDARTICLE block (rewriter / siterewriter tasks)
2123 $rewrite_prompt = $data['rewrite_prompt'] ?? '';
2124 if (!empty($rewrite_prompt)) {
2125 $prompt .= "\n-" . $rewrite_prompt;
2126 }
2127 if (!empty($title_prompt)) {
2128 $prompt .= "\n-The article title must be: " . $title_prompt;
2129 }
2130 if (!empty($content_prompt)) {
2131 $prompt .= "\n\n-Based on the following content (I indicate the end with the word ENDARTICLE):\n\n" . $content_prompt . "\n\nENDARTICLE\n";
2132 }
2133 botwriter_log('PROMPT BUILD: super2 title/content appended', [
2134 'title_prompt' => mb_substr($title_prompt, 0, 100),
2135 'content_prompt_len' => strlen($content_prompt),
2136 'has_rewrite_prompt' => !empty($rewrite_prompt),
2137 ]);
2138 }
2139 // For News: server still needs to fetch content
2140 if ($website_type === 'news') {
2141 $prompt .= "\n\n{{SERVER_SOURCE_CONTENT}}";
2142 }
2143
2144 // Replace variables: {{variable}}
2145 foreach ($replacements as $key => $value) {
2146 $prompt = str_replace('{{' . $key . '}}', $value, $prompt);
2147 }
2148
2149 $prompt_before_clean = $prompt;
2150 botwriter_log('PROMPT BUILD: BEFORE cleanup', [
2151 'prompt_len' => strlen($prompt),
2152 'contains_ENDARTICLE' => (strpos($prompt, 'ENDARTICLE') !== false),
2153 'contains_Based_on' => (strpos($prompt, 'Based on this') !== false),
2154 'first_300_chars' => mb_substr($prompt, 0, 300),
2155 'last_300_chars' => mb_substr($prompt, -300),
2156 ]);
2157
2158 // Clean up lines that have empty placeholders (lines ending with : or empty after replacement)
2159 $lines = explode("\n", $prompt);
2160 $cleaned_lines = [];
2161 $removed_lines = [];
2162 foreach ($lines as $line) {
2163 $trimmed = trim($line);
2164 // Skip short instruction lines left empty after variable replacement
2165 // e.g. "-Narrative style: " or "-Topic: " — never remove ENDARTICLE marker
2166 if (preg_match('/^-[^:]+:\s*$/', $trimmed) && strpos($trimmed, 'ENDARTICLE') === false && strlen($trimmed) < 40) {
2167 $removed_lines[] = $trimmed . ' (len=' . strlen($trimmed) . ')';
2168 continue;
2169 }
2170 $cleaned_lines[] = $line;
2171 }
2172 $prompt = implode("\n", $cleaned_lines);
2173
2174 if (!empty($removed_lines)) {
2175 botwriter_log('PROMPT BUILD: lines REMOVED by cleanup', [
2176 'count' => count($removed_lines),
2177 'lines' => $removed_lines,
2178 ]);
2179 }
2180
2181 // Clean up multiple blank lines
2182 $prompt = preg_replace('/\n{3,}/', "\n\n", $prompt);
2183 $prompt = trim($prompt);
2184
2185 botwriter_log('PROMPT BUILD: AFTER cleanup (final)', [
2186 'prompt_len' => strlen($prompt),
2187 'contains_ENDARTICLE' => (strpos($prompt, 'ENDARTICLE') !== false),
2188 'contains_Based_on' => (strpos($prompt, 'Based on this') !== false),
2189 'first_300_chars' => mb_substr($prompt, 0, 300),
2190 'last_300_chars' => mb_substr($prompt, -300),
2191 ]);
2192
2193 return $prompt;
2194 }
2195
2196
2197
2198 // Extending class
2199 class botwriter_tasks_Table extends WP_List_Table
2200 {
2201 // Define table columns
2202 function get_columns()
2203 {
2204 $columns = array(
2205 'cb' => '<input type="checkbox" />',
2206 'writer' => __('Writer', 'botwriter'),
2207 'task_name' => __('Task Name', 'botwriter'),
2208 'days' => __('Days', 'botwriter'),
2209 'times_per_day' => __('Times per Day', 'botwriter'),
2210 'type' => __('Type', 'botwriter'),
2211 'status' => __('Status', 'botwriter')
2212
2213 );
2214 return $columns;
2215 }
2216
2217
2218 // define $table_data property
2219 private $table_data;
2220
2221 // Bind table with columns, data and all
2222 function prepare_items()
2223 {
2224 //data
2225 if ( isset( $_POST['s'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'botwriter_nonce' ) ) {
2226 $search_query = sanitize_text_field(wp_unslash($_POST['s']));
2227 $this->table_data = $this->get_table_data($search_query);
2228 } else {
2229 $this->table_data = $this->get_table_data();
2230 }
2231
2232
2233 $columns = $this->get_columns();
2234 $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();
2235 $sortable = $this->get_sortable_columns();
2236 $primary = 'name';
2237 $this->_column_headers = array($columns, $hidden, $sortable, $primary);
2238 $this->process_bulk_action();
2239 $this->table_data = $this->get_table_data();
2240
2241 usort($this->table_data, array($this, 'usort_reorder'));
2242
2243 /* pagination */
2244 $per_page = $this->get_items_per_page('elements_per_page', 10);
2245 $current_page = $this->get_pagenum();
2246 $total_items = count($this->table_data);
2247
2248 $this->table_data = array_slice($this->table_data, (($current_page - 1) * $per_page), $per_page);
2249
2250 $this->set_pagination_args(array(
2251 'total_items' => $total_items, // total number of items
2252 'per_page' => $per_page, // items to show on a page
2253 'total_pages' => ceil( $total_items / $per_page ) // use ceil to round up
2254 ));
2255
2256 $this->items = $this->table_data;
2257 }
2258
2259
2260
2261 function column_task_name($item){
2262 $slug='botwriter_automatic_post_new';
2263 $page = isset($_REQUEST['page']) ? sanitize_text_field(wp_unslash($_REQUEST['page'])) : '';
2264
2265 if ($item["website_type"] == 'super2') {
2266 $slug='botwriter_super_page';
2267 $url_edit= wp_nonce_url('?page=' . $slug . '&id=' . $item['id'], "botwriter_tasks_action");
2268 } else {
2269 $url_edit= wp_nonce_url('?page=' . $slug . '&id=' . $item['id'], "botwriter_tasks_action");
2270 }
2271
2272 $url_delete= wp_nonce_url('?page=' . $page . '&action=delete&id=' . $item['id'], "botwriter_tasks_action");
2273
2274 $actions = array(
2275 'edit' => sprintf('<a href="%s">%s</a>', $url_edit, __('Edit', 'botwriter')),
2276 'delete' => sprintf('<a href="%s">%s</a>', $url_delete, __('Delete', 'botwriter')),
2277 );
2278
2279 $id=$item['id'];
2280 return sprintf('%s %s',
2281 "<a class='row-title' href='?page=$slug&id=$id&_wpnonce=" . wp_create_nonce('botwriter_tasks_action') . "'>" . $item['task_name'] . "</a>",
2282 $this->row_actions($actions)
2283 );
2284 }
2285
2286 function column_writer($item){
2287 $dir_images_writers = plugin_dir_url(__FILE__) . 'assets/images/writers/';
2288 $writer=$item['writer'];
2289 $writer = strtolower($writer);
2290
2291 $slug='botwriter_automatic_post_new';
2292 $id=$item['id'];
2293 $link="<a class='row-title' href='?page=$slug&id=$id'>";
2294 $img= '<img src="' . esc_url($dir_images_writers . $writer . '.jpeg') . '" alt="' . esc_attr($writer) . '" class="writer-photo">';
2295 return $link . $img . '</a>';
2296
2297 }
2298
2299
2300
2301 function column_status($item){
2302
2303 $status = $item['status'];
2304 $status_opuesto = $status ? 0 : 1;
2305 $icono = $status ? 'dashicons-yes' : 'dashicons-dismiss';
2306 $texto_status = $status ? 'Desactivate' : 'Activate';
2307
2308 return sprintf(
2309 '<a href="#" class="icono-status dashicons %s" data-id="%d" data-status="%d" title="%s"></a>',
2310 $icono,
2311 $item['id'],
2312 $status_opuesto,
2313 $texto_status
2314 );
2315
2316
2317 }
2318
2319 /*
2320 function column_category_id($item) {
2321 $categories = get_categories();
2322 $category_name = '';
2323 foreach ($categories as $category) {
2324 $aux_cateforias=explode(',',$item['category_id']);
2325 if (in_array($category->term_id, $aux_cateforias)) {
2326 $category_name .= $category->name . ', ';
2327 }
2328
2329 }
2330 $category_name = rtrim($category_name, ', ');
2331 return $category_name;
2332 }
2333 */
2334
2335
2336
2337
2338
2339 // To show bulk action dropdown
2340 function get_bulk_actions()
2341 {
2342 $actions = array(
2343 'delete_all' => __('Delete', 'botwriter'),
2344
2345 );
2346 return $actions;
2347 }
2348
2349 function process_bulk_action()
2350 {
2351 // Verify user has permission
2352 if (!current_user_can('manage_options')) {
2353 return;
2354 }
2355
2356 // Verify nonce
2357 if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])), "botwriter_tasks_action")) {
2358 return;
2359 }
2360
2361 global $wpdb;
2362
2363 $table = $wpdb->prefix . 'botwriter_tasks';
2364
2365 if ('delete_all' === $this->current_action() || ('delete' === $this->current_action() && isset($_REQUEST['id']))) {
2366 $request_id = isset($_REQUEST['id']) ? array_map('absint', (array) $_REQUEST['id']) : array();
2367
2368 if (!empty($request_id)) {
2369 // Prepare the DELETE query with proper escaping
2370 $placeholders = implode(',', array_fill(0, count($request_id), '%d'));
2371 $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE id IN({$placeholders})", $request_id));
2372 }
2373 }
2374 }
2375
2376
2377
2378 // Get table data
2379 private function get_table_data( $search = '' ) {
2380 global $wpdb;
2381
2382 $table = $wpdb->prefix."botwriter_tasks";
2383
2384
2385 if ( ! empty( $search ) ) {
2386 $prepared_search = $wpdb->esc_like( $search );
2387 $prepared_search = '%' . $wpdb->esc_like( $search ) . '%';
2388
2389 return $wpdb->get_results(
2390 $wpdb->prepare(
2391 "SELECT * FROM {$table} WHERE name LIKE %s AND (task_type IS NULL OR task_type <> %s)",
2392 $prepared_search,
2393 'writenow'
2394 ),
2395 ARRAY_A
2396 );
2397 } else {
2398 return $wpdb->get_results(
2399 $wpdb->prepare(
2400 "SELECT * FROM {$table} WHERE (task_type IS NULL OR task_type <> %s)",
2401 'writenow'
2402 ),
2403 ARRAY_A
2404 );
2405 }
2406 }
2407
2408 function column_default($item, $column_name)
2409 {
2410
2411 switch ($column_name) {
2412 case 'id':
2413 case 'website_type':
2414 case 'website_name':
2415 case 'task_name':
2416 case 'category_id':
2417 case 'website_category_id':
2418 default:
2419 return $item[$column_name];
2420 }
2421 }
2422
2423 // Render the combined Type column: website_type + task_type
2424 function column_type($item) {
2425 $parts = array();
2426 if (!empty($item['website_type'])) {
2427 $parts[] = sanitize_text_field($item['website_type']);
2428 }
2429 if (!empty($item['task_type'])) {
2430 $parts[] = sanitize_text_field($item['task_type']);
2431 }
2432 $label = !empty($parts) ? implode(' / ', $parts) : __('', 'botwriter');
2433 return esc_html($label);
2434 }
2435
2436 function column_cb($item){
2437 return sprintf(
2438 '<input type="checkbox" name="id[]" value="%s" />',
2439 $item['id']
2440 );
2441 }
2442
2443 public function get_sortable_columns(){
2444 $sortable_columns = array(
2445 'task_name' => array('task_name', false),
2446 'days' => array('days', false),
2447 'id' => array('id', true)
2448 );
2449 return $sortable_columns;
2450 }
2451
2452 // Sorting function
2453 function usort_reorder($a, $b)
2454 {
2455 // If no sort, default to task_name
2456 $sanitized_orderby = isset($_GET['orderby']) ? sanitize_text_field(wp_unslash($_GET['orderby'])) : '';
2457
2458 $orderby = (!empty($sanitized_orderby)) ? $sanitized_orderby : 'task_name';
2459
2460 $order = isset($_GET['order']) ? sanitize_text_field(wp_unslash($_GET['order'])) : 'asc';
2461
2462 // filtrar order solo asd o desc
2463 $order = in_array($order, array('asc', 'desc')) ? $order : 'asc';
2464 // filter orderby only allowed columns
2465 $orderby = in_array($orderby, array('task_name', 'days', 'id')) ? $orderby : 'task_name';
2466
2467
2468
2469
2470 // Determine sort order
2471 $result = strcmp($a[$orderby], $b[$orderby]);
2472
2473 // Send final sort direction to usort
2474 return ($order === 'asc') ? $result : -$result;
2475 }
2476
2477 } // end class botwriter_tasks_Table
2478
2479
2480
2481
2482
2483
2484 function botwriter_validate_website($item,$is_manual = false)
2485 {
2486
2487 $messages = array();
2488
2489
2490 if (empty($item['task_name'])) $messages[] = __('Task Name is required', 'botwriter');
2491 if (empty($item['website_type'])) $messages[] = __('Website Type is required', 'botwriter');
2492
2493 // Category/taxonomy validation: require category_id for 'post' type, or taxonomy_data for other types
2494 $post_type = isset($item['post_type']) ? $item['post_type'] : 'post';
2495 if ($post_type === 'post') {
2496 if (empty($item['category_id']) && empty($item['taxonomy_data'])) {
2497 $messages[] = __('Category is required', 'botwriter');
2498 }
2499 }
2500 // For other post types, taxonomy selection is optional
2501
2502
2503 if($item['website_type'] == 'wordpress'){
2504 if (empty($item['domain_name'])) {
2505 $messages[] = __('Domain Name is required', 'botwriter');
2506 }
2507 if( !botwriter_isValidDomain(sanitize_text_field($item['domain_name'])) ){
2508 $messages[] = __('Domain name should be valid.', 'botwriter');
2509 }
2510 }
2511
2512 if ($item['website_type'] == 'rss') {
2513 if (empty($item['rss_source'])) {
2514 $messages[] = __('RSS Source is required', 'botwriter');
2515 }
2516 }
2517
2518 if ($item['website_type'] == 'news') {
2519 if (empty($item['news_keyword'])) {
2520 $messages[] = __('News keyword is required', 'botwriter');
2521 }
2522 }
2523
2524 if (empty($messages)) return true;
2525 return implode('<br />', $messages);
2526 }
2527
2528
2529 function botwriter_isValidDomain($domain) {
2530 // WordPress wp_http_validate_url
2531 $valid_url = wp_http_validate_url( $domain);
2532
2533 if (!is_wp_error($valid_url)) {
2534 return true;
2535 } else {
2536 return false;
2537 }
2538 }
2539
2540
2541 function botwriter_is_site_working($site_url, $site_type) {
2542 $response = false;
2543
2544 if ($site_type === 'wordpress') {
2545 // Check if the WordPress REST API is accessible
2546 $api_url = rtrim($site_url, '/') . '/wp-json/wp/v2/posts';
2547 $headers = @get_headers($api_url);
2548 if ($headers && strpos((string)$headers[0], '200') !== false) {
2549 $response = true;
2550 }
2551 } elseif ($site_type === 'rss') {
2552 // Check if the RSS feed is accessible
2553 $rss = @simplexml_load_file($site_url);
2554 if ($rss) {
2555 $response = true;
2556 }
2557 }
2558
2559 return $response;
2560 }
2561
2562
2563 //wp-cron:
2564
2565
2566 // Add a custom schedule for cron jobs
2567
2568 function botwriter_add_custom_cron_schedule($schedules) {
2569 if (!isset($schedules['every_30'])) {
2570 $schedules['every_30'] = array(
2571 'interval' => 30, // 30 seconds
2572 'display' => __('Every thirty seconds', 'botwriter')
2573 );
2574 }
2575 return $schedules;
2576 }
2577 add_filter('cron_schedules', 'botwriter_add_custom_cron_schedule');
2578
2579 // Ensure cron is scheduled on admin load (in case activation hook didn't run)
2580 function botwriter_ensure_cron_scheduled() {
2581 if (get_option('botwriter_cron_active') === '0') {
2582 return;
2583 }
2584
2585 if (!wp_next_scheduled('botwriter_scheduled_events_plugin_cron')) {
2586 $scheduled = wp_schedule_event(time() + 30, 'every_30', 'botwriter_scheduled_events_plugin_cron');
2587 botwriter_log('Cron scheduled (admin init)', [
2588 'scheduled' => $scheduled ? 'yes' : 'no',
2589 ]);
2590 }
2591 }
2592 add_action('admin_init', 'botwriter_ensure_cron_scheduled');
2593
2594 // Schedule the cron job during plugin activation
2595 function botwriter_scheduled_events_plugin_activate() {
2596 if (get_option('botwriter_cron_active')=="0") {
2597 return;
2598 }
2599 if (!wp_next_scheduled('botwriter_scheduled_events_plugin_cron')) {
2600 wp_schedule_event(time(), 'every_30', 'botwriter_scheduled_events_plugin_cron');
2601 }
2602 }
2603 register_activation_hook(__FILE__, 'botwriter_scheduled_events_plugin_activate');
2604
2605 // Register the cron task
2606 add_action('botwriter_scheduled_events_plugin_cron', 'botwriter_scheduled_events_execute_tasks');
2607
2608
2609
2610
2611 // Clear the cron job upon plugin deactivation
2612 function botwriter_scheduled_events_plugin_deactivate() {
2613 wp_clear_scheduled_hook('botwriter_scheduled_events_plugin_cron');
2614 }
2615 register_deactivation_hook(__FILE__, 'botwriter_scheduled_events_plugin_deactivate');
2616
2617
2618
2619
2620 function botwriter_scheduled_events_execute_tasks() {
2621 global $wpdb;
2622 $table_name_tasks = $wpdb->prefix . 'botwriter_tasks';
2623 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
2624 $table_name_super = $wpdb->prefix . 'botwriter_super';
2625
2626 // ── Prevent overlapping cron runs (race condition guard) ──
2627 // Use a transient lock so two cron ticks cannot run simultaneously.
2628 // Lock expires after 120 seconds as a safety net.
2629 $lock_key = 'botwriter_cron_lock';
2630 if (get_transient($lock_key)) {
2631 botwriter_log('CRON SKIPPED — another cron run is still in progress');
2632 return;
2633 }
2634 set_transient($lock_key, time(), 120);
2635
2636 // Check if cron is active
2637 $cron_active = get_option('botwriter_cron_active');
2638 botwriter_log('=== CRON START ===', [
2639 'cron_active_option' => $cron_active,
2640 'timestamp' => current_time('Y-m-d H:i:s'),
2641 ]);
2642
2643 if ($cron_active !== '1') {
2644 botwriter_log('CRON DISABLED - exiting', ['cron_active' => $cron_active]);
2645 delete_transient($lock_key);
2646 return;
2647 }
2648
2649 // STOPFORMANY: refuse to dispatch new tasks while the flag is active
2650 if (get_option('botwriter_stopformany', false)) {
2651 botwriter_log('CRON BLOCKED by STOPFORMANY — too many consecutive errors on server');
2652 delete_transient($lock_key);
2653 return;
2654 }
2655
2656 // Get the current day (English name) and date based on WordPress local time
2657 // Use DateTime with wp_timezone() to respect site timezone and keep English day name
2658 try {
2659 $dt = new DateTime('now', wp_timezone());
2660 $current_day_en = $dt->format('l');
2661 } catch (Exception $e) {
2662 // Fallback, still English but GMT-based
2663 $current_day_en = gmdate('l');
2664 }
2665 $current_date = current_time('Y-m-d');
2666
2667 botwriter_log('Cron dispatcher triggered', [
2668 'day' => $current_day_en,
2669 'date' => $current_date,
2670 'timezone' => wp_timezone_string(),
2671 ]);
2672
2673 // PHASE 2
2674 botwriter_execute_events_pass2();
2675
2676 //PHASE 1 Execute each event if it meets the conditions
2677 // Get tasks scheduled for today and status=1
2678 // Exclude one-off Write now tasks from cron to avoid duplicate logs
2679 $tasks = (array) $wpdb->get_results(
2680 $wpdb->prepare(
2681 "SELECT * FROM {$table_name_tasks} WHERE days LIKE %s AND status = %d AND (task_type IS NULL OR task_type <> %s)",
2682 '%' . $wpdb->esc_like($current_day_en) . '%',
2683 1,
2684 'writenow'
2685 ),
2686 ARRAY_A
2687 );
2688 botwriter_log('Tasks evaluated for cron tick', [
2689 'count' => count($tasks),
2690 'query_day' => $current_day_en,
2691 ]);
2692
2693 // Log all tasks found for debugging
2694 if (count($tasks) === 0) {
2695 // Check total active tasks to see if it's a day mismatch
2696 $all_active = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name_tasks} WHERE status = 1");
2697 botwriter_log('NO TASKS FOUND for today', [
2698 'current_day' => $current_day_en,
2699 'total_active_tasks' => $all_active,
2700 ]);
2701 }
2702
2703 foreach ($tasks as $task) {
2704 botwriter_log('Evaluating task', [
2705 'task_id' => $task['id'],
2706 'task_name' => $task['task_name'],
2707 'days' => $task['days'],
2708 'times_per_day' => $task['times_per_day'],
2709 'execution_count' => $task['execution_count'],
2710 'last_execution_date' => $task['last_execution_date'],
2711 'last_execution_time' => $task['last_execution_time'],
2712 'website_type' => $task['website_type'],
2713 ]);
2714
2715 // Skip Write now tasks defensively (in case of legacy rows)
2716 if (!empty($task['task_type']) && $task['task_type'] === 'writenow') {
2717 botwriter_log('Skipping writenow task', ['task_id' => $task['id']]);
2718 continue;
2719 }
2720
2721 // Reset execution count daily
2722 if ($task["last_execution_date"] !== $current_date) {
2723 $wpdb->update($table_name_tasks, ['execution_count' => 0, 'last_execution_date' => $current_date], ['id' => $task["id"]]);
2724 $task["execution_count"] = 0;
2725 botwriter_log('Reset execution count for new day', ['task_id' => $task['id']]);
2726 }
2727
2728 // Check if the task is a supertask and if it exists
2729 $super_exists = true;
2730 if ($task["website_type"] == 'super2') { //supertask
2731 $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);
2732 if (!$super) {
2733 $super_exists = false;
2734 botwriter_log('Super2 task skipped, no pending outline', [
2735 'task_id' => $task['id'],
2736 ]);
2737 }
2738 }
2739
2740 // Check if the task can still be executed based on its daily limit
2741 if ($task["execution_count"] < $task["times_per_day"] && $super_exists) {
2742 $last_execution_time = $task["last_execution_time"];
2743 $now = current_time('timestamp');
2744 $diff = $now - strtotime($last_execution_time);
2745 $pause = get_option('botwriter_paused_tasks');
2746 $pause = is_numeric($pause) ? intval($pause) : 2;
2747
2748 botwriter_log('Checking pause time', [
2749 'task_id' => $task['id'],
2750 'last_execution_time' => $last_execution_time,
2751 'now' => current_time('Y-m-d H:i:s'),
2752 'diff_seconds' => $diff,
2753 'pause_minutes' => $pause,
2754 'required_seconds' => 60 * $pause,
2755 'can_execute' => ($diff > 60 * $pause) ? 'YES' : 'NO',
2756 ]);
2757
2758 if ($diff > 60 * $pause) { //
2759
2760 $event = $task;
2761 $event["task_status"] = "pending";
2762 $event["id_task"] = $task["id"];
2763 $event["intentosfase1"] = 0;
2764 $id_log = botwriter_logs_register($event); // create log in db
2765 $event["id"] = $id_log;
2766 if ($task["website_type"] == 'super2') { // supertask
2767 botwriter_log('Preparing super2 dispatch', [
2768 'task_id' => $task['id'],
2769 'log_id' => $id_log,
2770 ]);
2771 $prepared_event = botwriter_super_prepare_event($event);
2772 if ($prepared_event === false) {
2773 botwriter_log('Super2 preparation returned false', [
2774 'task_id' => $task['id'],
2775 'log_id' => $id_log,
2776 ]);
2777 continue;
2778 }
2779 $event = $prepared_event;
2780 $id_log = botwriter_logs_register($event, $id_log); // actualizamos log con los datos de super2
2781 }
2782 botwriter_log('Queueing phase 1 send', [
2783 'task_id' => $task['id'],
2784 'log_id' => $id_log,
2785 'website_type' => $task['website_type'],
2786 ]);
2787 // Update execution count BEFORE the HTTP call to prevent
2788 // overlapping cron ticks from creating duplicate logs.
2789 $current_time = current_time('Y-m-d H:i:s');
2790 $wpdb->update($table_name_tasks, ['execution_count' => $task["execution_count"] + 1, 'last_execution_time' => $current_time], ['id' => $task["id"]]);
2791 botwriter_send1_data_to_server((array) $event);
2792 } else {
2793 botwriter_log('Task paused - waiting for pause interval', [
2794 'task_id' => $task['id'],
2795 'diff_seconds' => $diff,
2796 'required_seconds' => 60 * $pause,
2797 'remaining_seconds' => (60 * $pause) - $diff,
2798 ]);
2799 }
2800 } else {
2801 if ($super_exists) {
2802 botwriter_log('Task skipped due to daily limit reached', [
2803 'task_id' => $task['id'],
2804 'execution_count' => $task['execution_count'],
2805 'times_per_day' => $task['times_per_day'],
2806 ]);
2807 }
2808 }
2809 } // end tasks
2810
2811 botwriter_log('=== CRON END ===', ['timestamp' => current_time('Y-m-d H:i:s')]);
2812
2813 // ── Release cron lock ──
2814 delete_transient('botwriter_cron_lock');
2815 }
2816
2817 function botwriter_execute_events_pass2(){
2818
2819 // check if the task is still in queue or finished
2820 global $wpdb;
2821 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
2822 // INQUEUE
2823 $events2 = (array) $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table_name_logs} WHERE task_status=%s", 'inqueue'));
2824 botwriter_log('Phase 2 queue check', ['inqueue_count' => count($events2)]);
2825 foreach ($events2 as $event) {
2826 $event = (array) $event;
2827
2828 // ── Atomically mark as 'polling' to prevent overlapping cron ticks ──
2829 // Only update if the status is still 'inqueue'; if another cron tick
2830 // already changed it, affected_rows will be 0 and we skip this log.
2831 $affected = $wpdb->query($wpdb->prepare(
2832 "UPDATE {$table_name_logs} SET task_status = 'polling' WHERE id = %d AND task_status = 'inqueue'",
2833 $event['id']
2834 ));
2835 if ($affected === 0) {
2836 botwriter_log('Phase 2 skipped — already being polled', ['log_id' => $event['id']]);
2837 continue;
2838 }
2839
2840 // Execute the event (send2 will set final status: completed/error/inqueue)
2841 $result = botwriter_send2_data_to_server( (array) $event);
2842
2843 // If send2 did NOT update the log status (returned false without changing it),
2844 // restore to 'inqueue' so the next tick can retry.
2845 if ($result === false) {
2846 $current_status = $wpdb->get_var($wpdb->prepare(
2847 "SELECT task_status FROM {$table_name_logs} WHERE id = %d",
2848 $event['id']
2849 ));
2850 if ($current_status === 'polling') {
2851 $wpdb->update($table_name_logs, ['task_status' => 'inqueue'], ['id' => $event['id']]);
2852 }
2853 }
2854 } // end INQUEUE
2855
2856
2857 //IN ERROR, depending on the attempt, it is resent later or marked as finished
2858 // Exclude 'writenow' tasks from automatic retries - they should only be retried manually
2859 $events1 = (array) $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table_name_logs} WHERE task_status=%s AND intentosfase1 < %d AND (task_type IS NULL OR task_type <> 'writenow')", 'error', 8));
2860 botwriter_log('Phase 1 retries fetched', ['error_count' => count($events1)]);
2861 $intento_tiempo = array(0=>0,1=>0,2=>5,3=>10,4=>30,5=>60,6=>120,7=>240,8=>480); // minutos
2862 foreach ($events1 as $event) {
2863 $event = (array) $event;
2864 // Execute the event if the time has passed
2865 $intentosfase1 = $event["intentosfase1"];
2866 $tiempo = $intento_tiempo[$intentosfase1+1];
2867 $created_at = strtotime($event["created_at"]);
2868 $now = current_time('timestamp');
2869
2870 $diff = $now - $created_at;
2871 if ($diff > $tiempo * 60) {
2872 botwriter_log('Retrying phase 1 request', [
2873 'log_id' => $event['id'],
2874 'task_id' => $event['id_task'],
2875 'attempt' => $intentosfase1 + 1,
2876 ]);
2877 botwriter_send1_data_to_server( (array) $event);
2878 }
2879
2880 } // END LOGS IN ERROR
2881
2882
2883 }
2884
2885
2886 function botwriter_generate_post($data){
2887 // Determine post type (default to 'post' for backward compatibility)
2888 $post_type = isset($data['post_type']) && !empty($data['post_type']) ? $data['post_type'] : 'post';
2889
2890 // Build post data array
2891 $post_data = array(
2892 'post_title' => $data['aigenerated_title'],
2893 'post_content' => $data['aigenerated_content'],
2894 'post_status' => $data['post_status'],
2895 'post_author' => $data['author_selection'],
2896 'post_type' => $post_type,
2897 );
2898
2899 // For 'post' type with category_id (backward compatibility)
2900 if ($post_type === 'post' && !empty($data['category_id'])) {
2901 $post_data['post_category'] = array_map('intval', explode(',', $data['category_id']));
2902 }
2903
2904 // Create the post
2905 $post_id = wp_insert_post($post_data);
2906
2907 if ($post_id === 0) {
2908 //error_log('Error creating post');
2909 return false;
2910 }
2911
2912 // Assign taxonomy terms from taxonomy_data (if present)
2913 if (!empty($data['taxonomy_data'])) {
2914 $taxonomy_data = json_decode($data['taxonomy_data'], true);
2915 if (is_array($taxonomy_data)) {
2916 foreach ($taxonomy_data as $taxonomy_name => $term_ids) {
2917 if (!empty($term_ids) && taxonomy_exists($taxonomy_name)) {
2918 $term_ids = array_map('intval', (array)$term_ids);
2919 wp_set_object_terms($post_id, $term_ids, $taxonomy_name);
2920 }
2921 }
2922 }
2923 }
2924
2925 // Add tags to the post (unless disabled in settings)
2926 $tags_disabled = get_option('botwriter_tags_disabled', '0');
2927 if ($tags_disabled !== '1' && !empty($data['aigenerated_tags'])) {
2928 $tags = explode(',', $data['aigenerated_tags']);
2929 wp_set_post_tags($post_id, $tags);
2930 }
2931
2932 // SEO Slug Translation: translate post slug, tag slugs, and get image slug
2933 $translated_image_slug = '';
2934 if (function_exists('botwriter_apply_translated_slugs')) {
2935 $translated_image_slug = botwriter_apply_translated_slugs(
2936 $post_id,
2937 $data['aigenerated_title'],
2938 $data['aigenerated_tags'] ?? ''
2939 );
2940 }
2941
2942 // Add image to the post only if image URL is provided and images are not disabled for this task
2943 $task_disable_images = isset($data['disable_ai_images']) ? intval($data['disable_ai_images']) : 0;
2944 if (!empty($data['aigenerated_image']) && $task_disable_images !== 1) {
2945 // Pass attribution data for stock photos
2946 $image_attribution = isset($data['image_attribution']) ? $data['image_attribution'] : null;
2947 botwriter_attach_image_to_post($post_id, $data['aigenerated_image'], $data['aigenerated_title'], $translated_image_slug, $image_attribution);
2948
2949 // Handle stock photo attribution in post content (footer mode)
2950 if (!empty($image_attribution) && is_array($image_attribution)) {
2951 $attribution_mode = get_option('botwriter_stockphoto_attribution', 'caption');
2952 if ($attribution_mode === 'content_footer') {
2953 $author = sanitize_text_field($image_attribution['author'] ?? '');
2954 $source = sanitize_text_field($image_attribution['source'] ?? '');
2955 $source_url = esc_url($image_attribution['source_url'] ?? '');
2956 $author_url = esc_url($image_attribution['author_url'] ?? '');
2957
2958 if ($author || $source) {
2959 $credit_parts = array();
2960 if ($author) {
2961 $credit_parts[] = $author_url
2962 ? sprintf('<a href="%s" rel="nofollow noopener" target="_blank">%s</a>', $author_url, esc_html($author))
2963 : esc_html($author);
2964 }
2965 if ($source) {
2966 $credit_parts[] = $source_url
2967 ? sprintf('<a href="%s" rel="nofollow noopener" target="_blank">%s</a>', $source_url, esc_html($source))
2968 : esc_html($source);
2969 }
2970 $credit_html = '<p class="botwriter-image-attribution"><small>'
2971 . sprintf(
2972 /* translators: %s: attribution credit (author / source) */
2973 esc_html__('Photo by %s', 'botwriter'),
2974 implode(' / ', $credit_parts)
2975 )
2976 . '</small></p>';
2977
2978 wp_update_post(array(
2979 'ID' => $post_id,
2980 'post_content' => get_post_field('post_content', $post_id) . "\n" . $credit_html,
2981 ));
2982 }
2983 }
2984 }
2985 } else {
2986 $skip_reason = '';
2987 if (empty($data['aigenerated_image'])) {
2988 $skip_reason = 'No image URL provided';
2989 } elseif ($task_disable_images === 1) {
2990 $skip_reason = 'AI images disabled for this task';
2991 }
2992
2993 botwriter_log('Image attachment skipped during post creation', [
2994 'post_id' => $post_id,
2995 'post_title' => $data['aigenerated_title'],
2996 'image_url' => $data['aigenerated_image'] ?? 'not provided',
2997 'reason' => $skip_reason
2998 ]);
2999 }
3000
3001 // Generate SEO meta description using AI (unless disabled)
3002 if ( function_exists( 'botwriter_generate_seo_meta' ) && get_option( 'botwriter_meta_disabled', '0' ) !== '1' ) {
3003 $post_language = $data['post_language'] ?? '';
3004 $meta_description = botwriter_generate_seo_meta(
3005 $data['aigenerated_title'],
3006 $data['aigenerated_content'],
3007 $post_language
3008 );
3009 if ( $meta_description ) {
3010 botwriter_apply_seo_meta( $post_id, $meta_description );
3011 }
3012 }
3013
3014 // Persist image prompt in post meta so regeneration never depends on logs.
3015 $saved_image_prompt = botwriter_resolve_image_prompt_from_post_data($data);
3016 botwriter_log('Post creation: image prompt resolution before meta save', array(
3017 'post_id' => $post_id,
3018 'resolved_prompt_len' => strlen((string) $saved_image_prompt),
3019 'incoming_image_prompt_len' => strlen(trim((string) ($data['image_prompt'] ?? ''))),
3020 'incoming_image_provider' => isset($data['image_provider']) ? sanitize_key((string) $data['image_provider']) : '',
3021 'has_image_attribution' => !empty($data['image_attribution']),
3022 ));
3023 if ($saved_image_prompt !== '') {
3024 $saved_provider = isset($data['image_provider']) ? sanitize_key((string) $data['image_provider']) : '';
3025
3026 if ($saved_provider === '' && !empty($data['image_attribution'])) {
3027 $saved_provider = 'stockphoto';
3028 }
3029
3030 if ($saved_provider === '') {
3031 $settings = botwriter_get_current_image_generation_settings();
3032 $saved_provider = (string) ($settings['provider'] ?? '');
3033 }
3034
3035 botwriter_log('Post creation: saving image prompt meta', array(
3036 'post_id' => $post_id,
3037 'provider' => $saved_provider,
3038 'prompt_len' => strlen((string) $saved_image_prompt),
3039 ));
3040
3041 botwriter_save_post_image_prompt_meta($post_id, $saved_image_prompt, $saved_provider);
3042 }
3043
3044 return $post_id;
3045 }
3046
3047
3048
3049
3050 // Function to send data to the server pass1
3051 function botwriter_send1_data_to_server($data) {
3052
3053 Global $botwriter_version;
3054 $remote_url = BOTWRITER_API_URL . 'redis_api_cola.php';
3055
3056 // Use constant to avoid get_plugin_data() and early translation loading
3057 $botwriter_version = BOTWRITER_VERSION;
3058
3059 // settings
3060 $data['version'] = $botwriter_version;
3061 $data['api_key'] = get_option('botwriter_api_key'); // la api_key del programa
3062 $data["user_domainname"] = esc_url(get_site_url());
3063 $data['site_token'] = get_option('botwriter_site_token', '');
3064
3065 $data["ai_image_size"]=get_option('botwriter_ai_image_size');
3066 $data["ai_image_quality"]=get_option('botwriter_ai_image_quality');
3067 $data["ai_image_style"]=get_option('botwriter_ai_image_style', 'realistic');
3068 $data["ai_image_style_custom"]=get_option('botwriter_ai_image_style_custom', '');
3069
3070 // Use task-specific setting for disable_ai_images (already in $data from task/log)
3071 // If not present, default to 0 (images enabled)
3072 if (!isset($data["disable_ai_images"])) {
3073 $data["disable_ai_images"] = 0;
3074 }
3075
3076 // If image generation fails, publish the post without image instead of erroring
3077 $data['image_error_continue'] = get_option('botwriter_image_error_continue', '0');
3078
3079 // Provider selections
3080 $data['text_provider'] = get_option('botwriter_text_provider', 'openai');
3081 $data['image_provider'] = get_option('botwriter_image_provider', 'dalle');
3082
3083 // Stock photo settings (sent always, used only when image_provider=stockphoto)
3084 $data['stockphoto_preferred'] = get_option('botwriter_stockphoto_preferred', 'pixabay');
3085 $data['stockphoto_selection'] = get_option('botwriter_stockphoto_selection', 'random_top10');
3086 $data['stockphoto_attribution'] = get_option('botwriter_stockphoto_attribution', 'caption');
3087
3088 // Get current text model based on provider
3089 $text_provider = $data['text_provider'];
3090 $text_model_defaults = [
3091 'openai' => 'gpt-5.4-mini',
3092 'anthropic' => 'claude-sonnet-4-6',
3093 'google' => 'gemini-2.5-flash',
3094 'mistral' => 'mistral-large-latest',
3095 'groq' => 'llama-3.3-70b-versatile',
3096 'openrouter' => 'anthropic/claude-sonnet-4.6',
3097 ];
3098 $data['text_model'] = get_option("botwriter_{$text_provider}_model", $text_model_defaults[$text_provider] ?? 'gpt-5.4-mini');
3099
3100 // Get current image model based on provider
3101 $image_provider = $data['image_provider'];
3102 $image_model_defaults = [
3103 'dalle' => 'gpt-image-1',
3104 'gemini' => 'gemini-2.5-flash-image',
3105 'fal' => 'fal-ai/flux-pro/v1.1',
3106 'replicate' => 'black-forest-labs/flux-1.1-pro',
3107 'stability' => 'sd3.5-large-turbo',
3108 'cloudflare' => 'flux-1-schnell',
3109 ];
3110 // Gemini uses a different option name (gemini_image_model instead of gemini_model to avoid conflict with text)
3111 $image_model_option = ($image_provider === 'gemini') ? 'botwriter_gemini_image_model' : "botwriter_{$image_provider}_model";
3112 $data['image_model'] = get_option($image_model_option, $image_model_defaults[$image_provider] ?? 'gpt-image-1');
3113
3114 // Also send the specific field name the server expects
3115 if ($image_provider === 'gemini') {
3116 $data['gemini_image_model'] = $data['image_model'];
3117 }
3118
3119 // Send all API keys (decrypted) - server will use the ones needed
3120 $data['openai_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_openai_api_key'));
3121 $data['anthropic_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_anthropic_api_key'));
3122 $data['google_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_google_api_key'));
3123 $data['mistral_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_mistral_api_key'));
3124 $data['groq_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_groq_api_key'));
3125 $data['openrouter_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_openrouter_api_key'));
3126 $data['fal_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_fal_api_key'));
3127 $data['replicate_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_replicate_api_key'));
3128 $data['stability_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_stability_api_key'));
3129 $data['cloudflare_api_key'] = botwriter_decrypt_api_key(get_option('botwriter_cloudflare_api_key'));
3130 $data['cloudflare_account_id'] = get_option('botwriter_cloudflare_account_id');
3131
3132 // Legacy field (kept for backward compatibility)
3133 $data["openai_model"]=get_option('botwriter_openai_model', 'gpt-4o-mini');
3134
3135 $current_time = gmdate('Y-m-d H:i:s', current_time('timestamp'));
3136 $data["last_execution_time"]=$current_time;
3137
3138 $last_execution_time = $data["last_execution_time"];
3139
3140
3141 $category_ids=array_map('intval', explode(',', $data['category_id']));
3142 $titles = botwriter_get_logs_titles($data['id_task']);
3143 if ($titles === false) {
3144 $data['titles'] = '';
3145 } else {
3146 $data['titles'] = implode(' | ', $titles);
3147 }
3148
3149
3150 // add the links of the posts where it has been copied
3151 $links = botwriter_get_logs_links($data['id_task']);
3152 if ($links === false) {
3153 $data['links'] = '';
3154 } else {
3155 $data['links'] = implode(',', $links);
3156 }
3157
3158 // post_lenght
3159 if ($data['post_length'] === 'custom') {
3160 $data['post_length'] = $data['custom_post_length'];
3161 }
3162
3163 // =====================================================
3164 // CLIENT-SIDE RSS FETCH (pre-fetch before prompt build)
3165 // =====================================================
3166 $website_type = $data['website_type'] ?? '';
3167 if ($website_type === 'rss') {
3168 $rss_result = botwriter_fetch_rss_content(
3169 $data['rss_source'] ?? '',
3170 $data['links'] ?? ''
3171 );
3172
3173 if ( ! $rss_result['success'] ) {
3174 botwriter_log('Client RSS fetch failed', [
3175 'log_id' => $data['id'] ?? null,
3176 'task_id' => $data['id_task'] ?? null,
3177 'error' => $rss_result['error'] ?? 'Unknown error',
3178 ]);
3179 $data['task_status'] = 'error';
3180 $data['error'] = $rss_result['error'] ?? 'RSS fetch failed';
3181 botwriter_logs_register($data, $data['id']);
3182 return false;
3183 }
3184
3185 // Populate data with the pre-fetched article so the prompt builder can use it
3186 $data['source_title'] = $rss_result['source_title'];
3187 $data['source_content'] = $rss_result['source_content'];
3188 $data['link_post_original'] = $rss_result['link_original'];
3189 $data['source_prefetched'] = '1';
3190
3191 botwriter_log('Client RSS article ready', [
3192 'log_id' => $data['id'] ?? null,
3193 'task_id' => $data['id_task'] ?? null,
3194 'article_link' => $rss_result['link_original'],
3195 ]);
3196 }
3197 // =====================================================
3198
3199 // =====================================================
3200 // CLIENT-SIDE WORDPRESS FETCH (pre-fetch before prompt build)
3201 // =====================================================
3202 if ($website_type === 'wordpress') {
3203 $wp_result = botwriter_fetch_wordpress_content(
3204 $data['domain_name'] ?? '',
3205 $data['website_category_id'] ?? '',
3206 $data['links'] ?? ''
3207 );
3208
3209 if ( ! $wp_result['success'] ) {
3210 botwriter_log('Client WordPress fetch failed', [
3211 'log_id' => $data['id'] ?? null,
3212 'task_id' => $data['id_task'] ?? null,
3213 'error' => $wp_result['error'] ?? 'Unknown error',
3214 ]);
3215 $data['task_status'] = 'error';
3216 $data['error'] = $wp_result['error'] ?? 'WordPress fetch failed';
3217 botwriter_logs_register($data, $data['id']);
3218 return false;
3219 }
3220
3221 $data['source_title'] = $wp_result['source_title'];
3222 $data['source_content'] = $wp_result['source_content'];
3223 $data['link_post_original'] = $wp_result['link_original'];
3224 $data['source_prefetched'] = '1';
3225
3226 botwriter_log('Client WordPress article ready', [
3227 'log_id' => $data['id'] ?? null,
3228 'task_id' => $data['id_task'] ?? null,
3229 'article_link' => $wp_result['link_original'],
3230 ]);
3231 }
3232 // =====================================================
3233
3234 // Build prompt from template (except for super1 which is handled by server)
3235 if ($website_type !== 'super1') {
3236 $data['client_prompt'] = botwriter_build_client_prompt($data);
3237 }
3238
3239 botwriter_log('SEND1: after prompt build', [
3240 'log_id' => $data['id'] ?? null,
3241 'website_type' => $website_type,
3242 'has_client_prompt' => !empty($data['client_prompt']),
3243 'client_prompt_len' => strlen($data['client_prompt'] ?? ''),
3244 'source_prefetched' => $data['source_prefetched'] ?? '0',
3245 'has_source_title' => !empty($data['source_title']),
3246 'has_source_content' => !empty($data['source_content']),
3247 'link_post_original' => $data['link_post_original'] ?? '',
3248 ]);
3249
3250 botwriter_log('Dispatching phase 1 request', [
3251 'log_id' => $data['id'] ?? null,
3252 'task_id' => $data['id_task'] ?? null,
3253 'website_type' => $data['website_type'] ?? null,
3254 'status' => $data['task_status'] ?? null,
3255 'attempt' => $data['intentosfase1'] ?? null,
3256 'text_provider' => $data['text_provider'] ?? null,
3257 'text_model' => $data['text_model'] ?? null,
3258 'image_provider' => $data['image_provider'] ?? null,
3259 'image_model' => $data['image_model'] ?? null,
3260 ]);
3261
3262 $data["error"]= "";
3263
3264 $ssl_verify = get_option('botwriter_sslverify');
3265
3266
3267 if ($ssl_verify === 'no') {
3268 $ssl_verify = false;
3269 } else {
3270 $ssl_verify = true;
3271 }
3272
3273
3274
3275 $response = wp_remote_post($remote_url, array(
3276 'method' => 'POST',
3277 'body' => $data,
3278 'timeout' => 45,
3279 'headers' => array(),
3280 'sslverify' => $ssl_verify,
3281 ));
3282
3283 $data["intentosfase1"]++;
3284 botwriter_logs_register($data, $data["id"]);
3285
3286 $last_execution_time=$data["last_execution_time"];
3287
3288
3289
3290 if (is_wp_error($response)) {
3291 $error_message = $response->get_error_message();
3292 botwriter_log('Phase 1 request error', [
3293 'log_id' => $data['id'] ?? null,
3294 'task_id' => $data['id_task'] ?? null,
3295 'website_type' => $data['website_type'] ?? null,
3296 'error' => $error_message,
3297 ]);
3298 $data["task_status"]="error";
3299 botwriter_logs_register($data, $data["id"]);
3300 return false;
3301
3302 } else {
3303
3304 if ($response['response']['code'] === 200) {
3305
3306 $body = wp_remote_retrieve_body($response);
3307 $result = json_decode($body, true);
3308
3309 //error_log("Data received: " . print_r($body, true));
3310
3311 // STOPFORMANY: server reports too many consecutive errors
3312 if (isset($result['error']) && strpos($result['error'], 'STOPFORMANY') === 0) {
3313 update_option('botwriter_stopformany', true);
3314 $data['task_status'] = 'error';
3315 $data['error'] = $result['error'];
3316 $data['intentosfase1'] = 8; // stop retries
3317 botwriter_logs_register($data, $data['id']);
3318 botwriter_log('STOPFORMANY activated from phase 1', [
3319 'log_id' => $data['id'] ?? null,
3320 'task_id' => $data['id_task'] ?? null,
3321 'error' => $result['error'],
3322 ]);
3323 return false;
3324 }
3325
3326 if (isset($result['id_task_server']) && $result['id_task_server'] !== 0) { // ok
3327 $data["id_task_server"]=$result['id_task_server'];
3328 $data["task_status"]='inqueue';
3329
3330 // Capture site_token from server response (auto-provisioning)
3331 if (!empty($result['site_token'])) {
3332 update_option('botwriter_site_token', sanitize_text_field($result['site_token']));
3333 }
3334
3335 botwriter_logs_register($data, $data["id"]);
3336 botwriter_log('Phase 1 request accepted', [
3337 'log_id' => $data['id'] ?? null,
3338 'task_id' => $data['id_task'] ?? null,
3339 'id_task_server' => $result['id_task_server'],
3340 ]);
3341 return $result['id_task_server'];
3342 } else { // error — id_task_server is 0 or missing
3343 $data["task_status"]="error";
3344 // Use full error_message (may include reset link) when available
3345 $data["error"] = !empty($result['error_message']) ? $result['error_message'] : ($result['error'] ?? '');
3346
3347 // Generic terminal flag — server says stop retrying
3348 if (!empty($result['terminal'])) {
3349 $data['intentosfase1'] = 8;
3350 }
3351 // Show server-provided admin notice
3352 if (!empty($result['error_message']) && (int)($result['error_level'] ?? 0) === 1) {
3353 botwriter_announcements_add(
3354 __('Service notice', 'botwriter'),
3355 wp_kses_post($result['error_message'])
3356 );
3357 }
3358
3359 // Handle known server errors (same logic as Phase 2)
3360 if ($data["error"] == "Maximum monthly posts limit reached") {
3361 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>");
3362 $data["intentosfase1"] = 8;
3363 }
3364 if ($data["error"] == "Payment date exceeded") {
3365 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>");
3366 $data["intentosfase1"] = 8;
3367 }
3368 if ($data["error"] == "API Key error") {
3369 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>");
3370 $data["intentosfase1"] = 8;
3371 }
3372
3373 botwriter_logs_register($data, $data["id"]);
3374 botwriter_log('Phase 1 request rejected', [
3375 'log_id' => $data['id'] ?? null,
3376 'task_id' => $data['id_task'] ?? null,
3377 'error' => $data['error'],
3378 'response_length' => isset($body) ? strlen($body) : null,
3379 ]);
3380 return false;
3381 }
3382 } else { // error
3383 $data["task_status"]="error";
3384 botwriter_logs_register($data, $data["id"]);
3385 botwriter_log('Phase 1 non-200 response', [
3386 'log_id' => $data['id'] ?? null,
3387 'task_id' => $data['id_task'] ?? null,
3388 'status_code' => $response['response']['code'],
3389 ]);
3390 return false;
3391 }
3392 }
3393
3394
3395 }
3396
3397 // Function to send data to the server pass2
3398 function botwriter_send2_data_to_server($data) {
3399 Global $wpdb;
3400
3401 $data['api_key'] = get_option('botwriter_api_key');
3402 $data["user_domainname"] = esc_url(get_site_url());
3403 $data['site_token'] = get_option('botwriter_site_token', '');
3404
3405 $remote_url = BOTWRITER_API_URL . 'redis_api_finish.php';
3406
3407 $ssl_verify = get_option('botwriter_sslverify');
3408 if ($ssl_verify === 'no') {
3409 $ssl_verify = false;
3410 } else {
3411 $ssl_verify = true;
3412 }
3413
3414 botwriter_log('Dispatching phase 2 request', [
3415 'log_id' => $data['id'] ?? null,
3416 'task_id' => $data['id_task'] ?? null,
3417 'website_type' => $data['website_type'] ?? null,
3418 'id_task_server' => $data['id_task_server'] ?? null,
3419 ]);
3420
3421 $response = wp_remote_post($remote_url, array(
3422 'method' => 'POST',
3423 'body' => $data,
3424 'timeout' => 45,
3425 'headers' => array(),
3426 'sslverify' => $ssl_verify
3427 ));
3428
3429
3430 if (is_wp_error($response)) {
3431 $error_message = $response->get_error_message();
3432 botwriter_log('Phase 2 request error', [
3433 'log_id' => $data['id'] ?? null,
3434 'task_id' => $data['id_task'] ?? null,
3435 'website_type' => $data['website_type'] ?? null,
3436 'error' => $error_message,
3437 ]);
3438 $data["error"]= "Error sending data " . $error_message;
3439 return false;
3440 } else {
3441
3442 if ($response['response']['code'] === 200) {
3443
3444 $body = wp_remote_retrieve_body($response);
3445 $result = json_decode($body, true);
3446
3447 //echo 'Data recive: <pre>' . print_r($result, true) . '</pre>';
3448
3449 // results errors
3450 if (isset($result["task_status"]) && $result["task_status"] == "error") {
3451 $data["task_status"]="error";
3452 // Use full error_message (may include reset link) when available
3453 $data["error"] = !empty($result['error_message']) ? $result['error_message'] : ($result['error'] ?? '');
3454 botwriter_log('Phase 2 reported error', [
3455 'log_id' => $data['id'] ?? null,
3456 'task_id' => $data['id_task'] ?? null,
3457 'id_task_server' => $data['id_task_server'] ?? null,
3458 'error' => $data['error'],
3459 ]);
3460
3461 // Generic terminal flag — server says stop retrying
3462 if (!empty($result['terminal'])) {
3463 $data['intentosfase1'] = 8;
3464 }
3465 // Show server-provided admin notice
3466 if (!empty($result['error_message']) && (int)($result['error_level'] ?? 0) === 1) {
3467 botwriter_announcements_add(
3468 __('Service notice', 'botwriter'),
3469 wp_kses_post($result['error_message'])
3470 );
3471 }
3472
3473 if ($data["error"]=="Maximum monthly posts limit reached") {
3474 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>");
3475 $data["intentosfase1"]=8;
3476 }
3477 if ($data["error"]=="Payment date exceeded") {
3478 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>");
3479 $data["intentosfase1"]=8;
3480 }
3481 if ($data["error"]=="API Key error") {
3482 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>");
3483 $data["intentosfase1"]=8;
3484 }
3485 // STOPFORMANY: server reports too many consecutive errors
3486 if (isset($result['error']) && strpos($result['error'], 'STOPFORMANY') === 0) {
3487 update_option('botwriter_stopformany', true);
3488 $data['intentosfase1'] = 8;
3489 botwriter_log('STOPFORMANY activated from phase 2', [
3490 'log_id' => $data['id'] ?? null,
3491 'task_id' => $data['id_task'] ?? null,
3492 'error' => $result['error'],
3493 ]);
3494 }
3495
3496 botwriter_logs_register($data, $data["id"]);
3497 return false;
3498 }
3499
3500 // result completed
3501 botwriter_log('Phase 2 response payload', [
3502 'log_id' => $data['id'] ?? null,
3503 'task_id' => $data['id_task'] ?? null,
3504 'id_task_server' => $data['id_task_server'] ?? null,
3505 'task_status' => $result['task_status'] ?? null,
3506 ]);
3507 if (isset($result["task_status"]) && $result["task_status"] == "completed") {
3508 // Preserve image_attribution from server response (not stored in DB)
3509 $image_attribution = isset($result['image_attribution']) ? $result['image_attribution'] : null;
3510
3511 botwriter_logs_register($result, $data["id"]);
3512 botwriter_log('Phase 2 completed event', [
3513 'log_id' => $data['id'] ?? null,
3514 'task_id' => $data['id_task'] ?? null,
3515 'id_task_server' => $data['id_task_server'] ?? null,
3516 ]);
3517
3518 $result=botwriter_logs_get($data["id"]); // merge the result with the log
3519
3520 // Re-attach image_attribution (not persisted in logs table)
3521 if ($image_attribution) {
3522 $result['image_attribution'] = $image_attribution;
3523 }
3524
3525 // ── Guard: prevent duplicate post creation ──
3526 // If this log already has a published post, skip generation.
3527 if (!empty($result['id_post_published']) && intval($result['id_post_published']) > 0) {
3528 botwriter_log('Phase 2 skipped — post already published', [
3529 'log_id' => $data['id'],
3530 'post_id' => $result['id_post_published'],
3531 ]);
3532 return $result;
3533 }
3534
3535 if ($result["website_type"] == "super1") {
3536 botwriter_super1_log_to_bd($result,$data["id"]);
3537
3538 } else {
3539 $post_id=botwriter_generate_post($result);
3540 $result["id_post_published"]=$post_id;
3541 }
3542
3543 botwriter_logs_register($result, $data["id"]);
3544 if ($result["website_type"] == "super2") {
3545 //update tabla super poner el task_Status en completed
3546 $wpdb->update($wpdb->prefix . 'botwriter_super', ['task_status' => 'completed'], ['id_log' => $data["id"]]);
3547 }
3548
3549
3550 return $result;
3551 }
3552
3553 // other results, inqueue, pending, etc
3554 $now=current_time('timestamp');
3555 $last_execution_time = strtotime($data["last_execution_time"]);
3556 $diff = $now - $last_execution_time;
3557 if ($diff > 60 * 5) { // 5 minutes
3558 $data["task_status"]="error";
3559 $data["error"]="Error in server";
3560 botwriter_logs_register($data, $data["id"]);
3561 botwriter_log('Phase 2 timeout detected', [
3562 'log_id' => $data['id'] ?? null,
3563 'task_id' => $data['id_task'] ?? null,
3564 ]);
3565 }
3566 return false;
3567
3568
3569
3570
3571
3572
3573 } else { // error
3574 // update log
3575 $data["task_status"]="error";
3576 botwriter_logs_register($data, $data["id"]);
3577 botwriter_log('Phase 2 non-200 response', [
3578 'log_id' => $data['id'] ?? null,
3579 'task_id' => $data['id_task'] ?? null,
3580 'status_code' => $response['response']['code'],
3581 ]);
3582 return false;
3583 }
3584 }
3585
3586
3587
3588 }
3589
3590
3591
3592
3593
3594 function botwriter_cambiar_status_ajax() {
3595 // Verify user has permission
3596 if (!current_user_can('manage_options')) {
3597 wp_send_json_error(['message' => 'Permission denied']);
3598 }
3599
3600 check_ajax_referer('botwriter_cambiar_status_nonce', 'nonce');
3601
3602 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
3603 $nuevo_status = isset($_POST['status']) ? intval($_POST['status']) : 0;
3604
3605
3606
3607 if ($id > 0) {
3608 global $wpdb;
3609 $table_name = $wpdb->prefix . 'botwriter_tasks';
3610
3611 $result = $wpdb->update(
3612 $table_name,
3613 ['status' => $nuevo_status],
3614 ['id' => $id],
3615 ['%d'],
3616 ['%d']
3617 );
3618
3619 if ($result !== false) {
3620 wp_send_json_success(['message' => 'Estado actualizado correctamente']);
3621 } else {
3622 wp_send_json_error(['message' => 'Error al actualizar el estado']);
3623 }
3624 } else {
3625 wp_send_json_error(['message' => 'ID inválido']);
3626 }
3627
3628 wp_die();
3629 }
3630 add_action('wp_ajax_botwriter_cambiar_status', 'botwriter_cambiar_status_ajax');
3631
3632 /**
3633 * AJAX handler to check/preview an RSS feed from the admin UI.
3634 * Reads the feed client-side using botwriter_fetch_rss_content().
3635 */
3636 function botwriter_check_rss_ajax() {
3637 if ( ! current_user_can( 'manage_options' ) ) {
3638 wp_send_json_error( [ 'error' => 'Permission denied' ] );
3639 }
3640
3641 check_ajax_referer( 'botwriter_check_rss_nonce', 'nonce' );
3642
3643 $url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
3644 if ( empty( $url ) ) {
3645 wp_send_json_error( [ 'error' => 'RSS URL is required.' ] );
3646 }
3647
3648 $result = botwriter_fetch_rss_content( $url );
3649 if ( $result['success'] ) {
3650 wp_send_json_success( [
3651 'title' => $result['source_title'],
3652 'description' => $result['source_content'],
3653 'link' => $result['link_original'],
3654 ] );
3655 } else {
3656 wp_send_json_error( [ 'error' => $result['error'] ] );
3657 }
3658 }
3659 add_action( 'wp_ajax_botwriter_check_rss', 'botwriter_check_rss_ajax' );
3660
3661 /**
3662 * AJAX handler to fetch categories from a remote WordPress site.
3663 * Reads categories client-side using botwriter_fetch_wordpress_categories().
3664 */
3665 function botwriter_get_wordpress_categories_ajax() {
3666 if ( ! current_user_can( 'manage_options' ) ) {
3667 wp_send_json_error( [ 'error' => 'Permission denied' ] );
3668 }
3669
3670 check_ajax_referer( 'botwriter_wp_categories_nonce', 'nonce' );
3671
3672 $domain = isset( $_POST['website_domainname'] ) ? esc_url_raw( wp_unslash( $_POST['website_domainname'] ) ) : '';
3673 if ( empty( $domain ) ) {
3674 wp_send_json_error( [ 'error' => 'WordPress domain is required.' ] );
3675 }
3676
3677 $result = botwriter_fetch_wordpress_categories( $domain );
3678 if ( $result['success'] ) {
3679 wp_send_json_success( $result['categories'] );
3680 } else {
3681 wp_send_json_error( [ 'error' => $result['error'] ] );
3682 }
3683 }
3684 add_action( 'wp_ajax_botwriter_get_wp_categories', 'botwriter_get_wordpress_categories_ajax' );
3685
3686 // AJAX handler for deleting a log entry
3687 add_action('wp_ajax_botwriter_delete_log', 'botwriter_delete_log_ajax');
3688 function botwriter_delete_log_ajax() {
3689 // Verify user has permission
3690 if (!current_user_can('manage_options')) {
3691 wp_send_json_error(['message' => 'Permission denied']);
3692 }
3693
3694 // Verify nonce
3695 check_ajax_referer('botwriter_logs_delete_nonce', 'nonce');
3696
3697 $log_id = isset($_POST['log_id']) ? intval($_POST['log_id']) : 0;
3698
3699 if ($log_id > 0) {
3700 global $wpdb;
3701 $table_name = $wpdb->prefix . 'botwriter_logs';
3702
3703 $result = $wpdb->delete($table_name, array('id' => $log_id), array('%d'));
3704
3705 if ($result !== false) {
3706 wp_send_json_success(['message' => __('Log deleted successfully', 'botwriter')]);
3707 } else {
3708 wp_send_json_error(['message' => __('Error deleting log', 'botwriter')]);
3709 }
3710 } else {
3711 wp_send_json_error(['message' => __('Invalid log ID', 'botwriter')]);
3712 }
3713
3714 wp_die();
3715 }
3716
3717 // AJAX handler for bulk deleting log entries
3718 add_action('wp_ajax_botwriter_bulk_delete_logs', 'botwriter_bulk_delete_logs_ajax');
3719 function botwriter_bulk_delete_logs_ajax() {
3720 if (!current_user_can('manage_options')) {
3721 wp_send_json_error(['message' => 'Permission denied']);
3722 }
3723
3724 check_ajax_referer('botwriter_logs_delete_nonce', 'nonce');
3725
3726 $log_ids = isset($_POST['log_ids']) ? array_map('intval', $_POST['log_ids']) : array();
3727 $log_ids = array_filter($log_ids, function($id) { return $id > 0; });
3728
3729 if (empty($log_ids)) {
3730 wp_send_json_error(['message' => __('No logs selected', 'botwriter')]);
3731 }
3732
3733 global $wpdb;
3734 $table_name = $wpdb->prefix . 'botwriter_logs';
3735 $placeholders = implode(',', array_fill(0, count($log_ids), '%d'));
3736 $deleted = $wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE id IN ($placeholders)", $log_ids));
3737
3738 if ($deleted !== false) {
3739 wp_send_json_success(['message' => sprintf(__('%d log(s) deleted successfully', 'botwriter'), $deleted)]);
3740 } else {
3741 wp_send_json_error(['message' => __('Error deleting logs', 'botwriter')]);
3742 }
3743
3744 wp_die();
3745 }
3746
3747 // AJAX handler for getting taxonomies and terms for a post type
3748 add_action('wp_ajax_botwriter_get_taxonomies', 'botwriter_get_taxonomies_ajax');
3749 function botwriter_get_taxonomies_ajax() {
3750 // Verify user has permission
3751 if (!current_user_can('manage_options')) {
3752 wp_send_json_error(['message' => 'Permission denied']);
3753 }
3754
3755 // Verify nonce
3756 check_ajax_referer('botwriter_taxonomies_nonce', 'nonce');
3757
3758 $post_type = isset($_POST['post_type']) ? sanitize_text_field(wp_unslash($_POST['post_type'])) : 'post';
3759
3760 // Get taxonomies for this post type
3761 $taxonomies = get_object_taxonomies($post_type, 'objects');
3762
3763 $result = array();
3764 // Skip tag taxonomies since AI generates tags automatically
3765 $skip_taxonomies = array('post_tag', 'product_tag');
3766
3767 foreach ($taxonomies as $taxonomy) {
3768 // Skip non-public taxonomies
3769 if (!$taxonomy->public) {
3770 continue;
3771 }
3772
3773 // Skip tag taxonomies (AI generates tags)
3774 if (in_array($taxonomy->name, $skip_taxonomies, true)) {
3775 continue;
3776 }
3777
3778 // Get terms for this taxonomy
3779 $terms = get_terms(array(
3780 'taxonomy' => $taxonomy->name,
3781 'hide_empty' => false,
3782 'orderby' => 'name',
3783 'order' => 'ASC',
3784 ));
3785
3786 $terms_data = array();
3787 if (!is_wp_error($terms)) {
3788 foreach ($terms as $term) {
3789 $terms_data[] = array(
3790 'id' => $term->term_id,
3791 'name' => $term->name,
3792 'slug' => $term->slug,
3793 'parent' => $term->parent,
3794 );
3795 }
3796 }
3797
3798 $result[] = array(
3799 'name' => $taxonomy->name,
3800 'label' => $taxonomy->label,
3801 'hierarchical' => $taxonomy->hierarchical,
3802 'terms' => $terms_data,
3803 );
3804 }
3805
3806 wp_send_json_success($result);
3807 }
3808
3809
3810 function botwriter_attach_image_to_post($post_id, $image_url, $post_title, $translated_image_slug = '', $image_attribution = null) {
3811 if (!$image_url || !$post_id || empty(trim($image_url))) {
3812 botwriter_log('Image attachment skipped', [
3813 'post_id' => $post_id,
3814 'image_url' => $image_url ?: 'empty',
3815 'reason' => 'Invalid post ID or empty image URL'
3816 ]);
3817 return 'Invalid post ID or image URL';
3818 }
3819
3820 if ($image_url && $post_id) {
3821 //$image_data = file_get_contents($image_url);
3822 $image_data = wp_remote_retrieve_body(wp_remote_get($image_url));
3823 $upload_dir = wp_upload_dir();
3824 // Use translated image slug if available, otherwise fall back to title
3825 if (!empty($translated_image_slug)) {
3826 $base_name = sanitize_file_name($translated_image_slug);
3827 } else {
3828 $base_name = sanitize_file_name(remove_accents($post_title));
3829 }
3830 if (empty($base_name)) {
3831 $base_name = 'botwriter-post-' . $post_id;
3832 }
3833 /**
3834 * Filter the filename used for BotWriter generated images.
3835 *
3836 * @param string $base_name Base filename without extension.
3837 * @param int $post_id Post ID.
3838 * @param string $post_title Post title.
3839 */
3840 $base_name = apply_filters('botwriter_image_filename', $base_name, $post_id, $post_title);
3841 $filename = $base_name . '.jpg';
3842 $filename = wp_unique_filename($upload_dir['path'], $filename);
3843
3844 if (wp_mkdir_p($upload_dir['path'])) {
3845 $file = $upload_dir['path'] . '/' . $filename;
3846 } else {
3847 $file = $upload_dir['basedir'] . '/' . $filename;
3848 }
3849
3850 global $wp_filesystem;
3851 if ( ! function_exists( 'WP_Filesystem' ) ) {
3852 require_once ABSPATH . 'wp-admin/includes/file.php';
3853 }
3854 WP_Filesystem();
3855 if ( $wp_filesystem->put_contents( $file, $image_data, FS_CHMOD_FILE ) ) {
3856 //error_log('Imagen guardada exitosamente en: ' . $file);
3857 } else {
3858 //error_log('Error al guardar la imagen en: ' . $file);
3859 }
3860
3861 $wp_filetype = wp_check_filetype($filename, null);
3862 $attachment = [
3863 'post_mime_type' => $wp_filetype['type'],
3864 'post_title' => sanitize_file_name($filename),
3865 'post_content' => '',
3866 'post_status' => 'inherit',
3867 ];
3868
3869 $attach_id = wp_insert_attachment($attachment, $file, $post_id);
3870 require_once(ABSPATH . 'wp-admin/includes/image.php');
3871
3872 // Apply post-processing if enabled
3873 $postprocess_enabled = get_option('botwriter_image_postprocess_enabled', '0');
3874 if ($postprocess_enabled === '1') {
3875 $processed_file = botwriter_process_image($file);
3876 if ($processed_file && $processed_file !== $file) {
3877 // Update file reference if format changed
3878 $file = $processed_file;
3879 // Update attachment with new file info
3880 $wp_filetype = wp_check_filetype(basename($file), null);
3881 wp_update_post([
3882 'ID' => $attach_id,
3883 'post_mime_type' => $wp_filetype['type'],
3884 ]);
3885 update_attached_file($attach_id, $file);
3886 }
3887 }
3888
3889 $attach_data = wp_generate_attachment_metadata($attach_id, $file);
3890 wp_update_attachment_metadata($attach_id, $attach_data);
3891 set_post_thumbnail($post_id, $attach_id);
3892
3893 // Apply stock photo attribution as image caption and alt text
3894 if (!empty($image_attribution) && is_array($image_attribution)) {
3895 $attribution_mode = get_option('botwriter_stockphoto_attribution', 'caption');
3896 if ($attribution_mode !== 'disabled') {
3897 $author = sanitize_text_field($image_attribution['author'] ?? '');
3898 $source = sanitize_text_field($image_attribution['source'] ?? '');
3899
3900 // Build caption: "Photo by Author on Source"
3901 $caption = '';
3902 if ($author && $source) {
3903 $caption = sprintf(
3904 /* translators: 1: photographer name, 2: image source name */
3905 esc_html__('Photo by %1$s on %2$s', 'botwriter'),
3906 $author,
3907 $source
3908 );
3909 } elseif ($source) {
3910 $caption = sprintf(esc_html__('Photo from %s', 'botwriter'), $source);
3911 }
3912
3913 if ($caption && $attribution_mode === 'caption') {
3914 wp_update_post(array(
3915 'ID' => $attach_id,
3916 'post_excerpt' => $caption,
3917 ));
3918 }
3919
3920 // Always set a descriptive alt text from the post title
3921 update_post_meta($attach_id, '_wp_attachment_image_alt', sanitize_text_field($post_title));
3922
3923 // Store full attribution data as post meta for later use
3924 update_post_meta($attach_id, '_botwriter_image_attribution', $image_attribution);
3925 }
3926 }
3927
3928
3929 }
3930 }
3931
3932 /**
3933 * Process image for optimization (resize, compress, convert format)
3934 * Uses WordPress wp_get_image_editor for maximum hosting compatibility.
3935 *
3936 * @param string $file_path Full path to the image file.
3937 * @return string|false New file path if processed, original path if no changes, false on error.
3938 */
3939 function botwriter_process_image($file_path) {
3940 if (!file_exists($file_path)) {
3941 botwriter_log('Image post-processing: File not found', ['path' => $file_path]);
3942 return false;
3943 }
3944
3945 // Get settings
3946 $output_format = get_option('botwriter_image_output_format', 'webp');
3947 $max_width = intval(get_option('botwriter_image_max_width', 1200));
3948 $compression = intval(get_option('botwriter_image_compression', 85));
3949 $max_filesize = intval(get_option('botwriter_image_max_filesize', 120)) * 1024; // Convert KB to bytes
3950
3951 // Log settings at start
3952 $original_size = filesize($file_path);
3953 botwriter_log('Image post-processing: Starting', [
3954 'file' => basename($file_path),
3955 'original_size_kb' => round($original_size / 1024, 1),
3956 'settings' => [
3957 'output_format' => $output_format,
3958 'max_width' => $max_width,
3959 'compression' => $compression,
3960 'max_filesize_kb' => $max_filesize / 1024
3961 ]
3962 ]);
3963
3964 // Get WordPress image editor
3965 $editor = wp_get_image_editor($file_path);
3966 if (is_wp_error($editor)) {
3967 botwriter_log('Image post-processing: Failed to load editor', [
3968 'path' => $file_path,
3969 'error' => $editor->get_error_message()
3970 ]);
3971 return $file_path; // Return original on error
3972 }
3973
3974 // Log which editor is being used
3975 botwriter_log('Image post-processing: Editor loaded', [
3976 'editor_class' => get_class($editor)
3977 ]);
3978
3979 $size = $editor->get_size();
3980 $modified = false;
3981
3982 botwriter_log('Image post-processing: Original dimensions', [
3983 'width' => $size['width'],
3984 'height' => $size['height']
3985 ]);
3986
3987 // Resize if wider than max width
3988 if ($max_width > 0 && $size['width'] > $max_width) {
3989 $new_height = intval($size['height'] * ($max_width / $size['width']));
3990 $result = $editor->resize($max_width, $new_height, false);
3991 if (!is_wp_error($result)) {
3992 $modified = true;
3993 botwriter_log('Image post-processing: Resized', [
3994 'from' => $size['width'] . 'x' . $size['height'],
3995 'to' => $max_width . 'x' . $new_height
3996 ]);
3997 }
3998 }
3999
4000 // Set quality
4001 $editor->set_quality($compression);
4002
4003 // Determine output file path and mime type
4004 $path_info = pathinfo($file_path);
4005 $new_extension = $path_info['extension'];
4006 $mime_type = null;
4007
4008 if ($output_format !== 'original') {
4009 switch ($output_format) {
4010 case 'webp':
4011 // Check if WebP is supported via GD or Imagick
4012 $webp_supported = function_exists('imagewebp');
4013 if (!$webp_supported && extension_loaded('imagick') && class_exists('Imagick')) {
4014 // Check Imagick WebP support dynamically to avoid static analysis errors
4015 $imagick_formats = call_user_func(['Imagick', 'queryFormats'], 'WEBP');
4016 $webp_supported = !empty($imagick_formats);
4017 }
4018 if ($webp_supported) {
4019 $new_extension = 'webp';
4020 $mime_type = 'image/webp';
4021 } else {
4022 // Fallback to JPEG if WebP not supported
4023 $new_extension = 'jpg';
4024 $mime_type = 'image/jpeg';
4025 botwriter_log('Image post-processing: WebP not supported, falling back to JPEG');
4026 }
4027 break;
4028 case 'jpeg':
4029 $new_extension = 'jpg';
4030 $mime_type = 'image/jpeg';
4031 break;
4032 case 'png':
4033 $new_extension = 'png';
4034 $mime_type = 'image/png';
4035 break;
4036 }
4037 }
4038
4039 // Build new file path
4040 $new_file_path = $path_info['dirname'] . '/' . $path_info['filename'] . '.' . $new_extension;
4041
4042 botwriter_log('Image post-processing: Format decision', [
4043 'original_extension' => $path_info['extension'],
4044 'new_extension' => $new_extension,
4045 'mime_type' => $mime_type,
4046 'new_file_path' => basename($new_file_path)
4047 ]);
4048
4049 // Save the image
4050 $save_args = [];
4051 if ($mime_type) {
4052 $save_args['mime_type'] = $mime_type;
4053 }
4054
4055 $saved = $editor->save($new_file_path, $mime_type);
4056
4057 if (is_wp_error($saved)) {
4058 botwriter_log('Image post-processing: Failed to save', [
4059 'path' => $new_file_path,
4060 'error' => $saved->get_error_message()
4061 ]);
4062 return $file_path; // Return original on error
4063 }
4064
4065 $new_file_path = $saved['path'];
4066
4067 botwriter_log('Image post-processing: Initial save complete', [
4068 'saved_file' => basename($new_file_path),
4069 'size_kb' => round(filesize($new_file_path) / 1024, 1)
4070 ]);
4071
4072 // If max filesize is set and file is too large, reduce quality iteratively
4073 if ($max_filesize > 0) {
4074 $current_size = filesize($new_file_path);
4075 $quality = $compression;
4076 $attempts = 0;
4077 $max_attempts = 5;
4078
4079 botwriter_log('Image post-processing: Checking filesize target', [
4080 'current_kb' => round($current_size / 1024, 1),
4081 'target_kb' => $max_filesize / 1024,
4082 'needs_compression' => $current_size > $max_filesize
4083 ]);
4084
4085 while ($current_size > $max_filesize && $quality > 40 && $attempts < $max_attempts) {
4086 $quality -= 10;
4087 $attempts++;
4088
4089 botwriter_log('Image post-processing: Compression attempt', [
4090 'attempt' => $attempts,
4091 'quality' => $quality
4092 ]);
4093
4094 // Reload and resave with lower quality
4095 $editor = wp_get_image_editor($new_file_path);
4096 if (!is_wp_error($editor)) {
4097 $editor->set_quality($quality);
4098 $saved = $editor->save($new_file_path, $mime_type);
4099 if (!is_wp_error($saved)) {
4100 $current_size = filesize($saved['path']);
4101 $new_file_path = $saved['path'];
4102 }
4103 }
4104 }
4105
4106 if ($attempts > 0) {
4107 botwriter_log('Image post-processing: Compressed for filesize target', [
4108 'target_kb' => $max_filesize / 1024,
4109 'final_kb' => round($current_size / 1024, 1),
4110 'final_quality' => $quality,
4111 'attempts' => $attempts
4112 ]);
4113 }
4114 }
4115
4116 // Delete original file if format changed
4117 if ($new_file_path !== $file_path && file_exists($file_path) && file_exists($new_file_path)) {
4118 wp_delete_file($file_path);
4119 botwriter_log('Image post-processing: Converted format', [
4120 'from' => $path_info['extension'],
4121 'to' => $new_extension,
4122 'new_size_kb' => round(filesize($new_file_path) / 1024, 1)
4123 ]);
4124 }
4125
4126 // Final summary log
4127 $final_size = filesize($new_file_path);
4128 botwriter_log('Image post-processing: Complete', [
4129 'original_file' => basename($file_path),
4130 'final_file' => basename($new_file_path),
4131 'original_size_kb' => round($original_size / 1024, 1),
4132 'final_size_kb' => round($final_size / 1024, 1),
4133 'size_reduction_percent' => round((1 - ($final_size / $original_size)) * 100, 1)
4134 ]);
4135
4136 return $new_file_path;
4137 }
4138
4139
4140
4141 add_action('plugins_loaded', 'botwriter_check_update');
4142 function botwriter_check_update() {
4143 // Ensure install date exists for legacy installs
4144 if (get_option('botwriter_install_date') === false) {
4145 update_option('botwriter_install_date', current_time('timestamp'));
4146 }
4147 // Use constant instead of get_plugin_data() to obtain current plugin version
4148 $plugin_version = BOTWRITER_VERSION;
4149
4150 // Get the previously installed version
4151 $version_instalada = get_option('botwriter_version');
4152
4153 if ($version_instalada != $plugin_version) {
4154 botwriter_create_table();
4155
4156 // Insert default templates if none exist (for updates from older versions)
4157 botwriter_insert_all_default_templates();
4158
4159 // Migration: reset 'custom' provider to defaults (removed in this version)
4160 if (get_option('botwriter_text_provider') === 'custom') {
4161 update_option('botwriter_text_provider', 'openai');
4162 }
4163 if (get_option('botwriter_image_provider') === 'custom') {
4164 update_option('botwriter_image_provider', 'dalle');
4165 }
4166 // Clean up custom provider options
4167 delete_option('botwriter_custom_text_url');
4168 delete_option('botwriter_custom_text_api_key');
4169 delete_option('botwriter_custom_text_model');
4170 delete_option('botwriter_custom_text_timeout');
4171 delete_option('botwriter_custom_image_url');
4172 delete_option('botwriter_custom_image_type');
4173 delete_option('botwriter_custom_image_model');
4174 delete_option('botwriter_custom_image_timeout');
4175
4176 // Drop direct mode table if it exists
4177 global $wpdb;
4178 $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}botwriter_direct_tasks");
4179
4180 update_option('botwriter_version', $plugin_version); // Update version in database
4181 }
4182 }
4183
4184
4185
4186 // new super functions
4187 add_action('wp_ajax_botwriter_actualizar_articulo', 'botwriter_actualizar_articulo_callback');
4188
4189 function botwriter_actualizar_articulo_callback() {
4190 // Verify user has permission
4191 if (!current_user_can('manage_options')) {
4192 wp_send_json_error('Permission denied');
4193 }
4194
4195 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
4196 global $wpdb;
4197 $table_name = $wpdb->prefix . 'botwriter_super';
4198
4199 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
4200 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
4201 $content = isset($_POST['content']) ? sanitize_textarea_field(wp_unslash($_POST['content'])) : '';
4202
4203 $result = $wpdb->update(
4204 $table_name,
4205 array('title' => $title, 'content' => $content),
4206 array('id' => $id)
4207 );
4208
4209 if ($result !== false) {
4210 wp_send_json_success('Artículo actualizado correctamente.');
4211 } else {
4212 wp_send_json_error('Error al actualizar el artículo.');
4213 }
4214
4215 wp_die();
4216 }
4217
4218 add_action('wp_ajax_botwriter_eliminar_articulo', 'botwriter_eliminar_articulo_callback');
4219
4220 function botwriter_eliminar_articulo_callback() {
4221 // Verify user has permission
4222 if (!current_user_can('manage_options')) {
4223 wp_send_json_error('Permission denied');
4224 }
4225
4226 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
4227 global $wpdb;
4228 $table_name = $wpdb->prefix . 'botwriter_super';
4229
4230 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
4231
4232 $result = $wpdb->delete($table_name, array('id' => $id));
4233 // consultar cuantos registros con id_task=0
4234 $result = $wpdb->get_results("SELECT * FROM $table_name WHERE id_task='0'");
4235 $num_rows = count($result);
4236 if ($num_rows == 0) {
4237 // borramos en la tabla logs la tarea super1 si no hay articulos
4238 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
4239 $wpdb->delete($table_name_logs, array('website_type' => 'super1'));
4240 }
4241
4242
4243
4244 if ($result !== false) {
4245 wp_send_json_success('Artículo eliminado correctamente.');
4246 } else {
4247 wp_send_json_error('Error al eliminar el artículo.');
4248 }
4249
4250 wp_die();
4251 }
4252
4253 add_action('wp_ajax_botwriter_check_super1', 'botwriter_check_super1_callback');
4254
4255 function botwriter_check_super1_callback() {
4256 // Verify user has permission
4257 if (!current_user_can('manage_options')) {
4258 wp_send_json_error('Permission denied');
4259 }
4260
4261 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
4262 $estado_super1=botwriter_super1_check_task_finish();
4263 if ($estado_super1=="completed") {
4264 $response = botwriter_super1_view_articles_html();
4265 wp_send_json_success($response);
4266 return;
4267 }
4268
4269 if ($estado_super1=="error") {
4270 // borramos la tarea super1
4271 global $wpdb;
4272 $table_name = $wpdb->prefix . 'botwriter_super';
4273 $wpdb->delete($table_name, array('id_task' => 0));
4274 wp_send_json_error("error");
4275 return;
4276 }
4277 wp_send_json_error('inqueue');
4278
4279
4280 }
4281
4282
4283 add_action('wp_ajax_botwriter_create_super1', 'botwriter_create_super1_callback');
4284
4285 function botwriter_create_super1_callback() {
4286 // Verify user has permission
4287 if (!current_user_can('manage_options')) {
4288 wp_send_json_error('Permission denied');
4289 }
4290
4291 check_ajax_referer('botwriter_super_nonce'); // Validamos el nonce para seguridad
4292 if (!isset($_POST['prompt']) || !isset($_POST['numarticles'])) {
4293 botwriter_log('Super1 creation request missing parameters');
4294 wp_send_json_error('Missing required parameters.');
4295 wp_die();
4296 }
4297
4298 $prompt = sanitize_text_field(wp_unslash($_POST['prompt']));
4299 $numarticles = intval(wp_unslash($_POST['numarticles']));
4300 if ($prompt=="Custom") {
4301 $category_id = isset($_POST['category_id']) ? sanitize_text_field(wp_unslash($_POST['category_id'])) : '0';
4302 }
4303
4304 botwriter_log('Super1 creation request received', [
4305 'prompt' => $prompt,
4306 'num_articles' => $numarticles,
4307 'category_id' => isset($category_id) ? $category_id : null,
4308 ]);
4309
4310 $title_prompt = $prompt;
4311
4312 if ($prompt=="Custom") {
4313 $content_prompt = sanitize_text_field(wp_unslash($_POST['custom_prompt']));
4314 } else {
4315 $info_blog = botwriter_get_info_blog();
4316 $json_info_blog = json_encode($info_blog, JSON_PRETTY_PRINT);
4317 $content_prompt = $json_info_blog;
4318 }
4319 $task_name = "Super1 Task " . current_time('Y-m-d H:i:s');
4320 $log_id = botwriter_super1_create_task($task_name, $title_prompt,$content_prompt, $numarticles, $category_id);
4321
4322 botwriter_log('Super1 task queued', [
4323 'log_id' => $log_id,
4324 'title_prompt' => $title_prompt,
4325 'num_articles' => $numarticles,
4326 ]);
4327
4328 wp_send_json_success("Task created successfully: " . $title_prompt . " " . $content_prompt . " " . $numarticles . " " . $category_id);
4329
4330
4331 }
4332
4333
4334 add_action('wp_ajax_botwriter_eliminar_super1', 'botwriter_eliminar_super1_y_logs0');
4335
4336 function botwriter_eliminar_super1_y_logs0() {
4337 // Verify user has permission
4338 if (!current_user_can('manage_options')) {
4339 wp_send_json_error('Permission denied');
4340 }
4341
4342 check_ajax_referer('botwriter_super_nonce');
4343 global $wpdb;
4344 $table_name = $wpdb->prefix . 'botwriter_super';
4345 $table_name_logs = $wpdb->prefix . 'botwriter_logs';
4346
4347 $result = $wpdb->delete($table_name, array('id_task' => 0));
4348 $result = $wpdb->delete($table_name_logs, array('website_type' => 'super1'));
4349
4350 if ($result !== false) {
4351 wp_send_json_success('Super1 task deleted successfully.');
4352 } else {
4353 wp_send_json_error('Error');
4354 }
4355 wp_die();
4356 }
4357
4358 add_action('wp_ajax_botwriter_create_super1_manual', 'botwriter_create_super1_manual_callback');
4359
4360 function botwriter_create_super1_manual_callback() {
4361 // Verify user has permission
4362 if (!current_user_can('manage_options')) {
4363 wp_send_json_error('Permission denied');
4364 }
4365
4366 check_ajax_referer('botwriter_super_nonce');
4367
4368 if (!isset($_POST['manual_titles']) || empty(trim(wp_unslash($_POST['manual_titles'])))) {
4369 wp_send_json_error('No titles provided.');
4370 wp_die();
4371 }
4372
4373 $raw_titles = sanitize_textarea_field(wp_unslash($_POST['manual_titles']));
4374 $global_prompt = isset($_POST['global_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['global_prompt'])) : '';
4375 $category_id = isset($_POST['category_id']) ? intval(wp_unslash($_POST['category_id'])) : 0;
4376
4377 // Split titles by newline and filter empty lines
4378 $titles = array_filter(array_map('trim', explode("\n", $raw_titles)), function($t) {
4379 return !empty($t);
4380 });
4381
4382 if (empty($titles)) {
4383 wp_send_json_error('No valid titles found.');
4384 wp_die();
4385 }
4386
4387 // Limit to 100 titles max
4388 if (count($titles) > 100) {
4389 $titles = array_slice($titles, 0, 100);
4390 }
4391
4392 botwriter_log('Manual Super1 creation request', [
4393 'num_titles' => count($titles),
4394 'category_id' => $category_id,
4395 'has_global_prompt' => !empty($global_prompt),
4396 ]);
4397
4398 global $wpdb;
4399
4400 // 1. Create a completed super1 log entry (bypassing AI generation)
4401 $log_data = array(
4402 'task_name' => 'Manual Super1 Task ' . current_time('Y-m-d H:i:s'),
4403 'task_status' => 'completed',
4404 'website_type' => 'super1',
4405 'title_prompt' => 'Manual',
4406 'content_prompt' => $global_prompt,
4407 'post_count' => count($titles),
4408 'category_id' => $category_id,
4409 );
4410 $log_id = botwriter_logs_register($log_data);
4411
4412 if (!$log_id) {
4413 wp_send_json_error('Error creating log entry.');
4414 wp_die();
4415 }
4416
4417 // 2. Insert each title into wp_botwriter_super
4418 $table_name = $wpdb->prefix . 'botwriter_super';
4419 $category_name = '';
4420 if ($category_id > 0) {
4421 $category_name = get_cat_name($category_id);
4422 }
4423
4424 foreach ($titles as $title) {
4425 $data = array(
4426 'title' => sanitize_text_field($title),
4427 'content' => $global_prompt,
4428 'category_name' => $category_name,
4429 'category_id' => $category_id,
4430 'id_log' => $log_id,
4431 'id_task' => 0, // draft, will be assigned on save
4432 'task_status' => '',
4433 );
4434 $wpdb->insert($table_name, $data);
4435 }
4436
4437 botwriter_log('Manual Super1 titles inserted', [
4438 'log_id' => $log_id,
4439 'count' => count($titles),
4440 ]);
4441
4442 // 3. Return the articles HTML for immediate review
4443 $html = botwriter_super1_view_articles_html(0);
4444 wp_send_json_success($html);
4445 }
4446
4447
4448 // ========================================
4449 // CONTENT REWRITER AJAX HANDLERS
4450 // ========================================
4451 add_action('wp_ajax_botwriter_rewriter_fetch', 'botwriter_rewriter_fetch_ajax');
4452 add_action('wp_ajax_botwriter_rewriter_create_task', 'botwriter_rewriter_create_task_ajax');
4453
4454 /**
4455 * AJAX: Fetch and extract content from URLs
4456 */
4457 function botwriter_rewriter_fetch_ajax() {
4458 check_ajax_referer('botwriter_rewriter_nonce');
4459
4460 if (!current_user_can('manage_options')) {
4461 wp_send_json_error(__('Permission denied.', 'botwriter'));
4462 }
4463
4464 $urls = isset($_POST['urls']) ? array_map('esc_url_raw', $_POST['urls']) : array();
4465
4466 if (empty($urls)) {
4467 wp_send_json_error(__('No URLs provided.', 'botwriter'));
4468 }
4469
4470 if (count($urls) > 20) {
4471 wp_send_json_error(__('Maximum 20 URLs allowed.', 'botwriter'));
4472 }
4473
4474 $articles = array();
4475 $errors = array();
4476
4477 foreach ($urls as $url) {
4478 if (empty($url)) {
4479 continue;
4480 }
4481
4482 $result = botwriter_rewriter_extract_content($url);
4483
4484 if (is_wp_error($result)) {
4485 $errors[] = array(
4486 'url' => $url,
4487 'error' => $result->get_error_message(),
4488 );
4489 } else {
4490 $articles[] = $result;
4491 }
4492 }
4493
4494 wp_send_json_success(array(
4495 'articles' => $articles,
4496 'errors' => $errors,
4497 ));
4498 }
4499
4500 /**
4501 * AJAX: Create a Super Task with the articles to rewrite.
4502 *
4503 * Inserts the task as an active super2 task and stores each article
4504 * in the botwriter_super table. The cron loop picks them up via
4505 * botwriter_super_prepare_event() just like normal Super Tasks.
4506 */
4507 function botwriter_rewriter_create_task_ajax() {
4508 check_ajax_referer('botwriter_rewriter_nonce');
4509
4510 if (!current_user_can('manage_options')) {
4511 wp_send_json_error(__('Permission denied.', 'botwriter'));
4512 }
4513
4514 // wp_unslash only — sanitize_text_field corrupts JSON (strips tags/newlines)
4515 $articles_json = isset($_POST['articles']) ? wp_unslash($_POST['articles']) : '';
4516 $rewrite_prompt = isset($_POST['rewrite_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['rewrite_prompt'])) : '';
4517 $category_id = isset($_POST['category_id']) ? intval($_POST['category_id']) : 0;
4518
4519 // Task properties from Step 3 form
4520 $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : 'draft';
4521 $post_language = isset($_POST['post_language']) ? sanitize_text_field(wp_unslash($_POST['post_language'])) : substr(get_locale(), 0, 2);
4522 $author_selection = isset($_POST['author_selection']) ? sanitize_text_field(wp_unslash($_POST['author_selection'])) : strval(get_current_user_id());
4523 $post_length = isset($_POST['post_length']) ? sanitize_text_field(wp_unslash($_POST['post_length'])) : '800';
4524 $custom_post_length = isset($_POST['custom_post_length']) ? sanitize_text_field(wp_unslash($_POST['custom_post_length'])) : '';
4525 $template_id = isset($_POST['template_id']) && !empty($_POST['template_id']) ? intval($_POST['template_id']) : null;
4526 $disable_ai_images = isset($_POST['disable_ai_images']) ? intval($_POST['disable_ai_images']) : 0;
4527 $days = isset($_POST['days']) ? sanitize_text_field(wp_unslash($_POST['days'])) : 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday';
4528 $times_per_day = isset($_POST['times_per_day']) ? intval($_POST['times_per_day']) : 1;
4529 $task_name_custom = isset($_POST['task_name']) ? sanitize_text_field(wp_unslash($_POST['task_name'])) : '';
4530
4531 // Validate post_status
4532 if (!in_array($post_status, array('draft', 'publish'), true)) {
4533 $post_status = 'draft';
4534 }
4535
4536 $articles = json_decode($articles_json, true);
4537 if (!is_array($articles) || empty($articles)) {
4538 wp_send_json_error(__('No articles provided.', 'botwriter'));
4539 }
4540
4541 global $wpdb;
4542 $tasks_table = $wpdb->prefix . 'botwriter_tasks';
4543 $super_table = $wpdb->prefix . 'botwriter_super';
4544
4545 // Build default rewrite prompt if user didn't provide one
4546 if (empty($rewrite_prompt)) {
4547 $rewrite_prompt = 'Rewrite this article completely in your own words while preserving all key information. Make it original, engaging, and well-structured.';
4548 }
4549
4550 // Count valid articles (filter empties before inserting)
4551 $valid_articles = array();
4552 foreach ($articles as $article) {
4553 $title = sanitize_text_field($article['title'] ?? '');
4554 $content = wp_kses_post($article['content'] ?? '');
4555 if (!empty($title) || !empty($content)) {
4556 $valid_articles[] = array('title' => $title, 'content' => $content);
4557 }
4558 }
4559
4560 if (empty($valid_articles)) {
4561 wp_send_json_error(__('All articles are empty.', 'botwriter'));
4562 }
4563
4564 $task_name = !empty($task_name_custom) ? $task_name_custom : 'Rewriter: ' . count($valid_articles) . ' articles - ' . wp_date('M j, Y H:i');
4565
4566 // Use the days and times_per_day from the form
4567 $wpdb->insert($tasks_table, array(
4568 'task_name' => $task_name,
4569 'post_status' => $post_status,
4570 'writer' => 'ai_cerebro',
4571 'narration' => 'Descriptive',
4572 'custom_style' => '',
4573 'post_language' => $post_language,
4574 'post_length' => $post_length,
4575 'custom_post_length' => $custom_post_length,
4576 'days' => $days,
4577 'times_per_day' => $times_per_day > 0 ? $times_per_day : 1,
4578 'status' => 1,
4579 'website_type' => 'super2',
4580 'task_type' => 'rewriter',
4581 'domain_name' => get_site_url(),
4582 'category_id' => $category_id > 0 ? strval($category_id) : '',
4583 'title_prompt' => '',
4584 'content_prompt' => $rewrite_prompt,
4585 'tags_prompt' => '',
4586 'image_prompt' => '',
4587 'aigenerated_title' => '',
4588 'aigenerated_content' => '',
4589 'aigenerated_tags' => '',
4590 'aigenerated_image' => '',
4591 'ai_keywords' => '',
4592 'author_selection' => $author_selection,
4593 'disable_ai_images' => $disable_ai_images,
4594 'template_id' => $template_id,
4595 ));
4596
4597 $task_id = $wpdb->insert_id;
4598
4599 if (!$task_id) {
4600 wp_send_json_error(__('Failed to create task.', 'botwriter'));
4601 }
4602
4603 // Insert each article into the super table.
4604 // Only the article content goes here — rewrite instructions stay in the task's content_prompt.
4605 // super_prepare_event() preserves the rewrite_prompt, and
4606 // botwriter_build_client_prompt() outputs it BEFORE the ENDARTICLE-wrapped content.
4607 $inserted = 0;
4608 foreach ($valid_articles as $art) {
4609 $wpdb->insert($super_table, array(
4610 'id_task' => $task_id,
4611 'id_log' => 0,
4612 'title' => $art['title'],
4613 'content' => $art['content'],
4614 ));
4615 // task_status left NULL — super_prepare_event picks rows with NULL/empty task_status
4616 $inserted++;
4617 }
4618
4619 botwriter_log('Content Rewriter: Task created', [
4620 'task_id' => $task_id,
4621 'article_count' => $inserted,
4622 ]);
4623
4624 wp_send_json_success(array(
4625 'task_id' => $task_id,
4626 'count' => $inserted,
4627 'edit_url' => wp_nonce_url(admin_url('admin.php?page=botwriter_super_page&id=' . $task_id), 'botwriter_tasks_action'),
4628 ));
4629 }
4630
4631 // ========================================
4632 // SITE REWRITER AJAX HANDLERS
4633 // ========================================
4634 add_action('wp_ajax_botwriter_siterewriter_crawl', 'botwriter_siterewriter_crawl_ajax');
4635 add_action('wp_ajax_botwriter_siterewriter_fetch', 'botwriter_siterewriter_fetch_ajax');
4636 add_action('wp_ajax_botwriter_siterewriter_create_task', 'botwriter_siterewriter_create_task_ajax');
4637
4638 /**
4639 * AJAX: Crawl a single page — returns title + internal links.
4640 * The JS manages the BFS queue for live/progressive UI updates.
4641 */
4642 function botwriter_siterewriter_crawl_ajax() {
4643 check_ajax_referer('botwriter_siterewriter_nonce');
4644
4645 if (!current_user_can('manage_options')) {
4646 wp_send_json_error(__('Permission denied.', 'botwriter'));
4647 }
4648
4649 $url = isset($_POST['url']) ? esc_url_raw(wp_unslash($_POST['url'])) : '';
4650 $base_domain = isset($_POST['base_domain']) ? sanitize_text_field(wp_unslash($_POST['base_domain'])) : '';
4651
4652 if (empty($url) || empty($base_domain)) {
4653 wp_send_json_error(__('Missing URL or domain.', 'botwriter'));
4654 }
4655
4656 $result = botwriter_siterewriter_crawl_page($url, $base_domain);
4657
4658 if (is_wp_error($result)) {
4659 wp_send_json_error($result->get_error_message());
4660 }
4661
4662 wp_send_json_success($result);
4663 }
4664
4665 /**
4666 * AJAX: Fetch and extract content from selected URLs.
4667 * Reuses botwriter_rewriter_extract_content() for full content extraction.
4668 */
4669 function botwriter_siterewriter_fetch_ajax() {
4670 check_ajax_referer('botwriter_siterewriter_nonce');
4671
4672 if (!current_user_can('manage_options')) {
4673 wp_send_json_error(__('Permission denied.', 'botwriter'));
4674 }
4675
4676 @set_time_limit(0);
4677
4678 $urls = isset($_POST['urls']) ? array_map('esc_url_raw', $_POST['urls']) : array();
4679
4680 if (empty($urls)) {
4681 wp_send_json_error(__('No URLs provided.', 'botwriter'));
4682 }
4683
4684 $articles = array();
4685 $errors = array();
4686
4687 foreach ($urls as $url) {
4688 if (empty($url)) continue;
4689
4690 $result = botwriter_rewriter_extract_content($url);
4691
4692 if (is_wp_error($result)) {
4693 $errors[] = array(
4694 'url' => $url,
4695 'error' => $result->get_error_message(),
4696 );
4697 } else {
4698 $articles[] = $result;
4699 }
4700 }
4701
4702 wp_send_json_success(array(
4703 'articles' => $articles,
4704 'errors' => $errors,
4705 ));
4706 }
4707
4708 /**
4709 * AJAX: Create a Super Task with the articles to rewrite.
4710 * Identical flow to Content Rewriter but with task_type = 'siterewriter'.
4711 */
4712 function botwriter_siterewriter_create_task_ajax() {
4713 check_ajax_referer('botwriter_siterewriter_nonce');
4714
4715 if (!current_user_can('manage_options')) {
4716 wp_send_json_error(__('Permission denied.', 'botwriter'));
4717 }
4718
4719 $articles_json = isset($_POST['articles']) ? wp_unslash($_POST['articles']) : '';
4720 $rewrite_prompt = isset($_POST['rewrite_prompt']) ? sanitize_textarea_field(wp_unslash($_POST['rewrite_prompt'])) : '';
4721 $category_id = isset($_POST['category_id']) ? intval($_POST['category_id']) : 0;
4722
4723 $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : 'draft';
4724 $post_language = isset($_POST['post_language']) ? sanitize_text_field(wp_unslash($_POST['post_language'])) : substr(get_locale(), 0, 2);
4725 $author_selection = isset($_POST['author_selection']) ? sanitize_text_field(wp_unslash($_POST['author_selection'])) : strval(get_current_user_id());
4726 $post_length = isset($_POST['post_length']) ? sanitize_text_field(wp_unslash($_POST['post_length'])) : '800';
4727 $custom_post_length = isset($_POST['custom_post_length'])? sanitize_text_field(wp_unslash($_POST['custom_post_length'])): '';
4728 $template_id = isset($_POST['template_id']) && !empty($_POST['template_id']) ? intval($_POST['template_id']) : null;
4729 $disable_ai_images = isset($_POST['disable_ai_images']) ? intval($_POST['disable_ai_images']) : 0;
4730 $days = isset($_POST['days']) ? sanitize_text_field(wp_unslash($_POST['days'])) : 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday';
4731 $times_per_day = isset($_POST['times_per_day']) ? intval($_POST['times_per_day']) : 1;
4732 $task_name_custom = isset($_POST['task_name']) ? sanitize_text_field(wp_unslash($_POST['task_name'])) : '';
4733
4734 if (!in_array($post_status, array('draft', 'publish'), true)) {
4735 $post_status = 'draft';
4736 }
4737
4738 $articles = json_decode($articles_json, true);
4739 if (!is_array($articles) || empty($articles)) {
4740 wp_send_json_error(__('No articles provided.', 'botwriter'));
4741 }
4742
4743 global $wpdb;
4744 $tasks_table = $wpdb->prefix . 'botwriter_tasks';
4745 $super_table = $wpdb->prefix . 'botwriter_super';
4746
4747 if (empty($rewrite_prompt)) {
4748 $rewrite_prompt = 'Rewrite this article completely in your own words while preserving all key information. Make it original, engaging, and well-structured.';
4749 }
4750
4751 $valid_articles = array();
4752 foreach ($articles as $article) {
4753 $title = sanitize_text_field($article['title'] ?? '');
4754 $content = wp_kses_post($article['content'] ?? '');
4755 if (!empty($title) || !empty($content)) {
4756 $valid_articles[] = array('title' => $title, 'content' => $content);
4757 }
4758 }
4759
4760 if (empty($valid_articles)) {
4761 wp_send_json_error(__('All articles are empty.', 'botwriter'));
4762 }
4763
4764 $task_name = !empty($task_name_custom)
4765 ? $task_name_custom
4766 : 'Site Rewriter: ' . count($valid_articles) . ' articles - ' . wp_date('M j, Y H:i');
4767
4768 $wpdb->insert($tasks_table, array(
4769 'task_name' => $task_name,
4770 'post_status' => $post_status,
4771 'writer' => 'ai_cerebro',
4772 'narration' => 'Descriptive',
4773 'custom_style' => '',
4774 'post_language' => $post_language,
4775 'post_length' => $post_length,
4776 'custom_post_length' => $custom_post_length,
4777 'days' => $days,
4778 'times_per_day' => $times_per_day > 0 ? $times_per_day : 1,
4779 'status' => 1,
4780 'website_type' => 'super2',
4781 'task_type' => 'siterewriter',
4782 'domain_name' => get_site_url(),
4783 'category_id' => $category_id > 0 ? strval($category_id) : '',
4784 'title_prompt' => '',
4785 'content_prompt' => $rewrite_prompt,
4786 'tags_prompt' => '',
4787 'image_prompt' => '',
4788 'aigenerated_title' => '',
4789 'aigenerated_content' => '',
4790 'aigenerated_tags' => '',
4791 'aigenerated_image' => '',
4792 'ai_keywords' => '',
4793 'author_selection' => $author_selection,
4794 'disable_ai_images' => $disable_ai_images,
4795 'template_id' => $template_id,
4796 ));
4797
4798 $task_id = $wpdb->insert_id;
4799
4800 if (!$task_id) {
4801 wp_send_json_error(__('Failed to create task.', 'botwriter'));
4802 }
4803
4804 // Insert each article into the super table.
4805 // Only the article content goes here — rewrite instructions stay in the task's content_prompt.
4806 // super_prepare_event() preserves the rewrite_prompt, and
4807 // botwriter_build_client_prompt() outputs it BEFORE the ENDARTICLE-wrapped content.
4808 $inserted = 0;
4809 foreach ($valid_articles as $art) {
4810 $wpdb->insert($super_table, array(
4811 'id_task' => $task_id,
4812 'id_log' => 0,
4813 'title' => $art['title'],
4814 'content' => $art['content'],
4815 ));
4816 $inserted++;
4817 }
4818
4819 botwriter_log('Site Rewriter: Task created', [
4820 'task_id' => $task_id,
4821 'article_count' => $inserted,
4822 ]);
4823
4824 wp_send_json_success(array(
4825 'task_id' => $task_id,
4826 'count' => $inserted,
4827 'edit_url' => wp_nonce_url(admin_url('admin.php?page=botwriter_super_page&id=' . $task_id), 'botwriter_tasks_action'),
4828 ));
4829 }
4830
4831 ?>
4832