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