false, 'message' => $msg, 'errors' => $errors, ), $status ); } public static function submit( \WP_REST_Request $r ) { $p = $r->get_body_params(); $ref = explode( ':', (string) ( $p['_nx_form'] ?? '' ) ); $post_id = (int) ( $ref[0] ?? 0 ); $el = sanitize_key( $ref[1] ?? '' ); $generic = __( 'Something went wrong. Please try again.', 'akram' ); if ( ! $post_id || ! $el ) { return self::err( $generic ); } $status = get_post_status( $post_id ); if ( 'publish' !== $status && ! current_user_can( 'edit_post', $post_id ) ) { return self::err( $generic, 404 ); } $out = Document::get_out( $post_id ); $form = $out['forms'][ $el ] ?? null; if ( ! $form ) { return self::err( __( 'This form is no longer available.', 'akram' ), 404 ); } $s = (array) ( $form['s'] ?? array() ); // Spam protection: honeypot, minimum fill time, rate limit, optional CAPTCHA. if ( ! empty( $p['_nx_hp'] ) ) { return new \WP_REST_Response( array( 'ok' => true, 'message' => $s['success'] ?? '', ), 200 ); } if ( (int) ( $p['_nx_t'] ?? 0 ) < 1500 ) { return self::err( __( 'Please take a moment before submitting.', 'akram' ), 429 ); } $ip = self::ip(); $rkey = 'akram_rate_' . md5( $ip ); $hits = (int) get_transient( $rkey ); if ( $hits >= max( 1, (int) Options::get( 'form_rate', 5 ) ) ) { return self::err( __( 'Too many submissions. Please wait a minute and try again.', 'akram' ), 429 ); } set_transient( $rkey, $hits + 1, MINUTE_IN_SECONDS ); if ( ! empty( $s['captcha'] ) && ! self::verify_captcha( $p, $ip ) ) { return self::err( __( 'CAPTCHA verification failed. Please try again.', 'akram' ), 403 ); } // Validate against the stored definition. $fields = array_values( array_filter( (array) ( $s['fields'] ?? array() ), 'is_array' ) ); $names = array(); foreach ( $fields as $i => $f ) { $names[ $i ] = self::field_name( $f, $i ); } $raw = array(); foreach ( $fields as $i => $f ) { $n = $names[ $i ]; if ( isset( $p[ $n ] ) ) { $raw[ $n ] = is_array( $p[ $n ] ) ? array_map( 'sanitize_text_field', array_map( 'wp_unslash', $p[ $n ] ) ) : wp_unslash( (string) $p[ $n ] ); } } $errors = array(); $values = array(); $files = $r->get_file_params(); foreach ( $fields as $i => $f ) { $type = (string) ( $f['type'] ?? 'text' ); $n = $names[ $i ]; if ( in_array( $type, array( 'step', 'html' ), true ) || ! self::field_visible( $f, $raw ) ) { continue; } $label = wp_strip_all_tags( (string) ( $f['label'] ?? $n ) ); $req = ! empty( $f['req'] ); $v = $raw[ $n ] ?? ''; if ( 'file' === $type ) { $res = self::handle_files( $files[ $n ] ?? null, $f, $req ); if ( is_wp_error( $res ) ) { $errors[ $n ] = $res->get_error_message(); continue; } $values[] = array( 'key' => $n, 'label' => $label, 'type' => $type, 'value' => implode( "\n", $res ), ); continue; } if ( 'signature' === $type ) { if ( $v && 0 === strpos( $v, 'data:image/png;base64,' ) ) { $url = self::save_signature( $v ); $v = $url ?: ''; } else { $v = ''; } } if ( 'calc' === $type ) { $v = (string) self::calc( (string) ( $f['formula'] ?? '' ), $raw, (int) ( $f['dec'] ?? 2 ) ); } $is_empty = is_array( $v ) ? ! array_filter( $v, 'strlen' ) : '' === trim( (string) $v ); if ( $req && $is_empty ) { $errors[ $n ] = __( 'This field is required.', 'akram' ); continue; } if ( $is_empty ) { $values[] = array( 'key' => $n, 'label' => $label, 'type' => $type, 'value' => '', ); continue; } switch ( $type ) { case 'email': $v = sanitize_email( $v ); if ( ! is_email( $v ) ) { $errors[ $n ] = __( 'Please enter a valid email address.', 'akram' ); } break; case 'url': $v = esc_url_raw( $v ); if ( ! $v ) { $errors[ $n ] = __( 'Please enter a valid URL.', 'akram' ); } break; case 'number': case 'range': if ( ! is_numeric( $v ) ) { $errors[ $n ] = __( 'Please enter a number.', 'akram' ); } elseif ( '' !== ( $f['min'] ?? '' ) && (float) $v < (float) $f['min'] || '' !== ( $f['max'] ?? '' ) && (float) $v > (float) $f['max'] ) { $errors[ $n ] = __( 'Value is out of range.', 'akram' ); } break; case 'rating': $v = (string) max( 0, min( 10, (int) $v ) ); break; case 'nps': if ( ! is_numeric( $v ) || (int) $v < 0 || (int) $v > 10 ) { $errors[ $n ] = __( 'Please choose a score from 0 to 10.', 'akram' ); } $v = (string) (int) $v; break; case 'toggle': $v = 'yes' === $v ? __( 'Yes', 'akram' ) : __( 'No', 'akram' ); break; case 'color': $v = (string) sanitize_hex_color( (string) $v ); if ( ! $v ) { $errors[ $n ] = __( 'Please choose a valid color.', 'akram' ); } break; case 'imgselect': case 'scale': case 'select': case 'radio': case 'checkbox': $allowed = self::opts( 'scale' === $type && '' === trim( (string) ( $f['opts'] ?? '' ) ) ? "Strongly disagree\nDisagree\nNeutral\nAgree\nStrongly agree" : ( $f['opts'] ?? '' ) ); $list = is_array( $v ) ? $v : array( $v ); foreach ( $list as $one ) { if ( ! isset( $allowed[ $one ] ) ) { $errors[ $n ] = __( 'Please choose a valid option.', 'akram' ); } } $v = implode( ', ', array_map( static function ( $x ) use ( $allowed ) { return $allowed[ $x ] ?? $x; }, $list ) ); break; case 'acceptance': $v = 'yes' === $v ? __( 'Yes', 'akram' ) : ''; if ( $req && ! $v ) { $errors[ $n ] = __( 'Please accept to continue.', 'akram' ); } break; case 'date': if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $v ) ) { $errors[ $n ] = __( 'Please enter a valid date.', 'akram' ); } break; case 'time': if ( ! preg_match( '/^\d{2}:\d{2}/', $v ) ) { $errors[ $n ] = __( 'Please enter a valid time.', 'akram' ); } break; case 'textarea': $v = sanitize_textarea_field( $v ); break; case 'password': $v = __( '(hidden)', 'akram' ); break; default: $v = sanitize_text_field( is_array( $v ) ? implode( ', ', $v ) : $v ); if ( ! empty( $f['pattern'] ) && @preg_match( '/^(?:' . str_replace( '/', '\/', $f['pattern'] ) . ')$/u', $v ) === 0 ) { $errors[ $n ] = __( 'Please match the requested format.', 'akram' ); } } if ( ! empty( $f['max'] ) && in_array( $type, array( 'text', 'textarea' ), true ) && mb_strlen( (string) $v ) > (int) $f['max'] ) { $errors[ $n ] = __( 'Too long.', 'akram' ); } $values[] = array( 'key' => $n, 'label' => $label, 'type' => $type, 'value' => is_array( $v ) ? implode( ', ', $v ) : (string) $v, ); } if ( $errors ) { return self::err( __( 'Please fix the highlighted fields.', 'akram' ), 422, $errors ); } $values = apply_filters( 'akram_form_values', $values, $s, $post_id, $el ); $map = array(); foreach ( $values as $row ) { $map[ $row['key'] ] = $row['value']; } $page = esc_url_raw( (string) ( $p['_nx_page'] ?? '' ) ); $meta = array( 'ip' => self::mask_ip( $ip ), 'ua' => substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), 0, 250 ), 'page' => $page, 'user' => get_current_user_id(), ); $sub_id = 0; if ( ! isset( $s['store'] ) || $s['store'] ) { $first = ''; foreach ( $values as $row ) { if ( 'email' === $row['type'] && $row['value'] ) { $first = $row['value']; break; } } $sub_id = wp_insert_post( array( 'post_type' => 'akram_submission', 'post_status' => 'private', 'post_title' => sanitize_text_field( ( $s['formName'] ?? 'Form' ) . ( $first ? ' — ' . $first : '' ) ), 'post_parent' => $post_id, ) ); if ( $sub_id && ! is_wp_error( $sub_id ) ) { update_post_meta( $sub_id, '_akram_form', $post_id . ':' . $el ); update_post_meta( $sub_id, '_akram_form_name', sanitize_text_field( $s['formName'] ?? 'Form' ) ); update_post_meta( $sub_id, '_akram_fields', wp_slash( wp_json_encode( $values ) ) ); update_post_meta( $sub_id, '_akram_meta', wp_slash( wp_json_encode( $meta ) ) ); } } $repl = static function ( $tpl, $html = false ) use ( $values, $map, $page ) { $all = ''; foreach ( $values as $row ) { if ( '' === $row['value'] ) { continue; } $all .= $html ? '
' . esc_html( sprintf( /* translators: %s: page url */ __( 'Sent from %s', 'akram' ), $page ?: home_url() ) ) . '
'; if ( ! wp_mail( $to, $subject, $body, $headers ) ) { $integration_errors[] = 'email'; } } if ( ! empty( $s['auto'] ) ) { $visitor = $map[ $s['autoField'] ?? 'email' ] ?? ''; if ( $visitor && is_email( $visitor ) ) { wp_mail( $visitor, $subject_of( $s['autoSubject'] ?? '' ), $repl( nl2br( esc_html( (string) ( $s['autoBody'] ?? '' ) ) ), true ), array( 'Content-Type: text/html; charset=UTF-8' ) ); } } if ( ! empty( $s['webhook'] ) && wp_http_validate_url( $s['webhook'] ) ) { $res = wp_safe_remote_post( $s['webhook'], array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'form' => $s['formName'] ?? '', 'fields' => $map, 'page' => $page, 'submitted_at' => gmdate( 'c' ), ) ), ) ); if ( is_wp_error( $res ) || wp_remote_retrieve_response_code( $res ) >= 400 ) { $integration_errors[] = 'webhook'; } } if ( ! empty( $s['mailchimp'] ) && ! empty( $s['mcList'] ) ) { $email = ''; foreach ( $values as $row ) { if ( 'email' === $row['type'] && is_email( $row['value'] ) ) { $email = $row['value']; break; } } if ( $email && ! self::mailchimp( $email, $map, $s ) ) { $integration_errors[] = 'mailchimp'; } } if ( $sub_id && $integration_errors ) { update_post_meta( $sub_id, '_akram_errors', $integration_errors ); } do_action( 'akram_form_submitted', $map, $s, $post_id, $el, $sub_id ); return new \WP_REST_Response( array( 'ok' => true, 'message' => $s['success'] ?? __( 'Thank you!', 'akram' ), 'redirect' => ! empty( $s['redirect'] ) ? esc_url_raw( $repl( $s['redirect'] ) ) : '', ), 200 ); } /** * Calculation fields: numbers, + - * / and parentheses, worked out by a small parser (nothing is evaluated as code). * Same rules as evaluate() in src/frontend/_calc.js, which shows the result in the browser; a division by zero gives 0. */ public static function calc( $formula, array $raw, $dec = 2 ) { $expr = preg_replace_callback( '/\{([a-z0-9_-]+)\}/i', static function ( $m ) use ( $raw ) { $v = $raw[ str_replace( '-', '_', strtolower( $m[1] ) ) ] ?? 0; $v = is_array( $v ) ? array_sum( array_map( 'floatval', $v ) ) : (float) preg_replace( '/[^0-9.\-]/', '', (string) $v ); return '(' . $v . ')'; }, $formula ); if ( ! preg_match( '/^[0-9+\-*\/().\s]*$/', $expr ) ) { return 0; } // Tokens: numbers and the operators + - * / ( ). preg_match_all( '/\d*\.?\d+|[-+*\/()]/', $expr, $m ); $pos = 0; $res = $m[0] ? self::calc_sum( $m[0], $pos ) : 0.0; return is_finite( $res ) ? round( $res, max( 0, min( 6, $dec ) ) ) : 0; } /** Whether a field's show-if condition holds for the submitted values (fields without one are always shown). */ private static function field_visible( array $f, array $raw ) { if ( empty( $f['cf'] ) ) { return true; } $key = str_replace( '-', '_', sanitize_title( (string) $f['cf'] ) ); $v = $raw[ $key ] ?? ''; $v = is_array( $v ) ? implode( ', ', $v ) : (string) $v; $t = (string) ( $f['cv'] ?? '' ); switch ( $f['cop'] ?? 'is' ) { case 'not': return $v !== $t; case 'has': return '' !== $t && false !== stripos( $v, $t ); case 'gt': return (float) $v > (float) $t; case 'lt': return (float) $v < (float) $t; case 'set': return '' !== $v; case 'empty': return '' === $v; default: return $v === $t || in_array( $t, explode( ', ', $v ), true ); } } /** Sum: products joined by + and -. */ private static function calc_sum( array $tokens, &$pos ) { $v = self::calc_product( $tokens, $pos ); while ( isset( $tokens[ $pos ] ) && ( '+' === $tokens[ $pos ] || '-' === $tokens[ $pos ] ) ) { $op = $tokens[ $pos++ ]; $r = self::calc_product( $tokens, $pos ); $v = '+' === $op ? $v + $r : $v - $r; } return $v; } /** Product: factors joined by * and /. */ private static function calc_product( array $tokens, &$pos ) { $v = self::calc_factor( $tokens, $pos ); while ( isset( $tokens[ $pos ] ) && ( '*' === $tokens[ $pos ] || '/' === $tokens[ $pos ] ) ) { $op = $tokens[ $pos++ ]; $r = self::calc_factor( $tokens, $pos ); $v = '*' === $op ? $v * $r : ( 0.0 === $r ? 0.0 : $v / $r ); } return $v; } /** Factor: a number, a signed factor or a bracketed sum. */ private static function calc_factor( array $tokens, &$pos ) { $t = $tokens[ $pos ] ?? '0'; $pos++; if ( '(' === $t ) { $v = self::calc_sum( $tokens, $pos ); $pos++; // closing parenthesis return $v; } if ( '-' === $t ) { return -1 * self::calc_factor( $tokens, $pos ); } if ( '+' === $t ) { return self::calc_factor( $tokens, $pos ); } return (float) $t; } private static function handle_files( $file, array $f, $req ) { if ( ! $file || empty( $file['name'] ) || ( is_array( $file['name'] ) && ! array_filter( $file['name'] ) ) ) { return $req ? new \WP_Error( 'req', __( 'Please attach a file.', 'akram' ) ) : array(); } require_once ABSPATH . 'wp-admin/includes/file.php'; $list = array(); if ( is_array( $file['name'] ) ) { foreach ( $file['name'] as $k => $name ) { $list[] = array( 'name' => $name, 'type' => $file['type'][ $k ], 'tmp_name' => $file['tmp_name'][ $k ], 'error' => $file['error'][ $k ], 'size' => $file['size'][ $k ], ); } } else { $list[] = $file; } $max = max( 1, (float) ( $f['maxmb'] ?? 5 ) ) * MB_IN_BYTES; $accept = array_filter( array_map( static function ( $x ) { return strtolower( ltrim( trim( $x ), '.' ) ); }, explode( ',', (string) ( $f['accept'] ?? '' ) ) ) ); $urls = array(); $dir_cb = static function ( $d ) { $d['subdir'] = '/akram-forms' . $d['subdir']; $d['path'] = $d['basedir'] . $d['subdir']; $d['url'] = $d['baseurl'] . $d['subdir']; return $d; }; add_filter( 'upload_dir', $dir_cb ); foreach ( array_slice( $list, 0, 10 ) as $one ) { if ( UPLOAD_ERR_OK !== (int) $one['error'] ) { continue; } if ( $one['size'] > $max ) { remove_filter( 'upload_dir', $dir_cb ); return new \WP_Error( 'size', __( 'File is too large.', 'akram' ) ); } $check = wp_check_filetype_and_ext( $one['tmp_name'], $one['name'] ); $ext = strtolower( (string) $check['ext'] ); if ( ! $ext || ( $accept && ! array_intersect( array( $ext, strtok( (string) $check['type'], '/' ) . '/*' ), array_merge( $accept, array_map( static function ( $a ) { return $a; }, $accept ) ) ) ) ) { remove_filter( 'upload_dir', $dir_cb ); return new \WP_Error( 'type', __( 'This file type is not allowed.', 'akram' ) ); } $one['name'] = wp_generate_password( 12, false ) . '-' . sanitize_file_name( $one['name'] ); $up = wp_handle_upload( $one, array( 'test_form' => false ) ); if ( ! empty( $up['url'] ) ) { $urls[] = $up['url']; } } remove_filter( 'upload_dir', $dir_cb ); return $urls; } private static function save_signature( $data ) { $bin = base64_decode( substr( $data, strlen( 'data:image/png;base64,' ) ), true ); if ( ! $bin || strlen( $bin ) > 2 * MB_IN_BYTES || "\x89PNG" !== substr( $bin, 0, 4 ) ) { return ''; } $up = wp_upload_bits( 'signature-' . wp_generate_password( 10, false ) . '.png', null, $bin ); return empty( $up['error'] ) ? $up['url'] : ''; } private static function verify_captcha( array $p, $ip ) { $type = Options::get( 'captcha' ); if ( 'recaptcha' === $type && Options::get( 'recaptcha_secret' ) ) { $res = wp_remote_post( self::RECAPTCHA_VERIFY, array( 'timeout' => 8, 'body' => array( 'secret' => Options::get( 'recaptcha_secret' ), 'response' => (string) ( $p['_nx_captcha'] ?? '' ), 'remoteip' => $ip, ), ) ); $body = json_decode( wp_remote_retrieve_body( $res ), true ); return ! empty( $body['success'] ) && ( ! isset( $body['score'] ) || $body['score'] >= 0.5 ); } if ( 'turnstile' === $type && Options::get( 'turnstile_secret' ) ) { $res = wp_remote_post( self::TURNSTILE_VERIFY, array( 'timeout' => 8, 'body' => array( 'secret' => Options::get( 'turnstile_secret' ), 'response' => (string) ( $p['cf-turnstile-response'] ?? '' ), 'remoteip' => $ip, ), ) ); $body = json_decode( wp_remote_retrieve_body( $res ), true ); return ! empty( $body['success'] ); } return true; } private static function mailchimp( $email, array $map, array $s ) { $key = (string) Options::get( 'mailchimp_key' ); if ( ! $key || false === strpos( $key, '-' ) ) { return false; } $dc = preg_replace( '/[^a-z0-9]/', '', substr( $key, strrpos( $key, '-' ) + 1 ) ); $list = preg_replace( '/[^a-z0-9]/', '', (string) $s['mcList'] ); $name = $map['name'] ?? ( $map['first_name'] ?? '' ); $res = wp_remote_request( "https://{$dc}.api.mailchimp.com/3.0/lists/{$list}/members/" . md5( strtolower( $email ) ), array( 'method' => 'PUT', 'timeout' => 10, 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( 'akram:' . $key ), 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'email_address' => $email, 'status_if_new' => ! empty( $s['mcDouble'] ) ? 'pending' : 'subscribed', 'merge_fields' => array_filter( array( 'FNAME' => $name, 'LNAME' => $map['last_name'] ?? '', ) ), ) ), ) ); return ! is_wp_error( $res ) && wp_remote_retrieve_response_code( $res ) < 300; } private static function ip() { $ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) ); return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0'; } private static function mask_ip( $ip ) { return false !== strpos( $ip, ':' ) ? preg_replace( '/:[0-9a-f]*:[0-9a-f]*$/i', ':0:0', $ip ) : preg_replace( '/\.\d+$/', '.0', $ip ); } // ------------------------------------------------------------------ // Admin: submissions // ------------------------------------------------------------------ private static function row( $post, $full = false ) { $fields = json_decode( (string) get_post_meta( $post->ID, '_akram_fields', true ), true ) ?: array(); $row = array( 'id' => $post->ID, 'title' => $post->post_title, 'form' => get_post_meta( $post->ID, '_akram_form_name', true ), 'formId' => get_post_meta( $post->ID, '_akram_form', true ), 'page' => (int) $post->post_parent, 'pageTitle' => get_the_title( $post->post_parent ), 'date' => get_post_time( 'c', true, $post ), 'read' => 'publish' === $post->post_status, 'star' => (bool) get_post_meta( $post->ID, '_akram_star', true ), 'preview' => implode( ' · ', array_slice( array_filter( array_map( static function ( $f ) { return is_string( $f['value'] ?? null ) ? wp_trim_words( $f['value'], 8 ) : ''; }, $fields ) ), 0, 3 ) ), 'errors' => array_values( array_filter( (array) get_post_meta( $post->ID, '_akram_errors', true ) ) ), ); if ( $full ) { $row['fields'] = $fields; $row['meta'] = json_decode( (string) get_post_meta( $post->ID, '_akram_meta', true ), true ) ?: array(); } return $row; } public static function rest_list( \WP_REST_Request $r ) { $args = array( 'post_type' => 'akram_submission', 'post_status' => array( 'private', 'publish' ), 'posts_per_page' => min( 100, max( 1, (int) ( $r['per'] ?: 25 ) ) ), 'paged' => max( 1, (int) $r['page'] ), 'orderby' => 'date', 'order' => 'DESC', ); if ( $r['form'] ) { $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded query on a builder meta key. array( 'key' => '_akram_form', 'value' => sanitize_text_field( $r['form'] ), ), ); } if ( $r['s'] ) { $args['s'] = sanitize_text_field( $r['s'] ); } if ( 'unread' === $r['status'] ) { $args['post_status'] = 'private'; } if ( 'starred' === $r['status'] ) { $args['meta_query'][] = array( 'key' => '_akram_star', 'value' => '1', ); } $q = new \WP_Query( $args ); $items = array_map( array( __CLASS__, 'row' ), $q->posts ); global $wpdb; // One grouped query for the per-form totals (no WordPress API aggregates meta); it takes no user input. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $forms = $wpdb->get_results( "SELECT m.meta_value AS id, MAX(n.meta_value) AS name, COUNT(*) AS total, SUM(p.post_status='private') AS unread FROM {$wpdb->postmeta} m JOIN {$wpdb->posts} p ON p.ID=m.post_id LEFT JOIN {$wpdb->postmeta} n ON n.post_id=m.post_id AND n.meta_key='_akram_form_name' WHERE m.meta_key='_akram_form' AND p.post_type='akram_submission' GROUP BY m.meta_value", ARRAY_A ); return array( 'items' => $items, 'total' => (int) $q->found_posts, 'pages' => (int) $q->max_num_pages, 'forms' => $forms, ); } public static function rest_get( \WP_REST_Request $r ) { $post = get_post( (int) $r['id'] ); if ( ! $post || 'akram_submission' !== $post->post_type ) { return new \WP_Error( 'akram_missing', 'Not found', array( 'status' => 404 ) ); } if ( 'private' === $post->post_status ) { wp_update_post( array( 'ID' => $post->ID, 'post_status' => 'publish', ) ); $post = get_post( $post->ID ); } return self::row( $post, true ); } public static function rest_update( \WP_REST_Request $r ) { $id = (int) $r['id']; if ( 'akram_submission' !== get_post_type( $id ) ) { return new \WP_Error( 'akram_missing', 'Not found', array( 'status' => 404 ) ); } $p = $r->get_json_params(); if ( isset( $p['read'] ) ) { wp_update_post( array( 'ID' => $id, 'post_status' => $p['read'] ? 'publish' : 'private', ) ); } if ( isset( $p['star'] ) ) { $p['star'] ? update_post_meta( $id, '_akram_star', '1' ) : delete_post_meta( $id, '_akram_star' ); } return self::row( get_post( $id ) ); } public static function rest_delete( \WP_REST_Request $r ) { $id = (int) $r['id']; if ( 'akram_submission' !== get_post_type( $id ) ) { return new \WP_Error( 'akram_missing', 'Not found', array( 'status' => 404 ) ); } wp_delete_post( $id, true ); return array( 'ok' => true ); } public static function rest_export( \WP_REST_Request $r ) { return array( 'url' => add_query_arg( array( 'action' => 'akram_export', 'form' => rawurlencode( (string) $r['form'] ), '_wpnonce' => wp_create_nonce( 'akram_export' ), ), admin_url( 'admin-post.php' ) ), ); } public static function export_csv() { if ( ! Rest::can_submissions() || ! check_admin_referer( 'akram_export' ) ) { wp_die( 'Forbidden', 403 ); } $form = isset( $_GET['form'] ) ? sanitize_text_field( wp_unslash( $_GET['form'] ) ) : ''; $args = array( 'post_type' => 'akram_submission', 'post_status' => array( 'private', 'publish' ), 'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC', ); if ( $form ) { $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded query on a builder meta key. array( 'key' => '_akram_form', 'value' => $form, ), ); } $rows = array(); $cols = array(); foreach ( get_posts( $args ) as $post ) { $fields = json_decode( (string) get_post_meta( $post->ID, '_akram_fields', true ), true ) ?: array(); $meta = json_decode( (string) get_post_meta( $post->ID, '_akram_meta', true ), true ) ?: array(); $row = array( 'Date' => get_post_time( 'Y-m-d H:i:s', false, $post ), 'Form' => get_post_meta( $post->ID, '_akram_form_name', true ), ); foreach ( $fields as $f ) { $row[ $f['label'] ] = $f['value']; } $row['Page'] = $meta['page'] ?? ''; $cols = array_unique( array_merge( $cols, array_keys( $row ) ) ); $rows[] = $row; } $safe = static function ( $v ) { $v = (string) $v; return preg_match( '/^[=+\-@\t\r]/', $v ) ? "'" . $v : $v; // CSV injection guard }; nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=akram-submissions-' . gmdate( 'Y-m-d' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- stream/temp file handled directly (no WP_Filesystem equivalent). fputcsv( $out, array_map( $safe, $cols ) ); foreach ( $rows as $row ) { fputcsv( $out, array_map( static function ( $c ) use ( $row, $safe ) { return $safe( $row[ $c ] ?? '' ); }, $cols ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- stream/temp file handled directly (no WP_Filesystem equivalent). exit; } public static function retention() { $days = (int) Options::get( 'retention_days', 0 ); if ( $days <= 0 ) { return; } foreach ( get_posts( array( 'post_type' => 'akram_submission', 'post_status' => array( 'private', 'publish' ), 'posts_per_page' => 500, 'fields' => 'ids', 'date_query' => array( array( 'before' => $days . ' days ago' ) ), ) ) as $id ) { wp_delete_post( $id, true ); } } public static function cart_fragments( $fragments ) { if ( function_exists( 'WC' ) && WC()->cart ) { $fragments['.nx-cart-n'] = '' . (int) WC()->cart->get_cart_contents_count() . ''; $fragments['.nx-cart-total'] = '' . wp_kses_post( WC()->cart->get_cart_total() ) . ''; } return $fragments; } }