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

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

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