1: | <?php |
2: | declare(strict_types=1); |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | |
16: | use Adoy\FastCGI\Client; |
17: | use Daphnie\Collector; |
18: | use Daphnie\Metrics\Php as PhpMetrics; |
19: | use Module\Support\Php; |
20: | use Opcenter\Http\Php\Fpm; |
21: | use Opcenter\Http\Php\Fpm\CacheInspector; |
22: | use Opcenter\Http\Php\Fpm\Configuration; |
23: | use Opcenter\Http\Php\Fpm\PoolPolicy; |
24: | use Opcenter\Http\Php\Fpm\PoolStatus; |
25: | use Opcenter\Http\Php\Fpm\SystemdSynthesizer; |
26: | use Opcenter\Map; |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | class Php_Module extends Php implements \Module\Skeleton\Contracts\Hookable, \Module\Skeleton\Contracts\Reactive |
34: | { |
35: | const COMPOSER_LOCATION = '/usr/share/pear/composer.phar'; |
36: | |
37: | public $exportedFunctions = array( |
38: | '*' => PRIVILEGE_SITE, |
39: | 'pool_get_version' => PRIVILEGE_SITE|PRIVILEGE_USER, |
40: | 'pool_name' => PRIVILEGE_SITE|PRIVILEGE_USER, |
41: | 'pool_set_policy' => PRIVILEGE_ADMIN, |
42: | 'pool_get_policy' => PRIVILEGE_SITE|PRIVILEGE_USER, |
43: | 'version' => PRIVILEGE_ALL |
44: | ); |
45: | |
46: | |
47: | |
48: | |
49: | |
50: | |
51: | |
52: | public function install_package($module) |
53: | { |
54: | if (!IS_CLI) { |
55: | return $this->query('php_install_package', $module); |
56: | } |
57: | |
58: | |
59: | if (!preg_match('!^[a-zA-Z0-9_-]+$!', $module)) { |
60: | return error($module . ': invalid package name'); |
61: | } |
62: | |
63: | $args = '-d display_errors=0 -d track_errors=1 -d include_path=/usr/local/share/pear:/usr/share/pear'; |
64: | if (version_compare(platform_version(), '4.5', '<')) { |
65: | $args .= ' -d disable_functions=ini_set'; |
66: | } |
67: | $pearcmd = '/usr/share/pear/pearcmd.php'; |
68: | $proc = Util_Process_Tee::watch(new Util_Process_Sudo); |
69: | $proc->log('Installing ' . $module); |
70: | if (file_exists($this->domain_fs_path() . '/usr/local/share/pear/pearcmd.php')) { |
71: | $this->_unsetPearIni(); |
72: | $pearcmd = '/usr/local/share/pear/pearcmd.php'; |
73: | } |
74: | |
75: | $status = $proc->exec('php %s %s install -f -o %s', |
76: | $args, |
77: | $pearcmd, |
78: | escapeshellarg($module) |
79: | ); |
80: | |
81: | return $status['success']; |
82: | } |
83: | |
84: | private function _unsetPearIni() |
85: | { |
86: | $pearfile = $this->domain_fs_path() . '/usr/local/share/pear/PEAR.php'; |
87: | if (!file_exists($pearfile)) { |
88: | return false; |
89: | } |
90: | $content = file_get_contents($pearfile, 0, null, 0, 1024); |
91: | $changed = false; |
92: | $pos = strpos($content, 'ini_set'); |
93: | if ($pos === false) { |
94: | return false; |
95: | } |
96: | $content = file_get_contents($pearfile); |
97: | file_put_contents($pearfile, str_replace('@ini_set', '// @ini_set', $content)); |
98: | |
99: | return true; |
100: | } |
101: | |
102: | |
103: | |
104: | |
105: | |
106: | |
107: | |
108: | |
109: | |
110: | |
111: | public function list_installed_packages() |
112: | { |
113: | if (!IS_CLI) { |
114: | return $this->query('php_list_installed_packages'); |
115: | } |
116: | $status = Util_Process::exec('pear list'); |
117: | if ($status instanceof Exception) { |
118: | return $status; |
119: | } |
120: | $packages = array(); |
121: | |
122: | $packageSizeSys = preg_match_all('!^(\S+)\s+([0-9,\. ]+)!m', $status['output'], $packageMatches); |
123: | |
124: | for ($i = 1; $i < $packageSizeSys; $i++) { |
125: | $packages[$packageMatches[1][$i]] = array('is_local' => false, 'version' => $packageMatches[2][$i]); |
126: | } |
127: | |
128: | |
129: | $status = Util_Process_Sudo::exec('pear list'); |
130: | if ($status instanceof Exception) { |
131: | return $status; |
132: | } |
133: | $packageSize = preg_match_all('!^(\S+)\s+([0-9,\. ]+)!m', $status['output'], $packageMatches); |
134: | |
135: | for ($i = 1; $i < $packageSize; $i++) { |
136: | |
137: | $packages[$packageMatches[1][$i]] = array( |
138: | 'is_local' => true, |
139: | 'version' => trim($packageMatches[2][$i]) |
140: | ); |
141: | } |
142: | ksort($packages); |
143: | |
144: | return $packages; |
145: | } |
146: | |
147: | |
148: | |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | public function package_description($mModule) |
156: | { |
157: | $packages = $this->list_remote_packages(); |
158: | if (!isset($packages[$mModule])) { |
159: | return false; |
160: | } |
161: | |
162: | return $packages[$mModule]['description']; |
163: | } |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | public function list_remote_packages() |
176: | { |
177: | if (file_exists(TEMP_DIR . '/pear-cache') && ((time() - filemtime(TEMP_DIR . '/pear-cache')) < 86400)) { |
178: | $data = \Util_PHP::unserialize(file_get_contents(TEMP_DIR . '/pear-cache')); |
179: | |
180: | return $data; |
181: | } |
182: | $status = Util_Process::exec('/usr/bin/pear list-all'); |
183: | if ($status instanceof Exception) { |
184: | return $status; |
185: | } |
186: | $pear = array(); |
187: | $pearCount = preg_match_all('!^pear/(\S+)\s+(\S+)\s+([0-9\.]*)\s+(.*)$!m', $status['output'], |
188: | $pearTmp); |
189: | |
190: | for ($i = 0; $i < $pearCount; $i++) { |
191: | $pear[$pearTmp[1][$i]] = array( |
192: | 'versions' => array(trim($pearTmp[2][$i])), |
193: | 'description' => $pearTmp[4][$i] |
194: | ); |
195: | } |
196: | file_put_contents(TEMP_DIR . '/pear-cache', serialize($pear)); |
197: | |
198: | return $pear; |
199: | } |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | |
206: | |
207: | public function add_pear_channel($xml) |
208: | { |
209: | if (substr($xml, -4) != '.xml') { |
210: | return error("channel `$xml' must refer to .xml"); |
211: | } |
212: | $status = Util_Process_Sudo::exec('pear add-channel %s', $xml); |
213: | |
214: | return $status['success']; |
215: | } |
216: | |
217: | |
218: | |
219: | |
220: | |
221: | |
222: | |
223: | |
224: | public function remove_channel($channel) |
225: | { |
226: | $status = Util_Process_Sudo::exec('pear remove-channel %s', $channel); |
227: | |
228: | return $status['success']; |
229: | } |
230: | |
231: | |
232: | |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | |
241: | |
242: | |
243: | |
244: | |
245: | |
246: | |
247: | |
248: | |
249: | |
250: | public function list_channels() |
251: | { |
252: | $channels = array(); |
253: | $status = Util_Process_Sudo::exec('pear list-channels'); |
254: | if (!$status['success']) { |
255: | return $channels; |
256: | } |
257: | $chmatches = null; |
258: | if (!preg_match_all(Regex::PEAR_CHANNELS_LONG, $status['output'], $chmatches, PREG_SET_ORDER)) { |
259: | return $channels; |
260: | } |
261: | foreach ($chmatches as $channel) { |
262: | $channels[] = array( |
263: | 'channel' => $channel['channel'], |
264: | 'summary' => $channel['summary'] |
265: | ); |
266: | } |
267: | |
268: | return $channels; |
269: | } |
270: | |
271: | |
272: | |
273: | |
274: | |
275: | |
276: | |
277: | |
278: | |
279: | |
280: | |
281: | |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | |
288: | |
289: | |
290: | public function get_channel_info($channel) |
291: | { |
292: | $info = array(); |
293: | $status = Util_Process_Sudo::exec('pear channel-info %s', $channel); |
294: | if (!$status['success']) { |
295: | return false; |
296: | } |
297: | $line = strtok($status['output'], '='); |
298: | $parse = false; |
299: | |
300: | for ($idx = null; $line !== false; $line = strtok("\n")) { |
301: | |
302: | if (!$parse) { |
303: | if ($line[0] != '=') { |
304: | continue; |
305: | } else { |
306: | $parse = true; |
307: | } |
308: | } |
309: | |
310: | if ($idx) { |
311: | $info[$idx] = trim($line); |
312: | } |
313: | $idx = null; |
314: | |
315: | $lookup = strtok(" \n"); |
316: | if ($lookup == 'Name') { |
317: | strtok(' '); |
318: | strtok(' '); |
319: | $idx = 'server'; |
320: | } else { |
321: | if ($lookup == 'Alias') { |
322: | $idx = 'alias'; |
323: | } else { |
324: | if ($lookup == 'Summary') { |
325: | $idx = 'summary'; |
326: | } else { |
327: | if ($lookup == 'Version') { |
328: | |
329: | $version = null; |
330: | $line = strtok("\n"); |
331: | if (false === strpos($line, 'SERVER CAPABILITIES')) { |
332: | $version = trim($line); |
333: | } |
334: | $info['version'] = $version; |
335: | } else { |
336: | if ($lookup[0] == '=') { |
337: | break; |
338: | } |
339: | } |
340: | } |
341: | } |
342: | } |
343: | } |
344: | |
345: | return $info; |
346: | } |
347: | |
348: | |
349: | |
350: | |
351: | |
352: | |
353: | |
354: | |
355: | |
356: | |
357: | public function version() |
358: | { |
359: | if ($this->jailed()) { |
360: | return $this->pool_get_version(); |
361: | } |
362: | |
363: | static $ver; |
364: | if (null === $ver) { |
365: | $key = 'php.version'; |
366: | $ver = apcu_fetch($key); |
367: | if ($ver) { |
368: | return $ver; |
369: | } |
370: | $ver = \Opcenter\Php::version(); |
371: | apcu_add($key, $ver, 86400); |
372: | } |
373: | |
374: | return $ver; |
375: | } |
376: | |
377: | public function _housekeeping() |
378: | { |
379: | |
380: | if ($this->composer_exists()) { |
381: | return true; |
382: | } |
383: | |
384: | $versions = file_get_contents('https://getcomposer.org/versions'); |
385: | if (!$versions) { |
386: | return false; |
387: | } |
388: | $versions = json_decode($versions, true); |
389: | $url = 'https://getcomposer.org/' . $versions['stable'][0]['path']; |
390: | $res = Util_HTTP::download($url, self::COMPOSER_LOCATION); |
391: | if (!$res) { |
392: | return error('failed to download composer'); |
393: | } |
394: | chmod(self::COMPOSER_LOCATION, 0755); |
395: | $fstPath = $this->service_template_path('siteinfo') . self::COMPOSER_LOCATION; |
396: | if (!file_exists($fstPath) || fileinode(self::COMPOSER_LOCATION) !== fileinode($fstPath)) { |
397: | copy(self::COMPOSER_LOCATION, $fstPath); |
398: | } |
399: | |
400: | info('installed %s!', basename(self::COMPOSER_LOCATION)); |
401: | |
402: | return true; |
403: | } |
404: | |
405: | public function composer_exists() |
406: | { |
407: | return file_exists(self::COMPOSER_LOCATION); |
408: | } |
409: | |
410: | public function _delete() |
411: | { |
412: | foreach ($this->get_fallbacks() as $fallback) { |
413: | if ($this->fallback_enabled($fallback)) { |
414: | $this->disable_fallback($fallback); |
415: | } |
416: | } |
417: | } |
418: | |
419: | public function get_fallbacks() |
420: | { |
421: | return $this->getPersonalities(); |
422: | } |
423: | |
424: | |
425: | |
426: | |
427: | |
428: | |
429: | |
430: | public function fallback_enabled($mode = null) |
431: | { |
432: | if ($this->jailed()) { |
433: | return false; |
434: | } |
435: | if (is_null($mode)) { |
436: | $mode = $this->getPersonalities(); |
437: | $mode = array_pop($mode); |
438: | } |
439: | |
440: | return file_exists($this->getPersonalityPathFromPersonality($mode, $this->site)); |
441: | } |
442: | |
443: | |
444: | |
445: | |
446: | |
447: | |
448: | public function disable_fallback($mode = '') |
449: | { |
450: | if (!IS_CLI) { |
451: | return $this->query('php_disable_fallback'); |
452: | } |
453: | if ($this->jailed()) { |
454: | return false; |
455: | } |
456: | |
457: | if ($mode) { |
458: | $personalities = [$mode]; |
459: | } else { |
460: | $personalities = $this->getPersonalities(); |
461: | } |
462: | foreach ($personalities as $personality) { |
463: | if (!$this->personalityExists($personality)) { |
464: | error("unknown personality `%s', skipping", $personality); |
465: | continue; |
466: | } |
467: | $path = $this->getPersonalityPathFromPersonality($personality, $this->site); |
468: | if (file_exists($path)) { |
469: | unlink($path); |
470: | } else { |
471: | warn("fallback `%s' not enabled", $personality); |
472: | } |
473: | } |
474: | |
475: | |
476: | return true; |
477: | } |
478: | |
479: | public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool |
480: | { |
481: | |
482: | } |
483: | |
484: | public function _create() |
485: | { |
486: | |
487: | } |
488: | |
489: | public function _edit() |
490: | { |
491: | foreach ($this->get_fallbacks() as $fallback) { |
492: | if ($this->fallback_enabled($fallback)) { |
493: | $this->disable_fallback($fallback) && $this->enable_fallback($fallback); |
494: | } |
495: | } |
496: | } |
497: | |
498: | |
499: | |
500: | |
501: | |
502: | |
503: | |
504: | public function enable_fallback($mode = null) |
505: | { |
506: | if (!IS_CLI) { |
507: | return $this->query('php_enable_fallback', $mode); |
508: | } |
509: | if ($this->jailed()) { |
510: | return error('Fallbacks may be used when PHP jails are disabled'); |
511: | } |
512: | if (!$mode) { |
513: | $mode = $this->getPersonalities(); |
514: | } |
515: | |
516: | $file = file_get_contents($this->web_config_dir() . '/virtual/' . |
517: | $this->site); |
518: | |
519: | $config = preg_replace(Regex::compile( |
520: | Regex::PHP_COMPILABLE_STRIP_NONHTTP_APACHE_CONTAINER, |
521: | ['port' => 80] |
522: | ), '', $file); |
523: | $serverip = (array)$this->common_get_ip_address(); |
524: | $in = $serverip[0] . ':80'; |
525: | foreach ((array)$mode as $m) { |
526: | if (!$this->personalityExists($m)) { |
527: | error("unknown personality `%s' - not enabling", $m); |
528: | continue; |
529: | } |
530: | $port = $this->getPersonalityPort($m); |
531: | $out = $serverip[0] . ':' . $port; |
532: | $newconfig = str_replace($in, $out, $config); |
533: | $confpath = $this->getPersonalityPathFromPersonality($m, $this->site); |
534: | file_put_contents($confpath, $newconfig) && info("enabled fallback for `%s'", $m); |
535: | } |
536: | Util_Account_Hooks::instantiateContexted($this->getAuthContext())->run('reload', ['php']); |
537: | |
538: | return true; |
539: | } |
540: | |
541: | |
542: | |
543: | |
544: | |
545: | |
546: | public function jailed(): bool |
547: | { |
548: | return (bool)$this->getConfig('apache', 'jail'); |
549: | } |
550: | |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | |
557: | public function pool_name(string $docroot = null): ?string |
558: | { |
559: | if (!$this->jailed()) { |
560: | return null; |
561: | } |
562: | |
563: | return $this->pools()[0] ?? null; |
564: | } |
565: | |
566: | |
567: | |
568: | |
569: | |
570: | |
571: | |
572: | public function pool_change_owner(string $owner, string $pool = null): bool |
573: | { |
574: | if (!IS_CLI) { |
575: | return $this->query('php_pool_change_owner', $owner, $pool); |
576: | } |
577: | |
578: | if (!HTTPD_FPM_USER_CHANGE) { |
579: | return error('Pool ownership disallowed'); |
580: | } |
581: | |
582: | if (!$this->jailed()) { |
583: | return error('Pools not utilized for site'); |
584: | } |
585: | |
586: | $pools = $this->pools(); |
587: | if ($pool && !\in_array($pool, $pools, true)) { |
588: | return error("Unknown pool `%s'", $pool); |
589: | } else if ($pool) { |
590: | warn('Per-pool ownership not supported yet - applying bulk change'); |
591: | } |
592: | |
593: | if (!$this->user_exists($owner)) { |
594: | return error("User `%s' does not exist", $owner); |
595: | } |
596: | if ($owner !== $this->web_get_sys_user() && $owner !== $this->username) { |
597: | return error("Unknown or unsupported pool owner `%s'", $owner); |
598: | } |
599: | |
600: | $editor = new \Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext()); |
601: | $editor->setConfig('apache', 'webuser', $owner); |
602: | if (!$editor->edit()) { |
603: | return false; |
604: | } |
605: | |
606: | $systemd = Fpm\SystemdSynthesizer::mock($this->site . '-' . ($pool ?? array_shift($pools))); |
607: | return $systemd::waitFor(10); |
608: | } |
609: | |
610: | |
611: | |
612: | |
613: | |
614: | |
615: | |
616: | public function pool_cache_status(string $pool = null): ?array |
617: | { |
618: | if (!IS_CLI) { |
619: | return $this->query('php_pool_cache_status', $pool); |
620: | } |
621: | if (!$this->jailed()) { |
622: | error('Jails disabled for site'); |
623: | return null; |
624: | } |
625: | |
626: | $ip = $this->site_ip_address(); |
627: | |
628: | $hostname = $this->domain ?? SERVER_NAME; |
629: | $docroot = $this->web_get_docroot($hostname); |
630: | return CacheInspector::instantiateContexted($this->getAuthContext(), [$ip, $hostname])->readFromPath($docroot); |
631: | } |
632: | |
633: | |
634: | |
635: | |
636: | |
637: | |
638: | |
639: | public function pool_owner(string $pool = null) { |
640: | if (!$this->jailed()) { |
641: | return error('Pools not utilized for site'); |
642: | } |
643: | |
644: | $pools = $this->pools(); |
645: | if ($pool && !\in_array($pool, $pools, true)) { |
646: | return error("Unknown pool `%s'", $pool); |
647: | } |
648: | $pool = $pool ?? array_shift($pools); |
649: | return $this->user_get_username_from_uid((new PoolStatus($this->php_pool_info($pool)))->getUser()); |
650: | } |
651: | |
652: | |
653: | |
654: | |
655: | |
656: | |
657: | |
658: | public function pool_restart(string $pool = null): bool |
659: | { |
660: | return $this->pool_set_state($pool, 'restart'); |
661: | } |
662: | |
663: | |
664: | |
665: | |
666: | |
667: | |
668: | |
669: | |
670: | public function pool_set_state(?string $pool = null, string $state = 'stop'): bool |
671: | { |
672: | if (!IS_CLI) { |
673: | return $this->query('php_pool_set_state', $pool, $state); |
674: | } |
675: | if (!\in_array($state, ['stop', 'start', 'restart', 'reload'], true)) { |
676: | return error("Unknown pool state `%s'", $state); |
677: | } |
678: | |
679: | if ($pool && !\in_array($pool, $this->pools(), true)) { |
680: | return error("Invalid pool specified `%s'", $pool); |
681: | } |
682: | if (!$pool) { |
683: | $pool = $this->pools(); |
684: | } |
685: | foreach ((array)$pool as $p) |
686: | { |
687: | $svc = Fpm\SystemdSynthesizer::mock($this->site . '-' . $p); |
688: | if ($state !== 'stop' && $svc::failed()) { |
689: | $svc::reset(); |
690: | } |
691: | if ($svc::run($state)) { |
692: | continue; |
693: | } |
694: | |
695: | if ($state === 'start' || $state === 'restart') { |
696: | |
697: | $log = Configuration::bindTo($this->domain_fs_path())-> |
698: | setServiceContainer(\Opcenter\SiteConfiguration::shallow($this->getAuthContext()))-> |
699: | setName($p)->getLog(); |
700: | if (!file_exists($this->domain_fs_path() . $log) && $this->file_touch($log) && |
701: | $this->file_chown($log, $this->pool_owner($p))) |
702: | { |
703: | warn("Log `%s' missing, possibly inhibiting startup. Created", $log); |
704: | $this->pool_set_state($p, $state); |
705: | continue; |
706: | } |
707: | } |
708: | warn('Failed to %s %s-%s', $state, $this->site, $p); |
709: | |
710: | } |
711: | |
712: | return true; |
713: | } |
714: | |
715: | |
716: | |
717: | |
718: | |
719: | |
720: | public function pools(): array |
721: | { |
722: | if (!$this->jailed()) { |
723: | return []; |
724: | } |
725: | $key = 'php.pools'; |
726: | $cache = \Cache_Account::spawn($this->getAuthContext()); |
727: | if (false !== ($pools = $cache->get($key))) { |
728: | return $pools; |
729: | } |
730: | $pools = array_map(function (string $pool) { |
731: | return substr($pool, strlen($this->site) + 1); |
732: | }, Fpm::getPoolsFromGroup($this->site)); |
733: | |
734: | $cache->set($key, $pools, 30); |
735: | return $pools; |
736: | } |
737: | |
738: | |
739: | |
740: | |
741: | |
742: | |
743: | |
744: | public function pool_status(string $pool = ''): array |
745: | { |
746: | if (!($status = $this->pool_info($pool))) { |
747: | return []; |
748: | } |
749: | return (new Fpm\PoolStatus($status))->getMetrics(); |
750: | } |
751: | |
752: | public function _cron(Cronus $c) { |
753: | static $ctr = 0; |
754: | |
755: | if (!TELEMETRY_ENABLED || ++$ctr < 600 / CRON_RESOLUTION) { |
756: | return; |
757: | } |
758: | |
759: | $ctr = 0; |
760: | $oldex = \Error_Reporter::exception_upgrade(Error_Reporter::E_FATAL, true); |
761: | $collector = new Collector(PostgreSQL::pdo()); |
762: | foreach (\Opcenter\Account\Enumerate::active() as $site) { |
763: | try { |
764: | $ctx = \Auth::context(null, $site); |
765: | $afi = \apnscpFunctionInterceptor::factory($ctx); |
766: | if (!$afi->php_jailed() || !($service = $afi->php_pool_info())) { |
767: | continue; |
768: | } |
769: | |
770: | $inspector = new Fpm\PoolStatus($service); |
771: | if (!$inspector->running()) { |
772: | continue; |
773: | } |
774: | $cacheMetrics = $inspector->getCacheMetrics($ctx); |
775: | $stats = $afi->php_pool_status(); |
776: | if (null === array_get($stats, 'traffic')) { |
777: | |
778: | $stats = PhpMetrics::fill(0); |
779: | } else { |
780: | $stats['uptime'] = time() - $inspector->getStart(); |
781: | $stats['cache-used'] = array_get($cacheMetrics, 'memory_usage.used_memory', 0)/1024; |
782: | $stats['cache-total'] = (array_get($cacheMetrics, 'memory_usage.free_memory') + $stats['cache-used'])/1024; |
783: | $stats['cache-hits'] = array_get($cacheMetrics, 'opcache_statistics.hits', 0); |
784: | $stats['cache-misses'] = array_get($cacheMetrics, 'opcache_statistics.misses', 0); |
785: | $stats['traffic'] *= 100; |
786: | } |
787: | |
788: | |
789: | foreach (PhpMetrics::getAttributeMap() as $attr => $metric) { |
790: | $val = $stats[$metric]; |
791: | |
792: | if ($val instanceof Closure) { |
793: | $val = $val($stats); |
794: | } |
795: | |
796: | $collector->add("php-${attr}", $ctx->site_id, (int)$val); |
797: | } |
798: | } catch (\apnscpException $e) { |
799: | |
800: | continue; |
801: | } |
802: | } |
803: | \Error_Reporter::exception_upgrade($oldex); |
804: | |
805: | } |
806: | |
807: | |
808: | |
809: | |
810: | |
811: | |
812: | public function pool_info(string $pool = ''): array |
813: | { |
814: | $pools = $this->pools(); |
815: | if ($pool && !in_array($pool, $pools, true)) { |
816: | error("Invalid pool specified `%s'", $pool); |
817: | |
818: | return []; |
819: | } |
820: | |
821: | if (!$pool) { |
822: | if (\in_array($this->domain, $pools, true)) { |
823: | $pool = $this->domain; |
824: | } else { |
825: | $pool = array_shift($pools); |
826: | } |
827: | } |
828: | |
829: | |
830: | if (class_exists(\Opcenter\Dbus\Systemd::class)) { |
831: | return (array)Fpm\SystemdSynthesizer::mock($this->site . '-' . $pool)->status() + Fpm\SystemdSynthesizer::mock($this->site . '-' . $pool)->unitInfo(['Id', 'Description', 'ActiveState']); |
832: | } |
833: | |
834: | return Fpm\SystemdSynthesizer::mock($this->site . '-' . $pool)->status() ?: []; |
835: | } |
836: | |
837: | |
838: | |
839: | |
840: | |
841: | |
842: | |
843: | public function pool_get_version(string $pool = ''): string |
844: | { |
845: | if (!IS_CLI) { |
846: | return $this->query('php_pool_get_version', $pool); |
847: | } |
848: | if (!$this->jailed()) { |
849: | return $this->version(); |
850: | } |
851: | |
852: | return PoolPolicy::instantiateContexted($this->getAuthContext())->getPoolVersion($pool); |
853: | } |
854: | |
855: | |
856: | |
857: | |
858: | |
859: | |
860: | |
861: | |
862: | |
863: | |
864: | public function pool_direct_read(string $hostname, string $file): ?string |
865: | { |
866: | if (!IS_CLI) { |
867: | return $this->query('php_pool_direct_read', $hostname, $file); |
868: | } |
869: | |
870: | if (!$this->jailed()) { |
871: | warn("PHP-FPM disabled for site - ignoring PHP-FPM pool read"); |
872: | return ''; |
873: | } |
874: | |
875: | $path = $this->web_normalize_path($hostname, $file) ?? (\Web_Module::MAIN_DOC_ROOT . "/$file"); |
876: | $pool = $this->php_pool_name($path); |
877: | $socket = \Opcenter\Http\Php\Fpm\Configuration::bindTo($this->domain_fs_path())-> |
878: | setGroup($this->site)->setName($pool)->getSocketPath(); |
879: | |
880: | $client = new Client('unix://' . $socket, -1); |
881: | try { |
882: | $contents = $client->request([ |
883: | 'REQUEST_METHOD' => 'GET', |
884: | 'SCRIPT_NAME' => basename($file), |
885: | 'SCRIPT_FILENAME' => $path, |
886: | 'DOCUMENT_ROOT' => $this->web_get_docroot($hostname), |
887: | 'SERVER_SOFTWARE' => PANEL_BRAND . ' ' . APNSCP_VERSION . ' Self-Referential Check', |
888: | 'REMOTE_ADDR' => '127.0.0.1', |
889: | 'SERVER_NAME' => $hostname |
890: | ], ''); |
891: | |
892: | return ltrim((string)strstr($contents, "\r\n\r\n")); |
893: | } catch (\Adoy\FastCGI\ForbiddenException $e) { |
894: | return nerror("Unable to access resource - are permissions correct?"); |
895: | } catch (\Adoy\FastCGI\TimedOutException $e) { |
896: | return nerror("Request timed out"); |
897: | } catch (\Exception $e) { |
898: | return nerror("Request failed: %s", $e->getMessage()); |
899: | } |
900: | |
901: | } |
902: | |
903: | |
904: | |
905: | |
906: | |
907: | |
908: | |
909: | public function pool_version_from_path(string $path): string |
910: | { |
911: | if (!IS_CLI) { |
912: | return $this->query('php_pool_version_from_path', $path); |
913: | } |
914: | |
915: | if (!$this->jailed()) { |
916: | return $this->version(); |
917: | } |
918: | |
919: | $pool = $this->pool_name($path); |
920: | return PoolPolicy::instantiateContexted($this->getAuthContext())->getPoolVersion($pool); |
921: | } |
922: | |
923: | |
924: | |
925: | |
926: | |
927: | |
928: | |
929: | |
930: | public function pool_set_version(string $version, string $pool = ''): bool |
931: | { |
932: | if (!IS_CLI) { |
933: | return $this->query('php_pool_set_version', $version, $pool); |
934: | } |
935: | |
936: | if (!$this->jailed()) { |
937: | return error("Cannot change version for non-jailed account"); |
938: | } |
939: | |
940: | $origPool = $pool; |
941: | if (!$pool) { |
942: | $pool = $this->pools(); |
943: | } else if (!$this->pool_exists($pool)) { |
944: | return error("Invalid pool specified `%s'", $pool); |
945: | } |
946: | |
947: | if (!PoolPolicy::instantiateContexted($this->getAuthContext())->allowed($version)) { |
948: | return error("PHP version `%s' not installed or permitted on account", $version); |
949: | } |
950: | |
951: | $version = \Opcenter\Versioning::asMinor($version); |
952: | |
953: | if (!array_key_exists((string)$version, Fpm\MultiPhp::list())) { |
954: | if ($version !== ($tmp = \Opcenter\Versioning::asMinor($version))) { |
955: | warn("Truncating version to `%s'", $tmp); |
956: | return $this->switch_version($version, $pool); |
957: | } |
958: | |
959: | return error("PHP interpreter %s does not exist", $version); |
960: | } |
961: | |
962: | $svc = \Opcenter\SiteConfiguration::shallow($this->getAuthContext()); |
963: | |
964: | foreach ((array)$pool as $p) { |
965: | |
966: | $config = Fpm\Configuration::bindTo($this->domain_fs_path()); |
967: | $config->setServiceContainer($svc); |
968: | $config->setVersion($version); |
969: | if ($config->isDefault()) { |
970: | $config->shimBinaries(); |
971: | } |
972: | } |
973: | |
974: | unset($config); |
975: | |
976: | return \Util_Account_Editor::instantiateContexted($this->getAuthContext())->setConfig(['apache.jail' => 1])->edit(); |
977: | } |
978: | |
979: | |
980: | |
981: | |
982: | |
983: | |
984: | |
985: | public function pool_get_policy(string $var, string $pool = null, string $default = null) |
986: | { |
987: | if (!IS_CLI) { |
988: | return $this->query('php_pool_get_policy', $var, $pool, $default); |
989: | } |
990: | return PoolPolicy::instantiateContexted($this->getAuthContext())->get($pool, $var, $default); |
991: | } |
992: | |
993: | |
994: | |
995: | |
996: | |
997: | |
998: | |
999: | |
1000: | |
1001: | |
1002: | |
1003: | public function pool_set_policy(string $anything, string $var, $val, string $pool = null): bool |
1004: | { |
1005: | if (!IS_CLI) { |
1006: | return $this->query('php_pool_set_policy', $anything, $var, $val, $pool); |
1007: | } |
1008: | |
1009: | $sites = \Opcenter\Account\Enumerate::matches($anything); |
1010: | if (!$sites) { |
1011: | return error("Unknown qualifier `%s'", $anything); |
1012: | } |
1013: | |
1014: | foreach ($sites as $site) { |
1015: | $ctx = \Auth::context(null, $site); |
1016: | $afi = \apnscpFunctionInterceptor::factory($ctx); |
1017: | if ($pool && !$afi->php_pool_exists($pool)) { |
1018: | return error("Unknown PHP pool `%s'", $pool); |
1019: | } |
1020: | PoolPolicy::instantiateContexted($ctx)->set($pool, $var, $val)->sync(); |
1021: | $cmd = (new \Util_Account_Editor(null, |
1022: | $ctx))->setMode('edit')->setFlags(['reconfig' => true])->getCommand(); |
1023: | $proc = (new Util_Process_Schedule)->setID("php." . $ctx->site); |
1024: | if (!$proc->preempted("php." . $ctx->site)) { |
1025: | $proc->run($cmd); |
1026: | } |
1027: | } |
1028: | info("PHP pool settings batched to background"); |
1029: | |
1030: | return true; |
1031: | } |
1032: | |
1033: | |
1034: | |
1035: | |
1036: | |
1037: | |
1038: | public function pool_versions(): array |
1039: | { |
1040: | if (!IS_CLI) { |
1041: | return $this->query('php_pool_versions'); |
1042: | } |
1043: | return array_keys(Fpm\MultiPhp::list()); |
1044: | } |
1045: | |
1046: | |
1047: | |
1048: | |
1049: | |
1050: | |
1051: | |
1052: | public function pool_exists(string $pool): bool |
1053: | { |
1054: | return in_array($pool, $this->pools(), true); |
1055: | } |
1056: | |
1057: | |
1058: | |
1059: | |
1060: | |
1061: | |
1062: | |
1063: | |
1064: | |
1065: | public function migrate_directives(string $hostname, string $path = '', string $from = null): bool |
1066: | { |
1067: | if (!IS_CLI) { |
1068: | return $this->query('php_migrate_directives', $hostname, $path, $from); |
1069: | } |
1070: | if (null === $from) { |
1071: | $from = $this->jailed() ? 'isapi' : 'fpm'; |
1072: | } |
1073: | |
1074: | if ($from !== 'isapi' && $from !== 'fpm') { |
1075: | return error("Unknown underlying PHP engine `%s'", $from); |
1076: | } |
1077: | |
1078: | if (! ($docroot = $this->web_get_docroot($hostname, $path)) ) { |
1079: | return false; |
1080: | } |
1081: | |
1082: | $srcFile = $docroot . DIRECTORY_SEPARATOR . ($from === 'isapi' ? '.htaccess' : '.user.ini'); |
1083: | $destFile = $docroot . DIRECTORY_SEPARATOR . ($from === 'isapi' ? '.user.ini' : '.htaccess'); |
1084: | if (!$this->file_exists($srcFile)) { |
1085: | return true; |
1086: | } |
1087: | $srcContents = $destContents = ''; |
1088: | if ($this->file_exists($srcFile)) { |
1089: | $srcContents = $this->file_get_file_contents($srcFile); |
1090: | } |
1091: | if ($this->file_exists($destFile)) { |
1092: | $destContents = $this->file_get_file_contents($destFile); |
1093: | } |
1094: | $directives = []; |
1095: | $lines = explode("\n", $srcContents); |
1096: | if ($from === 'fpm') { |
1097: | foreach ($lines as $line) { |
1098: | if (false === strpos($line, '=')) { |
1099: | continue; |
1100: | } else if (strpos($line, ';') === strcspn(';', $line)) { |
1101: | |
1102: | continue; |
1103: | } |
1104: | [$ini, $val] = explode('=', trim($line), 2); |
1105: | $directive = 'php_value'; |
1106: | if (strtolower($val) === 'on' || strtolower($val) === 'off') { |
1107: | $directive = 'php_flag'; |
1108: | } |
1109: | $directives[] = "${directive} ${ini} ${val}"; |
1110: | } |
1111: | } else { |
1112: | foreach ($lines as &$line) { |
1113: | if (false === strpos($line, 'php_flag') && false === strpos($line, 'php_value')) { |
1114: | continue; |
1115: | } |
1116: | if (!preg_match('/^\s*(?<directive>php_(?:flag|value))\s+(?<ini>[^ ]+)\s+(?<value>.+)$/', $line, $match)) { |
1117: | debug("Unknown line `%s' encountered - ignoring", $line); |
1118: | continue; |
1119: | } |
1120: | |
1121: | $directives[] = $match['ini'] . '=' . $match['value']; |
1122: | $line = null; |
1123: | } |
1124: | } |
1125: | |
1126: | |
1127: | return $this->file_put_file_contents($destFile, rtrim(rtrim($destContents) . "\n" . implode("\n", $directives)) . "\n") && |
1128: | $this->file_put_file_contents($srcFile, implode("\n", array_filter($lines)) . "\n"); |
1129: | } |
1130: | |
1131: | public function _create_user(string $user) |
1132: | { } |
1133: | |
1134: | public function _delete_user(string $user) |
1135: | { } |
1136: | |
1137: | public function _edit_user(string $userold, string $usernew, array $oldpwd) |
1138: | { } |
1139: | |
1140: | public function _reload(string $why = '', array $args = []) |
1141: | { |
1142: | if (!$this->permission_level & PRIVILEGE_SITE || $why !== \Opcenter\Timezone::RELOAD_HOOK) { |
1143: | return; |
1144: | } |
1145: | if (!$this->jailed()) { |
1146: | return false; |
1147: | } |
1148: | $ini = Map::load($this->domain_fs_path('/etc/php.ini'), 'cd', 'inifile')->section('Custom'); |
1149: | $ini['date.timezone'] = $args['timezone']; |
1150: | $ini->save(); |
1151: | |
1152: | return SystemdSynthesizer::mock(Fpm::getGroupActivator($this->site))::restart(); |
1153: | } |
1154: | } |