Completed
Push — 4.0 ( 8a9683...64d855 )
by Ryo
11:41 queued 04:40
created

PluginService::supportedVersion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 9
ccs 0
cts 4
cp 0
crap 6
rs 9.9666
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\Common\Constant;
20
use Eccube\Common\EccubeConfig;
21
use Eccube\Entity\Plugin;
22
use Eccube\Exception\PluginApiException;
23
use Eccube\Exception\PluginException;
24
use Eccube\Repository\PluginRepository;
25
use Eccube\Service\Composer\ComposerApiService;
26
use Eccube\Service\Composer\ComposerServiceInterface;
27
use Eccube\Util\CacheUtil;
28
use Eccube\Util\StringUtil;
29
use Symfony\Component\DependencyInjection\ContainerInterface;
30
use Symfony\Component\Filesystem\Filesystem;
31
32
class PluginService
33
{
34
    /**
35
     * @var EccubeConfig
36
     */
37
    protected $eccubeConfig;
38
39
    /**
40
     * @var EntityManager
41
     */
42
    protected $entityManager;
43
44
    /**
45
     * @var PluginRepository
46
     */
47
    protected $pluginRepository;
48
49
    /**
50
     * @var EntityProxyService
51
     */
52
    protected $entityProxyService;
53
54
    /**
55
     * @var SchemaService
56
     */
57
    protected $schemaService;
58
59
    /**
60
     * @var ComposerServiceInterface
61
     */
62
    protected $composerService;
63
64
    const VENDOR_NAME = 'ec-cube';
65
66
    /**
67
     * Plugin type/library of ec-cube
68
     */
69
    const ECCUBE_LIBRARY = 1;
70
71
    /**
72
     * Plugin type/library of other (except ec-cube)
73
     */
74
    const OTHER_LIBRARY = 2;
75
76
    /**
77
     * @var string %kernel.project_dir%
78
     */
79
    private $projectRoot;
80
81
    /**
82
     * @var string %kernel.environment%
83
     */
84
    private $environment;
85
86
    /**
87
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
88
     */
89
    protected $container;
90
91
    /** @var CacheUtil */
92
    protected $cacheUtil;
93
94
    /**
95
     * @var PluginApiService
96
     */
97
    private $pluginApiService;
98
99
    /**
100
     * PluginService constructor.
101
     *
102
     * @param EntityManagerInterface $entityManager
103
     * @param PluginRepository $pluginRepository
104
     * @param EntityProxyService $entityProxyService
105
     * @param SchemaService $schemaService
106
     * @param EccubeConfig $eccubeConfig
107
     * @param ContainerInterface $container
108
     * @param CacheUtil $cacheUtil
109
     * @param ComposerApiService $composerService
110
     * @param PluginApiService $pluginApiService
111
     */
112
    public function __construct(
113
        EntityManagerInterface $entityManager,
114
        PluginRepository $pluginRepository,
115
        EntityProxyService $entityProxyService,
116
        SchemaService $schemaService,
117
        EccubeConfig $eccubeConfig,
118
        ContainerInterface $container,
119
        CacheUtil $cacheUtil,
120 1
        ComposerApiService $composerService,
121
        PluginApiService $pluginApiService
122
    ) {
123
        $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...
124
        $this->pluginRepository = $pluginRepository;
125
        $this->entityProxyService = $entityProxyService;
126
        $this->schemaService = $schemaService;
127
        $this->eccubeConfig = $eccubeConfig;
128
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
129
        $this->environment = $eccubeConfig->get('kernel.environment');
130 1
        $this->container = $container;
131 1
        $this->cacheUtil = $cacheUtil;
132 1
        $this->composerService = $composerService;
133 1
        $this->pluginApiService = $pluginApiService;
134 1
    }
135 1
136 1
    /**
137 1
     * ファイル指定してのプラグインインストール
138 1
     *
139 1
     * @param string $path   path to tar.gz/zip plugin file
140
     * @param int    $source
141
     *
142
     * @return boolean
143
     *
144
     * @throws PluginException
145
     * @throws \Exception
146
     */
147
    public function install($path, $source = 0)
148
    {
149
        $pluginBaseDir = null;
150
        $tmp = null;
151
        try {
152
            // プラグイン配置前に実施する処理
153 1
            $this->preInstall();
154
            $tmp = $this->createTempDir();
155 1
156 1
            // 一旦テンポラリに展開
157
            $this->unpackPluginArchive($path, $tmp);
158
            $this->checkPluginArchiveContent($tmp);
159 1
160 1
            $config = $this->readConfig($tmp);
161
            // テンポラリのファイルを削除
162
            $this->deleteFile($tmp);
163 1
164 1
            // 重複していないかチェック
165
            $this->checkSamePlugin($config['code']);
166 1
167 1
            $pluginBaseDir = $this->calcPluginDir($config['code']);
168
            // 本来の置き場所を作成
169 1
            $this->createPluginDir($pluginBaseDir);
170
171
            // 問題なければ本当のplugindirへ
172 1
            $this->unpackPluginArchive($path, $pluginBaseDir);
173
174 1
            // Check dependent plugin
175
            // Don't install ec-cube library
176 1
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
177
//            if (!empty($dependents)) {
178
//                $package = $this->parseToComposerCommand($dependents);
179 1
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
180
//                $this->composerService->execRequire($package);
181
//            }
182
183
            // プラグイン配置後に実施する処理
184
            $this->postInstall($config, $source);
185
            // リソースファイルをコピー
186
            $this->copyAssets($pluginBaseDir, $config['code']);
187
        } catch (PluginException $e) {
188
            $this->deleteDirs([$tmp, $pluginBaseDir]);
189
            throw $e;
190
        } catch (\Exception $e) {
191 1
            // インストーラがどんなExceptionを上げるかわからないので
192
            $this->deleteDirs([$tmp, $pluginBaseDir]);
193
            throw $e;
194 1
        }
195 1
196 1
        return true;
197
    }
198
199
    /**
200
     * @param $code string sプラグインコード
201
     *
202
     * @throws PluginException
203
     */
204
    public function installWithCode($code)
205
    {
206
        $pluginDir = $this->calcPluginDir($code);
207
        $this->checkPluginArchiveContent($pluginDir);
208
        $config = $this->readConfig($pluginDir);
209
210
        if (isset($config['source']) && $config['source']) {
211
            // 依存プラグインが有効になっていない場合はエラー
212
            $requires = $this->getPluginRequired($config);
213 View Code Duplication
            $notInstalledOrDisabled = array_filter($requires, function ($req) {
214
                $code = preg_replace('/^ec-cube\//', '', $req['name']);
215 1
                /** @var Plugin $DependPlugin */
216
                $DependPlugin = $this->pluginRepository->findOneBy(['code' => $code]);
217
218
                return $DependPlugin ? $DependPlugin->isEnabled() == false : true;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
219 1
            });
220 1
221
            if (!empty($notInstalledOrDisabled)) {
222
                $names = array_map(function ($p) { return $p['name']; }, $notInstalledOrDisabled);
223
                throw new PluginException(implode(', ', $names).'を有効化してください。');
224 1
            }
225
        }
226
227
        $this->checkSamePlugin($config['code']);
228
        $this->postInstall($config, $config['source']);
229
    }
230
231
    // インストール事前処理
232
    public function preInstall()
233
    {
234
        // キャッシュの削除
235
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
236
//        $this->cacheUtil->clearCache();
237
    }
238
239
    // インストール事後処理
240
    public function postInstall($config, $source)
241 1
    {
242
        // dbにプラグイン登録
243
244 1
        $this->entityManager->getConnection()->beginTransaction();
245
246
        try {
247
            $Plugin = $this->pluginRepository->findByCode($config['code']);
248
249
            if (!$Plugin) {
250 1
                $Plugin = new Plugin();
251 1
                // インストール直後はプラグインは有効にしない
252 1
                $Plugin->setName($config['name'])
253
                    ->setEnabled(false)
254 1
                    ->setVersion($config['version'])
255
                    ->setSource($source)
256
                    ->setCode($config['code']);
257
                $this->entityManager->persist($Plugin);
258 1
                $this->entityManager->flush();
259
            }
260
261
            $this->generateProxyAndUpdateSchema($Plugin, $config);
262
263 1
            $this->callPluginManagerMethod($config, 'install');
264 1
265 1
            $Plugin->setInitialized(true);
266 1
            $this->entityManager->persist($Plugin);
267
            $this->entityManager->flush();
268
269
            $this->entityManager->flush();
270
            $this->entityManager->getConnection()->commit();
271
        } catch (\Exception $e) {
272
            $this->entityManager->getConnection()->rollback();
273
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
274
        }
275
    }
276
277
    public function generateProxyAndUpdateSchema(Plugin $plugin, $config)
278
    {
279 1
        if ($plugin->isEnabled()) {
280
            $generatedFiles = $this->regenerateProxy($plugin, false);
281 1
            $this->schemaService->updateSchema($generatedFiles, $this->projectRoot.'/app/proxy/entity');
282
        } else {
283
            // Proxyのクラスをロードせずにスキーマを更新するために、
284
            // インストール時には一時的なディレクトリにProxyを生成する
285
            $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
286
            @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...
287 1
288 1
            try {
289
                // プラグインmetadata定義を追加
290
                $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
291
                if (file_exists($entityDir)) {
292
                    $ormConfig = $this->entityManager->getConfiguration();
293
                    $chain = $ormConfig->getMetadataDriverImpl();
294
                    $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
295
                    $namespace = 'Plugin\\'.$config['code'].'\\Entity';
296
                    $chain->addDriver($driver, $namespace);
297
                    $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
298
                }
299
300
                // 一時的に利用するProxyを生成してからスキーマを更新する
301
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
302
                $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
303
            } finally {
304 1
                foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
305
                    unlink($f);
306
                }
307 1
                rmdir($tmpProxyOutputDir);
308
            }
309
        }
310
    }
311
312
    public function createTempDir()
313 1
    {
314
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
315
        @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...
316 1
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
317
318
        if (!mkdir($d, 0777)) {
319 1
            throw new PluginException($php_errormsg.$d);
320
        }
321
322
        return $d;
323 1
    }
324
325
    public function deleteDirs($arr)
326 1
    {
327
        foreach ($arr as $dir) {
328
            if (file_exists($dir)) {
329
                $fs = new Filesystem();
330 1
                $fs->remove($dir);
331
            }
332
        }
333
    }
334
335 1
    /**
336
     * @param string $archive
337
     * @param string $dir
338
     *
339
     * @throws PluginException
340
     */
341
    public function unpackPluginArchive($archive, $dir)
342
    {
343
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
344
        try {
345
            if ($extension == 'zip') {
346
                $zip = new \ZipArchive();
347 1
                $zip->open($archive);
348 1
                $zip->extractTo($dir);
349
                $zip->close();
350
            } else {
351 1
                $phar = new \PharData($archive);
352
                $phar->extractTo($dir, null, true);
353
            }
354
        } catch (\Exception $e) {
355
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
356 1
        }
357
    }
358
359
    /**
360
     * @param $dir
361
     * @param array $config_cache
362
     *
363
     * @throws PluginException
364
     */
365
    public function checkPluginArchiveContent($dir, array $config_cache = [])
366
    {
367 1
        try {
368 1
            if (!empty($config_cache)) {
369
                $meta = $config_cache;
370
            } else {
371
                $meta = $this->readConfig($dir);
372
            }
373 1
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
374 1
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
375
        }
376
377
        if (!is_array($meta)) {
378
            throw new PluginException('config.yml not found or syntax error');
379
        }
380
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
381 1
            throw new PluginException('config.yml code empty or invalid_character(\W)');
382
        }
383
        if (!isset($meta['name'])) {
384
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
385
            throw new PluginException('config.yml name empty');
386
        }
387
        if (!isset($meta['version'])) {
388
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
389
            throw new PluginException('config.yml version invalid_character(\W) ');
390
        }
391 1
    }
392 1
393
    /**
394
     * @param $pluginDir
395
     *
396
     * @return array
397
     *
398
     * @throws PluginException
399
     */
400
    public function readConfig($pluginDir)
401
    {
402
        $composerJsonPath = $pluginDir.DIRECTORY_SEPARATOR.'composer.json';
403
        if (file_exists($composerJsonPath) === false) {
404
            throw new PluginException("${composerJsonPath} not found.");
405
        }
406
407
        $json = json_decode(file_get_contents($composerJsonPath), true);
408 1
        if ($json === null) {
409 1
            throw new PluginException("Invalid json format. [${composerJsonPath}]");
410
        }
411 1
412
        if (!isset($json['version'])) {
413 1
            throw new PluginException("`version` is not defined in ${composerJsonPath}");
414 1
        }
415 1
416 1
        if (!isset($json['extra']['code'])) {
417 1
            throw new PluginException("`extra.code` is not defined in ${composerJsonPath}");
418 1
        }
419
420 1
        return [
421 1
            'code' => $json['extra']['code'],
422
            'name' => isset($json['description']) ? $json['description'] : $json['extra']['code'],
423 1
            'version' => $json['version'],
424 1
            'source' => isset($json['extra']['id']) ? $json['extra']['id'] : false,
425
        ];
426 1
    }
427
428
    public function checkSymbolName($string)
429
    {
430
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
431
        // plugin_nameやplugin_codeに使える文字のチェック
432
        // a-z A-Z 0-9 _
433
        // ディレクトリ名などに使われれるので厳しめ
434
    }
435
436
    /**
437
     * @param string $path
438
     */
439
    public function deleteFile($path)
440
    {
441
        $f = new Filesystem();
442
        $f->remove($path);
443
    }
444 1
445
    public function checkSamePlugin($code)
446 1
    {
447
        /** @var Plugin $Plugin */
448
        $Plugin = $this->pluginRepository->findOneBy(['code' => $code]);
449
        if ($Plugin && $Plugin->isInitialized()) {
450 1
            throw new PluginException('plugin already installed.');
451 1
        }
452 1
    }
453
454
    public function calcPluginDir($code)
455
    {
456
        return $this->projectRoot.'/app/Plugin/'.$code;
457
    }
458
459
    /**
460
     * @param string $d
461
     *
462
     * @throws PluginException
463
     */
464 1
    public function createPluginDir($d)
465 1
    {
466 1
        $b = @mkdir($d);
467 1
        if (!$b) {
468
            throw new PluginException($php_errormsg);
469 1
        }
470
    }
471
472
    /**
473
     * @param $meta
474
     * @param int $source
475
     *
476
     * @return Plugin
477
     *
478
     * @throws PluginException
479
     */
480
    public function registerPlugin($meta, $source = 0)
481
    {
482
        try {
483
            $p = new Plugin();
484
            // インストール直後はプラグインは有効にしない
485
            $p->setName($meta['name'])
486
                ->setEnabled(false)
487
                ->setVersion($meta['version'])
488
                ->setSource($source)
489
                ->setCode($meta['code']);
490
491
            $this->entityManager->persist($p);
492
            $this->entityManager->flush($p);
493
494
            $this->pluginApiService->pluginInstalled($p);
495
        } catch (\Exception $e) {
496
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
497
        }
498
499
        return $p;
500
    }
501
502
    /**
503
     * @param $meta
504
     * @param string $method
505
     */
506
    public function callPluginManagerMethod($meta, $method)
507
    {
508
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
509
        if (class_exists($class)) {
510
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
511
            if (method_exists($installer, $method)) {
512
                $installer->$method($meta, $this->container);
513
            }
514
        }
515
    }
516
517
    /**
518
     * @param Plugin $plugin
519
     * @param bool $force
520
     *
521
     * @return bool
522
     *
523
     * @throws \Exception
524
     */
525
    public function uninstall(Plugin $plugin, $force = true)
526
    {
527
        $pluginDir = $this->calcPluginDir($plugin->getCode());
528
        $this->cacheUtil->clearCache();
529
        $config = $this->readConfig($pluginDir);
530
531
        if ($plugin->isEnabled()) {
532
            $this->disable($plugin);
533
        }
534
        $this->callPluginManagerMethod($config, 'uninstall');
535
        $this->unregisterPlugin($plugin);
536
537
        // スキーマを更新する
538
        //FIXME: Update schema before no affect
539
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
540
541
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
542
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
543
        $this->schemaService->dropTable($namespace);
544
545
        if ($force) {
546
            $this->deleteFile($pluginDir);
547
            $this->removeAssets($plugin->getCode());
548
        }
549
550
        $this->pluginApiService->pluginUninstalled($plugin);
551
        return true;
552
    }
553
554
    public function unregisterPlugin(Plugin $p)
555
    {
556
        try {
557
            $em = $this->entityManager;
558
            $em->remove($p);
559
            $em->flush();
560
        } catch (\Exception $e) {
561
            throw $e;
562
        }
563
    }
564
565
    public function disable(Plugin $plugin)
566
    {
567
        return $this->enable($plugin, false);
568
    }
569
570
    /**
571
     * Proxyを再生成します.
572
     *
573
     * @param Plugin $plugin プラグイン
574
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
575
     * @param string|null $outputDir 出力先
576
     *
577
     * @return array 生成されたファイルのパス
578
     */
579
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
580
    {
581
        if (is_null($outputDir)) {
582
            $outputDir = $this->projectRoot.'/app/proxy/entity';
583
        }
584
        @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...
585
586
        $enabledPluginCodes = array_map(
587
            function ($p) { return $p->getCode(); },
588
            $this->pluginRepository->findAllEnabled()
589
        );
590
591
        $excludes = [];
592
        if ($temporary || $plugin->isEnabled()) {
593
            $enabledPluginCodes[] = $plugin->getCode();
594
        } else {
595
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
596
            if ($index >= 0) {
597
                array_splice($enabledPluginCodes, $index, 1);
598
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
599
            }
600
        }
601
602
        $enabledPluginEntityDirs = array_map(function ($code) {
603
            return $this->projectRoot."/app/Plugin/${code}/Entity";
604
        }, $enabledPluginCodes);
605
606
        return $this->entityProxyService->generate(
607
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
608
            $excludes,
609
            $outputDir
610
        );
611
    }
612
613
    public function enable(Plugin $plugin, $enable = true)
614
    {
615
        $em = $this->entityManager;
616
        try {
617
            $pluginDir = $this->calcPluginDir($plugin->getCode());
618
            $config = $this->readConfig($pluginDir);
619
            $em->getConnection()->beginTransaction();
620
            $plugin->setEnabled($enable ? true : false);
621
            $em->persist($plugin);
622
623
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
624
625
            // Proxyだけ再生成してスキーマは更新しない
626
            $this->regenerateProxy($plugin, false);
627
628
            $em->flush();
629
            $em->getConnection()->commit();
630
631
            if ($enable) {
632
                $this->pluginApiService->pluginEnabled($plugin);
633
            } else {
634
                $this->pluginApiService->pluginDisabled($plugin);
635
            }
636
        } catch (\Exception $e) {
637
            $em->getConnection()->rollback();
638
            throw $e;
639
        }
640
641
        return true;
642
    }
643
644
    /**
645
     * Update plugin
646
     *
647
     * @param Plugin $plugin
648
     * @param string $path
649
     *
650
     * @return bool
651
     *
652
     * @throws PluginException
653
     * @throws \Exception
654
     */
655
    public function update(Plugin $plugin, $path)
656
    {
657
        $pluginBaseDir = null;
658
        $tmp = null;
659
        try {
660
            $this->cacheUtil->clearCache();
661
            $tmp = $this->createTempDir();
662
663
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
664
            $this->checkPluginArchiveContent($tmp);
665
666
            $config = $this->readConfig($tmp);
667
668
            if ($plugin->getCode() != $config['code']) {
669
                throw new PluginException('new/old plugin code is different.');
670
            }
671
672
            $pluginBaseDir = $this->calcPluginDir($config['code']);
673
            $this->deleteFile($tmp); // テンポラリのファイルを削除
674
675
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
676
677
            // Check dependent plugin
678
            // Don't install ec-cube library
679
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
680
            if (!empty($dependents)) {
681
                $package = $this->parseToComposerCommand($dependents);
682
                $this->composerService->execRequire($package);
683
            }
684
685
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
686
        } catch (PluginException $e) {
687
            $this->deleteDirs([$tmp]);
688
            throw $e;
689
        } catch (\Exception $e) {
690
            // catch exception of composer
691
            $this->deleteDirs([$tmp]);
692
            throw $e;
693
        }
694
695
        return true;
696
    }
697
698
    /**
699
     * Update plugin
700
     *
701
     * @param Plugin $plugin
702
     * @param array  $meta     Config data
703
     *
704
     * @throws \Exception
705
     */
706
    public function updatePlugin(Plugin $plugin, $meta)
707
    {
708
        $em = $this->entityManager;
709
        try {
710
            $em->getConnection()->beginTransaction();
711
            $plugin->setVersion($meta['version'])
712
                ->setName($meta['name']);
713
714
            $em->persist($plugin);
715
            $this->callPluginManagerMethod($meta, 'update');
716
            $em->flush();
717
            $em->getConnection()->commit();
718
        } catch (\Exception $e) {
719
            $em->getConnection()->rollback();
720
            throw $e;
721
        }
722
    }
723
724
    /**
725
     * Get array require by plugin
726
     * Todo: need define dependency plugin mechanism
727
     *
728
     * @param array|Plugin $plugin format as plugin from api
729
     *
730
     * @return array|mixed
731
     *
732
     * @throws PluginException
733
     */
734
    public function getPluginRequired($plugin)
735
    {
736
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
737
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
738
739
        $results = [];
740
741
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Eccube\Service\Composer\ComposerServiceInterface as the method foreachRequires() does only exist in the following implementations of said interface: Eccube\Service\Composer\ComposerApiService.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
742
            $results[] = $package;
743
        }, 'eccube-plugin');
744
745
        return $results;
746
    }
747
748
    /**
749
     * Find the dependent plugins that need to be disabled
750
     *
751
     * @param string $pluginCode
752
     *
753
     * @return array plugin code
754
     */
755
    public function findDependentPluginNeedDisable($pluginCode)
756
    {
757
        return $this->findDependentPlugin($pluginCode, true);
758
    }
759
760
    /**
761
     * Find the other plugin that has requires on it.
762
     * Check in both dtb_plugin table and <PluginCode>/composer.json
763
     *
764
     * @param string $pluginCode
765
     * @param bool   $enableOnly
766
     *
767
     * @return array plugin code
768
     */
769
    public function findDependentPlugin($pluginCode, $enableOnly = false)
770
    {
771
        $criteria = Criteria::create()
772
            ->where(Criteria::expr()->neq('code', $pluginCode));
773
        if ($enableOnly) {
774
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
775
        }
776
        /**
777
         * @var Plugin[]
778
         */
779
        $plugins = $this->pluginRepository->matching($criteria);
780
        $dependents = [];
781
        foreach ($plugins as $plugin) {
782
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
783
            $fileName = $dir.'/composer.json';
784
            if (!file_exists($fileName)) {
785
                continue;
786
            }
787
            $jsonText = file_get_contents($fileName);
788
            if ($jsonText) {
789
                $json = json_decode($jsonText, true);
790
                if (!isset($json['require'])) {
791
                    continue;
792
                }
793
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
794
                    $dependents[] = $plugin->getCode();
795
                }
796
            }
797
        }
798
799
        return $dependents;
800
    }
801
802
    /**
803
     * Get dependent plugin by code
804
     * It's base on composer.json
805
     * Return the plugin code and version in the format of the composer
806
     *
807
     * @param string   $pluginCode
808
     * @param int|null $libraryType
809
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
810
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
811
     *                      default : return all library/plugin
812
     *
813
     * @return array format [packageName1 => version1, packageName2 => version2]
814
     */
815
    public function getDependentByCode($pluginCode, $libraryType = null)
816
    {
817
        $pluginDir = $this->calcPluginDir($pluginCode);
818
        $jsonFile = $pluginDir.'/composer.json';
819
        if (!file_exists($jsonFile)) {
820
            return [];
821
        }
822
        $jsonText = file_get_contents($jsonFile);
823
        $json = json_decode($jsonText, true);
824
        $dependents = [];
825
        if (isset($json['require'])) {
826
            $require = $json['require'];
827
            switch ($libraryType) {
828 View Code Duplication
                case self::ECCUBE_LIBRARY:
829
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
830
                    break;
831
832 View Code Duplication
                case self::OTHER_LIBRARY:
833
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
834
                    break;
835
836
                default:
837
                    $dependents = $json['require'];
838
                    break;
839
            }
840
        }
841
842
        return $dependents;
843
    }
844
845
    /**
846
     * Format array dependent plugin to string
847
     * It is used for commands.
848
     *
849
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
850
     * @param bool  $getVersion
851
     *
852
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
853
     */
854
    public function parseToComposerCommand(array $packages, $getVersion = true)
855
    {
856
        $result = array_keys($packages);
857
        if ($getVersion) {
858
            $result = array_map(function ($package, $version) {
859
                return $package.':'.$version;
860
            }, array_keys($packages), array_values($packages));
861
        }
862
863
        return implode(' ', $result);
864
    }
865
866
    /**
867
     * リソースファイル等をコピー
868
     * コピー元となるファイルの置き場所は固定であり、
869
     * [プラグインコード]/Resource/assets
870
     * 配下に置かれているファイルが所定の位置へコピーされる
871
     *
872
     * @param string $pluginBaseDir
873
     * @param $pluginCode
874
     */
875
    public function copyAssets($pluginBaseDir, $pluginCode)
876
    {
877
        $assetsDir = $pluginBaseDir.'/Resource/assets';
878
879
        // プラグインにリソースファイルがあれば所定の位置へコピー
880
        if (file_exists($assetsDir)) {
881
            $file = new Filesystem();
882
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
883
        }
884
    }
885
886
    /**
887
     * コピーしたリソースファイル等を削除
888
     *
889
     * @param string $pluginCode
890
     */
891
    public function removeAssets($pluginCode)
892
    {
893
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
894
895
        // コピーされているリソースファイルがあれば削除
896
        if (file_exists($assetsDir)) {
897
            $file = new Filesystem();
898
            $file->remove($assetsDir);
899
        }
900
    }
901
902
    /**
903
     * Plugin is exist check
904
     *
905
     * @param array  $plugins    get from api
906
     * @param string $pluginCode
907
     *
908
     * @return false|int|string
909
     */
910
    public function checkPluginExist($plugins, $pluginCode)
911
    {
912
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
913
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
914
        }
915
        // Find plugin in array
916
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
917
918
        return $index;
919
    }
920
921
    /**
922
     * @param string $code
923
     *
924
     * @return bool
925
     */
926
    private function isEnable($code)
927
    {
928
        $Plugin = $this->pluginRepository->findOneBy([
929
            'enabled' => Constant::ENABLED,
930
            'code' => $code,
931
        ]);
932
        if ($Plugin) {
933
            return true;
934
        }
935
936
        return false;
937
    }
938
}
939