1: <?php
2:
3: /**
4: * CLI script to build the site configuration
5: */
6:
7: if (mb_strtolower(php_sapi_name()) !== 'cli') {
8: die("Must be run from the command line\n");
9: }
10:
11: /* debug mode has to be set to something or include files will die() */
12: define('DEBUG_MODE', false);
13:
14: /* determine current absolute path used for require statements */
15: define('APP_PATH', dirname(dirname(__FILE__)).'/');
16: define('VENDOR_PATH', APP_PATH.'vendor/');
17: define('WEB_ROOT', '');
18: chdir(APP_PATH);
19:
20: /* get the framework */
21: require VENDOR_PATH.'autoload.php';
22: require APP_PATH.'lib/framework.php';
23:
24: $environment = Hm_Environment::getInstance();
25: $environment->load();
26:
27: /* check for proper php support */
28: check_php();
29:
30: /* create site */
31: build_config();
32:
33:
34: /**
35: * Check PHP for correct support
36: *
37: * @return void
38: * */
39: function check_php() {
40: $minVersion = 8.1;
41: $version = phpversion();
42: if (mb_substr($version, 0, 3) < $minVersion) {
43: die("Cypht requires PHP version $minVersion or greater");
44: }
45: if (!function_exists('mb_strpos')) {
46: die('Cypht requires PHP MB support');
47: }
48: if (!function_exists('curl_exec')) {
49: die('Cypht requires PHP cURL support');
50: }
51: if (!function_exists('openssl_random_pseudo_bytes')) {
52: die('Cypht requires PHP OpenSSL support');
53: }
54: if (!class_exists('PDO')) {
55: echo "\nWARNING: No PHP PDO support found, database featueres will not work\n\n";
56: }
57: if (!class_exists('Redis')) {
58: echo "\nWARNING: No PHP Redis support found, Redis caching or sessions will not work\n\n";
59: }
60: if (!class_exists('Memcached')) {
61: echo "\nWARNING: No PHP Memcached support found, Memcached caching or sessions will not work\n\n";
62: }
63: if (!class_exists('gnupg')) {
64: echo "\nWARNING: No PHP gnupg support found, The PGP module set will not work if enabled\n\n";
65: }
66: }
67:
68: /**
69: * build sub-resource integrity hash
70: */
71: function build_integrity_hash($data) {
72: return sprintf('sha512-%s', base64_encode(hash('sha512', $data, true)));
73: }
74:
75: /**
76: * Entry point into the configuration process
77: *
78: * @return void
79: */
80: function build_config() {
81: if (!Hm_Dispatch::is_php_setup()) {
82: printf("\nPHP is not correctly configured\n");
83: printf("\nMbstring: %s\n", function_exists('mb_strpos') ? 'yes' : 'no');
84: printf("Curl: %s\n", function_exists('curl_exec') ? 'yes' : 'no');
85: printf("Openssl: %s\n", function_exists('openssl_random_pseudo_bytes') ? 'yes' : 'no');
86: printf("PDO: %s\n\n", class_exists('PDO') ? 'yes' : 'no');
87: exit;
88: }
89:
90: /* get the site settings */
91: $settings = merge_config_files(APP_PATH.'config');
92:
93: if (is_array($settings) && !empty($settings)) {
94: $settings['version'] = VERSION;
95: /* determine compression commands */
96: list($js_compress, $css_compress) = compress_methods($settings);
97:
98: /* get module detail */
99: list($js, $css, $filters, $assets) = get_module_assignments($settings);
100:
101: /* combine and compress page content */
102: $hashes = combine_includes($js, $js_compress, $css, $css_compress, $settings);
103:
104: /* write out the dynamic.php file */
105: write_config_file($settings, $filters);
106:
107: /* create the production version */
108: create_production_site($assets, $settings, $hashes);
109:
110: process_bootswatch_files();
111: }
112: else {
113: printf("\nNo settings found in ini file\n");
114: }
115: }
116:
117: /**
118: * Compress a string
119: *
120: * @param $string string content to compress
121: * @param $command string command to do the compression
122: *
123: * @return string compressed string
124: */
125: function compress($string, $command, $file=false) {
126: if ($command) {
127: if ($file) {
128: exec("cat ./".$file." | $command", $output);
129: $result = join('', $output);
130: }
131: else {
132: exec("echo ".escapeshellarg($string)." | $command", $output);
133: $result = join('', $output);
134: }
135: }
136: else {
137: $result = $string;
138: }
139: if (!trim($result)) {
140: printf("WARNING: Compression command failed: %s\n", $command);
141: return $string;
142: }
143: return $result;
144: }
145:
146: /**
147: * Check for site specific compression commands
148: *
149: * @param $settings array site settings list
150: *
151: * @return array compression methods or false for none
152: */
153: function compress_methods($settings) {
154: $js_compress = false;
155: $css_compress = false;
156: if (isset($settings['js_compress']) && $settings['js_compress']) {
157: $js_compress = $settings['js_compress'];
158: }
159: if (isset($settings['css_compress']) && $settings['css_compress']) {
160: $css_compress = $settings['css_compress'];
161: }
162: return array($js_compress, $css_compress);
163: }
164:
165: /**
166: * Get module content and filters. This function has a side effect of setting
167: * up all the module assignments in Hm_Output_Modules and Hm_Handler_Modules.
168: * (this happens when the module set's setup.php file is included).
169: * These will be recorded later in the write_config_file function
170: *
171: * @param $settings array site settings list
172: *
173: * @return array js and css blobs, combined filers array, and module assets
174: */
175: function get_module_assignments($settings) {
176: $js = '';
177: $css = '';
178: $assets = array();
179: $core = false;
180: $js_exclude_dependencies = explode(',', ($settings['js_exclude_deps'] ?? ''));
181: $filters = array('allowed_output' => array(), 'allowed_get' => array(), 'allowed_cookie' => array(),
182: 'allowed_post' => array(), 'allowed_server' => array(), 'allowed_pages' => array());
183:
184: if (isset($settings['modules'])) {
185: $mods = get_modules($settings);
186: foreach ($mods as $mod) {
187: printf("scanning module %s ...\n", $mod);
188: if ($mod === 'core') {
189: // We'll load the navigation modules last, after all other modules have been loaded, as they depend on the others.
190: $core = true;
191: }
192: $directoriesPattern = str_replace('/', DIRECTORY_SEPARATOR, "{*,*/*}");
193: foreach (glob('modules' . DIRECTORY_SEPARATOR . $mod . DIRECTORY_SEPARATOR . 'js_modules' . DIRECTORY_SEPARATOR . $directoriesPattern . '*.js', GLOB_BRACE) as $js_module) {
194: if (preg_match('/\[(.+)\]/', $js_module, $matches)) {
195: $dep = $matches[1];
196: if (in_array($dep, $js_exclude_dependencies)) {
197: continue;
198: }
199: }
200: $js .= file_get_contents($js_module);
201: }
202: if (is_readable(sprintf("modules/%s/site.js", $mod))) {
203: $js .= str_replace("'use strict';", '', file_get_contents(sprintf("modules/%s/site.js", $mod)));
204: }
205:
206: if (is_readable(sprintf("modules/%s/site.css", $mod))) {
207: $css .= file_get_contents(sprintf("modules/%s/site.css", $mod));
208: }
209: if (is_readable(sprintf("modules/%s/setup.php", $mod))) {
210: $filters = Hm_Module_Exec::merge_filters($filters, require sprintf("modules/%s/setup.php", $mod));
211: }
212: if (is_readable(sprintf("modules/%s/assets/", $mod))) {
213: $assets[] = sprintf("modules/%s/assets", $mod);
214: }
215: }
216:
217: if ($core) {
218: foreach (glob('modules/core/navigation/*.js') as $js_module) {
219: $js .= file_get_contents($js_module);
220: }
221: }
222:
223: // load pcss3t.cs only if one of: ['contacts','local_contacts','ldap_contacts','gmail_contacts'] is enabled
224: if(count(array_intersect(['contacts','local_contacts','ldap_contacts','gmail_contacts'], $mods)) > 0){
225: if (is_readable(sprintf("third_party/contact-group.css", 'third_party'))) {
226: $css .= file_get_contents(sprintf("third_party/contact-group.css", 'third_party'));
227: }
228: }
229: $css .= file_get_contents(sprintf("third_party/nprogress.css", 'third_party'));
230: }
231: return array($js, $css, $filters, $assets);
232: }
233:
234: /**
235: * get module list from settings
236: * @param array $settings site settings list
237: * @return array
238: */
239: function get_modules($settings) {
240: $mods = array();
241: if (isset($settings['modules'])) {
242: $mods = $settings['modules'];
243: if (is_string($mods)) {
244: $mods = explode(',', $mods);
245: }
246: }
247: return $mods;
248: }
249:
250: /**
251: * Write out combined javascript and css files
252: *
253: * @param $js string combined javascript from all modules
254: * @param $js_compress string command to compress the js
255: * @param $css string combined css from all modules
256: * @param $css_compress string command to compress the css
257: * @param $settings array site settings list
258: *
259: * @return void
260: */
261: function combine_includes($js, $js_compress, $css, $css_compress, $settings) {
262: $js_hash = '';
263: $css_hash = '';
264: if ($css) {
265: $css_out = file_get_contents(VENDOR_PATH . "twbs/bootstrap-icons/font/bootstrap-icons.css");
266: $css_out .= compress($css, $css_compress);
267: $css_hash = build_integrity_hash($css_out);
268: file_put_contents('site.css', $css_out);
269: printf("site.css file created\n");
270: }
271: if ($js) {
272: $mods = get_modules($settings);
273: $js_lib = get_js_libs_content(explode(',', $settings['js_exclude_deps']));
274: if (in_array('desktop_notifications', $mods, true)) {
275: $js_lib .= file_get_contents("third_party/push.min.js");
276: }
277: if ((array_key_exists('encrypt_ajax_requests', $settings) &&
278: $settings['encrypt_ajax_requests']) ||
279: (array_key_exists('encrypt_local_storage', $settings) &&
280: $settings['encrypt_local_storage'])) {
281: $js_lib .= file_get_contents("third_party/forge.min.js");
282: }
283: file_put_contents('tmp.js', $js);
284: $js_out = $js_lib.compress($js, $js_compress, 'tmp.js');
285: $js_hash = build_integrity_hash($js_out);
286: file_put_contents('site.js', $js_out);
287: unlink('./tmp.js');
288: printf("site.js file created\n");
289: }
290: return array('js' => $js_hash, 'css' => $css_hash);
291: }
292:
293: /**
294: * Write the hm3.rc file to disk
295: *
296: * @param $settings array site settings list (unsued with .env support)
297: * @param $filters array combined list of filters from all modules (unsued with .env support)
298: *
299: * @return void
300: */
301: function write_config_file($settings, $filters) {
302: Hm_Handler_Modules::try_queued_modules();
303: Hm_Handler_Modules::process_all_page_queue();
304: Hm_Handler_Modules::try_queued_modules();
305: Hm_Output_Modules::try_queued_modules();
306: Hm_Output_Modules::process_all_page_queue();
307: Hm_Output_Modules::try_queued_modules();
308:
309: $data = [
310: 'handler_modules' => Hm_Handler_Modules::dump(),
311: 'output_modules' => Hm_Output_Modules::dump(),
312: 'input_filters' => $filters,
313: ];
314: $dynamicConfigPath = APP_PATH.'config/dynamic.php';
315: // Create or overwrite the PHP file
316: file_put_contents($dynamicConfigPath, '<?php return ' . var_export($data, true) . ';');
317: printf("dynamic.php file written\n");
318: }
319:
320: /**
321: * Copies bootstrap icons fonts folder as it is
322: * referenced and needed by bootstrap icons css file
323: *
324: * @return void
325: */
326: function append_bootstrap_icons_files() {
327: if (!is_dir("site/fonts")) {
328: mkdir('site/fonts', 0755);
329: }
330: $source_folder = VENDOR_PATH.'twbs/bootstrap-icons/font/fonts/';
331: $files = glob("$source_folder*.*");
332: foreach($files as $file){
333: $dest_forlder = str_replace($source_folder, "site/fonts/", $file);
334: copy($file, $dest_forlder);
335: }
336: }
337:
338: function process_bootswatch_files() {
339: $src = 'site/modules/themes/assets';
340: if (! is_dir($src)) {
341: return;
342: }
343: $dir = opendir($src);
344: while(false !== ($folder = readdir($dir))) {
345: if (($folder != '.' ) && ($folder != '..' )) {
346: if (is_dir($src . '/' . $folder) && $folder != 'fonts') {
347: $target = $src . '/' . $folder . '/css/' . $folder . '.css';
348: if ($folder == 'default') {
349: $content = file_get_contents(VENDOR_PATH . 'twbs/bootstrap/dist/css/bootstrap.min.css');
350: } else {
351: $content = file_get_contents(VENDOR_PATH . 'thomaspark/bootswatch/dist/' . $folder . '/bootstrap.min.css');
352: }
353: // Append customization done to the default theme
354: $custom = file_get_contents($target);
355: $custom = preg_replace('/^@import.+/m', '', $custom);
356: $custom = preg_replace('/^@charset.+/m', '', $custom);
357: $content .= "\n" . $custom;
358:
359: file_put_contents($target, $content);
360: }
361: }
362: }
363: closedir($dir);
364: }
365:
366: /**
367: * Copies the site.js and site.css files to the site/ directory, and creates
368: * a production version of the index.php file.
369: *
370: * @return void
371: */
372: function create_production_site($assets, $settings, $hashes) {
373: if (!is_readable('site/')) {
374: mkdir('site', 0755);
375: }
376: printf("creating production site\n");
377: copy('site.css', 'site/site.css');
378: copy('site.js', 'site/site.js');
379: append_bootstrap_icons_files();
380:
381: $index_file = file_get_contents('index.php');
382: $index_file = preg_replace("/APP_PATH', ''/", "APP_PATH', '".APP_PATH."'", $index_file);
383: $index_file = preg_replace("/CACHE_ID', ''/", "CACHE_ID', '".urlencode(Hm_Crypt::unique_id(32))."'", $index_file);
384: $index_file = preg_replace("/SITE_ID', ''/", "SITE_ID', '".urlencode(Hm_Crypt::unique_id(64))."'", $index_file);
385: $index_file = preg_replace("/DEBUG_MODE', true/", "DEBUG_MODE', false", $index_file);
386: $index_file = preg_replace("/JS_HASH', ''/", "JS_HASH', '".$hashes['js']."'", $index_file);
387: $index_file = preg_replace("/CSS_HASH', ''/", "CSS_HASH', '".$hashes['css']."'", $index_file);
388: file_put_contents('site/index.php', $index_file);
389: foreach ($assets as $path) {
390: copy_recursive($path);
391: }
392: }
393:
394: /**
395: * Recursively copy files
396: * @param string $path file path with no trailing slash
397: * @return void
398: */
399: function copy_recursive($path) {
400: $path .= '/';
401: if (!is_readable('site/'.$path)) {
402: mkdir('site/'.$path, 0755, true);
403: }
404: foreach (scandir($path) as $file) {
405: if (in_array($file, array('.', '..'), true)) {
406: continue;
407: }
408: elseif (is_dir($path.$file)) {
409: copy_recursive($path.$file);
410: }
411: else {
412: copy($path.$file, 'site/'.$path.$file);
413: }
414: }
415: }
416: