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

namespace Acelle\Model;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
use Acelle\Library\Traits\HasUid;
use Acelle\Library\Tool;
use Acelle\Library\Facades\Hook;
use Composer\Autoload\ClassLoader;
use GuzzleHttp\Client;
use App;
use Exception;
use DirectoryIterator;
use Validator;
use ZipArchive;

use function Acelle\Helpers\pcopy;

class Plugin extends Model
{
    use HasUid;
    protected $connection = 'mysql';

    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';

    public const STORAGE_PATH = 'app/plugins'; // do not use this constant, use getStoragePath instead

    public const INDEX_FILE = 'app/plugins/index.json';

    protected $fillable = ['name', 'title', 'description', 'version'];
    protected $requiredKeys = [
        'name', 'version', 'app_version'
    ];

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

    /**
     * Filter items.
     *
     * @return collect
     */
    public function scopeFilter($query, $request)
    {
        $user = $request->user();

        // Keyword
        if (!empty(trim($request->keyword))) {
            foreach (explode(' ', trim($request->keyword)) as $keyword) {
                $query = $query->where(function ($q) use ($keyword) {
                    $q->orwhere('plugins.name', 'like', '%'.$keyword.'%')
                        ->orWhere('plugins.description', 'like', '%'.$keyword.'%')
                        ->orWhere('plugins.version', 'like', '%'.$keyword.'%');
                });
            }
        }

        // filters
        $filters = $request->all();
        if (!empty($filters)) {
            if (!empty($filters['type'])) {
                $query = $query->where('plugins.type', '=', $filters['type']);
            }
        }

        return $query;
    }

    /**
     * Search items.
     *
     * @return collect
     */
    public function scopeSearch($query, $request)
    {
        $query = self::filter($request);

        if (!empty($request->sort_order)) {
            $query = $query->orderBy($request->sort_order, $request->sort_direction);
        }

        return $query;
    }

    /**
     * Get campaign validation rules.
     */
    public function rules()
    {
        $rules = array(
            'name' => 'required',
            'version' => 'required',
        );

        return $rules;
    }

    /**
     * Get all items.
     *
     * @return collect
     */
    public function scopeActive($query)
    {
        return $query->where('status', '=', self::STATUS_ACTIVE);
    }

    /**
     * Disable verification server.
     *
     * @return array
     */
    public function disable()
    {
        $this->status = self::STATUS_INACTIVE;
        $this->save();
        self::updatePluginMasterFile($this->name, [
            'status' => $this->status,
            'error' => null, // clean up error
        ]);
    }

    /**
     * Upload a plugin to tmp.
     */
    public static function upload($request)
    {
        $pluginsPath = storage_path('app/plugins');

        $rules = array(
            'file' => 'required|mimetypes:application/zip,application/octet-stream,application/x-zip-compressed,multipart/x-zip',
        );

        $validator = Validator::make($request->all(), $rules, [
            'file.mimetypes' => 'Input must be a valid .zip file',
        ]);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }

        // move file to temp place
        $tmp_path = storage_path('tmp/uploaded_plugin_'.time());
        $file_name = $request->file('file')->getClientOriginalName();
        $request->file('file')->move($tmp_path, $file_name);
        $tmp_zip = join_paths($tmp_path, $file_name);

        // read zip file check if zip archive invalid
        $zip = new ZipArchive();
        if ($zip->open($tmp_zip, ZipArchive::CREATE) !== true) {
            $validator->errors()->add('file', 'Cannot open plugin package');
            throw new ValidationException($validator);
        }

        // unzip template archive and remove zip file
        $zip->extractTo($tmp_path);
        $zip->close();
        unlink($tmp_zip);

        // read plugin file
        if (!is_file($configFile = $tmp_path.'/composer.json')) {
            throw new \Exception('Invalid plugin package. No meta data found');
        }

        try {
            $config = json_decode(file_get_contents($configFile), true);
        } catch (\Exception $ex) {
            throw new \Exception('Invalid plugin package. Cannot parse meta data');
        }

        $names = explode('/', $config['name']);
        // check valid plugin name
        if (count($names) != 2) {
            throw new \Exception('Invalid plugin package. No meta data found');
        }
        $pluginVendor = $names[0];
        $pluginName = $names[1];

        // chek if plugin exist
        $pluginsVendorPath = $pluginsPath . '/' . $pluginVendor;
        $pluginsSourcePath = $pluginsVendorPath . '/' . $pluginName;

        // check if overwrite plugin
        if ($request->overwrite) {
            $config['overwrite'] = true;
        }

        // throw exception if any required key is missing
        self::validateMetaData($config);

        // create folders
        $paths = [
            $pluginsPath,
            $pluginsVendorPath,
            $pluginsSourcePath,
        ];

        foreach ($paths as $path) {
            if (!file_exists($path)) {
                mkdir($path, 0777, true);
            }
        }

        // move from tmp to storage/app/plugins
        Tool::xdelete($pluginsSourcePath);
        if (!file_exists($pluginsSourcePath)) {
            mkdir($pluginsSourcePath, 0777, true);
        }
        Tool::xcopy($tmp_path, $pluginsSourcePath);
        Tool::xdelete($tmp_path);
        return $config['name'];
    }

    /**
     * Get plugin path.
     *
     * @return string
     */
    public function getPluginPath()
    {
        return storage_path(join_paths('app/plugins', $this->name));
    }

    /**
     * Get plugin type select options.
     *
     * @return array
     */
    public static function typeSelectOptions()
    {
        $options = [
            ['value' => 'payment', 'text' => trans('messages.plugin.type.payment')],
            ['value' => 'sending_server', 'text' => trans('messages.plugin.type.sending_server')],
            ['value' => 'email_verification', 'text' => trans('messages.plugin.type.email_verification')],
        ];

        return $options;
    }

    /**
     * Check if plugin is installed.
     *
     * @return array
     */
    public static function isInstalled($name)
    {
        return self::where('name', '=', $name)->count();
    }

    public function isActive()
    {
        return $this->status == self::STATUS_ACTIVE;
    }

    /**
     * Get plugin by name.
     *
     * @return array
     */
    public static function getByName($name)
    {
        return self::where('name', '=', $name)->first();
    }

    public static function validateMetaData($config = [])
    {
        // Check required keys
        $requiredKeys = ['name', 'title', 'version', 'app_version']; // ['name', 'title', 'version', 'app_version'];
        foreach ($requiredKeys as $key) {
            if (!array_key_exists($key, $config)) {
                throw new \Exception("Invalid plugin package. Unknown '{$key}'");
            }
        }

        /*
        $exist = self::where('name', $config['name'])->first();
        if ($exist && !isset($config['overwrite'])) {
            if (version_compare($exist->version, $config['version'], '>')) {
                throw new \Exception("A newer version of the {$config['name']} plugin already exists ({$exist->version}), do you want to replace it?");
            } else {
                throw new \Exception("Are you sure you want to overwrite the plugin {$config['name']}, version {$exist->version} with the newer one ({$config['version']})");
            }
        }
        */

        /* APP VERSION CHECK IS NO LONGER NEEDED, FOR COMPATIBILITY WITH OTHER VERSION NAMING
        // Check app version
        $currentVersion = app_version();
        $requiredVersion = $config['app_version'];
        $checked = version_compare($currentVersion, $requiredVersion, '>=');
        if (!$checked) {
            throw new \Exception(trans('messages.plugin.error.version', [
                'current' => $currentVersion,
                'required' => $requiredVersion
            ]));
        }
        */

        // // Check license type
        // if (empty(Setting::get('license_type'))) {
        //     throw new \Exception("The uploaded plugin requires a valid license of Acelle to proceed");
        // }

        // // Check license type
        // if (Setting::get('license_type') != 'extended') {
        //     throw new \Exception("The uploaded plugin requires an 'Extended' license of Acelle to proceed, your current license is 'Regular'");
        // }
    }

    /**
     * Get data.
     *
     * @var object | collect
     */
    public function getData()
    {
        if (!$this['data']) {
            return json_decode('{}', true);
        }

        return json_decode($this['data'], true);
    }

    /**
     * Get data.
     *
     * @var object | collect
     */
    public function updateData($data)
    {
        $data = (object) array_merge((array) $this->getData(), $data);
        $this['data'] = json_encode($data);

        $this->save();
    }

    public function activate()
    {
        $hookName = 'activate_plugin_'.$this->name;
        Hook::execute($hookName);

        $config = $this->getComposerJson();
        self::validateMetaData($config);

        $this->status = self::STATUS_ACTIVE;
        $this->save();
        self::updatePluginMasterFile($this->name, [
            'status' => $this->status,
            'error' => null, // clean up error
        ]);
    }

    public function reset()
    {
        $this->status = self::STATUS_INACTIVE;
        $this->data = null;
        $this->save();
        self::updatePluginMasterFile($this->name, [
            'status' => $this->status,
            'error' => null,
        ]);
    }

    public function getStoragePath($subpath = null, $absolute = true)
    {
        return self::getStoragePathByName($this->name, $subpath, $absolute);
    }

    public function getComposerJsonFilePath()
    {
        return self::getComposerJsonFilePathByName($this->name);
    }

    public function getComposerJson()
    {
        return self::getComposerJsonByName($this->name);
    }

    public static function autoload($includeInactivePlugin = false)
    {
        $activePlugins = self::all();
        foreach ($activePlugins as $plugin) {
            // $boot = $plugin->isActive();
            $boot = true; // need its route registration
            $errorTitle = 'Cannot load plugin '.$plugin->name;

            try {
                Notification::cleanupDuplicateNotifications($errorTitle);
                $plugin->load($boot);
            } catch (Exception $ex) {
                Notification::error([
                    'title' => $errorTitle,
                    'message' => $ex->getMessage(),
                ], false);
            }
        }
    }

    public function load($boot = true)
    {
        self::loadPluginByName($this->name, $boot);
    }

    public static function installFromDir($name)
    {
        // Instantiate the plugin object, not save yet
        $plugin = self::firstOrNew(['name' => $name]);

        // plugin.name attribute is required
        // before we can get other information
        // Validation may throw an exception
        $composer = $plugin->getComposerJson();

        if ($plugin->name != $composer['name']) {
            throw new \Exception("Plugin name in composer.json is expected to be '{$plugin->name}', found '".$composer['name']."'");
        }

        $plugin->title = $composer['title'];
        $plugin->description = array_key_exists('description', $composer) ? $composer['description'] : null;
        $plugin->version = array_key_exists('version', $composer) ? $composer['version'] : '0.0.0';
        $plugin->status = self::STATUS_INACTIVE;
        $plugin->save();
        self::updatePluginMasterFile($plugin->name, ['status' => $plugin->status]);

        // Important: publish language files after installing a plugin
        // Remember to load the plugin first, make sure its service provider runs first
        $plugin->load($withServiceProvider = true);

        // Dump language files
        Language::dump();

        // Public plugins' assets with 'plugin' tag
        \Artisan::call('vendor:publish', ['--force' => true, '--tag' => 'plugin']);

        // Return it
        return $plugin;
    }

    public function getIconUrl()
    {
        $possibleFileNames = ['icon.svg', 'icon.jpg', 'icon.png', 'icon.gif'];

        foreach ($possibleFileNames as $file) {
            $absPath = $this->getStoragePath($file);
            if (file_exists($absPath)) {
                return \Acelle\Helpers\generatePublicPath($absPath);
            }
        }

        return;
    }

    /**
     * Check if plugin enabled.
     *
     * @return array
     */
    public static function enabled($name)
    {
        return self::where('name', '=', $name)
            ->where('status', '=', self::STATUS_ACTIVE)
            ->exists();
    }

    public function deleteAndCleanup()
    {
        $name = $this->name;
        Hook::execute('delete_plugin_'.$name);
        $this->deletePluginDirectory();
        $this->delete();
        self::updatePluginMasterFile($name, null);
    }

    public function deletePluginDirectory()
    {
        $file = $this->getPluginPath();

        if (!file_exists($file)) {
            return;
        }

        if (is_link($file) || is_file($file)) {
            \Illuminate\Support\Facades\File::delete($file);
        } else {
            \Illuminate\Support\Facades\File::deleteDirectory($file);
        }
    }

    public static function init($plugin)
    {
        $validator = Validator::make(['plugin' => $plugin], [
            'plugin' => [
                'required',
                'regex:/^[0-9a-z_]+\/[0-9a-z_]+$/',
                function ($attribute, $value, $fail) {
                    //
                },
            ],
        ]);

        if ($validator->fails()) {
            throw new Exception("Plugin name must be in the \"author/name\" format.\nOnly alphanumeric lowercase characters and underscores are accepted for author and name.\nFor example: awesomelab/my_plugin123");
        }

        list($author, $name) = explode('/', $plugin);

        $rules = [
            'min:3',
            'max:32',
            'regex:/^[a-z0-9]/',
            'regex:/[a-z0-9]$/',
            function ($attribute, $value, $fail) {
                if (preg_match('/__/', $value)) {
                    $fail("Invalid :attribute ':input', must not contain double underscores '__'");
                }
            }
        ];

        $messages = [
            'author.min' => 'Author name must be at least 3 characters in length',
            'name.min' => 'Plugin name must be at least 3 characters in length',
            'author.max' => 'Author name is too long',
            'name.max' => 'Plugin name is too long',
            'author.regex' => 'Author name ":input" is invalid. Name must start and end with a lowercase character.',
            'name.regex' => 'Plugin name ":input" is invalid. Name must start and end with a lowercase character.',
        ];

        $validator = Validator::make(
            ['author' => $author, 'name' => $name],
            ['author' => $rules, 'name' => $rules],
            $messages
        );

        if ($validator->fails()) {
            // throw the first error
            throw new Exception($validator->errors()->first());
        }

        // OK, make the plugin
        // Copy from template
        $templatePath = database_path('plugins/awesome/hello');
        $pluginPath = storage_path("app/plugins/{$plugin}");

        // Clone the template plugin
        pcopy($templatePath, $pluginPath);

        // Replace the templates with actual plugin names
        $loader = new \Twig\Loader\FilesystemLoader($pluginPath);
        $twig = new \Twig\Environment($loader);

        $params = [
            'plugin' => $plugin,
            'name' => $name,
            'author' => $author,
            'name_class' => self::makeClassNameFromString($name),
            'author_class' => self::makeClassNameFromString($author),
            'NAME' => strtoupper($name), // uppercase name
        ];

        // Files to replace
        $files = [
            'composer.json',
            'routes.php',
            'build.sh',
            'src/ServiceProvider.php',
            'src/Controllers/DashboardController.php',
            'resources/views/index.blade.php'
        ];

        foreach ($files as $file) {
            $fcontent = $fcontent = $twig->render($file, $params);
            file_put_contents(join_paths($pluginPath, $file), $fcontent);
        }

        // Register the plugin with Acelle (add a record to plugins DB table)
        self::installFromDir($plugin);
    }

    public static function makeClassNameFromString($name)
    {
        // Change "my_1st_plugin" to "My1StPlugin"
        return implode('', explode(' ', ucwords(preg_replace('/(?<=[0-9])(?=[a-z])|_/', ' ', $name))));
    }

    // This is the alternative to the static::autoload() function
    // Instead querying the DB, this function reads the list of plugins from master file
    // It is to avoid making DB queries which is not recommended in service provider
    public static function autoloadWithoutDbQuery()
    {
        // File storing plugin index (meta data)
        $index = self::getPluginMasterFile();

        // Get json data from file
        if (!file_exists($index)) {
            return;
        }

        $plugins = json_decode(file_get_contents($index), true);

        foreach ($plugins as $name => $attributes) {
            $status = $attributes['status'];

            // Currently, we need to load all plugins
            // In order to use its routes
            if ($status == self::STATUS_ACTIVE || self::STATUS_INACTIVE) {
                try {
                    self::loadPluginByName($name);
                } catch (\Throwable $t) {
                    // Important: this block cannot catch exception in plugin's service provider itself
                    // It is because the service is registered at this time with App::register(...)
                    // but shall be executed later on, not immediately
                    self::updatePluginMasterFile($name, [
                        'error' => $t->getMessage()
                    ]);
                }
            } else {
                // echo "Skipped $name\n";
            }
        }
    }

    public static function getStoragePathByName($name, $subpath = null, $absolute = true)
    {
        if (is_null($name)) {
            throw new \Exception("Plugin name is null");
        }

        if (preg_match('/^[a-z0-9\-]+\/[a-z0-9\-]+$/', $name) == 0) {
            throw new \Exception("Plugin name must match 'author/name' pattern. Only lowercase letters, numbers and/or dash (-) are allowed for 'author' and 'name'");
        }

        $base = join_paths(self::STORAGE_PATH, $name);

        if ($absolute) {
            $base = storage_path($base);
        }

        $path = join_paths($base, $subpath);
        return $path;
    }

    public static function getComposerJsonFilePathByName($name)
    {
        return self::getStoragePathByName($name, 'composer.json');
    }

    public static function getComposerJsonByName($name)
    {
        $pluginDir = self::getStoragePathByName($name);
        if (!file_exists($pluginDir)) {
            throw new \Exception("Plugin folder '{$pluginDir}' missing");
        }

        $composerFile = self::getComposerJsonFilePathByName($name);

        if (!file_exists($composerFile)) {
            throw new \Exception("Cannot find {$composerFile} in plugin folder. Invalid plugin?");
        }

        try {
            $composerJson = json_decode(file_get_contents($composerFile), true);
        } catch (\Exception $ex) {
            throw new \Exception("Cannot parse {$composerFile}"); // for easy debugging
        }

        return $composerJson;
    }

    public static function loadPluginByName($name, $boot = true)
    {
        $composerJson = self::getComposerJsonByName($name);

        if (!array_key_exists('autoload', $composerJson)) {
            throw new \Exception("Cannot boot plugin '{$name}'. No 'autoload' found in composer.json");
        }

        if (!array_key_exists('psr-4', $composerJson['autoload'])) {
            throw new \Exception("Cannot boot plugin '{$name}'. No 'autoload.psr4' found in composer.json");
        }

        $loader = new ClassLoader();

        $psr4 = $composerJson['autoload']['psr-4'];
        foreach ($psr4 as $prefix => $path) {
            $realpath = self::getStoragePathByName($name, $path);
            $loader->addPsr4($prefix, $realpath);
        }

        $loader->register();

        if (!$boot) {
            return;
        }

        // Load and boot ServiceProvider
        $serviceProviders = [];
        if (
            array_key_exists('extra', $composerJson) &&
            array_key_exists('laravel', $composerJson['extra']) &&
            array_key_exists('providers', $composerJson['extra']['laravel'])) {
            $serviceProviders = $composerJson['extra']['laravel']['providers'];
        }

        // Register service, bootstraping
        foreach ($serviceProviders as $service) {
            App::register($service);
        }
    }

    // Use a master file to manage a list of plugins
    // This file stores plugin status information, for example
    //     [
    //         'awesometeam/sample': { 'status' : 'active' },
    //         'others/myplugin': { 'status' : 'inactive' }
    //     ]
    //
    // Currently, only 'status' is required in this master file
    //
    public static function updatePluginMasterFile($name, $params)
    {
        try {
            // File storing plugin index (meta data)
            $index = self::getPluginMasterFile();

            // Get json data from file
            if (!file_exists($index)) {
                $data = [];
            } else {
                // @note: repeatedly refresh the admin dashboard and sometimes $data becomes null!
                $data = json_decode(file_get_contents($index), true);
            }

            if (is_null($params)) {
                // null means: delete plugin from index
                unset($data[$name]);
            } else {
                $plugininfo = array_key_exists($name, $data) ? $data[$name] : [];
                $plugininfo = array_merge($plugininfo, $params);
                $plugininfo = array_filter($plugininfo); // filter out null keys
                $data[$name] = $plugininfo;

                // Currently, only 'status' is required in this master file
                if (!array_key_exists('status', $data[$name])) {
                    throw new \Exception('Plugin\'s "status" attribute is required in plugin master file');
                }
            }

            // Write back to file
            file_put_contents($index, json_encode($data));
        } catch (\Throwable $t) {
            throw new Exception(trans('messages.plugin.error.cannot_load', ['p' => $name])." | ".$t->getMessage());
        }

    }

    public static function resetPluginMasterFile()
    {
        $index = self::getPluginMasterFile();

        if (file_exists($index)) {
            \Illuminate\Support\Facades\File::delete($index);
        }

        foreach (self::all() as $plugin) {
            self::updatePluginMasterFile($plugin->name, [
                'status' => $plugin->status,
                'error' => null, // drop error
            ]);
        }
    }

    public static function getPluginMasterFile()
    {
        return storage_path(self::INDEX_FILE);
    }

    public function getPluginInfo($key)
    {
        return self::getPluginInfoByName($this->name, $key);
    }

    public static function getPluginInfoByName($name, $key)
    {
        // File storing plugin index (meta data)
        $index = self::getPluginMasterFile();

        // Get json data from file
        if (!file_exists($index)) {
            $data = [];
        } else {
            $data = json_decode(file_get_contents($index), true);
        }

        $plugininfo = array_key_exists($name, $data) ? $data[$name] : [];

        if ($key) {
            return array_key_exists($key, $plugininfo) ? $plugininfo[$key] : null;
        } else {
            return $plugininfo;
        }
    }
}