Completed
Push — dev/recommend-plugins ( 237cf5...24efa6 )
by Kiyotaka
07:07
created

PluginService::supportedVersion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 9
rs 9.9666
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\ComposerApiService;
25
use Eccube\Service\Composer\ComposerServiceInterface;
26
use Eccube\Util\CacheUtil;
27
use Eccube\Util\StringUtil;
28
use Symfony\Component\DependencyInjection\ContainerInterface;
29
use Symfony\Component\Filesystem\Filesystem;
30
31
class PluginService
32
{
33
    /**
34
     * @var EccubeConfig
35
     */
36
    protected $eccubeConfig;
37
38
    /**
39
     * @var EntityManager
40
     */
41
    protected $entityManager;
42
43
    /**
44
     * @var PluginRepository
45
     */
46
    protected $pluginRepository;
47
48
    /**
49
     * @var EntityProxyService
50
     */
51
    protected $entityProxyService;
52
53
    /**
54
     * @var SchemaService
55
     */
56
    protected $schemaService;
57
58
    /**
59
     * @var ComposerServiceInterface
60
     */
61
    protected $composerService;
62
63
    const VENDOR_NAME = 'ec-cube';
64
65
    /**
66
     * Plugin type/library of ec-cube
67
     */
68
    const ECCUBE_LIBRARY = 1;
69
70
    /**
71
     * Plugin type/library of other (except ec-cube)
72
     */
73
    const OTHER_LIBRARY = 2;
74
75
    /**
76
     * @var string %kernel.project_dir%
77
     */
78
    private $projectRoot;
79
80
    /**
81
     * @var string %kernel.environment%
82
     */
83
    private $environment;
84
85
    /**
86
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
87
     */
88
    protected $container;
89
90
    /** @var CacheUtil */
91
    protected $cacheUtil;
92
93
    /**
94
     * PluginService constructor.
95
     *
96
     * @param EntityManagerInterface $entityManager
97
     * @param PluginRepository $pluginRepository
98
     * @param EntityProxyService $entityProxyService
99
     * @param SchemaService $schemaService
100
     * @param EccubeConfig $eccubeConfig
101
     * @param ContainerInterface $container
102
     * @param CacheUtil $cacheUtil
103
     * @param ComposerApiService $composerService
104
     */
105
    public function __construct(
106
        EntityManagerInterface $entityManager,
107
        PluginRepository $pluginRepository,
108
        EntityProxyService $entityProxyService,
109
        SchemaService $schemaService,
110
        EccubeConfig $eccubeConfig,
111
        ContainerInterface $container,
112
        CacheUtil $cacheUtil,
113
        ComposerApiService $composerService
114
    ) {
115
        $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...
116
        $this->pluginRepository = $pluginRepository;
117
        $this->entityProxyService = $entityProxyService;
118
        $this->schemaService = $schemaService;
119
        $this->eccubeConfig = $eccubeConfig;
120
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
121
        $this->environment = $eccubeConfig->get('kernel.environment');
122
        $this->container = $container;
123
        $this->cacheUtil = $cacheUtil;
124
        $this->composerService = $composerService;
125
    }
126
127
    /**
128
     * ファイル指定してのプラグインインストール
129
     *
130
     * @param string $path   path to tar.gz/zip plugin file
131
     * @param int    $source
132
     *
133
     * @return boolean
134
     *
135
     * @throws PluginException
136
     * @throws \Exception
137
     */
138
    public function install($path, $source = 0)
139
    {
140
        $pluginBaseDir = null;
141
        $tmp = null;
142
        try {
143
            // プラグイン配置前に実施する処理
144
            $this->preInstall();
145
            $tmp = $this->createTempDir();
146
147
            // 一旦テンポラリに展開
148
            $this->unpackPluginArchive($path, $tmp);
149
            $this->checkPluginArchiveContent($tmp);
150
151
            $config = $this->readConfig($tmp);
152
            // テンポラリのファイルを削除
153
            $this->deleteFile($tmp);
154
155
            // 重複していないかチェック
156
            $this->checkSamePlugin($config['code']);
157
158
            $pluginBaseDir = $this->calcPluginDir($config['code']);
159
            // 本来の置き場所を作成
160
            $this->createPluginDir($pluginBaseDir);
161
162
            // 問題なければ本当のplugindirへ
163
            $this->unpackPluginArchive($path, $pluginBaseDir);
164
165
            // Check dependent plugin
166
            // Don't install ec-cube library
167
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
168
//            if (!empty($dependents)) {
169
//                $package = $this->parseToComposerCommand($dependents);
170
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
171
//                $this->composerService->execRequire($package);
172
//            }
173
174
            // プラグイン配置後に実施する処理
175
            $this->postInstall($config, $source);
176
            // リソースファイルをコピー
177
            $this->copyAssets($pluginBaseDir, $config['code']);
178
        } catch (PluginException $e) {
179
            $this->deleteDirs([$tmp, $pluginBaseDir]);
180
            throw $e;
181
        } catch (\Exception $e) {
182
            // インストーラがどんなExceptionを上げるかわからないので
183
            $this->deleteDirs([$tmp, $pluginBaseDir]);
184
            throw $e;
185
        }
186
187
        return true;
188
    }
189
190
    /**
191
     * @param $code string sプラグインコード
192
     *
193
     * @throws PluginException
194
     */
195
    public function installWithCode($code)
196
    {
197
        $pluginDir = $this->calcPluginDir($code);
198
        $this->checkPluginArchiveContent($pluginDir);
199
        $config = $this->readConfig($pluginDir);
200
201
        if (isset($config['source']) && $config['source']) {
202
            // 依存プラグインが有効になっていない場合はエラー
203
            $requires = $this->getPluginRequired($config);
204 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...
205
                $code = preg_replace('/^ec-cube\//', '', $req['name']);
206
                /** @var Plugin $DependPlugin */
207
                $DependPlugin = $this->pluginRepository->findOneBy(['code' => $code]);
208
209
                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...
210
            });
211
212
            if (!empty($notInstalledOrDisabled)) {
213
                $names = array_map(function ($p) { return $p['name']; }, $notInstalledOrDisabled);
214
                throw new PluginException(implode(', ', $names).'を有効化してください。');
215
            }
216
        }
217
218
        $this->checkSamePlugin($config['code']);
219
        $this->postInstall($config, $config['source']);
220
    }
221
222
    // インストール事前処理
223
    public function preInstall()
224
    {
225
        // キャッシュの削除
226
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
227
//        $this->cacheUtil->clearCache();
228
    }
229
230
    // インストール事後処理
231
    public function postInstall($config, $source)
232
    {
233
        // dbにプラグイン登録
234
235
        $this->entityManager->getConnection()->beginTransaction();
236
237
        try {
238
            $Plugin = $this->pluginRepository->findByCode($config['code']);
239
240
            if (!$Plugin) {
241
                $Plugin = new Plugin();
242
                // インストール直後はプラグインは有効にしない
243
                $Plugin->setName($config['name'])
244
                    ->setEnabled(false)
245
                    ->setVersion($config['version'])
246
                    ->setSource($source)
247
                    ->setCode($config['code']);
248
                $this->entityManager->persist($Plugin);
249
                $this->entityManager->flush();
250
            }
251
252
            $this->generateProxyAndUpdateSchema($Plugin, $config);
253
254
            $this->callPluginManagerMethod($config, 'install');
255
256
            $Plugin->setInitialized(true);
257
            $this->entityManager->persist($Plugin);
258
            $this->entityManager->flush();
259
260
            $this->entityManager->flush();
261
            $this->entityManager->getConnection()->commit();
262
        } catch (\Exception $e) {
263
            $this->entityManager->getConnection()->rollback();
264
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
265
        }
266
    }
267
268
    public function generateProxyAndUpdateSchema(Plugin $plugin, $config)
269
    {
270
        if ($plugin->isEnabled()) {
271
            $generatedFiles = $this->regenerateProxy($plugin, false);
272
            $this->schemaService->updateSchema($generatedFiles, $this->projectRoot.'/app/proxy/entity');
273
        } else {
274
            // Proxyのクラスをロードせずにスキーマを更新するために、
275
            // インストール時には一時的なディレクトリにProxyを生成する
276
            $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
277
            @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...
278
279
            try {
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
                // 一時的に利用するProxyを生成してからスキーマを更新する
292
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
293
                $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
294
            } finally {
295
                foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
296
                    unlink($f);
297
                }
298
                rmdir($tmpProxyOutputDir);
299
            }
300
        }
301
    }
302
303
    public function createTempDir()
304
    {
305
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
306
        @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...
307
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
308
309
        if (!mkdir($d, 0777)) {
310
            throw new PluginException($php_errormsg.$d);
311
        }
312
313
        return $d;
314
    }
315
316
    public function deleteDirs($arr)
317
    {
318
        foreach ($arr as $dir) {
319
            if (file_exists($dir)) {
320
                $fs = new Filesystem();
321
                $fs->remove($dir);
322
            }
323
        }
324
    }
325
326
    /**
327
     * @param string $archive
328
     * @param string $dir
329
     *
330
     * @throws PluginException
331
     */
332
    public function unpackPluginArchive($archive, $dir)
333
    {
334
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
335
        try {
336
            if ($extension == 'zip') {
337
                $zip = new \ZipArchive();
338
                $zip->open($archive);
339
                $zip->extractTo($dir);
340
                $zip->close();
341
            } else {
342
                $phar = new \PharData($archive);
343
                $phar->extractTo($dir, null, true);
344
            }
345
        } catch (\Exception $e) {
346
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
347
        }
348
    }
349
350
    /**
351
     * @param $dir
352
     * @param array $config_cache
353
     *
354
     * @throws PluginException
355
     */
356
    public function checkPluginArchiveContent($dir, array $config_cache = [])
357
    {
358
        try {
359
            if (!empty($config_cache)) {
360
                $meta = $config_cache;
361
            } else {
362
                $meta = $this->readConfig($dir);
363
            }
364
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
365
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
366
        }
367
368
        if (!is_array($meta)) {
369
            throw new PluginException('config.yml not found or syntax error');
370
        }
371
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
372
            throw new PluginException('config.yml code empty or invalid_character(\W)');
373
        }
374
        if (!isset($meta['name'])) {
375
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
376
            throw new PluginException('config.yml name empty');
377
        }
378
        if (!isset($meta['version'])) {
379
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
380
            throw new PluginException('config.yml version invalid_character(\W) ');
381
        }
382
    }
383
384
    /**
385
     * @param $pluginDir
386
     *
387
     * @return array
388
     *
389
     * @throws PluginException
390
     */
391
    public function readConfig($pluginDir)
392
    {
393
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
394
        if (file_exists($composerJsonPath) === false) {
395
            throw new PluginException("${composerJsonPath} not found.");
396
        }
397
398
        $json = json_decode(file_get_contents($composerJsonPath), true);
399
        if ($json === null) {
400
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
401
        }
402
403
        if (!isset($json['version'])) {
404
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
405
        }
406
407
        if (!isset($json['extra']['code'])) {
408
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
409
        }
410
411
        return [
412
            'code' => $json['extra']['code'],
413
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
414
            'version' => $json['version'],
415
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
416
        ];
417
    }
418
419
    public function checkSymbolName($string)
420
    {
421
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
422
        // plugin_nameやplugin_codeに使える文字のチェック
423
        // a-z A-Z 0-9 _
424
        // ディレクトリ名などに使われれるので厳しめ
425
    }
426
427
    /**
428
     * @param string $path
429
     */
430
    public function deleteFile($path)
431
    {
432
        $f = new Filesystem();
433
        $f->remove($path);
434
    }
435
436
    public function checkSamePlugin($code)
437
    {
438
        /** @var Plugin $Plugin */
439
        $Plugin = $this->pluginRepository->findOneBy(['code' => $code]);
440
        if ($Plugin && $Plugin->isInitialized()) {
441
            throw new PluginException('plugin already installed.');
442
        }
443
    }
444
445
    public function calcPluginDir($code)
446
    {
447
        return $this->projectRoot.'/app/Plugin/'.$code;
448
    }
449
450
    /**
451
     * @param string $d
452
     *
453
     * @throws PluginException
454
     */
455
    public function createPluginDir($d)
456
    {
457
        $b = @mkdir($d);
458
        if (!$b) {
459
            throw new PluginException($php_errormsg);
460
        }
461
    }
462
463
    /**
464
     * @param $meta
465
     * @param int $source
466
     *
467
     * @return Plugin
468
     *
469
     * @throws PluginException
470
     */
471
    public function registerPlugin($meta, $source = 0)
472
    {
473
        try {
474
            $p = new Plugin();
475
            // インストール直後はプラグインは有効にしない
476
            $p->setName($meta['name'])
477
                ->setEnabled(false)
478
                ->setVersion($meta['version'])
479
                ->setSource($source)
480
                ->setCode($meta['code']);
481
482
            $this->entityManager->persist($p);
483
            $this->entityManager->flush($p);
484
        } catch (\Exception $e) {
485
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
486
        }
487
488
        return $p;
489
    }
490
491
    /**
492
     * @param $meta
493
     * @param string $method
494
     */
495
    public function callPluginManagerMethod($meta, $method)
496
    {
497
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
498
        if (class_exists($class)) {
499
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
500
            if (method_exists($installer, $method)) {
501
                $installer->$method($meta, $this->container);
502
            }
503
        }
504
    }
505
506
    /**
507
     * @param Plugin $plugin
508
     * @param bool $force
509
     *
510
     * @return bool
511
     *
512
     * @throws \Exception
513
     */
514
    public function uninstall(Plugin $plugin, $force = true)
515
    {
516
        $pluginDir = $this->calcPluginDir($plugin->getCode());
517
        $this->cacheUtil->clearCache();
518
        $config = $this->readConfig($pluginDir);
519
520
        if ($plugin->isEnabled()) {
521
            $this->disable($plugin);
522
        }
523
        $this->callPluginManagerMethod($config, 'uninstall');
524
        $this->unregisterPlugin($plugin);
525
526
        // スキーマを更新する
527
        //FIXME: Update schema before no affect
528
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
529
530
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
531
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
532
        $this->schemaService->dropTable($namespace);
533
534
        if ($force) {
535
            $this->deleteFile($pluginDir);
536
            $this->removeAssets($plugin->getCode());
537
        }
538
539
        return true;
540
    }
541
542
    public function unregisterPlugin(Plugin $p)
543
    {
544
        try {
545
            $em = $this->entityManager;
546
            $em->remove($p);
547
            $em->flush();
548
        } catch (\Exception $e) {
549
            throw $e;
550
        }
551
    }
552
553
    public function disable(Plugin $plugin)
554
    {
555
        return $this->enable($plugin, false);
556
    }
557
558
    /**
559
     * Proxyを再生成します.
560
     *
561
     * @param Plugin $plugin プラグイン
562
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
563
     * @param string|null $outputDir 出力先
564
     *
565
     * @return array 生成されたファイルのパス
566
     */
567
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
568
    {
569
        if (is_null($outputDir)) {
570
            $outputDir = $this->projectRoot.'/app/proxy/entity';
571
        }
572
        @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...
573
574
        $enabledPluginCodes = array_map(
575
            function ($p) { return $p->getCode(); },
576
            $this->pluginRepository->findAllEnabled()
577
        );
578
579
        $excludes = [];
580
        if ($temporary || $plugin->isEnabled()) {
581
            $enabledPluginCodes[] = $plugin->getCode();
582
        } else {
583
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
584
            if ($index >= 0) {
585
                array_splice($enabledPluginCodes, $index, 1);
586
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
587
            }
588
        }
589
590
        $enabledPluginEntityDirs = array_map(function ($code) {
591
            return $this->projectRoot."/app/Plugin/${code}/Entity";
592
        }, $enabledPluginCodes);
593
594
        return $this->entityProxyService->generate(
595
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
596
            $excludes,
597
            $outputDir
598
        );
599
    }
600
601
    public function enable(Plugin $plugin, $enable = true)
602
    {
603
        $em = $this->entityManager;
604
        try {
605
            $pluginDir = $this->calcPluginDir($plugin->getCode());
606
            $config = $this->readConfig($pluginDir);
607
            $em->getConnection()->beginTransaction();
608
            $plugin->setEnabled($enable ? true : false);
609
            $em->persist($plugin);
610
611
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
612
613
            // Proxyだけ再生成してスキーマは更新しない
614
            $this->regenerateProxy($plugin, false);
615
616
            $em->flush();
617
            $em->getConnection()->commit();
618
        } catch (\Exception $e) {
619
            $em->getConnection()->rollback();
620
            throw $e;
621
        }
622
623
        return true;
624
    }
625
626
    /**
627
     * Update plugin
628
     *
629
     * @param Plugin $plugin
630
     * @param string $path
631
     *
632
     * @return bool
633
     *
634
     * @throws PluginException
635
     * @throws \Exception
636
     */
637
    public function update(Plugin $plugin, $path)
638
    {
639
        $pluginBaseDir = null;
640
        $tmp = null;
641
        try {
642
            $this->cacheUtil->clearCache();
643
            $tmp = $this->createTempDir();
644
645
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
646
            $this->checkPluginArchiveContent($tmp);
647
648
            $config = $this->readConfig($tmp);
649
650
            if ($plugin->getCode() != $config['code']) {
651
                throw new PluginException('new/old plugin code is different.');
652
            }
653
654
            $pluginBaseDir = $this->calcPluginDir($config['code']);
655
            $this->deleteFile($tmp); // テンポラリのファイルを削除
656
657
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
658
659
            // Check dependent plugin
660
            // Don't install ec-cube library
661
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
662
            if (!empty($dependents)) {
663
                $package = $this->parseToComposerCommand($dependents);
664
                $this->composerService->execRequire($package);
665
            }
666
667
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
668
        } catch (PluginException $e) {
669
            $this->deleteDirs([$tmp]);
670
            throw $e;
671
        } catch (\Exception $e) {
672
            // catch exception of composer
673
            $this->deleteDirs([$tmp]);
674
            throw $e;
675
        }
676
677
        return true;
678
    }
679
680
    /**
681
     * Update plugin
682
     *
683
     * @param Plugin $plugin
684
     * @param array  $meta     Config data
685
     *
686
     * @throws \Exception
687
     */
688
    public function updatePlugin(Plugin $plugin, $meta)
689
    {
690
        $em = $this->entityManager;
691
        try {
692
            $em->getConnection()->beginTransaction();
693
            $plugin->setVersion($meta['version'])
694
                ->setName($meta['name']);
695
696
            $em->persist($plugin);
697
            $this->callPluginManagerMethod($meta, 'update');
698
            $em->flush();
699
            $em->getConnection()->commit();
700
        } catch (\Exception $e) {
701
            $em->getConnection()->rollback();
702
            throw $e;
703
        }
704
    }
705
706
    /**
707
     * Get array require by plugin
708
     * Todo: need define dependency plugin mechanism
709
     *
710
     * @param array|Plugin $plugin format as plugin from api
711
     *
712
     * @return array|mixed
713
     *
714
     * @throws PluginException
715
     */
716
    public function getPluginRequired($plugin)
717
    {
718
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
719
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
720
721
        $results = [];
722
723
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Eccube\Service\Composer\ComposerServiceInterface as the method foreachRequires() does only exist in the following implementations of said interface: Eccube\Service\Composer\ComposerApiService.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
724
            $results[] = $package;
725
        }, 'eccube-plugin');
726
727
        return $results;
728
    }
729
730
    /**
731
     * Find the dependent plugins that need to be disabled
732
     *
733
     * @param string $pluginCode
734
     *
735
     * @return array plugin code
736
     */
737
    public function findDependentPluginNeedDisable($pluginCode)
738
    {
739
        return $this->findDependentPlugin($pluginCode, true);
740
    }
741
742
    /**
743
     * Find the other plugin that has requires on it.
744
     * Check in both dtb_plugin table and <PluginCode>/composer.json
745
     *
746
     * @param string $pluginCode
747
     * @param bool   $enableOnly
748
     *
749
     * @return array plugin code
750
     */
751
    public function findDependentPlugin($pluginCode, $enableOnly = false)
752
    {
753
        $criteria = Criteria::create()
754
            ->where(Criteria::expr()->neq('code', $pluginCode));
755
        if ($enableOnly) {
756
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
757
        }
758
        /**
759
         * @var Plugin[]
760
         */
761
        $plugins = $this->pluginRepository->matching($criteria);
762
        $dependents = [];
763
        foreach ($plugins as $plugin) {
764
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
765
            $fileName = $dir.'/composer.json';
766
            if (!file_exists($fileName)) {
767
                continue;
768
            }
769
            $jsonText = file_get_contents($fileName);
770
            if ($jsonText) {
771
                $json = json_decode($jsonText, true);
772
                if (!isset($json['require'])) {
773
                    continue;
774
                }
775
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
776
                    $dependents[] = $plugin->getCode();
777
                }
778
            }
779
        }
780
781
        return $dependents;
782
    }
783
784
    /**
785
     * Get dependent plugin by code
786
     * It's base on composer.json
787
     * Return the plugin code and version in the format of the composer
788
     *
789
     * @param string   $pluginCode
790
     * @param int|null $libraryType
791
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
792
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
793
     *                      default : return all library/plugin
794
     *
795
     * @return array format [packageName1 => version1, packageName2 => version2]
796
     */
797
    public function getDependentByCode($pluginCode, $libraryType = null)
798
    {
799
        $pluginDir = $this->calcPluginDir($pluginCode);
800
        $jsonFile = $pluginDir.'/composer.json';
801
        if (!file_exists($jsonFile)) {
802
            return [];
803
        }
804
        $jsonText = file_get_contents($jsonFile);
805
        $json = json_decode($jsonText, true);
806
        $dependents = [];
807
        if (isset($json['require'])) {
808
            $require = $json['require'];
809
            switch ($libraryType) {
810 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...
811
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
812
                    break;
813
814 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...
815
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
816
                    break;
817
818
                default:
819
                    $dependents = $json['require'];
820
                    break;
821
            }
822
        }
823
824
        return $dependents;
825
    }
826
827
    /**
828
     * Format array dependent plugin to string
829
     * It is used for commands.
830
     *
831
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
832
     * @param bool  $getVersion
833
     *
834
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
835
     */
836
    public function parseToComposerCommand(array $packages, $getVersion = true)
837
    {
838
        $result = array_keys($packages);
839
        if ($getVersion) {
840
            $result = array_map(function ($package, $version) {
841
                return $package.':'.$version;
842
            }, array_keys($packages), array_values($packages));
843
        }
844
845
        return implode(' ', $result);
846
    }
847
848
    /**
849
     * リソースファイル等をコピー
850
     * コピー元となるファイルの置き場所は固定であり、
851
     * [プラグインコード]/Resource/assets
852
     * 配下に置かれているファイルが所定の位置へコピーされる
853
     *
854
     * @param string $pluginBaseDir
855
     * @param $pluginCode
856
     */
857
    public function copyAssets($pluginBaseDir, $pluginCode)
858
    {
859
        $assetsDir = $pluginBaseDir.'/Resource/assets';
860
861
        // プラグインにリソースファイルがあれば所定の位置へコピー
862
        if (file_exists($assetsDir)) {
863
            $file = new Filesystem();
864
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
865
        }
866
    }
867
868
    /**
869
     * コピーしたリソースファイル等を削除
870
     *
871
     * @param string $pluginCode
872
     */
873
    public function removeAssets($pluginCode)
874
    {
875
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
876
877
        // コピーされているリソースファイルがあれば削除
878
        if (file_exists($assetsDir)) {
879
            $file = new Filesystem();
880
            $file->remove($assetsDir);
881
        }
882
    }
883
884
    /**
885
     * Is update
886
     *
887
     * @param string $pluginVersion
888
     * @param string $remoteVersion
889
     *
890
     * @return boolean
891
     */
892
    public function isUpdate($pluginVersion, $remoteVersion)
893
    {
894
        return version_compare($pluginVersion, $remoteVersion, '<');
895
    }
896
897
    /**
898
     * Plugin is exist check
899
     *
900
     * @param array  $plugins    get from api
901
     * @param string $pluginCode
902
     *
903
     * @return false|int|string
904
     */
905
    public function checkPluginExist($plugins, $pluginCode)
906
    {
907
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
908
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
909
        }
910
        // Find plugin in array
911
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
912
913
        return $index;
914
    }
915
916
    /**
917
     * @param string $code
918
     *
919
     * @return bool
920
     */
921
    private function isEnable($code)
922
    {
923
        $Plugin = $this->pluginRepository->findOneBy([
924
            'enabled' => Constant::ENABLED,
925
            'code' => $code,
926
        ]);
927
        if ($Plugin) {
928
            return true;
929
        }
930
931
        return false;
932
    }
933
}
934