<?php
/**
 * A utility class to work around the $_POST max_input_vars limitation.
 *
 * @package Ealab\Deployer
 * @author Quang-Minh DANG <quang-minh@emerya.fr>
 */

namespace Ealab\Deployer\Utility;

/**
 * This is an evolved version of PostFormDataParser.
 *
 * This is useful to work around the max_input_vars limitation by
 * working with a JSON object instead of the $_POST values.
 *
 * It uses a specific structure to be able to build data
 * according to the field names, especially fields which share the same
 * name such as multivalued_field_name[].
 *
 * PostFormDataParser is based on the generic object representation of
 * the FormData object, which loses the data for fields with the same
 * shared name (the last field value overwrites the other values).
 */
class PostFormDataAsJsonParser {
	/**
	 * A placeholder value used by parse_str().
	 *
	 * This is used to discriminate between real values and the default value set by parse_str()
	 * when it parses the field names. If we don't have this, parse_str() returns an array (of arrays)
	 * with the final value being an empty string.
	 */
	private const PLACEHOLDER_VALUE = '_random_placeholder_f8d4r6d4tj6d4j65dgj4h6d8th4q6sd5g4dgc_eg_sekudv';

	/**
	 * Parses the raw post data.
	 *
	 * @param array|null $raw_post_input The raw post data. If not set, it will be retrieved internally
	 *     using php://input.
	 *
	 * @return array The processed post data.
	 */
	public static function parseRawPostData( ?array $raw_post_input = null ): array {
		if ( ! is_array( $raw_post_input ) ) {
			$raw_post_input = json_decode( file_get_contents( 'php://input' ), true );
		}

		if ( empty( $raw_post_input ) ) {
			return array();
		}

		$assembled_post = array();

		foreach ( $raw_post_input as $complete_field_name => $field_val ) {
			parse_str( $complete_field_name . '=' . static::PLACEHOLDER_VALUE, $parsed_field_name );
			if ( is_array( $parsed_field_name ) ) {
				$deeper = static function ( &$array_to_dig ) use ( &$deeper, $complete_field_name, $field_val ) {
					$e = reset( $array_to_dig );
					if ( is_array( $e ) ) {
						$deeper( $array_to_dig[ key( $array_to_dig ) ] );
					} else {
						static::setOrAppend( $array_to_dig, key( $array_to_dig ), $complete_field_name, $field_val );
					}
				};

				$deeper( $parsed_field_name );

				$assembled_post = array_merge_recursive( $assembled_post, $parsed_field_name );
			} else {
				// This should never happen if the values sent by the JS are well formatted.
				static::setOrAppend( $assembled_post, $complete_field_name, $complete_field_name, $field_val );
			}
		}

		return $assembled_post;
	}

	/**
	 * Sets or append a value to the given array, key and field name.
	 *
	 * @param array  $array_to_modify Array to modify by reference.
	 * @param string $key The key that should be set.
	 * @param string $complete_field_name The complete field name, as set in the HTML form, e.g. "field_name[k][]".
	 * @param mixed  $value_to_insert The value to insert. When in the POST data retrieval context, this is an array.
	 *     In other cases, this can be anything.
	 */
	public static function setOrAppend( array &$array_to_modify, string $key, string $complete_field_name, $value_to_insert ): void {
		$is_array_field = '[]' === substr( $complete_field_name, -2, 2 );

		if ( static::PLACEHOLDER_VALUE === $array_to_modify[ $key ] ) {
			// This is the default value set by parse_str.
			if ( $is_array_field ) {
				// When it's an array field, the given value is already an array
				// containing all the values of the field when this function is
				// called during POST data retrieval.
				// If not in an array wrapper, we wrap it.
				$array_to_modify = is_array( $value_to_insert )
					? $value_to_insert
					: array( $value_to_insert );

				return;
			}

			// This is not an array field, so we set the first (and only) value.
			$array_to_modify[ $key ] = reset( $value_to_insert );

			return;
		}

		// This key already has a real value set.
		// This case happens only when this function is called out of the
		// POST data retrieval context (someone intentionally called this
		// function to add a value).
		if ( $is_array_field ) {
			// Make sure that the value is an array, then append the value.
			if ( ! is_array( $array_to_modify[ $key ] ) ) {
				$array_to_modify[ $key ] = array( $array_to_modify[ $key ] );
			}

			// Append the value.
			$array_to_modify[ $key ][] = $value_to_insert;

			return;
		}

		// This is not an array. So we replace the value.
		$array_to_modify[ $key ] = $value_to_insert;
	}
}
