, count: int}> */ public static function discover(): array { global $wpdb; // %i is WordPress's identifier placeholder, so the table name is // prepared like any other value rather than interpolated and excused // with a suppression comment. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- reads a table this plugin owns; nothing to cache for a one-off admin action. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT cal_name, cal_type, COUNT(*) AS uses FROM %i GROUP BY cal_name, cal_type ORDER BY cal_name', $wpdb->prefix . 'webcal_site_extras' ), ARRAY_A ); if ( ! is_array( $rows ) || array() === $rows ) { return array(); } $known = array(); foreach ( SiteExtraFields::all() as $field ) { $known[ $field['name'] ] = true; } $found = array(); foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $name = isset( $row['cal_name'] ) && is_scalar( $row['cal_name'] ) ? trim( (string) $row['cal_name'] ) : ''; if ( '' === $name || isset( $known[ $name ] ) || isset( $found[ $name ] ) ) { continue; } $legacy_type = isset( $row['cal_type'] ) && is_numeric( $row['cal_type'] ) ? (int) $row['cal_type'] : 0; $type = SiteExtraFields::type_for_legacy( $legacy_type ); $definition = array( 'name' => $name, // The label lived in 1.9's PHP file, so the best available // answer is the name. An administrator renaming it afterwards // costs a moment; inventing a prettier label risks inventing // the wrong one. 'label' => $name, 'type' => $type, 'count' => isset( $row['uses'] ) && is_numeric( $row['uses'] ) ? (int) $row['uses'] : 0, ); if ( 'select' === $type ) { $options = self::options_in_use( $name ); if ( array() === $options ) { // A select with no observed values cannot offer choices, // and an empty select is a field nobody can fill in. $definition['type'] = 'text'; } else { $definition['options'] = $options; } } $found[ $name ] = $definition; } return array_values( $found ); } /** * The distinct values a field actually holds, as its options. * * Inferred, and therefore incomplete by nature: a choice 1.9 offered but * nobody ever picked leaves no trace in the data. Capped so that a field * which turns out to be free text does not produce a thousand-entry * dropdown — past the cap it is treated as text, which is what it is. * * @return list */ private static function options_in_use( string $name ): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- reads a table this plugin owns; nothing to cache for a one-off admin action. $values = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT cal_data FROM %i WHERE cal_name = %s AND cal_data <> '' LIMIT 51", $wpdb->prefix . 'webcal_site_extras', $name ) ); if ( ! is_array( $values ) || count( $values ) > 50 ) { return array(); } $options = array(); foreach ( $values as $value ) { if ( ! is_scalar( $value ) ) { continue; } $text = trim( (string) $value ); if ( '' !== $text && ! in_array( $text, $options, true ) ) { $options[] = $text; } } sort( $options ); return $options; } }