Skip to content

Commit 33448d6

Browse files
brownagjennybc
andauthored
feat: check_win: add webform argument as alternative to FTP upload (#2619)
* feat: check_win: add `webform` argument to POST to web form instead of passive FTP * Use httr conditionally * Style with air * Use httr2 * Use `walk()` * Use xml2 devtools has an indirect dependency on xml2 already: pak::pkg_deps_explain("devtools", "xml2") #> devtools -> pkgdown -> xml2 #> devtools -> roxygen2 -> xml2 #> devtools -> urlchecker -> xml2 * Add a test * The whole rlang namespace is imported * Add a NEWS bullet --------- Co-authored-by: Jenny Bryan <jenny.f.bryan@gmail.com>
1 parent f1fe8cd commit 33448d6

5 files changed

Lines changed: 103 additions & 8 deletions

File tree

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ Suggests:
5454
remotes (>= 2.5.0),
5555
rmarkdown (>= 2.14),
5656
rstudioapi (>= 0.13),
57-
spelling (>= 2.2)
57+
spelling (>= 2.2),
58+
xml2
5859
VignetteBuilder: knitr, quarto
5960
Config/Needs/website: tidyverse/tidytemplate
6061
Config/testthat/edition: 3

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Other improvements
1818
* `build_site()` now just calls `pkgdown::build_site()`, meaning that you will get more (informative) output by default (#2578).
1919
* `check_doc_fields()` is a new function that checks for missing `\value` and `\examples` fields in Rd files, which are commonly flagged by CRAN (#2525).
2020
* `check_mac_devel()` is a new function to check a package using the macOS builder at https://mac.r-project.org/macbuilder/submit.html (@nfrerebeau, #2507)
21+
* `check_win()` and friends gain a `webform` argument that uses a webform instead of passive FTP upload (@brownag, #2619).
2122
* `dev_sitrep()` now works correctly inside Positron (#2618), uses pak instead of remotes to check for dependencies that are missing/behind/ahead (#2663), and uses cli for user-facing messages.
2223
* `is_loading()` is now re-exported from pkgload (#2556).
2324
* `load_all()` now errors if called recursively, i.e. if you accidentally include a `load_all()` call in one of your R source files (#2617).

R/check-win.R

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#' @param email An alternative email address to use. If `NULL`, the default is
1414
#' to use the package maintainer's email.
1515
#' @param quiet If `TRUE`, suppresses output.
16+
#' @param webform If `TRUE`, uses web form instead of passive FTP upload.
1617
#' @param ... Additional arguments passed to [pkgbuild::build()].
1718
#' @family build functions
1819
#' @name check_win
@@ -26,6 +27,7 @@ check_win_devel <- function(
2627
manual = TRUE,
2728
email = NULL,
2829
quiet = FALSE,
30+
webform = FALSE,
2931
...
3032
) {
3133
check_dots_used(action = getOption("devtools.ellipsis_action", warn))
@@ -37,6 +39,7 @@ check_win_devel <- function(
3739
manual = manual,
3840
email = email,
3941
quiet = quiet,
42+
webform = webform,
4043
...
4144
)
4245
}
@@ -49,6 +52,7 @@ check_win_release <- function(
4952
manual = TRUE,
5053
email = NULL,
5154
quiet = FALSE,
55+
webform = FALSE,
5256
...
5357
) {
5458
check_dots_used(action = getOption("devtools.ellipsis_action", warn))
@@ -60,6 +64,7 @@ check_win_release <- function(
6064
manual = manual,
6165
email = email,
6266
quiet = quiet,
67+
webform = webform,
6368
...
6469
)
6570
}
@@ -72,6 +77,7 @@ check_win_oldrelease <- function(
7277
manual = TRUE,
7378
email = NULL,
7479
quiet = FALSE,
80+
webform = FALSE,
7581
...
7682
) {
7783
check_dots_used(action = getOption("devtools.ellipsis_action", warn))
@@ -83,6 +89,7 @@ check_win_oldrelease <- function(
8389
manual = manual,
8490
email = email,
8591
quiet = quiet,
92+
webform = webform,
8693
...
8794
)
8895
}
@@ -94,6 +101,7 @@ check_win <- function(
94101
manual = TRUE,
95102
email = NULL,
96103
quiet = FALSE,
104+
webform = FALSE,
97105
...
98106
) {
99107
pkg <- as.package(pkg)
@@ -131,13 +139,11 @@ check_win <- function(
131139
)
132140
on.exit(file_delete(built_path), add = TRUE)
133141

134-
url <- paste0(
135-
"ftp://win-builder.r-project.org/",
136-
version,
137-
"/",
138-
path_file(built_path)
139-
)
140-
walk(url, upload_ftp, file = built_path)
142+
if (webform) {
143+
submit_winbuilder_webform(built_path, version)
144+
} else {
145+
submit_winbuilder_ftp(built_path, version)
146+
}
141147

142148
if (!quiet) {
143149
time <- strftime(Sys.time() + 30 * 60, "%I:%M %p")
@@ -152,6 +158,20 @@ check_win <- function(
152158
invisible()
153159
}
154160

161+
submit_winbuilder_ftp <- function(path, version) {
162+
url <- paste0(
163+
"ftp://win-builder.r-project.org/",
164+
version,
165+
"/",
166+
path_file(path)
167+
)
168+
walk(url, upload_ftp, file = path)
169+
}
170+
171+
submit_winbuilder_webform <- function(path, version) {
172+
walk(version, upload_webform, file = path)
173+
}
174+
155175
confirm_maintainer_email <- function(email, call = parent.frame()) {
156176
if (!rlang::is_interactive()) {
157177
return(FALSE)
@@ -216,3 +236,52 @@ upload_ftp <- function(file, url, verbose = FALSE) {
216236
)
217237
curl::curl_fetch_memory(url, handle = h)
218238
}
239+
240+
parse_winbuilder_form <- function(url, version) {
241+
req <- httr2::request(url)
242+
resp <- httr2::req_perform(req)
243+
html <- xml2::read_html(httr2::resp_body_string(resp))
244+
245+
# Extract hidden fields shared by the whole form
246+
hidden_nodes <- xml2::xml_find_all(html, ".//input[@type='hidden']")
247+
hidden <- as.list(xml2::xml_attr(hidden_nodes, "value"))
248+
names(hidden) <- xml2::xml_attr(hidden_nodes, "name")
249+
250+
# Find the <h2> heading for the requested version, then grab the file
251+
# input and submit button from the <div> that follows it
252+
headings <- xml2::xml_find_all(html, ".//h2")
253+
heading_texts <- xml2::xml_text(headings)
254+
idx <- match(version, heading_texts)
255+
if (is.na(idx)) {
256+
cli::cli_abort(
257+
"Could not find {.val {version}} section in the WinBuilder form."
258+
)
259+
}
260+
261+
section <- xml2::xml_find_first(headings[[idx]], "following-sibling::div")
262+
file_field <- xml2::xml_attr(
263+
xml2::xml_find_first(section, ".//input[@type='file']"),
264+
"name"
265+
)
266+
button_field <- xml2::xml_attr(
267+
xml2::xml_find_first(section, ".//input[@type='submit']"),
268+
"name"
269+
)
270+
271+
list(hidden = hidden, file_field = file_field, button_field = button_field)
272+
}
273+
274+
upload_webform <- function(file, version) {
275+
check_installed(c("httr2", "xml2"))
276+
277+
upload_url <- "https://win-builder.r-project.org/upload.aspx"
278+
form <- parse_winbuilder_form(upload_url, version)
279+
280+
body <- form$hidden
281+
body[[form$file_field]] <- curl::form_file(file)
282+
body[[form$button_field]] <- "Upload File"
283+
284+
req <- httr2::request(upload_url)
285+
req <- httr2::req_body_multipart(req, !!!body)
286+
httr2::req_perform(req)
287+
}

man/check_win.Rd

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-check-win.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
test_that("parse_winbuilder_form() can extract form fields from live page", {
2+
skip_on_cran()
3+
skip_if_not_installed("httr2")
4+
skip_if_not_installed("xml2")
5+
6+
url <- "https://win-builder.r-project.org/upload.aspx"
7+
8+
for (version in c("R-devel", "R-release", "R-oldrelease")) {
9+
form <- parse_winbuilder_form(url, version)
10+
11+
expect_named(form, c("hidden", "file_field", "button_field"))
12+
expect_true("__VIEWSTATE" %in% names(form$hidden))
13+
expect_true("__VIEWSTATEGENERATOR" %in% names(form$hidden))
14+
expect_true("__EVENTVALIDATION" %in% names(form$hidden))
15+
expect_type(form$file_field, "character")
16+
expect_type(form$button_field, "character")
17+
}
18+
})
19+
120
test_that("change_maintainer_email checks fields", {
221
path <- withr::local_tempfile()
322

0 commit comments

Comments
 (0)