PluginProbe
BotWriter – AI Writer & SEO Content Generator / trunk
BotWriter – AI Writer & SEO Content Generator vtrunk
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 / includes / logs.php

logs.php in BotWriter – AI Writer & SEO Content Generator trunk, at includes/logs.php

548 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 if (!class_exists('WP_List_Table')) {
7 require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
8 }
9
10 function botwriter_logs_page_handler() {
11
12 // Check if the user has the necessary capability
13 if (!current_user_can('manage_options')) {
14 return;
15 }
16
17 if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
18 if (isset($_POST['botwriter_logs_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['botwriter_logs_nonce'])), 'botwriter_logs_action')
19 ) {
20 // The nonce is valid, continue execution
21 } else {
22 wp_die(esc_html__('Security check failed', 'botwriter'));
23 }
24 }
25
26 // Instantiate the logs table class
27 $logs_table = new botwriter_Logs_Table();
28 $logs_table->prepare_items();
29
30 // Display the page
31 echo '<div class="wrap">';
32 echo '<h1>' . esc_html__('BotWriter Logs', 'botwriter') . '</h1>';
33 echo '<div id="countup"></div>';
34
35 echo '<form method="post">';
36 wp_nonce_field('botwriter_logs_action', 'botwriter_logs_nonce');
37 $logs_table->search_box(__('Search logs', 'botwriter'), 'botwriter-logs-search');
38 $logs_table->display();
39 echo '</form>';
40 echo '</div>';
41 }
42
43
44
45 class botwriter_Logs_Table extends WP_List_Table {
46
47
48 private $table_data;
49
50 public function __construct() {
51 parent::__construct(array(
52 'singular' => 'log',
53 'plural' => 'logs',
54 'ajax' => false,
55 ));
56 }
57
58 public function get_columns() {
59 return array(
60 'cb' => '<input type="checkbox" />',
61 'created_at' => __('Created At', 'botwriter'),
62 'writer' => __('Writer', 'botwriter'),
63 'task_name' => __('Task Name', 'botwriter'),
64 'task_status' => __('Task Status', 'botwriter'),
65 'aigenerated_title' => __('AI Generated Title', 'botwriter'),
66 'aigenerated_image' => __('AI Generated Image', 'botwriter'),
67 'id_post_published' => __('Post Published', 'botwriter'),
68 'actions' => __('Actions', 'botwriter'),
69 );
70 }
71
72 public function column_cb($item) {
73 return sprintf(
74 '<input type="checkbox" name="log_ids[]" value="%d" />',
75 intval($item['id'])
76 );
77 }
78
79 public function get_bulk_actions() {
80 return array(
81 'bulk_delete' => __('Delete', 'botwriter'),
82 );
83 }
84
85 public function process_bulk_action() {
86 if ('bulk_delete' === $this->current_action()) {
87 if ( ! isset($_POST['botwriter_logs_nonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['botwriter_logs_nonce'])), 'botwriter_logs_action') ) {
88 return;
89 }
90 if ( ! current_user_can('manage_options') ) {
91 return;
92 }
93 $log_ids = isset($_POST['log_ids']) ? array_map('intval', $_POST['log_ids']) : array();
94 if ( ! empty($log_ids) ) {
95 global $wpdb;
96 $table_name = $wpdb->prefix . 'botwriter_logs';
97 $placeholders = implode(',', array_fill(0, count($log_ids), '%d'));
98 $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE id IN ({$placeholders})", $log_ids));
99 }
100 }
101 }
102
103 public function column_actions($item) {
104 $log_id = intval($item['id']);
105 return '<button type="button" class="button button-small botwriter-delete-log" data-log-id="' . esc_attr($log_id) . '" title="' . esc_attr__('Delete this log', 'botwriter') . '"><span class="dashicons dashicons-trash" style="vertical-align: middle;"></span></button>';
106 }
107
108 function column_writer($item){
109 $dir_images_writers = BOTWRITER_URL . '/assets/images/writers/';
110 $writer=$item['writer'];
111 $writer = strtolower($writer);
112
113
114 $img= '<img src="' . esc_url($dir_images_writers . $writer . '.jpeg') . '" alt="' . esc_attr($writer) . '" class="writer-photo">';
115 return $img;
116
117 }
118
119 // Create a function for the id_post_published column that shows View and Edit buttons
120 function column_id_post_published($item){
121 $id_post_published = $item['id_post_published'];
122 if ($id_post_published == 0 || empty($id_post_published)) {
123 return '<span style="color: #999;">—</span>';
124 } else {
125 $view_link = get_permalink($id_post_published);
126 $edit_link = get_edit_post_link($id_post_published);
127
128 $buttons = '<div style="display: flex; gap: 5px;">';
129
130 // View button
131 $buttons .= '<a href="' . esc_url($view_link) . '" target="_blank" class="button button-small" title="' . esc_attr__('View post', 'botwriter') . '" style="display: inline-flex; align-items: center; gap: 3px;">';
132 $buttons .= '<span class="dashicons dashicons-visibility" style="font-size: 14px; width: 14px; height: 14px;"></span>';
133 $buttons .= '<span>' . esc_html__('View', 'botwriter') . '</span>';
134 $buttons .= '</a>';
135
136 // Edit button
137 $buttons .= '<a href="' . esc_url($edit_link) . '" class="button button-small" title="' . esc_attr__('Edit post', 'botwriter') . '" style="display: inline-flex; align-items: center; gap: 3px;">';
138 $buttons .= '<span class="dashicons dashicons-edit" style="font-size: 14px; width: 14px; height: 14px;"></span>';
139 $buttons .= '<span>' . esc_html__('Edit', 'botwriter') . '</span>';
140 $buttons .= '</a>';
141
142 $buttons .= '</div>';
143
144 return $buttons;
145 }
146 }
147
148
149 function prepare_items()
150 {
151 // Read-only search term from list table request.
152 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only search param for list table.
153 $search_query = isset($_REQUEST['s']) ? sanitize_text_field(wp_unslash($_REQUEST['s'])) : '';
154
155 // Process actions first (e.g., bulk delete), then fetch filtered rows.
156 $this->process_bulk_action();
157 $this->table_data = $this->get_table_data($search_query);
158
159 $columns = $this->get_columns();
160 $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();
161 $sortable = $this->get_sortable_columns();
162 $primary = 'name';
163 $this->_column_headers = array($columns, $hidden, $sortable, $primary);
164
165 usort($this->table_data, array($this, 'usort_reorder'));
166
167 /* pagination */
168 $per_page = $this->get_items_per_page('elements_per_page', 10);
169 $current_page = $this->get_pagenum();
170 $total_items = count($this->table_data);
171
172 $this->table_data = array_slice($this->table_data, (($current_page - 1) * $per_page), $per_page);
173
174
175
176
177 $this->set_pagination_args(array(
178 'total_items' => $total_items, // total number of items
179 'per_page' => $per_page, // items to show on a page
180 'total_pages' => ceil( $total_items / $per_page ) // use ceil to round up
181 ));
182
183 $this->items = $this->table_data;
184 }
185
186 // Get table data
187 private function get_table_data( $search = '' ) {
188 global $wpdb;
189
190
191 $table = $wpdb->prefix."botwriter_logs";
192
193
194 if ( ! empty( $search ) ) {
195 $like = '%' . $wpdb->esc_like( $search ) . '%';
196
197 return $wpdb->get_results(
198 $wpdb->prepare(
199 "SELECT *
200 FROM {$table}
201 WHERE website_type <> %s
202 AND (
203 task_name LIKE %s
204 OR aigenerated_title LIKE %s
205 OR link_post_original LIKE %s
206 OR rss_source LIKE %s
207 OR error LIKE %s
208 OR task_status LIKE %s
209 )",
210 'super1',
211 $like,
212 $like,
213 $like,
214 $like,
215 $like,
216 $like
217 ),
218 ARRAY_A
219 );
220 }
221
222 return $wpdb->get_results(
223 $wpdb->prepare("SELECT * FROM {$table} WHERE website_type <> %s", 'super1'),
224 ARRAY_A
225 );
226 }
227
228
229 // Sorting function
230 function usort_reorder($a, $b)
231 {
232 // If no sort, default to task_name
233 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sort param for list table.
234 $sanitized_orderby = isset($_GET['orderby']) ? sanitize_text_field(wp_unslash($_GET['orderby'])) : '';
235
236 $orderby = (!empty($sanitized_orderby)) ? $sanitized_orderby : 'created_at';
237
238 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sort param for list table.
239 $order = isset($_GET['order']) ? sanitize_text_field(wp_unslash($_GET['order'])) : 'desc ';
240
241 // filtrar order solo asd o desc
242 $order = in_array($order, array('asc', 'desc')) ? $order : 'desc';
243 // filter orderby only allowed columns
244 $orderby = in_array($orderby, array('created_at', 'task_status', 'aigenerated_title')) ? $orderby : 'task_name';
245
246
247
248
249 // Determine sort order
250 $result = strcmp($a[$orderby], $b[$orderby]);
251
252 // Send final sort direction to usort
253 return ($order === 'asc') ? $result : -$result;
254 }
255
256
257 public function column_default($item, $column_name) {
258 // Default handler for columns
259 switch ($column_name) {
260 case 'created_at':
261 case 'task_status':
262 case 'aigenerated_title':
263 return esc_html($item[$column_name]); // Escape output for safety
264 default:
265 return isset($item[$column_name]) ? esc_html($item[$column_name]) : ''; // Fallback for other columns
266 }
267 }
268
269 public function column_task_status($item) {
270 // 4 cases: inqueue, pending, completed, error
271 $task_status = $item['task_status'];
272 $intento_tiempo = array(0=>0,1=>0,2=>5,3=>10,4=>30,5=>60,6=>120,7=>240,8=>480); // minutes
273 $intentosfase1 = $item["intentosfase1"];
274
275 // if it is error show in red and show that it will retry in time * attempts
276 if ($task_status == 'error') {
277 $id_task_server = $item['id_task_server'];
278 $txt= '<span style="color:red;">Error (id server:' . $id_task_server . ')</span>';
279 if ($item["error"]!='') {
280 $txt.= '<br>' . wp_kses_post($item["error"]) . "<br>";
281 }
282 // writenow tasks don't retry, so don't show retry info
283 $task_type = isset($item['task_type']) ? $item['task_type'] : '';
284 if ($intentosfase1 < 8 && $task_type !== 'writenow') { // these are the ones it has taken
285 $tiempo = $intento_tiempo[$intentosfase1+1]; // next attempt
286 $created_at = strtotime($item["created_at"]);
287 $tiempo_siguiente_intento = $created_at + $tiempo*60;
288 $txt.= '<br>Attempt ' . $intentosfase1 . ' of 8';
289 $txt.= '<br>Will retry at ' . gmdate('Y-m-d H:i:s', $tiempo_siguiente_intento);
290 }
291 } else {
292 $txt=esc_html($task_status);
293 }
294
295 return $txt;
296
297 }
298
299 public function column_aigenerated_image($item) {
300 $id_post_published = intval($item['id_post_published'] ?? 0);
301 $image_html = '';
302
303 // check if the post is published
304 if ($id_post_published != 0) {
305 // get the post image url
306 $post_thumbnail_id = get_post_thumbnail_id($id_post_published);
307 $post_thumbnail_url = wp_get_attachment_image_src($post_thumbnail_id, 'thumbnail');
308 if ($post_thumbnail_url) {
309 $image_html = '<img src="' . esc_url($post_thumbnail_url[0]) . '" alt="Post Image" width="50">';
310 }
311 }
312
313 // Render the image or fallback to text if invalid
314 if ($image_html === '' && filter_var($item['aigenerated_image'], FILTER_VALIDATE_URL)) {
315 $image_html = '<img src="' . esc_url($item['aigenerated_image']) . '" alt="Generated Image" width="50">';
316 }
317
318 if ($image_html === '' && !filter_var($item['aigenerated_image'], FILTER_VALIDATE_URL)) {
319 $fallback = trim((string) ($item['aigenerated_image'] ?? ''));
320 $image_html = ($fallback !== '')
321 ? '<span style="font-size:11px;color:#666;">' . esc_html($fallback) . '</span>'
322 : '<span style="color:#999;">—</span>';
323 }
324
325 $edit_link = '';
326 if ($id_post_published > 0) {
327 $edit_link = '<a href="#" class="botwriter-regenerate-image-link" data-post-id="' . esc_attr($id_post_published) . '" title="' . esc_attr__('Edit featured image', 'botwriter') . '" style="display:inline-flex;align-items:center;gap:4px;font-size:12px;margin-top:6px;">'
328 . '<span class="dashicons dashicons-edit" style="font-size:14px;width:14px;height:14px;"></span>'
329 . '<span>' . esc_html__('Edit', 'botwriter') . '</span>'
330 . '</a>';
331 }
332
333 return '<div class="botwriter-log-image-actions" style="display:flex;flex-direction:column;align-items:flex-start;">' . $image_html . $edit_link . '</div>';
334 }
335
336 public function column_task_name($item) {
337 $txt = esc_html($item['task_name'] ?? '');
338
339 if (($item['website_type'] ?? '') === 'super2') {
340 $txt .= '<br><span style="color:blue;">' . esc_html($item['title_prompt']) . '</span>';
341 }
342
343 if (($item['website_type'] ?? '') === 'rss') {
344 $source_url_raw = trim((string) ($item['link_post_original'] ?? ''));
345 $source_label = esc_html__('Source URL', 'botwriter');
346
347 if ($source_url_raw !== '') {
348 $source_url = esc_url($source_url_raw);
349 if ($source_url !== '') {
350 $txt .= '<br><span style="font-size:11px;color:#555;"><strong>' . $source_label . ':</strong> <a href="' . $source_url . '" target="_blank" rel="noopener" style="word-break:break-all;">' . esc_html($source_url_raw) . '</a></span>';
351 } else {
352 $txt .= '<br><span style="font-size:11px;color:#555;"><strong>' . $source_label . ':</strong> ' . esc_html($source_url_raw) . '</span>';
353 }
354 } else {
355 $txt .= '<br><span style="font-size:11px;color:#999;"><strong>' . $source_label . ':</strong> ' . esc_html__('not available yet', 'botwriter') . '</span>';
356 }
357 }
358
359 return $txt;
360 }
361
362
363 public function no_items() {
364 esc_html__('No logs found.', 'botwriter');
365 }
366
367 public function get_sortable_columns() {
368 return array(
369 'created_at' => array('created_at', true),
370
371 'task_status' => array('task_status', false),
372 'aigenerated_title' => array('aigenerated_title', false),
373 );
374 }
375
376
377 }
378
379
380 function botwriter_get_logs_links($id_task) {
381 global $wpdb;
382
383 // Per-task "already-handled" list. Excludes rows that ended in error so a
384 // failed task can be retried on the same source URL, but keeps pending /
385 // in-flight rows so two cron ticks don't pick the same article.
386 $links = $wpdb->get_results($wpdb->prepare(
387 "SELECT link_post_original FROM {$wpdb->prefix}botwriter_logs
388 WHERE id_task = %d
389 AND (task_status IS NULL OR task_status <> 'error')
390 ORDER BY id DESC LIMIT 50",
391 $id_task
392 ), ARRAY_A);
393 $links_array = [];
394 if (empty($links)) {
395 return false;
396 } else {
397 foreach ($links as $link) {
398 if ($link['link_post_original'] != '') {
399 $links_array[] = $link['link_post_original'];
400 }
401 }
402 }
403 return $links_array;
404
405 }
406
407
408 function botwriter_get_logs_titles($id_task) {
409 global $wpdb;
410
411 // Used to feed prompt context (avoid repeating titles). Same status filter
412 // as botwriter_get_logs_links: skip error rows so retries are allowed.
413 $results = $wpdb->get_results($wpdb->prepare(
414 "SELECT aigenerated_title FROM {$wpdb->prefix}botwriter_logs
415 WHERE id_task = %d
416 AND (task_status IS NULL OR task_status <> 'error')
417 ORDER BY id DESC LIMIT 50",
418 $id_task
419 ), ARRAY_A);
420 $titles_array = [];
421 if (empty($results)) {
422 return false;
423 } else {
424 foreach ($results as $result) {
425 if ($result['aigenerated_title'] != '') {
426 $titles_array[] = $result['aigenerated_title'];
427 //echo "<br>title in the db: " . $result['aigenerated_title'];
428 }
429 }
430 }
431 return $titles_array;
432
433 }
434
435
436
437 // Register a log in the botwriter_logs table (insert or update)
438 function botwriter_logs_register($data, $id = null) {
439 global $wpdb;
440 $table_name = $wpdb->prefix . 'botwriter_logs';
441
442 // Validate that $data is an array
443 if (!is_array($data)) {
444 return false; // Or throw an exception as needed
445 }
446
447 // List of allowed keys
448 $allowed_keys = array(
449 'id_task',
450 'id_task_server',
451 'post_status',
452 'task_name',
453 'task_type',
454 'writer',
455 'narration',
456 'custom_style',
457 'post_language',
458 'post_length',
459 'link_post_original',
460 'id_post_published',
461 'task_status',
462 'error',
463 'website_name',
464 'website_type',
465 'domain_name',
466 'post_type',
467 'category_id',
468 'taxonomy_data',
469 'website_category_id',
470 'aigenerated_title',
471 'aigenerated_content',
472 'aigenerated_tags',
473 'aigenerated_image',
474 'post_count',
475 'post_order',
476 'title_prompt',
477 'content_prompt',
478 'tags_prompt',
479 'image_prompt',
480 'image_generating_status',
481 'author_selection',
482 'news_time_published',
483 'news_language',
484 'news_country',
485 'news_keyword',
486 'news_source',
487 'rss_source',
488 'ai_keywords',
489 'disable_ai_images',
490 'template_id',
491 'intentosfase1',
492 'last_execution_time'
493
494 );
495
496 // Create the array with only the existing values in $data
497 $insert_data = array();
498 foreach ($allowed_keys as $key) {
499 if (isset($data[$key])) {
500 $insert_data[$key] = $data[$key];
501 }
502 }
503
504 if ($id) {
505 // Update the existing record
506 $where = array('id' => $id);
507 $updated = $wpdb->update($table_name, $insert_data, $where);
508
509 // If task_status is 'completed', reset the errors notice
510 if (isset($insert_data['task_status']) && $insert_data['task_status'] === 'completed') {
511 if (function_exists('botwriter_reset_errors_notice_dismissed')) {
512 botwriter_reset_errors_notice_dismissed();
513 }
514 }
515
516 // Return the result of the update
517 return $updated !== false ? $id : false;
518 } else {
519 // Insert a new record
520 // Add the creation date
521 $current_time = current_time('mysql');
522 $insert_data['created_at'] = $current_time;
523
524 $wpdb->insert($table_name, $insert_data);
525
526 // If the new log is 'completed', reset the errors notice dismissed flag
527 // This allows the notice to reappear if errors occur again later
528 if (isset($insert_data['task_status']) && $insert_data['task_status'] === 'completed') {
529 if (function_exists('botwriter_reset_errors_notice_dismissed')) {
530 botwriter_reset_errors_notice_dismissed();
531 }
532 }
533
534 // Return the ID of the new record
535 return $wpdb->insert_id;
536 }
537 }
538
539 // Function that given an id returns an array with the data of a log
540 function botwriter_logs_get($id) {
541 global $wpdb;
542 $table_name = $wpdb->prefix . 'botwriter_logs';
543
544 $log = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id), ARRAY_A);
545
546 return $log;
547 }
548