PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.1
WebberZone Top 10 — Popular Posts v4.5.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.5.1, at includes/util/class-csv-helper.php

220 lines 7.5 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 * 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