LGV_TZ_Lookup 1.4.2
Time zone lookup for PHP applications and HTTP clients
Loading...
Searching...
No Matches
Setup.php
Go to the documentation of this file.
1<?php
2/***************************************************************************************************************************/
3/**
4 © Copyright 2023-2026, [Little Green Viper Software Development LLC](https://littlegreenviper.com)
5
6 LICENSE:
7
8 MIT License
9
10 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
11 files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
12 modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
16
17 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
20 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22 [Little Green Viper Software Development LLC](https://littlegreenviper.com)
23*/
24/***************************************************************************************************************************/
25/**
26 \file
27 \brief These are shared Composer, download, and extraction helpers for the demo and installer.
28
29 Downloads require HTTPS and stream to disk. Composer is checked against its official SHA-256 checksum,
30 and runs with scripts and plugins disabled. ZIP extraction writes only the expected GeoJSON file.
31*/
32
33declare(strict_types=1);
34
35/***************************************************************************************************************************/
36/** \brief This prepares a Composer application and its boundary data. */
38 const PACKAGE = 'littlegreenviper/lgv_tz_lookup'; ///< The package installed by both command-line tools.
39
40 /***********************************************************************************************************************/
41 /** \brief Discover the latest full boundary release with oceans. \throws RuntimeException if metadata is incomplete. */
42 public static function latestBoundaries(string $directory, ?callable $download = null): array {
43 $download = $download ?? [self::class, 'download'];
44 $download('https://api.github.com/repos/evansiroky/timezone-boundary-builder/releases/latest', $directory.'/release.json');
45 $release = json_decode(file_get_contents($directory.'/release.json'), true, 512, JSON_THROW_ON_ERROR);
46 if (!is_array($release) || !is_string($release['tag_name'] ?? null) || trim($release['tag_name']) === '') {
47 throw new RuntimeException('The latest boundary release has no version tag.');
48 }
49 foreach ($release['assets'] ?? [] as $asset) {
50 if (($asset['name'] ?? '') === 'timezones-with-oceans.geojson.zip' &&
51 is_string($asset['browser_download_url'] ?? null) &&
52 str_starts_with($asset['browser_download_url'], 'https://')) {
53 return ['version' => $release['tag_name'], 'asset' => $asset];
54 }
55 }
56 throw new RuntimeException('The latest release has no full oceans GeoJSON archive.');
57 }
58
59 /***********************************************************************************************************************/
60 /** \brief Verify the archive against the release's size and optional SHA-256 digest. */
61 public static function verifyBoundaries(string $archive, array $asset): string {
62 $size = filesize($archive);
63 if ($size === false || $size === 0 || (isset($asset['size']) && $size !== $asset['size'])) {
64 throw new RuntimeException('The boundary archive is empty or incomplete.');
65 }
66 $checksum = hash_file('sha256', $archive);
67 if (!empty($asset['digest']) && str_starts_with($asset['digest'], 'sha256:') &&
68 !hash_equals(substr($asset['digest'], 7), $checksum)) {
69 throw new RuntimeException('The boundary archive checksum differs from release metadata.');
70 }
71 return $checksum;
72 }
73
74 /***********************************************************************************************************************/
75 /** \brief Download an HTTPS resource to a local file. \throws RuntimeException if preparation fails. */
76 public static function download(string $url, ///< The HTTPS resource to download.
77 string $destination ///< The local filename to create in the demo's working directory.
78 ): void {
79 $output = fopen($destination, 'wb');
80 if (false === $output) {
81 throw new RuntimeException('Could not write '.$destination);
82 }
83 $request = curl_init($url);
84 try {
85 curl_setopt_array($request, [
86 CURLOPT_FILE => $output,
87 CURLOPT_FOLLOWLOCATION => true,
88 CURLOPT_MAXREDIRS => 5,
89 CURLOPT_CONNECTTIMEOUT => 30,
90 CURLOPT_TIMEOUT => 1800,
91 CURLOPT_FAILONERROR => true,
92 CURLOPT_USERAGENT => 'LGV-TZ-Lookup-Setup',
93 CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
94 CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
95 ]);
96 if (!curl_exec($request)) {
97 throw new RuntimeException('Download failed: '.curl_error($request).' ('.$url.')');
98 }
99 } finally {
100 fclose($output);
101 unset($request);
102 }
103 }
104
105 /***********************************************************************************************************************/
106 /** \brief Extract exactly one GeoJSON entry without using archive paths. \throws RuntimeException if preparation fails. */
107 public static function extract(string $archive, ///< The downloaded boundary ZIP file.
108 string $destination ///< The local GeoJSON filename to create.
109 ): void {
110 $zip = new ZipArchive();
111 if (true !== $zip->open($archive)) {
112 throw new RuntimeException('Could not open the boundary archive.');
113 }
114 try {
115 $entries = [];
116 for ($i = 0; $i < $zip->numFiles; ++$i) {
117 $name = $zip->getNameIndex($i);
118 if (preg_match('/\.(?:json|geojson)$/i', $name)) {
119 $entries[] = $name;
120 }
121 }
122 if (count($entries) !== 1) {
123 throw new RuntimeException('Expected one GeoJSON file in the boundary archive.');
124 }
125 // Stream into our own filename instead of extracting arbitrary archive paths or reading the file into memory.
126 $input = $zip->getStream($entries[0]);
127 $output = fopen($destination, 'wb');
128 if (false === $input || false === $output) {
129 if (is_resource($input)) { fclose($input); }
130 if (is_resource($output)) { fclose($output); }
131 throw new RuntimeException('Could not extract the boundary file.');
132 }
133 try {
134 $bytes = stream_copy_to_stream($input, $output);
135 $entry = $zip->statName($entries[0]);
136 if (false === $bytes || $bytes === 0 || $bytes !== $entry['size']) {
137 throw new RuntimeException('The extracted boundary file is empty or incomplete.');
138 }
139 } finally {
140 fclose($input);
141 fclose($output);
142 }
143 } finally {
144 $zip->close();
145 }
146 }
147
148 /***********************************************************************************************************************/
149 /** \brief Install the library and optional loader, then require its Composer autoloader. \throws RuntimeException if preparation fails. */
150 public static function install(string $directory, string $driver, string $packagePath = '', string $version = '^1.4'): void {
151 echo "Installing a Composer application...\n";
152 $composer = $directory.'/composer.phar';
153 self::download('https://getcomposer.org/download/latest-stable/composer.phar', $composer);
154 self::download('https://getcomposer.org/download/latest-stable/composer.phar.sha256sum', $directory.'/composer.sha256');
155 $checksum = preg_split('/\s+/', trim(file_get_contents($directory.'/composer.sha256')))[0];
156 if (!hash_equals($checksum, hash_file('sha256', $composer))) {
157 throw new RuntimeException('Composer download checksum did not match.');
158 }
159 $repository = ['type' => 'vcs', 'url' => 'https://github.com/LittleGreenViper/LGV_TZ_Lookup.git'];
160 if ($packagePath !== '') {
161 $packagePath = realpath($packagePath);
162 if (false === $packagePath || !is_file($packagePath.'/composer.json')) {
163 throw new RuntimeException('The package path must point to a checkout containing composer.json.');
164 }
165 $version = 'dev-local';
166 $repository = ['type' => 'path', 'url' => $packagePath,
167 'options' => ['symlink' => false, 'versions' => [self::PACKAGE => $version]]];
168 }
169 file_put_contents($directory.'/composer.json', json_encode([
170 'name' => 'littlegreenviper/tz-lookup-application',
171 'repositories' => [$repository],
172 'require' => [self::PACKAGE => $version, 'ext-pdo_'.$driver => '*', 'salsify/json-streaming-parser' => '8.3.*'],
173 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR).PHP_EOL);
174 putenv('COMPOSER_HOME='.$directory.'/composer-home');
175 putenv('COMPOSER_CACHE_DIR='.$directory.'/composer-cache');
176 putenv('COMPOSER_ROOT_VERSION=1.0.0');
177 $process = proc_open([
178 PHP_BINARY, $composer, 'install', '--working-dir='.$directory, '--no-dev', '--no-interaction',
179 '--no-scripts', '--no-plugins', '--prefer-dist', '--no-progress', '--no-ansi', '--quiet',
180 ], [0 => ['file', '/dev/null', 'r'], 1 => STDOUT, 2 => STDERR], $pipes);
181 if (false === $process) {
182 throw new RuntimeException('Could not start Composer.');
183 }
184 try {
185 $status = proc_close($process);
186 $process = null;
187 if ($status !== 0) {
188 throw new RuntimeException('Composer installation failed (exit '.$status.').');
189 }
190 } finally {
191 if (is_resource($process)) {
192 proc_terminate($process);
193 proc_close($process);
194 }
195 }
196 require $directory.'/vendor/autoload.php';
197 echo 'Installed '.self::PACKAGE.' '.Composer\InstalledVersions::getPrettyVersion(self::PACKAGE).".\n";
198 }
199}
This prepares a Composer application and its boundary data.
Definition Setup.php:37
static install(string $directory, string $driver, string $packagePath='', string $version='^1.4')
Install the library and optional loader, then require its Composer autoloader.
Definition Setup.php:150
static extract(string $archive, string $destination)
Extract exactly one GeoJSON entry without using archive paths.
Definition Setup.php:107
static latestBoundaries(string $directory, ?callable $download=null)
Discover the latest full boundary release with oceans.
Definition Setup.php:42
static verifyBoundaries(string $archive, array $asset)
Verify the archive against the release's size and optional SHA-256 digest.
Definition Setup.php:61
static download(string $url, string $destination)
Download an HTTPS resource to a local file.
Definition Setup.php:76
const PACKAGE
The package installed by both command-line tools.
Definition Setup.php:38
$driver
Definition databases.php:38
if(!defined( '__CONFIG_FILE_'))
Definition index.php:51
$directory
Definition demo.php:107
$process
Definition demo.php:113