main_keywords_table = $wpdb->prefix . 'aagl_main_keywords'; $this->sub_keywords_table = $wpdb->prefix . 'aagl_sub_keywords'; // Ajax アクション登録 add_action('wp_ajax_aagl_get_keywords', array($this, 'ajax_get_keywords')); add_action('wp_ajax_aagl_add_main_keyword', array($this, 'ajax_add_main_keyword')); add_action('wp_ajax_aagl_update_main_keyword', array($this, 'ajax_update_main_keyword')); add_action('wp_ajax_aagl_delete_main_keyword', array($this, 'ajax_delete_main_keyword')); add_action('wp_ajax_aagl_add_sub_keyword', array($this, 'ajax_add_sub_keyword')); add_action('wp_ajax_aagl_update_sub_keyword', array($this, 'ajax_update_sub_keyword')); add_action('wp_ajax_aagl_get_sub_keywords_for_main', array($this, 'ajax_get_sub_keywords_for_main')); } /** * メインキーワードの追加 * * @param array $data キーワードデータ * @return array 実行結果 */ public function add_main_keyword($data) { global $wpdb; // データバリデーション $validation = $this->validate_main_keyword_data($data); if (!$validation['valid']) { return array( 'success' => false, 'error' => $validation['error'] ); } // 重複チェック $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_main_keywords WHERE keyword = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $validation['data']['keyword'] )); if ($existing > 0) { return array( 'success' => false, 'error' => 'このキーワードは既に登録されています。' ); } // データベースに挿入 $result = $wpdb->insert( $this->main_keywords_table, array( 'keyword' => $validation['data']['keyword'], 'category' => $validation['data']['category'], 'search_volume' => $validation['data']['search_volume'], 'difficulty' => $validation['data']['difficulty'], 'priority' => $validation['data']['priority'], 'status' => $validation['data']['status'], 'notes' => $validation['data']['notes'], 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ), array('%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s', '%s') ); if ($result === false) { AAGL_Utilities::log( 'DB error in add_main_keyword: ' . $wpdb->last_error ); return array( 'success' => false, 'error' => __( 'A database error occurred.', 'aio-agent-article-generator' ) ); } return array( 'success' => true, 'id' => $wpdb->insert_id, 'message' => 'メインキーワードを追加しました。' ); } /** * メインキーワードの更新 * * @param int $id キーワードID * @param array $data 更新データ * @return array 実行結果 */ public function update_main_keyword($id, $data) { global $wpdb; // IDバリデーション $id = intval($id); if ($id <= 0) { return array( 'success' => false, 'error' => '無効なIDです。' ); } // 存在チェック $existing = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aagl_main_keywords WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $id )); if (!$existing) { return array( 'success' => false, 'error' => '指定されたキーワードが見つかりません。' ); } // データバリデーション $validation = $this->validate_main_keyword_data($data, $id); if (!$validation['valid']) { return array( 'success' => false, 'error' => $validation['error'] ); } // 重複チェック(自分以外) $duplicate = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_main_keywords WHERE keyword = %s AND id != %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $validation['data']['keyword'], $id )); if ($duplicate > 0) { return array( 'success' => false, 'error' => 'このキーワードは既に登録されています。' ); } // データベースを更新 $result = $wpdb->update( $this->main_keywords_table, array( 'keyword' => $validation['data']['keyword'], 'category' => $validation['data']['category'], 'priority' => $validation['data']['priority'], 'status' => $validation['data']['status'], 'notes' => $validation['data']['notes'], 'updated_at' => current_time('mysql') ), array('id' => $id), array('%s', '%s', '%d', '%s', '%s', '%s'), array('%d') ); if ($result === false) { AAGL_Utilities::log( 'DB error in update_main_keyword: ' . $wpdb->last_error ); return array( 'success' => false, 'error' => __( 'A database error occurred.', 'aio-agent-article-generator' ) ); } return array( 'success' => true, 'message' => 'メインキーワードを更新しました。' ); } /** * メインキーワードの削除 * * @param int $id キーワードID * @return array 実行結果 */ public function delete_main_keyword($id) { global $wpdb; // IDバリデーション $id = intval($id); if ($id <= 0) { return array( 'success' => false, 'error' => '無効なIDです。' ); } // 存在チェック $existing = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aagl_main_keywords WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $id )); if (!$existing) { return array( 'success' => false, 'error' => '指定されたキーワードが見つかりません。' ); } // 関連サブキーワード数をチェック $sub_keyword_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_sub_keywords WHERE main_keyword_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $id )); // トランザクション開始 $wpdb->query('START TRANSACTION'); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared try { // サブキーワードを先に削除 if ($sub_keyword_count > 0) { $sub_result = $wpdb->delete( $this->sub_keywords_table, array('main_keyword_id' => $id), array('%d') ); if ($sub_result === false) { throw new Exception('サブキーワードの削除に失敗しました。'); } } // メインキーワードを削除 $result = $wpdb->delete( $this->main_keywords_table, array('id' => $id), array('%d') ); if ($result === false) { throw new Exception('メインキーワードの削除に失敗しました。'); } $wpdb->query('COMMIT'); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return array( 'success' => true, 'message' => sprintf( 'メインキーワードと関連するサブキーワード(%d件)を削除しました。', $sub_keyword_count ) ); } catch (Exception $e) { $wpdb->query('ROLLBACK'); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared AAGL_Utilities::log('Error deleting main keyword: ' . $e->getMessage(), 'error'); return array( 'success' => false, 'error' => 'キーワード削除中にエラーが発生しました。管理者にお問い合わせください。' ); } } /** * サブキーワードの追加 * * @param array $data キーワードデータ * @return array 実行結果 */ public function add_sub_keyword($data) { global $wpdb; // データバリデーション $validation = $this->validate_sub_keyword_data($data); if (!$validation['valid']) { return array( 'success' => false, 'error' => $validation['error'] ); } // メインキーワード存在チェック $main_keyword_exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_main_keywords WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $validation['data']['main_keyword_id'] )); if ( 0 === (int) $main_keyword_exists ) { return array( 'success' => false, 'error' => '指定されたメインキーワードが見つかりません。' ); } // 重複チェック $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_sub_keywords WHERE main_keyword_id = %d AND sub_keyword = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $validation['data']['main_keyword_id'], $validation['data']['keyword'] )); if ($existing > 0) { return array( 'success' => false, 'error' => 'このサブキーワードは既に登録されています。' ); } // データベースに挿入 $result = $wpdb->insert( $this->sub_keywords_table, array( 'main_keyword_id' => $validation['data']['main_keyword_id'], 'sub_keyword' => $validation['data']['keyword'], 'search_volume' => $validation['data']['search_volume'], 'status' => $validation['data']['status'], 'source' => 'manual', 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ), array('%d', '%s', '%d', '%s', '%s', '%s', '%s') ); if ($result === false) { AAGL_Utilities::log( 'DB error in add_sub_keyword: ' . $wpdb->last_error ); return array( 'success' => false, 'error' => __( 'A database error occurred.', 'aio-agent-article-generator' ) ); } return array( 'success' => true, 'id' => $wpdb->insert_id, 'message' => 'サブキーワードを追加しました。' ); } /** * キーワード取得(検索・フィルター・ページネーション対応) * * @param array $params 検索パラメータ * @return array 取得結果 */ public function get_keywords($params = array()) { global $wpdb; // デフォルトパラメータ $default_params = array( 'type' => 'main', // main, sub, all 'search' => '', 'category' => '', 'status' => '', 'main_keyword_id' => 0, 'order_by' => 'created_at', 'order' => 'DESC', 'page' => 1, 'per_page' => 20 ); $params = array_merge($default_params, $params); // ページネーション計算 $page = max(1, intval($params['page'])); $per_page = max(1, min(100, intval($params['per_page']))); $offset = ($page - 1) * $per_page; // WHERE句の構築 $where_conditions = array(); $where_values = array(); if ($params['type'] === 'main') { $table = $this->main_keywords_table; $select_fields = 'id, keyword, category, search_volume, difficulty, priority, status, notes, created_at, updated_at'; } elseif ($params['type'] === 'sub') { $table = $this->sub_keywords_table; $select_fields = 'id, main_keyword_id, sub_keyword, search_volume, status, source, created_at, updated_at'; } else { return array( 'success' => false, 'error' => '無効なキーワードタイプです。' ); } // 検索条件の追加 if (!empty($params['search'])) { $search_column = ($params['type'] === 'sub') ? 'sub_keyword' : 'keyword'; $where_conditions[] = $search_column . ' LIKE %s'; $where_values[] = '%' . $wpdb->esc_like($params['search']) . '%'; } if (!empty($params['category']) && $params['type'] === 'main') { $where_conditions[] = 'category = %s'; $where_values[] = $params['category']; } if (!empty($params['status'])) { $where_conditions[] = 'status = %s'; $where_values[] = $params['status']; } if ($params['main_keyword_id'] > 0 && $params['type'] === 'sub') { $where_conditions[] = 'main_keyword_id = %d'; $where_values[] = $params['main_keyword_id']; } // WHERE句の組み立て $where_clause = ''; if (!empty($where_conditions)) { $where_clause = 'WHERE ' . implode(' AND ', $where_conditions); } // ORDER BY句 $allowed_order_columns = array( 'id', 'keyword', 'category', 'search_volume', 'difficulty', 'priority', 'status', 'created_at', 'updated_at' ); $order_by = in_array($params['order_by'], $allowed_order_columns, true) ? $params['order_by'] : 'created_at'; $order = strtoupper($params['order']) === 'ASC' ? 'ASC' : 'DESC'; // クエリ実行(データ取得) if (!empty($where_values)) { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $query = $wpdb->prepare( "SELECT {$select_fields} FROM {$table} {$where_clause} ORDER BY {$order_by} {$order} LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared array_merge($where_values, array($per_page, $offset)) ); } else { $query = $wpdb->prepare( "SELECT {$select_fields} FROM {$table} {$where_clause} ORDER BY {$order_by} {$order} LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $per_page, $offset ); } $results = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // 総数取得 if (!empty($where_values)) { $count_query = $wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where_clause}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $where_values ); } else { $count_query = "SELECT COUNT(*) FROM {$table} {$where_clause}"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } $total_count = $wpdb->get_var($count_query); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // メインキーワード情報の追加(サブキーワードの場合) if ($params['type'] === 'sub' && !empty($results)) { $main_keyword_ids = array_unique(array_column($results, 'main_keyword_id')); $main_keywords = array(); if (!empty($main_keyword_ids)) { $ids_placeholder = implode(',', array_fill(0, count($main_keyword_ids), '%d')); $main_query = $wpdb->prepare( "SELECT id, keyword FROM {$wpdb->prefix}aagl_main_keywords WHERE id IN ({$ids_placeholder})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $main_keyword_ids ); $main_results = $wpdb->get_results($main_query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared foreach ($main_results as $main_row) { $main_keywords[$main_row['id']] = $main_row['keyword']; } } // サブキーワード結果にメインキーワード情報を追加 foreach ($results as &$result) { $result['main_keyword_name'] = $main_keywords[$result['main_keyword_id']] ?? '不明'; } } return array( 'success' => true, 'data' => $results, 'pagination' => array( 'page' => $page, 'per_page' => $per_page, 'total_count' => intval($total_count), 'total_pages' => ceil($total_count / $per_page) ) ); } /** * メインキーワードデータのバリデーション * * @param array $data 入力データ * @param int $exclude_id 除外ID(更新時) * @return array バリデーション結果 */ private function validate_main_keyword_data($data, $exclude_id = 0) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $errors = array(); $validated_data = array(); // キーワード if (empty($data['keyword'])) { $errors[] = 'キーワードは必須です。'; } else { $keyword = sanitize_text_field(trim($data['keyword'])); if (mb_strlen($keyword) < 2 || mb_strlen($keyword) > 255) { $errors[] = 'キーワードは2-255文字で入力してください。'; } else { $validated_data['keyword'] = $keyword; } } // カテゴリ $validated_data['category'] = sanitize_text_field($data['category'] ?? ''); if (mb_strlen($validated_data['category']) > 100) { $errors[] = 'カテゴリは100文字以内で入力してください。'; } // 優先度バリデーション(高・中・低の3段階) $priority = intval($data['priority'] ?? 2); $validated_data['priority'] = in_array($priority, array( 1, 2, 3 ), true) ? $priority : 2; // ステータス $allowed_statuses = array('active', 'inactive', 'completed'); $status = sanitize_text_field($data['status'] ?? 'active'); $validated_data['status'] = in_array($status, $allowed_statuses, true) ? $status : 'active'; // ノート $validated_data['notes'] = sanitize_textarea_field($data['notes'] ?? ''); if (mb_strlen($validated_data['notes']) > 1000) { $errors[] = 'ノートは1000文字以内で入力してください。'; } return array( 'valid' => empty($errors), 'error' => empty($errors) ? '' : implode(' ', $errors), 'data' => $validated_data ); } /** * サブキーワードデータのバリデーション * * @param array $data 入力データ * @return array バリデーション結果 */ private function validate_sub_keyword_data($data) { $errors = array(); $validated_data = array(); // メインキーワードID $validated_data['main_keyword_id'] = intval($data['main_keyword_id'] ?? 0); if ($validated_data['main_keyword_id'] <= 0) { $errors[] = 'メインキーワードIDが無効です。'; } // キーワード if (empty($data['keyword'])) { $errors[] = 'キーワードは必須です。'; } else { $keyword = sanitize_text_field(trim($data['keyword'])); if (mb_strlen($keyword) < 2 || mb_strlen($keyword) > 255) { $errors[] = 'キーワードは2-255文字で入力してください。'; } else { $validated_data['keyword'] = $keyword; } } // 検索ボリューム $validated_data['search_volume'] = max(0, intval($data['search_volume'] ?? 0)); // 難易度 $difficulty = intval($data['difficulty'] ?? 50); $validated_data['difficulty'] = max(0, min(100, $difficulty)); // ステータス(サブキーワードのenum: active, inactive, used) $allowed_statuses = array('active', 'inactive', 'used'); $status = sanitize_text_field($data['status'] ?? 'active'); $validated_data['status'] = in_array($status, $allowed_statuses, true) ? $status : 'active'; // AI生成フラグ $validated_data['ai_generated'] = isset($data['ai_generated']) ? 1 : 0; // ノート $validated_data['notes'] = sanitize_textarea_field($data['notes'] ?? ''); if (mb_strlen($validated_data['notes']) > 1000) { $errors[] = 'ノートは1000文字以内で入力してください。'; } return array( 'valid' => empty($errors), 'error' => empty($errors) ? '' : implode(' ', $errors), 'data' => $validated_data ); } // Ajax メソッド /** * Ajax: キーワード取得 */ public function ajax_get_keywords() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $params = array( 'type' => sanitize_text_field(wp_unslash($_POST['type'] ?? 'main')), 'search' => sanitize_text_field(wp_unslash($_POST['search'] ?? '')), 'category' => sanitize_text_field(wp_unslash($_POST['category'] ?? '')), 'status' => sanitize_text_field(wp_unslash($_POST['status'] ?? '')), 'main_keyword_id' => intval(wp_unslash($_POST['main_keyword_id'] ?? 0)), 'order_by' => sanitize_text_field(wp_unslash($_POST['order_by'] ?? 'created_at')), 'order' => sanitize_text_field(wp_unslash($_POST['order'] ?? 'DESC')), 'page' => intval(wp_unslash($_POST['page'] ?? 1)), 'per_page' => intval(wp_unslash($_POST['per_page'] ?? 20)), ); $result = $this->get_keywords($params); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * Ajax: メインキーワード追加 */ public function ajax_add_main_keyword() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $result = $this->add_main_keyword(wp_unslash($_POST)); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * Ajax: メインキーワード更新 */ public function ajax_update_main_keyword() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $id = intval(wp_unslash($_POST['keyword_id'] ?? 0)); $result = $this->update_main_keyword($id, wp_unslash($_POST)); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * Ajax: メインキーワード削除 */ public function ajax_delete_main_keyword() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $id = intval(wp_unslash($_POST['id'] ?? 0)); $result = $this->delete_main_keyword($id); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * Ajax: サブキーワード追加 */ public function ajax_add_sub_keyword() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $result = $this->add_sub_keyword(wp_unslash($_POST)); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * サブキーワードの更新 * * @param int $id サブキーワードID * @param array $data 更新データ * @return array 実行結果 */ public function update_sub_keyword($id, $data) { global $wpdb; // IDバリデーション $id = intval($id); if ($id <= 0) { return array( 'success' => false, 'error' => '無効なIDです。' ); } // 存在チェック $existing = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aagl_sub_keywords WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $id )); if (!$existing) { return array( 'success' => false, 'error' => '指定されたサブキーワードが見つかりません。' ); } // データサニタイズ $update_data = array( 'sub_keyword' => sanitize_text_field($data['sub_keyword'] ?? ''), 'status' => sanitize_text_field($data['status'] ?? 'active'), 'updated_at' => current_time('mysql') ); // バリデーション if (empty($update_data['sub_keyword'])) { return array( 'success' => false, 'error' => 'サブキーワードは必須です。' ); } if (!in_array($update_data['status'], array( 'active', 'inactive', 'used' ), true)) { return array( 'success' => false, 'error' => '無効なステータスです。' ); } // 重複チェック(同じメインキーワード内で自分以外) $duplicate = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_sub_keywords WHERE sub_keyword = %s AND main_keyword_id = %d AND id != %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $update_data['sub_keyword'], $existing->main_keyword_id, $id )); if ($duplicate > 0) { return array( 'success' => false, 'error' => 'このサブキーワードは既に登録されています。' ); } // データベースを更新 $result = $wpdb->update( $this->sub_keywords_table, $update_data, array('id' => $id), array('%s', '%s', '%s'), array('%d') ); if ($result === false) { AAGL_Utilities::log( 'DB error in update_sub_keyword: ' . $wpdb->last_error ); return array( 'success' => false, 'error' => __( 'A database error occurred.', 'aio-agent-article-generator' ) ); } return array( 'success' => true, 'message' => 'サブキーワードを更新しました。' ); } /** * メインキーワード一覧取得 * * @param array $options オプション * @return array メインキーワード一覧 */ public function get_main_keywords($options = array()) { global $wpdb; $default_options = array( 'status' => '', 'category' => '', 'limit' => 0, 'offset' => 0, 'order_by' => 'created_at', 'order' => 'DESC' ); $options = array_merge($default_options, $options); // クエリ構築 $where_clauses = array(); $where_values = array(); if (!empty($options['status'])) { $where_clauses[] = "status = %s"; $where_values[] = $options['status']; } if (!empty($options['category'])) { $where_clauses[] = "category = %s"; $where_values[] = $options['category']; } $where_sql = empty($where_clauses) ? '' : 'WHERE ' . implode(' AND ', $where_clauses); // 並び順の検証 $allowed_columns = array('id', 'keyword', 'category', 'priority', 'status', 'created_at', 'updated_at'); $order_by = in_array($options['order_by'], $allowed_columns, true) ? $options['order_by'] : 'created_at'; $order = strtoupper($options['order']) === 'ASC' ? 'ASC' : 'DESC'; $limit_sql = ''; if ($options['limit'] > 0) { $limit_sql = $wpdb->prepare(" LIMIT %d", $options['limit']); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ($options['offset'] > 0) { $limit_sql = $wpdb->prepare(" LIMIT %d, %d", $options['offset'], $options['limit']); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query = "SELECT * FROM {$wpdb->prefix}aagl_main_keywords {$where_sql} ORDER BY {$order_by} {$order} {$limit_sql}"; if (!empty($where_values)) { $query = $wpdb->prepare($query, $where_values); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } return $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * サブキーワード一覧取得 * * @param array $options オプション * @return array サブキーワード一覧 */ public function get_sub_keywords($options = array()) { global $wpdb; $default_options = array( 'id' => 0, 'main_keyword_id' => 0, 'status' => '', 'category' => '', 'limit' => 0, 'offset' => 0, 'order_by' => 'created_at', 'order' => 'DESC' ); $options = array_merge($default_options, $options); // クエリ構築 $where_clauses = array(); $where_values = array(); if ($options['main_keyword_id'] > 0) { $where_clauses[] = "s.main_keyword_id = %d"; $where_values[] = $options['main_keyword_id']; } if (!empty($options['status'])) { $where_clauses[] = "s.status = %s"; $where_values[] = $options['status']; } if (!empty($options['category'])) { $where_clauses[] = "category = %s"; $where_values[] = $options['category']; } $where_sql = empty($where_clauses) ? '' : 'WHERE ' . implode(' AND ', $where_clauses); // 並び順の検証 $allowed_columns = array('id', 'keyword', 'category', 'priority', 'status', 'created_at', 'updated_at'); $order_by = in_array($options['order_by'], $allowed_columns, true) ? $options['order_by'] : 'created_at'; $order = strtoupper($options['order']) === 'ASC' ? 'ASC' : 'DESC'; $limit_sql = ''; if ($options['limit'] > 0) { $limit_sql = $wpdb->prepare(" LIMIT %d", $options['limit']); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare if ($options['offset'] > 0) { $limit_sql = $wpdb->prepare(" LIMIT %d, %d", $options['offset'], $options['limit']); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare } } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $query = "SELECT s.*, m.keyword as main_keyword_text, s.sub_keyword as keyword, 'サブキーワード' as category, 'medium' as priority FROM {$wpdb->prefix}aagl_sub_keywords s LEFT JOIN {$wpdb->prefix}aagl_main_keywords m ON s.main_keyword_id = m.id {$where_sql} ORDER BY s.{$order_by} {$order} {$limit_sql}"; if (!empty($where_values)) { $query = $wpdb->prepare($query, $where_values); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } return $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** * Ajax: サブキーワード更新 */ public function ajax_update_sub_keyword() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $id = intval(wp_unslash($_POST['sub_keyword_id'] ?? 0)); $result = $this->update_sub_keyword($id, wp_unslash($_POST)); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['error']); } } /** * ID でメインキーワードを取得 */ public function get_main_keyword_by_id($id) { global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aagl_main_keywords WHERE id = %d AND status = 'active'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $id ), ARRAY_A ); return $result; } /** * ID配列でサブキーワードを取得 */ public function get_sub_keywords_by_ids($ids) { global $wpdb; if (empty($ids) || !is_array($ids)) { return array(); } $placeholders = implode(',', array_fill(0, count($ids), '%d')); $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aagl_sub_keywords WHERE id IN ($placeholders) AND status = 'active' ORDER BY created_at DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare ...$ids ), ARRAY_A ); return $results ? $results : array(); } /** * Ajax: 指定メインキーワードのサブキーワード取得 */ public function ajax_get_sub_keywords_for_main() { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'aagl_ajax_nonce')) { wp_send_json_error('セキュリティチェックに失敗しました。'); return; } if (!current_user_can('manage_options')) { wp_send_json_error('権限が不十分です。'); return; } $main_keyword_id = intval(wp_unslash($_POST['main_keyword_id'] ?? 0)); if ($main_keyword_id <= 0) { wp_send_json_error('無効なメインキーワードIDです。'); } // まず全サブキーワード数を確認 global $wpdb; $total_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_sub_keywords WHERE main_keyword_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $main_keyword_id )); $active_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}aagl_sub_keywords WHERE main_keyword_id = %d AND status = 'active'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $main_keyword_id )); // アクティブなサブキーワードのみ取得 $sub_keywords = $this->get_sub_keywords([ 'main_keyword_id' => $main_keyword_id, 'status' => 'active', 'order_by' => 'created_at', 'order' => 'DESC' ]); wp_send_json_success($sub_keywords); } /** * タイトル生成の内部メソッド(スケジュール処理で使用) */ public function generate_title_candidates_internal($main_keyword_id, $sub_keyword_ids, $ai_provider, $options = array()) { try { // メインキーワード取得 global $wpdb; $main_keyword = $wpdb->get_row( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->prepare("SELECT * FROM {$wpdb->prefix}aagl_main_keywords WHERE id = %d", $main_keyword_id), ARRAY_A ); if (!$main_keyword) { return ['success' => false, 'error' => 'Main keyword not found']; } // サブキーワード取得(バッチクエリ) $sub_keywords = []; if (!empty($sub_keyword_ids)) { $sanitized_ids = array_map('intval', $sub_keyword_ids); $ids_placeholder = implode(',', array_fill(0, count($sanitized_ids), '%d')); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $sub_keywords = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}aagl_sub_keywords WHERE id IN ($ids_placeholder)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- IN句の %d は件数に応じて組み立てる $sanitized_ids ), ARRAY_A ); if (!$sub_keywords) { $sub_keywords = []; } } // AI統合クラスからタイトル生成(オプション伝播) $ai_integration = AAGL_AI_Integration::get_instance(); $title_result = $ai_integration->generate_article_title($main_keyword, $sub_keywords, $ai_provider, $options); if ($title_result['success']) { return [ 'success' => true, 'titles' => $title_result['candidates'] ?? [$title_result['title']] ]; } else { return ['success' => false, 'error' => $title_result['error']]; } } catch (Exception $e) { return ['success' => false, 'error' => $e->getMessage()]; } } } // インスタンス初期化 AAGL_Keyword_Functions::get_instance();