<?php

/**
 * @file
 * Advanced aggregation validation module.
 */

/**
 * Test all css files used by AdvAgg.
 *
 * @return array
 *   An array of files with the result from the w3c validator.
 */
function advagg_validator_test_advagg_css($options = array()) {
  // Get list of files.
  $query_files = db_select('advagg_files', 'af')
    ->fields('af', array('filename_hash', 'filename'))
    ->condition('af.filetype', 'css')
    ->orderBy('filename', 'ASC')
    ->execute()
    ->fetchAllKeyed();
  $files = array_values($query_files);

  // Test and return list.
  return advagg_validator_test_css_files($files, $options);
}

/**
 * Recursively scan the drupal webroot and test all css files.
 *
 * @return array
 *   An array of files with the result from the w3c validator.
 */
function advagg_validator_test_all_css($options = array()) {
  // Get list of files.
  $files = advagg_validator_scan_all_dirs('css');

  // Test and return list.
  return advagg_validator_test_css_files($files, $options);
}

/**
 * Recursively scan the drupal webroot for files matching the given extension.
 *
 * @param string $ext
 *   Usually css or js.
 *
 * @return array
 *   An array of files.
 */
function advagg_validator_scan_all_dirs($ext) {
  $options = array(
    'nodirmask' => '/(\.git|.*\/files*)/',
  );
  $output = advagg_validator_file_scan_directory(DRUPAL_ROOT, '/.*\.' . $ext . '$/', $options);
  $files = array();
  foreach ($output as $values) {
    $files[] = str_replace(DRUPAL_ROOT . '/', '', $values->uri);
  }
  return $files;
}

/**
 * Test all given files with the w3c validator.
 *
 * @return array
 *   An array of files with the result from the w3c validator.
 */
function advagg_validator_test_css_files($files, $options = array()) {
  $output = array();
  foreach ($files as $filename) {
    // Skip missing files.
    if (!file_exists($filename)) {
      continue;
    }

    $file_contents = (string) @advagg_file_get_contents($filename);
    $lines = file($filename);
    $filename_hash = drupal_hash_base64($filename);
    $content_hash = drupal_hash_base64($file_contents);

    // See if this file needs to be scanned.
    $file_ok = db_select('advagg_validator', 'av')
      ->fields('av', array('content_hash', 'data'))
      ->condition('filename_hash', $filename_hash)
      ->condition('filetype', 'css')
      ->execute()
      ->fetchAllKeyed();
    if (!empty($file_ok)) {
      foreach ($file_ok as $content_hash_db => $serialized_data) {
        if ($content_hash_db == $content_hash) {
          $output[$filename] = unserialize($serialized_data);
          continue 2;
        }
      }
    }

    // Run jigsaw.w3.org validator.
    $output[$filename]['jigsaw.w3.org'] = advagg_validator_test_css_file_w3c($filename, $options);

    // Get extra context for errors.
    if (!empty($output[$filename]['jigsaw.w3.org']['errors'])) {
      foreach ($output[$filename]['jigsaw.w3.org']['errors'] as &$value) {
        if (isset($value['line'])) {
          $value['linedata'] = $lines[($value['line'] - 1)];
          if (strlen($value['linedata']) > 512) {
            unset($value['linedata']);
          }
        }
      }
      unset($value);
    }
    if (!empty($output[$filename]['jigsaw.w3.org']['warnings'])) {
      foreach ($output[$filename]['jigsaw.w3.org']['warnings'] as &$value) {
        if (isset($value['line'])) {
          $value['linedata'] = $lines[$value['line'] - 1];
          if (strlen($value['linedata']) > 512) {
            unset($value['linedata']);
          }
        }
      }
      unset($value);
    }

    // Save data.
    if (empty($output[$filename]['jigsaw.w3.org']['error'])) {
      $record = array(
        'filename' => $filename,
        'filename_hash' => $filename_hash,
        'content_hash' => $content_hash,
        'filetype' => 'css',
        'data' => serialize($output[$filename]),
      );
      db_merge('advagg_validator')
        ->key(array(
          'filename_hash' => $record['filename_hash'],
        ))
        ->fields($record)
        ->execute();
    }
  }
  return $output;
}

/**
 * Given a CSS file, test to make sure it is valid CSS.
 *
 * @param string $filename
 *   The name of the file.
 * @param array $validator_options
 *   List of options to pass along to the CSS Validator.
 *
 * @return array
 *   Info from the w3c server.
 */
function advagg_validator_test_css_file_w3c($filename, array &$validator_options = array()) {
  // Get CSS files contents.
  $validator_options['text'] = (string) @advagg_file_get_contents($filename);

  // Add in defaults.
  $validator_options += array(
    'output' => 'soap12',
    'warning' => '1',
    'profile' => 'css3',
    'usermedium' => 'all',
    'lang' => 'en',
  );

  // Build request URL.
  // API Documentation http://jigsaw.w3.org/css-validator/api.html
  $request_url = 'http://jigsaw.w3.org/css-validator/validator';
  $query = http_build_query($validator_options, '', '&');
  $url = $request_url . '?' . $query;

  // Send request.
  $result = drupal_http_request($url);
  if (!empty($result->data) && $result->code == 200) {
    // Parse XML and return info.
    $return = advagg_validator_parse_soap_response_w3c($result->data);
    $return['filename'] = $filename;
    if (isset($validator_options['text'])) {
      unset($validator_options['text']);
    }
    $return['options'] = $validator_options;
    return $return;
  }

  return array('error' => t('W3C Server did not return a 200 or request data was empty.'));
}

/**
 * Parse an XML response from the validator.
 *
 * This function parses a SOAP 1.2 response XML string from the validator.
 *
 * @param string $xml
 *   The raw SOAP 1.2 XML response from the validator.
 *
 * @return array
 *   Info from the w3c server.
 */
function advagg_validator_parse_soap_response_w3c($xml) {
  $doc = new DOMDocument();
  $response = array();

  // Try to load soap 1.2 XML response, and suppress warning reports if any.
  if (!@$doc->loadXML($xml)) {
    // Could not load the XML document.
    return $response;
  }

  // Get the standard CDATA elements.
  $cdata = array('uri', 'checkedby', 'csslevel', 'date');
  foreach ($cdata as $var) {
    $element = $doc->getElementsByTagName($var);
    if ($element->length) {
      $response[$var] = $element->item(0)->nodeValue;
    }
  }

  // Handle the element validity and get errors if not valid.
  $element = $doc->getElementsByTagName('validity');
  if ($element->length && $element->item(0)->nodeValue === 'true') {
    $response['validity'] = TRUE;
  }
  else {
    $response['validity'] = FALSE;
    $errors = $doc->getElementsByTagName('error');
    foreach ($errors as $error) {
      $response['errors'][] = advagg_validator_dom_extractor($error);
    }
  }

  // Get warnings.
  $warnings = $doc->getElementsByTagName('warning');
  foreach ($warnings as $warning) {
    $response['warnings'][] = advagg_validator_dom_extractor($warning);
  }

  // Return response array.
  return $response;
}

/**
 * Extract info from the DOMNode Object.
 *
 * @param object $dom
 *   DOMNode Class.
 *
 * @return array
 *   Key Value pair from the DOM Node.
 */
function advagg_validator_dom_extractor($dom) {
  $node = $dom->firstChild;
  $output = array();
  do {
    $text = trim($node->nodeValue);
    if (!empty($text)) {
      $key = str_replace('m:', '', $node->nodeName);
      $output[$key] = $text;
    }
  } while ($node = $node->nextSibling);
  return $output;
}

/**
 * Finds all files that match a given mask in a given directory.
 *
 * Directories and files beginning with a period are excluded; this
 * prevents hidden files and directories (such as SVN working directories)
 * from being scanned.
 *
 * @param string $dir
 *   The base directory or URI to scan, without trailing slash.
 * @param string $mask
 *   The preg_match() regular expression of the files to find.
 * @param array $options
 *   An associative array of additional options, with the following elements:
 *   - 'nomask': The preg_match() regular expression of the files to ignore.
 *     Defaults to '/(\.\.?|CVS)$/'.
 *   - 'nomask': The preg_match() regular expression of the dirs to ignore.
 *     Defaults to '/(\.git)/'.
 *   - 'callback': The callback function to call for each match. There is no
 *     default callback.
 *   - 'recurse': When TRUE, the directory scan will recurse the entire tree
 *     starting at the provided directory. Defaults to TRUE.
 *   - 'key': The key to be used for the returned associative array of files.
 *     Possible values are 'uri', for the file's URI; 'filename', for the
 *     basename of the file; and 'name' for the name of the file without the
 *     extension. Defaults to 'uri'.
 *   - 'min_depth': Minimum depth of directories to return files from. Defaults
 *     to 0.
 * @param int $depth
 *   Current depth of recursion. This parameter is only used internally and
 *   should not be passed in.
 *
 * @return array
 *   An associative array (keyed on the chosen key) of objects with 'uri',
 *   'filename', and 'name' members corresponding to the matching files.
 */
function advagg_validator_file_scan_directory($dir, $mask, array $options = array(), $depth = 0) {
  // Merge in defaults.
  $options += array(
    'nomask' => '/(\.\.?|CVS)$/',
    'nodirmask' => '/(\.git)/',
    'callback' => 0,
    'recurse' => TRUE,
    'key' => 'uri',
    'min_depth' => 0,
  );

  $options['key'] = in_array($options['key'], array('uri', 'filename', 'name')) ? $options['key'] : 'uri';
  $files = array();
  // @ignore druplart_andor_assignment
  if (is_dir($dir) && $handle = opendir($dir)) {
    while (FALSE !== ($filename = readdir($handle))) {
      if (!preg_match($options['nomask'], $filename) && $filename[0] !== '.') {
        $uri = "$dir/$filename";
        $uri = file_stream_wrapper_uri_normalize($uri);
        if (is_dir($uri) && $options['recurse'] && !preg_match($options['nodirmask'], $uri)) {
          // Give priority to files in this folder by merging them in after any
          // subdirectory files.
          $files = array_merge(advagg_validator_file_scan_directory($uri, $mask, $options, $depth + 1), $files);
        }
        elseif ($depth >= $options['min_depth'] && preg_match($mask, $filename)) {
          // Always use this match over anything already set in $files with the
          // same $$options['key'].
          $file = new stdClass();
          $file->uri = $uri;
          $file->filename = $filename;
          $file->name = pathinfo($filename, PATHINFO_FILENAME);
          $key = $options['key'];
          $files[$file->$key] = $file;
          if ($options['callback']) {
            $options['callback']($uri);
          }
        }
      }
    }

    closedir($handle);
  }

  return $files;
}
