HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Http/Controllers/Admin/AddonController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Addon;
use App\Services\UpdateServerService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use ZipArchive;

class AddonController extends Controller
{
    protected array $catalog = [
        [
            'slug'        => 'cold-email-outreach',
            'name'        => 'Cold Email Outreach',
            'author'      => 'MailPurse',
            'version'     => '1.0.0',
            'category'    => 'outreach',
            'description' => 'Run cold email outreach campaigns with sequences, A/B testing, reply detection, and scheduling.',
            'icon'        => 'outreach',
            'price'       => '$29',
            'purchase_url' => 'https://codecanyon.net/item/coldmail-cold-email-outreach-addons-for-mailpurse/62976476?s_rank=1'
        ],
        // [
        //     'slug'        => 'super-scrape',  
        //     'name'        => 'SuperScrape',
        //     'author'      => 'MailPurse',
        //     'version'     => '1.0.0',
        //     'category'    => 'lead-generation',
        //     'description' => 'Scrape Google Maps, Places, Reviews, News & Images for lead generation. Export to CSV or push directly to your email lists.',
        //     'icon'        => 'scraper',
        //     'price'       => '$29',
        //     'purchase_url' => '#'
        // ],
        [
            'slug'         => 'unibox',
            'name'         => 'UniBox',
            'author'       => 'MailPurse',
            'version'      => '1.0.0',
            'category'     => 'inbox',
            'description'  => 'Unified inbox for receiving and sending emails with team collaboration, contact intelligence, internal notes, and conversation assignment.',
            'icon'         => 'inbox',
            'price'        => '$29',
            'purchase_url' => 'https://codecanyon.net/item/unibox-unified-inbox-addons-for-mailpurse/63337656',
        ],
        // [
        //     'slug'         => 'workspace',
        //     'name'         => 'Workspace & Teams',
        //     'author'       => 'MailPurse',
        //     'version'      => '1.0.0',
        //     'category'     => 'collaboration',
        //     'description'  => 'Multi-user workspaces with custom roles, team invitations, and shared resource access for collaborative email marketing.',
        //     'icon'         => 'building',
        //     'price'        => '$29',
        //     'purchase_url' => '#',
        // ],
    ];

    public function index(): View
    {
        $installed = Addon::latest()->get()->keyBy('slug');
        $updateInfoBySlug = $this->getUpdateInfoBySlug($installed);

        $catalog = collect($this->catalog)->map(function ($item) use ($installed, $updateInfoBySlug) {
            $item['record'] = $installed->get($item['slug']);
            $item['status'] = $item['record']?->status ?? 'available';
            $item['update_info'] = $updateInfoBySlug[$item['slug']] ?? null;
            return $item;
        });

        $addons = $installed->map(function (Addon $addon) use ($updateInfoBySlug) {
            $addon->setAttribute('update_info', $updateInfoBySlug[$addon->slug] ?? null);

            return $addon;
        });

        return view('admin.addons.index', compact('addons', 'catalog'));
    }

    public function remoteInstall(Request $request, UpdateServerService $updateServerService): JsonResponse
    {
        $slug       = (string) $request->input('slug', '');
        $licenseKey = trim((string) $request->input('license_key', ''));

        $catalogItem = collect($this->catalog)->firstWhere('slug', $slug);
        if (!$catalogItem) {
            return response()->json(['error' => __('Addon not found in catalog.')], 404);
        }

        if (Addon::where('slug', $slug)->exists()) {
            return response()->json(['error' => __('Addon ":name" is already installed.', ['name' => $catalogItem['name']])], 409);
        }

        if ($licenseKey === '') {
            return response()->json(['error' => __('Purchase code is required.')], 422);
        }

        $addonConfig = config('services.update_server.addons.' . $slug);
        if (!is_array($addonConfig)) {
            return response()->json(['error' => __('Addon server configuration not found for ":slug".', ['slug' => $slug])], 500);
        }

        ['baseUrl' => $baseUrl, 'productId' => $productId, 'productSecret' => $productSecret, 'productName' => $productName, 'domain' => $domain] = $this->resolveAddonServerConfig($slug);

        // Always activate the license for this addon (ensures server registers domain)
        $licenseActivate = $updateServerService->licenseActivateAddon($licenseKey, $domain, $productId, $productSecret, $productName);
        if (!($licenseActivate['valid'] ?? false)) {
            $message = is_string($licenseActivate['message'] ?? null) && trim((string) $licenseActivate['message']) !== ''
                ? (string) $licenseActivate['message']
                : __('Purchase code is not valid.');

            $raw = $licenseActivate['raw'] ?? null;
            if ($raw !== null) {
                $rawStr = is_string($raw) ? trim($raw) : (is_array($raw) ? trim((string) json_encode($raw)) : '');
                if ($rawStr !== '' && $rawStr !== $message) {
                    $message .= ' — ' . $rawStr;
                }
            }

            return response()->json(['error' => $message], 422);
        }

        // Debug: log activation response to help diagnose 403 on download
        Log::debug('Addon license activate response', [
            'slug'    => $slug,
            'domain'  => $domain,
            'valid'   => $licenseActivate['valid'] ?? null,
            'message' => $licenseActivate['message'] ?? null,
            'status'  => $licenseActivate['status'] ?? null,
            'data'    => $licenseActivate['data'] ?? null,
        ]);

        // Brief pause to let the server persist the activation before we request the download
        usleep(500000);

        $installResult = $this->downloadAndExtractAddonPackage(
            $updateServerService,
            $slug,
            $licenseKey,
            $baseUrl,
            $domain,
            $productId,
            $productSecret,
            $productName
        );

        if (!($installResult['success'] ?? false)) {
            return response()->json(['error' => $installResult['message'] ?? __('Installation failed.')], (int) ($installResult['status'] ?? 422));
        }

        $manifest = $installResult['manifest'];

        Addon::create([
            'slug'         => $slug,
            'name'         => $manifest['name'],
            'author'       => $manifest['author'] ?? $catalogItem['author'],
            'version'      => $manifest['version'] ?? $catalogItem['version'],
            'category'     => $manifest['category'] ?? $catalogItem['category'],
            'description'  => $manifest['description'] ?? $catalogItem['description'],
            'status'       => 'active',
            'license_key'  => $licenseKey,
            'installed_at' => now(),
            'activated_at' => now(),
        ]);

        return response()->json([
            'success' => true,
            'message' => __(':name v:version installed and activated successfully.', [
                'name'    => $manifest['name'],
                'version' => $manifest['version'] ?? $catalogItem['version'],
            ]),
        ]);
    }

    public function installUpdate(Addon $addon, UpdateServerService $updateServerService): RedirectResponse
    {
        Log::info('Addon update started', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'name' => $addon->name]);

        if (!is_string($addon->license_key) || trim($addon->license_key) === '') {
            Log::warning('Addon update failed: no license key', ['addon_id' => $addon->id, 'slug' => $addon->slug]);
            return back()->with('error', __('Purchase code is required before installing updates for :name.', ['name' => $addon->name]));
        }

        try {
            ['baseUrl' => $baseUrl, 'productId' => $productId, 'productSecret' => $productSecret, 'productName' => $productName, 'domain' => $domain] = $this->resolveAddonServerConfig($addon->slug);
        } catch (\Throwable $e) {
            Log::error('Addon update failed: could not resolve server config', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'error' => $e->getMessage()]);
            return back()->with('error', __('Addon server configuration not found for :slug.', ['slug' => $addon->slug]));
        }

        $updateInfo = $this->getAddonUpdateInfo($addon, $updateServerService, $baseUrl, $productId);
        if (!($updateInfo['update_available'] ?? false)) {
            Log::info('Addon update: no update available', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'update_info' => $updateInfo]);
            return back()->with('error', __('No update available for :name.', ['name' => $addon->name]));
        }

        $licenseKey = trim((string) $addon->license_key);

        Log::info('Addon update: activating license', ['addon_id' => $addon->id, 'slug' => $addon->slug]);
        $licenseActivate = $updateServerService->licenseActivateAddon($licenseKey, $domain, $productId, $productSecret, $productName);
        if (!($licenseActivate['valid'] ?? false)) {
            $message = is_string($licenseActivate['message'] ?? null) && trim((string) $licenseActivate['message']) !== ''
                ? (string) $licenseActivate['message']
                : __('Purchase code is not valid.');

            Log::error('Addon update failed: license activation failed', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'message' => $message, 'response' => $licenseActivate]);
            return back()->with('error', $message);
        }

        usleep(500000);

        Log::info('Addon update: downloading package', ['addon_id' => $addon->id, 'slug' => $addon->slug]);
        $installResult = $this->downloadAndExtractAddonPackage(
            $updateServerService,
            $addon->slug,
            $licenseKey,
            $baseUrl,
            $domain,
            $productId,
            $productSecret,
            $productName
        );

        if (!($installResult['success'] ?? false)) {
            Log::error('Addon update failed: download/extract failed', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'message' => $installResult['message'] ?? null]);
            return back()->with('error', $installResult['message'] ?? __('Failed to install addon update.'));
        }

        $manifest = $installResult['manifest'];
        $extra = array_diff_key($manifest, array_flip([
            'slug', 'name', 'author', 'version', 'category', 'description',
        ]));

        Log::info('Addon update: updating database', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'new_version' => $manifest['version'] ?? null]);
        $addon->update([
            'name'         => $manifest['name'] ?? $addon->name,
            'author'       => $manifest['author'] ?? $addon->author,
            'version'      => $manifest['version'] ?? $addon->version,
            'category'     => $manifest['category'] ?? $addon->category,
            'description'  => $manifest['description'] ?? $addon->description,
            'meta'         => !empty($extra) ? $extra : $addon->meta,
            'installed_at' => now(),
            'activated_at' => $addon->status === 'active' ? now() : $addon->activated_at,
        ]);

        Log::info('Addon update completed successfully', ['addon_id' => $addon->id, 'slug' => $addon->slug, 'new_version' => $manifest['version'] ?? null]);
        return back()->with('success', __(':name updated to v:version successfully.', [
            'name' => $addon->name,
            'version' => $manifest['version'] ?? $addon->version,
        ]));
    }

    public function upload(Request $request): RedirectResponse
    {
        $request->validate([
            'addon_zip' => 'required|file|mimes:zip|max:102400',
        ], [
            'addon_zip.required' => __('Please select a zip file to upload.'),
            'addon_zip.mimes'    => __('The file must be a valid .zip archive.'),
            'addon_zip.max'      => __('The zip file must not exceed 100MB.'),
        ]);

        $zip     = new ZipArchive();
        $tmpPath = $request->file('addon_zip')->getPathname();

        if ($zip->open($tmpPath) !== true) {
            return back()->with('error', __('Could not open zip file. Please verify it is a valid archive.'));
        }

        $manifestContent = null;
        $rootDir         = '';

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);
            if (basename($name) === 'addon.json') {
                $manifestContent = $zip->getFromIndex($i);
                $rootDir         = (dirname($name) === '.') ? '' : explode('/', $name)[0];
                break;
            }
        }

        if ($manifestContent === null) {
            $zip->close();
            return back()->with('error', __('Invalid addon package: addon.json manifest not found inside the zip.'));
        }

        $manifest = json_decode($manifestContent, true);

        if (!is_array($manifest) || empty($manifest['slug']) || empty($manifest['name'])) {
            $zip->close();
            return back()->with('error', __('Invalid addon.json: required fields "slug" and "name" are missing.'));
        }

        $slug = preg_replace('/[^a-z0-9\-]/', '', strtolower((string) $manifest['slug']));

        if ($slug === '') {
            $zip->close();
            return back()->with('error', __('Invalid addon slug in addon.json.'));
        }

        if (Addon::where('slug', $slug)->exists()) {
            $zip->close();
            return back()->with('error', __('Addon ":name" is already installed. Uninstall it first before re-uploading.', ['name' => $manifest['name']]));
        }

        $addonPath = storage_path("app/addons/{$slug}");

        if (!is_dir($addonPath)) {
            mkdir($addonPath, 0755, true);
        }

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);

            $relative = ($rootDir !== '' && str_starts_with($name, $rootDir . '/'))
                ? substr($name, strlen($rootDir) + 1)
                : $name;

            if ($relative === '' || str_contains($relative, '..')) {
                continue;
            }

            $target = $addonPath . '/' . $relative;

            if (str_ends_with($name, '/')) {
                if (!is_dir($target)) {
                    mkdir($target, 0755, true);
                }
                continue;
            }

            $dir = dirname($target);
            if (!is_dir($dir)) {
                mkdir($dir, 0755, true);
            }

            file_put_contents($target, $zip->getFromIndex($i));
        }

        $zip->close();

        $extra = array_diff_key($manifest, array_flip([
            'slug', 'name', 'author', 'version', 'category', 'description',
        ]));

        Addon::create([
            'slug'         => $slug,
            'name'         => $manifest['name'],
            'author'       => $manifest['author'] ?? 'Unknown',
            'version'      => $manifest['version'] ?? '1.0.0',
            'category'     => $manifest['category'] ?? 'general',
            'description'  => $manifest['description'] ?? '',
            'status'       => 'installed',
            'meta'         => !empty($extra) ? $extra : null,
            'installed_at' => now(),
        ]);

        return back()->with('success', __(':name v:version installed successfully. Enter your purchase code to activate it.', [
            'name'    => $manifest['name'],
            'version' => $manifest['version'] ?? '1.0.0',
        ]));
    }

    public function activate(Request $request, Addon $addon): RedirectResponse
    {
        $licenseKey = trim((string) $request->input('license_key', ''));

        if ($licenseKey === '') {
            return back()->with('error', __('Purchase code is required to activate the addon.'));
        }

        $addon->update([
            'status'       => 'active',
            'license_key'  => $licenseKey,
            'activated_at' => now(),
        ]);

        return back()->with('success', __(':name has been activated successfully.', ['name' => $addon->name]));
    }

    public function deactivate(Addon $addon): RedirectResponse
    {
        $addon->update([
            'status'       => 'installed',
            'activated_at' => null,
        ]);

        return back()->with('success', __(':name has been deactivated.', ['name' => $addon->name]));
    }

    public function uninstall(Addon $addon): RedirectResponse
    {
        $name = $addon->name;
        $slug = $addon->slug;

        $addonPath = storage_path("app/addons/{$slug}");
        if (is_dir($addonPath)) {
            $this->deleteDirectory($addonPath);
        }

        $addon->delete();

        return back()->with('success', __(':name has been uninstalled and its files removed.', ['name' => $name]));
    }

    private function resolveAddonServerConfig(string $slug): array
    {
        $addonConfig = config('services.update_server.addons.' . $slug);

        if (!is_array($addonConfig)) {
            abort(500, __('Addon server configuration not found for ":slug".', ['slug' => $slug]));
        }

        $baseUrl       = (string) config('services.update_server.base_url');
        $productId     = (int) ($addonConfig['product_id'] ?? 0);
        $productSecret = (string) ($addonConfig['product_secret'] ?? '');
        $productName   = (string) ($addonConfig['product_name'] ?? $slug);

        $appUrl = (string) config('app.url');
        $parsed = parse_url($appUrl);
        $domain = is_array($parsed) && is_string($parsed['host'] ?? null)
            ? (string) $parsed['host']
            : $appUrl;

        return compact('baseUrl', 'productId', 'productSecret', 'productName', 'domain');
    }

    private function getUpdateInfoBySlug($installed): array
    {
        $service = app(UpdateServerService::class);
        $baseUrl = (string) config('services.update_server.base_url');
        $data = [];

        foreach ($installed as $slug => $addon) {
            $addonConfig = config('services.update_server.addons.' . $slug);
            if (!is_array($addonConfig)) {
                continue;
            }

            $productId = (int) ($addonConfig['product_id'] ?? 0);
            if ($productId <= 0) {
                continue;
            }

            $data[$slug] = $this->getAddonUpdateInfo($addon, $service, $baseUrl, $productId);
        }

        return $data;
    }

    private function getAddonUpdateInfo(Addon $addon, UpdateServerService $updateServerService, string $baseUrl, int $productId): ?array
    {
        $productResult = $updateServerService->getProduct($productId, $baseUrl);
        $changelogResult = $updateServerService->getChangelogs($productId, $baseUrl);

        Log::debug('Addon update: raw API responses', [
            'addon_id' => $addon->id,
            'slug' => $addon->slug,
            'product_id' => $productId,
            'product_result' => $productResult,
            'changelog_result' => $changelogResult,
        ]);

        $latestVersion = $this->extractLatestVersion($productResult, $changelogResult);

        if (!is_string($latestVersion) || trim($latestVersion) === '') {
            Log::debug('Addon update: could not determine latest version', [
                'addon_id' => $addon->id,
                'slug' => $addon->slug,
                'product_result' => $productResult,
                'changelog_result' => $changelogResult,
            ]);
            return null;
        }

        $installedVersion = is_string($addon->version) ? trim($addon->version) : '';
        if ($installedVersion === '') {
            return null;
        }

        $updateAvailable = version_compare($latestVersion, $installedVersion, '>');

        Log::debug('Addon update check result', [
            'addon_id' => $addon->id,
            'slug' => $addon->slug,
            'installed_version' => $installedVersion,
            'latest_version' => $latestVersion,
            'update_available' => $updateAvailable,
        ]);

        return [
            'installed_version' => $installedVersion,
            'latest_version' => $latestVersion,
            'update_available' => $updateAvailable,
        ];
    }

    private function extractLatestVersion(?array $productResult, ?array $changelogResult): ?string
    {
        $latest = null;

        $payload = is_array($productResult) ? ($productResult['data'] ?? null) : null;
        if (is_array($payload)) {
            $productData = null;
            if (is_array($payload['data'] ?? null)) {
                $productData = $payload['data'];
            } elseif (is_array($payload['product'] ?? null)) {
                $productData = $payload['product'];
            } else {
                $productData = $payload;
            }

            if (is_array($productData)) {
                $latest = $productData['latest_version'] ?? ($productData['latestVersion'] ?? ($productData['latest'] ?? null));
                if (!is_string($latest) || trim((string) $latest) === '') {
                    $latest = $productData['version'] ?? null;
                }
            }
        }

        if (is_string($latest) && trim((string) $latest) !== '') {
            return trim((string) $latest);
        }

        $changelogData = is_array($changelogResult) ? ($changelogResult['data'] ?? null) : null;
        if (!is_array($changelogData)) {
            return null;
        }

        $versionMap = null;
        if (is_array($changelogData['releases'] ?? null)) {
            $versionMap = $changelogData['releases'];
        } elseif (is_array($changelogData['changelog'] ?? null)) {
            $versionMap = $changelogData['changelog'];
        }

        if (!is_array($versionMap) || empty($versionMap)) {
            return null;
        }

        $versions = array_keys($versionMap);
        usort($versions, function ($a, $b) {
            return version_compare((string) $b, (string) $a);
        });

        $top = $versions[0] ?? null;
        return is_string($top) && trim($top) !== '' ? trim($top) : null;
    }

    private function downloadAndExtractAddonPackage(
        UpdateServerService $updateServerService,
        string $slug,
        string $licenseKey,
        string $baseUrl,
        string $domain,
        int $productId,
        string $productSecret,
        string $productName
    ): array {
        $downloadResult = $updateServerService->requestAddonDownloadUrl($baseUrl, $licenseKey, $domain, $productId, $productSecret, $productName);
        if (!($downloadResult['success'] ?? false)) {
            return [
                'success' => false,
                'message' => $this->formatDownloadError($downloadResult),
                'status' => 502,
            ];
        }

        $downloadUrl = is_string($downloadResult['download_url'] ?? null) ? trim((string) $downloadResult['download_url']) : '';
        if ($downloadUrl === '') {
            return [
                'success' => false,
                'message' => __('Download URL not available.'),
                'status' => 502,
            ];
        }

        try {
            $response = Http::timeout(120)->get($downloadUrl);
            if (!$response->successful()) {
                return [
                    'success' => false,
                    'message' => __('Download failed (HTTP :status).', ['status' => $response->status()]),
                    'status' => 502,
                ];
            }
        } catch (\Throwable $e) {
            return [
                'success' => false,
                'message' => __('Download error: ') . $e->getMessage(),
                'status' => 502,
            ];
        }

        $tmpPath = sys_get_temp_dir() . '/' . $slug . '_' . time() . '.zip';
        file_put_contents($tmpPath, $response->body());

        $zip = new ZipArchive();
        if ($zip->open($tmpPath) !== true) {
            @unlink($tmpPath);

            return [
                'success' => false,
                'message' => __('Downloaded file is not a valid zip archive.'),
                'status' => 422,
            ];
        }

        $manifestContent = null;
        $rootDir = '';

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);
            if (basename($name) === 'addon.json') {
                $manifestContent = $zip->getFromIndex($i);
                $rootDir = (dirname($name) === '.') ? '' : explode('/', $name)[0];
                break;
            }
        }

        if ($manifestContent === null) {
            $zip->close();
            @unlink($tmpPath);

            return [
                'success' => false,
                'message' => __('Invalid addon package: addon.json not found.'),
                'status' => 422,
            ];
        }

        $manifest = json_decode($manifestContent, true);
        if (!is_array($manifest) || empty($manifest['slug']) || empty($manifest['name'])) {
            $zip->close();
            @unlink($tmpPath);

            return [
                'success' => false,
                'message' => __('Invalid addon.json manifest.'),
                'status' => 422,
            ];
        }

        $addonPath = storage_path("app/addons/{$slug}");
        if (is_dir($addonPath)) {
            $this->deleteDirectory($addonPath);
        }

        mkdir($addonPath, 0755, true);

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);
            $relative = ($rootDir !== '' && str_starts_with($name, $rootDir . '/'))
                ? substr($name, strlen($rootDir) + 1)
                : $name;

            if ($relative === '' || str_contains($relative, '..')) {
                continue;
            }

            $target = $addonPath . '/' . $relative;

            if (str_ends_with($name, '/')) {
                if (!is_dir($target)) {
                    mkdir($target, 0755, true);
                }

                continue;
            }

            $dir = dirname($target);
            if (!is_dir($dir)) {
                mkdir($dir, 0755, true);
            }

            file_put_contents($target, $zip->getFromIndex($i));
        }

        $zip->close();
        @unlink($tmpPath);

        return [
            'success' => true,
            'manifest' => $manifest,
        ];
    }

    private function formatDownloadError(array $downloadResult): string
    {
        $msg = is_string($downloadResult['message'] ?? null) ? trim((string) $downloadResult['message']) : '';
        if ($msg === '') {
            $msg = __('Failed to get download URL from server.');
        }

        if (is_numeric($downloadResult['status'] ?? null)) {
            $msg .= ' (HTTP ' . (int) $downloadResult['status'] . ')';
        }

        $raw = $downloadResult['raw'] ?? null;
        if ($raw !== null) {
            $rawStr = is_string($raw) ? trim($raw) : (is_array($raw) ? trim((string) json_encode($raw)) : '');
            if ($rawStr !== '') {
                $msg .= ': ' . $rawStr;
            }
        }

        return $msg;
    }

    private function deleteDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }
        foreach (array_diff(scandir($dir), ['.', '..']) as $item) {
            $path = $dir . DIRECTORY_SEPARATOR . $item;
            is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
        }
        rmdir($dir);
    }
}