Completed
Pull Request — 4.0 (#4058)
by Kentaro
08:07
created

PluginService::registerPlugin()   A

Complexity

Conditions 2
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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