class-bulk-editor-list-table.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\Admin\Bulk Editor
  6. * @since 1.5.0
  7. */
  8. /**
  9. * Implements table for bulk editing.
  10. */
  11. class WPSEO_Bulk_List_Table extends WP_List_Table {
  12. /**
  13. * The nonce that was passed with the request
  14. *
  15. * @var string
  16. */
  17. private $nonce;
  18. /**
  19. * Array of post types for which the current user has `edit_others_posts` capabilities.
  20. *
  21. * @var array
  22. */
  23. private $all_posts;
  24. /**
  25. * Array of post types for which the current user has `edit_posts` capabilities, but not `edit_others_posts`.
  26. *
  27. * @var array
  28. */
  29. private $own_posts;
  30. /**
  31. * Saves all the metadata into this array.
  32. *
  33. * @var array
  34. */
  35. protected $meta_data = array();
  36. /**
  37. * The current requested page_url
  38. *
  39. * @var string
  40. */
  41. private $request_url;
  42. /**
  43. * The current page (depending on $_GET['paged']) if current tab is for current page_type, else it will be 1
  44. *
  45. * @var integer
  46. */
  47. private $current_page;
  48. /**
  49. * The current post filter, if is used (depending on $_GET['post_type_filter'])
  50. *
  51. * @var string
  52. */
  53. private $current_filter;
  54. /**
  55. * The current post status, if is used (depending on $_GET['post_status'])
  56. *
  57. * @var string
  58. */
  59. private $current_status;
  60. /**
  61. * The current sorting, if used (depending on $_GET['order'] and $_GET['orderby'])
  62. *
  63. * @var string
  64. */
  65. private $current_order;
  66. /**
  67. * The page_type for current class instance (for example: title / description).
  68. *
  69. * @var string
  70. */
  71. protected $page_type;
  72. /**
  73. * Based on the page_type ($this->page_type) there will be constructed an url part, for subpages and
  74. * navigation
  75. *
  76. * @var string
  77. */
  78. protected $page_url;
  79. /**
  80. * The settings which will be used in the __construct.
  81. *
  82. * @var array
  83. */
  84. protected $settings;
  85. /**
  86. * @var array
  87. */
  88. protected $pagination = array();
  89. /**
  90. * Class constructor
  91. */
  92. public function __construct() {
  93. parent::__construct( $this->settings );
  94. $this->request_url = $_SERVER['REQUEST_URI'];
  95. $this->current_page = ( ! empty( $_GET['paged'] ) ) ? $_GET['paged'] : 1;
  96. $this->current_filter = ( ! empty( $_GET['post_type_filter'] ) ) ? $_GET['post_type_filter'] : 1;
  97. $this->current_status = ( ! empty( $_GET['post_status'] ) ) ? $_GET['post_status'] : 1;
  98. $this->current_order = array(
  99. 'order' => ( ! empty( $_GET['order'] ) ) ? $_GET['order'] : 'asc',
  100. 'orderby' => ( ! empty( $_GET['orderby'] ) ) ? $_GET['orderby'] : 'post_title',
  101. );
  102. $this->verify_nonce();
  103. $this->nonce = wp_create_nonce( 'bulk-editor-table' );
  104. $this->page_url = "&nonce={$this->nonce}&type={$this->page_type}#top#{$this->page_type}";
  105. $this->populate_editable_post_types();
  106. }
  107. /**
  108. * Verifies nonce if additional parameters have been sent.
  109. *
  110. * Shows an error notification if the nonce check fails.
  111. */
  112. private function verify_nonce() {
  113. if ( $this->should_verify_nonce() && ! wp_verify_nonce( filter_input( INPUT_GET, 'nonce' ), 'bulk-editor-table' ) ) {
  114. Yoast_Notification_Center::get()->add_notification(
  115. new Yoast_Notification(
  116. __( 'You are not allowed to access this page.', 'wordpress-seo' ),
  117. array( 'type' => Yoast_Notification::ERROR )
  118. )
  119. );
  120. Yoast_Notification_Center::get()->display_notifications();
  121. die;
  122. }
  123. }
  124. /**
  125. * Checks if additional parameters have been sent to determine if nonce should be checked or not.
  126. *
  127. * @return bool
  128. */
  129. private function should_verify_nonce() {
  130. $possible_params = array(
  131. 'type',
  132. 'paged',
  133. 'post_type_filter',
  134. 'post_status',
  135. 'order',
  136. 'orderby',
  137. );
  138. foreach ( $possible_params as $param_name ) {
  139. if ( filter_input( INPUT_GET, $param_name ) ) {
  140. return true;
  141. }
  142. }
  143. }
  144. /**
  145. * Prepares the data and renders the page.
  146. */
  147. public function show_page() {
  148. $this->prepare_page_navigation();
  149. $this->prepare_items();
  150. $this->views();
  151. $this->display();
  152. }
  153. /**
  154. * Used in the constructor to build a reference list of post types the current user can edit.
  155. */
  156. protected function populate_editable_post_types() {
  157. $post_types = get_post_types(
  158. array(
  159. 'public' => true,
  160. 'exclude_from_search' => false,
  161. ),
  162. 'object'
  163. );
  164. $this->all_posts = array();
  165. $this->own_posts = array();
  166. if ( is_array( $post_types ) && $post_types !== array() ) {
  167. foreach ( $post_types as $post_type ) {
  168. if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
  169. continue;
  170. }
  171. if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
  172. $this->all_posts[] = esc_sql( $post_type->name );
  173. }
  174. else {
  175. $this->own_posts[] = esc_sql( $post_type->name );
  176. }
  177. }
  178. }
  179. }
  180. /**
  181. * Will shown the navigation for the table like pagenavigation and pagefilter;
  182. *
  183. * @param string $which Table nav location (such as top).
  184. */
  185. public function display_tablenav( $which ) {
  186. $post_status = sanitize_text_field( filter_input( INPUT_GET, 'post_status' ) );
  187. ?>
  188. <div class="tablenav <?php echo esc_attr( $which ); ?>">
  189. <?php if ( 'top' === $which ) { ?>
  190. <form id="posts-filter" action="" method="get">
  191. <input type="hidden" name="nonce" value="<?php echo esc_attr( $this->nonce ); ?>"/>
  192. <input type="hidden" name="page" value="wpseo_tools"/>
  193. <input type="hidden" name="tool" value="bulk-editor"/>
  194. <input type="hidden" name="type" value="<?php echo esc_attr( $this->page_type ); ?>"/>
  195. <input type="hidden" name="orderby"
  196. value="<?php echo esc_attr( filter_input( INPUT_GET, 'orderby' ) ); ?>"/>
  197. <input type="hidden" name="order"
  198. value="<?php echo esc_attr( filter_input( INPUT_GET, 'order' ) ); ?>"/>
  199. <input type="hidden" name="post_type_filter"
  200. value="<?php echo esc_attr( filter_input( INPUT_GET, 'post_type_filter' ) ); ?>"/>
  201. <?php if ( ! empty( $post_status ) ) { ?>
  202. <input type="hidden" name="post_status" value="<?php echo esc_attr( $post_status ); ?>"/>
  203. <?php } ?>
  204. <?php } ?>
  205. <?php
  206. $this->extra_tablenav( $which );
  207. $this->pagination( $which );
  208. ?>
  209. <br class="clear"/>
  210. <?php if ( 'top' === $which ) { ?>
  211. </form>
  212. <?php } ?>
  213. </div>
  214. <?php
  215. }
  216. /**
  217. * This function builds the base sql subquery used in this class.
  218. *
  219. * This function takes into account the post types in which the current user can
  220. * edit all posts, and the ones the current user can only edit his/her own.
  221. *
  222. * @return string $subquery The subquery, which should always be used in $wpdb->prepare(), passing the current user_id in as the first parameter.
  223. */
  224. public function get_base_subquery() {
  225. global $wpdb;
  226. $all_posts_string = "'" . implode( "', '", $this->all_posts ) . "'";
  227. $own_posts_string = "'" . implode( "', '", $this->own_posts ) . "'";
  228. $post_author = esc_sql( (int) get_current_user_id() );
  229. $subquery = "(
  230. SELECT *
  231. FROM {$wpdb->posts}
  232. WHERE post_type IN ({$all_posts_string})
  233. UNION ALL
  234. SELECT *
  235. FROM {$wpdb->posts}
  236. WHERE post_type IN ({$own_posts_string}) AND post_author = {$post_author}
  237. ) sub_base";
  238. return $subquery;
  239. }
  240. /**
  241. * @return array
  242. */
  243. public function get_views() {
  244. global $wpdb;
  245. $status_links = array();
  246. $states = get_post_stati( array( 'show_in_admin_all_list' => true ) );
  247. $states = esc_sql( $states );
  248. $all_states = "'" . implode( "', '", $states ) . "'";
  249. $subquery = $this->get_base_subquery();
  250. $total_posts = $wpdb->get_var(
  251. "
  252. SELECT COUNT(ID) FROM {$subquery}
  253. WHERE post_status IN ({$all_states})
  254. "
  255. );
  256. $post_status = filter_input( INPUT_GET, 'post_status' );
  257. $class = empty( $post_status ) ? ' class="current"' : '';
  258. $localized_text = sprintf(
  259. /* translators: %s expands to the number of posts in localized format. */
  260. _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts', 'wordpress-seo' ),
  261. number_format_i18n( $total_posts )
  262. );
  263. $status_links['all'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) . '"' . $class . '>' . $localized_text . '</a>';
  264. $post_stati = get_post_stati( array( 'show_in_admin_all_list' => true ), 'objects' );
  265. if ( is_array( $post_stati ) && $post_stati !== array() ) {
  266. foreach ( $post_stati as $status ) {
  267. $status_name = esc_sql( $status->name );
  268. $total = (int) $wpdb->get_var(
  269. $wpdb->prepare(
  270. "
  271. SELECT COUNT(ID) FROM {$subquery}
  272. WHERE post_status = %s
  273. ",
  274. $status_name
  275. )
  276. );
  277. if ( $total === 0 ) {
  278. continue;
  279. }
  280. $class = '';
  281. if ( $status_name === $post_status ) {
  282. $class = ' class="current"';
  283. }
  284. $status_links[ $status_name ] = '<a href="' . esc_url( add_query_arg( array( 'post_status' => $status_name ), admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) ) . '"' . $class . '>' . sprintf( translate_nooped_plural( $status->label_count, $total ), number_format_i18n( $total ) ) . '</a>';
  285. }
  286. }
  287. unset( $post_stati, $status, $status_name, $total, $class );
  288. $trashed_posts = $wpdb->get_var(
  289. "
  290. SELECT COUNT(ID) FROM {$subquery}
  291. WHERE post_status IN ('trash')
  292. "
  293. );
  294. $class = '';
  295. if ( 'trash' === $post_status ) {
  296. $class = 'class="current"';
  297. }
  298. $localized_text = sprintf(
  299. /* translators: %s expands to the number of trashed posts in localized format. */
  300. _nx( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts', 'wordpress-seo' ),
  301. number_format_i18n( $trashed_posts )
  302. );
  303. $status_links['trash'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor&post_status=trash' . $this->page_url ) ) . '"' . $class . '>' . $localized_text . '</a>';
  304. return $status_links;
  305. }
  306. /**
  307. * @param string $which Table nav location (such as top).
  308. */
  309. public function extra_tablenav( $which ) {
  310. if ( 'top' === $which ) {
  311. $post_types = get_post_types(
  312. array(
  313. 'public' => true,
  314. 'exclude_from_search' => false,
  315. )
  316. );
  317. $instance_type = esc_attr( $this->page_type );
  318. if ( is_array( $post_types ) && $post_types !== array() ) {
  319. global $wpdb;
  320. echo '<div class="alignleft actions">';
  321. $post_types = esc_sql( $post_types );
  322. $post_types = "'" . implode( "', '", $post_types ) . "'";
  323. $states = get_post_stati( array( 'show_in_admin_all_list' => true ) );
  324. $states['trash'] = 'trash';
  325. $states = esc_sql( $states );
  326. $all_states = "'" . implode( "', '", $states ) . "'";
  327. $subquery = $this->get_base_subquery();
  328. $post_types = $wpdb->get_results(
  329. "
  330. SELECT DISTINCT post_type FROM {$subquery}
  331. WHERE post_status IN ({$all_states})
  332. ORDER BY 'post_type' ASC
  333. "
  334. );
  335. $post_type_filter = filter_input( INPUT_GET, 'post_type_filter' );
  336. $selected = ( ! empty( $post_type_filter ) ) ? sanitize_text_field( $post_type_filter ) : '-1';
  337. $options = '<option value="-1">' . esc_html__( 'Show All Content Types', 'wordpress-seo' ) . '</option>';
  338. if ( is_array( $post_types ) && $post_types !== array() ) {
  339. foreach ( $post_types as $post_type ) {
  340. $obj = get_post_type_object( $post_type->post_type );
  341. $options .= sprintf(
  342. '<option value="%2$s" %3$s>%1$s</option>',
  343. esc_html( $obj->labels->name ),
  344. esc_attr( $post_type->post_type ),
  345. selected( $selected, $post_type->post_type, false )
  346. );
  347. }
  348. }
  349. printf(
  350. '<label for="%1$s" class="screen-reader-text">%2$s</label>',
  351. esc_attr( 'post-type-filter-' . $instance_type ),
  352. esc_html__( 'Filter by content type', 'wordpress-seo' )
  353. );
  354. printf(
  355. '<select name="post_type_filter" id="%2$s">%1$s</select>',
  356. $options,
  357. esc_attr( 'post-type-filter-' . $instance_type )
  358. );
  359. submit_button( __( 'Filter', 'wordpress-seo' ), 'button', false, false, array( 'id' => 'post-query-submit' ) );
  360. echo '</div>';
  361. }
  362. }
  363. }
  364. /**
  365. *
  366. * @return array
  367. */
  368. public function get_sortable_columns() {
  369. return array(
  370. 'col_page_title' => array( 'post_title', true ),
  371. 'col_post_type' => array( 'post_type', false ),
  372. 'col_post_date' => array( 'post_date', false ),
  373. );
  374. }
  375. /**
  376. * Sets the correct pagenumber and pageurl for the navigation
  377. */
  378. public function prepare_page_navigation() {
  379. $request_url = $this->request_url . $this->page_url;
  380. $current_page = $this->current_page;
  381. $current_filter = $this->current_filter;
  382. $current_status = $this->current_status;
  383. $current_order = $this->current_order;
  384. // If current type doesn't compare with objects page_type, than we have to unset some vars in the requested url (which will be use for internal table urls).
  385. if ( $_GET['type'] !== $this->page_type ) {
  386. $request_url = remove_query_arg( 'paged', $request_url ); // Page will be set with value 1 below.
  387. $request_url = remove_query_arg( 'post_type_filter', $request_url );
  388. $request_url = remove_query_arg( 'post_status', $request_url );
  389. $request_url = remove_query_arg( 'orderby', $request_url );
  390. $request_url = remove_query_arg( 'order', $request_url );
  391. $request_url = add_query_arg( 'pages', 1, $request_url );
  392. $current_page = 1;
  393. $current_filter = '-1';
  394. $current_status = '';
  395. $current_order = array(
  396. 'orderby' => 'post_title',
  397. 'order' => 'asc',
  398. );
  399. }
  400. $_SERVER['REQUEST_URI'] = $request_url;
  401. $_GET['paged'] = $current_page;
  402. $_REQUEST['paged'] = $current_page;
  403. $_REQUEST['post_type_filter'] = $current_filter;
  404. $_GET['post_type_filter'] = $current_filter;
  405. $_GET['post_status'] = $current_status;
  406. $_GET['orderby'] = $current_order['orderby'];
  407. $_GET['order'] = $current_order['order'];
  408. }
  409. /**
  410. * Preparing the requested pagerows and setting the needed variables
  411. */
  412. public function prepare_items() {
  413. $post_type_clause = $this->get_post_type_clause();
  414. $all_states = $this->get_all_states();
  415. $subquery = $this->get_base_subquery();
  416. // Setting the column headers.
  417. $this->set_column_headers();
  418. // Count the total number of needed items and setting pagination given $total_items.
  419. $total_items = $this->count_items( $subquery, $all_states, $post_type_clause );
  420. $this->set_pagination( $total_items );
  421. // Getting items given $query.
  422. $query = $this->parse_item_query( $subquery, $all_states, $post_type_clause );
  423. $this->get_items( $query );
  424. // Get the metadata for the current items ($this->items).
  425. $this->get_meta_data();
  426. }
  427. /**
  428. * Getting the columns for first row
  429. *
  430. * @return array
  431. */
  432. public function get_columns() {
  433. return $this->merge_columns();
  434. }
  435. /**
  436. * Setting the column headers
  437. */
  438. protected function set_column_headers() {
  439. $columns = $this->get_columns();
  440. $hidden = array();
  441. $sortable = $this->get_sortable_columns();
  442. $this->_column_headers = array( $columns, $hidden, $sortable );
  443. }
  444. /**
  445. * Counting total items
  446. *
  447. * @param string $subquery SQL FROM part.
  448. * @param string $all_states SQL IN part.
  449. * @param string $post_type_clause SQL post type part.
  450. *
  451. * @return mixed
  452. */
  453. protected function count_items( $subquery, $all_states, $post_type_clause ) {
  454. global $wpdb;
  455. $total_items = $wpdb->get_var(
  456. "
  457. SELECT COUNT(ID)
  458. FROM {$subquery}
  459. WHERE post_status IN ({$all_states}) $post_type_clause
  460. "
  461. );
  462. return $total_items;
  463. }
  464. /**
  465. * Getting the post_type_clause filter
  466. *
  467. * @return string
  468. */
  469. protected function get_post_type_clause() {
  470. // Filter Block.
  471. $post_types = null;
  472. $post_type_clause = '';
  473. $post_type_filter = filter_input( INPUT_GET, 'post_type_filter' );
  474. if ( ! empty( $post_type_filter ) && get_post_type_object( sanitize_text_field( $post_type_filter ) ) ) {
  475. $post_types = esc_sql( sanitize_text_field( $post_type_filter ) );
  476. $post_type_clause = "AND post_type IN ('{$post_types}')";
  477. }
  478. return $post_type_clause;
  479. }
  480. /**
  481. * Setting the pagination.
  482. *
  483. * Total items is the number of all visible items.
  484. *
  485. * @param int $total_items Total items counts.
  486. */
  487. protected function set_pagination( $total_items ) {
  488. // Calculate items per page.
  489. $per_page = $this->get_items_per_page( 'wpseo_posts_per_page', 10 );
  490. $paged = esc_sql( sanitize_text_field( filter_input( INPUT_GET, 'paged' ) ) );
  491. if ( empty( $paged ) || ! is_numeric( $paged ) || $paged <= 0 ) {
  492. $paged = 1;
  493. }
  494. $this->set_pagination_args(
  495. array(
  496. 'total_items' => $total_items,
  497. 'total_pages' => ceil( $total_items / $per_page ),
  498. 'per_page' => $per_page,
  499. )
  500. );
  501. $this->pagination = array(
  502. 'per_page' => $per_page,
  503. 'offset' => ( $paged - 1 ) * $per_page,
  504. );
  505. }
  506. /**
  507. * Parse the query to get items from database.
  508. *
  509. * Based on given parameters there will be parse a query which will get all the pages/posts and other post_types
  510. * from the database.
  511. *
  512. * @param string $subquery SQL FROM part.
  513. * @param string $all_states SQL IN part.
  514. * @param string $post_type_clause SQL post type part.
  515. *
  516. * @return string
  517. */
  518. protected function parse_item_query( $subquery, $all_states, $post_type_clause ) {
  519. // Order By block.
  520. $orderby = filter_input( INPUT_GET, 'orderby' );
  521. $orderby = ! empty( $orderby ) ? esc_sql( sanitize_text_field( $orderby ) ) : 'post_title';
  522. $orderby = $this->sanitize_orderby( $orderby );
  523. // Order clause.
  524. $order = filter_input( INPUT_GET, 'order' );
  525. $order = ! empty( $order ) ? esc_sql( strtoupper( sanitize_text_field( $order ) ) ) : 'ASC';
  526. $order = $this->sanitize_order( $order );
  527. // Get all needed results.
  528. $query = "
  529. SELECT ID, post_title, post_type, post_status, post_modified, post_date
  530. FROM {$subquery}
  531. WHERE post_status IN ({$all_states}) $post_type_clause
  532. ORDER BY {$orderby} {$order}
  533. LIMIT %d,%d
  534. ";
  535. return $query;
  536. }
  537. /**
  538. * Heavily restricts the possible columns by which a user can order the table in the bulk editor, thereby preventing a possible CSRF vulnerability.
  539. *
  540. * @param string $orderby The column by which we want to order.
  541. *
  542. * @return string $orderby
  543. */
  544. protected function sanitize_orderby( $orderby ) {
  545. $valid_column_names = array(
  546. 'post_title',
  547. 'post_type',
  548. 'post_date',
  549. );
  550. if ( in_array( $orderby, $valid_column_names, true ) ) {
  551. return $orderby;
  552. }
  553. return 'post_title';
  554. }
  555. /**
  556. * Makes sure the order clause is always ASC or DESC for the bulk editor table, thereby preventing a possible CSRF vulnerability.
  557. *
  558. * @param string $order Whether we want to sort ascending or descending.
  559. *
  560. * @return string $order SQL order string (ASC, DESC).
  561. */
  562. protected function sanitize_order( $order ) {
  563. if ( in_array( strtoupper( $order ), array( 'ASC', 'DESC' ), true ) ) {
  564. return $order;
  565. }
  566. return 'ASC';
  567. }
  568. /**
  569. * Getting all the items.
  570. *
  571. * @param string $query SQL query to use.
  572. */
  573. protected function get_items( $query ) {
  574. global $wpdb;
  575. $this->items = $wpdb->get_results(
  576. $wpdb->prepare(
  577. $query,
  578. $this->pagination['offset'],
  579. $this->pagination['per_page']
  580. )
  581. );
  582. }
  583. /**
  584. * Getting all the states.
  585. *
  586. * @return string
  587. */
  588. protected function get_all_states() {
  589. $states = get_post_stati( array( 'show_in_admin_all_list' => true ) );
  590. $states['trash'] = 'trash';
  591. if ( ! empty( $_GET['post_status'] ) ) {
  592. $requested_state = sanitize_text_field( $_GET['post_status'] );
  593. if ( in_array( $requested_state, $states, true ) ) {
  594. $states = array( $requested_state );
  595. }
  596. if ( $requested_state !== 'trash' ) {
  597. unset( $states['trash'] );
  598. }
  599. }
  600. $states = esc_sql( $states );
  601. $all_states = "'" . implode( "', '", $states ) . "'";
  602. return $all_states;
  603. }
  604. /**
  605. * Based on $this->items and the defined columns, the table rows will be displayed.
  606. */
  607. public function display_rows() {
  608. $records = $this->items;
  609. list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
  610. if ( ( is_array( $records ) && $records !== array() ) && ( is_array( $columns ) && $columns !== array() ) ) {
  611. foreach ( $records as $rec ) {
  612. echo '<tr id="', esc_attr( 'record_' . $rec->ID ), '">';
  613. foreach ( $columns as $column_name => $column_display_name ) {
  614. $classes = '';
  615. if ( $primary === $column_name ) {
  616. $classes .= ' has-row-actions column-primary';
  617. }
  618. $attributes = $this->column_attributes( $column_name, $hidden, $classes, $column_display_name );
  619. $column_value = $this->parse_column( $column_name, $rec );
  620. if ( method_exists( $this, 'parse_page_specific_column' ) && empty( $column_value ) ) {
  621. $column_value = $this->parse_page_specific_column( $column_name, $rec, $attributes );
  622. }
  623. if ( ! empty( $column_value ) ) {
  624. printf( '<td %2$s>%1$s</td>', $column_value, $attributes );
  625. }
  626. }
  627. echo '</tr>';
  628. }
  629. }
  630. }
  631. /**
  632. * Getting the attributes for each table cell.
  633. *
  634. * @param string $column_name Column name string.
  635. * @param array $hidden Set of hidden columns.
  636. * @param string $classes Additional CSS classes.
  637. * @param string $column_display_name Column display name string.
  638. *
  639. * @return string
  640. */
  641. protected function column_attributes( $column_name, $hidden, $classes, $column_display_name ) {
  642. $attributes = '';
  643. $class = array( $column_name, "column-$column_name$classes" );
  644. if ( in_array( $column_name, $hidden, true ) ) {
  645. $class[] = 'hidden';
  646. }
  647. if ( ! empty( $class ) ) {
  648. $attributes = 'class="' . implode( ' ', $class ) . '"';
  649. }
  650. $attributes .= ' data-colname="' . esc_attr( $column_display_name ) . '"';
  651. return $attributes;
  652. }
  653. /**
  654. * Parsing the title.
  655. *
  656. * @param WP_Post $rec Post object.
  657. *
  658. * @return string
  659. */
  660. protected function parse_page_title_column( $rec ) {
  661. $title = empty( $rec->post_title ) ? __( '(no title)', 'wordpress-seo' ) : $rec->post_title;
  662. $return = sprintf( '<strong>%1$s</strong>', stripslashes( wp_strip_all_tags( $title ) ) );
  663. $post_type_object = get_post_type_object( $rec->post_type );
  664. $can_edit_post = current_user_can( $post_type_object->cap->edit_post, $rec->ID );
  665. $actions = array();
  666. if ( $can_edit_post && 'trash' !== $rec->post_status ) {
  667. $actions['edit'] = sprintf(
  668. '<a href="%s" aria-label="%s">%s</a>',
  669. esc_url( get_edit_post_link( $rec->ID, true ) ),
  670. /* translators: %s: post title */
  671. esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
  672. __( 'Edit', 'wordpress-seo' )
  673. );
  674. }
  675. if ( $post_type_object->public ) {
  676. if ( in_array( $rec->post_status, array( 'pending', 'draft', 'future' ), true ) ) {
  677. if ( $can_edit_post ) {
  678. $actions['view'] = sprintf(
  679. '<a href="%s" aria-label="%s">%s</a>',
  680. esc_url( add_query_arg( 'preview', 'true', get_permalink( $rec->ID ) ) ),
  681. /* translators: %s: post title */
  682. esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
  683. __( 'Preview', 'wordpress-seo' )
  684. );
  685. }
  686. }
  687. elseif ( 'trash' !== $rec->post_status ) {
  688. $actions['view'] = sprintf(
  689. '<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
  690. esc_url( get_permalink( $rec->ID ) ),
  691. /* translators: %s: post title */
  692. esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
  693. __( 'View', 'wordpress-seo' )
  694. );
  695. }
  696. }
  697. $return .= $this->row_actions( $actions );
  698. return $return;
  699. }
  700. /**
  701. * Parsing the column based on the $column_name.
  702. *
  703. * @param string $column_name Column name.
  704. * @param WP_Post $rec Post object.
  705. *
  706. * @return string
  707. */
  708. protected function parse_column( $column_name, $rec ) {
  709. static $date_format;
  710. if ( ! isset( $date_format ) ) {
  711. $date_format = get_option( 'date_format' );
  712. }
  713. switch ( $column_name ) {
  714. case 'col_page_title':
  715. $column_value = $this->parse_page_title_column( $rec );
  716. break;
  717. case 'col_page_slug':
  718. $permalink = get_permalink( $rec->ID );
  719. $display_slug = str_replace( get_bloginfo( 'url' ), '', $permalink );
  720. $column_value = sprintf( '<a href="%2$s" target="_blank">%1$s</a>', stripslashes( rawurldecode( $display_slug ) ), esc_url( $permalink ) );
  721. break;
  722. case 'col_post_type':
  723. $post_type = get_post_type_object( $rec->post_type );
  724. $column_value = $post_type->labels->singular_name;
  725. break;
  726. case 'col_post_status':
  727. $post_status = get_post_status_object( $rec->post_status );
  728. $column_value = $post_status->label;
  729. break;
  730. case 'col_post_date':
  731. $column_value = date_i18n( $date_format, strtotime( $rec->post_date ) );
  732. break;
  733. case 'col_row_action':
  734. $column_value = sprintf(
  735. '<a href="#" role="button" class="wpseo-save" data-id="%1$s">%2$s</a> <span aria-hidden="true">|</span> <a href="#" role="button" class="wpseo-save-all">%3$s</a>',
  736. $rec->ID,
  737. esc_html__( 'Save', 'wordpress-seo' ),
  738. esc_html__( 'Save all', 'wordpress-seo' )
  739. );
  740. break;
  741. }
  742. if ( ! empty( $column_value ) ) {
  743. return $column_value;
  744. }
  745. }
  746. /**
  747. * Parse the field where the existing meta-data value is displayed.
  748. *
  749. * @param integer $record_id Record ID.
  750. * @param string $attributes HTML attributes.
  751. * @param bool|array $values Optional values data array.
  752. *
  753. * @return string
  754. */
  755. protected function parse_meta_data_field( $record_id, $attributes, $values = false ) {
  756. // Fill meta data if exists in $this->meta_data.
  757. $meta_data = ( ! empty( $this->meta_data[ $record_id ] ) ) ? $this->meta_data[ $record_id ] : array();
  758. $meta_key = WPSEO_Meta::$meta_prefix . $this->target_db_field;
  759. $meta_value = ( ! empty( $meta_data[ $meta_key ] ) ) ? $meta_data[ $meta_key ] : '';
  760. if ( ! empty( $values ) ) {
  761. $meta_value = $values[ $meta_value ];
  762. }
  763. return sprintf( '<td %2$s id="wpseo-existing-%4$s-%3$s">%1$s</td>', $meta_value, $attributes, $record_id, $this->target_db_field );
  764. }
  765. /**
  766. * Method for setting the meta data, which belongs to the records that will be shown on the current page.
  767. *
  768. * This method will loop through the current items ($this->items) for getting the post_id. With this data
  769. * ($needed_ids) the method will query the meta-data table for getting the title.
  770. */
  771. protected function get_meta_data() {
  772. $post_ids = $this->get_post_ids();
  773. $meta_data = $this->get_meta_data_result( $post_ids );
  774. $this->parse_meta_data( $meta_data );
  775. // Little housekeeping.
  776. unset( $post_ids, $meta_data );
  777. }
  778. /**
  779. * Getting all post_ids from to $this->items.
  780. *
  781. * @return string
  782. */
  783. protected function get_post_ids() {
  784. $needed_ids = array();
  785. foreach ( $this->items as $item ) {
  786. $needed_ids[] = $item->ID;
  787. }
  788. $post_ids = "'" . implode( "', '", $needed_ids ) . "'";
  789. return $post_ids;
  790. }
  791. /**
  792. * Getting the meta_data from database.
  793. *
  794. * @param string $post_ids Post IDs string for SQL IN part.
  795. *
  796. * @return mixed
  797. */
  798. protected function get_meta_data_result( $post_ids ) {
  799. global $wpdb;
  800. $meta_data = $wpdb->get_results(
  801. "
  802. SELECT *
  803. FROM {$wpdb->postmeta}
  804. WHERE post_id IN({$post_ids}) && meta_key = '" . WPSEO_Meta::$meta_prefix . $this->target_db_field . "'
  805. "
  806. );
  807. return $meta_data;
  808. }
  809. /**
  810. * Setting $this->meta_data.
  811. *
  812. * @param array $meta_data Meta data set.
  813. */
  814. protected function parse_meta_data( $meta_data ) {
  815. foreach ( $meta_data as $row ) {
  816. $this->meta_data[ $row->post_id ][ $row->meta_key ] = $row->meta_value;
  817. }
  818. }
  819. /**
  820. * This method will merge general array with given parameter $columns.
  821. *
  822. * @param array $columns Optional columns set.
  823. *
  824. * @return array
  825. */
  826. protected function merge_columns( $columns = array() ) {
  827. $columns = array_merge(
  828. array(
  829. 'col_page_title' => __( 'WP Page Title', 'wordpress-seo' ),
  830. 'col_post_type' => __( 'Content Type', 'wordpress-seo' ),
  831. 'col_post_status' => __( 'Post Status', 'wordpress-seo' ),
  832. 'col_post_date' => __( 'Publication date', 'wordpress-seo' ),
  833. 'col_page_slug' => __( 'Page URL/Slug', 'wordpress-seo' ),
  834. ),
  835. $columns
  836. );
  837. $columns['col_row_action'] = __( 'Action', 'wordpress-seo' );
  838. return $columns;
  839. }
  840. } /* End of class */