Completed
Push — dev/plugin-misc ( 80e15a...5b9e26 )
by Kiyotaka
05:50
created

PluginService::createPluginDir()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service;
15
16
use Doctrine\Common\Collections\Criteria;
17
use Doctrine\ORM\EntityManager;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Eccube\Common\Constant;
20
use Eccube\Common\EccubeConfig;
21
use Eccube\Entity\Plugin;
22
use Eccube\Exception\PluginException;
23
use Eccube\Repository\PluginRepository;
24
use Eccube\Service\Composer\ComposerServiceInterface;
25
use Eccube\Util\CacheUtil;
26
use Eccube\Util\StringUtil;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
use Symfony\Component\Filesystem\Filesystem;
29
30
class PluginService
31
{
32
    /**
33
     * @var EccubeConfig
34
     */
35
    protected $eccubeConfig;
36
37
    /**
38
     * @var EntityManager
39
     */
40
    protected $entityManager;
41
42
    /**
43
     * @var PluginRepository
44
     */
45
    protected $pluginRepository;
46
47
    /**
48
     * @var EntityProxyService
49
     */
50
    protected $entityProxyService;
51
52
    /**
53
     * @var SchemaService
54
     */
55
    protected $schemaService;
56
57
    /**
58
     * @var ComposerServiceInterface
59
     */
60
    protected $composerService;
61
62
    const VENDOR_NAME = 'ec-cube';
63
64
    /**
65
     * Plugin type/library of ec-cube
66
     */
67
    const ECCUBE_LIBRARY = 1;
68
69
    /**
70
     * Plugin type/library of other (except ec-cube)
71
     */
72
    const OTHER_LIBRARY = 2;
73
74
    /**
75
     * @var string %kernel.project_dir%
76
     */
77
    private $projectRoot;
78
79
    /**
80
     * @var string %kernel.environment%
81
     */
82
    private $environment;
83
84
    /**
85
     * @var ContainerInterface
86
     */
87
    protected $container;
88
89
    /** @var CacheUtil */
90
    protected $cacheUtil;
91
92
    /**
93
     * @var PluginApiService
94
     */
95
    private $pluginApiService;
96
97
    /**
98
     * PluginService constructor.
99
     *
100
     * @param EntityManagerInterface $entityManager
101
     * @param PluginRepository $pluginRepository
102
     * @param EntityProxyService $entityProxyService
103
     * @param SchemaService $schemaService
104
     * @param EccubeConfig $eccubeConfig
105
     * @param ContainerInterface $container
106
     * @param CacheUtil $cacheUtil
107
     * @param ComposerServiceInterface $composerService
108
     * @param PluginApiService $pluginApiService
109
     */
110
    public function __construct(
111
        EntityManagerInterface $entityManager,
112
        PluginRepository $pluginRepository,
113
        EntityProxyService $entityProxyService,
114
        SchemaService $schemaService,
115
        EccubeConfig $eccubeConfig,
116
        ContainerInterface $container,
117
        CacheUtil $cacheUtil,
118
        ComposerServiceInterface $composerService,
119
        PluginApiService $pluginApiService
120
    ) {
121
        $this->entityManager = $entityManager;
0 ignored issues
show
Documentation Bug introduced by
$entityManager is of type object<Doctrine\ORM\EntityManagerInterface>, but the property $entityManager was declared to be of type object<Doctrine\ORM\EntityManager>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
122
        $this->pluginRepository = $pluginRepository;
123
        $this->entityProxyService = $entityProxyService;
124
        $this->schemaService = $schemaService;
125
        $this->eccubeConfig = $eccubeConfig;
126
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
127
        $this->environment = $eccubeConfig->get('kernel.environment');
128
        $this->container = $container;
129
        $this->cacheUtil = $cacheUtil;
130
        $this->composerService = $composerService;
131
        $this->pluginApiService = $pluginApiService;
132
    }
133
134
    /**
135
     * ファイル指定してのプラグインインストール
136
     *
137
     * @param string $path   path to tar.gz/zip plugin file
138
     * @param int    $source
139
     *
140
     * @return boolean
141
     *
142
     * @throws PluginException
143
     * @throws \Exception
144
     */
145
    public function install($path, $source = 0)
146
    {
147
        $pluginBaseDir = null;
148
        $tmp = null;
149
        try {
150
            // プラグイン配置前に実施する処理
151
            $this->preInstall();
152
            $tmp = $this->createTempDir();
153
154
            // 一旦テンポラリに展開
155
            $this->unpackPluginArchive($path, $tmp);
156
            $this->checkPluginArchiveContent($tmp);
157
158
            $config = $this->readConfig($tmp);
159
            // テンポラリのファイルを削除
160
            $this->deleteFile($tmp);
161
162
            // 重複していないかチェック
163
            $this->checkSamePlugin($config['code']);
164
165
            $pluginBaseDir = $this->calcPluginDir($config['code']);
166
            // 本来の置き場所を作成
167
            $this->createPluginDir($pluginBaseDir);
168
169
            // 問題なければ本当のplugindirへ
170
            $this->unpackPluginArchive($path, $pluginBaseDir);
171
172
            // Check dependent plugin
173
            // Don't install ec-cube library
174
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
175
//            if (!empty($dependents)) {
176
//                $package = $this->parseToComposerCommand($dependents);
177
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
178
//                $this->composerService->execRequire($package);
179
//            }
180
            // リソースファイルをコピー
181
            $this->copyAssets($config['code']);
182
            // プラグイン配置後に実施する処理
183
            $this->postInstall($config, $source);
184
        } catch (PluginException $e) {
185
            $this->deleteDirs([$tmp, $pluginBaseDir]);
186
            throw $e;
187
        } catch (\Exception $e) {
188
            // インストーラがどんなExceptionを上げるかわからないので
189
            $this->deleteDirs([$tmp, $pluginBaseDir]);
190
            throw $e;
191
        }
192
193
        return true;
194
    }
195
196
    /**
197
     * @param $code string sプラグインコード
198
     *
199
     * @throws PluginException
200
     */
201
    public function installWithCode($code)
202
    {
203
        $pluginDir = $this->calcPluginDir($code);
204
        $this->checkPluginArchiveContent($pluginDir);
205
        $config = $this->readConfig($pluginDir);
206
207
        if (isset($config['source']) && $config['source']) {
208
            // 依存プラグインが有効になっていない場合はエラー
209
            $requires = $this->getPluginRequired($config);
210 View Code Duplication
            $notInstalledOrDisabled = array_filter($requires, function ($req) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
                $code = preg_replace('/^ec-cube\//', '', $req['name']);
212
                /** @var Plugin $DependPlugin */
213
                $DependPlugin = $this->pluginRepository->findOneBy(['code' => $code]);
214
215
                return $DependPlugin ? $DependPlugin->isEnabled() == false : true;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
216
            });
217
218
            if (!empty($notInstalledOrDisabled)) {
219
                $names = array_map(function ($p) { return $p['name']; }, $notInstalledOrDisabled);
220
                throw new PluginException(implode(', ', $names).'を有効化してください。');
221
            }
222
        }
223
224
        $this->checkSamePlugin($config['code']);
225
        $this->copyAssets($config['code']);
226
        $this->postInstall($config, $config['source']);
227
    }
228
229
    // インストール事前処理
230
    public function preInstall()
231
    {
232
        // キャッシュの削除
233
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
234
//        $this->cacheUtil->clearCache();
235
    }
236
237
    // インストール事後処理
238
    public function postInstall($config, $source)
239
    {
240
        // dbにプラグイン登録
241
242
        $this->entityManager->getConnection()->beginTransaction();
243
244
        try {
245
            $Plugin = $this->pluginRepository->findByCode($config['code']);
246
247
            if (!$Plugin) {
248
                $Plugin = new Plugin();
249
                // インストール直後はプラグインは有効にしない
250
                $Plugin->setName($config['name'])
251
                    ->setEnabled(false)
252
                    ->setVersion($config['version'])
253
                    ->setSource($source)
254
                    ->setCode($config['code']);
255
                $this->entityManager->persist($Plugin);
256
                $this->entityManager->flush();
257
            }
258
259
            $this->generateProxyAndUpdateSchema($Plugin, $config);
260
261
            $this->callPluginManagerMethod($config, 'install');
262
263
            $Plugin->setInitialized(true);
264
            $this->entityManager->persist($Plugin);
265
            $this->entityManager->flush();
266
267
            $this->entityManager->flush();
268
            $this->entityManager->getConnection()->commit();
269
        } catch (\Exception $e) {
270
            $this->entityManager->getConnection()->rollback();
271
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
272
        }
273
    }
274
275
    public function generateProxyAndUpdateSchema(Plugin $plugin, $config)
276
    {
277
        if ($plugin->isEnabled()) {
278
            $generatedFiles = $this->regenerateProxy($plugin, false);
279
            $this->schemaService->updateSchema($generatedFiles, $this->projectRoot.'/app/proxy/entity');
280
        } else {
281
            // Proxyのクラスをロードせずにスキーマを更新するために、
282
            // インストール時には一時的なディレクトリにProxyを生成する
283
            $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
284
            @mkdir($tmpProxyOutputDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
285
286
            try {
287
                // プラグインmetadata定義を追加
288
                $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
289
                if (file_exists($entityDir)) {
290
                    $ormConfig = $this->entityManager->getConfiguration();
291
                    $chain = $ormConfig->getMetadataDriverImpl();
292
                    $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
293
                    $namespace = 'Plugin\\'.$config['code'].'\\Entity';
294
                    $chain->addDriver($driver, $namespace);
295
                    $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
296
                }
297
298
                // 一時的に利用するProxyを生成してからスキーマを更新する
299
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
300
                $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
301
            } finally {
302
                foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
303
                    unlink($f);
304
                }
305
                rmdir($tmpProxyOutputDir);
306
            }
307
        }
308
    }
309
310
    public function createTempDir()
311
    {
312
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
313
        @mkdir($tempDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
314
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
315
316
        if (!mkdir($d, 0777)) {
317
            throw new PluginException(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
318
        }
319
320
        return $d;
321
    }
322
323
    public function deleteDirs($arr)
324
    {
325
        foreach ($arr as $dir) {
326
            if (file_exists($dir)) {
327
                $fs = new Filesystem();
328
                $fs->remove($dir);
329
            }
330
        }
331
    }
332
333
    /**
334
     * @param string $archive
335
     * @param string $dir
336
     *
337
     * @throws PluginException
338
     */
339
    public function unpackPluginArchive($archive, $dir)
340
    {
341
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
342
        try {
343
            if ($extension == 'zip') {
344
                $zip = new \ZipArchive();
345
                $zip->open($archive);
346
                $zip->extractTo($dir);
347
                $zip->close();
348
            } else {
349
                $phar = new \PharData($archive);
350
                $phar->extractTo($dir, null, true);
351
            }
352
        } catch (\Exception $e) {
353
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
354
        }
355
    }
356
357
    /**
358
     * @param $dir
359
     * @param array $config_cache
360
     *
361
     * @throws PluginException
362
     */
363
    public function checkPluginArchiveContent($dir, array $config_cache = [])
364
    {
365
        try {
366
            if (!empty($config_cache)) {
367
                $meta = $config_cache;
368
            } else {
369
                $meta = $this->readConfig($dir);
370
            }
371
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
372
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
373
        }
374
375
        if (!is_array($meta)) {
376
            throw new PluginException('config.yml not found or syntax error');
377
        }
378
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
379
            throw new PluginException('config.yml code empty or invalid_character(\W)');
380
        }
381
        if (!isset($meta['name'])) {
382
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
383
            throw new PluginException('config.yml name empty');
384
        }
385
        if (!isset($meta['version'])) {
386
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
387
            throw new PluginException('config.yml version invalid_character(\W) ');
388
        }
389
    }
390
391
    /**
392
     * @param $pluginDir
393
     *
394
     * @return array
395
     *
396
     * @throws PluginException
397
     */
398
    public function readConfig($pluginDir)
399
    {
400
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
401
        if (file_exists($composerJsonPath) === false) {
402
            throw new PluginException("${composerJsonPath} not found.");
403
        }
404
405
        $json = json_decode(file_get_contents($composerJsonPath), true);
406
        if ($json === null) {
407
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
408
        }
409
410
        if (!isset($json['version'])) {
411
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
412
        }
413
414
        if (!isset($json['extra']['code'])) {
415
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
416
        }
417
418
        return [
419
            'code' => $json['extra']['code'],
420
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
421
            'version' => $json['version'],
422
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
423
        ];
424
    }
425
426
    public function checkSymbolName($string)
427
    {
428
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
429
        // plugin_nameやplugin_codeに使える文字のチェック
430
        // a-z A-Z 0-9 _
431
        // ディレクトリ名などに使われれるので厳しめ
432
    }
433
434
    /**
435
     * @param string $path
436
     */
437
    public function deleteFile($path)
438
    {
439
        $f = new Filesystem();
440
        $f->remove($path);
441
    }
442
443
    public function checkSamePlugin($code)
444
    {
445
        /** @var Plugin $Plugin */
446
        $Plugin = $this->pluginRepository->findOneBy(['code' => $code]);
447
        if ($Plugin && $Plugin->isInitialized()) {
448
            throw new PluginException('plugin already installed.');
449
        }
450
    }
451
452
    public function calcPluginDir($code)
453
    {
454
        return $this->projectRoot.'/app/Plugin/'.$code;
455
    }
456
457
    /**
458
     * @param string $d
459
     *
460
     * @throws PluginException
461
     */
462
    public function createPluginDir($d)
463
    {
464
        $b = @mkdir($d);
465
        if (!$b) {
466
            throw new PluginException(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
467
        }
468
    }
469
470
    /**
471
     * @param $meta
472
     * @param int $source
473
     *
474
     * @return Plugin
475
     *
476
     * @throws PluginException
477
     */
478
    public function registerPlugin($meta, $source = 0)
479
    {
480
        try {
481
            $p = new Plugin();
482
            // インストール直後はプラグインは有効にしない
483
            $p->setName($meta['name'])
484
                ->setEnabled(false)
485
                ->setVersion($meta['version'])
486
                ->setSource($source)
487
                ->setCode($meta['code']);
488
489
            $this->entityManager->persist($p);
490
            $this->entityManager->flush($p);
491
492
            $this->pluginApiService->pluginInstalled($p);
493
        } catch (\Exception $e) {
494
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
495
        }
496
497
        return $p;
498
    }
499
500
    /**
501
     * @param $meta
502
     * @param string $method
503
     */
504
    public function callPluginManagerMethod($meta, $method)
505
    {
506
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
507
        if (class_exists($class)) {
508
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
509
            if (method_exists($installer, $method)) {
510
                $installer->$method($meta, $this->container);
511
            }
512
        }
513
    }
514
515
    /**
516
     * @param Plugin $plugin
517
     * @param bool $force
518
     *
519
     * @return bool
520
     *
521
     * @throws \Exception
522
     */
523
    public function uninstall(Plugin $plugin, $force = true)
524
    {
525
        $pluginDir = $this->calcPluginDir($plugin->getCode());
526
        $this->cacheUtil->clearCache();
527
        $config = $this->readConfig($pluginDir);
528
529
        if ($plugin->isEnabled()) {
530
            $this->disable($plugin);
531
        }
532
533
        // 初期化されていない場合はPluginManager#uninstall()は実行しない
534
        if ($plugin->isInitialized()) {
535
            $this->callPluginManagerMethod($config, 'uninstall');
536
        }
537
        $this->unregisterPlugin($plugin);
538
539
        // スキーマを更新する
540
        //FIXME: Update schema before no affect
541
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
542
543
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
544
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
545
        $this->schemaService->dropTable($namespace);
546
547
        if ($force) {
548
            $this->deleteFile($pluginDir);
549
            $this->removeAssets($plugin->getCode());
550
        }
551
552
        $this->pluginApiService->pluginUninstalled($plugin);
553
554
        return true;
555
    }
556
557
    public function unregisterPlugin(Plugin $p)
558
    {
559
        try {
560
            $em = $this->entityManager;
561
            $em->remove($p);
562
            $em->flush();
563
        } catch (\Exception $e) {
564
            throw $e;
565
        }
566
    }
567
568
    public function disable(Plugin $plugin)
569
    {
570
        return $this->enable($plugin, false);
571
    }
572
573
    /**
574
     * Proxyを再生成します.
575
     *
576
     * @param Plugin $plugin プラグイン
577
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
578
     * @param string|null $outputDir 出力先
579
     *
580
     * @return array 生成されたファイルのパス
581
     */
582
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
583
    {
584
        if (is_null($outputDir)) {
585
            $outputDir = $this->projectRoot.'/app/proxy/entity';
586
        }
587
        @mkdir($outputDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
588
589
        $enabledPluginCodes = array_map(
590
            function ($p) { return $p->getCode(); },
591
            $temporary ? $this->pluginRepository->findAll() : $this->pluginRepository->findAllEnabled()
592
        );
593
594
        $excludes = [];
595
        if ($temporary || $plugin->isEnabled()) {
596
            $enabledPluginCodes[] = $plugin->getCode();
597
        } else {
598
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
599
            if ($index >= 0) {
600
                array_splice($enabledPluginCodes, $index, 1);
601
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
602
            }
603
        }
604
605
        $enabledPluginEntityDirs = array_map(function ($code) {
606
            return $this->projectRoot."/app/Plugin/${code}/Entity";
607
        }, $enabledPluginCodes);
608
609
        return $this->entityProxyService->generate(
610
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
611
            $excludes,
612
            $outputDir
613
        );
614
    }
615
616
    public function enable(Plugin $plugin, $enable = true)
617
    {
618
        $em = $this->entityManager;
619
        try {
620
            $pluginDir = $this->calcPluginDir($plugin->getCode());
621
            $config = $this->readConfig($pluginDir);
622
            $em->getConnection()->beginTransaction();
623
            $plugin->setEnabled($enable ? true : false);
624
            $em->persist($plugin);
625
626
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
627
628
            // Proxyだけ再生成してスキーマは更新しない
629
            $this->regenerateProxy($plugin, false);
630
631
            $em->flush();
632
            $em->getConnection()->commit();
633
634
            if ($enable) {
635
                $this->pluginApiService->pluginEnabled($plugin);
636
            } else {
637
                $this->pluginApiService->pluginDisabled($plugin);
638
            }
639
        } catch (\Exception $e) {
640
            $em->getConnection()->rollback();
641
            throw $e;
642
        }
643
644
        return true;
645
    }
646
647
    /**
648
     * Update plugin
649
     *
650
     * @param Plugin $plugin
651
     * @param string $path
652
     *
653
     * @return bool
654
     *
655
     * @throws PluginException
656
     * @throws \Exception
657
     */
658
    public function update(Plugin $plugin, $path)
659
    {
660
        $pluginBaseDir = null;
661
        $tmp = null;
662
        try {
663
            $this->cacheUtil->clearCache();
664
            $tmp = $this->createTempDir();
665
666
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
667
            $this->checkPluginArchiveContent($tmp);
668
669
            $config = $this->readConfig($tmp);
670
671
            if ($plugin->getCode() != $config['code']) {
672
                throw new PluginException('new/old plugin code is different.');
673
            }
674
675
            $pluginBaseDir = $this->calcPluginDir($config['code']);
676
            $this->deleteFile($tmp); // テンポラリのファイルを削除
677
678
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
679
680
            // Check dependent plugin
681
            // Don't install ec-cube library
682
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
683
            if (!empty($dependents)) {
684
                $package = $this->parseToComposerCommand($dependents);
685
                $this->composerService->execRequire($package);
686
            }
687
688
            $this->copyAssets($plugin->getCode());
689
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
690
        } catch (PluginException $e) {
691
            $this->deleteDirs([$tmp]);
692
            throw $e;
693
        } catch (\Exception $e) {
694
            // catch exception of composer
695
            $this->deleteDirs([$tmp]);
696
            throw $e;
697
        }
698
699
        return true;
700
    }
701
702
    /**
703
     * Update plugin
704
     *
705
     * @param Plugin $plugin
706
     * @param array  $meta     Config data
707
     *
708
     * @throws \Exception
709
     */
710
    public function updatePlugin(Plugin $plugin, $meta)
711
    {
712
        $em = $this->entityManager;
713
        try {
714
            $em->getConnection()->beginTransaction();
715
            $plugin->setVersion($meta['version'])
716
                ->setName($meta['name']);
717
718
            $em->persist($plugin);
719
            $this->callPluginManagerMethod($meta, 'update');
720
            $this->copyAssets($plugin->getCode());
721
            $em->flush();
722
            $em->getConnection()->commit();
723
        } catch (\Exception $e) {
724
            $em->getConnection()->rollback();
725
            throw $e;
726
        }
727
    }
728
729
    /**
730
     * Get array require by plugin
731
     * Todo: need define dependency plugin mechanism
732
     *
733
     * @param array|Plugin $plugin format as plugin from api
734
     *
735
     * @return array|mixed
736
     *
737
     * @throws PluginException
738
     */
739
    public function getPluginRequired($plugin)
740
    {
741
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
742
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
743
744
        $results = [];
745
746
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
747
            $results[] = $package;
748
        }, 'eccube-plugin');
749
750
        return $results;
751
    }
752
753
    /**
754
     * Find the dependent plugins that need to be disabled
755
     *
756
     * @param string $pluginCode
757
     *
758
     * @return array plugin code
759
     */
760
    public function findDependentPluginNeedDisable($pluginCode)
761
    {
762
        return $this->findDependentPlugin($pluginCode, true);
763
    }
764
765
    /**
766
     * Find the other plugin that has requires on it.
767
     * Check in both dtb_plugin table and <PluginCode>/composer.json
768
     *
769
     * @param string $pluginCode
770
     * @param bool   $enableOnly
771
     *
772
     * @return array plugin code
773
     */
774
    public function findDependentPlugin($pluginCode, $enableOnly = false)
775
    {
776
        $criteria = Criteria::create()
777
            ->where(Criteria::expr()->neq('code', $pluginCode));
778
        if ($enableOnly) {
779
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
780
        }
781
        /**
782
         * @var Plugin[]
783
         */
784
        $plugins = $this->pluginRepository->matching($criteria);
785
        $dependents = [];
786
        foreach ($plugins as $plugin) {
787
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
788
            $fileName = $dir.'/composer.json';
789
            if (!file_exists($fileName)) {
790
                continue;
791
            }
792
            $jsonText = file_get_contents($fileName);
793
            if ($jsonText) {
794
                $json = json_decode($jsonText, true);
795
                if (!isset($json['require'])) {
796
                    continue;
797
                }
798
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
799
                    $dependents[] = $plugin->getCode();
800
                }
801
            }
802
        }
803
804
        return $dependents;
805
    }
806
807
    /**
808
     * Get dependent plugin by code
809
     * It's base on composer.json
810
     * Return the plugin code and version in the format of the composer
811
     *
812
     * @param string   $pluginCode
813
     * @param int|null $libraryType
814
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
815
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
816
     *                      default : return all library/plugin
817
     *
818
     * @return array format [packageName1 => version1, packageName2 => version2]
819
     */
820
    public function getDependentByCode($pluginCode, $libraryType = null)
821
    {
822
        $pluginDir = $this->calcPluginDir($pluginCode);
823
        $jsonFile = $pluginDir.'/composer.json';
824
        if (!file_exists($jsonFile)) {
825
            return [];
826
        }
827
        $jsonText = file_get_contents($jsonFile);
828
        $json = json_decode($jsonText, true);
829
        $dependents = [];
830
        if (isset($json['require'])) {
831
            $require = $json['require'];
832
            switch ($libraryType) {
833 View Code Duplication
                case self::ECCUBE_LIBRARY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
834
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
835
                    break;
836
837 View Code Duplication
                case self::OTHER_LIBRARY:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
838
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
839
                    break;
840
841
                default:
842
                    $dependents = $json['require'];
843
                    break;
844
            }
845
        }
846
847
        return $dependents;
848
    }
849
850
    /**
851
     * Format array dependent plugin to string
852
     * It is used for commands.
853
     *
854
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
855
     * @param bool  $getVersion
856
     *
857
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
858
     */
859
    public function parseToComposerCommand(array $packages, $getVersion = true)
860
    {
861
        $result = array_keys($packages);
862
        if ($getVersion) {
863
            $result = array_map(function ($package, $version) {
864
                return $package.':'.$version;
865
            }, array_keys($packages), array_values($packages));
866
        }
867
868
        return implode(' ', $result);
869
    }
870
871
    /**
872
     * リソースファイル等をコピー
873
     * コピー元となるファイルの置き場所は固定であり、
874
     * [プラグインコード]/Resource/assets
875
     * 配下に置かれているファイルが所定の位置へコピーされる
876
     *
877
     * @param $pluginCode
878
     */
879
    public function copyAssets($pluginCode)
880
    {
881
        $assetsDir = $this->calcPluginDir($pluginCode).'/Resource/assets';
882
883
        // プラグインにリソースファイルがあれば所定の位置へコピー
884
        if (file_exists($assetsDir)) {
885
            $file = new Filesystem();
886
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
887
        }
888
    }
889
890
    /**
891
     * コピーしたリソースファイル等を削除
892
     *
893
     * @param string $pluginCode
894
     */
895
    public function removeAssets($pluginCode)
896
    {
897
        $assetsDir = $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets';
898
899
        // コピーされているリソースファイルがあれば削除
900
        if (file_exists($assetsDir)) {
901
            $file = new Filesystem();
902
            $file->remove($assetsDir);
903
        }
904
    }
905
906
    /**
907
     * Plugin is exist check
908
     *
909
     * @param array  $plugins    get from api
910
     * @param string $pluginCode
911
     *
912
     * @return false|int|string
913
     */
914
    public function checkPluginExist($plugins, $pluginCode)
915
    {
916
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
917
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
918
        }
919
        // Find plugin in array
920
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
921
922
        return $index;
923
    }
924
925
    /**
926
     * @param string $code
927
     *
928
     * @return bool
929
     */
930
    private function isEnable($code)
931
    {
932
        $Plugin = $this->pluginRepository->findOneBy([
933
            'enabled' => Constant::ENABLED,
934
            'code' => $code,
935
        ]);
936
        if ($Plugin) {
937
            return true;
938
        }
939
940
        return false;
941
    }
942
}
943