Viewing File: /usr/local/cpanel/base/frontend/jupiter/ls_web_cache_manager/core/Lsc/UserWPInstallStorage.php

<?php

/** *********************************************
 * LiteSpeed Web Cache Management Plugin for cPanel
 *
 * @author    Michael Alegre
 * @copyright 2018-2025 LiteSpeed Technologies, Inc.
 * *******************************************
 */

namespace LsUserPanel\Lsc;

use LsUserPanel\CPanelWrapper;
use LsUserPanel\Ls_WebCacheMgr_Util as Util;
use LsUserPanel\Lsc\Context\UserContext;
use LsUserPanel\PluginSettings;
use LsUserPanel\QuicCloudApiUtil;

/**
 * Map to data file.
 */
class UserWPInstallStorage
{

    /**
     * @since 2.2
     * @var   string
     */
    const CMD_QUICCLOUD_CLEAR_CACHE = 'quicCloudClearCache';

    /**
     * @since 2.1
     * @var   string
     */
    const CMD_QUICCLOUD_UPLOAD_SSL_CERT = 'quicCloudUploadSslCert';

    /**
     * @var string
     */
    const DATA_VERSION = '1.0';

    /**
     * @var int
     */
    const ERR_NOT_EXIST = 1;

    /**
     * @var int
     */
    const ERR_CORRUPTED = 2;

    /**
     * @var int
     */
    const ERR_VERSION_HIGH = 3;

    /**
     * @var int
     */
    const ERR_VERSION_LOW = 4;

    /**
     * @var string
     */
    private $dataFile;

    /**
     * @var null|UserWPInstall[]  Key is WordPress installation path.
     */
    private $wpInstalls = null;

    /**
     * @var int
     */
    private $error;

    /**
     * @var UserWPInstall[]
     */
    private $workingQueue = array();

    /**
     *
     * @param string $dataFile
     *
     * @throws UserLSCMException  Thrown indirectly by $this->init() call.
     */
    public function __construct( $dataFile )
    {
        $this->dataFile = $dataFile;
        $this->error    = $this->init();
    }

    /**
     *
     * @return int
     *
     * @throws UserLSCMException  Thrown indirectly by $this->checkDataFileVer()
     *     call.
     */
    private function init()
    {
        if ( !file_exists($this->dataFile) ) {
            return self::ERR_NOT_EXIST;
        }

        $content = file_get_contents($this->dataFile);

        if ( ($data = json_decode($content, true)) === null ) {
            /**
             * Data file may be in old serialized format. Try unserializing.
             */
            $data = unserialize($content);
        }

        if ( $data === false || !is_array($data) || !isset($data['__VER__']) ) {
            return self::ERR_CORRUPTED;
        }

        if ( ($err = $this->checkDataFileVer($data['__VER__'])) ) {
            return $err;
        }

        unset($data['__VER__']);

        $this->wpInstalls = array();

        foreach ( $data as $path => $idata ) {
            $i = new UserWPInstall($path);
            $i->initData($idata);
            $this->wpInstalls[$path] = $i;
        }

        return 0;
    }

    /**
     *
     * @return int
     */
    public function getError()
    {
        return $this->error;
    }

    /**
     *
     * @param bool $nonFatalOnly
     *
     * @return int
     */
    public function getCount( $nonFatalOnly = false )
    {
        $count = 0;

        if ( $this->wpInstalls != null ) {

            if ( !$nonFatalOnly ) {
                return count($this->wpInstalls);
            }

            foreach ( $this->wpInstalls as $install ) {

                if ( !$install->hasFatalError() ) {
                    $count++;
                }
            }
        }

        return $count;
    }

    /**
     *
     * @return null|UserWPInstall[]
     */
    public function getWPInstalls()
    {
        return $this->wpInstalls;
    }

    /**
     * Get all known WPInstall paths.
     *
     * @return string[]
     */
    public function getPaths()
    {
        if ( $this->wpInstalls == null ) {
            return array();
        }

        return array_keys($this->wpInstalls);
    }

    /**
     *
     * @param string $path
     *
     * @return UserWPInstall|null
     */
    public function getWPInstall( $path )
    {
        if ( ($realPath = realpath($path)) !== false ) {
            $index = $realPath;
        }
        else {
            $index = $path;
        }

        if ( isset($this->wpInstalls[$index]) ) {
            return $this->wpInstalls[$index];
        }

        return null;
    }

    /**
     *
     * @param UserWPInstall $wpInstall
     */
    public function addWPInstall( UserWPInstall $wpInstall )
    {
        $this->wpInstalls[$wpInstall->getPath()] = $wpInstall;
    }

    /**
     *
     * @throws UserLSCMException  Thrown indirectly by $this->log() call.
     */
    public function syncToDisk()
    {
        $data = array( '__VER__' => self::DATA_VERSION );

        if ( !empty($this->wpInstalls) ) {

            foreach ( $this->wpInstalls as $path => $install ) {

                if ( !$install->shouldRemove() ) {
                    $data[$path] = $install->getData();
                }
            }

            ksort($data);
        }

        file_put_contents($this->dataFile, json_encode($data), LOCK_EX);
        chmod($this->dataFile, 0600);

        $this->log("Data file saved $this->dataFile", UserLogger::L_DEBUG);
    }

    /**
     * Updates data file to the latest format if possible/needed.
     *
     * @param string $dataFileVer
     *
     * @return int
     *
     * @throws UserLSCMException  Thrown indirectly by UserLogger::logMsg()
     *     call.
     * @throws UserLSCMException  Thrown indirectly by $this->upgradeDataFile()
     *      call.
     */
    private function checkDataFileVer( $dataFileVer )
    {
        $res = Util::betterVersionCompare($dataFileVer, self::DATA_VERSION);

        if ( $res == 1 ) {
            UserLogger::logMsg(
                'Data file version is higher than expected and cannot be used.',
                UserLogger::L_INFO
            );
            return self::ERR_VERSION_HIGH;
        }

        if ( $res == -1 && !$this->upgradeDataFile($dataFileVer) ) {
            return self::ERR_VERSION_LOW;
        }

        return 0;
    }

    /**
     *
     * @param string $dataFileVersion
     *
     * @return bool
     *
     * @throws UserLSCMException  Thrown indirectly by UserLogger::logMsg()
     *     call.
     * @throws UserLSCMException  Thrown indirectly by UserUtil::createBackup()
     *     call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::logMsg()
     *     call.
     */
    private function upgradeDataFile( $dataFileVersion )
    {
        UserLogger::logMsg(
            'Old data file version detected. Attempting to update...',
            UserLogger::L_INFO
        );

        /**
         * Currently no versions are upgradeable to 1.5
         */
        $updatableVersions = array();

        if ( !in_array($dataFileVersion, $updatableVersions)
                || !UserUtil::createBackup($this->dataFile) ) {

            UserLogger::logMsg(
                'Data file could not be updated to version '
                    . self::DATA_VERSION,
                UserLogger::L_ERROR
            );

            return false;
        }

        /**
         * Upgrade funcs will be called here.
         */

        return true;
    }

    /**
     *
     * @param string   $action
     * @param string   $path
     * @param string[] $extraArgs
     *
     * @throws UserLSCMException  Thrown indirectly by
     *     $wpInstall->hasValidPath() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     $wpInstall->addUserFlagFile() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     $wpInstall->hasValidPath() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     $this->quicCloudUploadSslCert() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     $this->quicCloudTryToClearCache() call.
     * @throws UserLSCMException  Thrown indirectly by UserUserCommand::issue()
     *     call.
     */
    private function doWPInstallAction( $action, $path, array $extraArgs )
    {
        if ( ($wpInstall = $this->getWPInstall($path)) == null ) {
            $wpInstall = new UserWPInstall($path);
            $this->addWPInstall($wpInstall);
        }

        switch ($action) {

            case 'flag':

                if ( !$wpInstall->hasValidPath() ) {
                    return;
                }

                if ( $wpInstall->addUserFlagFile() ) {
                    $wpInstall->setCmdStatusAndMsg(
                        UserUserCommand::EXIT_SUCC,
                        _('Flag file set')
                    );
                }
                else {
                    $wpInstall->setCmdStatusAndMsg(
                        UserUserCommand::EXIT_FAIL,
                        _('Could not create flag file')
                    );
                }

                $this->workingQueue[] = $wpInstall;
                return;

            case 'unflag':

                if ( !$wpInstall->hasValidPath() ) {
                    return;
                }

                $wpInstall->removeFlagFile();

                $wpInstall->setCmdStatusAndMsg(
                    UserUserCommand::EXIT_SUCC,
                    _('Flag file unset')
                );

                $this->workingQueue[] = $wpInstall;
                return;

            case UserUserCommand::CMD_DIRECT_ENABLE:
            case UserUserCommand::CMD_DISABLE:

                if ( $wpInstall->hasFatalError() ) {
                    $wpInstall->setCmdStatusAndMsg(
                        UserUserCommand::EXIT_FAIL,
                        _(
                            'Install skipped due to Error status. Please '
                                . 'Refresh Status before trying again.'
                        )
                    );

                    $this->workingQueue[] = $wpInstall;
                    return;
                }

                break;

            case UserWPInstallStorage::CMD_QUICCLOUD_UPLOAD_SSL_CERT:
                $this->quicCloudUploadSslCert($wpInstall);
                return;

            case UserWPInstallStorage::CMD_QUICCLOUD_CLEAR_CACHE:
                $this->quicCloudTryToClearCache($wpInstall);
                return;

            //no default
        }

        if ( UserUserCommand::issue($action, $wpInstall, $extraArgs) ) {
            $this->workingQueue[] = $wpInstall;
        }
    }

    /**
     *
     * @param string   $action
     * @param string[] $list
     * @param string[] $extraArgs
     *
     * @throws UserLSCMException  Thrown indirectly by $this->log() call.
     * @throws UserLSCMException  Thrown indirectly by $this->scan() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     $this->doWPInstallAction() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::logMsg()
     *     call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg()
     *     call.
     * @throws UserLSCMException  Thrown indirectly by $this->syncToDisk() call.
     */
    public function doAction( $action, array $list, array $extraArgs = array() )
    {
        $this->log(
            "doAction $action for " . count($list) . " items",
            UserLogger::L_VERBOSE
        );

        $newWpInstalls = array();

        foreach ( $list as $path ) {

            if ( $action == 'scan' ) {
                $this->scan($path, $newWpInstalls, true);
            }
            else {
                $this->doWPInstallAction($action, $path, $extraArgs);
            }
        }

        if ( $action == 'scan' ) {

            if ( $this->wpInstalls !== null ) {
                /**
                 * Add an error message for any remaining $this->wpInstalls[]
                 * installations not re-discovered during scan.
                 */
                foreach ( $this->wpInstalls as $key => $wpInstall ) {
                    UserLogger::logMsg(
                        "$key - Installation could not be found during Scan "
                            . 'and has been removed from the Cache Manager '
                            . 'list.',
                        UserLogger::L_NOTICE
                    );
                    UserLogger::addUiMsg(
                        "$key - "
                            . _(
                                'Installation could not be found during Scan '
                                    . 'and has been removed from the '
                                    . 'Cache Manager list.'
                            ),
                        UserLogger::UI_ERR
                    );
                }
            }

            $this->wpInstalls = $newWpInstalls;

            /**
             * Explicitly clear any data file errors after scanning as initial
             * data file load and scan operation happen in the same
             * process/page load.
             */
            $this->error = 0;
        }

        $this->syncToDisk();
    }

    /**
     *
     * @param string          $docroot
     * @param UserWPInstall[] $newWpInstalls
     * @param bool            $forceRefresh
     *
     * @return void
     *
     * @throws UserLSCMException  Thrown indirectly by
     *     UserContext::getScanDepth() call.
     * @throws UserLSCMException  Thrown indirectly by $this->log() call.
     * @throws UserLSCMException  Thrown indirectly by $this->log() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     $newWpInstalls[$wp_path]->refreshStatus() call.
     */
    private function scan(
              $docroot,
        array &$newWpInstalls,
              $forceRefresh = false )
    {
        $cpanel = CPanelWrapper::getCpanelObj();

        $result = $cpanel->uapi(
            'lsws',
            'getScanDirs',
            array(
                'docroot' => $docroot,
                'depth'   => UserContext::getScanDepth()
            )
        );

        $installationsFound = preg_match_all(
            "|$docroot(.*)(?=/wp-admin)|",
            $result['cpanelresult']['result']['data']['scanData'],
            $matches
        );

        /**
         * Example:
         * /home/user/public_html/wordpress/wp-admin
         * /home/user/public_html/blog/wp-admin
         * /home/user/public_html/wp/wp-admin
         */
        if ( !$installationsFound ) {
            return;
        }

        foreach ( $matches[1] as $path ) {
            $wp_path = $docroot . $path;
            $refresh = $forceRefresh;

            if ( !isset($this->wpInstalls[$wp_path]) ) {
                $newWpInstalls[$wp_path] = new UserWPInstall($wp_path);
                $refresh = true;
                $this->log(
                    "New Installation Found: $wp_path",
                    UserLogger::L_INFO
                );
            }
            else {
                $newWpInstalls[$wp_path] = $this->wpInstalls[$wp_path];
                unset($this->wpInstalls[$wp_path]);
                $this->log(
                    "Installation already found: $wp_path",
                    UserLogger::L_DEBUG
                );
            }

            if ( $refresh ) {
                $newWpInstalls[$wp_path]->refreshStatus();
                $this->workingQueue[] = $newWpInstalls[$wp_path];
            }
        }
    }

    /**
     *
     * @since 2.2
     *
     * @param UserWPInstall $wpInstall
     *
     * @throws UserLSCMException  Thrown indirectly by $this->log() call.
     */
    private function quicCloudTryToClearCache( UserWPInstall $wpInstall )
    {
        $siteUrl = $wpInstall->getData(UserWPInstall::FLD_SITEURL);

        if ( ! preg_match('#^https?://#', $siteUrl) ) {
            $siteUrl = "http://$siteUrl";
        }

        $path          = $wpInstall->getPath();
        $purgeFileName = Util::createRandomizedQcPurgeFile($path);

        $this->log(
            "Clear all QUIC.cloud cache call for site URL $siteUrl returned: "
                . var_export(
                    UserUtil::makeUrlRequest("$siteUrl/$purgeFileName", 'GET'),
                    true
                ),
            UserLogger::L_DEBUG
        );

        unlink("$path/$purgeFileName");
    }

    /**
     *
     * @since 2.1
     *
     * @param UserWPInstall $wpInstall
     *
     * @throws UserLSCMException  Thrown indirectly by
     *     $wpInstall->refreshStatus() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     PluginSettings::getSetting() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg() call.
     * @throws UserLSCMException  Thrown indirectly by
     *     UserUserCommand::getValueFromWordPress() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg() call.
     * @throws UserLSCMException  Thrown indirectly by UserLogger::addUiMsg() call.
     */
    private function quicCloudUploadSslCert( UserWPInstall $wpInstall )
    {
        if ( ! $wpInstall->isLscwpEnabled() ) {
            $wpInstall->refreshStatus();

            if ( ! $wpInstall->isLscwpEnabled() ) {
                UserLogger::addUiMsg(
                    "{$wpInstall->getPath()} - "
                        . sprintf(
                            _(
                                '%1$s for %2$s must be installed and enabled '
                                    . 'before attempting to upload SSL '
                                    . 'certificate to %3$s.'
                            ),
                            'LiteSpeed Cache Plugin',
                            'WordPress',
                            'QUIC.cloud'
                        ),
                    UserLogger::UI_ERR
                );
                return;
            }
        }

        $allowEcCertGen = (
            PluginSettings::getSetting(PluginSettings::FLD_GENERATE_EC_CERTS)
            ==
            PluginSettings::SETTING_ON_PLUS_AUTO
        );

        $domain = $wpInstall->getData(UserWPInstall::FLD_SERVERNAME);

        $sslData = Util::getDomainSslData($domain, $allowEcCertGen);

        $certFingerprint = $sslData['retCert'];
        $key             = $sslData['retKey'];

        if ( $certFingerprint == '' || $key == '' ) {
            UserLogger::addUiMsg(
                "{$wpInstall->getPath()} - "
                    . _(
                        'Unable to get certificate/private key information for '
                            . 'this domain.'
                    ),
                UserLogger::UI_ERR
            );
            return;
        }

        $apiKey = UserUserCommand::getValueFromWordPress(
            UserUserCommand::CMD_GET_QUICCLOUD_API_KEY,
            $wpInstall
        );

        if ( $apiKey == '' ) {
            UserLogger::addUiMsg(
                "{$wpInstall->getPath()} - "
                    . sprintf(
                        _(
                            'Unable to retrieve %1$s API key. Please visit '
                                . '%2$s in the %3$s Dashboard and confirm that '
                                . 'the API key has already been generated.'
                        ),
                        'QUIC.cloud',
                        '"LiteSPeed Cache -> General"',
                        'WordPress'
                    ),
                UserLogger::UI_ERR
            );
            return;
        }

        $siteUrl = $wpInstall->getData(UserWPInstall::FLD_SITEURL);

        if ( ! preg_match('#^https?://#', $siteUrl) ) {
            $siteUrl = "http://$siteUrl";
        }

        try {
            $qcDomainList = QuicCloudApiUtil::callInfo($apiKey, $siteUrl);
        }
        catch ( UserLSCMException $e ) {
            UserLogger::addUiMsg(
                "{$wpInstall->getPath()} - {$e->getMessage()}",
                UserLogger::UI_ERR
            );
            return;
        }

        $certFullyCoversSite = empty(
            array_diff(
                $qcDomainList,
                Util::getCertificateAltNames($certFingerprint)
            )
        );

        if ( !$certFullyCoversSite ) {
            UserLogger::addUiMsg(
                sprintf(
                    _(
                        'Detected SSL certificate does not contain all %s '
                            . 'configured domain and alias entries for this '
                            . 'site.'
                    ),
                    'QUIC.cloud'
                ),
                UserLogger::UI_ERR
            );
            return;
        }

        try {
            UserLogger::addUiMsg(
                "{$wpInstall->getPath()} - "
                    . QuicCloudApiUtil::uploadCert(
                        $apiKey,
                        $domain,
                        $siteUrl,
                        $certFingerprint,
                        $key
                    ),
                UserLogger::UI_SUCC
            );
        }
        catch ( UserLSCMException $e ) {
            UserLogger::addUiMsg(
                "{$wpInstall->getPath()} - {$e->getMessage()}",
                UserLogger::UI_ERR
            );
            return;
        }
    }

    /**
     * Get all WPInstall command messages as a key=>value array.
     *
     * @return string[][]
     */
    public function getAllCmdMsgs()
    {
        $succ = $fail = $err = array();

        foreach ( $this->workingQueue as $wpInstall ) {
            $cmdStatus = $wpInstall->getCmdStatus();

            switch (true) {

                case $cmdStatus & UserUserCommand::EXIT_SUCC:
                    $msgType = &$succ;
                    break;

                case $cmdStatus & UserUserCommand::EXIT_FAIL:
                    $msgType = &$fail;
                    break;

                case $cmdStatus & UserUserCommand::EXIT_ERROR:
                    $msgType = &$err;
                    break;

                default:
                    continue 2;
            }

            if ( ($msg = $wpInstall->getCmdMsg()) ) {
                $msgType[] = "{$wpInstall->getPath()} - $msg";
            }
        }

        return array( 'succ' => $succ, 'fail' => $fail, 'err' => $err );
    }

    /**
     *
     * @param string $msg
     * @param int    $level
     *
     * @throws UserLSCMException  Thrown indirectly by
     *     UserLogger::logMsg() call.
     */
    protected function log( $msg, $level )
    {
        UserLogger::logMsg("WPInstallStorage - $msg", $level);
    }

}
Back to Directory File Manager