Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/Models.tar

WritingAssistantPost.php000066600000006734151146251270011462 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Class Posts.
 *
 * @since 4.7.4
 */
class WritingAssistantPost extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	protected $table = 'aioseo_writing_assistant_posts';

	/**
	 * Fields that should be integer values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'post_id', 'keyword_id' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'content_analysis' ];

	/**
	 * Gets a post's content analysis.
	 *
	 * @since 4.7.4
	 *
	 * @param  int   $postId A post ID.
	 * @return array         The post content's analysis.
	 */
	public static function getContentAnalysis( $postId ) {
		$post = self::getPost( $postId );

		return ! empty( $post->content_analysis ) && is_object( $post->content_analysis ) ? (array) $post->content_analysis : [];
	}

	/**
	 * Gets a writing assistant post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int                  $postId A post ID.
	 * @return WritingAssistantPost         The post object.
	 */
	public static function getPost( $postId ) {
		$post = aioseo()->core->db->start( 'aioseo_writing_assistant_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantPost' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
		}

		return $post;
	}

	/**
	 * Gets a post's current keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  int                          $postId A post ID.
	 * @return WritingAssistantKeyword|bool         An attached keyword.
	 */
	public static function getKeyword( $postId ) {
		$post = self::getPost( $postId );
		if ( ! $post->exists() || empty( $post->keyword_id ) ) {
			return false;
		}

		$keyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
			->where( 'id', $post->keyword_id )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );

		// This is here so this property is reactive in the frontend.
		if ( ! empty( $keyword->keywords ) ) {
			foreach ( $keyword->keywords as &$keyph ) {
				$keyph->contentCount = 0;
			}
		}

		// Help sorting in the frontend.
		if ( ! empty( $keyword->competitors->competitors ) ) {
			foreach ( $keyword->competitors->competitors as &$competitor ) {
				$competitor->wasAnalyzed = true;
				if ( 0 >= $competitor->wordCount ) {
					$competitor->wordCount        = 0;
					$competitor->readabilityScore = 999;
					$competitor->readabilityGrade = '';
					$competitor->gradeScore       = 0;
					$competitor->grade            = '';
					$competitor->wasAnalyzed      = false;
				}

				$competitor->readabilityScore = (float) $competitor->readabilityScore;
			}
		}

		return $keyword;
	}

	/**
	 * Return if a post has a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  int     $postId A post ID.
	 * @return boolean         Has a keyword.
	 */
	public static function hasKeyword( $postId ) {
		$post = self::getPost( $postId );

		return (bool) $post->keyword_id;
	}

	/**
	 * Attaches a keyword to a post.
	 *
	 * @since 4.7.4
	 *
	 * @param  int  $keywordId The keyword ID.
	 * @return void
	 */
	public function attachKeyword( $keywordId ) {
		$this->keyword_id = $keywordId;
		$this->save();
	}
}Model.php000066600000025156151146251270006336 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Base Model class.
 *
 * @since 4.0.0
 */
#[\AllowDynamicProperties]
class Model implements \JsonSerializable {
	/**
	 * Fields that can be null when saving to the database.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be integer values.
	 *
	 * @since   4.1.0
	 * @version 4.7.3 Renamed from numericFields to integerFields.
	 *
	 * @var array
	 */
	protected $integerFields = [];

	/**
	 * Fields that should be float values.
	 *
	 * @since 4.7.3
	 *
	 * @var array
	 */
	protected $floatFields = [];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $hidden = [];

	/**
	 * The table used in the SQL query.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = '';

	/**
	 * The primary key retrieved from the table.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $pk = 'id';

	/**
	 * The ID of the model.
	 * This needs to be null in order for MySQL to auto-increment correctly if the NO_AUTO_VALUE_ON_ZERO SQL mode is enabled.
	 *
	 * @since 4.2.7
	 *
	 * @var int|null
	 */
	public $id = null;

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	private static $columns = [];

	/**
	 * Class constructor.
	 *
	 * @since 4.0.0
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		$skip = [ 'id', 'created', 'updated' ];
		$fields = [];
		foreach ( $this->getColumns() as $column => $value ) {
			if ( ! in_array( $column, $skip, true ) ) {
				$fields[ $column ] = $value;
			}
		}

		$this->applyKeys( $fields );

		// Process straight through if we were given a valid array.
		if ( is_array( $var ) || is_object( $var ) ) {
			// Apply keys to object.
			$this->applyKeys( $var );

			if ( $this->exists() ) {
				return true;
			}

			return false;
		}

		return $this->loadData( $var );
	}

	/**
	 * Load the data from the database!
	 *
	 * @since 4.0.0
	 *
	 * @param  mixed      $var Generally the primary key to load up the model from the DB.
	 * @return Model|bool      Returns the current object.
	 */
	protected function loadData( $var = null ) {
		// Return false if var is invalid or not supplied.
		if ( null === $var ) {
			return false;
		}

		$query = aioseo()->core->db
			->start( $this->table )
			->where( $this->pk, $var )
			->limit( 1 )
			->output( 'ARRAY_A' );

		$result = $query->run();
		if ( ! $result || $result->nullSet() ) {
			return $this;
		}

		// Apply keys to object.
		$this->applyKeys( $result->result()[0] );

		return $this;
	}

	/**
	 * Take the keys from the result array and add them to the Model.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $array The array of keys and values to add to the Model.
	 * @return void
	 */
	protected function applyKeys( $array ) {
		if ( ! is_object( $array ) && ! is_array( $array ) ) {
			throw new \Exception( '$array must either be an object or an array.' );
		}

		foreach ( (array) $array as $key => $value ) {
			$key = trim( $key );
			$this->$key = $value;

			if ( null === $value && in_array( $key, $this->nullFields, true ) ) {
				continue;
			}

			if ( in_array( $key, $this->jsonFields, true ) ) {
				if ( $value ) {
					$this->$key = is_string( $value ) ? json_decode( $value ) : $value;
				}
				continue;
			}

			if ( in_array( $key, $this->booleanFields, true ) ) {
				$this->$key = (bool) $value;
				continue;
			}

			if ( in_array( $key, $this->integerFields, true ) ) {
				$this->$key = (int) $value;
				continue;
			}

			if ( in_array( $key, $this->floatFields, true ) ) {
				$this->$key = (float) $value;
				continue;
			}
		}
	}

	/**
	 * Let's filter out any properties we cannot save to the database.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $keys The array of keys to filter.
	 * @return array       The array of valid columns for the database query.
	 */
	protected function filter( $keys ) {
		$fields    = [];
		$skip      = [ 'created', 'updated' ];
		$dbColumns = aioseo()->db->getColumns( $this->table );

		foreach ( $dbColumns as $column ) {
			if ( ! in_array( $column, $skip, true ) && array_key_exists( $column, $keys ) ) {
				$fields[ $column ] = $keys[ $column ];
			}
		}

		return $fields;
	}

	/**
	 * Transforms the data to be null if it exists in the nullFields variables.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		foreach ( $this->nullFields as $field ) {
			if ( isset( $data[ $field ] ) && empty( $data[ $field ] ) ) {
				// Because sitemap prio can both be 0 and null, we need to make sure it's 0 if it's set.
				if ( 'priority' === $field && 0.0 === $data[ $field ] ) {
					continue;
				}

				$data[ $field ] = null;
			}
		}

		foreach ( $this->booleanFields as $field ) {
			if ( isset( $data[ $field ] ) && '' === $data[ $field ] ) {
				unset( $data[ $field ] );
			} elseif ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (bool) $data[ $field ] ? 1 : 0;
			}
		}

		if ( $set ) {
			return $data;
		}

		foreach ( $this->integerFields as $field ) {
			if ( isset( $data[ $field ] ) ) {
				$data[ $field ] = (int) $data[ $field ];
			}
		}

		foreach ( $this->jsonFields as $field ) {
			if ( isset( $data[ $field ] ) && ! aioseo()->helpers->isJsonString( $data[ $field ] ) ) {
				if ( is_array( $data[ $field ] ) && aioseo()->helpers->isArrayNumeric( $data[ $field ] ) ) {
					$data[ $field ] = array_values( $data[ $field ] );
				}
				$data[ $field ] = wp_json_encode( $data[ $field ] );
			}
		}

		return $data;
	}

	/**
	 * Sets a piece of data to the requested resource.
	 *
	 * @since 4.0.0
	 */
	public function set() {
		$args  = func_get_args();
		$count = func_num_args();

		if ( ! is_array( $args[0] ) && $count < 2 ) {
			throw new \Exception( 'The set method must contain at least 2 arguments: key and value. Or an array of data. Only one argument was passed and it was not an array.' );
		}

		$key   = $args[0];
		$value = ! empty( $args[1] ) ? $args[1] : null;

		// Make sure we have a key.
		if ( false === $key ) {
			return false;
		}

		// If it's not an array, make it one.
		if ( ! is_array( $key ) ) {
			$key = [ $key => $value ];
		}

		// Preprocess data.
		$key = $this->transform( $key, true );

		// Save the items in this object.
		foreach ( $key as $k => $v ) {
			if ( ! empty( $k ) ) {
				$this->$k = $v;
			}
		}
	}

	/**
	 * Delete the Model Resource itself.
	 *
	 * @since 4.0.0
	 *
	 * @return null
	 */
	public function delete() {
		if ( ! $this->exists() ) {
			return;
		}

		aioseo()->core->db
			->delete( $this->table )
			->where( $this->pk, $this->id )
			->run();

		return null;
	}

	/**
	 * Saves data to the requested resource.
	 *
	 * @since 4.0.0
	 */
	public function save() {
		$fields = $this->transform( $this->filter( (array) get_object_vars( $this ) ) );

		$id = null;
		if ( count( $fields ) > 0 ) {
			$pk = $this->pk;

			if ( isset( $this->$pk ) && '' !== $this->$pk ) {
				// PK specified.
				$pkv   = $this->$pk;
				$query = aioseo()->core->db
					->start( $this->table )
					->where( [ $pk => $pkv ] )
					->resetCache()
					->run();

				if ( ! $query->nullSet() ) {
					// Row exists in database.
					$fields['updated'] = gmdate( 'Y-m-d H:i:s' );
					aioseo()->core->db
						->update( $this->table )
						->set( $fields )
						->where( [ $pk => $pkv ] )
						->run();
					$id = $this->$pk;
				} else {
					// Row does not exist.
					$fields[ $pk ]     = $pkv;
					$fields['created'] = gmdate( 'Y-m-d H:i:s' );
					$fields['updated'] = gmdate( 'Y-m-d H:i:s' );

					$id = aioseo()->core->db
						->insert( $this->table )
						->set( $fields )
						->run()
						->insertId();

					if ( $id ) {
						$this->$pk = $id;
					}
				}
			} else {
				$fields['created'] = gmdate( 'Y-m-d H:i:s' );
				$fields['updated'] = gmdate( 'Y-m-d H:i:s' );

				$id = aioseo()->core->db
					->insert( $this->table )
					->set( $fields )
					->run()
					->insertId();

				if ( $id ) {
					$this->$pk = $id;
				}
			}
		}

		// Refresh the resource.
		$this->reset( $id );
	}

	/**
	 * Check if the model exists in the database.
	 *
	 * @since 4.0.0
	 *
	 * @return bool If the model exists, true otherwise false.
	 */
	public function exists() {
		return ( ! empty( $this->{$this->pk} ) ) ? true : false;
	}

	/**
	 * Resets a resource by forcing internal updates to be applied.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $id The resource ID.
	 * @return void
	 */
	public function reset( $id = null ) {
		$id = ! empty( $id ) ? $id : $this->{$this->pk};
		$this->__construct( $id );
	}

	/**
	 * Helper function to remove data we don't want serialized.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of data that we are okay with serializing.
	 */
	#[\ReturnTypeWillChange]
	// The attribute above omits a deprecation notice from PHP 8.1 that is thrown because the return type of jsonSerialize() isn't "mixed".
	// Once PHP 7.x is our minimum supported version, this can be removed in favour of overriding the return type in the method signature like this -
	// public function jsonSerialize() : array
	public function jsonSerialize() {
		$array = [];

		foreach ( $this->getColumns() as $column => $value ) {
			if ( in_array( $column, $this->hidden, true ) ) {
				continue;
			}

			$array[ $column ] = isset( $this->$column ) ? $this->$column : null;
		}

		return $array;
	}

	/**
	 * Retrieves the columns for the model.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of columns.
	 */
	public function getColumns() {
		if ( empty( self::$columns[ get_called_class() ] ) ) {
			self::$columns[ get_called_class() ] = [];

			// Let's set the columns that are available by default.
			$table   = aioseo()->core->db->prefix . $this->table;
			$results = aioseo()->core->db->start( $table )
				->output( 'OBJECT' )
				->execute( 'SHOW COLUMNS FROM `' . $table . '`', true )
				->result();

			foreach ( $results as $col ) {
				self::$columns[ get_called_class() ][ $col->Field ] = $col->Default;
			}

			if ( ! empty( $this->appends ) ) {
				foreach ( $this->appends as $append ) {
					self::$columns[ get_called_class() ][ $append ] = null;
				}
			}
		}

		return self::$columns[ get_called_class() ];
	}
}CrawlCleanupLog.php000066600000003434151146251270010313 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

use AIOSEO\Plugin\Common\Models as CommonModels;

/**
 * The Crawl Cleanup Log DB Model.
 *
 * @since 4.5.8
 */
class CrawlCleanupLog extends CommonModels\Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	protected $table = 'aioseo_crawl_cleanup_logs';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'hits' ];

	/**
	 * Field to count hits.
	 *
	 * @since 4.5.8
	 *
	 * @var integer
	 */
	public $hits = 0;


	/**
	 * Create a Log in case it doesn't exist.
	 *
	 * @since 4.5.8
	 *
	 * @return void
	 */
	public function create() {
		if ( null !== $this->id ) {
			$this->hits++;
		}

		parent::save();
	}

	/**
	 * Get Crawl Cleanup passing Slug
	 *
	 * @since 4.5.8
	 *
	 * @param  string          $slug The Slug to search.
	 * @return CrawlCleanupLog       The CrawlCleanupLog object.
	 */
	public static function getBySlug( $slug ) {
		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_logs' )
			->where( 'hash', sha1( $slug ) )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupLog' );
	}

	/**
	 * Transforms data as needed.
	 *
	 * @since 4.5.8
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		$data = parent::transform( $data, $set );

		// Create slug hash.
		if ( ! empty( $data['slug'] ) ) {
			$data['hash'] = sha1( $data['slug'] );
		}

		return $data;
	}
}Post.php000066600000076370151146251270006227 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The Post DB Model.
 *
 * @since 4.0.0
 */
class Post extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_posts';

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [
		'keywords',
		'keyphrases',
		'page_analysis',
		'schema',
		// 'schema_type_options',
		'images',
		'videos',
		'open_ai',
		'options',
		'local_seo',
		'primary_term',
		'breadcrumb_settings',
		'og_article_tags'
	];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.13
	 *
	 * @var array
	 */
	protected $booleanFields = [
		'twitter_use_og',
		'pillar_content',
		'robots_default',
		'robots_noindex',
		'robots_noarchive',
		'robots_nosnippet',
		'robots_nofollow',
		'robots_noimageindex',
		'robots_noodp',
		'robots_notranslate',
		'limit_modified_date',
	];

	/**
	 * Fields that can be null when saved.
	 *
	 * @since 4.5.7
	 *
	 * @var array
	 */
	protected $nullFields = [
		'priority'
	];

	/**
	 * Fields that should be float values.
	 *
	 * @since 4.7.3
	 *
	 * @var array
	 */
	protected $floatFields = [
		'priority'
	];

	/**
	 * Returns a Post with a given ID.
	 *
	 * @since 4.0.0
	 *
	 * @param  int  $postId The post ID.
	 * @return Post         The Post object.
	 */
	public static function getPost( $postId ) {
		// This is needed to prevent an error when upgrading from 4.1.8 to 4.1.9.
		// WordPress deletes the attachment .zip file for the new plugin version after installing it, which triggers the "delete_post" hook.
		// In-between the 4.1.8 to 4.1.9 update, the new Core class does not exist yet, causing the PHP error.
		// TODO: Delete this in a future release.
		$post = new self();
		if ( ! property_exists( aioseo(), 'core' ) ) {
			return $post;
		}

		$post = aioseo()->core->db->start( 'aioseo_posts' )
			->where( 'post_id', $postId )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\Post' );

		if ( ! $post->exists() ) {
			$post->post_id = $postId;
			$post          = self::setDynamicDefaults( $post, $postId );
		} else {
			$post = self::runDynamicMigrations( $post );
		}

		// Set options object.
		$post = self::setOptionsDefaults( $post );

		return apply_filters( 'aioseo_get_post', $post );
	}

	/**
	 * Sets the dynamic defaults on the post object if it doesn't exist in the DB yet.
	 *
	 * @since 4.1.4
	 *
	 * @param  Post $post   The Post object.
	 * @param  int  $postId The post ID.
	 * @return Post         The modified Post object.
	 */
	private static function setDynamicDefaults( $post, $postId ) {
		if ( 'page' === get_post_type( $postId ) ) { // This check cannot be deleted and is required to prevent errors after WordPress cleans up the attachment it creates when a plugin is updated.
			$isWooCommerceCheckoutPage = aioseo()->helpers->isWooCommerceCheckoutPage( $postId );
			if (
				$isWooCommerceCheckoutPage ||
				aioseo()->helpers->isWooCommerceCartPage( $postId ) ||
				aioseo()->helpers->isWooCommerceAccountPage( $postId )
			) {
				$post->robots_default = false;
				$post->robots_noindex = true;
			}
		}

		if ( aioseo()->helpers->isStaticHomePage( $postId ) ) {
			$post->og_object_type = 'website';
		}

		$post->twitter_use_og = aioseo()->options->social->twitter->general->useOgData;

		if ( property_exists( $post, 'schema' ) && null === $post->schema ) {
			$post->schema = self::getDefaultSchemaOptions();
		}

		return $post;
	}

	/**
	 * Migrates removed QAPage schema on-the-fly when the post is loaded.
	 *
	 * @since 4.1.8
	 *
	 * @param  Post $aioseoPost The post object.
	 * @return Post             The modified post object.
	 */
	private static function migrateRemovedQaSchema( $aioseoPost ) {
		if ( ! $aioseoPost->schema_type || 'webpage' !== strtolower( $aioseoPost->schema_type ) ) {
			return $aioseoPost;
		}

		$schemaTypeOptions = json_decode( $aioseoPost->schema_type_options );
		if ( 'qapage' !== strtolower( $schemaTypeOptions->webPage->webPageType ) ) {
			return $aioseoPost;
		}

		$schemaTypeOptions->webPage->webPageType = 'WebPage';
		$aioseoPost->schema_type_options         = wp_json_encode( $schemaTypeOptions );
		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Runs dynamic migrations whenever the post object is loaded.
	 *
	 * @since 4.1.7
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function runDynamicMigrations( $post ) {
		$post = self::migrateRemovedQaSchema( $post );
		$post = self::migrateImageTypes( $post );
		$post = self::runDynamicSchemaMigration( $post );
		$post = self::migrateKoreaCountryCodeSchemas( $post );

		return $post;
	}


	/**
	 * Migrates the post's schema data when it is loaded.
	 *
	 * @since 4.2.5
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function runDynamicSchemaMigration( $post ) {
		if ( ! property_exists( $post, 'schema' ) ) {
			return $post;
		}

		if ( null === $post->schema ) {
			$post = aioseo()->updates->migratePostSchemaHelper( $post );
		}

		// If the schema prop isn't set yet, we want to set it here.
		// We also want to run this regardless of whether it is already set to make sure the default schema graph
		// is correctly propagated on the frontend after changing it.
		$post->schema = self::getDefaultSchemaOptions( $post->schema );

		// Filter out null or empty graphs.
		$post->schema->graphs = array_filter( $post->schema->graphs, function( $graph ) {
			return ! empty( $graph );
		} );

		foreach ( $post->schema->graphs as $graph ) {
			// If the first character of the graph ID isn't a pound, add one.
			// We have to do this because the schema migration in 4.2.5 didn't add the pound for custom graphs.
			if ( property_exists( $graph, 'id' ) && '#' !== substr( $graph->id, 0, 1 ) ) {
				$graph->id = '#' . $graph->id;
			}

			// If the graph has an old rating value, we need to migrate it to the review.
			if (
				property_exists( $graph, 'id' ) &&
				preg_match( '/(movie|software-application)/', (string) $graph->id ) &&
				property_exists( $graph->properties, 'rating' ) &&
				property_exists( $graph->properties->rating, 'value' )
			) {
				$graph->properties->review->rating = $graph->properties->rating->value;
				unset( $graph->properties->rating->value );
			}

			// If the graph has audience data, we need to migrate it to the correct one.
			if (
				property_exists( $graph, 'id' ) &&
				preg_match( '/(product|product-review)/', $graph->id ) &&
				property_exists( $graph->properties, 'audience' )
			) {
				$graph->properties->audience = self::migratePostAudienceAgeSchema( $graph->properties->audience );
			}
		}

		return $post;
	}

	/**
	 * Migrates the post's image types when it is loaded.
	 *
	 * @since 4.2.5
	 *
	 * @param  Post $post The Post object.
	 * @return Post       The modified Post object.
	 */
	private static function migrateImageTypes( $post ) {
		$pageBuilder = aioseo()->helpers->getPostPageBuilderName( $post->post_id );
		if ( ! $pageBuilder ) {
			return $post;
		}

		$deprecatedImageSources = 'seedprod' === strtolower( $pageBuilder )
			? [ 'auto', 'custom', 'featured' ]
			: [ 'auto' ];

		if ( ! empty( $post->og_image_type ) && in_array( $post->og_image_type, $deprecatedImageSources, true ) ) {
			$post->og_image_type = 'default';
		}

		if ( ! empty( $post->twitter_image_type ) && in_array( $post->twitter_image_type, $deprecatedImageSources, true ) ) {
			$post->twitter_image_type = 'default';
		}

		return $post;
	}

	/**
	 * Saves the Post object.
	 *
	 * @since 4.0.3
	 *
	 * @param  int              $postId The Post ID.
	 * @param  array            $data   The post data to save.
	 * @return bool|void|string         Whether the post data was saved or a DB error message.
	 */
	public static function savePost( $postId, $data ) {
		if ( empty( $data ) ) {
			return false;
		}

		$thePost = self::getPost( $postId );
		$data    = apply_filters( 'aioseo_save_post', $data, $thePost );

		// Before setting the data, we check if the title/description are the same as the defaults and clear them if so.
		$data    = self::checkForDefaultFormat( $postId, $thePost, $data );
		$thePost = self::sanitizeAndSetDefaults( $postId, $thePost, $data );

		// Update traditional post meta so that it can be used by multilingual plugins.
		self::updatePostMeta( $postId, $data );

		$thePost->save();
		$thePost->reset();

		$lastError = aioseo()->core->db->lastError();
		if ( ! empty( $lastError ) ) {
			return $lastError;
		}

		// Fires once an AIOSEO post has been saved.
		do_action( 'aioseo_insert_post', $postId );
	}

	/**
	 * Checks if the title/description is the same as their default format in Search Appearance and nulls it if this is the case.
	 * Doing this ensures that updates to the default title/description format also propogate to the post.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId  The post ID.
	 * @param  Post  $thePost The Post object.
	 * @param  array $data    The data.
	 * @return array          The data.
	 */
	private static function checkForDefaultFormat( $postId, $thePost, $data ) {
		$data['title']       = trim( (string) $data['title'] );
		$data['description'] = trim( (string) $data['description'] );

		$post                     = aioseo()->helpers->getPost( $postId );
		$defaultTitleFormat       = trim( aioseo()->meta->title->getPostTypeTitle( $post->post_type ) );
		$defaultDescriptionFormat = trim( aioseo()->meta->description->getPostTypeDescription( $post->post_type ) );
		if ( ! empty( $data['title'] ) && $data['title'] === $defaultTitleFormat ) {
			$data['title'] = null;
		}

		if ( ! empty( $data['description'] ) && $data['description'] === $defaultDescriptionFormat ) {
			$data['description'] = null;
		}

		return $data;
	}

	/**
	 * Sanitize the keyphrases posted data.
	 *
	 * @since 4.2.8
	 *
	 * @param  array $data An array containing the keyphrases field data.
	 * @return array       The sanitized data.
	 */
	private static function sanitizeKeyphrases( $data ) {
		if (
			! empty( $data['focus']['analysis'] ) &&
			is_array( $data['focus']['analysis'] )
		) {
			foreach ( $data['focus']['analysis'] as &$analysis ) {
				// Remove unnecessary 'title' and 'description'.
				unset( $analysis['title'] );
				unset( $analysis['description'] );
			}
		}

		if (
			! empty( $data['additional'] ) &&
			is_array( $data['additional'] )
		) {
			foreach ( $data['additional'] as &$additional ) {
				if (
					! empty( $additional['analysis'] ) &&
					is_array( $additional['analysis'] )
				) {
					foreach ( $additional['analysis'] as &$additionalAnalysis ) {
						// Remove unnecessary 'title' and 'description'.
						unset( $additionalAnalysis['title'] );
						unset( $additionalAnalysis['description'] );
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitize the page_analysis posted data.
	 *
	 * @since 4.2.7
	 *
	 * @param  array $data An array containing the page_analysis field data.
	 * @return array       The sanitized data.
	 */
	private static function sanitizePageAnalysis( $data ) {
		if (
			empty( $data['analysis'] ) ||
			! is_array( $data['analysis'] )
		) {
			return $data;
		}

		foreach ( $data['analysis'] as &$analysis ) {
			foreach ( $analysis as $key => $result ) {
				// Remove unnecessary data.
				foreach ( [ 'title', 'description', 'highlightSentences' ] as $keyToRemove ) {
					if ( isset( $analysis[ $key ][ $keyToRemove ] ) ) {
						unset( $analysis[ $key ][ $keyToRemove ] );
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Sanitizes the post data and sets it (or the default value) to the Post object.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId  The post ID.
	 * @param  Post  $thePost The Post object.
	 * @param  array $data    The data.
	 * @return Post           The Post object with data set.
	 */
	protected static function sanitizeAndSetDefaults( $postId, $thePost, $data ) {
		// General
		$thePost->post_id                     = $postId;
		$thePost->title                       = ! empty( $data['title'] ) ? sanitize_text_field( $data['title'] ) : null;
		$thePost->description                 = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : null;
		$thePost->canonical_url               = ! empty( $data['canonicalUrl'] ) ? sanitize_text_field( $data['canonicalUrl'] ) : null;
		$thePost->keywords                    = ! empty( $data['keywords'] ) ? aioseo()->helpers->sanitize( $data['keywords'] ) : null;
		$thePost->pillar_content              = isset( $data['pillar_content'] ) ? rest_sanitize_boolean( $data['pillar_content'] ) : 0;
		// TruSEO
		$thePost->keyphrases                  = ! empty( $data['keyphrases'] ) ? self::sanitizeKeyphrases( $data['keyphrases'] ) : null;
		$thePost->page_analysis               = ! empty( $data['page_analysis'] ) ? self::sanitizePageAnalysis( $data['page_analysis'] ) : null;
		$thePost->seo_score                   = ! empty( $data['seo_score'] ) ? sanitize_text_field( $data['seo_score'] ) : 0;
		// Sitemap
		$thePost->priority                    = isset( $data['priority'] ) ? ( 'default' === sanitize_text_field( $data['priority'] ) ? null : (float) $data['priority'] ) : null;
		$thePost->frequency                   = ! empty( $data['frequency'] ) ? sanitize_text_field( $data['frequency'] ) : 'default';
		// Robots Meta
		$thePost->robots_default              = isset( $data['default'] ) ? rest_sanitize_boolean( $data['default'] ) : 1;
		$thePost->robots_noindex              = isset( $data['noindex'] ) ? rest_sanitize_boolean( $data['noindex'] ) : 0;
		$thePost->robots_nofollow             = isset( $data['nofollow'] ) ? rest_sanitize_boolean( $data['nofollow'] ) : 0;
		$thePost->robots_noarchive            = isset( $data['noarchive'] ) ? rest_sanitize_boolean( $data['noarchive'] ) : 0;
		$thePost->robots_notranslate          = isset( $data['notranslate'] ) ? rest_sanitize_boolean( $data['notranslate'] ) : 0;
		$thePost->robots_noimageindex         = isset( $data['noimageindex'] ) ? rest_sanitize_boolean( $data['noimageindex'] ) : 0;
		$thePost->robots_nosnippet            = isset( $data['nosnippet'] ) ? rest_sanitize_boolean( $data['nosnippet'] ) : 0;
		$thePost->robots_noodp                = isset( $data['noodp'] ) ? rest_sanitize_boolean( $data['noodp'] ) : 0;
		$thePost->robots_max_snippet          = isset( $data['maxSnippet'] ) && is_numeric( $data['maxSnippet'] ) ? (int) sanitize_text_field( $data['maxSnippet'] ) : -1;
		$thePost->robots_max_videopreview     = isset( $data['maxVideoPreview'] ) && is_numeric( $data['maxVideoPreview'] ) ? (int) sanitize_text_field( $data['maxVideoPreview'] ) : -1;
		$thePost->robots_max_imagepreview     = ! empty( $data['maxImagePreview'] ) ? sanitize_text_field( $data['maxImagePreview'] ) : 'large';
		// Open Graph Meta
		$thePost->og_title                    = ! empty( $data['og_title'] ) ? sanitize_text_field( $data['og_title'] ) : null;
		$thePost->og_description              = ! empty( $data['og_description'] ) ? sanitize_text_field( $data['og_description'] ) : null;
		$thePost->og_object_type              = ! empty( $data['og_object_type'] ) ? sanitize_text_field( $data['og_object_type'] ) : 'default';
		$thePost->og_image_type               = ! empty( $data['og_image_type'] ) ? sanitize_text_field( $data['og_image_type'] ) : 'default';
		$thePost->og_image_url                = null; // We'll reset this below.
		$thePost->og_image_width              = null; // We'll reset this below.
		$thePost->og_image_height             = null; // We'll reset this below.
		$thePost->og_image_custom_url         = ! empty( $data['og_image_custom_url'] ) ? esc_url_raw( $data['og_image_custom_url'] ) : null;
		$thePost->og_image_custom_fields      = ! empty( $data['og_image_custom_fields'] ) ? sanitize_text_field( $data['og_image_custom_fields'] ) : null;
		$thePost->og_video                    = ! empty( $data['og_video'] ) ? sanitize_text_field( $data['og_video'] ) : '';
		$thePost->og_article_section          = ! empty( $data['og_article_section'] ) ? sanitize_text_field( $data['og_article_section'] ) : null;
		$thePost->og_article_tags             = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->sanitize( $data['og_article_tags'] ) : null;
		// Twitter Meta
		$thePost->twitter_title               = ! empty( $data['twitter_title'] ) ? sanitize_text_field( $data['twitter_title'] ) : null;
		$thePost->twitter_description         = ! empty( $data['twitter_description'] ) ? sanitize_text_field( $data['twitter_description'] ) : null;
		$thePost->twitter_use_og              = isset( $data['twitter_use_og'] ) ? rest_sanitize_boolean( $data['twitter_use_og'] ) : 0;
		$thePost->twitter_card                = ! empty( $data['twitter_card'] ) ? sanitize_text_field( $data['twitter_card'] ) : 'default';
		$thePost->twitter_image_type          = ! empty( $data['twitter_image_type'] ) ? sanitize_text_field( $data['twitter_image_type'] ) : 'default';
		$thePost->twitter_image_url           = null; // We'll reset this below.
		$thePost->twitter_image_custom_url    = ! empty( $data['twitter_image_custom_url'] ) ? esc_url_raw( $data['twitter_image_custom_url'] ) : null;
		$thePost->twitter_image_custom_fields = ! empty( $data['twitter_image_custom_fields'] ) ? sanitize_text_field( $data['twitter_image_custom_fields'] ) : null;
		// Schema
		$thePost->schema                      = ! empty( $data['schema'] ) ? self::getDefaultSchemaOptions( $data['schema'] ) : null;
		$thePost->local_seo                   = ! empty( $data['local_seo'] ) ? $data['local_seo'] : null;
		$thePost->limit_modified_date         = isset( $data['limit_modified_date'] ) ? rest_sanitize_boolean( $data['limit_modified_date'] ) : 0;
		$thePost->open_ai                     = ! empty( $data['open_ai'] ) ? self::getDefaultOpenAiOptions( $data['open_ai'] ) : null;
		$thePost->updated                     = gmdate( 'Y-m-d H:i:s' );
		$thePost->primary_term                = ! empty( $data['primary_term'] ) ? $data['primary_term'] : null;
		$thePost->breadcrumb_settings         = isset( $data['breadcrumb_settings']['default'] ) && false === $data['breadcrumb_settings']['default'] ? $data['breadcrumb_settings'] : null;

		// Before we determine the OG/Twitter image, we need to set the meta data cache manually because the changes haven't been saved yet.
		aioseo()->meta->metaData->bustPostCache( $thePost->post_id, $thePost );

		// Set the OG/Twitter image data.
		$thePost = self::setOgTwitterImageData( $thePost );

		if ( ! $thePost->exists() ) {
			$thePost->created = gmdate( 'Y-m-d H:i:s' );
		}

		// Update defaults from addons.
		foreach ( aioseo()->addons->getLoadedAddons() as $addon ) {
			if ( isset( $addon->postModel ) && method_exists( $addon->postModel, 'sanitizeAndSetDefaults' ) ) {
				$thePost = $addon->postModel->sanitizeAndSetDefaults( $postId, $thePost, $data );
			}
		}

		return $thePost;
	}

	/**
	 * Set the OG/Twitter image data on the post object.
	 *
	 * @since 4.1.6
	 *
	 * @param  Post $thePost The Post object to modify.
	 * @return Post          The modified Post object.
	 */
	public static function setOgTwitterImageData( $thePost ) {
		// Set the OG image.
		if (
			in_array( $thePost->og_image_type, [
				'featured',
				'content',
				'attach',
				'custom',
				'custom_image'
			], true )
		) {
			// Disable the cache.
			aioseo()->social->image->useCache = false;

			// Set the image details.
			$ogImage                  = aioseo()->social->facebook->getImage( $thePost->post_id );
			$thePost->og_image_url    = is_array( $ogImage ) ? $ogImage[0] : $ogImage;
			$thePost->og_image_width  = aioseo()->social->facebook->getImageWidth();
			$thePost->og_image_height = aioseo()->social->facebook->getImageHeight();

			// Reset the cache property.
			aioseo()->social->image->useCache = true;
		}

		// Set the Twitter image.
		if (
			! $thePost->twitter_use_og &&
			in_array( $thePost->twitter_image_type, [
				'featured',
				'content',
				'attach',
				'custom',
				'custom_image'
			], true )
		) {
			// Disable the cache.
			aioseo()->social->image->useCache = false;

			// Set the image details.
			$ogImage                    = aioseo()->social->twitter->getImage( $thePost->post_id );
			$thePost->twitter_image_url = is_array( $ogImage ) ? $ogImage[0] : $ogImage;

			// Reset the cache property.
			aioseo()->social->image->useCache = true;
		}

		return $thePost;
	}

	/**
	 * Saves some of the data as post meta so that it can be used for localization.
	 *
	 * @since 4.1.5
	 *
	 * @param  int   $postId The post ID.
	 * @param  array $data   The data.
	 * @return void
	 */
	public static function updatePostMeta( $postId, $data ) {
		// Update the post meta as well for localization.
		$keywords      = ! empty( $data['keywords'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['keywords'] ) : [];
		$ogArticleTags = ! empty( $data['og_article_tags'] ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $data['og_article_tags'] ) : [];

		update_post_meta( $postId, '_aioseo_title', $data['title'] );
		update_post_meta( $postId, '_aioseo_description', $data['description'] );
		update_post_meta( $postId, '_aioseo_keywords', $keywords );
		update_post_meta( $postId, '_aioseo_og_title', $data['og_title'] );
		update_post_meta( $postId, '_aioseo_og_description', $data['og_description'] );
		update_post_meta( $postId, '_aioseo_og_article_section', $data['og_article_section'] );
		update_post_meta( $postId, '_aioseo_og_article_tags', $ogArticleTags );
		update_post_meta( $postId, '_aioseo_twitter_title', $data['twitter_title'] );
		update_post_meta( $postId, '_aioseo_twitter_description', $data['twitter_description'] );
	}

	/**
	 * Returns the default values for the TruSEO page analysis.
	 *
	 * @since 4.0.0
	 *
	 * @param  object|null $pageAnalysis The page analysis object.
	 * @return object                    The default values.
	 */
	public static function getPageAnalysisDefaults( $pageAnalysis = null ) {
		$defaults = [
			'analysis' => [
				'basic'       => [
					'lengthContent' => [
						'error'    => 1,
						'maxScore' => 9,
						'score'    => 6,
					],
				],
				'title'       => [
					'titleLength' => [
						'error'    => 1,
						'maxScore' => 9,
						'score'    => 1,
					],
				],
				'readability' => [
					'contentHasAssets' => [
						'error'    => 1,
						'maxScore' => 5,
						'score'    => 0,
					],
				]
			]
		];

		if ( empty( $pageAnalysis ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		return $pageAnalysis;
	}

	/**
	 * Returns a JSON object with default schema options.
	 *
	 * @since 4.2.5
	 *
	 * @param  string        $existingOptions The existing options in JSON.
	 * @param  null|\WP_Post $post            The post object.
	 * @return object                         The existing options with defaults added in JSON.
	 */
	public static function getDefaultSchemaOptions( $existingOptions = '', $post = null ) {
		$defaultGraphName = aioseo()->schema->getDefaultPostTypeGraph( $post );

		$defaults = [
			'blockGraphs'  => [],
			'customGraphs' => [],
			'default'      => [
				'data'      => [
					'Article'             => [],
					'Course'              => [],
					'Dataset'             => [],
					'FAQPage'             => [],
					'Movie'               => [],
					'Person'              => [],
					'Product'             => [],
					'ProductReview'       => [],
					'Car'                 => [],
					'Recipe'              => [],
					'Service'             => [],
					'SoftwareApplication' => [],
					'WebPage'             => []
				],
				'graphName' => $defaultGraphName,
				'isEnabled' => true,
			],
			'graphs'       => []
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		if ( isset( $existingOptions['defaultGraph'] ) && ! empty( $existingOptions['defaultPostTypeGraph'] ) ) {
			$existingOptions['default']['isEnabled'] = ! empty( $existingOptions['defaultGraph'] );

			unset( $existingOptions['defaultGraph'] );
			unset( $existingOptions['defaultPostTypeGraph'] );
		}

		// Reset the default graph type to make sure it's accurate.
		if ( $defaultGraphName ) {
			$existingOptions['default']['graphName'] = $defaultGraphName;
		}

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Returns the defaults for the keyphrases column.
	 *
	 * @since 4.1.7
	 *
	 * @param  null|object $keyphrases The database keyphrases.
	 * @return object                  The defaults.
	 */
	public static function getKeyphrasesDefaults( $keyphrases = null ) {
		$defaults = [
			'focus'      => [
				'keyphrase' => '',
				'score'     => 0,
				'analysis'  => [
					'keyphraseInTitle' => [
						'score'    => 0,
						'maxScore' => 9,
						'error'    => 1
					]
				]
			],
			'additional' => []
		];

		if ( empty( $keyphrases ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		if ( empty( $keyphrases->focus ) ) {
			$keyphrases->focus = $defaults['focus'];
		}

		if ( empty( $keyphrases->additional ) ) {
			$keyphrases->additional = $defaults['additional'];
		}

		return $keyphrases;
	}

	/**
	 * Returns the defaults for the options column.
	 *
	 * @since   4.2.2
	 * @version 4.2.9
	 *
	 * @param  Post $post   The Post object.
	 * @return Post         The modified Post object.
	 */
	public static function setOptionsDefaults( $post ) {
		$defaults = [
			'linkFormat'  => [
				'internalLinkCount'      => 0,
				'linkAssistantDismissed' => false
			],
			'primaryTerm' => [
				'productEducationDismissed' => false
			]
		];

		if ( empty( $post->options ) ) {
			$post->options = json_decode( wp_json_encode( $defaults ) );

			return $post;
		}

		$post->options = json_decode( wp_json_encode( $post->options ), true );
		$post->options = array_replace_recursive( $defaults, $post->options );
		$post->options = json_decode( wp_json_encode( $post->options ) );

		return $post;
	}

	/**
	 * Returns the default Open AI options.
	 *
	 * @since 4.3.2
	 *
	 * @param  array $existingOptions The existing options.
	 * @return object                 The default options.
	 */
	public static function getDefaultOpenAiOptions( $existingOptions = [] ) {
		$defaults = [
			'title'       => [
				'suggestions' => [],
				'usage'       => 0
			],
			'description' => [
				'suggestions' => [],
				'usage'       => 0
			]
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Returns the default breadcrumb settings options.
	 *
	 * @since 4.8.3
	 *
	 * @param  array  $postType        The post type.
	 * @param  array  $existingOptions The existing options.
	 * @return object                  The default options.
	 */
	public static function getDefaultBreadcrumbSettingsOptions( $postType, $existingOptions = [] ) {
		$default       = aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->useDefaultTemplate ?? true;
		$showHomeCrumb = $default ? aioseo()->options->breadcrumbs->homepageLink : aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showHomeCrumb ?? true;

		$defaults = [
			'default'            => true,
			'separator'          => aioseo()->options->breadcrumbs->separator,
			'showHomeCrumb'      => $showHomeCrumb ?? true,
			'showTaxonomyCrumbs' => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showTaxonomyCrumbs ?? true,
			'showParentCrumbs'   => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->showParentCrumbs ?? true,
			'template'           => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
			'parentTemplate'     => aioseo()->helpers->encodeOutputHtml( aioseo()->breadcrumbs->frontend->getDefaultTemplate( 'single' ) ),
			'taxonomy'           => aioseo()->dynamicOptions->breadcrumbs->postTypes->$postType->taxonomy ?? '',
			'primaryTerm'        => null
		];

		if ( empty( $existingOptions ) ) {
			return json_decode( wp_json_encode( $defaults ) );
		}

		$existingOptions = json_decode( wp_json_encode( $existingOptions ), true );
		$existingOptions = array_replace_recursive( $defaults, $existingOptions );

		return json_decode( wp_json_encode( $existingOptions ) );
	}

	/**
	 * Migrates the post's audience age schema data when it is loaded.
	 * Min age: [0 => newborns, 0.25 => infants, 1 => toddlers, 5 => kids, 13 => adults]
	 * Max age: [0.25 => newborns, 1 => infants, 5 => toddlers, 13 => kids]
	 *
	 * @since 4.7.9
	 *
	 * @param  object $audience The audience data.
	 * @return object
	 */
	public static function migratePostAudienceAgeSchema( $audience ) {
		$ages = [ 0, 0.25, 1, 5, 13 ];

		// converts variable to integer if it's a number otherwise is null.
		$parsedMinAge = filter_var( $audience->minimumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );
		$parsedMaxAge = filter_var( $audience->maximumAge, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE );

		if ( null === $parsedMinAge && null === $parsedMaxAge ) {
			return $audience;
		}

		$minAge = is_numeric( $parsedMinAge ) ? $parsedMinAge : 0;
		$maxAge = is_numeric( $parsedMaxAge ) ? $parsedMaxAge : null;

		// get the minimumAge if available or the nearest bigger one.
		foreach ( $ages as $age ) {
			if ( $age >= $minAge ) {
				$audience->minimumAge = $age;
				break;
			}
		}

		// get the maximumAge if available or the nearest bigger one.
		foreach ( $ages as $age ) {
			if ( $age >= $maxAge ) {
				$maxAge = $age;
				break;
			}
		}

		// makes sure the maximumAge is 13 below
		if ( null !== $maxAge ) {
			$audience->maximumAge = 13 < $maxAge ? 13 : $maxAge;
		}

		// Minimum age 13 is for adults.
		// If minimumAge is still higher or equal 13 then it's for adults and maximumAge should be empty.
		if ( 13 <= $audience->minimumAge ) {
			$audience->minimumAge = 13;
			$audience->maximumAge = null;
		}

		return $audience;
	}

	/**
	 * Migrates update Korea country code for Person, Product, Event, and JobsPosting schemas.
	 *
	 * @since 4.7.1
	 *
	 * @param  Post $aioseoPost The post object.
	 * @return Post             The modified post object.
	 */
	private static function migrateKoreaCountryCodeSchemas( $aioseoPost ) {
		if ( empty( $aioseoPost->schema ) || empty( $aioseoPost->schema->graphs ) ) {
			return $aioseoPost;
		}

		foreach ( $aioseoPost->schema->graphs as $key => $graph ) {
			if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->location->country ) ) {
				$aioseoPost->schema->graphs[ $key ]->properties->location->country = self::invertKoreaCode( $graph->properties->location->country );
			}

			if ( isset( $aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations ) ) {
				$aioseoPost->schema->graphs[ $key ]->properties->shippingDestinations = array_map( function( $item ) {
					$item->country = self::invertKoreaCode( $item->country );

					return $item;
				}, $graph->properties->shippingDestinations );
			}
		}

		$aioseoPost->save();

		return $aioseoPost;
	}

	/**
	 * Utility function to invert the country code for Korea.
	 *
	 * @since 4.7.1
	 *
	 * @param  string $code country code.
	 * @return string       country code.
	 */
	public static function invertKoreaCode( $code ) {
		return 'KP' === $code ? 'KR' : $code;
	}
}SeoAnalyzerResult.php000066600000011075151146251270010724 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The SeoAnalyzerResult Model.
 *
 * @since 4.8.3
 */
class SeoAnalyzerResult extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.8.3
	 *
	 * @var string
	 */
	protected $table = 'aioseo_seo_analyzer_results';

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $jsonFields = [
		'data'
	];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that can be null when saved.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $nullFields = [
		'competitor_url',
	];

	/**
	 * An array of columns from the DB that we can use.
	 *
	 * @since 4.8.3
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'score',
		'data',
		'competitor_url',
		'created',
		'updated',
	];

	/**
	 * Returns all not competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return array List of results.
	 */
	public static function getResults() {
		$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
			->select( '*' )
			->where( 'competitor_url', null )
			->run()
			->result();

		if ( empty( $results ) ) {
			return [];
		}

		return self::parseObjects( $results );
	}

	/**
	 * Returns all competitors results.
	 *
	 * @since 4.8.3
	 *
	 * @return array List of results.
	 */
	public static function getCompetitorsResults() {
		$results = aioseo()->core->db->start( 'aioseo_seo_analyzer_results' )
			->select( '*' )
			->whereRaw( 'competitor_url IS NOT NULL' )
			->orderBy( 'updated DESC' )
			->run()
			->result();

		if ( empty( $results ) ) {
			return [];
		}

		return self::parseObjects( $results, true );
	}

	/**
	 * Parse results to the front end format.
	 *
	 * @since 4.8.3
	 *
	 * @param  array $objects      List of objects.
	 * @param  bool  $isCompetitor Flag that indicates if is parsing a competitor or a homepage result.
	 * @return array               List of results.
	 */
	private static function parseObjects( $objects, $isCompetitor = false ) {
		$results = [];

		foreach ( $objects as $obj ) {
			$data = json_decode( $obj->data ?? '[]', true );

			if ( ! $isCompetitor ) {
				$results['score'] = $obj->score ?? 0;
			}

			foreach ( $data as $result ) {
				$metadata = $result['metadata'] ?? [];
				$item     = empty( $result['status'] ) && ! empty( $metadata['value'] ) ? $metadata['value'] : array_merge( $metadata, [ 'status' => $result['status'] ] );

				if ( $isCompetitor ) {
					if ( empty( $obj->competitor_url ) || empty( $result['group'] ) || empty( $result['name'] ) ) {
						continue;
					}

					$results[ $obj->competitor_url ]['results'][ $result['group'] ][ $result['name'] ] = $item;
					$results[ $obj->competitor_url ]['score'] = ! empty( $obj->score ) ? $obj->score : 0;
				} else {
					$results['results'][ $result['group'] ][ $result['name'] ] = $item;
				}
			}
		}

		return $results;
	}

	/**
	 * Delete results by competitor url, if null we are deleting the homepage results.
	 *
	 * @since 4.8.3
	 *
	 * @param  string $url The competitor url.
	 * @return void
	 */
	public static function deleteByUrl( $url ) {
		aioseo()->core->db
			->delete( 'aioseo_seo_analyzer_results' )
			->where( 'competitor_url', $url )
			->run();
	}

	/**
	 * Add multiple results at once.
	 *
	 * @since 4.8.3
	 *
	 * @return void
	 */
	public static function addResults( $results, $competitorUrl = null ) {
		if ( empty( $results['results'] ) ) {
			return;
		}

		// Delete the results for the competitor url if it exists.
		self::deleteByUrl( $competitorUrl );

		$data = [
			'competitor_url' => $competitorUrl,
			'score'          => $results['score'],
			'data'           => []
		];

		foreach ( $results['results'] as $group => $items ) {
			foreach ( $items as $name => $result ) {
				$fields = [
					'name'     => $name,
					'group'    => $group,
					'status'   => empty( $result['status'] ) ? null : $result['status'],
					'metadata' => null,
				];

				if ( ! is_array( $result ) ) {
					$fields['metadata'] = [ 'value' => $result ];
				} else {
					$metadata = [];
					foreach ( $result as $key => $value ) {
						if ( 'status' !== $key ) {
							$metadata[ $key ] = $value;
						}
					}

					if ( ! empty( $metadata ) ) {
						$fields['metadata'] = $metadata;
					}
				}

				$data['data'][] = $fields;
			}
		}

		$data['data'] = wp_json_encode( $data['data'] );
		$newResult = new SeoAnalyzerResult( $data );
		$newResult->save();
	}
}Notification.php000066600000023010151146251270007707 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * The Notification DB Model.
 *
 * @since 4.0.0
 */
class Notification extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $table = 'aioseo_notifications';

	/**
	 * An array of fields to set to null if already empty when saving to the database.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $nullFields = [
		'start',
		'end',
		'notification_id',
		'notification_name',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action'
	];

	/**
	 * Fields that should be json encoded on save and decoded on get.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'level' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $booleanFields = [ 'dismissed' ];

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * An array of fields attached to this resource.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $columns = [
		'id',
		'slug',
		'addon',
		'title',
		'content',
		'type',
		'level',
		'notification_id',
		'notification_name',
		'start',
		'end',
		'button1_label',
		'button1_action',
		'button2_label',
		'button2_action',
		'dismissed',
		'new',
		'created',
		'updated'
	];

	/**
	 * Get the list of notifications.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $reset Whether or not to reset the notifications.
	 * @return array        An array of notifications.
	 */
	public static function getNotifications( $reset = true ) {
		static $notifications = null;
		if ( null !== $notifications ) {
			return $notifications;
		}

		$notifications = [
			'active'    => self::getAllActiveNotifications(),
			'new'       => self::getNewNotifications( $reset ),
			'dismissed' => self::getAllDismissedNotifications()
		];

		return $notifications;
	}

	/**
	 * Get an array of active notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active notifications.
	 */
	public static function getAllActiveNotifications() {
		static $activeNotifications = null;
		if ( null !== $activeNotifications ) {
			return $activeNotifications;
		}

		$staticNotifications = self::getStaticNotifications();
		$notifications       = array_values( json_decode( wp_json_encode( self::getActiveNotifications() ), true ) );

		$activeNotifications = ! empty( $staticNotifications )
			? array_merge( $staticNotifications, $notifications )
			: $notifications;

		return $activeNotifications;
	}

	/**
	 * Get all new notifications. After retrieving them, this will reset them.
	 * This means that calling this method twice will result in no results
	 * the second time. The only exception is to pass false as a reset variable to prevent it.
	 *
	 * @since 4.1.3
	 *
	 * @param  bool  $reset Whether or not to reset the new notifications.
	 * @return array        An array of new notifications if any exist.
	 */
	public static function getNewNotifications( $reset = true ) {
		static $newNotifications = null;
		if ( null !== $newNotifications ) {
			return $newNotifications;
		}

		$newNotifications = self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 0 )
				->where( 'new', 1 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);

		if ( $reset ) {
			self::resetNewNotifications();
		}

		return $newNotifications;
	}

	/**
	 * Resets all new notifications.
	 *
	 * @since 4.1.3
	 *
	 * @return void
	 */
	public static function resetNewNotifications() {
		aioseo()->core->db
			->update( 'aioseo_notifications' )
			->where( 'new', 1 )
			->set( 'new', 0 )
			->run();
	}

	/**
	 * Returns all static notifications.
	 *
	 * @since 4.1.2
	 *
	 * @return array An array of static notifications.
	 */
	public static function getStaticNotifications() {
		$staticNotifications = [];
		$notifications       = [
			'unlicensed-addons',
			'review'
		];

		foreach ( $notifications as $notification ) {
			switch ( $notification ) {
				case 'review':
					// If they intentionally dismissed the main notification, we don't show the repeat one.
					$originalDismissed = get_user_meta( get_current_user_id(), '_aioseo_plugin_review_dismissed', true );
					if ( '4' !== $originalDismissed ) {
						break;
					}

					$dismissed = get_user_meta( get_current_user_id(), '_aioseo_notification_plugin_review_dismissed', true );
					if ( '3' === $dismissed ) {
						break;
					}

					if ( ! empty( $dismissed ) && $dismissed > time() ) {
						break;
					}

					$activated = aioseo()->internalOptions->internal->firstActivated( time() );
					if ( $activated > strtotime( '-20 days' ) ) {
						break;
					}

					$isV3                  = get_option( 'aioseop_options' ) || get_option( 'aioseo_options_v3' );
					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification . ( $isV3 ? '' : '2' )
					];
					break;
				case 'unlicensed-addons':
					$unlicensedAddons = aioseo()->addons->unlicensedAddons();
					if ( empty( $unlicensedAddons['addons'] ) ) {
						break;
					}

					$staticNotifications[] = [
						'slug'      => 'notification-' . $notification,
						'component' => 'notifications-' . $notification,
						'addons'    => $unlicensedAddons['addons'],
						'message'   => $unlicensedAddons['message']
					];
					break;
			}
		}

		return $staticNotifications;
	}

	/**
	 * Retrieve active notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of active notifications or empty.
	 */
	protected static function getActiveNotifications() {
		return self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 0 )
				->whereRaw( "(start <= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR start IS NULL)" )
				->whereRaw( "(end >= '" . gmdate( 'Y-m-d H:i:s' ) . "' OR end IS NULL)" )
				->orderBy( 'start DESC' )
				->orderBy( 'created DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);
	}

	/**
	 * Get an array of dismissed notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of dismissed notifications.
	 */
	protected static function getAllDismissedNotifications() {
		return array_values( json_decode( wp_json_encode( self::getDismissedNotifications() ), true ) );
	}

	/**
	 * Retrieve dismissed notifications.
	 *
	 * @since 4.0.0
	 *
	 * @return array An array of dismissed notifications or empty.
	 */
	protected static function getDismissedNotifications() {
		static $dismissedNotifications = null;
		if ( null !== $dismissedNotifications ) {
			return $dismissedNotifications;
		}

		$dismissedNotifications = self::filterNotifications(
			aioseo()->core->db
				->start( 'aioseo_notifications' )
				->where( 'dismissed', 1 )
				->orderBy( 'updated DESC' )
				->run()
				->models( 'AIOSEO\\Plugin\\Common\\Models\\Notification' )
		);

		return $dismissedNotifications;
	}

	/**
	 * Returns a notification by its name.
	 *
	 * @since 4.0.0
	 *
	 * @param  string       $name The notification name.
	 * @return Notification       The notification.
	 */
	public static function getNotificationByName( $name ) {
		return aioseo()->core->db
			->start( 'aioseo_notifications' )
			->where( 'notification_name', $name )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\Notification' );
	}

	/**
	 * Stores a new notification in the DB.
	 *
	 * @since 4.0.0
	 *
	 * @param  array        $fields       The fields.
	 * @return Notification $notification The notification.
	 */
	public static function addNotification( $fields ) {
		// Set the dismissed status to false.
		$fields['dismissed'] = 0;

		$notification = new self();
		$notification->set( $fields );
		$notification->save();

		return $notification;
	}

	/**
	 * Deletes a notification by its name.
	 *
	 * @since 4.0.0
	 *
	 * @param  string $name The notification name.
	 * @return void
	 */
	public static function deleteNotificationByName( $name ) {
		aioseo()->core->db
			->delete( 'aioseo_notifications' )
			->where( 'notification_name', $name )
			->run();
	}

	/**
	 * Filters the notifications based on the targeted plan levels.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $notifications          The notifications
	 * @return array $remainingNotifications The remaining notifications.
	 */
	protected static function filterNotifications( $notifications ) {
		$remainingNotifications = [];
		foreach ( $notifications as $notification ) {
			// If announcements are disabled and this is an announcement, skip adding it and move on.
			if (
				! aioseo()->options->advanced->announcements &&
				'success' === $notification->type
			) {
				continue;
			}

			// If this is an addon notification and the addon is disabled, skip adding it and move on.
			if ( ! empty( $notification->addon ) && ! aioseo()->addons->getLoadedAddon( $notification->addon ) ) {
				continue;
			}

			$levels = $notification->level;
			if ( ! is_array( $levels ) ) {
				$levels = empty( $notification->level ) ? [ 'all' ] : [ $notification->level ];
			}

			foreach ( $levels as $level ) {
				if ( ! aioseo()->notices->validateType( $level ) ) {
					continue 2;
				}
			}

			$remainingNotifications[] = $notification;
		}

		return $remainingNotifications;
	}
}WritingAssistantKeyword.php000066600000003047151146251270012153 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

/**
 * Class Keyword.
 *
 * @since 4.7.4
 */
class WritingAssistantKeyword extends Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.7.4
	 *
	 * @var string
	 */
	protected $table = 'aioseo_writing_assistant_keywords';

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'progress' ];

	/**
	 * Fields that should be boolean values.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $booleanFields = [];

	/**
	 * Fields that should be encoded/decoded on save/get.
	 *
	 * @since 4.7.4
	 *
	 * @var array
	 */
	protected $jsonFields = [ 'keywords', 'competitors', 'content_analysis' ];

	/**
	 * Gets a keyword.
	 *
	 * @since 4.7.4
	 *
	 * @param  string $keyword  A keyword.
	 * @param  string $country  The country code.
	 * @param  string $language The language code.
	 * @return object           A keyword found.
	 */
	public static function getKeyword( $keyword, $country, $language ) {
		$dbKeyword = aioseo()->core->db->start( 'aioseo_writing_assistant_keywords' )
			->where( 'keyword', $keyword )
			->where( 'country', $country )
			->where( 'language', $language )
			->run()
			->model( 'AIOSEO\Plugin\Common\Models\WritingAssistantKeyword' );

		if ( ! $dbKeyword->exists() ) {
			$dbKeyword->keyword  = $keyword;
			$dbKeyword->country  = $country;
			$dbKeyword->language = $language;
		}

		return $dbKeyword;
	}
}CrawlCleanupBlockedArg.php000066600000012252151146251270011565 0ustar00<?php
namespace AIOSEO\Plugin\Common\Models;

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

use AIOSEO\Plugin\Common\Models as CommonModels;
/**
 * The Crawl Cleanup Blocked Arg DB Model.
 *
 * @since 4.5.8
 */
class CrawlCleanupBlockedArg extends CommonModels\Model {
	/**
	 * The name of the table in the database, without the prefix.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	protected $table = 'aioseo_crawl_cleanup_blocked_args';

	/**
	 * Fields that should be hidden when serialized.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $hidden = [ 'id' ];

	/**
	 * Fields that should be numeric values.
	 *
	 * @since 4.5.8
	 *
	 * @var array
	 */
	protected $integerFields = [ 'id', 'hits' ];

	/**
	 * Field to count hits.
	 *
	 * @since 4.5.8
	 *
	 * @var integer
	 */
	protected $hits = 0;

	/**
	 * Field for Regex.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	public $regex = null;

	/**
	 * Field that contains the hash for key+value
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	public $key_value_hash = null;

	/**
	 * Separator used to merge key and value string.
	 *
	 * @since 4.5.8
	 *
	 * @var string
	 */
	private static $keyValueSeparator = '=';

	/**
	 * Separator used to merge key and value string.
	 *
	 * @since 4.5.8
	 *
	 * @var CrawlCleanupBlockedArg|null
	 */
	private static $regexBlockedArgs = null;

	/**
	 * Class constructor.
	 *
	 * @since 4.5.8
	 *
	 * @param mixed $var This can be the primary key of the resource, or it could be an array of data to manufacture a resource without a database query.
	 */
	public function __construct( $var = null ) {
		parent::__construct( $var );
	}

	/**
	 * Get Blocked row using Key and Value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $key   The key to search.
	 * @param  string                 $value The value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function getByKeyValue( $key, $value ) {
		$keyValue = self::getKeyValueString( $key, $value );

		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_blocked_args' )
			->where( 'key_value_hash', sha1( $keyValue ) )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
	}

	/**
	 * Get Blocked row using Regex Value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $regex The regex value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function getByRegex( $regex ) {
		return aioseo()->core->db
			->start( 'aioseo_crawl_cleanup_blocked_args' )
			->where( 'regex', $regex )
			->run()
			->model( 'AIOSEO\\Plugin\\Common\\Models\\CrawlCleanupBlockedArg' );
	}

	/**
	 * Look for regex match by key and value.
	 *
	 * @since 4.5.8
	 *
	 * @param  string                 $key   The key to search.
	 * @param  string                 $value The value to search.
	 * @return CrawlCleanupBlockedArg        The CrawlCleanupBlockedArg object.
	 */
	public static function matchRegex( $key, $value ) {
		$keyValue = self::getKeyValueString( $key, $value );
		$regexBlockedArgs = self::getRegexBlockedArgs();

		foreach ( $regexBlockedArgs as $regexQueryArg ) {
			$escapedRegex = str_replace( '@', '\@', $regexQueryArg->regex );
			if ( preg_match( "@{$escapedRegex}@", (string) $keyValue ) ) {
				return new CrawlCleanupBlockedArg( $regexQueryArg->id );
			}
		}

		return new CrawlCleanupBlockedArg();
	}

	/**
	 * Get Regex rows.
	 *
	 * @since 4.5.8
	 *
	 * @return CrawlCleanupBlockedArg The CrawlCleanupBlockedArg object.
	 */
	public static function getRegexBlockedArgs() {
		if ( null === self::$regexBlockedArgs ) {
			self::$regexBlockedArgs = aioseo()->core->db
				->start( 'aioseo_crawl_cleanup_blocked_args' )
				->select( 'id, regex' )
				->whereRaw( 'regex IS NOT NULL' )
				->run()
				->result();
		}

		return self::$regexBlockedArgs;
	}

	/**
	 * Transforms data as needed.
	 *
	 * @since 4.5.8
	 *
	 * @param  array $data The data array to transform.
	 * @return array       The transformed data.
	 */
	protected function transform( $data, $set = false ) {
		$data = parent::transform( $data, $set );

		// Create key+value hash.
		if ( ! empty( $data['key'] ) ) {
			$keyValue = self::getKeyValueString( $data['key'], $data['value'] );
			$data['key_value_hash'] = sha1( $keyValue );
		}

		// Case hits number are empty start with 0.
		if ( empty( $data['hits'] ) ) {
			$data['hits'] = 0;
		}

		return $data;
	}

	/**
	 * Increase hits and save.
	 *
	 * @since 4.5.8
	 *
	 */
	public function addHit() {
		if ( $this->id ) {
			$this->hits++;
			parent::save();
		}
	}

	/**
	 * Return string with key and value with pattern model defined.
	 *
	 * @since 4.5.8
	 *
	 * @param  string $key   The key to merge.
	 * @param  string $value The value to merge.
	 * @return string        The result string merging key and value (case not empty).
	 */
	public static function getKeyValueString( $key, $value ) {
		return $key . ( $value ? self::getKeyValueSeparator() . $value : '' );
	}

	/**
	 * Return string to separate key and value.
	 *
	 * @since 4.5.8
	 *
	 * @return string The separator for key and value.
	 */
	public static function getKeyValueSeparator() {
		return self::$keyValueSeparator;
	}
}