Failed Conditions
Pull Request — experimental/3.1 (#2723)
by
unknown
79:43
created

PluginService::doBackup()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1003
            try {
1004
                $file->mirror($backupDir, $pluginDir, null, ['override' => true, 'delete' => true]);
1005
                $file->remove($backupDir);
1006
                if ($Plugin) {
1007
                    $configYml = $this->readYml($pluginDir.'/'.self::CONFIG_YML);
1008
                    $eventYml = $this->readYml($pluginDir.'/'.self::EVENT_YML);
1009
                    $this->updatePlugin($Plugin, $configYml, $eventYml);
1010
                }
1011
            } catch (\Exception $e) {
1012
                log_error($e->getMessage());
1013
                throw $e;
1014
            }
1015
        }
1016
1017
        return true;
1018
    }
1019
1020
    /**
1021
     * Backup file before upgrade
1022
     *
1023
     * @param array $updateData
1024
     * @throws PluginException
1025
     * @return bool
1026
     */
1027
    public function doBackup($updateData)
1028
    {
1029
        if (empty($updateData)) {
1030
            return false;
1031
        }
1032
        $pluginRealDir = $this->appConfig['plugin_realdir'];
1033
        $pluginTempRealDir = $this->appConfig['plugin_temp_realdir'];
1034
1035
        $file = new Filesystem();
1036
        // Backup composer file
1037
        $file->copy($this->appConfig['root_dir'].'/composer.json', $pluginTempRealDir.'/composer.json', true);
1038
        $file->copy($this->appConfig['root_dir'].'/composer.lock', $pluginTempRealDir.'/composer.lock', true);
1039
1040
        foreach ($updateData as $value) {
1041
            if (!isset($value['product_code'])) {
1042
                throw new PluginException('The format incorrect!');
1043
            }
1044
            $pluginDir = $pluginRealDir.'/'.$value['product_code'];
1045
            if (!is_dir($pluginDir)) {
1046
                continue;
1047
            }
1048
            $backupDir = $pluginTempRealDir.'/'.$value['product_code'];
1049
            try {
1050
                $file->remove($backupDir);
1051
                $file->mirror($pluginDir, $backupDir);
1052
            } catch (\Exception $e) {
1053
                log_error($e->getMessage());
1054
                continue;
1055
            }
1056
        }
1057
1058
        return true;
1059
    }
1060
1061
    /**
1062
     * @param string $code
1063
     * @return bool
1064
     */
1065
    private function isEnable($code)
1066
    {
1067
        $Plugin = $this->pluginRepository->findOneBy([
0 ignored issues
show
introduced by
Add a comma after each item in a multi-line array
Loading history...
1068
            'enabled' => Constant::ENABLED,
1069
            'code' => $code
1070
        ]);
1071
        if ($Plugin) {
1072
            return true;
1073
        }
1074
1075
        return false;
1076
    }
1077
}
1078