Your IP : 216.73.216.79


Current Path : /var/www/html/ukas/backend/modules/mmngr/controllers/
Upload File :
Current File : /var/www/html/ukas/backend/modules/mmngr/controllers/ModuleImportController.php

<?php
namespace backend\modules\mmngr\controllers;

use Yii;
use yii\web\Controller;
use backend\modules\mmngr\models\ModuleImportSearch;
use backend\modules\mmngr\models\ModuleImport;
use yii\web\UploadedFile;
use ZipArchive;
use Exception;
use backend\modules\mmngr\models\ModuleRepositoryInstall;

/**
 * Module for import external module trough zip file.
 */
class ModuleImportController extends Controller
{

    public $container;

    public $modulePath = '/../../backend/modules';

    public $moduleClass = 'backend\modules';

    public $moduleClassName = 'Module';

    public $metaFileName = 'meta.json';

    public $moduleIdName = 'module_id';

    public $metaModuleName = 'Module.php';

    public $imageDir = 'images/modules';

    public function actionIndex()
    {
        $searchModel = new ModuleImportSearch();
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
            'imageDir' => $this->getImgDir()
        ]);
    }
    
    /**
     * Proccess zip file upload
     * @return \yii\web\Response
     */
    public function actionImport()
    {
        $m = new ModuleImport();
        if (Yii::$app->request->isPost) {
            try {
                $m->file = UploadedFile::getInstance($m, 'file');
                $this->unpack($m->file->tempName);
                return $this->asJson([
                    'success' => true,
                    'message' => 'ok!'
                ]);
            } catch (Exception $e) {
                Yii::$app->response->statusCode = 500;
                return $this->asJson([
                    'success' => false,
                    'message' => $e->getMessage()
                ]);
            }
        }
    }
    
    /**
     * Unpack zip and process all the content
     * @param string $file path to the zip file
     * @throws Exception
     */
    private function unpack(string $file)
    {
        $zip = new ZipArchive();
        $meta = null;
        $status = $zip->open($file);
        if ($status !== TRUE) {
            throw new Exception($this->ZipStatusString($status));
        }
        $filearr = null;
        if ($zip->numFiles > 0) {
            for ($i = 0; $i < $zip->numFiles; $i ++) {
                // get container name
                if ($i === 0) {
                    // fix problem with zip without container
                    list ($zipname) = explode('/', $zip->getNameIndex($i));
                    $this->container = $zipname . '/';
                }
                $filename = $zip->getNameIndex($i);
                $stat = $zip->statName($filename);
                if (trim(strtolower($filename)) === trim(strtolower($this->container . $this->metaFileName))) {
                    $content = $zip->getFromName($filename);
                    $meta = $this->checkMetaContent($content);
                }
                // get an array of filename
                if (! empty($filename) && $stat['size'] > 0) {
                    $filearr[] = trim(strtolower($filename));
                }
            }
            if (! empty($filearr)) {
                $this->checkRequiredFile($filearr);
            }
            if (! empty($meta)) {
                $module = $this->saveMeta($meta, $zip);
            }
            $zip->extractTo(Yii::getAlias('@webroot') . $this->modulePath);
            $zip->close();
        } else {
            throw new Exception('Empty Zip file');
        }
        
        Yii::$app->session->setFlash('success', 'Module Has been successfully install - Version ' . $module['version']);
        
        return $module;
    }
    
    /**
     * Remove file by module id
     * @param int $id
     * @return \yii\web\Response
     */
    public function actionRemove(int $id)
    {
        $m = ModuleImport::findOne($id);
        
        //run down sql
        if(!empty($m->data)) {
            $data = json_decode($m->data);
            
            if (!empty($data->sql->down)) {
                $this->downSql($data->sql->down);
            }
        }
        $repo = ModuleRepositoryInstall::find()->where([
            'module_id' => $id
        ])->one();
        
        //remove folder
        try {
            //delete the folder
            $this->deleteDir( Yii::getAlias('@webroot') . $this->modulePath . '/' . $m->module_id);
            //delete the data
            $m->delete();
            if(!empty($repo)) {
                $repo->delete();
            }
            
            Yii::$app->session->setFlash('success', 'Module Has Been removed');
            return $this->redirect(['/mmngr/module-import/']);
        } catch (\Exception $e) {
            Yii::$app->session->setFlash('danger', $e->getMessage());
            return $this->redirect(['/mmngr/module-import/']);
        }
    }
    
    /**
     * Check meta json validity
     * @param string $content
     * @throws Exception
     * @return mixed
     */
    private function checkMetaContent(string $content)
    {
        $meta = json_decode($content);

        if (json_last_error()) {
            throw new Exception('Error When Parsing Json! ' . json_last_error_msg());
        }
        if (! property_exists($meta, $this->moduleIdName)) {
            throw new Exception('Missing required meta attribute id');
        }
        if (! property_exists($meta, 'version')) {
            throw new Exception('Missing required meta attribute version');
        }
        if (trim(strtolower($meta->{$this->moduleIdName})) !== trim(strtolower($this->container), '/')) {
            throw new Exception('Module id must be the same with module container name');
        }
        if (preg_match('/[^a-zA-Z0-9\']/', trim(strtolower($meta->{$this->moduleIdName})))) {
            throw new Exception('Invalid Module Id, module id must alphanumeric no symbol is allowed');
        }

        $m = ModuleImport::checkIdExist(trim(strtolower($meta->module_id)), trim(strtolower($meta->version)));
        if (! $m) {
            throw new Exception('This version less than the current version');
        }
        
        return $meta;
    }

    /**
     * Process meta from meta file
     * @param string $meta Json string for meta
     * @param object $zip
     * @throws Exception
     * @return string | boolean return version on upgrade
     */
    private function saveMeta($meta, $zip)
    {
        $this->replaceMeta($meta->{$this->moduleIdName});
        $m = new ModuleImport();
        foreach (array_keys($m->attributeLabels()) as $key) {
            if (property_exists($meta, $key)) {
                $item = $meta->{$key};
                if (is_string($item)) {
                    $m->{$key} = $item;
                } else if (is_array($item)) {
                    $m->{$key} = json_encode($item);
                } else if (is_object($item)) {
                    $m->{$key} = json_encode($item);
                }
            }
        }
        
        if (property_exists($meta, 'image_path')) {
            if (is_array($meta->image_path)) {
                $img = $this->saveImage($meta->image_path, $zip);
                if (! empty($img)) {
                    $m->image_path = json_encode($img);
                }
            }
        }

        $m->class = "{$this->moduleClass}\\{$meta->module_id}\\{$this->moduleClassName}";

        if (property_exists($meta, 'data') && property_exists($meta->data, 'sql')) {
            
            $sql = $meta->data->sql;
            
            $upgradeQuery = null; //for upgrade
            
            if (! empty($sql->up)) {
                $query = $this->upSql($sql->up, $zip);
            } else {
                throw new Exception('Up properties missing for run SQL up on Sql Meta');
            }
            
            if(property_exists($sql, 'upgrade')) {
                if (property_exists($sql->upgrade, 'up') && ! empty($sql->upgrade->up)) {
                    $upgradeQuery = $this->upSql($sql->upgrade->up, $zip);
                }
            }
        }
        
        Yii::$app->db->transaction(function($db) use ($query, $upgradeQuery, $m) {
            try {
                $db->createCommand($query)->execute();
                if (! empty($upgradeQuery)) {
                    $db->createCommand($upgradeQuery)->execute();
                }
            } catch(\Throwable $t) {
                throw new Exception('Error Saving Module From Meta :-\n' . $t->getMessage());
            }
            
            if (! $m->save()) {
                throw new Exception('Error Saving Module From Meta' . json_encode($m->errors));
            }
        });
        return [
            'version' => $meta->version,
            'id' => $m->id
        ];
    }
    
    /***
     * Replace Meta for module import
     * @param string $moduleId
     * @return boolean true if module is upgrade
     */
    private function replaceMeta($moduleId){
        $m = ModuleImport::find()->where(['module_id' => $moduleId])->one();
        if (!empty($m)) {
            $m->delete();
            return true;
        }
        return false;
    }
    
    /***
     * Run sql querys
     * @param string $moduleId
     * @return boolean true if module is upgrade
     */
    private function upSql($filename, $zip)
    {
        $sql = $zip->getFromName($filename);
        if (! empty($sql)) {
            return $sql;
        } else {
            throw new \Exception('Cannot read sql file');
        }
    }
    
    /**
     * Run sql on module uninstallation
     * @param string $data
     * @return \yii\web\Response
     */
    private function downSql(string $data){
        try {
            $down = file_get_contents(Yii::getAlias('@webroot') . $this->modulePath . '/' . $data);
            Yii::$app->db->createCommand($down)->execute();
        } catch (\Exception $e){
            Yii::$app->session->setFlash('danger', $e->getMessage());
            return $this->redirect(['/mmngr/module-import/']);
        }
    }

    private function saveImage($images, $zip)
    {
        $dir = Yii::getAlias('@webroot') . '/' . $this->imageDir;
        if (! is_dir($dir)) {
            mkdir($dir, 0777, true);
        }

        if (is_array($images)) {
            $imgarr = [];
            foreach ($images as $image) {
                $img = $zip->getFromName($image);
                $im = imagecreatefromstring($img);
                $imgrand = preg_replace('/[^a-zA-Z0-9\']/', '_', $this->container . microtime());
                $imgarr[] = $imgrand . '.jpg';
                imagejpeg($im, $dir . '/' . $imgrand . '.jpg');
            }
            return $imgarr;
        }
    }
    
    /**
     * Check if the required file is present
     * @param array $filearr
     * @throws Exception
     */
    private function checkRequiredFile(array $filearr)
    {
        if (! in_array(trim(strtolower($this->container . $this->metaFileName)), $filearr)) {
            throw new Exception('Missing meta.json file!');
        }

        if (! in_array(trim(strtolower($this->container . $this->metaModuleName)), $filearr)) {
            throw new Exception('Missing Module.php file!');
        }
    }
    
    /**
     * Send zip status base on error code
     * @param string $status
     * @return string
     */
    private function ZipStatusString(string $status)
    {
        switch ((int) $status) {
            case ZipArchive::ER_OK:
                return 'N No error';
            case ZipArchive::ER_MULTIDISK:
                return 'N Multi-disk zip archives not supported';
            case ZipArchive::ER_RENAME:
                return 'S Renaming temporary file failed';
            case ZipArchive::ER_CLOSE:
                return 'S Closing zip archive failed';
            case ZipArchive::ER_SEEK:
                return 'S Seek error';
            case ZipArchive::ER_READ:
                return 'S Read error';
            case ZipArchive::ER_WRITE:
                return 'S Write error';
            case ZipArchive::ER_CRC:
                return 'N CRC error';
            case ZipArchive::ER_ZIPCLOSED:
                return 'N Containing zip archive was closed';
            case ZipArchive::ER_NOENT:
                return 'N No such file';
            case ZipArchive::ER_EXISTS:
                return 'N File already exists';
            case ZipArchive::ER_OPEN:
                return 'S Can\'t open file';
            case ZipArchive::ER_TMPOPEN:
                return 'S Failure to create temporary file';
            case ZipArchive::ER_ZLIB:
                return 'Z Zlib error';
            case ZipArchive::ER_MEMORY:
                return 'N Malloc failure';
            case ZipArchive::ER_CHANGED:
                return 'N Entry has been changed';
            case ZipArchive::ER_COMPNOTSUPP:
                return 'N Compression method not supported';
            case ZipArchive::ER_EOF:
                return 'N Premature EOF';
            case ZipArchive::ER_INVAL:
                return 'N Invalid argument';
            case ZipArchive::ER_NOZIP:
                return 'N Not a zip archive';
            case ZipArchive::ER_INTERNAL:
                return 'N Internal error';
            case ZipArchive::ER_INCONS:
                return 'N Zip archive inconsistent';
            case ZipArchive::ER_REMOVE:
                return 'S Can\'t remove file';
            case ZipArchive::ER_DELETED:
                return 'N Entry has been deleted';

            default:
                return sprintf('Unknown status %s', $status);
        }
    }
    
    /**
     * Delete the provided path
     * @param string $dirPath delete path
     * @throws Exception
     */
    private function deleteDir(string $dirPath)
    {
        try {
            if (! is_dir($dirPath)) {
                throw new Exception("$dirPath must be a directory");
            }
            if (substr($dirPath, strlen($dirPath) - 1, 1) != '/') {
                $dirPath .= '/';
            }
            $files = scandir($dirPath);
            foreach ($files as $file) {
                if ($file === '.' || $file === '..')
                    continue;
                if (is_dir($dirPath . $file)) {
                    $this->deleteDir($dirPath . $file);
                } else {
                    if ($dirPath . $file !== __FILE__) {
                        unlink($dirPath . $file);
                    }
                }
            }
            
                rmdir($dirPath);
        } catch (\Throwable $t) {
            //silent error folder not exist on module remove
        }
    }
    
    /**
     * Return image path
     * @return string
     */
    public function getImgDir() {
        return Yii::getAlias('@web') . '/' . $this->imageDir;
    }
    
    /**
     * External install provider
     * @param string $path Path to file
     **/
    public function actionRepositoryInstall($path=null) {
        
        $post = Yii::$app->request->post();

        if (empty($path))
            $path = $post['path'];

        if (! empty($post)) {
            try {
                // unpack and processing
                $module = $this->unpack($path);
                
                $repo = new ModuleRepositoryInstall();
                
                $repo->repo_module_id = $post['module_id'];
                $repo->name = $post['name'];
                $repo->version = $post['version'];
                $repo->url = $post['url'];
                $repo->module_id = $module['id'];

                if (! $repo->save()) {
                    throw new Exception('Can\'t Save repository installation data');
                }

                return $this->asJson([
                    'success' => true,
                    'message' => 'ok!'
                ]);
            } catch (Exception $e) {
                
                Yii::$app->response->statusCode = 500;
                
                return $this->asJson([
                    'success' => false,
                    'message' => $e->getMessage()
                ]);
            }
        }
    }
}