diff --git a/includes/class-post-collection-integration.php b/includes/class-post-collection-integration.php index d8251f4..f64c668 100644 --- a/includes/class-post-collection-integration.php +++ b/includes/class-post-collection-integration.php @@ -391,18 +391,27 @@ private function render_list( $app, $collection, $compact = false ) { */ private function get_download_request() { $url_var = $this->send_to_e_reader->get_download_url_var(); + // The initial picker URL, and a POST with no checked boxes, carry the selection in GET. if ( ! isset( $_GET[ $url_var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public download URL with the password in the parameter name. return false; } $value = wp_unslash( $_GET[ $url_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated below. - if ( is_array( $value ) ) { - $ids = array_values( array_filter( array_map( 'intval', $value ) ) ); - - return empty( $ids ) ? false : array( $ids, null ); + if ( ! is_string( $value ) ) { + return false; } $value = sanitize_key( $value ); + // Checked boxes are submitted in POST while the picker URL stays in GET. + if ( in_array( $value, array( 'list', 'compact' ), true ) && isset( $_POST[ $url_var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- password in URL authorizes this download. + $posted_ids = wp_unslash( $_POST[ $url_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- cast to integers below. + if ( is_array( $posted_ids ) ) { + $ids = array_values( array_filter( array_map( 'intval', $posted_ids ) ) ); + if ( $ids ) { + return array( $ids, null ); + } + } + } $limit = null; if ( preg_match( '/^([a-z]+)-([0-9]+)$/', $value, $matches ) ) { diff --git a/includes/class-send-to-e-reader.php b/includes/class-send-to-e-reader.php index 2d4f893..254b07a 100644 --- a/includes/class-send-to-e-reader.php +++ b/includes/class-send-to-e-reader.php @@ -1112,27 +1112,26 @@ public function get_download_url_var() { public function enable_download_via_url( $viewable ) { $ereader_url_var = $this->get_download_url_var(); + // The initial picker URL, and a POST with no checked boxes, carry the selection in GET. if ( ! isset( $_GET[ $ereader_url_var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public download URL with password in parameter name. return $viewable; } $request_value = wp_unslash( $_GET[ $ereader_url_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated via allowlist below. - if ( - ! is_array( $request_value ) - && ! in_array( - $request_value, - array( - 'new', - 'all', - 'last', - 'list', - 'compact', - ), - true - ) - ) { + if ( ! in_array( $request_value, array( 'new', 'all', 'last', 'list', 'compact' ), true ) ) { return $viewable; } + // Checked boxes are submitted in POST while the picker URL stays in GET. + if ( in_array( $request_value, array( 'list', 'compact' ), true ) && isset( $_POST[ $ereader_url_var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- password in URL authorizes this download. + $posted_ids = wp_unslash( $_POST[ $ereader_url_var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- cast to integers below. + if ( is_array( $posted_ids ) ) { + $ids = array_values( array_filter( array_map( 'intval', $posted_ids ) ) ); + if ( $ids ) { + $request_value = $ids; + } + } + } + self::prevent_response_caching(); $this->download_request = $request_value; return true; diff --git a/plain-list.js b/plain-list.js index 78d470e..9037dda 100644 --- a/plain-list.js +++ b/plain-list.js @@ -12,6 +12,11 @@ var count = document.querySelector('[data-send-to-e-reader-selection-count]'); var checkboxes = document.querySelectorAll('input[type="checkbox"]'); var selected = document.querySelectorAll('input[type="checkbox"]:checked').length; + var downloadButtons = document.querySelectorAll('form button[type="submit"]'); + + downloadButtons.forEach(function (button) { + button.disabled = selected === 0; + }); if (!count) { return; @@ -87,5 +92,11 @@ updateSelectionCount(); }); + document.addEventListener('submit', function (event) { + if (event.target.matches('form') && !event.target.querySelector('input[type="checkbox"]:checked')) { + event.preventDefault(); + } + }); + updateSelectionCount(); }()); diff --git a/templates/plain-list.php b/templates/plain-list.php index bc2cff6..787fea3 100644 --- a/templates/plain-list.php +++ b/templates/plain-list.php @@ -95,6 +95,11 @@ padding: 8px 14px; } + button:disabled { + cursor: default; + opacity: .5; + } + .header-controls, .list-actions { display: flex; @@ -108,6 +113,18 @@ flex: 0 1 360px; } + .list-actions { + gap: 0; + } + + .list-actions a { + margin-right: 10px; + } + + .list-actions a:last-child { + margin-right: 0; + } + .header-download { flex: 0 0 auto; } @@ -264,7 +281,7 @@