<?php

/**
 * @file
 * D7Security module hooks.
 */

/**
 * Implements hook_system_info_alter().
 *
 * This hook will swap out the update XML URL for projects now supported at
 * D7Security.
 *
 * Can also check for modules that are missing project information and have the
 * same name as a published contributed module. They should be updated to a
 * well-known versioned state.
 */
function d7security_client_system_info_alter(array &$info, \stdClass $file, string $type) {
  // Statically cache the list of supported projects to make only 1 HTTP
  // request.
  $supported_projects = &drupal_static(__FUNCTION__, NULL);
  if (is_null($supported_projects)) {
    $supported_projects = [];
    $supported_projects_url = variable_get('d7security_client_supported_projects_url', 'https://gitlab.com/d7security/d7security/-/raw/main/supported_projects.txt');
    $d7sec_supported = drupal_http_request($supported_projects_url);
    if ($d7sec_supported->code == 200) {
      $supported_projects = array_filter(array_map('trim', explode("\n", $d7sec_supported->data)));
    }
    else {
      watchdog('d7security_client', 'Could not fetch supported projects list from Gitlab: <pre>@error</pre>', [
        '@error' => print_r($d7sec_supported, TRUE),
      ], WATCHDOG_ERROR);
    }
  }
  if (!empty($info['project']) && in_array($info['project'], $supported_projects)) {
    $info['project status url'] = variable_get('d7security_client_project_status_base_url', 'https://gitlab.com/d7security/d7security/-/raw/main');
  }

  if (variable_get('d7security_client_check_missing_project_info', FALSE)) {
    // Ignore projects that have project package information set.
    if (!empty($info['project'])
      || !empty($info['hidden'])
      || empty($file->name)
      // Ignore projects that have a version set, then the project was removed
      // on purpose.
      || !empty($info['version'])
    ) {
      return;
    }
    $contrib_projects = d7security_client_get_project_names();
    if (in_array($file->name, $contrib_projects)) {
      $info['project'] = $file->name;
    }
  }
}

/**
 * Helper function that downloads all project names from drupal.org.
 *
 * This function is only meant to be run as module maintainer to refresh the TXT
 * file list of known contrib modules that ships with d7security_client.
 *
 * Can be executed with
 * `drush php-eval "d7security_client_write_project_names();"`.
 */
function d7security_client_collect_project_names(): array {
  $project_names = [];
  foreach (['module', 'theme', 'distribution'] as $type) {
    $page = 0;
    $index_url = "https://www.drupal.org/project/project_$type/index";
    do {
      $response = drupal_http_request($index_url . '?page=' . $page);
      if ($response->code == 200) {
        $doc = new DOMDocument();
        // We don't care about HTML errors.
        @$doc->loadHTML($response->data);
        $xpath = new DOMXPath($doc);
        $project_urls = $xpath->query('//span[@class="field-content"]/a/@href');
        // Stop if there is nothing on the page anymore.
        if ($project_urls->length == 0) {
          break;
        }
        foreach ($project_urls as $project_url) {
          // Skip any sandbox projects that are in the full project list by
          // accident.
          if (strpos($project_url->nodeValue, '/sandbox/') !== FALSE) {
            continue;
          }
          $project_names[] = strtolower(str_replace('/project/', '', $project_url->nodeValue));
        }
      }
      $page++;
    } while ($response->code == 200);
  }
  // Also add D7Security supported projects to ensure they are always there.
  $supported_projects_url = variable_get('d7security_client_supported_projects_url', 'https://gitlab.com/d7security/d7security/-/raw/main/supported_projects.txt');
  $d7sec_supported = drupal_http_request($supported_projects_url);
  if ($d7sec_supported->code == 200) {
    $supported_projects = array_filter(array_map('trim', explode("\n", $d7sec_supported->data)));
    $project_names = array_merge($project_names, $supported_projects);
  }
  // Add Drupal core.
  $project_names[] = 'drupal';
  sort($project_names);
  return array_unique(array_filter($project_names));
}

/**
 * Helper function that generates the project names shipped with this module.
 */
function d7security_client_write_project_names() {
  $project_names = d7security_client_collect_project_names();
  file_put_contents(__DIR__ . '/project_names.txt', implode("\n", $project_names));
}

/**
 * Returns the list of known drupal.org project names.
 */
function d7security_client_get_project_names(): array {
  $project_names = drupal_static(__FUNCTION__, []);
  if (empty($project_names)) {
    $list = file_get_contents(__DIR__ . '/project_names.txt');
    $project_names = explode("\n", $list);
  }
  return $project_names;
}

/**
 * Implements hook_cron().
 *
 * Send telemetry data to D7Security.
 */
function d7security_client_cron() {
  global $base_url;
  // By default we send telemetry data, it is opt-out. 2 reasons for that:
  // 1. If we don't have this enabled by default then many people will miss this
  //    and we will have inaccurate counters.
  // 2. drupal.org is collecting project usage data without consent from the
  //    site owner when they download update XML. That justifies it being
  //    enabled by default.
  if (!variable_get('d7security_client_telemetry_enabled', TRUE)
    // Only send telemetry data from production sites.
    || d7security_client_is_dev_site($base_url)
  ) {
    return;
  }
  // Run daily.
  $one_day = 86400;
  $two_days = 172800;
  $last_run = variable_get('d7security_client_telemetry_last_run', REQUEST_TIME - $one_day);
  // Ensure last run is not older than 2 days.
  if ($last_run < REQUEST_TIME - $two_days) {
    $last_run = REQUEST_TIME - $one_day;
  }
  if (REQUEST_TIME - $last_run >= $one_day) {
    module_load_include('inc', 'update', 'update.compare');
    $update_info = update_get_projects();
    $telemetry_data = ['projects' => []];
    // Send project name and version.
    foreach ($update_info as $project_name => $project_info) {
      $telemetry_data['projects'][$project_name] = [
        'version' => $project_info['info']['version'] ?? 'unknown',
      ];
    }
    $response = drupal_http_request(
      variable_get('d7security_client_telemetry_url', 'https://telemetry.d7security.org/api/v1/telemetry'),
      [
        'method' => 'POST',
        'data' => json_encode($telemetry_data),
        'headers' => [
          'Content-Type' => 'application/json',
          // Set a custom HTTP header to identify the site and make it harder
          // for CSRF from Javascript.
          'D7Security-Site-ID' => drupal_hmac_base64($base_url, drupal_get_private_key()),
        ],
      ]
    );
    if ($response->code != 200) {
      watchdog('d7security_client', 'Error sending project usage data: <pre>@error</pre>', [
        '@error' => print_r($response, TRUE),
      ], WATCHDOG_WARNING);
    }
    // Don't use REQUEST_TIME as it could slightly shift the execution later
    // and later. Instead use the last run time as fixed base.
    variable_set('d7security_client_telemetry_last_run', $last_run + $one_day);
  }
}

/**
 * Checks if the site is a development site.
 */
function d7security_client_is_dev_site(string $url): bool {
  $url_parts = parse_url($url);
  // Domain names without any dots are not real domains, must be a dev site.
  if (strpos($url_parts['host'], '.') === FALSE) {
    return TRUE;
  }
  $dev_domain_patterns = [
    '127\.0\.0\.1',
    'ddev\.site',
    'ddev\.local',
    'docksal\.site',
    'example\.com',
    'lndo\.site',
    'local',
    'localhost',
  ];
  // Matches any subdomain of the dev domain patterns.
  return (bool) preg_match('/(.+\.)?(' . implode('|', $dev_domain_patterns) . ')$/i', $url_parts['host']);
}
