| 1 |
<?php |
| 2 |
/** |
| 3 |
* CSV helper for import and export of count data. |
| 4 |
* |
| 5 |
* @package WebberZone\Top_Ten\Util |
| 6 |
* @since 4.3.0 |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace WebberZone\Top_Ten\Util; |
| 10 |
|
| 11 |
use WebberZone\Top_Ten\Database; |
| 12 |
|
| 13 |
// If this file is called directly, abort. |
| 14 |
if ( ! defined( 'WPINC' ) ) { |
| 15 |
die; |
| 16 |
} |
| 17 |
|
| 18 |
/** |
| 19 |
* Shared CSV read/write logic used by both the admin Import/Export page |
| 20 |
* and the WP-CLI `wp top10 counts export|import` commands. |
| 21 |
* |
| 22 |
* @since 4.3.0 |
| 23 |
*/ |
| 24 |
class Csv_Helper { |
| 25 |
|
| 26 |
/** |
| 27 |
* Write count rows to an open file handle in the Top 10 CSV format. |
| 28 |
* |
| 29 |
* Column layout: Overall = Post ID, Visits, Blog ID[, URL]. Daily = Post ID, Visits, |
| 30 |
* Date, Blog ID[, URL]. |
| 31 |
* |
| 32 |
* @since 4.3.0 |
| 33 |
* |
| 34 |
* @param resource $fh Open file handle (writable). |
| 35 |
* @param array $rows Result rows from the database. Each row |
| 36 |
* must contain at least postnumber, |
| 37 |
* cntaccess, and blog_id. Daily rows must |
| 38 |
* also contain dp_date. |
| 39 |
* @param bool $daily Whether rows are from the daily table. |
| 40 |
* @param bool $include_urls Whether to append a "URL" column. |
| 41 |
* @param bool $network_wide Whether this is a network-wide export |
| 42 |
* (used for correct permalink resolution). |
| 43 |
*/ |
| 44 |
public static function write_export_csv( $fh, array $rows, bool $daily, bool $include_urls = false, bool $network_wide = false ): void { |
| 45 |
// UTF-8 BOM for Excel / spreadsheet compatibility. |
| 46 |
fprintf( $fh, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); |
| 47 |
|
| 48 |
$header = array( 'Post ID', 'Visits' ); |
| 49 |
if ( $daily ) { |
| 50 |
$header[] = 'Date'; |
| 51 |
} |
| 52 |
$header[] = 'Blog ID'; |
| 53 |
if ( $include_urls ) { |
| 54 |
$header[] = 'URL'; |
| 55 |
} |
| 56 |
|
| 57 |
fputcsv( $fh, $header, ',', '"', '\\' ); |
| 58 |
|
| 59 |
$url_cache = array(); |
| 60 |
foreach ( $rows as $row ) { |
| 61 |
$line = array( $row['postnumber'], $row['cntaccess'] ); |
| 62 |
if ( $daily ) { |
| 63 |
$line[] = $row['dp_date']; |
| 64 |
} |
| 65 |
$line[] = $row['blog_id']; |
| 66 |
|
| 67 |
if ( $include_urls ) { |
| 68 |
$pid = (int) $row['postnumber']; |
| 69 |
$bid = (int) $row['blog_id']; |
| 70 |
if ( ! isset( $url_cache[ $bid ][ $pid ] ) ) { |
| 71 |
$url_cache[ $bid ][ $pid ] = is_multisite() && $network_wide |
| 72 |
? get_blog_permalink( $bid, $pid ) |
| 73 |
: get_permalink( $pid ); |
| 74 |
} |
| 75 |
$line[] = $url_cache[ $bid ][ $pid ]; |
| 76 |
} |
| 77 |
|
| 78 |
fputcsv( $fh, $line, ',', '"', '\\' ); |
| 79 |
} |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Parse an import CSV file and return structured rows. |
| 84 |
* |
| 85 |
* Auto-detects overall vs. daily table by the presence of a "Date" column. Caller |
| 86 |
* handles URL resolution, date normalisation, blog filtering, and database writes. |
| 87 |
* |
| 88 |
* @since 4.3.0 |
| 89 |
* |
| 90 |
* @param string $file_path Path to the CSV file. |
| 91 |
* @return array An associative array with the keys: |
| 92 |
* - daily: bool Whether the file targets the daily table. |
| 93 |
* - rows: array[] Parsed data rows. Each element is an |
| 94 |
* associative array with the keys |
| 95 |
* postnumber (int), cntaccess (int), |
| 96 |
* blog_id (int), plus dp_date (string) |
| 97 |
* when daily is true, and url (string) |
| 98 |
* when an optional URL column is present. |
| 99 |
* - total: int Total number of data rows. |
| 100 |
*/ |
| 101 |
public static function parse_import_file( string $file_path ): array { |
| 102 |
$handle = fopen( $file_path, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen |
| 103 |
if ( false === $handle ) { |
| 104 |
return array( |
| 105 |
'daily' => false, |
| 106 |
'rows' => array(), |
| 107 |
'total' => 0, |
| 108 |
); |
| 109 |
} |
| 110 |
|
| 111 |
// Strip BOM if present. |
| 112 |
$bom = fread( $handle, 3 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread |
| 113 |
if ( ( chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ) !== $bom ) { |
| 114 |
rewind( $handle ); |
| 115 |
} |
| 116 |
|
| 117 |
$headers = fgetcsv( $handle, 0, ',', '"', '\\' ); |
| 118 |
if ( false === $headers ) { |
| 119 |
fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose |
| 120 |
return array( |
| 121 |
'daily' => false, |
| 122 |
'rows' => array(), |
| 123 |
'total' => 0, |
| 124 |
); |
| 125 |
} |
| 126 |
|
| 127 |
$headers = array_map( 'trim', $headers ); |
| 128 |
|
| 129 |
// Auto-detect daily vs overall from presence of a "Date" column. |
| 130 |
$daily = in_array( 'Date', $headers, true ); |
| 131 |
$col_post_id = 0; |
| 132 |
$col_count = 1; |
| 133 |
$col_date = $daily ? 2 : -1; |
| 134 |
$col_blog_id = $daily ? 3 : 2; |
| 135 |
$col_url = $daily ? 4 : 3; |
| 136 |
$has_url = isset( $headers[ $col_url ] ) && 'URL' === $headers[ $col_url ]; |
| 137 |
|
| 138 |
$rows = array(); |
| 139 |
$total = 0; |
| 140 |
|
| 141 |
while ( false !== ( $line = fgetcsv( $handle, 0, ',', '"', '\\' ) ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition |
| 142 |
if ( 1 === count( $line ) && '' === $line[0] ) { |
| 143 |
continue; |
| 144 |
} |
| 145 |
|
| 146 |
$row = array( |
| 147 |
'postnumber' => isset( $line[ $col_post_id ] ) ? absint( $line[ $col_post_id ] ) : 0, |
| 148 |
'cntaccess' => isset( $line[ $col_count ] ) ? absint( $line[ $col_count ] ) : 0, |
| 149 |
'blog_id' => isset( $line[ $col_blog_id ] ) ? absint( $line[ $col_blog_id ] ) : get_current_blog_id(), |
| 150 |
); |
| 151 |
|
| 152 |
if ( $daily ) { |
| 153 |
$row['dp_date'] = isset( $line[ $col_date ] ) ? trim( $line[ $col_date ], " \t\n\r\0\x0B" ) : ''; |
| 154 |
} |
| 155 |
if ( $has_url && isset( $line[ $col_url ] ) ) { |
| 156 |
$row['url'] = trim( $line[ $col_url ], " \t\n\r\0\x0B" ); |
| 157 |
} |
| 158 |
|
| 159 |
$rows[] = $row; |
| 160 |
++$total; |
| 161 |
} |
| 162 |
|
| 163 |
fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose |
| 164 |
|
| 165 |
return array( |
| 166 |
'daily' => $daily, |
| 167 |
'rows' => $rows, |
| 168 |
'total' => $total, |
| 169 |
); |
| 170 |
} |
| 171 |
|
| 172 |
/** |
| 173 |
* Fetch raw count rows from the database for CSV export. |
| 174 |
* |
| 175 |
* Called by both the admin Import/Export page and the WP-CLI |
| 176 |
* `wp top10 counts export` command. |
| 177 |
* |
| 178 |
* @since 4.3.0 |
| 179 |
* |
| 180 |
* @param bool $daily Whether to query the daily table. |
| 181 |
* @param bool $network_wide Whether to collect rows from all sites (multisite only). |
| 182 |
* @param int $blog_id Specific blog ID (0 = current site). Only used when |
| 183 |
* $network_wide is false. |
| 184 |
* @param int $limit Max rows to return (0 = all). |
| 185 |
* @return array Raw result rows (ARRAY_A). |
| 186 |
*/ |
| 187 |
public static function fetch_export_data( bool $daily, bool $network_wide = false, int $blog_id = 0, int $limit = 0 ): array { |
| 188 |
global $wpdb; |
| 189 |
|
| 190 |
$table = Database::get_table( $daily ); |
| 191 |
|
| 192 |
if ( $network_wide && is_multisite() ) { |
| 193 |
$results = array(); |
| 194 |
$sites = get_sites( array( 'number' => 1000 ) ); |
| 195 |
foreach ( $sites as $site ) { |
| 196 |
switch_to_blog( (int) $site->blog_id ); |
| 197 |
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 198 |
$sql = $wpdb->prepare( "SELECT * FROM `{$table}` WHERE blog_id = %d", (int) $site->blog_id ); |
| 199 |
$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 200 |
restore_current_blog(); |
| 201 |
$results = array_merge( $results, is_array( $rows ) ? $rows : array() ); |
| 202 |
if ( $limit > 0 && count( $results ) >= $limit ) { |
| 203 |
break; |
| 204 |
} |
| 205 |
} |
| 206 |
return $limit > 0 ? array_slice( $results, 0, $limit ) : $results; |
| 207 |
} |
| 208 |
|
| 209 |
$bid = $blog_id > 0 ? $blog_id : get_current_blog_id(); |
| 210 |
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 211 |
$sql = $wpdb->prepare( "SELECT * FROM `{$table}` WHERE blog_id = %d", $bid ); |
| 212 |
if ( $limit > 0 ) { |
| 213 |
$sql .= $wpdb->prepare( ' LIMIT %d', $limit ); |
| 214 |
} |
| 215 |
|
| 216 |
$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 217 |
return is_array( $rows ) ? $rows : array(); |
| 218 |
} |
| 219 |
} |
| 220 |
|