File: //proc/self/cwd/wp-content/plugins/autodescription/inc/classes/admin/settings/listedit.class.php
<?php
/**
* @package The_SEO_Framework\Classes\Admin\Settings\ListEdit
* @subpackage The_SEO_Framework\Admin\Edit\List
*/
namespace The_SEO_Framework\Admin\Settings;
\defined( 'THE_SEO_FRAMEWORK_PRESENT' ) or die;
use \The_SEO_Framework\{
Admin,
Admin\Settings\Layout\HTML,
Data,
Data\Filter\Sanitize,
Helper\Query,
Helper\Taxonomy,
Helper\Template,
Meta,
};
/**
* The SEO Framework plugin
* Copyright (C) 2019 - 2024 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Prepares the List Edit view interface.
*
* @since 4.0.0
* @since 5.0.0 Moved from `\The_SEO_Framework\Bridges`.
* @access private
*/
final class ListEdit extends Admin\Lists\Table {
/**
* @since 4.0.0
* @var string The column name.
*/
private $column_name = 'tsf-quick-edit';
/**
* Setups class and prepares quick edit.
*
* @hook admin_init 10
* @since 5.0.0
*/
public static function init_quick_and_bulk_edit() {
$instance = new self;
\add_action( 'current_screen', [ $instance, 'prepare_edit_box' ] );
\add_filter( 'hidden_columns', [ $instance, 'hide_quick_edit_column' ], 10, 1 );
}
/**
* Prepares the quick/bulk edit output.
*
* @hook current_screen 10
* @since 4.0.0
* @since 5.0.0 Renamed from `prepare_edit_box`.
*
* @param \WP_Screen|string $screen \WP_Screen
*/
public function prepare_edit_box( $screen ) {
if ( empty( $screen->taxonomy ) ) {
// WordPress doesn't support this feature yet for taxonomies.
// Exclude it for when the time may come and faulty fields are displayed.
// Mind the "2".
\add_action( 'bulk_edit_custom_box', [ $this, 'display_bulk_edit_fields' ], 10, 2 );
}
\add_action( 'quick_edit_custom_box', [ $this, 'display_quick_edit_fields' ], 10, 3 );
}
/**
* Permanently hides quick/bulk-edit column.
*
* @hook hidden_columns 10
* @since 4.0.0
* @since 5.0.0 Renamed from `_hide_quick_edit_column`.
*
* @param array $hidden The existing hidden columns.
* @return array $columns the column data
*/
public function hide_quick_edit_column( $hidden ) {
$hidden[] = $this->column_name;
return $hidden;
}
/**
* Adds hidden column to access quick/bulk-edit.
* This column is a dummy, but it's required to display quick/bulk edit items.
*
* @hook manage_{$screen_id}_columns 10
* @hook manage_edit-{$taxonomy}_columns 1
* @since 4.0.0
* @since 5.0.0 Renamed from `_add_column`.
* @abstract
*
* @param array $columns The existing columns
* @return array $columns the column data
*/
public function add_column( $columns ) {
// Don't set a title, otherwise it's displayed in the screen settings.
$columns[ $this->column_name ] = '';
return $columns;
}
/**
* Displays the SEO bulk edit fields.
*
* @hook bulk_edit_custom_box 10
* @since 4.0.0
* @since 5.0.0 Renamed from `_display_bulk_edit_fields`.
*
* @param string $column_name Name of the column to edit.
* @param string $post_type The post type slug, or current screen name if this is a taxonomy list table.
* @param string $taxonomy The taxonomy name, if any.
*/
public function display_bulk_edit_fields( $column_name, $post_type, $taxonomy = '' ) {
if ( $this->column_name !== $column_name ) return;
// phpcs:ignore, Generic.CodeAnalysis.EmptyStatement -- For the future, when WordPress Core decides.
if ( $taxonomy ) {
// Not yet.
} else {
Template::output_view( 'list/bulk-post', $post_type, $taxonomy );
}
}
/**
* Displays the SEO quick edit fields.
*
* @hook quick_edit_custom_box 10
* @since 4.0.0
* @since 5.0.0 Renamed from `_display_quick_edit_fields`.
*
* @param string $column_name Name of the column to edit.
* @param string $post_type The post type slug, or current screen name if this is a taxonomy list table.
* @param string $taxonomy The taxonomy name, if any.
*/
public function display_quick_edit_fields( $column_name, $post_type, $taxonomy = '' ) {
if ( $this->column_name !== $column_name ) return;
if ( $taxonomy ) {
Template::output_view( 'list/quick-term', $post_type, $taxonomy );
} else {
Template::output_view( 'list/quick-post', $post_type, $taxonomy );
}
}
/**
* Outputs the quick edit data for posts and pages.
*
* @hook manage_posts_custom_column 1
* @hook manage_pages_custom_column 1
* @since 4.0.0
* @since 5.0.0 Renamed from `_output_column_contents_for_post`.
* @abstract
*
* @param string $column_name The name of the column to display.
* @param int $post_id The current post ID.
*/
public function output_column_contents_for_post( $column_name, $post_id ) {
if (
$this->column_name !== $column_name
|| ! \current_user_can( 'edit_post', $post_id )
) return;
$generator_args = [ 'id' => $post_id ];
$r_defaults = Meta\Robots::get_generated_meta(
$generator_args,
[ 'noindex', 'nofollow', 'noarchive' ],
\The_SEO_Framework\ROBOTS_IGNORE_SETTINGS,
);
$meta = Data\Plugin\Post::get_meta( $post_id );
$is_homepage = Query::is_static_front_page( $generator_args['id'] );
// NB: The indexes correspond to `autodescription-list[index]` field input names.
$data = [
'doctitle' => [
'value' => $meta['_genesis_title'],
],
'description' => [
'value' => $meta['_genesis_description'],
],
'canonical' => [
'value' => $meta['_genesis_canonical_uri'],
],
'noindex' => [
'value' => $meta['_genesis_noindex'],
'isSelect' => true,
'default' => empty( $r_defaults['noindex'] ) ? 'index' : 'noindex', // aka defaultUnprotected
],
'nofollow' => [
'value' => $meta['_genesis_nofollow'],
'isSelect' => true,
'default' => empty( $r_defaults['nofollow'] ) ? 'follow' : 'nofollow',
],
'noarchive' => [
'value' => $meta['_genesis_noarchive'],
'isSelect' => true,
'default' => empty( $r_defaults['noarchive'] ) ? 'archive' : 'noarchive',
],
'redirect' => [
'value' => $meta['redirect'],
'placeholder' => $is_homepage ? \esc_url( Data\Plugin::get_option( 'homepage_redirect' ) ) : '',
],
];
/**
* Tip: Prefix the indexes with your (plugin) name to prevent collisions.
* The index corresponds to field with the ID `autodescription-quick[%s]`, where %s is the index.
*
* Do not modify the structure or remove existing indexes!
*
* @since 4.0.5
* @since 4.1.0 Now has `doctitle` and `description` indexes in its first parameter.
* @since 4.2.3 Now supports the `placeholder` index for $data.
* @param array $data {
* The current data keyed by input field name.
*
* @type mixed $value The current value.
* @type bool $isSelect Optional. Whether the field is a select field.
* @type string $default Optional. Only works when $isSelect is true. The default value to be set in select index 0.
* @type string $placeholder Optional. Only works when $isSelect is false. Sets a placeholder for the input field.
* }
* @param array $generator_args The query data. Contains 'id' or 'taxonomy'.
*/
$data = \apply_filters( 'the_seo_framework_list_table_data', $data, $generator_args );
printf(
// '<span class=hidden id=%s data-le="%s"></span>',
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeData[%s]', (int) $post_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [ 'le' => $data ] )
);
if ( $is_homepage ) {
// When the homepage title is set, we can safely get the custom field.
$_has_home_title = (bool) \strlen( Data\Plugin::get_option( 'homepage_title' ) );
$default_title = $_has_home_title
? Meta\Title::get_custom_title( $generator_args )
: Meta\Title::get_bare_generated_title( $generator_args );
$addition = Meta\Title::get_addition_for_front_page();
$seplocation = Meta\Title::get_addition_location_for_front_page();
$is_title_ref_locked = $_has_home_title;
// When the homepage description is set, we can safely get the custom field.
$_has_home_desc = (bool) \strlen( Data\Plugin::get_option( 'homepage_description' ) );
$default_description = $_has_home_desc
? Meta\Description::get_custom_description( $generator_args )
: Meta\Description::get_generated_description( $generator_args );
$is_desc_ref_locked = $_has_home_desc;
$_has_home_canonical = (bool) Data\Plugin::get_option( 'homepage_canonical' );
$default_canonical = $_has_home_canonical
? Meta\URI::get_custom_canonical_url( $generator_args )
: Meta\URI::get_generated_url( $generator_args );
$is_canonical_ref_locked = $_has_home_canonical;
$permastruct = Meta\URI\Utils::get_url_permastruct( $generator_args );
$is_post_type_hierarchical = false; // Homepage cannot have a parent page.
} else {
static $memo = [];
$memo['addition'] ??= Meta\Title::get_addition();
$memo['seplocation'] ??= Meta\Title::get_addition_location();
$default_title = Meta\Title::get_bare_generated_title( $generator_args );
$addition = $memo['addition'];
$seplocation = $memo['seplocation'];
$is_title_ref_locked = false;
$default_description = Meta\Description::get_generated_description( $generator_args );
$is_desc_ref_locked = false;
$is_canonical_ref_locked = false;
$default_canonical = Meta\URI::get_generated_url( $generator_args );
$memo['post_type'] ??= Query::get_admin_post_type();
$memo['permastruct'] ??= Meta\URI\Utils::get_url_permastruct( $generator_args );
$memo['is_post_type_hierarchical'] ??= \is_post_type_hierarchical( $memo['post_type'] );
$post_type = $memo['post_type'];
$permastruct = $memo['permastruct'];
$parent_post_slugs = [];
$is_post_type_hierarchical = $memo['is_post_type_hierarchical'];
// Homepage doesn't care about its parent page.
if ( $is_post_type_hierarchical && str_contains( $permastruct, '%postname%' ) ) {
// self is filled by current post name.
foreach ( Data\Post::get_post_parents( $post_id ) as $parent_post ) {
// We write it like this instead of [ id => slug ] to prevent reordering numericals via JSON.parse.
$parent_post_slugs[] = [
'id' => $parent_post->ID,
'slug' => $parent_post->post_name,
];
}
}
// Only hierarchical taxonomies can be used in the URL.
// TODO filter post_tag here.
$memo['taxonomies'] ??= $post_type ? Taxonomy::get_hierarchical( 'names', $post_type ) : [];
$taxonomies = $memo['taxonomies'];
$parent_term_slugs_by_tax = [];
// Yes, on its surface, this is a very expensive procedure.
// However, WordPress needs to walk all the terms already to create the post links.
// Hence, it ought to net to zero impact.
foreach ( $taxonomies as $taxonomy ) {
if ( str_contains( $permastruct, "%$taxonomy%" ) ) {
// Broken in Core. Skip writing cache. We may reach this line 200 times, but nobody should be using %post_tag% anyway.
if ( 'post_tag' === $taxonomy ) continue;
$parent_term_slugs_by_tax[ $taxonomy ] = [];
// There's no need to test for hierarchy, because we want the full structure anyway (third parameter).
foreach (
Data\Term::get_term_parents(
Data\Plugin\Post::get_primary_term_id( $post_id, $taxonomy ),
$taxonomy,
true,
)
as $parent_term
) {
// We write it like this instead of [ id => slug ] to prevent reordering numericals via JSON.parse.
$parent_term_slugs_by_tax[ $taxonomy ][] = [
'id' => $parent_term->term_id,
'slug' => $parent_term->slug,
];
}
}
}
// Homepage cannot have an author.
if ( str_contains( $permastruct, '%author%' ) ) {
$author_id = Query::get_post_author_id( $post_id );
if ( $author_id ) {
$author_slugs = [
[
'id' => $author_id,
'slug' => Data\User::get_userdata( $author_id, 'user_nicename' ),
],
];
}
}
}
printf(
// '<span class=hidden id=%s data-le-post-data="%s"></span>',
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLePostData[%s]', (int) $post_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'lePostData' => [
'isFront' => Query::is_static_front_page( $generator_args['id'] ),
],
] ),
);
printf(
// '<span class=hidden id=%s data-le-title="%s"></span>',
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeTitleData[%s]', (int) $post_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'leTitle' => [
'refTitleLocked' => $is_title_ref_locked,
'defaultTitle' => \esc_html( $default_title ),
'addAdditions' => Meta\Title\Conditions::use_branding( $generator_args ),
'additionValue' => \esc_html( $addition ),
'additionPlacement' => 'left' === $seplocation ? 'before' : 'after',
],
] ),
);
printf(
// '<span class=hidden id=%s data-le-description="%s"></span>',
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeDescriptionData[%s]', (int) $post_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'leDescription' => [
'refDescriptionLocked' => $is_desc_ref_locked,
'defaultDescription' => $default_description,
],
] ),
);
printf(
// '<span class=hidden id=%s data-le-canonical="%s"></span>',
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeCanonicalData[%s]', (int) $post_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'leCanonical' => [
'refCanonicalLocked' => $is_canonical_ref_locked,
'defaultCanonical' => \esc_url( $default_canonical ),
'preferredScheme' => Meta\URI\Utils::get_preferred_url_scheme(),
'urlStructure' => $permastruct,
'parentPostSlugs' => $parent_post_slugs ?? [],
'parentTermSlugs' => $parent_term_slugs_by_tax ?? [],
'supportedTaxonomies' => $taxonomies ?? [],
'authorSlugs' => $author_slugs ?? [],
'isHierarchical' => $is_post_type_hierarchical,
// phpcs:ignore, WordPress.DateTime.RestrictedFunctions -- date() is used for URL generation. See `get_permalink()`.
'publishDate' => date( 'c', strtotime( \get_post( $post_id )->post_date ?? 'now' ) ),
],
] ),
);
if ( $this->doing_ajax )
echo $this->get_ajax_dispatch_updated_event(); // phpcs:ignore, WordPress.Security.EscapeOutput
}
/**
* Returns the quick edit data for terms.
*
* @hook manage_{$taxonomy}_custom_column 1
* @since 4.0.0
* @since 4.2.0 Now properly populates use_generated_archive_prefix() with a \WP_Term object.
* @since 5.0.0 Renamed from `_output_column_contents_for_term`.
* @abstract
* @NOTE Unlike `_output_column_post_data()`, this is a filter callback.
* Because of this, the first parameter is a useless string, which must be extended.
* Discrepancy: https://core.trac.wordpress.org/ticket/33521
*
* @param string $string Blank string.
* @param string $column_name Name of the column.
* @param string $term_id Term ID.
* @return string
*/
public function output_column_contents_for_term( $string, $column_name, $term_id ) {
if ( $this->column_name !== $column_name ) return $string;
if ( ! \current_user_can( 'edit_term', $term_id ) ) return $string;
$taxonomy = $this->taxonomy;
$generator_args = [
'id' => $term_id,
'tax' => $taxonomy,
];
$r_defaults = Meta\Robots::get_generated_meta(
$generator_args,
[ 'noindex', 'nofollow', 'noarchive' ],
\The_SEO_Framework\ROBOTS_IGNORE_SETTINGS,
);
$meta = Data\Plugin\Term::get_meta( $term_id );
// NB: The indexes correspond to `autodescription-list[index]` field input names.
$data = [
'doctitle' => [
'value' => $meta['doctitle'],
],
'description' => [
'value' => $meta['description'],
],
'canonical' => [
'value' => $meta['canonical'],
],
'noindex' => [
'value' => $meta['noindex'],
'isSelect' => true,
'default' => empty( $r_defaults['noindex'] ) ? 'index' : 'noindex',
],
'nofollow' => [
'value' => $meta['nofollow'],
'isSelect' => true,
'default' => empty( $r_defaults['nofollow'] ) ? 'follow' : 'nofollow',
],
'noarchive' => [
'value' => $meta['noarchive'],
'isSelect' => true,
'default' => empty( $r_defaults['noarchive'] ) ? 'archive' : 'noarchive',
],
'redirect' => [
'value' => $meta['redirect'],
],
];
/**
* Tip: Prefix the indexes with your (plugin) name to prevent collisions.
* The index corresponds to field with the ID `autodescription-quick[%s]`, where %s is the index.
*
* @since 4.0.5
* @since 4.1.0 Now has `doctitle` and `description` indexes in its first parameter.
* @since 4.2.3 Now supports the `placeholder` index for $data.
* @param array $data {
* The current data keyed by input field name.
*
* @type mixed $value The current value.
* @type bool $isSelect Optional. Whether the field is a select field.
* @type string $default Optional. Only works when $isSelect is true. The default value to be set in select index 0.
* @type string $placeholder Optional. Only works when $isSelect is false. Sets a placeholder for the input field.
* }
* @param array $generator_args The query data. Contains 'id' and 'tax'.
*/
$data = \apply_filters( 'the_seo_framework_list_table_data', $data, $generator_args );
static $memo = [];
$memo['addition'] ??= Meta\Title::get_addition();
$memo['seplocation'] ??= Meta\Title::get_addition_location();
$addition = $memo['addition'];
$seplocation = $memo['seplocation'];
$memo['tax_object'] ??= \get_taxonomy( $taxonomy );
$memo['permastruct'] ??= Meta\URI\Utils::get_url_permastruct( $generator_args );
$memo['is_taxonomy_hierarchical'] ??= $memo['tax_object']->hierarchical && $memo['tax_object']->rewrite['hierarchical'];
$permastruct = $memo['permastruct'];
$parent_term_slugs = [];
$is_taxonomy_hierarchical = $memo['is_taxonomy_hierarchical'];
if ( $is_taxonomy_hierarchical && str_contains( $permastruct, "%$taxonomy%" ) ) {
// self is filled by current term name.
foreach ( Data\Term::get_term_parents( $term_id, $taxonomy ) as $parent_term ) {
// We write it like this instead of [ id => slug ] to prevent reordering numericals via JSON.parse.
$parent_term_slugs[] = [
'id' => $parent_term->term_id,
'slug' => $parent_term->slug,
];
}
}
$container = '';
$container .= \sprintf(
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeData[%s]', (int) $term_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [ 'le' => $data ] )
);
$term_prefix = Meta\Title\Conditions::use_generated_archive_prefix( \get_term( $generator_args['id'], $generator_args['tax'] ) )
? \sprintf(
/* translators: %s: Taxonomy singular name. */
\_x( '%s:', 'taxonomy term archive title prefix', 'default' ),
Taxonomy::get_label( $generator_args['tax'] ),
)
: '';
$container .= \sprintf(
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeTitleData[%s]', (int) $term_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'leTitle' => [
'refTitleLocked' => false,
'defaultTitle' => \esc_html( Meta\Title::get_bare_generated_title( $generator_args ) ),
'addAdditions' => Meta\Title\Conditions::use_branding( $generator_args ),
'additionValue' => \esc_html( $addition ),
'additionPlacement' => 'left' === $seplocation ? 'before' : 'after',
'termPrefix' => \esc_html( $term_prefix ),
],
] ),
);
$container .= \sprintf(
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeDescriptionData[%s]', (int) $term_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'leDescription' => [
'refDescriptionLocked' => false,
'defaultDescription' => Meta\Description::get_generated_description( $generator_args ),
],
] ),
);
$container .= \sprintf(
'<span class=hidden id=%s %s></span>',
\sprintf( 'tsfLeCanonicalData[%s]', (int) $term_id ),
// phpcs:ignore, WordPress.Security.EscapeOutput -- make_data_attributes escapes.
HTML::make_data_attributes( [
'leCanonical' => [
'refCanonicalLocked' => false,
'defaultCanonical' => \esc_url( Meta\URI::get_generated_url( $generator_args ) ),
'preferredScheme' => Meta\URI\Utils::get_preferred_url_scheme(),
'urlStructure' => $permastruct,
'parentTermSlugs' => $parent_term_slugs,
'isHierarchical' => $is_taxonomy_hierarchical,
],
] ),
);
if ( $this->doing_ajax )
$container .= $this->get_ajax_dispatch_updated_event();
return "$string$container";
}
}