1
1
<?php
2
2
3
- function download_file ($ url )
4
- {
5
- $ ch = curl_init ($ url );
6
- curl_setopt ($ ch , CURLOPT_HEADER , 1 );
7
- curl_setopt ($ ch , CURLOPT_RETURNTRANSFER , 1 );
8
- curl_setopt ($ ch , CURLOPT_BINARYTRANSFER , 1 );
9
- curl_setopt ($ ch , CURLOPT_FOLLOWLOCATION , 0 );
3
+ ini_set ('display_errors ' , 0 );
10
4
11
- $ response = curl_exec ($ ch );
5
+ class ApiException extends Exception {}
6
+ class PluginDownloader {
12
7
13
- $ header_size = curl_getinfo ($ ch , CURLINFO_HEADER_SIZE );
14
- $ headers = array_map ('trim ' , explode ("\n" , substr ($ response , 0 , $ header_size )));
15
- $ body = substr ($ response , $ header_size );
8
+ private $ githubToken ;
16
9
17
- return [ $ headers , $ body ] ;
18
- }
10
+ public const PLUGINS = ' plugins ' ;
11
+ public const THEMES = ' themes ' ;
19
12
20
- if (isset ($ _GET ['plugin ' ])) {
21
- $ plugin_name = preg_replace ('#[^a-zA-Z0-9\.\-_]# ' , '' , $ _GET ['plugin ' ]);
22
- $ zip_url = 'https://downloads.wordpress.org/plugin/ ' . $ plugin_name ;
23
- } else if (isset ($ _GET ['theme ' ])) {
24
- $ theme_name = preg_replace ('#[^a-zA-Z0-9\.\-_]# ' , '' , $ _GET ['theme ' ]);
25
- $ zip_url = 'https://downloads.wordpress.org/theme/ ' . $ theme_name ;
26
- } else {
27
- die ('Invalid request ' );
28
- }
13
+ public function __construct ($ githubToken )
14
+ {
15
+ $ this ->githubToken = $ githubToken ;
16
+ }
17
+
18
+ public function streamFromDirectory ($ name , $ directory )
19
+ {
20
+ $ name = preg_replace ('#[^a-zA-Z0-9\.\-_]# ' , '' , $ name );
21
+ $ zipUrl = "https://downloads.wordpress.org/ $ directory/ $ name " ;
22
+ try {
23
+ $ this ->streamHttpResponse ($ zipUrl , [
24
+ 'content-length ' ,
25
+ 'x-frame-options ' ,
26
+ 'last-modified ' ,
27
+ 'etag ' ,
28
+ 'date ' ,
29
+ 'age ' ,
30
+ 'vary ' ,
31
+ 'cache-Control '
32
+ ]);
33
+ } catch (ApiException $ e ) {
34
+ throw new ApiException ("Plugin or theme ' $ name' not found " );
35
+ }
36
+ }
37
+
38
+ public function streamFromGithubPR ($ organization , $ repo , $ pr , $ workflow_name , $ artifact_name ) {
39
+ $ prDetails = $ this ->gitHubRequest ("https://api.github.com/repos/ $ organization/ $ repo/pulls/ $ pr " )['body ' ];
40
+ if (!$ prDetails ) {
41
+ throw new ApiException ('Invalid PR number ' );
42
+ }
43
+ $ branchName = $ prDetails ->head ->ref ;
44
+ $ ciRuns = $ this ->gitHubRequest ("https://api.github.com/repos/ $ organization/ $ repo/actions/runs?branch= $ branchName " )['body ' ];
45
+ if (!$ ciRuns ) {
46
+ throw new ApiException ('No CI runs found ' );
47
+ }
48
+
49
+ $ artifactsUrls = [];
50
+ foreach ($ ciRuns ->workflow_runs as $ run ) {
51
+ if ($ run ->name === $ workflow_name ) {
52
+ $ artifactsUrls [] = $ run ->artifacts_url ;
53
+ }
54
+ }
55
+ if (!$ artifactsUrls ) {
56
+ throw new ApiException ('No artifact URL found ' );
57
+ }
58
+
59
+ foreach ($ artifactsUrls as $ artifactsUrl ) {
60
+ $ zip_download_api_endpoint = $ zip_url = null ;
61
+
62
+ $ artifacts = $ this ->gitHubRequest ($ artifactsUrl )['body ' ];
63
+ if (!$ artifacts ) {
64
+ continue ;
65
+ }
66
+
67
+ foreach ($ artifacts ->artifacts as $ artifact ) {
68
+ if ($ artifact ->name === $ artifact_name ) {
69
+ $ zip_download_api_endpoint = $ artifact ->archive_download_url ;
70
+ break ;
71
+ }
72
+ }
73
+ if (!$ zip_download_api_endpoint ) {
74
+ continue ;
75
+ }
76
+
77
+ $ zip_download_headers = $ this ->gitHubRequest ($ zip_download_api_endpoint , true )['headers ' ];
78
+ // Find the location header and store it in $zip_url
79
+ foreach ($ zip_download_headers as $ header ) {
80
+ if (substr (strtolower ($ header ), 0 , 10 ) === 'location: ' ) {
81
+ $ zip_url = substr ($ header , 10 );
82
+ break ;
83
+ }
84
+ }
85
+ if (!$ zip_url ) {
86
+ continue ;
87
+ }
88
+ $ this ->streamHttpResponse ($ zip_url , [], [
89
+ 'Content-Length: ' .$ artifact ->size_in_bytes
90
+ ]);
91
+ }
92
+ if (!$ artifacts ) {
93
+ throw new ApiException ('No artifacts found under the URL ' );
94
+ }
95
+ if (!$ zip_download_api_endpoint ) {
96
+ throw new ApiException ('No artifact download URL found with the name ' );
97
+ }
98
+ if (!$ zip_url ) {
99
+ throw new ApiException ('No zip location returned by the artifact download API ' );
100
+ }
101
+ }
102
+
103
+ protected function gitHubRequest ($ url )
104
+ {
105
+ $ headers [] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36 ' ;
106
+ $ headers [] = 'Authorization: Bearer ' . $ this ->githubToken ;
107
+ $ context = stream_context_create ([
108
+ 'http ' => [
109
+ 'method ' => 'GET ' ,
110
+ 'header ' => implode ("\r\n" , $ headers ),
111
+ ]
112
+ ]);
113
+ $ response = file_get_contents ($ url , false , $ context );
114
+ if ($ response === false ) {
115
+ throw new ApiException ('Request failed ' );
116
+ }
117
+ return [
118
+ 'body ' => json_decode ($ response ),
119
+ 'headers ' => array_map ('trim ' , array_slice ($ http_response_header , 1 ))
120
+ ];
121
+ }
29
122
30
- [$ received_headers , $ bytes ] = download_file ($ zip_url );
31
-
32
- $ forward_headers = [
33
- 'content-length ' ,
34
- 'content-type ' ,
35
- 'content-disposition ' ,
36
- 'x-frame-options ' ,
37
- 'last-modified ' ,
38
- 'etag ' ,
39
- 'date ' ,
40
- 'age ' ,
41
- 'vary ' ,
42
- 'cache-Control '
43
- ];
44
-
45
- foreach ($ received_headers as $ received_header ) {
46
- $ comparable_header = strtolower ($ received_header );
47
- foreach ($ forward_headers as $ sought_header ) {
48
- if (substr ($ comparable_header , 0 , strlen ($ sought_header )) === $ sought_header ) {
49
- header ($ received_header );
50
- break ;
123
+ private function streamHttpResponse ($ url , $ allowed_headers =[], $ default_headers =[]) {
124
+ $ default_headers = array_merge ([
125
+ 'Content-Type: application/zip ' ,
126
+ 'Content-Disposition: attachment; filename="plugin.zip" ' ,
127
+ ], $ default_headers );
128
+ $ ch = curl_init ($ url );
129
+ curl_setopt_array ($ ch ,
130
+ [
131
+ CURLOPT_RETURNTRANSFER => true ,
132
+ CURLOPT_CONNECTTIMEOUT => 30 ,
133
+ CURLOPT_FAILONERROR => true ,
134
+ CURLOPT_FOLLOWLOCATION => true ,
135
+ ]
136
+ );
137
+
138
+ $ seen_headers = [];
139
+ curl_setopt ($ ch , CURLOPT_HEADERFUNCTION , function ($ curl , $ header_line ) use ($ seen_headers , $ allowed_headers )
140
+ {
141
+ $ header_name = strtolower (substr ($ header_line , 0 , strpos ($ header_line , ': ' )));
142
+ $ seen_headers [$ header_name ] = true ;
143
+ if (in_array ($ header_name , $ allowed_headers )) {
144
+ header ($ header_line );
145
+ }
146
+ return strlen ($ header_line );
147
+ }
148
+ );
149
+ $ extra_headers_sent = false ;
150
+ curl_setopt ($ ch , CURLOPT_WRITEFUNCTION , function ($ curl , $ body ) use (&$ extra_headers_sent , $ default_headers )
151
+ {
152
+ if (!$ extra_headers_sent ) {
153
+ foreach ($ default_headers as $ header_line ) {
154
+ $ header_name = strtolower (substr ($ header_line , 0 , strpos ($ header_line , ': ' )));
155
+ if (!isset ($ seen_headers [strtolower ($ header_name )])) {
156
+ header ($ header_line );
157
+ }
158
+ }
159
+ $ extra_headers_sent = true ;
160
+ }
161
+ echo $ body ;
162
+ flush ();
163
+ return strlen ($ body );
164
+ }
165
+ );
166
+ curl_exec ($ ch );
167
+ $ info = curl_getinfo ($ ch );
168
+ curl_close ($ ch );
169
+ if ($ info ['http_code ' ] > 299 || $ info ['http_code ' ] < 200 ) {
170
+ throw new ApiException ('Request failed ' );
51
171
}
52
172
}
173
+
53
174
}
54
175
55
- header ('Access-Control-Allow-Origin: * ' );
176
+ $ downloader = new PluginDownloader (
177
+ getenv ('GITHUB_TOKEN ' )
178
+ );
56
179
57
- echo $ bytes ;
180
+ // Serve the request:
181
+ header ('Access-Control-Allow-Origin: * ' );
182
+ $ pluginResponse ;
183
+ try {
184
+ if (isset ($ _GET ['plugin ' ])) {
185
+ $ downloader ->streamFromDirectory ($ _GET ['plugin ' ], PluginDownloader::PLUGINS );
186
+ } else if (isset ($ _GET ['theme ' ])) {
187
+ $ downloader ->streamFromDirectory ($ _GET ['plugin ' ], PluginDownloader::THEMES );
188
+ } else if (isset ($ _GET ['org ' ]) && isset ($ _GET ['repo ' ]) && isset ($ _GET ['workflow ' ]) && isset ($ _GET ['pr ' ]) && isset ($ _GET ['artifact ' ])) {
189
+ $ allowedInputs = [
190
+ [
191
+ 'org ' => 'WordPress ' ,
192
+ 'repo ' => 'gutenberg ' ,
193
+ 'workflow ' => 'Build Gutenberg Plugin Zip ' ,
194
+ 'artifact ' => 'gutenberg-plugin '
195
+ ],
196
+ [
197
+ 'org ' => 'woocommerce ' ,
198
+ 'repo ' => 'woocommerce ' ,
199
+ 'workflow ' => 'Build Live Branch ' ,
200
+ 'artifact ' => 'plugins '
201
+ ]
202
+ ];
203
+ $ allowed = false ;
204
+ foreach ($ allowedInputs as $ allowedInput ) {
205
+ if (
206
+ $ _GET ['org ' ] === $ allowedInput ['org ' ] &&
207
+ $ _GET ['repo ' ] === $ allowedInput ['repo ' ] &&
208
+ $ _GET ['workflow ' ] === $ allowedInput ['workflow ' ] &&
209
+ $ _GET ['artifact ' ] === $ allowedInput ['artifact ' ]
210
+ ) {
211
+ $ allowed = true ;
212
+ break ;
213
+ }
214
+ }
215
+ if (!$ allowed ) {
216
+ die ('Invalid request ' );
217
+ }
218
+ $ downloader ->streamFromGithubPR (
219
+ $ _GET ['org ' ],
220
+ $ _GET ['repo ' ],
221
+ $ _GET ['pr ' ],
222
+ $ _GET ['workflow ' ],
223
+ $ _GET ['artifact ' ]
224
+ );
225
+ } else {
226
+ throw new ApiException ('Invalid query parameters ' );
227
+ }
228
+ } catch (ApiException $ e ) {
229
+ header ('HTTP/1.1 400 Invalid request ' );
230
+ if (!headers_sent ()) {
231
+ header ('Content-Type: application/json ' );
232
+ }
233
+ die (json_encode (['error ' => $ e ->getMessage ()]));
234
+ }
0 commit comments