Completed
Push — fix/local-update ( a71959 )
by Kiyotaka
06:13
created

PluginService::update()   A

Complexity

Conditions 4
Paths 25

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 25
nop 2
dl 0
loc 34
rs 9.376
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, $uninstall = false)
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
                if (!$uninstall) {
288
                    // プラグインmetadata定義を追加
289
                    $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
290
                    if (file_exists($entityDir)) {
291
                        $ormConfig = $this->entityManager->getConfiguration();
292
                        $chain = $ormConfig->getMetadataDriverImpl();
293
                        $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
294
                        $namespace = 'Plugin\\'.$config['code'].'\\Entity';
295
                        $chain->addDriver($driver, $namespace);
296
                        $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
297
                    }
298
                }
299
300
                // 一時的に利用するProxyを生成してからスキーマを更新する
301
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir, $uninstall);
302
                $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
303
            } finally {
304
                foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
305
                    unlink($f);
306
                }
307
                rmdir($tmpProxyOutputDir);
308
            }
309
        }
310
    }
311
312
    public function createTempDir()
313
    {
314
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
315
        @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...
316
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
317
318
        if (!mkdir($d, 0777)) {
319
            throw new PluginException(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
320
        }
321
322
        return $d;
323
    }
324
325
    public function deleteDirs($arr)
326
    {
327
        foreach ($arr as $dir) {
328
            if (file_exists($dir)) {
329
                $fs = new Filesystem();
330
                $fs->remove($dir);
331
            }
332
        }
333
    }
334
335
    /**
336
     * @param string $archive
337
     * @param string $dir
338
     *
339
     * @throws PluginException
340
     */
341
    public function unpackPluginArchive($archive, $dir)
342
    {
343
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
344
        try {
345
            if ($extension == 'zip') {
346
                $zip = new \ZipArchive();
347
                $zip->open($archive);
348
                $zip->extractTo($dir);
349
                $zip->close();
350
            } else {
351
                $phar = new \PharData($archive);
352
                $phar->extractTo($dir, null, true);
353
            }
354
        } catch (\Exception $e) {
355
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
356
        }
357
    }
358
359
    /**
360
     * @param $dir
361
     * @param array $config_cache
362
     *
363
     * @throws PluginException
364
     */
365
    public function checkPluginArchiveContent($dir, array $config_cache = [])
366
    {
367
        try {
368
            if (!empty($config_cache)) {
369
                $meta = $config_cache;
370
            } else {
371
                $meta = $this->readConfig($dir);
372
            }
373
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
374
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
375
        }
376
377
        if (!is_array($meta)) {
378
            throw new PluginException('config.yml not found or syntax error');
379
        }
380
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
381
            throw new PluginException('config.yml code empty or invalid_character(\W)');
382
        }
383
        if (!isset($meta['name'])) {
384
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
385
            throw new PluginException('config.yml name empty');
386
        }
387
        if (!isset($meta['version'])) {
388
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
389
            throw new PluginException('config.yml version invalid_character(\W) ');
390
        }
391
    }
392
393
    /**
394
     * @param $pluginDir
395
     *
396
     * @return array
397
     *
398
     * @throws PluginException
399
     */
400
    public function readConfig($pluginDir)
401
    {
402
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
403
        if (file_exists($composerJsonPath) === false) {
404
            throw new PluginException("${composerJsonPath} not found.");
405
        }
406
407
        $json = json_decode(file_get_contents($composerJsonPath), true);
408
        if ($json === null) {
409
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
410
        }
411
412
        if (!isset($json['version'])) {
413
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
414
        }
415
416
        if (!isset($json['extra']['code'])) {
417
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
418
        }
419
420
        return [
421
            'code' => $json['extra']['code'],
422
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
423
            'version' => $json['version'],
424
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
425
        ];
426
    }
427
428
    public function checkSymbolName($string)
429
    {
430
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
431
        // plugin_nameやplugin_codeに使える文字のチェック
432
        // a-z A-Z 0-9 _
433
        // ディレクトリ名などに使われれるので厳しめ
434
    }
435
436
    /**
437
     * @param string $path
438
     */
439
    public function deleteFile($path)
440
    {
441
        $f = new Filesystem();
442
        $f->remove($path);
443
    }
444
445
    public function checkSamePlugin($code)
446
    {
447
        /** @var Plugin $Plugin */
448
        $Plugin = $this->pluginRepository->findOneBy(['code' => $code]);
449
        if ($Plugin && $Plugin->isInitialized()) {
450
            throw new PluginException('plugin already installed.');
451
        }
452
    }
453
454
    public function calcPluginDir($code)
455
    {
456
        return $this->projectRoot.'/app/Plugin/'.$code;
457
    }
458
459
    /**
460
     * @param string $d
461
     *
462
     * @throws PluginException
463
     */
464
    public function createPluginDir($d)
465
    {
466
        $b = @mkdir($d);
467
        if (!$b) {
468
            throw new PluginException(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
469
        }
470
    }
471
472
    /**
473
     * @param $meta
474
     * @param int $source
475
     *
476
     * @return Plugin
477
     *
478
     * @throws PluginException
479
     */
480
    public function registerPlugin($meta, $source = 0)
481
    {
482
        try {
483
            $p = new Plugin();
484
            // インストール直後はプラグインは有効にしない
485
            $p->setName($meta['name'])
486
                ->setEnabled(false)
487
                ->setVersion($meta['version'])
488
                ->setSource($source)
489
                ->setCode($meta['code']);
490
491
            $this->entityManager->persist($p);
492
            $this->entityManager->flush($p);
493
494
            $this->pluginApiService->pluginInstalled($p);
495
        } catch (\Exception $e) {
496
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
497
        }
498
499
        return $p;
500
    }
501
502
    /**
503
     * @param $meta
504
     * @param string $method
505
     */
506
    public function callPluginManagerMethod($meta, $method)
507
    {
508
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
509
        if (class_exists($class)) {
510
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
511
            if (method_exists($installer, $method)) {
512
                $installer->$method($meta, $this->container);
513
            }
514
        }
515
    }
516
517
    /**
518
     * @param Plugin $plugin
519
     * @param bool $force
520
     *
521
     * @return bool
522
     *
523
     * @throws \Exception
524
     */
525
    public function uninstall(Plugin $plugin, $force = true)
526
    {
527
        $pluginDir = $this->calcPluginDir($plugin->getCode());
528
        $this->cacheUtil->clearCache();
529
        $config = $this->readConfig($pluginDir);
530
531
        if ($plugin->isEnabled()) {
532
            $this->disable($plugin);
533
        }
534
535
        // 初期化されていない場合はPluginManager#uninstall()は実行しない
536
        if ($plugin->isInitialized()) {
537
            $this->callPluginManagerMethod($config, 'uninstall');
538
        }
539
        $this->unregisterPlugin($plugin);
540
541
        // スキーマを更新する
542
        $this->generateProxyAndUpdateSchema($plugin, $config, true);
543
544
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
545
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
546
        $this->schemaService->dropTable($namespace);
547
548
        if ($force) {
549
            $this->deleteFile($pluginDir);
550
            $this->removeAssets($plugin->getCode());
551
        }
552
553
        $this->pluginApiService->pluginUninstalled($plugin);
554
555
        return true;
556
    }
557
558
    public function unregisterPlugin(Plugin $p)
559
    {
560
        try {
561
            $em = $this->entityManager;
562
            $em->remove($p);
563
            $em->flush();
564
        } catch (\Exception $e) {
565
            throw $e;
566
        }
567
    }
568
569
    public function disable(Plugin $plugin)
570
    {
571
        return $this->enable($plugin, false);
572
    }
573
574
    /**
575
     * Proxyを再生成します.
576
     *
577
     * @param Plugin $plugin プラグイン
578
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
579
     * @param string|null $outputDir 出力先
580
     * @param bool $uninstall プラグイン削除の場合はtrue
581
     *
582
     * @return array 生成されたファイルのパス
583
     */
584
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null, $uninstall = false)
585
    {
586
        if (is_null($outputDir)) {
587
            $outputDir = $this->projectRoot.'/app/proxy/entity';
588
        }
589
        @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...
590
591
        $enabledPluginCodes = array_map(
592
            function ($p) { return $p->getCode(); },
593
            $temporary ? $this->pluginRepository->findAll() : $this->pluginRepository->findAllEnabled()
594
        );
595
596
        $excludes = [];
597
        if (!$uninstall && ($temporary || $plugin->isEnabled())) {
598
            $enabledPluginCodes[] = $plugin->getCode();
599
        } else {
600
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
601
            if ($index !== false && $index >= 0) {
602
                array_splice($enabledPluginCodes, $index, 1);
603
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
604
            }
605
        }
606
607
        $enabledPluginEntityDirs = array_map(function ($code) {
608
            return $this->projectRoot."/app/Plugin/${code}/Entity";
609
        }, $enabledPluginCodes);
610
611
        return $this->entityProxyService->generate(
612
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
613
            $excludes,
614
            $outputDir
615
        );
616
    }
617
618
    public function enable(Plugin $plugin, $enable = true)
619
    {
620
        $em = $this->entityManager;
621
        try {
622
            $pluginDir = $this->calcPluginDir($plugin->getCode());
623
            $config = $this->readConfig($pluginDir);
624
            $em->getConnection()->beginTransaction();
625
            $plugin->setEnabled($enable ? true : false);
626
            $em->persist($plugin);
627
628
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
629
630
            // Proxyだけ再生成してスキーマは更新しない
631
            $this->regenerateProxy($plugin, false);
632
633
            $em->flush();
634
            $em->getConnection()->commit();
635
636
            if ($enable) {
637
                $this->pluginApiService->pluginEnabled($plugin);
638
            } else {
639
                $this->pluginApiService->pluginDisabled($plugin);
640
            }
641
        } catch (\Exception $e) {
642
            $em->getConnection()->rollback();
643
            throw $e;
644
        }
645
646
        return true;
647
    }
648
649
    /**
650
     * Update plugin
651
     *
652
     * @param Plugin $plugin
653
     * @param string $path
654
     *
655
     * @return bool
656
     *
657
     * @throws PluginException
658
     * @throws \Exception
659
     */
660
    public function update(Plugin $plugin, $path)
661
    {
662
        $pluginBaseDir = null;
663
        $tmp = null;
664
        try {
665
            $this->cacheUtil->clearCache();
666
            $tmp = $this->createTempDir();
667
668
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
669
            $this->checkPluginArchiveContent($tmp);
670
671
            $config = $this->readConfig($tmp);
672
673
            if ($plugin->getCode() != $config['code']) {
674
                throw new PluginException('new/old plugin code is different.');
675
            }
676
677
            $pluginBaseDir = $this->calcPluginDir($config['code']);
678
            $this->deleteFile($tmp); // テンポラリのファイルを削除
679
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
680
681
            $this->copyAssets($plugin->getCode());
682
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
683
        } catch (PluginException $e) {
684
            $this->deleteDirs([$tmp]);
685
            throw $e;
686
        } catch (\Exception $e) {
687
            // catch exception of composer
688
            $this->deleteDirs([$tmp]);
689
            throw $e;
690
        }
691
692
        return true;
693
    }
694
695
    /**
696
     * Update plugin
697
     *
698
     * @param Plugin $plugin
699
     * @param array  $meta     Config data
700
     *
701
     * @throws \Exception
702
     */
703
    public function updatePlugin(Plugin $plugin, $meta)
704
    {
705
        $em = $this->entityManager;
706
        try {
707
            $em->getConnection()->beginTransaction();
708
            $plugin->setVersion($meta['version'])
709
                ->setName($meta['name']);
710
711
            $em->persist($plugin);
712
713
            if ($plugin->isInitialized()) {
714
                $this->callPluginManagerMethod($meta, 'update');
715
            }
716
            $this->copyAssets($plugin->getCode());
717
            $em->flush();
718
            $em->getConnection()->commit();
719
        } catch (\Exception $e) {
720
            $em->getConnection()->rollback();
721
            throw $e;
722
        }
723
    }
724
725
    /**
726
     * Get array require by plugin
727
     * Todo: need define dependency plugin mechanism
728
     *
729
     * @param array|Plugin $plugin format as plugin from api
730
     *
731
     * @return array|mixed
732
     *
733
     * @throws PluginException
734
     */
735
    public function getPluginRequired($plugin)
736
    {
737
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
738
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
739
740
        $results = [];
741
742
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
743
            $results[] = $package;
744
        }, 'eccube-plugin');
745
746
        return $results;
747
    }
748
749
    /**
750
     * Find the dependent plugins that need to be disabled
751
     *
752
     * @param string $pluginCode
753
     *
754
     * @return array plugin code
755
     */
756
    public function findDependentPluginNeedDisable($pluginCode)
757
    {
758
        return $this->findDependentPlugin($pluginCode, true);
759
    }
760
761
    /**
762
     * Find the other plugin that has requires on it.
763
     * Check in both dtb_plugin table and <PluginCode>/composer.json
764
     *
765
     * @param string $pluginCode
766
     * @param bool   $enableOnly
767
     *
768
     * @return array plugin code
769
     */
770
    public function findDependentPlugin($pluginCode, $enableOnly = false)
771
    {
772
        $criteria = Criteria::create()
773
            ->where(Criteria::expr()->neq('code', $pluginCode));
774
        if ($enableOnly) {
775
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
776
        }
777
        /**
778
         * @var Plugin[]
779
         */
780
        $plugins = $this->pluginRepository->matching($criteria);
781
        $dependents = [];
782
        foreach ($plugins as $plugin) {
783
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
784
            $fileName = $dir.'/composer.json';
785
            if (!file_exists($fileName)) {
786
                continue;
787
            }
788
            $jsonText = file_get_contents($fileName);
789
            if ($jsonText) {
790
                $json = json_decode($jsonText, true);
791
                if (!isset($json['require'])) {
792
                    continue;
793
                }
794
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
795
                    $dependents[] = $plugin->getCode();
796
                }
797
            }
798
        }
799
800
        return $dependents;
801
    }
802
803
    /**
804
     * Get dependent plugin by code
805
     * It's base on composer.json
806
     * Return the plugin code and version in the format of the composer
807
     *
808
     * @param string   $pluginCode
809
     * @param int|null $libraryType
810
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
811
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
812
     *                      default : return all library/plugin
813
     *
814
     * @return array format [packageName1 => version1, packageName2 => version2]
815
     */
816
    public function getDependentByCode($pluginCode, $libraryType = null)
817
    {
818
        $pluginDir = $this->calcPluginDir($pluginCode);
819
        $jsonFile = $pluginDir.'/composer.json';
820
        if (!file_exists($jsonFile)) {
821
            return [];
822
        }
823
        $jsonText = file_get_contents($jsonFile);
824
        $json = json_decode($jsonText, true);
825
        $dependents = [];
826
        if (isset($json['require'])) {
827
            $require = $json['require'];
828
            switch ($libraryType) {
829 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...
830
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
831
                    break;
832
833 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...
834
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
835
                    break;
836
837
                default:
838
                    $dependents = $json['require'];
839
                    break;
840
            }
841
        }
842
843
        return $dependents;
844
    }
845
846
    /**
847
     * Format array dependent plugin to string
848
     * It is used for commands.
849
     *
850
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
851
     * @param bool  $getVersion
852
     *
853
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
854
     */
855
    public function parseToComposerCommand(array $packages, $getVersion = true)
856
    {
857
        $result = array_keys($packages);
858
        if ($getVersion) {
859
            $result = array_map(function ($package, $version) {
860
                return $package.':'.$version;
861
            }, array_keys($packages), array_values($packages));
862
        }
863
864
        return implode(' ', $result);
865
    }
866
867
    /**
868
     * リソースファイル等をコピー
869
     * コピー元となるファイルの置き場所は固定であり、
870
     * [プラグインコード]/Resource/assets
871
     * 配下に置かれているファイルが所定の位置へコピーされる
872
     *
873
     * @param $pluginCode
874
     */
875
    public function copyAssets($pluginCode)
876
    {
877
        $assetsDir = $this->calcPluginDir($pluginCode).'/Resource/assets';
878
879
        // プラグインにリソースファイルがあれば所定の位置へコピー
880
        if (file_exists($assetsDir)) {
881
            $file = new Filesystem();
882
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
883
        }
884
    }
885
886
    /**
887
     * コピーしたリソースファイル等を削除
888
     *
889
     * @param string $pluginCode
890
     */
891
    public function removeAssets($pluginCode)
892
    {
893
        $assetsDir = $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets';
894
895
        // コピーされているリソースファイルがあれば削除
896
        if (file_exists($assetsDir)) {
897
            $file = new Filesystem();
898
            $file->remove($assetsDir);
899
        }
900
    }
901
902
    /**
903
     * Plugin is exist check
904
     *
905
     * @param array  $plugins    get from api
906
     * @param string $pluginCode
907
     *
908
     * @return false|int|string
909
     */
910
    public function checkPluginExist($plugins, $pluginCode)
911
    {
912
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
913
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
914
        }
915
        // Find plugin in array
916
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
917
918
        return $index;
919
    }
920
921
    /**
922
     * @param string $code
923
     *
924
     * @return bool
925
     */
926
    private function isEnable($code)
927
    {
928
        $Plugin = $this->pluginRepository->findOneBy([
929
            'enabled' => Constant::ENABLED,
930
            'code' => $code,
931
        ]);
932
        if ($Plugin) {
933
            return true;
934
        }
935
936
        return false;
937
    }
938
}
939