├── src
├── Libraries
│ ├── .gitkeep
│ └── IpUtils.php
├── Views
│ └── errors
│ │ ├── cli
│ │ └── error_503.php
│ │ └── html
│ │ └── error_503.php
├── Language
│ ├── en
│ │ └── MaintenanceMode.php
│ └── de
│ │ └── MaintenanceMode.php
├── Exceptions
│ ├── ExceptionInterface.php
│ └── ServiceUnavailableException.php
├── Config
│ └── MaintenanceMode.php
├── Commands
│ ├── Up.php
│ ├── Status.php
│ ├── Down.php
│ └── Publish.php
├── Filters
│ └── MaintenanceMode.php
└── Controllers
│ └── MaintenanceMode.php
├── .gitignore
├── composer.json
└── README.md
/src/Libraries/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 |
3 | .DS_Store
4 | *.DS_Store
5 |
--------------------------------------------------------------------------------
/src/Views/errors/cli/error_503.php:
--------------------------------------------------------------------------------
1 | "503 - Service Unavailable",
5 | 'serverDowMessage' => "Sorry! We'll be back. We're busy updating the server for you and will be back soon.",
6 | ];
7 |
--------------------------------------------------------------------------------
/src/Language/de/MaintenanceMode.php:
--------------------------------------------------------------------------------
1 | "503 - Dienst nicht verfügbar",
5 | 'serverDowMessage' => "Es tut uns leid! Wir sind damit beschäftigt, den Server für Sie zu aktualisieren und werden bald wieder Online.",
6 | ];
7 |
--------------------------------------------------------------------------------
/src/Exceptions/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 | FilePath . $config->FileName);
23 |
24 | CLI::write('');
25 | CLI::write('**** Application is now live. ****', 'black', 'green');
26 | CLI::write('');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Filters/MaintenanceMode.php:
--------------------------------------------------------------------------------
1 | \CodeigniterExt\MaintenanceMode\Filters\MaintenanceMode::class,
54 | ...
55 | ]
56 | ```
57 | and add "maintenancemode" in $globals['before'] array:
58 | ```php
59 | public $globals = [
60 | 'before' => [
61 | 'maintenancemode',
62 | ...
63 | ],
64 | 'after' => [
65 | ...
66 | ],
67 | ];
68 | ```
69 |
--------------------------------------------------------------------------------
/src/Views/errors/html/error_503.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | = lang('MaintenanceMode.serverDowTitle'); ?>
6 |
7 |
70 |
71 |
72 |
73 |
= lang('MaintenanceMode.serverDowTitle'); ?>
74 |
75 |
= lang('MaintenanceMode.serverDowMessage'); ?>
76 |
77 |
78 | = esc($message) ?>
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/src/Commands/Status.php:
--------------------------------------------------------------------------------
1 | FilePath.$config->FileName)){
20 |
21 | $data = json_decode(file_get_contents($config->FilePath.$config->FileName), true);
22 |
23 | CLI::newLine(1);
24 | CLI::error('Application is already DOWN.');
25 | CLI::newLine(1);
26 |
27 | //
28 | // echo keys and values in table
29 | // without allowed_ips
30 | //
31 | $thead = [
32 | "key",
33 | "value"
34 | ];
35 |
36 | $tbody = array();
37 |
38 | foreach ($data as $key => $value) {
39 |
40 | switch ($key)
41 | {
42 | case "allowed_ips":
43 | break;
44 | case "time":
45 |
46 | $tbody[] = [$key, date('Y-m-d H:i:s', $value)];
47 | break;
48 | default:
49 | $tbody[] = [$key, $value];
50 | }
51 | }
52 |
53 | CLI::table($tbody, $thead);
54 |
55 |
56 | //
57 | // echo allowed_ips in table
58 | //
59 | $thead = ["allowed ips"];
60 |
61 | $tbody = array();
62 |
63 | foreach ($data['allowed_ips'] as $ip) {
64 | $tbody[] = [$ip];
65 | }
66 |
67 | CLI::table($tbody, $thead);
68 |
69 | CLI::newLine(1);
70 |
71 | }else{
72 | CLI::newLine(1);
73 | CLI::write('**** Application is already live. ****', 'green');
74 | CLI::newLine(1);
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Commands/Down.php:
--------------------------------------------------------------------------------
1 | FilePath . $config->FileName)) {
20 |
21 | $message = CLI::prompt("Message");
22 | $ips_str = CLI::prompt("Allowed ips [example: 0.0.0.0 127.0.0.1]");
23 |
24 | $ips_array = explode(" ", $ips_str);
25 |
26 | //
27 | // dir doesn't exist, make it
28 | //
29 | if (!is_dir($config->FilePath)) {
30 | mkdir($config->FilePath);
31 | }
32 |
33 | //
34 | // write the file with json content
35 | //
36 | file_put_contents(
37 | $config->FilePath . $config->FileName,
38 | json_encode([
39 | "time" => strtotime("now"),
40 | "message" => $message,
41 | "cookie_name" => $this->randomhash(8),
42 | "allowed_ips" => $ips_array
43 | ], JSON_PRETTY_PRINT)
44 | );
45 |
46 | CLI::newLine(1);
47 | CLI::write('**** Application is now DOWN. ****', 'white', 'red');
48 | CLI::newLine(1);
49 |
50 | $this->call('mm:status');
51 |
52 | }else{
53 | CLI::newLine(1);
54 | CLI::error('**** Application is already DOWN. ****');
55 | CLI::newLine(1);
56 | }
57 | }
58 |
59 | function randomhash($len = 8){
60 | $seed = str_split('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
61 | shuffle($seed);
62 | $rand = '';
63 |
64 | foreach (array_rand($seed, $len) as $k)
65 | {
66 | $rand .= $seed[$k];
67 | }
68 |
69 | return $rand;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Controllers/MaintenanceMode.php:
--------------------------------------------------------------------------------
1 | getConfig();
38 |
39 | $donwFilePath = $config->FilePath . $config->FileName;
40 |
41 | //
42 | // if donw file does not exist app should keep running
43 | //
44 | if (!file_exists($donwFilePath)) {
45 | return true;
46 | }
47 |
48 |
49 | //
50 | // get all json data from donw file
51 | //
52 | $data = json_decode(file_get_contents($donwFilePath), true);
53 |
54 |
55 | //
56 | // if request ip was entered in allowed_ips
57 | // the app should continue running
58 | //
59 | $lib = new IpUtils();
60 | if ($lib->checkIp(Services::request()->getIPAddress(), $data["allowed_ips"])) {
61 | return true;
62 | }
63 |
64 | //
65 | // if user's browser has been used the cookie pass
66 | // the app should continue running
67 | //
68 | helper('cookie');
69 | $cookieName = get_cookie($data["cookie_name"]);
70 |
71 | if($cookieName == $data["cookie_name"]){
72 | return true;
73 | }
74 |
75 | throw ServiceUnavailableException::forServerDow($data["message"]);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Commands/Publish.php:
--------------------------------------------------------------------------------
1 | determineSourcePath();
28 |
29 | // Views
30 | if (CLI::prompt('Publish Views?', ['y', 'n']) == 'y')
31 | {
32 | $map = false;
33 | $map = directory_map($this->sourcePath . '/Views/errors/cli');
34 | $this->publishViews($map, 'errors/cli/');
35 |
36 | $map = false;
37 | $map = directory_map($this->sourcePath . '/Views/errors/html');
38 | $this->publishViews($map, 'errors/html/');
39 | }
40 |
41 | // Config
42 | if (CLI::prompt('Publish Config file?', ['y', 'n']) == 'y')
43 | {
44 | $this->publishConfig();
45 | }
46 | }
47 |
48 | protected function publishViews($map, $subfolder)
49 | {
50 |
51 | $prefix = '';
52 |
53 | foreach ($map as $key => $view)
54 | {
55 | if (is_array($view))
56 | {
57 | $oldPrefix = $prefix;
58 | $prefix .= $key;
59 |
60 | foreach ($view as $file)
61 | {
62 | $this->publishView($file, $prefix, $subfolder);
63 | }
64 |
65 | $prefix = $oldPrefix;
66 |
67 | continue;
68 | }
69 |
70 | $this->publishView($view, $prefix, $subfolder);
71 | }
72 | }
73 |
74 | protected function publishView($view, string $prefix = '', string $subfolder = '')
75 | {
76 | $path = "{$this->sourcePath}/Views/{$subfolder}{$prefix}{$view}";
77 | $namespace = defined('APP_NAMESPACE') ? APP_NAMESPACE : 'App';
78 |
79 | $content = file_get_contents($path);
80 |
81 | $this->writeFile("Views/{$subfolder}{$prefix}{$view}", $content);
82 | }
83 |
84 | protected function publishConfig()
85 | {
86 | $path = "{$this->sourcePath}/Config/MaintenanceMode.php";
87 |
88 | $content = file_get_contents($path);
89 | $appNamespace = APP_NAMESPACE;
90 | $content = str_replace('namespace CodeigniterExt\MaintenanceMode\Config', "namespace {$appNamespace}\Config", $content);
91 |
92 | $this->writeFile("Config/MaintenanceMode.php", $content);
93 | }
94 |
95 | /**
96 | * Determines the current source path from which all other files are located.
97 | */
98 | protected function determineSourcePath()
99 | {
100 | $this->sourcePath = realpath(__DIR__ . '/../');
101 |
102 | if ($this->sourcePath == '/' || empty($this->sourcePath))
103 | {
104 | CLI::error('Unable to determine the correct source directory. Bailing.');
105 | exit();
106 | }
107 | }
108 |
109 | /**
110 | * Write a file, catching any exceptions and showing a
111 | * nicely formatted error.
112 | *
113 | * @param string $path
114 | * @param string $content
115 | */
116 | protected function writeFile(string $path, string $content)
117 | {
118 | $config = new Autoload();
119 | $appPath = $config->psr4[APP_NAMESPACE];
120 |
121 | $directory = dirname($appPath . $path);
122 |
123 | if (! is_dir($directory))
124 | {
125 | mkdir($directory);
126 | }
127 |
128 | try
129 | {
130 | write_file($appPath . $path, $content);
131 | }
132 | catch (\Exception $e)
133 | {
134 | $this->showError($e);
135 | exit();
136 | }
137 |
138 | $path = str_replace($appPath, '', $path);
139 |
140 | CLI::write(CLI::color(' created: ', 'green') . $path);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Libraries/IpUtils.php:
--------------------------------------------------------------------------------
1 | 1 ? 'checkIp6' : 'checkIp4';
31 |
32 | foreach ($ips as $ip) {
33 | if (self::$method($requestIp, $ip)) {
34 | return true;
35 | }
36 | }
37 |
38 | return false;
39 | }
40 |
41 | /**
42 | * Compares two IPv4 addresses.
43 | * In case a subnet is given, it checks if it contains the request IP.
44 | *
45 | * @param string $requestIp IPv4 address to check
46 | * @param string $ip IPv4 address or subnet in CIDR notation
47 | *
48 | * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet
49 | */
50 | public static function checkIp4($requestIp, $ip)
51 | {
52 | $cacheKey = $requestIp.'-'.$ip;
53 | if (isset(self::$checkedIps[$cacheKey])) {
54 | return self::$checkedIps[$cacheKey];
55 | }
56 |
57 | if (!filter_var($requestIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
58 | return self::$checkedIps[$cacheKey] = false;
59 | }
60 |
61 | if (false !== strpos($ip, '/')) {
62 | list($address, $netmask) = explode('/', $ip, 2);
63 |
64 | if ('0' === $netmask) {
65 | return self::$checkedIps[$cacheKey] = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
66 | }
67 |
68 | if ($netmask < 0 || $netmask > 32) {
69 | return self::$checkedIps[$cacheKey] = false;
70 | }
71 | } else {
72 | $address = $ip;
73 | $netmask = 32;
74 | }
75 |
76 | if (false === ip2long($address)) {
77 | return self::$checkedIps[$cacheKey] = false;
78 | }
79 |
80 | return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
81 | }
82 |
83 | /**
84 | * Compares two IPv6 addresses.
85 | * In case a subnet is given, it checks if it contains the request IP.
86 | *
87 | * @author David Soria Parra
88 | *
89 | * @see https://github.com/dsp/v6tools
90 | *
91 | * @param string $requestIp IPv6 address to check
92 | * @param string $ip IPv6 address or subnet in CIDR notation
93 | *
94 | * @return bool Whether the IP is valid
95 | *
96 | * @throws \RuntimeException When IPV6 support is not enabled
97 | */
98 | public static function checkIp6($requestIp, $ip)
99 | {
100 | $cacheKey = $requestIp.'-'.$ip;
101 | if (isset(self::$checkedIps[$cacheKey])) {
102 | return self::$checkedIps[$cacheKey];
103 | }
104 |
105 | if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) {
106 | throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".');
107 | }
108 |
109 | if (false !== strpos($ip, '/')) {
110 | list($address, $netmask) = explode('/', $ip, 2);
111 |
112 | if ('0' === $netmask) {
113 | return (bool) unpack('n*', @inet_pton($address));
114 | }
115 |
116 | if ($netmask < 1 || $netmask > 128) {
117 | return self::$checkedIps[$cacheKey] = false;
118 | }
119 | } else {
120 | $address = $ip;
121 | $netmask = 128;
122 | }
123 |
124 | $bytesAddr = unpack('n*', @inet_pton($address));
125 | $bytesTest = unpack('n*', @inet_pton($requestIp));
126 |
127 | if (!$bytesAddr || !$bytesTest) {
128 | return self::$checkedIps[$cacheKey] = false;
129 | }
130 |
131 | for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
132 | $left = $netmask - 16 * ($i - 1);
133 | $left = ($left <= 16) ? $left : 16;
134 | $mask = ~(0xffff >> $left) & 0xffff;
135 | if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
136 | return self::$checkedIps[$cacheKey] = false;
137 | }
138 | }
139 |
140 | return self::$checkedIps[$cacheKey] = true;
141 | }
142 | }
--------------------------------------------------------------------------------