Completed
Pull Request — experimental/3.1 (#2707)
by Ryo
21:55
created

PluginService::postInstall()   B

Complexity

Conditions 3
Paths 30

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.3955

Importance

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