29declare(strict_types=1);
30require_once __DIR__.
'/Setup.php';
35 if (!is_file(
$config) || !is_readable(
$config)) {
throw new RuntimeException(
'The private configuration file is not readable.'); }
37 if (!isset($g_dbName, $g_dbUserName, $g_dbPassword) || !is_string($g_dbName) || $g_dbName ===
'') {
38 throw new RuntimeException(
'The configuration must define g_dbName, g_dbUserName, and g_dbPassword.');
40 $driver = strtolower($g_dbType ??
'mysql');
41 if (!in_array(
$driver, [
'mysql',
'pgsql'],
true)) {
throw new RuntimeException(
'The database driver must be mysql or pgsql.'); }
42 return [
'database' => $g_dbName,
'user' => $g_dbUserName,
'password' => $g_dbPassword,
43 'driver' =>
$driver,
'host' => $g_dbHost ??
'127.0.0.1',
'port' => $g_dbPort ?? (
$driver ===
'pgsql' ? 5432 : 3306)];
51 $dsn .=
$driver ===
'mysql' ?
';charset=utf8mb4' :
';options=--client_encoding=UTF8';
53 $sql =
$driver ===
'pgsql' ?
"SELECT pg_try_advisory_lock(hashtext('LGV_TZ_Lookup.install'))"
54 :
"SELECT GET_LOCK(CONCAT('lgv_tz_', SHA2(DATABASE(), 224)), 0)";
55 if (!
$connection->query($sql)->fetchColumn()) {
throw new RuntimeException(
'Another timezone installation or update is using this database.'); }
62 foreach ([
'boundaries.zip',
'boundaries.json',
'release.json'] as $file) {
63 if (is_file(
$work.
'/'.$file) && !unlink(
$work.
'/'.$file)) { throw new RuntimeException(
'Could not remove downloaded '.$file); }
65 if (!is_file(
$work.
'/update.json')) { return; }
66 $state = json_decode(file_get_contents(
$work.
'/update.json'),
true, 512, JSON_THROW_ON_ERROR);
67 if (!is_string($state[
'table'] ??
null) || !preg_match(
'/^lgv_tz_update_[a-f0-9]{16}$/D', $state[
'table'])) {
68 throw new RuntimeException(
'Invalid staging table record; refusing database cleanup.');
71 $connection->exec(
'DROP TABLE IF EXISTS '.$state[
'table']);
72 $connection->exec(
'DROP TABLE IF EXISTS lgv_tz_old_'.substr($state[
'table'], -16));
73 if (!unlink(
$work.
'/update.json')) {
throw new RuntimeException(
'Could not remove staging recovery information.'); }
79 if (!preg_match(
'/^lgv_tz_update_[a-f0-9]{16}$/D', $table)) {
throw new InvalidArgumentException(
'Invalid staging table name.'); }
82 public function __construct(array
$settings,
string $table) {
85 $this->_load_table = $table;
88 $stream = fopen($json,
'rb');
89 if ($stream ===
false) {
throw new RuntimeException(
'Could not open the downloaded boundary file.'); }
92 public bool $complete =
false;
94 public function endDocument(): void { parent::endDocument(); $this->complete =
true; }
96 (
new JsonStreamingParser\Parser($stream, $listener))->parse();
97 if (!$listener->complete) {
throw new RuntimeException(
'The boundary JSON is incomplete; existing boundaries were retained.'); }
99 $count = (int)
$connection->query(
'SELECT COUNT(*) FROM '.$table)->fetchColumn();
100 if ($count === 0) {
throw new RuntimeException(
'No polygons were loaded; existing boundaries were retained.'); }
102 $old =
'lgv_tz_old_'.substr($table, -16);
106 $connection->exec(
'ALTER TABLE timezones RENAME TO '.$old);
107 $connection->exec(
'ALTER TABLE '.$table.
' RENAME TO timezones');
110 }
catch (Throwable
$error) {
115 $connection->exec(
'RENAME TABLE timezones TO '.$old.
', '.$table.
' TO timezones');
118 catch (Throwable
$error) { fwrite(STDERR,
'Updated boundaries, but could not remove old table '.$old.
'.'.PHP_EOL); }
126 if (!extension_loaded(
'curl')) {
throw new RuntimeException(
'The PHP curl extension is required.'); }
127 $download = $download ?? [LGV_TZ_Lookup_Setup::class,
'download'];
129 echo
'Latest boundary release: '.$release[
'version'].PHP_EOL;
130 if ($check) {
return; }
131 foreach ([
'zip',
'pdo',
'mbstring',
'ctype'] as $extension) {
132 if (!extension_loaded($extension)) {
throw new RuntimeException(
'The PHP '.$extension.
' extension is required.'); }
135 if (
$config ===
false) {
throw new RuntimeException(
'The private configuration file was not found.'); }
138 foreach ([dirname(
$config).
'/app/vendor/autoload.php', dirname(
$config).
'/vendor/autoload.php',
139 dirname(__DIR__).
'/vendor/autoload.php'] as
$autoload) {
142 if (!class_exists(
'LGV_TZ_Lookup_Database')) {
143 require_once dirname(__DIR__).
'/src/Sources/LGV_TZ_Lookup_Database.class.php';
145 if (!class_exists(
'LGV_TZ_Lookup_Loader')) {
146 require_once dirname(__DIR__).
'/src/Sources/LGV_TZ_Lookup_Loader.class.php';
149 if (!property_exists(LGV_TZ_Lookup_Database::class,
'_load_table')) {
150 throw new RuntimeException(
'Update the installed Composer package to 1.4.1 or later before running this updater.');
153 $connection->query(
'SELECT id, tzname, east, west, north, south, polygon FROM timezones LIMIT 0')->closeCursor();
154 echo
"Downloading full timezone boundaries with oceans...\n";
155 $download($release[
'asset'][
'browser_download_url'],
$work.
'/boundaries.zip');
158 $table =
'lgv_tz_update_'.bin2hex(random_bytes(8));
159 if (file_put_contents(
$work.
'/update.json', json_encode([
'config' =>
$config,
'table' => $table], JSON_THROW_ON_ERROR)) ===
false ||
160 !chmod(
$work.
'/update.json', 0600)) {
throw new RuntimeException(
'Could not record staging recovery information.'); }
161 echo
'Loading boundary release '.$release[
'version'].
"...\n";
164 echo
'Updated to boundary release '.$release[
'version'].
'; loaded '.$count.
" polygons.\n";
168 unlink(
$work.
'/update.json');
172if (realpath($_SERVER[
'SCRIPT_FILENAME'] ??
'') !== __FILE__) {
return; }
175if (PHP_SAPI !==
'cli' || $argc < 3 || !is_dir(
$work)) {
176 fwrite(STDERR,
"Run ./update.sh [--check] [CONFIG_FILE].\n"); exit(2);
178if (function_exists(
'pcntl_async_signals')) {
179 pcntl_async_signals(
true);
180 foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
181 pcntl_signal($signal,
static function(
int $signal):
void {
throw new RuntimeException(
'Update interrupted by signal '.$signal.
'.'); });
187}
catch (Throwable
$error) {
188 fwrite(STDERR,
'Update error: '.
$error->getMessage().PHP_EOL); exit(1);
static extract(string $archive, string $destination)
Extract exactly one GeoJSON entry without using archive paths.
static latestBoundaries(string $directory, ?callable $download=null)
Discover the latest full boundary release with oceans.
static verifyBoundaries(string $archive, array $asset)
Verify the archive against the release's size and optional SHA-256 digest.
if(!defined( '__CONFIG_FILE_'))