Completed
Pull Request — 4.0 (#4067)
by NOBU
06:16
created

PluginService::update()   A

Complexity

Conditions 4
Paths 25

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
nc 25
nop 2
dl 0
loc 34
ccs 0
cts 23
cp 0
crap 20
rs 9.376
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\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
use Eccube\Service\SystemService;
30
31
class PluginService
32
{
33
    /**
34
     * @var EccubeConfig
35
     */
36
    protected $eccubeConfig;
37
38
    /**
39
     * @var EntityManager
40
     */
41
    protected $entityManager;
42
43
    /**
44
     * @var PluginRepository
45
     */
46
    protected $pluginRepository;
47
48
    /**
49
     * @var EntityProxyService
50
     */
51
    protected $entityProxyService;
52
53
    /**
54
     * @var SchemaService
55
     */
56
    protected $schemaService;
57
58
    /**
59
     * @var ComposerServiceInterface
60
     */
61
    protected $composerService;
62
63
    const VENDOR_NAME = 'ec-cube';
64
65
    /**
66
     * Plugin type/library of ec-cube
67
     */
68
    const ECCUBE_LIBRARY = 1;
69
70
    /**
71
     * Plugin type/library of other (except ec-cube)
72
     */
73
    const OTHER_LIBRARY = 2;
74
75
    /**
76
     * @var string %kernel.project_dir%
77
     */
78
    private $projectRoot;
79
80
    /**
81
     * @var string %kernel.environment%
82
     */
83
    private $environment;
84
85
    /**
86
     * @var ContainerInterface
87
     */
88
    protected $container;
89
90
    /** @var CacheUtil */
91
    protected $cacheUtil;
92
93
    /**
94
     * @var PluginApiService
95
     */
96
    private $pluginApiService;
97
98
    /**
99
     * @var SystemService
100
     */
101
    private $systemService;
102
103
    /**
104
     * PluginService constructor.
105
     *
106
     * @param EntityManagerInterface $entityManager
107
     * @param PluginRepository $pluginRepository
108
     * @param EntityProxyService $entityProxyService
109
     * @param SchemaService $schemaService
110
     * @param EccubeConfig $eccubeConfig
111
     * @param ContainerInterface $container
112
     * @param CacheUtil $cacheUtil
113
     * @param ComposerServiceInterface $composerService
114
     * @param PluginApiService $pluginApiService
115
     */
116
    public function __construct(
117
        EntityManagerInterface $entityManager,
118
        PluginRepository $pluginRepository,
119
        EntityProxyService $entityProxyService,
120 1
        SchemaService $schemaService,
121
        EccubeConfig $eccubeConfig,
122
        ContainerInterface $container,
123
        CacheUtil $cacheUtil,
124
        ComposerServiceInterface $composerService,
125
        PluginApiService $pluginApiService,
126
        SystemService $systemService
127
    ) {
128
        $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...
129
        $this->pluginRepository = $pluginRepository;
130 1
        $this->entityProxyService = $entityProxyService;
131 1
        $this->schemaService = $schemaService;
132 1
        $this->eccubeConfig = $eccubeConfig;
133 1
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
134 1
        $this->environment = $eccubeConfig->get('kernel.environment');
135 1
        $this->container = $container;
136 1
        $this->cacheUtil = $cacheUtil;
137 1
        $this->composerService = $composerService;
138 1
        $this->pluginApiService = $pluginApiService;
139 1
        $this->systemService = $systemService;
140
    }
141
142
    /**
143
     * ファイル指定してのプラグインインストール
144
     *
145
     * @param string $path   path to tar.gz/zip plugin file
146
     * @param int    $source
147
     *
148
     * @return boolean
149
     *
150
     * @throws PluginException
151
     * @throws \Exception
152
     */
153 1
    public function install($path, $source = 0)
154
    {
155 1
        $pluginBaseDir = null;
156 1
        $tmp = null;
157
        try {
158
            // プラグイン配置前に実施する処理
159 1
            $this->preInstall();
160 1
            $tmp = $this->createTempDir();
161
162
            // 一旦テンポラリに展開
163 1
            $this->unpackPluginArchive($path, $tmp);
164 1
            $this->checkPluginArchiveContent($tmp);
165
166 1
            $config = $this->readConfig($tmp);
167 1
            // テンポラリのファイルを削除
168
            $this->deleteFile($tmp);
169 1
170
            // 重複していないかチェック
171
            $this->checkSamePlugin($config['code']);
172 1
173
            $pluginBaseDir = $this->calcPluginDir($config['code']);
174 1
            // 本来の置き場所を作成
175
            $this->createPluginDir($pluginBaseDir);
176 1
177
            // 問題なければ本当のplugindirへ
178
            $this->unpackPluginArchive($path, $pluginBaseDir);
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
626
            $this->callPluginManagerMethod($config, $enable ? 'enable' : 'disable');
627
628
            $plugin->setEnabled($enable ? true : false);
629
            $em->persist($plugin);
630
631
            // Proxyだけ再生成してスキーマは更新しない
632
            $this->regenerateProxy($plugin, false);
633
634
            $em->flush();
635
            $em->getConnection()->commit();
636
637
            if ($enable) {
638
                $this->pluginApiService->pluginEnabled($plugin);
639
            } else {
640
                $this->pluginApiService->pluginDisabled($plugin);
641
            }
642
        } catch (\Exception $e) {
643
            $em->getConnection()->rollback();
644
            throw $e;
645
        }
646
647
        return true;
648
    }
649
650
    /**
651
     * Update plugin
652
     *
653
     * @param Plugin $plugin
654
     * @param string $path
655
     *
656
     * @return bool
657
     *
658
     * @throws PluginException
659
     * @throws \Exception
660
     */
661
    public function update(Plugin $plugin, $path)
662
    {
663
        $pluginBaseDir = null;
664
        $tmp = null;
665
        try {
666
            $this->cacheUtil->clearCache();
667
            $tmp = $this->createTempDir();
668
669
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
670
            $this->checkPluginArchiveContent($tmp);
671
672
            $config = $this->readConfig($tmp);
673
674
            if ($plugin->getCode() != $config['code']) {
675
                throw new PluginException('new/old plugin code is different.');
676
            }
677
678
            $pluginBaseDir = $this->calcPluginDir($config['code']);
679
            $this->deleteFile($tmp); // テンポラリのファイルを削除
680
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
681
682
            $this->copyAssets($plugin->getCode());
683
            $this->updatePlugin($plugin, $config); // dbにプラグイン登録
684
        } catch (PluginException $e) {
685
            $this->deleteDirs([$tmp]);
686
            throw $e;
687
        } catch (\Exception $e) {
688
            // catch exception of composer
689
            $this->deleteDirs([$tmp]);
690
            throw $e;
691
        }
692
693
        return true;
694
    }
695
696
    /**
697
     * Update plugin
698
     *
699
     * @param Plugin $plugin
700
     * @param array  $meta     Config data
701
     *
702
     * @throws \Exception
703
     */
704
    public function updatePlugin(Plugin $plugin, $meta)
705
    {
706
        $em = $this->entityManager;
707
        try {
708
            $em->getConnection()->beginTransaction();
709
            $plugin->setVersion($meta['version'])
710
                ->setName($meta['name']);
711
712
            $em->persist($plugin);
713
714
            if ($plugin->isInitialized()) {
715
                $this->callPluginManagerMethod($meta, 'update');
716
            }
717
            $this->copyAssets($plugin->getCode());
718
            $em->flush();
719
            $em->getConnection()->commit();
720
        } catch (\Exception $e) {
721
            $em->getConnection()->rollback();
722
            throw $e;
723
        }
724
    }
725
726
    /**
727
     * Get array require by plugin
728
     * Todo: need define dependency plugin mechanism
729
     *
730
     * @param array|Plugin $plugin format as plugin from api
731
     *
732
     * @return array|mixed
733
     *
734
     * @throws PluginException
735
     */
736
    public function getPluginRequired($plugin)
737
    {
738
        $pluginCode = $plugin instanceof Plugin ? $plugin->getCode() : $plugin['code'];
739
        $pluginVersion = $plugin instanceof Plugin ? $plugin->getVersion() : $plugin['version'];
740
741
        $results = [];
742
743
        $this->composerService->foreachRequires('ec-cube/'.$pluginCode, $pluginVersion, function ($package) use (&$results) {
744
            $results[] = $package;
745
        }, 'eccube-plugin');
746
747
        return $results;
748
    }
749
750
    /**
751
     * Find the dependent plugins that need to be disabled
752
     *
753
     * @param string $pluginCode
754
     *
755
     * @return array plugin code
756
     */
757
    public function findDependentPluginNeedDisable($pluginCode)
758
    {
759
        return $this->findDependentPlugin($pluginCode, true);
760
    }
761
762
    /**
763
     * Find the other plugin that has requires on it.
764
     * Check in both dtb_plugin table and <PluginCode>/composer.json
765
     *
766
     * @param string $pluginCode
767
     * @param bool   $enableOnly
768
     *
769
     * @return array plugin code
770
     */
771
    public function findDependentPlugin($pluginCode, $enableOnly = false)
772
    {
773
        $criteria = Criteria::create()
774
            ->where(Criteria::expr()->neq('code', $pluginCode));
775
        if ($enableOnly) {
776
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
777
        }
778
        /**
779
         * @var Plugin[]
780
         */
781
        $plugins = $this->pluginRepository->matching($criteria);
782
        $dependents = [];
783
        foreach ($plugins as $plugin) {
784
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
785
            $fileName = $dir.'/composer.json';
786
            if (!file_exists($fileName)) {
787
                continue;
788
            }
789
            $jsonText = file_get_contents($fileName);
790
            if ($jsonText) {
791
                $json = json_decode($jsonText, true);
792
                if (!isset($json['require'])) {
793
                    continue;
794
                }
795
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
796
                    $dependents[] = $plugin->getCode();
797
                }
798
            }
799
        }
800
801
        return $dependents;
802
    }
803
804
    /**
805
     * Get dependent plugin by code
806
     * It's base on composer.json
807
     * Return the plugin code and version in the format of the composer
808
     *
809
     * @param string   $pluginCode
810
     * @param int|null $libraryType
811
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
812
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
813
     *                      default : return all library/plugin
814
     *
815
     * @return array format [packageName1 => version1, packageName2 => version2]
816
     */
817
    public function getDependentByCode($pluginCode, $libraryType = null)
818
    {
819
        $pluginDir = $this->calcPluginDir($pluginCode);
820
        $jsonFile = $pluginDir.'/composer.json';
821
        if (!file_exists($jsonFile)) {
822
            return [];
823
        }
824
        $jsonText = file_get_contents($jsonFile);
825
        $json = json_decode($jsonText, true);
826
        $dependents = [];
827
        if (isset($json['require'])) {
828
            $require = $json['require'];
829
            switch ($libraryType) {
830 View Code Duplication
                case self::ECCUBE_LIBRARY:
831
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
832
                    break;
833
834 View Code Duplication
                case self::OTHER_LIBRARY:
835
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
836
                    break;
837
838
                default:
839
                    $dependents = $json['require'];
840
                    break;
841
            }
842
        }
843
844
        return $dependents;
845
    }
846
847
    /**
848
     * Format array dependent plugin to string
849
     * It is used for commands.
850
     *
851
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
852
     * @param bool  $getVersion
853
     *
854
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
855
     */
856
    public function parseToComposerCommand(array $packages, $getVersion = true)
857
    {
858
        $result = array_keys($packages);
859
        if ($getVersion) {
860
            $result = array_map(function ($package, $version) {
861
                return $package.':'.$version;
862
            }, array_keys($packages), array_values($packages));
863
        }
864
865
        return implode(' ', $result);
866
    }
867
868
    /**
869
     * リソースファイル等をコピー
870
     * コピー元となるファイルの置き場所は固定であり、
871
     * [プラグインコード]/Resource/assets
872
     * 配下に置かれているファイルが所定の位置へコピーされる
873
     *
874
     * @param $pluginCode
875
     */
876
    public function copyAssets($pluginCode)
877
    {
878
        $assetsDir = $this->calcPluginDir($pluginCode).'/Resource/assets';
879
880
        // プラグインにリソースファイルがあれば所定の位置へコピー
881
        if (file_exists($assetsDir)) {
882
            $file = new Filesystem();
883
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
884
        }
885
    }
886
887
    /**
888
     * コピーしたリソースファイル等を削除
889
     *
890
     * @param string $pluginCode
891
     */
892
    public function removeAssets($pluginCode)
893
    {
894
        $assetsDir = $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets';
895
896
        // コピーされているリソースファイルがあれば削除
897
        if (file_exists($assetsDir)) {
898
            $file = new Filesystem();
899
            $file->remove($assetsDir);
900
        }
901
    }
902
903
    /**
904
     * Plugin is exist check
905
     *
906
     * @param array  $plugins    get from api
907
     * @param string $pluginCode
908
     *
909
     * @return false|int|string
910
     */
911
    public function checkPluginExist($plugins, $pluginCode)
912
    {
913
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
914
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
915
        }
916
        // Find plugin in array
917
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
918
919
        return $index;
920
    }
921
922
    /**
923
     * @param string $code
924
     *
925
     * @return bool
926
     */
927
    private function isEnable($code)
928
    {
929
        $Plugin = $this->pluginRepository->findOneBy([
930
            'enabled' => Constant::ENABLED,
931
            'code' => $code,
932
        ]);
933
        if ($Plugin) {
934
            return true;
935
        }
936
937
        return false;
938
    }
939
940
    public function installComposer($code) {
941
942
        $pluginDir = $this->calcPluginDir($code);
943
        $this->checkPluginArchiveContent($pluginDir);
944
        $config = $this->readConfig($pluginDir);
945
946
        // Check dependent plugin
947
        // Don't install ec-cube library
948
        $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
949
        if (!empty($dependents)) {
950
            $package = $this->parseToComposerCommand($dependents);
951
            $this->composerService->execRequire($package);
952
        }
953
    }
954
}
955