<?php
/**
 * Functions for customizing the html <head> of each page.
 * Most modify the output of the WordPress wp_head() function.
 *
 * This file is included by the AUX parent theme functions.php. This file
 * cannot be overridden by a file of the same name in a child theme.
 *
 * @since 1.0.0
 */

use AuxCF\Entities\Field\Enum\DateFieldFormatEnum;
use WP_AUX\Entities\MetaTagsEntity;
use WP_AUX\Services\HeadService;
use WP_AUX\Services\PostService;
use WP_AUX\Services\StoryService;

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

/**
 * Enqueue css, javascript then icons.
 * https://developer.wordpress.org/reference/functions/wp_enqueue_script/.
 *
 * @since 1.0.0
 */
add_action("wp_enqueue_scripts", array("WP_AUX\Services\HeadService", "enqueueScripts"));

add_action('admin_head', function() {
  echo '<style type="text/css">.kint-rich { position: relative; z-index: 2003; }</style>';
});

/**
 * Remove items that are not used from the <head>
 * Remove rsd_link from filters (<link rel="EditURI" />),
 * remove links in head for things like the Comments rss feed,
 * remove WP version number
 *
 * @since 1.0.0
 */
function aux_remove_head_links() {
  remove_action("wp_head", "rsd_link");
  remove_action("wp_head", "feed_links_extra", 3);
  remove_action("wp_head", "wlwmanifest_link");

  // Remove WP version number
  remove_action("wp_head", "wp_generator");
}
add_action("wp", "aux_remove_head_links", 9);

/**
 * Disable the X-Pingback HTTP Header.
 * From code available at https://wordpress.stackexchange.com/questions/31943/is-there-a-way-to-completely-turn-off-pingbacks-trackbacks
 *
 * @param   array  $headers   Associative array of headers to be sent.
 * @param   object $wp_query  Current WordPress environment instance.
 *
 * @return  array  $headers   Filtered array of HTTP headers before they're sent to the browser.
 * @since 1.0.0
 */
function aux_disable_x_pingback($headers, $wp_query) {
  if (isset($headers["X-Pingback"])) {
    // Drop X-Pingback
    unset($headers["X-Pingback"]);
  }
  return $headers;
}
add_filter("wp_headers", "aux_disable_x_pingback", 11, 2);

/**
 * Disable pingback by removing the pingback_url from the get_bloginfo() function
 *
 * @param string $output     The requested non-URL site information.
 * @param string $property  Type of information requested.
 *
 * @return string $output  pingback_url property now set to null
 *
 * @since 1.0.0
 */
function aux_remove_pingback($output, $property) {
  if ($property == "pingback_url") {
    return null;
  } else {
    return $output;
  }
}
add_filter("bloginfo_url", "aux_remove_pingback", 11, 2);

/**
 * Description: Disable XMLRPC advertising/functionality side-wide
 * Author: EarnestoDev, http://earnestodev.com/
 *
 * @param   array  $methods  An array of XML-RPC methods, keyed by their methodName.
 *
 * @return  array  $methods  Array after items have been removed
 * @since 1.0.0
 */
function aux_remove_some_xmlrpc_methods($methods) {
  // Disable vector for brute force login attempts via xml-rpc, from https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/
  unset($methods["system.multicall"]);

  // Disable pingback.ping functionality
  unset($methods["pingback.ping"]);
  unset($methods["pingback.extensions.getPingbacks"]);

  return $methods;
}
add_filter("xmlrpc_methods", "aux_remove_some_xmlrpc_methods");

// Disable xml-rpc, this is a recurring vector for attack
add_filter("xmlrpc_enabled", "__return_false");

/**
 * Remove WP emoji code
 * @since 1.0.0
 */
remove_action("wp_head", "print_emoji_detection_script", 7);
remove_action("wp_print_styles", "print_emoji_styles");

/**
 * Remove oEmbed from WYSIWYG plugins
 *
 * @since 1.0.0
 */
function disable_embeds_tiny_mce_plugin(array $plugins): array
{
  return array_diff($plugins, array('wpembed'));
}

/**
 * Remove oEmbed rewrite rules
 *
 * @since 1.0.0
 */
function disable_embeds_rewrites(array $rules): array
{
  foreach($rules as $rule => $rewrite) {
    if (false !== strpos($rewrite, 'embed=true')) {
      unset($rules[$rule]);
    }
  }

  return $rules;
}

/**
 * Remove oEmbed filters and actions
 *
 * @since 1.0.0
 */
function disable_embeds_code_init(): void
{
  // Remove the REST API endpoint.
  remove_action( 'rest_api_init', 'wp_oembed_register_route' );

  // Turn off oEmbed auto discovery.
  add_filter( 'embed_oembed_discover', '__return_false' );

  // Don't filter oEmbed results.
  remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 );

  // Remove oEmbed discovery links.
  remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );

  // Remove oEmbed-specific JavaScript from the front-end and back-end.
  remove_action( 'wp_head', 'wp_oembed_add_host_js' );
  add_filter( 'tiny_mce_plugins', 'disable_embeds_tiny_mce_plugin' );

  // Remove all embeds rewrite rules.
  add_filter( 'rewrite_rules_array', 'disable_embeds_rewrites' );

  // Remove filter of the oEmbed result before any HTTP requests are made.
  remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 );
}
add_action( 'init', 'disable_embeds_code_init', 9999 );

/**
 * Add the Google Tag Manager script to the <head> of the page.
 *
 * @since 1.0.0
 */
add_action('wp_head', function(): void {
  // Don't load if the visitor is logged in as an admin.
  if (is_current_user_admin()) {
    return;
  }

  // Check if Google Tag Manager is selected as the tracking method
  $trackingMethod = get_field('aux_tracking_method', 'option');
  if ($trackingMethod !== 'google_tag_manager') {
    return;
  }

  // Get the GTM ID from the options page and sanitize it.
  $gtmId = wp_kses_post(get_field('aux_gtm_id', 'option'));

  // Validate GTM ID format: GTM- followed by 6-10 alphanumeric characters
  if (!$gtmId || !preg_match('/^GTM-[A-Za-z0-9]{6,10}$/', $gtmId)) {
    return;
  }

  $google_script = <<<HTML
    <!-- Google Tag Manager -->
    <script type="text/javascript">
    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&amp;l='+l:'';j.async=true;j.src=
    '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','$gtmId');
    </script>
    <!-- End Google Tag Manager -->
  HTML;

  echo $google_script;
});

/**
 * Add the Google Tag Manager noscript script to the <body> of the page.
 *
 * @since 1.0.0
 */
add_action('wp_body_open', function(): void {
  // Don't load if the visitor is logged in as an admin.
  if (is_current_user_admin()) {
    return;
  }

  // Check if Google Tag Manager is selected as the tracking method
  $trackingMethod = get_field('aux_tracking_method', 'option');
  if ($trackingMethod !== 'google_tag_manager') {
    return;
  }

  // Get the GTM ID from the options page and sanitize it.
  $gtmId = wp_kses_post(get_field('aux_gtm_id', 'option'));

  // Validate GTM ID format: GTM- followed by 6-10 alphanumeric characters
  if (!$gtmId || !preg_match('/^GTM-[A-Za-z0-9]{6,10}$/', $gtmId)) {
    return;
  }

  $google_noscript = <<<HTML
      <!-- Google Tag Manager (noscript) -->
      <noscript aria-hidden="true"><iframe height="0" src="//www.googletagmanager.com/ns.html?id=$gtmId" style="display:none;visibility:hidden" width="0"></iframe></noscript>
      <noscript><iframe height="0" src="https://www.googletagmanager.com/ns.html?id=$gtmId" style="display:none;visibility:hidden;" width="0"></iframe></noscript><!-- End Google Tag Manager (noscript) -->
  HTML;

  echo $google_noscript;
});

/**
 * Add the Google Analytics script to the <head> of the page.
 *
 * @since 1.0.0
 */
add_action('wp_head', function(): void {
  // Don't load if the visitor is logged in as an admin.
  if (is_current_user_admin()) {
    return;
  }

  // Check if Google Analytics is selected as the tracking method
  $trackingMethod = get_field('aux_tracking_method', 'option');
  if ($trackingMethod !== 'google_analytics') {
    return;
  }

  // Get the Google Analytics ID from the options page and sanitize it.
  $gaId = wp_kses_post(get_field('aux_google_analytics_id', 'option'));

  // Validate Google Analytics ID format: G-XXXXXXXXXX (GA4)
  if (!$gaId || !preg_match('/^G-[A-Za-z0-9]{10}$/', $gaId)) {
    return;
  }

  // GA4 script
  $google_script = <<<HTML
    <!-- Google Analytics (GA4) -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=$gaId"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', '$gaId');
    </script>
    <!-- End Google Analytics (GA4) -->
  HTML;

  echo $google_script;
});

add_filter('wp_robots', function($robots) {
  if (!is_admin() && (is_post_type_archive() || is_attachment())) {
    $robots['noindex'] = true;
  }

  return $robots;
});

/**
 * Add Dublin Core, Schema.org and Open Graph metatags for search engines.
 * These don't carry much, if any weight for Google, Bing, etc.
 * but they are useful for custom search engines like Elastic,
 * Cludo, or even Google Programmable search and could possibly
 * help with AI search and answer engines.
 *
 * Dublin Core is required by law for all Texas A&M University websites.
 *
 * Schema.org is preferred by search engines.
 * Open Graph by social media: https://ogp.me/
 *
 * @since 1.0.0
 */
function aux_add_meta_tags(): void {
  $seoPlugin = false;

  // Detect if Yoast SEO plugin is activated to avoid adding duplicate meta tags.
  if (defined('WPSEO_VERSION')) {
    $seoPlugin = 'yoast';
  }

  /**
   * All Open Graph Meta Tags will be rendered as...
   *
   * <meta property="og:{$name}" content="{$value}" class="tamu-meta tamu-meta-og-{$name}"/>
   *
   * Additional attributes can be added to a tag in a [attributeName => value] array argument
   * Item classes can be specified as disabled on a per-item basis
   */
  $ogMetaTags = MetaTagsEntity::create('og', true);

  /**
   * All Schema.org Meta Tags will be rendered as...
   *
   * <meta itemprop="{$name}" content="{$value}" class="tamu-meta tamu-meta-{$name}"/>
   *
   * Additional attributes can be added to a tag in a [attributeName => value] array argument
   * Item classes can be specified as disabled on a per-item basis
   */
  $schemaDotOrg = MetaTagsEntity::create('item', true);

  /**
   * All Standard Meta Tags will be rendered as...
   *
   * <meta property="{$name}" content="{$value}"/>
   *
   * Additional attributes can be added to a tag in a [attributeName => value] array argument
   * Item classes can be specified as enabled on a per-item basis
   */
  $standardMetaTags = MetaTagsEntity::create('standard', false);

  /**
   * Dublin Core Meta Tags will be rendered as...
   *
   * <meta name="{$name}" content="{$value}"/>
   * example: <meta name="DC.Author" content="Texas A&M University[ Unit Name]">
   *
   * Additional attributes can be added to a tag in a [attributeName => value] array argument
   * Item classes can be specified as enabled on a per-item basis
   */
  $dublinCoreMetaTags = MetaTagsEntity::create('dublin-core', false);

  global $wp;
  if (is_search()) {
    $permalink = esc_url(home_url(add_query_arg($wp->query_vars, $wp->request)));
  } else {
    $permalink = esc_url(home_url(add_query_arg([], $wp->request)));
  }

  $title = wp_get_document_title();
  $description = '';

  // Set description with priority: excerpt > archive description > site tagline > site title
  $description = get_the_excerpt() ?? get_bloginfo('description') ?? get_bloginfo('name');

  // If it's an archive, use the archive description.
  if (is_archive()) {
    // Get the archive title and strip HTML tags (removes span tags)
    $archiveTitle = wp_strip_all_tags(get_the_archive_title());
    $description = $archiveTitle . ' posts';

    // Use the archive description if it's set (not commonly used but we should use it if it's there).
    // @todo: Consider adding an ACF field for the archive description in the post type options page.
    if (get_the_archive_description()) {
      $description .= ' - ' . esc_attr(get_the_archive_description());
    }

    // If the archive is paginated, add the pagination count to the description
    global $wp_query;
    $paged = max(1, absint($wp_query->get('paged')));

    if ($paged > 1 && $wp_query->max_num_pages > 1) {
      $description .= ' - Page ' . $paged . ' of ' . $wp_query->max_num_pages;
    }
  }

  $publishDate = '';
  $modifiedDate = '';
  if (is_singular()) {
    // Get the publish date in ISO 8601 format for date metatags https://ogp.me/#datetime
    $publishDate = get_the_date('c');
    $modifiedDate = get_the_modified_date('c');
  }

  $language = 'en-US';
  if (!empty(get_bloginfo('language'))) {
    $language = get_bloginfo('language');
  }

  // If there is no SEO plugin, add meta tags required by Open Graph: metadata:
  // title, type, image, url and common optional tags like descripition.
  if (!$seoPlugin) {
    $ogMetaTags
      ->setTag('site_name', get_bloginfo('name'))
      ->setTag('title', $title)
      ->setTag('description', $description)
      ->setTag('url', $permalink);

    $schemaDotOrg
      ->setTag('name', $title)
      ->setTag('headline', $title)
      ->setTag('description', esc_attr($description))
      ->setTag('url', $permalink);

    $standardMetaTags->setTag('description', $description);

    if ($images = HeadService::getShareImages()) {
      // Open Graph supports multiple image sizes: https://ogp.me/#array
      foreach ($images as $image) {
        $ogMetaTags->addGroupTag(
          'image',
          esc_url($image['src']),
          $image['attr'],
          false
        );

        /** @todo: Resolve multiple duplicate images on some platforms */
        break;
      }

      // Use the first (largest) image as the item meta tag image
      $schemaDotOrg->setTag('image', array_values($images)[0]['src']);
    }
  }

  $dublinCoreKeywords = '';
  if ($categories = get_the_category()) {
    foreach ($categories as $category) {
      // article:section is for "A high-level section name. E.g. Technology".
      $standardMetaTags->addTag('article:section', esc_attr($category->name), false, ['data-slug' => esc_attr($category->slug), 'data-search-property' => 'category']);
      $dublinCoreKeywords .= esc_attr($category->name) . ', ';
    }
  }

  if ($tags = get_the_tags()) {
    foreach ($tags as $tag) {
      $standardMetaTags->addTag('article:tag', esc_attr($tag->name), false, ['data-slug' => esc_attr($tag->slug), 'data-search-property' => 'tag']);
      $dublinCoreKeywords .= esc_attr($tag->name) . ', ';
    }
  }

  $ogType = 'website';
  $postType = false;

  if (is_singular()) {
    // Only add these meta tags on single posts. Works for any post type, except attachments and pages.
    $originalDate = StoryService::getOriginalDate(null, DateFieldFormatEnum::ISO8601);

    // If a post has had a signifigant change, editors add the original publish date and
    // update the native WP publish date to the new date.
    if ($originalDate && $originalDate !== $publishDate) {
      $modifiedDate = $publishDate;
      $publishDate = $originalDate;
    }

    $readTime = PostService::pageReadTime();
    if ($readTime) {
      $standardMetaTags->setTag('timeRequired', esc_attr($readTime), false, ['data-search-property' => 'readTime']);
    }

    // Add the post type slug. Note, there is not a schema.org property for post type.
    // @see https://ogp.me/#types and https://ahrefs.com/blog/open-graph-meta-tags/
    $postType = 'webpage'; // Default to webpage, which is the schema.org type for all post types.
    if (is_singular('post') || is_singular('news')) {
      $postType = 'article'; // Use article for blog posts.
    } elseif (is_singular('attachment')) {
      $postType = 'mediaobject'; // Use mediaobject for attachments.
    }
  }

  if ($postType) {
    $ogMetaTags->setTag('type', $postType, false, ['data-search-property' => 'pageType']);
    $standardMetaTags->setTag('tamu:post_type', $postType, false, ['data-search-property' => 'postType']);
  } else {
    $ogMetaTags->setTag('type', $ogType, false);
  }

  if ($publishDate) {
    $schemaDotOrg->setTag('datePublished', $publishDate, false);
    $standardMetaTags->setTag('article:published_time', $publishDate);
  }

  if ($modifiedDate) {
    $schemaDotOrg->setTag('dateModified', $modifiedDate, false);
    $standardMetaTags->setTag('article:modified_time', $modifiedDate);
  }

  /**
   * Add Dublin Core metadata fields specified in Texas Records and Information Locator (TRAIL)
   * These are required by 1 TAC 206.54 and 13 TAC §3.3 for all pages on Texas A&M websites.
   * @see https://www.tsl.texas.gov/sites/default/files/public/tslac/statepubs/docs/trail.pdf
   * @see https://www.tsl.texas.gov/sites/default/files/public/tslac/statepubs/docs/subjects.pdf
   * @since 1.0.2
   */
  if (!empty($dublinCoreKeywords)) {
    $dublinCoreKeywords = trim($dublinCoreKeywords, ', ');
    $dublinCoreMetaTags->setTag('Subject.Keyword', $dublinCoreKeywords);
  }

  if (!empty($description)) {
    $dublinCoreMetaTags->setTag('Description', $description);
  }

  // Subject and type, selected from an approved list:
  // https://www.tsl.texas.gov/sites/default/files/public/tslac/statepubs/docs/subjects.pdf
  $dublinCoreMetaTags->setTag('Subject', 'Education'); // The topic
  $dublinCoreMetaTags->setTag('Type', 'Web documents – Undefined');

  // If the post type is news or story, set the DC.Type to 'News articles'
  if (is_singular('news') || is_singular('story')) {
    $dublinCoreMetaTags->setTag('Type', 'News or Press Releases');
  }

  $dublinCoreMetaTags
    ->setTag('Author', 'Texas A&amp;M University') // @todo: append unit name to 'Texas A&amp;M University' when we add that to aux-global-settings
    ->setTag('Language', $language);

  if ($publishDate) {
    $dublinCoreMetaTags->setTag('Date', $publishDate);
  }

  if ($modifiedDate) {
    $dublinCoreMetaTags->setTag('Modified', $modifiedDate);
  }

  $dublinCoreMetaTags->setTag('Coverage', 'Texas');

  echo $ogMetaTags . $schemaDotOrg . $standardMetaTags . $dublinCoreMetaTags;
}
add_action('wp_head', 'aux_add_meta_tags');
