$headers, 'cols' => $cols, ]; } public static function count_published_posts(array $settings): int { $q = new WP_Query(self::export_query($settings, [ 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ])); return (int)$q->found_posts; } /** * @param array $settings leexport_get_settings() */ private static function export_query(array $settings, array $overrides = []): array { return leexport_build_export_query_args($settings, $overrides); } /** * @param string[] $cols * @return string[] */ private static function build_post_row(WP_Post $post, array $cols, string $date_format, array $settings = []): array { $cats = []; $cat_urls = []; $tags = []; $tag_urls = []; $topics = []; $topic_urls = []; $topic_taxonomy = leexport_resolve_topic_taxonomy($settings); if (in_array('categories', $cols, true) || in_array('category_urls', $cols, true)) { $terms = get_the_terms($post, 'category'); if (is_array($terms)) { foreach ($terms as $t) { $cats[] = $t->name; $link = get_term_link($t); $cat_urls[] = is_wp_error($link) ? '' : (string)$link; } } } if (in_array('tags', $cols, true) || in_array('tag_urls', $cols, true)) { $terms = get_the_terms($post, 'post_tag'); if (is_array($terms)) { foreach ($terms as $t) { $tags[] = $t->name; $link = get_term_link($t); $tag_urls[] = is_wp_error($link) ? '' : (string)$link; } } } if ($topic_taxonomy !== '' && (in_array('topics', $cols, true) || in_array('topic_urls', $cols, true))) { $terms = get_the_terms($post, $topic_taxonomy); if (is_array($terms)) { foreach ($terms as $t) { $topics[] = $t->name; $link = get_term_link($t); $topic_urls[] = is_wp_error($link) ? '' : (string)$link; } } } $row = []; foreach ($cols as $col) { switch ($col) { case 'id': $row[] = (string)$post->ID; break; case 'title': $row[] = html_entity_decode(get_the_title($post), ENT_QUOTES, get_bloginfo('charset') ?: 'UTF-8'); break; case 'url': $row[] = (string)get_permalink($post); break; case 'categories': $row[] = implode(' | ', array_filter($cats, static fn($v) => $v !== '')); break; case 'category_urls': $row[] = implode(' | ', array_filter($cat_urls, static fn($v) => $v !== '')); break; case 'tags': $row[] = implode(' | ', array_filter($tags, static fn($v) => $v !== '')); break; case 'tag_urls': $row[] = implode(' | ', array_filter($tag_urls, static fn($v) => $v !== '')); break; case 'topics': $row[] = implode(' | ', array_filter($topics, static fn($v) => $v !== '')); break; case 'topic_urls': $row[] = implode(' | ', array_filter($topic_urls, static fn($v) => $v !== '')); break; case 'published_date': $row[] = (string)get_the_date($date_format, $post); break; default: $row[] = ''; break; } } return $row; } /** * @param array $settings leexport_get_settings() */ public static function output_posts_csv_download(array $settings, string $basename): void { $split_rows = leexport_split_rows_limit($settings); $total = self::count_published_posts($settings); if ($split_rows > 0 && $total > $split_rows) { self::output_posts_csv_split_zip($settings, $basename, $split_rows); return; } self::stream_posts_csv($settings, $basename . '.csv', 'php://output'); } private static function output_posts_csv_split_zip(array $settings, string $basename, int $split_rows): void { if (!class_exists('ZipArchive')) { wp_die( esc_html__('Split export requires the PHP Zip extension. Install php-zip, or set “Rows per file” to 0 for a single-file export.', 'leexport'), esc_html__('Export failed', 'leexport'), ['response' => 500] ); } $date_format = (string)get_option('date_format', 'Y-m-d'); $schema = self::get_export_schema($settings); $headers = $schema['headers']; $cols = $schema['cols']; $parts = self::write_posts_csv_parts($settings, $cols, $date_format, $headers, $split_rows, $basename, 'csv'); self::deliver_zip_parts($parts, $basename); } /** * Stream CSV to path or php://output. * * @param string[] $post_types */ private static function stream_posts_csv(array $settings, string $download_name, string $target): void { if ($target === 'php://output' && headers_sent()) { return; } $date_format = (string)get_option('date_format', 'Y-m-d'); $schema = self::get_export_schema($settings); $headers = $schema['headers']; $cols = $schema['cols']; if ($target === 'php://output') { nocache_headers(); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $download_name . '"'); header('Pragma: no-cache'); header('Expires: 0'); } $out = fopen($target, 'wb'); if ($out === false) { return; } fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $headers); $per_page = 500; $paged = 1; while (true) { $q = new WP_Query(self::export_query($settings, [ 'posts_per_page' => $per_page, 'paged' => $paged, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => true, ])); if (empty($q->posts)) { break; } foreach ($q->posts as $post) { /** @var WP_Post $post */ fputcsv($out, self::build_post_row($post, $cols, $date_format, $settings)); } wp_reset_postdata(); if ($target === 'php://output' && function_exists('flush')) { flush(); } $paged++; } fclose($out); } /** * @param array $settings leexport_get_settings() * @param string[] $cols * @param string[] $headers * @return array */ private static function write_posts_csv_parts( array $settings, array $cols, string $date_format, array $headers, int $split_rows, string $basename, string $ext ): array { $parts = []; $part_num = 0; $rows_in_part = 0; $out = null; $path = ''; $open_part = static function () use (&$part_num, &$out, &$path, &$rows_in_part, $headers, $basename, $ext, &$parts): void { if (is_resource($out)) { fclose($out); } $part_num++; $rows_in_part = 0; $path = wp_tempnam($basename . '-part'); if (!is_string($path) || $path === '') { return; } $out = fopen($path, 'wb'); if ($out === false) { return; } fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $headers); $parts[] = [ 'path' => $path, 'name' => sprintf('%s-part%03d.%s', $basename, $part_num, $ext), ]; }; $open_part(); $per_page = 500; $paged = 1; while (true) { $q = new WP_Query(self::export_query($settings, [ 'posts_per_page' => $per_page, 'paged' => $paged, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => true, ])); if (empty($q->posts)) { break; } foreach ($q->posts as $post) { /** @var WP_Post $post */ if ($rows_in_part >= $split_rows) { $open_part(); } if (!is_resource($out)) { break 2; } fputcsv($out, self::build_post_row($post, $cols, $date_format, $settings)); $rows_in_part++; } wp_reset_postdata(); $paged++; } if (is_resource($out)) { fclose($out); } return $parts; } /** * @param array $settings leexport_get_settings() */ public static function output_url_txt_download(array $settings, string $basename): void { $split_rows = leexport_split_rows_limit($settings); $total = self::count_published_posts($settings); if ($split_rows > 0 && $total > $split_rows) { self::output_url_txt_split_zip($settings, $split_rows, $basename); return; } if (headers_sent()) { return; } nocache_headers(); header('Content-Type: text/plain; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $basename . '.txt"'); self::stream_url_lines($settings, 'php://output'); exit; } private static function output_url_txt_split_zip(array $settings, int $split_rows, string $basename): void { if (!class_exists('ZipArchive')) { wp_die( esc_html__('Split export requires the PHP Zip extension. Install php-zip, or set “Rows per file” to 0 for a single-file export.', 'leexport'), esc_html__('Export failed', 'leexport'), ['response' => 500] ); } $parts = self::write_url_txt_parts($settings, $split_rows, $basename); self::deliver_zip_parts($parts, $basename); } /** * @param array $settings leexport_get_settings() * @return array */ private static function write_url_txt_parts(array $settings, int $split_rows, string $basename): array { $parts = []; $part_num = 0; $rows_in_part = 0; $out = null; $open_part = static function () use (&$part_num, &$out, &$rows_in_part, $basename, &$parts): void { if (is_resource($out)) { fclose($out); } $part_num++; $rows_in_part = 0; $path = wp_tempnam($basename . '-part'); if (!is_string($path) || $path === '') { return; } $out = fopen($path, 'wb'); if ($out === false) { return; } $parts[] = [ 'path' => $path, 'name' => sprintf('%s-part%03d.txt', $basename, $part_num), ]; }; $open_part(); $per_page = 1000; $paged = 1; while (true) { $q = new WP_Query(self::export_query($settings, [ 'fields' => 'ids', 'posts_per_page' => $per_page, 'paged' => $paged, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ])); if (empty($q->posts)) { break; } foreach ($q->posts as $pid) { $u = get_permalink((int)$pid); if (!is_string($u)) { continue; } $u = trim($u); if ($u === '') { continue; } if ($rows_in_part >= $split_rows) { $open_part(); } if (!is_resource($out)) { break 2; } if ($rows_in_part > 0) { fwrite($out, "\n"); } fwrite($out, $u); $rows_in_part++; } $paged++; } if (is_resource($out)) { fclose($out); } return $parts; } /** * @param array $settings leexport_get_settings() */ private static function stream_url_lines(array $settings, string $target): void { $per_page = 1000; $paged = 1; $first = true; while (true) { $q = new WP_Query(self::export_query($settings, [ 'fields' => 'ids', 'posts_per_page' => $per_page, 'paged' => $paged, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ])); if (empty($q->posts)) { break; } foreach ($q->posts as $pid) { $u = get_permalink((int)$pid); if (!is_string($u)) { continue; } $u = trim($u); if ($u === '') { continue; } if ($target === 'php://output') { if (!$first) { echo "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text download stream. } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text URL list download, not HTML. echo esc_url_raw($u); $first = false; } } if ($target === 'php://output' && function_exists('flush')) { flush(); } $paged++; } } /** * @param array $settings leexport_get_settings() */ public static function output_url_csv_download(array $settings, string $basename): void { $split_rows = leexport_split_rows_limit($settings); $total = self::count_published_posts($settings); if ($split_rows > 0 && $total > $split_rows) { self::output_url_csv_split_zip($settings, $split_rows, $basename); return; } if (headers_sent()) { return; } nocache_headers(); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $basename . '.csv"'); header('Pragma: no-cache'); header('Expires: 0'); self::stream_url_csv($settings, 'php://output'); exit; } private static function output_url_csv_split_zip(array $settings, int $split_rows, string $basename): void { if (!class_exists('ZipArchive')) { wp_die( esc_html__('Split export requires the PHP Zip extension. Install php-zip, or set “Rows per file” to 0 for a single-file export.', 'leexport'), esc_html__('Export failed', 'leexport'), ['response' => 500] ); } $parts = self::write_url_csv_parts($settings, $split_rows, $basename); self::deliver_zip_parts($parts, $basename); } /** * @param array $settings leexport_get_settings() * @return array */ private static function write_url_csv_parts(array $settings, int $split_rows, string $basename): array { $parts = []; $part_num = 0; $rows_in_part = 0; $out = null; $header = [__('URL', 'leexport')]; $open_part = static function () use (&$part_num, &$out, &$rows_in_part, $basename, &$parts, $header): void { if (is_resource($out)) { fclose($out); } $part_num++; $rows_in_part = 0; $path = wp_tempnam($basename . '-part'); if (!is_string($path) || $path === '') { return; } $out = fopen($path, 'wb'); if ($out === false) { return; } fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $header); $parts[] = [ 'path' => $path, 'name' => sprintf('%s-part%03d.csv', $basename, $part_num), ]; }; $open_part(); $per_page = 1000; $paged = 1; while (true) { $q = new WP_Query(self::export_query($settings, [ 'fields' => 'ids', 'posts_per_page' => $per_page, 'paged' => $paged, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ])); if (empty($q->posts)) { break; } foreach ($q->posts as $pid) { $u = get_permalink((int)$pid); if (!is_string($u)) { continue; } $u = trim($u); if ($u === '') { continue; } if ($rows_in_part >= $split_rows) { $open_part(); } if (!is_resource($out)) { break 2; } fputcsv($out, [$u]); $rows_in_part++; } $paged++; } if (is_resource($out)) { fclose($out); } return $parts; } /** * @param array $settings leexport_get_settings() */ private static function stream_url_csv(array $settings, string $target): void { $out = fopen($target, 'wb'); if ($out === false) { return; } fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, [__('URL', 'leexport')]); $per_page = 1000; $paged = 1; while (true) { $q = new WP_Query(self::export_query($settings, [ 'fields' => 'ids', 'posts_per_page' => $per_page, 'paged' => $paged, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ])); if (empty($q->posts)) { break; } foreach ($q->posts as $pid) { $u = get_permalink((int)$pid); if (!is_string($u)) { continue; } $u = trim($u); if ($u === '') { continue; } fputcsv($out, [$u]); } if ($target === 'php://output' && function_exists('flush')) { flush(); } $paged++; } fclose($out); } /** * @param array $rows */ public static function output_csv_download(string $filename, array $headers, array $rows, int $split_rows = 0): void { if ($split_rows > 0 && count($rows) > $split_rows) { self::output_csv_rows_split_zip($filename, $headers, $rows, $split_rows); return; } if (headers_sent()) { return; } nocache_headers(); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Pragma: no-cache'); header('Expires: 0'); $out = fopen('php://output', 'wb'); if ($out === false) { return; } fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $headers); foreach ($rows as $row) { fputcsv($out, $row); } fclose($out); } /** * @param array $rows * @param string[] $headers */ private static function output_csv_rows_split_zip(string $basename, array $headers, array $rows, int $split_rows): void { if (!class_exists('ZipArchive')) { wp_die( esc_html__('Split export requires the PHP Zip extension. Install php-zip, or set “Rows per file” to 0 for a single-file export.', 'leexport'), esc_html__('Export failed', 'leexport'), ['response' => 500] ); } $basename = preg_replace('/\.csv$/i', '', $basename) ?: $basename; $chunks = array_chunk($rows, $split_rows); $parts = []; $part_num = 0; foreach ($chunks as $chunk) { $part_num++; $path = wp_tempnam($basename . '-part'); if (!is_string($path) || $path === '') { continue; } $out = fopen($path, 'wb'); if ($out === false) { continue; } fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $headers); foreach ($chunk as $row) { fputcsv($out, $row); } fclose($out); $parts[] = [ 'path' => $path, 'name' => sprintf('%s-part%03d.csv', $basename, $part_num), ]; } self::deliver_zip_parts($parts, $basename); } /** * @param array $parts */ private static function deliver_zip_parts(array $parts, string $basename): void { if ($parts === []) { wp_die( esc_html__('No export data found.', 'leexport'), esc_html__('Export failed', 'leexport'), ['response' => 404] ); } if (count($parts) === 1) { $single = $parts[0]; $mime = substr(strtolower($single['name']), -4) === '.txt' ? 'text/plain' : 'text/csv'; self::deliver_file($single['path'], $single['name'], $mime); return; } $zip_path = wp_tempnam($basename . '.zip'); if (!is_string($zip_path) || $zip_path === '') { wp_die(esc_html__('Could not create export archive.', 'leexport')); } $zip = new ZipArchive(); if ($zip->open($zip_path, ZipArchive::OVERWRITE) !== true) { wp_delete_file($zip_path); wp_die(esc_html__('Could not create export archive.', 'leexport')); } foreach ($parts as $part) { if (is_readable($part['path'])) { $zip->addFile($part['path'], $part['name']); } } $zip->close(); foreach ($parts as $part) { wp_delete_file($part['path']); } self::deliver_file($zip_path, $basename . '.zip', 'application/zip'); } private static function deliver_file(string $path, string $download_name, string $mime): void { if (!is_readable($path)) { wp_die(esc_html__('Export file is missing.', 'leexport')); } if (headers_sent()) { wp_delete_file($path); return; } $safe_name = sanitize_file_name($download_name); nocache_headers(); header('Content-Type: ' . $mime . '; charset=UTF-8'); header('Content-Disposition: attachment; filename="' . $safe_name . '"'); header('Content-Length: ' . (string)filesize($path)); header('Pragma: no-cache'); header('Expires: 0'); $fh = fopen($path, 'rb'); if ($fh !== false) { while (!feof($fh)) { $buf = fread($fh, 1024 * 1024); if ($buf === false) { break; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary download stream (CSV/ZIP), not HTML. echo $buf; } fclose($fh); } wp_delete_file($path); exit; } } // phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fopen // phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fwrite // phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fclose // phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fread