Passed
Pull Request — 4 (#10237)
by Maxime
08:47
created

ManifestFileFinder::isInsideThemes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use RuntimeException;
6
use SilverStripe\Assets\FileFinder;
7
8
/**
9
 * An extension to the default file finder with some extra filters to facilitate
10
 * autoload and template manifest generation:
11
 *   - Only modules with _config.php files are scanned.
12
 *   - If a _manifest_exclude file is present inside a directory it is ignored.
13
 *   - Assets and module language directories are ignored.
14
 *   - Module tests directories are skipped if either of these conditions is meant:
15
 *     - the `ignore_tests` option is not set to false.
16
 *     - the module PHP CI configuration matches one of the `ignored_ci_configs`
17
 */
18
class ManifestFileFinder extends FileFinder
19
{
20
21
    const CONFIG_FILE = '_config.php';
22
    const CONFIG_DIR = '_config';
23
    const EXCLUDE_FILE = '_manifest_exclude';
24
    const LANG_DIR = 'lang';
25
    const TESTS_DIR = 'tests';
26
    const VENDOR_DIR = 'vendor';
27
28
    /**
29
     * @deprecated 4.4.0:5.0.0 Use global `RESOURCES_DIR` instead.
30
     */
31
    const RESOURCES_DIR = RESOURCES_DIR;
32
33
    protected static $default_options = [
34
        'include_themes' => false,
35
        'ignore_tests' => true,
36
        'min_depth' => 1,
37
        'ignore_dirs' => ['node_modules'],
38
        'ignored_ci_configs' => []
39
    ];
40
41
    public function acceptDir($basename, $pathname, $depth)
42
    {
43
        // Skip if ignored
44
        if ($this->isInsideIgnored($basename, $pathname, $depth)) {
45
            return false;
46
        }
47
48
        // Keep searching inside vendor
49
        $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
50
        if ($inVendor) {
51
            // Skip nested vendor folders (e.g. vendor/silverstripe/framework/vendor)
52
            if ($depth == 4 && basename($pathname) === self::VENDOR_DIR) {
53
                return false;
54
            }
55
56
            // Keep searching if we could have a subdir module
57
            if ($depth < 3) {
58
                return true;
59
            }
60
61
            // Stop searching if we are in a non-module library
62
            $libraryPath = $this->upLevels($pathname, $depth - 3);
63
            $libraryBase = basename($libraryPath);
64
            if (!$this->isDirectoryModule($libraryBase, $libraryPath, 3)) {
65
                return false;
66
            }
67
        }
68
69
        // Include themes
70
        if ($this->getOption('include_themes') && $this->isInsideThemes($basename, $pathname, $depth)) {
71
            return true;
72
        }
73
74
        // Skip if not in module
75
        if (!$this->isInsideModule($basename, $pathname, $depth)) {
76
            return false;
77
        }
78
79
        // Skip if test dir inside vendor module with unexpected CI Configuration
80
        if ($depth > 3 && $basename === self::TESTS_DIR && $ignoredCIConfig = $this->getOption('ignored_ci_configs')) {
81
            $ciLib = $this->findModuleCIPhpConfiguration($basename, $pathname, $depth);
82
            if (in_array($ciLib, $ignoredCIConfig)) {
83
                return false;
84
            }
85
        }
86
87
        return parent::acceptDir($basename, $pathname, $depth);
88
    }
89
90
    /**
91
     * Check if the given dir is, or is inside the vendor folder
92
     *
93
     * @param string $basename
94
     * @param string $pathname
95
     * @param int $depth
96
     * @return bool
97
     */
98
    public function isInsideVendor($basename, $pathname, $depth)
99
    {
100
        $base = basename($this->upLevels($pathname, $depth - 1) ?: '');
101
        return $base === self::VENDOR_DIR;
102
    }
103
104
    /**
105
     * Check if the given dir is, or is inside the themes folder
106
     *
107
     * @param string $basename
108
     * @param string $pathname
109
     * @param int $depth
110
     * @return bool
111
     */
112
    public function isInsideThemes($basename, $pathname, $depth)
113
    {
114
        $base = basename($this->upLevels($pathname, $depth - 1));
115
        return $base === THEMES_DIR;
116
    }
117
118
    /**
119
     * Check if this folder or any parent is ignored
120
     *
121
     * @param string $basename
122
     * @param string $pathname
123
     * @param int $depth
124
     * @return bool
125
     */
126
    public function isInsideIgnored($basename, $pathname, $depth)
127
    {
128
        return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
129
            return $this->isDirectoryIgnored($basename, $pathname, $depth);
130
        });
131
    }
132
133
    /**
134
     * Check if this folder is inside any module
135
     *
136
     * @param string $basename
137
     * @param string $pathname
138
     * @param int $depth
139
     * @return bool
140
     */
141
    public function isInsideModule($basename, $pathname, $depth)
142
    {
143
        return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
144
            return $this->isDirectoryModule($basename, $pathname, $depth);
145
        });
146
    }
147
148
    /**
149
     * Check if any parents match the given callback
150
     *
151
     * @param string $basename
152
     * @param string $pathname
153
     * @param int $depth
154
     * @param callable $callback
155
     * @return bool
156
     */
157
    protected function anyParents($basename, $pathname, $depth, $callback)
158
    {
159
        // Check all ignored dir up the path
160
        while ($depth >= 0) {
161
            $ignored = $callback($basename, $pathname, $depth);
162
            if ($ignored) {
163
                return true;
164
            }
165
            $pathname = dirname($pathname);
166
            $basename = basename($pathname);
167
            $depth--;
168
        }
169
        return false;
170
    }
171
172
    /**
173
     * Check if the given dir is a module root (not a subdir)
174
     *
175
     * @param string $basename
176
     * @param string $pathname
177
     * @param string $depth
178
     * @return bool
179
     */
180
    public function isDirectoryModule($basename, $pathname, $depth)
181
    {
182
        // Depth can either be 0, 1, or 3 (if and only if inside vendor)
183
        $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
0 ignored issues
show
Bug introduced by
$depth of type string is incompatible with the type integer expected by parameter $depth of SilverStripe\Core\Manife...inder::isInsideVendor(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

183
        $inVendor = $this->isInsideVendor($basename, $pathname, /** @scrutinizer ignore-type */ $depth);
Loading history...
184
        if ($depth > 0 && $depth !== ($inVendor ? 3 : 1)) {
185
            return false;
186
        }
187
188
        // True if config file exists
189
        if (file_exists($pathname . '/' . self::CONFIG_FILE)) {
190
            return true;
191
        }
192
193
        // True if config dir exists
194
        if (file_exists($pathname . '/' . self::CONFIG_DIR)) {
195
            return true;
196
        }
197
198
        return false;
199
    }
200
201
    /**
202
     * Get a parent path the given levels above
203
     *
204
     * @param string $pathname
205
     * @param int $depth Number of parents to rise
206
     * @return string
207
     */
208
    protected function upLevels($pathname, $depth)
209
    {
210
        if ($depth < 0) {
211
            return null;
212
        }
213
        while ($depth--) {
214
            $pathname = dirname($pathname);
215
        }
216
        return $pathname;
217
    }
218
219
    /**
220
     * Get all ignored directories
221
     *
222
     * @return array
223
     */
224
    protected function getIgnoredDirs()
225
    {
226
        $ignored = [self::LANG_DIR, 'node_modules'];
227
        if ($this->getOption('ignore_tests')) {
228
            $ignored[] = self::TESTS_DIR;
229
        }
230
        return $ignored;
231
    }
232
233
    /**
234
     * Check if the given directory is ignored
235
     * @param string $basename
236
     * @param string $pathname
237
     * @param string $depth
238
     * @return bool
239
     */
240
    public function isDirectoryIgnored($basename, $pathname, $depth)
241
    {
242
        // Don't ignore root
243
        if ($depth === 0) {
0 ignored issues
show
introduced by
The condition $depth === 0 is always false.
Loading history...
244
            return false;
245
        }
246
247
        // Check if manifest-ignored is present
248
        if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
249
            return true;
250
        }
251
252
        // Check if directory name is ignored
253
        $ignored = $this->getIgnoredDirs();
254
        if (in_array($basename, $ignored)) {
255
            return true;
256
        }
257
258
        // Ignore these dirs in the root only
259
        if ($depth === 1 && in_array($basename, [ASSETS_DIR, RESOURCES_DIR])) {
0 ignored issues
show
introduced by
The condition $depth === 1 is always false.
Loading history...
260
            return true;
261
        }
262
263
        return false;
264
    }
265
266
    /**
267
     * Find out the root of the current module and read the PHP CI configuration from tho composer file
268
     *
269
     * @param string $basename Name of the current folder
270
     * @param string $pathname Full path the parent folder
271
     * @param string $depth Depth of the current folder
272
     */
273
    private function findModuleCIPhpConfiguration(string $basename, string $pathname, int $depth): string
274
    {
275
        if ($depth < 1) {
276
            // We went all the way back to the root of the project
277
            return Module::CI_UNKNOWN;
278
        }
279
280
        // We pop the current folder and use the next entry the pathname
281
        $newBasename = basename($pathname);
282
        $newPathname = dirname($pathname);
283
        $newDepth = $depth - 1;
284
285
        if ($this->isDirectoryModule($newBasename, $newPathname, $newDepth)) {
286
            // We've reached the root of the module folder, we can read the PHP CI config now
287
            $module = new Module($newPathname, $this->upLevels($newPathname, $newDepth));
288
            $config = $module->getCIConfig();
289
290
            if (empty($config['PHP'])) {
291
                // This should never happen
292
                throw new RuntimeException('Module::getCIConfig() did not return a PHP CI value');
293
            }
294
295
            return $config['PHP'];
296
        }
297
298
        // We haven't reach our module root yet ... let's look up one more level
299
        return $this->findModuleCIPhpConfiguration($newBasename, $newPathname, $newDepth);
300
    }
301
}
302