├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── docs ├── 00_Overview.md └── 01_How_It_Works.md ├── favicon.ico ├── hcp.code-workspace ├── index.php └── lib ├── css ├── app-dark.css ├── app.css ├── bootstrap-table.css ├── bootstrap.min.css ├── color-modes.css ├── fonts │ ├── bootstrap-icons.css │ ├── bootstrap-icons.woff2 │ ├── nunito-latin-400-normal.woff2 │ ├── nunito-latin-600-normal.woff2 │ └── nunito-latin-700-normal.woff2 ├── style.css └── table-datatable.css ├── img ├── Blank-300x150.png ├── favicon.svg └── logo.svg ├── js ├── app.js ├── bootstrap-editable.js ├── bootstrap-table-editable.js ├── bootstrap-table-export.js ├── bootstrap-table.js ├── bootstrap.bundle.min.js ├── chart.umd.min.js ├── color-modes.js ├── dark.js ├── initTheme.js ├── perfect-scrollbar.min.js ├── simple-datatables.js ├── simple-umd-datatables.js └── tableExport.js └── php ├── .editorconfig ├── .vscode ├── extensions.json └── settings.json ├── db.php ├── home.tpl.example ├── init.php ├── plugin.php ├── plugins ├── accounts.php ├── auth.php ├── dkim.php ├── domains.php ├── home.php ├── infomail.php ├── infosys.php ├── processes.php ├── records.php ├── sshm.php ├── valias.php ├── vhosts.php └── vmails.php ├── theme.php ├── themes ├── bootstrap4 │ ├── accounts.php │ ├── auth.php │ ├── dkim.php │ ├── domains.php │ ├── home.php │ ├── infomail.php │ ├── infosys.php │ ├── processes.php │ ├── records.php │ ├── theme.php │ ├── valias.php │ ├── vhosts.php │ └── vmails.php └── bootstrap5 │ ├── accounts.php │ ├── auth.php │ ├── dkim.php │ ├── domains.php │ ├── home.php │ ├── infomail.php │ ├── infosys.php │ ├── processes.php │ ├── records.php │ ├── sshm.php │ ├── theme.php │ ├── valias.php │ ├── vhosts.php │ └── vmails.php └── util.php /.gitignore: -------------------------------------------------------------------------------- 1 | .ht* 2 | *kate-swp 3 | netserva.php 4 | adminer* 5 | phpinfo.php 6 | status/ 7 | phpmyadmin/ 8 | webmail/ 9 | lib/uploads/*.jpg 10 | whmcs/ 11 | .well-known/ 12 | rspamd/ 13 | phpliteadmin.* 14 | .php-cs-fixer.cache 15 | bs5-* 16 | .vscode 17 | hcp.code-workspace 18 | sysadm/ 19 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # ~/.sh/build.sh 20170301 - 20240904 3 | # Copyright (C) 2015-2023 Mark Constable (AGPL-3.0) 4 | 5 | [[ $1 =~ '-h' ]] && echo "Usage: [bash] build.sh [path(pwd)] 6 | 7 | Example: 8 | 9 | su - sysadm 10 | cd var/www/html/hcp 11 | bash build.sh . 12 | " && exit 1 13 | 14 | [[ $1 ]] && cd $1 15 | 16 | echo " (AGPL-3.0) 22 | // This is single script concatenation of all PHP files in lib/php at 23 | // https://github.com/markc/hcp 24 | " >netserva.php 25 | 26 | ( 27 | cat lib/php/db.php 28 | cat lib/php/init.php 29 | cat lib/php/plugin.php 30 | cat lib/php/plugins/accounts.php 31 | cat lib/php/plugins/auth.php 32 | cat lib/php/plugins/dkim.php 33 | cat lib/php/plugins/domains.php 34 | cat lib/php/plugins/home.php 35 | cat lib/php/plugins/infomail.php 36 | cat lib/php/plugins/infosys.php 37 | cat lib/php/plugins/processes.php 38 | cat lib/php/plugins/records.php 39 | cat lib/php/plugins/valias.php 40 | cat lib/php/plugins/vhosts.php 41 | cat lib/php/plugins/vmails.php 42 | cat lib/php/theme.php 43 | cat lib/php/themes/bootstrap5/theme.php 44 | cat lib/php/themes/bootstrap5/accounts.php 45 | cat lib/php/themes/bootstrap5/auth.php 46 | cat lib/php/themes/bootstrap5/dkim.php 47 | cat lib/php/themes/bootstrap5/domains.php 48 | cat lib/php/themes/bootstrap5/home.php 49 | cat lib/php/themes/bootstrap5/infomail.php 50 | cat lib/php/themes/bootstrap5/infosys.php 51 | cat lib/php/themes/bootstrap5/processes.php 52 | cat lib/php/themes/bootstrap5/records.php 53 | cat lib/php/themes/bootstrap5/valias.php 54 | cat lib/php/themes/bootstrap5/vhosts.php 55 | cat lib/php/themes/bootstrap5/vmails.php 56 | cat lib/php/util.php 57 | cat index.php 58 | ) | sed \ 59 | -e '/^?>/d' \ 60 | -e '/^>netserva.php 64 | 65 | chmod 640 netserva.php 66 | -------------------------------------------------------------------------------- /docs/00_Overview.md: -------------------------------------------------------------------------------- 1 | Here's a refactored and streamlined version of the documentation: 2 | 3 | # NetServa HCP: Architecture Overview 4 | 5 | ## Core Concepts 6 | 7 | 1. **Single Entry Point (index.php)** 8 | - Acts as the front controller 9 | - Contains global configuration 10 | - Implements autoloading via spl_autoload_register() 11 | 12 | 2. **Global Object ($this->g)** 13 | - Stores configuration, input, and output data 14 | - $this->g->out accumulates content for final rendering 15 | 16 | 3. **Encapsulated Rendering** 17 | - Plugin and theme classes generate HTML content 18 | - Content is assigned to $this->g->out['main'] and other keys 19 | 20 | 4. **Flexible Output** 21 | - Init class __toString() method handles final rendering 22 | - Supports HTML, plain text, and JSON outputs 23 | - Enables easy API integration 24 | 25 | ## Key Features 26 | 27 | - **Modular Structure**: Separate classes for different functionalities 28 | - **Configuration Override**: Optional lib/.ht_conf.php for environment-specific settings 29 | - **Autoloading**: Dynamically loads classes based on naming conventions 30 | - **Single Output**: Accumulates content before sending to browser 31 | 32 | ## Security Measures 33 | 34 | - **Centralized Request Handling**: All requests processed through index.php 35 | - **Nginx Protection**: Rule blocks direct access to .ht* files, including lib/.ht_conf.php 36 | - **Layered Approach**: Combines server-level and application-level security 37 | 38 | ## Benefits 39 | 40 | - Separation of concerns 41 | - Flexibility in output formats 42 | - Modular and maintainable codebase 43 | - Balanced security and convenience 44 | - Efficient performance through single output 45 | 46 | This architecture creates a robust, flexible, and secure foundation for web application development, suitable for various environments and scalable for larger projects. -------------------------------------------------------------------------------- /docs/01_How_It_Works.md: -------------------------------------------------------------------------------- 1 | Certainly. I'll expand on these concepts based on how the netserva.php script works: 2 | 3 | # NetServa HCP: Detailed Architecture and Security Overview 4 | 5 | ## Core Architecture 6 | 7 | 1. **Single Entry Point (index.php)** 8 | - In Netserva PHP, this is implemented at the bottom of the file. 9 | - Defines constants like DS (Directory Separator) and INC (Include Path). 10 | - Sets up the autoloader using spl_autoload_register(). 11 | - Initializes the main application by creating an instance of the Init class. 12 | - Example: 13 | ```php 14 | $config = new Config(); 15 | echo new Init($config); 16 | ``` 17 | 18 | 2. **Global Object ($this->g)** 19 | - Implemented as an anonymous class passed to the Init constructor. 20 | - Contains crucial arrays like $cfg (configuration), $in (input), $out (output), $db (database settings), $nav1 and $nav2 (navigation), $dns (DNS settings), and $acl (Access Control Levels). 21 | - These arrays hold all the necessary data for the application to function, providing a centralized data structure. 22 | 23 | 3. **Encapsulated Rendering** 24 | - Each plugin (e.g., Plugins_Accounts, Plugins_Auth) and theme (e.g., Themes_Bootstrap_Theme) class contains methods for generating specific parts of the HTML output. 25 | - Methods typically return strings of HTML content. 26 | - Content is assigned to $this->g->out['main'] or other relevant keys. 27 | - Example from Themes_Bootstrap_Home: 28 | ```php 29 | public function list(array $in): string 30 | { 31 | return << 33 | HTML; 34 | } 35 | ``` 36 | 37 | 4. **Flexible Output** 38 | - The Init class's __toString() method handles final rendering. 39 | - Checks $this->g->in['x'] to determine output format (HTML, text, or JSON). 40 | - For HTML, it calls $this->g->t->html() to render the full page. 41 | - For text, it strips HTML tags from the main content. 42 | - For JSON, it encodes the relevant data. 43 | - Allows for easy API integration by returning JSON when requested. 44 | 45 | ## Key Features 46 | 47 | - **Modular Structure**: 48 | - The script is divided into multiple classes (Db, Init, Plugin, various Plugins_* classes, Theme, various Themes_* classes). 49 | - Each class handles a specific aspect of functionality, promoting code organization and reusability. 50 | 51 | - **Configuration Override**: 52 | - The $cfg array in the global object includes a 'file' key pointing to 'lib/.ht_conf.php'. 53 | - This file, if it exists, can override default settings without modifying the main script. 54 | 55 | - **Autoloading**: 56 | - The autoloader function dynamically loads class files based on the class name. 57 | - It converts class names to file paths, allowing for a clean and organized file structure. 58 | - Example: 59 | ```php 60 | spl_autoload_register(function (string $className): void { 61 | $filePath = INC . str_replace(['\\', '_'], [DS, DS], strtolower($className)) . '.php'; 62 | if (DBG) { error_log("filePath=$filePath"); } 63 | if (!is_file($filePath)) { 64 | throw new \LogicException("Class $className not found"); 65 | } 66 | require $filePath; 67 | }); 68 | ``` 69 | 70 | - **Single Output**: 71 | - Content is accumulated in $this->g->out throughout script execution. 72 | - The Init::__toString() method renders all accumulated content at once, improving performance and allowing for proper header setting. 73 | 74 | ## Security Measures 75 | 76 | - **Centralized Request Handling**: 77 | - All requests go through index.php, allowing for consistent security checks and input validation. 78 | 79 | - **Nginx Protection**: 80 | - An Nginx rule (not visible in the PHP code) blocks direct access to .ht* files, including lib/.ht_conf.php. 81 | - This protects sensitive configuration data from being directly accessed via web requests. 82 | 83 | - **Layered Approach**: 84 | - Combines server-level protection (Nginx rules) with application-level security measures implemented throughout the PHP code. 85 | 86 | - **Configuration Management**: 87 | - Sensitive information like database passwords can be stored in separate, protected files (e.g., 'lib/.ht_pw' for database password). 88 | 89 | - **File Permissions**: 90 | - Relies on proper server configuration and file permissions to restrict access to sensitive files. 91 | 92 | ## Benefits 93 | 94 | - **Separation of Concerns**: Content generation (in plugins and themes) is separated from output formatting (in Init::__toString()). 95 | - **Flexibility**: The same core logic can output different formats (HTML, text, JSON) without major changes. 96 | - **Modularity**: Different parts of the application (plugins, themes) can contribute to the output independently. 97 | - **Performance**: Accumulating content before sending reduces the number of writes to the output buffer. 98 | - **Security**: The layered security approach provides robust protection for sensitive data and configurations. 99 | 100 | This architecture in Netserva PHP demonstrates a super lightweight yetsophisticated approach to web application development, balancing flexibility, security, and performance. It's particularly well-suited for applications that need to handle various types of output and integrate with different environments or APIs. 101 | 102 | ## More details 103 | 104 | 1. Autoloading: 105 | The code uses a custom autoloader to dynamically load class files based on their names. This allows for efficient loading of classes only when they're needed. 106 | 107 | 2. Configuration: 108 | A `Config` class is defined to hold various configuration settings for the application, including database settings, navigation menus, and access control levels. 109 | 110 | 3. Main Application Flow: 111 | The main execution starts by creating a `Config` object and then passing it to an `Init` class constructor. The `Init` class likely handles the initialization of the application and routing of requests. 112 | 113 | 4. Class Structure: 114 | The code defines several classes that handle different aspects of the application: 115 | - `Db`: Handles database operations 116 | - `Init`: Initializes the application 117 | - `Plugin`: Base class for various plugins (e.g., Accounts, Auth, Domains) 118 | - `Theme`: Handles the rendering of the user interface 119 | - `Util`: Provides utility functions 120 | 121 | 5. Plugins: 122 | There are several plugin classes (e.g., `Plugins_Accounts`, `Plugins_Auth`, `Plugins_Domains`) that extend the `Plugin` base class. These likely handle specific functionality areas of the application. 123 | 124 | 6. Themes: 125 | The application supports theming, with a base `Theme` class and specific theme implementations like `Themes_Bootstrap5_Theme`. 126 | 127 | 7. Utility Functions: 128 | The `Util` class provides various helper functions for tasks like logging, encoding, session management, and password handling. 129 | 130 | 8. Database Abstraction: 131 | The `Db` class provides an abstraction layer for database operations, supporting both MySQL and SQLite. 132 | 133 | 9. Security Features: 134 | The code includes security measures such as CSRF protection, password hashing, and input sanitization. 135 | 136 | 10. Modular Structure: 137 | The application is designed in a modular way, allowing for easy extension and modification of functionality through plugins and themes. 138 | 139 | 11. Execution: 140 | The application likely determines the requested action based on URL parameters, initializes the appropriate plugin and theme, and then renders the response. 141 | 142 | This structure allows for a flexible and extensible web application that can handle various aspects of server management and hosting control. The use of classes and object-oriented programming principles makes the code organized and maintainable. -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markc/hcp_bs5/c297d2fe20f0b891dca26016bc86bb3774435044/favicon.ico -------------------------------------------------------------------------------- /hcp.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "../../.sh" 8 | } 9 | ], 10 | "settings": {} 11 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | [ 67 | 'email' => 'test@example.com', 68 | 'admpw' => 'admin123', 69 | 'file' => LIB . '.ht_conf.php', 70 | 'hash' => 'SHA512-CRYPT', 71 | 'host' => '', 72 | 'perp' => 25, 73 | 'self' => '/', 74 | ], 75 | 'in' => [ 76 | 'a' => '', 77 | 'd' => '', 78 | 'g' => null, 79 | 'i' => null, 80 | 'l' => '', 81 | 'm' => 'list', 82 | 'o' => 'home', 83 | 'r' => 'local', 84 | 't' => 'bootstrap5', 85 | 'x' => '', 86 | ], 87 | 'out' => [ 88 | 'doc' => 'NetServa', 89 | 'css' => '', 90 | 'log' => '', 91 | 'nav1' => '', 92 | 'nav2' => '', 93 | 'nav3' => '', 94 | 'head' => 'NetServa HCP', 95 | 'main' => 'Welcome to NetServa HCP', 96 | 'foot' => 'Copyright (C) 2015-2024 Netserva HCP (AGPL-3.0)', 97 | 'js' => '', 98 | 'end' => '', 99 | ], 100 | 'db' => [ 101 | 'host' => '127.0.0.1', 102 | 'name' => 'sysadm', 103 | 'pass' => LIB . DS . '.ht_pw', 104 | //'path' => '/var/lib/sqlite/sysadm/sysadm.db', // orignal path 105 | 'path' => 'sysadm/sysadm.db', 106 | 'port' => '3306', 107 | 'sock' => '', 108 | 'type' => 'sqlite', 109 | 'user' => 'sysadm', 110 | ], 111 | 'nav1' => [ 112 | 'non' => [ 113 | ['Webmail', 'webmail/', 'bi bi-envelope-fill'], 114 | ['Phpmyadmin', 'phpmyadmin/', 'bi bi-globe'], 115 | ], 116 | 'usr' => [ 117 | ['Webmail', 'webmail/', 'bi bi-envelope-fill'], 118 | ['Phpmyadmin', 'phpmyadmin/', 'bi bi-globe'], 119 | ], 120 | 'adm' => [ 121 | ['Manage', [ 122 | ['Accounts', '?o=accounts', 'bi bi-people-fill'], 123 | ['SSH Manager', '?o=sshm', 'bi bi-key'], 124 | ['Vhosts', '?o=vhosts', 'bi bi-globe2'], 125 | ['Mailboxes', '?o=vmails', 'bi bi-envelope-fill'], 126 | ['Aliases', '?o=valias', 'bi bi-envelope-paper-fill'], 127 | ['DKIM', '?o=dkim', 'bi bi-person-vcard-fill'], 128 | ['Domains', '?o=domains', 'bi bi-globe-americas'], 129 | ], 'bi bi-stack'], 130 | ['Stats', [ 131 | ['Sys Info', '?o=infosys', 'bi bi-speedometer2'], 132 | ['Processes', '?o=processes', 'bi bi-bezier2'], 133 | ['Mail Info', '?o=infomail', 'bi bi-envelope-open'], 134 | ], 'bi bi-graph-up-arrow'], 135 | ['Links', [ 136 | ['Webmail', 'webmail/', 'bi bi-envelope-fill'], 137 | ['Phpmyadmin', 'phpmyadmin/', 'bi bi-globe'], 138 | ], 'bi bi-list'], 139 | ['Sites', [ 140 | ['local', '?r=local', 'bi bi-globe', 'r'], 141 | ['mgo', '?r=mgo', 'bi bi-globe', 'r'], 142 | ['vmd1', '?r=vmd1', 'bi bi-globe', 'r'], 143 | ], 'bi bi-globe'], 144 | ], 145 | ], 146 | 'nav2' => [ 147 | ['local', '?r=local', 'bi bi-globe'], 148 | ['mgo', '?r=mgo', 'bi bi-globe'], 149 | ['vmd1', '?r=vmd1', 'bi bi-globe'], 150 | ], 151 | 'dns' => [ 152 | 'a' => '127.0.0.1', 153 | 'mx' => '', 154 | 'ns1' => 'ns1.', 155 | 'ns2' => 'ns2.', 156 | 'prio' => 0, 157 | 'ttl' => 300, 158 | 'soa' => [ 159 | 'primary' => 'ns1.', 160 | 'email' => 'admin.', 161 | 'refresh' => 7200, 162 | 'retry' => 540, 163 | 'expire' => 604800, 164 | 'ttl' => 3600, 165 | ], 166 | 'db' => [ 167 | 'host' => '127.0.0.1', 168 | 'name' => 'pdns', 169 | 'pass' => 'lib' . DS . '.ht_dns_pw', 170 | 'path' => 'sysadm/pdns.db', 171 | 'port' => '3306', 172 | 'sock' => '', 173 | 'type' => 'sqlite', 174 | 'user' => 'sysadm', 175 | ], 176 | ], 177 | 'acl' => [ 178 | AclLevel::SuperAdmin->value => AclLevel::SuperAdmin->name, 179 | AclLevel::Admin->value => AclLevel::Admin->name, 180 | AclLevel::User->value => AclLevel::User->name, 181 | AclLevel::Suspended->value => AclLevel::Suspended->name, 182 | AclLevel::Anonymous->value => AclLevel::Anonymous->name, 183 | ], 184 | ]; 185 | 186 | $this->cfg = array_merge($defaults['cfg'], $cfg); 187 | $this->in = array_merge($defaults['in'], $in); 188 | $this->out = array_merge($defaults['out'], $out); 189 | $this->db = array_merge($defaults['db'], $db); 190 | $this->nav1 = array_merge($defaults['nav1'], $nav1); 191 | $this->nav2 = array_merge($defaults['nav2'], $nav2); 192 | $this->dns = array_merge($defaults['dns'], $dns); 193 | $this->acl = array_merge($defaults['acl'], $acl); 194 | $this->t = $t; 195 | } 196 | } 197 | 198 | $config = new Config(); 199 | echo new Init($config); 200 | -------------------------------------------------------------------------------- /lib/css/bootstrap-table.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @author zhixin wen 3 | * version: 1.11.1 4 | * https://github.com/wenzhixin/bootstrap-table/ 5 | */ 6 | 7 | .bootstrap-table .table { 8 | margin-bottom: 0 !important; 9 | border-bottom: 1px solid #dddddd; 10 | border-collapse: collapse !important; 11 | border-radius: 1px; 12 | } 13 | 14 | .bootstrap-table .table:not(.table-condensed), 15 | .bootstrap-table .table:not(.table-condensed) > tbody > tr > th, 16 | .bootstrap-table .table:not(.table-condensed) > tfoot > tr > th, 17 | .bootstrap-table .table:not(.table-condensed) > thead > tr > td, 18 | .bootstrap-table .table:not(.table-condensed) > tbody > tr > td, 19 | .bootstrap-table .table:not(.table-condensed) > tfoot > tr > td { 20 | padding: 8px; 21 | } 22 | 23 | .bootstrap-table .table.table-no-bordered > thead > tr > th, 24 | .bootstrap-table .table.table-no-bordered > tbody > tr > td { 25 | border-right: 2px solid transparent; 26 | } 27 | 28 | .bootstrap-table .table.table-no-bordered > tbody > tr > td:last-child { 29 | border-right: none; 30 | } 31 | 32 | .fixed-table-container { 33 | position: relative; 34 | clear: both; 35 | border: 1px solid #dddddd; 36 | border-radius: 4px; 37 | -webkit-border-radius: 4px; 38 | -moz-border-radius: 4px; 39 | } 40 | 41 | .fixed-table-container.table-no-bordered { 42 | border: 1px solid transparent; 43 | } 44 | 45 | .fixed-table-footer, 46 | .fixed-table-header { 47 | overflow: hidden; 48 | } 49 | 50 | .fixed-table-footer { 51 | border-top: 1px solid #dddddd; 52 | } 53 | 54 | .fixed-table-body { 55 | overflow-x: auto; 56 | overflow-y: auto; 57 | height: 100%; 58 | } 59 | 60 | .fixed-table-container table { 61 | width: 100%; 62 | } 63 | 64 | .fixed-table-container thead th { 65 | height: 0; 66 | padding: 0; 67 | margin: 0; 68 | border-left: 1px solid #dddddd; 69 | } 70 | 71 | .fixed-table-container thead th:focus { 72 | outline: 0 solid transparent; 73 | } 74 | 75 | .fixed-table-container thead th:first-child { 76 | border-left: none; 77 | border-top-left-radius: 4px; 78 | -webkit-border-top-left-radius: 4px; 79 | -moz-border-radius-topleft: 4px; 80 | } 81 | 82 | .fixed-table-container thead th .th-inner, 83 | .fixed-table-container tbody td .th-inner { 84 | padding: 8px; 85 | line-height: 24px; 86 | vertical-align: top; 87 | overflow: hidden; 88 | text-overflow: ellipsis; 89 | white-space: nowrap; 90 | } 91 | 92 | .fixed-table-container thead th .sortable { 93 | cursor: pointer; 94 | background-position: right; 95 | background-repeat: no-repeat; 96 | padding-right: 30px; 97 | } 98 | 99 | .fixed-table-container thead th .both { 100 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC'); 101 | } 102 | 103 | .fixed-table-container thead th .asc { 104 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg=='); 105 | } 106 | 107 | .fixed-table-container thead th .desc { 108 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= '); 109 | } 110 | 111 | .fixed-table-container th.detail { 112 | width: 30px; 113 | } 114 | 115 | .fixed-table-container tbody td { 116 | border-left: 1px solid #dddddd; 117 | } 118 | 119 | .fixed-table-container tbody tr:first-child td { 120 | border-top: none; 121 | } 122 | 123 | .fixed-table-container tbody td:first-child { 124 | border-left: none; 125 | } 126 | 127 | /* the same color with .active */ 128 | .fixed-table-container tbody .selected td { 129 | background-color: #f5f5f5; 130 | } 131 | 132 | .fixed-table-container .bs-checkbox { 133 | text-align: center; 134 | } 135 | 136 | .fixed-table-container .bs-checkbox .th-inner { 137 | padding: 8px 0; 138 | } 139 | 140 | .fixed-table-container input[type="radio"], 141 | .fixed-table-container input[type="checkbox"] { 142 | margin: 0 auto !important; 143 | } 144 | 145 | .fixed-table-container .no-records-found { 146 | text-align: center; 147 | } 148 | 149 | .fixed-table-pagination div.pagination, 150 | .fixed-table-pagination .pagination-detail { 151 | margin-top: 10px; 152 | margin-bottom: 10px; 153 | } 154 | 155 | .fixed-table-pagination div.pagination .pagination { 156 | margin: 0; 157 | } 158 | 159 | .fixed-table-pagination .pagination a { 160 | padding: 6px 12px; 161 | line-height: 1.428571429; 162 | } 163 | 164 | .fixed-table-pagination .pagination-info { 165 | line-height: 34px; 166 | margin-right: 5px; 167 | } 168 | 169 | .fixed-table-pagination .btn-group { 170 | position: relative; 171 | display: inline-block; 172 | vertical-align: middle; 173 | } 174 | 175 | .fixed-table-pagination .dropup .dropdown-menu { 176 | margin-bottom: 0; 177 | } 178 | 179 | .fixed-table-pagination .page-list { 180 | display: inline-block; 181 | } 182 | 183 | .fixed-table-toolbar .columns-left { 184 | margin-right: 5px; 185 | } 186 | 187 | .fixed-table-toolbar .columns-right { 188 | margin-left: 5px; 189 | } 190 | 191 | .fixed-table-toolbar .columns label { 192 | display: block; 193 | padding: 3px 20px; 194 | clear: both; 195 | font-weight: normal; 196 | line-height: 1.428571429; 197 | } 198 | 199 | .fixed-table-toolbar .bs-bars, 200 | .fixed-table-toolbar .search, 201 | .fixed-table-toolbar .columns { 202 | position: relative; 203 | margin-top: 10px; 204 | margin-bottom: 10px; 205 | line-height: 34px; 206 | } 207 | 208 | .fixed-table-pagination li.disabled a { 209 | pointer-events: none; 210 | cursor: default; 211 | } 212 | 213 | .fixed-table-loading { 214 | display: none; 215 | position: absolute; 216 | top: 42px; 217 | right: 0; 218 | bottom: 0; 219 | left: 0; 220 | z-index: 99; 221 | background-color: #fff; 222 | text-align: center; 223 | } 224 | 225 | .fixed-table-body .card-view .title { 226 | font-weight: bold; 227 | display: inline-block; 228 | min-width: 30%; 229 | text-align: left !important; 230 | } 231 | 232 | /* support bootstrap 2 */ 233 | .fixed-table-body thead th .th-inner { 234 | box-sizing: border-box; 235 | } 236 | 237 | .table th, .table td { 238 | vertical-align: middle; 239 | box-sizing: border-box; 240 | } 241 | 242 | .fixed-table-toolbar .dropdown-menu { 243 | text-align: left; 244 | max-height: 300px; 245 | overflow: auto; 246 | } 247 | 248 | .fixed-table-toolbar .btn-group > .btn-group { 249 | display: inline-block; 250 | margin-left: -1px !important; 251 | } 252 | 253 | .fixed-table-toolbar .btn-group > .btn-group > .btn { 254 | border-radius: 0; 255 | } 256 | 257 | .fixed-table-toolbar .btn-group > .btn-group:first-child > .btn { 258 | border-top-left-radius: 4px; 259 | border-bottom-left-radius: 4px; 260 | } 261 | 262 | .fixed-table-toolbar .btn-group > .btn-group:last-child > .btn { 263 | border-top-right-radius: 4px; 264 | border-bottom-right-radius: 4px; 265 | } 266 | 267 | .bootstrap-table .table > thead > tr > th { 268 | vertical-align: bottom; 269 | border-bottom: 1px solid #ddd; 270 | } 271 | 272 | /* support bootstrap 3 */ 273 | .bootstrap-table .table thead > tr > th { 274 | padding: 0; 275 | margin: 0; 276 | } 277 | 278 | .bootstrap-table .fixed-table-footer tbody > tr > td { 279 | padding: 0 !important; 280 | } 281 | 282 | .bootstrap-table .fixed-table-footer .table { 283 | border-bottom: none; 284 | border-radius: 0; 285 | padding: 0 !important; 286 | } 287 | 288 | .bootstrap-table .pull-right .dropdown-menu { 289 | right: 0; 290 | left: auto; 291 | } 292 | 293 | /* calculate scrollbar width */ 294 | p.fixed-table-scroll-inner { 295 | width: 100%; 296 | height: 200px; 297 | } 298 | 299 | div.fixed-table-scroll-outer { 300 | top: 0; 301 | left: 0; 302 | visibility: hidden; 303 | width: 200px; 304 | height: 150px; 305 | overflow: hidden; 306 | } 307 | 308 | /* for get correct heights */ 309 | .fixed-table-toolbar:after, .fixed-table-pagination:after { 310 | content: ""; 311 | display: block; 312 | clear: both; 313 | } 314 | -------------------------------------------------------------------------------- /lib/css/color-modes.css: -------------------------------------------------------------------------------- 1 | .bd-placeholder-img { 2 | font-size: 1.125rem; 3 | text-anchor: middle; 4 | -webkit-user-select: none; 5 | -moz-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | @media (min-width: 768px) { 10 | .bd-placeholder-img-lg { 11 | font-size: 3.5rem; 12 | } 13 | } 14 | 15 | .b-example-divider { 16 | width: 100%; 17 | height: 3rem; 18 | background-color: rgba(0, 0, 0, .1); 19 | border: solid rgba(0, 0, 0, .15); 20 | border-width: 1px 0; 21 | box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); 22 | } 23 | 24 | .b-example-vr { 25 | flex-shrink: 0; 26 | width: 1.5rem; 27 | height: 100vh; 28 | } 29 | 30 | .bi { 31 | vertical-align: -.125em; 32 | fill: currentColor; 33 | } 34 | 35 | .nav-scroller { 36 | position: relative; 37 | z-index: 2; 38 | height: 2.75rem; 39 | overflow-y: hidden; 40 | } 41 | 42 | .nav-scroller .nav { 43 | display: flex; 44 | flex-wrap: nowrap; 45 | padding-bottom: 1rem; 46 | margin-top: -1px; 47 | overflow-x: auto; 48 | text-align: center; 49 | white-space: nowrap; 50 | -webkit-overflow-scrolling: touch; 51 | } 52 | 53 | .btn-bd-primary { 54 | --bd-violet-bg: #712cf9; 55 | --bd-violet-rgb: 112.520718, 44.062154, 249.437846; 56 | 57 | --bs-btn-font-weight: 600; 58 | --bs-btn-color: var(--bs-white); 59 | --bs-btn-bg: var(--bd-violet-bg); 60 | --bs-btn-border-color: var(--bd-violet-bg); 61 | --bs-btn-hover-color: var(--bs-white); 62 | --bs-btn-hover-bg: #6528e0; 63 | --bs-btn-hover-border-color: #6528e0; 64 | --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb); 65 | --bs-btn-active-color: var(--bs-btn-hover-color); 66 | --bs-btn-active-bg: #5a23c8; 67 | --bs-btn-active-border-color: #5a23c8; 68 | } 69 | 70 | .bd-mode-toggle { 71 | z-index: 1500; 72 | } 73 | -------------------------------------------------------------------------------- /lib/css/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markc/hcp_bs5/c297d2fe20f0b891dca26016bc86bb3774435044/lib/css/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /lib/css/fonts/nunito-latin-400-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markc/hcp_bs5/c297d2fe20f0b891dca26016bc86bb3774435044/lib/css/fonts/nunito-latin-400-normal.woff2 -------------------------------------------------------------------------------- /lib/css/fonts/nunito-latin-600-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markc/hcp_bs5/c297d2fe20f0b891dca26016bc86bb3774435044/lib/css/fonts/nunito-latin-600-normal.woff2 -------------------------------------------------------------------------------- /lib/css/fonts/nunito-latin-700-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markc/hcp_bs5/c297d2fe20f0b891dca26016bc86bb3774435044/lib/css/fonts/nunito-latin-700-normal.woff2 -------------------------------------------------------------------------------- /lib/css/style.css: -------------------------------------------------------------------------------- 1 | .dataTable-wrapper.no-header .dataTable-container { 2 | border-top: 1px solid #d9d9d9; 3 | } 4 | 5 | .dataTable-wrapper.no-footer .dataTable-container { 6 | border-bottom: 1px solid #d9d9d9; 7 | } 8 | 9 | .dataTable-top, 10 | .dataTable-bottom { 11 | padding: 8px 10px; 12 | } 13 | 14 | .dataTable-top > nav:first-child, 15 | .dataTable-top > div:first-child, 16 | .dataTable-bottom > nav:first-child, 17 | .dataTable-bottom > div:first-child { 18 | float: left; 19 | } 20 | 21 | .dataTable-top > nav:last-child, 22 | .dataTable-top > div:last-child, 23 | .dataTable-bottom > nav:last-child, 24 | .dataTable-bottom > div:last-child { 25 | float: right; 26 | } 27 | 28 | .dataTable-selector { 29 | padding: 6px; 30 | } 31 | 32 | .dataTable-input { 33 | padding: 6px 12px; 34 | } 35 | 36 | .dataTable-info { 37 | margin: 7px 0; 38 | } 39 | 40 | /* PAGER */ 41 | .dataTable-pagination ul { 42 | margin: 0; 43 | padding-left: 0; 44 | } 45 | 46 | .dataTable-pagination li { 47 | list-style: none; 48 | float: left; 49 | } 50 | 51 | .dataTable-pagination a { 52 | border: 1px solid transparent; 53 | float: left; 54 | margin-left: 2px; 55 | padding: 6px 12px; 56 | position: relative; 57 | text-decoration: none; 58 | color: #333; 59 | } 60 | 61 | .dataTable-pagination a:hover { 62 | background-color: #d9d9d9; 63 | } 64 | 65 | .dataTable-pagination .active a, 66 | .dataTable-pagination .active a:focus, 67 | .dataTable-pagination .active a:hover { 68 | background-color: #d9d9d9; 69 | cursor: default; 70 | } 71 | 72 | .dataTable-pagination .ellipsis a, 73 | .dataTable-pagination .disabled a, 74 | .dataTable-pagination .disabled a:focus, 75 | .dataTable-pagination .disabled a:hover { 76 | cursor: not-allowed; 77 | } 78 | 79 | .dataTable-pagination .disabled a, 80 | .dataTable-pagination .disabled a:focus, 81 | .dataTable-pagination .disabled a:hover { 82 | cursor: not-allowed; 83 | opacity: 0.4; 84 | } 85 | 86 | .dataTable-pagination .pager a { 87 | font-weight: bold; 88 | } 89 | 90 | /* TABLE */ 91 | .dataTable-table { 92 | max-width: 100%; 93 | width: 100%; 94 | border-spacing: 0; 95 | border-collapse: separate; 96 | } 97 | 98 | .dataTable-table > tbody > tr > td, 99 | .dataTable-table > tbody > tr > th, 100 | .dataTable-table > tfoot > tr > td, 101 | .dataTable-table > tfoot > tr > th, 102 | .dataTable-table > thead > tr > td, 103 | .dataTable-table > thead > tr > th { 104 | vertical-align: top; 105 | padding: 8px 10px; 106 | } 107 | 108 | .dataTable-table > thead > tr > th { 109 | vertical-align: bottom; 110 | text-align: left; 111 | border-bottom: 1px solid #d9d9d9; 112 | } 113 | 114 | .dataTable-table > tfoot > tr > th { 115 | vertical-align: bottom; 116 | text-align: left; 117 | border-top: 1px solid #d9d9d9; 118 | } 119 | 120 | .dataTable-table th { 121 | vertical-align: bottom; 122 | text-align: left; 123 | } 124 | 125 | .dataTable-table th a { 126 | text-decoration: none; 127 | color: inherit; 128 | } 129 | 130 | .dataTable-sorter { 131 | display: inline-block; 132 | height: 100%; 133 | position: relative; 134 | width: 100%; 135 | } 136 | 137 | .dataTable-sorter::before, 138 | .dataTable-sorter::after { 139 | content: ""; 140 | height: 0; 141 | width: 0; 142 | position: absolute; 143 | right: 4px; 144 | border-left: 4px solid transparent; 145 | border-right: 4px solid transparent; 146 | opacity: 0.2; 147 | } 148 | 149 | .dataTable-sorter::before { 150 | border-top: 4px solid #000; 151 | bottom: 0px; 152 | } 153 | 154 | .dataTable-sorter::after { 155 | border-bottom: 4px solid #000; 156 | border-top: 4px solid transparent; 157 | top: 0px; 158 | } 159 | 160 | .asc .dataTable-sorter::after, 161 | .desc .dataTable-sorter::before { 162 | opacity: 0.6; 163 | } 164 | 165 | .dataTables-empty { 166 | text-align: center; 167 | } 168 | 169 | .dataTable-top::after, .dataTable-bottom::after { 170 | clear: both; 171 | content: " "; 172 | display: table; 173 | } 174 | 175 | table.dataTable-table:focus tr.dataTable-cursor > td:first-child { 176 | border-left: 3px blue solid; 177 | } 178 | 179 | table.dataTable-table:focus { 180 | outline: solid 1px black; 181 | outline-offset: -1px; 182 | } -------------------------------------------------------------------------------- /lib/css/table-datatable.css: -------------------------------------------------------------------------------- 1 | .dataTable-wrapper.no-footer .dataTable-container{border-bottom:none}.dataTable-selector{padding:.375rem 1.75rem .375rem .75rem}.dataTable-dropdown{display:inline-flex;align-items:center}.dataTable-dropdown label{white-space:nowrap;margin-left:15px}.page-item.active .page-link{color:#fff!important} 2 | -------------------------------------------------------------------------------- /lib/img/Blank-300x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markc/hcp_bs5/c297d2fe20f0b891dca26016bc86bb3774435044/lib/img/Blank-300x150.png -------------------------------------------------------------------------------- /lib/img/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/js/bootstrap-table-editable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author zhixin wen 3 | * extensions: https://github.com/vitalets/x-editable 4 | */ 5 | 6 | (function($) { 7 | 8 | 'use strict'; 9 | 10 | $.extend($.fn.bootstrapTable.defaults, { 11 | editable: true, 12 | onEditableInit: function() { 13 | return false; 14 | }, 15 | onEditableSave: function(field, row, oldValue, $el) { 16 | return false; 17 | }, 18 | onEditableShown: function(field, row, $el, editable) { 19 | return false; 20 | }, 21 | onEditableHidden: function(field, row, $el, reason) { 22 | return false; 23 | } 24 | }); 25 | 26 | $.extend($.fn.bootstrapTable.Constructor.EVENTS, { 27 | 'editable-init.bs.table': 'onEditableInit', 28 | 'editable-save.bs.table': 'onEditableSave', 29 | 'editable-shown.bs.table': 'onEditableShown', 30 | 'editable-hidden.bs.table': 'onEditableHidden' 31 | }); 32 | 33 | var BootstrapTable = $.fn.bootstrapTable.Constructor, 34 | _initTable = BootstrapTable.prototype.initTable, 35 | _initBody = BootstrapTable.prototype.initBody; 36 | 37 | BootstrapTable.prototype.initTable = function() { 38 | var that = this; 39 | _initTable.apply(this, Array.prototype.slice.apply(arguments)); 40 | 41 | if (!this.options.editable) { 42 | return; 43 | } 44 | 45 | $.each(this.columns, function(i, column) { 46 | if (!column.editable) { 47 | return; 48 | } 49 | 50 | var editableOptions = {}, 51 | editableDataMarkup = [], 52 | editableDataPrefix = 'editable-'; 53 | 54 | var processDataOptions = function(key, value) { 55 | // Replace camel case with dashes. 56 | var dashKey = key.replace(/([A-Z])/g, function($1) { 57 | return "-" + $1.toLowerCase(); 58 | }); 59 | if (dashKey.slice(0, editableDataPrefix.length) == editableDataPrefix) { 60 | var dataKey = dashKey.replace(editableDataPrefix, 'data-'); 61 | editableOptions[dataKey] = value; 62 | } 63 | }; 64 | 65 | $.each(that.options, processDataOptions); 66 | 67 | column.formatter = column.formatter || function(value, row, index) { 68 | return value; 69 | }; 70 | column._formatter = column._formatter ? column._formatter : column.formatter; 71 | column.formatter = function(value, row, index) { 72 | var result = column._formatter ? column._formatter(value, row, index) : value; 73 | 74 | $.each(column, processDataOptions); 75 | 76 | $.each(editableOptions, function(key, value) { 77 | editableDataMarkup.push(' ' + key + '="' + value + '"'); 78 | }); 79 | 80 | var _dont_edit_formatter = false; 81 | if (column.editable.hasOwnProperty('noeditFormatter')) { 82 | _dont_edit_formatter = column.editable.noeditFormatter(value, row, index); 83 | } 84 | 85 | if (_dont_edit_formatter === false) { 86 | return ['' + '' 92 | ].join(''); 93 | } else { 94 | return _dont_edit_formatter; 95 | } 96 | 97 | }; 98 | }); 99 | }; 100 | 101 | BootstrapTable.prototype.initBody = function() { 102 | var that = this; 103 | _initBody.apply(this, Array.prototype.slice.apply(arguments)); 104 | 105 | if (!this.options.editable) { 106 | return; 107 | } 108 | 109 | $.each(this.columns, function(i, column) { 110 | if (!column.editable) { 111 | return; 112 | } 113 | 114 | that.$body.find('a[data-name="' + column.field + '"]').editable(column.editable) 115 | .off('save').on('save', function(e, params) { 116 | var data = that.getData(), 117 | index = $(this).parents('tr[data-index]').data('index'), 118 | row = data[index], 119 | oldValue = row[column.field]; 120 | 121 | $(this).data('value', params.submitValue); 122 | row[column.field] = params.submitValue; 123 | that.trigger('editable-save', column.field, row, oldValue, $(this)); 124 | that.resetFooter(); 125 | }); 126 | that.$body.find('a[data-name="' + column.field + '"]').editable(column.editable) 127 | .off('shown').on('shown', function(e, editable) { 128 | var data = that.getData(), 129 | index = $(this).parents('tr[data-index]').data('index'), 130 | row = data[index]; 131 | 132 | that.trigger('editable-shown', column.field, row, $(this), editable); 133 | }); 134 | that.$body.find('a[data-name="' + column.field + '"]').editable(column.editable) 135 | .off('hidden').on('hidden', function(e, reason) { 136 | var data = that.getData(), 137 | index = $(this).parents('tr[data-index]').data('index'), 138 | row = data[index]; 139 | 140 | that.trigger('editable-hidden', column.field, row, $(this), reason); 141 | }); 142 | }); 143 | this.trigger('editable-init'); 144 | }; 145 | 146 | })(jQuery); 147 | -------------------------------------------------------------------------------- /lib/js/bootstrap-table-export.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author zhixin wen 3 | * extensions: https://github.com/kayalshri/tableExport.jquery.plugin 4 | */ 5 | 6 | (function ($) { 7 | 'use strict'; 8 | var sprintf = $.fn.bootstrapTable.utils.sprintf; 9 | 10 | var TYPE_NAME = { 11 | json: 'JSON', 12 | xml: 'XML', 13 | png: 'PNG', 14 | csv: 'CSV', 15 | txt: 'TXT', 16 | sql: 'SQL', 17 | doc: 'MS-Word', 18 | excel: 'MS-Excel', 19 | xlsx: 'MS-Excel (OpenXML)', 20 | powerpoint: 'MS-Powerpoint', 21 | pdf: 'PDF' 22 | }; 23 | 24 | $.extend($.fn.bootstrapTable.defaults, { 25 | showExport: false, 26 | exportDataType: 'basic', // basic, all, selected 27 | // 'json', 'xml', 'png', 'csv', 'txt', 'sql', 'doc', 'excel', 'powerpoint', 'pdf' 28 | exportTypes: ['json', 'xml', 'csv', 'txt', 'sql', 'excel'], 29 | exportOptions: {} 30 | }); 31 | 32 | $.extend($.fn.bootstrapTable.defaults.icons, { 33 | export: 'glyphicon-export icon-share' 34 | }); 35 | 36 | $.extend($.fn.bootstrapTable.locales, { 37 | formatExport: function () { 38 | return 'Export data'; 39 | } 40 | }); 41 | $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales); 42 | 43 | var BootstrapTable = $.fn.bootstrapTable.Constructor, 44 | _initToolbar = BootstrapTable.prototype.initToolbar; 45 | 46 | BootstrapTable.prototype.initToolbar = function () { 47 | this.showToolbar = this.options.showExport; 48 | 49 | _initToolbar.apply(this, Array.prototype.slice.apply(arguments)); 50 | 51 | if (this.options.showExport) { 52 | var that = this, 53 | $btnGroup = this.$toolbar.find('>.btn-group'), 54 | $export = $btnGroup.find('div.export'); 55 | 56 | if (!$export.length) { 57 | $export = $([ 58 | '
', 59 | '', 68 | '', 70 | '
'].join('')).appendTo($btnGroup); 71 | 72 | var $menu = $export.find('.dropdown-menu'), 73 | exportTypes = this.options.exportTypes; 74 | 75 | if (typeof this.options.exportTypes === 'string') { 76 | var types = this.options.exportTypes.slice(1, -1).replace(/ /g, '').split(','); 77 | 78 | exportTypes = []; 79 | $.each(types, function (i, value) { 80 | exportTypes.push(value.slice(1, -1)); 81 | }); 82 | } 83 | $.each(exportTypes, function (i, type) { 84 | if (TYPE_NAME.hasOwnProperty(type)) { 85 | $menu.append(['
  • ', 86 | '', 87 | TYPE_NAME[type], 88 | '', 89 | '
  • '].join('')); 90 | } 91 | }); 92 | 93 | $menu.find('li').click(function () { 94 | var type = $(this).data('type'), 95 | doExport = function () { 96 | that.$el.tableExport($.extend({}, that.options.exportOptions, { 97 | type: type, 98 | escape: false 99 | })); 100 | }; 101 | 102 | if (that.options.exportDataType === 'all' && that.options.pagination) { 103 | that.$el.one(that.options.sidePagination === 'server' ? 'post-body.bs.table' : 'page-change.bs.table', function () { 104 | doExport(); 105 | that.togglePagination(); 106 | }); 107 | that.togglePagination(); 108 | } else if (that.options.exportDataType === 'selected') { 109 | var data = that.getData(), 110 | selectedData = that.getAllSelections(); 111 | 112 | // Quick fix #2220 113 | if (that.options.sidePagination === 'server') { 114 | data = {total: that.options.totalRows}; 115 | data[that.options.dataField] = that.getData(); 116 | 117 | selectedData = {total: that.options.totalRows}; 118 | selectedData[that.options.dataField] = that.getAllSelections(); 119 | } 120 | 121 | that.load(selectedData); 122 | doExport(); 123 | that.load(data); 124 | } else { 125 | doExport(); 126 | } 127 | }); 128 | } 129 | } 130 | }; 131 | })(jQuery); 132 | -------------------------------------------------------------------------------- /lib/js/color-modes.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) 3 | * Copyright 2011-2023 The Bootstrap Authors 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. 5 | */ 6 | 7 | (() => { 8 | 'use strict' 9 | 10 | const getStoredTheme = () => localStorage.getItem('theme') 11 | const setStoredTheme = theme => localStorage.setItem('theme', theme) 12 | 13 | const getPreferredTheme = () => { 14 | const storedTheme = getStoredTheme() 15 | if (storedTheme) { 16 | return storedTheme 17 | } 18 | 19 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 20 | } 21 | 22 | const setTheme = theme => { 23 | if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) { 24 | document.documentElement.setAttribute('data-bs-theme', 'dark') 25 | } else { 26 | document.documentElement.setAttribute('data-bs-theme', theme) 27 | } 28 | } 29 | 30 | setTheme(getPreferredTheme()) 31 | 32 | const showActiveTheme = (theme, focus = false) => { 33 | const themeSwitcher = document.querySelector('#bd-theme') 34 | 35 | if (!themeSwitcher) { 36 | return 37 | } 38 | 39 | const themeSwitcherText = document.querySelector('#bd-theme-text') 40 | const activeThemeIcon = document.querySelector('.theme-icon-active use') 41 | const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) 42 | const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href') 43 | 44 | document.querySelectorAll('[data-bs-theme-value]').forEach(element => { 45 | element.classList.remove('active') 46 | element.setAttribute('aria-pressed', 'false') 47 | }) 48 | 49 | btnToActive.classList.add('active') 50 | btnToActive.setAttribute('aria-pressed', 'true') 51 | activeThemeIcon.setAttribute('href', svgOfActiveBtn) 52 | const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` 53 | themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) 54 | 55 | if (focus) { 56 | themeSwitcher.focus() 57 | } 58 | } 59 | 60 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 61 | const storedTheme = getStoredTheme() 62 | if (storedTheme !== 'light' && storedTheme !== 'dark') { 63 | setTheme(getPreferredTheme()) 64 | } 65 | }) 66 | 67 | window.addEventListener('DOMContentLoaded', () => { 68 | showActiveTheme(getPreferredTheme()) 69 | 70 | document.querySelectorAll('[data-bs-theme-value]') 71 | .forEach(toggle => { 72 | toggle.addEventListener('click', () => { 73 | const theme = toggle.getAttribute('data-bs-theme-value') 74 | setStoredTheme(theme) 75 | setTheme(theme) 76 | showActiveTheme(theme, true) 77 | }) 78 | }) 79 | }) 80 | })() 81 | -------------------------------------------------------------------------------- /lib/js/dark.js: -------------------------------------------------------------------------------- 1 | 2 | const THEME_KEY = "theme" 3 | 4 | function toggleDarkTheme() { 5 | setTheme( 6 | document.documentElement.getAttribute("data-bs-theme") === 'dark' 7 | ? "light" 8 | : "dark" 9 | ) 10 | } 11 | 12 | /** 13 | * Set theme for mazer 14 | * @param {"dark"|"light"} theme 15 | * @param {boolean} persist 16 | */ 17 | function setTheme(theme, persist = false) { 18 | document.body.classList.add(theme) 19 | document.documentElement.setAttribute('data-bs-theme', theme) 20 | 21 | if (persist) { 22 | localStorage.setItem(THEME_KEY, theme) 23 | } 24 | } 25 | 26 | /** 27 | * Init theme from setTheme() 28 | */ 29 | function initTheme() { 30 | //If the user manually set a theme, we'll load that 31 | const storedTheme = localStorage.getItem(THEME_KEY) 32 | if (storedTheme) { 33 | return setTheme(storedTheme) 34 | } 35 | //Detect if the user set his preferred color scheme to dark 36 | if (!window.matchMedia) { 37 | return 38 | } 39 | 40 | //Media query to detect dark preference 41 | const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") 42 | 43 | //Register change listener 44 | mediaQuery.addEventListener("change", (e) => 45 | setTheme(e.matches ? "dark" : "light", true) 46 | ) 47 | return setTheme(mediaQuery.matches ? "dark" : "light", true) 48 | } 49 | 50 | window.addEventListener('DOMContentLoaded', () => { 51 | const toggler = document.getElementById("toggle-dark") 52 | const theme = localStorage.getItem(THEME_KEY) 53 | 54 | if(toggler) { 55 | toggler.checked = theme === "dark" 56 | 57 | toggler.addEventListener("input", (e) => { 58 | setTheme(e.target.checked ? "dark" : "light", true) 59 | }) 60 | } 61 | 62 | }); 63 | 64 | initTheme() 65 | 66 | -------------------------------------------------------------------------------- /lib/js/initTheme.js: -------------------------------------------------------------------------------- 1 | const body = document.body; 2 | const theme = localStorage.getItem('theme') 3 | 4 | if (theme) 5 | document.documentElement.setAttribute('data-bs-theme', theme) 6 | -------------------------------------------------------------------------------- /lib/js/simple-datatables.js: -------------------------------------------------------------------------------- 1 | let dataTable = new simpleDatatables.DataTable( 2 | document.getElementById("table1") 3 | ) 4 | // Move "per page dropdown" selector element out of label 5 | // to make it work with bootstrap 5. Add bs5 classes. 6 | function adaptPageDropdown() { 7 | const selector = dataTable.wrapper.querySelector(".dataTable-selector") 8 | selector.parentNode.parentNode.insertBefore(selector, selector.parentNode) 9 | selector.classList.add("form-select") 10 | } 11 | 12 | // Add bs5 classes to pagination elements 13 | function adaptPagination() { 14 | const paginations = dataTable.wrapper.querySelectorAll( 15 | "ul.dataTable-pagination-list" 16 | ) 17 | 18 | for (const pagination of paginations) { 19 | pagination.classList.add(...["pagination", "pagination-primary"]) 20 | } 21 | 22 | const paginationLis = dataTable.wrapper.querySelectorAll( 23 | "ul.dataTable-pagination-list li" 24 | ) 25 | 26 | for (const paginationLi of paginationLis) { 27 | paginationLi.classList.add("page-item") 28 | } 29 | 30 | const paginationLinks = dataTable.wrapper.querySelectorAll( 31 | "ul.dataTable-pagination-list li a" 32 | ) 33 | 34 | for (const paginationLink of paginationLinks) { 35 | paginationLink.classList.add("page-link") 36 | } 37 | } 38 | 39 | const refreshPagination = () => { 40 | adaptPagination() 41 | } 42 | 43 | // Patch "per page dropdown" and pagination after table rendered 44 | dataTable.on("datatable.init", () => { 45 | adaptPageDropdown() 46 | refreshPagination() 47 | }) 48 | dataTable.on("datatable.update", refreshPagination) 49 | dataTable.on("datatable.sort", refreshPagination) 50 | 51 | // Re-patch pagination after the page was changed 52 | dataTable.on("datatable.page", adaptPagination) 53 | -------------------------------------------------------------------------------- /lib/php/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.{js,ts,json,css,html}] 14 | indent_size = 2 15 | 16 | [*.php] 17 | indent_size = 4 18 | 19 | [*.go] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /lib/php/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bmewburn.vscode-intelephense-client" 4 | ] 5 | } -------------------------------------------------------------------------------- /lib/php/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.detectIndentation": false 3 | } 4 | -------------------------------------------------------------------------------- /lib/php/home.tpl.example: -------------------------------------------------------------------------------- 1 | 2 |
    3 |

    Your Page Title

    4 |

    Lorem ipsum...

    5 |
    6 | -------------------------------------------------------------------------------- /lib/php/plugin.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugin 6 | { 7 | protected string $buf = ''; 8 | protected ?object $dbh = null; 9 | protected string $tbl = ''; 10 | protected array $in = []; 11 | 12 | public function __construct( 13 | protected readonly object $g, 14 | Theme $t 15 | ) { 16 | elog(__METHOD__); 17 | 18 | $this->validateAccess($t->g->in['o'], $t->g->in['m'], $t->g->cfg['self']); 19 | $this->initializePlugin($t); 20 | $this->setupDatabase($t); 21 | $this->executeMethod($t->g->in['m']); 22 | } 23 | 24 | protected function validateAccess(string $o, string $m, string $self): void 25 | { 26 | $allowedPlugins = ['auth', 'home']; 27 | $allowedMethods = ['list', 'create', 'resetpw']; 28 | if (!util::is_usr() && (!in_array($o, $allowedPlugins, true) || ($o === 'auth' && !in_array($m, $allowedMethods, true)))) { 29 | util::redirect($self . '?o=auth'); 30 | } 31 | } 32 | 33 | protected function initializePlugin(Theme $t): void 34 | { 35 | $this->in = util::esc($this->in); 36 | } 37 | 38 | protected function setupDatabase(Theme $t): void 39 | { 40 | if ($this->tbl === '') { 41 | return; 42 | } 43 | 44 | db::$dbh = $this->dbh ?? db::$dbh ?? new db($t->g->db); 45 | db::$tbl = $this->tbl; 46 | } 47 | 48 | protected function executeMethod(string $method): void 49 | { 50 | if (!method_exists($this, $method)) { 51 | throw new RuntimeException("Method '$method' not found"); 52 | } 53 | 54 | try { 55 | $this->buf .= $this->$method(); 56 | } catch (Throwable $e) { 57 | error_log("Error executing method '$method': " . $e->getMessage()); 58 | $this->buf .= "An error occurred while processing your request."; 59 | } 60 | } 61 | 62 | public function __toString() : string 63 | { 64 | elog(__METHOD__); 65 | 66 | return $this->buf; 67 | } 68 | 69 | protected function create(): string 70 | { 71 | elog(__METHOD__); 72 | 73 | if (!util::is_post()) { 74 | return $this->g->t->create($this->in); 75 | } 76 | 77 | try { 78 | $now = (new DateTimeImmutable())->format('Y-m-d H:i:s'); 79 | $this->in['updated'] = $now; 80 | $this->in['created'] = $now; 81 | 82 | $lid = db::create($this->in); 83 | if (!$lid) { 84 | throw new RuntimeException('Failed to create item'); 85 | } 86 | 87 | util::log("Item number $lid created", 'success'); 88 | util::relist(); 89 | } catch (Throwable $e) { 90 | error_log("Create error: " . $e->getMessage()); 91 | util::log('Error creating item: ' . $e->getMessage()); 92 | return $this->g->t->create($this->in); 93 | } 94 | 95 | return ''; 96 | } 97 | 98 | protected function read() : string 99 | { 100 | elog(__METHOD__); 101 | 102 | return $this->g->t->read(db::read('*', 'id', $this->g->in['i'], '', 'one')); 103 | } 104 | 105 | protected function update(): string 106 | { 107 | elog(__METHOD__); 108 | 109 | if (!util::is_post()) { 110 | return $this->read(); 111 | } 112 | 113 | try { 114 | $this->in['updated'] = (new DateTimeImmutable())->format('Y-m-d H:i:s'); 115 | $itemId = $this->g->in['i']; 116 | 117 | if (!db::update($this->in, [['id', '=', $itemId]])) { 118 | throw new RuntimeException('Update operation failed'); 119 | } 120 | 121 | util::log("Item number $itemId updated", 'success'); 122 | util::relist(); 123 | } catch (Throwable $e) { 124 | error_log("Update error: " . $e->getMessage()); 125 | util::log('Error updating item: ' . $e->getMessage()); 126 | return $this->read(); 127 | } 128 | 129 | return ''; 130 | } 131 | 132 | protected function delete(): string 133 | { 134 | elog(__METHOD__); 135 | 136 | if (!util::is_post()) { 137 | return 'Invalid request method'; 138 | } 139 | 140 | try { 141 | $itemId = $this->g->in['i']; 142 | if (empty($itemId)) { 143 | throw new RuntimeException('No item ID provided'); 144 | } 145 | 146 | if (!db::delete([['id', '=', $itemId]])) { 147 | throw new RuntimeException('Delete operation failed'); 148 | } 149 | 150 | util::log("Item number $itemId removed", 'success'); 151 | util::relist(); 152 | } catch (Throwable $e) { 153 | error_log("Delete error: " . $e->getMessage()); 154 | util::log('Error deleting item: ' . $e->getMessage()); 155 | } 156 | 157 | return ''; 158 | } 159 | 160 | protected function list() : string 161 | { 162 | elog(__METHOD__); 163 | 164 | return $this->g->t->list(db::read('*', '', '', 'ORDER BY `updated` DESC')); 165 | } 166 | 167 | public function __call(string $name, array $args): string 168 | { 169 | $message = sprintf( 170 | '%s() name = %s, args = %s', 171 | __METHOD__, 172 | $name, 173 | var_export($args, true) 174 | ); 175 | elog($message); 176 | 177 | return sprintf('Plugin::%s() not implemented', $name); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/php/plugins/accounts.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | declare(strict_types=1); 6 | 7 | class Plugins_Accounts extends Plugin 8 | { 9 | protected string $tbl = 'accounts'; 10 | protected array $in = [ 11 | 'grp' => 1, 12 | 'acl' => 2, 13 | 'vhosts' => 1, 14 | 'login' => '', 15 | 'fname' => '', 16 | 'lname' => '', 17 | 'altemail' => '', 18 | ]; 19 | 20 | protected function create() : string 21 | { 22 | elog(__METHOD__); 23 | 24 | if (util::is_adm()) return parent::create(); 25 | util::log('You are not authorized to perform this action, please contact your administrator.'); 26 | util::relist(); 27 | return ''; 28 | } 29 | 30 | protected function read() : string 31 | { 32 | elog(__METHOD__); 33 | 34 | $usr = db::read('*', 'id', $this->g->in['i'], '', 'one'); 35 | if (!$usr) { 36 | util::log('User not found.'); 37 | util::relist(); 38 | return ''; 39 | } 40 | 41 | if (util::is_acl(0)) { 42 | // superadmin 43 | } elseif (util::is_acl(1)) { // normal admin 44 | if ((int)$_SESSION['usr']['grp'] !== (int)$usr['grp']) { 45 | util::log('You are not authorized to perform this action.'); 46 | util::relist(); 47 | return ''; 48 | } 49 | } else { // Other users 50 | if ((int)$_SESSION['usr']['id'] !== (int)$usr['id']) { 51 | util::log('You are not authorized to perform this action.'); 52 | util::relist(); 53 | return ''; 54 | } 55 | } 56 | return $this->g->t->read($usr); 57 | } 58 | 59 | protected function delete() : string 60 | { 61 | elog(__METHOD__); 62 | 63 | if (util::is_post()) return parent::delete(); 64 | return ''; 65 | } 66 | 67 | protected function list() : string 68 | { 69 | elog(__METHOD__); 70 | 71 | if ($this->g->in['x'] === 'json') { 72 | $columns = [ 73 | ['dt' => null, 'db' => 'id'], 74 | ['dt' => 0, 'db' => 'login', 'formatter' => function(string $d, array $row) : string { 75 | return '' . $d . ''; 76 | }], 77 | ['dt' => 1, 'db' => 'fname'], 78 | ['dt' => 2, 'db' => 'lname'], 79 | ['dt' => 3, 'db' => 'altemail'], 80 | ['dt' => 4, 'db' => 'acl', 'formatter' => function(string $d) : string { 81 | return $this->g->acl[(int)$d]; 82 | }], 83 | ['dt' => 5, 'db' => 'grp'], 84 | ]; 85 | $jsonData = json_encode(db::simple($_GET, 'accounts', 'id', $columns), JSON_PRETTY_PRINT); 86 | $this->g->out['main'] = $jsonData; 87 | return $jsonData; 88 | } 89 | return $this->g->t->list($this->in); 90 | } 91 | 92 | protected function switch_user() : string 93 | { 94 | elog(__METHOD__); 95 | 96 | if (util::is_adm() && !is_null($this->g->in['i'])) { 97 | $usr = db::read('id,acl,grp,login,fname,lname,webpw,cookie', 'id', $this->g->in['i'], '', 'one'); 98 | if ($usr) { 99 | $_SESSION['usr'] = $usr; 100 | util::log('Switch to user: ' . $usr['login'], 'success'); 101 | } 102 | } else { 103 | util::log('Not authorized to switch users'); 104 | } 105 | util::relist(); 106 | return ''; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/php/plugins/auth.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | declare(strict_types=1); 6 | 7 | class Plugins_Auth extends Plugin 8 | { 9 | private const OTP_LENGTH = 10; 10 | private const REMEMBER_ME_EXP = 604800; // 7 days; 11 | 12 | protected string $tbl = 'accounts'; 13 | protected array $in = [ 14 | 'id' => null, 15 | 'acl' => null, 16 | 'grp' => null, 17 | 'login' => '', 18 | 'webpw' => '', 19 | 'remember' => '', 20 | 'otp' => '', 21 | 'passwd1' => '', 22 | 'passwd2' => '', 23 | ]; 24 | 25 | // forgotpw 26 | public function create() : string 27 | { 28 | elog(__METHOD__); 29 | 30 | $u = (string)$this->in['login']; 31 | 32 | if (util::is_post()) { 33 | if (filter_var($u, FILTER_VALIDATE_EMAIL)) { 34 | if ($usr = db::read('id,acl', 'login', $u, '', 'one')) { 35 | if ($usr['acl'] != 9) { 36 | $newpass = util::genpw(self::OTP_LENGTH); 37 | if ($this->mail_forgotpw($u, $newpass, 'From: ' . $this->g->cfg['email'])) { 38 | db::update([ 39 | 'otp' => $newpass, 40 | 'otpttl' => time() 41 | ], [['id', '=', $usr['id']]]); 42 | util::log('Sent reset password key for "' . $u . '" so please check your mailbox and click on the supplied link.', 'success'); 43 | } else util::log('Problem sending message to ' . $u, 'danger'); 44 | util::redirect($this->g->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list'); 45 | } else util::log('Account is disabled, contact your System Administrator'); 46 | } else util::log('User does not exist'); 47 | } else util::log('You must provide a valid email address'); 48 | } 49 | return $this->g->t->create(['login' => $u]); 50 | } 51 | 52 | // login 53 | public function list() : string 54 | { 55 | elog(__METHOD__); 56 | 57 | $u = (string)$this->in['login']; 58 | $p = (string)$this->in['webpw']; 59 | 60 | if ($u) { 61 | if (!empty($this->g->cfg['admpw']) && $u === 'admin@example.com' && $p === 'admin123') { 62 | $_SESSION['usr'] = [ 63 | 'id' => 0, 64 | 'grp' => 0, 65 | 'acl' => 0, 66 | 'login' => $u, 67 | 'fname' => 'Admin', 68 | 'lname' => 'User' 69 | ]; 70 | $_SESSION['adm'] = 0; 71 | util::log($u . ' is now logged in', 'success'); 72 | $_SESSION['m'] = 'list'; 73 | util::redirect($this->g->cfg['self']); 74 | } 75 | if ($usr = db::read('id,grp,acl,login,fname,lname,webpw,cookie', 'login', $u, '', 'one')) { 76 | $id = (int)$usr['id']; 77 | $acl = (int)$usr['acl']; 78 | $login = (string)$usr['login']; 79 | $webpw = (string)$usr['webpw']; 80 | 81 | if ($acl !== 9) { 82 | if (password_verify(html_entity_decode($p, ENT_QUOTES, 'UTF-8'), $webpw)) { 83 | if ($this->in['remember']) { 84 | $uniq = util::random_token(32); 85 | db::update(['cookie' => $uniq], [['id', '=', $id]]); 86 | util::put_cookie('remember', $uniq, self::REMEMBER_ME_EXP); 87 | } 88 | $_SESSION['usr'] = $usr; 89 | util::log($login.' is now logged in', 'success'); 90 | if ($acl === 0) $_SESSION['adm'] = $id; 91 | $_SESSION['m'] = 'list'; 92 | util::redirect($this->g->cfg['self']); 93 | } else util::log('Invalid Email Or Password'); 94 | } else util::log('Account is disabled, contact your System Administrator'); 95 | } else util::log('Invalid Email Or Password'); 96 | } 97 | return $this->g->t->list(['login' => $u]); 98 | } 99 | 100 | // resetpw 101 | public function update() : string 102 | { 103 | elog(__METHOD__); 104 | 105 | if (!(util::is_usr() || isset($_SESSION['resetpw']))) { 106 | util::log('Session expired! Please login and try again.'); 107 | util::relist(); 108 | } 109 | 110 | $i = (util::is_usr()) ? (int)$_SESSION['usr']['id'] : (int)$_SESSION['resetpw']['usr']['id']; 111 | $u = (util::is_usr()) ? (string)$_SESSION['usr']['login'] : (string)$_SESSION['resetpw']['usr']['login']; 112 | 113 | if (util::is_post()) { 114 | if ($usr = db::read('login,acl,otpttl', 'id', (string)$i, '', 'one')) { 115 | $p1 = html_entity_decode($this->in['passwd1'], ENT_QUOTES, 'UTF-8'); 116 | $p2 = html_entity_decode($this->in['passwd2'], ENT_QUOTES, 'UTF-8'); 117 | if (util::chkpw($p1, $p2)) { 118 | if (util::is_usr() || ($usr['otpttl'] && ((int)$usr['otpttl'] + 3600) > time())) { 119 | if (!is_null($usr['acl'])) { 120 | if (db::update([ 121 | 'webpw' => password_hash($p1, PASSWORD_DEFAULT), 122 | 'otp' => '', 123 | 'otpttl' => 0, 124 | 'updated' => date('Y-m-d H:i:s'), 125 | ], [['id', '=', $i]])) { 126 | util::log('Password reset for ' . $usr['login'], 'success'); 127 | if (util::is_usr()) { 128 | util::redirect($this->g->cfg['self']); 129 | } else { 130 | unset($_SESSION['resetpw']); 131 | util::relist(); 132 | } 133 | } else util::log('Problem updating database'); 134 | } else util::log($usr['login'] . ' is not allowed access'); 135 | } else util::log('Your one time password key has expired'); 136 | } 137 | } else util::log('User does not exist'); 138 | } 139 | return $this->g->t->update(['id' => $i, 'login' => $u]); 140 | } 141 | 142 | public function delete() : string 143 | { 144 | elog(__METHOD__); 145 | 146 | if(util::is_usr()){ 147 | $u = (string)$_SESSION['usr']['login']; 148 | $id = (int)$_SESSION['usr']['id']; 149 | if (isset($_SESSION['adm']) && $_SESSION['usr']['id'] === $_SESSION['adm']){ 150 | unset($_SESSION['adm']); 151 | } 152 | unset($_SESSION['usr']); 153 | if(isset($_COOKIE['remember'])){ 154 | db::update(['cookie' => ''], [['id', '=', $id]]); 155 | setcookie('remember', '', strtotime('-1 hour', 0)); 156 | } 157 | util::log($u . ' is now logged out', 'success'); 158 | } 159 | util::redirect($this->g->cfg['self']); 160 | return ''; 161 | } 162 | 163 | // Utilities 164 | public function resetpw() : string 165 | { 166 | elog(__METHOD__); 167 | 168 | $otp = html_entity_decode((string)$this->in['otp']); 169 | if (strlen($otp) === self::OTP_LENGTH) { 170 | if ($usr = db::read('id,acl,login,otp,otpttl', 'otp', $otp, '', 'one')) { 171 | $id = (int)$usr['id']; 172 | $acl = (int)$usr['acl']; 173 | $login = (string)$usr['login']; 174 | $otpttl = (int)$usr['otpttl']; 175 | 176 | if ($otpttl && (($otpttl + 3600) > time())) { 177 | if ($acl != 3) { // suspended 178 | $_SESSION['resetpw'] = [ 'usr'=> $usr ]; 179 | return $this->g->t->update(['id' => $id, 'login' => $login]); 180 | } else util::log($login . ' is not allowed access'); 181 | } else util::log('Your one time password key has expired'); 182 | } else util::log('Your one time password key no longer exists'); 183 | } else util::log('Incorrect one time password key'); 184 | util::redirect($this->g->cfg['self']); 185 | return ''; 186 | } 187 | 188 | private function mail_forgotpw(string $email, string $newpass, string $headers = '') : bool 189 | { 190 | elog(__METHOD__); 191 | 192 | $host = $_SERVER['REQUEST_SCHEME'] . '://' 193 | . $this->g->cfg['host'] 194 | . $this->g->cfg['self']; 195 | return mail( 196 | $email, 197 | 'Reset password for ' . $this->g->cfg['host'], 198 | 'Here is your new OTP (one time password) key that is valid for one hour. 199 | 200 | Please click on the link below and continue with reseting your password. 201 | 202 | If you did not request this action then please ignore this message. 203 | 204 | ' . $host . '?o=auth&m=resetpw&otp=' . $newpass, 205 | $headers 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/php/plugins/dkim.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_Dkim extends Plugin 6 | { 7 | protected 8 | $in = [ 9 | 'dnstxt' => '', 10 | 'domain' => '', 11 | 'keylen' => '2048', 12 | 'select' => 'mail', 13 | ]; 14 | 15 | public function create() : string 16 | { 17 | elog(__METHOD__); 18 | 19 | if (util::is_post()){ 20 | $domain = escapeshellarg($this->in['domain']); 21 | $select = escapeshellarg($this->in['select']); 22 | $keylen = escapeshellarg($this->in['keylen']); 23 | util::exe('dkim add ' . $domain . ' ' . $select . ' ' . $keylen); 24 | } 25 | util::redirect( $this->g->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list'); 26 | } 27 | 28 | public function read() : string 29 | { 30 | elog(__METHOD__); 31 | 32 | $domain = explode('._domainkey.', $this->in['dnstxt'])[1]; // too fragile? 33 | $domain_esc = escapeshellarg($domain); 34 | exec("sudo dkim show $domain_esc 2>&1", $retArr, $retVal); 35 | $buf = ' 36 | ' . $retArr[0] . '
    37 |
    ' . $retArr[1] . '
    '; 38 | return $this->g->t->read(['buf' => $buf, 'domain' => $domain]); 39 | } 40 | 41 | public function update() : string 42 | { 43 | elog(__METHOD__); 44 | 45 | //return $this->list(); // override parent update() 46 | util::redirect( $this->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list'); 47 | } 48 | 49 | public function delete() : string 50 | { 51 | elog(__METHOD__); 52 | 53 | if (util::is_post()){ 54 | $domain = escapeshellarg($this->in['domain']); 55 | util::exe('dkim del ' . $domain); 56 | } 57 | util::redirect( $this->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list'); 58 | } 59 | 60 | public function list() : string 61 | { 62 | elog(__METHOD__); 63 | 64 | $buf = '

    '; 65 | exec("sudo dkim list 2>&1", $retArr, $retVal); 66 | foreach($retArr as $line) $buf .= ' 67 | ' . $line . ''; 68 | return $this->g->t->list(['buf' => $buf . '

    ']); 69 | } 70 | } 71 | 72 | ?> 73 | -------------------------------------------------------------------------------- /lib/php/plugins/home.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_Home extends Plugin 6 | { 7 | public function list() : string 8 | { 9 | elog(__METHOD__); 10 | 11 | if (file_exists(INC . 'home.tpl')) { 12 | ob_start(); 13 | include INC . 'home.tpl'; 14 | return ob_get_clean(); 15 | } 16 | return $this->g->t->list([]); 17 | } 18 | } 19 | 20 | ?> 21 | -------------------------------------------------------------------------------- /lib/php/plugins/infomail.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_InfoMail extends Plugin 6 | { 7 | protected $pflog = '/tmp/pflogsumm.log'; 8 | 9 | public function list() : string 10 | { 11 | elog(__METHOD__); 12 | 13 | return $this->g->t->list([ 14 | 'mailq' => shell_exec('mailq'), 15 | 'pflogs' => is_readable($this->pflog) 16 | ? file_get_contents($this->pflog) 17 | : 'none', 18 | 'pflog_time' => is_readable($this->pflog) 19 | ? round(abs(date('U') - filemtime($this->pflog)) / 60, 0) . ' min.' 20 | : '0 min.', 21 | ]); 22 | } 23 | 24 | public function pflog_renew() 25 | { 26 | elog(__METHOD__); 27 | 28 | $this->pflogs = shell_exec('sudo pflogs'); 29 | return $this->list(); 30 | } 31 | } 32 | 33 | ?> 34 | -------------------------------------------------------------------------------- /lib/php/plugins/infosys.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_InfoSys extends Plugin 6 | { 7 | public function list() : string 8 | { 9 | elog(__METHOD__); 10 | 11 | $mem = $dif = $cpu = []; 12 | $cpu_name = $procs = ''; 13 | $cpu_num = 0; 14 | $os = 'Unknown OS'; 15 | 16 | $pmi = explode("\n", trim(file_get_contents('/proc/meminfo'))); 17 | 18 | $lavg = sys_getloadavg(); 19 | $lav = "1m: " . number_format($lavg[0], 2) . 20 | ", 5m: " . number_format($lavg[1], 2) . 21 | ", 15m: " . number_format($lavg[2], 2); 22 | elog("lav=$lav"); 23 | join(', ', sys_getloadavg()); 24 | 25 | $stat1 = file('/proc/stat'); 26 | sleep(1); 27 | $stat2 = file('/proc/stat'); 28 | 29 | if (is_readable('/proc/cpuinfo')) { 30 | $tmp = trim(file_get_contents('/proc/cpuinfo')); 31 | $ret = preg_match_all('/model name.+/', $tmp, $matches); 32 | $cpu_name = $ret ? explode(': ', $matches[0][0])[1] : 'Unknown CPU'; 33 | $cpu_num = count($matches[0]); 34 | } 35 | 36 | if (is_readable('/etc/os-release')) { 37 | $tmp = explode("\n", trim(file_get_contents('/etc/os-release'))); 38 | $osr = []; 39 | foreach ($tmp as $line) { 40 | list($k, $v) = explode('=', $line); 41 | $osr[$k] = trim($v, '" '); 42 | } 43 | $os = $osr['PRETTY_NAME'] ?? 'Unknown OS'; 44 | } 45 | 46 | foreach ($pmi as $line) { 47 | list($k, $v) = explode(':', $line); 48 | list($mem[$k],) = explode(' ', trim($v)); 49 | } 50 | 51 | $info1 = explode(" ", preg_replace("!cpu +!", "", $stat1[0])); 52 | $info2 = explode(" ", preg_replace("!cpu +!", "", $stat2[0])); 53 | $dif['user'] = $info2[0] - $info1[0]; 54 | $dif['nice'] = $info2[1] - $info1[1]; 55 | $dif['sys'] = $info2[2] - $info1[2]; 56 | $dif['idle'] = $info2[3] - $info1[3]; 57 | $total = array_sum($dif); 58 | foreach($dif as $x=>$y) $cpu[$x] = round($y / $total * 100, 2); 59 | $cpu_all = sprintf("User: %01.2f, System: %01.2f, Nice: %01.2f, Idle: %01.2f", $cpu['user'], $cpu['sys'], $cpu['nice'], $cpu['idle']); 60 | $cpu_pcnt = intval(round(100 - $cpu['idle'])); 61 | 62 | $dt = (float) disk_total_space('/'); 63 | $df = (float) disk_free_space('/'); 64 | $du = (float) $dt - $df; 65 | $dp = floor(($du / $dt) * 100); 66 | 67 | $mt = (float) $mem['MemTotal'] * 1000; 68 | //$mf = (float) ($mem['MemFree'] + $mem['Cached'] + $mem['Buffers'] + $mem['SReclaimable']) * 1024; 69 | //$mu = (float) ($mem['MemTotal'] - $mem['MemFree'] - $mem['Cached'] - $mem['SReclaimable'] - $mem['Buffers'] - $mem['Shmem']) * 1024; 70 | $mu = (float) ($mem['MemTotal'] - $mem['MemFree'] - $mem['Cached'] - $mem['SReclaimable'] - $mem['Buffers']) * 1000; 71 | $mf = (float) $mt - $mu; 72 | $mp = floor(($mu / $mt) * 100); 73 | 74 | $ip = gethostbyname(gethostname()); 75 | $hn = gethostbyaddr($ip); 76 | $knl = is_readable('/proc/version') 77 | ? explode(' ', trim(file_get_contents('/proc/version')))[2] 78 | : 'Unknown'; 79 | 80 | return $this->g->t->list([ 81 | 'dsk_color' => $dp > 90 ? 'danger' : ($dp > 80 ? 'warning' : 'default'), 82 | 'dsk_free' => util::numfmtsi($df), 83 | 'dsk_pcnt' => $dp, 84 | 'dsk_text' => $dp > 5 ? $dp. '%' : '', 85 | 'dsk_total' => util::numfmtsi($dt), 86 | 'dsk_used' => util::numfmtsi($du), 87 | 'mem_color' => $mp > 90 ? 'danger' : ($mp > 80 ? 'warning' : 'default'), 88 | 'mem_free' => util::numfmt($mf), 89 | 'mem_pcnt' => $mp, 90 | 'mem_text' => $mp > 5 ? $mp . '%' : '', 91 | 'mem_total' => util::numfmt($mt), 92 | 'mem_used' => util::numfmt($mu), 93 | 'os_name' => $os, 94 | 'uptime' => util::sec2time(intval(explode(' ', (string) file_get_contents('/proc/uptime'))[0])), 95 | 'loadav' => $lav, 96 | 'hostname' => $hn, 97 | 'host_ip' => $ip, 98 | 'kernel' => $knl, 99 | 'cpu_all' => $cpu_all, 100 | 'cpu_name' => $cpu_name, 101 | 'cpu_num' => $cpu_num, 102 | 'cpu_color' => $cpu_pcnt > 90 ? 'danger' : ($cpu_pcnt > 80 ? 'warning' : 'default'), 103 | 'cpu_pcnt' => $cpu_pcnt, 104 | 'cpu_text' => $cpu_pcnt > 5 ? $cpu_pcnt. '%' : '', 105 | ]); 106 | } 107 | } 108 | 109 | ?> 110 | -------------------------------------------------------------------------------- /lib/php/plugins/processes.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_Processes extends Plugin 6 | { 7 | public function list() : string 8 | { 9 | elog(__METHOD__); 10 | 11 | return $this->g->t->list(['procs' => shell_exec('sudo processes')]); 12 | } 13 | } 14 | 15 | ?> 16 | -------------------------------------------------------------------------------- /lib/php/plugins/records.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_Records extends Plugin 6 | { 7 | protected 8 | $tbl = 'records', 9 | $in = [ 10 | 'content' => '', 11 | 'name' => '', 12 | 'prio' => 0, 13 | 'ttl' => 300, 14 | 'type' => '', 15 | ]; 16 | 17 | public function __construct(Theme $t) 18 | { 19 | elog(__METHOD__); 20 | 21 | if ($t->g->dns['db']['type']) 22 | $this->dbh = new db($t->g->dns['db']); 23 | parent::__construct($t); 24 | } 25 | 26 | protected function create() : string 27 | { 28 | elog(__METHOD__); 29 | 30 | if (util::is_post()) { 31 | $in = $this->validate($this->in); 32 | if (!empty($in)) { 33 | $in['created'] = $in['updated']; 34 | $lid = db::create($in); 35 | $this->update_domains($in['domain_id'], $in['updated'] ); 36 | util::log('Created DNS record ID: ' . $lid . ' for ' . $in['name'], 'success'); 37 | } 38 | $i = intval(util::enc($_POST['did'])); 39 | util::redirect( $this->g->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list&i=' . $i); 40 | } 41 | return 'Error creating DNS record'; 42 | } 43 | 44 | protected function update() : string 45 | { 46 | elog(__METHOD__); 47 | 48 | if (util::is_post()) { 49 | $in = $this->validate($this->in); 50 | if (!empty($in)) { 51 | $dom = util::enc($_POST['domain']); 52 | $in['created'] = $in['updated']; 53 | db::update($in, [['id', '=', $this->g->in['i']]]); 54 | $this->update_domains($in['domain_id'], $in['updated'] ); 55 | util::log('Updated DNS record ID: ' . $this->g->in['i'] . ' for ' . $dom, 'success'); 56 | } 57 | $i = intval(util::enc($_POST['did'])); 58 | util::redirect( $this->g->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list&i=' . $i); 59 | } 60 | return 'Error updating DNS record'; 61 | } 62 | 63 | protected function delete() : string 64 | { 65 | elog(__METHOD__); 66 | 67 | if (util::is_post()) { 68 | $dom = util::enc($_POST['domain']); 69 | $did = intval(util::enc($_POST['did'])); 70 | $now = date('Y-m-d H:i:s'); 71 | 72 | db::delete([['id', '=', $this->g->in['i']]]); 73 | $this->update_domains($did, $now); 74 | util::log('Deleted DNS record ID: ' . $this->g->in['i'] . ' from ' . $dom, 'success'); 75 | $i = $did; 76 | util::redirect( $this->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list&i=' . $i); 77 | } 78 | return 'Error deleting DNS record'; 79 | } 80 | 81 | protected function list() : string 82 | { 83 | elog(__METHOD__); 84 | 85 | if ($this->g->in['x'] === 'json') { 86 | $columns = [ 87 | ['dt' => 0, 'db' => 'name'], 88 | ['dt' => 1, 'db' => 'content'], 89 | ['dt' => 2, 'db' => 'type'], 90 | ['dt' => 3, 'db' => 'prio'], 91 | ['dt' => 4, 'db' => 'ttl'], 92 | ['dt' => 5, 'db' => 'id', 'formatter' => function($d) { 93 | return ' 94 | 95 | 96 | 97 | '; 98 | }], 99 | ['dt' => 6, 'db' => 'active'], 100 | ['dt' => 7, 'db' => 'did'], 101 | ['dt' => 8, 'db' => 'domain'], 102 | ['dt' => 9, 'db' => 'updated'], 103 | ]; 104 | return json_encode(db::simple($_GET, 'records_view', 'id', $columns, 'did=' . $_GET['did']), JSON_PRETTY_PRINT); 105 | } 106 | 107 | $domain = db::qry(" 108 | SELECT name FROM domains 109 | WHERE id = :did", ['did' => $this->g->in['i']], 'col'); // i = domain id at this point 110 | 111 | return $this->g->t->list(['domain' => $domain, 'did' => $this->g->in['i']]); 112 | } 113 | 114 | private function update_domains(int $did, string $now) : bool 115 | { 116 | elog(__METHOD__); 117 | 118 | if ($did && $now) { 119 | $sql = " 120 | SELECT content 121 | FROM records 122 | WHERE type='SOA' 123 | AND domain_id=:did"; 124 | 125 | $soa = util::inc_soa(db::qry($sql, ['did' => $did], 'col')); 126 | $sql = " 127 | UPDATE records 128 | SET content=:content 129 | WHERE type='SOA' 130 | AND domain_id=:did"; 131 | 132 | db::qry($sql, ['did' => $did, 'content' => $soa]); 133 | db::$tbl = 'domains'; 134 | return db::update(['updated' => $now], [['id', '=', $did]]); 135 | } 136 | return false; 137 | } 138 | 139 | private function validate(array $in) : array 140 | { 141 | elog(__METHOD__); 142 | 143 | if (empty($in['content'])) { 144 | util::log('Content must not be empty'); 145 | return []; 146 | } elseif (($in['type'] === 'A') && !filter_var($in['content'], FILTER_VALIDATE_IP)) { 147 | util::log('An "A" record must contain a legitimate IP'); 148 | return []; 149 | } elseif ($in['type'] === 'CAA' && !preg_match('/^[a-zA-Z0-9"]+/', $in['content'])) { 150 | util::log('CAA record content must only contain letters and numbers'); 151 | return []; 152 | } elseif ($in['name'] && $in['name'] !== '*' && !preg_match('/^[a-zA-Z0-9_-]+/', $in['name'])) { 153 | util::log('Record name must contain letters, numbers, _ - or only *'); 154 | return []; 155 | } 156 | 157 | if ($in['type'] === 'TXT') 158 | $in['content'] = '"' . trim(htmlspecialchars_decode($in['content'], ENT_COMPAT), '"') . '"'; 159 | 160 | if ($in['type'] === 'CAA') 161 | $in['content'] = htmlspecialchars_decode($in['content'], ENT_COMPAT); 162 | 163 | $domain = strtolower(util::enc($_POST['domain'])); 164 | $in['name'] = strtolower(rtrim(str_replace($domain, '', $in['name']), '.')); 165 | $in['name'] = $in['name'] ? $in['name'] . '.' . $domain : $domain; 166 | 167 | $in['ttl'] = intval($in['ttl']); 168 | $in['prio'] = intval($in['prio']); 169 | $in['updated'] = date('Y-m-d H:i:s'); 170 | $in['domain_id'] = intval(util::enc($_POST['did'])); 171 | 172 | return $in; 173 | } 174 | } 175 | 176 | ?> 177 | -------------------------------------------------------------------------------- /lib/php/plugins/sshm.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Plugins_Sshm extends Plugin 9 | { 10 | public array $inp = [ 11 | 'name' => '', 12 | 'host' => '', 13 | 'port' => '22', 14 | 'user' => 'root', 15 | 'skey' => 'none', 16 | 'key_name' => '', 17 | 'key_cmnt' => '', 18 | 'key_pass' => '', 19 | ]; 20 | 21 | public function create(): string 22 | { 23 | elog(__METHOD__); 24 | 25 | if (util::is_post()) { 26 | util::run('sshm create ' . implode(' ', $this->inp)); 27 | util::relist(); 28 | return null; 29 | } 30 | 31 | $output = util::run('sshm key_list'); 32 | $this->inp['keys'] = $output ? explode("\n", $output) : []; 33 | return $this->g->t->create($this->inp); 34 | } 35 | 36 | public function update(): string 37 | { 38 | elog(__METHOD__); 39 | 40 | if (util::is_post()) { 41 | util::run('sshm create ' . implode(' ', $this->inp)); 42 | util::relist(); 43 | return null; 44 | } 45 | 46 | $output = util::run('sshm read ' . $this->inp['name']); 47 | $host_data = $output ? explode("\n", $output) : []; 48 | $inp = array_combine( 49 | array_keys($this->inp), 50 | array_map(fn($k, $i) => $host_data[$i] ?? '', array_keys($this->inp), array_keys($this->inp)) 51 | ); 52 | $keys_output = util::run('sshm key_list'); 53 | $inp['keys'] = $keys_output ? explode("\n", $keys_output) : []; 54 | return $this->g->t->update($inp); 55 | } 56 | 57 | public function delete(): string 58 | { 59 | elog(__METHOD__); 60 | 61 | if (util::is_post()) { 62 | util::run('sshm delete ' . $this->inp['name']); 63 | util::relist(); 64 | return null; 65 | } 66 | return $this->g->t->delete($this->inp); 67 | } 68 | 69 | public function list(): string 70 | { 71 | elog(__METHOD__); 72 | 73 | $output = util::run('sshm list'); 74 | return $this->g->t->list(['ary' => $output ? explode("\n", $output) : []]); 75 | } 76 | 77 | public function help(): string 78 | { 79 | elog(__METHOD__); 80 | 81 | return $this->g->t->help( 82 | $this->inp['name'], 83 | util::run('sshm help ' . escapeshellarg($this->inp['name'])) 84 | ); 85 | } 86 | 87 | public function key_create(): ?string 88 | { 89 | elog(__METHOD__); 90 | 91 | if (util::is_post()) { 92 | util::run( 93 | 'sshm key_create ' . 94 | $this->inp['key_name'] . ' ' . 95 | $this->inp['key_cmnt'] . ' ' . 96 | $this->inp['key_pass'] 97 | ); 98 | util::relist('key_list'); 99 | return null; 100 | } 101 | return $this->g->t->key_create($this->inp); 102 | } 103 | 104 | protected function key_read(): string 105 | { 106 | elog(__METHOD__); 107 | 108 | return $this->g->t->key_read( 109 | $this->inp['skey'], 110 | shell_exec('sshm key_read ' . $this->inp['skey']) 111 | ); 112 | } 113 | 114 | public function key_delete(): ?string 115 | { 116 | elog(__METHOD__); 117 | 118 | if (util::is_post()) { 119 | util::run('sshm key_delete ' . $this->inp['key_name']); 120 | util::relist('key_list'); 121 | return null; 122 | } 123 | return $this->g->t->key_delete($this->inp); 124 | } 125 | 126 | public function key_list(): string 127 | { 128 | elog(__METHOD__); 129 | 130 | $output = util::run('sshm key_list all'); 131 | return $this->g->t->key_list(['ary' => $output ? explode("\n", $output) : [], 'err' => 0]); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/php/plugins/vhosts.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_Vhosts extends Plugin 6 | { 7 | protected 8 | $tbl = 'vhosts', 9 | $in = [ 10 | 'active' => 0, 11 | 'aid' => 0, 12 | 'aliases' => 10, 13 | 'diskquota' => 1000000000, 14 | 'domain' => '', 15 | 'gid' => 1000, 16 | 'mailboxes' => 1, 17 | 'mailquota' => 500000000, 18 | 'uid' => 1000, 19 | 'uname' => '', 20 | 'cms' => '', 21 | 'ssl' => '', 22 | 'ip' => '', 23 | 'uuser' => '', 24 | ]; 25 | 26 | protected function create() : string 27 | { 28 | elog(__METHOD__); 29 | 30 | if (util::is_post()) { 31 | extract($this->in); 32 | // $active = $active ? 1 : 0; 33 | 34 | // if(!util::is_valid_plan($plan)){ 35 | // util::log('Invalid plan ' . $plan); 36 | // util::redirect($this->g->cfg['self'] . '?o=vhosts'); 37 | // } 38 | 39 | if (file_exists('/home/u/' . $domain)) { 40 | util::log('/home/u/' . $domain . ' already exists', 'warning'); 41 | $_POST = []; return $this->g->t->create($this->in); 42 | } 43 | 44 | // if ($mailquota > $diskquota) { 45 | // util::log('Mailbox quota exceeds domain disk quota'); 46 | // $_POST = []; return $this->g->t->create($this->in); 47 | // } 48 | 49 | $num_results = db::read('COUNT(id)', 'domain', $domain, '', 'col'); 50 | 51 | if ($num_results != 0) { 52 | util::log('Domain already exists'); 53 | $_POST = []; return $this->t->create($this->in); 54 | } 55 | 56 | $cms = ($cms === 'on') ? 'wp' : 'none'; 57 | $ssl = ($ssl === 'on') ? 'self' : 'le'; 58 | $vhost = $uuser ? $uuser . '@' . $domain : $domain; 59 | 60 | shell_exec("nohup sh -c 'sudo addvhost $vhost $cms $ssl $ip' > /tmp/addvhost.log 2>&1 &"); 61 | util::log('Added ' . $domain . ', please wait another few minutes for the setup to complete', 'success'); 62 | util::redirect($this->g->cfg['self'] . '?o=vhosts'); 63 | } 64 | return $this->g->t->create($this->in); 65 | } 66 | 67 | protected function read() : string 68 | { 69 | elog(__METHOD__); 70 | 71 | return $this->g->t->update(db::read('*', 'id', $this->g->in['i'], '', 'one')); 72 | } 73 | 74 | protected function update() : string 75 | { 76 | elog(__METHOD__); 77 | 78 | if (util::is_post()) { 79 | extract($this->in); 80 | $diskquota *= 1000000; 81 | $mailquota *= 1000000; 82 | $active = $active ? 1 : 0; 83 | 84 | $domain = db::read('domain', 'id', $this->g->in['i'], '', 'col'); 85 | 86 | if ($mailquota > $diskquota) { 87 | util::log('Mailbox quota exceeds disk quota'); 88 | $_POST = []; return $this->read(); 89 | } 90 | 91 | $sql = " 92 | UPDATE `vhosts` SET 93 | `active` = :active, 94 | `aliases` = :aliases, 95 | `diskquota` = :diskquota, 96 | `domain` = :domain, 97 | `mailboxes` = :mailboxes, 98 | `mailquota` = :mailquota, 99 | `updated` = :updated 100 | WHERE `id` = :id"; 101 | 102 | $res = db::qry($sql, [ 103 | 'id' => $this->g->in['i'], 104 | 'active' => $active, 105 | 'aliases' => $aliases, 106 | 'diskquota' => $diskquota, 107 | 'domain' => $domain, 108 | 'mailboxes' => $mailboxes, 109 | 'mailquota' => $mailquota, 110 | 'updated' => date('Y-m-d H:i:s'), 111 | ]); 112 | 113 | util::log('Vhost ID ' . $this->g->in['i'] . ' updated', 'success'); 114 | util::redirect( $this->cfg['self'] . '?o=' . $this->g->in['o'] . '&m=list'); 115 | } elseif ($this->g->in['i']) { 116 | return $this->read(); 117 | } else return 'Error updating item'; 118 | } 119 | 120 | protected function delete() : string 121 | { 122 | elog(__METHOD__); 123 | 124 | if (util::is_post() && $this->g->in['i']) { 125 | $domain = db::read('domain', 'id', $this->g->in['i'], '', 'col'); 126 | if ($domain) { 127 | shell_exec("nohup sh -c 'sudo delvhost $domain' > /tmp/delvhost.log 2>&1 &"); 128 | util::log('Removed ' . $domain, 'success'); 129 | util::redirect($this->g->cfg['self'] . '?o=vhosts'); 130 | } else util::log('ERROR: domain does not exist'); 131 | } 132 | return 'Error deleting item'; 133 | } 134 | 135 | protected function list() : string 136 | { 137 | elog(__METHOD__); 138 | 139 | if ($this->g->in['x'] === 'json') { 140 | $columns = [ 141 | ['dt' => 0, 'db' => 'domain', 'formatter' => function($d, $row) { 142 | return ' 143 | 144 | ' . $row['domain'] . ''; 145 | }], 146 | ['dt' => 1, 'db' => 'num_aliases'], 147 | ['dt' => 2, 'db' => null, 'formatter' => function($d) { return '/'; } ], 148 | ['dt' => 3, 'db' => 'aliases'], 149 | ['dt' => 4, 'db' => 'num_mailboxes'], 150 | ['dt' => 5, 'db' => null, 'formatter' => function($d) { return '/'; } ], 151 | ['dt' => 6, 'db' => 'mailboxes'], 152 | ['dt' => 7, 'db' => 'size_mpath', 'formatter' => function($d) { return util::numfmt(intval($d)); }], 153 | ['dt' => 8, 'db' => null, 'formatter' => function($d) { return '/'; } ], 154 | ['dt' => 9, 'db' => 'mailquota', 'formatter' => function($d) { return util::numfmt(intval($d)); }], 155 | ['dt' => 10, 'db' => 'size_upath', 'formatter' => function($d) { return util::numfmt(intval($d)); }], 156 | ['dt' => 11, 'db' => null, 'formatter' => function($d) { return '/'; } ], 157 | ['dt' => 12, 'db' => 'diskquota', 'formatter' => function($d) { return util::numfmt(intval($d)); }], 158 | ['dt' => 13, 'db' => 'active', 'formatter' => function($d) { 159 | return ''; 160 | }], 161 | ['dt' => 14, 'db' => 'id'], 162 | ['dt' => 15, 'db' => 'updated'], 163 | ]; 164 | return json_encode(db::simple($_GET, 'vhosts_view', 'id', $columns), JSON_PRETTY_PRINT); 165 | } 166 | return $this->g->t->list([]); 167 | } 168 | } 169 | 170 | ?> 171 | -------------------------------------------------------------------------------- /lib/php/plugins/vmails.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Plugins_Vmails extends Plugin 6 | { 7 | protected 8 | $tbl = 'vmails', 9 | $in = [ 10 | 'newpw' => 0, 11 | 'password' => '', 12 | 'shpw' => 0, 13 | 'user' => '', 14 | ]; 15 | 16 | protected function create() : string 17 | { 18 | elog(__METHOD__); 19 | 20 | if (util::is_post()) { 21 | if (!filter_var($this->in['user'], FILTER_VALIDATE_EMAIL)) { 22 | util::log('Email address (' . $this->in['user'] . ') is invalid'); 23 | $_POST = []; return $this->read(); 24 | } 25 | util::exe('addvmail ' . $this->in['user']); 26 | } 27 | util::relist(); 28 | } 29 | 30 | protected function update() : string 31 | { 32 | elog(__METHOD__); 33 | 34 | extract($this->in); 35 | 36 | if ($shpw) { 37 | return util::run("shpw $user"); 38 | } elseif ($newpw) { 39 | return util::run("newpw"); 40 | } elseif (util::is_post()) { 41 | $password = html_entity_decode($password, ENT_QUOTES, 'UTF-8'); 42 | if (util::chkpw($password)) { 43 | util::exe("chpw $user '$password'"); 44 | } 45 | } 46 | util::relist(); 47 | } 48 | 49 | protected function delete() : string 50 | { 51 | elog(__METHOD__); 52 | 53 | if (util::is_post()) { 54 | util::exe('delvmail ' . $this->in['user']); 55 | } 56 | util::relist(); 57 | } 58 | 59 | protected function list() : string 60 | { 61 | elog(__METHOD__); 62 | 63 | if ($this->g->in['x'] === 'json') { 64 | $columns = [ 65 | ['dt' => null, 'db' => 'id'], 66 | ['dt' => 0, 'db' => 'user', 'formatter' => function($d, $row) { 67 | return '' . $d . ''; 68 | }], 69 | ['dt' => 1, 'db' => 'size_mail', 'formatter' => function($d) { return util::numfmt(intval($d)); }], 70 | ['dt' => 2, 'db' => 'num_total', 'formatter' => function($d) { return number_format(intval($d)); }], 71 | ['dt' => 3, 'db' => null, 'formatter' => function($d, $row) { 72 | return ''; 73 | }], 74 | ['dt' => 4, 'db' => 'updated'], 75 | ]; 76 | return json_encode(db::simple($_GET, 'vmails_view', 'id', $columns), JSON_PRETTY_PRINT); 77 | } 78 | return $this->g->t->list([]); 79 | } 80 | } 81 | 82 | ?> 83 | -------------------------------------------------------------------------------- /lib/php/theme.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Theme 6 | { 7 | private string $buf; 8 | private array $in; 9 | 10 | public function __construct( 11 | public readonly object $g 12 | ) { 13 | elog(__METHOD__); 14 | $this->buf = ''; 15 | $this->in = []; 16 | } 17 | 18 | protected function escape(string $value): string 19 | { 20 | return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 21 | } 22 | 23 | public function __toString() : string 24 | { 25 | elog(__METHOD__); 26 | return $this->g->out['main'] ?? ''; 27 | } 28 | 29 | public function log(): string 30 | { 31 | elog(__METHOD__); 32 | 33 | return implode("\n", array_map( 34 | fn(string $msg, string $lvl) => $msg 35 | ? sprintf('

    %s

    ', 36 | $this->escape($lvl), 37 | $this->escape($msg) 38 | ) 39 | : '', 40 | util::log(), 41 | array_keys(util::log()) 42 | )); 43 | } 44 | 45 | public function nav1(): string 46 | { 47 | elog(__METHOD__); 48 | 49 | $currentPath = '?o=' . $this->escape($this->g->in['o']); 50 | 51 | $links = array_map( 52 | function(array $nav) use ($currentPath): string { 53 | [$label, $path] = $nav; 54 | $activeClass = $currentPath === $path ? ' class="active"' : ''; 55 | return sprintf( 56 | "\n %s", 57 | $activeClass, 58 | $this->escape($path), 59 | $this->escape($label) 60 | ); 61 | }, 62 | $this->g->nav1 63 | ); 64 | 65 | return "\n "; 66 | } 67 | 68 | public function head(): string 69 | { 70 | elog(__METHOD__); 71 | 72 | return sprintf( 73 | "\n
    \n

    \n %s\n

    %s\n
    ", 74 | $this->escape($this->g->cfg['self']), 75 | $this->escape($this->g->out['head']), 76 | $this->g->out['nav1'] 77 | ); 78 | } 79 | 80 | public function main(): string 81 | { 82 | elog(__METHOD__); 83 | 84 | return sprintf( 85 | "\n
    %s%s\n
    ", 86 | $this->g->out['log'], 87 | $this->g->out['main'] 88 | ); 89 | } 90 | 91 | public function foot(): string 92 | { 93 | elog(__METHOD__); 94 | 95 | return sprintf( 96 | "\n
    \n
    \n

    %s

    \n
    ", 97 | $this->escape($this->g->out['foot']) 98 | ); 99 | } 100 | 101 | public function end(): string 102 | { 103 | elog(__METHOD__); 104 | 105 | return sprintf( 106 | "\n
    %s\n    
    ", 107 | $this->escape($this->g->out['end']) 108 | ); 109 | } 110 | 111 | public function html(): string 112 | { 113 | elog(__METHOD__); 114 | 115 | $out = $this->g->out; 116 | 117 | return << 119 | 120 | 121 | 122 | 123 | 124 | 125 | {$this->escape($out['doc'])} 126 | {$out['css']} 127 | {$out['js']} 128 | 129 | 130 | {$out['head']} 131 | {$out['main']} 132 | {$out['foot']} 133 | {$out['end']} 134 | 135 | 136 | HTML; 137 | } 138 | 139 | public static function dropdown( 140 | array $options, 141 | string $name, 142 | string $selected = '', 143 | ?string $label = null, 144 | ?string $class = null, 145 | ?string $extra = null 146 | ): string { 147 | elog(__METHOD__); 148 | 149 | $theme = new self((object)[]); 150 | 151 | $labelOption = $label 152 | ? sprintf("\n ", 153 | $theme->escape(ucfirst($label)) 154 | ) 155 | : ''; 156 | 157 | $classAttr = $class 158 | ? sprintf(' class="%s"', $theme->escape($class)) 159 | : ''; 160 | 161 | $extraAttr = $extra 162 | ? ' ' . $theme->escape($extra) 163 | : ''; 164 | 165 | $optionsHtml = array_map( 166 | function(array $option) use ($theme, $selected): string { 167 | [$label, $value] = $option; 168 | $value = str_replace('?t=', '', $value); 169 | $isSelected = $selected === $value ? ' selected' : ''; 170 | 171 | return sprintf( 172 | "\n ", 173 | $theme->escape($value), 174 | $isSelected, 175 | $theme->escape($label) 176 | ); 177 | }, 178 | $options 179 | ); 180 | 181 | return sprintf( 182 | "\n ", 183 | $theme->escape($name), 184 | $theme->escape($name), 185 | $classAttr, 186 | $extraAttr, 187 | $labelOption, 188 | implode('', $optionsHtml) 189 | ); 190 | } 191 | 192 | public function __call(string $name, array $args): string 193 | { 194 | $message = sprintf( 195 | '%s() name = %s', 196 | __METHOD__, 197 | $this->escape($name) 198 | ); 199 | elog($message); 200 | 201 | return sprintf( 202 | 'Theme::%s() not implemented', 203 | $this->escape($name) 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/accounts.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Accounts extends Themes_Bootstrap4_Theme 6 | { 7 | public function create(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | return $this->editor($in); 12 | } 13 | 14 | public function read(array $in) : string 15 | { 16 | elog(__METHOD__); 17 | 18 | return $this->editor($in); 19 | } 20 | 21 | public function update(array $in) : string 22 | { 23 | elog(__METHOD__); 24 | 25 | return $this->editor($in); 26 | } 27 | 28 | public function list(array $in) : string 29 | { 30 | elog(__METHOD__.' '.var_export($in, true)); 31 | 32 | extract($in); 33 | $aclgrp_buf = ''; 34 | 35 | if (util::is_adm()) { 36 | $acl = $_SESSION['usr']['acl']; 37 | $grp = $_SESSION['usr']['grp']; 38 | $acl_ary = $grp_ary = []; 39 | foreach($this->g->acl as $k => $v) $acl_ary[] = [$v, $k]; 40 | $acl_buf = $this->dropdown($acl_ary, 'acl', "$acl", '', 'custom-select'); 41 | $res = db::qry(" 42 | SELECT login,id 43 | FROM `accounts` 44 | WHERE acl = :0 OR acl = :1", ['0' => 0, '1' => 1]); 45 | 46 | foreach($res as $k => $v) $grp_ary[] = [$v['login'], $v['id']]; 47 | $grp_buf = $this->dropdown($grp_ary, 'grp', "$grp", '', 'custom-select'); 48 | 49 | $aclgrp_buf = ' 50 |
    51 |
    52 |
    53 | ' . $acl_buf . ' 54 |
    55 |
    56 |
    57 |
    58 | ' . $grp_buf . ' 59 |
    60 |
    61 |
    '; 62 | } 63 | 64 | $createmodal = $this->modal([ 65 | 'id' => 'createmodal', 66 | 'title' => 'Create New Account', 67 | 'action' => 'create', 68 | 'footer' => 'Create', 69 | 'body' => ' 70 |
    71 | 72 | 73 |
    74 |
    75 | 76 | 77 |
    78 |
    79 | 80 | 81 |
    82 |
    83 | 84 | 85 |
    ' . $aclgrp_buf, 86 | ]); 87 | 88 | return ' 89 |
    90 |

    91 | Accounts 92 | 93 | 94 | 95 |

    96 |
    97 | 98 |
    99 |
    100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
    User IDFirst NameLast NameAlt EmailACLGrp
    114 |
    ' . $createmodal . ' 115 | '; 130 | } 131 | 132 | private function editor(array $in) : string 133 | { 134 | elog(__METHOD__); 135 | 136 | extract($in); 137 | 138 | $removemodal = $this->modal([ 139 | 'id' => 'removemodal', 140 | 'title' => 'Remove User', 141 | 'action' => 'delete', 142 | 'footer' => 'Remove', 143 | 'hidden' => ' 144 | ', 145 | 'body' => ' 146 |

    Are you sure you want to remove this user?
    ' . $in['login'] . '

    ', 147 | ]); 148 | 149 | if ($this->g->in['m'] === 'create') { 150 | $header = 'Add Account'; 151 | $switch = ''; 152 | $submit = ' 153 | « Back 154 | '; 155 | } else { 156 | $header = 'Update Account'; 157 | $switch = !util::is_usr($id) && (util::is_acl(0) || util::is_acl(1)) ? ' 158 | Switch to ' . $login . '' : ''; 159 | $submit = ' 160 | « Back 161 | '; 162 | } 163 | 164 | if (util::is_adm()) { 165 | $acl_ary = $grp_ary = []; 166 | foreach($this->g->acl as $k => $v) $acl_ary[] = [$v, $k]; 167 | $acl_buf = $this->dropdown($acl_ary, 'acl', "$acl", '', 'custom-select'); 168 | $res = db::qry(" 169 | SELECT login,id 170 | FROM `accounts` 171 | WHERE acl = :0 OR acl = :1", ['0' => 0, '1' => 1]); 172 | 173 | foreach($res as $k => $v) $grp_ary[] = [$v['login'], $v['id']]; 174 | $grp_buf = $this->dropdown($grp_ary, 'grp', "$grp", '', 'custom-select'); 175 | $aclgrp_buf = ' 176 |
    177 |
    ' . $acl_buf . ' 178 |
    179 |
    180 |
    ' . $grp_buf . ' 181 |
    '; 182 | } else { 183 | $aclgrp_buf = ''; 184 | $anotes_buf = ''; 185 | } 186 | 187 | return ' 188 |
    189 |

    190 | Accounts 191 | 192 | 193 |

    194 |
    195 |
    196 |
    197 |
    198 |
    199 | 200 | 201 | 202 |
    203 |
    204 |
    205 | 206 | 207 |
    208 |
    209 | 210 | 211 |
    212 |
    213 |
    214 |
    215 | 216 | 217 |
    218 |
    219 | 220 | 221 |
    222 |
    223 |
    ' . $aclgrp_buf . ' 224 |
    225 |
    226 |
    227 |
    ' . $switch . ' 228 |
    229 |
    230 |
    ' . $submit . ' 231 |
    232 |
    233 |
    234 |
    235 |
    ' . $removemodal; 236 | } 237 | } 238 | 239 | ?> 240 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/auth.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Auth extends Themes_Bootstrap4_Theme 6 | { 7 | // forgotpw (create new pw) 8 | public function create(array $in) : string 9 | { 10 | elog(__METHOD__); 11 | 12 | extract($in); 13 | 14 | return ' 15 |
    16 |

    Forgot password

    17 |
    18 | 19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 | 27 | You will receive an email with further instructions and please note that this only resets the password for this website interface. 28 | 29 |
    30 |
    31 | « Back 32 | 33 |
    34 |
    35 | 36 |
    37 |
    '; 38 | } 39 | 40 | // signin (read current pw) 41 | public function list(array $in) : string 42 | { 43 | elog(__METHOD__); 44 | elog(var_export($in,true)); 45 | 46 | extract($in); 47 | 48 | return ' 49 |
    50 |

    Sign in

    51 |
    52 | 53 | 54 | 55 |
    56 |
    57 |
    58 |
    59 | 60 |
    61 | 62 |
    63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 |
    70 | 71 | 72 |
    73 |
    74 |
    75 |
    76 | Forgot password 77 | 78 |
    79 |
    80 |
    81 |
    '; 82 | } 83 | 84 | // resetpw (update pw) 85 | public function update(array $in) : string 86 | { 87 | elog(__METHOD__); 88 | elog(var_export($in,true)); 89 | 90 | extract($in); 91 | 92 | return ' 93 |
    94 |

    Update Password

    95 |
    96 | 97 | 98 | 99 | 100 |

    For ' . $login . '

    101 | 102 |
    103 |
    104 |
    105 |
    106 | 107 |
    108 | 109 |
    110 |
    111 |
    112 |
    113 | 114 |
    115 |
    116 |
    117 | 118 |
    119 |
    120 |
    121 |
    '; 122 | } 123 | } 124 | 125 | ?> 126 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/dkim.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Dkim extends Themes_Bootstrap4_Theme 6 | { 7 | public function read(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | $remove = $this->modal([ 12 | 'id' => 'removemodal', 13 | 'title' => 'Remove DKIM Record', 14 | 'action' => 'delete', 15 | 'footer' => 'Remove', 16 | 'hidden' => ' 17 | ', 18 | 'body' => ' 19 |

    Are you sure you want to remove DKIM record for
    ' . $in['domain'] . '

    ', 20 | ]); 21 | 22 | return ' 23 |
    24 |

    25 | DKIM 26 | 27 | 28 |

    29 |
    30 |
    31 |
    32 |
    ' . $in['buf'] . ' 33 |
    34 |
    35 | ' . $remove; 36 | 37 | } 38 | 39 | public function list(array $in) : string 40 | { 41 | elog(__METHOD__); 42 | 43 | $keybuf = $this->dropdown([ 44 | ['1024', '1024'], 45 | ['2048', '2048'], 46 | ['4096', '4096'], 47 | ], 'keylen', '2048', '', 'custom-select'); 48 | 49 | $create = $this->modal([ 50 | 'id' => 'createmodal', 51 | 'title' => 'Create DKIM Record', 52 | 'action' => 'create', 53 | 'footer' => 'Create', 54 | 'body' => ' 55 |
    56 | 57 | 58 |
    59 |
    60 |
    61 |
    62 | 63 | 64 |
    65 |
    66 |
    67 |
    68 | ' . $keybuf . ' 69 |
    70 |
    71 |
    ', 72 | ]); 73 | 74 | return ' 75 |
    76 |

    77 | DKIM 78 | 79 | 80 |

    81 |
    82 | 83 |
    84 |
    ' . $in['buf'] . ' 85 |
    86 |
    87 | ' . $create; 88 | } 89 | } 90 | 91 | ?> 92 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/home.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Home extends Themes_Bootstrap4_Theme 6 | { 7 | public function list(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | return ' 12 |
    13 |

    14 | NetServa HCP 15 |

    16 |

    17 | This is an ultra simple web based Hosting Control Panel for a 18 | lightweight Mail, Web and DNS server based on Ubuntu Bionic (18.04). It 19 | uses PowerDNS for DNS, Postfix/Dovecot + Spamprobe for SMTP and spam 20 | filtered IMAP email hosting along with nginx + PHP7 FPM + LetsEncrypt SSL 21 | for efficient and secure websites. It can use either SQLite or MySQL as 22 | database backends and the SQLite version only requires 60Mb of ram 23 | on a fresh install so it is ideal for lightweight 256Mb ram LXD containers 24 | or KVM/Xen cloud provisioning. 25 |

    26 |

    27 | Some of the features are... 28 |

    29 |
      30 |
    • NetServa HCP does not reqire Python or Ruby, just PHP and Bash
    • 31 |
    • Fully functional Mail server with personalised Spam filtering
    • 32 |
    • Secure SSL enabled nginx web server with PHP FPM 7+
    • 33 |
    • Always based and tested on the latest release of *buntu
    • 34 |
    • Optional DNS server for local LAN or real-world DNS provisioning
    • 35 |
    • Built from the ground up using Bootstrap 4 and DataTables
    • 36 |
    37 |

    38 | You can change the content of this page by creating a file called 39 | lib/php/home.tpl and add any Bootstrap 4 based layout and 40 | text you care to. For example... 41 |

    42 |
    43 | <div class="col-12">
    44 | <h1>Your Page Title</h1>
    45 | <p>Lorem ipsum...</p>
    46 | </div>
    47 | 
    48 |

    49 | Modifying the navigation menus above can be done by creating 50 | a lib/.ht_conf.php file and copying the 51 | 52 | $nav1 array from index.php into that optional config override file. 53 | Comments and pull requests are most welcome via the Issue Tracker link below. 54 |

    55 |

    56 | 57 | Project Page 58 | 59 | Issue Tracker 60 |

    61 |
    '; 62 | } 63 | } 64 | 65 | ?> 66 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/infomail.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_InfoMail extends Themes_Bootstrap4_Theme 6 | { 7 | public function list(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | extract($in); 12 | 13 | return ' 14 |
    15 |

    MailServer Info

    16 |
    17 |
    18 |
    19 | 20 | 21 |
    22 | 23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 |
    Mail Queue
    30 |
    ' . $mailq . '
    31 |
    32 |
    33 |
    34 |
    35 |
    ' . $pflogs . '
    36 |             
    37 |
    38 |
    '; 39 | } 40 | } 41 | // 42 | 43 | ?> 44 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/infosys.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_InfoSys extends Themes_Bootstrap4_Theme 6 | { 7 | public function list(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | elog(var_export($in,true)); 12 | 13 | extract($in); 14 | 15 | return ' 16 |
    17 |

    System Info

    18 |
    19 |
    20 |
    21 | 22 | 23 |
    24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 |
    31 |
    32 |
    RAM ' . $mem_used . ' / ' . $mem_total . ', ' . $mem_free . ' free
    33 |
    34 |
    ' . $mem_text . ' 36 |
    37 |
    38 |
    39 |
    Disk ' . $dsk_used . ' / ' . $dsk_total . ', ' . $dsk_free . ' free
    40 |
    41 |
    ' . $dsk_text . ' 43 |
    44 |
    45 |
    46 |
    CPU ' .$cpu_all . '
    47 |
    48 |
    ' . $cpu_text . ' 50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
    Hostname' .$hostname . '
    Host IP' . $host_ip . '
    Distro' . $os_name . '
    Uptime' . $uptime . '
    CPU Load' . $loadav . ' (' . $cpu_num . ' cpus)
    CPU Model' . $cpu_name . '
    Kernel Version' . $kernel . '
    88 |
    89 |
    90 |
    '; 91 | } 92 | } 93 | 94 | ?> 95 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/processes.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Processes extends Themes_Bootstrap4_Theme 6 | { 7 | public function list(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | return ' 12 |
    13 |

    Processes

    14 |
    15 |
    16 |
    17 | 18 | 19 |
    20 | 21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 |
    Process List (' . (count(explode("\n", $in['procs'])) - 1) . ')
    28 |
    ' . $in['procs'] . '
    29 |             
    30 |
    31 |
    '; 32 | } 33 | } 34 | 35 | ?> 36 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/records.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Records extends Themes_Bootstrap4_Theme 6 | { 7 | private $types = [ 8 | ['A', 'A'], 9 | ['MX', 'MX'], 10 | ['NS', 'NS'], 11 | ['TXT', 'TXT'], 12 | ['AAAA', 'AAAA'], 13 | ['CAA', 'CAA'], 14 | ['AFSDB', 'AFSDB'], 15 | ['CERT', 'CERT'], 16 | ['CNAME', 'CNAME'], 17 | ['DHCID', 'DHCID'], 18 | ['DLV', 'DLV'], 19 | ['DNSKEY', 'DNSKEY'], 20 | ['DS', 'DS'], 21 | ['EUI48', 'EUI48'], 22 | ['EUI64', 'EUI64'], 23 | ['HINFO', 'HINFO'], 24 | ['IPSECKEY', 'IPSECKEY'], 25 | ['KEY', 'KEY'], 26 | ['KX', 'KX'], 27 | ['LOC', 'LOC'], 28 | ['MINFO', 'MINFO'], 29 | ['MR', 'MR'], 30 | ['NAPTR', 'NAPTR'], 31 | ['NSEC', 'NSEC'], 32 | ['NSEC3', 'NSEC3'], 33 | ['NSEC3PARAM', 'NSEC3PARAM'], 34 | ['OPT', 'OPT'], 35 | ['PTR', 'PTR'], 36 | ['RKEY', 'RKEY'], 37 | ['RP', 'RP'], 38 | ['RRSIG', 'RRSIG'], 39 | ['SPF', 'SPF'], 40 | ['SRV', 'SRV'], 41 | ['SSHFP', 'SSHFP'], 42 | ['TLSA', 'TLSA'], 43 | ['TSIG', 'TSIG'], 44 | ['WKS', 'WKS'], 45 | ]; 46 | 47 | public function list(array $in) : string 48 | { 49 | elog(__METHOD__); 50 | 51 | elog('in='.var_export($in, true)); 52 | 53 | return ' 54 |
    55 |

    56 | ' . $in['domain'] . ' 57 | 58 | 59 |

    60 |
    61 | 62 |
    63 |
    64 |
    65 | 66 | 67 | 68 | 69 | 70 |
    71 |
    72 | 73 |
    74 |
    75 |
    76 |
    77 | 78 |
    79 |
    80 |
    81 |
    ' . ($this->dropdown($this->types, 'type', 'A', '', 'custom-select')) . ' 82 |
    83 |
    84 |
    85 |
    86 | 87 |
    88 |
    89 |
    90 |
    91 | 92 |
    93 |
    94 |
    95 |
    96 | 97 |
    98 |
    99 |
    100 |
    101 |
    102 |
    103 |
    104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
    NameContentTypePriorityTTL
    118 |
    119 | '; 184 | } 185 | } 186 | 187 | ?> 188 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/theme.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Theme extends Theme 6 | { 7 | public function css() : string 8 | { 9 | elog(__METHOD__); 10 | 11 | return ' 12 | 13 | 14 | 15 | 16 | '; 80 | } 81 | 82 | public function log() : string 83 | { 84 | elog(__METHOD__); 85 | 86 | $alts = ''; 87 | foreach (util::log() as $lvl => $msg) { 88 | $alts .= $msg ? ' 89 |
    90 | 95 |
    ' : ''; 96 | } 97 | return $alts; 98 | } 99 | 100 | public function head() : string 101 | { 102 | elog(__METHOD__); 103 | 104 | return ' 105 | '; 121 | } 122 | 123 | public function nav1(array $a = []) : string 124 | { 125 | elog(__METHOD__); 126 | 127 | $a = isset($a[0]) ? $a : util::get_nav($this->g->nav1); 128 | $o = '?o=' . $this->g->in['o']; 129 | $t = '?t=' . util::ses('t'); 130 | return join('', array_map(function ($n) use ($o, $t) { 131 | if (is_array($n[1])) return $this->nav_dropdown($n); 132 | $c = $o === $n[1] || $t === $n[1] ? ' active' : ''; 133 | $i = isset($n[2]) ? ' ' : ''; 134 | return ' 135 | '; 136 | }, $a)); 137 | } 138 | 139 | public function nav2() : string 140 | { 141 | elog(__METHOD__); 142 | 143 | return $this->nav_dropdown(['Theme', $this->g->nav2, 'fa fa-th fa-fw']); 144 | } 145 | 146 | public function nav3() : string 147 | { 148 | elog(__METHOD__); 149 | 150 | if (util::is_usr()) { 151 | $usr[] = ['Change Profile', '?o=accounts&m=read&i=' . $_SESSION['usr']['id'], 'fas fa-user fa-fw']; 152 | $usr[] = ['Change Password', '?o=auth&m=update&i=' . $_SESSION['usr']['id'], 'fas fa-key fa-fw']; 153 | $usr[] = ['Sign out', '?o=auth&m=delete', 'fas fa-sign-out-alt fa-fw']; 154 | 155 | if (util::is_adm() && !util::is_acl(0)) $usr[] = 156 | ['Switch to sysadm', '?o=accounts&m=switch_user&i=' . $_SESSION['adm'], 'fas fa-user fa-fw']; 157 | 158 | return $this->nav_dropdown([$_SESSION['usr']['login'], $usr, 'fas fa-user fa-fw']); 159 | } else return ''; 160 | } 161 | 162 | public function nav_dropdown(array $a = []) : string 163 | { 164 | elog(__METHOD__); 165 | 166 | $o = '?o=' . $this->g->in['o']; 167 | $i = isset($a[2]) ? ' ' : ''; 168 | return ' 169 | '; 179 | } 180 | 181 | public function main() : string 182 | { 183 | elog(__METHOD__); 184 | 185 | return ' 186 |
    187 |
    ' . $this->g->out['log'] . $this->g->out['main'] . ' 188 |
    189 |
    '; 190 | } 191 | 192 | public function js() : string 193 | { 194 | elog(__METHOD__); 195 | 196 | return ' 197 | 198 | 199 | 200 | 201 | 202 | 203 | '; 204 | } 205 | 206 | protected function modal(array $ary) : string 207 | { 208 | elog(__METHOD__); 209 | 210 | extract($ary); 211 | $hidden = isset($hidden) && $hidden ? $hidden : ''; 212 | $footer = $footer ? ' 213 | ' : ''; 217 | 218 | return ' 219 | '; 239 | } 240 | 241 | } 242 | 243 | ?> 244 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/valias.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Valias extends Themes_Bootstrap4_Theme 6 | { 7 | public function create(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | return $this->editor($in); 12 | } 13 | 14 | public function update(array $in) : string 15 | { 16 | elog(__METHOD__); 17 | 18 | return $this->editor($in); 19 | } 20 | 21 | public function list(array $in) : string 22 | { 23 | elog(__METHOD__); 24 | 25 | return ' 26 |
    27 |

    28 | Aliases 29 | 30 | 31 | 32 |

    33 |
    34 |
    35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
    AliasTarget AddressDomain
    49 |
    50 | '; 67 | } 68 | 69 | private function editor(array $in) : string 70 | { 71 | elog(__METHOD__); 72 | 73 | extract($in); 74 | 75 | $active = $active ? 1 : 0; 76 | $actbuf = $active ? ' checked' : ''; 77 | $header = $this->g->in['m'] === 'create' ? 'Add new Alias' : 'Aliases 78 | 79 | '; 80 | $tolist = ' 81 | « Back'; 82 | $submit = $this->g->in['m'] === 'create' ? $tolist . ' 83 | ' : $tolist . ' 84 | '; 85 | $remove = $this->g->in['m'] === 'create' ? '' : $this->modal([ 86 | 'id' => 'removemodal', 87 | 'title' => 'Remove Alias', 88 | 'action' => 'delete', 89 | 'footer' => 'Remove', 90 | 'hidden' => ' 91 | ', 92 | 'body' => ' 93 |

    Are you sure you want to remove this alias?
    ' . $source . '

    ', 94 | ]); 95 | 96 | return ' 97 |
    98 |

    99 | ' . $header . ' 100 |

    101 |
    102 |
    103 |
    104 |
    105 |

    Note: If your chosen destination address is an external mailbox, the receiving mailserver may reject your message due to an SPF failure.

    106 |
    107 | 108 | 109 | 110 |
    111 |
    112 | 113 | 114 |

    Full email address/es or @example.com, to catch all messages for a domain (comma-separated). Locally hosted domains only.

    115 |
    116 |
    117 | 118 | 119 |

    Full email address/es (comma-separated).

    120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 | 127 | 128 |
    129 |
    130 |
    131 |
    132 |
    ' . $submit . ' 133 |
    134 |
    135 |
    136 |
    137 |
    ' . $remove; 138 | } 139 | } 140 | 141 | ?> 142 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/vhosts.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Vhosts extends Themes_Bootstrap4_Theme 6 | { 7 | public function update(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | $remove = $this->modal([ 12 | 'id' => 'removemodal', 13 | 'title' => 'Remove Vhost', 14 | 'action' => 'delete', 15 | 'footer' => 'Remove', 16 | // 'hidden' => ' 17 | // ', 18 | 'body' => ' 19 |

    Are you sure you want to remove this Vhost?
    ' . $in['domain'] . '

    ', 20 | ]); 21 | 22 | return ' 23 |
    24 |

    25 | Vhosts 26 | 27 | 28 |

    29 |
    30 |
    31 |
    32 |
    33 |
    34 | 35 | 36 | 37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 | 44 | 45 |
    46 |
    47 | 48 | 49 |
    50 |
    51 | 52 | 53 |
    54 |
    55 | 56 | 57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    63 | 64 | 65 |
    66 |
    67 |
    68 |
    69 |
    70 | « Back 71 | 72 |
    73 |
    74 |
    75 |
    76 |
    ' . $remove; 77 | } 78 | 79 | public function list(array $in) : string 80 | { 81 | elog(__METHOD__); 82 | 83 | $create = $this->modal([ 84 | 'id' => 'createmodal', 85 | 'title' => 'Create New Vhost', 86 | 'action' => 'create', 87 | 'footer' => 'Create', 88 | 'body' => ' 89 |
    90 | 91 | 92 |
    93 |
    94 |
    95 |
    96 |
    97 | 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 | 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 | 115 | 116 |
    117 |
    118 |
    119 |
    120 | 121 | 122 |
    123 |
    124 |
    ', 125 | ]); 126 | 127 | return ' 128 |
    129 |

    130 | Vhosts 131 | 132 | 133 |

    134 |
    135 |
    136 |
    137 |
    138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
    DomainAlias Mbox Mail Disk 
    160 |
    ' . $create . ' 161 | '; 190 | } 191 | } 192 | 193 | ?> 194 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap4/vmails.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 4 | 5 | class Themes_Bootstrap4_Vmails extends Themes_Bootstrap4_Theme 6 | { 7 | public function list(array $in) : string 8 | { 9 | elog(__METHOD__); 10 | 11 | $create = $this->modal([ 12 | 'id' => 'createmodal', 13 | 'title' => 'Create New Mailbox', 14 | 'action' => 'create', 15 | 'footer' => 'Create', 16 | 'body' => ' 17 |
    18 | 19 | 20 |
    ', 21 | ]); 22 | 23 | $remove = $this->modal([ 24 | 'id' => 'removemodal', 25 | 'title' => 'Remove Mailbox', 26 | 'action' => 'delete', 27 | 'footer' => 'Remove', 28 | 'body' => ' 29 | 30 |

    Are you sure you want to remove this mailbox?

    ', 31 | ]); 32 | 33 | $update = $this->modal([ 34 | 'id' => 'updatemodal', 35 | 'title' => 'Change Password', 36 | 'action' => 'update', 37 | 'footer' => 'Update', 38 | 'body' => ' 39 | 40 |
    41 |
    42 | Show 43 |
    44 | 45 |
    46 | NewPW 47 |
    48 |
    ', 49 | ]); 50 | 51 | return ' 52 |
    53 |

    54 | Mailboxes 55 | 56 | 57 | 58 |

    59 |
    60 |
    61 |
    62 |
    63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    EmailUsage Messages 
    75 |
    ' . $create . $remove . $update .' 76 | '; 125 | } 126 | } 127 | 128 | ?> 129 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/accounts.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Accounts extends Themes_Bootstrap5_Theme 9 | { 10 | public function create(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | return $this->modal_content([ 15 | 'title' => 'Create new user', 16 | 'action' => 'create', 17 | 'lhs_cmd' => '', 18 | 'rhs_cmd' => 'Create', 19 | 'body' => $this->modal_body($in) 20 | ]); 21 | } 22 | 23 | public function read(array $in): string 24 | { 25 | elog(__METHOD__); 26 | 27 | return $this->modal_content([ 28 | 'title' => 'Update user', 29 | 'action' => 'update', 30 | 'lhs_cmd' => 'Delete', 31 | 'rhs_cmd' => 'Update', 32 | 'body' => $this->modal_body($in) 33 | ]); 34 | } 35 | 36 | public function delete(): ?string 37 | { 38 | elog(__METHOD__); 39 | 40 | $usr = db::read('login', 'id', $this->g->in['i'], '', 'one'); 41 | 42 | return $this->modal_content([ 43 | 'title' => 'Remove User', 44 | 'action' => 'delete', 45 | 'lhs_cmd' => '', 46 | 'rhs_cmd' => 'Remove', 47 | 'hidden' => sprintf('', $this->g->in['i']), 48 | 'body' => sprintf('

    Are you sure you want to remove this user?
    %s

    ', $usr['login']), 49 | ]); 50 | } 51 | 52 | public function list(array $in): string 53 | { 54 | elog(__METHOD__); 55 | 56 | return << 58 |

    59 | Accounts 60 | 61 | 62 | 63 |

    64 |
    65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
    User IDFirst NameLast NameAlt EmailACLGrp
    80 |
    81 | 85 | 89 | 93 | 116 | HTML; 117 | } 118 | 119 | private function modal_body(array $in): string 120 | { 121 | elog(__METHOD__); 122 | 123 | $acl = $_SESSION['usr']['acl']; 124 | $grp = $_SESSION['usr']['grp']; 125 | 126 | $acl_ary = array_map(fn($k, $v) => [$v, $k], array_keys($this->g->acl), $this->g->acl); 127 | $acl_buf = $this->dropdown($acl_ary, 'acl', "{$acl}", '', 'form-select'); 128 | 129 | $res = db::qry('SELECT login, id FROM `accounts` WHERE acl IN (0, 1)'); 130 | $grp_ary = array_map(fn($row) => [$row['login'], $row['id']], $res); 131 | $grp_buf = $this->dropdown($grp_ary, 'grp', "{$grp}", '', 'form-select'); 132 | 133 | $aclgrp_buf = << 135 |
    136 | $acl_buf 137 |
    138 |
    139 | $grp_buf 140 |
    141 | 142 | HTML; 143 | 144 | return << 146 |
    147 | 148 | 149 |
    150 |
    151 | 152 | 153 |
    154 | 155 |
    156 |
    157 | 158 | 159 |
    160 |
    161 | 162 | 163 |
    164 |
    165 | $aclgrp_buf 166 | HTML; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/auth.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Auth extends Themes_Bootstrap5_Theme 9 | { 10 | public function create(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | $login = $in['login'] ?? ''; 15 | return << 17 |

    Forgot password

    18 |
    19 | 20 | 21 |
    22 |
    23 |
    24 |
    25 | 26 |
    27 | 28 | You will receive an email with further instructions and please note that this only resets the password for this website interface. 29 | 30 |
    31 |
    32 | « Back 33 | 34 |
    35 |
    36 |
    37 | 38 | HTML; 39 | } 40 | 41 | public function list(array $in): string 42 | { 43 | elog(__METHOD__); 44 | 45 | $login = $in['login'] ?? ''; 46 | return << 48 |

    Sign in

    49 |
    50 | 51 | 52 |
    53 | 54 |
    55 | 56 | 57 |
    58 |
    59 |
    60 | 61 |
    62 | 63 | 64 |
    65 |
    66 |
    67 | 68 | 71 |
    72 |
    73 |
    74 | Forgot password 75 | 76 |
    77 |
    78 |
    79 | 80 | HTML; 81 | } 82 | 83 | public function update(array $in): string 84 | { 85 | elog(__METHOD__); 86 | 87 | $id = $in['id'] ?? ''; 88 | $login = $in['login'] ?? ''; 89 | return << 91 |

    Update Password

    92 |
    93 | 94 | 95 | 96 | 97 |

    For {$login}

    98 | 99 |
    100 |
    101 |
    102 |
    103 | 104 |
    105 | 106 |
    107 |
    108 |
    109 |
    110 | 111 |
    112 |
    113 |
    114 | 115 |
    116 |
    117 |
    118 | 119 | HTML; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/dkim.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Dkim extends Themes_Bootstrap5_Theme 9 | { 10 | public function create(): string 11 | { 12 | elog(__METHOD__); 13 | 14 | $keybuf = $this->dropdown([ 15 | ['1024', '1024'], 16 | ['2048', '2048'], 17 | ['4096', '4096'], 18 | ], 'keylen', '2048', '', 'form-select'); 19 | 20 | return $this->modal([ 21 | 'id' => 'createmodal', 22 | 'title' => 'Create SSH Host', 23 | 'action' => 'create', 24 | 'lhs_cmd' => '', 25 | 'rhs_cmd' => 'Create', 26 | 'body' => << 28 | 29 | 30 | 31 |
    32 |
    33 | 34 | 35 |
    36 |
    37 | 38 | $keybuf 39 |
    40 |
    41 | HTML, 42 | ]); 43 | } 44 | 45 | public function read(array $in): string 46 | { 47 | elog(__METHOD__); 48 | 49 | return << 51 |

    52 | DKIM 53 | 54 | 55 | 56 |

    57 | 58 |
    {$in['buf']}
    59 | {$this->delete($in)} 60 | HTML; 61 | } 62 | 63 | public function delete(array $in): string 64 | { 65 | elog(__METHOD__); 66 | 67 | return $this->modal([ 68 | 'id' => 'removemodal', 69 | 'title' => 'Remove DKIM Record', 70 | 'action' => 'delete', 71 | 'lhs_cmd' => '', 72 | 'rhs_cmd' => 'Remove', 73 | 'hidden' => sprintf('', $in['domain']), 74 | 'body' => sprintf('

    Are you sure you want to remove DKIM record for
    %s

    ', $in['domain']), 75 | ]); 76 | } 77 | 78 | public function list(array $in): string 79 | { 80 | elog(__METHOD__); 81 | 82 | return << 84 |

    85 | DKIM 86 | 87 | 88 | 89 |

    90 | 91 |
    {$in['buf']}
    92 | {$this->create()} 93 | HTML; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/home.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Home extends Themes_Bootstrap5_Theme 9 | { 10 | public function list(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | return << 16 |

    NetServa HCP

    17 |

    18 | This is a lightweight Web, Mail and DNS server with a PHP based 19 | Hosting Control Panel for servicing multiple virtually 20 | hosted domains. The operating system is based on the latest 21 | Debian or Ubuntu packages and can use either SQLite or MySQL as 22 | a backend database. The entire server can run in as little as 256 23 | MB of ram when paired with SQLite and still serve a dozen lightly 24 | loaded hosts so it is ideal for LXD and Proxmox virtual machines 25 | and containers. 26 |

    27 | 28 | Project Page 29 | 30 | 31 | Issue Tracker 32 | 33 | 34 |
    35 |
    36 |
    37 |

    Features

    38 |
      39 |
    • NetServa HCP does not require Python or Ruby, just PHP and Bash
    • 40 |
    • Fully functional Mail server with personalised Spam filtering
    • 41 |
    • Secure SSL enabled nginx web server with PHP FPM 8+
    • 42 |
    • Always based and tested on the latest release on Ubuntu and Debian
    • 43 |
    • Optional DNS server for local LAN or real-world DNS provisioning
    • 44 |
    • Built from the ground up using Bootstrap 5 and DataTables
    • 45 |
    46 |

    Software

    47 |
      48 |
    • nginx and PHP8+ FPM for web services
    • 49 |
    • Postfix for SMTP email delivery
    • 50 |
    • Dovecot and Spamprobe spam filtered IMAP email
    • 51 |
    • Acme.sh and LetsEncrypt SSL for SSL certificates
    • 52 |
    • PowerDNS for DNS
    • 53 |
    • WordPress when paired with Mysql/Mariadb
    • 54 |
    55 |
    56 |
    57 |
    58 |
    59 |

    Notes

    60 |

    61 | You can change the content of this page by creating a file 62 | called lib/php/home.tpl and add any Bootstrap 5 63 | based layout and text you care to. Modifying the navigation 64 | menus above can be done by creating a lib/.ht_conf.php 65 | file and copying the 66 | \$nav1 array 67 | from index.php into that optional config override file. 68 |

    69 |
    70 |
    71 |
    72 | HTML; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/infomail.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_InfoMail extends Themes_Bootstrap5_Theme 9 | { 10 | public function list(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | $csrfToken = $_SESSION['c'] ?? ''; 15 | $mailq = htmlspecialchars($in['mailq'] ?? ''); 16 | $pflogs = htmlspecialchars($in['pflogs'] ?? ''); 17 | 18 | return << 20 |

    Mailserver Info

    21 |
    22 | 23 | 24 |
    25 | 26 |
    27 |
    28 | 29 |
    30 |
    31 |

    Mail Queue

    32 |
    {$mailq}
    33 |
    34 |
    35 |
    36 |
    37 |
    {$pflogs}
    38 |
    39 |
    40 | HTML; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/infosys.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_InfoSys extends Themes_Bootstrap5_Theme 9 | { 10 | public function list(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | $data = $this->prepareData($in); 15 | return $this->generateInfoSysContent($data); 16 | } 17 | 18 | private function prepareData(array $in): array 19 | { 20 | elog(__METHOD__); 21 | 22 | return [ 23 | 'csrfToken' => $_SESSION['c'] ?? '', 24 | 'hostname' => $in['hostname'] ?? '', 25 | 'host_ip' => $in['host_ip'] ?? '', 26 | 'os_name' => $in['os_name'] ?? '', 27 | 'uptime' => $in['uptime'] ?? '', 28 | 'loadav' => $in['loadav'] ?? '', 29 | 'cpu_num' => $in['cpu_num'] ?? '', 30 | 'cpu_name' => $in['cpu_name'] ?? '', 31 | 'kernel' => $in['kernel'] ?? '', 32 | 'mem_used' => $in['mem_used'] ?? '', 33 | 'mem_total' => $in['mem_total'] ?? '', 34 | 'mem_free' => $in['mem_free'] ?? '', 35 | 'mem_pcnt' => $in['mem_pcnt'] ?? '', 36 | 'mem_color' => $in['mem_color'] ?? '', 37 | 'mem_text' => $in['mem_text'] ?? '', 38 | 'dsk_used' => $in['dsk_used'] ?? '', 39 | 'dsk_total' => $in['dsk_total'] ?? '', 40 | 'dsk_free' => $in['dsk_free'] ?? '', 41 | 'dsk_pcnt' => $in['dsk_pcnt'] ?? '', 42 | 'dsk_color' => $in['dsk_color'] ?? '', 43 | 'dsk_text' => $in['dsk_text'] ?? '', 44 | 'cpu_all' => $in['cpu_all'] ?? '', 45 | 'cpu_pcnt' => $in['cpu_pcnt'] ?? '', 46 | 'cpu_color' => $in['cpu_color'] ?? '', 47 | 'cpu_text' => $in['cpu_text'] ?? '', 48 | ]; 49 | } 50 | 51 | private function generateInfoSysContent(array $data): string 52 | { 53 | elog(__METHOD__); 54 | 55 | $progressBar = fn($label, $used, $total, $free, $pcnt, $color, $text) => <<{$label}
    Used: {$used} - Total: {$total} - Free: {$free} 57 |
    58 |
    {$text} 60 |
    61 |
    62 | HTML; 63 | $progressBar2 = fn($label, $used, $total, $free, $pcnt, $color, $text) => <<{$label}
    {$used} 65 |
    66 |
    {$text} 68 |
    69 |
    70 | HTML; 71 | 72 | return << 74 |

    System Info

    75 |
    76 | 77 | 78 |
    79 | 80 |
    81 |
    82 | 83 |
    84 |
    85 |
    86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
    Hostname{$data['hostname']}
    Host IP{$data['host_ip']}
    Distro{$data['os_name']}
    Uptime{$data['uptime']}
    CPU Load{$data['loadav']} - {$data['cpu_num']} cpus
    CPU Model{$data['cpu_name']}
    Kernel Version{$data['kernel']}
    97 |
    98 |
    99 |
    100 |
    101 | {$progressBar('RAM', $data['mem_used'], $data['mem_total'], $data['mem_free'], $data['mem_pcnt'], $data['mem_color'], $data['mem_text'])} 102 | {$progressBar('DISK', $data['dsk_used'], $data['dsk_total'], $data['dsk_free'], $data['dsk_pcnt'], $data['dsk_color'], $data['dsk_text'])} 103 | {$progressBar2('CPU', $data['cpu_all'], '', '', $data['cpu_pcnt'], $data['cpu_color'], $data['cpu_text'])} 104 |
    105 |
    106 |
    107 | HTML; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/processes.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Processes extends Themes_Bootstrap5_Theme 9 | { 10 | public function list(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | $csrfToken = $_SESSION['c'] ?? ''; 15 | $procs = htmlspecialchars($in['procs'] ?? ''); 16 | $processCount = count(explode("\n", $in['procs'] ?? '')) - 1; 17 | 18 | return << 20 |

    21 | Processes ({$processCount}) 22 |

    23 |
    24 | 25 | 26 |
    27 | 30 |
    31 |
    32 | 33 |
    34 |
    35 |
    {$procs}
    36 |
    37 |
    38 | HTML; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/records.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Records extends Themes_Bootstrap5_Theme 9 | { 10 | private const TYPES = [ 11 | 'A', 'MX', 'NS', 'TXT', 'AAAA', 'CAA', 'AFSDB', 'CERT', 'CNAME', 'DHCID', 12 | 'DLV', 'DNSKEY', 'DS', 'EUI48', 'EUI64', 'HINFO', 'IPSECKEY', 'KEY', 'KX', 13 | 'LOC', 'MINFO', 'MR', 'NAPTR', 'NSEC', 'NSEC3', 'NSEC3PARAM', 'OPT', 'PTR', 14 | 'RKEY', 'RP', 'RRSIG', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'TSIG', 'WKS' 15 | ]; 16 | 17 | public function list(array $in): string 18 | { 19 | elog(__METHOD__); 20 | 21 | return $this->generateHtml($in) . $this->generateJavaScript($in); 22 | } 23 | 24 | private function generateHtml(array $in): string 25 | { 26 | elog(__METHOD__); 27 | 28 | $csrfToken = $_SESSION['c'] ?? ''; 29 | $currentObject = $this->g->in['o'] ?? ''; 30 | $domainId = $in['did'] ?? ''; 31 | $domain = htmlspecialchars($in['domain'] ?? ''); 32 | $typeDropdown = $this->dropdown(array_map(fn($type) => [$type, $type], self::TYPES), 'type', 'A', '', 'form-select'); 33 | 34 | return << 36 |

    37 | {$domain} 38 | 39 | 40 | 41 |

    42 | 43 | 44 |
    45 |
    46 |
    47 | 48 | 49 | 50 | 51 | 52 |
    53 |
    54 | 55 |
    56 |
    57 |
    58 |
    59 | 60 |
    61 |
    62 |
    63 |
    {$typeDropdown}
    64 |
    65 |
    66 |
    67 | 68 |
    69 |
    70 |
    71 |
    72 | 73 |
    74 |
    75 |
    76 |
    77 | 78 |
    79 |
    80 |
    81 |
    82 |
    83 |
    84 |
    85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
    NameContentTypePriorityTTL
    99 |
    100 | HTML; 101 | } 102 | 103 | private function generateJavaScript(array $in): string 104 | { 105 | elog(__METHOD__); 106 | 107 | $domainId = $in['did'] ?? ''; 108 | return << 110 | $(document).ready(function() { 111 | $("#records").DataTable({ 112 | "processing": true, 113 | "serverSide": true, 114 | "ajax": "?x=json&o=records&m=list&did={$domainId}", 115 | "order": [[ 9, "desc" ]], 116 | "scrollX": true, 117 | "columnDefs": [ 118 | {"targets":0, "width":"30%"}, 119 | {"targets":1, "width":"40%", 120 | render: function ( data, type, row ) { 121 | return type === "display" && data.length > 40 ? data.substr( 0, 40 ) + "…" : data; }}, 122 | {"targets":2, "width":"3rem"}, 123 | {"targets":3, "width":"3rem"}, 124 | {"targets":4, "width":"3rem"}, 125 | {"targets":5, "width":"2rem", "className":"text-end", "sortable": false}, 126 | {"targets":6, "visible":false}, 127 | {"targets":7, "visible":false}, 128 | {"targets":8, "visible":false}, 129 | {"targets":9, "visible":false}, 130 | ], 131 | }); 132 | 133 | $(document).on("click", ".create", function(e) { 134 | e.preventDefault(); 135 | $("#m").val("Create").attr("class", "btn btn-success").removeAttr("disabled"); 136 | $("#name, #content").val(""); 137 | }); 138 | 139 | $(document).on("click", ".delete", function(e) { 140 | e.preventDefault(); 141 | $("#m").val("Delete").attr("class", "btn btn-danger").removeAttr("disabled"); 142 | }); 143 | 144 | $(document).on("click", ".update", function(e) { 145 | e.preventDefault(); 146 | $("#m").val("Update").attr("class", "btn btn-primary").removeAttr("disabled"); 147 | }); 148 | 149 | $(document).on("click", ".update,.delete", function(e) { 150 | e.preventDefault(); 151 | var row = $(this).closest("tr"); 152 | $("#i").val($(this).attr("data-rowid")); 153 | $("#name").val(row.find("td:eq(0)").text()); 154 | $("#content").val(row.find("td:eq(1)").text()); 155 | $("#type").val(row.find("td:eq(2)").text()); 156 | $("#prio").val(row.find("td:eq(3)").text()); 157 | $("#ttl").val(row.find("td:eq(4)").text()); 158 | }); 159 | }); 160 | 161 | JavaScript; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/php/themes/bootstrap5/valias.php: -------------------------------------------------------------------------------- 1 | (AGPL-3.0) 7 | 8 | class Themes_Bootstrap5_Valias extends Themes_Bootstrap5_Theme 9 | { 10 | public function create(array $in): string 11 | { 12 | elog(__METHOD__); 13 | 14 | return $this->modalContent( 15 | 'Create New Alias', 16 | 'create', 17 | '', 18 | 'Create', 19 | $this->modalBody($in) 20 | ); 21 | } 22 | 23 | public function update(array $in): string 24 | { 25 | elog(__METHOD__); 26 | 27 | return $this->modalContent( 28 | 'Update Alias', 29 | 'update', 30 | 'Delete', 31 | 'Update', 32 | $this->modalBody($in) 33 | ); 34 | } 35 | 36 | public function delete(): string 37 | { 38 | elog(__METHOD__); 39 | 40 | $source = db::read('source', 'id', $this->g->in['i'], '', 'one'); 41 | 42 | return $this->modalContent( 43 | 'Remove Alias', 44 | 'delete', 45 | '', 46 | 'Remove', 47 | $this->deleteModalBody($source['source'] ?? '') 48 | ); 49 | } 50 | 51 | public function list(array $in): string 52 | { 53 | elog(__METHOD__); 54 | 55 | return $this->generateListHTML(); 56 | } 57 | 58 | private function modalContent(string $title, string $action, string $lhsCmd, string $rhsCmd, string $body): string 59 | { 60 | elog(__METHOD__); 61 | 62 | return << 64 | 68 |
    69 | 72 | 75 |
    76 |
    77 | HTML; 78 | } 79 | 80 | private function modalBody(array $in): string 81 | { 82 | elog(__METHOD__); 83 | 84 | $activeChecked = ($in['active'] ?? false) ? ' checked' : ''; 85 | return << 87 | 88 | 89 |
    Full email address/es or @example.com, to catch all messages for a domain (comma-separated). Locally hosted domains only.
    90 | 91 |
    92 | 93 | 94 |
    Full email address/es (comma-separated).
    95 |
    96 |
    97 | 98 | 99 |
    100 | HTML; 101 | } 102 | 103 | private function deleteModalBody(string $source): string 104 | { 105 | elog(__METHOD__); 106 | 107 | $id = htmlspecialchars($this->g->in['i'], ENT_QUOTES, 'UTF-8'); 108 | return <<Are you sure you want to remove this alias?
    $source

    110 | 111 | HTML; 112 | } 113 | 114 | private function modalFooter(string $lhsCmd, string $rhsCmd): string 115 | { 116 | elog(__METHOD__); 117 | 118 | $lhsButton = $lhsCmd ? "" : ''; 119 | return <<Cancel 122 | 123 | HTML; 124 | } 125 | 126 | private function generateListHTML(): string 127 | { 128 | elog(__METHOD__); 129 | 130 | return << 132 |

    133 | Aliases 134 | 135 | 136 | 137 |

    138 | 139 |
    140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
    AliasTarget AddressDomain
    150 |
    151 | 154 | 157 | 160 | 192 | HTML; 193 | } 194 | } 195 | --------------------------------------------------------------------------------