| @@ -94,8 +94,19 @@ | ||
| 94 | 94 | } |
| 95 | 95 | } |
| 96 | 96 | |
| 97 | 97 | /** |
| 98 | + * Verify that the current user can edit a chart. | |
| 99 | + * | |
| 100 | + * @param int $chart_id Chart ID. | |
| 101 | + */ | |
| 102 | + private function _verify_chart_access( $chart_id ): void { | |
| 103 | + if ( ! self::can_edit_chart( $chart_id ) ) { | |
| 104 | + wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'visualizer' ) ), 403 ); | |
| 105 | + } | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** | |
| 98 | 109 | * Persist chart data + series from a source. |
| 99 | 110 | * |
| 100 | 111 | * @param int $chart_id Chart ID. |
| 101 | 112 | * @param Visualizer_Source $source Data source instance. |
| @@ -158,8 +169,9 @@ | ||
| 158 | 169 | $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 ); |
| 159 | 170 | if ( ! $chart_id || ! get_post( $chart_id ) ) { |
| 160 | 171 | wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) ); |
| 161 | 172 | } |
| 173 | + $this->_verify_chart_access( $chart_id ); | |
| 162 | 174 | wp_send_json_success( |
| 163 | 175 | array( |
| 164 | 176 | 'upload_nonce' => wp_create_nonce( 'visualizer-ai-upload-' . $chart_id ), |
| 165 | 177 | ) |
| @@ -179,8 +191,9 @@ | ||
| 179 | 191 | $chart = $chart_id ? get_post( $chart_id ) : null; |
| 180 | 192 | if ( ! $chart || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) { |
| 181 | 193 | wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) ); |
| 182 | 194 | } |
| 195 | + $this->_verify_chart_access( $chart_id ); | |
| 183 | 196 | |
| 184 | 197 | $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true ); |
| 185 | 198 | $data = Visualizer_Module::get_chart_data( $chart, '', false ); |
| 186 | 199 | $code = get_post_meta( $chart_id, self::CF_D3_CODE, true ); |
| @@ -205,9 +218,9 @@ | ||
| 205 | 218 | |
| 206 | 219 | /** |
| 207 | 220 | * Determines whether a remote URL serves an XLSX file. |
| 208 | 221 | * |
| 209 | - * Uses wp_safe_remote_get() and checks ZIP magic number (PK\x03\x04). | |
| 222 | + * Uses the shared remote-fetch policy and checks ZIP magic number (PK\x03\x04). | |
| 210 | 223 | * |
| 211 | 224 | * @access private |
| 212 | 225 | * @param string $url The remote URL to probe. |
| 213 | 226 | * @return bool TRUE if the file appears to be XLSX, FALSE otherwise. |
| @@ -217,16 +230,17 @@ | ||
| 217 | 230 | if ( ! $tmpfile ) { |
| 218 | 231 | return false; |
| 219 | 232 | } |
| 220 | 233 | |
| 221 | - $response = wp_safe_remote_get( | |
| 234 | + $response = Visualizer_Remote_Fetch::request( | |
| 222 | 235 | $url, |
| 223 | 236 | array( |
| 224 | - 'timeout' => 15, | |
| 225 | - 'redirection' => 5, | |
| 226 | - 'stream' => true, | |
| 227 | - 'filename' => $tmpfile, | |
| 228 | - 'headers' => array( 'Range' => 'bytes=0-3' ), | |
| 237 | + 'timeout' => 15, | |
| 238 | + 'redirection' => 5, | |
| 239 | + 'stream' => true, | |
| 240 | + 'filename' => $tmpfile, | |
| 241 | + 'headers' => array( 'Range' => 'bytes=0-3' ), | |
| 242 | + 'limit_response_size' => 4, | |
| 229 | 243 | ) |
| 230 | 244 | ); |
| 231 | 245 | |
| 232 | 246 | if ( is_wp_error( $response ) ) { |
| @@ -257,8 +271,9 @@ | ||
| 257 | 271 | } |
| 258 | 272 | if ( ! get_post( $chart_id ) ) { |
| 259 | 273 | wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) ); |
| 260 | 274 | } |
| 275 | + $this->_verify_chart_access( $chart_id ); | |
| 261 | 276 | |
| 262 | 277 | $source_type = isset( $_POST['source_type'] ) ? sanitize_key( $_POST['source_type'] ) : 'csv_string'; |
| 263 | 278 | $source = null; |
| 264 | 279 | $tmp_files = array(); |
| @@ -296,19 +311,8 @@ | ||
| 296 | 311 | wp_send_json_error( array( 'message' => __( 'No URL provided.', 'visualizer' ) ) ); |
| 297 | 312 | } |
| 298 | 313 | $url = wp_unslash( $_POST['file_url'] ); |
| 299 | 314 | |
| 300 | - // Allow local absolute paths in dev (same CSVs used by Classic). | |
| 301 | - if ( is_string( $url ) && file_exists( $url ) && is_readable( $url ) ) { | |
| 302 | - $ext = strtolower( pathinfo( $url, PATHINFO_EXTENSION ) ); | |
| 303 | - if ( 'xlsx' === $ext && class_exists( 'Visualizer_Source_Xlsx' ) ) { | |
| 304 | - $source = new Visualizer_Source_Xlsx( $url ); | |
| 305 | - } else { | |
| 306 | - $source = new Visualizer_Source_Csv( $url ); | |
| 307 | - } | |
| 308 | - break; | |
| 309 | - } | |
| 310 | - | |
| 311 | 315 | if ( function_exists( 'wp_http_validate_url' ) ) { |
| 312 | 316 | $validated_url = wp_http_validate_url( (string) $url ); |
| 313 | 317 | $url = false === $validated_url ? false : (string) $validated_url; |
| 314 | 318 | } else { |
| @@ -364,9 +368,9 @@ | ||
| 364 | 368 | break; |
| 365 | 369 | |
| 366 | 370 | // ── Database query ──────────────────────────────────────────────── |
| 367 | 371 | case 'db_query': |
| 368 | - if ( ! current_user_can( 'manage_options' ) && ! is_super_admin() ) { | |
| 372 | + if ( ! current_user_can( 'manage_options' ) || ! is_super_admin() || ! Visualizer_Module::is_pro() ) { | |
| 369 | 373 | wp_send_json_error( array( 'message' => __( 'Action not allowed for this user.', 'visualizer' ) ), 403 ); |
| 370 | 374 | } |
| 371 | 375 | if ( empty( $_POST['db_query'] ) ) { |
| 372 | 376 | wp_send_json_error( array( 'message' => __( 'No query provided.', 'visualizer' ) ) ); |
| @@ -434,8 +438,9 @@ | ||
| 434 | 438 | $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 ); |
| 435 | 439 | if ( ! $chart_id || ! get_post( $chart_id ) ) { |
| 436 | 440 | wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) ); |
| 437 | 441 | } |
| 442 | + $this->_verify_chart_access( $chart_id ); | |
| 438 | 443 | |
| 439 | 444 | $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : ''; |
| 440 | 445 | $series = isset( $_POST['series'] ) ? wp_unslash( $_POST['series'] ) : ''; |
| 441 | 446 | $data = isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : ''; |
| @@ -496,8 +501,12 @@ | ||
| 496 | 501 | $workflow_id = $response_body['data']['workflowId']; |
| 497 | 502 | } |
| 498 | 503 | } |
| 499 | 504 | |
| 505 | + if ( ! empty( $workflow_id ) ) { | |
| 506 | + set_transient( 'viz_ai_wf_' . $workflow_id, get_current_user_id(), 6 * HOUR_IN_SECONDS ); | |
| 507 | + } | |
| 508 | + | |
| 500 | 509 | wp_send_json_success( |
| 501 | 510 | array( |
| 502 | 511 | 'workflow_id' => $workflow_id, |
| 503 | 512 | ) |
| @@ -518,8 +527,12 @@ | ||
| 518 | 527 | if ( empty( $workflow_id ) ) { |
| 519 | 528 | wp_send_json_error( array( 'message' => __( 'Missing workflow ID.', 'visualizer' ) ) ); |
| 520 | 529 | } |
| 521 | 530 | |
| 531 | + if ( (int) get_transient( 'viz_ai_wf_' . $workflow_id ) !== get_current_user_id() ) { | |
| 532 | + wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'visualizer' ) ), 403 ); | |
| 533 | + } | |
| 534 | + | |
| 522 | 535 | $agents_url = VISUALIZER_AGENTS_URL; |
| 523 | 536 | $workflow_slug = $this->_get_workflow_slug(); |
| 524 | 537 | $headers = $this->_get_agents_headers(); |
| 525 | 538 | |
| @@ -559,8 +572,9 @@ | ||
| 559 | 572 | |
| 560 | 573 | if ( ! $chart_id || ! get_post( $chart_id ) ) { |
| 561 | 574 | wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) ); |
| 562 | 575 | } |
| 576 | + $this->_verify_chart_access( $chart_id ); | |
| 563 | 577 | if ( empty( $code ) ) { |
| 564 | 578 | wp_send_json_error( array( 'message' => __( 'No chart code found. Generate a chart first.', 'visualizer' ) ) ); |
| 565 | 579 | } |
| 566 | 580 | |