PluginProbe
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards / 5.5.83
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards v5.5.83
5.5.83 5.5.82 5.5.81 5.5.80 5.5.79 5.5.77 5.5.76 5.5.75 5.5.73 5.5.72 5.5.22 5.5.23 5.5.29 5.5.3 5.5.31 5.5.32 5.5.34 5.5.35 5.5.36 5.5.37 5.5.4 5.5.40 5.5.41 5.5.42 5.5.43 All 159 releases
wp-data-access / WPDataAccess / API / WPDA_AI.php

WPDA_AI.php in WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards 5.5.83, at WPDataAccess/API/WPDA_AI.php

278 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDataAccess\API;
4
5 use WPDataAccess\Connection\WPDADB;
6 use WPDataAccess\Data_Dictionary\WPDA_Dictionary_Lists;
7 use WPDataAccess\WPDA;
8 // phpcs:disable PluginCheck.CodeAnalysis.AIProvider.DirectIntegration
9 class WPDA_AI extends WPDA_API_Core {
10 const CIPHER = 'AES-256-CBC';
11
12 const AI_API_KEY = 'wpda_ai_key';
13
14 const SUPPORTED_MODELS = ['gpt-3.5-turbo', 'gpt-4-turbo'];
15
16 public function register_rest_routes() {
17 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/sql', array(
18 'methods' => array('POST'),
19 'callback' => array($this, 'ai_sql'),
20 'permission_callback' => function () {
21 return $this->current_user_can_access();
22 },
23 'args' => array(
24 'prompt' => array(
25 'required' => true,
26 'type' => 'string',
27 'description' => __( 'Prompt', 'wp-data-access' ),
28 'sanitize_callback' => 'sanitize_text_field',
29 'validate_callback' => 'rest_validate_request_arg',
30 ),
31 'model' => array(
32 'required' => true,
33 'type' => 'string',
34 'description' => __( 'Model', 'wp-data-access' ),
35 'sanitize_callback' => 'sanitize_text_field',
36 'validate_callback' => function ( $param ) {
37 return in_array( $param, self::SUPPORTED_MODELS );
38 },
39 ),
40 'explain' => array(
41 'required' => true,
42 'type' => 'boolean',
43 'description' => __( 'Add explanations', 'wp-data-access' ),
44 'sanitize_callback' => 'sanitize_text_field',
45 'validate_callback' => 'rest_validate_request_arg',
46 ),
47 ),
48 ) );
49 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/hints', array(
50 'methods' => array('POST'),
51 'callback' => array($this, 'hints'),
52 'permission_callback' => '__return_true',
53 'args' => array(
54 'dbs' => $this->get_param( 'dbs' ),
55 ),
56 ) );
57 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/enabled', array(
58 'methods' => array('POST'),
59 'callback' => array($this, 'enabled'),
60 'permission_callback' => '__return_true',
61 'args' => array(),
62 ) );
63 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/enable', array(
64 'methods' => array('POST'),
65 'callback' => array($this, 'enable'),
66 'permission_callback' => '__return_true',
67 'args' => array(
68 'key' => array(
69 'required' => true,
70 'type' => 'string',
71 'description' => __( 'API Key', 'wp-data-access' ),
72 'sanitize_callback' => 'sanitize_text_field',
73 'validate_callback' => 'rest_validate_request_arg',
74 ),
75 'encrypt' => array(
76 'required' => true,
77 'type' => 'boolean',
78 'description' => __( 'Encrypt API key', 'wp-data-access' ),
79 'sanitize_callback' => 'sanitize_text_field',
80 'validate_callback' => 'rest_validate_request_arg',
81 ),
82 ),
83 ) );
84 }
85
86 public function ai_sql( $request ) {
87 $timeout = 30;
88 $prompt = $request['prompt'];
89 $model = $request['model'];
90 $explain = ( $request['explain'] ? 'Write the SQL query first in a code block. After the code block, provide a clear, concise explanation of what the query does.' : 'Provide only the query without further explanation.' );
91 $prompt = "\nYou are a professional MySQL consultant helping developers write SQL queries.\nAlways respond with clean, optimized MySQL code.\nPlace the query inside a single Markdown code block using triple backticks (```sql).\nDo not include a semicolon at the end of the query.\nImportant: Do not include a LIMIT clause unless the user specifically requests limiting the number of results.\nAssume the system will handle limits automatically if needed.\n{$explain}\nExample input for a user:\nWrite a join between tables dept and emp and show the average and total salaries per department.\nExpected Output:\n```sql\nSELECT d.dname AS department_name, \n AVG(e.sal) AS average_salary, \n SUM(e.sal) AS total_salary\nFROM dept d\nJOIN emp e ON d.deptno = e.deptno\nGROUP BY d.dname\n```\n{$prompt}\n";
92 WPDA::wpda_log_wp_error( $prompt );
93 $api_key_saved = get_user_meta( get_current_user_id(), self::AI_API_KEY, true );
94 if ( false === $api_key_saved || '' === $api_key_saved ) {
95 $api_key = '';
96 } else {
97 $api_key = substr( $api_key_saved, 0, -2 );
98 $is_encrypted = substr( $api_key_saved, -1 );
99 if ( '1' === $is_encrypted ) {
100 // Decrypt API key
101 $api_key = $this->decrypt( $api_key );
102 }
103 }
104 if ( '' === trim( $api_key ) ) {
105 return new \WP_Error('error', 'Invalid or missing API Key', array(
106 'status' => 403,
107 ));
108 }
109 return $this->ask_ai(
110 $api_key,
111 $model,
112 $prompt,
113 $timeout
114 );
115 }
116
117 private function ask_ai(
118 $api_key,
119 $model,
120 $prompt,
121 $timeout,
122 $msg = ''
123 ) {
124 $response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', array(
125 'headers' => array(
126 'Authorization' => 'Bearer ' . $api_key,
127 'Content-Type' => 'application/json',
128 ),
129 'body' => json_encode( array(
130 'model' => $model,
131 'messages' => array(array(
132 'role' => 'user',
133 'content' => $prompt,
134 )),
135 ) ),
136 'timeout' => $timeout,
137 ) );
138 if ( !is_wp_error( $response ) ) {
139 $body = json_decode( wp_remote_retrieve_body( $response ), true );
140 if ( '' !== $msg ) {
141 $body['msg'] = $msg;
142 }
143 return rest_ensure_response( $body );
144 }
145 if ( self::SUPPORTED_MODELS[1] === $model ) {
146 // Try 'gpt-3.5-turbo' if 'gpt-4-turbo' failed
147 return $this->ask_ai(
148 $api_key,
149 self::SUPPORTED_MODELS[0],
150 $prompt,
151 $timeout,
152 'Note: This result was generated using gpt-3.5-turbo due to a timeout using gpt-4-turbo.'
153 );
154 }
155 return new \WP_Error('error', $response->get_error_message(), array(
156 'status' => 403,
157 ));
158 }
159
160 public function hints( $request ) {
161 if ( !$this->current_user_can_access() ) {
162 return $this->unauthorized();
163 }
164 if ( !$this->current_user_token_valid( $request ) ) {
165 return $this->invalid_nonce();
166 }
167 $dbs = $request->get_param( 'dbs' );
168 $tables = WPDA_Dictionary_Lists::get_tables( true, $dbs );
169 $wpdadb = WPDADB::get_db_connection( $dbs );
170 if ( null === $wpdadb ) {
171 // Error connecting.
172 return new \WP_Error('error', "Error connecting to database {$dbs}", array(
173 'status' => 420,
174 ));
175 }
176 $hints = array();
177 foreach ( $tables as $table ) {
178 $table_name = WPDA::remove_backticks( $table['table_name'] );
179 $sql_cmd = $wpdadb->get_results( "SHOW CREATE TABLE `{$table_name}`", 'ARRAY_N' );
180 if ( '' === $wpdadb->last_error && isset( $sql_cmd[0][1] ) ) {
181 $hints[$table_name] = $sql_cmd[0][1];
182 }
183 }
184 return $this->WPDA_Rest_Response( '', $hints );
185 }
186
187 public function enabled( $request ) {
188 if ( !$this->current_user_can_access() ) {
189 return $this->unauthorized();
190 }
191 if ( !$this->current_user_token_valid( $request ) ) {
192 return $this->invalid_nonce();
193 }
194 $api_key = get_user_meta( WPDA::get_current_user_id(), self::AI_API_KEY, true );
195 $is_enabled = false !== $api_key && '' !== $api_key;
196 return $this->WPDA_Rest_Response( '', array(
197 'enabled' => $is_enabled,
198 'encryption' => $this->get_encryption_key() !== null,
199 ) );
200 }
201
202 public function enable( $request ) {
203 if ( !$this->current_user_can_access() ) {
204 return $this->unauthorized();
205 }
206 if ( !$this->current_user_token_valid( $request ) ) {
207 return $this->invalid_nonce();
208 }
209 $key = $request['key'];
210 $encrypt = ( $request['encrypt'] ? 1 : 0 );
211 if ( $encrypt ) {
212 $key = $this->encrypt( $key );
213 }
214 update_user_meta( WPDA::get_current_user_id(), self::AI_API_KEY, "{$key}|{$encrypt}" );
215 return $this->WPDA_Rest_Response( '' );
216 }
217
218 private function encrypt( $string ) {
219 $key = $this->get_encryption_key();
220 if ( null === $key ) {
221 return $string;
222 }
223 $ivlen = openssl_cipher_iv_length( self::CIPHER );
224 $iv = openssl_random_pseudo_bytes( $ivlen );
225 $ciphertext_raw = openssl_encrypt(
226 $string,
227 self::CIPHER,
228 $key,
229 OPENSSL_RAW_DATA,
230 $iv
231 );
232 $hmac = hash_hmac(
233 'sha256',
234 $ciphertext_raw,
235 $key,
236 true
237 );
238 return base64_encode( $iv . $hmac . $ciphertext_raw );
239 }
240
241 private function decrypt( $string ) {
242 $key = $this->get_encryption_key();
243 if ( null === $key ) {
244 return $string;
245 }
246 $c = base64_decode( $string );
247 $ivlen = openssl_cipher_iv_length( self::CIPHER );
248 $iv = substr( $c, 0, $ivlen );
249 $hmac = substr( $c, $ivlen, 32 );
250 $ciphertext_raw = substr( $c, $ivlen + 32 );
251 $calculated_hmac = hash_hmac(
252 'sha256',
253 $ciphertext_raw,
254 $key,
255 true
256 );
257 if ( !hash_equals( $hmac, $calculated_hmac ) ) {
258 return false;
259 }
260 return openssl_decrypt(
261 $ciphertext_raw,
262 self::CIPHER,
263 $key,
264 OPENSSL_RAW_DATA,
265 $iv
266 );
267 }
268
269 private function get_encryption_key() {
270 if ( defined( 'WPDA_ENCRYPT_AI_KEY' ) && '' !== trim( constant( 'WPDA_ENCRYPT_AI_KEY' ) ) ) {
271 return constant( 'WPDA_ENCRYPT_AI_KEY' );
272 } else {
273 return null;
274 }
275 }
276
277 }
278