translator = new polylai_Translator(); } /** * Count posts based on post type, source language and translation status */ public function count_posts() { // Verify nonce for security if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'polylai_settings_nonce')) { wp_send_json_error(['message' => 'Invalid security token.']); exit; } // Get parameters $post_type = isset($_POST['post_type']) ? sanitize_text_field($_POST['post_type']) : ''; $translate_from = isset($_POST['translate_from']) ? sanitize_text_field($_POST['translate_from']) : ''; $only_missing = isset($_POST['only_missing']) ? (bool)$_POST['only_missing'] : false; $categories = isset($_POST['categories']) ? sanitize_text_field($_POST['categories']) : ''; $category_array = ! empty( $categories ) ? array_map( 'intval', explode( ',', $categories ) ) : []; if (empty($post_type)) { wp_send_json_error(['message' => 'Post type is required.']); exit; } if (empty($translate_from)) { wp_send_json_error(['message' => 'Source language is required.']); exit; } // Make sure post type exists and is valid $post_types = get_post_types(['public' => true]); if (!in_array($post_type, $post_types)) { wp_send_json_error(['message' => 'Invalid post type.']); exit; } // Get languages from Polylang $languages = []; if (function_exists('pll_languages_list')) { $languages = pll_languages_list(); } if (empty($languages)) { wp_send_json_error(['message' => 'No languages found.']); exit; } // Query parameters for posts in the selected source language $args = [ 'post_type' => $post_type, 'posts_per_page' => -1, // Get all posts 'post_status' => 'publish', 'fields' => 'ids', // Only get post IDs for efficiency 'lang' => $translate_from, // Filter by source language 'category__in' => $category_array, // Filter by selected categories ]; // If only missing translations requested, we'll filter the results $post_count = 0; $posts = get_posts($args); $post_count = count($posts); wp_send_json_success(['count' => $post_count]); exit; } public function check_cloud() { if (isset($_GET['cloud']) && $_GET['cloud'] == '1') { $this->update_cloud_status(); } } public function trigger() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } try { $this->update_cloud_status(); $has_cron = get_transient(polylai_Cron::LOCK_TRANSIENT_KEY); if (!$has_cron && polylai_Cron::getInstance()->check_for_posts()) { polylai_Utils::db_log('debug', 'progress', "launching cron"); $exec_url = home_url('/index.php?action=polylai_run_cron'); wp_remote_post($exec_url, [ 'timeout' => 0.01, 'blocking' => false, 'headers' => [ 'Connection' => 'Close', 'Contect-length' => 0 ] ]); } else { polylai_Utils::db_log('debug', 'progress', "no posts to translate"); } } catch (Exception $e) { polylai_Utils::db_log('error', 'trigger', $e->getMessage()); wp_send_json_error(['message' => $e->getMessage()]); wp_die(); } } public function update_cloud_status() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } $license = get_option('polylai_credits_license'); if (!$license) { return; } $running = get_transient(POLYLAI_CLOUD_TRANSIENT); if ($running) { polylai_Utils::db_log( 'debug', 'update_cloud_status', "already running" ); return; } set_transient(POLYLAI_CLOUD_TRANSIENT, '1', 2); /// TODO only fetch cloud pending $results = polylai_Manager::getInstance()->list_pending_translating(); $guids = []; foreach ($results as $item) { if ($item->cloud_guid) { $guids[] = $item->cloud_guid; } } if (count($guids) == 0) { return; } polylai_Utils::db_log( 'debug', 'update_cloud_status', "found " . count($results) . " to update" ); $statusRequest = new polylai_CloudStatus(); $statusRequest->license_key = get_option('polylai_credits_license'); $statusRequest->guids = $guids; $status = polylai_Cloud::getStatus($statusRequest); polylai_Utils::db_log( 'debug', 'update_cloud_status', "get status", null, null, json_encode($status) ); foreach ($status as $item) { $postTranslation = polylai_Manager::getInstance()->get_from_list($results, $item->guid); $post_id = $item->post_id; polylai_Utils::db_log( 'debug', 'update_cloud_status', "get post id", $post_id, null, $item->used_tokens ); polylai_Manager::getInstance()->set_status( $postTranslation->id, $item->status, $item->error_code, $item->used_tokens, $item->translations ); if ($item->status == "complete") { $post_title = ""; $post_content = ""; $post_excerpt = ""; $meta = []; foreach ($item->translations as $translation) { if ($translation->field == "post_title") { $post_title = $translation->translated_text; } if ($translation->field == "post_content") { $post_content = $translation->translated_text; } if ($translation->field == "post_excerpt") { $post_excerpt = $translation->translated_text; } if ($translation->field == "meta") { $meta_key = $translation->meta_key; $meta[$meta_key] = $translation->translated_text; } } $meta = array_merge($meta, polylai_ACF::copy_meta($post_id)); polylai_Utils::db_log( 'debug', 'update_cloud_status', "pre create translation", $post_id, $post_title ); $duplicator = new polylai_Duplicator(); $new_post_id = $duplicator->duplicate($post_id, $post_title, $post_content, $post_excerpt, $postTranslation->final_post_status, $meta); $this->translator->create_translation( $post_id, $new_post_id, $item->locale_to, $meta ); try { polylai_Manager::getInstance()->set_completed($postTranslation->id); } catch (\Exception $e) { polylai_Utils::db_log('debug', 'ajax -> error', $e->getMessage()); } } } } private function enqueue_single_translation($id, $locales, $langs, $post_status, $date, $service) { $post = get_post($id); $post_lang = pll_get_post_language($id, 'name'); $index = 0; $locales_arr = explode(",", $locales); $source_post_id = $id; $errors = []; foreach (explode(",", $langs) as $lang) { $data = null; $locale_to = $locales_arr[$index]; $post_data = new polylai_Data(); // $post_data->date = $date; $post_data->lang_from = $post_lang; $post_data->lang_to = $lang; $post_data->service = $service; $post_data->final_post_status = $post_status; $post_data->locale_to = $locale_to; $post_data->source_post_id = $source_post_id; $post_data->used_tokens = 0; $requestError = null; if ($service == 'cloud') { $license_key = get_option('polylai_credits_license'); $cloudRequest = new polylai_CloudRequest($post_lang, $lang, $locale_to, $post, $license_key); $post_data->request_id = $cloudRequest->request_id; // Add meta values to the cloud request $meta_keys = [ '_elementor_data', '_yoast_wpseo_bctitle', '_yoast_wpseo_metadesc', '_yoast_wpseo_focuskw', '_yoast_wpseo_title', 'rank_math_title', 'rank_math_description', 'rank_math_facebook_title', 'rank_math_facebook_description', 'rank_math_twitter_use_facebook', 'rank_math_twitter_title', 'rank_math_twitter_description', 'rank_math_focus_keyword', '_purchase_note', '_product_attributes', '_downloadable_files' ]; foreach ($meta_keys as $meta_key) { $meta_value = get_post_meta($id, $meta_key, true); if (!empty($meta_value)) { $cloudRequest->meta[$meta_key] = $meta_value; } } $response = polylai_Cloud::requestTranslation($cloudRequest); if ($response->success) { $post_data->cloud_guid = $response->meta->guid; $id = polylai_Manager::getInstance()->add($post->post_title, $post->ID, $post_data); } else { polylai_Utils::db_log('error', 'enqueue', "Unable to request translation", $id, $post->post_title, json_encode($response)); $errors[] = [ 'request' => $cloudRequest, 'post' => [ 'ID' => $post->ID, 'title' => $post->post_title, 'data' => $post_data ] ]; } $data = $cloudRequest; } else { $id = polylai_Manager::getInstance()->add($post->post_title, $post->ID, $post_data); if ($requestError) { polylai_Manager::getInstance()->set_status($id, 'error', $requestError); } } $index++; } } public function enqueue_translations() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } $id = intval($_POST['id']); $locales = sanitize_text_field($_POST['locales']); $langs = sanitize_text_field($_POST['localesNames']); $post_status = sanitize_text_field($_POST['status']); // $date = sanitize_text_field($_POST['date']); $service = $_POST['service']; $this->enqueue_single_translation($id, $locales, $langs, $post_status, null, $service); wp_send_json_success(); exit; } public function enqueue_bulk() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } $post_type = isset($_POST['post_type']) ? sanitize_text_field($_POST['post_type']) : ''; $translate_from = isset($_POST['translate_from']) ? sanitize_text_field($_POST['translate_from']) : ''; $locales = isset($_POST['locales']) ? sanitize_text_field($_POST['locales']) : ''; $langs = isset($_POST['localesNames']) ? sanitize_text_field($_POST['localesNames']) : ''; $service = isset($_POST['service']) ? sanitize_text_field(wp_unslash($_POST['service'])) : ''; $post_status = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : ''; $categories = isset($_POST['categories']) ? sanitize_text_field($_POST['categories']) : ''; $category_array = ! empty($categories) ? array_map('intval', explode(',', $categories)) : []; $args = [ 'post_type' => $post_type, 'posts_per_page' => -1, 'post_status' => 'publish', 'fields' => 'ids', 'lang' => $translate_from, 'category__in' => $category_array, ]; $post_ids = get_posts($args); foreach ($post_ids as $post_id) { error_log("enqueue bulk " . $post_id); $this->enqueue_single_translation($post_id, $locales, $langs, $post_status, null, $service); } wp_send_json_success(['count' => count($post_ids)]); wp_die(); } public function add_translation() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } $data = json_decode(stripslashes($_POST['data'])); $id = intval($_POST['ID']); $title = $_POST['title']; polylai_Manager::getInstance()->add($title, $id, $data); wp_die(); } public function list_history() { if (wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['polylai_nonce'])), 'polylai_nonce')) { // Get pagination parameters $page = isset($_REQUEST['page']) ? absint($_REQUEST['page']) : 1; $per_page = isset($_REQUEST['per_page']) ? absint($_REQUEST['per_page']) : 10; // Ensure valid values $page = max(1, $page); $per_page = max(5, min(50, $per_page)); // Get paginated history $result = polylai_Manager::getInstance()->list_history($page, $per_page); header("Content-type: application/json"); echo wp_json_encode([ 'status' => 'OK', 'data' => $result['items'], 'pagination' => [ 'total_pages' => $result['total_pages'], 'total_items' => $result['total_items'], 'current_page' => $result['current_page'], 'per_page' => $per_page ] ]); wp_die(); } } public function dashboard_health() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Insufficient permissions']); wp_die(); } wp_send_json_success([ 'health' => polylai_Manager::getInstance()->get_health_summary(), 'diagnostics' => [ 'local_cron_lock_status' => (bool) get_transient(polylai_Cron::LOCK_TRANSIENT_KEY), 'last_real_cron_activity' => get_transient(polylai_Cron::REAL_CRON_TRANSIENT_KEY) ?: null, 'cloud_status_lock' => (bool) get_transient(POLYLAI_CLOUD_TRANSIENT), 'local_api_key_configured' => (bool) polylai_Utils::has_api_key(), 'cloud_license_configured' => (bool) get_option('polylai_credits_license'), 'latest_error_log' => $this->get_latest_error_log(), ], ]); } private function get_latest_error_log() { global $wpdb; $table_name = $wpdb->prefix . 'polylai_log'; $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table_name)); if ($table_exists !== $table_name) { return null; } $row = $wpdb->get_row( $wpdb->prepare( 'SELECT id, time, type, operation, post_id, post_title, message FROM `' . esc_sql($table_name) . '` WHERE type = %s ORDER BY id DESC LIMIT 1', 'error' ), ARRAY_A ); if (!$row) { return null; } $row['id'] = (int) $row['id']; $row['post_id'] = $row['post_id'] === null ? null : (int) $row['post_id']; return $row; } public function list_pending() { //$this->triggerCron(); if (wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['polylai_nonce'])), 'polylai_nonce')) { // get all posts of type translation-polilay $list = polylai_Manager::getInstance()->list_pending_translating(); header("Content-type: application/json"); echo wp_json_encode([ 'status' => 'OK', 'data' => $list ]); wp_die(); } } /** * Stop all pending and in-progress translations */ public function stop_all_translations() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } try { delete_transient(polylai_Cron::LOCK_TRANSIENT_KEY); delete_transient(polylai_Cron::REAL_CRON_TRANSIENT_KEY); // Get all pending or translating items $translations = polylai_Manager::getInstance()->list_pending_translating(); $count = count($translations); // Delete each translation post foreach ($translations as $translation) { polylai_Manager::getInstance()->set_status($translation->id, 'error', 'cancelled_by_user', 0); } // Log the action polylai_Utils::db_log('info', 'stop_all_translations', "Stopped {$count} translations by user request"); wp_send_json_success(['message' => sprintf(_n('%d translation stopped', '%d translations stopped', $count, 'ai-translator-for-polylang'), $count), 'count' => $count]); } catch (Exception $e) { polylai_Utils::db_log('error', 'stop_all_translations', $e->getMessage()); wp_send_json_error(['message' => $e->getMessage()]); } wp_die(); } public function get_post_tr_status() { if (!isset($_POST['polylai_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['polylai_nonce'])), 'polylai_nonce')) { wp_send_json_error(['message' => 'Invalid nonce']); wp_die(); } // nonce not needed here as it's a read-only action $id = intval($_POST['id']); // [[it] => (id) [en] => (id)] $pll_translations = pll_get_post_translations($id); $translations = []; $completed = []; foreach ($pll_translations as $lang => $translation) { $post = get_post($translation); if ($post->post_status != 'trash') { $completed[] = $lang; } } $pendingTranslating = array_filter( polylai_Manager::getInstance()->list_pending_translating(), function ($item) use ($id) { return $item->source_post_id == $id; } ); $pendingTranslating = array_map(function ($item) { return $item->locale_to; }, $pendingTranslating); // take only values of $pendingTranslating $pendingTranslating = array_values($pendingTranslating); $allowed = polylai_Utils::allowed_langs(); $has_cron = get_transient(polylai_Cron::REAL_CRON_TRANSIENT_KEY); header('Content-type: application/json'); echo wp_json_encode([ 'post_slug' => pll_get_post_language($id, 'slug'), 'post_lang' => pll_get_post_language($id, 'name'), 'translations' => $translations, 'langs' => pll_languages_list(['fields' => 'name']), 'langs_slug' => pll_languages_list(['fields' => 'slug']), 'locales' => pll_the_languages(['raw' => 1, 'hide_if_empty' => 0]), 'completed' => $completed, 'allowed' => $allowed, 'has_cron' => !!$has_cron, 'has_key' => !!polylai_Utils::has_api_key(), 'pending_translating' => $pendingTranslating, 'nonce' => wp_create_nonce('polylai_run_cron') ]); wp_die(); } }