35declare(strict_types=1);
36require_once __DIR__.
'/Setup.php';
48 $file =
$work.
'/deployment.json';
49 if (
false === file_put_contents($file.
'.new', json_encode($state, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)) ||
50 !chmod($file.
'.new', 0600) || !rename($file.
'.new', $file)) {
51 throw new RuntimeException(
'Could not record installation recovery information.');
60 if (!$administrative ||
$driver ===
'pgsql') {
63 $dsn .=
$driver ===
'mysql' ?
';charset=utf8mb4' :
';options=--client_encoding=UTF8';
64 return new PDO(
$dsn,
$settings[
'user'],
$settings[
'password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
70 $sql =
$connection->getAttribute(PDO::ATTR_DRIVER_NAME) ===
'pgsql'
71 ?
"SELECT pg_try_advisory_lock(hashtext('LGV_TZ_Lookup.install'))"
72 :
"SELECT GET_LOCK(CONCAT('lgv_tz_', SHA2(DATABASE(), 224)), 0)";
74 throw new RuntimeException(
'Another timezone installation is using this database.');
81 $schema =
$connection->getAttribute(PDO::ATTR_DRIVER_NAME) ===
'pgsql' ?
'current_schema()' :
'DATABASE()';
82 $statement =
$connection->prepare(
'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '.$schema.
' AND table_name = ?');
83 $statement->execute([$table]);
84 return (
int)$statement->fetchColumn() > 0;
92function deployAsk($terminal,
string $label,
string $default =
'',
bool $hidden =
false): string {
93 fwrite(STDOUT, $label.($default ===
'' ?
'' :
' ['.$default.
']').
': ');
95 if ($hidden && stream_isatty($terminal)) {
96 $process = proc_open([
'stty',
'-g'], [0 => $terminal, 1 => [
'pipe',
'w'], 2 => STDERR], $pipes);
97 if (
false ===
$process) {
throw new RuntimeException(
'Could not read terminal settings.'); }
98 $mode = trim(stream_get_contents($pipes[1]));
100 if (proc_close(
$process) !== 0 || $mode ===
'') {
throw new RuntimeException(
'Could not read terminal settings.'); }
101 $process = proc_open([
'stty',
'-echo'], [0 => $terminal, 1 => STDOUT, 2 => STDERR], $pipes);
102 if (
false ===
$process || proc_close(
$process) !== 0) {
throw new RuntimeException(
'Could not hide password input.'); }
105 if (stream_isatty($terminal)) {
109 $read = [$terminal]; $write = $except =
null;
110 $ready = @stream_select($read, $write, $except, 0, 250000);
111 }
while ($ready ===
false || $ready === 0);
113 $answer = fgets($terminal);
114 if (
false === $answer) {
throw new RuntimeException(
'Installation input ended before setup was complete.'); }
115 $answer = $hidden ? rtrim($answer,
"\r\n") : trim($answer);
116 return $answer ===
'' ? $default : $answer;
118 if ($mode !==
null) {
119 $process = proc_open([
'stty', $mode], [0 => $terminal, 1 => STDOUT, 2 => STDERR], $pipes);
121 fwrite(STDOUT,
"\n");
129 $webUrl = trim($webUrl);
130 $parts = parse_url($webUrl);
131 if (filter_var($webUrl, FILTER_VALIDATE_URL) ===
false || !is_array($parts) ||
132 !in_array(strtolower($parts[
'scheme'] ??
''), [
'http',
'https'],
true) ||
133 isset($parts[
'user']) || isset($parts[
'pass']) || isset($parts[
'query']) || isset($parts[
'fragment']) ||
134 str_contains($webUrl,
'\\')) {
135 throw new InvalidArgumentException(
'Enter the full http:// or https:// URL of the web directory, without credentials, a query, or a fragment.');
137 if (!preg_match(
'/^[a-zA-Z0-9_-]+$/D', $endpoint)) {
138 throw new InvalidArgumentException(
'Use a simple service directory name.');
140 return rtrim($webUrl,
'/').
'/'.$endpoint.
'/';
147 $files =
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$directory, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
148 foreach ($files as $file) {
149 $ok = $file->isDir() && !$file->isLink() ? rmdir($file->getPathname()) : unlink($file->getPathname());
150 if (!$ok) {
throw new RuntimeException(
'Could not remove '.$file->getPathname()); }
158 clearstatcache(true, $path);
159 return is_dir($path) && !is_link($path) && !(
new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS))->valid();
165 clearstatcache(true, $path);
166 if (!is_dir($path) || is_link($path)) {
return false; }
167 $stat = lstat($path);
168 return $stat !==
false &&
169 $stat[
'dev'] === $original[
'device'] && $stat[
'ino'] === $original[
'inode'];
176 foreach ($destinations as $key => $path) {
177 if (!file_exists($path) && !is_link($path)) {
continue; }
178 if (is_link($path) || !is_dir($path)) {
179 throw new RuntimeException(
'Installation destination must be a new or empty directory, not a file or link: '.$path);
182 $stat = lstat($path);
183 $empty[$key] = [
'device' => $stat[
'dev'],
'inode' => $stat[
'ino'],
184 'backup' => dirname($path).
'/.lgv-tz-empty-'.bin2hex(random_bytes(12))];
193 foreach ([
'private',
'public'] as $key) {
194 $path = $state[$key];
195 $original = $state[
'empty_directories'][$key] ??
null;
196 if ($original !==
null) {
198 throw new RuntimeException(
'The selected empty directory changed during installation; refusing to replace '.$path);
200 if (file_exists($original[
'backup']) || is_link($original[
'backup']) || !rename($path, $original[
'backup'])) {
201 throw new RuntimeException(
'Could not retain the original empty directory for recovery: '.$path);
203 } elseif (file_exists($path) || is_link($path)) {
204 throw new RuntimeException(
'Installation destination appeared during setup; refusing to replace '.$path);
206 if (!rename($state[$key.
'_stage'], $path)) {
207 throw new RuntimeException(
'Could not publish the installation directories.');
215 foreach ($state[
'empty_directories'] ?? [] as $key => $original) {
216 $backup = $original[
'backup'];
217 if (!file_exists($backup) && !is_link($backup)) {
continue; }
219 throw new RuntimeException(
'Original directory identity differs; refusing recovery for '.$backup);
221 if (!empty($state[
'published'])) {
223 throw new RuntimeException(
'Could not remove the original empty-directory backup: '.$backup);
226 if (file_exists($state[$key]) || is_link($state[$key]) || !rename($backup, $state[$key])) {
227 throw new RuntimeException(
'Could not restore the original empty directory: '.$state[$key]);
238 is_link(
$directory.
'/.lgv-tz-install-owner')) {
239 throw new RuntimeException(
'Existing directories are not a matching installer-owned service; choose new paths.');
242 $token = trim(file_get_contents($private.
'/.lgv-tz-install-owner'));
243 if (!preg_match(
'/^[a-f0-9]{64}$/D', $token) || !hash_equals($token, trim(file_get_contents($public.
'/.lgv-tz-install-owner')))) {
244 throw new RuntimeException(
'Installation directory ownership differs; refusing to refresh.');
248 throw new RuntimeException(
'Existing installation contains an unsupported linked directory; refusing to refresh.');
251 if (!is_file($public.
'/index.php') || is_link($public.
'/index.php')) {
252 throw new RuntimeException(
'Existing public entry point is missing or linked; refusing to refresh.');
254 foreach ([
'config.php',
'installation.json',
'app/vendor/autoload.php',
'update.sh',
'tools/update.php',
'tools/Setup.php'] as $file) {
255 if (!is_file($private.
'/'.$file) || is_link($private.
'/'.$file)) {
256 throw new RuntimeException(
'Existing installation is incomplete; refusing to refresh.');
259 $configuration = (
static function(
string $file): array {
261 return [
'settings' => [
'database' => $g_dbName ??
'',
'user' => $g_dbUserName ??
'',
'password' => $g_dbPassword ??
'',
262 'driver' => strtolower($g_dbType ??
'mysql'),
'host' => $g_dbHost ??
'127.0.0.1',
263 'port' => (int)($g_dbPort ?? (($g_dbType ??
'mysql') ===
'pgsql' ? 5432 : 3306))],
264 'secret' => $g_server_secret ??
''];
265 })($private.
'/config.php');
266 foreach ([
'database',
'user',
'password',
'driver',
'host',
'port'] as $key) {
267 if (
$settings[$key] !== $configuration[
'settings'][$key]) {
268 throw new RuntimeException(
'Database settings differ from the existing private configuration; use the original settings to refresh.');
271 $metadata = json_decode(file_get_contents($private.
'/installation.json'),
true, 512, JSON_THROW_ON_ERROR);
272 if (!is_array($metadata) || ($metadata[
'driver'] ??
'') !==
$settings[
'driver'] || !is_string($configuration[
'secret'])) {
273 throw new RuntimeException(
'Existing installation metadata is invalid; refusing to refresh.');
275 return [
'token' => $token,
'metadata' => $metadata,
'secret' => $configuration[
'secret']];
281 $mode = fileperms(
$source) & 0777;
282 if (!mkdir($destination, $mode) || !chmod($destination, $mode)) {
throw new RuntimeException(
'Could not stage the installation.'); }
283 foreach (
new DirectoryIterator(
$source) as $entry) {
284 if ($entry->isDot()) {
continue; }
285 $target = $destination.
'/'.$entry->getFilename();
286 if ($entry->isLink()) { $ok = symlink(readlink($entry->getPathname()), $target); }
287 elseif ($entry->isDir()) {
deployCopyTree($entry->getPathname(), $target);
continue; }
288 else { $ok = copy($entry->getPathname(), $target) && chmod($target, $entry->getPerms() & 0777); }
289 if (!$ok) {
throw new RuntimeException(
'Could not stage '.$entry->getFilename()); }
295function deployEndpointCode(
string $private,
string $relativePackage,
string $licenseHeader): string {
296 return $licenseHeader.
297 "define('__CONFIG_FILE_', ".var_export($private.
'/config.php', true).
");\n".
298 "ini_set('display_errors', '0');\ntry {\n require ".var_export($private.
'/app/vendor/autoload.php', true).
";\n".
299 ' require '.var_export($private.$relativePackage.
'/src/index.php', true).
";\n} catch (Throwable \$error) {\n".
300 " http_response_code(503);\n error_log('Timezone service: '.\$error->getMessage());\n echo 'Timezone service unavailable.';\n}\n";
306 $verify = static function(string $path, string $token): void {
307 if (is_link($path) || !is_dir($path) || !is_file($path.
'/.lgv-tz-install-owner') || is_link($path.
'/.lgv-tz-install-owner') ||
308 !hash_equals($token, trim(file_get_contents($path.
'/.lgv-tz-install-owner')))) {
309 throw new RuntimeException(
'Refresh directory ownership differs; refusing recovery for '.$path);
312 foreach ([
'private',
'public'] as $key) {
313 $backup = $state[$key.
'_backup'];
314 if (!file_exists($backup) && !is_link($backup)) {
continue; }
315 $verify($backup, $state[
'previous_token']);
316 if (empty($state[
'published'])) {
317 if (file_exists($state[$key]) || is_link($state[$key])) {
318 $verify($state[$key], $state[
'token']);
321 if (!rename($backup, $state[$key])) {
throw new RuntimeException(
'Could not restore the prior installation.'); }
323 $verify($state[$key], $state[
'token']);
327 foreach ([
'private_stage',
'public_stage'] as $key) {
328 if (!file_exists($state[$key]) && !is_link($state[$key])) {
continue; }
329 $verify($state[$key], $state[
'token']);
332 unlink(
$work.
'/deployment.json');
338 string $serviceUrl, array $existing, callable $install): void {
342 throw new RuntimeException(
'An installation recovery table remains; finish its cleanup before refreshing.');
344 $count = (int)
$connection->query(
'SELECT COUNT(*) FROM timezones')->fetchColumn();
345 if ($count === 0) {
throw new RuntimeException(
'The existing boundary table is empty; refusing to refresh.'); }
346 $suffix = bin2hex(random_bytes(12));
347 $state = [
'refresh' =>
true,
'token' => bin2hex(random_bytes(32)),
'previous_token' => $existing[
'token'],
348 'private' => $private,
'public' => $public,
'private_stage' => dirname($private).
'/.lgv-tz-install-'.$suffix,
349 'public_stage' => dirname($public).
'/.lgv-tz-install-'.$suffix,
350 'private_backup' => dirname($private).
'/.lgv-tz-previous-'.$suffix,
351 'public_backup' => dirname($public).
'/.lgv-tz-previous-'.$suffix,
'published' =>
false];
353 foreach ([
'private',
'public'] as $key) {
355 if (!mkdir($state[$key.
'_stage'], 0700) ||
356 file_put_contents($state[$key.
'_stage'].
'/.lgv-tz-install-owner', $state[
'token']) ===
false) {
357 throw new RuntimeException(
'Could not create refresh staging directories.');
359 foreach (
new DirectoryIterator($state[$key]) as $entry) {
360 if ($entry->isDot() || $entry->getFilename() ===
'.lgv-tz-install-owner') {
continue; }
361 $target = $state[$key.
'_stage'].
'/'.$entry->getFilename();
362 if ($entry->isLink()) { $ok = symlink(readlink($entry->getPathname()), $target); }
363 elseif ($entry->isDir()) {
deployCopyTree($entry->getPathname(), $target);
continue; }
364 else { $ok = copy($entry->getPathname(), $target) && chmod($target, $entry->getPerms() & 0777); }
365 if (!$ok) {
throw new RuntimeException(
'Could not stage the existing installation.'); }
368 $privateStage = $state[
'private_stage'];
369 $publicStage = $state[
'public_stage'];
370 echo
"Refreshing installed code; keeping the existing boundaries, configuration, and secret.\n";
372 mkdir($privateStage.
'/app', 0755);
373 $install($privateStage.
'/app',
$settings[
'driver'], dirname(__DIR__));
377 require $package.
'/src/TestLocations.php';
381 $lng = $case[
'params'][
'lng']; $lat = $case[
'params'][
'lat'];
382 while ($lng < -180) { $lng += 360; }
while ($lng > 180) { $lng -= 360; }
383 while ($lat < -90) { $lat += 180; }
while ($lat > 90) { $lat -= 180; }
386 if (
$failures) {
throw new RuntimeException(
$failures.
' known-location tests failed; the existing installation was retained.'); }
387 foreach ([
'update.php',
'Setup.php'] as $file) {
388 if (!copy(__DIR__.
'/'.$file, $privateStage.
'/tools/'.$file)) {
throw new RuntimeException(
'Could not refresh updater code.'); }
390 if (!copy(dirname(__DIR__).
'/update.sh', $privateStage.
'/update.sh')) {
throw new RuntimeException(
'Could not refresh the updater command.'); }
391 $source = file_get_contents(__FILE__);
392 $license = explode(
"/***************************************************************************************************************************/\n/**\n \\file",
$source, 2)[0];
393 $relativePackage = substr($package, strlen($privateStage));
394 if (file_put_contents($publicStage.
'/index.php',
deployEndpointCode($private, $relativePackage, $license)) ===
false) {
395 throw new RuntimeException(
'Could not write the refreshed public entry point.');
397 foreach ([
'composer.phar',
'composer.sha256'] as $file) {
if (is_file($privateStage.
'/app/'.$file)) { unlink($privateStage.
'/app/'.$file); } }
398 foreach ([
'composer-cache',
'composer-home'] as $dir) {
if (is_dir($privateStage.
'/app/'.$dir)) {
deployRemoveTree($privateStage.
'/app/'.$dir); } }
399 $files =
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($privateStage.
'/app', FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
400 foreach ($files as $file) {
401 if (!$file->isLink() && !chmod($file->getPathname(), $file->isDir() || ($file->getPerms() & 0111) ? 0755 : 0644)) {
402 throw new RuntimeException(
'Could not set refreshed code permissions.');
405 if (!chgrp($privateStage, $group) || !chgrp($privateStage.
'/config.php', $group) ||
406 !chmod($privateStage, 0750) || !chmod($privateStage.
'/config.php', 0640) ||
407 !chmod($privateStage.
'/update.sh', 0755) || !chmod($publicStage, 0755) || !chmod($publicStage.
'/index.php', 0644)) {
408 throw new RuntimeException(
'Could not set refreshed installation permissions.');
410 $metadata = $existing[
'metadata'];
411 $metadata[
'service_url'] = $serviceUrl;
413 if (file_put_contents($privateStage.
'/installation.json', json_encode($metadata, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)) ===
false) {
414 throw new RuntimeException(
'Could not write refreshed installation metadata.');
416 foreach ([
'private',
'public'] as $key) {
417 if (!rename($state[$key], $state[$key.
'_backup']) || !rename($state[$key.
'_stage'], $state[$key])) {
418 throw new RuntimeException(
'Could not publish refreshed code; recovery will restore the prior installation.');
421 $state[
'published'] =
true;
424 $secretQuery = $existing[
'secret'] ===
'' ?
'' :
'&secret='.rawurlencode($existing[
'secret']);
426 echo
'Public endpoint: '.$public.
'/index.php'.
"\nPrivate configuration: ".$private.
'/config.php'.
"\n";
427 echo
'Service URL: '.$serviceUrl.
"\n";
428 echo
'Example request: '.$serviceUrl.
'?ll=-77.036543,38.895037'.$secretQuery.
"\n";
429 echo
'Test request: '.$serviceUrl.
'?test'.$secretQuery.
"\n";
439 foreach ([
'boundaries.zip',
'boundaries.json'] as $file) {
440 if (is_file(
$work.
'/'.$file) && !unlink(
$work.
'/'.$file)) { throw new RuntimeException(
'Could not remove downloaded '.$file); }
442 $file =
$work.
'/deployment.json';
443 if (!is_file($file)) {
return; }
444 $state = json_decode(file_get_contents($file),
true, 512, JSON_THROW_ON_ERROR);
446 if (!empty($state[
'guard_created'])) {
450 $statement =
$connection->prepare(
'SELECT token FROM lgv_tz_install_guard');
451 $statement->execute();
452 $token = $statement->fetchColumn();
453 $statement->closeCursor();
455 if (!is_string($token) || !hash_equals($state[
'token'], $token)) {
456 throw new RuntimeException(
'Installation ownership differs; refusing to remove database tables.');
458 if (empty($state[
'published'])) {
$connection->exec(
'DROP TABLE IF EXISTS timezones'); }
459 $connection->exec(
'DROP TABLE lgv_tz_install_guard');
460 } elseif (empty($state[
'published'])) {
461 throw new RuntimeException(
'Installation ownership record is missing; refusing to remove database tables.');
463 $state[
'guard_created'] =
false;
467 if (empty($state[
'published']) && !empty($state[
'database_created'])) {
469 $state[
'database_created'] =
false;
472 $paths = [$state[
'private_stage'], $state[
'public_stage']];
473 if (empty($state[
'published'])) { $paths = array_merge($paths, [$state[
'private'], $state[
'public']]); }
474 foreach ($paths as $path) {
475 if (!is_dir($path)) {
continue; }
476 foreach ($state[
'empty_directories'] ?? [] as $key => $original) {
480 $marker = $path.
'/.lgv-tz-install-owner';
481 if (!is_file($marker) || !hash_equals($state[
'token'], trim(file_get_contents($marker)))) {
482 throw new RuntimeException(
'Directory ownership differs; refusing to remove '.$path);
498function deployRun(
string $work,
bool $noSecret =
false, $terminal =
null, ?callable $install =
null): void {
499 $started = hrtime(true);
500 foreach ([
'curl',
'zip',
'mbstring',
'ctype',
'pdo',
'posix'] as $extension) {
501 if (!extension_loaded($extension)) {
throw new RuntimeException(
'The PHP '.$extension.
' extension is required.'); }
503 if (!function_exists(
'proc_open')) {
throw new RuntimeException(
'PHP proc_open is required.'); }
504 $terminal = $terminal ?? @fopen(
'/dev/tty',
'r+');
505 if (!is_resource($terminal)) {
throw new RuntimeException(
'Run the installer from an interactive terminal.'); }
506 echo
"LGV_TZ_Lookup server installation\n\n";
507 $driver = strtolower(
deployAsk($terminal,
'Database driver (mysql or pgsql)',
'mysql'));
508 if (!in_array(
$driver, [
'mysql',
'pgsql'],
true) || !extension_loaded(
'pdo_'.
$driver)) {
509 throw new RuntimeException(
'Choose mysql or pgsql, with its PHP PDO extension enabled.');
513 if (!ctype_digit(
$port) || (
int)
$port < 1 || (
int)
$port > 65535) {
throw new RuntimeException(
'Port must be between 1 and 65535.'); }
516 $maxName =
$driver ===
'pgsql' ? 63 : 64;
517 if (!preg_match(
'/^[a-zA-Z0-9_-]{1,'.$maxName.
'}$/D',
$settings[
'database'])) {
518 throw new RuntimeException(
'Use a database name containing letters, numbers, underscores, or hyphens.');
520 $settings[
'user'] =
deployAsk($terminal,
'Database user',
$driver ===
'pgsql' ? (getenv(
'USER') ?: get_current_user()) :
'root');
521 $settings[
'password'] =
deployAsk($terminal,
'Database password (input hidden; empty is allowed)',
'',
true);
522 $settings[
'admin_database'] =
$driver ===
'pgsql' ?
deployAsk($terminal,
'Administrative database (used if creating a database)',
'postgres') :
'';
523 $create = strtolower(
deployAsk($terminal,
'Create the database if it does not exist? (y/n)',
'y')) ===
'y';
524 $webRoot = realpath(
deployAsk($terminal,
'Existing web directory (filesystem path; parent of the new service)', getcwd()));
525 if (
false === $webRoot || !is_dir($webRoot) || !is_writable($webRoot)) {
throw new RuntimeException(
'The web directory must be an existing writable directory.'); }
526 $webUrl =
deployAsk($terminal,
'Public URL of that directory (e.g. https://example.com/path)');
527 $endpoint =
deployAsk($terminal,
'Service subdirectory (appended to the path and URL above)',
'timezone');
529 $private =
deployAsk($terminal,
'Private application directory (new or empty; outside the web root)', dirname($webRoot).
'/lgv-tz-server');
530 $parent = realpath(dirname($private));
531 if (
false === $parent || !is_writable($parent) || in_array(basename($private), [
'.',
'..',
''],
true)) {
532 throw new RuntimeException(
'The private application directory needs an existing writable parent.');
534 $private = $parent.
'/'.basename($private);
535 $sourceRoot = realpath(dirname(__DIR__));
536 if ($private === $sourceRoot || str_starts_with($private, $sourceRoot.
'/')) {
537 throw new RuntimeException(
'Install private files outside the source checkout to avoid copying the installation into itself.');
539 if ($webRoot ===
'/' || $private === $webRoot || str_starts_with($private, $webRoot.
'/')) {
540 throw new RuntimeException(
'The private application directory must be outside the web document root.');
542 $public = $webRoot.
'/'.$endpoint;
544 $existing = (file_exists($private) && !isset($emptyDirectories[
'private'])) ||
545 (file_exists($public) && !isset($emptyDirectories[
'public']))
547 $groupName =
deployAsk($terminal,
'Group that runs PHP (for private-file access)', posix_getgrgid(posix_getegid())[
'name']);
548 $group = posix_getgrnam($groupName);
549 if (
false === $group) {
throw new RuntimeException(
'The PHP group does not exist.'); }
550 $install = $install ?? [LGV_TZ_Lookup_Setup::class,
'install'];
551 if ($existing !==
null) {
555 $secret = !$noSecret && strtolower(
deployAsk($terminal,
'Require a server secret? (y/n)',
'y')) !==
'n' ? bin2hex(random_bytes(32)) :
'';
556 $token = bin2hex(random_bytes(32));
557 $privateStage = $parent.
'/.lgv-tz-install-'.bin2hex(random_bytes(12));
558 $publicStage = $webRoot.
'/.lgv-tz-install-'.bin2hex(random_bytes(12));
559 $state = [
'settings' =>
$settings,
'token' => $token,
'private' => $private,
'public' => $public,
560 'private_stage' => $privateStage,
'public_stage' => $publicStage,
'database_created' =>
false,
561 'guard_created' =>
false,
'published' =>
false,
'empty_directories' => $emptyDirectories];
563 foreach ([$privateStage, $publicStage] as $path) {
564 if (!mkdir($path, 0700) ||
false === file_put_contents($path.
'/.lgv-tz-install-owner', $token)) {
565 throw new RuntimeException(
'Could not create installation staging directories.');
568 if (!chgrp($privateStage, $group[
'gid'])) {
throw new RuntimeException(
'Could not assign the PHP group.'); }
570 catch (PDOException
$error) {
571 $missing =
$driver ===
'pgsql' ? (
$error->errorInfo[0] ??
'') ===
'08006' &&
572 str_contains(
$error->getMessage(),
'database "'.$settings[
'database'].
'" does not exist')
573 : (int)(
$error->errorInfo[1] ?? 0) === 1049;
574 if (!$create || !$missing) {
throw $error; }
577 $state[
'database_created'] =
true;
584 throw new RuntimeException(
'This database already contains timezone or installer tables; choose an empty database.');
586 $connection->exec(
'CREATE TABLE lgv_tz_install_guard (token VARCHAR(64) PRIMARY KEY)');
587 $statement =
$connection->prepare(
'INSERT INTO lgv_tz_install_guard VALUES (?)');
588 $statement->execute([$token]);
589 $statement->closeCursor();
590 $state[
'guard_created'] =
true;
592 mkdir($privateStage.
'/app', 0755);
593 $install($privateStage.
'/app',
$driver, dirname(__DIR__));
595 echo
"Downloading the latest timezone boundaries...\n";
597 $asset = $release[
'asset'];
602 echo
'Loading boundary release '.$release[
'version'].
"...\n";
603 $stream = fopen(
$work.
'/boundaries.json',
'rb');
605 finally { fclose($stream); unlink(
$work.
'/boundaries.zip'); unlink(
$work.
'/boundaries.json'); }
607 $count = (int)
$connection->query(
'SELECT COUNT(*) FROM timezones')->fetchColumn();
608 if ($count === 0) {
throw new RuntimeException(
'No polygons were loaded.'); }
609 require $package.
'/src/TestLocations.php';
612 $testStart = hrtime(
true);
614 $lng = $case[
'params'][
'lng']; $lat = $case[
'params'][
'lat'];
615 while ($lng < -180) { $lng += 360; }
while ($lng > 180) { $lng -= 360; }
616 while ($lat < -90) { $lat += 180; }
while ($lat > 90) { $lat -= 180; }
617 if (
$lookup->get_tz($lng, $lat) !== $case[
'result']) { ++
$failures; echo
'FAIL '.$case[
'title'].
"\n"; }
619 if (
$failures) {
throw new RuntimeException(
$failures.
' known-location tests failed; the service was not published.'); }
620 $lookupSeconds = (hrtime(
true) - $testStart) / 1e9;
621 $source = file_get_contents(__FILE__);
622 $licenseHeader = explode(
"/***************************************************************************************************************************/\n/**\n \\file",
$source, 2)[0];
625 'g_dbType' =>
$driver,
'g_dbHost' =>
$settings[
'host'],
'g_dbPort' =>
$settings[
'port'],
'g_server_secret' => $secret] as $key => $value) {
626 $config .=
'$'.$key.
' = '.var_export($value,
true).
";\n";
628 file_put_contents($privateStage.
'/config.php',
$config);
629 if (!chmod($privateStage.
'/config.php', 0640) || !chgrp($privateStage.
'/config.php', $group[
'gid'])) {
630 throw new RuntimeException(
'Could not set private configuration permissions for the PHP group.');
632 mkdir($privateStage.
'/tools', 0755);
633 foreach ([
'update.php',
'Setup.php'] as $file) {
634 if (!copy(__DIR__.
'/'.$file, $privateStage.
'/tools/'.$file) || !chmod($privateStage.
'/tools/'.$file, 0644)) {
635 throw new RuntimeException(
'Could not install the boundary updater.');
638 if (!copy(dirname(__DIR__).
'/update.sh', $privateStage.
'/update.sh') || !chmod($privateStage.
'/update.sh', 0755)) {
639 throw new RuntimeException(
'Could not install the boundary update command.');
641 $relativePackage = substr($package, strlen($privateStage));
643 file_put_contents($publicStage.
'/index.php', $endpointCode);
644 chmod($publicStage.
'/index.php', 0644);
645 chmod($publicStage.
'/.lgv-tz-install-owner', 0644);
646 foreach ([
'composer.phar',
'composer.sha256'] as $file) { unlink($privateStage.
'/app/'.$file); }
647 foreach ([
'composer-cache',
'composer-home'] as $dir) {
deployRemoveTree($privateStage.
'/app/'.$dir); }
649 $files =
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($privateStage.
'/app', FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
650 foreach ($files as $file) {
651 if ($file->isLink()) {
continue; }
652 $mode = $file->isDir() || ($file->getPerms() & 0111) ? 0755 : 0644;
653 if (!chmod($file->getPathname(), $mode)) {
throw new RuntimeException(
'Could not set installed code permissions.'); }
655 chmod($privateStage.
'/app', 0755);
656 file_put_contents($privateStage.
'/installation.json', json_encode([
'release' => $release[
'version'],
'archive_sha256' => $checksum,
658 'service_url' => $serviceUrl], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
659 chmod($privateStage, 0750);
660 chmod($publicStage, 0755);
662 $state[
'published'] =
true;
665 printf(
"Installation: %.2f s; PHP peak %.2f MiB.\n", (hrtime(
true) - $started) / 1e9, memory_get_peak_usage() / 1048576);
666 echo
'Public endpoint: '.$public.
"/index.php\nPrivate configuration: ".$private.
"/config.php\n";
667 echo
'Service URL: '.$serviceUrl.
"\n";
668 echo
'Boundary updater: '.$private.
"/update.sh (use --check to report the latest release).\n";
669 echo $secret ===
'' ?
"Server secret: disabled.\n" :
'Server secret: '.$secret.
"\n";
670 $secretQuery = $secret ===
'' ?
'' :
'&secret='.rawurlencode($secret);
671 echo
'Example request: '.$serviceUrl.
'?ll=-77.036543,38.895037'.$secretQuery.
"\n";
672 echo
'Test request: '.$serviceUrl.
'?test'.$secretQuery.
"\n";
673 echo
"Boundary downloads deleted. The installed database is retained.\n";
676if (realpath($_SERVER[
'SCRIPT_FILENAME'] ??
'') !== __FILE__) {
return; }
677if (PHP_SAPI !==
'cli' || $argc < 3 || !in_array($argv[1], [
'run',
'cleanup'],
true) || !is_dir($argv[2])) {
678 fwrite(STDERR,
"Run ./deploy.sh [--no-secret].\n"); exit(2);
680if (function_exists(
'pcntl_async_signals')) {
681 pcntl_async_signals(
true);
682 foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
683 pcntl_signal($signal,
static function(
int $signal):
void {
throw new RuntimeException(
'Installation interrupted by signal '.$signal.
'.'); });
688 else {
deployRun($argv[2], ($argv[3] ??
'') ===
'--no-secret'); }
689}
catch (Throwable
$error) {
690 fwrite(STDERR,
'Installation error: '.
$error->getMessage().PHP_EOL); exit(1);
$test_locations_param_array
This file has a list of long/lat pairs, along with the expected results, when presented to the server...
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.
static download(string $url, string $destination)
Download an HTTPS resource to a local file.
const PACKAGE
The package installed by both command-line tools.
if($driver==='pgsql') $admin
if(!defined( '__CONFIG_FILE_'))