Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions includes/class-post-collection-integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ) {
Expand Down
27 changes: 13 additions & 14 deletions includes/class-send-to-e-reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions plain-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}());
19 changes: 18 additions & 1 deletion templates/plain-list.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
padding: 8px 14px;
}

button:disabled {
cursor: default;
opacity: .5;
}

.header-controls,
.list-actions {
display: flex;
Expand All @@ -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;
}
Expand Down Expand Up @@ -264,7 +281,7 @@
</head>
<body class="<?php echo esc_attr( $compact ? 'plain-list-compact' : 'plain-list-full' ); ?>">
<main>
<form>
<form method="post">
<header>
<div class="header-title">
<h1><?php echo esc_html( $args['title'] ); ?></h1>
Expand Down
9 changes: 8 additions & 1 deletion tests/Test_Post_Collection_Integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function tearDown(): void {
unset( $GLOBALS['send_to_e_reader_test_caps'] );
unset( $GLOBALS['send_to_e_reader_test_posts'] );
unset( $_GET['epubsecret'] );
unset( $_POST['epubsecret'] );
parent::tearDown();
}

Expand Down Expand Up @@ -243,8 +244,14 @@ public function test_the_download_url_only_accepts_known_selections() {
$_GET['epubsecret'] = 'everything-3';
$this->assertFalse( $this->call( $integration, 'get_download_request' ) );

$_GET['epubsecret'] = array( '4', '0', 'seven', '9' );
$_GET['epubsecret'] = 'list';
$this->assertSame( array( 'list', null ), $this->call( $integration, 'get_download_request' ) );
$_POST['epubsecret'] = array( '4', '9' );
$this->assertSame( array( array( 4, 9 ), null ), $this->call( $integration, 'get_download_request' ) );
unset( $_POST['epubsecret'] );

$_GET['epubsecret'] = array( '4', '0', 'seven', '9' );
$this->assertFalse( $this->call( $integration, 'get_download_request' ) );
}

/**
Expand Down
24 changes: 24 additions & 0 deletions tests/Test_Send_To_E_Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function tearDown(): void {
unset( $GLOBALS['send_to_e_reader_test_scripts'] );
unset( $GLOBALS['send_to_e_reader_test_nocache_headers_sent'] );
unset( $_GET['epubsecret'] );
unset( $_POST['epubsecret'] );
parent::tearDown();
}

Expand Down Expand Up @@ -265,6 +266,8 @@ public function test_plain_list_shows_original_article_date() {
$this->assertStringContainsString( 'Collected Link', $output );
$this->assertStringContainsString( 'June 29, 2026', $output );
$this->assertStringContainsString( '1 articles (1 selected)', $output );
$this->assertStringContainsString( '<form method="post">', $output );
$this->assertStringContainsString( 'name="epubsecret[]" value="456"', $output );
}

/**
Expand Down Expand Up @@ -305,6 +308,27 @@ public function test_compact_list_download_url_is_accepted() {
$this->assertTrue( $send_to_e_reader->enable_download_via_url( false ) );
}

/**
* Test that a POST to the password-backed picker downloads selected posts only.
*/
public function test_picker_post_keeps_an_empty_selection_on_the_list() {
update_option( Send_To_E_Reader::DOWNLOAD_PASSWORD_OPTION, 'secret' );
$_GET['epubsecret'] = 'list';
$send_to_e_reader = new Send_To_E_Reader( null );
$request = new \ReflectionProperty( Send_To_E_Reader::class, 'download_request' );
$request->setAccessible( true );

$this->assertTrue( $send_to_e_reader->enable_download_via_url( false ) );
$this->assertSame( 'list', $request->getValue( $send_to_e_reader ) );

$_POST['epubsecret'] = array( '4', '9' );
$this->assertTrue( $send_to_e_reader->enable_download_via_url( false ) );
$this->assertSame( array( 4, 9 ), $request->getValue( $send_to_e_reader ) );

$_GET['epubsecret'] = array( '4', '9' );
$this->assertFalse( $send_to_e_reader->enable_download_via_url( false ) );
}

/**
* Test get_post_author_name returns author name for a post.
*/
Expand Down