'ailsug-suggestion', 'plural' => 'ailsug-suggestions', 'ajax' => false, ) ); } /** * Current filters (whitelisted). * * @return array */ public static function filters() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only filters. $get = static function ( $key ) { return isset( $_GET[ $key ] ) ? sanitize_text_field( wp_unslash( $_GET[ $key ] ) ) : ''; }; $status = sanitize_key( $get( 'status' ) ); $f = array( 'status' => in_array( $status, array_merge( array( 'all' ), Store::STATUSES ), true ) ? $status : 'pending', 'post_type' => sanitize_key( $get( 'post_type_filter' ) ), 'min_score' => '' === $get( 'min_score' ) ? (int) Settings::get( 'min_score', 60 ) : max( 0, min( 100, (int) $get( 'min_score' ) ) ), 'cat' => absint( $get( 'cat' ) ), 'orphan' => '1' === $get( 'orphan' ), 'source' => absint( $get( 'source' ) ), 's' => $get( 's' ), 'orderby' => in_array( $get( 'orderby' ), array( 'score', 'created_at', 'source', 'target' ), true ) ? $get( 'orderby' ) : 'score', 'order' => 'asc' === strtolower( $get( 'order' ) ) ? 'ASC' : 'DESC', ); // phpcs:enable return $f; } /** * Static WHERE clause SQL format string with placeholders. * * @return string */ public static function where_clause() { return '( %s = \'all\' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = \'\' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = \'\' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d )'; } /** * Parameter array for the static WHERE clause. * * @param array $f Filters. * @param bool $with_status Include the status filter. * @return array */ public static function where_params( array $f, $with_status = true ) { global $wpdb; $status = $with_status ? $f['status'] : 'all'; $min_score = ( $f['min_score'] > 0 && ! in_array( $f['status'], array( 'applied', 'reverted' ), true ) ) ? (int) $f['min_score'] : 0; $post_type = (string) $f['post_type']; $cat = (int) $f['cat']; $orphan = $f['orphan'] ? 1 : 0; $source = (int) $f['source']; $search_like = ( '' !== $f['s'] ) ? '%' . $wpdb->esc_like( $f['s'] ) . '%' : ''; $author_id = current_user_can( 'edit_others_posts' ) ? 0 : (int) get_current_user_id(); return array( $status, $status, $min_score, $min_score, $post_type, $post_type, $cat, $wpdb->term_relationships, $wpdb->term_taxonomy, $cat, $orphan, $source, $source, $search_like, $search_like, $search_like, $search_like, $author_id, $author_id, ); } /** * WHERE clause for filters. * * @param array $f Filters. * @param bool $with_status Include the status filter. * @return array{0:string,1:array} */ public static function where( array $f, $with_status = true ) { return array( self::where_clause(), self::where_params( $f, $with_status ) ); } /** * FROM clause SQL format string with placeholders. * * @return string */ public static function from_clause() { return '%i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id'; } /** * Table parameters for FROM clause. * * @return array */ public static function from_params() { global $wpdb; $t = Activator::tables(); return array( $t['suggestions'], $t['index'], $t['index'], $wpdb->posts ); } /** * FROM clause. * * @return string */ public static function from() { return self::from_clause(); } /** * Columns. * * @return array */ public function get_columns() { return array( 'cb' => '', 'source' => __( 'Page to edit', 'linkcortex' ), 'anchor' => __( 'Suggested link text (in context)', 'linkcortex' ), 'target' => __( 'Links to', 'linkcortex' ), 'score' => __( 'Relevance', 'linkcortex' ), 'reason' => __( 'Why', 'linkcortex' ), 'status' => __( 'Status', 'linkcortex' ), ); } /** * Sortable columns. * * @return array */ protected function get_sortable_columns() { return array( 'score' => array( 'score', true ), 'source' => array( 'source', false ), 'target' => array( 'target', false ), ); } /** * Status views. * * @return array */ protected function get_views() { global $wpdb; $f = self::filters(); $from_params = self::from_params(); $w_params = self::where_params( $f, false ); $args = array_merge( $from_params, $w_params ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own table. $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Arguments passed as array. $wpdb->prepare( "SELECT s.status, COUNT(*) AS n FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) GROUP BY s.status", $args ), OBJECT_K ); $labels = array( 'pending' => __( 'To review', 'linkcortex' ), 'applied' => __( 'Applied', 'linkcortex' ), 'rejected' => __( 'Rejected', 'linkcortex' ), 'stale' => __( 'Out of date', 'linkcortex' ), 'reverted' => __( 'Undone', 'linkcortex' ), 'all' => __( 'All', 'linkcortex' ), ); if ( isset( $rows['accepted'] ) && (int) $rows['accepted']->n > 0 ) { $labels = array_slice( $labels, 0, 1, true ) + array( 'accepted' => __( 'Accepted (legacy)', 'linkcortex' ) ) + array_slice( $labels, 1, null, true ); } $views = array(); $total = 0; foreach ( (array) $rows as $row ) { $total += (int) $row->n; } foreach ( $labels as $status => $label ) { $n = 'all' === $status ? $total : (int) ( isset( $rows[ $status ] ) ? $rows[ $status ]->n : 0 ); $views[ $status ] = sprintf( '%3$s (%4$s)', esc_url( Menu::url( 'suggestions', array( 'status' => $status ) ) ), $f['status'] === $status ? ' class="current" aria-current="page"' : '', esc_html( $label ), esc_html( number_format_i18n( $n ) ) ); } return $views; } /** * Bulk actions by view. * * @return array */ protected function get_bulk_actions() { $status = self::filters()['status']; if ( 'applied' === $status ) { return array( 'revert' => __( 'Undo (remove links)', 'linkcortex' ) ); } return array( 'accept' => __( 'Accept & Apply', 'linkcortex' ), 'reject' => __( 'Reject', 'linkcortex' ), ); } /** * Filters above the table. * * @param string $which top|bottom. * @return void */ protected function extra_tablenav( $which ) { if ( 'top' !== $which ) { return; } $f = self::filters(); echo '
'; echo ''; wp_dropdown_categories( array( 'show_option_all' => __( 'All categories', 'linkcortex' ), 'name' => 'cat', 'id' => 'ailsug-f-cat', 'selected' => $f['cat'], 'hide_empty' => true, 'hierarchical' => true, ) ); printf( ' ', esc_html__( 'Min. relevance', 'linkcortex' ), (int) $f['min_score'] ); printf( ' ', checked( $f['orphan'], true, false ), esc_html__( 'Only links to orphan pages', 'linkcortex' ) ); submit_button( __( 'Filter', 'linkcortex' ), '', 'filter_action', false ); echo '
'; } /** * Load items. * * @return void */ public function prepare_items() { global $wpdb; $f = self::filters(); $page = $this->get_pagenum(); $offset = ( $page - 1 ) * self::PER_PAGE; $from_params = self::from_params(); $w_params = self::where_params( $f, true ); $count_args = array_merge( $from_params, $w_params ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own table + posts table. $total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic filter parameters passed as array. $wpdb->prepare( "SELECT COUNT(*) FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d )", $count_args ) ); $orderby = in_array( $f['orderby'], array( 'score', 'created_at', 'source', 'target' ), true ) ? $f['orderby'] : 'score'; $order = ( 'asc' === strtolower( $f['order'] ) ) ? 'ASC' : 'DESC'; $list_args = array_merge( $from_params, $w_params, array( self::PER_PAGE, $offset ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Direct query on own custom table with dynamic filter parameters. switch ( "{$orderby}_{$order}" ) { case 'created_at_ASC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY s.created_at ASC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'created_at_DESC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY s.created_at DESC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'source_ASC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY src.title ASC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'source_DESC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY src.title DESC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'target_ASC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY tgt.title ASC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'target_DESC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY tgt.title DESC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'score_ASC': $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY s.score ASC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; case 'score_DESC': default: $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, src.title AS source_title, src.post_type AS source_type, src.is_builder AS source_builder, tgt.title AS target_title, tgt.primary_keyword AS target_keyword, tgt.inbound_count AS target_inbound FROM %i s LEFT JOIN %i src ON src.post_id = s.source_post_id LEFT JOIN %i tgt ON tgt.post_id = s.target_post_id INNER JOIN %i p ON p.ID = s.source_post_id WHERE ( %s = 'all' OR s.status = %s ) AND ( %d = 0 OR s.score >= %d ) AND ( %s = '' OR src.post_type = %s ) AND ( %d = 0 OR EXISTS (SELECT 1 FROM %i tr INNER JOIN %i tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = s.source_post_id AND tt.term_id = %d) ) AND ( %d = 0 OR tgt.inbound_count = 0 ) AND ( %d = 0 OR s.source_post_id = %d ) AND ( %s = '' OR s.anchor_text LIKE %s OR src.title LIKE %s OR tgt.title LIKE %s ) AND ( %d = 0 OR p.post_author = %d ) ORDER BY s.score DESC, s.id DESC LIMIT %d OFFSET %d", $list_args ), ARRAY_A ); break; } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber if ( 'applied' === $f['status'] && $this->items ) { $t = Activator::tables(); foreach ( $this->items as $item ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Own table. $r = $wpdb->get_row( $wpdb->prepare( 'SELECT a.*, (SELECT COUNT(*) FROM %i b WHERE b.batch_id = a.batch_id AND b.reverted_at IS NULL) AS batch_size FROM %i a WHERE a.reverted_at IS NULL AND a.suggestion_id = %d LIMIT 1', $t['applied'], $t['applied'], (int) $item['id'] ), ARRAY_A ); if ( $r ) { $this->applied[ (int) $r['suggestion_id'] ] = $r; } } } $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns(), 'source' ); $this->set_pagination_args( array( 'total_items' => $total, 'per_page' => self::PER_PAGE, ) ); } /** * Empty state with a call to action. * * @return void */ public function no_items() { $status = self::filters()['status']; if ( 'pending' === $status ) { esc_html_e( 'Nothing to review. Run a scan from the LinkCortex dashboard to get link ideas, or lower the minimum relevance filter.', 'linkcortex' ); } else { esc_html_e( 'No suggestions match these filters.', 'linkcortex' ); } } /** * Checkbox column. * * @param array $item Row. * @return string */ protected function column_cb( $item ) { if ( 'applied' === $item['status'] ) { $rec = $this->applied[ (int) $item['id'] ] ?? null; if ( ! $rec ) { return ''; } return sprintf( '', (int) $item['id'], esc_html__( 'Select', 'linkcortex' ), (int) $rec['id'] ); } return sprintf( '', (int) $item['id'], esc_html__( 'Select', 'linkcortex' ) ); } /** * Action URL with nonce. * * @param string $action Action. * @param array $args Args. * @return string */ public static function action_url( $action, array $args ) { return wp_nonce_url( Menu::url( 'suggestions', array_merge( self::filters_query(), $args, array( 'ailsug_action' => $action ) ) ), 'ailsug_row_' . $action ); } /** * Current filters as query args (to keep them across actions). * * @return array */ private static function filters_query() { $f = self::filters(); return array_filter( array( 'status' => $f['status'], 'post_type_filter' => $f['post_type'], 'cat' => $f['cat'], 'orphan' => $f['orphan'] ? 1 : 0, ) ); } /** * Source column. * * @param array $item Row. * @return string */ protected function column_source( $item ) { $id = (int) $item['source_post_id']; $title = '' !== (string) $item['source_title'] ? $item['source_title'] : get_the_title( $id ); $out = '' . esc_html( $title ) . ''; if ( (int) $item['source_builder'] ) { $out .= '
' . esc_html__( 'Page builder: view only', 'linkcortex' ) . ''; } $actions = array( 'view' => '' . esc_html__( 'View page', 'linkcortex' ) . '', 'only' => '' . esc_html__( 'All for this page', 'linkcortex' ) . '', ); if ( current_user_can( (string) apply_filters( 'ailsug_run_capability', 'manage_options' ) ) ) { $actions['regen'] = ''; } return $out . $this->row_actions( $actions ); } /** * Anchor in context, with preview and inline edit. * * @param array $item Row. * @return string */ protected function column_anchor( $item ) { $ctx = Store::split_snippet( (string) $item['context_snippet'] ); $id = (int) $item['id']; $out = '

…' . esc_html( $ctx['before'] ) . '' . esc_html( '' !== $ctx['match'] ? $ctx['match'] : $item['anchor_text'] ) . '' . esc_html( $ctx['after'] ) . '…

'; $actions = array(); if ( in_array( $item['status'], array( 'pending', 'stale', 'rejected', 'reverted', 'accepted' ), true ) ) { if ( ! (int) $item['source_builder'] ) { $actions['accept'] = '' . esc_html__( 'Accept & Apply', 'linkcortex' ) . ''; } } if ( in_array( $item['status'], array( 'pending', 'accepted' ), true ) ) { $actions['reject'] = '' . esc_html__( 'Reject', 'linkcortex' ) . ''; } if ( 'applied' === $item['status'] && isset( $this->applied[ $id ] ) ) { $rec = $this->applied[ $id ]; $actions['undo'] = '' . esc_html__( 'Undo', 'linkcortex' ) . ''; if ( (int) $rec['batch_size'] > 1 ) { /* translators: %d: number of links in the batch. */ $actions['undo_batch'] = '' . esc_html( sprintf( __( 'Undo whole batch (%d links)', 'linkcortex' ), (int) $rec['batch_size'] ) ) . ''; } } if ( ! in_array( $item['status'], array( 'applied' ), true ) ) { $actions['edit'] = ''; } $actions['preview'] = ''; $out .= $this->row_actions( $actions, true ); $out .= ''; if ( 'applied' !== $item['status'] ) { $out .= sprintf( '', $id, esc_html__( 'Link text (must appear in the page exactly)', 'linkcortex' ), esc_attr( $item['anchor_text'] ), esc_html__( 'Target page (ID or address)', 'linkcortex' ), esc_attr( (string) get_permalink( (int) $item['target_post_id'] ) ), esc_html__( 'Save changes', 'linkcortex' ) ); } return $out; } /** * Target column. * * @param array $item Row. * @return string */ protected function column_target( $item ) { $id = (int) $item['target_post_id']; $title = '' !== (string) $item['target_title'] ? $item['target_title'] : get_the_title( $id ); $out = '' . esc_html( $title ) . ''; if ( '' !== (string) $item['target_keyword'] ) { /* translators: %s: keyword. */ $out .= '
' . esc_html( sprintf( __( 'Keyword: %s', 'linkcortex' ), $item['target_keyword'] ) ) . ''; } if ( null !== $item['target_inbound'] && 0 === (int) $item['target_inbound'] ) { $out .= '
' . esc_html__( 'Orphan page', 'linkcortex' ) . ''; } return $out; } /** * Score badge with text. * * @param array $item Row. * @return string */ protected function column_score( $item ) { $score = (int) $item['score']; $label = $score >= 80 ? __( 'strong', 'linkcortex' ) : ( $score >= 60 ? __( 'good', 'linkcortex' ) : __( 'weak', 'linkcortex' ) ); $class = $score >= 80 ? 'is-strong' : ( $score >= 60 ? 'is-good' : 'is-weak' ); return '' . esc_html( $score . ' · ' . $label ) . ''; } /** * Reason. * * @param array $item Row. * @return string */ protected function column_reason( $item ) { return esc_html( (string) $item['reason'] ); } /** * Status (with applied details). * * @param array $item Row. * @return string */ protected function column_status( $item ) { $labels = array( 'pending' => __( 'To review', 'linkcortex' ), 'accepted' => __( 'Accepted', 'linkcortex' ), 'applied' => __( 'Applied', 'linkcortex' ), 'rejected' => __( 'Rejected', 'linkcortex' ), 'stale' => __( 'Out of date', 'linkcortex' ), 'reverted' => __( 'Undone', 'linkcortex' ), ); $out = '' . esc_html( $labels[ $item['status'] ] ?? $item['status'] ) . ''; if ( 'applied' === $item['status'] && isset( $this->applied[ (int) $item['id'] ] ) ) { $rec = $this->applied[ (int) $item['id'] ]; $user = get_userdata( (int) $rec['applied_by'] ); /* translators: 1: date, 2: user name. */ $out .= '
' . esc_html( sprintf( __( '%1$s by %2$s', 'linkcortex' ), get_date_from_gmt( $rec['applied_at'], get_option( 'date_format' ) ), $user ? $user->display_name : '—' ) ) . ''; if ( (int) $rec['revision_id'] && get_post( (int) $rec['revision_id'] ) ) { $out .= '
' . esc_html__( 'Compare revisions', 'linkcortex' ) . ''; } } if ( 'stale' === $item['status'] ) { $out .= '
' . esc_html__( 'A page changed since this idea was made. Re-generate, or edit the link text.', 'linkcortex' ) . ''; } return $out; } }