1:    2:    3:    4:    5:    6:    7:    8:    9:   10:   11:   12:   13:   14:   15:   16:   17:   18:   19:   20:   21:   22:   23:   24:   25:   26:   27:   28:   29:   30:   31:   32:   33:   34:   35:   36:   37:   38:   39:   40:   41:   42:   43:   44:   45:   46:   47:   48:   49:   50:   51:   52:   53:   54:   55:   56:   57:   58:   59:   60:   61:   62:   63:   64:   65:   66:   67:   68:   69:   70:   71:   72:   73:   74:   75:   76:   77:   78:   79:   80:   81:   82:   83:   84:   85:   86:   87:   88:   89:   90:   91:   92:   93:   94:   95:   96:   97:   98:   99:  100:  101:  102:  103:  104:  105:  106:  107:  108:  109:  110:  111:  112:  113:  114:  115:  116:  117:  118:  119:  120:  121:  122:  123:  124:  125:  126:  127:  128:  129:  130:  131:  132:  133:  134:  135:  136:  137:  138:  139:  140:  141:  142:  143:  144:  145:  146:  147:  148:  149:  150:  151:  152:  153:  154:  155:  156:  157:  158:  159:  160:  161:  162:  163:  164:  165:  166:  167:  168:  169:  170:  171:  172:  173:  174:  175:  176:  177:  178:  179:  180:  181:  182:  183:  184:  185:  186:  187:  188:  189:  190:  191:  192:  193:  194:  195:  196:  197:  198:  199:  200:  201:  202:  203:  204:  205:  206:  207:  208:  209:  210:  211:  212:  213:  214:  215:  216:  217:  218:  219:  220:  221:  222:  223:  224:  225:  226:  227:  228:  229:  230:  231:  232:  233:  234:  235:  236:  237:  238:  239:  240:  241:  242:  243:  244:  245:  246:  247:  248:  249:  250:  251:  252:  253:  254:  255:  256:  257:  258:  259:  260:  261:  262:  263:  264:  265:  266:  267:  268:  269:  270:  271:  272:  273:  274:  275:  276:  277:  278:  279:  280:  281:  282:  283:  284:  285:  286:  287:  288:  289:  290:  291:  292:  293:  294:  295:  296:  297:  298:  299:  300:  301:  302:  303:  304:  305:  306:  307:  308:  309:  310:  311:  312:  313:  314:  315:  316:  317:  318:  319:  320:  321:  322:  323:  324:  325:  326:  327:  328:  329:  330:  331:  332:  333:  334:  335:  336:  337:  338:  339:  340:  341:  342:  343:  344:  345:  346:  347:  348:  349:  350:  351:  352:  353:  354:  355:  356:  357:  358:  359:  360:  361:  362:  363:  364:  365:  366:  367:  368:  369:  370:  371:  372:  373:  374:  375:  376:  377:  378:  379:  380:  381:  382:  383:  384:  385:  386:  387:  388:  389:  390:  391:  392:  393:  394:  395:  396:  397:  398:  399:  400:  401:  402:  403:  404:  405:  406:  407:  408:  409:  410:  411:  412:  413:  414:  415:  416:  417:  418:  419:  420:  421:  422:  423:  424:  425:  426:  427:  428:  429:  430:  431:  432:  433:  434:  435:  436:  437:  438:  439:  440:  441:  442:  443:  444:  445:  446:  447:  448:  449:  450:  451:  452:  453:  454:  455:  456:  457:  458:  459:  460:  461:  462:  463:  464:  465:  466:  467:  468:  469:  470:  471:  472:  473:  474:  475:  476:  477:  478:  479:  480:  481:  482:  483:  484:  485:  486:  487:  488:  489:  490:  491:  492:  493:  494:  495:  496:  497:  498:  499:  500:  501:  502:  503:  504:  505:  506:  507:  508:  509:  510:  511:  512:  513:  514:  515:  516:  517:  518:  519:  520:  521:  522:  523:  524:  525:  526:  527:  528:  529:  530:  531:  532:  533:  534:  535:  536:  537:  538:  539:  540:  541:  542:  543:  544:  545:  546:  547:  548:  549:  550:  551:  552:  553:  554:  555:  556:  557:  558:  559:  560:  561:  562:  563:  564:  565:  566:  567:  568:  569:  570:  571:  572:  573:  574:  575:  576:  577:  578:  579:  580:  581:  582:  583:  584:  585:  586:  587:  588:  589:  590:  591:  592:  593:  594:  595:  596:  597:  598:  599:  600:  601:  602:  603:  604:  605:  606:  607:  608:  609:  610:  611:  612:  613:  614:  615:  616:  617:  618:  619:  620:  621:  622:  623:  624:  625:  626:  627:  628:  629:  630:  631:  632:  633:  634:  635:  636:  637:  638:  639:  640:  641:  642:  643:  644:  645:  646:  647:  648:  649:  650:  651:  652:  653:  654:  655:  656:  657:  658:  659:  660:  661:  662:  663:  664:  665:  666:  667:  668:  669:  670:  671:  672:  673:  674:  675:  676:  677:  678:  679:  680:  681:  682:  683:  684:  685:  686:  687:  688:  689:  690:  691:  692:  693:  694:  695:  696:  697:  698:  699:  700:  701:  702:  703:  704:  705:  706:  707:  708:  709:  710:  711:  712:  713:  714:  715:  716:  717:  718:  719:  720:  721:  722:  723:  724:  725:  726:  727:  728:  729:  730:  731:  732:  733:  734:  735:  736:  737:  738:  739:  740:  741:  742:  743:  744:  745:  746:  747:  748:  749:  750:  751:  752:  753:  754:  755:  756:  757:  758:  759:  760:  761:  762:  763:  764:  765:  766:  767:  768:  769:  770:  771:  772:  773:  774:  775:  776:  777:  778:  779:  780:  781:  782:  783:  784:  785:  786:  787:  788:  789:  790:  791:  792:  793:  794:  795:  796:  797:  798:  799:  800:  801:  802:  803:  804:  805:  806:  807:  808:  809:  810:  811:  812:  813:  814:  815:  816:  817:  818:  819:  820:  821:  822:  823:  824:  825:  826:  827:  828:  829:  830:  831:  832:  833:  834:  835:  836:  837:  838:  839:  840:  841:  842:  843:  844:  845:  846:  847:  848:  849:  850:  851:  852:  853:  854:  855:  856:  857:  858:  859:  860:  861:  862:  863:  864:  865:  866:  867:  868:  869:  870:  871:  872:  873:  874:  875:  876:  877:  878:  879:  880:  881:  882:  883:  884:  885:  886:  887:  888:  889:  890:  891:  892:  893:  894:  895:  896:  897:  898:  899:  900:  901:  902:  903:  904:  905:  906:  907:  908:  909:  910:  911:  912:  913:  914:  915:  916:  917:  918:  919:  920:  921:  922:  923:  924:  925:  926:  927:  928:  929:  930:  931:  932:  933:  934:  935:  936:  937:  938:  939:  940:  941:  942:  943:  944:  945:  946:  947:  948:  949:  950:  951:  952:  953:  954:  955:  956:  957:  958:  959:  960:  961:  962:  963:  964:  965:  966:  967:  968:  969:  970:  971:  972:  973:  974:  975:  976:  977:  978:  979:  980:  981:  982:  983:  984:  985:  986:  987:  988:  989:  990:  991:  992:  993:  994:  995:  996:  997:  998:  999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 1028: 1029: 1030: 1031: 1032: 1033: 1034: 1035: 1036: 1037: 1038: 1039: 1040: 1041: 1042: 1043: 1044: 1045: 1046: 1047: 1048: 1049: 1050: 1051: 1052: 1053: 1054: 1055: 1056: 1057: 1058: 1059: 1060: 1061: 1062: 
<?php
    declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    /**
     * Drupal drush interface
     *
     * @package core
     */
    class Drupal_Module extends \Module\Support\Webapps
    {
        const APP_NAME = 'Drupal';

        // primary domain document root
        const DRUPAL_CLI = '/usr/share/pear/drupal.phar';
        const DEFAULT_BRANCH = '8.x';
        const DRUPAL_MAJORS = ['6.x', '7.x', '8.x'];
        // latest release
        const DRUPAL_CLI_URL = 'https://github.com/drush-ops/drush/releases/download/8.3.5/drush.phar';
        const VERSION_CHECK_URL = 'https://updates.drupal.org/release-history';
        const DEFAULT_VERSION_LOCK = 'major';

        protected $_aclList = array(
            'max' => array('sites/*/files')
        );

        /**
         * void __construct(void)
         *
         * @ignore
         */
        public function __construct()
        {
            parent::__construct();
        }

        /**
         * Install WordPress into a pre-existing location
         *
         * @param string $hostname domain or subdomain to install WordPress
         * @param string $path     optional path under hostname
         * @param array  $opts     additional install options
         * @return bool
         */
        public function install(string $hostname, string $path = '', array $opts = array()): bool
        {
            if (!$this->mysql_enabled()) {
                return error("MySQL must be enabled to install %s", ucwords($this->getInternalName()));
            }
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('failed to install Drupal');
            }

            if (!$this->parseInstallOptions($opts, $hostname, $path)) {
                return false;
            }

            // can't fetch translation file from ftp??
            // don't worry about it for now
            if (!isset($opts['locale'])) {
                $opts['locale'] = 'us';
            }

            if (!isset($opts['dist'])) {
                $opts['profile'] = 'standard';
                $opts['dist'] = 'drupal';
                if (isset($opts['version'])) {
                    if (strcspn($opts['version'], '.0123456789x')) {
                        return error('invalid version number, %s', $opts['version']);
                    }
                    $opts['dist'] .= '-' . $opts['version'];
                }

            } else if (!isset($opts['profile'])) {
                $opts['profile'] = $opts['dist'];
            }

            $cmd = 'dl %(dist)s';

            $tmpdir = '/tmp/drupal' . crc32((string)\Util_PHP::random_int());
            $args = array(
                'tempdir' => $tmpdir,
                'path'    => $docroot,
                'dist'    => $opts['dist']
            );
            /**
             * drupal expects destination dir to exist
             * move /tmp/<RANDOM NAME>/drupal to <DOCROOT> instead
             * of downloading to <DOCROOT>/drupal and moving everything down 1
             */
            $this->file_create_directory($tmpdir);
            $ret = $this->_exec('/tmp', $cmd . ' --drupal-project-rename --destination=%(tempdir)s -q', $args);

            if (!$ret['success']) {
                return error('failed to download Drupal - out of space? Error: `%s\'',
                    coalesce($ret['stderr'], $ret['stdout'])
                );
            }
            if ($this->file_exists($docroot)) {
                $this->file_delete($docroot, true);
            }

            $this->file_purge();
            $ret = $this->file_rename($tmpdir . '/drupal', $docroot);
            $this->file_delete($tmpdir, true);
            if (!$ret) {
                return error("failed to move Drupal install to `%s'", $docroot);
            }

            if (isset($opts['site-email']) && !preg_match(Regex::EMAIL, $opts['site-email'])) {
                return error("invalid site email `%s' provided", $opts['site-email']);
            }

            if (!isset($opts['site-email'])) {
                // default to active domain, hope it's valid!
                if (false === strpos($hostname, '.')) {
                    $hostname .= '.' . $this->domain;
                }
                $split = $this->web_split_host($hostname);
                if (!$this->email_address_exists('postmaster', $split['domain'])) {
                    if (!$this->email_transport_exists($split['domain'])) {
                        warn("email is not configured for domain `%s', messages sent from installation may " .
                            'be unrespondable', $split['domain']);
                    } else if ($this->email_add_alias('postmaster', $split['domain'], $opts['email'])) {
                        info("created `postmaster@%s' address for Drupal mailings that " .
                            "will forward to `%s'", $split['domain'], $opts['email']);
                    } else {
                        warn("failed to create Drupal postmaster address `postmaster@%s', messages " .
                            'sent from installation may be unrespondable', $split['domain']);
                    }
                }
                $opts['site-email'] = 'postmaster@' . $split['domain'];
            }


            $db = $this->_suggestDB($hostname);
            if (!$db) {
                return false;
            }

            $dbuser = $this->_suggestUser($db);
            if (!$dbuser) {
                return false;
            }
            $dbpass = $this->suggestPassword();
            $credentials = array(
                'db'       => $db,
                'user'     => $dbuser,
                'password' => $dbpass,
            );

            if (!parent::setupDatabase($credentials)) {
                return false;
            }

            $proto = 'mysql';
            if (!empty($opts['version']) && version_compare($opts['version'], '7.0', '<')) {
                $proto = 'mysqli';
            }
            $dburi = $proto . '://' . $credentials['user'] . ':' .
                $credentials['password'] . '@localhost/' . $credentials['db'];

            if (!isset($opts['title'])) {
                $opts['title'] = 'A Random Drupal Install';
            }

            $autogenpw = false;
            if (!isset($opts['password'])) {
                $autogenpw = true;
                $opts['password'] = $this->suggestPassword(10);
                info("autogenerated password `%s'", $opts['password']);
            }

            info("setting admin user to `%s'", $opts['user']);

            $xtra = array(
                "install_configure_form.update_status_module='array(FALSE,FALSE)'"
            );
            // drush reqs name if dist not drupal otherwise
            // getPath() on null error

            if ($opts['dist'] === 'drupal') {
                $fmtstr = '';
                $dist = '';
            } else {
                $fmtstr = '%(dist)s ';
                $dist = $opts['dist'];
            }
            $args = array(
                'dist'         => $dist,
                'profile'      => $opts['profile'],
                'dburi'        => $dburi,
                'account-name' => $opts['user'],
                'account-pass' => $opts['password'],
                'account-mail' => $opts['email'],
                'locale'       => $opts['locale'],
                'site-mail'    => $opts['site-email'],
                'title'        => $opts['title'],
                'xtraopts'     => implode(' ', $xtra)
            );

            $ret = $this->_exec($docroot,
                'site-install %(profile)s -q --db-url=%(dburi)s --account-name=%(account-name)s ' .
                '--account-pass=%(account-pass)s --account-mail=%(account-mail)s ' .
                '--site-mail=%(site-mail)s --site-name=%(title)s %(xtraopts)s', $args);

            if (!$ret['success']) {
                info('removing temporary files');
                $this->file_delete($docroot, true);
                $this->sql_delete_mysql_database($db);
                $this->sql_delete_mysql_user($dbuser, 'localhost');

                return error('failed to install Drupal: %s', $ret['stderr']);
            }
            // by default, let's only open up ACLs to the bare minimum
            $this->file_touch($docroot . '/.htaccess');
            $this->removeInvalidDirectives($docroot, 'sites/default/files/');
            $this->fortify($hostname, $path, 'max');

            // confirm version
            $opts['version'] = $this->get_version($hostname, $path);
            $params = array(
                'version'    => $opts['version'],
                'hostname'   => $hostname,
                'path'       => $path,
                'autoupdate' => (bool)$opts['autoupdate'],
                'options'    => $opts
            );
            $this->map('add', $docroot, $params);
            $fqdn = $this->web_normalize_hostname($hostname);
            /**
             * Make sure RewriteBase is present, move to Webapps?
             */
            parent::fixRewriteBase($docroot, $path);

            $this->_postInstallTrustedHost($dist, $hostname, $docroot);
            if (!empty($opts['ssl'])) {
                // @todo force redirect to HTTPS
            }
            if (array_get($opts, 'notify', true)) {
                \Lararia\Bootstrapper::minstrap();
                \Illuminate\Support\Facades\Mail::to($opts['email'])->
                send((new \Module\Support\Webapps\Mailer('install.drupal', [
                    'login'    => $opts['user'],
                    'password' => $opts['password'],
                    'uri'      => rtrim($fqdn . '/' . $path, '/'),
                    'proto'    => empty($opts['ssl']) ? 'http://' : 'https://',
                    'appname'  => static::APP_NAME
                ]))->setAppName(static::APP_NAME));
            }

            if (!$opts['squash']) {
                parent::unsquash($docroot);
            }

            return info('Drupal installed - confirmation email with login info sent to %s', $opts['email']);
        }

        private function _exec($path = null, $cmd, array $args = array())
        {
            // client may override tz, propagate to bin
            $tz = date_default_timezone_get();
            $debug = is_debug() ? ' -v' : '';
            $cli = 'php -d pdo_mysql.default_socket=' . escapeshellarg(ini_get('mysqli.default_socket')) .
                ' -d date.timezone=' . $tz . ' -d memory_limit=192m ' . self::DRUPAL_CLI . $debug . ' -y';
            if (!is_array($args)) {
                $args = func_get_args();
                array_shift($args);
            }
            $user = $this->username;
            if ($path) {
                $user = parent::getDocrootUser($path);
                $cli = 'cd %(path)s && ' . $cli;
                $args['path'] = $path;
            }
            $cmd = $cli . ' ' . $cmd;
            $ret = $this->pman_run($cmd, $args, null, ['user' => $user]);
            if (0 === strpos((string)coalesce($ret['stderr'], $ret['stdout']), 'Error:')) {
                // move stdout to stderr on error for consistency
                $ret['success'] = false;
                if (!$ret['stderr']) {
                    $ret['stderr'] = $ret['stdout'];
                }

            }

            return $ret;
        }

        /**
         * Get installed version
         *
         * @param string $hostname
         * @param string $path
         * @return null|string version number
         */
        public function get_version(string $hostname, string $path = ''): ?string
        {

            if (!$this->valid($hostname, $path)) {
                return null;
            }
            $docroot = $this->getAppRoot($hostname, $path);

            return $this->_getVersion($docroot);
        }

        /**
         * Location is a valid WP install
         *
         * @param string $hostname or $docroot
         * @param string $path
         * @return bool
         */
        public function valid(string $hostname, string $path = ''): bool
        {
            if ($hostname[0] === '/') {
                $docroot = $hostname;
            } else {
                $docroot = $this->getAppRoot($hostname, $path);
                if (!$docroot) {
                    return false;
                }
            }

            return $this->file_exists($docroot . '/sites/default')
                || $this->file_exists($docroot . '/sites/all');
        }

        /**
         * Get version using exact docroot
         *
         * @param $docroot
         * @return string
         */
        protected function _getVersion($docroot): ?string
        {
            $ret = $this->_exec($docroot, 'status --format=json');
            if (!$ret['success']) {
                return null;
            }

            $output = json_decode($ret['stdout'], true);

            return $output['drupal-version'] ?? null;
        }

        /**
         * Add trusted_host_patterns if necessary
         *
         * @param $version
         * @param $hostname
         * @param $docroot
         * @return bool
         */
        private function _postInstallTrustedHost($version, $hostname, $docroot): bool
        {
            if (version_compare((string)$version, '8.0', '<')) {
                return true;
            }
            $file = $docroot . '/sites/default/settings.php';
            $content = $this->file_get_file_contents($file);
            if (!$content) {
                return error('unable to add trusted_host_patterns configuration - cannot get ' .
                    "Drupal configuration for `%s'", $hostname);
            }
            $content .= "\n\n" .
                '/** in the event the domain name changes, trust site configuration */' . "\n" .
                '$settings["trusted_host_patterns"] = array(' . "\n" .
                "\t" . "'^(www\.)?' . " . 'str_replace(".", "\\\\.", $_SERVER["DOMAIN"]) . ' . "'$'" . "\n" .
                ');' . "\n";

            return $this->file_put_file_contents($file, $content, true, true);
        }

        /**
         * Install and activate plugin
         *
         * @param string $hostname domain or subdomain of wp install
         * @param string $path     optional path component of wp install
         * @param string $plugin   plugin name
         * @param string $version  optional plugin version
         * @return bool
         */
        public function install_plugin(string $hostname, string $path = '', string $plugin, string $version = ''): bool
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }
            $dlplugin = $plugin;
            if ($version) {
                if (false === strpos($version, '-')) {
                    // Drupal seems to like <major>-x naming conventions
                    $version .= '-x';
                }
                $dlplugin .= '-' . $version;
            }
            $args = array($plugin);
            $ret = $this->_exec($docroot, 'pm-download -y %s', $args);
            if (!$ret['success']) {
                return error("failed to install plugin `%s': %s", $plugin, $ret['stderr']);
            }

            if (!$this->enable_plugin($hostname, $path, $plugin)) {
                return warn("downloaded plugin `%s' but failed to activate: %s", $plugin, $ret['stderr']);
            }
            info("installed plugin `%s'", $plugin);

            return true;
        }

        public function enable_plugin($hostname, $path = '', $plugin)
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }
            $ret = $this->_exec($docroot, 'pm-enable -y %s', array($plugin));
            if (!$ret) {
                return error("failed to enable plugin `%s': %s", $plugin, $ret['stderr']);
            }

            return true;
        }

        /**
         * Uninstall a plugin
         *
         * @param string      $hostname
         * @param string      $path
         * @param string      $plugin plugin name
         * @param bool|string $force  delete even if plugin activated
         * @return bool
         */
        public function uninstall_plugin(string $hostname, string $path = '', string $plugin, bool $force = false): bool
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }

            $args = array($plugin);

            if ($this->plugin_active($hostname, $path, $plugin)) {
                if (!$force) {
                    return error("plugin `%s' is active, disable first");
                }
                $this->disable_plugin($hostname, $path, $plugin);
            }

            $cmd = 'pm-uninstall %s';

            $ret = $this->_exec($docroot, $cmd, $args);

            if (!$ret['stdout'] || !strncmp($ret['stdout'], 'Warning:', strlen('Warning:'))) {
                return error("failed to uninstall plugin `%s': %s", $plugin, $ret['stderr']);
            }
            info("uninstalled plugin `%s'", $plugin);

            return true;
        }

        public function plugin_active($hostname, $path = '', $plugin)
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }
            $plugin = $this->plugin_status($hostname, $path, $plugin);

            return $plugin['status'] === 'enabled';
        }

        public function plugin_status(string $hostname, string $path = '', string $plugin = null): ?array
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }
            $cmd = 'pm-info --format=json %(plugin)s';
            $ret = $this->_exec($docroot, $cmd, ['plugin' => $plugin]);
            if (!$ret['success']) {
                return null;
            }
            $plugins = [];
            foreach (json_decode($ret['stdout'], true) as $name => $meta) {
                $plugins[$name] = [
                    'version' => $meta['version'],
                    'next'    => null,
                    'current' => true,
                    'max'     => $meta['version']
                ];
            }

            return $plugin ? $array_pop($plugins) : $plugins;
        }

        public function disable_plugin($hostname, $path = '', $plugin)
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }
            $ret = $this->_exec($docroot, 'pm-disable -y %s', array($plugin));
            if (!$ret) {
                return error("failed to disable plugin `%s': %s", $plugin, $ret['stderr']);
            }
            info("disabled plugin `%s'", $plugin);

            return true;
        }

        /**
         * Recovery mode to disable all plugins
         *
         * @param string $hostname subdomain or domain of WP
         * @param string $path     optional path
         * @return bool
         */
        public function disable_all_plugins(string $hostname, string $path = ''): bool
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('failed to determine path');
            }
            $plugins = array();
            $installed = $this->list_all_plugins($hostname, $path);
            if (!$installed) {
                return true;
            }
            foreach ($installed as $plugin => $info) {
                if (strtolower($info['status']) !== 'enabled') {
                    continue;
                }
                $this->disable_plugin($hostname, $path, $plugin);
                $plugins[] = $info['name'];

            }
            if ($plugins) {
                info("disabled plugins: `%s'", implode(',', $plugins));
            }

            return true;
        }

        public function list_all_plugins($hostname, $path = '', $status = '')
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('invalid Drupal location');
            }
            if ($status) {
                $status = strtolower($status);
                $status = '--status=' . $status;
            }
            $ret = $this->_exec($docroot, 'pm-list --format=json --no-core %s', array($status));
            if (!$ret['success']) {
                return error('failed to enumerate plugins: %s', $ret['stderr']);
            }

            return json_decode($ret['stdout'], true);
        }

        /**
         * Uninstall Drupal from a location
         *
         * @param string $hostname
         * @param string $path
         * @param string $delete
         * @return bool
         * @internal param string $deletefiles remove all files under docroot
         */
        public function uninstall(string $hostname, string $path = '', string $delete = 'all'): bool
        {
            return parent::uninstall($hostname, $path, $delete);
        }

        /**
         * Get database configuration for a blog
         *
         * @param string $hostname domain or subdomain of Drupal
         * @param string $path     optional path
         * @return array|bool
         */
        public function db_config(string $hostname, string $path = '')
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('failed to determine Drupal');
            }
            $code = 'include("./sites/default/settings.php"); $conf = $databases["default"]["default"]; print serialize(array("user" => $conf["username"], "password" => $conf["password"], "db" => $conf["database"], "prefix" => $conf["prefix"], "host" => $conf["host"]));';
            $cmd = 'cd %(path)s && php -r %(code)s';
            $ret = $this->pman_run($cmd, array('path' => $docroot, 'code' => $code));

            if (!$ret['success']) {
                return error("failed to obtain Drupal configuration for `%s'", $docroot);
            }

            return \Util_PHP::unserialize(trim($ret['stdout']));
        }

        /**
         * Check if version is latest or get latest version
         *
         * @param null|string $version
         * @param string|null $branchcomp
         * @return int|string
         */
        public function is_current(string $version = null, string $branchcomp = null)
        {
            $vermask = $version ? substr($version, 0, strpos($version, '.')) : null;
            $latest = $this->_getLastestVersion($vermask);
            if (!$version) {
                return $latest;
            }
            if (version_compare((string)$version, (string)$latest, '=')) {
                return 1;
            }
            if (version_compare((string)$version, (string)$latest, '<')) {
                return 0;
            }

            return -1;
        }

        /**
         * Get latest Drupal release
         *
         * @param null $version
         * @return null|string
         */
        private function _getLastestVersion($version = null): ?string
        {
            if (!$version) {
                $version = self::DEFAULT_BRANCH;
            }
            $version = $this->_extractBranch($version);
            $versions = $this->_getVersions('drupal', $version);

            if (!$versions) {
                return null;
            }
            $releases = $versions['releases']['release'];
            for ($i = 0, $n = count($releases); $i < $n; $i++) {
                // dev, alpha, etc
                if (!isset($releases[$i]['version_extra'])) {
                    return $releases[$i]['version'];
                }
            }

            // can't find a suitable release, return the first one
            return $releases[0]['version'];
        }

        private function _extractBranch($version)
        {
            if (substr($version, -2) === '.x') {
                return $version;
            }
            $pos = strpos($version, '.');
            if (false === $pos) {
                // sent major alone
                return $version . '.x';
            }
            $newver = substr($version, 0, $pos);

            return $newver . '.x';
        }

        /**
         * Get all current major versions
         *
         * @param string $module
         * @param string $version
         * @return array
         */
        private function _getVersions($module = 'drupal', $version = self::DEFAULT_BRANCH): array
        {
            $version = $this->_extractBranch($version);
            $key = 'drupal.versions:' . $module;

            $cache = Cache_Super_Global::spawn();
            if (false !== ($ver = $cache->get($key)) && isset($ver[$version])) {
                return $ver[$version];
            }
            $url = self::VERSION_CHECK_URL;
            $url .= '/' . $module . '/' . $version;
            $contents = file_get_contents($url);

            if (!$contents) {
                return array();
            }
            if (!is_array($ver)) {
                $ver = array();
            }

            $versions = json_decode(json_encode(simplexml_load_string($contents)), true);
            $ver[$version] = $versions;
            $cache->set($key, $versions, 43200);

            return $versions;
        }

        /**
         * Change WP admin credentials
         *
         * $fields is a hash whose indices match password
         *
         * @param string $hostname
         * @param string $path
         * @param array  $fields password only field supported for now
         * @return bool
         */
        public function change_admin(string $hostname, string $path = '', array $fields): bool
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return warn('failed to change administrator information');
            }
            $admin = $this->get_admin($hostname, $path);

            if (!$admin) {
                return error('cannot determine admin of Drupal install');
            }

            $args = array(
                'user' => $admin
            );

            if (isset($fields['password'])) {
                $args['password'] = $fields['password'];
                $ret = $this->_exec($docroot, 'user-password --password=%(password)s %(user)s', $args);
                if (!$ret['success']) {
                    return error("failed to update password for user `%s': %s", $admin, $ret['stderr']);
                }
            }

            return true;
        }

        /**
         * Get the primary admin for a Drupal instance
         *
         * @param string $hostname
         * @param string $path
         * @return null|string admin or false on failure
         */
        public function get_admin(string $hostname, string $path = ''): ?string
        {
            $docroot = $this->getAppRoot($hostname, $path);
            $ret = $this->_exec($docroot, 'user-information 1 --format=json');
            if (!$ret['success']) {
                warn('failed to enumerate Drupal administrative users');

                return null;
            }
            $tmp = json_decode($ret['stdout'], true);
            if (!$tmp) {
                return null;
            }
            $tmp = array_pop($tmp);

            return $tmp['name'];
        }

        /**
         * Update core, plugins, and themes atomically
         *
         * @param string $hostname subdomain or domain
         * @param string $path     optional path under hostname
         * @param string $version
         * @return bool
         */
        public function update_all(string $hostname, string $path = '', string $version = null): bool
        {
            $ret = ($this->update($hostname, $path, $version) && $this->update_plugins($hostname, $path))
                || error('failed to update all components');

            parent::setInfo($this->getAppRoot($hostname, $path), [
                'version' => $this->get_version($hostname, $path),
                'failed'  => !$ret
            ]);

            return $ret;
        }

        /**
         * Update Drupal to latest version
         *
         * @param string $hostname domain or subdomain under which WP is installed
         * @param string $path     optional subdirectory
         * @param string $version
         * @return bool
         */
        public function update(string $hostname, string $path = '', string $version = null): bool
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('update failed');
            }
            if ($this->isLocked($docroot)) {
                return error('Drupal is locked - remove lock file from `%s\' and try again', $docroot);
            }
            if ($version) {
                if (!is_scalar($version) || strcspn($version, '.0123456789x-')) {
                    return error('invalid version number, %s', $version);
                }
                $current = $this->_extractBranch($version);
            } else {
                $current = $this->_extractBranch($this->get_version($hostname, $path));
                $version = $this->_getLastestVersion($current);
            }

            // save .htaccess
            $htaccess = $docroot . DIRECTORY_SEPARATOR . '.htaccess';
            if ($this->file_exists($htaccess) && !$this->file_move($htaccess, $htaccess . '.bak', true)) {
                return error('upgrade failure: failed to save copy of original .htaccess');
            }
            $this->file_purge();
            $cmd = 'pm-update drupal-%(version)s';
            $args = array('version' => $version);

            $this->_setMaintenance($docroot, true, $current);
            $ret = $this->_exec($docroot, $cmd, $args);
            $this->file_purge();
            $this->_setMaintenance($docroot, false, $current);

            if ($this->file_exists($htaccess . '.bak') && !$this->file_move($htaccess . '.bak', $htaccess, true)
                && ($this->file_purge() || true)
            ) {
                warn("failed to rename backup `%s/.htaccess.bak' to .htaccess", $docroot);
            }

            parent::setInfo($docroot, [
                'version' => $this->get_version($hostname, $path) ?? $version,
                'failed'  => !$ret['success']
            ]);
            $this->fortify($hostname, $path, array_get($this->getOptions($docroot), 'fortify', 'max'));

            if (!$ret['success']) {
                return error('failed to update Drupal: %s', coalesce($ret['stderr'], $ret['stdout']));
            }


            return $ret['success'];
        }

        public function isLocked(string $docroot): bool
        {
            return file_exists($this->domain_fs_path() . $docroot . DIRECTORY_SEPARATOR .
                '.drush-lock-update');
        }

        /**
         * Set Drupal maintenance mode before/after update
         *
         * @param      $docroot
         * @param      $mode
         * @param null $version
         * @return bool
         */
        private function _setMaintenance($docroot, $mode, $version = null)
        {
            if (null === $version) {
                $version = $this->_getVersion($docroot);
            }
            if ($version[0] >= 8) {
                $maintenancecmd = 'sset system.maintenance_mode %(mode)d';
                $cachecmd = 'cr';
            } else {
                $maintenancecmd = 'vset --exact maintenance_mode %(mode)d';
                $cachecmd = 'cache-clear all';
            }

            $ret = $this->_exec($docroot, $maintenancecmd, array('mode' => (int)$mode));
            if (!$ret['success']) {
                warn('failed to set maintenance mode');
            }
            $ret = $this->_exec($docroot, $cachecmd);
            if (!$ret['success']) {
                warn('failed to rebuild cache');
            }

            return true;
        }

        /**
         * Restrict write-access by the app
         *
         * @param string $hostname
         * @param string $path
         * @param string $mode
         * @return bool
         */
        public function fortify(string $hostname, string $path = '', string $mode = 'max'): bool
        {
            return parent::fortify($hostname, $path, $mode);
        }

        /**
         * Update Drupal plugins and themes
         *
         * @param string $hostname domain or subdomain
         * @param string $path     optional path within host
         * @param array  $plugins
         * @return bool
         */
        public function update_plugins(string $hostname, string $path = '', array $plugins = array()): bool
        {
            $docroot = $this->getAppRoot($hostname, $path);
            if (!$docroot) {
                return error('update failed');
            }
            $cmd = 'pm-update --check-disabled --no-core';

            $args = array();
            if ($plugins) {
                for ($i = 0, $n = count($plugins); $i < $n; $i++) {
                    $plugin = $plugins[$i];
                    $version = null;
                    if (isset($plugin['version'])) {
                        $version = $plugin['version'];
                    }
                    if (isset($plugin['name'])) {
                        $plugin = $plugin['name'];
                    }

                    $name = 'p' . $i;
                    $cmd .= ' %(' . $name . ')s';
                    $args[$name] = $plugin . ($version ? '-' . $version : '');
                }
            }

            $ret = $this->_exec($docroot, $cmd, $args);
            if (!$ret['success']) {
                /**
                 * NB: "Command pm-update needs a higher bootstrap level"...
                 * Use an older version of Drush to bring the version up
                 * to use the latest drush
                 */
                return error("plugin update failed: `%s'", coalesce($ret['stderr'], $ret['stdout']));
            }

            return $ret['success'];
        }

        /**
         * Web application supports fortification
         *
         * @param string|null $mode optional mode (min, max)
         * @return bool
         */
        public function has_fortification(string $mode = null): bool
        {
            return parent::has_fortification($mode);
        }

        /**
         * Relax permissions to allow write-access
         *
         * @param string $hostname
         * @param string $path
         * @return bool
         */
        public function unfortify(string $hostname, string $path = ''): bool
        {
            return parent::unfortify($hostname, $path);
        }

        public function _housekeeping()
        {
            if (!file_exists(self::DRUPAL_CLI) || filemtime(self::DRUPAL_CLI) < filemtime(__FILE__)) {
                $url = self::DRUPAL_CLI_URL;
                $res = Util_HTTP::download($url, self::DRUPAL_CLI);
                if (!$res) {
                    return error('failed to install Drupal CLI');
                }
                info('downloaded Drupal CLI');
                chmod(self::DRUPAL_CLI, 0755);
            }

            $local = $this->service_template_path('siteinfo') . '/' . self::DRUPAL_CLI;
            if (!file_exists($local)) {
                copy(self::DRUPAL_CLI, $local);
                chmod($local, 755);
            }

            return true;
        }

        /**
         * Get all available stable versions
         *
         * @return array
         */
        public function get_versions(): array
        {
            $key = 'drupal.verflat';
            $cache = \Cache_Global::spawn();
            if (false !== ($versions = $cache->get($key))) {
                return $versions;
            }
            $tmp = [];
            foreach (array_reverse(self::DRUPAL_MAJORS) as $branch) {
                $branchversions = $this->_getVersions('drupal', $branch);
                $tmp = array_merge_recursive($tmp, $branchversions);
            }

            $versions = array_column(array_filter(array_reverse(array_get($tmp, 'releases.release')), function ($v) {
                return empty($v['version_extra']) && $v['status'] === 'published';
            }), 'version');
            $cache->set($key, $versions, 86400);

            return $versions;
        }

        /**
         * Update WordPress themes
         *
         * @param string $hostname subdomain or domain
         * @param string $path     optional path under hostname
         * @param array  $themes
         * @return bool
         */
        public function update_themes(string $hostname, string $path = '', array $themes = array()): bool
        {
            return false;
        }

        public function next_version(string $version, string $maximalbranch = '99999999.99999999.99999999'): ?string
        {
            return parent::next_version($version, $maximalbranch);
        }

        public function theme_status(string $hostname, string $path = '', string $theme = null)
        {
            return parent::theme_status($hostname, $path, $theme); // TODO: Change the autogenerated stub
        }

        public function install_theme(string $hostname, string $path = '', string $theme, string $version = null): bool
        {
            return parent::install_theme($hostname, $path, $theme, $version);
        }

        private function _getCommand()
        {
            return 'php ' . self::DRUPAL_CLI;
        }
    }