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