Failed Conditions
Pull Request — 4.0 (#3855)
by Kiyotaka
10:49 queued 03:55
created

PluginService::regenerateProxy()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
nc 6
nop 4
dl 0
loc 33
rs 8.1475
c 0
b 0
f 0
ccs 0
cts 12
cp 0
crap 72
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service;
15
16
use Doctrine\Common\Collections\Criteria;
17
use Doctrine\ORM\EntityManager;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Eccube\Common\Constant;
20
use Eccube\Common\EccubeConfig;
21
use Eccube\Entity\Plugin;
22
use Eccube\Exception\PluginException;
23
use Eccube\Repository\PluginRepository;
24
use Eccube\Service\Composer\ComposerServiceInterface;
25
use Eccube\Util\CacheUtil;
26
use Eccube\Util\StringUtil;
27
use Symfony\Component\DependencyInjection\ContainerInterface;
28
use Symfony\Component\Filesystem\Filesystem;
29
30
class PluginService
31
{
32
    /**
33
     * @var EccubeConfig
34
     */
35
    protected $eccubeConfig;
36
37
    /**
38
     * @var EntityManager
39
     */
40
    protected $entityManager;
41
42
    /**
43
     * @var PluginRepository
44
     */
45
    protected $pluginRepository;
46
47
    /**
48
     * @var EntityProxyService
49
     */
50
    protected $entityProxyService;
51
52
    /**
53
     * @var SchemaService
54
     */
55
    protected $schemaService;
56
57
    /**
58
     * @var ComposerServiceInterface
59
     */
60
    protected $composerService;
61
62
    const VENDOR_NAME = 'ec-cube';
63
64
    /**
65
     * Plugin type/library of ec-cube
66
     */
67
    const ECCUBE_LIBRARY = 1;
68
69
    /**
70
     * Plugin type/library of other (except ec-cube)
71
     */
72
    const OTHER_LIBRARY = 2;
73
74
    /**
75
     * @var string %kernel.project_dir%
76
     */
77
    private $projectRoot;
78
79
    /**
80
     * @var string %kernel.environment%
81
     */
82
    private $environment;
83
84
    /**
85
     * @var ContainerInterface
86
     */
87
    protected $container;
88
89
    /** @var CacheUtil */
90
    protected $cacheUtil;
91
92
    /**
93
     * @var PluginApiService
94
     */
95
    private $pluginApiService;
96
97
    /**
98
     * PluginService constructor.
99
     *
100
     * @param EntityManagerInterface $entityManager
101
     * @param PluginRepository $pluginRepository
102
     * @param EntityProxyService $entityProxyService
103
     * @param SchemaService $schemaService
104
     * @param EccubeConfig $eccubeConfig
105
     * @param ContainerInterface $container
106
     * @param CacheUtil $cacheUtil
107
     * @param ComposerServiceInterface $composerService
108
     * @param PluginApiService $pluginApiService
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
        ComposerServiceInterface $composerService,
119
        PluginApiService $pluginApiService
120 1
    ) {
121
        $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...
122
        $this->pluginRepository = $pluginRepository;
123
        $this->entityProxyService = $entityProxyService;
124
        $this->schemaService = $schemaService;
125
        $this->eccubeConfig = $eccubeConfig;
126
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
127
        $this->environment = $eccubeConfig->get('kernel.environment');
128
        $this->container = $container;
129
        $this->cacheUtil = $cacheUtil;
130 1
        $this->composerService = $composerService;
131 1
        $this->pluginApiService = $pluginApiService;
132 1
    }
133 1
134 1
    /**
135 1
     * ファイル指定してのプラグインインストール
136 1
     *
137 1
     * @param string $path   path to tar.gz/zip plugin file
138 1
     * @param int    $source
139 1
     *
140
     * @return boolean
141
     *
142
     * @throws PluginException
143
     * @throws \Exception
144
     */
145
    public function install($path, $source = 0)
146
    {
147
        $pluginBaseDir = null;
148
        $tmp = null;
149
        try {
150
            // プラグイン配置前に実施する処理
151
            $this->preInstall();
152
            $tmp = $this->createTempDir();
153 1
154
            // 一旦テンポラリに展開
155 1
            $this->unpackPluginArchive($path, $tmp);
156 1
            $this->checkPluginArchiveContent($tmp);
157
158
            $config = $this->readConfig($tmp);
159 1
            // テンポラリのファイルを削除
160 1
            $this->deleteFile($tmp);
161
162
            // 重複していないかチェック
163 1
            $this->checkSamePlugin($config['code']);
164 1
165
            $pluginBaseDir = $this->calcPluginDir($config['code']);
166 1
            // 本来の置き場所を作成
167 1
            $this->createPluginDir($pluginBaseDir);
168
169 1
            // 問題なければ本当のplugindirへ
170
            $this->unpackPluginArchive($path, $pluginBaseDir);
171
172 1
            // Check dependent plugin
173
            // Don't install ec-cube library
174 1
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
175
//            if (!empty($dependents)) {
176 1
//                $package = $this->parseToComposerCommand($dependents);
177
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
178
//                $this->composerService->execRequire($package);
179 1
//            }
180
            // リソースファイルをコピー
181
            $this->copyAssets($config['code']);
182
            // プラグイン配置後に実施する処理
183
            $this->postInstall($config, $source);
184
        } catch (PluginException $e) {
185
            $this->deleteDirs([$tmp, $pluginBaseDir]);
186
            throw $e;
187
        } catch (\Exception $e) {
188
            // インストーラがどんなExceptionを上げるかわからないので
189
            $this->deleteDirs([$tmp, $pluginBaseDir]);
190
            throw $e;
191 1
        }
192
193
        return true;
194 1
    }
195 1
196 1
    /**
197
     * @param $code string sプラグインコード
198
     *
199
     * @throws PluginException
200
     */
201
    public function installWithCode($code)
202
    {
203
        $pluginDir = $this->calcPluginDir($code);
204
        $this->checkPluginArchiveContent($pluginDir);
205
        $config = $this->readConfig($pluginDir);
206
207
        if (isset($config['source']) && $config['source']) {
208
            // 依存プラグインが有効になっていない場合はエラー
209
            $requires = $this->getPluginRequired($config);
210 View Code Duplication
            $notInstalledOrDisabled = array_filter($requires, function ($req) {
211
                $code = preg_replace('/^ec-cube\//', '', $req['name']);
212
                /** @var Plugin $DependPlugin */
213
                $DependPlugin = $this->pluginRepository->findOneBy(['code' => $code]);
214
215 1
                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...
216
            });
217
218
            if (!empty($notInstalledOrDisabled)) {
219 1
                $names = array_map(function ($p) { return $p['name']; }, $notInstalledOrDisabled);
220 1
                throw new PluginException(implode(', ', $names).'を有効化してください。');
221
            }
222
        }
223
224 1
        $this->checkSamePlugin($config['code']);
225
        $this->copyAssets($config['code']);
226
        $this->postInstall($config, $config['source']);
227
    }
228
229
    // インストール事前処理
230
    public function preInstall()
231
    {
232
        // キャッシュの削除
233
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
234
//        $this->cacheUtil->clearCache();
235
    }
236
237
    // インストール事後処理
238
    public function postInstall($config, $source)
239
    {
240
        // dbにプラグイン登録
241 1
242
        $this->entityManager->getConnection()->beginTransaction();
243
244 1
        try {
245
            $Plugin = $this->pluginRepository->findByCode($config['code']);
246
247
            if (!$Plugin) {
248
                $Plugin = new Plugin();
249
                // インストール直後はプラグインは有効にしない
250 1
                $Plugin->setName($config['name'])
251 1
                    ->setEnabled(false)
252 1
                    ->setVersion($config['version'])
253
                    ->setSource($source)
254 1
                    ->setCode($config['code']);
255
                $this->entityManager->persist($Plugin);
256
                $this->entityManager->flush();
257
            }
258 1
259
            $this->generateProxyAndUpdateSchema($Plugin, $config);
260
261
            $this->callPluginManagerMethod($config, 'install');
262
263 1
            $Plugin->setInitialized(true);
264 1
            $this->entityManager->persist($Plugin);
265 1
            $this->entityManager->flush();
266 1
267
            $this->entityManager->flush();
268
            $this->entityManager->getConnection()->commit();
269
        } catch (\Exception $e) {
270
            $this->entityManager->getConnection()->rollback();
271
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
272
        }
273
    }
274
275
    public function generateProxyAndUpdateSchema(Plugin $plugin, $config, $uninstall = false)
276
    {
277
        if ($plugin->isEnabled()) {
278
            $generatedFiles = $this->regenerateProxy($plugin, false);
279 1
            $this->schemaService->updateSchema($generatedFiles, $this->projectRoot.'/app/proxy/entity');
280
        } else {
281 1
            // Proxyのクラスをロードせずにスキーマを更新するために、
282
            // インストール時には一時的なディレクトリにProxyを生成する
283
            $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
284
            @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...
285
286
            try {
287 1
                if (!$uninstall) {
288 1
                    // プラグインmetadata定義を追加
289
                    $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
290
                    if (file_exists($entityDir)) {
291
                        $ormConfig = $this->entityManager->getConfiguration();
292
                        $chain = $ormConfig->getMetadataDriverImpl();
293
                        $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
294
                        $namespace = 'Plugin\\'.$config['code'].'\\Entity';
295
                        $chain->addDriver($driver, $namespace);
296
                        $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
297
                    }
298
                }
299
300
                // 一時的に利用するProxyを生成してからスキーマを更新する
301
                $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir, $uninstall);
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(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $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(trans('admin.store.plugin.mkdir.error', ['%dir_name%' => $d]));
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
535
        // 初期化されていない場合はPluginManager#uninstall()は実行しない
536
        if ($plugin->isInitialized()) {
537
            $this->callPluginManagerMethod($config, 'uninstall');
538
        }
539
        $this->unregisterPlugin($plugin);
540
541
        // スキーマを更新する
542
        $this->generateProxyAndUpdateSchema($plugin, $config, true);
543
544
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
545
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
546
        $this->schemaService->dropTable($namespace);
547
548
        if ($force) {
549
            $this->deleteFile($pluginDir);
550
            $this->removeAssets($plugin->getCode());
551
        }
552
553
        $this->pluginApiService->pluginUninstalled($plugin);
554
555
        return true;
556
    }
557
558
    public function unregisterPlugin(Plugin $p)
559
    {
560
        try {
561
            $em = $this->entityManager;
562
            $em->remove($p);
563
            $em->flush();
564
        } catch (\Exception $e) {
565
            throw $e;
566
        }
567
    }
568
569
    public function disable(Plugin $plugin)
570
    {
571
        return $this->enable($plugin, false);
572
    }
573
574
    /**
575
     * Proxyを再生成します.
576
     *
577
     * @param Plugin $plugin プラグイン
578
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
579
     * @param string|null $outputDir 出力先
580
     * @param bool $uninstall プラグイン削除の場合はtrue
581
     *
582
     * @return array 生成されたファイルのパス
583
     */
584
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null, $uninstall = false)
585
    {
586
        if (is_null($outputDir)) {
587
            $outputDir = $this->projectRoot.'/app/proxy/entity';
588
        }
589
        @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...
590
591
        $enabledPluginCodes = array_map(
592
            function ($p) { return $p->getCode(); },
593
            $temporary ? $this->pluginRepository->findAll() : $this->pluginRepository->findAllEnabled()
594
        );
595
596
        $excludes = [];
597
        if (!$uninstall && ($temporary || $plugin->isEnabled())) {
598
            $enabledPluginCodes[] = $plugin->getCode();
599
        } else {
600
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
601
            if ($index !== false && $index >= 0) {
602
                array_splice($enabledPluginCodes, $index, 1);
603
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
604
            }
605
        }
606
607
        $enabledPluginEntityDirs = array_map(function ($code) {
608
            return $this->projectRoot."/app/Plugin/${code}/Entity";
609
        }, $enabledPluginCodes);
610
611
        return $this->entityProxyService->generate(
612
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
613
            $excludes,
614
            $outputDir
615
        );
616
    }
617
618
    public function enable(Plugin $plugin, $enable = true)
619
    {
620
        $em = $this->entityManager;
621
        try {
622
            $pluginDir = $this->calcPluginDir($plugin->getCode());
623
            $config = $this->readConfig($pluginDir);
624
            $em->getConnection()->beginTransaction();
625
            $plugin->setEnabled($enable ? true : false);
626
            $em->persist($plugin);
627
628
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
629
630
            // Proxyだけ再生成してスキーマは更新しない
631
            $this->regenerateProxy($plugin, false);
632
633
            $em->flush();
634
            $em->getConnection()->commit();
635
636
            if ($enable) {
637
                $this->pluginApiService->pluginEnabled($plugin);
638
            } else {
639
                $this->pluginApiService->pluginDisabled($plugin);
640
            }
641
        } catch (\Exception $e) {
642
            $em->getConnection()->rollback();
643
            throw $e;
644
        }
645
646
        return true;
647
    }
648
649
    /**
650
     * Update plugin
651
     *
652
     * @param Plugin $plugin
653
     * @param string $path
654
     *
655
     * @return bool
656
     *
657
     * @throws PluginException
658
     * @throws \Exception
659
     */
660
    public function update(Plugin $plugin, $path)
661
    {
662
        $pluginBaseDir = null;
663
        $tmp = null;
664
        try {
665
            $this->cacheUtil->clearCache();
666
            $tmp = $this->createTempDir();
667
668
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
669
            $this->checkPluginArchiveContent($tmp);
670
671
            $config = $this->readConfig($tmp);
672
673
            if ($plugin->getCode() != $config['code']) {
674
                throw new PluginException('new/old plugin code is different.');
675
            }
676
677
            $pluginBaseDir = $this->calcPluginDir($config['code']);
678
            $this->deleteFile($tmp); // テンポラリのファイルを削除
679
680
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
681
682
            // Check dependent plugin
683
            // Don't install ec-cube library
684
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
685
            if (!empty($dependents)) {
686
                $package = $this->parseToComposerCommand($dependents);
687
                $this->composerService->execRequire($package);
688
            }
689
690
            $this->copyAssets($plugin->getCode());
691
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
692
        } catch (PluginException $e) {
693
            $this->deleteDirs([$tmp]);
694
            throw $e;
695
        } catch (\Exception $e) {
696
            // catch exception of composer
697
            $this->deleteDirs([$tmp]);
698
            throw $e;
699
        }
700
701
        return true;
702
    }
703
704
    /**
705
     * Update plugin
706
     *
707
     * @param Plugin $plugin
708
     * @param array  $meta     Config data
709
     *
710
     * @throws \Exception
711
     */
712
    public function updatePlugin(Plugin $plugin, $meta)
713
    {
714
        $em = $this->entityManager;
715
        try {
716
            $em->getConnection()->beginTransaction();
717
            $plugin->setVersion($meta['version'])
718
                ->setName($meta['name']);
719
720
            $em->persist($plugin);
721
722
            if ($plugin->isInitialized()) {
723
                $this->callPluginManagerMethod($meta, 'update');
724
            }
725
            $this->copyAssets($plugin->getCode());
726
            $em->flush();
727
            $em->getConnection()->commit();
728
        } catch (\Exception $e) {
729
            $em->getConnection()->rollback();
730
            throw $e;
731
        }
732
    }
733
734
    /**
735
     * Get array require by plugin
736
     * Todo: need define dependency plugin mechanism
737
     *
738
     * @param array|Plugin $plugin format as plugin from api
739
     *
740
     * @return array|mixed
741
     *
742
     * @throws PluginException
743
     */
744
    public function getPluginRequired($plugin)
745
    {
746
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
747
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
748
749
        $results = [];
750
751
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
752
            $results[] = $package;
753
        }, 'eccube-plugin');
754
755
        return $results;
756
    }
757
758
    /**
759
     * Find the dependent plugins that need to be disabled
760
     *
761
     * @param string $pluginCode
762
     *
763
     * @return array plugin code
764
     */
765
    public function findDependentPluginNeedDisable($pluginCode)
766
    {
767
        return $this->findDependentPlugin($pluginCode, true);
768
    }
769
770
    /**
771
     * Find the other plugin that has requires on it.
772
     * Check in both dtb_plugin table and <PluginCode>/composer.json
773
     *
774
     * @param string $pluginCode
775
     * @param bool   $enableOnly
776
     *
777
     * @return array plugin code
778
     */
779
    public function findDependentPlugin($pluginCode, $enableOnly = false)
780
    {
781
        $criteria = Criteria::create()
782
            ->where(Criteria::expr()->neq('code', $pluginCode));
783
        if ($enableOnly) {
784
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
785
        }
786
        /**
787
         * @var Plugin[]
788
         */
789
        $plugins = $this->pluginRepository->matching($criteria);
790
        $dependents = [];
791
        foreach ($plugins as $plugin) {
792
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
793
            $fileName = $dir.'/composer.json';
794
            if (!file_exists($fileName)) {
795
                continue;
796
            }
797
            $jsonText = file_get_contents($fileName);
798
            if ($jsonText) {
799
                $json = json_decode($jsonText, true);
800
                if (!isset($json['require'])) {
801
                    continue;
802
                }
803
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
804
                    $dependents[] = $plugin->getCode();
805
                }
806
            }
807
        }
808
809
        return $dependents;
810
    }
811
812
    /**
813
     * Get dependent plugin by code
814
     * It's base on composer.json
815
     * Return the plugin code and version in the format of the composer
816
     *
817
     * @param string   $pluginCode
818
     * @param int|null $libraryType
819
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
820
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
821
     *                      default : return all library/plugin
822
     *
823
     * @return array format [packageName1 => version1, packageName2 => version2]
824
     */
825
    public function getDependentByCode($pluginCode, $libraryType = null)
826
    {
827
        $pluginDir = $this->calcPluginDir($pluginCode);
828
        $jsonFile = $pluginDir.'/composer.json';
829
        if (!file_exists($jsonFile)) {
830
            return [];
831
        }
832
        $jsonText = file_get_contents($jsonFile);
833
        $json = json_decode($jsonText, true);
834
        $dependents = [];
835
        if (isset($json['require'])) {
836
            $require = $json['require'];
837
            switch ($libraryType) {
838 View Code Duplication
                case self::ECCUBE_LIBRARY:
839
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
840
                    break;
841
842 View Code Duplication
                case self::OTHER_LIBRARY:
843
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
844
                    break;
845
846
                default:
847
                    $dependents = $json['require'];
848
                    break;
849
            }
850
        }
851
852
        return $dependents;
853
    }
854
855
    /**
856
     * Format array dependent plugin to string
857
     * It is used for commands.
858
     *
859
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
860
     * @param bool  $getVersion
861
     *
862
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
863
     */
864
    public function parseToComposerCommand(array $packages, $getVersion = true)
865
    {
866
        $result = array_keys($packages);
867
        if ($getVersion) {
868
            $result = array_map(function ($package, $version) {
869
                return $package.':'.$version;
870
            }, array_keys($packages), array_values($packages));
871
        }
872
873
        return implode(' ', $result);
874
    }
875
876
    /**
877
     * リソースファイル等をコピー
878
     * コピー元となるファイルの置き場所は固定であり、
879
     * [プラグインコード]/Resource/assets
880
     * 配下に置かれているファイルが所定の位置へコピーされる
881
     *
882
     * @param $pluginCode
883
     */
884
    public function copyAssets($pluginCode)
885
    {
886
        $assetsDir = $this->calcPluginDir($pluginCode).'/Resource/assets';
887
888
        // プラグインにリソースファイルがあれば所定の位置へコピー
889
        if (file_exists($assetsDir)) {
890
            $file = new Filesystem();
891
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
892
        }
893
    }
894
895
    /**
896
     * コピーしたリソースファイル等を削除
897
     *
898
     * @param string $pluginCode
899
     */
900
    public function removeAssets($pluginCode)
901
    {
902
        $assetsDir = $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets';
903
904
        // コピーされているリソースファイルがあれば削除
905
        if (file_exists($assetsDir)) {
906
            $file = new Filesystem();
907
            $file->remove($assetsDir);
908
        }
909
    }
910
911
    /**
912
     * Plugin is exist check
913
     *
914
     * @param array  $plugins    get from api
915
     * @param string $pluginCode
916
     *
917
     * @return false|int|string
918
     */
919
    public function checkPluginExist($plugins, $pluginCode)
920
    {
921
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
922
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
923
        }
924
        // Find plugin in array
925
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
926
927
        return $index;
928
    }
929
930
    /**
931
     * @param string $code
932
     *
933
     * @return bool
934
     */
935
    private function isEnable($code)
936
    {
937
        $Plugin = $this->pluginRepository->findOneBy([
938
            'enabled' => Constant::ENABLED,
939
            'code' => $code,
940
        ]);
941
        if ($Plugin) {
942
            return true;
943
        }
944
945
        return false;
946
    }
947
}
948