Viewing File: /usr/local/cpanel/whostmgr/docroot/cgi/ncssl/source/src/Service/Certificate/SyncCertificate.php

<?php

namespace App\Service\Certificate;

use App\Repository\CertificateRepository;
use App\Service\CpanelHelper;
use App\Service\Manager\HttpsRedirectManager;
use App\Service\Message\EventCoreNotifierInterface;
use App\Service\NcGatewayApi\NcGatewayApi;
use App\Service\NcPlugin\PluginException;
use App\Service\PluginGateway\NcCoreApi;
use App\Service\State\StateUser;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use App\Entity\Certificate as CertificateEntity;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class SyncCertificate
{
    public function __construct(
        private readonly StateUser $stateUser,
        private readonly EntityManagerInterface $entityManager,
        private readonly CertificateRepository $certificateRepository,
        private readonly NcGatewayApi $ncGatewayApi,
        private readonly NcCoreApi $ncApi,
        private readonly CpanelHelper $cpanelHelper,
        private readonly Install $install,
        private readonly EventCoreNotifierInterface $eventCoreNotifier,
        private readonly LoggerInterface $syncLogger,
        private readonly HttpsRedirectManager $httpsRedirectManager,
        private readonly Certificate $certificateService,
        #[Autowire(param: 'maxCertificateInstallationAttempts')]
        private readonly int $maxCertificateInstallationAttempts,
    ) {
    }

    public function syncForLocalDbUpdate(): void
    {
        $this->syncLogger->info('Start sync for local db update');
        $localDbCertificates = $this->certificateService->getCertificatesForLocalDbUpdate();
        if (!count($localDbCertificates)) {
            return;
        }

        $pendingCertificatesList = array_filter($localDbCertificates, function ($certificate) {
            return !$this->isCertificatePendingInstallation($certificate);
        });
        if (count($pendingCertificatesList)) {
            $pendingUserCertificates = [];
            foreach ($pendingCertificatesList as $certificate) {
                $pendingUserCertificates[$certificate['ncId']] = $certificate;
            }
            $this->syncAndUpdateLocalDb($pendingUserCertificates);
        }
        $this->syncLogger->info('Finish sync for local db update');
    }

    /**
     * @throws Exception
     */
    public function synchronizeAndInstall(): void
    {
        $certificates = $this->certificateRepository->getAllPendingInstallation(
            $this->stateUser->getUser(),
            $this->maxCertificateInstallationAttempts
        );

        if (!$certificates) {
            return;
        }

        $pendingInstallationCertificates = $this->filterPendingInstallationByNamecheap($certificates);

        if (!$pendingInstallationCertificates) {
            return;
        }

        $syncedCerts = $this->syncAndInstall($pendingInstallationCertificates);

        $this->switchOnHttps($syncedCerts, $certificates);
    }

    /**
     * @return void
     * @throws PluginException
     */
    public function syncAll(): void
    {
        $this->syncLogger->info('Start sync all certificates');
        $userLocalDbCertificates = $this->certificateService->getCertificatesForLocalDbUpdate();

        if (count($userLocalDbCertificates) === 0) {
            return;
        }

        $userCertificates = [];
        foreach ($userLocalDbCertificates as $certificate) {
            $userCertificates[$certificate['ncId']] = $certificate;
        }

        $certificatesForInstallationIds = $this->updateNcStatus($userCertificates);
        $certificatesForInstallation = [];
        foreach ($certificatesForInstallationIds as $certificateId) {
            $certificatesForInstallation[] = $this->certificateRepository->findOneBy(['id' => $certificateId]);
        }

        $syncedCerts = $this->syncAndInstall($certificatesForInstallation);

        try {
            $this->switchOnHttps($syncedCerts, $certificatesForInstallation);
        } catch (Exception $e) {
            $this->syncLogger->error('Can\'t switch on https', $e->getTrace());
        }
        $this->syncLogger->info('Finish sync all certificates');
    }

    /**
     * @throws Exception
     */
    private function filterPendingInstallationByNamecheap(array $certificates): array
    {
        $namecheapCertificates = $this->ncGatewayApi->getSslList();

        return array_filter($certificates,  function (CertificateEntity $cPanelCertificate) use ($namecheapCertificates)  {
            $namecheapCertificate = $this->findByKeyInArray((string) $cPanelCertificate->getNcId(), 'id', $namecheapCertificates);

            if ($namecheapCertificate && $this->isCertificateActiveInNC($cPanelCertificate, $namecheapCertificate)) {
                return true;
            }

            $cPanelCertificate->increaseFailedInstallationAttempts();
            $this->entityManager->flush();

            return false;
        }, ARRAY_FILTER_USE_BOTH);
    }

    private function isCertificateActiveInNC(CertificateEntity $certificate, array $namecheapCertificate): bool
    {
        return (($certificate->isInProgress() && $namecheapCertificate['status'] === CertificateEntity::STATUS_ACTIVE) ||
            ($certificate->isActive() && empty($certificate->getCpanelId())));
    }

    /**
     *
     * Example:
     *  $array = [
     *      ['id' => 1, 'name' => 'Melony'],
     *      ['id' => 2, 'name' => 'Henry']
     *  ]
     *
     *   findByKeyInArray('Melony', 'name', $array);
     *
     *  Result: ['id' => 1, 'name' => 'Melony'
     *
     * @param string|int $searchValue
     * @param string|int $searchKey
     * @param array $array
     * @return array|null
     * @throws InvalidArgumentException
     */
    private function findByKeyInArray(string|int $searchValue, string|int $searchKey, array $array): ?array
    {
        if (!$searchKey) {
            throw new \InvalidArgumentException('Please provide searchKey parameter');
        }

        $filtered = array_filter($array, static function($value) use ($searchValue, $searchKey){
            return $value[$searchKey] === $searchValue;
        },ARRAY_FILTER_USE_BOTH);

        return count($filtered) ? array_shift($filtered) : null;
    }

    private function isCertificatePendingInstallation(array $certificate): bool
    {
        return ($certificate['status'] == CertificateEntity::STATUS_INPROGRESS)
            || ($certificate['status'] == CertificateEntity::STATUS_ACTIVE && empty($certificate['cpanel_id']));
    }

    /**
     * @param CertificateEntity[] $certificates
     * @return array
     * @throws PluginException
     */
    private function syncAndInstall(array $certificates): array
    {
        $syncedCerts = [];
        $domainList = $this->cpanelHelper->getDomainsList();

        $filteredCertificates = array_filter($certificates, function($certificate) use ($domainList) {
            $certificateHost = $certificate->getHost();

            if ($certificateHost === null || !in_array($certificateHost, $domainList, true)) {
                $this->syncLogger->warning("The certificate cannot be installed because the certificate's domain is not present on the hosting server.", context: [
                    'certificate' => [
                        'ncId' => $certificate->getNcId(),
                        'host' => $certificateHost,
                        'ncUser' => $certificate->getNcUser(),
                    ],
                    'userDomainList' => $domainList,
                ]);
                $certificate->increaseFailedInstallationAttempts();
                $this->entityManager->flush();

                return false;
            }

            return true;
        });

        foreach ($filteredCertificates as $certificate) {
            try {
                $namecheapCertificate = $this->ncGatewayApi->getInfo((string) $certificate->getNcId(), true, $certificate->getUser()->getNcLogin());
                if ($this->isCertificateActiveInNC($certificate, $namecheapCertificate)) {
                    try {
                        $this->install->install($certificate->getId(), $namecheapCertificate['cert_body'], $namecheapCertificate['ca_certs']);
                    }  catch (\Throwable $throwable) {
                        $certificate->increaseFailedInstallationAttempts();
                        $this->syncLogger->error("The certificate installation failed.", context: [
                            'error' => $throwable->getMessage(),
                            'certificate' => [
                                'ncId' => $certificate->getNcId(),
                                'host' => $certificate->getHost(),
                                'ncUser' => $certificate->getNcUser(),
                                'status' => $certificate->getStatus(),
                                'ncStatus' => $certificate->getNcStatus(),
                                'failedInstallationAttempts' => $certificate->getFailedInstallationAttempts(),
                            ]
                        ]);

                        $this->entityManager->flush();
                        continue;
                    }

                    $syncedCerts[] = $certificate->getNcId();

                    $certificate
                        ->setNcStatus($namecheapCertificate['status'])
                        ->setExpires($namecheapCertificate['expires'])
                        ->setInstalledAt((new \DateTime())->getTimestamp());

                    $this->entityManager->flush();

                    $this->eventCoreNotifier->sendEvent(EventCoreNotifierInterface::SUCCESS_INSTALLATION_EVENT_CODE, $certificate->getNcId(), $certificate->getHost());
                } else if ($this->isCertificateReadyToInstall($certificate)) {
                    $this->syncLogger->error('Can\'t install certificate. Wrong Status.', ['certificateNcId' => $certificate->getNcId()]);
                }
            } catch (\Exception $exception) {
                $this->syncLogger->error('Can\'t install certificate', [
                    'certificateNcId' => $certificate->getNcId(),
                    'errorMessage' => $exception->getMessage(),
                    'errorCode' => $exception->getCode(),
                ]);
            }
        }

        return $syncedCerts;
    }

    private function isCertificateReadyToInstall(CertificateEntity $certificate): bool
    {
        return $certificate->isActive() && empty($certificate->getCpanelId()) && empty($certificate->getPrivatekeyId());
    }

    /**
     * @param CertificateEntity[] $certificates
     * @param $syncedIds
     * @throws \JsonException
     */
    private function switchOnHttps(array $syncedIds, array $certificates = []): void
    {
        foreach ($certificates as $certificate) {
            if (!in_array($certificate->getNcId(), $syncedIds, true)) {
                continue;
            }

            if (!$certificate->isAutoRedirect()) {
                $this->httpsRedirectManager->toggleHttpRedirect($certificate->getHost(), false);
                continue;
            }

            $result = $this->httpsRedirectManager->toggleHttpRedirect($certificate->getHost(), true);
            if ($result['status'] !== HttpsRedirectManager::STATUS_OK) {
                continue;
            }

            //@TODO Success message
        }
    }

    /**
     * @param array $certificates
     *
     * @return void
     */
    private function syncAndUpdateLocalDb(array $certificates): void
    {
        try {
            $nc_certs = $this->ncApi->getSslList();
        } catch (Exception $e) {
            $this->syncLogger->error('Can\'t get certificates from Namecheap', $e->getTrace());
            return;
        }

        // sync cetificates from db with data from namecheap
        // certs in statuses ['REPLACED', 'CANCELLED', 'REVOKED'] should be deleted from db
        array_walk($certificates, function ($cPanelCertificate, $cPanelCertificateId) use ($nc_certs) {
            $namecheapCertificate = $this->findMatchingNamecheapCertificate($nc_certs, $cPanelCertificateId);
            if ($namecheapCertificate !== null) {
                if ($namecheapCertificate['status'] == CertificateEntity::NCSTATUS_NEWPURCHASE && $cPanelCertificate['status'] != CertificateEntity::STATUS_ACTIVE) {
                    $this->syncLogger->info('Delete certificate', $cPanelCertificate);
                    $this->certificateService->deleteById($cPanelCertificate['id']);
                } elseif (in_array(
                    $namecheapCertificate['status'],
                    [CertificateEntity::NCSTATUS_REPLACED, CertificateEntity::NCSTATUS_CANCELLED, CertificateEntity::NCSTATUS_REVOKED], true)
                ) {
                    $this->syncLogger->info('Delete certificate', $cPanelCertificate);
                    $this->certificateService->deleteById($cPanelCertificate['id']);
                } else {
                    if ($cPanelCertificate['status'] !== $namecheapCertificate['status']) {
                        $this->syncLogger->info('Update NC status of certificate', ['cpanelCertificate' => $cPanelCertificate, 'ncStatus' => $namecheapCertificate['status']]);
                        $this->certificateService->updateNcStatus($cPanelCertificate['id'], $namecheapCertificate['status']);
                    }

                }
            }
        });
    }

    /**
     * @param array $namecheapCertificates
     * @param $certificateId
     *
     * @return mixed|null
     */
    private function findMatchingNamecheapCertificate(array $namecheapCertificates, $certificateId): mixed
    {
        $matchingNamecheapCertificates = array_filter($namecheapCertificates, function ($namecheapCertificate) use ($certificateId) {
            return intval($namecheapCertificate['id']) === $certificateId;
        });

        return array_shift($matchingNamecheapCertificates);
    }

    /**
     * @param array $userCertificates
     *
     * @return array
     */
    private function updateNcStatus(array $userCertificates): array
    {
        $certsForInstallationIds = [];
        try {
            $ncCertificates = $this->ncApi->getSslList();
        } catch (Exception $e) {
            $this->syncLogger->error('Can\'t get certificates from Namecheap', $e->getTrace());
            return [];
        }


        foreach ($ncCertificates as $ncCertificate) {
           if (!array_key_exists($ncCertificate['id'], $userCertificates)) {
               continue;
           }

           $userCertificate = $userCertificates[$ncCertificate['id']];

           if ($userCertificate['status'] == CertificateEntity::STATUS_INPROGRESS &&
               $ncCertificate['status'] == CertificateEntity::NCSTATUS_ACTIVE) {
               $certsForInstallationIds[] = $userCertificate['id'];
           } elseif ($userCertificate['status'] == CertificateEntity::STATUS_ACTIVE &&
               empty($userCertificate['cpanelId'])) {
               $certsForInstallationIds[] = $userCertificate['id'];
           } elseif ($userCertificate['status'] != CertificateEntity::STATUS_ACTIVE &&
               $ncCertificate['status'] == CertificateEntity::NCSTATUS_NEWPURCHASE) {
               $this->syncLogger->info('Delete certificate', $userCertificate);
               $this->certificateService->deleteById($userCertificate['id']);
           } elseif (in_array($ncCertificate['status'], [CertificateEntity::NCSTATUS_REPLACED, CertificateEntity::NCSTATUS_CANCELLED, CertificateEntity::NCSTATUS_REVOKED], true)) {
               $this->syncLogger->info('Delete certificate', $userCertificate);
               $this->certificateService->deleteById($userCertificate['id']);
               try {
                   $this->cpanelHelper->deleteCertificate($userCertificate['cpanelId']);
               } catch (PluginException $e) {
                   $this->syncLogger->error('Can\'t delete certificate from cPanel', $e->getTrace());
               }
           } else {
               if ($userCertificate['status'] !== $ncCertificate['status']) {
                   $this->syncLogger->info('Update NC status of certificate', ['userCertificate' => $userCertificate, 'ncStatus' => $ncCertificate['status']]);
                   $this->certificateService->updateNcStatus($userCertificate['id'], $ncCertificate['status']);
               }

           }
        }

        return $certsForInstallationIds;
    }
}
Back to Directory File Manager