Passed
Pull Request — 4 (#10142)
by Maxime
07:16
created

ManifestFileFinder::acceptFile()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

172
        $inVendor = $this->isInsideVendor($basename, $pathname, /** @scrutinizer ignore-type */ $depth);
Loading history...
173
        if ($depth > 0 && $depth !== ($inVendor ? 3 : 1)) {
174
            return false;
175
        }
176
177
        // True if config file exists
178
        if (file_exists($pathname . '/' . self::CONFIG_FILE)) {
179
            return true;
180
        }
181
182
        // True if config dir exists
183
        if (file_exists($pathname . '/' . self::CONFIG_DIR)) {
184
            return true;
185
        }
186
187
        return false;
188
    }
189
190
    /**
191
     * Get a parent path the given levels above
192
     *
193
     * @param string $pathname
194
     * @param int $depth Number of parents to rise
195
     * @return string
196
     */
197
    protected function upLevels($pathname, $depth)
198
    {
199
        if ($depth < 0) {
200
            return null;
201
        }
202
        while ($depth--) {
203
            $pathname = dirname($pathname);
204
        }
205
        return $pathname;
206
    }
207
208
    /**
209
     * Get all ignored directories
210
     *
211
     * @return array
212
     */
213
    protected function getIgnoredDirs()
214
    {
215
        $ignored = [self::LANG_DIR, 'node_modules'];
216
        if ($this->getOption('ignore_tests')) {
217
            $ignored[] = self::TESTS_DIR;
218
        }
219
        return $ignored;
220
    }
221
222
    /**
223
     * Check if the given directory is ignored
224
     * @param string $basename
225
     * @param string $pathname
226
     * @param string $depth
227
     * @return bool
228
     */
229
    public function isDirectoryIgnored($basename, $pathname, $depth)
230
    {
231
        // Don't ignore root
232
        if ($depth === 0) {
0 ignored issues
show
introduced by
The condition $depth === 0 is always false.
Loading history...
233
            return false;
234
        }
235
236
        // Check if manifest-ignored is present
237
        if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
238
            return true;
239
        }
240
241
        // Check if directory name is ignored
242
        $ignored = $this->getIgnoredDirs();
243
        if (in_array($basename, $ignored)) {
244
            return true;
245
        }
246
247
        // Ignore these dirs in the root only
248
        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...
249
            return true;
250
        }
251
252
        return false;
253
    }
254
255
    protected function acceptFile($basename, $pathname, $depth)
256
    {
257
        if (preg_match('/^.+Test\.php$/', $basename) && preg_match('/^.+\/tests/', $pathname)) {
258
            return false;
259
        }
260
261
        return parent::acceptFile($basename, $pathname, $depth);
262
    }
263
}
264