Completed
Push — sf/composer-json ( e61037...e160a8 )
by Kiyotaka
07:00
created

PluginService::checkSymbolName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 7
rs 10
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
    public function checkSymbolName($string)
359
    {
360
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
361
        // plugin_nameやplugin_codeに使える文字のチェック
362
        // a-z A-Z 0-9 _
363
        // ディレクトリ名などに使われれるので厳しめ
364
    }
365
366
    /**
367
     * @param string $path
368
     */
369
    public function deleteFile($path)
370
    {
371
        $f = new Filesystem();
372
        $f->remove($path);
373
    }
374
375
    public function checkSamePlugin($code)
376
    {
377
        $repo = $this->pluginRepository->findOneBy(['code' => $code]);
378
        if ($repo) {
379
            throw new PluginException('plugin already installed.');
380
        }
381
    }
382
383
    public function calcPluginDir($name)
384
    {
385
        return $this->projectRoot.'/app/Plugin/'.$name;
386
    }
387
388
    /**
389
     * @param string $d
390
     *
391
     * @throws PluginException
392
     */
393
    public function createPluginDir($d)
394
    {
395
        $b = @mkdir($d);
396
        if (!$b) {
397
            throw new PluginException($php_errormsg);
398
        }
399
    }
400
401
    /**
402
     * @param $meta
403
     * @param int $source
404
     *
405
     * @return Plugin
406
     *
407
     * @throws PluginException
408
     * @throws \Doctrine\DBAL\ConnectionException
409
     */
410
    public function registerPlugin($meta, $source = 0)
411
    {
412
        $em = $this->entityManager;
413
        $em->getConnection()->beginTransaction();
414
        try {
415
            $p = new Plugin();
416
            // インストール直後はプラグインは有効にしない
417
            $p->setName($meta['name'])
418
                ->setEnabled(false)
419
                ->setVersion($meta['version'])
420
                ->setSource($source)
421
                ->setCode($meta['code'])
422
                // TODO 日付の自動設定
423
                ->setCreateDate(new \DateTime())
424
                ->setUpdateDate(new \DateTime());
425
426
            $em->persist($p);
427
            $em->flush();
428
429
            $this->callPluginManagerMethod($meta, 'install');
430
431
            $em->flush();
432
            $em->getConnection()->commit();
433
        } catch (\Exception $e) {
434
            $em->getConnection()->rollback();
435
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
436
        }
437
438
        return $p;
439
    }
440
441
    /**
442
     * @param $meta
443
     * @param string $method
444
     */
445
    public function callPluginManagerMethod($meta, $method)
446
    {
447
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
448
        if (class_exists($class)) {
449
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
450
            if (method_exists($installer, $method)) {
451
                // FIXME appを削除.
452
                $installer->$method($meta, $this->app, $this->container);
453
            }
454
        }
455
    }
456
457
    /**
458
     * @param Plugin $plugin
459
     * @param bool $force
460
     *
461
     * @return bool
462
     * @throws \Exception
463
     */
464
    public function uninstall(Plugin $plugin, $force = true)
465
    {
466
        $pluginDir = $this->calcPluginDir($plugin->getCode());
467
        $this->cacheUtil->clearCache();
468
        $config = $this->readConfig($pluginDir);
469
        $this->callPluginManagerMethod($config, 'disable');
470
        $this->callPluginManagerMethod($config, 'uninstall');
471
        $this->disable($plugin);
472
        $this->unregisterPlugin($plugin);
473
474
        // スキーマを更新する
475
        //FIXME: Update schema before no affect
476
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
477
478
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
479
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
480
        $this->schemaService->dropTable($namespace);
481
482
        if ($force) {
483
            $this->deleteFile($pluginDir);
484
            $this->removeAssets($plugin->getCode());
485
        }
486
487
        return true;
488
    }
489
490
    public function unregisterPlugin(Plugin $p)
491
    {
492
        try {
493
            $em = $this->entityManager;
494
            $em->remove($p);
495
            $em->flush();
496
        } catch (\Exception $e) {
497
            throw $e;
498
        }
499
    }
500
501
    public function disable(Plugin $plugin)
502
    {
503
        return $this->enable($plugin, false);
504
    }
505
506
    /**
507
     * Proxyを再生成します.
508
     *
509
     * @param Plugin $plugin プラグイン
510
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
511
     * @param string|null $outputDir 出力先
512
     *
513
     * @return array 生成されたファイルのパス
514
     */
515
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
516
    {
517
        if (is_null($outputDir)) {
518
            $outputDir = $this->projectRoot.'/app/proxy/entity';
519
        }
520
        @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...
521
522
        $enabledPluginCodes = array_map(
523
            function ($p) { return $p->getCode(); },
524
            $this->pluginRepository->findAllEnabled()
525
        );
526
527
        $excludes = [];
528
        if ($temporary || $plugin->isEnabled()) {
529
            $enabledPluginCodes[] = $plugin->getCode();
530
        } else {
531
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
532
            if ($index >= 0) {
533
                array_splice($enabledPluginCodes, $index, 1);
534
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
535
            }
536
        }
537
538
        $enabledPluginEntityDirs = array_map(function ($code) {
539
            return $this->projectRoot."/app/Plugin/${code}/Entity";
540
        }, $enabledPluginCodes);
541
542
        return $this->entityProxyService->generate(
543
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
544
            $excludes,
545
            $outputDir
546
        );
547
    }
548
549
    public function enable(Plugin $plugin, $enable = true)
550
    {
551
        $em = $this->entityManager;
552
        try {
553
            $pluginDir = $this->calcPluginDir($plugin->getCode());
554
            $config = $this->readConfig($pluginDir);
555
            $em->getConnection()->beginTransaction();
556
            $plugin->setEnabled($enable ? true : false);
557
            $em->persist($plugin);
558
559
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
560
561
            // Proxyだけ再生成してスキーマは更新しない
562
            $this->regenerateProxy($plugin, false);
563
564
            $em->flush();
565
            $em->getConnection()->commit();
566
        } catch (\Exception $e) {
567
            $em->getConnection()->rollback();
568
            throw $e;
569
        }
570
571
        return true;
572
    }
573
574
    /**
575
     * Update plugin
576
     *
577
     * @param Plugin $plugin
578
     * @param string $path
579
     *
580
     * @return bool
581
     *
582
     * @throws PluginException
583
     * @throws \Exception
584
     */
585
    public function update(Plugin $plugin, $path)
586
    {
587
        $pluginBaseDir = null;
588
        $tmp = null;
589
        try {
590
            $this->cacheUtil->clearCache();
591
            $tmp = $this->createTempDir();
592
593
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
594
            $this->checkPluginArchiveContent($tmp);
595
596
            $config = $this->readConfig($tmp);
597
598
            if ($plugin->getCode() != $config['code']) {
599
                throw new PluginException('new/old plugin code is different.');
600
            }
601
602
            $pluginBaseDir = $this->calcPluginDir($config['code']);
603
            $this->deleteFile($tmp); // テンポラリのファイルを削除
604
605
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
606
607
            // Check dependent plugin
608
            // Don't install ec-cube library
609
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
610
            if (!empty($dependents)) {
611
                $package = $this->parseToComposerCommand($dependents);
612
                $this->composerService->execRequire($package);
613
            }
614
615
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
616
        } catch (PluginException $e) {
617
            $this->deleteDirs([$tmp]);
618
            throw $e;
619
        } catch (\Exception $e) {
620
            // catch exception of composer
621
            $this->deleteDirs([$tmp]);
622
            throw $e;
623
        }
624
625
        return true;
626
    }
627
628
    /**
629
     * Update plugin
630
     *
631
     * @param Plugin $plugin
632
     * @param array  $meta     Config data
633
     *
634
     * @throws \Exception
635
     */
636
    public function updatePlugin(Plugin $plugin, $meta)
637
    {
638
        $em = $this->entityManager;
639
        try {
640
            $em->getConnection()->beginTransaction();
641
            $plugin->setVersion($meta['version'])
642
                ->setName($meta['name']);
643
644
            $em->persist($plugin);
645
            $this->callPluginManagerMethod($meta, 'update');
646
            $em->flush();
647
            $em->getConnection()->commit();
648
        } catch (\Exception $e) {
649
            $em->getConnection()->rollback();
650
            throw $e;
651
        }
652
    }
653
654
    /**
655
     * Do check dependency plugin
656
     *
657
     * @param array $plugins    get from api
658
     * @param array $plugin     format as plugin from api
659
     * @param array $dependents template output
660
     *
661
     * @return array|mixed
662
     */
663
    public function getDependency($plugins, $plugin, $dependents = [])
664
    {
665
        // Prevent infinity loop
666
        if (empty($dependents)) {
667
            $dependents[] = $plugin;
668
        }
669
670
        // Check dependency
671
        if (!isset($plugin['require']) || empty($plugin['require'])) {
672
            return $dependents;
673
        }
674
675
        $require = $plugin['require'];
676
        // Check dependency
677
        foreach ($require as $pluginName => $version) {
678
            $dependPlugin = $this->buildInfo($plugins, $pluginName);
679
            // Prevent call self
680
            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...
681
                continue;
682
            }
683
684
            // Check duplicate in dependency
685
            $index = array_search($dependPlugin['product_code'], array_column($dependents, 'product_code'));
686
            if ($index === false) {
687
                // Update require version
688
                $dependPlugin['version'] = $version;
689
                $dependents[] = $dependPlugin;
690
                // Check child dependency
691
                $dependents = $this->getDependency($plugins, $dependPlugin, $dependents);
692
            }
693
        }
694
695
        return $dependents;
696
    }
697
698
    /**
699
     * Get plugin information
700
     *
701
     * @param array  $plugins    get from api
702
     * @param string $pluginCode
703
     *
704
     * @return array|null
705
     */
706
    public function buildInfo($plugins, $pluginCode)
707
    {
708
        $plugin = [];
709
        $index = $this->checkPluginExist($plugins, $pluginCode);
710
        if ($index === false) {
711
            return $plugin;
712
        }
713
        // Get target plugin in return of api
714
        $plugin = $plugins[$index];
715
716
        // Check the eccube version that the plugin supports.
717
        $plugin['is_supported_eccube_version'] = 0;
718
        if (in_array(Constant::VERSION, $plugin['eccube_version'])) {
719
            // Match version
720
            $plugin['is_supported_eccube_version'] = 1;
721
        }
722
723
        $plugin['depend'] = $this->getRequirePluginName($plugins, $plugin);
724
725
        return $plugin;
726
    }
727
728
    /**
729
     * Get dependency name and version only
730
     *
731
     * @param array $plugins get from api
732
     * @param array $plugin  target plugin from api
733
     *
734
     * @return mixed format [0 => ['name' => pluginName1, 'version' => pluginVersion1], 1 => ['name' => pluginName2, 'version' => pluginVersion2]]
735
     */
736
    public function getRequirePluginName($plugins, $plugin)
737
    {
738
        $depend = [];
739
        if (isset($plugin['require']) && !empty($plugin['require'])) {
740
            foreach ($plugin['require'] as $name => $version) {
741
                $ret = $this->checkPluginExist($plugins, $name);
742
                if ($ret === false) {
743
                    continue;
744
                }
745
                $depend[] = [
746
                    'name' => $plugins[$ret]['name'],
747
                    'version' => $version,
748
                ];
749
            }
750
        }
751
752
        return $depend;
753
    }
754
755
    /**
756
     * Check require plugin in enable
757
     *
758
     * @param string $pluginCode
759
     *
760
     * @return array plugin code
761
     */
762
    public function findRequirePluginNeedEnable($pluginCode)
763
    {
764
        $dir = $this->eccubeConfig['plugin_realdir'].'/'.$pluginCode;
765
        $composerFile = $dir.'/composer.json';
766
        if (!file_exists($composerFile)) {
767
            return [];
768
        }
769
        $jsonText = file_get_contents($composerFile);
770
        $json = json_decode($jsonText, true);
771
        // Check require
772
        if (!isset($json['require']) || empty($json['require'])) {
773
            return [];
774
        }
775
        $require = $json['require'];
776
777
        // Remove vendor plugin
778
        if (isset($require[self::VENDOR_NAME.'/plugin-installer'])) {
779
            unset($require[self::VENDOR_NAME.'/plugin-installer']);
780
        }
781
        $requires = [];
782
        foreach ($require as $name => $version) {
783
            // Check plugin of ec-cube only
784
            if (strpos($name, self::VENDOR_NAME.'/') !== false) {
785
                $requireCode = str_replace(self::VENDOR_NAME.'/', '', $name);
786
                $ret = $this->isEnable($requireCode);
787
                if ($ret) {
788
                    continue;
789
                }
790
                $requires[] = $requireCode;
791
            }
792
        }
793
794
        return $requires;
795
    }
796
797
    /**
798
     * Find the dependent plugins that need to be disabled
799
     *
800
     * @param string $pluginCode
801
     *
802
     * @return array plugin code
803
     */
804
    public function findDependentPluginNeedDisable($pluginCode)
805
    {
806
        return $this->findDependentPlugin($pluginCode, true);
807
    }
808
809
    /**
810
     * Find the other plugin that has requires on it.
811
     * Check in both dtb_plugin table and <PluginCode>/composer.json
812
     *
813
     * @param string $pluginCode
814
     * @param bool   $enableOnly
815
     *
816
     * @return array plugin code
817
     */
818
    public function findDependentPlugin($pluginCode, $enableOnly = false)
819
    {
820
        $criteria = Criteria::create()
821
            ->where(Criteria::expr()->neq('code', $pluginCode));
822
        if ($enableOnly) {
823
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
824
        }
825
        /**
826
         * @var Plugin[]
827
         */
828
        $plugins = $this->pluginRepository->matching($criteria);
829
        $dependents = [];
830
        foreach ($plugins as $plugin) {
831
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
832
            $fileName = $dir.'/composer.json';
833
            if (!file_exists($fileName)) {
834
                continue;
835
            }
836
            $jsonText = file_get_contents($fileName);
837
            if ($jsonText) {
838
                $json = json_decode($jsonText, true);
839
                if (!isset($json['require'])) {
840
                    continue;
841
                }
842
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
843
                    $dependents[] = $plugin->getCode();
844
                }
845
            }
846
        }
847
848
        return $dependents;
849
    }
850
851
    /**
852
     * Get dependent plugin by code
853
     * It's base on composer.json
854
     * Return the plugin code and version in the format of the composer
855
     *
856
     * @param string   $pluginCode
857
     * @param int|null $libraryType
858
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
859
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
860
     *                      default : return all library/plugin
861
     *
862
     * @return array format [packageName1 => version1, packageName2 => version2]
863
     */
864
    public function getDependentByCode($pluginCode, $libraryType = null)
865
    {
866
        $pluginDir = $this->calcPluginDir($pluginCode);
867
        $jsonFile = $pluginDir.'/composer.json';
868
        if (!file_exists($jsonFile)) {
869
            return [];
870
        }
871
        $jsonText = file_get_contents($jsonFile);
872
        $json = json_decode($jsonText, true);
873
        $dependents = [];
874
        if (isset($json['require'])) {
875
            $require = $json['require'];
876
            switch ($libraryType) {
877 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...
878
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
879
                    break;
880
881 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...
882
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
883
                    break;
884
885
                default:
886
                    $dependents = $json['require'];
887
                    break;
888
            }
889
        }
890
891
        return $dependents;
892
    }
893
894
    /**
895
     * Format array dependent plugin to string
896
     * It is used for commands.
897
     *
898
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
899
     * @param bool  $getVersion
900
     *
901
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
902
     */
903
    public function parseToComposerCommand(array $packages, $getVersion = true)
904
    {
905
        $result = array_keys($packages);
906
        if ($getVersion) {
907
            $result = array_map(function ($package, $version) {
908
                return $package.':'.$version;
909
            }, array_keys($packages), array_values($packages));
910
        }
911
912
        return implode(' ', $result);
913
    }
914
915
    /**
916
     * リソースファイル等をコピー
917
     * コピー元となるファイルの置き場所は固定であり、
918
     * [プラグインコード]/Resource/assets
919
     * 配下に置かれているファイルが所定の位置へコピーされる
920
     *
921
     * @param string $pluginBaseDir
922
     * @param $pluginCode
923
     */
924
    public function copyAssets($pluginBaseDir, $pluginCode)
925
    {
926
        $assetsDir = $pluginBaseDir.'/Resource/assets';
927
928
        // プラグインにリソースファイルがあれば所定の位置へコピー
929
        if (file_exists($assetsDir)) {
930
            $file = new Filesystem();
931
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
932
        }
933
    }
934
935
    /**
936
     * コピーしたリソースファイル等を削除
937
     *
938
     * @param string $pluginCode
939
     */
940
    public function removeAssets($pluginCode)
941
    {
942
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
943
944
        // コピーされているリソースファイルがあれば削除
945
        if (file_exists($assetsDir)) {
946
            $file = new Filesystem();
947
            $file->remove($assetsDir);
948
        }
949
    }
950
951
    /**
952
     * Is update
953
     *
954
     * @param string $pluginVersion
955
     * @param string $remoteVersion
956
     *
957
     * @return boolean
958
     */
959
    public function isUpdate($pluginVersion, $remoteVersion)
960
    {
961
        return version_compare($pluginVersion, $remoteVersion, '<');
962
    }
963
964
    /**
965
     * Plugin is exist check
966
     *
967
     * @param array  $plugins    get from api
968
     * @param string $pluginCode
969
     *
970
     * @return false|int|string
971
     */
972
    public function checkPluginExist($plugins, $pluginCode)
973
    {
974
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
975
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
976
        }
977
        // Find plugin in array
978
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
979
980
        return $index;
981
    }
982
983
    /**
984
     * @param string $code
985
     *
986
     * @return bool
987
     */
988
    private function isEnable($code)
989
    {
990
        $Plugin = $this->pluginRepository->findOneBy([
991
            'enabled' => Constant::ENABLED,
992
            'code' => $code,
993
        ]);
994
        if ($Plugin) {
995
            return true;
996
        }
997
998
        return false;
999
    }
1000
}
1001