├── 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 |

74 | 75 |

76 |

77 | 78 | 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 | } --------------------------------------------------------------------------------