<?php
namespace AIOSEO\Plugin\Common\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Traits\Helpers as TraitHelpers;

/**
 * Contains helper functions
 *
 * @since 4.0.0
 */
class Helpers {
	use TraitHelpers\Api;
	use TraitHelpers\Arrays;
	use TraitHelpers\Blocks;
	use TraitHelpers\Buffer;
	use TraitHelpers\Constants;
	use TraitHelpers\Deprecated;
	use TraitHelpers\DateTime;
	use TraitHelpers\Images;
	use TraitHelpers\Language;
	use TraitHelpers\Numbers;
	use TraitHelpers\PostType;
	use TraitHelpers\Request;
	use TraitHelpers\Shortcodes;
	use TraitHelpers\Strings;
	use TraitHelpers\Svg;
	use TraitHelpers\ThirdParty;
	use TraitHelpers\Url;
	use TraitHelpers\Vue;
	use TraitHelpers\Wp;
	use TraitHelpers\WpContext;
	use TraitHelpers\WpMultisite;
	use TraitHelpers\WpUri;

	/**
	 * Holds the data for Vue.
	 *
	 * @since   4.4.9
	 * @version 4.8.6.1 Moved from Vue trait as it's shared between Lite, Common and Pro.
	 *
	 * @var array
	 */
	protected $data = [];

	/**
	 * Optional arguments for setting the data.
	 *
	 * @since   4.4.9
	 * @version 4.8.6.1 Moved from Vue trait as it's shared between Lite, Common and Pro.
	 *
	 * @var array
	 */
	protected $args = [];

	/**
	 * Holds the cached data.
	 *
	 * @since   4.5.1
	 * @version 4.8.6.1 Moved from Vue trait as it's shared between Lite, Common and Pro.
	 *
	 * @var array
	 */
	protected $cache = [];

	/**
	 * Generate a UTM URL from the url and medium/content passed in.
	 *
	 * @since 4.0.0
	 *
	 * @param  string      $url     The URL to parse.
	 * @param  string      $medium  The UTM medium parameter.
	 * @param  string|null $content The UTM content parameter or null.
	 * @param  boolean     $esc     Whether or not to escape the URL.
	 * @return string               The new URL.
	 */
	public function utmUrl( $url, $medium, $content = null, $esc = true ) {
		// First, remove any existing utm parameters on the URL.
		$url = remove_query_arg( [
			'utm_source',
			'utm_medium',
			'utm_campaign',
			'utm_content'
		], $url );

		// Generate the new arguments.
		$args = [
			'utm_source'   => 'WordPress',
			'utm_campaign' => aioseo()->pro ? 'proplugin' : 'liteplugin',
			'utm_medium'   => $medium
		];

		// Content is not used by default.
		if ( $content ) {
			$args['utm_content'] = $content;
		}

		// Return the new URL.
		$url = add_query_arg( $args, $url );

		return $esc ? esc_url( $url ) : $url;
	}

	/**
	 * Checks if we are in a dev environment or not.
	 *
	 * @since 4.1.0
	 *
	 * @return boolean True if we are, false if not.
	 */
	public function isDev() {
		return aioseo()->isDev || isset( $_REQUEST['aioseo-dev'] ); // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Checks if the server is running on Apache.
	 *
	 * @since 4.0.0
	 *
	 * @return boolean Whether or not it is on apache.
	 */
	public function isApache() {
		if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
			return false;
		}

		return stripos( sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ), 'apache' ) !== false;
	}

	/**
	 * Checks if the server is running on nginx.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether or not it is on nginx.
	 */
	public function isNginx() {
		if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
			return false;
		}

		$server = sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) );

		if (
			false !== stripos( $server, 'Flywheel' ) ||
			false !== stripos( $server, 'nginx' )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if the server is running on LiteSpeed.
	 *
	 * @since 4.5.3
	 *
	 * @return bool Whether it is on LiteSpeed.
	 */
	public function isLiteSpeed() {
		if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
			return false;
		}

		$server = strtolower( sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) );

		return false !== stripos( $server, 'litespeed' );
	}

	/**
	 * Returns the server name: Apache, nginx or LiteSpeed.
	 *
	 * @since 4.5.3
	 *
	 * @return string The server name. An empty string if it's unknown.
	 */
	public function getServerName() {
		if ( aioseo()->helpers->isApache() ) {
			return 'apache';
		}

		if ( aioseo()->helpers->isNginx() ) {
			return 'nginx';
		}

		if ( aioseo()->helpers->isLiteSpeed() ) {
			return 'litespeed';
		}

		return '';
	}

	/**
	 * Validate IP addresses.
	 *
	 * @since 4.0.0
	 *
	 * @param  string  $ip The IP address to validate.
	 * @return boolean     If the IP address is valid or not.
	 */
	public function validateIp( $ip ) {
		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			return true;
		}

		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
			return true;
		}

		// Doesn't seem to be a valid IP.
		return false;
	}

	/**
	 * Convert bytes to readable format.
	 *
	 * @since 4.0.0
	 *
	 * @param  integer $bytes The size of the file.
	 * @return array          The original and readable file size.
	 */
	public function convertFileSize( $bytes ) {
		if ( empty( $bytes ) ) {
			return [
				'original' => 0,
				'readable' => '0 B'
			];
		}
		$i = floor( log( $bytes ) / log( 1024 ) );
		$sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ];

		return [
			'original' => $bytes,
			'readable' => sprintf( '%.02F', $bytes / pow( 1024, $i ) ) * 1 . ' ' . $sizes[ $i ]
		];
	}

	/**
	 * Sanitizes a given option value before we store it in the DB.
	 *
	 * Used by the migration and importer classes.
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed $value The value.
	 * @return mixed $value The sanitized value.
	 */
	public function sanitizeOption( $value ) {
		switch ( gettype( $value ) ) {
			case 'boolean':
				return (bool) $value;
			case 'string':
				$value = aioseo()->helpers->decodeHtmlEntities( $value );

				return aioseo()->helpers->encodeOutputHtml( wp_strip_all_tags( wp_check_invalid_utf8( trim( $value ) ) ) );
			case 'integer':
				return intval( $value );
			case 'double':
				return floatval( $value );
			case 'array':
				$sanitized = [];
				foreach ( (array) $value as $child ) {
					$sanitized[] = aioseo()->helpers->sanitizeOption( $child );
				}

				return $sanitized;
			case 'object':
				$sanitized = [];
				foreach ( (array) $value as $key => $child ) {
					$sanitized[ $key ] = aioseo()->helpers->sanitizeOption( $child );
				}

				return $sanitized;
			default:
				return false;
		}
	}

	/**
	 * Checks if the given string is serialized, and if so, unserializes it.
	 * If the serialized string contains an object, we abort to prevent PHP object injection.
	 *
	 * @since 4.1.0.2
	 *
	 * @param  string        $string         The string.
	 * @param  array|boolean $allowedClasses The allowed classes for unserialize.
	 * @return string|array                  The string or unserialized data.
	 */
	public function maybeUnserialize( $string, $allowedClasses = false ) {
		if ( ! is_string( $string ) ) {
			return $string;
		}

		$string = trim( $string );
		if ( is_serialized( $string ) ) {
			return @unserialize( $string, [ 'allowed_classes' => $allowedClasses ] ); // phpcs:disable PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
		}

		return $string;
	}

	/**
	 * Returns a deep clone of the given object.
	 * The built-in PHP clone KW provides a shallow clone. This method returns a deep clone that also clones nested object properties.
	 * You can use this method to sever the reference to nested objects.
	 *
	 * @since 4.4.7
	 *
	 * @return object The cloned object.
	 */
	public function deepClone( $object ) {
		return unserialize( serialize( $object ) );
	}

	/**
	 * Sanitizes a given variable
	 *
	 * @since 4.5.6
	 *
	 * @param  mixed $variable             The variable.
	 * @param  bool  $preserveHtml         Whether or not to preserve HTML for ALL fields.
	 * @param  array $fieldsToPreserveHtml Specific fields to preserve HTML for.
	 * @param  string $fieldName           The name of the current field (when looping over a list).
	 * @return mixed                       The sanitized variable.
	 */
	public function sanitize( $variable, $preserveHtml = false, $fieldsToPreserveHtml = [], $fieldName = '' ) {
		$type = gettype( $variable );
		switch ( $type ) {
			case 'boolean':
				return (bool) $variable;
			case 'string':
				if ( $preserveHtml || in_array( $fieldName, $fieldsToPreserveHtml, true ) ) {
					return aioseo()->helpers->decodeHtmlEntities( sanitize_text_field( htmlspecialchars( $variable, ENT_NOQUOTES, 'UTF-8' ) ) );
				}

				return sanitize_text_field( $variable );
			case 'integer':
				return intval( $variable );
			case 'float':
			case 'double':
				return floatval( $variable );
			case 'array':
				$array = [];
				foreach ( (array) $variable as $k => $v ) {
					$array[ $k ] = $this->sanitize( $v, $preserveHtml, $fieldsToPreserveHtml, $k );
				}

				return $array;
			default:
				return false;
		}
	}

	/**
	 * Return the version number with a filter to enable users to hide the version.
	 *
	 * @since 4.3.7
	 *
	 * @return string The current version or empty if the filter is active. Using ?aioseo-dev will override the filter.
	 */
	public function getAioseoVersion() {
		$version = aioseo()->version;

		if ( ! $this->isDev() && apply_filters( 'aioseo_hide_version_number', false ) ) {
			$version = '';
		}

		return $version;
	}

	/**
	 * Retrieves the marketing site articles.
	 *
	 * @since 4.7.2
	 *
	 * @param  bool  $fetchImage Whether to fetch the article image.
	 * @return array             The articles or an empty array on failure.
	 */
	public function fetchAioseoArticles( $fetchImage = false ) {
		$items = aioseo()->core->networkCache->get( 'rss_feed' );
		if ( is_array( $items ) ) {
			return $items;
		}

		$lockKey = 'rss_feed_fetch_lock';
		if ( null !== aioseo()->core->cache->get( $lockKey ) ) {
			return [];
		}

		aioseo()->core->cache->update( $lockKey, true, MINUTE_IN_SECONDS );
		$response = aioseo()->helpers->wpRemoteGetExternal( 'https://aioseo.com/wp-json/wp/v2/posts?per_page=4' );
		if ( is_wp_error( $response ) ) {
			aioseo()->core->networkCache->update( 'rss_feed', [], 10 * MINUTE_IN_SECONDS );
			aioseo()->core->cache->delete( $lockKey );

			return [];
		}

		$body  = wp_remote_retrieve_body( $response );
		$items = ! empty( $body ) ? json_decode( $body, true ) : [];

		// is_wp_error() above only catches transport-level failures (DNS, etc.); HTTP error
		// statuses come back as a successful response with a WP REST error body. Treat
		// anything other than 200 as a failure, and verify the first element is itself an
		// array (i.e. a post object) so a non-list response like `{"code":"rest_forbidden",...}`
		// doesn't fatal in the foreach below. See issue #8124.
		if (
			200 !== wp_remote_retrieve_response_code( $response ) ||
			! is_array( $items ) ||
			empty( $items ) ||
			! is_array( $items[0] ?? null )
		) {
			aioseo()->core->networkCache->update( 'rss_feed', [], HOUR_IN_SECONDS );
			aioseo()->core->cache->delete( $lockKey );

			return [];
		}

		$cached = [];
		foreach ( $items as $k => $item ) {
			$cached[ $k ] = [
				'url'     => $item['link'],
				'title'   => $item['title']['rendered'],
				'date'    => date_i18n( get_option( 'date_format' ), strtotime( $item['date'] ) ),
				'content' => wp_html_excerpt( $item['content']['rendered'], 128, '&hellip;' ),
			];

			if ( $fetchImage ) {
				$imageUrl = $item['_links']['wp:featuredmedia'][0]['href'] ?? '';
				if ( empty( $imageUrl ) ) {
					continue;
				}

				$response = aioseo()->helpers->wpRemoteGetExternal( $imageUrl );
				if ( is_wp_error( $response ) ) {
					continue;
				}

				$body = wp_remote_retrieve_body( $response );
				if ( ! $body ) {
					continue;
				}

				$image = json_decode( $body, true );

				$cached[ $k ]['image'] = [
					'url'   => $image['source_url'] ?? '',
					'alt'   => $image['alt_text'] ?? '',
					'sizes' => $image['media_details']['sizes'] ?? ''
				];
			}
		}

		aioseo()->core->networkCache->update( 'rss_feed', $cached, DAY_IN_SECONDS );
		aioseo()->core->cache->delete( $lockKey );

		return $cached;
	}

	/**
	 * Returns if the admin bar is enabled.
	 *
	 * @since 4.8.1
	 *
	 * @return bool Whether the admin bar is enabled.
	 */
	public function isAdminBarEnabled() {
		$showAdminBarMenu = aioseo()->options->advanced->adminBarMenu;

		return is_admin_bar_showing() && ( $showAdminBarMenu ?? true );
	}
}