1: | <?php |
2: | declare(strict_types=1); |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | use Module\Support\Webapps\PhpWrapper; |
16: | use Opcenter\Versioning; |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | |
23: | class Joomla_Module extends \Module\Support\Webapps |
24: | { |
25: | |
26: | const APP_NAME = 'Joomla!'; |
27: | |
28: | |
29: | |
30: | const JOOMLA_CLI = '/usr/share/pear/joomlatools.phar'; |
31: | |
32: | const UPDATE_URI = 'https://github.com/joomla/joomla-cms/releases/download/%version%/Joomla_%version%-Stable-Update_Package.zip'; |
33: | |
34: | |
35: | const DEFAULT_FORTIFY_MODE = 'max'; |
36: | |
37: | const JOOMLA_CLI_VERSION = '2.0.3'; |
38: | const JOOMLA_MODULE_XML = 'http://update.joomla.org/core/extensions/%extension%.xml'; |
39: | |
40: | const DEFAULT_VERSION_LOCK = 'major'; |
41: | |
42: | const VERSION_MINIMUMS = [ |
43: | '0' => '5.6', |
44: | '4.0.0' => '7.2.4', |
45: | '5.0.0' => '8.1.0' |
46: | ]; |
47: | const JOOMLA_MIRROR = 'https://github.com/joomla/joomla-cms'; |
48: | protected $aclList = array( |
49: | 'min' => array( |
50: | 'cache', |
51: | 'tmp', |
52: | 'administrator', |
53: | 'logs', |
54: | 'media', |
55: | 'images', |
56: | 'plugins', |
57: | 'log' |
58: | ), |
59: | 'max' => array( |
60: | 'cache', |
61: | 'tmp', |
62: | 'administrator/cache', |
63: | 'logs', |
64: | 'log' |
65: | ) |
66: | ); |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | public function __construct() |
74: | { |
75: | |
76: | parent::__construct(); |
77: | |
78: | } |
79: | |
80: | |
81: | |
82: | |
83: | |
84: | |
85: | |
86: | |
87: | |
88: | public function install(string $hostname, string $path = '', array $opts = array()): bool |
89: | { |
90: | if (!$this->mysql_enabled()) { |
91: | return error('%(what)s must be enabled to install %(app)s', |
92: | ['what' => 'MySQL', 'app' => static::APP_NAME]); |
93: | } |
94: | |
95: | if (!$this->parseInstallOptions($opts, $hostname, $path)) { |
96: | return false; |
97: | } |
98: | |
99: | $docroot = $this->getAppRoot($hostname, $path); |
100: | $version = $opts['version']; |
101: | |
102: | if (!isset($opts['title'])) { |
103: | $opts['title'] = ''; |
104: | } |
105: | |
106: | $this->_exec(null, 'versions --refresh', ['version' => $version]); |
107: | $fqdn = $this->web_normalize_hostname($hostname); |
108: | |
109: | if (count(explode('.', $version)) < 3) { |
110: | $version = Versioning::asPatch($version); |
111: | } |
112: | |
113: | $required = Versioning::satisfy($opts['version'], static::VERSION_MINIMUMS); |
114: | if (Opcenter\Versioning::compare($local = $this->php_pool_version_from_path($docroot), $required, '<')) { |
115: | return error("%(app)s requires %(what)s %(version)s. Version %(detected)s detected", [ |
116: | 'app' => static::APP_NAME, 'what' => 'PHP', 'version' => $required, 'detected' => $local]); |
117: | } |
118: | |
119: | $args = array( |
120: | 'mode' => 'site:install', |
121: | 'docroot' => $docroot, |
122: | 'site' => $fqdn, |
123: | 'repo' => self::JOOMLA_MIRROR |
124: | ); |
125: | |
126: | if (!$this->file_exists($docroot)) { |
127: | $this->file_create_directory($docroot, 0755, true); |
128: | } |
129: | |
130: | $args['version'] = $version; |
131: | if (isset($opts['sampledata'])) { |
132: | $data = $opts['sampledata']; |
133: | if ($data !== 'blog' && $data !== 'default' && $data !== 'brochure' && |
134: | $data !== 'learn' && $data !== 'testing' |
135: | ) { |
136: | return error("unknown sample data `%s'", $data); |
137: | } |
138: | $args['sampledata'] = '--sampledata=' . $data; |
139: | } else { |
140: | $args['sampledata'] = null; |
141: | } |
142: | |
143: | $db = \Module\Support\Webapps\DatabaseGenerator::mysql($this->getAuthContext(), $hostname); |
144: | if (!$db->create()) { |
145: | return false; |
146: | } |
147: | |
148: | $args['dbhost'] = $db->hostname; |
149: | $args['dbuser'] = $db->username; |
150: | $args['dbpass'] = $db->password; |
151: | $args['dbname'] = $db->database; |
152: | $args['dbdriver'] = 'mysqli'; |
153: | |
154: | $ret = $this->_exec($docroot, |
155: | 'site:download --www=%(docroot)s --use-webroot-dir --release %(version)s -- ""', $args); |
156: | |
157: | $this->_fixMySQLSchema($docroot); |
158: | if ($ret['success']) { |
159: | $ret = $this->_exec($docroot, |
160: | 'database:install --skip-exists-check --mysql-host=%(dbhost)s --mysql-login=%(dbuser)s:%(dbpass)s ' . |
161: | '--mysql-database=%(dbname)s --mysql-driver=%(dbdriver)s %(sampledata)s ' . |
162: | '--www=%(docroot)s -- ""', $args); |
163: | if ($ret['success']) { |
164: | $file = $this->_generateConfig($docroot, $opts); |
165: | $args['tmpfile'] = $file; |
166: | |
167: | $ret = $this->_exec($docroot, |
168: | 'site:configure --options=%(tmpfile)s --mysql-host=%(dbhost)s --mysql-login=%(dbuser)s:%(dbpass)s ' . |
169: | '--mysql-database=%(dbname)s --mysql-driver=%(dbdriver)s ' . |
170: | '--www=%(docroot)s -- ""', $args); |
171: | unlink($this->domain_fs_path() . $file); |
172: | } |
173: | } |
174: | |
175: | if (!$ret['success']) { |
176: | if (!empty($opts['keep'])) { |
177: | return error('failed to install Joomla'); |
178: | } |
179: | error('failed to install Joomla - removing temporary files: %s', $ret['stderr']); |
180: | $this->file_delete($docroot, true); |
181: | $db->rollback(); |
182: | |
183: | return false; |
184: | } |
185: | |
186: | $this->fixRewriteBase($docroot); |
187: | $this->removeInvalidDirectives($docroot); |
188: | $autogenpw = false; |
189: | if (!isset($opts['password'])) { |
190: | $autogenpw = true; |
191: | $opts['password'] = \Opcenter\Auth\Password::generate(10); |
192: | info("autogenerated password `%s'", $opts['password']); |
193: | } |
194: | |
195: | info("setting admin user to `%s'", $opts['user']); |
196: | |
197: | $adminargs = array( |
198: | 'username' => $opts['user'], |
199: | 'password' => $opts['password'], |
200: | 'email' => $opts['email'] |
201: | ); |
202: | |
203: | if (!$this->change_admin($hostname, $path, $adminargs)) { |
204: | warn('failed to set admin user, using default admin/admin combination'); |
205: | } |
206: | |
207: | $opts['url'] = rtrim($fqdn . '/' . $path, '/'); |
208: | |
209: | |
210: | $this->file_touch($docroot . '/.htaccess'); |
211: | if (!$this->file_exists($docroot . '/logs')) { |
212: | $this->file_create_directory($docroot . '/logs'); |
213: | } |
214: | info('fortification mode set to %s', strtoupper($fortifymode)); |
215: | |
216: | $this->notifyInstalled($hostname, $path, $opts); |
217: | |
218: | return info('%(app)s installed - confirmation email with login info sent to %(email)s', |
219: | ['app' => static::APP_NAME, 'email' => $opts['email']]); |
220: | } |
221: | |
222: | |
223: | |
224: | |
225: | |
226: | |
227: | |
228: | |
229: | protected function getInternalName(): string |
230: | { |
231: | return 'joomla'; |
232: | } |
233: | |
234: | private function cliFromVersion(string $version, string $poolVersion = null): string |
235: | { |
236: | $selections = [ |
237: | dirname(self::JOOMLA_CLI) . '/joomlatools-1.6.0-1.phar', |
238: | self::JOOMLA_CLI |
239: | ]; |
240: | $choice = version_compare($version, '3.5.0', '<') ? 0 : 1; |
241: | if ($poolVersion && Versioning::compare($poolVersion, '7.3.0', '<')) { |
242: | return $selections[0]; |
243: | } |
244: | |
245: | return $selections[$choice]; |
246: | } |
247: | |
248: | |
249: | |
250: | |
251: | |
252: | |
253: | |
254: | private function assertCliTypeFromInstall(string $path): string |
255: | { |
256: | return $this->file_exists($path . '/administrator/manifests/files/joomla.xml') ? '999.999.999' : '3.4.99999'; |
257: | } |
258: | |
259: | private function _exec(?string $path, $cmd, array $args = array()) |
260: | { |
261: | $wrapper = PhpWrapper::instantiateContexted($this->getAuthContext()); |
262: | $jtcli = $this->cliFromVersion($args['version'] ?? $this->assertCliTypeFromInstall($path), $this->php_pool_version_from_path((string)$path)); |
263: | $ret = $wrapper->exec($path, $jtcli . ' --no-interaction ' . $cmd, $args); |
264: | |
265: | if (!strncmp($ret['stdout'], 'Error:', strlen('Error:'))) { |
266: | |
267: | $ret['success'] = false; |
268: | if (!$ret['stderr']) { |
269: | $ret['stderr'] = $ret['stdout']; |
270: | } |
271: | } else if (!$ret['success'] && !$ret['stderr']) { |
272: | $ret['stderr'] = $ret['stdout']; |
273: | } |
274: | |
275: | return $ret; |
276: | } |
277: | |
278: | private function _fixMySQLSchema($docroot) |
279: | { |
280: | if ($this->sql_mysql_version() >= 50503) { |
281: | |
282: | return true; |
283: | } |
284: | $prefix = $this->domain_shadow_path(); |
285: | $glob = $prefix . $docroot . '/{_,}installation/sql/mysql/*.sql'; |
286: | foreach (glob($glob, GLOB_BRACE) as $f) { |
287: | $f = substr($f, strlen($prefix)); |
288: | $contents = $this->file_get_file_contents($f); |
289: | $matches = 0; |
290: | $contents = str_replace('utf8mb4', 'utf8', $contents, $matches); |
291: | $this->file_put_file_contents($f, $contents, true); |
292: | } |
293: | |
294: | return true; |
295: | } |
296: | |
297: | private function _generateConfig($docroot, $opts) |
298: | { |
299: | $domainfsprefix = $this->domain_fs_path(); |
300: | $file = tempnam($domainfsprefix . '/' . sys_get_temp_dir(), 'joomla'); |
301: | chmod($file, 0644); |
302: | $tz = date('T'); |
303: | $fullpath = ($this->php_jailed() ? '' : $this->domain_fs_path()) . $docroot; |
304: | $opts = array( |
305: | |
306: | 'gzip' => false, |
307: | 'debug' => 0, |
308: | 'ftp_host' => 'localhost', |
309: | 'ftp_user' => $this->username . '@' . $this->domain, |
310: | 'ftp_root' => $docroot, |
311: | 'sitename' => $opts['title'], |
312: | 'offset' => $tz, |
313: | 'tmp_path' => $fullpath . '/tmp', |
314: | 'log_path' => $fullpath . '/logs', |
315: | 'sendmail' => '/usr/sbin/sendmail', |
316: | 'mailer' => 'mail', |
317: | 'smtpport' => 587, |
318: | 'force_ssl' => !empty($opts['ssl']) ? 2 : 0, |
319: | ); |
320: | |
321: | file_put_contents($file, Symfony\Component\Yaml\Yaml::dump($opts, 2, 2)); |
322: | |
323: | return substr($file, strlen($domainfsprefix)); |
324: | } |
325: | |
326: | |
327: | |
328: | |
329: | |
330: | |
331: | |
332: | |
333: | |
334: | |
335: | |
336: | public function change_admin(string $hostname, string $path, array $fields): bool |
337: | { |
338: | $docroot = $this->getAppRoot($hostname, $path); |
339: | if (!$docroot) { |
340: | return warn('failed to change administrator information'); |
341: | } |
342: | $admin = $this->get_admin($hostname, $path); |
343: | |
344: | if (!$admin) { |
345: | return error('cannot determine admin of Joomla install'); |
346: | } |
347: | $dbconfig = $this->db_config($hostname, $path); |
348: | $conn = $this->_connectDB($dbconfig); |
349: | if (!$conn) { |
350: | return error("unable to connect to Joomla! database `%s'", $dbconfig['db']); |
351: | } |
352: | $fields = array_merge(array( |
353: | 'password' => null, |
354: | 'username' => null, |
355: | 'name' => null, |
356: | 'email' => null |
357: | ), $fields); |
358: | if ($fields['password']) { |
359: | $fields['password'] = $this->_saltedPassword($docroot, $fields['password']); |
360: | } |
361: | $filtered = array_filter($fields, static function ($f) { |
362: | return null !== $f; |
363: | }); |
364: | |
365: | $args = array(); |
366: | $builtQuery = array(); |
367: | foreach ($filtered as $k => $v) { |
368: | $builtQuery[] = "$k = ?"; |
369: | $args[] = &$filtered[$k]; |
370: | } |
371: | |
372: | |
373: | $args[] = &$admin; |
374: | $q = 'UPDATE ' . $dbconfig['prefix'] . 'users SET ' . implode(', ', $builtQuery) . ' WHERE username = ?'; |
375: | $stmt = $conn->prepare($q); |
376: | $types = str_repeat('s', count($args)); |
377: | array_unshift($args, $types); |
378: | call_user_func_array(array($stmt, 'bind_param'), $args); |
379: | $rs = $stmt->execute(); |
380: | |
381: | return $rs && $stmt->affected_rows > 0; |
382: | } |
383: | |
384: | |
385: | |
386: | |
387: | |
388: | |
389: | |
390: | |
391: | public function get_admin(string $hostname, string $path = ''): ?string |
392: | { |
393: | $dbconfig = $this->db_config($hostname, $path); |
394: | $mysqli = $this->_connectDB($dbconfig); |
395: | if (!$mysqli) { |
396: | error('cannot get admin user - failed to connect to database'); |
397: | |
398: | return null; |
399: | } |
400: | $q = 'select id, username FROM ' . $dbconfig['prefix'] . 'users ORDER BY registerDate, id ASC limit 1'; |
401: | $rs = $mysqli->query($q); |
402: | if (!$rs || $rs->num_rows < 1) { |
403: | warn('failed to enumerate Joomla administrative users'); |
404: | |
405: | return null; |
406: | } |
407: | |
408: | return $rs->fetch_object()->username; |
409: | |
410: | } |
411: | |
412: | |
413: | |
414: | |
415: | |
416: | |
417: | |
418: | |
419: | public function db_config(string $hostname, string $path = '') |
420: | { |
421: | $docroot = $this->getAppRoot($hostname, $path); |
422: | if (!$docroot) { |
423: | return error('failed to determine Joomla'); |
424: | } |
425: | |
426: | $j = $this->_getConfiguration($docroot); |
427: | |
428: | return array( |
429: | 'user' => $j['user'], |
430: | 'password' => $j['password'], |
431: | 'db' => $j['db'], |
432: | 'prefix' => $j['dbprefix'], |
433: | 'host' => $j['host'] |
434: | ); |
435: | } |
436: | |
437: | private function _getConfiguration($docroot) |
438: | { |
439: | |
440: | $code = 'include("./configuration.php"); $j = new JConfig(); print serialize($j);'; |
441: | $cmd = 'cd %(path)s && php -d mysqli.default_socket=' . escapeshellarg(ini_get('mysqli.default_socket')) . ' -r %(code)s'; |
442: | $ret = $this->pman_run($cmd, array('path' => $docroot, 'code' => $code)); |
443: | if (!$ret['success']) { |
444: | return error("failed to obtain Joomla configuration for `%s'", $docroot); |
445: | } |
446: | |
447: | return get_object_vars(\Util_PHP::unserialize(trim($ret['stdout']))); |
448: | } |
449: | |
450: | private function _connectDB($dbconfig) |
451: | { |
452: | static $conn; |
453: | if ($conn) { |
454: | return $conn; |
455: | } |
456: | $conn = new mysqli($dbconfig['host'], $dbconfig['user'], $dbconfig['password'], $dbconfig['db']); |
457: | if ($conn->connect_error) { |
458: | return false; |
459: | } |
460: | |
461: | return $conn; |
462: | } |
463: | |
464: | |
465: | |
466: | |
467: | |
468: | |
469: | |
470: | |
471: | |
472: | |
473: | private function _saltedPassword($docroot, $password): string |
474: | { |
475: | $salt = \Opcenter\Auth\Password::generate(32); |
476: | $hash = md5($password . $salt); |
477: | |
478: | return $hash . ':' . $salt; |
479: | } |
480: | |
481: | |
482: | |
483: | |
484: | |
485: | |
486: | |
487: | |
488: | |
489: | |
490: | public function install_plugin(string $hostname, string $path, string $plugin, string $version = ''): bool |
491: | { |
492: | $docroot = $this->getAppRoot($hostname, $path); |
493: | if (!$docroot) { |
494: | return error('invalid Joomla location'); |
495: | } |
496: | |
497: | $args = array($plugin); |
498: | $ret = $this->_exec($docroot, 'plugin install %s --activate', $args); |
499: | if (!$ret['success']) { |
500: | return error("failed to install plugin `%s': %s", $plugin, $ret['stderr']); |
501: | } |
502: | info("installed plugin `%s'", $plugin); |
503: | |
504: | return true; |
505: | } |
506: | |
507: | |
508: | |
509: | |
510: | |
511: | |
512: | |
513: | |
514: | |
515: | |
516: | public function uninstall_plugin(string $hostname, string $path, string $plugin, bool $force = false): bool |
517: | { |
518: | $docroot = $this->getAppRoot($hostname, $path); |
519: | if (!$docroot) { |
520: | return error('invalid Joomla location'); |
521: | } |
522: | |
523: | $args = array($plugin); |
524: | $cmd = 'plugin uninstall %s'; |
525: | if ($force) { |
526: | $cmd .= ' --deactivate'; |
527: | } |
528: | $ret = $this->_exec($docroot, $cmd, $args); |
529: | |
530: | if (!$ret['stdout'] || !strncmp($ret['stdout'], 'Warning:', strlen('Warning:'))) { |
531: | return error("failed to uninstall plugin `%s': %s", $plugin, $ret['stderr']); |
532: | } |
533: | info("uninstalled plugin `%s'", $plugin); |
534: | |
535: | return true; |
536: | } |
537: | |
538: | public function plugin_status(string $hostname, string $path = '', string $plugin = null): array |
539: | { |
540: | return []; |
541: | } |
542: | |
543: | |
544: | |
545: | |
546: | |
547: | |
548: | |
549: | |
550: | public function disable_all_plugins(string $hostname, string $path = ''): bool |
551: | { |
552: | $docroot = $this->getAppRoot($hostname, $path); |
553: | if (!$docroot) { |
554: | return error('failed to determine path'); |
555: | } |
556: | |
557: | $ret = $this->_exec($docroot, 'plugin deactivate --all --skip-plugins'); |
558: | if (!$ret['success']) { |
559: | return error('failed to deactivate all plugins: %s', $ret['stderr']); |
560: | } |
561: | |
562: | return info('plugin deactivation successful: %s', $ret['stdout']); |
563: | } |
564: | |
565: | |
566: | |
567: | |
568: | |
569: | |
570: | |
571: | |
572: | |
573: | public function update_all(string $hostname, string $path = '', string $version = null): bool |
574: | { |
575: | $ret = ($this->update($hostname, $path, $version)) |
576: | || error('failed to update all components'); |
577: | |
578: | parent::setInfo($this->getDocumentRoot($hostname, $path), [ |
579: | 'version' => $this->get_version($hostname, $path), |
580: | 'failed' => !$ret |
581: | ]); |
582: | |
583: | return $ret; |
584: | } |
585: | |
586: | |
587: | |
588: | |
589: | |
590: | |
591: | |
592: | |
593: | |
594: | |
595: | public function update(string $hostname, string $path = '', string $version = null): bool |
596: | { |
597: | if (!IS_CLI) { |
598: | return $this->query('joomla_update', $hostname, $path, $version); |
599: | } |
600: | |
601: | $docroot = $this->getAppRoot($hostname, $path); |
602: | if (!$docroot) { |
603: | return error('update failed'); |
604: | } |
605: | |
606: | $ret = $this->update_real($docroot, $version); |
607: | |
608: | parent::setInfo($docroot, [ |
609: | 'version' => $this->get_version($hostname, $path) ?? $version, |
610: | 'failed' => !$ret |
611: | ]); |
612: | |
613: | $mode = array_get( |
614: | $this->getOptions($docroot), |
615: | 'fortify', |
616: | self::DEFAULT_FORTIFY_MODE |
617: | ); |
618: | $this->fortify($hostname, $path, $mode); |
619: | info('Joomla updated, fortification set to %s', $mode); |
620: | |
621: | return info('Upgrade partially completed. Login to Joomla! admin portal to finish upgrade.'); |
622: | } |
623: | |
624: | |
625: | |
626: | |
627: | |
628: | |
629: | |
630: | |
631: | public function get_version(string $hostname, string $path = ''): ?string |
632: | { |
633: | if (!$this->valid($hostname, $path)) { |
634: | return null; |
635: | } |
636: | $docroot = $this->getAppRoot($hostname, $path); |
637: | $fsroot = $this->domain_fs_path(); |
638: | $path = $fsroot . $docroot; |
639: | $versigs = array( |
640: | '/libraries/cms/version/version.php', |
641: | '/libraries/joomla/version.php', |
642: | '/includes/version.php,' |
643: | ); |
644: | $file = $path . '/administrator/manifests/files/joomla.xml'; |
645: | if (file_exists($file)) { |
646: | |
647: | $xml = simplexml_load_string(file_get_contents($file)); |
648: | $version = data_get($xml, 'version'); |
649: | if ($version) { |
650: | return (string)$version[0]; |
651: | } |
652: | } |
653: | if (!defined('JPATH_PLATFORM')) { |
654: | define('JPATH_PLATFORM', 'goddamn sanity checks'); |
655: | } |
656: | if (!defined('_JEXEC')) { |
657: | define('_JEXEC', 'this is also a PITA'); |
658: | } |
659: | $version = null; |
660: | foreach ($versigs as $sig) { |
661: | $mypath = $path . $sig; |
662: | if (!file_exists($mypath)) { |
663: | continue; |
664: | } |
665: | $code = "define('JPATH_PLATFORM', 'foo'); define('_JEXEC', 'bar'); include_once './$sig'; class_exists('JVersion') or exit(1); " . |
666: | 'print (new JVersion)->getShortVersion();'; |
667: | $ret = $this->pman_run('cd %(docroot)s && php -r %(code)s', [ |
668: | 'docroot' => $docroot, |
669: | 'path' => $sig, |
670: | 'code' => $code |
671: | ]); |
672: | if ($ret['success']) { |
673: | return trim($ret['output']); |
674: | } |
675: | break; |
676: | } |
677: | if (null === $version) { |
678: | error('cannot determine Joomla! version - incomplete install?'); |
679: | |
680: | return null; |
681: | } |
682: | |
683: | return $version; |
684: | } |
685: | |
686: | |
687: | |
688: | |
689: | |
690: | |
691: | |
692: | |
693: | public function valid(string $hostname, string $path = ''): bool |
694: | { |
695: | if ($hostname[0] === '/') { |
696: | $docroot = $hostname; |
697: | } else { |
698: | $docroot = $this->getAppRoot($hostname, $path); |
699: | if (!$docroot) { |
700: | return false; |
701: | } |
702: | } |
703: | |
704: | return $this->file_exists($docroot . '/libraries/joomla') || $this->file_exists($docroot . '/cli/joomla.php'); |
705: | } |
706: | |
707: | protected function update_real(string $docroot, string $version = null): bool |
708: | { |
709: | if ($version) { |
710: | if (!is_scalar($version) || strcspn($version, '.0123456789')) { |
711: | return error('invalid version number, %s', $version); |
712: | } |
713: | if (!in_array($version, $this->get_versions())) { |
714: | return error("unknown version `%s'", $version); |
715: | } |
716: | } else { |
717: | $version = $this->getLatestVersion(); |
718: | } |
719: | |
720: | $replace = array( |
721: | 'version' => $version |
722: | ); |
723: | |
724: | $uri = preg_replace_callback(Regex::LAZY_SUB, static function ($m) use ($replace) { |
725: | return $replace[$m[1]]; |
726: | }, self::UPDATE_URI); |
727: | |
728: | $user = $this->getDocrootUser($docroot); |
729: | |
730: | if (!parent::download($uri, $docroot)) { |
731: | return error('failed to update Joomla! - download failed'); |
732: | } |
733: | if ($user !== $this->username) { |
734: | $this->file_chown($docroot, $user, true); |
735: | } |
736: | |
737: | |
738: | |
739: | $cfgfile = $docroot . '/configuration.php'; |
740: | $config = $this->file_get_file_contents($cfgfile); |
741: | $newconfig = preg_replace('/public\s+\$cache_handler\s*=[^;]+;/', '', $config); |
742: | $this->file_put_file_contents($cfgfile, $newconfig); |
743: | if (!$this->_updateJoomlaUpdatePlugin($docroot, $user, $version)) { |
744: | warn('Upgrade incomplete - failed to fetch Joomla! Update extension. Login to Joomla! admin portal to finish.'); |
745: | } else { |
746: | warn('Login to Joomla! administrative panel to complete upgrade'); |
747: | } |
748: | $this->file_put_file_contents($cfgfile, $config, true); |
749: | |
750: | return true; |
751: | } |
752: | |
753: | public function get_versions(): array |
754: | { |
755: | if (!IS_CLI) { |
756: | return $this->query('joomla_get_versions'); |
757: | } |
758: | |
759: | return $this->_getVersions(); |
760: | } |
761: | |
762: | |
763: | |
764: | |
765: | |
766: | |
767: | protected function _getVersions() |
768: | { |
769: | $key = 'joomla.versions'; |
770: | $cache = Cache_Super_Global::spawn(); |
771: | if (false !== ($ver = $cache->get($key))) { |
772: | return $ver; |
773: | } |
774: | |
775: | $oldCli = $this->cliFromVersion('1.0'); |
776: | $proc = Util_Process::exec( |
777: | 'php ' . $oldCli . ' --repo=%(repo)s --refresh versions', |
778: | array( |
779: | 'repo' => self::JOOMLA_MIRROR |
780: | ) |
781: | ); |
782: | if (!$proc['success'] || !preg_match_all(Regex::JOOMLA_VERSIONS, $proc['stdout'], $matches)) { |
783: | error('failed to fetch Joomla versions: %s', $proc['stderr']); |
784: | |
785: | return []; |
786: | } |
787: | $versions = $matches[1]; |
788: | usort($versions, 'version_compare'); |
789: | $cache->set($key, $versions, 43200); |
790: | |
791: | return $versions; |
792: | } |
793: | |
794: | private function _updateJoomlaUpdatePlugin($docroot, $user, $version) |
795: | { |
796: | $juext = $this->get_plugin_info('com_joomlaupdate'); |
797: | if (!$juext) { |
798: | return false; |
799: | } |
800: | |
801: | |
802: | $updatever = (string)( |
803: | array_get($juext, 'update.targetplatform.@attributes.version') ?? |
804: | array_get($juext, 'update.0.targetplatform.@attributes.version')); |
805: | if (version_compare((string)$version, $updatever, '<')) { |
806: | return true; |
807: | } |
808: | |
809: | |
810: | $uri = array_get($juext, 'update.downloads.downloadurl') ?? |
811: | array_get($juext, 'update.0.downloads.downloadurl'); |
812: | $path = $docroot . '/tmp/com_joomlaupdate'; |
813: | if ($this->file_exists($path)) { |
814: | return false; |
815: | } |
816: | |
817: | $this->file_create_directory($path); |
818: | if (!parent::download($uri, $path)) { |
819: | return false; |
820: | } |
821: | $this->file_chown($path, $user, true); |
822: | |
823: | $ret = $this->_exec($docroot, 'extension:installfile -- . %(plugin)s', array('plugin' => $path)); |
824: | $this->file_delete($path, true); |
825: | |
826: | return $ret['success']; |
827: | } |
828: | |
829: | public function get_plugin_info($plugin, $ver = null) |
830: | { |
831: | $replace = array( |
832: | 'extension' => $plugin, |
833: | 'ver' => $ver |
834: | ); |
835: | |
836: | $uri = preg_replace_callback(Regex::LAZY_SUB, static function ($m) use ($replace) { |
837: | return $replace[$m[1]]; |
838: | 'http://update.joomla.org/core/extensions/%extension%.xml'; |
839: | }, self::JOOMLA_MODULE_XML . '?ver=%ver%'); |
840: | $content = silence(static function () use ($uri) { |
841: | return simplexml_load_string(file_get_contents($uri), 'SimpleXMLElement', LIBXML_NOCDATA); |
842: | }); |
843: | if (!$content) { |
844: | return false; |
845: | } |
846: | |
847: | |
848: | return json_decode(json_encode((array)$content), true); |
849: | } |
850: | |
851: | |
852: | |
853: | |
854: | |
855: | |
856: | |
857: | |
858: | |
859: | public function update_plugins(string $hostname, string $path = '', array $plugins = array()): bool |
860: | { |
861: | $docroot = $this->getAppRoot($hostname, $path); |
862: | if (!$docroot) { |
863: | return error('update failed'); |
864: | } |
865: | |
866: | $cmd = 'plugin update'; |
867: | $args = array(); |
868: | if (!$plugins) { |
869: | $cmd .= ' --all'; |
870: | } else { |
871: | for ($i = 0, $n = count($plugins); $i < $n; $i++) { |
872: | $plugin = $plugins[$i]; |
873: | $version = null; |
874: | if (isset($plugin['version'])) { |
875: | $version = $plugin['version']; |
876: | } |
877: | if (isset($plugin['name'])) { |
878: | $plugin = $plugin['name']; |
879: | } |
880: | |
881: | $name = 'p' . $i; |
882: | |
883: | $cmd .= ' %(' . $name . ')s'; |
884: | $args[$name] = $plugin; |
885: | if ($version) { |
886: | $cmd .= ' --version=%(' . $name . 'v)s'; |
887: | $args[$name . 'v'] = $version; |
888: | } |
889: | } |
890: | } |
891: | |
892: | $ret = $this->_exec($docroot, $cmd, $args); |
893: | if (!$ret['success']) { |
894: | return error("plugin update failed: `%s'", $ret['stderr']); |
895: | } |
896: | |
897: | return $ret['success']; |
898: | } |
899: | |
900: | |
901: | |
902: | |
903: | |
904: | |
905: | |
906: | |
907: | |
908: | public function update_themes(string $hostname, string $path = '', array $themes = array()): bool |
909: | { |
910: | return error('not implemented yet'); |
911: | } |
912: | |
913: | |
914: | |
915: | |
916: | |
917: | |
918: | public function _housekeeping(): bool |
919: | { |
920: | $versions = [ |
921: | 'joomlatools-1.6.0-1.phar' => 'joomlatools-1.6.0-1.phar', |
922: | 'joomlatools-' . self::JOOMLA_CLI_VERSION . '.phar' => basename(self::JOOMLA_CLI) |
923: | ]; |
924: | |
925: | foreach ($versions as $full => $short) { |
926: | $src = resource_path('storehouse') . '/' . $full; |
927: | $dest = dirname(self::JOOMLA_CLI) . '/' . $short; |
928: | if (is_file($dest) && sha1_file($src) === sha1_file($dest)) { |
929: | continue; |
930: | } |
931: | |
932: | copy($src, $dest); |
933: | chmod($dest, 0755); |
934: | info('Copied %s to PEAR', $full); |
935: | } |
936: | |
937: | return true; |
938: | } |
939: | } |
940: | |