<?php
/*
 * License: GPLv3
 * License URI: https://www.gnu.org/licenses/gpl.txt
 * Copyright 2012-2026 Jean-Sebastien Morisset (https://wpsso.com/)
 */

if ( ! defined( 'ABSPATH' ) ) {

	die( 'These aren\'t the droids you\'re looking for.' );
}

if ( ! defined( 'WPSSO_PLUGINDIR' ) ) {

	die( 'Do. Or do not. There is no try.' );
}

if ( ! class_exists( 'WpssoAbstractWpMeta' ) ) {

	require_once WPSSO_PLUGINDIR . 'lib/abstract/wp-meta.php';
}

if ( ! class_exists( 'WpssoComment' ) ) {

	class WpssoComment extends WpssoAbstractWpMeta {

		public function __construct( &$plugin ) {

			$this->p =& $plugin;

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark();
			}

			/*
			 * This hook is fired once WordPress, plugins, and the theme are fully loaded and instantiated.
			 */
			add_action( 'wp_loaded', array( $this, 'add_wp_callbacks' ) );
		}

		/*
		 * Add WordPress action and filter callbacks.
		 */
		public function add_wp_callbacks() {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark();
			}

			/*
			 * Since WPSSO Core v17.0.0.
			 *
			 * Register our comment meta.
			 */
			$this->register_meta( $object_type = 'comment', WPSSO_META_NAME );

			/*
			 * Called by wp_insert_comment(), which is called by wp_new_comment().
			 */
			add_action( 'wp_insert_comment', array( $this, 'refresh_cache_insert_comment' ), PHP_INT_MAX, 2 );

			/*
			 * Called by wp_transition_comment_status().
			 */
			add_action( 'transition_comment_status', array( $this, 'refresh_cache_comment_status' ), PHP_INT_MAX, 3 );
		}

		/*
		 * Get the $mod object for a comment ID.
		 */
		public function get_mod( $comment_id ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark_caller();

				$this->p->debug->log_args( array(
					'comment_id' => $comment_id,
				) );
			}

			static $local_fifo = array();

			/*
			 * Maybe return the array from the local cache.
			 */
			if ( isset( $local_fifo[ $comment_id ] ) ) {

				if ( ! $this->md_cache_disabled ) {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'exiting early: comment id ' . $comment_id . ' mod array from local cache' );
					}

					return $local_fifo[ $comment_id ];

				} else unset( $local_fifo[ $comment_id ] );
			}

			/*
			 * Maybe limit the number of array elements.
			 */
			$local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );

			$mod = self::get_mod_defaults();

			/*
			 * Common elements.
			 */
			$mod[ 'id' ]          = is_numeric( $comment_id ) ? (int) $comment_id : 0;	// Cast as integer.
			$mod[ 'name' ]        = 'comment';
			$mod[ 'name_transl' ] = _x( 'comment', 'module name', 'wpsso' );
			$mod[ 'obj' ]         =& $this;

			/*
			 * WpssoComment elements.
			 */
			$mod[ 'is_comment' ] = true;

			if ( $mod[ 'id' ] ) {	// Just in case.

				$mod[ 'wp_obj' ] = SucomUtilWP::get_comment_object( $mod[ 'id' ] );

				if ( $mod[ 'wp_obj' ] instanceof WP_Comment ) {	// Just in case.

					$mod[ 'comment_author' ]      = (int) $mod[ 'wp_obj' ]->user_id;	// Comment author user ID.
					$mod[ 'comment_author_name' ] = $mod[ 'wp_obj' ]->comment_author;	// Comment author name.
					$mod[ 'comment_author_url' ]  = $mod[ 'wp_obj' ]->comment_author_url;
					$mod[ 'comment_parent' ]      = $mod[ 'wp_obj' ]->comment_parent;
					$mod[ 'comment_time' ]        = mysql2date( 'c', $mod[ 'wp_obj' ]->comment_date_gmt );	// ISO 8601 date.
					$mod[ 'comment_timestamp' ]   = mysql2date( 'U', $mod[ 'wp_obj' ]->comment_date_gmt );	// Unix timestamp.
					$mod[ 'is_public' ]           = $mod[ 'wp_obj' ]->comment_approved ? true : false;

					$comment_rating = self::get_meta( $mod[ 'id' ], WPSSO_META_RATING_NAME, $single = true );

					if ( is_numeric( $comment_rating ) ) {

						$mod[ 'comment_rating' ] = $comment_rating;
					}

				} else $mod[ 'wp_obj' ] = false;
			}

			/*
			 * Filter the comment mod array.
			 */
			$mod = apply_filters( 'wpsso_get_comment_mod', $mod, $comment_id );

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log_arr( 'mod', $mod );
			}

			/*
			 * Maybe save the array to the local cache.
			 */
			if ( ! $this->md_cache_disabled ) {

				$local_fifo[ $comment_id ] = $mod;

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log_size( 'local_fifo', $local_fifo );
				}
			}

			return $mod;
		}

		public function get_mod_wp_object( array $mod ) {

			if ( $mod[ 'wp_obj' ] instanceof WP_Comment ) {

				return $mod[ 'wp_obj' ];
			}

			return SucomUtilWP::get_comment_object( $mod[ 'id' ] );
		}

		/*
		 * Option handling methods:
		 *
		 *	get_defaults()
		 *	get_options()
		 *	save_options()
		 *	delete_options()
		 */
		public function get_options( $comment_id, $md_key = false, $filter_opts = true, $merge_defs = false ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->mark_caller();

				$this->p->debug->log_args( array(
					'comment_id'  => $comment_id,
					'md_key'      => $md_key,
					'filter_opts' => $filter_opts,
					'merge_defs'  => $merge_defs,
				) );
			}

			static $local_fifo = array();

			/*
			 * Use $comment_id and $filter_opts to create the cache ID string, but do not add $merge_defs.
			 */
			$cache_id = SucomUtil::get_assoc_salt( array( 'id' => $comment_id, 'filter' => $filter_opts ) );

			/*
			 * Maybe initialize a new local cache element. Use isset() instead of empty() to allow for an empty array.
			 */
			if ( ! isset( $local_fifo[ $cache_id ] ) ) {

				/*
				 * Maybe limit the number of array elements.
				 */
				$local_fifo = SucomUtil::array_slice_fifo( $local_fifo, WPSSO_CACHE_ARRAY_FIFO_MAX );

				$local_fifo[ $cache_id ] = null;	// Create an element to reference.
			}

			$md_opts =& $local_fifo[ $cache_id ];	// Reference the local cache element.

			if ( null === $md_opts ) {	// Maybe read metadata into a new local cache element.

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'getting metadata for comment id ' . $comment_id );
				}

				$md_opts = self::get_meta( $comment_id, WPSSO_META_NAME, $single = true );

				if ( ! is_array( $md_opts ) ) {

					$md_opts = array();	// WPSSO_META_NAME not found.
				}

				unset( $md_opts[ 'opt_filtered' ] );	// Just in case.

				/*
				 * Check if options need to be upgraded and saved.
				 */
				if ( $this->p->opt->is_upgrade_required( $md_opts ) ) {

					$md_opts = $this->upgrade_options( $md_opts, $comment_id );

					self::update_meta( $comment_id, WPSSO_META_NAME, $md_opts );
				}
			}

			if ( $filter_opts ) {

				if ( ! empty( $md_opts[ 'opt_filtered' ] ) ) {	// Set before calling filters to prevent recursion.

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'skipping filters: options already filtered' );
					}

				} else {

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'setting opt_filtered to 1' );
					}

					$md_opts[ 'opt_filtered' ] = 1;	// Set before calling filters to prevent recursion.

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'required call to WpssoComment->get_mod() for comment ID ' . $comment_id );
					}

					$mod = $this->get_mod( $comment_id );

					$filter_name = 'wpsso_get_md_options';

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
					}

					$md_opts = apply_filters( $filter_name, $md_opts, $mod );

					/*
					 * Hooked by several integration modules to provide information about the current content.
					 * e-Commerce integration modules will provide information on their product (price,
					 * condition, etc.) and disable these options in the Document SSO metabox.
					 */
					$filter_name = 'wpsso_get_' . $mod[ 'name' ] . '_options';

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
					}

					$md_opts = apply_filters( $filter_name, $md_opts, $comment_id, $mod );

					$filter_name = 'wpsso_sanitize_md_options';

					if ( $this->p->debug->enabled ) {

						$this->p->debug->log( 'applying filters "' . $filter_name . '"' );
					}

					$md_opts = apply_filters( $filter_name, $md_opts, $mod );
				}
			}

			/*
			 * Maybe save the array to the local cache.
			 */
			if ( $this->md_cache_disabled ) {

				$deref_md_opts = $local_fifo[ $cache_id ];	// Dereference.

				unset( $local_fifo, $md_opts );

				return $this->return_options( $comment_id, $deref_md_opts, $md_key, $merge_defs );
			}

			return $this->return_options( $comment_id, $md_opts, $md_key, $merge_defs );
		}

		/*
		 * Use $rel = false to extend WpssoAbstractWpMeta->save_options().
		 */
		public function save_options( $comment_id, $rel = false ) {

			if ( $this->p->debug->enabled ) {

				$this->p->debug->log_args( array(
					'comment_id' => $comment_id,
				) );
			}

			if ( empty( $comment_id ) ) {	// Just in case.

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'exiting early: comment id is empty' );
				}

				return;

			} elseif ( ! $this->verify_submit_nonce() ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'exiting early: verify_submit_nonce failed' );
				}

				return;

			/*
			 * Check user capability for the comment id.
			 */
			} elseif ( ! $this->user_can_edit( $comment_id ) ) {

				if ( $this->p->debug->enabled ) {

					$user_id = get_current_user_id();

					$this->p->debug->log( 'exiting early: user id ' . $user_id . ' cannot edit comment id ' . $comment_id );
				}

				return;
			}

			$this->md_cache_disable();	// Disable the local cache.

			$mod = $this->get_mod( $comment_id );

			$md_opts = $this->get_submit_opts( $mod );	// Merge previous + submitted options and then sanitize.

			$this->md_cache_enable();	// Re-enable the local cache.

			if ( false === $md_opts ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'exiting early: returned submit options is false' );
				}

				return;
			}

			$md_opts = apply_filters( 'wpsso_save_md_options', $md_opts, $mod );

			$md_opts = apply_filters( 'wpsso_save_' . $mod[ 'name' ] . '_options', $md_opts, $comment_id, $mod );

			return self::update_meta( $comment_id, WPSSO_META_NAME, $md_opts );
		}

		/*
		 * Use $rel = false to extend WpssoAbstractWpMeta->delete_options().
		 */
		public function delete_options( $comment_id, $rel = false ) {

			return self::delete_meta( $comment_id, WPSSO_META_NAME );
		}

		public function refresh_cache_insert_comment( $comment_id, $comment ) {

			if ( ! empty( $comment->comment_approved ) ) {

				if ( ! empty( $comment->comment_post_ID ) ) {

					$this->p->post->refresh_cache( $comment->comment_post_ID );	// Refresh the cache for a single post ID.
				}
			}
		}

		public function refresh_cache_comment_status( $new_status, $old_status, $comment ) {

			if ( 'approved' === $new_status || 'approved' === $old_status ) {

				if ( ! empty( $comment->comment_post_ID ) ) {

					$this->p->post->refresh_cache( $comment->comment_post_ID );	// Refresh the cache for a single post ID.
				}
			}
		}

		/*
		 * Retrieves or updates the metadata cache by key and group.
		 */
		public function get_update_meta_cache( $comment_id ) {

			return SucomUtilWP::get_update_meta_cache( $comment_id, $meta_type = 'comment' );
		}

		/*
		 * Check user capability for the comment id.
		 *
		 * Use $rel = false to extend WpssoAbstractWpMeta->user_can_edit().
		 */
		public function user_can_edit( $comment_id, $rel = false ) {

			$capability = 'edit_comment';

			if ( ! current_user_can( $capability, $comment_id ) ) {

				if ( $this->p->debug->enabled ) {

					$this->p->debug->log( 'exiting early: cannot ' . $capability . ' for comment id ' . $comment_id );
				}

				/*
				 * Add notice only if the admin notices have not already been shown.
				 */
				if ( $this->p->notice->is_admin_pre_notices() ) {

					$this->p->notice->err( sprintf( __( 'Insufficient privileges to edit comment ID %1$s.', 'wpsso' ), $comment_id ) );
				}

				return false;
			}

			return true;
		}

		/*
		 * If $meta_key is en empty string, retrieves all metadata for the specified object ID.
		 *
		 * See https://developer.wordpress.org/reference/functions/get_metadata/.
		 */
		public static function get_meta( $comment_id, $meta_key = '', $single = false ) {

			return get_metadata( 'comment', $comment_id, $meta_key, $single );
		}

		public static function update_meta( $comment_id, $meta_key, $value ) {

			return update_metadata( 'comment', $comment_id, $meta_key, $value );
		}

		public static function delete_meta( $comment_id, $meta_key ) {

			return delete_metadata( 'comment', $comment_id, $meta_key );
		}
	}
}
