Changeset 3666532
- Timestamp:
- 08/26/2026 08:32:47 AM (2 months ago)
- Location:
- neutrope-lead-desk-for-contact-form-7
- Files:
-
- 22 edited
- 1 copied
-
tags/0.3.0 (copied) (copied from neutrope-lead-desk-for-contact-form-7/trunk )
-
tags/0.3.0/admin/class-admin-menu.php (modified) (1 diff)
-
tags/0.3.0/admin/class-entry-detail-page.php (modified) (1 diff)
-
tags/0.3.0/admin/class-entry-list-table.php (modified) (6 diffs)
-
tags/0.3.0/admin/views/entry-detail.php (modified) (4 diffs)
-
tags/0.3.0/includes/class-entry-repository.php (modified) (5 diffs)
-
tags/0.3.0/includes/class-export-service.php (modified) (1 diff)
-
tags/0.3.0/languages/neutrope-lead-desk-for-contact-form-7-ja.mo (modified) ( previous)
-
tags/0.3.0/languages/neutrope-lead-desk-for-contact-form-7-ja.po (modified) (14 diffs)
-
tags/0.3.0/languages/neutrope-lead-desk-for-contact-form-7.pot (modified) (13 diffs)
-
tags/0.3.0/neutrope-lead-desk-for-contact-form-7.php (modified) (2 diffs)
-
tags/0.3.0/readme.txt (modified) (6 diffs)
-
trunk/admin/class-admin-menu.php (modified) (1 diff)
-
trunk/admin/class-entry-detail-page.php (modified) (1 diff)
-
trunk/admin/class-entry-list-table.php (modified) (6 diffs)
-
trunk/admin/views/entry-detail.php (modified) (4 diffs)
-
trunk/includes/class-entry-repository.php (modified) (5 diffs)
-
trunk/includes/class-export-service.php (modified) (1 diff)
-
trunk/languages/neutrope-lead-desk-for-contact-form-7-ja.mo (modified) ( previous)
-
trunk/languages/neutrope-lead-desk-for-contact-form-7-ja.po (modified) (14 diffs)
-
trunk/languages/neutrope-lead-desk-for-contact-form-7.pot (modified) (13 diffs)
-
trunk/neutrope-lead-desk-for-contact-form-7.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/admin/class-admin-menu.php
r3666139 r3666532 185 185 $message .= ' ' . esc_html( 186 186 sprintf( 187 /* translators: 1: number of records processed, 2: total records , 3: percentage complete*/188 __( ' %1$s of %2$s (%3$s%%)', 'neutrope-lead-desk-for-contact-form-7' ),187 /* translators: 1: number of records processed, 2: total records to process, 3: percentage complete. "Records" counts processing units (entries plus notes), NOT the number of inquiries. */ 188 __( 'Processed %1$s of %2$s records (%3$s%%).', 'neutrope-lead-desk-for-contact-form-7' ), 189 189 number_format_i18n( (int) $progress['processed'] ), 190 190 number_format_i18n( (int) $progress['total'] ), -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/admin/class-entry-detail-page.php
r3666139 r3666532 127 127 $backfill_pending = class_exists( 'NLDCF7_Event_Backfill' ) && NLDCF7_Event_Backfill::is_pending(); 128 128 129 // The "history from before this plugin was updated" note only makes 130 // sense where there IS such history. The migration snapshot records how 131 // much there was to reconstruct at upgrade time: both totals are zero on 132 // a site that installed this version fresh, and the note is then simply 133 // untrue. The snapshot is used rather than today's entry count because 134 // entries received since the upgrade say nothing about what came before. 135 // 136 // If the snapshot is missing or unreadable the note is shown: an 137 // unnecessary explanation is a smaller harm than silently omitting the 138 // one that tells an admin why their older entries look sparse. 139 $show_history_note = true; 140 if ( class_exists( 'NLDCF7_Event_Backfill' ) && NLDCF7_Event_Backfill::is_initialised() ) { 141 $backfill_state = NLDCF7_Event_Backfill::get_state(); 142 if ( isset( $backfill_state['entry_total'], $backfill_state['note_total'] ) ) { 143 $show_history_note = ( (int) $backfill_state['entry_total'] > 0 ) 144 || ( (int) $backfill_state['note_total'] > 0 ); 145 } 146 } 147 129 148 if ( $this->event_repo ) { 130 149 $events_total = $this->event_repo->count_for_entry( (int) $entry['id'] ); -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/admin/class-entry-list-table.php
r3666139 r3666532 129 129 } 130 130 131 return $args; 131 return self::apply_carried_args_filter( $args ); 132 } 133 134 /** 135 * Maximum number of add-on contributed args carried in a list URL. 136 */ 137 const CARRIED_MAX_ARGS = 20; 138 139 /** 140 * Maximum length of a single carried value, in characters. 141 */ 142 const CARRIED_MAX_VALUE = 200; 143 144 /** 145 * Query keys an add-on may never contribute to a carried list URL. 146 * 147 * `page` is here because Free sets it itself; `paged` because v0.2.0 148 * deliberately returns to the first page after an action; the rest are 149 * mutation, authentication or routing state that must never be replayed 150 * from a link the admin can bookmark or share. 151 * 152 * @var string[] 153 */ 154 private static $carried_denylist = array( 155 'page', 156 'paged', 157 'action', 158 'action2', 159 'entry_id', 160 '_wpnonce', 161 '_wp_http_referer', 162 'view', 163 'event_page', 164 self::VIEW_QUERY_VAR, 165 ); 166 167 /** 168 * Let add-ons contribute their own filter state, then re-impose Free's rules. 169 * 170 * Everything an add-on adds is treated as untrusted: the deny-list, key 171 * normalisation, scalar-only rule and size caps are applied AFTER the 172 * filter, so a careless listener cannot put `_wpnonce` or an array back 173 * into a URL. Core args are re-applied last and always win. 174 * 175 * This is the one place custom filter state enters a list URL. Every path 176 * that needs it -- the All/Trash views, the row actions, and the redirect 177 * after a row or bulk action -- reaches it through current_filter_args(), 178 * so there is a single implementation rather than one per call site. 179 * 180 * @param array<string,string|int> $core Sanitized core filter args. 181 * @return array<string,string|int> 182 */ 183 private static function apply_carried_args_filter( array $core ) { 184 /** 185 * Filter the list state carried across view switches and row/bulk actions. 186 * 187 * Add-ons append their own already-sanitized scalar args. Free re-checks 188 * the result regardless; see the class constants for the size caps and 189 * `$carried_denylist` for the keys that are always stripped. 190 * 191 * Free never forwards the raw query string, so an add-on filter that 192 * adds nothing leaves the URL byte-identical to v0.2.0. 193 * 194 * @since 0.3.0 195 * 196 * @param array<string,string|int> $args Args to carry. Core keys included. 197 * @param array<string,string|int> $core The core args, for reference. 198 */ 199 $filtered = apply_filters( 'nldcf7_entry_list_carried_args', $core, $core ); 200 201 if ( ! is_array( $filtered ) ) { 202 return $core; 203 } 204 205 $extra = array(); 206 foreach ( $filtered as $key => $value ) { 207 if ( count( $extra ) >= self::CARRIED_MAX_ARGS ) { 208 self::debug_carried( 'too many carried args; extra dropped' ); 209 break; 210 } 211 212 $key = sanitize_key( (string) $key ); 213 if ( '' === $key || array_key_exists( $key, $core ) ) { 214 // Empty/unusable key, or a core key -- core is authoritative and 215 // is re-applied below either way. 216 continue; 217 } 218 if ( in_array( $key, self::$carried_denylist, true ) ) { 219 self::debug_carried( 'reserved key refused: ' . $key ); 220 continue; 221 } 222 if ( ! is_scalar( $value ) ) { 223 // Arrays and objects are dropped outright. Casting one would 224 // print the literal string "Array" into a URL. 225 self::debug_carried( 'non-scalar value dropped for key: ' . $key ); 226 continue; 227 } 228 229 $value = (string) $value; 230 if ( '' === $value ) { 231 continue; 232 } 233 if ( function_exists( 'mb_substr' ) ) { 234 $value = mb_substr( $value, 0, self::CARRIED_MAX_VALUE ); 235 } else { 236 $value = substr( $value, 0, self::CARRIED_MAX_VALUE ); 237 } 238 239 $extra[ $key ] = sanitize_text_field( $value ); 240 } 241 242 // Core last: an add-on cannot override the screen's own filter state. 243 return array_merge( $extra, $core ); 244 } 245 246 /** 247 * Log a rejected carried-arg contribution while debugging. 248 * 249 * @param string $message 250 * @return void 251 */ 252 private static function debug_carried( $message ) { 253 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 254 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated by WP_DEBUG; an add-on silently losing filter state is otherwise very hard to diagnose. 255 error_log( '[neutrope-lead-desk-for-contact-form-7] carried args: ' . $message ); 256 } 132 257 } 133 258 … … 234 359 235 360 public function get_columns() { 236 returnarray(361 $core = array( 237 362 'cb' => '<input type="checkbox" />', 238 363 'submitted_at' => __( 'Received', 'neutrope-lead-desk-for-contact-form-7' ), … … 246 371 'note_count' => __( 'Notes', 'neutrope-lead-desk-for-contact-form-7' ), 247 372 ); 373 374 /** 375 * Filter the entries list columns. 376 * 377 * Add-ons may append their own columns. They may NOT remove, rename or 378 * repurpose a core column: every core key is restored to its own label 379 * after this filter runs, so reusing one silently changes nothing. That 380 * is enforced in code rather than left to convention, because a column 381 * whose header says one thing and whose cells say another is a data 382 * integrity problem, not a styling preference. 383 * 384 * Cells for added columns are rendered through 385 * `nldcf7_entry_list_column_content`. 386 * 387 * @since 0.3.0 388 * 389 * @param array<string,string> $columns column key => header label. 390 */ 391 $filtered = apply_filters( 'nldcf7_entry_list_columns', $core ); 392 393 if ( ! is_array( $filtered ) ) { 394 return $core; 395 } 396 397 // Keep only usable entries: a non-empty sanitize_key()-able key mapped 398 // to a scalar label. Anything else is a malformed contribution and is 399 // dropped rather than being allowed to break the header row. 400 $clean = array(); 401 foreach ( $filtered as $key => $label ) { 402 $key = sanitize_key( (string) $key ); 403 if ( '' === $key || ! is_scalar( $label ) ) { 404 continue; 405 } 406 $clean[ $key ] = (string) $label; 407 } 408 409 // Core wins: restore every core column's own label, and re-add any the 410 // filter dropped. `cb` goes back to the front so the bulk-select box 411 // never ends up mid-row. 412 foreach ( $core as $key => $label ) { 413 $clean[ $key ] = $label; 414 } 415 if ( isset( $clean['cb'] ) ) { 416 $cb = $clean['cb']; 417 unset( $clean['cb'] ); 418 $clean = array_merge( array( 'cb' => $cb ), $clean ); 419 } 420 421 return $clean; 422 } 423 424 /** 425 * Render a cell for a column this class has no `column_{key}()` method for. 426 * 427 * Only add-on columns reach here: every core column has its own method. 428 * WordPress core's own `column_default()` is an empty no-op, so without 429 * this an added column would render as a blank cell. 430 * 431 * The listener returns HTML and is responsible for escaping it; Free then 432 * runs the result through `wp_kses_post()` as a second layer, which is the 433 * reason this is a filter with a return value rather than an action that 434 * echoes -- output that Free never sees is output Free cannot make safe. 435 * 436 * @param array $item Entry row. 437 * @param string $column_name Column key. 438 * @return string Escaped HTML, or '' when nothing is contributed. 439 */ 440 public function column_default( $item, $column_name ) { 441 /** 442 * Filter the cell content for an add-on column on the entries list. 443 * 444 * @since 0.3.0 445 * 446 * @param string $html Cell HTML. Default ''. 447 * @param string $column_name Column key being rendered. 448 * @param array $item Entry row, as stored. 449 */ 450 $html = apply_filters( 'nldcf7_entry_list_column_content', '', (string) $column_name, $item ); 451 452 if ( ! is_string( $html ) || '' === $html ) { 453 return ''; 454 } 455 456 return wp_kses_post( $html ); 248 457 } 249 458 … … 495 704 </label> 496 705 497 <?php submit_button( __( 'Filter', 'neutrope-lead-desk-for-contact-form-7' ), '', 'nldcf7-filter-submit', false ); ?> 706 <?php 707 /** 708 * Fires inside the entries list filter bar, before the Filter button. 709 * 710 * Listeners print their own form controls here. The position is 711 * guaranteed to be inside the existing GET form, so a plain 712 * `<select>` or `<input>` submits with the core filters and needs 713 * nothing else wired up. 714 * 715 * No nonce is involved: this bar only reads, and Free's own filters 716 * carry none either. The listener owns its field names, its values 717 * and its escaping; Free owns only the placement. 718 * 719 * @since 0.3.0 720 * 721 * @param array<string,string|int> $current_args Sanitized core filter 722 * state for this request. 723 */ 724 do_action( 'nldcf7_entry_list_filters', self::current_filter_args() ); 725 726 submit_button( __( 'Filter', 'neutrope-lead-desk-for-contact-form-7' ), '', 'nldcf7-filter-submit', false ); 727 ?> 498 728 </div> 499 729 <?php … … 529 759 // behaving as a soft delete rather than turning into a filter. 530 760 'trashed_only' => ( self::VIEW_TRASH === self::current_view() ), 761 // Identifies this caller to the query extension hooks so an add-on 762 // can constrain the list without also constraining CSV export. 763 'context' => NLDCF7_Entry_Repository::CONTEXT_LIST, 531 764 ); 532 765 … … 551 784 ) 552 785 ); 786 787 /** 788 * Fires once the rows for the current list page are known. 789 * 790 * Add-ons that render their own columns use this to load whatever they 791 * need for these rows in one query, before any cell is rendered. 792 * Without it they are pushed into N+1: the cell filter is handed a 793 * single row and has no way to learn what else is on the page, so the 794 * only alternative is a lookup per cell -- twenty rows with two custom 795 * columns being up to forty extra queries. 796 * 797 * Free's own note-count cache above is the same pattern; this is that 798 * opportunity handed to extensions. 799 * 800 * Contract: 801 * - $items is THIS PAGE's rows, never the whole result set. 802 * - Read-only. Listeners must not mutate the list's state, and no 803 * return value is used. 804 * - Fires exactly once per prepare cycle, including with an empty 805 * array when the page has no rows, so "once per cycle" holds 806 * without a special case for empty results. 807 * - The entries screen only. CSV export does not build a list table 808 * and so never reaches here. 809 * - Running the secondary query safely -- preparing it, checking 810 * capabilities -- belongs to the listener. 811 * 812 * Fired after this class has finished its own preparation so that a 813 * listener cannot interleave with it. The repository, the controller 814 * and this table instance are deliberately not passed. 815 * 816 * @since 0.3.0 817 * 818 * @param array<int,array<string,mixed>> $items Rows on the current page. 819 */ 820 do_action( 'nldcf7_entry_list_items_prepared', $this->items ); 553 821 } 554 822 -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/admin/views/entry-detail.php
r3666139 r3666532 12 12 * $notice array|null Flash notice ['message'=>string,'type'=>string] 13 13 * pulled from per-user transient (consume_notice()). 14 * $show_history_note bool Whether the pre-upgrade history caveat applies. 14 15 * 15 16 * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Locals in an included view, not actual globals. … … 140 141 <div class="nldcf7-card nldcf7-timeline-card"> 141 142 <h2><?php esc_html_e( 'Activity', 'neutrope-lead-desk-for-contact-form-7' ); ?></h2> 142 <p class="description"> 143 <?php esc_html_e( 'History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved.', 'neutrope-lead-desk-for-contact-form-7' ); ?> 144 </p> 143 <?php if ( $show_history_note ) : ?> 144 <p class="description"> 145 <?php esc_html_e( 'History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved.', 'neutrope-lead-desk-for-contact-form-7' ); ?> 146 </p> 147 <?php endif; ?> 145 148 146 149 <?php if ( empty( $events ) ) : ?> … … 222 225 </div> 223 226 227 <?php 228 /** 229 * Fires after the main column's own cards on the entry detail screen. 230 * 231 * Add-ons print their own markup here -- including the surrounding 232 * `<div class="nldcf7-card">` -- and own escaping it. Nothing is 233 * buffered or filtered on the way out, so a listener that prints 234 * nothing costs nothing and leaves the page byte-identical. 235 * 236 * The position is deliberately "after every core card" rather than 237 * between two of them: card order is an internal detail that may 238 * change, and a between-cards contract could not be kept stable. 239 * 240 * @since 0.3.0 241 * 242 * @param array $entry Entry row, as stored. Passed by value. 243 */ 244 do_action( 'nldcf7_entry_detail_main', $entry ); 245 ?> 246 224 247 </div> 225 248 … … 273 296 </table> 274 297 </div> 298 299 <?php 300 /** 301 * Fires after the sidebar's own cards on the entry detail screen. 302 * 303 * Same ownership rules as `nldcf7_entry_detail_main`: the listener 304 * prints and escapes its own markup. Two hooks exist rather than one 305 * because an add-on may legitimately need to place content in both 306 * columns of the same screen. 307 * 308 * @since 0.3.0 309 * 310 * @param array $entry Entry row, as stored. Passed by value. 311 */ 312 do_action( 'nldcf7_entry_detail_sidebar', $entry ); 313 ?> 275 314 </aside> 276 315 </div> -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/includes/class-entry-repository.php
r3666139 r3666532 14 14 15 15 class NLDCF7_Entry_Repository { 16 17 /** 18 * Query contexts passed to the query extension hooks. 19 * 20 * The entries screen and the CSV exporter share this repository, so an 21 * add-on constraint that did not say which one it meant would change both. 22 * Listeners check $args['context'] to scope themselves. 23 * 24 * @since 0.3.0 25 */ 26 const CONTEXT_LIST = 'list'; 27 const CONTEXT_EXPORT = 'export'; 28 const CONTEXT_DEFAULT = 'default'; 16 29 17 30 /** … … 133 146 public function count( array $args = array() ) { 134 147 global $wpdb; 135 $where = $this->build_where( $args ); 136 $table = NLDCF7_DB::entries_table(); 137 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from internal NLDCF7_DB method. 138 $sql = "SELECT COUNT(*) FROM {$table} {$where['clause']}"; 139 $prepared = $where['prepare_args'] 140 ? $wpdb->prepare( $sql, $where['prepare_args'] ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 148 149 // Same builder as query(). Sharing it is what makes it impossible for a 150 // filtered list to report an unfiltered total: there is one query plan, 151 // not two implementations that have to be kept in step. 152 $parts = $this->build_query_parts( $args ); 153 $table = NLDCF7_DB::entries_table(); 154 $distinct = $parts['distinct'] ? 'DISTINCT ' : ''; 155 156 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from internal NLDCF7_DB method; join/clause carry no request values (those are in prepare_args). 157 $sql = "SELECT COUNT({$distinct}{$table}.id) FROM {$table} {$parts['join']} {$parts['clause']}"; 158 $prepared = $parts['prepare_args'] 159 ? $wpdb->prepare( $sql, $parts['prepare_args'] ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 141 160 : $sql; 142 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Internal plugin table, built via build_ where() with prepare_args.161 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Internal plugin table, built via build_query_parts() with prepare_args. 143 162 return (int) $wpdb->get_var( $prepared ); 144 163 } … … 176 195 $args = array_merge( $defaults, $args ); 177 196 178 $ where = $this->build_where( $args );197 $parts = $this->build_query_parts( $args ); 179 198 180 199 $orderby = $this->sanitize_orderby( $args['orderby'] ); … … 185 204 $offset = ( $page - 1 ) * $per_page; 186 205 187 $table = NLDCF7_DB::entries_table(); 188 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table / orderby / order are internal allow-listed values; user input is carried in $prepare_args. 189 $sql = "SELECT * FROM {$table} {$where['clause']} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; 190 191 $prepare_args = $where['prepare_args']; 206 $table = NLDCF7_DB::entries_table(); 207 $distinct = $parts['distinct'] ? 'DISTINCT ' : ''; 208 209 // `{$table}.*` rather than `*`: once an add-on may contribute a JOIN, a 210 // bare `*` would also pull the joined table's columns into the row and 211 // any shared column name (id, created_at) would overwrite ours. With no 212 // join present the two forms select exactly the same thing, so this is 213 // behaviour-preserving when nothing is listening. 214 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table / orderby / order are internal allow-listed values; join/clause carry no request values (those are in prepare_args). 215 $sql = "SELECT {$distinct}{$table}.* FROM {$table} {$parts['join']} {$parts['clause']} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; 216 217 $prepare_args = $parts['prepare_args']; 192 218 $prepare_args[] = $per_page; 193 219 $prepare_args[] = $offset; … … 463 489 } 464 490 491 /** 492 * Build the complete constraint plan for one query. 493 * 494 * This is the single source of JOIN / WHERE / prepared values / DISTINCT for 495 * BOTH query() and count(). Extending it in one place is what guarantees a 496 * filtered list and its total can never disagree. 497 * 498 * Two extension points run here, in this order: 499 * 500 * 1. `nldcf7_entry_query_args` -- high level. Adjust the documented 501 * args (including the long-standing `ids`) without writing SQL. 502 * 2. `nldcf7_entry_query_clauses` -- low level. Contribute a JOIN and 503 * extra conditions with their values kept separate, for constraints 504 * that live in another table and should be resolved by the database 505 * rather than by loading every matching id into PHP. 506 * 507 * @param array $args Query args. `context` identifies the caller. 508 * @return array{join:string,clause:string,prepare_args:array,distinct:bool} 509 */ 510 private function build_query_parts( array $args ) { 511 if ( ! isset( $args['context'] ) || ! is_string( $args['context'] ) || '' === $args['context'] ) { 512 $args['context'] = self::CONTEXT_DEFAULT; 513 } 514 515 /** 516 * Filter the entry query args before the SQL is built. 517 * 518 * Use this when the constraint can be said with the args the repository 519 * already understands -- most usefully `ids`, which restricts the query 520 * to a set of entry IDs. Anything needing a JOIN belongs in 521 * `nldcf7_entry_query_clauses` instead. 522 * 523 * `$args['context']` says who is asking: 'list' for the entries screen, 524 * 'export' for CSV, 'default' otherwise. Listeners should check it -- 525 * both callers share this repository, so an unconditional constraint 526 * meant for the list screen would silently change what CSV exports. 527 * 528 * @since 0.3.0 529 * 530 * @param array $args Query args, including 'context'. 531 */ 532 $filtered = apply_filters( 'nldcf7_entry_query_args', $args ); 533 if ( is_array( $filtered ) ) { 534 $args = $filtered; 535 } 536 537 $where = $this->build_where( $args ); 538 539 $parts = array( 540 'join' => '', 541 'where' => array(), 542 'prepare_args' => array(), 543 'distinct' => false, 544 ); 545 546 /** 547 * Filter the SQL parts for an entry query. 548 * 549 * Contribute a JOIN and conditions whose values stay separate from the 550 * SQL: put `%s` / `%d` placeholders in each `where` entry and the values 551 * in `prepare_args`, in the same order. Free passes them to 552 * $wpdb->prepare(). A single pre-built SQL string is deliberately not 553 * accepted, so request values are never concatenated into a statement. 554 * 555 * Set `distinct` when the JOIN can match a row more than once; it is 556 * applied to the row query and the count together. 557 * 558 * Table and column identifiers inside `join` are the listener's own 559 * responsibility -- Free cannot validate another plugin's schema. 560 * 561 * @since 0.3.0 562 * 563 * @param array{join:string,where:array,prepare_args:array,distinct:bool} $parts 564 * @param array $args Query args, including 'context'. 565 */ 566 $contributed = apply_filters( 'nldcf7_entry_query_clauses', $parts, $args ); 567 568 $join = ''; 569 $conditions = array(); 570 $prepare_args = $where['prepare_args']; 571 $distinct = false; 572 573 if ( is_array( $contributed ) ) { 574 $join = isset( $contributed['join'] ) && is_string( $contributed['join'] ) ? trim( $contributed['join'] ) : ''; 575 $distinct = ! empty( $contributed['distinct'] ); 576 577 $raw_where = isset( $contributed['where'] ) && is_array( $contributed['where'] ) ? $contributed['where'] : array(); 578 $raw_prep = isset( $contributed['prepare_args'] ) && is_array( $contributed['prepare_args'] ) ? array_values( $contributed['prepare_args'] ) : array(); 579 580 foreach ( $raw_where as $condition ) { 581 if ( is_string( $condition ) && '' !== trim( $condition ) ) { 582 $conditions[] = '( ' . trim( $condition ) . ' )'; 583 } 584 } 585 586 // The placeholder count has to match the values supplied, or 587 // $wpdb->prepare() would mis-bind every value after the mismatch and 588 // return a broken statement. Rather than run that, drop the whole 589 // contribution and say so while debugging. 590 $expected = 0; 591 foreach ( $conditions as $condition ) { 592 $expected += preg_match_all( '/%[dfs]/', $condition ); 593 } 594 if ( $expected !== count( $raw_prep ) ) { 595 $this->debug_clauses( 596 sprintf( 'placeholder/value mismatch (%d placeholders, %d values); clause contribution dropped', $expected, count( $raw_prep ) ) 597 ); 598 $join = ''; 599 $conditions = array(); 600 $raw_prep = array(); 601 $distinct = false; 602 } 603 604 foreach ( $raw_prep as $value ) { 605 $prepare_args[] = $value; 606 } 607 } 608 609 $clause = $where['clause']; 610 if ( $conditions ) { 611 $extra = implode( ' AND ', $conditions ); 612 $clause = '' === $clause ? ( 'WHERE ' . $extra ) : ( $clause . ' AND ' . $extra ); 613 } 614 615 return array( 616 'join' => $join, 617 'clause' => $clause, 618 'prepare_args' => $prepare_args, 619 'distinct' => (bool) $distinct, 620 ); 621 } 622 623 /** 624 * Log a rejected clause contribution while debugging. 625 * 626 * @param string $message 627 * @return void 628 */ 629 private function debug_clauses( $message ) { 630 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 631 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated by WP_DEBUG; a silently dropped clause would otherwise look like a filter that simply did not fire. 632 error_log( '[neutrope-lead-desk-for-contact-form-7] entry query clauses: ' . $message ); 633 } 634 } 635 465 636 private function sanitize_orderby( $field ) { 466 637 $allowed = array( 'id', 'submitted_at', 'status', 'form_post_id' ); -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/includes/class-export-service.php
r3553617 r3666532 132 132 'orderby' => 'submitted_at', 133 133 'order' => 'DESC', 134 // Tells the query extension hooks this is CSV export, not the 135 // entries screen, so a list-only constraint cannot reach it. 136 'context' => NLDCF7_Entry_Repository::CONTEXT_EXPORT, 134 137 ) 135 138 ); -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/languages/neutrope-lead-desk-for-contact-form-7-ja.po
r3666139 r3666532 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0. 2.0\n"5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/neutrope-lead-desk-for-contact-form-7\n" 7 7 "Last-Translator: \n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-08-2 5T14:05:36+00:00\n"13 "PO-Revision-Date: 2026-08-2 500:00+0000\n"12 "POT-Creation-Date: 2026-08-26T04:22:33+00:00\n" 13 "PO-Revision-Date: 2026-08-26 00:00+0000\n" 14 14 "Language: ja\n" 15 15 "Plural-Forms: nplurals=1; plural=0;\n" … … 154 154 msgstr "指定された問い合わせは見つかりませんでした。" 155 155 156 #: admin/class-entry-list-table.php: 211156 #: admin/class-entry-list-table.php:336 157 157 #: admin/views/export.php:43 158 158 msgid "All" 159 159 msgstr "すべて" 160 160 161 #: admin/class-entry-list-table.php: 227162 #: admin/class-entry-list-table.php: 361161 #: admin/class-entry-list-table.php:352 162 #: admin/class-entry-list-table.php:570 163 163 #: admin/views/export.php:72 164 164 msgid "Trash" … … 166 166 167 167 #: admin/class-entry-event-presenter.php:53 168 #: admin/class-entry-list-table.php: 238169 #: admin/views/entry-detail.php:2 53168 #: admin/class-entry-list-table.php:363 169 #: admin/views/entry-detail.php:276 170 170 #: includes/class-export-service.php:47 171 171 msgid "Received" 172 172 msgstr "受信日時" 173 173 174 #: admin/class-entry-list-table.php: 239175 #: admin/class-entry-list-table.php: 471176 #: admin/views/entry-detail.php:2 28174 #: admin/class-entry-list-table.php:364 175 #: admin/class-entry-list-table.php:680 176 #: admin/views/entry-detail.php:251 177 177 #: admin/views/export.php:40 178 178 #: includes/class-export-service.php:48 … … 180 180 msgstr "ステータス" 181 181 182 #: admin/class-entry-list-table.php: 240183 #: admin/class-entry-list-table.php: 479184 #: admin/views/entry-detail.php:2 54182 #: admin/class-entry-list-table.php:365 183 #: admin/class-entry-list-table.php:688 184 #: admin/views/entry-detail.php:277 185 185 #: admin/views/export.php:51 186 186 #: includes/class-export-service.php:49 … … 188 188 msgstr "フォーム" 189 189 190 #: admin/class-entry-list-table.php: 241190 #: admin/class-entry-list-table.php:366 191 191 #: admin/views/settings.php:73 192 192 #: includes/class-export-service.php:51 … … 194 194 msgstr "名前" 195 195 196 #: admin/class-entry-list-table.php: 242196 #: admin/class-entry-list-table.php:367 197 197 #: admin/views/settings.php:77 198 198 #: includes/class-export-service.php:52 … … 200 200 msgstr "メール" 201 201 202 #: admin/class-entry-list-table.php: 243202 #: admin/class-entry-list-table.php:368 203 203 #: admin/views/settings.php:81 204 204 #: includes/class-export-service.php:53 … … 206 206 msgstr "電話" 207 207 208 #: admin/class-entry-list-table.php: 244208 #: admin/class-entry-list-table.php:369 209 209 msgid "Source" 210 210 msgstr "参照元" 211 211 212 #: admin/class-entry-list-table.php: 245212 #: admin/class-entry-list-table.php:370 213 213 msgid "UTM source" 214 214 msgstr "UTM source" 215 215 216 #: admin/class-entry-list-table.php: 246217 #: admin/views/entry-detail.php:9 6216 #: admin/class-entry-list-table.php:371 217 #: admin/views/entry-detail.php:97 218 218 msgid "Notes" 219 219 msgstr "メモ" 220 220 221 #: admin/class-entry-list-table.php: 264222 #: admin/class-entry-list-table.php: 342221 #: admin/class-entry-list-table.php:473 222 #: admin/class-entry-list-table.php:551 223 223 msgid "Restore" 224 224 msgstr "復元" 225 225 226 #: admin/class-entry-list-table.php: 269226 #: admin/class-entry-list-table.php:478 227 227 msgid "Set status: New" 228 228 msgstr "ステータス: 未対応" 229 229 230 #: admin/class-entry-list-table.php: 270230 #: admin/class-entry-list-table.php:479 231 231 msgid "Set status: In progress" 232 232 msgstr "ステータス: 対応中" 233 233 234 #: admin/class-entry-list-table.php: 271234 #: admin/class-entry-list-table.php:480 235 235 msgid "Set status: On hold" 236 236 msgstr "ステータス: 保留" 237 237 238 #: admin/class-entry-list-table.php: 272238 #: admin/class-entry-list-table.php:481 239 239 msgid "Set status: Done" 240 240 msgstr "ステータス: 完了" 241 241 242 #: admin/class-entry-list-table.php: 273242 #: admin/class-entry-list-table.php:482 243 243 msgid "Move to trash" 244 244 msgstr "ゴミ箱へ移動" 245 245 246 #: admin/class-entry-list-table.php: 274246 #: admin/class-entry-list-table.php:483 247 247 msgid "Mark as spam" 248 248 msgstr "スパムにする" 249 249 250 #: admin/class-entry-list-table.php: 299250 #: admin/class-entry-list-table.php:508 251 251 msgid "View" 252 252 msgstr "詳細" 253 253 254 #: admin/class-entry-list-table.php: 430254 #: admin/class-entry-list-table.php:639 255 255 msgid "None" 256 256 msgstr "なし" 257 257 258 #: admin/class-entry-list-table.php: 437258 #: admin/class-entry-list-table.php:646 259 259 msgid "No entries are in the trash." 260 260 msgstr "ゴミ箱に問い合わせはありません。" 261 261 262 #: admin/class-entry-list-table.php: 440262 #: admin/class-entry-list-table.php:649 263 263 msgid "No entries match the current filters." 264 264 msgstr "該当する問い合わせはありません。" 265 265 266 #: admin/class-entry-list-table.php: 468267 #: admin/class-entry-list-table.php: 469266 #: admin/class-entry-list-table.php:677 267 #: admin/class-entry-list-table.php:678 268 268 msgid "Search keywords" 269 269 msgstr "キーワード検索" 270 270 271 #: admin/class-entry-list-table.php: 473271 #: admin/class-entry-list-table.php:682 272 272 msgid "All statuses" 273 273 msgstr "すべてのステータス" 274 274 275 #: admin/class-entry-list-table.php: 481275 #: admin/class-entry-list-table.php:690 276 276 #: admin/views/export.php:54 277 277 msgid "All forms" 278 278 msgstr "すべてのフォーム" 279 279 280 #: admin/class-entry-list-table.php: 487280 #: admin/class-entry-list-table.php:696 281 281 #: admin/views/export.php:32 282 282 msgid "From date" 283 283 msgstr "開始日" 284 284 285 #: admin/class-entry-list-table.php: 489285 #: admin/class-entry-list-table.php:698 286 286 #: admin/views/export.php:36 287 287 msgid "To date" 288 288 msgstr "終了日" 289 289 290 #: admin/class-entry-list-table.php: 494290 #: admin/class-entry-list-table.php:703 291 291 msgid "Show spam" 292 292 msgstr "スパムも表示" 293 293 294 #: admin/class-entry-list-table.php: 497294 #: admin/class-entry-list-table.php:726 295 295 msgid "Filter" 296 296 msgstr "絞り込み" 297 297 298 298 #. translators: %d: entry ID 299 #: admin/views/entry-detail.php:3 1299 #: admin/views/entry-detail.php:32 300 300 #, php-format 301 301 msgid "Entry #%d" 302 302 msgstr "問い合わせ #%d" 303 303 304 #: admin/views/entry-detail.php:3 4304 #: admin/views/entry-detail.php:35 305 305 msgid "Back to list" 306 306 msgstr "一覧へ戻る" 307 307 308 #: admin/views/entry-detail.php:4 7308 #: admin/views/entry-detail.php:48 309 309 msgid "Submitted fields" 310 310 msgstr "送信内容" 311 311 312 #: admin/views/entry-detail.php:5 1312 #: admin/views/entry-detail.php:52 313 313 msgid "No submitted fields stored." 314 314 msgstr "送信フィールドが保存されていません。" 315 315 316 #: admin/views/entry-detail.php:7 2316 #: admin/views/entry-detail.php:73 317 317 msgid "Attachments (metadata only)" 318 318 msgstr "添付ファイル(メタ情報のみ)" 319 319 320 #: admin/views/entry-detail.php:7 6320 #: admin/views/entry-detail.php:77 321 321 msgid "File name" 322 322 msgstr "ファイル名" 323 323 324 #: admin/views/entry-detail.php:7 7324 #: admin/views/entry-detail.php:78 325 325 msgid "MIME" 326 326 msgstr "MIME" 327 327 328 #: admin/views/entry-detail.php:7 8328 #: admin/views/entry-detail.php:79 329 329 msgid "Size (bytes)" 330 330 msgstr "サイズ (bytes)" 331 331 332 #: admin/views/entry-detail.php:9 1332 #: admin/views/entry-detail.php:92 333 333 msgid "Only metadata is captured. The uploaded files themselves are not stored by this plugin." 334 334 msgstr "添付ファイルの実体は保存せず、メタ情報(ファイル名・MIME・サイズ)のみを記録しています。" 335 335 336 #: admin/views/entry-detail.php:10 2336 #: admin/views/entry-detail.php:103 337 337 msgid "Note body" 338 338 msgstr "メモ本文" 339 339 340 #: admin/views/entry-detail.php:10 5340 #: admin/views/entry-detail.php:106 341 341 msgid "Add note" 342 342 msgstr "メモを追加" 343 343 344 #: admin/views/entry-detail.php:1 09344 #: admin/views/entry-detail.php:110 345 345 msgid "No notes yet." 346 346 msgstr "まだメモはありません。" 347 347 348 348 #: admin/class-entry-event-presenter.php:119 349 #: admin/views/entry-detail.php:11 5349 #: admin/views/entry-detail.php:116 350 350 msgid "Unknown user" 351 351 msgstr "不明なユーザー" 352 352 353 #: admin/views/entry-detail.php:2 38353 #: admin/views/entry-detail.php:261 354 354 msgid "Change status" 355 355 msgstr "ステータスを変更" 356 356 357 #: admin/views/entry-detail.php:2 46357 #: admin/views/entry-detail.php:269 358 358 msgid "Update" 359 359 msgstr "更新" 360 360 361 #: admin/views/entry-detail.php:2 51361 #: admin/views/entry-detail.php:274 362 362 msgid "Submission info" 363 363 msgstr "送信情報" 364 364 365 #: admin/views/entry-detail.php:2 55365 #: admin/views/entry-detail.php:278 366 366 #: includes/class-export-service.php:50 367 367 msgid "Form ID" 368 368 msgstr "フォーム ID" 369 369 370 #: admin/views/entry-detail.php:2 56370 #: admin/views/entry-detail.php:279 371 371 #: includes/class-export-service.php:55 372 372 msgid "Submission page URL" 373 373 msgstr "送信ページ URL" 374 374 375 #: admin/views/entry-detail.php:2 60375 #: admin/views/entry-detail.php:283 376 376 #: includes/class-export-service.php:56 377 377 msgid "Referrer" 378 378 msgstr "参照元" 379 379 380 #: admin/views/entry-detail.php:2 64380 #: admin/views/entry-detail.php:287 381 381 #: includes/class-export-service.php:57 382 382 msgid "Landing URL" 383 383 msgstr "ランディング URL" 384 384 385 #: admin/views/entry-detail.php:2 70385 #: admin/views/entry-detail.php:293 386 386 msgid "User Agent" 387 387 msgstr "User Agent" 388 388 389 #: admin/views/entry-detail.php:2 71389 #: admin/views/entry-detail.php:294 390 390 #: includes/class-export-service.php:64 391 391 msgid "IP address" 392 392 msgstr "IP アドレス" 393 393 394 #: admin/views/entry-detail.php:2 72394 #: admin/views/entry-detail.php:295 395 395 #: includes/class-export-service.php:65 396 396 msgid "Spam flag" 397 397 msgstr "スパム判定" 398 398 399 #: admin/views/entry-detail.php:2 72399 #: admin/views/entry-detail.php:295 400 400 #: admin/views/export.php:62 401 401 msgid "Spam" 402 402 msgstr "スパム" 403 403 404 #: admin/views/entry-detail.php:2 72404 #: admin/views/entry-detail.php:295 405 405 msgid "Normal" 406 406 msgstr "通常" … … 665 665 msgstr "設定を保存" 666 666 667 #: includes/class-entry-repository.php: 25667 #: includes/class-entry-repository.php:38 668 668 msgid "New" 669 669 msgstr "未対応" 670 670 671 #: includes/class-entry-repository.php: 26671 #: includes/class-entry-repository.php:39 672 672 msgid "In progress" 673 673 msgstr "対応中" 674 674 675 #: includes/class-entry-repository.php: 27675 #: includes/class-entry-repository.php:40 676 676 msgid "On hold" 677 677 msgstr "保留" 678 678 679 #: includes/class-entry-repository.php: 28679 #: includes/class-entry-repository.php:41 680 680 msgid "Done" 681 681 msgstr "完了" … … 693 693 msgstr "過去の問い合わせから対応履歴を準備しています。通常どおりご利用いただけます。" 694 694 695 #. translators: 1: number of records processed, 2: total records, 3: percentage complete696 #: admin/class-admin-menu.php:188697 #, php-format698 msgid "%1$s of %2$s (%3$s%%)"699 msgstr "%2$s 件中 %1$s 件(%3$s%%)"700 701 695 #: admin/class-entry-event-presenter.php:54 702 696 msgid "Status changed" … … 720 714 721 715 #: admin/class-entry-event-presenter.php:89 722 #: admin/views/entry-detail.php:14 1716 #: admin/views/entry-detail.php:142 723 717 msgid "Activity" 724 718 msgstr "対応履歴" … … 762 756 msgstr "メモを見る" 763 757 764 #: admin/views/entry-detail.php:14 3758 #: admin/views/entry-detail.php:145 765 759 msgid "History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved." 766 760 msgstr "アップデート前の履歴は、受信日時と既存メモから確認できる範囲のみ表示されます。" 767 761 768 #: admin/views/entry-detail.php:15 5762 #: admin/views/entry-detail.php:158 769 763 msgid "No activity on this page." 770 764 msgstr "このページに表示する対応履歴はありません。" 771 765 772 #: admin/views/entry-detail.php:1 57766 #: admin/views/entry-detail.php:160 773 767 msgid "Back to the latest activity" 774 768 msgstr "最新の対応履歴へ戻る" 775 769 776 #: admin/views/entry-detail.php:16 2770 #: admin/views/entry-detail.php:165 777 771 msgid "Preparing activity history…" 778 772 msgstr "対応履歴を準備しています…" 779 773 780 #: admin/views/entry-detail.php:16 4774 #: admin/views/entry-detail.php:167 781 775 msgid "No activity recorded yet." 782 776 msgstr "対応履歴はまだありません。" 783 777 784 #: admin/views/entry-detail.php: 198778 #: admin/views/entry-detail.php:201 785 779 msgid "Activity pagination" 786 780 msgstr "対応履歴のページ送り" 787 781 788 782 #. translators: 1: current page number, 2: total number of pages 789 #: admin/views/entry-detail.php:20 3783 #: admin/views/entry-detail.php:206 790 784 #, php-format 791 785 msgid "Page %1$s of %2$s" 792 786 msgstr "%2$s ページ中 %1$s ページ" 793 787 794 #: admin/views/entry-detail.php:21 1788 #: admin/views/entry-detail.php:214 795 789 msgid "Newer activity" 796 790 msgstr "新しい対応履歴" 797 791 798 #: admin/views/entry-detail.php:21 6792 #: admin/views/entry-detail.php:219 799 793 msgid "Older activity" 800 794 msgstr "古い対応履歴" … … 815 809 msgid "For entries received before this feature was added, activity is limited to the received timestamp and any internal notes already saved. Earlier status, trash, and spam changes were never recorded, so they are not shown." 816 810 msgstr "この機能が追加される前に受信した問い合わせでは、対応履歴は受信日時と保存済みのメモのみとなります。それ以前のステータス・ゴミ箱・スパムの変更は記録されていないため、表示されません。" 811 812 #. translators: 1: number of records processed, 2: total records to process, 3: percentage complete. "Records" counts processing units (entries plus notes), NOT the number of inquiries. 813 #: admin/class-admin-menu.php:188 814 #, php-format 815 msgid "Processed %1$s of %2$s records (%3$s%%)." 816 msgstr "%2$s 件の履歴データのうち %1$s 件を処理しました(%3$s%%)。" -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/languages/neutrope-lead-desk-for-contact-form-7.pot
r3666139 r3666532 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0. 2.0\n"5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/neutrope-lead-desk-for-contact-form-7\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-08-2 5T14:05:36+00:00\n"12 "POT-Creation-Date: 2026-08-26T04:22:33+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 70 70 msgstr "" 71 71 72 #. translators: 1: number of records processed, 2: total records , 3: percentage complete72 #. translators: 1: number of records processed, 2: total records to process, 3: percentage complete. "Records" counts processing units (entries plus notes), NOT the number of inquiries. 73 73 #: admin/class-admin-menu.php:188 74 74 #, php-format 75 msgid " %1$s of %2$s (%3$s%%)"75 msgid "Processed %1$s of %2$s records (%3$s%%)." 76 76 msgstr "" 77 77 … … 172 172 173 173 #: admin/class-entry-event-presenter.php:53 174 #: admin/class-entry-list-table.php: 238175 #: admin/views/entry-detail.php:2 53174 #: admin/class-entry-list-table.php:363 175 #: admin/views/entry-detail.php:276 176 176 #: includes/class-export-service.php:47 177 177 msgid "Received" … … 199 199 200 200 #: admin/class-entry-event-presenter.php:89 201 #: admin/views/entry-detail.php:14 1201 #: admin/views/entry-detail.php:142 202 202 msgid "Activity" 203 203 msgstr "" … … 208 208 209 209 #: admin/class-entry-event-presenter.php:119 210 #: admin/views/entry-detail.php:11 5210 #: admin/views/entry-detail.php:116 211 211 msgid "Unknown user" 212 212 msgstr "" … … 246 246 msgstr "" 247 247 248 #: admin/class-entry-list-table.php: 211248 #: admin/class-entry-list-table.php:336 249 249 #: admin/views/export.php:43 250 250 msgid "All" 251 251 msgstr "" 252 252 253 #: admin/class-entry-list-table.php: 227254 #: admin/class-entry-list-table.php: 361253 #: admin/class-entry-list-table.php:352 254 #: admin/class-entry-list-table.php:570 255 255 #: admin/views/export.php:72 256 256 msgid "Trash" 257 257 msgstr "" 258 258 259 #: admin/class-entry-list-table.php: 239260 #: admin/class-entry-list-table.php: 471261 #: admin/views/entry-detail.php:2 28259 #: admin/class-entry-list-table.php:364 260 #: admin/class-entry-list-table.php:680 261 #: admin/views/entry-detail.php:251 262 262 #: admin/views/export.php:40 263 263 #: includes/class-export-service.php:48 … … 265 265 msgstr "" 266 266 267 #: admin/class-entry-list-table.php: 240268 #: admin/class-entry-list-table.php: 479269 #: admin/views/entry-detail.php:2 54267 #: admin/class-entry-list-table.php:365 268 #: admin/class-entry-list-table.php:688 269 #: admin/views/entry-detail.php:277 270 270 #: admin/views/export.php:51 271 271 #: includes/class-export-service.php:49 … … 273 273 msgstr "" 274 274 275 #: admin/class-entry-list-table.php: 241275 #: admin/class-entry-list-table.php:366 276 276 #: admin/views/settings.php:73 277 277 #: includes/class-export-service.php:51 … … 279 279 msgstr "" 280 280 281 #: admin/class-entry-list-table.php: 242281 #: admin/class-entry-list-table.php:367 282 282 #: admin/views/settings.php:77 283 283 #: includes/class-export-service.php:52 … … 285 285 msgstr "" 286 286 287 #: admin/class-entry-list-table.php: 243287 #: admin/class-entry-list-table.php:368 288 288 #: admin/views/settings.php:81 289 289 #: includes/class-export-service.php:53 … … 291 291 msgstr "" 292 292 293 #: admin/class-entry-list-table.php: 244293 #: admin/class-entry-list-table.php:369 294 294 msgid "Source" 295 295 msgstr "" 296 296 297 #: admin/class-entry-list-table.php: 245297 #: admin/class-entry-list-table.php:370 298 298 msgid "UTM source" 299 299 msgstr "" 300 300 301 #: admin/class-entry-list-table.php: 246302 #: admin/views/entry-detail.php:9 6301 #: admin/class-entry-list-table.php:371 302 #: admin/views/entry-detail.php:97 303 303 msgid "Notes" 304 304 msgstr "" 305 305 306 #: admin/class-entry-list-table.php: 264307 #: admin/class-entry-list-table.php: 342306 #: admin/class-entry-list-table.php:473 307 #: admin/class-entry-list-table.php:551 308 308 msgid "Restore" 309 309 msgstr "" 310 310 311 #: admin/class-entry-list-table.php: 269311 #: admin/class-entry-list-table.php:478 312 312 msgid "Set status: New" 313 313 msgstr "" 314 314 315 #: admin/class-entry-list-table.php: 270315 #: admin/class-entry-list-table.php:479 316 316 msgid "Set status: In progress" 317 317 msgstr "" 318 318 319 #: admin/class-entry-list-table.php: 271319 #: admin/class-entry-list-table.php:480 320 320 msgid "Set status: On hold" 321 321 msgstr "" 322 322 323 #: admin/class-entry-list-table.php: 272323 #: admin/class-entry-list-table.php:481 324 324 msgid "Set status: Done" 325 325 msgstr "" 326 326 327 #: admin/class-entry-list-table.php: 273327 #: admin/class-entry-list-table.php:482 328 328 msgid "Move to trash" 329 329 msgstr "" 330 330 331 #: admin/class-entry-list-table.php: 274331 #: admin/class-entry-list-table.php:483 332 332 msgid "Mark as spam" 333 333 msgstr "" 334 334 335 #: admin/class-entry-list-table.php: 299335 #: admin/class-entry-list-table.php:508 336 336 msgid "View" 337 337 msgstr "" 338 338 339 #: admin/class-entry-list-table.php: 430339 #: admin/class-entry-list-table.php:639 340 340 msgid "None" 341 341 msgstr "" 342 342 343 #: admin/class-entry-list-table.php: 437343 #: admin/class-entry-list-table.php:646 344 344 msgid "No entries are in the trash." 345 345 msgstr "" 346 346 347 #: admin/class-entry-list-table.php: 440347 #: admin/class-entry-list-table.php:649 348 348 msgid "No entries match the current filters." 349 349 msgstr "" 350 350 351 #: admin/class-entry-list-table.php: 468352 #: admin/class-entry-list-table.php: 469351 #: admin/class-entry-list-table.php:677 352 #: admin/class-entry-list-table.php:678 353 353 msgid "Search keywords" 354 354 msgstr "" 355 355 356 #: admin/class-entry-list-table.php: 473356 #: admin/class-entry-list-table.php:682 357 357 msgid "All statuses" 358 358 msgstr "" 359 359 360 #: admin/class-entry-list-table.php: 481360 #: admin/class-entry-list-table.php:690 361 361 #: admin/views/export.php:54 362 362 msgid "All forms" 363 363 msgstr "" 364 364 365 #: admin/class-entry-list-table.php: 487365 #: admin/class-entry-list-table.php:696 366 366 #: admin/views/export.php:32 367 367 msgid "From date" 368 368 msgstr "" 369 369 370 #: admin/class-entry-list-table.php: 489370 #: admin/class-entry-list-table.php:698 371 371 #: admin/views/export.php:36 372 372 msgid "To date" 373 373 msgstr "" 374 374 375 #: admin/class-entry-list-table.php: 494375 #: admin/class-entry-list-table.php:703 376 376 msgid "Show spam" 377 377 msgstr "" 378 378 379 #: admin/class-entry-list-table.php: 497379 #: admin/class-entry-list-table.php:726 380 380 msgid "Filter" 381 381 msgstr "" 382 382 383 383 #. translators: %d: entry ID 384 #: admin/views/entry-detail.php:3 1384 #: admin/views/entry-detail.php:32 385 385 #, php-format 386 386 msgid "Entry #%d" 387 387 msgstr "" 388 388 389 #: admin/views/entry-detail.php:3 4389 #: admin/views/entry-detail.php:35 390 390 msgid "Back to list" 391 391 msgstr "" 392 392 393 #: admin/views/entry-detail.php:4 7393 #: admin/views/entry-detail.php:48 394 394 msgid "Submitted fields" 395 395 msgstr "" 396 396 397 #: admin/views/entry-detail.php:5 1397 #: admin/views/entry-detail.php:52 398 398 msgid "No submitted fields stored." 399 399 msgstr "" 400 400 401 #: admin/views/entry-detail.php:7 2401 #: admin/views/entry-detail.php:73 402 402 msgid "Attachments (metadata only)" 403 403 msgstr "" 404 404 405 #: admin/views/entry-detail.php:7 6405 #: admin/views/entry-detail.php:77 406 406 msgid "File name" 407 407 msgstr "" 408 408 409 #: admin/views/entry-detail.php:7 7409 #: admin/views/entry-detail.php:78 410 410 msgid "MIME" 411 411 msgstr "" 412 412 413 #: admin/views/entry-detail.php:7 8413 #: admin/views/entry-detail.php:79 414 414 msgid "Size (bytes)" 415 415 msgstr "" 416 416 417 #: admin/views/entry-detail.php:9 1417 #: admin/views/entry-detail.php:92 418 418 msgid "Only metadata is captured. The uploaded files themselves are not stored by this plugin." 419 419 msgstr "" 420 420 421 #: admin/views/entry-detail.php:10 2421 #: admin/views/entry-detail.php:103 422 422 msgid "Note body" 423 423 msgstr "" 424 424 425 #: admin/views/entry-detail.php:10 5425 #: admin/views/entry-detail.php:106 426 426 msgid "Add note" 427 427 msgstr "" 428 428 429 #: admin/views/entry-detail.php:1 09429 #: admin/views/entry-detail.php:110 430 430 msgid "No notes yet." 431 431 msgstr "" 432 432 433 #: admin/views/entry-detail.php:14 3433 #: admin/views/entry-detail.php:145 434 434 msgid "History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved." 435 435 msgstr "" 436 436 437 #: admin/views/entry-detail.php:15 5437 #: admin/views/entry-detail.php:158 438 438 msgid "No activity on this page." 439 439 msgstr "" 440 440 441 #: admin/views/entry-detail.php:1 57441 #: admin/views/entry-detail.php:160 442 442 msgid "Back to the latest activity" 443 443 msgstr "" 444 444 445 #: admin/views/entry-detail.php:16 2445 #: admin/views/entry-detail.php:165 446 446 msgid "Preparing activity history…" 447 447 msgstr "" 448 448 449 #: admin/views/entry-detail.php:16 4449 #: admin/views/entry-detail.php:167 450 450 msgid "No activity recorded yet." 451 451 msgstr "" 452 452 453 #: admin/views/entry-detail.php: 198453 #: admin/views/entry-detail.php:201 454 454 msgid "Activity pagination" 455 455 msgstr "" 456 456 457 457 #. translators: 1: current page number, 2: total number of pages 458 #: admin/views/entry-detail.php:20 3458 #: admin/views/entry-detail.php:206 459 459 #, php-format 460 460 msgid "Page %1$s of %2$s" 461 461 msgstr "" 462 462 463 #: admin/views/entry-detail.php:21 1463 #: admin/views/entry-detail.php:214 464 464 msgid "Newer activity" 465 465 msgstr "" 466 466 467 #: admin/views/entry-detail.php:21 6467 #: admin/views/entry-detail.php:219 468 468 msgid "Older activity" 469 469 msgstr "" 470 470 471 #: admin/views/entry-detail.php:2 38471 #: admin/views/entry-detail.php:261 472 472 msgid "Change status" 473 473 msgstr "" 474 474 475 #: admin/views/entry-detail.php:2 46475 #: admin/views/entry-detail.php:269 476 476 msgid "Update" 477 477 msgstr "" 478 478 479 #: admin/views/entry-detail.php:2 51479 #: admin/views/entry-detail.php:274 480 480 msgid "Submission info" 481 481 msgstr "" 482 482 483 #: admin/views/entry-detail.php:2 55483 #: admin/views/entry-detail.php:278 484 484 #: includes/class-export-service.php:50 485 485 msgid "Form ID" 486 486 msgstr "" 487 487 488 #: admin/views/entry-detail.php:2 56488 #: admin/views/entry-detail.php:279 489 489 #: includes/class-export-service.php:55 490 490 msgid "Submission page URL" 491 491 msgstr "" 492 492 493 #: admin/views/entry-detail.php:2 60493 #: admin/views/entry-detail.php:283 494 494 #: includes/class-export-service.php:56 495 495 msgid "Referrer" 496 496 msgstr "" 497 497 498 #: admin/views/entry-detail.php:2 64498 #: admin/views/entry-detail.php:287 499 499 #: includes/class-export-service.php:57 500 500 msgid "Landing URL" 501 501 msgstr "" 502 502 503 #: admin/views/entry-detail.php:2 70503 #: admin/views/entry-detail.php:293 504 504 msgid "User Agent" 505 505 msgstr "" 506 506 507 #: admin/views/entry-detail.php:2 71507 #: admin/views/entry-detail.php:294 508 508 #: includes/class-export-service.php:64 509 509 msgid "IP address" 510 510 msgstr "" 511 511 512 #: admin/views/entry-detail.php:2 72512 #: admin/views/entry-detail.php:295 513 513 #: includes/class-export-service.php:65 514 514 msgid "Spam flag" 515 515 msgstr "" 516 516 517 #: admin/views/entry-detail.php:2 72517 #: admin/views/entry-detail.php:295 518 518 #: admin/views/export.php:62 519 519 msgid "Spam" 520 520 msgstr "" 521 521 522 #: admin/views/entry-detail.php:2 72522 #: admin/views/entry-detail.php:295 523 523 msgid "Normal" 524 524 msgstr "" … … 799 799 msgstr "" 800 800 801 #: includes/class-entry-repository.php: 25801 #: includes/class-entry-repository.php:38 802 802 msgid "New" 803 803 msgstr "" 804 804 805 #: includes/class-entry-repository.php: 26805 #: includes/class-entry-repository.php:39 806 806 msgid "In progress" 807 807 msgstr "" 808 808 809 #: includes/class-entry-repository.php: 27809 #: includes/class-entry-repository.php:40 810 810 msgid "On hold" 811 811 msgstr "" 812 812 813 #: includes/class-entry-repository.php: 28813 #: includes/class-entry-repository.php:41 814 814 msgid "Done" 815 815 msgstr "" -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/neutrope-lead-desk-for-contact-form-7.php
r3666139 r3666532 4 4 * Plugin URI: https://neutrope.biz/neutrope-lead-desk-for-contact-form-7/ 5 5 * Description: Save Contact Form 7 submissions to the database and manage them as leads in the WordPress admin (list, detail, status, notes, CSV export). 6 * Version: 0. 2.06 * Version: 0.3.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 22 22 } 23 23 24 define( 'NLDCF7_VERSION', '0. 2.0' );24 define( 'NLDCF7_VERSION', '0.3.0' ); 25 25 define( 'NLDCF7_DB_VERSION', '1.1.0' ); 26 26 define( 'NLDCF7_PLUGIN_FILE', __FILE__ ); -
neutrope-lead-desk-for-contact-form-7/tags/0.3.0/readme.txt
r3666139 r3666532 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 2.07 Stable tag: 0.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 16 16 17 17 The free version is intentionally focused: it saves submissions reliably, gives you an admin list/detail UI, lets you change status, add internal notes, and export to CSV. It does this without pulling in external services, trackers, or upsell banners on unrelated screens. 18 19 Version 0.3.0 is mainly a foundation release for add-ons: it adds documented, generic extension points so other plugins can add columns, filters and detail-screen panels without forking this one. Nothing in the plugin's own behaviour changes because of them, and no database change is involved. See "Extending the plugin" below. 18 20 19 21 Since 0.2.0 each entry also keeps an **Activity Timeline** (対応履歴) so you can see what has already been done about an inquiry -- when it arrived, who changed its status and to what, when notes were added, and when it was moved to trash, restored, or marked as spam -- instead of inferring the handling history from the current status alone. … … 34 36 * Privacy-friendly defaults: IP capture is OFF by default 35 37 * Opt-in data deletion on uninstall (off by default so accidental deletes don't nuke your leads) 36 * Extensibility via action/filter hooks: `nldcf7_after_entry_saved`, `nldcf7_after_note_added`, `nldcf7_after_status_changed`, `nldcf7_after_entry_trashed`, `nldcf7_after_entry_restored`, `nldcf7_after_spam_changed`, `nldcf7_entry_data_before_insert`, `nldcf7_export_columns`, `nldcf7_export_row`, `nldcf7_statuses`, `nldcf7_event_render`, `nldcf7_required_capability` 38 * Extensibility via action/filter hooks: `nldcf7_after_entry_saved`, `nldcf7_after_note_added`, `nldcf7_after_status_changed`, `nldcf7_after_entry_trashed`, `nldcf7_after_entry_restored`, `nldcf7_after_spam_changed`, `nldcf7_entry_data_before_insert`, `nldcf7_export_columns`, `nldcf7_export_row`, `nldcf7_statuses`, `nldcf7_event_labels`, `nldcf7_event_render`, `nldcf7_required_capability` 39 * Since 0.3.0, nine further extension points let an add-on place panels on the entry detail screen, add list columns and filter controls, keep its own filter state across list actions, and constrain the entries query -- all documented under "Extending the plugin" 37 40 38 41 **Requirements** … … 106 109 Please open an issue on the plugin's support forum or the project's issue tracker. 107 110 111 == Extending the plugin == 112 113 Version 0.3.0 adds nine extension points for add-on developers. They are 114 generic: this plugin knows nothing about what an add-on stores. All of them are 115 no-ops when nothing is listening, so a site with no add-on behaves exactly as 116 it did in 0.2.0. 117 118 = nldcf7_entry_detail_main = 119 120 Action. Introduced in 0.3.0. Fires on the entry detail screen after this 121 plugin's own cards in the main column, before the column closes. 122 123 `do_action( 'nldcf7_entry_detail_main', array $entry )` 124 125 `$entry` is the entry row as stored, passed by value. The listener prints its 126 own markup, including its own card wrapper, and is responsible for escaping it. 127 Output is not buffered or altered. 128 129 = nldcf7_entry_detail_sidebar = 130 131 Action. Introduced in 0.3.0. The same contract, for the sidebar column. Two 132 hooks exist so an add-on can place content in both columns of one screen. 133 134 `do_action( 'nldcf7_entry_detail_sidebar', array $entry )` 135 136 = nldcf7_entry_list_columns = 137 138 Filter. Introduced in 0.3.0. Adds columns to the entries list. 139 140 `apply_filters( 'nldcf7_entry_list_columns', array $columns )` 141 142 Return the column map with your own keys appended; prefix them to avoid 143 collisions. Core columns cannot be removed, renamed or repurposed -- every core 144 key is restored to its own label after the filter runs, and the checkbox column 145 is forced back to the front. Labels are plain text and are escaped on output. 146 147 = nldcf7_entry_list_column_content = 148 149 Filter. Introduced in 0.3.0. Renders a cell for a column you added. 150 151 `apply_filters( 'nldcf7_entry_list_column_content', string $html, string $column_name, array $item )` 152 153 Return HTML for your own column, or the value you were given for anything else. 154 Escape your own output; the plugin then passes the result through 155 `wp_kses_post()` as a second layer, so ordinary post markup survives while 156 scripts and event handlers do not. A non-string return renders an empty cell. 157 158 = nldcf7_entry_list_filters = 159 160 Action. Introduced in 0.3.0. Fires inside the list's existing GET filter form, 161 before the Filter button, so a plain select or input submits alongside the 162 built-in filters. 163 164 `do_action( 'nldcf7_entry_list_filters', array $current_args )` 165 166 `$current_args` is this plugin's own sanitized filter state, for reference. You 167 own your field names, your values and your escaping. No nonce is involved: the 168 filter bar only reads. 169 170 = nldcf7_entry_list_carried_args = 171 172 Filter. Introduced in 0.3.0. Keeps your own filter state across the All and 173 Trash views, the row actions, and the redirect after a row or bulk action. 174 175 `apply_filters( 'nldcf7_entry_list_carried_args', array $args, array $core )` 176 177 Add your own already-sanitized scalar values. The plugin re-checks the result 178 afterwards: reserved keys (`page`, `paged`, `action`, `action2`, `entry_id`, 179 `_wpnonce`, `_wp_http_referer`, `view`, `event_page`, `status_view`) are 180 stripped, keys are normalised, non-scalars are dropped, and both value length 181 and argument count are capped. Core values always win. Do not attempt to 182 forward the whole query string. 183 184 = nldcf7_entry_query_args = 185 186 Filter. Introduced in 0.3.0. Adjusts the entries query using arguments the 187 repository already understands, without writing SQL. 188 189 `apply_filters( 'nldcf7_entry_query_args', array $args )` 190 191 The most useful is `ids`, an array of entry IDs to restrict the query to. 192 `$args['context']` says who is asking: `list` for the entries screen, `export` 193 for CSV, `default` otherwise. Check it -- both callers share the same 194 repository, so an unconditional constraint would also change what CSV exports. 195 196 = nldcf7_entry_query_clauses = 197 198 Filter. Introduced in 0.3.0. Constrains the entries query using a table of your 199 own, resolved by the database rather than by loading matching IDs into PHP. 200 201 `apply_filters( 'nldcf7_entry_query_clauses', array $parts, array $args )` 202 203 `$parts` has four keys: 204 205 * `join` -- a JOIN clause string. 206 * `where` -- an array of condition strings, each using `%s` or `%d` placeholders. 207 * `prepare_args` -- the values for those placeholders, in order. 208 * `distinct` -- boolean, default false. 209 210 Values must go in `prepare_args`, never inside the condition strings; the plugin 211 passes them to `$wpdb->prepare()`. A single pre-built SQL string is not 212 accepted. If the placeholder count and the value count disagree, the whole 213 contribution is discarded. Table and column identifiers inside `join` are your 214 responsibility. Set `distinct` when your JOIN can match a row more than once: it 215 is applied to the row query and the count together, so the list total and 216 pagination stay correct. `$args['context']` applies here too. 217 218 = nldcf7_entry_list_items_prepared = 219 220 Action. Introduced in 0.3.0. Fires once per list page after the rows are known 221 and before any cell is rendered. 222 223 `do_action( 'nldcf7_entry_list_items_prepared', array $items )` 224 225 `$items` is the current page's rows only -- never the whole result set. Use it 226 to load everything your columns need in one query and keep it in a 227 request-local cache, instead of querying once per cell. It is read-only, has no 228 return value, and fires with an empty array when the page has no rows. It 229 belongs to the entries screen only and never fires during CSV export. 230 108 231 == Screenshots == 109 232 … … 115 238 116 239 == Changelog == 240 241 = 0.3.0 = 242 * Add: generic extension points for the entry detail screen -- `nldcf7_entry_detail_main` and `nldcf7_entry_detail_sidebar` -- so an add-on can place panels in either column without modifying this plugin. 243 * Add: generic list extension points -- `nldcf7_entry_list_columns` for columns and `nldcf7_entry_list_column_content` for their cells. Core columns are protected: an add-on cannot remove, rename or repurpose one, and returned cell markup passes through `wp_kses_post()`. 244 * Add: `nldcf7_entry_list_filters`, which places an add-on's own controls inside the existing filter bar. 245 * Add: `nldcf7_entry_list_carried_args`, so an add-on's own filter stays applied across the All and Trash views and after row and bulk actions. Reserved keys such as nonces, action names and entry IDs are refused, non-scalar values are dropped, and size and count are capped, so a careless listener cannot put unsafe state into a list URL. 246 * Add: `nldcf7_entry_query_args` for high-level query constraints, including the existing `ids` argument, and `nldcf7_entry_query_clauses` for constraints that live in an add-on's own table. Clauses are structured -- a JOIN, conditions with placeholders, and their values kept separate -- rather than a raw SQL string, and values always go through `$wpdb->prepare()`. 247 * Add: optional `distinct` handling for add-on joins that can match a row more than once. It is applied to the row query and the count together, so the list and its total can never disagree. 248 * Add: query context. The entries screen and the CSV exporter share one repository, so each query now says which it is. An add-on constraint meant for the list no longer reaches CSV export unless the add-on opts in. 249 * Add: `nldcf7_entry_list_items_prepared`, which hands an add-on the current page's rows once, before any cell is rendered, so custom columns can load their data in a single query instead of one per cell. 250 * Fix: the Activity preparation notice counted processing records (entries plus notes) but read as a number of inquiries. It now names the unit; the arithmetic is unchanged. 251 * Fix: the "history from before this plugin was updated" note no longer appears on sites that installed this version fresh and therefore have no earlier history to explain. 252 * No database change. The schema and database version (1.1.0) are identical to 0.2.0, no migration runs, and existing entries, notes, attachments, activity, settings, capabilities and URLs are untouched. With no add-on installed the plugin behaves exactly as 0.2.0 did. 117 253 118 254 = 0.2.0 = … … 165 301 == Upgrade Notice == 166 302 303 = 0.3.0 = 304 Adds a documented extension foundation for add-ons, plus two small Activity-history wording fixes. Your leads, notes, activity and database schema are unchanged, and no migration runs. With no add-on installed the plugin behaves exactly as before. 305 167 306 = 0.2.0 = 168 307 Adds a per-entry activity history. Existing entries have their received time and existing notes reconstructed in the background; status, trash, and spam changes made before this update were never recorded and are not invented. Existing lead data is preserved. -
neutrope-lead-desk-for-contact-form-7/trunk/admin/class-admin-menu.php
r3666139 r3666532 185 185 $message .= ' ' . esc_html( 186 186 sprintf( 187 /* translators: 1: number of records processed, 2: total records , 3: percentage complete*/188 __( ' %1$s of %2$s (%3$s%%)', 'neutrope-lead-desk-for-contact-form-7' ),187 /* translators: 1: number of records processed, 2: total records to process, 3: percentage complete. "Records" counts processing units (entries plus notes), NOT the number of inquiries. */ 188 __( 'Processed %1$s of %2$s records (%3$s%%).', 'neutrope-lead-desk-for-contact-form-7' ), 189 189 number_format_i18n( (int) $progress['processed'] ), 190 190 number_format_i18n( (int) $progress['total'] ), -
neutrope-lead-desk-for-contact-form-7/trunk/admin/class-entry-detail-page.php
r3666139 r3666532 127 127 $backfill_pending = class_exists( 'NLDCF7_Event_Backfill' ) && NLDCF7_Event_Backfill::is_pending(); 128 128 129 // The "history from before this plugin was updated" note only makes 130 // sense where there IS such history. The migration snapshot records how 131 // much there was to reconstruct at upgrade time: both totals are zero on 132 // a site that installed this version fresh, and the note is then simply 133 // untrue. The snapshot is used rather than today's entry count because 134 // entries received since the upgrade say nothing about what came before. 135 // 136 // If the snapshot is missing or unreadable the note is shown: an 137 // unnecessary explanation is a smaller harm than silently omitting the 138 // one that tells an admin why their older entries look sparse. 139 $show_history_note = true; 140 if ( class_exists( 'NLDCF7_Event_Backfill' ) && NLDCF7_Event_Backfill::is_initialised() ) { 141 $backfill_state = NLDCF7_Event_Backfill::get_state(); 142 if ( isset( $backfill_state['entry_total'], $backfill_state['note_total'] ) ) { 143 $show_history_note = ( (int) $backfill_state['entry_total'] > 0 ) 144 || ( (int) $backfill_state['note_total'] > 0 ); 145 } 146 } 147 129 148 if ( $this->event_repo ) { 130 149 $events_total = $this->event_repo->count_for_entry( (int) $entry['id'] ); -
neutrope-lead-desk-for-contact-form-7/trunk/admin/class-entry-list-table.php
r3666139 r3666532 129 129 } 130 130 131 return $args; 131 return self::apply_carried_args_filter( $args ); 132 } 133 134 /** 135 * Maximum number of add-on contributed args carried in a list URL. 136 */ 137 const CARRIED_MAX_ARGS = 20; 138 139 /** 140 * Maximum length of a single carried value, in characters. 141 */ 142 const CARRIED_MAX_VALUE = 200; 143 144 /** 145 * Query keys an add-on may never contribute to a carried list URL. 146 * 147 * `page` is here because Free sets it itself; `paged` because v0.2.0 148 * deliberately returns to the first page after an action; the rest are 149 * mutation, authentication or routing state that must never be replayed 150 * from a link the admin can bookmark or share. 151 * 152 * @var string[] 153 */ 154 private static $carried_denylist = array( 155 'page', 156 'paged', 157 'action', 158 'action2', 159 'entry_id', 160 '_wpnonce', 161 '_wp_http_referer', 162 'view', 163 'event_page', 164 self::VIEW_QUERY_VAR, 165 ); 166 167 /** 168 * Let add-ons contribute their own filter state, then re-impose Free's rules. 169 * 170 * Everything an add-on adds is treated as untrusted: the deny-list, key 171 * normalisation, scalar-only rule and size caps are applied AFTER the 172 * filter, so a careless listener cannot put `_wpnonce` or an array back 173 * into a URL. Core args are re-applied last and always win. 174 * 175 * This is the one place custom filter state enters a list URL. Every path 176 * that needs it -- the All/Trash views, the row actions, and the redirect 177 * after a row or bulk action -- reaches it through current_filter_args(), 178 * so there is a single implementation rather than one per call site. 179 * 180 * @param array<string,string|int> $core Sanitized core filter args. 181 * @return array<string,string|int> 182 */ 183 private static function apply_carried_args_filter( array $core ) { 184 /** 185 * Filter the list state carried across view switches and row/bulk actions. 186 * 187 * Add-ons append their own already-sanitized scalar args. Free re-checks 188 * the result regardless; see the class constants for the size caps and 189 * `$carried_denylist` for the keys that are always stripped. 190 * 191 * Free never forwards the raw query string, so an add-on filter that 192 * adds nothing leaves the URL byte-identical to v0.2.0. 193 * 194 * @since 0.3.0 195 * 196 * @param array<string,string|int> $args Args to carry. Core keys included. 197 * @param array<string,string|int> $core The core args, for reference. 198 */ 199 $filtered = apply_filters( 'nldcf7_entry_list_carried_args', $core, $core ); 200 201 if ( ! is_array( $filtered ) ) { 202 return $core; 203 } 204 205 $extra = array(); 206 foreach ( $filtered as $key => $value ) { 207 if ( count( $extra ) >= self::CARRIED_MAX_ARGS ) { 208 self::debug_carried( 'too many carried args; extra dropped' ); 209 break; 210 } 211 212 $key = sanitize_key( (string) $key ); 213 if ( '' === $key || array_key_exists( $key, $core ) ) { 214 // Empty/unusable key, or a core key -- core is authoritative and 215 // is re-applied below either way. 216 continue; 217 } 218 if ( in_array( $key, self::$carried_denylist, true ) ) { 219 self::debug_carried( 'reserved key refused: ' . $key ); 220 continue; 221 } 222 if ( ! is_scalar( $value ) ) { 223 // Arrays and objects are dropped outright. Casting one would 224 // print the literal string "Array" into a URL. 225 self::debug_carried( 'non-scalar value dropped for key: ' . $key ); 226 continue; 227 } 228 229 $value = (string) $value; 230 if ( '' === $value ) { 231 continue; 232 } 233 if ( function_exists( 'mb_substr' ) ) { 234 $value = mb_substr( $value, 0, self::CARRIED_MAX_VALUE ); 235 } else { 236 $value = substr( $value, 0, self::CARRIED_MAX_VALUE ); 237 } 238 239 $extra[ $key ] = sanitize_text_field( $value ); 240 } 241 242 // Core last: an add-on cannot override the screen's own filter state. 243 return array_merge( $extra, $core ); 244 } 245 246 /** 247 * Log a rejected carried-arg contribution while debugging. 248 * 249 * @param string $message 250 * @return void 251 */ 252 private static function debug_carried( $message ) { 253 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 254 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated by WP_DEBUG; an add-on silently losing filter state is otherwise very hard to diagnose. 255 error_log( '[neutrope-lead-desk-for-contact-form-7] carried args: ' . $message ); 256 } 132 257 } 133 258 … … 234 359 235 360 public function get_columns() { 236 returnarray(361 $core = array( 237 362 'cb' => '<input type="checkbox" />', 238 363 'submitted_at' => __( 'Received', 'neutrope-lead-desk-for-contact-form-7' ), … … 246 371 'note_count' => __( 'Notes', 'neutrope-lead-desk-for-contact-form-7' ), 247 372 ); 373 374 /** 375 * Filter the entries list columns. 376 * 377 * Add-ons may append their own columns. They may NOT remove, rename or 378 * repurpose a core column: every core key is restored to its own label 379 * after this filter runs, so reusing one silently changes nothing. That 380 * is enforced in code rather than left to convention, because a column 381 * whose header says one thing and whose cells say another is a data 382 * integrity problem, not a styling preference. 383 * 384 * Cells for added columns are rendered through 385 * `nldcf7_entry_list_column_content`. 386 * 387 * @since 0.3.0 388 * 389 * @param array<string,string> $columns column key => header label. 390 */ 391 $filtered = apply_filters( 'nldcf7_entry_list_columns', $core ); 392 393 if ( ! is_array( $filtered ) ) { 394 return $core; 395 } 396 397 // Keep only usable entries: a non-empty sanitize_key()-able key mapped 398 // to a scalar label. Anything else is a malformed contribution and is 399 // dropped rather than being allowed to break the header row. 400 $clean = array(); 401 foreach ( $filtered as $key => $label ) { 402 $key = sanitize_key( (string) $key ); 403 if ( '' === $key || ! is_scalar( $label ) ) { 404 continue; 405 } 406 $clean[ $key ] = (string) $label; 407 } 408 409 // Core wins: restore every core column's own label, and re-add any the 410 // filter dropped. `cb` goes back to the front so the bulk-select box 411 // never ends up mid-row. 412 foreach ( $core as $key => $label ) { 413 $clean[ $key ] = $label; 414 } 415 if ( isset( $clean['cb'] ) ) { 416 $cb = $clean['cb']; 417 unset( $clean['cb'] ); 418 $clean = array_merge( array( 'cb' => $cb ), $clean ); 419 } 420 421 return $clean; 422 } 423 424 /** 425 * Render a cell for a column this class has no `column_{key}()` method for. 426 * 427 * Only add-on columns reach here: every core column has its own method. 428 * WordPress core's own `column_default()` is an empty no-op, so without 429 * this an added column would render as a blank cell. 430 * 431 * The listener returns HTML and is responsible for escaping it; Free then 432 * runs the result through `wp_kses_post()` as a second layer, which is the 433 * reason this is a filter with a return value rather than an action that 434 * echoes -- output that Free never sees is output Free cannot make safe. 435 * 436 * @param array $item Entry row. 437 * @param string $column_name Column key. 438 * @return string Escaped HTML, or '' when nothing is contributed. 439 */ 440 public function column_default( $item, $column_name ) { 441 /** 442 * Filter the cell content for an add-on column on the entries list. 443 * 444 * @since 0.3.0 445 * 446 * @param string $html Cell HTML. Default ''. 447 * @param string $column_name Column key being rendered. 448 * @param array $item Entry row, as stored. 449 */ 450 $html = apply_filters( 'nldcf7_entry_list_column_content', '', (string) $column_name, $item ); 451 452 if ( ! is_string( $html ) || '' === $html ) { 453 return ''; 454 } 455 456 return wp_kses_post( $html ); 248 457 } 249 458 … … 495 704 </label> 496 705 497 <?php submit_button( __( 'Filter', 'neutrope-lead-desk-for-contact-form-7' ), '', 'nldcf7-filter-submit', false ); ?> 706 <?php 707 /** 708 * Fires inside the entries list filter bar, before the Filter button. 709 * 710 * Listeners print their own form controls here. The position is 711 * guaranteed to be inside the existing GET form, so a plain 712 * `<select>` or `<input>` submits with the core filters and needs 713 * nothing else wired up. 714 * 715 * No nonce is involved: this bar only reads, and Free's own filters 716 * carry none either. The listener owns its field names, its values 717 * and its escaping; Free owns only the placement. 718 * 719 * @since 0.3.0 720 * 721 * @param array<string,string|int> $current_args Sanitized core filter 722 * state for this request. 723 */ 724 do_action( 'nldcf7_entry_list_filters', self::current_filter_args() ); 725 726 submit_button( __( 'Filter', 'neutrope-lead-desk-for-contact-form-7' ), '', 'nldcf7-filter-submit', false ); 727 ?> 498 728 </div> 499 729 <?php … … 529 759 // behaving as a soft delete rather than turning into a filter. 530 760 'trashed_only' => ( self::VIEW_TRASH === self::current_view() ), 761 // Identifies this caller to the query extension hooks so an add-on 762 // can constrain the list without also constraining CSV export. 763 'context' => NLDCF7_Entry_Repository::CONTEXT_LIST, 531 764 ); 532 765 … … 551 784 ) 552 785 ); 786 787 /** 788 * Fires once the rows for the current list page are known. 789 * 790 * Add-ons that render their own columns use this to load whatever they 791 * need for these rows in one query, before any cell is rendered. 792 * Without it they are pushed into N+1: the cell filter is handed a 793 * single row and has no way to learn what else is on the page, so the 794 * only alternative is a lookup per cell -- twenty rows with two custom 795 * columns being up to forty extra queries. 796 * 797 * Free's own note-count cache above is the same pattern; this is that 798 * opportunity handed to extensions. 799 * 800 * Contract: 801 * - $items is THIS PAGE's rows, never the whole result set. 802 * - Read-only. Listeners must not mutate the list's state, and no 803 * return value is used. 804 * - Fires exactly once per prepare cycle, including with an empty 805 * array when the page has no rows, so "once per cycle" holds 806 * without a special case for empty results. 807 * - The entries screen only. CSV export does not build a list table 808 * and so never reaches here. 809 * - Running the secondary query safely -- preparing it, checking 810 * capabilities -- belongs to the listener. 811 * 812 * Fired after this class has finished its own preparation so that a 813 * listener cannot interleave with it. The repository, the controller 814 * and this table instance are deliberately not passed. 815 * 816 * @since 0.3.0 817 * 818 * @param array<int,array<string,mixed>> $items Rows on the current page. 819 */ 820 do_action( 'nldcf7_entry_list_items_prepared', $this->items ); 553 821 } 554 822 -
neutrope-lead-desk-for-contact-form-7/trunk/admin/views/entry-detail.php
r3666139 r3666532 12 12 * $notice array|null Flash notice ['message'=>string,'type'=>string] 13 13 * pulled from per-user transient (consume_notice()). 14 * $show_history_note bool Whether the pre-upgrade history caveat applies. 14 15 * 15 16 * phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Locals in an included view, not actual globals. … … 140 141 <div class="nldcf7-card nldcf7-timeline-card"> 141 142 <h2><?php esc_html_e( 'Activity', 'neutrope-lead-desk-for-contact-form-7' ); ?></h2> 142 <p class="description"> 143 <?php esc_html_e( 'History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved.', 'neutrope-lead-desk-for-contact-form-7' ); ?> 144 </p> 143 <?php if ( $show_history_note ) : ?> 144 <p class="description"> 145 <?php esc_html_e( 'History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved.', 'neutrope-lead-desk-for-contact-form-7' ); ?> 146 </p> 147 <?php endif; ?> 145 148 146 149 <?php if ( empty( $events ) ) : ?> … … 222 225 </div> 223 226 227 <?php 228 /** 229 * Fires after the main column's own cards on the entry detail screen. 230 * 231 * Add-ons print their own markup here -- including the surrounding 232 * `<div class="nldcf7-card">` -- and own escaping it. Nothing is 233 * buffered or filtered on the way out, so a listener that prints 234 * nothing costs nothing and leaves the page byte-identical. 235 * 236 * The position is deliberately "after every core card" rather than 237 * between two of them: card order is an internal detail that may 238 * change, and a between-cards contract could not be kept stable. 239 * 240 * @since 0.3.0 241 * 242 * @param array $entry Entry row, as stored. Passed by value. 243 */ 244 do_action( 'nldcf7_entry_detail_main', $entry ); 245 ?> 246 224 247 </div> 225 248 … … 273 296 </table> 274 297 </div> 298 299 <?php 300 /** 301 * Fires after the sidebar's own cards on the entry detail screen. 302 * 303 * Same ownership rules as `nldcf7_entry_detail_main`: the listener 304 * prints and escapes its own markup. Two hooks exist rather than one 305 * because an add-on may legitimately need to place content in both 306 * columns of the same screen. 307 * 308 * @since 0.3.0 309 * 310 * @param array $entry Entry row, as stored. Passed by value. 311 */ 312 do_action( 'nldcf7_entry_detail_sidebar', $entry ); 313 ?> 275 314 </aside> 276 315 </div> -
neutrope-lead-desk-for-contact-form-7/trunk/includes/class-entry-repository.php
r3666139 r3666532 14 14 15 15 class NLDCF7_Entry_Repository { 16 17 /** 18 * Query contexts passed to the query extension hooks. 19 * 20 * The entries screen and the CSV exporter share this repository, so an 21 * add-on constraint that did not say which one it meant would change both. 22 * Listeners check $args['context'] to scope themselves. 23 * 24 * @since 0.3.0 25 */ 26 const CONTEXT_LIST = 'list'; 27 const CONTEXT_EXPORT = 'export'; 28 const CONTEXT_DEFAULT = 'default'; 16 29 17 30 /** … … 133 146 public function count( array $args = array() ) { 134 147 global $wpdb; 135 $where = $this->build_where( $args ); 136 $table = NLDCF7_DB::entries_table(); 137 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from internal NLDCF7_DB method. 138 $sql = "SELECT COUNT(*) FROM {$table} {$where['clause']}"; 139 $prepared = $where['prepare_args'] 140 ? $wpdb->prepare( $sql, $where['prepare_args'] ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 148 149 // Same builder as query(). Sharing it is what makes it impossible for a 150 // filtered list to report an unfiltered total: there is one query plan, 151 // not two implementations that have to be kept in step. 152 $parts = $this->build_query_parts( $args ); 153 $table = NLDCF7_DB::entries_table(); 154 $distinct = $parts['distinct'] ? 'DISTINCT ' : ''; 155 156 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from internal NLDCF7_DB method; join/clause carry no request values (those are in prepare_args). 157 $sql = "SELECT COUNT({$distinct}{$table}.id) FROM {$table} {$parts['join']} {$parts['clause']}"; 158 $prepared = $parts['prepare_args'] 159 ? $wpdb->prepare( $sql, $parts['prepare_args'] ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 141 160 : $sql; 142 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Internal plugin table, built via build_ where() with prepare_args.161 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Internal plugin table, built via build_query_parts() with prepare_args. 143 162 return (int) $wpdb->get_var( $prepared ); 144 163 } … … 176 195 $args = array_merge( $defaults, $args ); 177 196 178 $ where = $this->build_where( $args );197 $parts = $this->build_query_parts( $args ); 179 198 180 199 $orderby = $this->sanitize_orderby( $args['orderby'] ); … … 185 204 $offset = ( $page - 1 ) * $per_page; 186 205 187 $table = NLDCF7_DB::entries_table(); 188 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table / orderby / order are internal allow-listed values; user input is carried in $prepare_args. 189 $sql = "SELECT * FROM {$table} {$where['clause']} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; 190 191 $prepare_args = $where['prepare_args']; 206 $table = NLDCF7_DB::entries_table(); 207 $distinct = $parts['distinct'] ? 'DISTINCT ' : ''; 208 209 // `{$table}.*` rather than `*`: once an add-on may contribute a JOIN, a 210 // bare `*` would also pull the joined table's columns into the row and 211 // any shared column name (id, created_at) would overwrite ours. With no 212 // join present the two forms select exactly the same thing, so this is 213 // behaviour-preserving when nothing is listening. 214 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table / orderby / order are internal allow-listed values; join/clause carry no request values (those are in prepare_args). 215 $sql = "SELECT {$distinct}{$table}.* FROM {$table} {$parts['join']} {$parts['clause']} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; 216 217 $prepare_args = $parts['prepare_args']; 192 218 $prepare_args[] = $per_page; 193 219 $prepare_args[] = $offset; … … 463 489 } 464 490 491 /** 492 * Build the complete constraint plan for one query. 493 * 494 * This is the single source of JOIN / WHERE / prepared values / DISTINCT for 495 * BOTH query() and count(). Extending it in one place is what guarantees a 496 * filtered list and its total can never disagree. 497 * 498 * Two extension points run here, in this order: 499 * 500 * 1. `nldcf7_entry_query_args` -- high level. Adjust the documented 501 * args (including the long-standing `ids`) without writing SQL. 502 * 2. `nldcf7_entry_query_clauses` -- low level. Contribute a JOIN and 503 * extra conditions with their values kept separate, for constraints 504 * that live in another table and should be resolved by the database 505 * rather than by loading every matching id into PHP. 506 * 507 * @param array $args Query args. `context` identifies the caller. 508 * @return array{join:string,clause:string,prepare_args:array,distinct:bool} 509 */ 510 private function build_query_parts( array $args ) { 511 if ( ! isset( $args['context'] ) || ! is_string( $args['context'] ) || '' === $args['context'] ) { 512 $args['context'] = self::CONTEXT_DEFAULT; 513 } 514 515 /** 516 * Filter the entry query args before the SQL is built. 517 * 518 * Use this when the constraint can be said with the args the repository 519 * already understands -- most usefully `ids`, which restricts the query 520 * to a set of entry IDs. Anything needing a JOIN belongs in 521 * `nldcf7_entry_query_clauses` instead. 522 * 523 * `$args['context']` says who is asking: 'list' for the entries screen, 524 * 'export' for CSV, 'default' otherwise. Listeners should check it -- 525 * both callers share this repository, so an unconditional constraint 526 * meant for the list screen would silently change what CSV exports. 527 * 528 * @since 0.3.0 529 * 530 * @param array $args Query args, including 'context'. 531 */ 532 $filtered = apply_filters( 'nldcf7_entry_query_args', $args ); 533 if ( is_array( $filtered ) ) { 534 $args = $filtered; 535 } 536 537 $where = $this->build_where( $args ); 538 539 $parts = array( 540 'join' => '', 541 'where' => array(), 542 'prepare_args' => array(), 543 'distinct' => false, 544 ); 545 546 /** 547 * Filter the SQL parts for an entry query. 548 * 549 * Contribute a JOIN and conditions whose values stay separate from the 550 * SQL: put `%s` / `%d` placeholders in each `where` entry and the values 551 * in `prepare_args`, in the same order. Free passes them to 552 * $wpdb->prepare(). A single pre-built SQL string is deliberately not 553 * accepted, so request values are never concatenated into a statement. 554 * 555 * Set `distinct` when the JOIN can match a row more than once; it is 556 * applied to the row query and the count together. 557 * 558 * Table and column identifiers inside `join` are the listener's own 559 * responsibility -- Free cannot validate another plugin's schema. 560 * 561 * @since 0.3.0 562 * 563 * @param array{join:string,where:array,prepare_args:array,distinct:bool} $parts 564 * @param array $args Query args, including 'context'. 565 */ 566 $contributed = apply_filters( 'nldcf7_entry_query_clauses', $parts, $args ); 567 568 $join = ''; 569 $conditions = array(); 570 $prepare_args = $where['prepare_args']; 571 $distinct = false; 572 573 if ( is_array( $contributed ) ) { 574 $join = isset( $contributed['join'] ) && is_string( $contributed['join'] ) ? trim( $contributed['join'] ) : ''; 575 $distinct = ! empty( $contributed['distinct'] ); 576 577 $raw_where = isset( $contributed['where'] ) && is_array( $contributed['where'] ) ? $contributed['where'] : array(); 578 $raw_prep = isset( $contributed['prepare_args'] ) && is_array( $contributed['prepare_args'] ) ? array_values( $contributed['prepare_args'] ) : array(); 579 580 foreach ( $raw_where as $condition ) { 581 if ( is_string( $condition ) && '' !== trim( $condition ) ) { 582 $conditions[] = '( ' . trim( $condition ) . ' )'; 583 } 584 } 585 586 // The placeholder count has to match the values supplied, or 587 // $wpdb->prepare() would mis-bind every value after the mismatch and 588 // return a broken statement. Rather than run that, drop the whole 589 // contribution and say so while debugging. 590 $expected = 0; 591 foreach ( $conditions as $condition ) { 592 $expected += preg_match_all( '/%[dfs]/', $condition ); 593 } 594 if ( $expected !== count( $raw_prep ) ) { 595 $this->debug_clauses( 596 sprintf( 'placeholder/value mismatch (%d placeholders, %d values); clause contribution dropped', $expected, count( $raw_prep ) ) 597 ); 598 $join = ''; 599 $conditions = array(); 600 $raw_prep = array(); 601 $distinct = false; 602 } 603 604 foreach ( $raw_prep as $value ) { 605 $prepare_args[] = $value; 606 } 607 } 608 609 $clause = $where['clause']; 610 if ( $conditions ) { 611 $extra = implode( ' AND ', $conditions ); 612 $clause = '' === $clause ? ( 'WHERE ' . $extra ) : ( $clause . ' AND ' . $extra ); 613 } 614 615 return array( 616 'join' => $join, 617 'clause' => $clause, 618 'prepare_args' => $prepare_args, 619 'distinct' => (bool) $distinct, 620 ); 621 } 622 623 /** 624 * Log a rejected clause contribution while debugging. 625 * 626 * @param string $message 627 * @return void 628 */ 629 private function debug_clauses( $message ) { 630 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 631 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Gated by WP_DEBUG; a silently dropped clause would otherwise look like a filter that simply did not fire. 632 error_log( '[neutrope-lead-desk-for-contact-form-7] entry query clauses: ' . $message ); 633 } 634 } 635 465 636 private function sanitize_orderby( $field ) { 466 637 $allowed = array( 'id', 'submitted_at', 'status', 'form_post_id' ); -
neutrope-lead-desk-for-contact-form-7/trunk/includes/class-export-service.php
r3553617 r3666532 132 132 'orderby' => 'submitted_at', 133 133 'order' => 'DESC', 134 // Tells the query extension hooks this is CSV export, not the 135 // entries screen, so a list-only constraint cannot reach it. 136 'context' => NLDCF7_Entry_Repository::CONTEXT_EXPORT, 134 137 ) 135 138 ); -
neutrope-lead-desk-for-contact-form-7/trunk/languages/neutrope-lead-desk-for-contact-form-7-ja.po
r3666139 r3666532 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0. 2.0\n"5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/neutrope-lead-desk-for-contact-form-7\n" 7 7 "Last-Translator: \n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-08-2 5T14:05:36+00:00\n"13 "PO-Revision-Date: 2026-08-2 500:00+0000\n"12 "POT-Creation-Date: 2026-08-26T04:22:33+00:00\n" 13 "PO-Revision-Date: 2026-08-26 00:00+0000\n" 14 14 "Language: ja\n" 15 15 "Plural-Forms: nplurals=1; plural=0;\n" … … 154 154 msgstr "指定された問い合わせは見つかりませんでした。" 155 155 156 #: admin/class-entry-list-table.php: 211156 #: admin/class-entry-list-table.php:336 157 157 #: admin/views/export.php:43 158 158 msgid "All" 159 159 msgstr "すべて" 160 160 161 #: admin/class-entry-list-table.php: 227162 #: admin/class-entry-list-table.php: 361161 #: admin/class-entry-list-table.php:352 162 #: admin/class-entry-list-table.php:570 163 163 #: admin/views/export.php:72 164 164 msgid "Trash" … … 166 166 167 167 #: admin/class-entry-event-presenter.php:53 168 #: admin/class-entry-list-table.php: 238169 #: admin/views/entry-detail.php:2 53168 #: admin/class-entry-list-table.php:363 169 #: admin/views/entry-detail.php:276 170 170 #: includes/class-export-service.php:47 171 171 msgid "Received" 172 172 msgstr "受信日時" 173 173 174 #: admin/class-entry-list-table.php: 239175 #: admin/class-entry-list-table.php: 471176 #: admin/views/entry-detail.php:2 28174 #: admin/class-entry-list-table.php:364 175 #: admin/class-entry-list-table.php:680 176 #: admin/views/entry-detail.php:251 177 177 #: admin/views/export.php:40 178 178 #: includes/class-export-service.php:48 … … 180 180 msgstr "ステータス" 181 181 182 #: admin/class-entry-list-table.php: 240183 #: admin/class-entry-list-table.php: 479184 #: admin/views/entry-detail.php:2 54182 #: admin/class-entry-list-table.php:365 183 #: admin/class-entry-list-table.php:688 184 #: admin/views/entry-detail.php:277 185 185 #: admin/views/export.php:51 186 186 #: includes/class-export-service.php:49 … … 188 188 msgstr "フォーム" 189 189 190 #: admin/class-entry-list-table.php: 241190 #: admin/class-entry-list-table.php:366 191 191 #: admin/views/settings.php:73 192 192 #: includes/class-export-service.php:51 … … 194 194 msgstr "名前" 195 195 196 #: admin/class-entry-list-table.php: 242196 #: admin/class-entry-list-table.php:367 197 197 #: admin/views/settings.php:77 198 198 #: includes/class-export-service.php:52 … … 200 200 msgstr "メール" 201 201 202 #: admin/class-entry-list-table.php: 243202 #: admin/class-entry-list-table.php:368 203 203 #: admin/views/settings.php:81 204 204 #: includes/class-export-service.php:53 … … 206 206 msgstr "電話" 207 207 208 #: admin/class-entry-list-table.php: 244208 #: admin/class-entry-list-table.php:369 209 209 msgid "Source" 210 210 msgstr "参照元" 211 211 212 #: admin/class-entry-list-table.php: 245212 #: admin/class-entry-list-table.php:370 213 213 msgid "UTM source" 214 214 msgstr "UTM source" 215 215 216 #: admin/class-entry-list-table.php: 246217 #: admin/views/entry-detail.php:9 6216 #: admin/class-entry-list-table.php:371 217 #: admin/views/entry-detail.php:97 218 218 msgid "Notes" 219 219 msgstr "メモ" 220 220 221 #: admin/class-entry-list-table.php: 264222 #: admin/class-entry-list-table.php: 342221 #: admin/class-entry-list-table.php:473 222 #: admin/class-entry-list-table.php:551 223 223 msgid "Restore" 224 224 msgstr "復元" 225 225 226 #: admin/class-entry-list-table.php: 269226 #: admin/class-entry-list-table.php:478 227 227 msgid "Set status: New" 228 228 msgstr "ステータス: 未対応" 229 229 230 #: admin/class-entry-list-table.php: 270230 #: admin/class-entry-list-table.php:479 231 231 msgid "Set status: In progress" 232 232 msgstr "ステータス: 対応中" 233 233 234 #: admin/class-entry-list-table.php: 271234 #: admin/class-entry-list-table.php:480 235 235 msgid "Set status: On hold" 236 236 msgstr "ステータス: 保留" 237 237 238 #: admin/class-entry-list-table.php: 272238 #: admin/class-entry-list-table.php:481 239 239 msgid "Set status: Done" 240 240 msgstr "ステータス: 完了" 241 241 242 #: admin/class-entry-list-table.php: 273242 #: admin/class-entry-list-table.php:482 243 243 msgid "Move to trash" 244 244 msgstr "ゴミ箱へ移動" 245 245 246 #: admin/class-entry-list-table.php: 274246 #: admin/class-entry-list-table.php:483 247 247 msgid "Mark as spam" 248 248 msgstr "スパムにする" 249 249 250 #: admin/class-entry-list-table.php: 299250 #: admin/class-entry-list-table.php:508 251 251 msgid "View" 252 252 msgstr "詳細" 253 253 254 #: admin/class-entry-list-table.php: 430254 #: admin/class-entry-list-table.php:639 255 255 msgid "None" 256 256 msgstr "なし" 257 257 258 #: admin/class-entry-list-table.php: 437258 #: admin/class-entry-list-table.php:646 259 259 msgid "No entries are in the trash." 260 260 msgstr "ゴミ箱に問い合わせはありません。" 261 261 262 #: admin/class-entry-list-table.php: 440262 #: admin/class-entry-list-table.php:649 263 263 msgid "No entries match the current filters." 264 264 msgstr "該当する問い合わせはありません。" 265 265 266 #: admin/class-entry-list-table.php: 468267 #: admin/class-entry-list-table.php: 469266 #: admin/class-entry-list-table.php:677 267 #: admin/class-entry-list-table.php:678 268 268 msgid "Search keywords" 269 269 msgstr "キーワード検索" 270 270 271 #: admin/class-entry-list-table.php: 473271 #: admin/class-entry-list-table.php:682 272 272 msgid "All statuses" 273 273 msgstr "すべてのステータス" 274 274 275 #: admin/class-entry-list-table.php: 481275 #: admin/class-entry-list-table.php:690 276 276 #: admin/views/export.php:54 277 277 msgid "All forms" 278 278 msgstr "すべてのフォーム" 279 279 280 #: admin/class-entry-list-table.php: 487280 #: admin/class-entry-list-table.php:696 281 281 #: admin/views/export.php:32 282 282 msgid "From date" 283 283 msgstr "開始日" 284 284 285 #: admin/class-entry-list-table.php: 489285 #: admin/class-entry-list-table.php:698 286 286 #: admin/views/export.php:36 287 287 msgid "To date" 288 288 msgstr "終了日" 289 289 290 #: admin/class-entry-list-table.php: 494290 #: admin/class-entry-list-table.php:703 291 291 msgid "Show spam" 292 292 msgstr "スパムも表示" 293 293 294 #: admin/class-entry-list-table.php: 497294 #: admin/class-entry-list-table.php:726 295 295 msgid "Filter" 296 296 msgstr "絞り込み" 297 297 298 298 #. translators: %d: entry ID 299 #: admin/views/entry-detail.php:3 1299 #: admin/views/entry-detail.php:32 300 300 #, php-format 301 301 msgid "Entry #%d" 302 302 msgstr "問い合わせ #%d" 303 303 304 #: admin/views/entry-detail.php:3 4304 #: admin/views/entry-detail.php:35 305 305 msgid "Back to list" 306 306 msgstr "一覧へ戻る" 307 307 308 #: admin/views/entry-detail.php:4 7308 #: admin/views/entry-detail.php:48 309 309 msgid "Submitted fields" 310 310 msgstr "送信内容" 311 311 312 #: admin/views/entry-detail.php:5 1312 #: admin/views/entry-detail.php:52 313 313 msgid "No submitted fields stored." 314 314 msgstr "送信フィールドが保存されていません。" 315 315 316 #: admin/views/entry-detail.php:7 2316 #: admin/views/entry-detail.php:73 317 317 msgid "Attachments (metadata only)" 318 318 msgstr "添付ファイル(メタ情報のみ)" 319 319 320 #: admin/views/entry-detail.php:7 6320 #: admin/views/entry-detail.php:77 321 321 msgid "File name" 322 322 msgstr "ファイル名" 323 323 324 #: admin/views/entry-detail.php:7 7324 #: admin/views/entry-detail.php:78 325 325 msgid "MIME" 326 326 msgstr "MIME" 327 327 328 #: admin/views/entry-detail.php:7 8328 #: admin/views/entry-detail.php:79 329 329 msgid "Size (bytes)" 330 330 msgstr "サイズ (bytes)" 331 331 332 #: admin/views/entry-detail.php:9 1332 #: admin/views/entry-detail.php:92 333 333 msgid "Only metadata is captured. The uploaded files themselves are not stored by this plugin." 334 334 msgstr "添付ファイルの実体は保存せず、メタ情報(ファイル名・MIME・サイズ)のみを記録しています。" 335 335 336 #: admin/views/entry-detail.php:10 2336 #: admin/views/entry-detail.php:103 337 337 msgid "Note body" 338 338 msgstr "メモ本文" 339 339 340 #: admin/views/entry-detail.php:10 5340 #: admin/views/entry-detail.php:106 341 341 msgid "Add note" 342 342 msgstr "メモを追加" 343 343 344 #: admin/views/entry-detail.php:1 09344 #: admin/views/entry-detail.php:110 345 345 msgid "No notes yet." 346 346 msgstr "まだメモはありません。" 347 347 348 348 #: admin/class-entry-event-presenter.php:119 349 #: admin/views/entry-detail.php:11 5349 #: admin/views/entry-detail.php:116 350 350 msgid "Unknown user" 351 351 msgstr "不明なユーザー" 352 352 353 #: admin/views/entry-detail.php:2 38353 #: admin/views/entry-detail.php:261 354 354 msgid "Change status" 355 355 msgstr "ステータスを変更" 356 356 357 #: admin/views/entry-detail.php:2 46357 #: admin/views/entry-detail.php:269 358 358 msgid "Update" 359 359 msgstr "更新" 360 360 361 #: admin/views/entry-detail.php:2 51361 #: admin/views/entry-detail.php:274 362 362 msgid "Submission info" 363 363 msgstr "送信情報" 364 364 365 #: admin/views/entry-detail.php:2 55365 #: admin/views/entry-detail.php:278 366 366 #: includes/class-export-service.php:50 367 367 msgid "Form ID" 368 368 msgstr "フォーム ID" 369 369 370 #: admin/views/entry-detail.php:2 56370 #: admin/views/entry-detail.php:279 371 371 #: includes/class-export-service.php:55 372 372 msgid "Submission page URL" 373 373 msgstr "送信ページ URL" 374 374 375 #: admin/views/entry-detail.php:2 60375 #: admin/views/entry-detail.php:283 376 376 #: includes/class-export-service.php:56 377 377 msgid "Referrer" 378 378 msgstr "参照元" 379 379 380 #: admin/views/entry-detail.php:2 64380 #: admin/views/entry-detail.php:287 381 381 #: includes/class-export-service.php:57 382 382 msgid "Landing URL" 383 383 msgstr "ランディング URL" 384 384 385 #: admin/views/entry-detail.php:2 70385 #: admin/views/entry-detail.php:293 386 386 msgid "User Agent" 387 387 msgstr "User Agent" 388 388 389 #: admin/views/entry-detail.php:2 71389 #: admin/views/entry-detail.php:294 390 390 #: includes/class-export-service.php:64 391 391 msgid "IP address" 392 392 msgstr "IP アドレス" 393 393 394 #: admin/views/entry-detail.php:2 72394 #: admin/views/entry-detail.php:295 395 395 #: includes/class-export-service.php:65 396 396 msgid "Spam flag" 397 397 msgstr "スパム判定" 398 398 399 #: admin/views/entry-detail.php:2 72399 #: admin/views/entry-detail.php:295 400 400 #: admin/views/export.php:62 401 401 msgid "Spam" 402 402 msgstr "スパム" 403 403 404 #: admin/views/entry-detail.php:2 72404 #: admin/views/entry-detail.php:295 405 405 msgid "Normal" 406 406 msgstr "通常" … … 665 665 msgstr "設定を保存" 666 666 667 #: includes/class-entry-repository.php: 25667 #: includes/class-entry-repository.php:38 668 668 msgid "New" 669 669 msgstr "未対応" 670 670 671 #: includes/class-entry-repository.php: 26671 #: includes/class-entry-repository.php:39 672 672 msgid "In progress" 673 673 msgstr "対応中" 674 674 675 #: includes/class-entry-repository.php: 27675 #: includes/class-entry-repository.php:40 676 676 msgid "On hold" 677 677 msgstr "保留" 678 678 679 #: includes/class-entry-repository.php: 28679 #: includes/class-entry-repository.php:41 680 680 msgid "Done" 681 681 msgstr "完了" … … 693 693 msgstr "過去の問い合わせから対応履歴を準備しています。通常どおりご利用いただけます。" 694 694 695 #. translators: 1: number of records processed, 2: total records, 3: percentage complete696 #: admin/class-admin-menu.php:188697 #, php-format698 msgid "%1$s of %2$s (%3$s%%)"699 msgstr "%2$s 件中 %1$s 件(%3$s%%)"700 701 695 #: admin/class-entry-event-presenter.php:54 702 696 msgid "Status changed" … … 720 714 721 715 #: admin/class-entry-event-presenter.php:89 722 #: admin/views/entry-detail.php:14 1716 #: admin/views/entry-detail.php:142 723 717 msgid "Activity" 724 718 msgstr "対応履歴" … … 762 756 msgstr "メモを見る" 763 757 764 #: admin/views/entry-detail.php:14 3758 #: admin/views/entry-detail.php:145 765 759 msgid "History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved." 766 760 msgstr "アップデート前の履歴は、受信日時と既存メモから確認できる範囲のみ表示されます。" 767 761 768 #: admin/views/entry-detail.php:15 5762 #: admin/views/entry-detail.php:158 769 763 msgid "No activity on this page." 770 764 msgstr "このページに表示する対応履歴はありません。" 771 765 772 #: admin/views/entry-detail.php:1 57766 #: admin/views/entry-detail.php:160 773 767 msgid "Back to the latest activity" 774 768 msgstr "最新の対応履歴へ戻る" 775 769 776 #: admin/views/entry-detail.php:16 2770 #: admin/views/entry-detail.php:165 777 771 msgid "Preparing activity history…" 778 772 msgstr "対応履歴を準備しています…" 779 773 780 #: admin/views/entry-detail.php:16 4774 #: admin/views/entry-detail.php:167 781 775 msgid "No activity recorded yet." 782 776 msgstr "対応履歴はまだありません。" 783 777 784 #: admin/views/entry-detail.php: 198778 #: admin/views/entry-detail.php:201 785 779 msgid "Activity pagination" 786 780 msgstr "対応履歴のページ送り" 787 781 788 782 #. translators: 1: current page number, 2: total number of pages 789 #: admin/views/entry-detail.php:20 3783 #: admin/views/entry-detail.php:206 790 784 #, php-format 791 785 msgid "Page %1$s of %2$s" 792 786 msgstr "%2$s ページ中 %1$s ページ" 793 787 794 #: admin/views/entry-detail.php:21 1788 #: admin/views/entry-detail.php:214 795 789 msgid "Newer activity" 796 790 msgstr "新しい対応履歴" 797 791 798 #: admin/views/entry-detail.php:21 6792 #: admin/views/entry-detail.php:219 799 793 msgid "Older activity" 800 794 msgstr "古い対応履歴" … … 815 809 msgid "For entries received before this feature was added, activity is limited to the received timestamp and any internal notes already saved. Earlier status, trash, and spam changes were never recorded, so they are not shown." 816 810 msgstr "この機能が追加される前に受信した問い合わせでは、対応履歴は受信日時と保存済みのメモのみとなります。それ以前のステータス・ゴミ箱・スパムの変更は記録されていないため、表示されません。" 811 812 #. translators: 1: number of records processed, 2: total records to process, 3: percentage complete. "Records" counts processing units (entries plus notes), NOT the number of inquiries. 813 #: admin/class-admin-menu.php:188 814 #, php-format 815 msgid "Processed %1$s of %2$s records (%3$s%%)." 816 msgstr "%2$s 件の履歴データのうち %1$s 件を処理しました(%3$s%%)。" -
neutrope-lead-desk-for-contact-form-7/trunk/languages/neutrope-lead-desk-for-contact-form-7.pot
r3666139 r3666532 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0. 2.0\n"5 "Project-Id-Version: Neutrope Lead Desk for Contact Form 7 0.3.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/neutrope-lead-desk-for-contact-form-7\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-08-2 5T14:05:36+00:00\n"12 "POT-Creation-Date: 2026-08-26T04:22:33+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 70 70 msgstr "" 71 71 72 #. translators: 1: number of records processed, 2: total records , 3: percentage complete72 #. translators: 1: number of records processed, 2: total records to process, 3: percentage complete. "Records" counts processing units (entries plus notes), NOT the number of inquiries. 73 73 #: admin/class-admin-menu.php:188 74 74 #, php-format 75 msgid " %1$s of %2$s (%3$s%%)"75 msgid "Processed %1$s of %2$s records (%3$s%%)." 76 76 msgstr "" 77 77 … … 172 172 173 173 #: admin/class-entry-event-presenter.php:53 174 #: admin/class-entry-list-table.php: 238175 #: admin/views/entry-detail.php:2 53174 #: admin/class-entry-list-table.php:363 175 #: admin/views/entry-detail.php:276 176 176 #: includes/class-export-service.php:47 177 177 msgid "Received" … … 199 199 200 200 #: admin/class-entry-event-presenter.php:89 201 #: admin/views/entry-detail.php:14 1201 #: admin/views/entry-detail.php:142 202 202 msgid "Activity" 203 203 msgstr "" … … 208 208 209 209 #: admin/class-entry-event-presenter.php:119 210 #: admin/views/entry-detail.php:11 5210 #: admin/views/entry-detail.php:116 211 211 msgid "Unknown user" 212 212 msgstr "" … … 246 246 msgstr "" 247 247 248 #: admin/class-entry-list-table.php: 211248 #: admin/class-entry-list-table.php:336 249 249 #: admin/views/export.php:43 250 250 msgid "All" 251 251 msgstr "" 252 252 253 #: admin/class-entry-list-table.php: 227254 #: admin/class-entry-list-table.php: 361253 #: admin/class-entry-list-table.php:352 254 #: admin/class-entry-list-table.php:570 255 255 #: admin/views/export.php:72 256 256 msgid "Trash" 257 257 msgstr "" 258 258 259 #: admin/class-entry-list-table.php: 239260 #: admin/class-entry-list-table.php: 471261 #: admin/views/entry-detail.php:2 28259 #: admin/class-entry-list-table.php:364 260 #: admin/class-entry-list-table.php:680 261 #: admin/views/entry-detail.php:251 262 262 #: admin/views/export.php:40 263 263 #: includes/class-export-service.php:48 … … 265 265 msgstr "" 266 266 267 #: admin/class-entry-list-table.php: 240268 #: admin/class-entry-list-table.php: 479269 #: admin/views/entry-detail.php:2 54267 #: admin/class-entry-list-table.php:365 268 #: admin/class-entry-list-table.php:688 269 #: admin/views/entry-detail.php:277 270 270 #: admin/views/export.php:51 271 271 #: includes/class-export-service.php:49 … … 273 273 msgstr "" 274 274 275 #: admin/class-entry-list-table.php: 241275 #: admin/class-entry-list-table.php:366 276 276 #: admin/views/settings.php:73 277 277 #: includes/class-export-service.php:51 … … 279 279 msgstr "" 280 280 281 #: admin/class-entry-list-table.php: 242281 #: admin/class-entry-list-table.php:367 282 282 #: admin/views/settings.php:77 283 283 #: includes/class-export-service.php:52 … … 285 285 msgstr "" 286 286 287 #: admin/class-entry-list-table.php: 243287 #: admin/class-entry-list-table.php:368 288 288 #: admin/views/settings.php:81 289 289 #: includes/class-export-service.php:53 … … 291 291 msgstr "" 292 292 293 #: admin/class-entry-list-table.php: 244293 #: admin/class-entry-list-table.php:369 294 294 msgid "Source" 295 295 msgstr "" 296 296 297 #: admin/class-entry-list-table.php: 245297 #: admin/class-entry-list-table.php:370 298 298 msgid "UTM source" 299 299 msgstr "" 300 300 301 #: admin/class-entry-list-table.php: 246302 #: admin/views/entry-detail.php:9 6301 #: admin/class-entry-list-table.php:371 302 #: admin/views/entry-detail.php:97 303 303 msgid "Notes" 304 304 msgstr "" 305 305 306 #: admin/class-entry-list-table.php: 264307 #: admin/class-entry-list-table.php: 342306 #: admin/class-entry-list-table.php:473 307 #: admin/class-entry-list-table.php:551 308 308 msgid "Restore" 309 309 msgstr "" 310 310 311 #: admin/class-entry-list-table.php: 269311 #: admin/class-entry-list-table.php:478 312 312 msgid "Set status: New" 313 313 msgstr "" 314 314 315 #: admin/class-entry-list-table.php: 270315 #: admin/class-entry-list-table.php:479 316 316 msgid "Set status: In progress" 317 317 msgstr "" 318 318 319 #: admin/class-entry-list-table.php: 271319 #: admin/class-entry-list-table.php:480 320 320 msgid "Set status: On hold" 321 321 msgstr "" 322 322 323 #: admin/class-entry-list-table.php: 272323 #: admin/class-entry-list-table.php:481 324 324 msgid "Set status: Done" 325 325 msgstr "" 326 326 327 #: admin/class-entry-list-table.php: 273327 #: admin/class-entry-list-table.php:482 328 328 msgid "Move to trash" 329 329 msgstr "" 330 330 331 #: admin/class-entry-list-table.php: 274331 #: admin/class-entry-list-table.php:483 332 332 msgid "Mark as spam" 333 333 msgstr "" 334 334 335 #: admin/class-entry-list-table.php: 299335 #: admin/class-entry-list-table.php:508 336 336 msgid "View" 337 337 msgstr "" 338 338 339 #: admin/class-entry-list-table.php: 430339 #: admin/class-entry-list-table.php:639 340 340 msgid "None" 341 341 msgstr "" 342 342 343 #: admin/class-entry-list-table.php: 437343 #: admin/class-entry-list-table.php:646 344 344 msgid "No entries are in the trash." 345 345 msgstr "" 346 346 347 #: admin/class-entry-list-table.php: 440347 #: admin/class-entry-list-table.php:649 348 348 msgid "No entries match the current filters." 349 349 msgstr "" 350 350 351 #: admin/class-entry-list-table.php: 468352 #: admin/class-entry-list-table.php: 469351 #: admin/class-entry-list-table.php:677 352 #: admin/class-entry-list-table.php:678 353 353 msgid "Search keywords" 354 354 msgstr "" 355 355 356 #: admin/class-entry-list-table.php: 473356 #: admin/class-entry-list-table.php:682 357 357 msgid "All statuses" 358 358 msgstr "" 359 359 360 #: admin/class-entry-list-table.php: 481360 #: admin/class-entry-list-table.php:690 361 361 #: admin/views/export.php:54 362 362 msgid "All forms" 363 363 msgstr "" 364 364 365 #: admin/class-entry-list-table.php: 487365 #: admin/class-entry-list-table.php:696 366 366 #: admin/views/export.php:32 367 367 msgid "From date" 368 368 msgstr "" 369 369 370 #: admin/class-entry-list-table.php: 489370 #: admin/class-entry-list-table.php:698 371 371 #: admin/views/export.php:36 372 372 msgid "To date" 373 373 msgstr "" 374 374 375 #: admin/class-entry-list-table.php: 494375 #: admin/class-entry-list-table.php:703 376 376 msgid "Show spam" 377 377 msgstr "" 378 378 379 #: admin/class-entry-list-table.php: 497379 #: admin/class-entry-list-table.php:726 380 380 msgid "Filter" 381 381 msgstr "" 382 382 383 383 #. translators: %d: entry ID 384 #: admin/views/entry-detail.php:3 1384 #: admin/views/entry-detail.php:32 385 385 #, php-format 386 386 msgid "Entry #%d" 387 387 msgstr "" 388 388 389 #: admin/views/entry-detail.php:3 4389 #: admin/views/entry-detail.php:35 390 390 msgid "Back to list" 391 391 msgstr "" 392 392 393 #: admin/views/entry-detail.php:4 7393 #: admin/views/entry-detail.php:48 394 394 msgid "Submitted fields" 395 395 msgstr "" 396 396 397 #: admin/views/entry-detail.php:5 1397 #: admin/views/entry-detail.php:52 398 398 msgid "No submitted fields stored." 399 399 msgstr "" 400 400 401 #: admin/views/entry-detail.php:7 2401 #: admin/views/entry-detail.php:73 402 402 msgid "Attachments (metadata only)" 403 403 msgstr "" 404 404 405 #: admin/views/entry-detail.php:7 6405 #: admin/views/entry-detail.php:77 406 406 msgid "File name" 407 407 msgstr "" 408 408 409 #: admin/views/entry-detail.php:7 7409 #: admin/views/entry-detail.php:78 410 410 msgid "MIME" 411 411 msgstr "" 412 412 413 #: admin/views/entry-detail.php:7 8413 #: admin/views/entry-detail.php:79 414 414 msgid "Size (bytes)" 415 415 msgstr "" 416 416 417 #: admin/views/entry-detail.php:9 1417 #: admin/views/entry-detail.php:92 418 418 msgid "Only metadata is captured. The uploaded files themselves are not stored by this plugin." 419 419 msgstr "" 420 420 421 #: admin/views/entry-detail.php:10 2421 #: admin/views/entry-detail.php:103 422 422 msgid "Note body" 423 423 msgstr "" 424 424 425 #: admin/views/entry-detail.php:10 5425 #: admin/views/entry-detail.php:106 426 426 msgid "Add note" 427 427 msgstr "" 428 428 429 #: admin/views/entry-detail.php:1 09429 #: admin/views/entry-detail.php:110 430 430 msgid "No notes yet." 431 431 msgstr "" 432 432 433 #: admin/views/entry-detail.php:14 3433 #: admin/views/entry-detail.php:145 434 434 msgid "History from before this plugin was updated is limited to what could be reconstructed: the time each entry arrived and any notes already saved." 435 435 msgstr "" 436 436 437 #: admin/views/entry-detail.php:15 5437 #: admin/views/entry-detail.php:158 438 438 msgid "No activity on this page." 439 439 msgstr "" 440 440 441 #: admin/views/entry-detail.php:1 57441 #: admin/views/entry-detail.php:160 442 442 msgid "Back to the latest activity" 443 443 msgstr "" 444 444 445 #: admin/views/entry-detail.php:16 2445 #: admin/views/entry-detail.php:165 446 446 msgid "Preparing activity history…" 447 447 msgstr "" 448 448 449 #: admin/views/entry-detail.php:16 4449 #: admin/views/entry-detail.php:167 450 450 msgid "No activity recorded yet." 451 451 msgstr "" 452 452 453 #: admin/views/entry-detail.php: 198453 #: admin/views/entry-detail.php:201 454 454 msgid "Activity pagination" 455 455 msgstr "" 456 456 457 457 #. translators: 1: current page number, 2: total number of pages 458 #: admin/views/entry-detail.php:20 3458 #: admin/views/entry-detail.php:206 459 459 #, php-format 460 460 msgid "Page %1$s of %2$s" 461 461 msgstr "" 462 462 463 #: admin/views/entry-detail.php:21 1463 #: admin/views/entry-detail.php:214 464 464 msgid "Newer activity" 465 465 msgstr "" 466 466 467 #: admin/views/entry-detail.php:21 6467 #: admin/views/entry-detail.php:219 468 468 msgid "Older activity" 469 469 msgstr "" 470 470 471 #: admin/views/entry-detail.php:2 38471 #: admin/views/entry-detail.php:261 472 472 msgid "Change status" 473 473 msgstr "" 474 474 475 #: admin/views/entry-detail.php:2 46475 #: admin/views/entry-detail.php:269 476 476 msgid "Update" 477 477 msgstr "" 478 478 479 #: admin/views/entry-detail.php:2 51479 #: admin/views/entry-detail.php:274 480 480 msgid "Submission info" 481 481 msgstr "" 482 482 483 #: admin/views/entry-detail.php:2 55483 #: admin/views/entry-detail.php:278 484 484 #: includes/class-export-service.php:50 485 485 msgid "Form ID" 486 486 msgstr "" 487 487 488 #: admin/views/entry-detail.php:2 56488 #: admin/views/entry-detail.php:279 489 489 #: includes/class-export-service.php:55 490 490 msgid "Submission page URL" 491 491 msgstr "" 492 492 493 #: admin/views/entry-detail.php:2 60493 #: admin/views/entry-detail.php:283 494 494 #: includes/class-export-service.php:56 495 495 msgid "Referrer" 496 496 msgstr "" 497 497 498 #: admin/views/entry-detail.php:2 64498 #: admin/views/entry-detail.php:287 499 499 #: includes/class-export-service.php:57 500 500 msgid "Landing URL" 501 501 msgstr "" 502 502 503 #: admin/views/entry-detail.php:2 70503 #: admin/views/entry-detail.php:293 504 504 msgid "User Agent" 505 505 msgstr "" 506 506 507 #: admin/views/entry-detail.php:2 71507 #: admin/views/entry-detail.php:294 508 508 #: includes/class-export-service.php:64 509 509 msgid "IP address" 510 510 msgstr "" 511 511 512 #: admin/views/entry-detail.php:2 72512 #: admin/views/entry-detail.php:295 513 513 #: includes/class-export-service.php:65 514 514 msgid "Spam flag" 515 515 msgstr "" 516 516 517 #: admin/views/entry-detail.php:2 72517 #: admin/views/entry-detail.php:295 518 518 #: admin/views/export.php:62 519 519 msgid "Spam" 520 520 msgstr "" 521 521 522 #: admin/views/entry-detail.php:2 72522 #: admin/views/entry-detail.php:295 523 523 msgid "Normal" 524 524 msgstr "" … … 799 799 msgstr "" 800 800 801 #: includes/class-entry-repository.php: 25801 #: includes/class-entry-repository.php:38 802 802 msgid "New" 803 803 msgstr "" 804 804 805 #: includes/class-entry-repository.php: 26805 #: includes/class-entry-repository.php:39 806 806 msgid "In progress" 807 807 msgstr "" 808 808 809 #: includes/class-entry-repository.php: 27809 #: includes/class-entry-repository.php:40 810 810 msgid "On hold" 811 811 msgstr "" 812 812 813 #: includes/class-entry-repository.php: 28813 #: includes/class-entry-repository.php:41 814 814 msgid "Done" 815 815 msgstr "" -
neutrope-lead-desk-for-contact-form-7/trunk/neutrope-lead-desk-for-contact-form-7.php
r3666139 r3666532 4 4 * Plugin URI: https://neutrope.biz/neutrope-lead-desk-for-contact-form-7/ 5 5 * Description: Save Contact Form 7 submissions to the database and manage them as leads in the WordPress admin (list, detail, status, notes, CSV export). 6 * Version: 0. 2.06 * Version: 0.3.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 22 22 } 23 23 24 define( 'NLDCF7_VERSION', '0. 2.0' );24 define( 'NLDCF7_VERSION', '0.3.0' ); 25 25 define( 'NLDCF7_DB_VERSION', '1.1.0' ); 26 26 define( 'NLDCF7_PLUGIN_FILE', __FILE__ ); -
neutrope-lead-desk-for-contact-form-7/trunk/readme.txt
r3666139 r3666532 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0. 2.07 Stable tag: 0.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 16 16 17 17 The free version is intentionally focused: it saves submissions reliably, gives you an admin list/detail UI, lets you change status, add internal notes, and export to CSV. It does this without pulling in external services, trackers, or upsell banners on unrelated screens. 18 19 Version 0.3.0 is mainly a foundation release for add-ons: it adds documented, generic extension points so other plugins can add columns, filters and detail-screen panels without forking this one. Nothing in the plugin's own behaviour changes because of them, and no database change is involved. See "Extending the plugin" below. 18 20 19 21 Since 0.2.0 each entry also keeps an **Activity Timeline** (対応履歴) so you can see what has already been done about an inquiry -- when it arrived, who changed its status and to what, when notes were added, and when it was moved to trash, restored, or marked as spam -- instead of inferring the handling history from the current status alone. … … 34 36 * Privacy-friendly defaults: IP capture is OFF by default 35 37 * Opt-in data deletion on uninstall (off by default so accidental deletes don't nuke your leads) 36 * Extensibility via action/filter hooks: `nldcf7_after_entry_saved`, `nldcf7_after_note_added`, `nldcf7_after_status_changed`, `nldcf7_after_entry_trashed`, `nldcf7_after_entry_restored`, `nldcf7_after_spam_changed`, `nldcf7_entry_data_before_insert`, `nldcf7_export_columns`, `nldcf7_export_row`, `nldcf7_statuses`, `nldcf7_event_render`, `nldcf7_required_capability` 38 * Extensibility via action/filter hooks: `nldcf7_after_entry_saved`, `nldcf7_after_note_added`, `nldcf7_after_status_changed`, `nldcf7_after_entry_trashed`, `nldcf7_after_entry_restored`, `nldcf7_after_spam_changed`, `nldcf7_entry_data_before_insert`, `nldcf7_export_columns`, `nldcf7_export_row`, `nldcf7_statuses`, `nldcf7_event_labels`, `nldcf7_event_render`, `nldcf7_required_capability` 39 * Since 0.3.0, nine further extension points let an add-on place panels on the entry detail screen, add list columns and filter controls, keep its own filter state across list actions, and constrain the entries query -- all documented under "Extending the plugin" 37 40 38 41 **Requirements** … … 106 109 Please open an issue on the plugin's support forum or the project's issue tracker. 107 110 111 == Extending the plugin == 112 113 Version 0.3.0 adds nine extension points for add-on developers. They are 114 generic: this plugin knows nothing about what an add-on stores. All of them are 115 no-ops when nothing is listening, so a site with no add-on behaves exactly as 116 it did in 0.2.0. 117 118 = nldcf7_entry_detail_main = 119 120 Action. Introduced in 0.3.0. Fires on the entry detail screen after this 121 plugin's own cards in the main column, before the column closes. 122 123 `do_action( 'nldcf7_entry_detail_main', array $entry )` 124 125 `$entry` is the entry row as stored, passed by value. The listener prints its 126 own markup, including its own card wrapper, and is responsible for escaping it. 127 Output is not buffered or altered. 128 129 = nldcf7_entry_detail_sidebar = 130 131 Action. Introduced in 0.3.0. The same contract, for the sidebar column. Two 132 hooks exist so an add-on can place content in both columns of one screen. 133 134 `do_action( 'nldcf7_entry_detail_sidebar', array $entry )` 135 136 = nldcf7_entry_list_columns = 137 138 Filter. Introduced in 0.3.0. Adds columns to the entries list. 139 140 `apply_filters( 'nldcf7_entry_list_columns', array $columns )` 141 142 Return the column map with your own keys appended; prefix them to avoid 143 collisions. Core columns cannot be removed, renamed or repurposed -- every core 144 key is restored to its own label after the filter runs, and the checkbox column 145 is forced back to the front. Labels are plain text and are escaped on output. 146 147 = nldcf7_entry_list_column_content = 148 149 Filter. Introduced in 0.3.0. Renders a cell for a column you added. 150 151 `apply_filters( 'nldcf7_entry_list_column_content', string $html, string $column_name, array $item )` 152 153 Return HTML for your own column, or the value you were given for anything else. 154 Escape your own output; the plugin then passes the result through 155 `wp_kses_post()` as a second layer, so ordinary post markup survives while 156 scripts and event handlers do not. A non-string return renders an empty cell. 157 158 = nldcf7_entry_list_filters = 159 160 Action. Introduced in 0.3.0. Fires inside the list's existing GET filter form, 161 before the Filter button, so a plain select or input submits alongside the 162 built-in filters. 163 164 `do_action( 'nldcf7_entry_list_filters', array $current_args )` 165 166 `$current_args` is this plugin's own sanitized filter state, for reference. You 167 own your field names, your values and your escaping. No nonce is involved: the 168 filter bar only reads. 169 170 = nldcf7_entry_list_carried_args = 171 172 Filter. Introduced in 0.3.0. Keeps your own filter state across the All and 173 Trash views, the row actions, and the redirect after a row or bulk action. 174 175 `apply_filters( 'nldcf7_entry_list_carried_args', array $args, array $core )` 176 177 Add your own already-sanitized scalar values. The plugin re-checks the result 178 afterwards: reserved keys (`page`, `paged`, `action`, `action2`, `entry_id`, 179 `_wpnonce`, `_wp_http_referer`, `view`, `event_page`, `status_view`) are 180 stripped, keys are normalised, non-scalars are dropped, and both value length 181 and argument count are capped. Core values always win. Do not attempt to 182 forward the whole query string. 183 184 = nldcf7_entry_query_args = 185 186 Filter. Introduced in 0.3.0. Adjusts the entries query using arguments the 187 repository already understands, without writing SQL. 188 189 `apply_filters( 'nldcf7_entry_query_args', array $args )` 190 191 The most useful is `ids`, an array of entry IDs to restrict the query to. 192 `$args['context']` says who is asking: `list` for the entries screen, `export` 193 for CSV, `default` otherwise. Check it -- both callers share the same 194 repository, so an unconditional constraint would also change what CSV exports. 195 196 = nldcf7_entry_query_clauses = 197 198 Filter. Introduced in 0.3.0. Constrains the entries query using a table of your 199 own, resolved by the database rather than by loading matching IDs into PHP. 200 201 `apply_filters( 'nldcf7_entry_query_clauses', array $parts, array $args )` 202 203 `$parts` has four keys: 204 205 * `join` -- a JOIN clause string. 206 * `where` -- an array of condition strings, each using `%s` or `%d` placeholders. 207 * `prepare_args` -- the values for those placeholders, in order. 208 * `distinct` -- boolean, default false. 209 210 Values must go in `prepare_args`, never inside the condition strings; the plugin 211 passes them to `$wpdb->prepare()`. A single pre-built SQL string is not 212 accepted. If the placeholder count and the value count disagree, the whole 213 contribution is discarded. Table and column identifiers inside `join` are your 214 responsibility. Set `distinct` when your JOIN can match a row more than once: it 215 is applied to the row query and the count together, so the list total and 216 pagination stay correct. `$args['context']` applies here too. 217 218 = nldcf7_entry_list_items_prepared = 219 220 Action. Introduced in 0.3.0. Fires once per list page after the rows are known 221 and before any cell is rendered. 222 223 `do_action( 'nldcf7_entry_list_items_prepared', array $items )` 224 225 `$items` is the current page's rows only -- never the whole result set. Use it 226 to load everything your columns need in one query and keep it in a 227 request-local cache, instead of querying once per cell. It is read-only, has no 228 return value, and fires with an empty array when the page has no rows. It 229 belongs to the entries screen only and never fires during CSV export. 230 108 231 == Screenshots == 109 232 … … 115 238 116 239 == Changelog == 240 241 = 0.3.0 = 242 * Add: generic extension points for the entry detail screen -- `nldcf7_entry_detail_main` and `nldcf7_entry_detail_sidebar` -- so an add-on can place panels in either column without modifying this plugin. 243 * Add: generic list extension points -- `nldcf7_entry_list_columns` for columns and `nldcf7_entry_list_column_content` for their cells. Core columns are protected: an add-on cannot remove, rename or repurpose one, and returned cell markup passes through `wp_kses_post()`. 244 * Add: `nldcf7_entry_list_filters`, which places an add-on's own controls inside the existing filter bar. 245 * Add: `nldcf7_entry_list_carried_args`, so an add-on's own filter stays applied across the All and Trash views and after row and bulk actions. Reserved keys such as nonces, action names and entry IDs are refused, non-scalar values are dropped, and size and count are capped, so a careless listener cannot put unsafe state into a list URL. 246 * Add: `nldcf7_entry_query_args` for high-level query constraints, including the existing `ids` argument, and `nldcf7_entry_query_clauses` for constraints that live in an add-on's own table. Clauses are structured -- a JOIN, conditions with placeholders, and their values kept separate -- rather than a raw SQL string, and values always go through `$wpdb->prepare()`. 247 * Add: optional `distinct` handling for add-on joins that can match a row more than once. It is applied to the row query and the count together, so the list and its total can never disagree. 248 * Add: query context. The entries screen and the CSV exporter share one repository, so each query now says which it is. An add-on constraint meant for the list no longer reaches CSV export unless the add-on opts in. 249 * Add: `nldcf7_entry_list_items_prepared`, which hands an add-on the current page's rows once, before any cell is rendered, so custom columns can load their data in a single query instead of one per cell. 250 * Fix: the Activity preparation notice counted processing records (entries plus notes) but read as a number of inquiries. It now names the unit; the arithmetic is unchanged. 251 * Fix: the "history from before this plugin was updated" note no longer appears on sites that installed this version fresh and therefore have no earlier history to explain. 252 * No database change. The schema and database version (1.1.0) are identical to 0.2.0, no migration runs, and existing entries, notes, attachments, activity, settings, capabilities and URLs are untouched. With no add-on installed the plugin behaves exactly as 0.2.0 did. 117 253 118 254 = 0.2.0 = … … 165 301 == Upgrade Notice == 166 302 303 = 0.3.0 = 304 Adds a documented extension foundation for add-ons, plus two small Activity-history wording fixes. Your leads, notes, activity and database schema are unchanged, and no migration runs. With no add-on installed the plugin behaves exactly as before. 305 167 306 = 0.2.0 = 168 307 Adds a per-entry activity history. Existing entries have their received time and existing notes reconstructed in the background; status, trash, and spam changes made before this update were never recorded and are not invented. Existing lead data is preserved.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)