Failed Conditions
Pull Request — 4.0 (#3723)
by Kiyotaka
07:29
created

PluginService::generateProxyAndUpdateSchema()   A

Complexity

Conditions 4
Paths 25

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 7.456

Importance

Changes 0
Metric Value
cc 4
nc 25
nop 2
dl 0
loc 34
rs 9.376
c 0
b 0
f 0
ccs 4
cts 10
cp 0.4
crap 7.456
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 1
        $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 1
     * @param string $path   path to tar.gz/zip plugin file
131 1
     * @param int    $source
132 1
     *
133 1
     * @return boolean
134 1
     *
135 1
     * @throws PluginException
136 1
     * @throws \Exception
137 1
     */
138 1
    public function install($path, $source = 0)
139 1
    {
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 1
            $this->deleteFile($tmp);
154
155 1
            // 重複していないかチェック
156 1
            $this->checkSamePlugin($config['code']);
157
158
            $pluginBaseDir = $this->calcPluginDir($config['code']);
159 1
            // 本来の置き場所を作成
160 1
            $this->createPluginDir($pluginBaseDir);
161
162
            // 問題なければ本当のplugindirへ
163 1
            $this->unpackPluginArchive($path, $pluginBaseDir);
164 1
165
            // Check dependent plugin
166 1
            // Don't install ec-cube library
167 1
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
168
//            if (!empty($dependents)) {
169 1
//                $package = $this->parseToComposerCommand($dependents);
170
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
171
//                $this->composerService->execRequire($package);
172 1
//            }
173
174 1
            // プラグイン配置後に実施する処理
175
            $this->postInstall($config, $source);
176 1
            // リソースファイルをコピー
177
            $this->copyAssets($pluginBaseDir, $config['code']);
178
        } catch (PluginException $e) {
179 1
            $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 1
     * @param $code string sプラグインコード
192
     *
193
     * @throws PluginException
194 1
     */
195 1
    public function installWithCode($code)
196 1
    {
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) {
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 1
            }
216
        }
217
218
        $this->checkSamePlugin($config['code']);
219 1
        $this->postInstall($config, $config['source']);
220 1
    }
221
222
    // インストール事前処理
223
    public function preInstall()
224 1
    {
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 1
                $Plugin = new Plugin();
242
                // インストール直後はプラグインは有効にしない
243
                $Plugin->setName($config['name'])
244 1
                    ->setEnabled(false)
245
                    ->setVersion($config['version'])
246
                    ->setSource($source)
247
                    ->setCode($config['code']);
248
                $this->entityManager->persist($Plugin);
249
                $this->entityManager->flush();
250 1
            }
251 1
252 1
            $this->generateProxyAndUpdateSchema($Plugin, $config);
253
254 1
            $this->callPluginManagerMethod($config, 'install');
255
256
            $Plugin->setInitialized(true);
257
            $this->entityManager->persist($Plugin);
258 1
            $this->entityManager->flush();
259
260
            $this->entityManager->flush();
261
            $this->entityManager->getConnection()->commit();
262
        } catch (\Exception $e) {
263 1
            $this->entityManager->getConnection()->rollback();
264 1
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
265 1
        }
266 1
    }
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 1
            try {
280
                // プラグインmetadata定義を追加
281 1
                $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 1
                    $chain->addDriver($driver, $namespace);
288 1
                    $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 1
    {
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 1
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
308
309
        if (!mkdir($d, 0777)) {
310
            throw new PluginException($php_errormsg.$d);
311
        }
312
313 1
        return $d;
314
    }
315
316 1
    public function deleteDirs($arr)
317
    {
318
        foreach ($arr as $dir) {
319 1
            if (file_exists($dir)) {
320
                $fs = new Filesystem();
321
                $fs->remove($dir);
322
            }
323 1
        }
324
    }
325
326 1
    /**
327
     * @param string $archive
328
     * @param string $dir
329
     *
330 1
     * @throws PluginException
331
     */
332
    public function unpackPluginArchive($archive, $dir)
333
    {
334
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
335 1
        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 1
        }
348 1
    }
349
350
    /**
351 1
     * @param $dir
352
     * @param array $config_cache
353
     *
354
     * @throws PluginException
355
     */
356 1
    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 1
368 1
        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 1
        }
374 1
        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 1
        }
382
    }
383
384
    /**
385
     * @param $pluginDir
386
     *
387
     * @return array
388
     *
389
     * @throws PluginException
390
     */
391 1
    public function readConfig($pluginDir)
392 1
    {
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 1
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
409 1
        }
410
411 1
        return [
412
            'code' => $json['extra']['code'],
413 1
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
414 1
            'version' => $json['version'],
415 1
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
416 1
        ];
417 1
    }
418 1
419
    public function checkSymbolName($string)
420 1
    {
421 1
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
422
        // plugin_nameやplugin_codeに使える文字のチェック
423 1
        // a-z A-Z 0-9 _
424 1
        // ディレクトリ名などに使われれるので厳しめ
425
    }
426 1
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 1
445
    public function calcPluginDir($code)
446 1
    {
447
        return $this->projectRoot.'/app/Plugin/'.$code;
448
    }
449
450 1
    /**
451 1
     * @param string $d
452 1
     *
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 1
     * @param $meta
465 1
     * @param int $source
466 1
     *
467 1
     * @return Plugin
468
     *
469 1
     * @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
     * Get plugin information
732
     *
733
     * @param array $plugin
734
     *
735
     * @return array|null
736
     */
737
    public function buildInfo($plugin)
738
    {
739
        $this->supportedVersion($plugin);
740
741
        return $plugin;
742
    }
743
744
    /**
745
     * Check support version
746
     *
747
     * @param $plugin
748
     */
749
    public function supportedVersion(&$plugin)
750
    {
751
        // Check the eccube version that the plugin supports.
752
        $plugin['version_check'] = false;
753
        if (in_array(Constant::VERSION, $plugin['supported_versions'])) {
754
            // Match version
755
            $plugin['version_check'] = true;
756
        }
757
    }
758
759
    /**
760
     * Find the dependent plugins that need to be disabled
761
     *
762
     * @param string $pluginCode
763
     *
764
     * @return array plugin code
765
     */
766
    public function findDependentPluginNeedDisable($pluginCode)
767
    {
768
        return $this->findDependentPlugin($pluginCode, true);
769
    }
770
771
    /**
772
     * Find the other plugin that has requires on it.
773
     * Check in both dtb_plugin table and <PluginCode>/composer.json
774
     *
775
     * @param string $pluginCode
776
     * @param bool   $enableOnly
777
     *
778
     * @return array plugin code
779
     */
780
    public function findDependentPlugin($pluginCode, $enableOnly = false)
781
    {
782
        $criteria = Criteria::create()
783
            ->where(Criteria::expr()->neq('code', $pluginCode));
784
        if ($enableOnly) {
785
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
786
        }
787
        /**
788
         * @var Plugin[]
789
         */
790
        $plugins = $this->pluginRepository->matching($criteria);
791
        $dependents = [];
792
        foreach ($plugins as $plugin) {
793
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
794
            $fileName = $dir.'/composer.json';
795
            if (!file_exists($fileName)) {
796
                continue;
797
            }
798
            $jsonText = file_get_contents($fileName);
799
            if ($jsonText) {
800
                $json = json_decode($jsonText, true);
801
                if (!isset($json['require'])) {
802
                    continue;
803
                }
804
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
805
                    $dependents[] = $plugin->getCode();
806
                }
807
            }
808
        }
809
810
        return $dependents;
811
    }
812
813
    /**
814
     * Get dependent plugin by code
815
     * It's base on composer.json
816
     * Return the plugin code and version in the format of the composer
817
     *
818
     * @param string   $pluginCode
819
     * @param int|null $libraryType
820
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
821
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
822
     *                      default : return all library/plugin
823
     *
824
     * @return array format [packageName1 => version1, packageName2 => version2]
825
     */
826
    public function getDependentByCode($pluginCode, $libraryType = null)
827
    {
828
        $pluginDir = $this->calcPluginDir($pluginCode);
829
        $jsonFile = $pluginDir.'/composer.json';
830
        if (!file_exists($jsonFile)) {
831
            return [];
832
        }
833
        $jsonText = file_get_contents($jsonFile);
834
        $json = json_decode($jsonText, true);
835
        $dependents = [];
836
        if (isset($json['require'])) {
837
            $require = $json['require'];
838
            switch ($libraryType) {
839 View Code Duplication
                case self::ECCUBE_LIBRARY:
840
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
841
                    break;
842
843 View Code Duplication
                case self::OTHER_LIBRARY:
844
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
845
                    break;
846
847
                default:
848
                    $dependents = $json['require'];
849
                    break;
850
            }
851
        }
852
853
        return $dependents;
854
    }
855
856
    /**
857
     * Format array dependent plugin to string
858
     * It is used for commands.
859
     *
860
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
861
     * @param bool  $getVersion
862
     *
863
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
864
     */
865
    public function parseToComposerCommand(array $packages, $getVersion = true)
866
    {
867
        $result = array_keys($packages);
868
        if ($getVersion) {
869
            $result = array_map(function ($package, $version) {
870
                return $package.':'.$version;
871
            }, array_keys($packages), array_values($packages));
872
        }
873
874
        return implode(' ', $result);
875
    }
876
877
    /**
878
     * リソースファイル等をコピー
879
     * コピー元となるファイルの置き場所は固定であり、
880
     * [プラグインコード]/Resource/assets
881
     * 配下に置かれているファイルが所定の位置へコピーされる
882
     *
883
     * @param string $pluginBaseDir
884
     * @param $pluginCode
885
     */
886
    public function copyAssets($pluginBaseDir, $pluginCode)
887
    {
888
        $assetsDir = $pluginBaseDir.'/Resource/assets';
889
890
        // プラグインにリソースファイルがあれば所定の位置へコピー
891
        if (file_exists($assetsDir)) {
892
            $file = new Filesystem();
893
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
894
        }
895
    }
896
897
    /**
898
     * コピーしたリソースファイル等を削除
899
     *
900
     * @param string $pluginCode
901
     */
902
    public function removeAssets($pluginCode)
903
    {
904
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
905
906
        // コピーされているリソースファイルがあれば削除
907
        if (file_exists($assetsDir)) {
908
            $file = new Filesystem();
909
            $file->remove($assetsDir);
910
        }
911
    }
912
913
    /**
914
     * Is update
915
     *
916
     * @param string $pluginVersion
917
     * @param string $remoteVersion
918
     *
919
     * @return boolean
920
     */
921
    public function isUpdate($pluginVersion, $remoteVersion)
922
    {
923
        return version_compare($pluginVersion, $remoteVersion, '<');
924
    }
925
926
    /**
927
     * Plugin is exist check
928
     *
929
     * @param array  $plugins    get from api
930
     * @param string $pluginCode
931
     *
932
     * @return false|int|string
933
     */
934
    public function checkPluginExist($plugins, $pluginCode)
935
    {
936
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
937
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
938
        }
939
        // Find plugin in array
940
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
941
942
        return $index;
943
    }
944
945
    /**
946
     * @param string $code
947
     *
948
     * @return bool
949
     */
950
    private function isEnable($code)
951
    {
952
        $Plugin = $this->pluginRepository->findOneBy([
953
            'enabled' => Constant::ENABLED,
954
            'code' => $code,
955
        ]);
956
        if ($Plugin) {
957
            return true;
958
        }
959
960
        return false;
961
    }
962
}
963