📁
SKYSHELL MANAGER
PHP v8.2.31
Create
Create
Path:
root
/
home
/
thevaxnx
/
nativize.com
/
staging
/
wp-includes
/
js
/
tinymce
/
themes
/
Name
Size
Perm
Actions
📁
inlite
-
0755
🗑️
🏷️
🔒
📁
modern
-
0755
🗑️
🏷️
🔒
📄
wp-links-opml.php
6.83 KB
0444
🗑️
🏷️
⬇️
✏️
🔒
Edit: SyncCertificate.php
<?php namespace App\Service\Certificate; use App\Exception\CertificateNotFoundException; 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'); } /** * cron entry point: reconciles ncStatus for every user certificate (Expired/Paused, Replaced, * Cancelled, Revoked), installs pending certificates respecting the attempt cap, and backfills * missing subscription_expires. Sources the SSL list from the Gateway API (server token) so it * works for every user without a per-user OAuth token. * * The Gateway SSL list is fetched only when there is a reason to: the user has certificates * pending installation, or certificates that are locally ACTIVE but already expired (which a * fresh sync should reconcile). Otherwise getSslList() is skipped to avoid wasteful API calls. * Sync candidates are stamped even when the fetch fails, so a user whose fetch persistently * errors retries once per guard window instead of on every cron run. Candidates the fetched * list no longer contains are flagged as missing and permanently excluded from candidacy; * the flag is cleared when a certificate reappears in the list (applyMissingFromSslListChanges). * The subscription_expires backfill does not depend on the SSL list and always runs. * * @throws Exception|\Throwable */ public function synchronizeCertificates(): void { $user = $this->stateUser->getUser(); $username = $user->getName(); // Decide whether the Gateway SSL list is needed this run. $pendingInstallationCertificates = $this->certificateRepository->getAllPendingInstallation( $user, $this->maxCertificateInstallationAttempts ); $potentialNcIdsForSync = $this->certificateRepository->getPotentialCertificatesForSync($user); if (!empty($pendingInstallationCertificates) || !empty($potentialNcIdsForSync)) { try { $ncCertificates = $this->ncGatewayApi->getSslList(); } catch (\Throwable $throwable) { $this->syncLogger->error('Can\'t get certificates from Namecheap (Gateway)', [ 'errorMessage' => $throwable->getMessage(), 'username' => $username ]); // Stamp the candidates on failure too, otherwise a persistently failing user // re-triggers getSslList() on every cron run instead of once per guard window. if (!empty($potentialNcIdsForSync)) { try { $this->certificateRepository->markSyncedByCron( $potentialNcIdsForSync, (new \DateTime())->getTimestamp() ); } catch (\Throwable $stampError) { // Swallow so the original gateway error stays the propagated exception $this->syncLogger->warning('Failed to stamp sync candidates after gateway error', [ 'errorMessage' => $stampError->getMessage(), 'username' => $username, ]); } } throw $throwable; } // 1. Update certificates' statuses, remove certificates if they have terminal status $userCertificates = []; foreach ($this->certificateService->getCertificatesForLocalDbUpdate() as $certificate) { $userCertificates[$certificate['ncId']] = $certificate; } $this->applyNcStatusChanges($ncCertificates, $userCertificates); $this->applyMissingFromSslListChanges($ncCertificates, $userCertificates, $potentialNcIdsForSync); if (!empty($potentialNcIdsForSync)) { $this->certificateRepository->markSyncedByCron( $potentialNcIdsForSync, (new \DateTime())->getTimestamp() ); } // 2. Install pending certificates respecting the max attempts cap if (!empty($pendingInstallationCertificates)) { $filteredCertificates = $this->filterPendingInstallationByNamecheap($pendingInstallationCertificates, $ncCertificates); if ($filteredCertificates) { try { $syncedCerts = $this->installCertificates($filteredCertificates); $this->switchOnHttps($syncedCerts, $pendingInstallationCertificates); } catch (Exception $e) { $this->syncLogger->error('Can\'t install certificates / switch on https', $e->getTrace()); } } } } // 3. Backfill missing subscription_expires $this->syncMissingSubscriptionExpires($username); } /** * Refresh ncStatus and subscription_expires from NC for each candidate before auto-reissue. * Deletes candidates whose NC status is terminal so the next call to * Certificate::getCertificatesReadyForReissue() re-selects an accurate list. * * @param CertificateEntity[] $candidates */ public function syncForReissueCandidates(array $candidates): void { foreach ($candidates as $candidate) { $this->stateUser->setUser($candidate->getUser()); try { $info = $this->ncGatewayApi->getInfo( (string) $candidate->getNcId(), true, $candidate->getUser()->getNcLogin(), ); } catch (CertificateNotFoundException $e) { $this->certificateService->markIneligible($candidate); $this->syncLogger->info('Failed to sync for reissue. Certificate is not found for the user account, marked as INELIGIBLE', [ 'ncId' => $candidate->getNcId(), 'ncUser' => $candidate->getUser()->getNcLogin(), ]); continue; } catch (\Throwable $e) { $this->syncLogger->error('Failed to fetch NC info before auto-reissue', [ 'ncId' => $candidate->getNcId(), 'ncUser' => $candidate->getNcUser(), 'errorMessage' => $e->getMessage(), ]); continue; } $deleted = $this->applyNcStatusChange($candidate, $info['status'] ?? ''); if ($deleted) { continue; } if (!empty($info['subscription_expires'])) { $candidate->setSubscriptionExpires((int) $info['subscription_expires']); } } $this->entityManager->flush(); } public function syncMissingSubscriptionExpires(string $username): void { $certificates = $this->certificateRepository->getCertificatesMissingSubscriptionExpires($username); $updatedCertificates = 0; foreach ($certificates as $certificate) { $this->certificateService->updateSubscriptionExpires($certificate); $this->entityManager->flush(); $updatedCertificates += 1; $this->syncLogger->info('Subscription expiry synced', [ 'ncId' => $certificate->getNcId(), 'ncUser' => $certificate->getNcUser(), 'subscriptionExpires' => $certificate->getSubscriptionExpires(), ]); } if ($updatedCertificates > 0) { $this->syncLogger->info('Finish syncing missing subscription_expires', [ 'username' => $username, 'updatedCertificates' => $updatedCertificates]); } } /** * @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->installCertificates($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): array { 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 installCertificates(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']) ->setSubscriptionExpires($namecheapCertificate['subscription_expires']) ->setInstalledAt((new \DateTime())->getTimestamp()); $this->entityManager->flush(); $this->syncLogger->info('Certificate installed', [ 'ncId' => $certificate->getNcId(), 'host' => $certificate->getHost(), 'ncUser' => $certificate->getNcUser(), ]); $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) { if ($cPanelCertificate['status'] === CertificateEntity::STATUS_INELIGIBLE) { return; } $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 { try { $ncCertificates = $this->ncApi->getSslList(); } catch (Exception $e) { $this->syncLogger->error('Can\'t get certificates from Namecheap', $e->getTrace()); return []; } return $this->applyNcStatusChanges($ncCertificates, $userCertificates); } /** * Update ncStatus / terminal-delete each matched certificate against the given NC SSL list and * collect the ids of certificates that are ready to (re)install. * * List-agnostic: works with both the Core (UI Sync) and Gateway (cron) SSL lists, since each row * only needs to expose 'id' and an upper-cased 'status'. * * @param array $ncCertificates SSL list rows, each with 'id' and 'status' * @param array $userCertificates local cert rows keyed by ncId * @return int[] ids of certificates to (re)install */ private function applyNcStatusChanges(array $ncCertificates, array $userCertificates): array { $certsForInstallationIds = []; foreach ($ncCertificates as $ncCertificate) { if (!array_key_exists($ncCertificate['id'], $userCertificates)) { continue; } $userCertificate = $userCertificates[$ncCertificate['id']]; // Ineligible is terminal: never resurrect or terminal-delete a reappearing cert if ($userCertificate['status'] == CertificateEntity::STATUS_INELIGIBLE) { continue; } if ($userCertificate['status'] == CertificateEntity::STATUS_INPROGRESS && $ncCertificate['status'] == CertificateEntity::NCSTATUS_ACTIVE) { $certsForInstallationIds[] = $userCertificate['id']; continue; } if ($userCertificate['status'] == CertificateEntity::STATUS_ACTIVE && empty($userCertificate['cpanelId'])) { $certsForInstallationIds[] = $userCertificate['id']; continue; } $certEntity = $this->certificateRepository->findOneBy([ 'ncId' => $ncCertificate['id'], ]); if ($certEntity !== null) { $this->applyNcStatusChange($certEntity, $ncCertificate['status']); } } return $certsForInstallationIds; } /** * Flag sync candidates that the fetched SSL list no longer contains (e.g. the certificate was * transferred to another account): the fetch can never update them, so as candidates they * would re-trigger it on every guard window forever. Flagged certificates are permanently * excluded from getPotentialCertificatesForSync(). * * Clear the flag for flagged certificates that reappear in the list — their statuses are * refreshed by applyNcStatusChanges() as usual, and they become regular sync candidates again. * * @param array $ncCertificates SSL list rows, each with 'id' * @param array $userCertificates local cert rows keyed by ncId * @param int[] $potentialNcIdsForSync */ private function applyMissingFromSslListChanges( array $ncCertificates, array $userCertificates, array $potentialNcIdsForSync ): void { $ncListNcIds = array_map(static fn (array $ncCertificate) => (int) $ncCertificate['id'], $ncCertificates); $missingNcIds = array_values(array_diff($potentialNcIdsForSync, $ncListNcIds)); if ($missingNcIds) { $this->certificateRepository->setMissingFromSslList($missingNcIds, true); $this->syncLogger->info('Certificates are missing from the SSL list, excluded from cron sync candidates', [ 'ncIds' => $missingNcIds, ]); } $reappearedNcIds = []; foreach ($userCertificates as $userCertificate) { $isFlaggedMissing = !empty($userCertificate['isMissingFromSslList']); $isBackInSslList = in_array((int) $userCertificate['ncId'], $ncListNcIds, true); if ($isFlaggedMissing && $isBackInSslList) { $reappearedNcIds[] = (int) $userCertificate['ncId']; } } if ($reappearedNcIds) { $this->certificateRepository->setMissingFromSslList($reappearedNcIds, false); $this->syncLogger->info('Certificates reappeared in the SSL list, restored as cron sync candidates', [ 'ncIds' => $reappearedNcIds, ]); } } /** * Apply terminal-status deletion or ncStatus update based on the latest NC view of a cert. * * @return bool true when the certificate was deleted (caller should skip further processing) */ private function applyNcStatusChange(CertificateEntity $cert, string $newNcStatus): bool { $logContext = [ 'ncUser' => $cert->getNcUser(), 'ncId' => $cert->getNcId(), 'currentStatus' => $cert->getStatus(), 'currentNcStatus' => $cert->getNcStatus(), 'newNcStatus' => $newNcStatus, ]; if ($cert->getStatus() !== CertificateEntity::STATUS_ACTIVE && $newNcStatus === CertificateEntity::NCSTATUS_NEWPURCHASE) { $this->syncLogger->info('Delete certificate', $logContext); $this->certificateService->deleteById($cert->getId()); return true; } if (in_array($newNcStatus, [ CertificateEntity::NCSTATUS_REPLACED, CertificateEntity::NCSTATUS_CANCELLED, CertificateEntity::NCSTATUS_REVOKED, ], true)) { $this->syncLogger->info('Delete certificate', $logContext); $this->certificateService->deleteById($cert->getId()); if ($cert->getCpanelId()) { try { $this->cpanelHelper->deleteCertificate($cert->getCpanelId()); } catch (PluginException $e) { $this->syncLogger->error("Can't delete certificate from cPanel", $e->getTrace()); } } return true; } if ($cert->getNcStatus() !== $newNcStatus) { $this->syncLogger->info('Update NC status of certificate', [ ...$logContext, 'ncStatus' => $newNcStatus, ]); $this->certificateService->updateNcStatus($cert->getId(), $newNcStatus); } return false; } }
Save