Completed
Push — 4.0 ( 87d096...bcc1be )
by Kiyotaka
05:44 queued 11s
created

src/Eccube/Service/PluginService.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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