Completed
Pull Request — 4.0 (#3630)
by Kiyotaka
07:25
created

PluginService::getDependentByCode()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 29

Duplication

Lines 6
Ratio 20.69 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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