--- /dev/null
+<?php
+
+/**
+ * This script checks the constants defined in the curl PHP extension, against those documented on the cURL website:
+ * https://curl.haxx.se/libcurl/c/symbols-in-versions.html
+ *
+ * See the discussion at: https://github.com/php/php-src/pull/2961
+ */
+
+const CURL_DOC_FILE = 'https://curl.haxx.se/libcurl/c/symbols-in-versions.html';
+
+const SOURCE_FILE = __DIR__ . '/interface.c';
+
+const MIN_SUPPORTED_CURL_VERSION = '7.12.1';
+
+const IGNORED_CONSTANTS = [
+ 'CURLOPT_PROGRESSDATA'
+];
+
+$curlConstants = getCurlConstants();
+$sourceConstants = getSourceConstants();
+
+$notInPHP = []; // In cURL doc, but missing from PHP
+$notInCurl = []; // In the PHP source, but not in the cURL doc
+$outdated = []; // In the PHP source, but removed before the minimum supported cURL version
+
+foreach ($curlConstants as $name => [$introduced, $deprecated, $removed]) {
+ $inPHP = in_array($name, $sourceConstants);
+
+ if ($removed !== null) {
+ if (version_compare($removed, MIN_SUPPORTED_CURL_VERSION) < 0) {
+ // constant removed before the minimum supported version
+ continue;
+ }
+ }
+
+ if (! $inPHP) {
+ $notInPHP[$name] = [$introduced, $removed];
+ }
+}
+
+foreach ($sourceConstants as $name) {
+ if (! isset($curlConstants[$name])) {
+ $notInCurl[] = $name;
+ continue;
+ }
+
+ $removed = $curlConstants[$name][2];
+
+ if ($removed === null) {
+ continue;
+ }
+
+ if (version_compare($removed, MIN_SUPPORTED_CURL_VERSION) < 0) {
+ // constant removed before the minimum supported version
+ $outdated[$name] = $removed;
+ }
+}
+
+$allGood = true;
+
+if ($notInPHP) {
+ uasort($notInPHP, function($a, $b) {
+ return version_compare($a[0], $b[0]);
+ });
+
+ $table = new AsciiTable();
+ $table->add('Constant', 'Introduced', '', 'Removed', '');
+
+ foreach ($notInPHP as $name => [$introduced, $removed]) {
+ if ($removed === null) {
+ $removed = '';
+ $removedHex = '';
+ } else {
+ $removedHex = getHexVersion($removed);
+ }
+
+ $table->add($name, $introduced, getHexVersion($introduced), $removed, $removedHex);
+ }
+
+ echo "Constants missing from the PHP source:\n\n";
+ echo $table, "\n";
+
+ $allGood = false;
+}
+
+if ($notInCurl) {
+ $table = new AsciiTable();
+
+ foreach ($notInCurl as $name) {
+ $table->add($name);
+ }
+
+ echo "Constants defined in the PHP source, but absent from the cURL documentation:\n\n";
+ echo $table, "\n";
+
+ $allGood = false;
+}
+
+if ($outdated) {
+ uasort($outdated, function($a, $b) {
+ return version_compare($a, $b);
+ });
+
+ $table = new AsciiTable();
+ $table->add('Constant', 'Removed');
+
+ foreach ($outdated as $name => $version) {
+ $table->add($name, $version);
+ }
+
+ echo "Constants defined in the PHP source, but removed before the minimum supported cURL version:\n\n";
+ echo $table, "\n";
+
+ $allGood = false;
+}
+
+if ($allGood) {
+ echo "All good! Source code and cURL documentation are in sync.\n";
+}
+
+/**
+ * Loads and parses the cURL constants from the online documentation.
+ *
+ * The result is an associative array where the key is the constant name, and the value is a numeric array with:
+ * - the introduced version
+ * - the deprecated version (nullable)
+ * - the removed version (nullable)
+ *
+ * @return array
+ */
+function getCurlConstants() : array
+{
+ $html = file_get_contents(CURL_DOC_FILE);
+
+ // Extract the constant list from the HTML file (located in the only <pre> tag in the page)
+ preg_match('~<pre>([^<]+)</pre>~', $html, $matches);
+ $constantList = $matches[1];
+
+ /**
+ * Parse the cURL constant lines. Possible formats:
+ *
+ * Name Introduced Deprecated Removed
+ * CURLOPT_CRLFILE 7.19.0
+ * CURLOPT_DNS_USE_GLOBAL_CACHE 7.9.3 7.11.1
+ * CURLOPT_FTPASCII 7.1 7.11.1 7.15.5
+ * CURLOPT_HTTPREQUEST 7.1 - 7.15.5
+ */
+ $regexp = '/^([A-Za-z0-9_]+) +([0-9\.]+)(?: +([0-9\.\-]+))?(?: +([0-9\.]+))?/m';
+ preg_match_all($regexp, $constantList, $matches, PREG_SET_ORDER);
+
+ $constants = [];
+
+ foreach ($matches as $match) {
+ $name = $match[1];
+ $introduced = $match[2];
+ $deprecated = $match[3] ?? null;
+ $removed = $match[4] ?? null;
+
+ if (strpos($name, 'CURLOPT_') !== 0) {
+ // not a CURLOPT_* constant
+ continue;
+ }
+
+ if (in_array($name, IGNORED_CONSTANTS)) {
+ // purposefully ignored constant
+ continue;
+ }
+
+ if ($deprecated === '-') { // deprecated version can be a hyphen
+ $deprecated = null;
+ }
+
+ $constants[$name] = [$introduced, $deprecated, $removed];
+ }
+
+ return $constants;
+}
+
+/**
+ * Parses the defined cURL constants from the PHP extension source code.
+ *
+ * The result is a numeric array whose values are the constant names.
+ *
+ * @return array
+ */
+function getSourceConstants() : array
+{
+ $source = file_get_contents(SOURCE_FILE);
+
+ preg_match_all('/REGISTER_CURL_CONSTANT\(([A-Za-z0-9_]+)\)/', $source, $matches);
+
+ $constants = [];
+
+ foreach ($matches[1] as $name) {
+ if ($name === '__c') { // macro
+ continue;
+ }
+
+ if (strpos($name, 'CURLOPT_') !== 0) {
+ // not a CURLOPT_* constant
+ continue;
+ }
+
+ $constants[] = $name;
+ }
+
+ return $constants;
+}
+
+/**
+ * Converts a version number to its hex representation as used in the extension source code.
+ *
+ * Example: 7.25.1 => 0x071901
+ *
+ * @param string $version
+ *
+ * @return string
+ *
+ * @throws \RuntimeException
+ */
+function getHexVersion(string $version) : string
+{
+ $parts = explode('.', $version);
+
+ if (count($parts) === 2) {
+ $parts[] = '0';
+ }
+
+ if (count($parts) !== 3) {
+ throw new \RuntimeException('Invalid version number: ' . $version);
+ }
+
+ $hex = '0x';
+
+ foreach ($parts as $value) {
+ if (! ctype_digit($value) || strlen($value) > 3) {
+ throw new \RuntimeException('Invalid version number: ' . $version);
+ }
+
+ $value = (int) $value;
+
+ if ($value > 255) {
+ throw new \RuntimeException('Invalid version number: ' . $version);
+ }
+
+ $value = dechex($value);
+
+ if (strlen($value) === 1) {
+ $value = '0' . $value;
+ }
+
+ $hex .= $value;
+ }
+
+ return $hex;
+}
+
+/**
+ * A simple helper to create ASCII tables.
+ * It assumes that the same number of columns is always given to add().
+ */
+class AsciiTable
+{
+ /**
+ * @var array
+ */
+ private $values = [];
+
+ /**
+ * @var array
+ */
+ private $length = [];
+
+ /**
+ * @var int
+ */
+ private $padding = 4;
+
+ /**
+ * @param string[] $values
+ *
+ * @return void
+ */
+ public function add(string ...$values) : void
+ {
+ $this->values[] = $values;
+
+ foreach ($values as $key => $value) {
+ $length = strlen($value);
+
+ if (isset($this->length[$key])) {
+ $this->length[$key] = max($this->length[$key], $length);
+ } else {
+ $this->length[$key] = $length;
+ }
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString() : string
+ {
+ $result = '';
+
+ foreach ($this->values as $values) {
+ foreach ($values as $key => $value) {
+ if ($key !== 0) {
+ $result .= str_repeat(' ', $this->padding);
+ }
+
+ $result .= str_pad($value, $this->length[$key]);
+ }
+
+ $result .= "\n";
+ }
+
+ return $result;
+ }
+}