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/ai.naniguide.com/app/Model/Template.php
<?php

/**
 * Template class.
 *
 * Model class for template
 *
 * LICENSE: This product includes software developed at
 * the Acelle Co., Ltd. (http://acellemail.com/).
 *
 * @category   MVC Model
 *
 * @copyright  Acelle Co., Ltd
 * @license    Acelle Co., Ltd
 *
 * @version    1.0
 *
 * @link       http://acellemail.com
 */

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use Validator;
use App\Library\Traits\HasUid;
use KubAT\PhpSimple\HtmlDomParser;
use App\Library\Tool;
use App\Library\StringHelper;
use DOMDocument;
use Exception;
use Closure;
use League\Pipeline\PipelineBuilder;
use App\Library\HtmlHandler\TransformWidgets;
use App\Library\HtmlHandler\DecodeHtmlSpecialChars;
use App\Library\HtmlHandler\GenerateSpintax;
use App\Library\HtmlHandler\AddDoctype;
use App\Library\HtmlHandler\ParseRss;
use App\Library\HtmlHandler\MakeInlineCss;
use App\Library\Storage\StoragePathResolver;
use App\Library\Storage\StorageEngineResolver;
use App\Library\Storage\AssetPath;
use Symfony\Component\HttpFoundation\File\File;

use function App\Helpers\url_get_contents_ssl_safe;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Template extends Model
{
    use HasFactory;
    use HasUid;

    protected static function newFactory()
    {
        return \Database\Factories\TemplateFactory::new();
    }

    /**
     * Default builder JSON for new/blank templates.
     * Every template MUST have valid BuilderJS JSON — empty string crashes
     * the builder because blade outputs {!! $template->json !!} directly
     * into JavaScript, producing `const themeJson = ;` (SyntaxError).
     */
    public const DEFAULT_BUILDER_JSON = '{"name":"PageElement","template":"Page","blocks":[],"formats":{"background_color":"#FFFFFF"}}';

    public const SOURCE_BUILDER  = 'builder';
    public const SOURCE_UPLOADED = 'uploaded';

    /**
     * Validate json is never empty on save.
     * This is the application-level enforcement — mirrors the DB CHECK constraint
     * added in migration 2026_04_14_200000.
     */
    protected static function booted(): void
    {
        static::saving(function (Template $template) {
            $json = $template->getAttributes()['json'] ?? null;

            if ($json === null || $json === '' || $json === '{}') {
                throw new \InvalidArgumentException(
                    'Template.json must be valid BuilderJS JSON — cannot be empty/null. '
                    . 'Use Template::DEFAULT_BUILDER_JSON for blank templates. '
                    . 'Got: ' . var_export($json, true)
                );
            }
        });
    }

    /**
     * Return json as a decoded array for safe use in controllers/views.
     * Controllers should use this instead of raw $template->json to pass
     * typed data to views via @json().
     */
    public function getBuilderJson(): array
    {
        $json = $this->json;

        if (empty($json)) {
            $json = self::DEFAULT_BUILDER_JSON;
        }

        $decoded = json_decode($json, true);

        if (!is_array($decoded)) {
            return json_decode(self::DEFAULT_BUILDER_JSON, true);
        }

        // Ensure theme key is present (BuilderJS requires it)
        if (empty($decoded['theme'])) {
            $decoded['theme'] = $this->theme ?: 'default';
        }

        return $decoded;
    }

    public const BASE_PATH = 'themes';

    public static function getPath(): string
    {
        return storage_path(self::BASE_PATH);
    }

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'uid', 'name', 'content', 'theme', 'type', 'json', 'source'
    ];

    public function isUploaded(): bool
    {
        return $this->source === self::SOURCE_UPLOADED;
    }

    /**
     * Items per page.
     *
     * @var array
     */
    public static $itemsPerPage = 25;

    /**
     * Associations.
     *
     * @var object | collect
     */
    public function customer()
    {
        return $this->belongsTo('App\Model\Customer');
    }

    public function admin()
    {
        return $this->belongsTo('App\Model\Admin');
    }

    /**
     * The template that belong to the categories.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<TemplateCategory, $this>
     */
    public function categories(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
    {
        return $this->belongsToMany(TemplateCategory::class, 'templates_categories', 'template_id', 'category_id');
    }

    /**
     * Search.
     *
     * @return collect
     */
    public function scopeCategoryUid($query, $uid)
    {
        $category = \App\Model\TemplateCategory::findByUid($uid);
        // Category
        if ($category) {
            $query = $query->whereHas('categories', function ($q) use ($category) {
                $q->whereIn('template_categories.id', [$category->id]);
            });
        }
    }

    /**
     * Search.
     *
     * @return collect
     */
    public function scopeSearch($query, $keyword)
    {
        // Keyword
        if (!empty($keyword)) {
            $query = $query->where('name', 'like', '%'.trim($keyword).'%');
        }
    }

    /**
     * Customer templates.
     *
     * @return collect
     */
    public function scopeCustom($query)
    {
        $query = $query->whereNotNull('customer_id');
    }

    /**
     * Public/Gallery templates.
     *
     * @return collect
     */
    public static function scopeShared($query)
    {
        $query = $query->whereNull('customer_id');
    }

    public static function scopeEmail($query)
    {
        $query = $query->where('type', '=', self::TYPE_EMAIL);
    }

    public static function scopePopup($query)
    {
        $query = $query->where('type', '=', self::TYPE_POPUP);
    }

    /**
     * Template tags.
     *
     * All availabel template tags
     */
    public static function tags($list = null)
    {
        $tags = [];

        $tags[] = ['name' => 'SUBSCRIBER_EMAIL', 'required' => false];

        // List field tags
        if (isset($list)) {
            foreach ($list->fields as $field) {
                if ($field->tag != 'EMAIL') {
                    $tags[] = ['name' => 'SUBSCRIBER_'.$field->tag, 'required' => false];
                }
            }
        }

        $tags = array_merge($tags, [
            ['name' => 'UNSUBSCRIBE_URL', 'required' => false],
            ['name' => 'SUBSCRIBER_UID', 'required' => false],
            ['name' => 'WEB_VIEW_URL', 'required' => false],
            ['name' => 'UPDATE_PROFILE_URL', 'required' => false],
            ['name' => 'CAMPAIGN_NAME', 'required' => false],
            ['name' => 'CAMPAIGN_UID', 'required' => false],
            ['name' => 'CAMPAIGN_SUBJECT', 'required' => false],
            ['name' => 'CAMPAIGN_FROM_EMAIL', 'required' => false],
            ['name' => 'CAMPAIGN_FROM_NAME', 'required' => false],
            ['name' => 'CAMPAIGN_REPLY_TO', 'required' => false],
            ['name' => 'CURRENT_YEAR', 'required' => false],
            ['name' => 'CURRENT_MONTH', 'required' => false],
            ['name' => 'CURRENT_DAY', 'required' => false],
            ['name' => 'LIST_NAME', 'required' => false],
            ['name' => 'LIST_FROM_NAME', 'required' => false],
            ['name' => 'LIST_FROM_EMAIL', 'required' => false],
        ]);

        return $tags;
    }

    /**
     * Contain category
     *
     * @return bool
     */
    public function hasCategory($category)
    {
        return $this->categories()->where('template_categories.id', $category->id)->exists();
    }

    /**
     * Add category
     *
     * @return void
     */
    public function addCategory($category)
    {
        if (!$this->hasCategory($category)) {
            $this->categories()->attach($category->id);
        }
    }

    /**
     * Remove category
     *
     * @return void
     */
    public function removeCategory($category)
    {
        if ($this->hasCategory($category)) {
            $this->categories()->detach($category->id);
        }
    }

    /**
     * Copy new template.
     */
    public function copy($name = null)
    {
        $copy = new self();

        // UID and Customer ID must be present first, in order to create directory and to transformURL
        $copy->generateUid();

        // Copy content
        $copy->content = $this->content;
        $copy->json = $this->json;

        // copy theme
        $copy->theme = $this->theme;

        // Preserve source (builder vs uploaded)
        $copy->source = $this->source ?: self::SOURCE_BUILDER;

        // Copy flags
        $copy->is_default = false; // no longer default

        // Overwrite attributes :name
        $copy->name = $name ?? $this->name;

        // Thumbnail — skip silently when source is missing (uploaded templates have none).
        $thumbSource = storage_path(join_paths('app', $this->getThumbPath()));
        if (file_exists($thumbSource)) {
            $copy->updateThumbnailFromPath($thumbSource);
        }

        // Then save, generate UID, created_at, updated_at
        $copy->save();

        // Important: save before adding
        foreach ($this->categories as $category) {
            $copy->addCategory($category);
        }

        // return
        return $copy;
    }

    // Private means a template that is associated to a campaign, email or form
    public function copyAsPrivate($attributes)
    {
        $copy = $this->copy($attributes['name']);
        $copy->save();

        return $copy;
    }

    // Actual URL prefix
    public function getUrl($path = null): string
    {
        return app(\App\Services\UrlService::class)->generateThemeAssetUrl($this->theme ?: 'default', $path);
    }

    public function getThemeMetadata()
    {
        $theme = $this->theme ?: 'default';

        // Use fixed storage local here, do not resolve @important
        $storage = app(\App\Library\Storage\StorageEngineResolver::class)->resolve('local');
        $metadataFile = "themes/{$theme}/index.json";
        $path = AssetPath::from($metadataFile);

        if ($storage->fileExists($path)) {
            return json_decode($storage->getFileContent($path), true);
        }

        // Fallback: check resources/themes/ for built-in themes
        $resourcePath = resource_path("themes/{$theme}/index.json");
        if (file_exists($resourcePath)) {
            return json_decode(file_get_contents($resourcePath), true);
        }

        throw new \Exception("Cannot find theme metadata file at: {$metadataFile}");
    }

    public function getUrlPrefix()
    {
        // @important $path = null means prefix
        return $this->getUrl($path = null);
    }

    public function updateThumbnailFromPath($sourcePath)
    {
        if (!file_exists($sourcePath)) {
            throw new \RuntimeException("Thumbnail source file not found: {$sourcePath}");
        }

        $storagePath = AssetPath::from($this->getThumbPath());
        $dir = storage_path('app/' . $storagePath->getPath());
        if (!is_dir($dir) && !mkdir($dir, 0775, true)) {
            throw new \RuntimeException("Cannot create thumbnail directory: {$dir}");
        }
        if (!is_writable($dir)) {
            throw new \RuntimeException("Thumbnail directory not writable: {$dir}");
        }

        $sourceFile = new File($sourcePath);
        $storage = app(\App\Library\Storage\StorageInterface::class);
        $storage->store($sourceFile, $storagePath);
    }

    public function getThumbPath()
    {
        // Full path like: public/template/thumb/{uid}
        return app(StoragePathResolver::class)->templateThumbPath($this);
    }

    public function makeThumbUrl($checkIfFileExists = false): ?string
    {
        $asset = AssetPath::from($this->getThumbPath());
        return app(\App\Services\UrlService::class)->makePublicUrl($asset);
    }

    public function getThumbUrl()
    {
        $storage = app(\App\Library\Storage\StorageInterface::class);
        $asset = AssetPath::from($this->getThumbPath());
        if (!$storage->fileExists($asset)) {
            return url('images/placeholder.jpg');
        }

        // Cache-bust from updated_at so that when the thumbnail file is
        // overwritten (e.g. Change Theme copies a new one in place), browsers
        // refetch instead of rendering the stale cached image.
        $url = $this->makeThumbUrl();
        $version = optional($this->updated_at)->timestamp ?: time();
        return $url . (str_contains($url, '?') ? '&' : '?') . 'v=' . $version;
    }

    public function findCssFiles()
    {
        // IMPORTANT
        // + No external CSS
        // + Only CSS in the template folder is considered
        $files = [];

        $document = new DOMDocument();
        $document->loadHTML($this->content, LIBXML_NOWARNING | LIBXML_NOERROR);
        $links = $document->getElementsByTagName('link');
        foreach ($links as $link) {
            $href = $link->getAttribute('href');
            if (!empty($href)) {
                $path = $this->getAssetFileFromUrl($href);
                if ($path) {
                    $files[] = $path;
                }
            }
        }
        return $files;
    }

    public function getAssetFileFromUrl($url)
    {
        // To remove query string if any like ?search=abc
        // For example
        //     parse_url('/assets/path/file.jpg?id=320943&search=', PHP_URL_PATH)
        // Returns
        //     /assets/path/file.jpg
        $url = parse_url($url, PHP_URL_PATH);

        // Clean up subdirectory, leaving the url as '/assets/path/file.jpg' only
        $subdirectory = app(\App\Services\UrlService::class)->getAppSubdirectory();
        if ($subdirectory) {
            // Make sure $subdirectory looks like '/subdir' ==> with a leading slash but without trailing one
            $subdirectory = rtrim(join_paths('/', $subdirectory), '/');

            // Remove subdirectory of URL
            $url = str_replace($subdirectory, '', $url);
        }

        // 'theme/assets/{path}/f/{subpath?}'
        if (preg_match('/\/theme\/assets\/(?<path>[^\/]+)\/f\/(?<subpath>.+)/', $url, $match)) {
            $theme = \App\Library\StringHelper::base64UrlDecode($match['path']);
            $absPath = storage_path(join_paths('themes', $theme, $match['subpath']));
            return $absPath;
        }
        // 'assets/{dirname}/{basename}'
        elseif (preg_match('/\/assets\/(?<dirname>[^\/]+)\/(?<basename>[^\/]+)/', $url, $match)) {
            $dirname = StringHelper::base64UrlDecode($match['dirname']);
            $absPath = storage_path(join_paths($dirname, $match['basename']));
            return $absPath;
        } else {
            return null;
        }
    }

    public function wooTransform($body)
    {
        // find all links from contents
        $document = HtmlDomParser::str_get_html($body);

        // Woo Items List
        foreach ($document->find('[builder-element=ProductListElement]') as $element) {
            $max = $element->getAttribute('data-max-items');
            $display = $element->getAttribute('data-display');
            $sort = $element->getAttribute('data-sort-by');

            $request = request();
            $request->merge(['per_page' => $max]);
            $request->merge(['sort_by' => $sort]);

            $items = Product::search($request)->paginate($request->per_page)
                ->map(function ($product, $key) {
                    return [
                        'id' => $product->uid,
                        'name' => $product->title,
                        'price' => $product->price,
                        'image' => $product->getImageUrl(),
                        'description' => substr(strip_tags($product->description), 0, 100),
                        'link' => action('ProductController@index'),
                    ];
                })->toArray();
            $itemsHtml = [];
            foreach ($items as $item) {
                // $element->find('.woo-items')[0]->innertext = 'dddddd';
                $itemsHtml[] = '
                    <div class="woo-col-item mb-4 mt-4 col-md-' . (12 / $display) . '">
                        <div class="">
                            <div class="img-col mb-3">
                                <div class="d-flex align-items-center justify-content-center" style="height: 200px;">
                                    <a style="width:100%" href="'.$item["link"].'" class="mr-4"><img width="100%" src="'.($item["image"] ? $item["image"] : url('images/cart_item.svg')).'" style="max-height:200px;max-width:100%;" /></a>
                                </div>
                            </div>
                            <div class="">
                                <p class="font-weight-normal product-name mb-1">
                                    <a style="color: #333;" href="'.$item["link"].'" class="mr-4">'.$item["name"].'</a>
                                </p>
                                <p class=" product-description">'.$item["description"].'</p>
                                <p><strong>'.$item["price"].'</strong></p>
                                <a href="'.$item["link"].'" style="background-color: #9b5c8f;
    border-color: #9b5c8f;" class="btn btn-primary text-white">
                                    ' . trans('messages.automation.view_more') . '
                                </a>
                            </div>
                        </div>
                    </div>
                ';
            }

            $element->find('.products')[0]->innertext = implode('', $itemsHtml);
        }

        // Woo Single Item
        foreach ($document->find('[builder-element=ProductElement]') as $element) {
            $productId = $element->getAttribute('product-id');

            if ($productId) {
                $product = Product::findByUid($productId);

                $item = [
                    'id' => $product->uid,
                    'name' => $product->title,
                    'price' => $product->price,
                    'image' => $product->getImageUrl(),
                    'description' => substr(strip_tags($product->description), 0, 100),
                    'link' => action('ProductController@index'),
                ];
                // $element->find('.product-name', 0)->innertext = $item["name"];
                // $element->find('.product-description', 0)->innertext = $item["description"];
                // $element->find('.product-link', 0)->href = $item["link"];
                // $element->find('.product-price', 0)->innertext = $item["price"];
                $element->find('.product-link img', 0)->src = $item["image"];
                $html = $element->innertext;
                $html = str_replace('*|PRODUCT_NAME|*', $item["name"], $html);
                $html = str_replace('*|PRODUCT_DESCRIPTION|*', $item["description"], $html);
                $html = str_replace('*|PRODUCT_URL|*', $item["link"], $html);
                $html = str_replace('*|PRODUCT_PRICE|*', $item["price"], $html);
                // $html = str_replace('*|PRODUCT_QUANTITY|*', $item["quantity"], $html);
                $element->innertext = $html;
            }
        }

        $body = $document;

        return $body;
    }

    public function uploadAssetFromBase64($base64)
    {
        throw new \Exception("No longer supported");
    }

    public function uploadAssetFromUrl($url)
    {
        return $url;
        /* Another way is to fetch and save the image to the local directory of the template */
    }

    /**
     * Template tags.
     *
     * All availabel template tags
     */
    public static function builderTags($list = null)
    {
        $tags = self::tags($list);

        $result = [];

        if (true) {
            // Unsubscribe link
            $result[] = [
                'type' => 'label',
                'text' => '<a href="{{ unsubscribe_url }}">' . trans('messages.editor.unsubscribe_text') . '</a>',
                'tag' => '{UNSUBSCRIBE_LINK}',
                'required' => true,
            ];

            // web view link
            $result[] = [
                'type' => 'label',
                'text' => '<a href="{{ web_view_url }}">' . trans('messages.editor.click_view_web_version') . '</a>',
                'tag' => '{WEB_VIEW_LINK}',
                'required' => true,
            ];
        }

        foreach ($tags as $tag) {
            $result[] = [
                'type' => 'label',
                'text' => '{'.$tag['name'].'}',
                'tag' => '{'.$tag['name'].'}',
                'required' => true,
            ];
        }

        return $result;
    }

    public function deleteAndCleanup()
    {
        $this->delete();

        $path = AssetPath::from($this->getThumbPath());
        $storage = app(\App\Library\Storage\StorageInterface::class);

        return $storage->delete($path);
    }

    public function updateContent($json, $content)
    {
        $rules = array(
            'json' => 'required',
            'content' => 'required',
        );

        // Validate
        $validator = \Validator::make([
            'json' => $json,
            'content' => $content,
        ], $rules);

        if ($validator->fails()) {
            return $validator;
        }

        // save both json and html (content)
        $this->content = $content;
        $this->json = $json;
        $this->save();

        //
        return $validator;
    }

    public function urlTagsDropdown()
    {
        return [
            ['value' => '{{ unsubscribe_url }}', 'text' => trans('messages.editor.unsubscribe_text')],
            ['value' => '{{ update_profile_url }}', 'text' => trans('messages.editor.update_profile_text')],
            ['value' => '{{ web_view_url }}', 'text' => trans('messages.editor.click_view_web_version')],
        ];
    }

    public function getPreviewContent()
    {
        // Bind subscriber/message/server information to email content
        $pipeline = new PipelineBuilder();
        $pipeline->add(new AddDoctype());
        $pipeline->add(new MakeInlineCss($this->findCssFiles()));
        $pipeline->add(new TransformWidgets());
        $pipeline->add(new DecodeHtmlSpecialChars());
        $pipeline->add(new GenerateSpintax());

        // Actually push HTML to pipeline for processing
        $html = $pipeline->build()->process($this->content);

        // Return subscriber's bound html
        return $html;
    }

    public static function defaultRssConfig()
    {
        return [
            'url' => '',
            'size' => 10,
            'templates' => [
                'FeedTitle' => [
                    'title' => trans('messages.rss.feed_title'),
                    'show' => true,
                    'template' => '@feed_title',
                ],
                'FeedSubtitle' => [
                    'title' => trans('messages.rss.feed_subtitle'),
                    'show' => true,
                    'template' => 'Updated at: @feed_build_date',
                ],
                'FeedTagdLine' => [
                    'title' => trans('messages.rss.feed_tagline'),
                    'show' => true,
                    'template' => trans('messages.rss.top_stories_for_you'),
                ],
                'ItemTitle' => [
                    'title' => trans('messages.rss.item_title'),
                    'show' => true,
                    'template' => 'Title: @item_title',
                ],
                'ItemMeta' => [
                    'title' => trans('messages.rss.meta_line'),
                    'show' => true,
                    'template' => '<img src="'.url('images/avatar1.svg').'" width="30px" style="margin-right:5px" /> something here - @item_pubdate',
                ],
                'ItemDescription' => [
                    'title' => trans('messages.rss.item_description'),
                    'show' => true,
                    'template' => '@item_description <a href="@item_url">Read more</a>',
                ],
                'ItemStats' => [
                    'title' => trans('messages.rss.stats_line'),
                    'show' => true,
                    'template' => '<img src="'.url('images/icon-up.svg').'" width="16px" style="margin-right:5px" /> 400k updates, &nbsp; &nbsp;
                        <img src="'.url('images/icon-comment.svg').'" width="16px" style="margin-right:5px" /> 1.2k comments',
                ],
                'ItemEnclosure' => [
                    'title' => trans('messages.rss.enclosure'),
                    'show' => false,
                    'template' => '@item_enclosure',
                ],
            ],
        ];
    }

    /**
     * Copy theme / json / content / thumbnail from another template.
     * Used by the "Change Theme" flow in customer + admin builders.
     */
    public function applyTemplateFrom(Template $source): void
    {
        $this->theme = $source->theme;
        $this->json = $source->json;
        $this->content = $source->content;
        $this->save();

        $this->copyThumbnailFrom($source);
    }

    /**
     * Copy the thumbnail file from another template's storage path into ours.
     * Silently no-ops if the source template has no thumbnail on disk.
     */
    public function copyThumbnailFrom(Template $source): void
    {
        $storage = app(\App\Library\Storage\StorageInterface::class);
        $sourceAsset = AssetPath::from($source->getThumbPath());

        if (!$storage->fileExists($sourceAsset)) {
            return;
        }

        $tmpPath = tempnam(sys_get_temp_dir(), 'tmpl_thumb_');
        try {
            file_put_contents($tmpPath, $storage->getFileContent($sourceAsset));
            $this->updateThumbnailFromPath($tmpPath);
        } finally {
            if ($tmpPath && file_exists($tmpPath)) {
                @unlink($tmpPath);
            }
        }
    }

    public function duplicate(): Template
    {
        $newTemplate = self::makeTemplate($this->theme, $this->name, $this->json, $this->content, $this->thumbnail);

        // save
        $newTemplate->save();

        //
        return $newTemplate;
    }

    public static function createTemplate(string $theme, string $name, string $json, string $content): Template
    {
        //
        $template = self::makeTemplate($theme, $name, $json, $content);

        // save
        $template->save();

        //
        return $template;
    }

    public static function makeTemplate(string $theme, string $name, string $json, string $content): Template
    {
        $template = new self();
        $template->theme = $theme;
        $template->name = $name;
        $template->is_default = true;
        $template->json = $json;
        $template->content = $content;
        return $template;
    }

    /**
     * Build a Template that wraps uploaded raw HTML.
     *
     * Why: the Email content layer is single-pathed through Template — any HTML, whether
     * authored in BuilderJS or pasted as raw, lives in Template.content and runs through
     * the same send pipeline. source='uploaded' marks it so re-edit UX skips BuilderJS.
     * json is forced to DEFAULT_BUILDER_JSON to satisfy the booted() non-empty invariant.
     */
    public static function makeUploaded(string $name, string $html): Template
    {
        $template = self::makeTemplate(
            theme: 'default',
            name: $name,
            json: self::DEFAULT_BUILDER_JSON,
            content: $html,
        );
        $template->source = self::SOURCE_UPLOADED;
        $template->is_default = false;
        return $template;
    }

    public function getThemeConfigData()
    {
        $theme = $this->theme ?: 'default';
        return json_decode(file_get_contents(resource_path('themes/'.$theme.'/index.json')), true);
    }
}