PluginProbe
WebberZone Top 10 — Popular Posts / 4.4.1
WebberZone Top 10 — Popular Posts v4.4.1
4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 All 117 releases
top-10 / includes / util / class-csv-helper.php

class-csv-helper.php in WebberZone Top 10 — Popular Posts 4.4.1, at includes/util/class-csv-helper.php

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