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

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

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