403 ) ); } $entry_id = isset( $_GET['entry'] ) ? absint( wp_unslash( $_GET['entry'] ) ) : 0; $index = isset( $_GET['i'] ) ? absint( wp_unslash( $_GET['i'] ) ) : 0; check_admin_referer( self::ACTION . '_' . $entry_id . '_' . $index ); $file = self::find( $entry_id, $index ); if ( ! $file ) { wp_die( esc_html__( 'That file no longer exists.', '2ranq-forms' ), '', array( 'response' => 404 ) ); } $dir = Ranq_Forms_Uploads::dir(); if ( ! $dir ) { wp_die( esc_html__( 'That file no longer exists.', '2ranq-forms' ), '', array( 'response' => 404 ) ); } // The path is built from OUR folder and the name out of the database. $path = trailingslashit( $dir ) . ltrim( (string) $file['file'], '/\\' ); $real = realpath( $path ); $base = realpath( $dir ); // And after all that, one more check that the file really is inside our folder. if ( ! $real || ! $base || 0 !== strpos( $real, $base ) || ! is_file( $real ) ) { wp_die( esc_html__( 'That file no longer exists.', '2ranq-forms' ), '', array( 'response' => 404 ) ); } $name = isset( $file['name'] ) ? sanitize_file_name( $file['name'] ) : wp_basename( $real ); $type = wp_check_filetype( $real ); $mime = $type['type'] ? $type['type'] : 'application/octet-stream'; nocache_headers(); header( 'Content-Type: ' . $mime ); header( 'Content-Length: ' . filesize( $real ) ); header( 'Content-Disposition: attachment; filename="' . $name . '"' ); // This is the most important header here. Without it the browser could decide // for itself that the file is something other than it says, and open it so. header( 'X-Content-Type-Options: nosniff' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- the file is sent to the browser, it is not read into memory. readfile( $real ); exit; } /** * Finds the data about a file inside an entry. * * @param int $entry_id The entry id. * @param int $index Which file in order. * * @return array|null */ private static function find( $entry_id, $index ) { global $wpdb; $table = Ranq_Forms_Install::entries_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $row = $wpdb->get_row( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- the table name comes from $wpdb->prefix and cannot come from a visitor, and `prepare` cannot bind a table name. $wpdb->prepare( "SELECT fields FROM {$table} WHERE id = %d", $entry_id ), ARRAY_A ); if ( ! $row ) { return null; } $values = json_decode( $row['fields'], true ); if ( ! is_array( $values ) ) { return null; } // Every file out of every field of that entry, in order. $all = array(); foreach ( $values as $value ) { if ( is_array( $value ) && ! empty( $value['files'] ) && is_array( $value['files'] ) ) { foreach ( $value['files'] as $one ) { $all[] = $one; } } } return isset( $all[ $index ] ) ? $all[ $index ] : null; } /** * Every file of one entry, with the address to open it by. * * @param int $entry_id The entry id. * @param array $values The entry values. * * @return array */ public static function list_for_entry( $entry_id, $values ) { $list = array(); $index = 0; foreach ( $values as $value ) { if ( ! is_array( $value ) || empty( $value['files'] ) || ! is_array( $value['files'] ) ) { continue; } foreach ( $value['files'] as $one ) { $list[] = array( 'name' => isset( $one['name'] ) ? $one['name'] : '', 'size' => isset( $one['size'] ) ? (int) $one['size'] : 0, 'url' => self::url( $entry_id, $index ), ); ++$index; } } return $list; } }