Failed Conditions
Push — sf/package-api ( 831849...a77dd1 )
by Kiyotaka
06:40
created

PluginService::installWithCode()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 23

Duplication

Lines 6
Ratio 26.09 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 6
loc 23
rs 9.552
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\Application;
20
use Eccube\Common\Constant;
21
use Eccube\Common\EccubeConfig;
22
use Eccube\Entity\Plugin;
23
use Eccube\Exception\PluginException;
24
use Eccube\Repository\PluginRepository;
25
use Eccube\Service\Composer\ComposerApiService;
26
use Eccube\Service\Composer\ComposerServiceInterface;
27
use Eccube\Util\CacheUtil;
28
use Eccube\Util\StringUtil;
29
use Symfony\Component\DependencyInjection\ContainerInterface;
30
use Symfony\Component\Filesystem\Filesystem;
31
32
class PluginService
33
{
34
    /**
35
     * @var EccubeConfig
36
     */
37
    protected $eccubeConfig;
38
39
    /**
40
     * @var EntityManager
41
     */
42
    protected $entityManager;
43
44
    /**
45
     * @var PluginRepository
46
     */
47
    protected $pluginRepository;
48
49
    /**
50
     * @var Application
51
     */
52
    protected $app;
53
54
    /**
55
     * @var EntityProxyService
56
     */
57
    protected $entityProxyService;
58
59
    /**
60
     * @var SchemaService
61
     */
62
    protected $schemaService;
63
64
    /**
65
     * @var ComposerServiceInterface
66
     */
67
    protected $composerService;
68
69
    const VENDOR_NAME = 'ec-cube';
70
71
    /**
72
     * Plugin type/library of ec-cube
73
     */
74
    const ECCUBE_LIBRARY = 1;
75
76
    /**
77
     * Plugin type/library of other (except ec-cube)
78
     */
79
    const OTHER_LIBRARY = 2;
80
81
    /**
82
     * @var string %kernel.project_dir%
83
     */
84
    private $projectRoot;
85
86
    /**
87
     * @var string %kernel.environment%
88
     */
89
    private $environment;
90
91
    /**
92
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
93
     */
94
    protected $container;
95
96
    /** @var CacheUtil */
97
    protected $cacheUtil;
98
99
    /**
100
     * PluginService constructor.
101
     *
102
     * @param EntityManagerInterface $entityManager
103
     * @param PluginRepository $pluginRepository
104
     * @param EntityProxyService $entityProxyService
105
     * @param SchemaService $schemaService
106
     * @param EccubeConfig $eccubeConfig
107
     * @param ContainerInterface $container
108
     * @param CacheUtil $cacheUtil
109
     * @param ComposerApiService $composerService
110
     */
111
    public function __construct(
112
        EntityManagerInterface $entityManager,
113
        PluginRepository $pluginRepository,
114
        EntityProxyService $entityProxyService,
115
        SchemaService $schemaService,
116
        EccubeConfig $eccubeConfig,
117
        ContainerInterface $container,
118
        CacheUtil $cacheUtil,
119
        ComposerApiService $composerService
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
    }
132
133
    /**
134
     * ファイル指定してのプラグインインストール
135
     *
136
     * @param string $path   path to tar.gz/zip plugin file
137
     * @param int    $source
138
     *
139
     * @return boolean
140
     *
141
     * @throws PluginException
142
     * @throws \Exception
143
     */
144
    public function install($path, $source = 0)
145
    {
146
        $pluginBaseDir = null;
147
        $tmp = null;
148
        try {
149
            // プラグイン配置前に実施する処理
150
            $this->preInstall();
151
            $tmp = $this->createTempDir();
152
153
            // 一旦テンポラリに展開
154
            $this->unpackPluginArchive($path, $tmp);
155
            $this->checkPluginArchiveContent($tmp);
156
157
            $config = $this->readConfig($tmp);
158
            // テンポラリのファイルを削除
159
            $this->deleteFile($tmp);
160
161
            // 重複していないかチェック
162
            $this->checkSamePlugin($config['code']);
163
164
            $pluginBaseDir = $this->calcPluginDir($config['code']);
165
            // 本来の置き場所を作成
166
            $this->createPluginDir($pluginBaseDir);
167
168
            // 問題なければ本当のplugindirへ
169
            $this->unpackPluginArchive($path, $pluginBaseDir);
170
171
            // Check dependent plugin
172
            // Don't install ec-cube library
173
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
174
//            if (!empty($dependents)) {
175
//                $package = $this->parseToComposerCommand($dependents);
176
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
177
//                $this->composerService->execRequire($package);
178
//            }
179
180
            // プラグイン配置後に実施する処理
181
            $this->postInstall($config, $source);
182
            // リソースファイルをコピー
183
            $this->copyAssets($pluginBaseDir, $config['code']);
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
     *
198
     *
199
     * @param $code string sプラグインコード
200
     * @throws PluginException
201
     */
202
    public function installWithCode($code)
203
    {
204
        $pluginDir = $this->calcPluginDir($code);
205
        $this->checkPluginArchiveContent($pluginDir);
206
        $config = $this->readConfig($pluginDir);
207
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
            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...
215
        });
216
217
        if (!empty($notInstalledOrDisabled)) {
218
            $names = array_map(function($p) { return $p['name']; }, $notInstalledOrDisabled);
219
            throw new PluginException(implode(', ', $names)."を有効化してください。");
220
        }
221
222
        $this->checkSamePlugin($config['code']);
223
        $this->postInstall($config, $config['source']);
224
    }
225
226
    // インストール事前処理
227
    public function preInstall()
228
    {
229
        // キャッシュの削除
230
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
231
//        $this->cacheUtil->clearCache();
232
    }
233
234
    // インストール事後処理
235
    public function postInstall($config, $source)
236
    {
237
        // dbにプラグイン登録
238
239
        $this->entityManager->getConnection()->beginTransaction();
240
241
        try {
242
            $Plugin = $this->pluginRepository->findByCode($config['code']);
243
244
            if (!$Plugin) {
245
                $Plugin = new Plugin();
246
                // インストール直後はプラグインは有効にしない
247
                $Plugin->setName($config['name'])
248
                    ->setEnabled(false)
249
                    ->setVersion($config['version'])
250
                    ->setSource($source)
251
                    ->setCode($config['code']);
252
                $this->entityManager->persist($Plugin);
253
                $this->entityManager->flush();
254
            }
255
256
            $this->generateProxyAndUpdateSchema($Plugin, $config);
257
258
            $this->callPluginManagerMethod($config, 'install');
259
260
            $Plugin->setInitialized(true);
261
            $this->entityManager->persist($Plugin);
262
            $this->entityManager->flush();
263
264
            $this->entityManager->flush();
265
            $this->entityManager->getConnection()->commit();
266
267
        } catch (\Exception $e) {
268
            $this->entityManager->getConnection()->rollback();
269
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
270
        }
271
    }
272
273
    public function generateProxyAndUpdateSchema(Plugin $plugin, $config)
274
    {
275
        if ($plugin->isEnabled()) {
276
            $generatedFiles = $this->regenerateProxy($plugin, false);
277
            $this->schemaService->updateSchema($generatedFiles, $this->projectRoot . '/app/proxy/entity');
278
        } else {
279
            // Proxyのクラスをロードせずにスキーマを更新するために、
280
            // インストール時には一時的なディレクトリにProxyを生成する
281
            $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
282
            @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...
283
284
            try {
285
                // プラグインmetadata定義を追加
286
                $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
287
                if (file_exists($entityDir)) {
288
                    $ormConfig = $this->entityManager->getConfiguration();
289
                    $chain = $ormConfig->getMetadataDriverImpl();
290
                    $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
291
                    $namespace = 'Plugin\\'.$config['code'].'\\Entity';
292
                    $chain->addDriver($driver, $namespace);
293
                    $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
294
                }
295
296
                // 一時的に利用するProxyを生成してからスキーマを更新する
297
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
298
                $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
299
            } finally {
300
                foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
301
                    unlink($f);
302
                }
303
                rmdir($tmpProxyOutputDir);
304
            }
305
        }
306
    }
307
308
    public function createTempDir()
309
    {
310
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
311
        @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...
312
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
313
314
        if (!mkdir($d, 0777)) {
315
            throw new PluginException($php_errormsg.$d);
316
        }
317
318
        return $d;
319
    }
320
321
    public function deleteDirs($arr)
322
    {
323
        foreach ($arr as $dir) {
324
            if (file_exists($dir)) {
325
                $fs = new Filesystem();
326
                $fs->remove($dir);
327
            }
328
        }
329
    }
330
331
    /**
332
     * @param string $archive
333
     * @param string $dir
334
     *
335
     * @throws PluginException
336
     */
337
    public function unpackPluginArchive($archive, $dir)
338
    {
339
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
340
        try {
341
            if ($extension == 'zip') {
342
                $zip = new \ZipArchive();
343
                $zip->open($archive);
344
                $zip->extractTo($dir);
345
                $zip->close();
346
            } else {
347
                $phar = new \PharData($archive);
348
                $phar->extractTo($dir, null, true);
349
            }
350
        } catch (\Exception $e) {
351
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
352
        }
353
    }
354
355
    /**
356
     * @param $dir
357
     * @param array $config_cache
358
     *
359
     * @throws PluginException
360
     */
361
    public function checkPluginArchiveContent($dir, array $config_cache = [])
362
    {
363
        try {
364
            if (!empty($config_cache)) {
365
                $meta = $config_cache;
366
            } else {
367
                $meta = $this->readConfig($dir);
368
            }
369
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
370
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
371
        }
372
373
        if (!is_array($meta)) {
374
            throw new PluginException('config.yml not found or syntax error');
375
        }
376
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
377
            throw new PluginException('config.yml code empty or invalid_character(\W)');
378
        }
379
        if (!isset($meta['name'])) {
380
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
381
            throw new PluginException('config.yml name empty');
382
        }
383
        if (!isset($meta['version'])) {
384
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
385
            throw new PluginException('config.yml version invalid_character(\W) ');
386
        }
387
    }
388
389
    /**
390
     * @param $pluginDir
391
     *
392
     * @return array
393
     *
394
     * @throws PluginException
395
     */
396
    public function readConfig($pluginDir)
397
    {
398
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
399
        if (file_exists($composerJsonPath) === false) {
400
            throw new PluginException("${composerJsonPath} not found.");
401
        }
402
403
        $json = json_decode(file_get_contents($composerJsonPath), true);
404
        if ($json === null) {
405
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
406
        }
407
408
        if (!isset($json['version'])) {
409
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
410
        }
411
412
        if (!isset($json['extra']['code'])) {
413
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
414
        }
415
416
        return [
417
            'code' => $json['extra']['code'],
418
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
419
            'version' => $json['version'],
420
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
421
        ];
422
    }
423
424
    public function checkSymbolName($string)
425
    {
426
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
427
        // plugin_nameやplugin_codeに使える文字のチェック
428
        // a-z A-Z 0-9 _
429
        // ディレクトリ名などに使われれるので厳しめ
430
    }
431
432
    /**
433
     * @param string $path
434
     */
435
    public function deleteFile($path)
436
    {
437
        $f = new Filesystem();
438
        $f->remove($path);
439
    }
440
441
    public function checkSamePlugin($code)
442
    {
443
        /** @var Plugin $Plugin */
444
        $Plugin = $this->pluginRepository->findOneBy(['code' => $code]);
445
        if ($Plugin && $Plugin->isInitialized()) {
446
            throw new PluginException('plugin already installed.');
447
        }
448
    }
449
450
    public function calcPluginDir($code)
451
    {
452
        return $this->projectRoot.'/app/Plugin/'.$code;
453
    }
454
455
    /**
456
     * @param string $d
457
     *
458
     * @throws PluginException
459
     */
460
    public function createPluginDir($d)
461
    {
462
        $b = @mkdir($d);
463
        if (!$b) {
464
            throw new PluginException($php_errormsg);
465
        }
466
    }
467
468
    /**
469
     * @param $meta
470
     * @param int $source
471
     *
472
     * @return Plugin
473
     *
474
     * @throws PluginException
475
     */
476
    public function registerPlugin($meta, $source = 0)
477
    {
478
        try {
479
            $p = new Plugin();
480
            // インストール直後はプラグインは有効にしない
481
            $p->setName($meta['name'])
482
                ->setEnabled(false)
483
                ->setVersion($meta['version'])
484
                ->setSource($source)
485
                ->setCode($meta['code']);
486
487
            $this->entityManager->persist($p);
488
            $this->entityManager->flush($p);
489
        } catch (\Exception $e) {
490
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
491
        }
492
493
        return $p;
494
    }
495
496
    /**
497
     * @param $meta
498
     * @param string $method
499
     */
500
    public function callPluginManagerMethod($meta, $method)
501
    {
502
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
503
        if (class_exists($class)) {
504
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
505
            if (method_exists($installer, $method)) {
506
                // FIXME appを削除.
507
                $installer->$method($meta, $this->app, $this->container);
508
            }
509
        }
510
    }
511
512
    /**
513
     * @param Plugin $plugin
514
     * @param bool $force
515
     *
516
     * @return bool
517
     *
518
     * @throws \Exception
519
     */
520
    public function uninstall(Plugin $plugin, $force = true)
521
    {
522
        $pluginDir = $this->calcPluginDir($plugin->getCode());
523
        $this->cacheUtil->clearCache();
524
        $config = $this->readConfig($pluginDir);
525
526
        if ($plugin->isEnabled()) {
527
            $this->disable($plugin);
528
        }
529
        $this->callPluginManagerMethod($config, 'uninstall');
530
        $this->unregisterPlugin($plugin);
531
532
        // スキーマを更新する
533
        //FIXME: Update schema before no affect
534
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
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
        return true;
546
    }
547
548
    public function unregisterPlugin(Plugin $p)
549
    {
550
        try {
551
            $em = $this->entityManager;
552
            $em->remove($p);
553
            $em->flush();
554
        } catch (\Exception $e) {
555
            throw $e;
556
        }
557
    }
558
559
    public function disable(Plugin $plugin)
560
    {
561
        return $this->enable($plugin, false);
562
    }
563
564
    /**
565
     * Proxyを再生成します.
566
     *
567
     * @param Plugin $plugin プラグイン
568
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
569
     * @param string|null $outputDir 出力先
570
     *
571
     * @return array 生成されたファイルのパス
572
     */
573
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
574
    {
575
        if (is_null($outputDir)) {
576
            $outputDir = $this->projectRoot.'/app/proxy/entity';
577
        }
578
        @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...
579
580
        $enabledPluginCodes = array_map(
581
            function ($p) { return $p->getCode(); },
582
            $this->pluginRepository->findAllEnabled()
583
        );
584
585
        $excludes = [];
586
        if ($temporary || $plugin->isEnabled()) {
587
            $enabledPluginCodes[] = $plugin->getCode();
588
        } else {
589
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
590
            if ($index >= 0) {
591
                array_splice($enabledPluginCodes, $index, 1);
592
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
593
            }
594
        }
595
596
        $enabledPluginEntityDirs = array_map(function ($code) {
597
            return $this->projectRoot."/app/Plugin/${code}/Entity";
598
        }, $enabledPluginCodes);
599
600
        return $this->entityProxyService->generate(
601
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
602
            $excludes,
603
            $outputDir
604
        );
605
    }
606
607
    public function enable(Plugin $plugin, $enable = true)
608
    {
609
        $em = $this->entityManager;
610
        try {
611
            $pluginDir = $this->calcPluginDir($plugin->getCode());
612
            $config = $this->readConfig($pluginDir);
613
            $em->getConnection()->beginTransaction();
614
            $plugin->setEnabled($enable ? true : false);
615
            $em->persist($plugin);
616
617
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
618
619
            // Proxyだけ再生成してスキーマは更新しない
620
            $this->regenerateProxy($plugin, false);
621
622
            $em->flush();
623
            $em->getConnection()->commit();
624
        } catch (\Exception $e) {
625
            $em->getConnection()->rollback();
626
            throw $e;
627
        }
628
629
        return true;
630
    }
631
632
    /**
633
     * Update plugin
634
     *
635
     * @param Plugin $plugin
636
     * @param string $path
637
     *
638
     * @return bool
639
     *
640
     * @throws PluginException
641
     * @throws \Exception
642
     */
643
    public function update(Plugin $plugin, $path)
644
    {
645
        $pluginBaseDir = null;
646
        $tmp = null;
647
        try {
648
            $this->cacheUtil->clearCache();
649
            $tmp = $this->createTempDir();
650
651
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
652
            $this->checkPluginArchiveContent($tmp);
653
654
            $config = $this->readConfig($tmp);
655
656
            if ($plugin->getCode() != $config['code']) {
657
                throw new PluginException('new/old plugin code is different.');
658
            }
659
660
            $pluginBaseDir = $this->calcPluginDir($config['code']);
661
            $this->deleteFile($tmp); // テンポラリのファイルを削除
662
663
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
664
665
            // Check dependent plugin
666
            // Don't install ec-cube library
667
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
668
            if (!empty($dependents)) {
669
                $package = $this->parseToComposerCommand($dependents);
670
                $this->composerService->execRequire($package);
671
            }
672
673
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
674
        } catch (PluginException $e) {
675
            $this->deleteDirs([$tmp]);
676
            throw $e;
677
        } catch (\Exception $e) {
678
            // catch exception of composer
679
            $this->deleteDirs([$tmp]);
680
            throw $e;
681
        }
682
683
        return true;
684
    }
685
686
    /**
687
     * Update plugin
688
     *
689
     * @param Plugin $plugin
690
     * @param array  $meta     Config data
691
     *
692
     * @throws \Exception
693
     */
694
    public function updatePlugin(Plugin $plugin, $meta)
695
    {
696
        $em = $this->entityManager;
697
        try {
698
            $em->getConnection()->beginTransaction();
699
            $plugin->setVersion($meta['version'])
700
                ->setName($meta['name']);
701
702
            $em->persist($plugin);
703
            $this->callPluginManagerMethod($meta, 'update');
704
            $em->flush();
705
            $em->getConnection()->commit();
706
707
        } catch (\Exception $e) {
708
            $em->getConnection()->rollback();
709
            throw $e;
710
        }
711
    }
712
713
    /**
714
     * Get array require by plugin
715
     * Todo: need define dependency plugin mechanism
716
     *
717
     * @param array|Plugin $plugin format as plugin from api
718
     *
719
     * @return array|mixed
720
     * @throws PluginException
721
     */
722
    public function getPluginRequired($plugin)
723
    {
724
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
725
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
726
727
        $results = [];
728
729
        $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...
730
            $results[] = $package;
731
        }, 'eccube-plugin');
732
733
        return $results;
734
    }
735
736
    /**
737
     * Get plugin information
738
     *
739
     * @param array $plugin
740
     *
741
     * @return array|null
742
     */
743
    public function buildInfo($plugin)
744
    {
745
        $this->supportedVersion($plugin);
746
747
        return $plugin;
748
    }
749
750
    /**
751
     * Check support version
752
     *
753
     * @param $plugin
754
     */
755
    public function supportedVersion(&$plugin)
756
    {
757
        // Check the eccube version that the plugin supports.
758
        $plugin['version_check'] = false;
759
        if (in_array(Constant::VERSION, $plugin['supported_versions'])) {
760
            // Match version
761
            $plugin['version_check'] = true;
762
        }
763
    }
764
765
    /**
766
     * Find the dependent plugins that need to be disabled
767
     *
768
     * @param string $pluginCode
769
     *
770
     * @return array plugin code
771
     */
772
    public function findDependentPluginNeedDisable($pluginCode)
773
    {
774
        return $this->findDependentPlugin($pluginCode, true);
775
    }
776
777
    /**
778
     * Find the other plugin that has requires on it.
779
     * Check in both dtb_plugin table and <PluginCode>/composer.json
780
     *
781
     * @param string $pluginCode
782
     * @param bool   $enableOnly
783
     *
784
     * @return array plugin code
785
     */
786
    public function findDependentPlugin($pluginCode, $enableOnly = false)
787
    {
788
        $criteria = Criteria::create()
789
            ->where(Criteria::expr()->neq('code', $pluginCode));
790
        if ($enableOnly) {
791
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
792
        }
793
        /**
794
         * @var Plugin[]
795
         */
796
        $plugins = $this->pluginRepository->matching($criteria);
797
        $dependents = [];
798
        foreach ($plugins as $plugin) {
799
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
800
            $fileName = $dir.'/composer.json';
801
            if (!file_exists($fileName)) {
802
                continue;
803
            }
804
            $jsonText = file_get_contents($fileName);
805
            if ($jsonText) {
806
                $json = json_decode($jsonText, true);
807
                if (!isset($json['require'])) {
808
                    continue;
809
                }
810
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
811
                    $dependents[] = $plugin->getCode();
812
                }
813
            }
814
        }
815
816
        return $dependents;
817
    }
818
819
    /**
820
     * Get dependent plugin by code
821
     * It's base on composer.json
822
     * Return the plugin code and version in the format of the composer
823
     *
824
     * @param string   $pluginCode
825
     * @param int|null $libraryType
826
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
827
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
828
     *                      default : return all library/plugin
829
     *
830
     * @return array format [packageName1 => version1, packageName2 => version2]
831
     */
832
    public function getDependentByCode($pluginCode, $libraryType = null)
833
    {
834
        $pluginDir = $this->calcPluginDir($pluginCode);
835
        $jsonFile = $pluginDir.'/composer.json';
836
        if (!file_exists($jsonFile)) {
837
            return [];
838
        }
839
        $jsonText = file_get_contents($jsonFile);
840
        $json = json_decode($jsonText, true);
841
        $dependents = [];
842
        if (isset($json['require'])) {
843
            $require = $json['require'];
844
            switch ($libraryType) {
845 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...
846
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
847
                    break;
848
849 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...
850
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
851
                    break;
852
853
                default:
854
                    $dependents = $json['require'];
855
                    break;
856
            }
857
        }
858
859
        return $dependents;
860
    }
861
862
    /**
863
     * Format array dependent plugin to string
864
     * It is used for commands.
865
     *
866
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
867
     * @param bool  $getVersion
868
     *
869
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
870
     */
871
    public function parseToComposerCommand(array $packages, $getVersion = true)
872
    {
873
        $result = array_keys($packages);
874
        if ($getVersion) {
875
            $result = array_map(function ($package, $version) {
876
                return $package.':'.$version;
877
            }, array_keys($packages), array_values($packages));
878
        }
879
880
        return implode(' ', $result);
881
    }
882
883
    /**
884
     * リソースファイル等をコピー
885
     * コピー元となるファイルの置き場所は固定であり、
886
     * [プラグインコード]/Resource/assets
887
     * 配下に置かれているファイルが所定の位置へコピーされる
888
     *
889
     * @param string $pluginBaseDir
890
     * @param $pluginCode
891
     */
892
    public function copyAssets($pluginBaseDir, $pluginCode)
893
    {
894
        $assetsDir = $pluginBaseDir.'/Resource/assets';
895
896
        // プラグインにリソースファイルがあれば所定の位置へコピー
897
        if (file_exists($assetsDir)) {
898
            $file = new Filesystem();
899
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
900
        }
901
    }
902
903
    /**
904
     * コピーしたリソースファイル等を削除
905
     *
906
     * @param string $pluginCode
907
     */
908
    public function removeAssets($pluginCode)
909
    {
910
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
911
912
        // コピーされているリソースファイルがあれば削除
913
        if (file_exists($assetsDir)) {
914
            $file = new Filesystem();
915
            $file->remove($assetsDir);
916
        }
917
    }
918
919
    /**
920
     * Is update
921
     *
922
     * @param string $pluginVersion
923
     * @param string $remoteVersion
924
     *
925
     * @return boolean
926
     */
927
    public function isUpdate($pluginVersion, $remoteVersion)
928
    {
929
        return version_compare($pluginVersion, $remoteVersion, '<');
930
    }
931
932
    /**
933
     * Plugin is exist check
934
     *
935
     * @param array  $plugins    get from api
936
     * @param string $pluginCode
937
     *
938
     * @return false|int|string
939
     */
940
    public function checkPluginExist($plugins, $pluginCode)
941
    {
942
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
943
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
944
        }
945
        // Find plugin in array
946
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
947
948
        return $index;
949
    }
950
951
    /**
952
     * @param string $code
953
     *
954
     * @return bool
955
     */
956
    private function isEnable($code)
957
    {
958
        $Plugin = $this->pluginRepository->findOneBy([
959
            'enabled' => Constant::ENABLED,
960
            'code' => $code,
961
        ]);
962
        if ($Plugin) {
963
            return true;
964
        }
965
966
        return false;
967
    }
968
}
969