Settings bugfix, frontend implementation, API CLI template integration
This commit is contained in:
		
							parent
							
								
									10f7025569
								
							
						
					
					
						commit
						98fcd2822c
					
				| @ -17,8 +17,13 @@ class StringType extends Parameter { | |||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // as long as it's numeric or bool, we can safely cast it to a string
 | ||||||
|     if (!is_string($value)) { |     if (!is_string($value)) { | ||||||
|       return false; |       if (is_bool($value) || is_int($value) || is_float($value)) { | ||||||
|  |         $this->value = strval($value); | ||||||
|  |       } else { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if ($this->maxLength > 0 && strlen($value) > $this->maxLength) { |     if ($this->maxLength > 0 && strlen($value) > $this->maxLength) { | ||||||
|  | |||||||
| @ -129,6 +129,8 @@ abstract class Request { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   protected abstract function _execute(): bool; |   protected abstract function _execute(): bool; | ||||||
|  | 
 | ||||||
|  |   // TODO: replace this function with two abstract methods: getDefaultPermittedGroups and getDescription
 | ||||||
|   public static function getDefaultACL(Insert $insert): void { } |   public static function getDefaultACL(Insert $insert): void { } | ||||||
| 
 | 
 | ||||||
|   protected function check2FA(?TwoFactorToken $tfaToken = null): bool { |   protected function check2FA(?TwoFactorToken $tfaToken = null): bool { | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ namespace Core\API { | |||||||
| 
 | 
 | ||||||
| namespace Core\API\Settings { | namespace Core\API\Settings { | ||||||
| 
 | 
 | ||||||
|  |   use Core\API\Parameter\ArrayType; | ||||||
|   use Core\API\Parameter\Parameter; |   use Core\API\Parameter\Parameter; | ||||||
|   use Core\API\Parameter\StringType; |   use Core\API\Parameter\StringType; | ||||||
|   use Core\API\SettingsAPI; |   use Core\API\SettingsAPI; | ||||||
| @ -20,8 +21,6 @@ namespace Core\API\Settings { | |||||||
|   use Core\Driver\SQL\Column\Column; |   use Core\Driver\SQL\Column\Column; | ||||||
|   use Core\Driver\SQL\Condition\CondBool; |   use Core\Driver\SQL\Condition\CondBool; | ||||||
|   use Core\Driver\SQL\Condition\CondIn; |   use Core\Driver\SQL\Condition\CondIn; | ||||||
|   use Core\Driver\SQL\Condition\CondNot; |  | ||||||
|   use Core\Driver\SQL\Condition\CondRegex; |  | ||||||
|   use Core\Driver\SQL\Query\Insert; |   use Core\Driver\SQL\Query\Insert; | ||||||
|   use Core\Driver\SQL\Strategy\UpdateStrategy; |   use Core\Driver\SQL\Strategy\UpdateStrategy; | ||||||
|   use Core\Objects\Context; |   use Core\Objects\Context; | ||||||
| @ -57,7 +56,7 @@ namespace Core\API\Settings { | |||||||
|   class Set extends SettingsAPI { |   class Set extends SettingsAPI { | ||||||
|     public function __construct(Context $context, bool $externalCall = false) { |     public function __construct(Context $context, bool $externalCall = false) { | ||||||
|       parent::__construct($context, $externalCall, array( |       parent::__construct($context, $externalCall, array( | ||||||
|         'settings' => new Parameter('settings', Parameter::TYPE_ARRAY) |         'settings' => new ArrayType("settings", Parameter::TYPE_MIXED) | ||||||
|       )); |       )); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -75,13 +74,13 @@ namespace Core\API\Settings { | |||||||
|       $keys = array(); |       $keys = array(); | ||||||
|       $deleteKeys = array(); |       $deleteKeys = array(); | ||||||
| 
 | 
 | ||||||
|       foreach($values as $key => $value) { |       foreach ($values as $key => $value) { | ||||||
|         if (!$paramKey->parseParam($key)) { |         if (!$paramKey->parseParam($key)) { | ||||||
|           $key = print_r($key, true); |           $key = print_r($key, true); | ||||||
|           return $this->createError("Invalid Type for key in parameter settings: '$key' (Required: " . $paramKey->getTypeName() . ")"); |           return $this->createError("Invalid Type for key in parameter settings: '$key' (Required: " . $paramKey->getTypeName() . ")"); | ||||||
|         } else if(!is_null($value) && !$paramValue->parseParam($value)) { |         } else if (!is_null($value) && !$paramValue->parseParam($value)) { | ||||||
|           $value = print_r($value, true); |           $value = print_r($value, true); | ||||||
|           return $this->createError("Invalid Type for value in parameter settings: '$value' (Required: " . $paramValue->getTypeName() . ")"); |           return $this->createError("Invalid Type for value in parameter settings for key '$key': '$value' (Required: " . $paramValue->getTypeName() . ")"); | ||||||
|         } else if(preg_match("/^[a-zA-Z_][a-zA-Z_0-9-]*$/", $paramKey->value) !== 1) { |         } else if(preg_match("/^[a-zA-Z_][a-zA-Z_0-9-]*$/", $paramKey->value) !== 1) { | ||||||
|           return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes"); |           return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes"); | ||||||
|         } else { |         } else { | ||||||
| @ -91,6 +90,8 @@ namespace Core\API\Settings { | |||||||
|             $deleteKeys[] = $paramKey->value; |             $deleteKeys[] = $paramKey->value; | ||||||
|           } |           } | ||||||
|           $keys[] = $paramKey->value; |           $keys[] = $paramKey->value; | ||||||
|  |           $paramKey->reset(); | ||||||
|  |           $paramValue->reset(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -103,7 +104,7 @@ namespace Core\API\Settings { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (!empty($deleteKeys) && !$this->deleteKeys($keys)) { |       if (!empty($deleteKeys) && !$this->deleteKeys($deleteKeys)) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| return [ | return [ | ||||||
|   "settings" => "Einstellungen", |   "title" => "Einstellungen", | ||||||
|   "information" => "Informationen", |   "information" => "Informationen", | ||||||
| 
 | 
 | ||||||
|   # API Key
 |   # API Key
 | ||||||
| @ -21,5 +21,40 @@ return [ | |||||||
|                               "Unter Android kannst du den Google Authenticator benutzen.", |                               "Unter Android kannst du den Google Authenticator benutzen.", | ||||||
|   "register_2fa_fido_text" => "Möglicherweise musst du mit dem Gerät interagieren, zum Beispiel durch Eingeben einer PIN oder durch Berühren des Geräts", |   "register_2fa_fido_text" => "Möglicherweise musst du mit dem Gerät interagieren, zum Beispiel durch Eingeben einer PIN oder durch Berühren des Geräts", | ||||||
|   "remove_2fa" => "2FA-Token entfernen", |   "remove_2fa" => "2FA-Token entfernen", | ||||||
|   "remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen" |   "remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen", | ||||||
|  | 
 | ||||||
|  |   # settings
 | ||||||
|  |   "key" => "Schlüssel", | ||||||
|  |   "value" => "Wert", | ||||||
|  |   "general" => "Allgemein", | ||||||
|  |   "mail" => "Mail", | ||||||
|  |   "recaptcha" => "reCaptcha", | ||||||
|  |   "uncategorized" => "Unkategorisiert", | ||||||
|  |   "unchanged" => "Unverändert", | ||||||
|  | 
 | ||||||
|  |   # general settings
 | ||||||
|  |   "site_name" => "Seitenname", | ||||||
|  |   "base_url" => "Basis URL", | ||||||
|  |   "user_registration_enabled" => "Benutzerregistrierung erlauben", | ||||||
|  |   "allowed_extensions" => "Erlaubte Dateierweiterungen", | ||||||
|  |   "time_zone" => "Zeitzone", | ||||||
|  | 
 | ||||||
|  |   # mail settings
 | ||||||
|  |   "mail_enabled" => "E-Mail Versand aktiviert", | ||||||
|  |   "mail_from" => "Absender E-Mailadresse", | ||||||
|  |   "mail_host" => "Mail-Server Host", | ||||||
|  |   "mail_port" => "Mail-Server Port", | ||||||
|  |   "mail_username" => "Mail-Server Benutzername", | ||||||
|  |   "mail_password" => "Mail-Server Passwort", | ||||||
|  |   "mail_footer" => "Pfad zum E-Mail-Footer", | ||||||
|  | 
 | ||||||
|  |   # recaptcha
 | ||||||
|  |   "recaptcha_enabled" => "Aktiviere Google reCaptcha", | ||||||
|  |   "recaptcha_public_key" => "reCaptcha öffentlicher Schlüssel", | ||||||
|  |   "recaptcha_private_key" => "reCaptcha privater Schlüssel", | ||||||
|  | 
 | ||||||
|  |   # dialog
 | ||||||
|  |   "fetch_settings_error" => "Fehler beim Holen der Einstellungen", | ||||||
|  |   "save_settings_success" => "Einstellungen erfolgreich gespeichert", | ||||||
|  |   "save_settings_error" => "Fehler beim Speichern der Einstellungen", | ||||||
| ]; | ]; | ||||||
| @ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| return [ | return [ | ||||||
|   "settings" => "Settings", |   "title" => "Settings", | ||||||
|   "information" => "Information", |   "information" => "Information", | ||||||
| 
 | 
 | ||||||
|   # API Key
 |   # API Key
 | ||||||
| @ -22,4 +22,39 @@ return [ | |||||||
|   "register_2fa_fido_text" => "You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration.", |   "register_2fa_fido_text" => "You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration.", | ||||||
|   "remove_2fa" => "Remove 2FA Token", |   "remove_2fa" => "Remove 2FA Token", | ||||||
|   "remove_2fa_text" => "Enter your current password to confirm the removal of your 2FA Token", |   "remove_2fa_text" => "Enter your current password to confirm the removal of your 2FA Token", | ||||||
|  | 
 | ||||||
|  |   # settings
 | ||||||
|  |   "key" => "Key", | ||||||
|  |   "value" => "Value", | ||||||
|  |   "general" => "General", | ||||||
|  |   "mail" => "Mail", | ||||||
|  |   "recaptcha" => "reCaptcha", | ||||||
|  |   "uncategorized" => "Uncategorized", | ||||||
|  |   "unchanged" => "Unchanged", | ||||||
|  | 
 | ||||||
|  |   # general settings
 | ||||||
|  |   "site_name" => "Site Name", | ||||||
|  |   "base_url" => "Base URL", | ||||||
|  |   "user_registration_enabled" => "Allow user registration", | ||||||
|  |   "allowed_extensions" => "Allowed file extensions", | ||||||
|  |   "time_zone" => "Time zone", | ||||||
|  | 
 | ||||||
|  |   # mail settings
 | ||||||
|  |   "mail_enabled" => "Enable e-mail transport", | ||||||
|  |   "mail_from" => "Sender e-mail address", | ||||||
|  |   "mail_host" => "Mail server host", | ||||||
|  |   "mail_port" => "Mail server port", | ||||||
|  |   "mail_username" => "Mail server username", | ||||||
|  |   "mail_password" => "Mail server password", | ||||||
|  |   "mail_footer" => "Path to e-mail footer", | ||||||
|  | 
 | ||||||
|  |   # recaptcha
 | ||||||
|  |   "recaptcha_enabled" => "Enable Google reCaptcha", | ||||||
|  |   "recaptcha_public_key" => "reCaptcha Public Key", | ||||||
|  |   "recaptcha_private_key" => "reCaptcha Private Key", | ||||||
|  | 
 | ||||||
|  |   # dialog
 | ||||||
|  |   "fetch_settings_error" => "Error fetching settings", | ||||||
|  |   "save_settings_success" => "Settings saved successfully", | ||||||
|  |   "save_settings_error" => "Error saving settings", | ||||||
| ]; | ]; | ||||||
							
								
								
									
										34
									
								
								README.md
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										34
									
								
								README.md
									
									
									
									
									
								
							| @ -165,10 +165,12 @@ To access and view any frontend pages, the internal router is used. Available ro | |||||||
| A static route targets a file, usually located in [/static](/static) and does nothing more, than returning its content. A dynamic route is usually the way to go: | A static route targets a file, usually located in [/static](/static) and does nothing more, than returning its content. A dynamic route is usually the way to go: | ||||||
| It takes two parameters, firstly the target document and secondly, an optional view. For example, take the following routing table: | It takes two parameters, firstly the target document and secondly, an optional view. For example, take the following routing table: | ||||||
| 
 | 
 | ||||||
| | Route                     | Action | Target | Extra   | | | Route                     | Action     | Target                       | Extra   | | ||||||
| |---------------------------| ------ | ------ |---------| | |---------------------------|------------|------------------------------|---------| | ||||||
| | `/funnyCatImage`          | `Serve Static` | `/static/cat.jpg` |         | | | `/funnyCatImage`          | `Static`   | `/static/cat.jpg`            |         | | ||||||
| | `/someRoute/{param:str?}` | `Redirect Dynamic` | `\Documents\MyDocument\` | `param` | | | `/someRoute/{param:str?}` | `Dynamic`  | `\Site\Documents\MyDocument` | `param` | | ||||||
|  | | `/redirectMe`             | `Redirect` | `https://romanh.de/`         |         | | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| The first route would return the cat image, if the case-insensitive path `/funnyCatImage` is requested. | The first route would return the cat image, if the case-insensitive path `/funnyCatImage` is requested. | ||||||
| The second route is more interesting, as it includes an optional parameter of type string, which means, any route starting with `/someRoute/` or just `/someRoute` is accepted. | The second route is more interesting, as it includes an optional parameter of type string, which means, any route starting with `/someRoute/` or just `/someRoute` is accepted. | ||||||
| @ -182,12 +184,12 @@ we could have one document, when showing different articles and products, and a | |||||||
| To create a new document, a class inside [/Core/Documents](/Core/Documents) is created with the following scheme: | To create a new document, a class inside [/Core/Documents](/Core/Documents) is created with the following scheme: | ||||||
| 
 | 
 | ||||||
| ```php | ```php | ||||||
| namespace Documents { | namespace Site\Documents { | ||||||
| 
 | 
 | ||||||
|   use Elements\Document; |   use Core\Elements\Document; | ||||||
|   use Objects\Router\Router; |   use Core\Objects\Router\Router; | ||||||
|   use Documents\Example\ExampleHead; |   use Site\Documents\Example\ExampleHead; | ||||||
|   use Documents\Example\ExampleBody; |   use Site\Documents\Example\ExampleBody; | ||||||
| 
 | 
 | ||||||
|   class ExampleDocument extends Document { |   class ExampleDocument extends Document { | ||||||
|     public function __construct(Router $router, ?string $view = NULL) { |     public function __construct(Router $router, ?string $view = NULL) { | ||||||
| @ -196,10 +198,10 @@ namespace Documents { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| namespace Documents\Example { | namespace Site\Documents\Example { | ||||||
| 
 | 
 | ||||||
|   use Elements\Head; |   use Core\Elements\Head; | ||||||
|   use Elements\Body; |   use Core\Elements\Body; | ||||||
| 
 | 
 | ||||||
|   class ExampleHead extends Head { |   class ExampleHead extends Head { | ||||||
|    |    | ||||||
| @ -255,7 +257,7 @@ we firstly have to load the module. This is done by adding the class, or the obj | |||||||
| To translate the defined strings, we can use the global `L()` function. The following code snipped shows the use of  | To translate the defined strings, we can use the global `L()` function. The following code snipped shows the use of  | ||||||
| our sample language module: | our sample language module: | ||||||
| 
 | 
 | ||||||
| **/Core/Localization/de_DE/example.php**: | **/Site/Localization/de_DE/example.php**: | ||||||
| ```php | ```php | ||||||
| <?php | <?php | ||||||
| return [ | return [ | ||||||
| @ -316,6 +318,12 @@ php cli.php frontend rm <module-name> | |||||||
| php cli.php frontend dev <module-name> | php cli.php frontend dev <module-name> | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ### API commands | ||||||
|  | ```bash | ||||||
|  | php cli.php api ls | ||||||
|  | php cli.php api <add> # interactive wizard | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ## Project Structure | ## Project Structure | ||||||
| ``` | ``` | ||||||
| ├── Core                     | ├── Core                     | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								cli.php
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										10
									
								
								cli.php
									
									
									
									
									
								
							| @ -859,6 +859,10 @@ function onAPI(array $argv): void { | |||||||
|       // TODO: auto-generated method stub
 |       // TODO: auto-generated method stub
 | ||||||
|       return \$this->success; |       return \$this->success; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static function getDefaultACL(Insert \$insert): void { | ||||||
|  |       \$insert->addRow(self::getEndpoint(), [], \"Short description, what users are allowed to do with this permission\", false);
 | ||||||
|  |     } | ||||||
|   }";
 |   }";
 | ||||||
|       }, $methodNames)); |       }, $methodNames)); | ||||||
|       $content = "<?php
 |       $content = "<?php
 | ||||||
| @ -867,6 +871,7 @@ namespace Site\API { | |||||||
|    |    | ||||||
|   use Core\API\Request; |   use Core\API\Request; | ||||||
|   use Core\Objects\Context; |   use Core\Objects\Context; | ||||||
|  |   use Core\Driver\SQL\Query\Insert; | ||||||
|    |    | ||||||
|   abstract class {$apiName}API extends Request { |   abstract class {$apiName}API extends Request { | ||||||
|     public function __construct(Context \$context, bool \$externalCall = false, array \$params = []) { |     public function __construct(Context \$context, bool \$externalCall = false, array \$params = []) { | ||||||
| @ -891,6 +896,7 @@ namespace Site\API; | |||||||
| 
 | 
 | ||||||
| use Core\API\Request; | use Core\API\Request; | ||||||
| use Core\Objects\Context; | use Core\Objects\Context; | ||||||
|  | use Core\Driver\SQL\Query\Insert; | ||||||
| 
 | 
 | ||||||
| class $apiName extends Request { | class $apiName extends Request { | ||||||
| 
 | 
 | ||||||
| @ -903,6 +909,10 @@ class $apiName extends Request { | |||||||
|     // TODO: auto-generated method stub
 |     // TODO: auto-generated method stub
 | ||||||
|     return \$this->success; |     return \$this->success; | ||||||
|   } |   } | ||||||
|  |    | ||||||
|  |   public static function getDefaultACL(Insert \$insert): void { | ||||||
|  |     \$insert->addRow(self::getEndpoint(), [], \"Short description, what users are allowed to do with this permission\", false);
 | ||||||
|  |   } | ||||||
| } | } | ||||||
| ";
 | ";
 | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ const LogView = lazy(() => import("./views/log-view")); | |||||||
| const AccessControlList = lazy(() => import("./views/access-control-list")); | const AccessControlList = lazy(() => import("./views/access-control-list")); | ||||||
| const RouteListView = lazy(() => import("./views/route/route-list")); | const RouteListView = lazy(() => import("./views/route/route-list")); | ||||||
| const RouteEditView = lazy(() => import("./views/route/route-edit")); | const RouteEditView = lazy(() => import("./views/route/route-edit")); | ||||||
|  | const SettingsView = lazy(() => import("./views/settings")); | ||||||
| 
 | 
 | ||||||
| export default function AdminDashboard(props) { | export default function AdminDashboard(props) { | ||||||
| 
 | 
 | ||||||
| @ -83,6 +84,7 @@ export default function AdminDashboard(props) { | |||||||
|                             <Route path={"/admin/permissions"} element={<AccessControlList {...controlObj} />}/> |                             <Route path={"/admin/permissions"} element={<AccessControlList {...controlObj} />}/> | ||||||
|                             <Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/> |                             <Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/> | ||||||
|                             <Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/> |                             <Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/> | ||||||
|  |                             <Route path={"/admin/settings"} element={<SettingsView {...controlObj} />}/> | ||||||
|                             <Route path={"*"} element={<View404 />} /> |                             <Route path={"*"} element={<View404 />} /> | ||||||
|                         </Routes> |                         </Routes> | ||||||
|                     </Suspense> |                     </Suspense> | ||||||
|  | |||||||
| @ -219,7 +219,8 @@ export default function EditGroupView(props) { | |||||||
|                                 onClick={() => navigate("/admin/groups")}> |                                 onClick={() => navigate("/admin/groups")}> | ||||||
|                             {L("general.go_back")} |                             {L("general.go_back")} | ||||||
|                         </Button> |                         </Button> | ||||||
|                         <Button startIcon={<Save />} color={"primary"} |                         <Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />} | ||||||
|  |                                 color={"primary"} | ||||||
|                                 variant={"outlined"} |                                 variant={"outlined"} | ||||||
|                                 disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))} |                                 disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))} | ||||||
|                                 onClick={onSave}> |                                 onClick={onSave}> | ||||||
|  | |||||||
| @ -138,8 +138,10 @@ export default function RouteEditView(props) { | |||||||
|                     onClick={() => navigate("/admin/routes")}> |                     onClick={() => navigate("/admin/routes")}> | ||||||
|                 {L("general.cancel")} |                 {L("general.cancel")} | ||||||
|             </Button> |             </Button> | ||||||
|             <Button startIcon={<Save />} color={"primary"} |             <Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />} | ||||||
|                     variant={"outlined"} disabled={isSaving} |                     color={"primary"} | ||||||
|  |                     variant={"outlined"} | ||||||
|  |                     disabled={isSaving} | ||||||
|                     onClick={onSave}> |                     onClick={onSave}> | ||||||
|                 {isSaving ? L("general.saving") + "…" : L("general.save")} |                 {isSaving ? L("general.saving") + "…" : L("general.save")} | ||||||
|             </Button> |             </Button> | ||||||
|  | |||||||
							
								
								
									
										367
									
								
								react/admin-panel/src/views/settings.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										367
									
								
								react/admin-panel/src/views/settings.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,367 @@ | |||||||
|  | import {useCallback, useContext, useEffect, useState} from "react"; | ||||||
|  | import {LocaleContext} from "shared/locale"; | ||||||
|  | import { | ||||||
|  |     Box, Button, Checkbox, | ||||||
|  |     CircularProgress, FormControl, FormControlLabel, | ||||||
|  |     FormGroup, FormLabel, IconButton, | ||||||
|  |     Paper, Select, styled, | ||||||
|  |     Tab, | ||||||
|  |     Table, | ||||||
|  |     TableBody, | ||||||
|  |     TableCell, | ||||||
|  |     TableHead, | ||||||
|  |     TableRow, | ||||||
|  |     Tabs, TextField | ||||||
|  | } from "@mui/material"; | ||||||
|  | import {Link} from "react-router-dom"; | ||||||
|  | import {Add, Delete, Google, LibraryBooks, Mail, RestartAlt, Save, SettingsApplications} from "@mui/icons-material"; | ||||||
|  | import {TableContainer} from "@material-ui/core"; | ||||||
|  | import TIME_ZONES from "shared/time-zones"; | ||||||
|  | 
 | ||||||
|  | const SettingsFormGroup = styled(FormGroup)((props) => ({ | ||||||
|  |     marginBottom: props.theme.spacing(1), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const ButtonBar = styled(Box)((props) => ({ | ||||||
|  |     "& > button": { | ||||||
|  |         marginRight: props.theme.spacing(1) | ||||||
|  |     } | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | export default function SettingsView(props) { | ||||||
|  | 
 | ||||||
|  |     // meta
 | ||||||
|  |     const api = props.api; | ||||||
|  |     const showDialog = props.showDialog; | ||||||
|  |     const {translate: L, requestModules, currentLocale} = useContext(LocaleContext); | ||||||
|  |     const KNOWN_SETTING_KEYS = { | ||||||
|  |       "general": [ | ||||||
|  |           "base_url", | ||||||
|  |           "site_name", | ||||||
|  |           "user_registration_enabled", | ||||||
|  |           "time_zone", | ||||||
|  |           "allowed_extensions", | ||||||
|  |       ], | ||||||
|  |       "mail": [ | ||||||
|  |           "mail_enabled", | ||||||
|  |           "mail_footer", | ||||||
|  |           "mail_from", | ||||||
|  |           "mail_host", | ||||||
|  |           "mail_port", | ||||||
|  |           "mail_username", | ||||||
|  |           "mail_password", | ||||||
|  |       ], | ||||||
|  |       "recaptcha": [ | ||||||
|  |           "recaptcha_enabled", | ||||||
|  |           "recaptcha_private_key", | ||||||
|  |           "recaptcha_public_key", | ||||||
|  |       ], | ||||||
|  |       "hidden": ["installation_completed", "mail_last_sync"] | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // data
 | ||||||
|  |     const [fetchSettings, setFetchSettings] = useState(true); | ||||||
|  |     const [settings, setSettings] = useState(null); | ||||||
|  |     const [uncategorizedKeys, setUncategorizedKeys] = useState([]); | ||||||
|  | 
 | ||||||
|  |     // ui
 | ||||||
|  |     const [selectedTab, setSelectedTab] = useState("general"); | ||||||
|  |     const [hasChanged, setChanged] = useState(false); | ||||||
|  |     const [isSaving, setSaving] = useState(false); | ||||||
|  |     const [newKey, setNewKey] = useState(""); | ||||||
|  | 
 | ||||||
|  |     const isUncategorized = (key) => { | ||||||
|  |         return !(Object.values(KNOWN_SETTING_KEYS).reduce((acc, arr) => { | ||||||
|  |             return [ ...acc, ...arr ]; | ||||||
|  |         }, [])).includes(key); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         requestModules(props.api, ["general", "settings"], currentLocale).then(data => { | ||||||
|  |             if (!data.success) { | ||||||
|  |                 showDialog("Error fetching translations: " + data.msg); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }, [currentLocale]); | ||||||
|  | 
 | ||||||
|  |     const onFetchSettings = useCallback((force = false) => { | ||||||
|  |         if (fetchSettings || force) { | ||||||
|  |             setFetchSettings(false); | ||||||
|  |             api.getSettings().then(data => { | ||||||
|  |                 if (!data.success) { | ||||||
|  |                     showDialog(data.msg, L("settings.fetch_settings_error")); | ||||||
|  |                 } else { | ||||||
|  |                     setSettings(Object.keys(data.settings) | ||||||
|  |                         .filter(key => !KNOWN_SETTING_KEYS.hidden.includes(key)) | ||||||
|  |                         .reduce((obj, key) => { | ||||||
|  |                             obj[key] = data.settings[key]; | ||||||
|  |                             return obj; | ||||||
|  |                         }, {}) | ||||||
|  |                     ); | ||||||
|  |                     setUncategorizedKeys(Object.keys(data.settings).filter(key => isUncategorized(key))); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }, [api, showDialog, fetchSettings]); | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         onFetchSettings(); | ||||||
|  |     }, [fetchSettings]); | ||||||
|  | 
 | ||||||
|  |     const onChangeValue = useCallback((key, value) => { | ||||||
|  |         setChanged(true); | ||||||
|  |         setSettings({...settings, [key]: value}); | ||||||
|  |     }, [settings]); | ||||||
|  | 
 | ||||||
|  |     const onSaveSettings = useCallback(() => { | ||||||
|  |         setSaving(true); | ||||||
|  |         api.saveSettings(settings).then(data => { | ||||||
|  |             setSaving(false); | ||||||
|  |             if (data.success) { | ||||||
|  |                 showDialog(L("settings.save_settings_success"), L("general.success")); | ||||||
|  |                 setChanged(false); | ||||||
|  |             } else { | ||||||
|  |                 showDialog(data.msg, L("settings.save_settings_error")); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }, [api, showDialog, settings]); | ||||||
|  | 
 | ||||||
|  |     const onDeleteKey = useCallback(key => { | ||||||
|  |         if (key && settings.hasOwnProperty(key)) { | ||||||
|  |             let index = uncategorizedKeys.indexOf(key); | ||||||
|  |             if (index !== -1) { | ||||||
|  |                 let newUncategorizedKeys = [...uncategorizedKeys]; | ||||||
|  |                 newUncategorizedKeys.splice(index, 1); | ||||||
|  |                 setUncategorizedKeys(newUncategorizedKeys); | ||||||
|  |             } | ||||||
|  |             setChanged(true); | ||||||
|  |             setSettings({...settings, [key]: null}); | ||||||
|  |         } | ||||||
|  |     }, [settings, uncategorizedKeys]); | ||||||
|  | 
 | ||||||
|  |     const onAddKey = useCallback(key => { | ||||||
|  |         if (key) { | ||||||
|  |             if (!isUncategorized(key) || !settings.hasOwnProperty(key) || settings[key] === null) { | ||||||
|  |                 setChanged(true); | ||||||
|  |                 setSettings({...settings, [key]: ""}); | ||||||
|  |                 setUncategorizedKeys([...uncategorizedKeys, key]); | ||||||
|  |                 setNewKey(""); | ||||||
|  |             } else { | ||||||
|  |                 showDialog("This key is already defined", L("general.error")); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }, [settings, uncategorizedKeys, showDialog]); | ||||||
|  | 
 | ||||||
|  |     const onChangeKey = useCallback((oldKey, newKey) => { | ||||||
|  |         if (settings.hasOwnProperty(oldKey) && !settings.hasOwnProperty(newKey)) { | ||||||
|  |             let newSettings = {...settings, [newKey]: settings[oldKey]}; | ||||||
|  |             delete newSettings[oldKey]; | ||||||
|  |             setChanged(true); | ||||||
|  |             setSettings(newSettings); | ||||||
|  |         } | ||||||
|  |     }, [settings]); | ||||||
|  | 
 | ||||||
|  |     if (settings === null) { | ||||||
|  |         return <CircularProgress /> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const parseBool = (v) => v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase()); | ||||||
|  | 
 | ||||||
|  |     const renderTextInput = (key_name, disabled=false, props={}) => { | ||||||
|  |         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||||
|  |             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||||
|  |             <FormControl> | ||||||
|  |                 <TextField size={"small"} variant={"outlined"} | ||||||
|  |                            disabled={disabled} | ||||||
|  |                            value={settings[key_name]} | ||||||
|  |                            onChange={e => onChangeValue(key_name, e.target.value)} /> | ||||||
|  |             </FormControl> | ||||||
|  |         </SettingsFormGroup> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const renderPasswordInput = (key_name, disabled=false, props={}) => { | ||||||
|  |         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||||
|  |             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||||
|  |             <FormControl> | ||||||
|  |                 <TextField size={"small"} variant={"outlined"} | ||||||
|  |                            type={"password"} | ||||||
|  |                            disabled={disabled} | ||||||
|  |                            placeholder={"(" + L("settings.unchanged") + ")"} | ||||||
|  |                            value={settings[key_name]} | ||||||
|  |                            onChange={e => onChangeValue(key_name, e.target.value)} /> | ||||||
|  |             </FormControl> | ||||||
|  |         </SettingsFormGroup> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const renderNumberInput = (key_name, minValue, maxValue, disabled=false, props={}) => { | ||||||
|  |         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||||
|  |             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||||
|  |             <FormControl> | ||||||
|  |                 <TextField size={"small"} variant={"outlined"} | ||||||
|  |                            type={"number"} | ||||||
|  |                            disabled={disabled} | ||||||
|  |                            inputProps={{min: minValue, max: maxValue}} | ||||||
|  |                            value={settings[key_name]} | ||||||
|  |                            onChange={e => onChangeValue(key_name, e.target.value)} /> | ||||||
|  |             </FormControl> | ||||||
|  |         </SettingsFormGroup> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const renderCheckBox = (key_name, disabled=false, props={}) => { | ||||||
|  |         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||||
|  |             <FormControlLabel | ||||||
|  |                 disabled={disabled} | ||||||
|  |                 control={<Checkbox | ||||||
|  |                     disabled={disabled} | ||||||
|  |                     checked={parseBool(settings[key_name])} | ||||||
|  |                     onChange={(e, v) => onChangeValue(key_name, v)} />} | ||||||
|  |                 label={L("settings." + key_name)} /> | ||||||
|  |         </SettingsFormGroup> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const renderSelection = (key_name, options, disabled=false, props={}) => { | ||||||
|  |         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||||
|  |             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||||
|  |             <FormControl> | ||||||
|  |                 <Select native value={settings[key_name]} | ||||||
|  |                         disabled={disabled} | ||||||
|  |                         size={"small"} onChange={e => onChangeValue(key_name, e.target.value)}> | ||||||
|  |                         {options.map(option => <option | ||||||
|  |                             key={"option-" + option} | ||||||
|  |                             value={option}> | ||||||
|  |                                 {option} | ||||||
|  |                         </option>)} | ||||||
|  |                 </Select> | ||||||
|  |             </FormControl> | ||||||
|  |         </SettingsFormGroup> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const renderTab = () => { | ||||||
|  |         if (selectedTab === "general") { | ||||||
|  |             return [ | ||||||
|  |                 renderTextInput("site_name"), | ||||||
|  |                 renderTextInput("base_url"), | ||||||
|  |                 renderCheckBox("user_registration_enabled"), | ||||||
|  |                 renderTextInput("allowed_extensions"), | ||||||
|  |                 renderSelection("time_zone", TIME_ZONES), | ||||||
|  |             ]; | ||||||
|  |         } else if (selectedTab === "mail") { | ||||||
|  |             return [ | ||||||
|  |                 renderCheckBox("mail_enabled"), | ||||||
|  |                 renderTextInput("mail_from", !parseBool(settings.mail_enabled)), | ||||||
|  |                 renderTextInput("mail_host", !parseBool(settings.mail_enabled)), | ||||||
|  |                 renderNumberInput("mail_port", 1, 65535, !parseBool(settings.mail_enabled)), | ||||||
|  |                 renderTextInput("mail_username", !parseBool(settings.mail_enabled)), | ||||||
|  |                 renderPasswordInput("mail_password", !parseBool(settings.mail_enabled)), | ||||||
|  |                 renderTextInput("mail_footer", !parseBool(settings.mail_enabled)), | ||||||
|  |             ]; | ||||||
|  |         } else if (selectedTab === "recaptcha") { | ||||||
|  |             return [ | ||||||
|  |                 renderCheckBox("recaptcha_enabled"), | ||||||
|  |                 renderTextInput("recaptcha_public_key", !parseBool(settings.recaptcha_enabled)), | ||||||
|  |                 renderPasswordInput("recaptcha_private_key", !parseBool(settings.recaptcha_enabled)), | ||||||
|  |             ]; | ||||||
|  |         } else if (selectedTab === "uncategorized") { | ||||||
|  |             return <TableContainer component={Paper}> | ||||||
|  |                     <Table> | ||||||
|  |                         <TableHead> | ||||||
|  |                             <TableRow> | ||||||
|  |                                 <TableCell>{L("settings.key")}</TableCell> | ||||||
|  |                                 <TableCell>{L("settings.value")}</TableCell> | ||||||
|  |                                 <TableCell align={"center"}>{L("general.controls")}</TableCell> | ||||||
|  |                             </TableRow> | ||||||
|  |                         </TableHead> | ||||||
|  |                         <TableBody> | ||||||
|  |                             {uncategorizedKeys.map(key => <TableRow key={"settings-" + key}> | ||||||
|  |                                 <TableCell> | ||||||
|  |                                     <TextField fullWidth={true} size={"small"} value={key} | ||||||
|  |                                                onChange={e => onChangeKey(key, e.target.value)} /> | ||||||
|  |                                 </TableCell> | ||||||
|  |                                 <TableCell> | ||||||
|  |                                     <TextField fullWidth={true} size={"small"} value={settings[key]} | ||||||
|  |                                         onChange={e => onChangeValue(key, e.target.value)} /> | ||||||
|  |                                 </TableCell> | ||||||
|  |                                 <TableCell align={"center"}> | ||||||
|  |                                     <IconButton onClick={() => onDeleteKey(key)} | ||||||
|  |                                         color={"secondary"}> | ||||||
|  |                                         <Delete /> | ||||||
|  |                                     </IconButton> | ||||||
|  |                                 </TableCell> | ||||||
|  |                             </TableRow>)} | ||||||
|  |                             <TableRow> | ||||||
|  |                                 <TableCell> | ||||||
|  |                                     <TextField fullWidth={true} size={"small"} onChange={e => setNewKey(e.target.value)} | ||||||
|  |                                         onBlur={() => onAddKey(newKey)} value={newKey}/> | ||||||
|  |                                 </TableCell> | ||||||
|  |                                 <TableCell> | ||||||
|  |                                     <TextField fullWidth={true} size={"small"} /> | ||||||
|  |                                 </TableCell> | ||||||
|  |                                 <TableCell align={"center"}> | ||||||
|  |                                 </TableCell> | ||||||
|  |                             </TableRow> | ||||||
|  |                         </TableBody> | ||||||
|  |                     </Table> | ||||||
|  |                     <Box p={1}> | ||||||
|  |                         <Button startIcon={<Add />} variant={"outlined"} | ||||||
|  |                                 size={"small"}> | ||||||
|  |                                     {L("general.add")} | ||||||
|  |                                 </Button> | ||||||
|  |                     </Box> | ||||||
|  |                 </TableContainer> | ||||||
|  |         } else { | ||||||
|  |             return <i>Invalid tab: {selectedTab}</i> | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return <> | ||||||
|  |         <div className={"content-header"}> | ||||||
|  |             <div className={"container-fluid"}> | ||||||
|  |                 <div className={"row mb-2"}> | ||||||
|  |                     <div className={"col-sm-6"}> | ||||||
|  |                         <h1 className={"m-0 text-dark"}>{L("settings.title")}</h1> | ||||||
|  |                     </div> | ||||||
|  |                     <div className={"col-sm-6"}> | ||||||
|  |                         <ol className={"breadcrumb float-sm-right"}> | ||||||
|  |                             <li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li> | ||||||
|  |                             <li className="breadcrumb-item active">{L("settings.title")}</li> | ||||||
|  |                         </ol> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div className={"content"}> | ||||||
|  |             <Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)} component={Paper}> | ||||||
|  |                 <Tab value={"general"} label={L("settings.general")} | ||||||
|  |                      icon={<SettingsApplications />} iconPosition={"start"} /> | ||||||
|  |                 <Tab value={"mail"} label={L("settings.mail")} | ||||||
|  |                      icon={<Mail />} iconPosition={"start"} /> | ||||||
|  |                 <Tab value={"recaptcha"} label={L("settings.recaptcha")} | ||||||
|  |                      icon={<Google />} iconPosition={"start"} /> | ||||||
|  |                 <Tab value={"uncategorized"} label={L("settings.uncategorized")} | ||||||
|  |                      icon={<LibraryBooks />} iconPosition={"start"} /> | ||||||
|  |             </Tabs> | ||||||
|  |             <Box p={2}> | ||||||
|  |             { | ||||||
|  |                 renderTab() | ||||||
|  |             } | ||||||
|  |             </Box> | ||||||
|  |             <ButtonBar> | ||||||
|  |                 <Button color={"primary"} | ||||||
|  |                         onClick={onSaveSettings} | ||||||
|  |                         disabled={isSaving} | ||||||
|  |                         startIcon={isSaving ? <CircularProgress size={14} /> : <Save />} | ||||||
|  |                         variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}> | ||||||
|  |                     {isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))} | ||||||
|  |                 </Button> | ||||||
|  |                 <Button color={"secondary"} | ||||||
|  |                         onClick={() => { setFetchSettings(true); setNewKey(""); }} | ||||||
|  |                         disabled={isSaving} | ||||||
|  |                         startIcon={<RestartAlt />} | ||||||
|  |                         variant={"outlined"} title={L("general.reset")}> | ||||||
|  |                     {L("general.reset")} | ||||||
|  |                 </Button> | ||||||
|  |             </ButtonBar> | ||||||
|  |         </div> | ||||||
|  |     </> | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										603
									
								
								react/shared/time-zones.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										603
									
								
								react/shared/time-zones.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,603 @@ | |||||||
|  | // Source: https://pecl.php.net/get/timezonedb
 | ||||||
|  | // Date: 2024-04-05 12:00:00
 | ||||||
|  | const TIME_ZONES = [ | ||||||
|  |     "Africa/Abidjan", | ||||||
|  |     "Africa/Accra", | ||||||
|  |     "Africa/Addis_Ababa", | ||||||
|  |     "Africa/Algiers", | ||||||
|  |     "Africa/Asmara", | ||||||
|  |     "Africa/Asmera", | ||||||
|  |     "Africa/Bamako", | ||||||
|  |     "Africa/Bangui", | ||||||
|  |     "Africa/Banjul", | ||||||
|  |     "Africa/Bissau", | ||||||
|  |     "Africa/Blantyre", | ||||||
|  |     "Africa/Brazzaville", | ||||||
|  |     "Africa/Bujumbura", | ||||||
|  |     "Africa/Cairo", | ||||||
|  |     "Africa/Casablanca", | ||||||
|  |     "Africa/Ceuta", | ||||||
|  |     "Africa/Conakry", | ||||||
|  |     "Africa/Dakar", | ||||||
|  |     "Africa/Dar_es_Salaam", | ||||||
|  |     "Africa/Djibouti", | ||||||
|  |     "Africa/Douala", | ||||||
|  |     "Africa/El_Aaiun", | ||||||
|  |     "Africa/Freetown", | ||||||
|  |     "Africa/Gaborone", | ||||||
|  |     "Africa/Harare", | ||||||
|  |     "Africa/Johannesburg", | ||||||
|  |     "Africa/Juba", | ||||||
|  |     "Africa/Kampala", | ||||||
|  |     "Africa/Khartoum", | ||||||
|  |     "Africa/Kigali", | ||||||
|  |     "Africa/Kinshasa", | ||||||
|  |     "Africa/Lagos", | ||||||
|  |     "Africa/Libreville", | ||||||
|  |     "Africa/Lome", | ||||||
|  |     "Africa/Luanda", | ||||||
|  |     "Africa/Lubumbashi", | ||||||
|  |     "Africa/Lusaka", | ||||||
|  |     "Africa/Malabo", | ||||||
|  |     "Africa/Maputo", | ||||||
|  |     "Africa/Maseru", | ||||||
|  |     "Africa/Mbabane", | ||||||
|  |     "Africa/Mogadishu", | ||||||
|  |     "Africa/Monrovia", | ||||||
|  |     "Africa/Nairobi", | ||||||
|  |     "Africa/Ndjamena", | ||||||
|  |     "Africa/Niamey", | ||||||
|  |     "Africa/Nouakchott", | ||||||
|  |     "Africa/Ouagadougou", | ||||||
|  |     "Africa/Porto-Novo", | ||||||
|  |     "Africa/Sao_Tome", | ||||||
|  |     "Africa/Timbuktu", | ||||||
|  |     "Africa/Tripoli", | ||||||
|  |     "Africa/Tunis", | ||||||
|  |     "Africa/Windhoek", | ||||||
|  |     "America/Adak", | ||||||
|  |     "America/Anchorage", | ||||||
|  |     "America/Anguilla", | ||||||
|  |     "America/Antigua", | ||||||
|  |     "America/Araguaina", | ||||||
|  |     "America/Argentina/Buenos_Aires", | ||||||
|  |     "America/Argentina/Catamarca", | ||||||
|  |     "America/Argentina/ComodRivadavia", | ||||||
|  |     "America/Argentina/Cordoba", | ||||||
|  |     "America/Argentina/Jujuy", | ||||||
|  |     "America/Argentina/La_Rioja", | ||||||
|  |     "America/Argentina/Mendoza", | ||||||
|  |     "America/Argentina/Rio_Gallegos", | ||||||
|  |     "America/Argentina/Salta", | ||||||
|  |     "America/Argentina/San_Juan", | ||||||
|  |     "America/Argentina/San_Luis", | ||||||
|  |     "America/Argentina/Tucuman", | ||||||
|  |     "America/Argentina/Ushuaia", | ||||||
|  |     "America/Aruba", | ||||||
|  |     "America/Asuncion", | ||||||
|  |     "America/Atikokan", | ||||||
|  |     "America/Atka", | ||||||
|  |     "America/Bahia", | ||||||
|  |     "America/Bahia_Banderas", | ||||||
|  |     "America/Barbados", | ||||||
|  |     "America/Belem", | ||||||
|  |     "America/Belize", | ||||||
|  |     "America/Blanc-Sablon", | ||||||
|  |     "America/Boa_Vista", | ||||||
|  |     "America/Bogota", | ||||||
|  |     "America/Boise", | ||||||
|  |     "America/Buenos_Aires", | ||||||
|  |     "America/Cambridge_Bay", | ||||||
|  |     "America/Campo_Grande", | ||||||
|  |     "America/Cancun", | ||||||
|  |     "America/Caracas", | ||||||
|  |     "America/Catamarca", | ||||||
|  |     "America/Cayenne", | ||||||
|  |     "America/Cayman", | ||||||
|  |     "America/Chicago", | ||||||
|  |     "America/Chihuahua", | ||||||
|  |     "America/Ciudad_Juarez", | ||||||
|  |     "America/Coral_Harbour", | ||||||
|  |     "America/Cordoba", | ||||||
|  |     "America/Costa_Rica", | ||||||
|  |     "America/Creston", | ||||||
|  |     "America/Cuiaba", | ||||||
|  |     "America/Curacao", | ||||||
|  |     "America/Danmarkshavn", | ||||||
|  |     "America/Dawson", | ||||||
|  |     "America/Dawson_Creek", | ||||||
|  |     "America/Denver", | ||||||
|  |     "America/Detroit", | ||||||
|  |     "America/Dominica", | ||||||
|  |     "America/Edmonton", | ||||||
|  |     "America/Eirunepe", | ||||||
|  |     "America/El_Salvador", | ||||||
|  |     "America/Ensenada", | ||||||
|  |     "America/Fort_Nelson", | ||||||
|  |     "America/Fort_Wayne", | ||||||
|  |     "America/Fortaleza", | ||||||
|  |     "America/Glace_Bay", | ||||||
|  |     "America/Godthab", | ||||||
|  |     "America/Goose_Bay", | ||||||
|  |     "America/Grand_Turk", | ||||||
|  |     "America/Grenada", | ||||||
|  |     "America/Guadeloupe", | ||||||
|  |     "America/Guatemala", | ||||||
|  |     "America/Guayaquil", | ||||||
|  |     "America/Guyana", | ||||||
|  |     "America/Halifax", | ||||||
|  |     "America/Havana", | ||||||
|  |     "America/Hermosillo", | ||||||
|  |     "America/Indiana/Indianapolis", | ||||||
|  |     "America/Indiana/Knox", | ||||||
|  |     "America/Indiana/Marengo", | ||||||
|  |     "America/Indiana/Petersburg", | ||||||
|  |     "America/Indiana/Tell_City", | ||||||
|  |     "America/Indiana/Vevay", | ||||||
|  |     "America/Indiana/Vincennes", | ||||||
|  |     "America/Indiana/Winamac", | ||||||
|  |     "America/Indianapolis", | ||||||
|  |     "America/Inuvik", | ||||||
|  |     "America/Iqaluit", | ||||||
|  |     "America/Jamaica", | ||||||
|  |     "America/Jujuy", | ||||||
|  |     "America/Juneau", | ||||||
|  |     "America/Kentucky/Louisville", | ||||||
|  |     "America/Kentucky/Monticello", | ||||||
|  |     "America/Knox_IN", | ||||||
|  |     "America/Kralendijk", | ||||||
|  |     "America/La_Paz", | ||||||
|  |     "America/Lima", | ||||||
|  |     "America/Los_Angeles", | ||||||
|  |     "America/Louisville", | ||||||
|  |     "America/Lower_Princes", | ||||||
|  |     "America/Maceio", | ||||||
|  |     "America/Managua", | ||||||
|  |     "America/Manaus", | ||||||
|  |     "America/Marigot", | ||||||
|  |     "America/Martinique", | ||||||
|  |     "America/Matamoros", | ||||||
|  |     "America/Mazatlan", | ||||||
|  |     "America/Mendoza", | ||||||
|  |     "America/Menominee", | ||||||
|  |     "America/Merida", | ||||||
|  |     "America/Metlakatla", | ||||||
|  |     "America/Mexico_City", | ||||||
|  |     "America/Miquelon", | ||||||
|  |     "America/Moncton", | ||||||
|  |     "America/Monterrey", | ||||||
|  |     "America/Montevideo", | ||||||
|  |     "America/Montreal", | ||||||
|  |     "America/Montserrat", | ||||||
|  |     "America/Nassau", | ||||||
|  |     "America/New_York", | ||||||
|  |     "America/Nipigon", | ||||||
|  |     "America/Nome", | ||||||
|  |     "America/Noronha", | ||||||
|  |     "America/North_Dakota/Beulah", | ||||||
|  |     "America/North_Dakota/Center", | ||||||
|  |     "America/North_Dakota/New_Salem", | ||||||
|  |     "America/Nuuk", | ||||||
|  |     "America/Ojinaga", | ||||||
|  |     "America/Panama", | ||||||
|  |     "America/Pangnirtung", | ||||||
|  |     "America/Paramaribo", | ||||||
|  |     "America/Phoenix", | ||||||
|  |     "America/Port-au-Prince", | ||||||
|  |     "America/Port_of_Spain", | ||||||
|  |     "America/Porto_Acre", | ||||||
|  |     "America/Porto_Velho", | ||||||
|  |     "America/Puerto_Rico", | ||||||
|  |     "America/Punta_Arenas", | ||||||
|  |     "America/Rainy_River", | ||||||
|  |     "America/Rankin_Inlet", | ||||||
|  |     "America/Recife", | ||||||
|  |     "America/Regina", | ||||||
|  |     "America/Resolute", | ||||||
|  |     "America/Rio_Branco", | ||||||
|  |     "America/Rosario", | ||||||
|  |     "America/Santa_Isabel", | ||||||
|  |     "America/Santarem", | ||||||
|  |     "America/Santiago", | ||||||
|  |     "America/Santo_Domingo", | ||||||
|  |     "America/Sao_Paulo", | ||||||
|  |     "America/Scoresbysund", | ||||||
|  |     "America/Shiprock", | ||||||
|  |     "America/Sitka", | ||||||
|  |     "America/St_Barthelemy", | ||||||
|  |     "America/St_Johns", | ||||||
|  |     "America/St_Kitts", | ||||||
|  |     "America/St_Lucia", | ||||||
|  |     "America/St_Thomas", | ||||||
|  |     "America/St_Vincent", | ||||||
|  |     "America/Swift_Current", | ||||||
|  |     "America/Tegucigalpa", | ||||||
|  |     "America/Thule", | ||||||
|  |     "America/Thunder_Bay", | ||||||
|  |     "America/Tijuana", | ||||||
|  |     "America/Toronto", | ||||||
|  |     "America/Tortola", | ||||||
|  |     "America/Vancouver", | ||||||
|  |     "America/Virgin", | ||||||
|  |     "America/Whitehorse", | ||||||
|  |     "America/Winnipeg", | ||||||
|  |     "America/Yakutat", | ||||||
|  |     "America/Yellowknife", | ||||||
|  |     "Antarctica/Casey", | ||||||
|  |     "Antarctica/Davis", | ||||||
|  |     "Antarctica/DumontDUrville", | ||||||
|  |     "Antarctica/Macquarie", | ||||||
|  |     "Antarctica/Mawson", | ||||||
|  |     "Antarctica/McMurdo", | ||||||
|  |     "Antarctica/Palmer", | ||||||
|  |     "Antarctica/Rothera", | ||||||
|  |     "Antarctica/South_Pole", | ||||||
|  |     "Antarctica/Syowa", | ||||||
|  |     "Antarctica/Troll", | ||||||
|  |     "Antarctica/Vostok", | ||||||
|  |     "Arctic/Longyearbyen", | ||||||
|  |     "Asia/Aden", | ||||||
|  |     "Asia/Almaty", | ||||||
|  |     "Asia/Amman", | ||||||
|  |     "Asia/Anadyr", | ||||||
|  |     "Asia/Aqtau", | ||||||
|  |     "Asia/Aqtobe", | ||||||
|  |     "Asia/Ashgabat", | ||||||
|  |     "Asia/Ashkhabad", | ||||||
|  |     "Asia/Atyrau", | ||||||
|  |     "Asia/Baghdad", | ||||||
|  |     "Asia/Bahrain", | ||||||
|  |     "Asia/Baku", | ||||||
|  |     "Asia/Bangkok", | ||||||
|  |     "Asia/Barnaul", | ||||||
|  |     "Asia/Beirut", | ||||||
|  |     "Asia/Bishkek", | ||||||
|  |     "Asia/Brunei", | ||||||
|  |     "Asia/Calcutta", | ||||||
|  |     "Asia/Chita", | ||||||
|  |     "Asia/Choibalsan", | ||||||
|  |     "Asia/Chongqing", | ||||||
|  |     "Asia/Chungking", | ||||||
|  |     "Asia/Colombo", | ||||||
|  |     "Asia/Dacca", | ||||||
|  |     "Asia/Damascus", | ||||||
|  |     "Asia/Dhaka", | ||||||
|  |     "Asia/Dili", | ||||||
|  |     "Asia/Dubai", | ||||||
|  |     "Asia/Dushanbe", | ||||||
|  |     "Asia/Famagusta", | ||||||
|  |     "Asia/Gaza", | ||||||
|  |     "Asia/Harbin", | ||||||
|  |     "Asia/Hebron", | ||||||
|  |     "Asia/Ho_Chi_Minh", | ||||||
|  |     "Asia/Hong_Kong", | ||||||
|  |     "Asia/Hovd", | ||||||
|  |     "Asia/Irkutsk", | ||||||
|  |     "Asia/Istanbul", | ||||||
|  |     "Asia/Jakarta", | ||||||
|  |     "Asia/Jayapura", | ||||||
|  |     "Asia/Jerusalem", | ||||||
|  |     "Asia/Kabul", | ||||||
|  |     "Asia/Kamchatka", | ||||||
|  |     "Asia/Karachi", | ||||||
|  |     "Asia/Kashgar", | ||||||
|  |     "Asia/Kathmandu", | ||||||
|  |     "Asia/Katmandu", | ||||||
|  |     "Asia/Khandyga", | ||||||
|  |     "Asia/Kolkata", | ||||||
|  |     "Asia/Krasnoyarsk", | ||||||
|  |     "Asia/Kuala_Lumpur", | ||||||
|  |     "Asia/Kuching", | ||||||
|  |     "Asia/Kuwait", | ||||||
|  |     "Asia/Macao", | ||||||
|  |     "Asia/Macau", | ||||||
|  |     "Asia/Magadan", | ||||||
|  |     "Asia/Makassar", | ||||||
|  |     "Asia/Manila", | ||||||
|  |     "Asia/Muscat", | ||||||
|  |     "Asia/Nicosia", | ||||||
|  |     "Asia/Novokuznetsk", | ||||||
|  |     "Asia/Novosibirsk", | ||||||
|  |     "Asia/Omsk", | ||||||
|  |     "Asia/Oral", | ||||||
|  |     "Asia/Phnom_Penh", | ||||||
|  |     "Asia/Pontianak", | ||||||
|  |     "Asia/Pyongyang", | ||||||
|  |     "Asia/Qatar", | ||||||
|  |     "Asia/Qostanay", | ||||||
|  |     "Asia/Qyzylorda", | ||||||
|  |     "Asia/Rangoon", | ||||||
|  |     "Asia/Riyadh", | ||||||
|  |     "Asia/Saigon", | ||||||
|  |     "Asia/Sakhalin", | ||||||
|  |     "Asia/Samarkand", | ||||||
|  |     "Asia/Seoul", | ||||||
|  |     "Asia/Shanghai", | ||||||
|  |     "Asia/Singapore", | ||||||
|  |     "Asia/Srednekolymsk", | ||||||
|  |     "Asia/Taipei", | ||||||
|  |     "Asia/Tashkent", | ||||||
|  |     "Asia/Tbilisi", | ||||||
|  |     "Asia/Tehran", | ||||||
|  |     "Asia/Tel_Aviv", | ||||||
|  |     "Asia/Thimbu", | ||||||
|  |     "Asia/Thimphu", | ||||||
|  |     "Asia/Tokyo", | ||||||
|  |     "Asia/Tomsk", | ||||||
|  |     "Asia/Ujung_Pandang", | ||||||
|  |     "Asia/Ulaanbaatar", | ||||||
|  |     "Asia/Ulan_Bator", | ||||||
|  |     "Asia/Urumqi", | ||||||
|  |     "Asia/Ust-Nera", | ||||||
|  |     "Asia/Vientiane", | ||||||
|  |     "Asia/Vladivostok", | ||||||
|  |     "Asia/Yakutsk", | ||||||
|  |     "Asia/Yangon", | ||||||
|  |     "Asia/Yekaterinburg", | ||||||
|  |     "Asia/Yerevan", | ||||||
|  |     "Atlantic/Azores", | ||||||
|  |     "Atlantic/Bermuda", | ||||||
|  |     "Atlantic/Canary", | ||||||
|  |     "Atlantic/Cape_Verde", | ||||||
|  |     "Atlantic/Faeroe", | ||||||
|  |     "Atlantic/Faroe", | ||||||
|  |     "Atlantic/Jan_Mayen", | ||||||
|  |     "Atlantic/Madeira", | ||||||
|  |     "Atlantic/Reykjavik", | ||||||
|  |     "Atlantic/South_Georgia", | ||||||
|  |     "Atlantic/St_Helena", | ||||||
|  |     "Atlantic/Stanley", | ||||||
|  |     "Australia/ACT", | ||||||
|  |     "Australia/Adelaide", | ||||||
|  |     "Australia/Brisbane", | ||||||
|  |     "Australia/Broken_Hill", | ||||||
|  |     "Australia/Canberra", | ||||||
|  |     "Australia/Currie", | ||||||
|  |     "Australia/Darwin", | ||||||
|  |     "Australia/Eucla", | ||||||
|  |     "Australia/Hobart", | ||||||
|  |     "Australia/LHI", | ||||||
|  |     "Australia/Lindeman", | ||||||
|  |     "Australia/Lord_Howe", | ||||||
|  |     "Australia/Melbourne", | ||||||
|  |     "Australia/North", | ||||||
|  |     "Australia/NSW", | ||||||
|  |     "Australia/Perth", | ||||||
|  |     "Australia/Queensland", | ||||||
|  |     "Australia/South", | ||||||
|  |     "Australia/Sydney", | ||||||
|  |     "Australia/Tasmania", | ||||||
|  |     "Australia/Victoria", | ||||||
|  |     "Australia/West", | ||||||
|  |     "Australia/Yancowinna", | ||||||
|  |     "Brazil/Acre", | ||||||
|  |     "Brazil/DeNoronha", | ||||||
|  |     "Brazil/East", | ||||||
|  |     "Brazil/West", | ||||||
|  |     "Canada/Atlantic", | ||||||
|  |     "Canada/Central", | ||||||
|  |     "Canada/Eastern", | ||||||
|  |     "Canada/Mountain", | ||||||
|  |     "Canada/Newfoundland", | ||||||
|  |     "Canada/Pacific", | ||||||
|  |     "Canada/Saskatchewan", | ||||||
|  |     "Canada/Yukon", | ||||||
|  |     "CET", | ||||||
|  |     "Chile/Continental", | ||||||
|  |     "Chile/EasterIsland", | ||||||
|  |     "CST6CDT", | ||||||
|  |     "Cuba", | ||||||
|  |     "EET", | ||||||
|  |     "Egypt", | ||||||
|  |     "Eire", | ||||||
|  |     "EST", | ||||||
|  |     "EST5EDT", | ||||||
|  |     "Etc/GMT", | ||||||
|  |     "Etc/GMT+0", | ||||||
|  |     "Etc/GMT+1", | ||||||
|  |     "Etc/GMT+10", | ||||||
|  |     "Etc/GMT+11", | ||||||
|  |     "Etc/GMT+12", | ||||||
|  |     "Etc/GMT+2", | ||||||
|  |     "Etc/GMT+3", | ||||||
|  |     "Etc/GMT+4", | ||||||
|  |     "Etc/GMT+5", | ||||||
|  |     "Etc/GMT+6", | ||||||
|  |     "Etc/GMT+7", | ||||||
|  |     "Etc/GMT+8", | ||||||
|  |     "Etc/GMT+9", | ||||||
|  |     "Etc/GMT-0", | ||||||
|  |     "Etc/GMT-1", | ||||||
|  |     "Etc/GMT-10", | ||||||
|  |     "Etc/GMT-11", | ||||||
|  |     "Etc/GMT-12", | ||||||
|  |     "Etc/GMT-13", | ||||||
|  |     "Etc/GMT-14", | ||||||
|  |     "Etc/GMT-2", | ||||||
|  |     "Etc/GMT-3", | ||||||
|  |     "Etc/GMT-4", | ||||||
|  |     "Etc/GMT-5", | ||||||
|  |     "Etc/GMT-6", | ||||||
|  |     "Etc/GMT-7", | ||||||
|  |     "Etc/GMT-8", | ||||||
|  |     "Etc/GMT-9", | ||||||
|  |     "Etc/GMT0", | ||||||
|  |     "Etc/Greenwich", | ||||||
|  |     "Etc/UCT", | ||||||
|  |     "Etc/Universal", | ||||||
|  |     "Etc/UTC", | ||||||
|  |     "Etc/Zulu", | ||||||
|  |     "Europe/Amsterdam", | ||||||
|  |     "Europe/Andorra", | ||||||
|  |     "Europe/Astrakhan", | ||||||
|  |     "Europe/Athens", | ||||||
|  |     "Europe/Belfast", | ||||||
|  |     "Europe/Belgrade", | ||||||
|  |     "Europe/Berlin", | ||||||
|  |     "Europe/Bratislava", | ||||||
|  |     "Europe/Brussels", | ||||||
|  |     "Europe/Bucharest", | ||||||
|  |     "Europe/Budapest", | ||||||
|  |     "Europe/Busingen", | ||||||
|  |     "Europe/Chisinau", | ||||||
|  |     "Europe/Copenhagen", | ||||||
|  |     "Europe/Dublin", | ||||||
|  |     "Europe/Gibraltar", | ||||||
|  |     "Europe/Guernsey", | ||||||
|  |     "Europe/Helsinki", | ||||||
|  |     "Europe/Isle_of_Man", | ||||||
|  |     "Europe/Istanbul", | ||||||
|  |     "Europe/Jersey", | ||||||
|  |     "Europe/Kaliningrad", | ||||||
|  |     "Europe/Kiev", | ||||||
|  |     "Europe/Kirov", | ||||||
|  |     "Europe/Kyiv", | ||||||
|  |     "Europe/Lisbon", | ||||||
|  |     "Europe/Ljubljana", | ||||||
|  |     "Europe/London", | ||||||
|  |     "Europe/Luxembourg", | ||||||
|  |     "Europe/Madrid", | ||||||
|  |     "Europe/Malta", | ||||||
|  |     "Europe/Mariehamn", | ||||||
|  |     "Europe/Minsk", | ||||||
|  |     "Europe/Monaco", | ||||||
|  |     "Europe/Moscow", | ||||||
|  |     "Europe/Nicosia", | ||||||
|  |     "Europe/Oslo", | ||||||
|  |     "Europe/Paris", | ||||||
|  |     "Europe/Podgorica", | ||||||
|  |     "Europe/Prague", | ||||||
|  |     "Europe/Riga", | ||||||
|  |     "Europe/Rome", | ||||||
|  |     "Europe/Samara", | ||||||
|  |     "Europe/San_Marino", | ||||||
|  |     "Europe/Sarajevo", | ||||||
|  |     "Europe/Saratov", | ||||||
|  |     "Europe/Simferopol", | ||||||
|  |     "Europe/Skopje", | ||||||
|  |     "Europe/Sofia", | ||||||
|  |     "Europe/Stockholm", | ||||||
|  |     "Europe/Tallinn", | ||||||
|  |     "Europe/Tirane", | ||||||
|  |     "Europe/Tiraspol", | ||||||
|  |     "Europe/Ulyanovsk", | ||||||
|  |     "Europe/Uzhgorod", | ||||||
|  |     "Europe/Vaduz", | ||||||
|  |     "Europe/Vatican", | ||||||
|  |     "Europe/Vienna", | ||||||
|  |     "Europe/Vilnius", | ||||||
|  |     "Europe/Volgograd", | ||||||
|  |     "Europe/Warsaw", | ||||||
|  |     "Europe/Zagreb", | ||||||
|  |     "Europe/Zaporozhye", | ||||||
|  |     "Europe/Zurich", | ||||||
|  |     "Factory", | ||||||
|  |     "GB", | ||||||
|  |     "GB-Eire", | ||||||
|  |     "GMT", | ||||||
|  |     "GMT+0", | ||||||
|  |     "GMT-0", | ||||||
|  |     "GMT0", | ||||||
|  |     "Greenwich", | ||||||
|  |     "Hongkong", | ||||||
|  |     "HST", | ||||||
|  |     "Iceland", | ||||||
|  |     "Indian/Antananarivo", | ||||||
|  |     "Indian/Chagos", | ||||||
|  |     "Indian/Christmas", | ||||||
|  |     "Indian/Cocos", | ||||||
|  |     "Indian/Comoro", | ||||||
|  |     "Indian/Kerguelen", | ||||||
|  |     "Indian/Mahe", | ||||||
|  |     "Indian/Maldives", | ||||||
|  |     "Indian/Mauritius", | ||||||
|  |     "Indian/Mayotte", | ||||||
|  |     "Indian/Reunion", | ||||||
|  |     "Iran", | ||||||
|  |     "Israel", | ||||||
|  |     "Jamaica", | ||||||
|  |     "Japan", | ||||||
|  |     "Kwajalein", | ||||||
|  |     "Libya", | ||||||
|  |     "MET", | ||||||
|  |     "Mexico/BajaNorte", | ||||||
|  |     "Mexico/BajaSur", | ||||||
|  |     "Mexico/General", | ||||||
|  |     "MST", | ||||||
|  |     "MST7MDT", | ||||||
|  |     "Navajo", | ||||||
|  |     "NZ", | ||||||
|  |     "NZ-CHAT", | ||||||
|  |     "Pacific/Apia", | ||||||
|  |     "Pacific/Auckland", | ||||||
|  |     "Pacific/Bougainville", | ||||||
|  |     "Pacific/Chatham", | ||||||
|  |     "Pacific/Chuuk", | ||||||
|  |     "Pacific/Easter", | ||||||
|  |     "Pacific/Efate", | ||||||
|  |     "Pacific/Enderbury", | ||||||
|  |     "Pacific/Fakaofo", | ||||||
|  |     "Pacific/Fiji", | ||||||
|  |     "Pacific/Funafuti", | ||||||
|  |     "Pacific/Galapagos", | ||||||
|  |     "Pacific/Gambier", | ||||||
|  |     "Pacific/Guadalcanal", | ||||||
|  |     "Pacific/Guam", | ||||||
|  |     "Pacific/Honolulu", | ||||||
|  |     "Pacific/Johnston", | ||||||
|  |     "Pacific/Kanton", | ||||||
|  |     "Pacific/Kiritimati", | ||||||
|  |     "Pacific/Kosrae", | ||||||
|  |     "Pacific/Kwajalein", | ||||||
|  |     "Pacific/Majuro", | ||||||
|  |     "Pacific/Marquesas", | ||||||
|  |     "Pacific/Midway", | ||||||
|  |     "Pacific/Nauru", | ||||||
|  |     "Pacific/Niue", | ||||||
|  |     "Pacific/Norfolk", | ||||||
|  |     "Pacific/Noumea", | ||||||
|  |     "Pacific/Pago_Pago", | ||||||
|  |     "Pacific/Palau", | ||||||
|  |     "Pacific/Pitcairn", | ||||||
|  |     "Pacific/Pohnpei", | ||||||
|  |     "Pacific/Ponape", | ||||||
|  |     "Pacific/Port_Moresby", | ||||||
|  |     "Pacific/Rarotonga", | ||||||
|  |     "Pacific/Saipan", | ||||||
|  |     "Pacific/Samoa", | ||||||
|  |     "Pacific/Tahiti", | ||||||
|  |     "Pacific/Tarawa", | ||||||
|  |     "Pacific/Tongatapu", | ||||||
|  |     "Pacific/Truk", | ||||||
|  |     "Pacific/Wake", | ||||||
|  |     "Pacific/Wallis", | ||||||
|  |     "Pacific/Yap", | ||||||
|  |     "Poland", | ||||||
|  |     "Portugal", | ||||||
|  |     "PRC", | ||||||
|  |     "PST8PDT", | ||||||
|  |     "ROC", | ||||||
|  |     "ROK", | ||||||
|  |     "Singapore", | ||||||
|  |     "Turkey", | ||||||
|  |     "UCT", | ||||||
|  |     "Universal", | ||||||
|  |     "US/Alaska", | ||||||
|  |     "US/Aleutian", | ||||||
|  |     "US/Arizona", | ||||||
|  |     "US/Central", | ||||||
|  |     "US/East-Indiana", | ||||||
|  |     "US/Eastern", | ||||||
|  |     "US/Hawaii", | ||||||
|  |     "US/Indiana-Starke", | ||||||
|  |     "US/Michigan", | ||||||
|  |     "US/Mountain", | ||||||
|  |     "US/Pacific", | ||||||
|  |     "US/Samoa", | ||||||
|  |     "UTC", | ||||||
|  |     "W-SU", | ||||||
|  |     "WET", | ||||||
|  |     "Zulu", | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | export default TIME_ZONES; | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user