1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace EdmondsCommerce\PHPQA\Markdown; |
4
|
|
|
|
5
|
|
|
use EdmondsCommerce\PHPQA\Helper; |
6
|
|
|
|
7
|
|
|
class LinksChecker |
8
|
|
|
{ |
9
|
|
|
/** |
10
|
|
|
* @param string $projectRootDirectory |
11
|
|
|
* |
12
|
|
|
* @return array |
13
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
14
|
|
|
*/ |
15
|
4 |
|
private static function getFiles(string $projectRootDirectory): array |
16
|
|
|
{ |
17
|
4 |
|
$files = self::getDocsFiles($projectRootDirectory); |
18
|
4 |
|
$files[] = self::getMainReadme($projectRootDirectory); |
19
|
|
|
|
20
|
3 |
|
return $files; |
21
|
|
|
} |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @param string $projectRootDirectory |
25
|
|
|
* |
26
|
|
|
* @return string |
27
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
28
|
|
|
*/ |
29
|
4 |
|
private static function getMainReadme(string $projectRootDirectory): string |
30
|
|
|
{ |
31
|
4 |
|
$path = $projectRootDirectory.'/README.md'; |
32
|
4 |
|
if (!is_file($path)) { |
33
|
1 |
|
throw new \RuntimeException( |
34
|
|
|
"\n\nYou have no README.md file in your project" |
35
|
1 |
|
."\n\nAs the bear minimum you need to have this file to pass QA" |
36
|
|
|
); |
37
|
|
|
} |
38
|
|
|
|
39
|
3 |
|
return $path; |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @param string $projectRootDirectory |
44
|
|
|
* |
45
|
|
|
* @return array |
46
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
47
|
|
|
*/ |
48
|
4 |
|
private static function getDocsFiles(string $projectRootDirectory): array |
49
|
|
|
{ |
50
|
4 |
|
$files = []; |
51
|
4 |
|
$dir = $projectRootDirectory.'/docs'; |
52
|
4 |
|
if (!is_dir($dir)) { |
53
|
2 |
|
return $files; |
54
|
|
|
} |
55
|
2 |
|
$directory = new \RecursiveDirectoryIterator($dir); |
56
|
2 |
|
$recursive = new \RecursiveIteratorIterator($directory); |
57
|
2 |
|
$regex = new \RegexIterator( |
58
|
2 |
|
$recursive, |
59
|
2 |
|
'/^.+\.md/i', |
60
|
2 |
|
\RecursiveRegexIterator::GET_MATCH |
61
|
|
|
); |
62
|
2 |
|
foreach ($regex as $file) { |
63
|
2 |
|
if (!empty($file[0])) { |
64
|
2 |
|
$files[] = $file[0]; |
65
|
|
|
} |
66
|
|
|
} |
67
|
|
|
|
68
|
2 |
|
return $files; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @param string $file |
73
|
|
|
* |
74
|
|
|
* @return array |
75
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
76
|
|
|
*/ |
77
|
3 |
|
private static function getLinks(string $file): array |
78
|
|
|
{ |
79
|
3 |
|
$links = []; |
80
|
3 |
|
$contents = file_get_contents($file); |
81
|
3 |
|
if (preg_match_all( |
82
|
3 |
|
'/\[(.+?)\].*?\((.+?)\)/', |
83
|
3 |
|
$contents, |
84
|
3 |
|
$matches, |
85
|
3 |
|
PREG_SET_ORDER |
86
|
|
|
)) { |
87
|
3 |
|
$links = array_merge($links, $matches); |
88
|
|
|
} |
89
|
|
|
|
90
|
3 |
|
return $links; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* @param string $projectRootDirectory |
95
|
|
|
* @param array $link |
96
|
|
|
* @param string $file |
97
|
|
|
* @param array $errors |
98
|
|
|
* @param int $return |
99
|
|
|
* |
100
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
101
|
|
|
*/ |
102
|
3 |
|
private static function checkLink( |
103
|
|
|
string $projectRootDirectory, |
104
|
|
|
array $link, |
105
|
|
|
string $file, |
106
|
|
|
array &$errors, |
107
|
|
|
int &$return |
108
|
|
|
) { |
109
|
3 |
|
$path = \trim($link[2]); |
110
|
3 |
|
if (0 === \strpos($path, '#')) { |
111
|
1 |
|
return; |
112
|
|
|
} |
113
|
3 |
|
if (\preg_match('%^(http|//)%', $path)) { |
114
|
1 |
|
return self::validateHttpLink($link, $errors, $return); |
|
|
|
|
115
|
|
|
} |
116
|
|
|
|
117
|
2 |
|
$path = \current(\explode('#', $path, 2)); |
118
|
2 |
|
$start = \rtrim($projectRootDirectory, '/'); |
119
|
2 |
|
if ($path[0] !== '/' || 0 === \strpos($path, './')) { |
120
|
2 |
|
$relativeSubdirs = \preg_replace( |
121
|
2 |
|
'%^'.$projectRootDirectory.'%', |
122
|
2 |
|
'', |
123
|
2 |
|
\dirname($file) |
124
|
|
|
); |
125
|
2 |
|
$start .= '/'.\rtrim($relativeSubdirs, '/'); |
126
|
|
|
} |
127
|
2 |
|
$realpath = \realpath($start.'/'.$path); |
128
|
2 |
|
if (empty($realpath) || (!\file_exists($realpath) && !\is_dir($realpath))) { |
129
|
1 |
|
$errors[] = \sprintf("\nBad link for \"%s\" to \"%s\"\n", $link[1], $link[2]); |
130
|
1 |
|
$return = 1; |
131
|
|
|
} |
132
|
2 |
|
} |
133
|
|
|
|
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* @param string|null $projectRootDirectory |
137
|
|
|
* |
138
|
|
|
* @return int |
139
|
|
|
* @throws \Exception |
140
|
|
|
* @SuppressWarnings(PHPMD.StaticAccess) |
141
|
|
|
*/ |
142
|
4 |
|
public static function main(string $projectRootDirectory = null): int |
143
|
|
|
{ |
144
|
4 |
|
$return = 0; |
145
|
4 |
|
$projectRootDirectory = $projectRootDirectory ?? Helper::getProjectRootDirectory(); |
146
|
4 |
|
$files = static::getFiles($projectRootDirectory); |
147
|
3 |
|
foreach ($files as $file) { |
148
|
3 |
|
$relativeFile = str_replace($projectRootDirectory, '', $file); |
149
|
3 |
|
$title = "\n$relativeFile\n".str_repeat('-', strlen($relativeFile))."\n"; |
150
|
3 |
|
$errors = []; |
151
|
3 |
|
$links = static::getLinks($file); |
152
|
3 |
|
foreach ($links as $link) { |
153
|
3 |
|
static::checkLink($projectRootDirectory, $link, $file, $errors, $return); |
154
|
|
|
} |
155
|
3 |
|
if (!empty($errors)) { |
156
|
3 |
|
echo $title.implode('', $errors); |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
3 |
|
return $return; |
161
|
|
|
} |
162
|
|
|
|
163
|
1 |
|
private static function validateHttpLink(array $link, array &$errors, int &$return) |
164
|
|
|
{ |
165
|
1 |
|
list(, $anchor, $href) = $link; |
166
|
1 |
|
static $checked = []; |
167
|
1 |
|
$hashPos = strpos($href, '#'); |
168
|
1 |
|
if ($hashPos > 0) { |
169
|
|
|
$href = substr($href, 0, $hashPos); |
170
|
|
|
} |
171
|
1 |
|
if (isset($checked[$href])) { |
172
|
|
|
return; |
173
|
|
|
} |
174
|
1 |
|
$checked[$href] = true; |
175
|
|
|
#$start = microtime(true); |
176
|
|
|
#fwrite(STDERR, "\n".'Validating link: '.$href); |
177
|
1 |
|
$context = stream_context_create([ |
178
|
1 |
|
'http' => [ |
179
|
|
|
'method' => 'HEAD', |
180
|
|
|
'protocol_version' => 1.1, |
181
|
|
|
'header' => [ |
182
|
|
|
'Connection: close', |
183
|
|
|
], |
184
|
|
|
], |
185
|
|
|
]); |
186
|
1 |
|
$result = null; |
187
|
|
|
try { |
188
|
1 |
|
$headers = get_headers($href, 0, $context); |
|
|
|
|
189
|
1 |
|
foreach ($headers as $header) { |
190
|
1 |
|
if (false !== strpos($header, ' 200 ')) { |
191
|
|
|
#$time = round(microtime(true) - $start, 2); |
192
|
|
|
#fwrite(STDERR, "\n".'OK ('.$time.' seconds): '.$href); |
193
|
|
|
|
194
|
1 |
|
return; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
} catch (\Throwable $e) { |
|
|
|
|
198
|
|
|
} |
199
|
|
|
|
200
|
1 |
|
$errors[] = \sprintf( |
201
|
1 |
|
"\nBad link for \"%s\" to \"%s\"\nresult: %s\n", |
202
|
1 |
|
$anchor, |
203
|
1 |
|
$href, |
204
|
1 |
|
var_export($result, true) |
205
|
|
|
); |
206
|
1 |
|
$return = 1; |
207
|
|
|
#$time = round(microtime(true) - $start, 2); |
208
|
|
|
#fwrite(STDERR, "\n".'Failed ('.$time.' seconds): '.$href); |
209
|
1 |
|
} |
210
|
|
|
} |
211
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.