Completed
Pull Request — master (#22)
by Maxime
02:45
created

Library   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 84
dl 0
loc 311
rs 9.52
c 0
b 0
f 0
wmc 36

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 12 3
A getType() 0 8 2
A exposePaths() 0 14 3
A getPath() 0 3 1
A getBasePublicPath() 0 8 2
A getJson() 0 9 2
A getBasePath() 0 3 1
A getResourcesDir() 0 23 4
A publicPathExists() 0 3 1
A getPublicPath() 0 11 3
A requiresExpose() 0 9 3
A __construct() 0 8 1
A getExposedFolders() 0 17 4
A installedIntoVendor() 0 3 1
A validateFolder() 0 12 4
A getRelativePath() 0 3 1
1
<?php
2
3
namespace SilverStripe\VendorPlugin;
4
5
use Composer\IO\NullIO;
6
use Composer\Json\JsonFile;
7
use LogicException;
8
use SilverStripe\VendorPlugin\Methods\ExposeMethod;
9
10
/**
11
 * Represents a library being installed
12
 */
13
class Library
14
{
15
    const TRIM_CHARS = '/\\';
16
17
    /**
18
     * Hard-coded 'public' web-root folder
19
     */
20
    const PUBLIC_PATH = 'public';
21
22
    /**
23
     * Default folder where vendor resources will be exposed.
24
     */
25
    const DEFAULT_RESOURCES_DIR = 'resources';
26
27
    /**
28
     * Subfolder to map within public webroot
29
     * @deprecated 1.4.0..2.0.0 Use Library::getResourcesDir() instead
30
     */
31
    const RESOURCES_PATH = self::DEFAULT_RESOURCES_DIR;
32
33
    /**
34
     * Project root
35
     *
36
     * @var string
37
     */
38
    protected $basePath = null;
39
40
    /**
41
     * Install path of this library
42
     *
43
     * @var string
44
     */
45
    protected $path = null;
46
47
    /**
48
     * Build a vendor module library
49
     *
50
     * @param string $basePath Project root folder
51
     * @param string $libraryPath Path to this library
52
     * @param string $name Composer name of this library
53
     */
54
    public function __construct(
55
        $basePath,
56
        $libraryPath,
57
        $name = null
58
    ) {
59
        $this->basePath = realpath($basePath);
60
        $this->path = realpath($libraryPath);
61
        $this->name = $name;
62
    }
63
64
    /**
65
     * Module name
66
     *
67
     * @var string
68
     */
69
    protected $name = null;
70
71
    /**
72
     * Get module name
73
     *
74
     * @return string
75
     */
76
    public function getName()
77
    {
78
        if ($this->name) {
79
            return $this->name;
80
        }
81
        // Get from composer
82
        $json = $this->getJson();
83
84
        if (isset($json['name'])) {
85
            $this->name = $json['name'];
86
        }
87
        return $this->name;
88
    }
89
90
    /**
91
     * Get type of library
92
     *
93
     * @return string
94
     */
95
    public function getType()
96
    {
97
        // Get from composer
98
        $json = $this->getJson();
99
        if (isset($json['type'])) {
100
            return $json['type'];
101
        }
102
        return 'module';
103
    }
104
105
    /**
106
     * Get path to base project for this module
107
     *
108
     * @return string Path with no trailing slash E.g. /var/www/
109
     */
110
    public function getBasePath()
111
    {
112
        return $this->basePath;
113
    }
114
115
    /**
116
     * Get base path to expose all libraries to
117
     *
118
     * @return string Path with no trailing slash E.g. /var/www/public/_resources
119
     */
120
    public function getBasePublicPath()
121
    {
122
        $projectPath = $this->getBasePath();
123
        $resourceDir = $this->getResourcesDir();
124
        $publicPath = $this->publicPathExists()
125
            ? Util::joinPaths($projectPath, self::PUBLIC_PATH, $resourceDir)
126
            : Util::joinPaths($projectPath, $resourceDir);
127
        return $publicPath;
128
    }
129
130
    /**
131
     * Get path for this module
132
     *
133
     * @return string Path with no trailing slash E.g. /var/www/vendor/silverstripe/module
134
     */
135
    public function getPath()
136
    {
137
        return $this->path;
138
    }
139
140
    /**
141
     * Get path relative to base dir.
142
     * If module path is base this will be empty string
143
     *
144
     * @return string Path with trimmed slashes. E.g. vendor/silverstripe/module.
145
     * This will be empty for the base project.
146
     */
147
    public function getRelativePath()
148
    {
149
        return trim(substr($this->path, strlen($this->basePath)), self::TRIM_CHARS);
150
    }
151
152
    /**
153
     * Get base path to map resources for this module
154
     *
155
     * @return string Path with trimmed slashes. E.g. /var/www/public/_resources/vendor/silverstripe/module
156
     */
157
    public function getPublicPath()
158
    {
159
        $relativePath = $this->getRelativePath();
160
161
        // 4.0 compatibility: If there is no public folder, and this is a vendor path,
162
        // remove the leading `vendor` from the destination
163
        if (!$this->publicPathExists() && $this->installedIntoVendor()) {
164
            $relativePath = substr($relativePath, strlen('vendor/'));
165
        }
166
167
        return Util::joinPaths($this->getBasePublicPath(), $relativePath);
168
    }
169
170
    /**
171
     * Cache of composer.json content
172
     *
173
     * @var array
174
     */
175
    protected $json = [];
176
177
    /**
178
     * Get json content for this module from composer.json
179
     *
180
     * @return array
181
     */
182
    protected function getJson()
183
    {
184
        if ($this->json) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->json 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...
185
            return $this->json;
186
        }
187
        $composer = Util::joinPaths($this->getPath(), 'composer.json');
188
        $file = new JsonFile($composer);
189
        $this->json = $file->read();
190
        return $this->json;
191
    }
192
193
    /**
194
     * Determine if this module should be exposed.
195
     * Note: If not using public folders, only vendor modules need to be exposed
196
     *
197
     * @return bool
198
     */
199
    public function requiresExpose()
200
    {
201
        // Don't expose if no folders configured
202
        if (!$this->getExposedFolders()) {
203
            return false;
204
        }
205
206
        // Expose if either public root exists, or vendor module
207
        return $this->publicPathExists() || $this->installedIntoVendor();
208
    }
209
210
    /**
211
     * Expose all web accessible paths for this module
212
     *
213
     * @param ExposeMethod $method
214
     */
215
    public function exposePaths(ExposeMethod $method)
216
    {
217
        // No-op if exposure not necessary for this configuration
218
        if (!$this->requiresExpose()) {
219
            return;
220
        }
221
        $folders = $this->getExposedFolders();
222
        $sourcePath = $this->getPath();
223
        $targetPath = $this->getPublicPath();
224
        foreach ($folders as $folder) {
225
            // Get paths for this folder and delegate to expose method
226
            $folderSourcePath = Util::joinPaths($sourcePath, $folder);
227
            $folderTargetPath = Util::joinPaths($targetPath, $folder);
228
            $method->exposeDirectory($folderSourcePath, $folderTargetPath);
229
        }
230
    }
231
232
    /**
233
     * Get name of all folders to expose (relative to module root)
234
     *
235
     * @return array
236
     */
237
    public function getExposedFolders()
238
    {
239
        $data = $this->getJson();
240
241
        // Get all dirs to expose
242
        if (empty($data['extra']['expose'])) {
243
            return [];
244
        }
245
        $expose = $data['extra']['expose'];
246
247
        // Validate all paths are safe
248
        foreach ($expose as $exposeFolder) {
249
            if (!$this->validateFolder($exposeFolder)) {
250
                throw new LogicException("Invalid module folder " . $exposeFolder);
251
            }
252
        }
253
        return $expose;
254
    }
255
256
    /**
257
     * Validate the given folder is allowed
258
     *
259
     * @param string $exposeFolder Relative folder name to check
260
     * @return bool
261
     */
262
    protected function validateFolder($exposeFolder)
263
    {
264
        if (strstr($exposeFolder, '.')) {
265
            return false;
266
        }
267
        if (strpos($exposeFolder, '/') === 0) {
268
            return false;
269
        }
270
        if (strpos($exposeFolder, '\\') === 0) {
271
            return false;
272
        }
273
        return true;
274
    }
275
276
    /**
277
     * Determin eif the public folder exists
278
     *
279
     * @return bool
280
     */
281
    public function publicPathExists()
282
    {
283
        return is_dir(Util::joinPaths($this->getBasePath(), self::PUBLIC_PATH));
284
    }
285
286
    /**
287
     * Check if this module is installed in vendor
288
     *
289
     * @return bool
290
     */
291
    protected function installedIntoVendor()
292
    {
293
        return preg_match('#^vendor[/\\\\]#', $this->getRelativePath());
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_match('#^ven...his->getRelativePath()) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
294
    }
295
296
    /**
297
     * Determine the name of the folder where vendor module's resources will be exposed. e.g. `_resources`
298
     * @throws LogicException
299
     * @return string
300
     */
301
    public function getResourcesDir()
302
    {
303
        $rootComposerFile = $this->getBasePath() . '/composer.json';
304
        $rootProject = new JsonFile($rootComposerFile, null, new NullIO());
305
306
        if (!$rootProject->exists()) {
307
            return self::DEFAULT_RESOURCES_DIR;
308
        }
309
310
        $rootProjectData = $rootProject->read();
311
        $resourcesDir = isset($rootProjectData['extra']['resources-dir'])
312
            ? $rootProjectData['extra']['resources-dir']
313
            : self::DEFAULT_RESOURCES_DIR;
314
315
316
        if (preg_match('/^[_\-a-z0-9]+$/i', $resourcesDir)) {
317
            return $resourcesDir;
318
        }
319
320
        throw new LogicException(sprintf(
321
            'Resources dir error: "%s" is not a valid resources directory name. Update the ' .
322
            '`extra.resources-dir` key in your composer.json file',
323
            $resourcesDir
324
        ));
325
    }
326
}
327