<?php
/**
 * This file builds an external CSS file for our options.
 *
 * @package GP Premium
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // No direct access, please.
}

/**
 * Build and enqueue a dynamic stylsheet if needed.
 */
class GeneratePress_External_CSS_File {
	/**
	 * Instance.
	 *
	 * @access private
	 * @var object Instance
	 * @since 1.11.0
	 */
	private static $instance;

	/**
	 * Initiator.
	 *
	 * @since 1.11.0
	 * @return object initialized object of class.
	 */
	public static function get_instance() {
		if ( ! isset( self::$instance ) ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_dynamic_css' ), 20 );
		add_action( 'wp', array( $this, 'init' ), 9 );
		add_action( 'customize_save_after', array( $this, 'delete_saved_time' ) );
		add_action( 'customize_register', array( $this, 'add_customizer_field' ) );
		add_filter( 'generate_option_defaults', array( $this, 'add_option_default' ) );
		add_filter( 'generatepress_dynamic_css_print_method', array( $this, 'set_print_method' ) );

		if ( ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Just checking, false positive.
			add_action( 'wp_ajax_generatepress_regenerate_css_file', array( $this, 'regenerate_css_file' ) );
		}
	}

	/**
	 * Set our CSS Print Method default.
	 *
	 * @param array $defaults Our existing defaults.
	 */
	public function add_option_default( $defaults ) {
		$defaults['css_print_method'] = 'inline';

		return $defaults;
	}

	/**
	 * Add our option to the Customizer.
	 *
	 * @param object $wp_customize The Customizer object.
	 */
	public function add_customizer_field( $wp_customize ) {
		if ( ! function_exists( 'generate_get_defaults' ) ) {
			return;
		}

		$defaults = generate_get_defaults();

		require_once GP_LIBRARY_DIRECTORY . 'customizer-helpers.php';

		if ( method_exists( $wp_customize, 'register_control_type' ) ) {
			$wp_customize->register_control_type( 'GeneratePress_Action_Button_Control' );
		}

		$wp_customize->add_setting(
			'generate_settings[css_print_method]',
			array(
				'default' => $defaults['css_print_method'],
				'type' => 'option',
				'sanitize_callback' => 'generate_premium_sanitize_choices',
			)
		);

		$wp_customize->add_control(
			'generate_settings[css_print_method]',
			array(
				'type' => 'select',
				'label' => __( 'Dynamic CSS Print Method', 'gp-premium' ),
				'description' => __( 'Generating your dynamic CSS in an external file offers significant performance advantages.', 'gp-premium' ),
				'section' => 'generate_general_section',
				'choices' => array(
					'inline' => __( 'Inline Embedding', 'gp-premium' ),
					'file' => __( 'External File', 'gp-premium' ),
				),
				'settings' => 'generate_settings[css_print_method]',
			)
		);

		$wp_customize->add_control(
			new GeneratePress_Action_Button_Control(
				$wp_customize,
				'generate_regenerate_external_css_file',
				array(
					'section' => 'generate_general_section',
					'data_type' => 'regenerate_external_css',
					'nonce' => esc_html( wp_create_nonce( 'generatepress_regenerate_css_file' ) ),
					'label' => __( 'Regenerate CSS File', 'gp-premium' ),
					'settings' => ( isset( $wp_customize->selective_refresh ) ) ? array() : 'blogname',
					'active_callback' => 'generate_is_using_external_css_file_callback',
				)
			)
		);
	}

	/**
	 * Set our CSS Print Method.
	 *
	 * @param string $method The existing method.
	 */
	public function set_print_method( $method ) {
		if ( ! function_exists( 'generate_get_option' ) ) {
			return $method;
		}

		return generate_get_option( 'css_print_method' );
	}

	/**
	 * Determine if we're using file mode or inline mode.
	 */
	public function mode() {
		$mode = generate_get_css_print_method();

		if ( 'file' === $mode && $this->needs_update() ) {
			$data = get_option( 'generatepress_dynamic_css_data', array() );

			if ( ! isset( $data['updated_time'] ) ) {
				// No time set, so set the current time minus 5 seconds so the file is still generated.
				$data['updated_time'] = time() - 5;
				update_option( 'generatepress_dynamic_css_data', $data );
			}

			// Only allow processing 1 file every 5 seconds.
			$current_time = (int) time();
			$last_time    = (int) $data['updated_time'];

			if ( 5 <= ( $current_time - $last_time ) ) {

				// Attempt to write to the file.
				$mode = ( $this->can_write() && $this->make_css() ) ? 'file' : 'inline';

				// Does again if the file exists.
				if ( 'file' === $mode ) {
					$mode = ( file_exists( $this->file( 'path' ) ) ) ? 'file' : 'inline';
				}
			}
		}

		return $mode;
	}

	/**
	 * Set things up.
	 */
	public function init() {
		if ( 'file' === $this->mode() ) {
			add_filter( 'generate_using_dynamic_css_external_file', '__return_true' );
			add_filter( 'generate_dynamic_css_skip_cache', '__return_true', 20 );

			// Remove inline CSS in GP < 3.0.0.
			if ( ! function_exists( 'generate_get_dynamic_css' ) && function_exists( 'generate_enqueue_dynamic_css' ) ) {
				remove_action( 'wp_enqueue_scripts', 'generate_enqueue_dynamic_css', 50 );
			}
		}
	}

	/**
	 * Enqueue the dynamic CSS.
	 */
	public function enqueue_dynamic_css() {
		if ( 'file' === $this->mode() ) {
			wp_enqueue_style( 'generatepress-dynamic', esc_url( $this->file( 'uri' ) ), array( 'generate-style' ), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion

			// Move the child theme after our dynamic stylesheet.
			if ( is_child_theme() && wp_style_is( 'generate-child', 'enqueued' ) ) {
				wp_dequeue_style( 'generate-child' );
				wp_enqueue_style( 'generate-child' );
			}

			// Re-add no-cache CSS in GP < 3.0.0.
			if ( ! function_exists( 'generate_get_dynamic_css' ) && function_exists( 'generate_no_cache_dynamic_css' ) ) {
				$nocache_css = generate_no_cache_dynamic_css();

				if ( function_exists( 'generate_do_icon_css' ) ) {
					$nocache_css .= generate_do_icon_css();
				}

				wp_add_inline_style( 'generate-style', wp_strip_all_tags( $nocache_css ) );
			}
		}
	}

	/**
	 * Make our CSS.
	 */
	public function make_css() {
		$content = '';

		if ( function_exists( 'generate_get_dynamic_css' ) ) {
			$content = generate_get_dynamic_css();
		} elseif ( function_exists( 'generate_base_css' ) && function_exists( 'generate_font_css' ) && function_exists( 'generate_advanced_css' ) && function_exists( 'generate_spacing_css' ) ) {
			$content = generate_base_css() . generate_font_css() . generate_advanced_css() . generate_spacing_css();
		}

		$content = apply_filters( 'generate_external_dynamic_css_output', $content );

		if ( ! $content ) {
			return false;
		}

		global $wp_filesystem;

		// Initialize the WordPress filesystem.
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}

		// Take care of domain mapping.
		if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
			if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
				$mapped_domain = domain_mapping_siteurl( false );
				$mapped_domain = str_replace( 'https://', '//', $domain_mapping );
				$mapped_domain = str_replace( 'http://', '//', $mapped_domain );

				$original_domain = get_original_url( 'siteurl' );
				$original_domain = str_replace( 'https://', '//', $original_domain );
				$original_domain = str_replace( 'http://', '//', $original_domain );

				$content = str_replace( $original_domain, $mapped_domain, $content );
			}
		}

		// Strip protocols.
		$content = str_replace( 'https://', '//', $content );
		$content = str_replace( 'http://', '//', $content );

		if ( is_writable( $this->file( 'path' ) ) || ( ! file_exists( $this->file( 'path' ) ) && is_writable( dirname( $this->file( 'path' ) ) ) ) ) {

			if ( ! $wp_filesystem->put_contents( $this->file( 'path' ), wp_strip_all_tags( $content ), FS_CHMOD_FILE ) ) {

				// Fail!
				return false;

			} else {

				$this->update_saved_time();

				// Success!
				return true;

			}
		}
	}

	/**
	 * Determines if the CSS file is writable.
	 */
	public function can_write() {
		global $blog_id;

		// Get the upload directory for this site.
		$upload_dir = wp_upload_dir();

		// If this is a multisite installation, append the blogid to the filename.
		$css_blog_id = ( is_multisite() && $blog_id > 1 ) ? '_blog-' . $blog_id : null;

		$file_name   = '/style' . $css_blog_id . '.min.css';
		$folder_path = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'generatepress';

		// Does the folder exist?
		if ( file_exists( $folder_path ) ) {
			// Folder exists, but is the folder writable?
			if ( ! is_writable( $folder_path ) ) {
				// Folder is not writable.
				// Does the file exist?
				if ( ! file_exists( $folder_path . $file_name ) ) {
					// File does not exist, therefore it can't be created
					// since the parent folder is not writable.
					return false;
				} else {
					// File exists, but is it writable?
					if ( ! is_writable( $folder_path . $file_name ) ) {
						// Nope, it's not writable.
						return false;
					}
				}
			} else {
				// The folder is writable.
				// Does the file exist?
				if ( file_exists( $folder_path . $file_name ) ) {
					// File exists.
					// Is it writable?
					if ( ! is_writable( $folder_path . $file_name ) ) {
						// Nope, it's not writable.
						return false;
					}
				}
			}
		} else {
			// Can we create the folder?
			// returns true if yes and false if not.
			return wp_mkdir_p( $folder_path );
		}

		// all is well!
		return true;
	}

	/**
	 * Gets the css path or url to the stylesheet
	 *
	 * @param string $target path/url.
	 */
	public function file( $target = 'path' ) {
		global $blog_id;

		// Get the upload directory for this site.
		$upload_dir = wp_upload_dir();

		// If this is a multisite installation, append the blogid to the filename.
		$css_blog_id = ( is_multisite() && $blog_id > 1 ) ? '_blog-' . $blog_id : null;

		$file_name   = 'style' . $css_blog_id . '.min.css';
		$folder_path = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'generatepress';

		// The complete path to the file.
		$file_path = $folder_path . DIRECTORY_SEPARATOR . $file_name;

		// Get the URL directory of the stylesheet.
		$css_uri_folder = $upload_dir['baseurl'];

		$css_uri = trailingslashit( $css_uri_folder ) . 'generatepress/' . $file_name;

		// Take care of domain mapping.
		if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
			if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
				$mapped_domain   = domain_mapping_siteurl( false );
				$original_domain = get_original_url( 'siteurl' );
				$css_uri         = str_replace( $original_domain, $mapped_domain, $css_uri );
			}
		}

		// Strip protocols.
		$css_uri = str_replace( 'https://', '//', $css_uri );
		$css_uri = str_replace( 'http://', '//', $css_uri );

		if ( 'path' === $target ) {
			return $file_path;
		} elseif ( 'url' === $target || 'uri' === $target ) {
			$timestamp = ( file_exists( $file_path ) ) ? '?ver=' . filemtime( $file_path ) : '';
			return $css_uri . $timestamp;
		}
	}

	/**
	 * Update the our updated file time.
	 */
	public function update_saved_time() {
		$data = get_option( 'generatepress_dynamic_css_data', array() );
		$data['updated_time'] = time();

		update_option( 'generatepress_dynamic_css_data', $data );
	}

	/**
	 * Delete the saved time.
	 */
	public function delete_saved_time() {
		$data = get_option( 'generatepress_dynamic_css_data', array() );

		if ( isset( $data['updated_time'] ) ) {
			unset( $data['updated_time'] );
		}

		update_option( 'generatepress_dynamic_css_data', $data );
	}

	/**
	 * Update our plugin/theme versions.
	 */
	public function update_versions() {
		$data = get_option( 'generatepress_dynamic_css_data', array() );

		$data['theme_version'] = GENERATE_VERSION;
		$data['plugin_version'] = GP_PREMIUM_VERSION;

		update_option( 'generatepress_dynamic_css_data', $data );
	}

	/**
	 * Do we need to update the CSS file?
	 */
	public function needs_update() {
		$data = get_option( 'generatepress_dynamic_css_data', array() );
		$update = false;

		// If there's no updated time, needs update.
		// The time is set in mode().
		if ( ! isset( $data['updated_time'] ) ) {
			$update = true;
		}

		// If we haven't set our versions, do so now.
		if ( ! isset( $data['theme_version'] ) && ! isset( $data['plugin_version'] ) ) {
			$update = true;
			$this->update_versions();

			// Bail early so we don't check undefined versions below.
			return $update;
		}

		// Version numbers have changed, needs update.
		if ( (string) GENERATE_VERSION !== (string) $data['theme_version'] || (string) GP_PREMIUM_VERSION !== (string) $data['plugin_version'] ) {
			$update = true;
			$this->update_versions();
		}

		return $update;
	}

	/**
	 * Regenerate the CSS file.
	 */
	public function regenerate_css_file() {
		check_ajax_referer( 'generatepress_regenerate_css_file', '_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'Security check failed.', 'gp-premium' ) );
		}

		$this->delete_saved_time();

		wp_send_json_success();
	}
}

GeneratePress_External_CSS_File::get_instance();