__( 'Crypto Analysis', 'adams-crypto-analysis' ), 'subtitle' => __( 'AI-Powered Technical Analysis', 'adams-crypto-analysis' ), ), $attributes, 'adamca_crypto_analysis' ); $title_text = esc_html( $parsed_atts['title'] ); $subtitle_text = esc_html( $parsed_atts['subtitle'] ); // Enqueue assets now that we know shortcode is used. $this->enqueue_frontend_assets(); $banner_url = esc_url( ADAMS_CRYPTO_ANALYSIS_URL . 'assets/images/scbanner.png' ); $cg_icon_url = esc_url( ADAMS_CRYPTO_ANALYSIS_URL . 'assets/images/CG-icon.png' ); $output_html = '
'; // Branding banner. $output_html .= '
'; $output_html .= '' . esc_attr__( 'Crypto Nerd Analysis', 'adams-crypto-analysis' ) . ''; $output_html .= '
'; // Legal disclaimer. $output_html .= '
'; $output_html .= '

' . esc_html__( 'For informational purposes only. Not financial advice. Cryptocurrency markets are highly volatile — always do your own research before making any investment decisions. Analysis may take up to 2 minutes depending on server load.', 'adams-crypto-analysis' ) . '

'; $output_html .= '
'; $output_html .= '
'; $output_html .= '
'; $output_html .= '

' . $title_text . '

'; $output_html .= '

' . $subtitle_text . '

'; $output_html .= ''; $output_html .= '
'; $output_html .= '
'; $output_html .= ''; $output_html .= ''; $output_html .= ''; $output_html .= '
'; $output_html .= '
'; $output_html .= ''; $output_html .= ''; $output_html .= ''; $output_html .= '
'; $output_html .= ''; $output_html .= ''; $output_html .= ''; $output_html .= '
'; $output_html .= ''; // CoinGecko attribution. $output_html .= '
'; $output_html .= 'CoinGecko'; $output_html .= '' . esc_html__( 'Data provided by CoinGecko', 'adams-crypto-analysis' ) . ''; $output_html .= '
'; $output_html .= '
'; // .adamca-inner $output_html .= '
'; // .adamca-container return $output_html; } /** * Conditionally enqueue assets on wp_enqueue_scripts (fallback). */ public function maybe_enqueue_assets() { global $post; if ( is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'adamca_crypto_analysis' ) ) { $this->enqueue_frontend_assets(); } } /** * Enqueue frontend CSS and JS with localized data. */ private function enqueue_frontend_assets() { static $already_enqueued = false; if ( $already_enqueued ) { return; } $already_enqueued = true; $plugin_version = ADAMS_CRYPTO_ANALYSIS_VERSION; wp_enqueue_style( 'adamca-frontend', ADAMS_CRYPTO_ANALYSIS_URL . 'assets/css/crypto-analysis.css', array(), $plugin_version ); wp_enqueue_script( 'adamca-frontend', ADAMS_CRYPTO_ANALYSIS_URL . 'assets/js/crypto-analysis.js', array(), $plugin_version, true ); $top10_raw = get_option( 'adamca_top10_coins', '' ); $top10_coins = array_filter( array_map( 'sanitize_key', array_map( 'trim', explode( "\n", $top10_raw ) ) ) ); wp_localize_script( 'adamca-frontend', 'ADAMCA', array( 'apiEndpoint' => esc_url_raw( rest_url( 'adams-crypto/v1/analyze' ) ), 'top10Coins' => array_values( $top10_coins ), 'pluginUrl' => esc_url_raw( ADAMS_CRYPTO_ANALYSIS_URL ), 'i18n' => array( 'loading' => __( 'Loading...', 'adams-crypto-analysis' ), 'failedToLoad' => __( 'Failed to load coins', 'adams-crypto-analysis' ), 'selectCoin' => __( 'Select a coin...', 'adams-crypto-analysis' ), 'pleaseSelect' => __( 'Please select or enter a coin ID.', 'adams-crypto-analysis' ), 'analysisFailed' => __( 'Analysis failed. Please try again.', 'adams-crypto-analysis' ), 'timedOut' => __( 'Request timed out after 120 seconds. The AI provider may be overloaded. Please try again.', 'adams-crypto-analysis' ), 'networkError' => __( 'Network error: ', 'adams-crypto-analysis' ), /* translators: %d: number of minutes since the analysis was cached */ 'cachedAnalysis' => __( 'Cached analysis (%d min ago)', 'adams-crypto-analysis' ), 'freshAnalysis' => __( 'Fresh analysis (just generated)', 'adams-crypto-analysis' ), ), ) ); } /** * Register REST API routes. */ public function register_rest_routes() { $namespace = 'adams-crypto/v1'; register_rest_route( $namespace, '/analyze', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_analyze_request' ), 'permission_callback' => '__return_true', ) ); register_rest_route( $namespace, '/status', array( 'methods' => 'GET', 'callback' => array( $this, 'handle_status_request' ), 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); register_rest_route( $namespace, '/clear', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_clear_request' ), 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); } /** * Handle POST /analyze request. * * @param WP_REST_Request $request REST request object. * @return WP_REST_Response|WP_Error Response or error. */ public function handle_analyze_request( $request ) { // Rate limit: max 5 requests per IP per minute to prevent API abuse. $client_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $rate_key = 'adamca_rate_' . substr( md5( $client_ip ), 0, 12 ); $rate_count = (int) get_transient( $rate_key ); if ( $rate_count >= 5 ) { return new WP_REST_Response( array( 'success' => false, 'error' => __( 'Rate limit exceeded. Please try again shortly.', 'adams-crypto-analysis' ), ), 429 ); } set_transient( $rate_key, $rate_count + 1, 60 ); // Optional nonce verification: if X-WP-Nonce header is sent, validate it. $nonce_header = $request->get_header( 'X-WP-Nonce' ); if ( null !== $nonce_header && ! wp_verify_nonce( $nonce_header, 'wp_rest' ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => __( 'Invalid nonce.', 'adams-crypto-analysis' ), ), 403 ); } $coin_id = isset( $request['coin_id'] ) ? sanitize_key( $request['coin_id'] ) : ''; if ( ! preg_match( '/^[a-z0-9\-]{1,100}$/', $coin_id ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => __( 'Invalid coin ID.', 'adams-crypto-analysis' ), ), 400 ); } // Check cache first. $cached_html = ADAMCA_Cache::retrieve( $coin_id ); if ( false !== $cached_html ) { $metadata = ADAMCA_Cache::get_cache_metadata( $coin_id ); $age_minutes = $metadata ? (int) floor( ( time() - $metadata['timestamp'] ) / 60 ) : 0; return new WP_REST_Response( array( 'success' => true, 'html' => wp_kses_post( $cached_html ), 'cached' => true, 'cache_age_minutes' => $age_minutes, 'is_top_ten' => ADAMCA_Cache::is_top_ten( $coin_id ), 'coin_id' => $coin_id, 'provider' => $metadata ? $metadata['provider'] : '', 'model' => $metadata ? $metadata['model'] : '', ), 200 ); } // Fetch from CoinGecko. $market_data = ADAMCA_CoinGecko::fetch_analysis_data( $coin_id ); if ( is_wp_error( $market_data ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => $market_data->get_error_message(), ), 500 ); } // Generate AI analysis. $html_output = ADAMCA_AI_Client::generate_analysis( $coin_id, $market_data ); if ( is_wp_error( $html_output ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => $html_output->get_error_message(), ), 500 ); } // Sanitize AI-generated HTML before caching and output. $html_output = wp_kses_post( $html_output ); // Store in cache. ADAMCA_Cache::store_analysis( $coin_id, $html_output ); return new WP_REST_Response( array( 'success' => true, 'html' => $html_output, 'cached' => false, 'cache_age_minutes' => 0, 'is_top_ten' => ADAMCA_Cache::is_top_ten( $coin_id ), 'coin_id' => $coin_id, 'provider' => get_option( 'adamca_ai_provider' ), 'model' => get_option( 'adamca_ai_model' ), ), 200 ); } /** * Handle GET /status request (admin only). * * @return WP_REST_Response Cache status response. */ public function handle_status_request() { return new WP_REST_Response( array( 'success' => true, 'cached_coins' => ADAMCA_Cache::get_all_cached_status(), ), 200 ); } /** * Handle POST /clear request (admin only). * * @param WP_REST_Request $request REST request object. * @return WP_REST_Response Response. */ public function handle_clear_request( $request ) { $coin_id = isset( $request['coin_id'] ) ? sanitize_key( $request['coin_id'] ) : ''; if ( empty( $coin_id ) ) { return new WP_REST_Response( array( 'success' => false, 'error' => __( 'No coin ID provided.', 'adams-crypto-analysis' ), ), 400 ); } if ( 'all' === $coin_id ) { $clear_count = ADAMCA_Cache::clear_all_cache(); return new WP_REST_Response( array( 'success' => true, 'cleared' => $clear_count, ), 200 ); } ADAMCA_Cache::clear_cache( $coin_id ); return new WP_REST_Response( array( 'success' => true, 'cleared' => $coin_id, ), 200 ); } }