Completed
Pull Request — experimental/sf (#3393)
by chihiro
41:59
created

PluginService::getDependency()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
nc 10
nop 3
dl 0
loc 34
rs 8.1315
c 0
b 0
f 0
ccs 0
cts 15
cp 0
crap 72
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service;
15
16
use Doctrine\Common\Collections\Criteria;
17
use Doctrine\ORM\EntityManager;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Eccube\Application;
20
use Eccube\Common\Constant;
21
use Eccube\Common\EccubeConfig;
22
use Eccube\Entity\Plugin;
23
use Eccube\Entity\PluginEventHandler;
24
use Eccube\Exception\PluginException;
25
use Eccube\Repository\PluginEventHandlerRepository;
26
use Eccube\Repository\PluginRepository;
27
use Eccube\Service\Composer\ComposerServiceInterface;
28
use Eccube\Util\CacheUtil;
29
use Eccube\Util\StringUtil;
30
use Symfony\Component\DependencyInjection\ContainerInterface;
31
use Symfony\Component\Filesystem\Filesystem;
32
use Symfony\Component\Yaml\Yaml;
33
34
class PluginService
0 ignored issues
show
introduced by
Missing class doc comment
Loading history...
35
{
36
    /**
37
     * @var EccubeConfig
38
     */
39
    protected $eccubeConfig;
40
41
    /**
42
     * @var PluginEventHandlerRepository
43
     */
44
    protected $pluginEventHandlerRepository;
45
46
    /**
47
     * @var EntityManager
48
     */
49
    protected $entityManager;
50
51
    /**
52
     * @var PluginRepository
53
     */
54
    protected $pluginRepository;
55
56
    /**
57
     * @var Application
58
     */
59
    protected $app;
60
61
    /**
62
     * @var EntityProxyService
63
     */
64
    protected $entityProxyService;
65
66
    /**
67
     * @var SchemaService
68
     */
69
    protected $schemaService;
70
71
    /**
72
     * @var ComposerServiceInterface
73
     */
74
    protected $composerService;
75
76
    const CONFIG_YML = 'config.yml';
77
    const EVENT_YML = 'event.yml';
78
    const VENDOR_NAME = 'ec-cube';
79
80
    /**
81
     * Plugin type/library of ec-cube
82
     */
83
    const ECCUBE_LIBRARY = 1;
84
85
    /**
86
     * Plugin type/library of other (except ec-cube)
87
     */
88
    const OTHER_LIBRARY = 2;
89
90
    /**
91
     * @var string %kernel.project_dir%
92
     */
93
    private $projectRoot;
94
95
    /**
96
     * @var string %kernel.environment%
97
     */
98
    private $environment;
99
100
    /**
101
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
102
     */
103
    protected $container;
104
105
    /** @var CacheUtil */
106
    protected $cacheUtil;
107
108
    /**
109
     * PluginService constructor.
110
     *
111
     * @param PluginEventHandlerRepository $pluginEventHandlerRepository
112
     * @param EntityManagerInterface $entityManager
0 ignored issues
show
introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
113
     * @param PluginRepository $pluginRepository
0 ignored issues
show
introduced by
Expected 13 spaces after parameter type; 1 found
Loading history...
114
     * @param EntityProxyService $entityProxyService
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
115
     * @param SchemaService $schemaService
0 ignored issues
show
introduced by
Expected 16 spaces after parameter type; 1 found
Loading history...
116
     * @param EccubeConfig $eccubeConfig
0 ignored issues
show
introduced by
Expected 17 spaces after parameter type; 1 found
Loading history...
117
     * @param ContainerInterface $container
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
118
     * @param CacheUtil $cacheUtil
0 ignored issues
show
introduced by
Expected 20 spaces after parameter type; 1 found
Loading history...
119
     */
120 1
    public function __construct(
121
        PluginEventHandlerRepository $pluginEventHandlerRepository,
122
        EntityManagerInterface $entityManager,
123
        PluginRepository $pluginRepository,
124
        EntityProxyService $entityProxyService,
125
        SchemaService $schemaService,
126
        EccubeConfig $eccubeConfig,
127
        ContainerInterface $container,
128
        CacheUtil $cacheUtil
129
    ) {
130 1
        $this->pluginEventHandlerRepository = $pluginEventHandlerRepository;
131 1
        $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...
132 1
        $this->pluginRepository = $pluginRepository;
133 1
        $this->entityProxyService = $entityProxyService;
134 1
        $this->schemaService = $schemaService;
135 1
        $this->eccubeConfig = $eccubeConfig;
136 1
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
137 1
        $this->environment = $eccubeConfig->get('kernel.environment');
138 1
        $this->container = $container;
139 1
        $this->cacheUtil = $cacheUtil;
140
    }
141
142
    /**
143
     * ファイル指定してのプラグインインストール
144
     *
145
     * @param string $path   path to tar.gz/zip plugin file
146
     * @param int    $source
147
     *
148
     * @return mixed
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->readYml($tmp.'/'.self::CONFIG_YML);
167 1
            $event = $this->readYml($tmp.'/'.self::EVENT_YML);
168
            // テンポラリのファイルを削除
169 1
            $this->deleteFile($tmp);
170
171
            // 重複していないかチェック
172 1
            $this->checkSamePlugin($config['code']);
173
174 1
            $pluginBaseDir = $this->calcPluginDir($config['code']);
175
            // 本来の置き場所を作成
176 1
            $this->createPluginDir($pluginBaseDir);
177
178
            // 問題なければ本当のplugindirへ
179 1
            $this->unpackPluginArchive($path, $pluginBaseDir);
180
181
            // Check dependent plugin
182
            // Don't install ec-cube library
183
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
184
//            if (!empty($dependents)) {
185
//                $package = $this->parseToComposerCommand($dependents);
186
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
187
//                $this->composerService->execRequire($package);
188
//            }
189
190
            // プラグイン配置後に実施する処理
191 1
            $this->postInstall($config, $event, $source);
192
            // リソースファイルをコピー
193
            $this->copyAssets($pluginBaseDir, $config['code']);
194 1
        } catch (PluginException $e) {
195 1
            $this->deleteDirs([$tmp, $pluginBaseDir]);
196 1
            throw $e;
197
        } catch (\Exception $e) {
198
            // インストーラがどんなExceptionを上げるかわからないので
199
            $this->deleteDirs([$tmp, $pluginBaseDir]);
200
            throw $e;
201
        }
202
203
        return true;
204
    }
205
206
    // インストール事前処理
207
    public function preInstall()
0 ignored issues
show
introduced by
You must use "/**" style comments for a function comment
Loading history...
208
    {
209
        // キャッシュの削除
210
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
211
//        $this->cacheUtil->clearCache();
212
    }
213
214
    // インストール事後処理
215 1
    public function postInstall($config, $event, $source)
0 ignored issues
show
introduced by
You must use "/**" style comments for a function comment
Loading history...
216
    {
217
        // Proxyのクラスをロードせずにスキーマを更新するために、
218
        // インストール時には一時的なディレクトリにProxyを生成する
219 1
        $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
220 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...
221
222
        try {
223
            // dbにプラグイン登録
224 1
            $plugin = $this->registerPlugin($config, $event, $source);
225
226
            // プラグインmetadata定義を追加
227
            $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
228
            if (file_exists($entityDir)) {
229
                $ormConfig = $this->entityManager->getConfiguration();
230
                $chain = $ormConfig->getMetadataDriverImpl();
231
                $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
232
                $namespace = 'Plugin\\'.$config['code'].'\\Entity';
233
                $chain->addDriver($driver, $namespace);
234
                $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
235
            }
236
237
            // インストール時には一時的に利用するProxyを生成してからスキーマを更新する
238
            $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
239
            $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
240
        } finally {
241 1
            foreach (glob("${tmpProxyOutputDir}/*") as  $f) {
0 ignored issues
show
Coding Style introduced by
There should be 1 space after "as" as per the coding-style, but found 2.
Loading history...
242
                unlink($f);
243
            }
244 1
            rmdir($tmpProxyOutputDir);
245
        }
246
    }
247
248
    public function createTempDir()
249
    {
250 1
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
251 1
        @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...
252 1
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
253
254 1
        if (!mkdir($d, 0777)) {
255
            throw new PluginException($php_errormsg.$d);
256
        }
257
258 1
        return $d;
259
    }
260
261
    public function deleteDirs($arr)
262
    {
263 1
        foreach ($arr as $dir) {
264 1
            if (file_exists($dir)) {
265 1
                $fs = new Filesystem();
266 1
                $fs->remove($dir);
267
            }
268
        }
269
    }
270
271
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$archive" missing
Loading history...
introduced by
Doc comment for parameter "$dir" missing
Loading history...
272
     * @param $archive
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
273
     * @param $dir
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
274
     *
275
     * @throws PluginException
276
     */
277
    public function unpackPluginArchive($archive, $dir)
278
    {
279 1
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
280
        try {
281 1
            if ($extension == 'zip') {
282
                $zip = new \ZipArchive();
283
                $zip->open($archive);
284
                $zip->extractTo($dir);
285
                $zip->close();
286
            } else {
287 1
                $phar = new \PharData($archive);
288 1
                $phar->extractTo($dir, null, true);
289
            }
290
        } catch (\Exception $e) {
291
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
292
        }
293
    }
294
295
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$dir" missing
Loading history...
296
     * @param $dir
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
297
     * @param array $config_cache
298
     *
299
     * @throws PluginException
300
     */
301
    public function checkPluginArchiveContent($dir, array $config_cache = [])
302
    {
303
        try {
304 1
            if (!empty($config_cache)) {
305
                $meta = $config_cache;
306
            } else {
307 1
                $meta = $this->readYml($dir.'/config.yml');
308
            }
309
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
310
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
311
        }
312
313 1
        if (!is_array($meta)) {
314
            throw new PluginException('config.yml not found or syntax error');
315
        }
316 1 View Code Duplication
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
317
            throw new PluginException('config.yml code empty or invalid_character(\W)');
318
        }
319 1
        if (!isset($meta['name'])) {
320
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
321
            throw new PluginException('config.yml name empty');
322
        }
323 1 View Code Duplication
        if (isset($meta['event']) && !$this->checkSymbolName($meta['event'])) { // eventだけは必須ではない
324
            throw new PluginException('config.yml event empty or invalid_character(\W) ');
325
        }
326 1
        if (!isset($meta['version'])) {
327
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
328
            throw new PluginException('config.yml version invalid_character(\W) ');
329
        }
330 1
        if (isset($meta['orm.path'])) {
331
            if (!is_array($meta['orm.path'])) {
332
                throw new PluginException('config.yml orm.path invalid_character(\W) ');
333
            }
334
        }
335 1
        if (isset($meta['service'])) {
336
            if (!is_array($meta['service'])) {
337
                throw new PluginException('config.yml service invalid_character(\W) ');
338
            }
339
        }
340
    }
341
342
    public function readYml($yml)
343
    {
344 1
        if (file_exists($yml)) {
345 1
            return Yaml::parse(file_get_contents($yml));
346
        }
347
348 1
        return false;
349
    }
350
351
    public function checkSymbolName($string)
352
    {
353 1
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
354
        // plugin_nameやplugin_codeに使える文字のチェック
355
        // a-z A-Z 0-9 _
356
        // ディレクトリ名などに使われれるので厳しめ
357
    }
358
359
    public function deleteFile($path)
360
    {
361 1
        $f = new Filesystem();
362 1
        $f->remove($path);
363
    }
364
365
    public function checkSamePlugin($code)
366
    {
367 1
        $repo = $this->pluginRepository->findOneBy(['code' => $code]);
368 1
        if ($repo) {
369
            throw new PluginException('plugin already installed.');
370
        }
371
    }
372
373
    public function calcPluginDir($name)
374
    {
375 1
        return $this->projectRoot.'/app/Plugin/'.$name;
376
    }
377
378
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$d" missing
Loading history...
379
     * @param $d
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
380
     *
381
     * @throws PluginException
382
     */
383
    public function createPluginDir($d)
384
    {
385 1
        $b = @mkdir($d);
386 1
        if (!$b) {
387
            throw new PluginException($php_errormsg);
388
        }
389
    }
390
391
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$meta" missing
Loading history...
introduced by
Doc comment for parameter "$event_yml" missing
Loading history...
392
     * @param $meta
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
393
     * @param $event_yml
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
394
     * @param int $source
0 ignored issues
show
introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
395
     *
396
     * @return Plugin
397
     *
398
     * @throws PluginException
399
     */
400
    public function registerPlugin($meta, $event_yml, $source = 0)
401
    {
402 1
        $em = $this->entityManager;
403 1
        $em->getConnection()->beginTransaction();
404
        try {
405 1
            $p = new \Eccube\Entity\Plugin();
406
            // インストール直後はプラグインは有効にしない
407 1
            $p->setName($meta['name'])
408 1
                ->setEnabled(false)
409 1
                ->setClassName(isset($meta['event']) ? $meta['event'] : '')
410 1
                ->setVersion($meta['version'])
411 1
                ->setSource($source)
412 1
                ->setCode($meta['code'])
413
                // TODO 日付の自動設定
414 1
                ->setCreateDate(new \DateTime())
415 1
                ->setUpdateDate(new \DateTime());
416
417 1
            $em->persist($p);
418 1
            $em->flush();
419
420 1
            if (is_array($event_yml)) {
421
                foreach ($event_yml as $event => $handlers) {
422
                    foreach ($handlers as $handler) {
423
                        if (!$this->checkSymbolName($handler[0])) {
424
                            throw new PluginException('Handler name format error');
425
                        }
426
                        $peh = new \Eccube\Entity\PluginEventHandler();
427
                        $peh->setPlugin($p)
428
                            ->setEvent($event)
429
                            ->setHandler($handler[0])
430
                            ->setHandlerType($handler[1])
431
                            ->setPriority($this->pluginEventHandlerRepository->calcNewPriority($event, $handler[1]));
432
                        $em->persist($peh);
433
                        $em->flush();
434
                    }
435
                }
436
            }
437
438 1
            $em->persist($p);
439
440 1
            $this->callPluginManagerMethod($meta, 'install');
441
442
            $em->flush();
443
            $em->getConnection()->commit();
444 1
        } catch (\Exception $e) {
445 1
            $em->getConnection()->rollback();
446 1
            throw new PluginException($e->getMessage());
447
        }
448
449
        return $p;
450
    }
451
452
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$meta" missing
Loading history...
introduced by
Doc comment for parameter "$method" missing
Loading history...
453
     * @param $meta
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
454
     * @param $method
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
455
     */
456
    public function callPluginManagerMethod($meta, $method)
457
    {
458 1
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
459 1
        if (class_exists($class)) {
460 1
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
461 1
            if (method_exists($installer, $method)) {
462
                // FIXME appを削除.
463 1
                $installer->$method($meta, $this->app, $this->container);
464
            }
465
        }
466
    }
467
468
    /**
469
     * @param Plugin $plugin
470
     * @param bool $force
0 ignored issues
show
introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
471
     *
472
     * @return bool
473
     */
474
    public function uninstall(\Eccube\Entity\Plugin $plugin, $force = true)
475
    {
476
        $pluginDir = $this->calcPluginDir($plugin->getCode());
477
        $this->cacheUtil->clearCache();
478
        $this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), 'disable');
479
        $this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), 'uninstall');
480
        $this->disable($plugin);
481
        $this->unregisterPlugin($plugin);
482
483
        // スキーマを更新する
484
        //FIXME: Update schema before no affect
485
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
486
487
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
488
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
489
        $this->schemaService->dropTable($namespace);
490
491
        if ($force) {
492
            $this->deleteFile($pluginDir);
493
            $this->removeAssets($plugin->getCode());
494
        }
495
496
        return true;
497
    }
498
499
    public function unregisterPlugin(\Eccube\Entity\Plugin $p)
500
    {
501
        try {
502
            $em = $this->entityManager;
503
            foreach ($p->getPluginEventHandlers()->toArray() as $peh) {
504
                $em->remove($peh);
505
            }
506
            $em->remove($p);
507
            $em->flush();
508
        } catch (\Exception $e) {
509
            throw $e;
510
        }
511
    }
512
513
    public function disable(\Eccube\Entity\Plugin $plugin)
514
    {
515
        return $this->enable($plugin, false);
516
    }
517
518
    /**
519
     * Proxyを再生成します.
520
     *
521
     * @param Plugin $plugin プラグイン
522
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
523
     * @param string|null $outputDir 出力先
524
     *
525
     * @return array 生成されたファイルのパス
526
     */
527
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
528
    {
529
        if (is_null($outputDir)) {
530
            $outputDir = $this->projectRoot.'/app/proxy/entity';
531
        }
532
        @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...
533
534
        $enabledPluginCodes = array_map(
535
            function ($p) { return $p->getCode(); },
0 ignored issues
show
Coding Style introduced by
Opening brace must be the last content on the line
Loading history...
introduced by
Missing blank line before return statement
Loading history...
536
            $this->pluginRepository->findAllEnabled()
537
        );
538
539
        $excludes = [];
540
        if ($temporary || $plugin->isEnabled()) {
541
            $enabledPluginCodes[] = $plugin->getCode();
542
        } else {
543
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
544
            if ($index >= 0) {
545
                array_splice($enabledPluginCodes, $index, 1);
546
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
547
            }
548
        }
549
550
        $enabledPluginEntityDirs = array_map(function ($code) {
551
            return $this->projectRoot."/app/Plugin/${code}/Entity";
552
        }, $enabledPluginCodes);
553
554
        return $this->entityProxyService->generate(
555
            array_merge([$this->projectRoot.'/app/Customize/Entity'], $enabledPluginEntityDirs),
556
            $excludes,
557
            $outputDir
558
        );
559
    }
560
561
    public function enable(\Eccube\Entity\Plugin $plugin, $enable = true)
0 ignored issues
show
introduced by
Declare public methods first, then protected ones and finally private ones
Loading history...
562
    {
563
        $em = $this->entityManager;
564
        try {
565
            $pluginDir = $this->calcPluginDir($plugin->getCode());
566
            $em->getConnection()->beginTransaction();
567
            $plugin->setEnabled($enable ? true : false);
568
            $em->persist($plugin);
569
570
            $this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), $enable ? 'enable' : 'disable');
571
572
            // Proxyだけ再生成してスキーマは更新しない
573
            $this->regenerateProxy($plugin, false);
574
575
            $em->flush();
576
            $em->getConnection()->commit();
577
        } catch (\Exception $e) {
578
            $em->getConnection()->rollback();
579
            throw $e;
580
        }
581
582
        return true;
583
    }
584
585
    /**
586
     * Update plugin
587
     *
588
     * @param Plugin $plugin
589
     * @param string $path
590
     *
591
     * @return bool
592
     *
593
     * @throws PluginException
594
     * @throws \Exception
595
     */
596
    public function update(\Eccube\Entity\Plugin $plugin, $path)
597
    {
598
        $pluginBaseDir = null;
599
        $tmp = null;
600
        try {
601
            $this->cacheUtil->clearCache();
602
            $tmp = $this->createTempDir();
603
604
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
605
            $this->checkPluginArchiveContent($tmp);
606
607
            $config = $this->readYml($tmp.'/'.self::CONFIG_YML);
608
            $event = $this->readYml($tmp.'/event.yml');
609
610
            if ($plugin->getCode() != $config['code']) {
611
                throw new PluginException('new/old plugin code is different.');
612
            }
613
614
            $pluginBaseDir = $this->calcPluginDir($config['code']);
615
            $this->deleteFile($tmp); // テンポラリのファイルを削除
616
617
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
618
619
            // Check dependent plugin
620
            // Don't install ec-cube library
621
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
622
            if (!empty($dependents)) {
623
                $package = $this->parseToComposerCommand($dependents);
624
                $this->composerService->execRequire($package);
625
            }
626
627
            $this->updatePlugin($plugin, $config, $event); // dbにプラグイン登録
628
        } catch (PluginException $e) {
629
            $this->deleteDirs([$tmp]);
630
            throw $e;
631
        } catch (\Exception $e) {
632
            // catch exception of composer
633
            $this->deleteDirs([$tmp]);
634
            throw $e;
635
        }
636
637
        return true;
638
    }
639
640
    /**
641
     * Update plugin
642
     *
643
     * @param Plugin $plugin
644
     * @param array  $meta     Config data
645
     * @param array  $eventYml event data
646
     *
647
     * @throws \Exception
648
     */
649
    public function updatePlugin(Plugin $plugin, $meta, $eventYml)
650
    {
651
        $em = $this->entityManager;
652
        try {
653
            $em->getConnection()->beginTransaction();
654
            $plugin->setVersion($meta['version'])
655
                ->setName($meta['name']);
656
            if (isset($meta['event'])) {
657
                $plugin->setClassName($meta['event']);
658
            }
659
            $rep = $this->pluginEventHandlerRepository;
660
            if (!empty($eventYml) && is_array($eventYml)) {
661
                foreach ($eventYml as $event => $handlers) {
662
                    foreach ($handlers as $handler) {
663
                        if (!$this->checkSymbolName($handler[0])) {
664
                            throw new PluginException('Handler name format error');
665
                        }
666
                        // updateで追加されたハンドラかどうか調べる
667
                        $peh = $rep->findBy(
668
                            [
669
                            'plugin_id' => $plugin->getId(),
670
                            'event' => $event,
671
                            'handler' => $handler[0],
672
                            'handler_type' => $handler[1],
673
                                ]
674
                        );
675
676
                        // 新規にevent.ymlに定義されたハンドラなのでinsertする
677
                        if (!$peh) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $peh of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
678
                            $peh = new PluginEventHandler();
679
                            $peh->setPlugin($plugin)
680
                                ->setEvent($event)
681
                                ->setHandler($handler[0])
682
                                ->setHandlerType($handler[1])
683
                                ->setPriority($rep->calcNewPriority($event, $handler[1]));
684
                            $em->persist($peh);
685
                            $em->flush();
686
                        }
687
                    }
688
                }
689
690
                // アップデート後のevent.ymlで削除されたハンドラをdtb_plugin_event_handlerから探して削除
691
                /** @var PluginEventHandler $peh */
692
                foreach ($rep->findBy(['plugin_id' => $plugin->getId()]) as $peh) {
693
                    if (!isset($eventYml[$peh->getEvent()])) {
694
                        $em->remove($peh);
695
                        $em->flush();
696
                    } else {
697
                        $match = false;
698
                        foreach ($eventYml[$peh->getEvent()] as $handler) {
699
                            if ($peh->getHandler() == $handler[0] && $peh->getHandlerType() == $handler[1]) {
700
                                $match = true;
701
                            }
702
                        }
703
                        if (!$match) {
704
                            $em->remove($peh);
705
                            $em->flush();
706
                        }
707
                    }
708
                }
709
            }
710
711
            $em->persist($plugin);
712
            $this->callPluginManagerMethod($meta, 'update');
713
            $em->flush();
714
            $em->getConnection()->commit();
715
        } catch (\Exception $e) {
716
            $em->getConnection()->rollback();
717
            throw $e;
718
        }
719
    }
720
721
    /**
722
     * Do check dependency plugin
723
     *
724
     * @param array $plugins    get from api
725
     * @param array $plugin     format as plugin from api
726
     * @param array $dependents template output
727
     *
728
     * @return array|mixed
729
     */
730
    public function getDependency($plugins, $plugin, $dependents = [])
731
    {
732
        // Prevent infinity loop
733
        if (empty($dependents)) {
734
            $dependents[] = $plugin;
735
        }
736
737
        // Check dependency
738
        if (!isset($plugin['require']) || empty($plugin['require'])) {
739
            return $dependents;
740
        }
741
742
        $require = $plugin['require'];
743
        // Check dependency
744
        foreach ($require as $pluginName => $version) {
745
            $dependPlugin = $this->buildInfo($plugins, $pluginName);
746
            // Prevent call self
747
            if (!$dependPlugin || $dependPlugin['product_code'] == $plugin['product_code']) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dependPlugin of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
748
                continue;
749
            }
750
751
            // Check duplicate in dependency
752
            $index = array_search($dependPlugin['product_code'], array_column($dependents, 'product_code'));
753
            if ($index === false) {
754
                // Update require version
755
                $dependPlugin['version'] = $version;
756
                $dependents[] = $dependPlugin;
757
                // Check child dependency
758
                $dependents = $this->getDependency($plugins, $dependPlugin, $dependents);
759
            }
760
        }
761
762
        return $dependents;
763
    }
764
765
    /**
766
     * Get plugin information
767
     *
768
     * @param array  $plugins    get from api
769
     * @param string $pluginCode
770
     *
771
     * @return array|null
772
     */
773
    public function buildInfo($plugins, $pluginCode)
774
    {
775
        $plugin = [];
776
        $index = $this->checkPluginExist($plugins, $pluginCode);
777
        if ($index === false) {
778
            return $plugin;
779
        }
780
        // Get target plugin in return of api
781
        $plugin = $plugins[$index];
782
783
        // Check the eccube version that the plugin supports.
784
        $plugin['is_supported_eccube_version'] = 0;
785
        if (in_array(Constant::VERSION, $plugin['eccube_version'])) {
786
            // Match version
787
            $plugin['is_supported_eccube_version'] = 1;
788
        }
789
790
        $plugin['depend'] = $this->getRequirePluginName($plugins, $plugin);
791
792
        return $plugin;
793
    }
794
795
    /**
796
     * Get dependency name and version only
797
     *
798
     * @param array $plugins get from api
799
     * @param array $plugin  target plugin from api
800
     *
801
     * @return mixed format [0 => ['name' => pluginName1, 'version' => pluginVersion1], 1 => ['name' => pluginName2, 'version' => pluginVersion2]]
802
     */
803
    public function getRequirePluginName($plugins, $plugin)
804
    {
805
        $depend = [];
806
        if (isset($plugin['require']) && !empty($plugin['require'])) {
807
            foreach ($plugin['require'] as $name => $version) {
808
                $ret = $this->checkPluginExist($plugins, $name);
809
                if ($ret === false) {
810
                    continue;
811
                }
812
                $depend[] = [
813
                    'name' => $plugins[$ret]['name'],
814
                    'version' => $version,
815
                ];
816
            }
817
        }
818
819
        return $depend;
820
    }
821
822
    /**
823
     * Check require plugin in enable
824
     *
825
     * @param string $pluginCode
826
     *
827
     * @return array plugin code
828
     */
829
    public function findRequirePluginNeedEnable($pluginCode)
830
    {
831
        $dir = $this->eccubeConfig['plugin_realdir'].'/'.$pluginCode;
832
        $composerFile = $dir.'/composer.json';
833
        if (!file_exists($composerFile)) {
834
            return [];
835
        }
836
        $jsonText = file_get_contents($composerFile);
837
        $json = json_decode($jsonText, true);
838
        // Check require
839
        if (!isset($json['require']) || empty($json['require'])) {
840
            return [];
841
        }
842
        $require = $json['require'];
843
844
        // Remove vendor plugin
845
        if (isset($require[self::VENDOR_NAME.'/plugin-installer'])) {
846
            unset($require[self::VENDOR_NAME.'/plugin-installer']);
847
        }
848
        $requires = [];
849
        foreach ($require as $name => $version) {
850
            // Check plugin of ec-cube only
851
            if (strpos($name, self::VENDOR_NAME.'/') !== false) {
852
                $requireCode = str_replace(self::VENDOR_NAME.'/', '', $name);
853
                $ret = $this->isEnable($requireCode);
854
                if ($ret) {
855
                    continue;
856
                }
857
                $requires[] = $requireCode;
858
            }
859
        }
860
861
        return $requires;
862
    }
863
864
    /**
865
     * Find the dependent plugins that need to be disabled
866
     *
867
     * @param string $pluginCode
868
     *
869
     * @return array plugin code
870
     */
871
    public function findDependentPluginNeedDisable($pluginCode)
872
    {
873
        return $this->findDependentPlugin($pluginCode, true);
874
    }
875
876
    /**
877
     * Find the other plugin that has requires on it.
878
     * Check in both dtb_plugin table and <PluginCode>/composer.json
879
     *
880
     * @param string $pluginCode
881
     * @param bool   $enableOnly
882
     *
883
     * @return array plugin code
884
     */
885
    public function findDependentPlugin($pluginCode, $enableOnly = false)
886
    {
887
        $criteria = Criteria::create()
888
            ->where(Criteria::expr()->neq('code', $pluginCode));
889
        if ($enableOnly) {
890
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
891
        }
892
        /**
893
         * @var Plugin[]
894
         */
895
        $plugins = $this->pluginRepository->matching($criteria);
896
        $dependents = [];
897
        foreach ($plugins as $plugin) {
898
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
899
            $fileName = $dir.'/composer.json';
900
            if (!file_exists($fileName)) {
901
                continue;
902
            }
903
            $jsonText = file_get_contents($fileName);
904
            if ($jsonText) {
905
                $json = json_decode($jsonText, true);
906
                if (!isset($json['require'])) {
907
                    continue;
908
                }
909
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
910
                    $dependents[] = $plugin->getCode();
911
                }
912
            }
913
        }
914
915
        return $dependents;
916
    }
917
918
    /**
919
     * Get dependent plugin by code
920
     * It's base on composer.json
921
     * Return the plugin code and version in the format of the composer
922
     *
923
     * @param string   $pluginCode
924
     * @param int|null $libraryType
925
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
926
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
927
     *                      default : return all library/plugin
928
     *
929
     * @return array format [packageName1 => version1, packageName2 => version2]
930
     */
931
    public function getDependentByCode($pluginCode, $libraryType = null)
932
    {
933
        $pluginDir = $this->calcPluginDir($pluginCode);
934
        $jsonFile = $pluginDir.'/composer.json';
935
        if (!file_exists($jsonFile)) {
936
            return [];
937
        }
938
        $jsonText = file_get_contents($jsonFile);
939
        $json = json_decode($jsonText, true);
940
        $dependents = [];
941
        if (isset($json['require'])) {
942
            $require = $json['require'];
943
            switch ($libraryType) {
944 View Code Duplication
                case self::ECCUBE_LIBRARY:
945
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
946
                    break;
947
948 View Code Duplication
                case self::OTHER_LIBRARY:
949
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
950
                    break;
951
952
                default:
953
                    $dependents = $json['require'];
954
                    break;
955
            }
956
        }
957
958
        return $dependents;
959
    }
960
961
    /**
962
     * Format array dependent plugin to string
963
     * It is used for commands.
964
     *
965
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
966
     * @param bool  $getVersion
967
     *
968
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
969
     */
970
    public function parseToComposerCommand(array $packages, $getVersion = true)
971
    {
972
        $result = array_keys($packages);
973
        if ($getVersion) {
974
            $result = array_map(function ($package, $version) {
975
                return $package.':'.$version;
976
            }, array_keys($packages), array_values($packages));
977
        }
978
979
        return implode(' ', $result);
980
    }
981
982
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$pluginBaseDir" missing
Loading history...
introduced by
Doc comment for parameter "$pluginCode" missing
Loading history...
983
     * リソースファイル等をコピー
984
     * コピー元となるファイルの置き場所は固定であり、
985
     * [プラグインコード]/Resource/assets
986
     * 配下に置かれているファイルが所定の位置へコピーされる
987
     *
988
     * @param $pluginBaseDir
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
989
     * @param $pluginCode
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
990
     */
991
    public function copyAssets($pluginBaseDir, $pluginCode)
992
    {
993
        $assetsDir = $pluginBaseDir.'/Resource/assets';
994
995
        // プラグインにリソースファイルがあれば所定の位置へコピー
996
        if (file_exists($assetsDir)) {
997
            $file = new Filesystem();
998
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
999
        }
1000
    }
1001
1002
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$pluginCode" missing
Loading history...
1003
     * コピーしたリソースファイル等を削除
1004
     *
1005
     * @param $pluginCode
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
1006
     */
1007
    public function removeAssets($pluginCode)
1008
    {
1009
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
1010
1011
        // コピーされているリソースファイルがあれば削除
1012
        if (file_exists($assetsDir)) {
1013
            $file = new Filesystem();
1014
            $file->remove($assetsDir);
1015
        }
1016
    }
1017
1018
    /**
1019
     * Is update
1020
     *
1021
     * @param string $pluginVersion
1022
     * @param string $remoteVersion
1023
     *
1024
     * @return mixed
1025
     */
1026
    public function isUpdate($pluginVersion, $remoteVersion)
1027
    {
1028
        return version_compare($pluginVersion, $remoteVersion, '<');
1029
    }
1030
1031
    /**
1032
     * Plugin is exist check
1033
     *
1034
     * @param array  $plugins    get from api
1035
     * @param string $pluginCode
1036
     *
1037
     * @return false|int|string
1038
     */
1039
    public function checkPluginExist($plugins, $pluginCode)
1040
    {
1041
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
1042
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
1043
        }
1044
        // Find plugin in array
1045
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
1046
1047
        return $index;
1048
    }
1049
1050
    /**
1051
     * @param string $code
1052
     *
1053
     * @return bool
1054
     */
1055
    private function isEnable($code)
1056
    {
1057
        $Plugin = $this->pluginRepository->findOneBy([
1058
            'enabled' => Constant::ENABLED,
1059
            'code' => $code,
1060
        ]);
1061
        if ($Plugin) {
1062
            return true;
1063
        }
1064
1065
        return false;
1066
    }
1067
}
1068