VYPR
High severity8.6NVD Advisory· Published May 21, 2025· Updated Apr 15, 2026

CVE-2025-48201

CVE-2025-48201

Description

The ns_backup extension through 13.0.0 for TYPO3 has a Predictable Resource Location.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nitsan/ns-backupPackagist
< 13.0.113.0.1

Patches

1
67b8102a19e8

[TASK] Security fix

https://github.com/nitsan-technologies/ns_backupHimanshu RamavatMay 20, 2025via ghsa
15 files changed · +455 157
  • Classes/Controller/BackupBaseController.php+173 96 modified
    @@ -4,11 +4,10 @@
     
     use RuntimeException;
     use TYPO3\CMS\Core\Core\Environment;
    -use TYPO3\CMS\Core\Exception;
     use TYPO3\CMS\Core\Utility\GeneralUtility;
     use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
     use NITSAN\NsBackup\Domain\Repository\BackupglobalRepository;
    -use TYPO3\CMS\Extbase\Utility\LocalizationUtility as transalte;
    +use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
     
     /***
      *
    @@ -144,7 +143,7 @@ class BackupBaseController extends ActionController
         public function __construct(
             protected  BackupglobalRepository $backupglobalRepository
         ) {
    -        $this->exceptionMessage = transalte::translate('something.wrong.here', 'ns_backup');
    +        $this->exceptionMessage = LocalizationUtility::translate('something.wrong.here', 'ns_backup');
     
         }
     
    @@ -161,7 +160,7 @@ public function globalErrorValidation(): string
             $arrKeys = ['emails', 'emailSubject', 'compress', 'php', 'root', 'siteurl', 'cleanup', 'cleanupQuantity'];
             $arrValidation = [];
             foreach ($arrKeys as $key) {
    -            $arrValidation[$key] = transalte::translate("global.error.$key", 'ns_backup');
    +            $arrValidation[$key] = LocalizationUtility::translate("global.error.$key", 'ns_backup');
             }
     
             $errorValidation = implode('', array_map(function ($key, $value) {
    @@ -173,12 +172,12 @@ public function globalErrorValidation(): string
             $arrExtensionsToCheck = ['curl', 'dom', 'json'];
             foreach ($arrExtensionsToCheck as $extension) {
                 if (!in_array($extension, $arrGetLoadedExtensions)) {
    -                $errorValidation .= '<li>' . transalte::translate("global.error.$extension", 'ns_backup') . '</li>';
    +                $errorValidation .= '<li>' . LocalizationUtility::translate("global.error.$extension", 'ns_backup') . '</li>';
                 }
             }
             // Check if exec() works
             if (!exec('echo EXEC') == 'EXEC') {
    -            $errorValidation .= '<li>' . transalte::translate('global.error.exec', 'ns_backup') . '</li>';
    +            $errorValidation .= '<li>' . LocalizationUtility::translate('global.error.exec', 'ns_backup') . '</li>';
             }
             return $errorValidation;
         }
    @@ -200,7 +199,6 @@ public function generateBackup(array $arrPost): array
     
             // Get TYPO3 Path
             $this->rootPath = $this->globalSettingsData[0]->root ?? (Environment::getProjectPath() ?? '');
    -        $this->phpbuPath = $this->rootPath.'/typo3conf/ext/ns_backup/phpbu.phar';
     
             // Let's change root path to /public in Composer-based installation
             if(Environment::isComposerMode()) {
    @@ -210,10 +208,17 @@ public function generateBackup(array $arrPost): array
             }
     
             // Get Local Storage Path
    -        $this->localStoragePath = $this->rootPath.'/uploads/tx_nsbackup/';
    +        $globalBackupStorePath = $this->globalSettingsData[0]->getBackupStorePath();
    +        $isPublicPath = $this->isPathPublic($globalBackupStorePath);
    +        if ($globalBackupStorePath == '') {
    +            $this->localStoragePath = $this->rootPath . '/tx_nsbackup/';
    +            $jsonFolder = $this->rootPath . '/tx_nsbackup/json/';
    +        } else {
    +            $this->localStoragePath = $globalBackupStorePath . '/tx_nsbackup/';
    +            $jsonFolder = $globalBackupStorePath . '/tx_nsbackup/json/';
    +        }
             try {
                 if (!file_exists($this->localStoragePath)) {
    -
                     GeneralUtility::mkdir_deep($this->localStoragePath);
                 }
             } catch (RuntimeException $e) {
    @@ -224,8 +229,19 @@ public function generateBackup(array $arrPost): array
             }
     
             // Get Base URL
    -        $this->siteUrl = $this->globalSettingsData[0]->siteurl ?? '';
    -        $this->baseURL = $this->siteUrl . '/uploads/tx_nsbackup/';
    +        $this->siteUrl = '';
    +        if (!empty($this->globalSettingsData[0]->siteurl)) {
    +            $this->siteUrl = $this->globalSettingsData[0]->siteurl;
    +        }
    +        $path = str_replace($this->rootPath, '', $globalBackupStorePath);
    +        $this->baseURL = $this->siteUrl . $path . '/tx_nsbackup/';
    +
    +        // Get PHPHBU Path
    +        if (Environment::isComposerMode()) {
    +            $this->phpbuPath = $this->composerRootPath . '/vendor/nitsan/ns-backup/phpbu.phar';
    +        } else {
    +            $this->phpbuPath = $this->rootPath . '/typo3conf/ext/ns_backup/phpbu.phar';
    +        }
     
             // Get Database Configuration
             $this->arrDatabase = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default'];
    @@ -234,22 +250,28 @@ public function generateBackup(array $arrPost): array
             // Get Current Date time
             $this->prefixFileName = date('dmY_Hi');
     
    +        // Prepare backup filename
    +        $backupName = $arrPost['backupName'].'_'.$this->prefixFileName;
    +        $currentDateTime = '';
             $backupNameOriginal = $arrPost['backupName'];
    -        $backupName = $this->prefixFileName.'_'.$arrPost['backupName'];
    +        $backupFileName = strtolower(trim($backupName));
    +        $backupFileName = preg_replace(['/[\s-]+/', '/[^A-Za-z0-9_]/', '/_+/'], '_', $backupFileName);
             $backupType = $arrPost['backupFolderSettings'];
     
    +        // Generates an 8-character random string
    +        $randomString = substr(md5(uniqid(mt_rand(), true)), 0, 8);
    +        $backupBaseName = GeneralUtility::trimExplode('_', $backupFileName, true, 3)[1];
    +
    +        $jsonFile = $backupBaseName . '_' . $randomString . '_' . $backupType . '_configuration.json';
    +        $logFile = $jsonFolder . $backupBaseName . '_' . $randomString . '_' . $backupType . '_log.json';
    +        $jsonPath = $jsonFolder . $jsonFile;
    +
             // Prepare backup filename
             $backupFileName = preg_replace(
                 '/[^A-Za-z0-9]+/',
                 '_',
                 preg_replace('/[\s-]+/', '_', strtolower(trim($backupName)))
             );
    -
    -        $jsonFolder = $this->rootPath.'/uploads/tx_nsbackup/json/';
    -        $jsonFile = GeneralUtility::trimExplode('_', $backupFileName, true, 3)[2] . '_' . $backupType . '_configuration.json';
    -        $logFile = $jsonFolder . GeneralUtility::trimExplode('_', $backupFileName, true, 3)[2] . '_' . $backupType . '_log.json';
    -        $jsonPath = $jsonFolder.$jsonFile;
    -
             // Let's create LOG file if not existis
             if (!file_exists($logFile)) {
                 $fh = @fopen($logFile, 'a');
    @@ -272,7 +294,7 @@ public function generateBackup(array $arrPost): array
                             "options": {
                             "transport": "mail",
                             "recipients": "'.$this->globalSettingsData[0]->emails.'",
    -                        "subject": "['.$backupType.'] '.$backupNameOriginal. ' - '.$this->globalSettingsData[0]->emailSubject.'",
    +                        "subject": "[' . $backupType . '] ' . $backupNameOriginal . ' - ' . $this->globalSettingsData[0]->emailSubject . '",
                             "sendOnlyOnError": "'.$this->globalSettingsData[0]->emailNotificationOnError.'"
                             }
                         }
    @@ -281,11 +303,16 @@ public function generateBackup(array $arrPost): array
     
             // Let's check if admin wants "Backup Everyting"
             if ($backupType == 'all') {
    +
    +            // store date and time before backup
    +            $currentDateTime = date('Ymd-Hi');
                 // Create Database Backup
    -            $json .= $this->getPhpbuBackup($backupName, 'mysqldump', $backupFileName). ',';
    +            $json .= $this->getPhpbuBackup($backupName, 'mysqldump', $backupFileName) . ',';
     
                 // Create Code Backup
                 $json .= $this->getPhpbuBackup($backupName, $backupType, $backupFileName);
    +        } elseif ($backupType == 'other') {
    +            $json .= $this->getPhpbuBackup($backupName, $backupType, $backupFileName, $arrPost['custompath']);
             } else {
                 // Create Specific Selected Type of Backup
                 $json .= $this->getPhpbuBackup($backupName, $backupType, $backupFileName);
    @@ -297,23 +324,28 @@ public function generateBackup(array $arrPost): array
             ';
     
             try {
    -            // Let's create JSCON folder does not exists
    +            // Ensure the directory exists
                 if (!file_exists($jsonFolder)) {
    -                GeneralUtility::mkdir_deep($jsonFolder);
    +                GeneralUtility::mkdir($jsonFolder);
                 }
     
    -            // Let's create JSON file
    +            // Write JSON content to file
                 file_put_contents($jsonPath, $json);
     
    +            // Validate and sanitize PHP path
    +            if (!is_string($this->phpPath) || !file_exists($this->phpPath) || !is_executable($this->phpPath)) {
    +                throw new RuntimeException("Invalid PHP executable path.");
    +            }
    +
                 // Prepare SSH Command
    -            $command = $this->phpPath. ' '. $this->phpbuPath.' --configuration='.$jsonPath.' --verbose';
    +            $command = $this->phpPath . ' ' . $this->phpbuPath . ' --configuration=' . $jsonPath . ' --verbose';
     
                 // Execute Backup SSH Command
    -            exec($command, $log);
    +            exec($command, $log, $return_var);
             } catch (RuntimeException $e) {
                 return  [
                     'log' => 'error',
    -                'backup_file' => $this->exceptionMessage,
    +                'backup_file' => 'Something is wrong here.' . $e->getMessage(),
                 ];
             }
     
    @@ -327,42 +359,65 @@ public function generateBackup(array $arrPost): array
                 // If Backup Everything, Then let's first-insert MySQL as special case
                 if ($backupType == 'all') {
                     $arrPost['backup_type'] = 'mysqldump';
    -                $arrPost['download_url'] = $this->backupDownloadPathMySQL;
    -                $fileSize = $this->convertFilesize(filesize($this->backupFileMySQL));
    +                $arrPost['download_url'] = '';
    +                if ($isPublicPath) {
    +                    $arrPost['download_url'] = $this->backupDownloadPathMySQL;
    +                }
    +                $compressTechnique = $this->globalSettingsData[0]->compress;
    +                $compressTechniques = [
    +                    'bzip2' => '.bz2',
    +                    'zip' => '',
    +                    'gzip' => '.gz',
    +                    'xz' => '.xz',
    +                ];
    +
    +                $compressTechnique = $compressTechniques[$compressTechnique] ?? '.bz2';
    +
    +                $path = str_replace("/all", "", $this->backupFilePath);
    +                $backupFileMySQL = $path . '/mysqldump' . '/mysqldump' . '-' . $currentDateTime . '.sql' . $compressTechnique;
    +                $fileSize = $this->convertFilesize(filesize($backupFileMySQL));
                     $arrPost['size'] = $fileSize;
    -                $arrPost['filenames'] = $this->backupFileMySQL;
    +
    +                $arrPost['filenames'] = $backupFileMySQL;
                     $this->backupglobalRepository->addBackupData($arrPost);
    +
                 }
     
                 // Insert to Database > Backup History
    -            $arrPost['download_url'] = $this->backupDownloadPath;
    +            $arrPost['download_url'] = '';
    +            if ($isPublicPath) {
    +                $arrPost['download_url'] = $this->backupDownloadPath;
    +            }
                 $arrPost['log'] = $log;
    +
                 try {
                     $fileSize = $this->convertFilesize(filesize($this->backupFile));
    -            } catch (Exception $e) {
    +            } catch (\Exception $e) {
                     return  [
                         'log' => 'error',
    -                    'backup_file' => $this->exceptionMessage,
    +                    'backup_file' => 'Something is wrong here. Please check you Global Settings',
                     ];
                 }
     
                 $arrPost['size'] = $fileSize;
                 $arrPost['filenames'] = $this->backupFile;
     
    +            $downloadURL = '';
    +            if ($isPublicPath) {
    +                $downloadURL = $this->backupDownloadPath;
    +            }
                 $this->backupglobalRepository->addBackupData($arrPost);
    -
                 $arrReturn = [
                     'log' => $log,
                     'backup_file' => $this->backupFile,
    -                'download_url' => $this->backupDownloadPath,
    +                'download_url' => $downloadURL,
                 ];
             } else {
                 $arrReturn = [
                     'log' => 'error',
                     'backup_file' => $this->backupFile,
                 ];
             }
    -
             return $arrReturn;
         }
     
    @@ -371,18 +426,20 @@ public function generateBackup(array $arrPost): array
          * @param string $backupName
          * @param string $backupType
          * @param string $backupFileName
    +     * @param string|null $rawName
          * @return string
          */
    -    protected function getPhpbuBackup(string $backupName, string $backupType, string $backupFileName): string
    +    protected function getPhpbuBackup(string $backupName, string $backupType, string $backupFileName, ?string $rawName = null): string
         {
    -        $json = '';
    +        $json = $ignoreUploads = '';
             $json .= '
                 {
    -                "name": "'.$backupName.'",';
    +                "name": "' . $backupName . '",';
     
             $backupExtFile = '.tar';
    -        if ($backupType == 'mysqldump') {
    -            $json .= '
    +        switch ($backupType) {
    +            case 'mysqldump':
    +                $json .= '
                     "source": {
                         "type": "mysqldump",
                         "options": {
    @@ -393,94 +450,100 @@ protected function getPhpbuBackup(string $backupName, string $backupType, string
                             "password": "' . $this->arrDatabase['password'] . '"
                         }
                     },';
    -            $backupExtFile = '.sql';
    -        } else {
    -            $targetPath = ($backupType == 'all') ? '' : $backupType;
    -            // Exclude uploads/tx_nsbackup
    -            if ($backupType == 'uploads') {
    -                $ignoreUploads = ',"exclude": "tx_nsbackup"';
    -            }
    -            if ($backupType == 'all') {
    -                $ignoreUploads = ',"exclude": "uploads/tx_nsbackup,typo3temp"';
    -            }
    +                $backupExtFile = '.sql';
    +                break;
     
    -            $sourcePath = $this->rootPath . '/' . $targetPath;
    +            default:
    +                $targetPath = ($backupType == 'all') ? '' : $backupType;
     
    -            // In composer-mode, let's figure out vendor folder
    -            if ($backupType == 'vendor' && $this->composerRootPath && strlen($this->composerRootPath) > 0) {
    -                $sourcePath = $this->composerRootPath . '/' . $targetPath;
    -            }
    +                // Exclude uploads/tx_nsbackup
    +                if ($backupType == 'uploads') {
    +                    $ignoreUploads = ',"exclude": "tx_nsbackup"';
    +                }
    +                if ($backupType == 'all') {
    +                    $ignoreUploads = ',"exclude": "uploads/tx_nsbackup,typo3temp"';
    +                }
     
    -            $ignoreUploads = $ignoreUploads ?? '';
    -            $json .= '
    -                "source": {
    -                    "type": "tar",
    -                    "options": {
    -                        "path": "' . $sourcePath . '"' . $ignoreUploads . '
    -                    }
    -                },';
    +                $sourcePath = $this->rootPath . '/' . $targetPath;
    +
    +                // In composer-mode, let's figure out vendor folder
    +                if (($backupType == 'vendor') && ($this->composerRootPath !== null && strlen($this->composerRootPath) > 0)) {
    +                    $sourcePath = $this->composerRootPath . '/' . $targetPath;
    +                }
    +
    +                if ($backupType == 'other') {
    +                    $json .= '
    +                    "source": {
    +                        "type": "tar",
    +                        "options": {
    +                            "path": "' . $rawName . '"' . $ignoreUploads . '
    +                        }
    +                    },';
    +                } else {
    +                    $json .= '
    +                    "source": {
    +                        "type": "tar",
    +                        "options": {
    +                            "path": "' . $sourcePath . '"' . $ignoreUploads . '
    +                        }
    +                    },';
    +                }
             }
     
             // PATCH If compress=bzip2
             $compressTechnique = $this->globalSettingsData[0]->compress;
    -        switch ($compressTechnique) {
    -            case 'bzip2':
    -            case '':
    -                $compressTechnique = '.bz2';
    -                break;
    -            case 'zip':
    -                $compressTechnique = '';
    -                break;
    -            case 'gzip':
    -                $compressTechnique = '.gz';
    -                break;
    -            case 'xz':
    -                $compressTechnique = '.xz';
    -                break;
    -            default:
    -                break;
    -        }
    +        $compressTechniques = [
    +            'bzip2' => '.bz2',
    +            'zip' => '',
    +            'gzip' => '.gz',
    +            'xz' => '.xz',
    +        ];
    +
    +        $compressTechnique = $compressTechniques[$compressTechnique] ?? '.bz2';
    +        //echo $compressTechnique;exit;
    +
    +        $this->backupFilePath = $this->localStoragePath . $backupType;
     
    -        $this->backupFilePath = $this->localStoragePath.$backupType;
    -        $this->backupFileName = $backupFileName.$backupExtFile;
             // Physical file
    -        $this->backupFile = $this->backupFilePath . '/' . $backupType.'-'.date('Ymd-Hi') . $backupExtFile . $compressTechnique;
    +        $this->backupFile = $this->backupFilePath . '/' . md5($backupType).'-'.date('Ymd-Hi') . $backupExtFile . $compressTechnique;
     
             // Download file
             $this->backupDownloadPath =
                 $this->baseURL .
                 $backupType . '/' .
    -            $backupType.'-'.date('Ymd-Hi') . $backupExtFile . $compressTechnique;
    +            md5($backupType) . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique;
     
             // If Backup Type = ALL then, Let's consider mysql as special-case
             if ($backupType == 'mysqldump') {
    -            $compressTechnique = $this->globalSettingsData[0]->compress == 'zip' ? '' : $compressTechnique;
    -            $fileName = 'mysqldump' . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique;
    -            $this->backupFileMySQL = $this->backupFilePath . '/' . $fileName;
    -            $this->backupDownloadPathMySQL = $this->baseURL . $backupType . '/' . $fileName;
    +            if ($this->globalSettingsData[0]->compress == 'zip') {
    +                $compressTechnique = '';
    +            }
    +            $this->backupFileMySQL = $this->backupFilePath . '/' . md5($backupType) . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique;
    +            $this->backupDownloadPathMySQL =
    +                $this->baseURL . $this->backupFileMySQL =
    +                    $backupType . '/' . md5($backupType) . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique;
             }
    -
    -        $this->backupFileName =  $backupType.'-%Y%m%d-%H%i' . $backupExtFile;
    -
    +        $this->backupFileName = md5($backupType) . '-%Y%m%d-%H%i' . $backupExtFile;
             $json .= '
                     "target": {
    -                    "dirname": "'.$this->backupFilePath.'",
    -                    "filename": "'.$this->backupFileName.'",
    -                    "compress": "'.$this->globalSettingsData[0]->compress.'"
    +                    "dirname": "' . $this->backupFilePath . '",
    +                    "filename": "' . $this->backupFileName . '",
    +                    "compress": "' . $this->globalSettingsData[0]->compress . '"
                     },';
     
             $json .= '
                     "cleanup": {
    -                    "type": "'.$this->globalSettingsData[0]->cleanup.'",
    +                    "type": "' . $this->globalSettingsData[0]->cleanup . '",
                         "options": {
    -                        "amount": '.$this->globalSettingsData[0]->cleanupQuantity.'
    +                        "amount": "' . $this->globalSettingsData[0]->cleanupQuantity . '"
                         }
                     }
                 }
             ';
             return $json;
         }
     
    +
         /**
          * Convert File Size
          * @param mixed $bytes
    @@ -503,4 +566,18 @@ protected function convertFilesize(mixed $bytes): string
             }
             return $bytes;
         }
    +
    +    /**
    +     * @param string $path
    +     * @return boolean
    +     */
    +    public function isPathPublic(string $path): bool
    +    {
    +        if (!Environment::isComposerMode()) {
    +            $valuesToCheck = ['typo3', 'typo3conf', 'vendor', 'typo3temp', 'bin'];
    +            $parts = array_filter(explode('/', rtrim($path, '/')));
    +            return empty(array_intersect($parts, $valuesToCheck));
    +        }
    +        return str_contains(rtrim($path, '/'), Environment::getPublicPath());
    +    }
     }
    
  • Classes/Controller/BackupglobalController.php+33 13 modified
    @@ -12,7 +12,8 @@
     use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
     use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
     use NITSAN\NsBackup\Domain\Repository\BackupglobalRepository;
    -use TYPO3\CMS\Extbase\Utility\LocalizationUtility as transalte;
    +use TYPO3\CMS\Core\Core\Environment;
    +use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
     use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException;
     use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException;
     
    @@ -66,24 +67,20 @@ public function initializeView(): void
          */
         public function globalsettingAction(): ResponseInterface
         {
    -        if(!empty($this->errorValidation)) {
    -            $header = transalte::translate('global.errorvalidation', 'ns_backup');
    -            $message = transalte::translate('global.errorvalidation.message', 'ns_backup');
    -            $this->addFlashMessage($message, $header, ContextualFeedbackSeverity::ERROR);
    -        }
    -
             $pageRenderer = GeneralUtility::makeInstance(className: PageRenderer::class);
             $pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/jquery.js');
             $pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/Main.js');
             $view = $this->initializeModuleTemplate($this->request);
             $globalSettingsData = $this->backupglobalRepository->findAll();
    +        $varPath = Environment::getVarPath();
             $view->assignMultiple([
                 'cleanup' => constant('cleanup'),
                 'backupglobal' => $globalSettingsData[0],
                 'compress' => constant('compress'),
                 'action' => 'globalsetting',
                 'errorValidation' => $this->errorValidation,
    -            'modalAttr' => 'data-bs-'
    +            'modalAttr' => 'data-bs-',
    +            'varPath' => $varPath
             ]);
             return $view->renderResponse('Backupglobal/Globalsetting');
         }
    @@ -100,13 +97,24 @@ public function createAction(Backupglobal $backupglobal): ResponseInterface
             $emails = GeneralUtility::trimExplode(',', $backupglobal->getEmails());
             foreach ($emails as $email) {
                 if(!GeneralUtility::validEmail($email)) {
    -                $msg = transalte::translate('email.not.valid', 'ns_backup');
    +                $msg = LocalizationUtility::translate('email.not.valid', 'ns_backup');
                     $this->addFlashMessage('', $msg);
                     return $this->redirect('globalsetting', ContextualFeedbackSeverity::ERROR);
                 }
             }
    -
    -        $msg = transalte::translate('globalsettings.create', 'ns_backup');
    +        if (!is_dir($backupglobal->getBackupStorePath())) {
    +            $msg = LocalizationUtility::translate('storePath.not.valid', 'ns_backup');
    +            $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
    +            return $this->redirect('globalsetting');
    +        }
    +        $phpPath = trim($backupglobal->getPhp());
    +        $backupglobal->setPhp($phpPath);
    +        if (!is_executable($backupglobal->getPhp())) {
    +            $msg = LocalizationUtility::translate('phpPath.not.valid', 'ns_backup');
    +            $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
    +            return $this->redirect('globalsetting');
    +        }
    +        $msg = LocalizationUtility::translate('globalsettings.create', 'ns_backup');
             $this->addFlashMessage('', $msg);
             $this->backupglobalRepository->add($backupglobal);
     
    @@ -126,12 +134,24 @@ public function updateAction(Backupglobal $backupglobal): ResponseInterface
             $emails = GeneralUtility::trimExplode(',', $backupglobal->getEmails());
             foreach ($emails as $email) {
                 if(!GeneralUtility::validEmail($email)) {
    -                $msg = transalte::translate('email.not.valid', 'ns_backup');
    +                $msg = LocalizationUtility::translate('email.not.valid', 'ns_backup');
                     $this->addFlashMessage('', $msg);
                     return $this->redirect('globalsetting', ContextualFeedbackSeverity::ERROR);
                 }
             }
    -        $msg = transalte::translate('globalsettings.update', 'ns_backup');
    +        if (!is_dir($backupglobal->getBackupStorePath())) {
    +            $msg = LocalizationUtility::translate('storePath.not.valid', 'ns_backup');
    +            $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
    +            return $this->redirect('globalsetting');
    +        }
    +        $phpPath = trim($backupglobal->getPhp());
    +        $backupglobal->setPhp($phpPath);
    +        if (!is_executable($backupglobal->getPhp())) {
    +            $msg = LocalizationUtility::translate('phpPath.not.valid', 'ns_backup');
    +            $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
    +            return $this->redirect('globalsetting');
    +        }
    +        $msg = LocalizationUtility::translate('globalsettings.update', 'ns_backup');
             $this->addFlashMessage('', $msg);
             $this->backupglobalRepository->update($backupglobal);
     
    
  • Classes/Controller/BackupsController.php+50 4 modified
    @@ -109,6 +109,11 @@ public function backuprestoreAction(): ResponseInterface
             $pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/Main.js');
     
             $globalSettingsData = $this->backupglobalRepository->findAll();
    +        // Get Local Storage Path
    +        if ($globalSettingsData[0]) {
    +            $globalBackupStorePath = $globalSettingsData[0]->getBackupStorePath();
    +            $isPublicPath = $this->isPathPublic($globalBackupStorePath);
    +        }
             $arrMultipleVars = [
                 'cleanup' => constant('cleanup'),
                 'backuptype' => constant('backuptype'),
    @@ -119,6 +124,19 @@ public function backuprestoreAction(): ResponseInterface
             ];
     
             $arrPost = $this->request->getArguments();
    +        $backupName = trim($arrPost['backuprestore']['backupName'] ?? '');
    +        if (preg_match('/[^0-9A-Za-z _-]/', $backupName)) {
    +            $sanitizedName = htmlspecialchars($backupName, ENT_QUOTES, 'UTF-8');
    +
    +            $this->addFlashMessage(
    +                "Invalid backup name: '{$sanitizedName}'. " . transalte::translate('manualbackup.error.description', 'ns_backup'),
    +                transalte::translate('manualbackup.error', 'ns_backup'),
    +                ContextualFeedbackSeverity::ERROR
    +            );
    +
    +            return $this->redirect('backuprestore');
    +        }
    +
             // "RUN" Backup from "Manual Backup Module"
             $arrPost = $arrPost['backuprestore'] ?? '';
     
    @@ -137,7 +155,7 @@ public function backuprestoreAction(): ResponseInterface
                     $mesHeader = transalte::translate('manualbackup.success', 'ns_backup');
                     $backup_file = transalte::translate('backup.downloaded', 'ns_backup').' '.$arrResponse['backup_file'];
                     $this->addFlashMessage($backup_file, $mesHeader);
    -                
    +
                     $response = (array) json_decode($arrResponse['log']);
                     if (isset($response['errorCount']) && $response['errorCount'] > 0) {
                         $globalSettingsData = $this->backupglobalRepository->findAll();
    @@ -160,7 +178,10 @@ public function backuprestoreAction(): ResponseInterface
                     // Pass to Fluid
                     $arrMultipleVars['isManualBackup'] = '1';
                     $arrMultipleVars['log'] = '<pre class="pre-scrollable"><code class="json">'. json_encode(json_decode($arrResponse['log']), JSON_PRETTY_PRINT) .'</code></pre>';
    -                $arrMultipleVars['download_url'] = $arrResponse['download_url'];
    +                $arrMultipleVars['download_url'] = '';
    +                if ($isPublicPath) {
    +                    $arrMultipleVars['download_url'] = $arrResponse['download_url'];
    +                }
                 }
             }
     
    @@ -194,6 +215,15 @@ public function deletebackupbackupAction(): ResponseInterface
             $request = $this->request->getQueryParams();
             $uid = $request['uid'];
     
    +        $globalSettingsData = $this->backupglobalRepository->findAll();
    +        $globalSettingsData = !empty($globalSettingsData[0]) ? $globalSettingsData[0] : null;
    +
    +        if (!$globalSettingsData) {
    +            $headerMsg = transalte::translate('something.wrong.here', 'ns_backup');
    +            $this->addFlashMessage($headerMsg, '', ContextualFeedbackSeverity::ERROR, true);
    +            die;
    +        }
    +
             $arrBackup = $this->backupglobalRepository->findBackupByUid($uid);
             // Let's delete it
             $this->backupglobalRepository->removeBackupData($uid);
    @@ -203,11 +233,13 @@ public function deletebackupbackupAction(): ResponseInterface
                 unlink($arrBackup['filenames']);
             }
     
    -        $rootPath = $this->globalSettingsData[0]->root ?? (Environment::getProjectPath() ?? '');
    +        $rootPath = $globalSettingsData->root ?? (Environment::getProjectPath() ?? '');
             if(Environment::isComposerMode()) {
                 $rootPath = Environment::getPublicPath();
             }
    -        $jsonFolder = $rootPath.'/uploads/tx_nsbackup/json/';
    +
    +        $rootPath = $globalSettingsData->backupStorePath ?? ($rootPath . '/uploads');
    +        $jsonFolder = $rootPath.'/tx_nsbackup/json/';
             if(file_exists($jsonFolder.$arrBackup['jsonfile'])) {
                 unlink($jsonFolder.$arrBackup['jsonfile']);
             }
    @@ -231,4 +263,18 @@ protected function initializeModuleTemplate(
         ): ModuleTemplate {
             return $this->moduleTemplateFactory->create($request);
         }
    +
    +    /**
    +     * @param string $path
    +     * @return boolean
    +     */
    +    public function isPathPublic(string $path): bool
    +    {
    +        if (!Environment::isComposerMode()) {
    +            $valuesToCheck = ['typo3', 'typo3conf', 'vendor', 'typo3temp', 'bin'];
    +            $parts = array_filter(explode('/', rtrim($path, '/')));
    +            return empty(array_intersect($parts, $valuesToCheck));
    +        }
    +        return str_contains(rtrim($path, '/'), Environment::getPublicPath());
    +    }
     }
    
  • Classes/Domain/Model/Backupglobal.php+28 0 modified
    @@ -27,6 +27,13 @@ class Backupglobal extends AbstractEntity
          */
         public string $emails = '';
     
    +    /**
    +     * backupStorePath
    +     *
    +     * @var string
    +     */
    +    public string $backupStorePath = '';
    +
         /**
          * emailFrom
          *
    @@ -301,4 +308,25 @@ public function setCleanup(string $cleanup): void
         {
             $this->cleanup = $cleanup;
         }
    +
    +    /**
    +     * Returns the backupStorePath
    +     *
    +     * @return string backupStorePath
    +     */
    +    public function getBackupStorePath(): string
    +    {
    +        return $this->backupStorePath;
    +    }
    +
    +    /**
    +     * Sets the backupStorePath
    +     *
    +     * @param string $backupStorePath
    +     * @return void
    +     */
    +    public function setBackupStorePath(string $backupStorePath): void
    +    {
    +        $this->backupStorePath = $backupStorePath;
    +    }
     }
    
  • Configuration/Backend/Modules.php+2 2 modified
    @@ -7,8 +7,8 @@
         'nitsan_nsbackup' => [
             'parent' => 'tools',
             'position' => ['before' => 'top'],
    -        'access' => 'user',
    -        'path' => '/module/nitsan/NsBackupBackup    ',
    +        'access' => 'admin',
    +        'path' => '/module/nitsan/NsBackupBackup',
             'icon' => 'EXT:ns_backup/Resources/Public/Icons/module-nsbackup.svg',
             'labels' => 'LLL:EXT:ns_backup/Resources/Private/Language/locallang_backup.xlf',
             'extensionName' => 'NsBackup',
    
  • Configuration/TCA/tx_nsbackup_domain_model_backupglobal.php+9 0 modified
    @@ -176,5 +176,14 @@
                     'eval' => 'trim'
                 ],
             ],
    +        'backup_store_path' => [
    +            'exclude' => true,
    +            'label' => 'LLL:EXT:ns_backup/Resources/Private/Language/locallang_db.xlf:tx_nsbackup_domain_model_backupglobal.backup_store_path',
    +            'config' => [
    +                'type' => 'input',
    +                'size' => 30,
    +                'eval' => 'trim'
    +            ],
    +        ],
         ],
     ];
    
  • ext_emconf.php+2 2 modified
    @@ -6,13 +6,13 @@
     
         *** Live Demo: https://demo.t3planet.com/t3-extensions/backup *** Premium Version, Documentation & Free Support: https://t3planet.com/typo3-backup-extension',
         'category' => 'module',
    -    'author' => 'T3: Rohan Parmar, T3: Divya Goklani, T3: Nilesh Malankiya, QA: Gautam Kunjadiya',
    +    'author' => 'T3: Rohan Parmar, T3: Divya Goklani, T3: Nilesh Malankiya, QA: Krishna Dhapa',
         'author_email' => 'sanjay@nitsan.in',
         'author_company' => 'T3Planet // NITSAN',
         'state' => 'stable',
         'uploadfolder' => 1,
         'createDirs' => '',
    -    'version' => '13.0.0',
    +    'version' => '13.0.1',
         'constraints' => [
             'depends' => [
                 'typo3' => '12.0.0-13.9.99',
    
  • ext_tables.sql+1 0 modified
    @@ -34,6 +34,7 @@ CREATE TABLE tx_nsbackup_domain_model_backupglobal (
     	sys_language_uid int(11) DEFAULT '0' NOT NULL,
     	l10n_parent int(11) DEFAULT '0' NOT NULL,
     	l10n_diffsource mediumblob,
    +    backup_store_path varchar(255) DEFAULT '',
     	l10n_state text,
     	PRIMARY KEY (uid),
     	KEY parent (pid),
    
  • Resources/Private/Language/locallang_db.xlf+3 0 modified
    @@ -82,6 +82,9 @@
     			<trans-unit id="tx_nsbackup_domain_model_backupdata.status">
     				<source>Status</source>
     			</trans-unit>
    +			<trans-unit id="tx_nsbackup_domain_model_backupglobal.backup_store_path">
    +				<source>Backup Store Path</source>
    +			</trans-unit>
     		</body>
     	</file>
     </xliff>
    
  • Resources/Private/Language/locallang.xlf+24 0 modified
    @@ -193,6 +193,12 @@
     			<trans-unit id="globalsettings.form.cleanup_quantity.desc">
     				<source>Enter at how many backups clean or remove backups at your web-server and remote servers/cloud too, Min:1 And Max:500 allow</source>
     			</trans-unit>
    +			<trans-unit id="globalsettings.form.backup_store_path">
    +				<source>Backup Store Path</source>
    +			</trans-unit>
    +			<trans-unit id="globalsettings.form.backup_store_path.desc">
    +				<source>Please enter a path to store the backup (e.g., /var/www/html/public/, /var/www/html/). We recommend using a protected path to restrict external access.</source>
    +			</trans-unit>
     			<trans-unit id="globalsettings.form.servercleanup">
     				<source>Server Cleanups?</source>
     			</trans-unit>
    @@ -445,9 +451,27 @@
     			<trans-unit id="email.not.valid">
     				<source>Email format is not valid</source>
     			</trans-unit>
    +			<trans-unit id="phpPath.not.valid">
    +				<source>PHP path is not executable</source>
    +			</trans-unit>
    +			<trans-unit id="storePath.not.valid">
    +				<source>The backup storage does not exist.</source>
    +			</trans-unit>
                 <trans-unit id="something.wrong.here">
                     <source>Something is wrong here. Please check you Global Settings</source>
                 </trans-unit>
    +			<trans-unit id="servercloud.content.downloadBackupError">
    +				<source>This backup contains the private path. Due to security purposes, it is not able to download.  You can download it manually from your server.</source>
    +			</trans-unit>
    +			<trans-unit id="servercloud.content.downloadErrorTitle">
    +				<source>Download Backup</source>
    +			</trans-unit>
    +			<trans-unit id="manualbackup.error.description">
    +				<source>Allowed characters: letters, numbers, spaces, dashes (-), and underscores (_).</source>
    +			</trans-unit>
    +			<trans-unit id="servercloud.content.downloadBackupNotFound">
    +				<source>This backup is no longer available.</source>
    +			</trans-unit>
     		</body>
     	</file>
     </xliff>
    
  • Resources/Private/Layouts/Default.html+43 1 modified
    @@ -50,7 +50,7 @@
                     <f:flashMessages />
                     <f:if condition="{errorValidation}">
                         <div class="alert alert-warning">
    -                        <f:format.raw>{errorValidation}</f:format.raw>
    +                        <f:sanitize.html>{errorValidation}</f:sanitize.html>
                         </div>
                     </f:if>
                     <f:render section="content" />
    @@ -203,5 +203,47 @@ <h5 class="modal-title">Download Backup</h5>
     		</div>
     	</div>
     
    +	<!-- Private Backup Download Error  -->
    +	<div class="modal fade" id="backupDownloadError" role="dialog"
    +		 aria-labelledby="nsBackupDeleteRecordModal" aria-hidden="true">
    +		<div class="modal-dialog" role="document">
    +			<div class="modal-content">
    +				<div class="modal-header">
    +					<h5 class="modal-title"><f:translate key="servercloud.content.downloadErrorTitle" /> <span class="backup-title"></span></h5>
    +					<button type="button" class="close" {modalAttr}dismiss="modal" aria-label="Close">
    +						<span aria-hidden="true">&times;</span>
    +					</button>
    +				</div>
    +				<div class="modal-body">
    +					<p class="delete-msg"><f:translate key="servercloud.content.downloadBackupError" /></p>
    +				</div>
    +				<div class="modal-footer">
    +					<button type="button" class="btn btn-warning" {modalAttr}dismiss="modal"><em class="fa fa-close"
    +																								 aria-hidden="true"></em><f:translate key="servercloud.content.cancel" /></button>
    +				</div>
    +			</div>
    +		</div>
    +	</div>
     
    +	<!-- Private Backup Download Error  -->
    +	<div class="modal fade" id="backupNotAvailable" tabindex="-1" role="dialog"
    +		 aria-labelledby="nsBackupNotAvailableModal" aria-hidden="true">
    +		<div class="modal-dialog" role="document">
    +			<div class="modal-content">
    +				<div class="modal-header">
    +					<h5 class="modal-title"><f:translate key="servercloud.content.downloadErrorTitle" /> <span class="backup-title"></span></h5>
    +					<button type="button" class="close" {modalAttr}dismiss="modal" aria-label="Close">
    +						<span aria-hidden="true">&times;</span>
    +					</button>
    +				</div>
    +				<div class="modal-body">
    +					<p class="delete-msg"><f:translate key="servercloud.content.downloadBackupNotFound" /></p>
    +				</div>
    +				<div class="modal-footer">
    +					<button type="button" class="btn btn-warning" {modalAttr}dismiss="modal"><em class="fa fa-close"
    +																								 aria-hidden="true"></em><f:translate key="servercloud.content.cancel" /></button>
    +				</div>
    +			</div>
    +		</div>
    +	</div>
     </html>
    
  • Resources/Private/Partials/GlobalSetting/Globalform.html+26 16 modified
    @@ -35,21 +35,6 @@
     		</div>
     	</div>
     </div>
    -<div class="form-group">
    -	<div class="row">
    -		<div class="col-md-3">
    -			<label><f:translate key="globalsettings.form.notification" /></label>
    -		</div>
    -		<div class="col-md-6">
    -			<div class="form-check">
    -				<f:form.checkbox  name="emailNotificationOnError" property="emailNotificationOnError" value="1" class="form-check-input check" checked="{f:if(condition: '{backupglobal.emailNotificationOnError}==1', then: 'checked')}" id="emailNotificationOnError" />
    -				<label class="form-check-label" for="emailNotificationOnError">
    -					<f:translate key="globalsettings.form.emailNotificationOnError" />
    -				</label>
    -			</div>
    -		</div>
    -	</div>
    -</div>
     
     <div class="form-group">
     	<div class="row">
    @@ -85,6 +70,31 @@
     		</div>
     	</div>
     </div>
    +<div class="form-group">
    +    <div class="row">
    +        <div class="col-md-3">
    +            <div class="d-flex justify-content-between">
    +                <label for="backupStorePath"><f:translate key="globalsettings.form.backup_store_path" /></label>
    +            </div>
    +        </div>
    +        <div class="col-md-6">
    +            <div class="input-group">
    +				<span class="input-group-text custom-reset" data-id="backupStorePath">
    +					<i aria-hidden="true" class="fa fa-repeat"></i>
    +				</span>
    +				<f:variable name="backupStorePathValue">{f:if(condition: backupglobal.backupStorePath, then: '{backupglobal.backupStorePath}', else: '{varPath}')}</f:variable>
    +                <f:form.textfield name="backupStorePath" 
    +					type="input" property="backupStorePath" 
    +					class="form-control" id="backupStorePath" value="{backupStorePathValue}"/>
    +            </div>
    +            <div class="field-info-text">
    +                <p class="form-control-field"><f:translate key="globalsettings.form.backup_store_path.desc" /></p>
    +            </div>
    +            <p class="backupStorePath-error error" style="display: none;"><f:translate key="servercloud.nodata" /></p>
    +        </div>
    +    </div>
    +</div>
    +
     <div class="form-group">
     	<div class="row">
     		<div class="col-md-3">
    @@ -128,4 +138,4 @@
     				aria-hidden="true"></em><f:translate key="globalsettings.form.savesettings" /></button>
     		</div>
     	</div>
    -</div>
    \ No newline at end of file
    +</div>
    
  • Resources/Private/Templates/Backups/Backuprestore.html+40 17 modified
    @@ -4,11 +4,21 @@
     <f:section name="content">
         <f:flashMessages />
         <f:if condition="{isManualBackup} == '1'">
    -        <a href="{download_url}" target="_blank" class="btn btn-success btn-backupnow"><em class="fa fa-download" aria-hidden="true"></em><f:translate key="download.backup.now" extensionName = "NsBackup" /></a>
    +		<f:if condition="{download_url}==''">
    +			<f:then>
    +				<a href="javascript:;" class="backup-download-btn btn btn-primary btn-sm" {modalAttr}toggle="modal"
    +					{modalAttr}target="#backupDownloadError" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}">
    +					<em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" />
    +				</a>
    +			</f:then>
    +			<f:else>
    +				<a href="{download_url}" target="_blank" class="btn btn-success btn-backupnow"><em class="fa fa-download" aria-hidden="true"></em><f:translate key="download.backup.now" extensionName = "NsBackup" /></a>
    +			</f:else>
    +		</f:if>
             <p>&nbsp;</p>
             <div class="alert alert-secondary json-data">
                 <p><strong><f:translate key="logs" extensionName = "NsBackup" />:</strong></p>
    -            <f:format.raw><p>{log}</p></f:format.raw>
    +            <f:sanitize.html><p>{log}</p></f:sanitize.html>
             </div>
         </f:if>
     
    @@ -46,27 +56,40 @@ <h5><f:translate key="menu.backupsrestore" extensionName = "NsBackup" /></h5>
                                         </f:for>
                                     </td>
     								<td>{backup.size}</td>
    -								<td>
    -									<div class="button-group ns-backup-actions-wrap">
    -										<f:if condition="{backup.isDownload}">
    +								<td class="text-center">
    +									<div class="button-group ns-backup-actions-wrap ns-ext-actions-wrap">
    +										<f:if condition="{backup.download_url}==''">
     											<f:then>
    -												<a target="_blank" href="{backup.download_url}" class="btn btn-primary"><em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" extensionName = "NsBackup" /></a>
    +												<a href="javascript:;" class="backup-download-btn btn btn-primary btn-sm" {modalAttr}toggle="modal"
    +												   {modalAttr}target="#backupDownloadError" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}">
    +													<em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" />
    +												</a>
     											</f:then>
     											<f:else>
    -												<a href="javascript:;" class="btn btn-primary btn-sm" {modalAttr}toggle="modal"
    -												   {modalAttr}target="#downloadBackup"><em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" extensionName = "NsBackup" /></a>
    +												<f:if condition="!{backup.isDownload}">
    +													<f:then>
    +														<a href="javascript:;" class="backup-download-btn btn btn-primary btn-sm" {modalAttr}toggle="modal"
    +														   {modalAttr}target="#backupNotAvailable" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}">
    +															<em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" />
    +														</a>
    +													</f:then>
    +													<f:else>
    +														<a target="_blank" href="{backup.download_url}" class="backup-download-btn btn btn-primary btn-sm"><em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" /></a>
    +													</f:else>
    +												</f:if>
    +
     											</f:else>
     										</f:if>
     										<a href="javascript:;" class="btn btn-success btn-sm btn-log-show" {modalAttr}toggle="modal"
    -                                            {modalAttr}target="#logBackup" data-id="{backup.uid}"><em class="fa fa-file-text-o" aria-hidden="true"></em>Logs</a>
    -                                        <a href="javascript:;" class="btn btn-danger btn-sm delete-backup" {modalAttr}toggle="modal"
    -                                            {modalAttr}target="#nsBackupDeletebackupModal" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' extensionName = 'NsBackup' />" data-id="{backup.uid}">
    -                                            <em class="fa fa-trash-o" aria-hidden="true"></em><f:translate key="servercloud.content.delete" extensionName = "NsBackup" />
    -                                        </a>
    -                                    </div>
    -                                    <div style="display: none;" id="logDiv_{backup.uid}">
    -                                        <f:format.raw>{backup.logs}</f:format.raw>
    -                                    </div>
    +										   {modalAttr}target="#logBackup" data-id="{backup.uid}"><em class="fa fa-file-text-o" aria-hidden="true"></em>Logs</a>
    +										<a href="javascript:;" class="btn btn-danger btn-sm delete-backup" {modalAttr}toggle="modal"
    +										   {modalAttr}target="#nsBackupDeletebackupModal" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}">
    +											<em class="fa fa-trash-o" aria-hidden="true"></em><f:translate key="servercloud.content.delete" />
    +										</a>
    +									</div>
    +									<div style="display: none;" id="logDiv_{backup.uid}">
    +										<f:sanitize.html>{backup.logs}</f:sanitize.html>
    +									</div>
     								</td>
                                 </tr>
                                 </f:for>
    
  • Resources/Public/Css/main.css+4 0 modified
    @@ -1149,4 +1149,8 @@ div.dt-container .dt-paging .dt-paging-button.disabled:active {
       font-size: 14px;
       border-color: light-dark(#c2c2c2, #464646);
       box-shadow: none;
    +}
    +
    +.close {
    +  font-size: 22px;
     }
    \ No newline at end of file
    
  • Resources/Public/JavaScript/Main.js+17 6 modified
    @@ -97,25 +97,26 @@ $(document).ready(function() {
             return isError !== 1;
         });
     
    -    // Remove Backup Data
    +    // Remove Backup Data (XSS-Safe)
         $('.delete-backup').on('click', function () {
             const title = $(this).data('title');
             const id = $(this).data('id');
             const msg = $(this).data('msg');
    -        $("#nsBackupDeletebackupModal .backup-title").html(title);
    -        $("#nsBackupDeletebackupModal .delete-msg").html(msg);
    +
    +        $("#nsBackupDeletebackupModal .backup-title").text(title);
    +        $("#nsBackupDeletebackupModal .delete-msg").text(msg);
    +
             $("#nsBackupDeletebackupModal .delete-backup-id").val(id);
             $("#nsBackupDeletebackupModal .deletetype").val('single');
             $("#nsBackupDeletebackupModal .delete-backup-backup-del").removeAttr("disabled");
         });
     
         $('.paginate_button').on('click', function () {
    -        console.log("hello")
             const title = $('.delete-backup').data('title');
             const id = $('.delete-backup').data('id');
             const msg = $('.delete-backup').data('msg');
    -        $("#nsBackupDeletebackupModal .backup-title").html(title);
    -        $("#nsBackupDeletebackupModal .delete-msg").html(msg);
    +        $("#nsBackupDeletebackupModal .backup-title").text(title);
    +        $("#nsBackupDeletebackupModal .delete-msg").text(msg);
             $("#nsBackupDeletebackupModal .delete-backup-id").val(id);
             $("#nsBackupDeletebackupModal .deletetype").val('single');
             $("#nsBackupDeletebackupModal .delete-backup-backup-del").removeAttr("disabled");
    @@ -146,6 +147,16 @@ $(document).ready(function() {
         // Code Highlight
         // hljs.initHighlightingOnLoad();
     
    +    $('.custom-reset').on('click', function () {
    +        var that = $(this);
    +        var hideId = this.dataset.id;
    +        that.find('i').addClass('fa-spin');
    +        $('#' + hideId).val('');
    +        setTimeout(function () {
    +            that.find('i').removeClass('fa-spin');
    +        }, 2000);
    +    });
    +
         $('.ns-backup-datatable').DataTable({
             language: {
                 zeroRecords: "Nothing found - sorry",
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.