Completed
Push — sf/composer-json ( fccd30...96025e )
by Kiyotaka
06:00
created

PluginService::readConfig()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
694
                continue;
695
            }
696
697
            // Check duplicate in dependency
698
            $index = array_search($dependPlugin['product_code'], array_column($dependents, 'product_code'));
699
            if ($index === false) {
700
                // Update require version
701
                $dependPlugin['version'] = $version;
702
                $dependents[] = $dependPlugin;
703
                // Check child dependency
704
                $dependents = $this->getDependency($plugins, $dependPlugin, $dependents);
705
            }
706
        }
707
708
        return $dependents;
709
    }
710
711
    /**
712
     * Get plugin information
713
     *
714
     * @param array  $plugins    get from api
715
     * @param string $pluginCode
716
     *
717
     * @return array|null
718
     */
719
    public function buildInfo($plugins, $pluginCode)
720
    {
721
        $plugin = [];
722
        $index = $this->checkPluginExist($plugins, $pluginCode);
723
        if ($index === false) {
724
            return $plugin;
725
        }
726
        // Get target plugin in return of api
727
        $plugin = $plugins[$index];
728
729
        // Check the eccube version that the plugin supports.
730
        $plugin['is_supported_eccube_version'] = 0;
731
        if (in_array(Constant::VERSION, $plugin['eccube_version'])) {
732
            // Match version
733
            $plugin['is_supported_eccube_version'] = 1;
734
        }
735
736
        $plugin['depend'] = $this->getRequirePluginName($plugins, $plugin);
737
738
        return $plugin;
739
    }
740
741
    /**
742
     * Get dependency name and version only
743
     *
744
     * @param array $plugins get from api
745
     * @param array $plugin  target plugin from api
746
     *
747
     * @return mixed format [0 => ['name' => pluginName1, 'version' => pluginVersion1], 1 => ['name' => pluginName2, 'version' => pluginVersion2]]
748
     */
749
    public function getRequirePluginName($plugins, $plugin)
750
    {
751
        $depend = [];
752
        if (isset($plugin['require']) && !empty($plugin['require'])) {
753
            foreach ($plugin['require'] as $name => $version) {
754
                $ret = $this->checkPluginExist($plugins, $name);
755
                if ($ret === false) {
756
                    continue;
757
                }
758
                $depend[] = [
759
                    'name' => $plugins[$ret]['name'],
760
                    'version' => $version,
761
                ];
762
            }
763
        }
764
765
        return $depend;
766
    }
767
768
    /**
769
     * Check require plugin in enable
770
     *
771
     * @param string $pluginCode
772
     *
773
     * @return array plugin code
774
     */
775
    public function findRequirePluginNeedEnable($pluginCode)
776
    {
777
        $dir = $this->eccubeConfig['plugin_realdir'].'/'.$pluginCode;
778
        $composerFile = $dir.'/composer.json';
779
        if (!file_exists($composerFile)) {
780
            return [];
781
        }
782
        $jsonText = file_get_contents($composerFile);
783
        $json = json_decode($jsonText, true);
784
        // Check require
785
        if (!isset($json['require']) || empty($json['require'])) {
786
            return [];
787
        }
788
        $require = $json['require'];
789
790
        // Remove vendor plugin
791
        if (isset($require[self::VENDOR_NAME.'/plugin-installer'])) {
792
            unset($require[self::VENDOR_NAME.'/plugin-installer']);
793
        }
794
        $requires = [];
795
        foreach ($require as $name => $version) {
796
            // Check plugin of ec-cube only
797
            if (strpos($name, self::VENDOR_NAME.'/') !== false) {
798
                $requireCode = str_replace(self::VENDOR_NAME.'/', '', $name);
799
                $ret = $this->isEnable($requireCode);
800
                if ($ret) {
801
                    continue;
802
                }
803
                $requires[] = $requireCode;
804
            }
805
        }
806
807
        return $requires;
808
    }
809
810
    /**
811
     * Find the dependent plugins that need to be disabled
812
     *
813
     * @param string $pluginCode
814
     *
815
     * @return array plugin code
816
     */
817
    public function findDependentPluginNeedDisable($pluginCode)
818
    {
819
        return $this->findDependentPlugin($pluginCode, true);
820
    }
821
822
    /**
823
     * Find the other plugin that has requires on it.
824
     * Check in both dtb_plugin table and <PluginCode>/composer.json
825
     *
826
     * @param string $pluginCode
827
     * @param bool   $enableOnly
828
     *
829
     * @return array plugin code
830
     */
831
    public function findDependentPlugin($pluginCode, $enableOnly = false)
832
    {
833
        $criteria = Criteria::create()
834
            ->where(Criteria::expr()->neq('code', $pluginCode));
835
        if ($enableOnly) {
836
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
837
        }
838
        /**
839
         * @var Plugin[]
840
         */
841
        $plugins = $this->pluginRepository->matching($criteria);
842
        $dependents = [];
843
        foreach ($plugins as $plugin) {
844
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
845
            $fileName = $dir.'/composer.json';
846
            if (!file_exists($fileName)) {
847
                continue;
848
            }
849
            $jsonText = file_get_contents($fileName);
850
            if ($jsonText) {
851
                $json = json_decode($jsonText, true);
852
                if (!isset($json['require'])) {
853
                    continue;
854
                }
855
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
856
                    $dependents[] = $plugin->getCode();
857
                }
858
            }
859
        }
860
861
        return $dependents;
862
    }
863
864
    /**
865
     * Get dependent plugin by code
866
     * It's base on composer.json
867
     * Return the plugin code and version in the format of the composer
868
     *
869
     * @param string   $pluginCode
870
     * @param int|null $libraryType
871
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
872
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
873
     *                      default : return all library/plugin
874
     *
875
     * @return array format [packageName1 => version1, packageName2 => version2]
876
     */
877
    public function getDependentByCode($pluginCode, $libraryType = null)
878
    {
879
        $pluginDir = $this->calcPluginDir($pluginCode);
880
        $jsonFile = $pluginDir.'/composer.json';
881
        if (!file_exists($jsonFile)) {
882
            return [];
883
        }
884
        $jsonText = file_get_contents($jsonFile);
885
        $json = json_decode($jsonText, true);
886
        $dependents = [];
887
        if (isset($json['require'])) {
888
            $require = $json['require'];
889
            switch ($libraryType) {
890 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...
891
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
892
                    break;
893
894 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...
895
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
896
                    break;
897
898
                default:
899
                    $dependents = $json['require'];
900
                    break;
901
            }
902
        }
903
904
        return $dependents;
905
    }
906
907
    /**
908
     * Format array dependent plugin to string
909
     * It is used for commands.
910
     *
911
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
912
     * @param bool  $getVersion
913
     *
914
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
915
     */
916
    public function parseToComposerCommand(array $packages, $getVersion = true)
917
    {
918
        $result = array_keys($packages);
919
        if ($getVersion) {
920
            $result = array_map(function ($package, $version) {
921
                return $package.':'.$version;
922
            }, array_keys($packages), array_values($packages));
923
        }
924
925
        return implode(' ', $result);
926
    }
927
928
    /**
929
     * リソースファイル等をコピー
930
     * コピー元となるファイルの置き場所は固定であり、
931
     * [プラグインコード]/Resource/assets
932
     * 配下に置かれているファイルが所定の位置へコピーされる
933
     *
934
     * @param string $pluginBaseDir
935
     * @param $pluginCode
936
     */
937
    public function copyAssets($pluginBaseDir, $pluginCode)
938
    {
939
        $assetsDir = $pluginBaseDir.'/Resource/assets';
940
941
        // プラグインにリソースファイルがあれば所定の位置へコピー
942
        if (file_exists($assetsDir)) {
943
            $file = new Filesystem();
944
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
945
        }
946
    }
947
948
    /**
949
     * コピーしたリソースファイル等を削除
950
     *
951
     * @param string $pluginCode
952
     */
953
    public function removeAssets($pluginCode)
954
    {
955
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
956
957
        // コピーされているリソースファイルがあれば削除
958
        if (file_exists($assetsDir)) {
959
            $file = new Filesystem();
960
            $file->remove($assetsDir);
961
        }
962
    }
963
964
    /**
965
     * Is update
966
     *
967
     * @param string $pluginVersion
968
     * @param string $remoteVersion
969
     *
970
     * @return boolean
971
     */
972
    public function isUpdate($pluginVersion, $remoteVersion)
973
    {
974
        return version_compare($pluginVersion, $remoteVersion, '<');
975
    }
976
977
    /**
978
     * Plugin is exist check
979
     *
980
     * @param array  $plugins    get from api
981
     * @param string $pluginCode
982
     *
983
     * @return false|int|string
984
     */
985
    public function checkPluginExist($plugins, $pluginCode)
986
    {
987
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
988
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
989
        }
990
        // Find plugin in array
991
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
992
993
        return $index;
994
    }
995
996
    /**
997
     * @param string $code
998
     *
999
     * @return bool
1000
     */
1001
    private function isEnable($code)
1002
    {
1003
        $Plugin = $this->pluginRepository->findOneBy([
1004
            'enabled' => Constant::ENABLED,
1005
            'code' => $code,
1006
        ]);
1007
        if ($Plugin) {
1008
            return true;
1009
        }
1010
1011
        return false;
1012
    }
1013
}
1014