Failed Conditions
Branch experimental/sf (68db07)
by Kentaro
42:17 queued 33:39
created

PluginService::checkPluginArchiveContent()   C

Complexity

Conditions 14
Paths 25

Size

Total Lines 40

Duplication

Lines 6
Ratio 15 %

Code Coverage

Tests 9
CRAP Score 50.5659

Importance

Changes 0
Metric Value
cc 14
nc 25
nop 2
dl 6
loc 40
rs 6.2666
c 0
b 0
f 0
ccs 9
cts 21
cp 0.4286
crap 50.5659

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Plugin\ConfigManager;
26
use Eccube\Plugin\ConfigManager as PluginConfigManager;
27
use Eccube\Repository\PluginEventHandlerRepository;
28
use Eccube\Repository\PluginRepository;
29
use Eccube\Service\Composer\ComposerServiceInterface;
30
use Eccube\Util\CacheUtil;
31
use Eccube\Util\StringUtil;
32
use Symfony\Component\DependencyInjection\ContainerInterface;
33
use Symfony\Component\Filesystem\Filesystem;
34
use Symfony\Component\Yaml\Yaml;
35
36
class PluginService
0 ignored issues
show
introduced by
Missing class doc comment
Loading history...
37
{
38
    /**
39
     * @var EccubeConfig
40
     */
41
    protected $eccubeConfig;
42
43
    /**
44
     * @var PluginEventHandlerRepository
45
     */
46
    protected $pluginEventHandlerRepository;
47
48
    /**
49
     * @var EntityManager
50
     */
51
    protected $entityManager;
52
53
    /**
54
     * @var PluginRepository
55
     */
56
    protected $pluginRepository;
57
58
    /**
59
     * @var Application
60
     */
61
    protected $app;
62
63
    /**
64
     * @var EntityProxyService
65
     */
66
    protected $entityProxyService;
67
68
    /**
69
     * @var SchemaService
70
     */
71
    protected $schemaService;
72
73
    /**
74
     * @var ComposerServiceInterface
75
     */
76
    protected $composerService;
77
78
    const CONFIG_YML = 'config.yml';
79
    const EVENT_YML = 'event.yml';
80
    const VENDOR_NAME = 'ec-cube';
81
82
    /**
83
     * Plugin type/library of ec-cube
84
     */
85
    const ECCUBE_LIBRARY = 1;
86
87
    /**
88
     * Plugin type/library of other (except ec-cube)
89
     */
90
    const OTHER_LIBRARY = 2;
91
92
    /**
93
     * @var string %kernel.project_dir%
94
     */
95
    private $projectRoot;
96
97
    /**
98
     * @var string %kernel.environment%
99
     */
100
    private $environment;
101
102
    /**
103
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
104
     */
105
    protected $container;
106
107
    /** @var CacheUtil */
108
    protected $cacheUtil;
109
110
    /**
111
     * PluginService constructor.
112
     *
113
     * @param PluginEventHandlerRepository $pluginEventHandlerRepository
114
     * @param EntityManagerInterface $entityManager
0 ignored issues
show
introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
115
     * @param PluginRepository $pluginRepository
0 ignored issues
show
introduced by
Expected 13 spaces after parameter type; 1 found
Loading history...
116
     * @param EntityProxyService $entityProxyService
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
117
     * @param SchemaService $schemaService
0 ignored issues
show
introduced by
Expected 16 spaces after parameter type; 1 found
Loading history...
118
     * @param EccubeConfig $eccubeConfig
0 ignored issues
show
introduced by
Expected 17 spaces after parameter type; 1 found
Loading history...
119
     * @param ContainerInterface $container
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
120
     * @param CacheUtil $cacheUtil
0 ignored issues
show
introduced by
Expected 20 spaces after parameter type; 1 found
Loading history...
121
     */
122 1
    public function __construct(
123
        PluginEventHandlerRepository $pluginEventHandlerRepository,
124
        EntityManagerInterface $entityManager,
125
        PluginRepository $pluginRepository,
126
        EntityProxyService $entityProxyService,
127
        SchemaService $schemaService,
128
        EccubeConfig $eccubeConfig,
129
        ContainerInterface $container,
130
        CacheUtil $cacheUtil
131
    ) {
132 1
        $this->pluginEventHandlerRepository = $pluginEventHandlerRepository;
133 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...
134 1
        $this->pluginRepository = $pluginRepository;
135 1
        $this->entityProxyService = $entityProxyService;
136 1
        $this->schemaService = $schemaService;
137 1
        $this->eccubeConfig = $eccubeConfig;
138 1
        $this->projectRoot = $eccubeConfig->get('kernel.project_dir');
139 1
        $this->environment = $eccubeConfig->get('kernel.environment');
140 1
        $this->container = $container;
141 1
        $this->cacheUtil = $cacheUtil;
142
    }
143
144
    /**
145
     * ファイル指定してのプラグインインストール
146
     *
147
     * @param string $path   path to tar.gz/zip plugin file
148
     * @param int    $source
149
     *
150
     * @return mixed
151
     *
152
     * @throws PluginException
153
     * @throws \Exception
154
     */
155 1
    public function install($path, $source = 0)
156
    {
157 1
        $pluginBaseDir = null;
158 1
        $tmp = null;
159
        try {
160
            // プラグイン配置前に実施する処理
161 1
            $this->preInstall();
162 1
            $tmp = $this->createTempDir();
163
164
            // 一旦テンポラリに展開
165 1
            $this->unpackPluginArchive($path, $tmp);
166 1
            $this->checkPluginArchiveContent($tmp);
167
168 1
            $config = $this->readYml($tmp.'/'.self::CONFIG_YML);
169 1
            $event = $this->readYml($tmp.'/'.self::EVENT_YML);
170
            // テンポラリのファイルを削除
171 1
            $this->deleteFile($tmp);
172
173
            // 重複していないかチェック
174 1
            $this->checkSamePlugin($config['code']);
175
176 1
            $pluginBaseDir = $this->calcPluginDir($config['code']);
177
            // 本来の置き場所を作成
178 1
            $this->createPluginDir($pluginBaseDir);
179
180
            // 問題なければ本当のplugindirへ
181 1
            $this->unpackPluginArchive($path, $pluginBaseDir);
182
183
            // Check dependent plugin
184
            // Don't install ec-cube library
185
//            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
186
//            if (!empty($dependents)) {
187
//                $package = $this->parseToComposerCommand($dependents);
188
            //FIXME: how to working with ComposerProcessService or ComposerApiService ?
189
//                $this->composerService->execRequire($package);
190
//            }
191
192
            // プラグイン配置後に実施する処理
193 1
            $this->postInstall($config, $event, $source);
194
            // リソースファイルをコピー
195
            $this->copyAssets($pluginBaseDir, $config['code']);
196 1
        } catch (PluginException $e) {
197 1
            $this->deleteDirs([$tmp, $pluginBaseDir]);
198 1
            throw $e;
199
        } catch (\Exception $e) {
200
            // インストーラがどんなExceptionを上げるかわからないので
201
            $this->deleteDirs([$tmp, $pluginBaseDir]);
202
            throw $e;
203
        }
204
205
        return true;
206
    }
207
208
    // インストール事前処理
209 1
    public function preInstall()
0 ignored issues
show
introduced by
You must use "/**" style comments for a function comment
Loading history...
210
    {
211
        // キャッシュの削除
212 1
        PluginConfigManager::removePluginConfigCache();
213
        // FIXME: Please fix clearCache function (because it's clear all cache and this file just upload)
214
//        $this->cacheUtil->clearCache();
215
    }
216
217
    // インストール事後処理
218 1
    public function postInstall($config, $event, $source)
0 ignored issues
show
introduced by
You must use "/**" style comments for a function comment
Loading history...
219
    {
220
        // Proxyのクラスをロードせずにスキーマを更新するために、
221
        // インストール時には一時的なディレクトリにProxyを生成する
222 1
        $tmpProxyOutputDir = sys_get_temp_dir().'/proxy_'.StringUtil::random(12);
223 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...
224
225
        try {
226
            // dbにプラグイン登録
227 1
            $plugin = $this->registerPlugin($config, $event, $source);
228
229
            // プラグインmetadata定義を追加
230
            $entityDir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode().'/Entity';
231
            if (file_exists($entityDir)) {
232
                $ormConfig = $this->entityManager->getConfiguration();
233
                $chain = $ormConfig->getMetadataDriverImpl();
234
                $driver = $ormConfig->newDefaultAnnotationDriver([$entityDir], false);
235
                $namespace = 'Plugin\\'.$config['code'].'\\Entity';
236
                $chain->addDriver($driver, $namespace);
237
                $ormConfig->addEntityNamespace($plugin->getCode(), $namespace);
238
            }
239
240
            // インストール時には一時的に利用するProxyを生成してからスキーマを更新する
241
            $generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
242
            $this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);
243
244
            ConfigManager::writePluginConfigCache();
245
        } finally {
246 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...
247
                unlink($f);
248
            }
249 1
            rmdir($tmpProxyOutputDir);
250
        }
251
    }
252
253
    public function createTempDir()
254
    {
255 1
        $tempDir = $this->projectRoot.'/var/cache/'.$this->environment.'/Plugin';
256 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...
257 1
        $d = ($tempDir.'/'.sha1(StringUtil::random(16)));
258
259 1
        if (!mkdir($d, 0777)) {
260
            throw new PluginException($php_errormsg.$d);
261
        }
262
263 1
        return $d;
264
    }
265
266
    public function deleteDirs($arr)
267
    {
268 1
        foreach ($arr as $dir) {
269 1
            if (file_exists($dir)) {
270 1
                $fs = new Filesystem();
271 1
                $fs->remove($dir);
272
            }
273
        }
274
    }
275
276
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$archive" missing
Loading history...
introduced by
Doc comment for parameter "$dir" missing
Loading history...
277
     * @param $archive
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
278
     * @param $dir
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
279
     *
280
     * @throws PluginException
281
     */
282
    public function unpackPluginArchive($archive, $dir)
283
    {
284 1
        $extension = pathinfo($archive, PATHINFO_EXTENSION);
285
        try {
286 1
            if ($extension == 'zip') {
287
                $zip = new \ZipArchive();
288
                $zip->open($archive);
289
                $zip->extractTo($dir);
290
                $zip->close();
291
            } else {
292 1
                $phar = new \PharData($archive);
293 1
                $phar->extractTo($dir, null, true);
294
            }
295
        } catch (\Exception $e) {
296
            throw new PluginException(trans('pluginservice.text.error.upload_failure'));
297
        }
298
    }
299
300
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$dir" missing
Loading history...
301
     * @param $dir
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
302
     * @param array $config_cache
303
     *
304
     * @throws PluginException
305
     */
306
    public function checkPluginArchiveContent($dir, array $config_cache = [])
307
    {
308
        try {
309 1
            if (!empty($config_cache)) {
310
                $meta = $config_cache;
311
            } else {
312 1
                $meta = $this->readYml($dir.'/config.yml');
313
            }
314
        } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
315
            throw new PluginException($e->getMessage(), $e->getCode(), $e);
316
        }
317
318 1
        if (!is_array($meta)) {
319
            throw new PluginException('config.yml not found or syntax error');
320
        }
321 1 View Code Duplication
        if (!isset($meta['code']) || !$this->checkSymbolName($meta['code'])) {
322
            throw new PluginException('config.yml code empty or invalid_character(\W)');
323
        }
324 1
        if (!isset($meta['name'])) {
325
            // nameは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
326
            throw new PluginException('config.yml name empty');
327
        }
328 1 View Code Duplication
        if (isset($meta['event']) && !$this->checkSymbolName($meta['event'])) { // eventだけは必須ではない
329
            throw new PluginException('config.yml event empty or invalid_character(\W) ');
330
        }
331 1
        if (!isset($meta['version'])) {
332
            // versionは直接クラス名やPATHに使われるわけではないため文字のチェックはなしし
333
            throw new PluginException('config.yml version invalid_character(\W) ');
334
        }
335 1
        if (isset($meta['orm.path'])) {
336
            if (!is_array($meta['orm.path'])) {
337
                throw new PluginException('config.yml orm.path invalid_character(\W) ');
338
            }
339
        }
340 1
        if (isset($meta['service'])) {
341
            if (!is_array($meta['service'])) {
342
                throw new PluginException('config.yml service invalid_character(\W) ');
343
            }
344
        }
345
    }
346
347
    public function readYml($yml)
348
    {
349 1
        if (file_exists($yml)) {
350 1
            return Yaml::parse(file_get_contents($yml));
351
        }
352
353 1
        return false;
354
    }
355
356
    public function checkSymbolName($string)
357
    {
358 1
        return strlen($string) < 256 && preg_match('/^\w+$/', $string);
359
        // plugin_nameやplugin_codeに使える文字のチェック
360
        // a-z A-Z 0-9 _
361
        // ディレクトリ名などに使われれるので厳しめ
362
    }
363
364
    public function deleteFile($path)
365
    {
366 1
        $f = new Filesystem();
367 1
        $f->remove($path);
368
    }
369
370
    public function checkSamePlugin($code)
371
    {
372 1
        $repo = $this->pluginRepository->findOneBy(['code' => $code]);
373 1
        if ($repo) {
374
            throw new PluginException('plugin already installed.');
375
        }
376
    }
377
378
    public function calcPluginDir($name)
379
    {
380 1
        return $this->projectRoot.'/app/Plugin/'.$name;
381
    }
382
383
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$d" missing
Loading history...
384
     * @param $d
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
385
     *
386
     * @throws PluginException
387
     */
388
    public function createPluginDir($d)
389
    {
390 1
        $b = @mkdir($d);
391 1
        if (!$b) {
392
            throw new PluginException($php_errormsg);
393
        }
394
    }
395
396
    /**
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...
397
     * @param $meta
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
398
     * @param $event_yml
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
399
     * @param int $source
0 ignored issues
show
introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
400
     *
401
     * @return Plugin
402
     *
403
     * @throws PluginException
404
     */
405
    public function registerPlugin($meta, $event_yml, $source = 0)
406
    {
407 1
        $em = $this->entityManager;
408 1
        $em->getConnection()->beginTransaction();
409
        try {
410 1
            $p = new \Eccube\Entity\Plugin();
411
            // インストール直後はプラグインは有効にしない
412 1
            $p->setName($meta['name'])
413 1
                ->setEnabled(false)
414 1
                ->setClassName(isset($meta['event']) ? $meta['event'] : '')
415 1
                ->setVersion($meta['version'])
416 1
                ->setSource($source)
417 1
                ->setCode($meta['code'])
418
                // TODO 日付の自動設定
419 1
                ->setCreateDate(new \DateTime())
420 1
                ->setUpdateDate(new \DateTime());
421
422 1
            $em->persist($p);
423 1
            $em->flush();
424
425 1
            if (is_array($event_yml)) {
426
                foreach ($event_yml as $event => $handlers) {
427
                    foreach ($handlers as $handler) {
428
                        if (!$this->checkSymbolName($handler[0])) {
429
                            throw new PluginException('Handler name format error');
430
                        }
431
                        $peh = new \Eccube\Entity\PluginEventHandler();
432
                        $peh->setPlugin($p)
433
                            ->setEvent($event)
434
                            ->setHandler($handler[0])
435
                            ->setHandlerType($handler[1])
436
                            ->setPriority($this->pluginEventHandlerRepository->calcNewPriority($event, $handler[1]));
437
                        $em->persist($peh);
438
                        $em->flush();
439
                    }
440
                }
441
            }
442
443 1
            $em->persist($p);
444
445 1
            $this->callPluginManagerMethod($meta, 'install');
446
447
            $em->flush();
448
            $em->getConnection()->commit();
449 1
        } catch (\Exception $e) {
450 1
            $em->getConnection()->rollback();
451 1
            throw new PluginException($e->getMessage());
452
        }
453
454
        return $p;
455
    }
456
457
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$meta" missing
Loading history...
introduced by
Doc comment for parameter "$method" missing
Loading history...
458
     * @param $meta
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
459
     * @param $method
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
460
     */
461
    public function callPluginManagerMethod($meta, $method)
462
    {
463 1
        $class = '\\Plugin'.'\\'.$meta['code'].'\\'.'PluginManager';
464 1
        if (class_exists($class)) {
465 1
            $installer = new $class(); // マネージャクラスに所定のメソッドがある場合だけ実行する
466 1
            if (method_exists($installer, $method)) {
467
                // FIXME appを削除.
468 1
                $installer->$method($meta, $this->app, $this->container);
469
            }
470
        }
471
    }
472
473
    /**
474
     * @param Plugin $plugin
475
     * @param bool $force
0 ignored issues
show
introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
476
     *
477
     * @return bool
478
     */
479
    public function uninstall(\Eccube\Entity\Plugin $plugin, $force = true)
480
    {
481
        $pluginDir = $this->calcPluginDir($plugin->getCode());
482
        ConfigManager::removePluginConfigCache();
483
        $this->cacheUtil->clearCache();
484
        $this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), 'disable');
485
        $this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), 'uninstall');
486
        $this->disable($plugin);
487
        $this->unregisterPlugin($plugin);
488
489
        // スキーマを更新する
490
        //FIXME: Update schema before no affect
491
        $this->schemaService->updateSchema([], $this->projectRoot.'/app/proxy/entity');
492
493
        // プラグインのネームスペースに含まれるEntityのテーブルを削除する
494
        $namespace = 'Plugin\\'.$plugin->getCode().'\\Entity';
495
        $this->schemaService->dropTable($namespace);
496
497
        if ($force) {
498
            $this->deleteFile($pluginDir);
499
            $this->removeAssets($plugin->getCode());
500
        }
501
502
        ConfigManager::writePluginConfigCache();
503
504
        return true;
505
    }
506
507
    public function unregisterPlugin(\Eccube\Entity\Plugin $p)
508
    {
509
        try {
510
            $em = $this->entityManager;
511
            foreach ($p->getPluginEventHandlers()->toArray() as $peh) {
512
                $em->remove($peh);
513
            }
514
            $em->remove($p);
515
            $em->flush();
516
        } catch (\Exception $e) {
517
            throw $e;
518
        }
519
    }
520
521
    public function disable(\Eccube\Entity\Plugin $plugin)
522
    {
523
        return $this->enable($plugin, false);
524
    }
525
526
    /**
527
     * Proxyを再生成します.
528
     *
529
     * @param Plugin $plugin プラグイン
530
     * @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
531
     * @param string|null $outputDir 出力先
532
     *
533
     * @return array 生成されたファイルのパス
534
     */
535
    private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
536
    {
537
        if (is_null($outputDir)) {
538
            $outputDir = $this->projectRoot.'/app/proxy/entity';
539
        }
540
        @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...
541
542
        $enabledPluginCodes = array_map(
543
            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...
544
            $this->pluginRepository->findAllEnabled()
545
        );
546
547
        $excludes = [];
548
        if ($temporary || $plugin->isEnabled()) {
549
            $enabledPluginCodes[] = $plugin->getCode();
550
        } else {
551
            $index = array_search($plugin->getCode(), $enabledPluginCodes);
552
            if ($index >= 0) {
553
                array_splice($enabledPluginCodes, $index, 1);
554
                $excludes = [$this->projectRoot.'/app/Plugin/'.$plugin->getCode().'/Entity'];
555
            }
556
        }
557
558
        $enabledPluginEntityDirs = array_map(function ($code) {
559
            return $this->projectRoot."/app/Plugin/${code}/Entity";
560
        }, $enabledPluginCodes);
561
562
        return $this->entityProxyService->generate(
563
            array_merge([$this->projectRoot.'/app/Acme/Entity'], $enabledPluginEntityDirs),
564
            $excludes,
565
            $outputDir
566
        );
567
    }
568
569
    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...
570
    {
571
        $em = $this->entityManager;
572
        try {
573
            PluginConfigManager::removePluginConfigCache();
574
            $pluginDir = $this->calcPluginDir($plugin->getCode());
575
            $em->getConnection()->beginTransaction();
576
            $plugin->setEnabled($enable ? true : false);
577
            $em->persist($plugin);
578
579
            $this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), $enable ? 'enable' : 'disable');
580
581
            // Proxyだけ再生成してスキーマは更新しない
582
            $this->regenerateProxy($plugin, false);
583
584
            $em->flush();
585
            $em->getConnection()->commit();
586
            PluginConfigManager::writePluginConfigCache();
587
        } catch (\Exception $e) {
588
            $em->getConnection()->rollback();
589
            throw $e;
590
        }
591
592
        return true;
593
    }
594
595
    /**
596
     * Update plugin
597
     *
598
     * @param Plugin $plugin
599
     * @param string $path
600
     *
601
     * @return bool
602
     *
603
     * @throws PluginException
604
     * @throws \Exception
605
     */
606
    public function update(\Eccube\Entity\Plugin $plugin, $path)
607
    {
608
        $pluginBaseDir = null;
609
        $tmp = null;
610
        try {
611
            PluginConfigManager::removePluginConfigCache();
612
            $this->cacheUtil->clearCache();
613
            $tmp = $this->createTempDir();
614
615
            $this->unpackPluginArchive($path, $tmp); //一旦テンポラリに展開
616
            $this->checkPluginArchiveContent($tmp);
617
618
            $config = $this->readYml($tmp.'/'.self::CONFIG_YML);
619
            $event = $this->readYml($tmp.'/event.yml');
620
621
            if ($plugin->getCode() != $config['code']) {
622
                throw new PluginException('new/old plugin code is different.');
623
            }
624
625
            $pluginBaseDir = $this->calcPluginDir($config['code']);
626
            $this->deleteFile($tmp); // テンポラリのファイルを削除
627
628
            $this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ
629
630
            // Check dependent plugin
631
            // Don't install ec-cube library
632
            $dependents = $this->getDependentByCode($config['code'], self::OTHER_LIBRARY);
633
            if (!empty($dependents)) {
634
                $package = $this->parseToComposerCommand($dependents);
635
                $this->composerService->execRequire($package);
636
            }
637
638
            $this->updatePlugin($plugin, $config, $event); // dbにプラグイン登録
639
            PluginConfigManager::writePluginConfigCache();
640
        } catch (PluginException $e) {
641
            $this->deleteDirs([$tmp]);
642
            throw $e;
643
        } catch (\Exception $e) {
644
            // catch exception of composer
645
            $this->deleteDirs([$tmp]);
646
            throw $e;
647
        }
648
649
        return true;
650
    }
651
652
    /**
653
     * Update plugin
654
     *
655
     * @param Plugin $plugin
656
     * @param array  $meta     Config data
657
     * @param array  $eventYml event data
658
     *
659
     * @throws \Exception
660
     */
661
    public function updatePlugin(Plugin $plugin, $meta, $eventYml)
662
    {
663
        $em = $this->entityManager;
664
        try {
665
            $em->getConnection()->beginTransaction();
666
            $plugin->setVersion($meta['version'])
667
                ->setName($meta['name']);
668
            if (isset($meta['event'])) {
669
                $plugin->setClassName($meta['event']);
670
            }
671
            $rep = $this->pluginEventHandlerRepository;
672
            if (!empty($eventYml) && is_array($eventYml)) {
673
                foreach ($eventYml as $event => $handlers) {
674
                    foreach ($handlers as $handler) {
675
                        if (!$this->checkSymbolName($handler[0])) {
676
                            throw new PluginException('Handler name format error');
677
                        }
678
                        // updateで追加されたハンドラかどうか調べる
679
                        $peh = $rep->findBy(
680
                            [
681
                            'plugin_id' => $plugin->getId(),
682
                            'event' => $event,
683
                            'handler' => $handler[0],
684
                            'handler_type' => $handler[1],
685
                                ]
686
                        );
687
688
                        // 新規にevent.ymlに定義されたハンドラなのでinsertする
689
                        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...
690
                            $peh = new PluginEventHandler();
691
                            $peh->setPlugin($plugin)
692
                                ->setEvent($event)
693
                                ->setHandler($handler[0])
694
                                ->setHandlerType($handler[1])
695
                                ->setPriority($rep->calcNewPriority($event, $handler[1]));
696
                            $em->persist($peh);
697
                            $em->flush();
698
                        }
699
                    }
700
                }
701
702
                // アップデート後のevent.ymlで削除されたハンドラをdtb_plugin_event_handlerから探して削除
703
                /** @var PluginEventHandler $peh */
704
                foreach ($rep->findBy(['plugin_id' => $plugin->getId()]) as $peh) {
705
                    if (!isset($eventYml[$peh->getEvent()])) {
706
                        $em->remove($peh);
707
                        $em->flush();
708
                    } else {
709
                        $match = false;
710
                        foreach ($eventYml[$peh->getEvent()] as $handler) {
711
                            if ($peh->getHandler() == $handler[0] && $peh->getHandlerType() == $handler[1]) {
712
                                $match = true;
713
                            }
714
                        }
715
                        if (!$match) {
716
                            $em->remove($peh);
717
                            $em->flush();
718
                        }
719
                    }
720
                }
721
            }
722
723
            $em->persist($plugin);
724
            $this->callPluginManagerMethod($meta, 'update');
725
            $em->flush();
726
            $em->getConnection()->commit();
727
        } catch (\Exception $e) {
728
            $em->getConnection()->rollback();
729
            throw $e;
730
        }
731
    }
732
733
    /**
734
     * Do check dependency plugin
735
     *
736
     * @param array $plugins    get from api
737
     * @param array $plugin     format as plugin from api
738
     * @param array $dependents template output
739
     *
740
     * @return array|mixed
741
     */
742
    public function getDependency($plugins, $plugin, $dependents = [])
743
    {
744
        // Prevent infinity loop
745
        if (empty($dependents)) {
746
            $dependents[] = $plugin;
747
        }
748
749
        // Check dependency
750
        if (!isset($plugin['require']) || empty($plugin['require'])) {
751
            return $dependents;
752
        }
753
754
        $require = $plugin['require'];
755
        // Check dependency
756
        foreach ($require as $pluginName => $version) {
757
            $dependPlugin = $this->buildInfo($plugins, $pluginName);
758
            // Prevent call self
759
            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...
760
                continue;
761
            }
762
763
            // Check duplicate in dependency
764
            $index = array_search($dependPlugin['product_code'], array_column($dependents, 'product_code'));
765
            if ($index === false) {
766
                // Update require version
767
                $dependPlugin['version'] = $version;
768
                $dependents[] = $dependPlugin;
769
                // Check child dependency
770
                $dependents = $this->getDependency($plugins, $dependPlugin, $dependents);
771
            }
772
        }
773
774
        return $dependents;
775
    }
776
777
    /**
778
     * Get plugin information
779
     *
780
     * @param array  $plugins    get from api
781
     * @param string $pluginCode
782
     *
783
     * @return array|null
784
     */
785
    public function buildInfo($plugins, $pluginCode)
786
    {
787
        $plugin = [];
788
        $index = $this->checkPluginExist($plugins, $pluginCode);
789
        if ($index === false) {
790
            return $plugin;
791
        }
792
        // Get target plugin in return of api
793
        $plugin = $plugins[$index];
794
795
        // Check the eccube version that the plugin supports.
796
        $plugin['is_supported_eccube_version'] = 0;
797
        if (in_array(Constant::VERSION, $plugin['eccube_version'])) {
798
            // Match version
799
            $plugin['is_supported_eccube_version'] = 1;
800
        }
801
802
        $plugin['depend'] = $this->getRequirePluginName($plugins, $plugin);
803
804
        return $plugin;
805
    }
806
807
    /**
808
     * Get dependency name and version only
809
     *
810
     * @param array $plugins get from api
811
     * @param array $plugin  target plugin from api
812
     *
813
     * @return mixed format [0 => ['name' => pluginName1, 'version' => pluginVersion1], 1 => ['name' => pluginName2, 'version' => pluginVersion2]]
814
     */
815
    public function getRequirePluginName($plugins, $plugin)
816
    {
817
        $depend = [];
818
        if (isset($plugin['require']) && !empty($plugin['require'])) {
819
            foreach ($plugin['require'] as $name => $version) {
820
                $ret = $this->checkPluginExist($plugins, $name);
821
                if ($ret === false) {
822
                    continue;
823
                }
824
                $depend[] = [
825
                    'name' => $plugins[$ret]['name'],
826
                    'version' => $version,
827
                ];
828
            }
829
        }
830
831
        return $depend;
832
    }
833
834
    /**
835
     * Check require plugin in enable
836
     *
837
     * @param string $pluginCode
838
     *
839
     * @return array plugin code
840
     */
841
    public function findRequirePluginNeedEnable($pluginCode)
842
    {
843
        $dir = $this->eccubeConfig['plugin_realdir'].'/'.$pluginCode;
844
        $composerFile = $dir.'/composer.json';
845
        if (!file_exists($composerFile)) {
846
            return [];
847
        }
848
        $jsonText = file_get_contents($composerFile);
849
        $json = json_decode($jsonText, true);
850
        // Check require
851
        if (!isset($json['require']) || empty($json['require'])) {
852
            return [];
853
        }
854
        $require = $json['require'];
855
856
        // Remove vendor plugin
857
        if (isset($require[self::VENDOR_NAME.'/plugin-installer'])) {
858
            unset($require[self::VENDOR_NAME.'/plugin-installer']);
859
        }
860
        $requires = [];
861
        foreach ($require as $name => $version) {
862
            // Check plugin of ec-cube only
863
            if (strpos($name, self::VENDOR_NAME.'/') !== false) {
864
                $requireCode = str_replace(self::VENDOR_NAME.'/', '', $name);
865
                $ret = $this->isEnable($requireCode);
866
                if ($ret) {
867
                    continue;
868
                }
869
                $requires[] = $requireCode;
870
            }
871
        }
872
873
        return $requires;
874
    }
875
876
    /**
877
     * Find the dependent plugins that need to be disabled
878
     *
879
     * @param string $pluginCode
880
     *
881
     * @return array plugin code
882
     */
883
    public function findDependentPluginNeedDisable($pluginCode)
884
    {
885
        return $this->findDependentPlugin($pluginCode, true);
886
    }
887
888
    /**
889
     * Find the other plugin that has requires on it.
890
     * Check in both dtb_plugin table and <PluginCode>/composer.json
891
     *
892
     * @param string $pluginCode
893
     * @param bool   $enableOnly
894
     *
895
     * @return array plugin code
896
     */
897
    public function findDependentPlugin($pluginCode, $enableOnly = false)
898
    {
899
        $criteria = Criteria::create()
900
            ->where(Criteria::expr()->neq('code', $pluginCode));
901
        if ($enableOnly) {
902
            $criteria->andWhere(Criteria::expr()->eq('enabled', Constant::ENABLED));
903
        }
904
        /**
905
         * @var Plugin[]
906
         */
907
        $plugins = $this->pluginRepository->matching($criteria);
908
        $dependents = [];
909
        foreach ($plugins as $plugin) {
910
            $dir = $this->eccubeConfig['plugin_realdir'].'/'.$plugin->getCode();
911
            $fileName = $dir.'/composer.json';
912
            if (!file_exists($fileName)) {
913
                continue;
914
            }
915
            $jsonText = file_get_contents($fileName);
916
            if ($jsonText) {
917
                $json = json_decode($jsonText, true);
918
                if (!isset($json['require'])) {
919
                    continue;
920
                }
921
                if (array_key_exists(self::VENDOR_NAME.'/'.$pluginCode, $json['require'])) {
922
                    $dependents[] = $plugin->getCode();
923
                }
924
            }
925
        }
926
927
        return $dependents;
928
    }
929
930
    /**
931
     * Get dependent plugin by code
932
     * It's base on composer.json
933
     * Return the plugin code and version in the format of the composer
934
     *
935
     * @param string   $pluginCode
936
     * @param int|null $libraryType
937
     *                      self::ECCUBE_LIBRARY only return library/plugin of eccube
938
     *                      self::OTHER_LIBRARY only return library/plugin of 3rd part ex: symfony, composer, ...
939
     *                      default : return all library/plugin
940
     *
941
     * @return array format [packageName1 => version1, packageName2 => version2]
942
     */
943
    public function getDependentByCode($pluginCode, $libraryType = null)
944
    {
945
        $pluginDir = $this->calcPluginDir($pluginCode);
946
        $jsonFile = $pluginDir.'/composer.json';
947
        if (!file_exists($jsonFile)) {
948
            return [];
949
        }
950
        $jsonText = file_get_contents($jsonFile);
951
        $json = json_decode($jsonText, true);
952
        $dependents = [];
953
        if (isset($json['require'])) {
954
            $require = $json['require'];
955
            switch ($libraryType) {
956 View Code Duplication
                case self::ECCUBE_LIBRARY:
957
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require))));
958
                    break;
959
960 View Code Duplication
                case self::OTHER_LIBRARY:
961
                    $dependents = array_intersect_key($require, array_flip(preg_grep('/^'.self::VENDOR_NAME.'\//i', array_keys($require), PREG_GREP_INVERT)));
962
                    break;
963
964
                default:
965
                    $dependents = $json['require'];
966
                    break;
967
            }
968
        }
969
970
        return $dependents;
971
    }
972
973
    /**
974
     * Format array dependent plugin to string
975
     * It is used for commands.
976
     *
977
     * @param array $packages   format [packageName1 => version1, packageName2 => version2]
978
     * @param bool  $getVersion
979
     *
980
     * @return string format if version=true: "packageName1:version1 packageName2:version2", if version=false: "packageName1 packageName2"
981
     */
982
    public function parseToComposerCommand(array $packages, $getVersion = true)
983
    {
984
        $result = array_keys($packages);
985
        if ($getVersion) {
986
            $result = array_map(function ($package, $version) {
987
                return $package.':'.$version;
988
            }, array_keys($packages), array_values($packages));
989
        }
990
991
        return implode(' ', $result);
992
    }
993
994
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$pluginBaseDir" missing
Loading history...
introduced by
Doc comment for parameter "$pluginCode" missing
Loading history...
995
     * リソースファイル等をコピー
996
     * コピー元となるファイルの置き場所は固定であり、
997
     * [プラグインコード]/Resource/assets
998
     * 配下に置かれているファイルが所定の位置へコピーされる
999
     *
1000
     * @param $pluginBaseDir
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
1001
     * @param $pluginCode
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
1002
     */
1003
    public function copyAssets($pluginBaseDir, $pluginCode)
1004
    {
1005
        $assetsDir = $pluginBaseDir.'/Resource/assets';
1006
1007
        // プラグインにリソースファイルがあれば所定の位置へコピー
1008
        if (file_exists($assetsDir)) {
1009
            $file = new Filesystem();
1010
            $file->mirror($assetsDir, $this->eccubeConfig['plugin_html_realdir'].$pluginCode.'/assets');
1011
        }
1012
    }
1013
1014
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$pluginCode" missing
Loading history...
1015
     * コピーしたリソースファイル等を削除
1016
     *
1017
     * @param $pluginCode
0 ignored issues
show
introduced by
Missing parameter name
Loading history...
1018
     */
1019
    public function removeAssets($pluginCode)
1020
    {
1021
        $assetsDir = $this->projectRoot.'/app/Plugin/'.$pluginCode;
1022
1023
        // コピーされているリソースファイルがあれば削除
1024
        if (file_exists($assetsDir)) {
1025
            $file = new Filesystem();
1026
            $file->remove($assetsDir);
1027
        }
1028
    }
1029
1030
    /**
1031
     * Is update
1032
     *
1033
     * @param string $pluginVersion
1034
     * @param string $remoteVersion
1035
     *
1036
     * @return mixed
1037
     */
1038
    public function isUpdate($pluginVersion, $remoteVersion)
1039
    {
1040
        return version_compare($pluginVersion, $remoteVersion, '<');
1041
    }
1042
1043
    /**
1044
     * Plugin is exist check
1045
     *
1046
     * @param array  $plugins    get from api
1047
     * @param string $pluginCode
1048
     *
1049
     * @return false|int|string
1050
     */
1051
    public function checkPluginExist($plugins, $pluginCode)
1052
    {
1053
        if (strpos($pluginCode, self::VENDOR_NAME.'/') !== false) {
1054
            $pluginCode = str_replace(self::VENDOR_NAME.'/', '', $pluginCode);
1055
        }
1056
        // Find plugin in array
1057
        $index = array_search($pluginCode, array_column($plugins, 'product_code'));
1058
1059
        return $index;
1060
    }
1061
1062
    /**
1063
     * @param string $code
1064
     *
1065
     * @return bool
1066
     */
1067
    private function isEnable($code)
1068
    {
1069
        $Plugin = $this->pluginRepository->findOneBy([
1070
            'enabled' => Constant::ENABLED,
1071
            'code' => $code,
1072
        ]);
1073
        if ($Plugin) {
1074
            return true;
1075
        }
1076
1077
        return false;
1078
    }
1079
}
1080