Skip to content
Open
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
72 changes: 64 additions & 8 deletions classes/helpers/FrmEntriesListHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,13 +414,15 @@ public function single_row( $item, $style = '' ) {
if ( $this->column_name === 'cb' ) {
$r .= "<th scope='row' class='check-column'>$checkbox</th>";
} else {
$val = in_array( $column_name, $hidden, true ) ? '' : $this->column_value( $item );
$r .= "<td $attributes>";

// phpcs:ignore Universal.Operators.StrictComparisons
if ( $column_name == $action_col ) {
$is_action_col = $column_name == $action_col;
$val = in_array( $column_name, $hidden, true ) ? '' : $this->column_value( $item, $is_action_col );
$r .= "<td $attributes>";

if ( $is_action_col ) {
$edit_link = admin_url( 'admin.php?page=formidable-entries&frm_action=edit&id=' . $item->id );
$r .= '<a href="' . esc_url( isset( $actions['edit'] ) ? $edit_link : $view_link ) . '" class="row-title" >' . $val . '</a> ';
$link_text = self::get_row_title_text( $val, $item );
$r .= '<a href="' . esc_url( isset( $actions['edit'] ) ? $edit_link : $view_link ) . '" class="row-title" >' . $link_text . '</a> ';
$r .= $action_links;
} else {
$r .= $val;
Expand All @@ -433,6 +435,29 @@ public function single_row( $item, $style = '' ) {
return $r . '</tr>';
}

/**
* Get the content of the row title link, so the link is never empty for screen readers.
*
* A link nested inside the row title link (like a field or custom column value with its own link) is invalid markup,
* and browsers close the row title link before it, leaving the row title link with no text.
*
* @since x.x
*
* @param mixed $val The column value.
* @param stdClass $item The entry.
*
* @return string
*/
private static function get_row_title_text( $val, $item ) {
$val = preg_replace( '/<\/?a\b[^>]*>/i', '', (string) $val );

if ( '' === trim( wp_strip_all_tags( $val ) ) ) {
$val .= '<span class="screen-reader-text">' . esc_html( self::get_entry_label( $item ) ) . '</span>';
}

return $val;
}

/**
* Get the column names that the logged in user can action on
*
Expand All @@ -444,10 +469,12 @@ private function get_action_columns() {

/**
* @param object $item
* @param bool $is_action_col Whether this column is the row's primary/action column
* (wrapped in the row-title link by the caller).
*
* @return mixed
*/
private function column_value( $item ) {
private function column_value( $item, $is_action_col = false ) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`column_value` has a cyclomatic complexity of 18 with "High" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`column_value` has a cyclomatic complexity of 19 with "High" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

$col_name = $this->maybe_fix_column_name( $this->column_name );

switch ( $col_name ) {
Expand Down Expand Up @@ -484,14 +511,19 @@ private function column_value( $item ) {
$form_id = $item->form_id;
$user_can_edit_forms = false === FrmAppHelper::permission_nonce_error( 'frm_edit_forms' );

if ( $user_can_edit_forms ) {
if ( $user_can_edit_forms && ! $is_action_col ) {
// When this column is the row's own action column, the caller already
// wraps it in a row-title link to the entry -- nesting a second <a> here
// (to the form itself) produces invalid, empty-named markup once the
// browser closes the outer anchor early. Fall back to the plain label,
// same as when the user can't edit forms.
$val = FrmFormsHelper::edit_form_link( $form_id );
} else {
$val = FrmFormsHelper::edit_form_link_label( $form_id );
}
break;
case 'post_id':
$val = FrmAppHelper::post_edit_link( $item->post_id );
$val = $this->get_post_column_value( $item, $is_action_col );
break;
case 'user_id':
$user = get_userdata( $item->user_id );
Expand All @@ -501,6 +533,8 @@ private function column_value( $item ) {
$val = $item->parent_item_id;
break;
default:
// $is_action_col isn't passed to these filters. If a custom column value with its own <a>
// becomes the row's action column, single_row() strips the nested link in get_row_title_text().
$val = apply_filters( 'frm_entries_' . $col_name . '_column', false, compact( 'item' ) );

if ( $val === false ) {
Expand All @@ -521,6 +555,28 @@ private function column_value( $item ) {
return $val;
}

/**
* Get the Post column value.
*
* @since x.x
*
* @param object $item The entry.
* @param bool $is_action_col Whether this column is the row's primary/action column.
*
* @return string
*/
private function get_post_column_value( $item, $is_action_col ) {
if ( ! $is_action_col ) {
return FrmAppHelper::post_edit_link( $item->post_id );
}

// Same reasoning as the form_id case in column_value(): post_edit_link() builds its
// own nested <a>, which breaks the outer row-title link when this column
// is the action column.
$post = get_post( $item->post_id );
return $post ? FrmAppHelper::truncate( $post->post_title, 50 ) : FrmFormsHelper::get_no_title_text();
}

/**
* When a form has entries with the 0 item meta value, the values do not appear properly in the entries list.
*
Expand Down
145 changes: 143 additions & 2 deletions tests/phpunit/entries/test_FrmEntriesListHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,96 @@ public function test_column_value() {
$this->assertSame( 'My entry name', $column_value );
}

/**
* The Form column's own value is wrapped in a second, nested row-title
* link when it's the row's action column (e.g. Entry Key/ID columns
* hidden on the "all forms" Entries screen) -- browsers implicitly close
* the outer row-title <a> as soon as they hit that nested one, leaving it
* with no accessible name. When acting as the action column, the form
* value must be plain text instead.
*
* @covers FrmEntriesListHelper::column_value
*/
public function test_column_value_form_id_as_action_col_has_no_nested_link() {
FrmAppHelper::set_current_screen_and_hook_suffix();
wp_set_current_user( 1 );

$item = new stdClass();
$item->form_id = $this->factory->form->create( array( 'name' => 'My Form' ) );

// Not the action column: existing behavior, links to the form itself.
$column_value = $this->column_value( $item, 'form_id', false );
$this->assertStringContainsString( '<a ', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertStringContainsString( 'My Form', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringContainsString()


The method you are trying to call is not defined, which can result in a fatal error.


// As the action column: no nested <a>, plain label only.
$column_value = $this->column_value( $item, 'form_id', true );
$this->assertStringNotContainsString( '<a ', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringNotContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertSame( 'My Form', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* An entry whose form no longer exists (form_id doesn't resolve to a
* row) must still render non-empty fallback text as the action column,
* not an empty row-title link.
*
* @covers FrmEntriesListHelper::column_value
*/
public function test_column_value_form_id_as_action_col_missing_form_falls_back() {
FrmAppHelper::set_current_screen_and_hook_suffix();
wp_set_current_user( 1 );

$item = new stdClass();
$item->form_id = 999999999;

$column_value = $this->column_value( $item, 'form_id', true );
$this->assertNotEmpty( $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertNotEmpty()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertStringNotContainsString( '<a ', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringNotContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* Same bug class as the form_id case above, on the Post column shown
* when a form has a create-post action: post_edit_link() builds its own
* nested <a>, which must be dropped in favor of a plain label when this
* column is the row's action column.
*
* @covers FrmEntriesListHelper::column_value
*/
public function test_column_value_post_id_as_action_col_has_no_nested_link() {
FrmAppHelper::set_current_screen_and_hook_suffix();

$item = new stdClass();
$item->post_id = $this->factory->post->create( array( 'post_title' => 'My Post' ) );

// Not the action column: existing behavior, links to the post itself.
$column_value = $this->column_value( $item, 'post_id', false );
$this->assertStringContainsString( '<a ', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertStringContainsString( 'My Post', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringContainsString()


The method you are trying to call is not defined, which can result in a fatal error.


// As the action column: no nested <a>, plain label only.
$column_value = $this->column_value( $item, 'post_id', true );
$this->assertStringNotContainsString( '<a ', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringNotContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertSame( 'My Post', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* An entry whose linked post no longer exists must still render
* non-empty fallback text as the action column, not an empty
* row-title link.
*
* @covers FrmEntriesListHelper::column_value
*/
public function test_column_value_post_id_as_action_col_missing_post_falls_back() {
FrmAppHelper::set_current_screen_and_hook_suffix();

$item = new stdClass();
$item->post_id = 999999999;

$column_value = $this->column_value( $item, 'post_id', true );
$this->assertNotEmpty( $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertNotEmpty()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertStringNotContainsString( '<a ', $column_value );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringNotContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* Reasons support stored metadata, manual moderation, and missing sources.
*/
Expand All @@ -64,13 +154,64 @@ public function test_spam_reason_column() {
}
}

/**
* The row title link must never contain a nested link or be left without text,
* whichever column ends up as the row title column.
*
* @see https://github.com/Strategy11/formidable-pro/issues/6662
*
* @covers FrmEntriesListHelper::single_row
*/
public function test_single_row_title_link_has_no_nested_link() {
FrmAppHelper::set_current_screen_and_hook_suffix();
wp_set_current_user( 1 );

$item = (object) array(
'id' => 5,
'name' => '',
'ip' => '',
'form_id' => $this->factory->form->create( array( 'name' => 'My Form' ) ),
);

$row = $this->single_row( $item, array( '0_form_id' => 'Form' ) );
$this->assertMatchesRegularExpression( '/<a [^>]*class="row-title" >My Form<\/a>/', $row );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertMatchesRegularExpression()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertSame( 1, substr_count( $row, '>My Form<' ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertSame()


The method you are trying to call is not defined, which can result in a fatal error.


// A custom column value with its own link is reduced to its text.
$custom_column = static function () {
return '<a href="https://example.com">Custom value</a>';
};
add_filter( 'frm_entries_custom_column', $custom_column );
$row = $this->single_row( $item, array( '0_custom' => 'Custom' ) );
remove_filter( 'frm_entries_custom_column', $custom_column );
$this->assertStringContainsString( 'class="row-title" >Custom value</a>', $row );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

$this->assertStringNotContainsString( 'example.com', $row );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringNotContainsString()


The method you are trying to call is not defined, which can result in a fatal error.


// An empty value still gives the link an accessible name.
$row = $this->single_row( $item, array( '0_ip' => 'IP' ) );
$this->assertStringContainsString( 'class="row-title" ><span class="screen-reader-text">Entry 5</span></a>', $row );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to an undefined method test_FrmEntriesListHelper::assertStringContainsString()


The method you are trying to call is not defined, which can result in a fatal error.

}

/**
* @param stdClass $item
* @param array $columns
*
* @return string
*/
private function single_row( $item, $columns ) {
$list_helper = new FrmEntriesListHelper( array( 'params' => array( 'form' => 0 ) ) );
$this->set_private_property( $list_helper, '_column_headers', array( $columns, array(), array(), key( $columns ) ) );
return $list_helper->single_row( $item );
}

/**
* @param stdClass $item
* @param string $column_name
* @param bool $is_action_col
*/
private function column_value( $item, $column_name ) {
private function column_value( $item, $column_name, $is_action_col = false ) {
$list_helper = new FrmEntriesListHelper( array() );
$this->set_private_property( $list_helper, 'column_name', $column_name );
return $this->run_private_method( array( $list_helper, 'column_value' ), array( $item ) );
return $this->run_private_method( array( $list_helper, 'column_value' ), array( $item, $is_action_col ) );
}
}
Loading