$p ) { $p = (string) $p; if ( '' === (string) preg_replace( '/[\s\p{C}\p{Z}]+/u', '', $p ) ) { continue; // blank / whitespace / zero-width → silent skip } // Re-use the single source of truth instead of re-testing here: matching against // an EMPTY subject never backtracks, so it silently declares a catastrophic // pattern healthy. validate_regex_pattern() probes a ~1080-char string and also // treats a compile failure as invalid. // A short dot-less pattern is not a regex at all — it is the too-broad host // fragment OWCO_Core::match_pattern() refuses and records — so keep it as well. $too_short = ( strlen( $p ) < 6 && strpos( $p, '.' ) === false ); if ( $too_short || ! OWCO_Core::validate_regex_pattern( $p ) ) { $still_bad[ $h ] = $p; } } if ( empty( $still_bad ) ) { // Nothing genuinely invalid anymore → clear the stale transient and show nothing. delete_transient( 'owco_invalid_patterns' ); return; } if ( count( $still_bad ) !== count( $list ) ) { set_transient( 'owco_invalid_patterns', $still_bad, DAY_IN_SECONDS ); } echo '
OW Consent: ' . esc_html__( 'Some tracker-catalogue patterns are invalid and were skipped, so whatever they were meant to block is not blocked:', 'ow-consent' ) . '
' . esc_html( $p ) . '' . esc_html__( 'Repair them in the tracker catalogue', 'ow-consent' ) . '
OW Consent: ' . esc_html( $warning ) . '
Consent
·
optionweb.dev
, 2: applied profile in , 3: detection source in */
wp_kses_post( __( 'Visitor detected: country %1$s — applied profile: %2$s — source: %3$s', 'ow-consent' ) ),
'' . esc_html( $geo['country'] ?: '—' ) . '',
'' . esc_html( $geo['profile'] ) . '',
'' . esc_html( $geo['source'] ) . ''
);
?>
✓
0 && $newest > 0 && $oldest !== $newest ) { printf( /* translators: 1: number of refused writes. 2: date and time of the oldest one. 3: date and time of the most recent one. */ esc_html( _n( '%1$s choice was refused, between %2$s and %3$s.', '%1$s choices were refused, between %2$s and %3$s.', $total, 'ow-consent' ) ), esc_html( number_format_i18n( $total ) ), esc_html( self::refusal_time_display( $oldest ) ), esc_html( self::refusal_time_display( $newest ) ) ); } elseif ( $newest > 0 ) { printf( /* translators: 1: number of refused writes. 2: date and time of the most recent one. */ esc_html( _n( '%1$s choice was refused, the last one at %2$s.', '%1$s choices were refused, the last one at %2$s.', $total, 'ow-consent' ) ), esc_html( number_format_i18n( $total ) ), esc_html( self::refusal_time_display( $newest ) ) ); } else { printf( /* translators: %s: number of refused writes. */ esc_html( _n( '%s choice was refused.', '%s choices were refused.', $total, 'ow-consent' ) ), esc_html( number_format_i18n( $total ) ) ); } ?>
' . esc_html__( 'Recorded, but not cleanly', 'ow-consent' ) . ''; } else { echo '
| 0 ? (string) $row['status'] : '—' ); ?> |
'', 'fix' => '', 'hint' => '' ); switch ( (string) $code ) { case 'owco_bad_origin': $out['cause'] = __( 'This site is being browsed on an address WordPress does not know it by. Writes are refused on the spot, because a request arriving from an address the site does not recognise cannot be told apart from one forged by another site.', 'ow-consent' ); $out['fix'] = __( 'Make the addresses match. The comparison below is the entire answer — one of those lines is different from the others.', 'ow-consent' ); break; case 'owco_missing_origin': $out['cause'] = __( 'The choice arrived with no Origin header, no Referer header and no security token, so nothing whatsoever showed that it came from this site, and it was refused rather than trusted. Something sitting in front of WordPress — a CDN, a reverse proxy, a security or optimisation layer — is removing those headers.', 'ow-consent' ); $out['fix'] = __( 'Ask whoever runs that layer to stop stripping Origin and Referer from POST requests to the address below. If these are instead your own server-to-server calls, allow them deliberately with the owco_allow_headerless_write filter.', 'ow-consent' ); $out['hint'] = '/wp-json/owco/v1/consent'; break; case 'owco_rate_limited': $out['cause'] = __( 'Too many choices were counted against one visitor address within five minutes. This is almost never a real flood: behind a proxy or a CDN the plugin has not been told about, every visitor arrives wearing the same address, so a handful of them exhaust a single budget between them.', 'ow-consent' ); $out['fix'] = __( 'Declare the proxy in wp-config.php so the real visitor address is read instead of the proxy address: give OWCO_TRUSTED_PROXY your proxy or CDN address ranges, comma separated, in place of the example range below.', 'ow-consent' ); $out['hint'] = "define( 'OWCO_TRUSTED_PROXY', '203.0.113.0/24' );"; break; case 'owco_bad_nonce': $out['cause'] = __( 'The one-time security token sent with the choice was rejected as expired or invalid. On a cached site that normally means the banner is being served from a stored copy older than the token it carries, or that the token was issued for one address and sent back to another.', 'ow-consent' ); $out['fix'] = __( 'Exclude the addresses below from every page cache, object cache and CDN copy, then empty the caches once. They have to be answered by WordPress itself, every time.', 'ow-consent' ); $out['hint'] = '/wp-json/owco/v1/'; break; case 'owco_headers_already_sent': $out['cause'] = __( 'By the time the consent cookie had to be written, this site had already started sending the page, so the cookie could not be set at all. A stray blank line or a warning printed by a theme or another plugin is the usual culprit.', 'ow-consent' ); $out['fix'] = __( 'On a staging copy, switch debugging on to find what is printed before the page headers, or deactivate plugins one at a time until it stops. One blank line after a closing PHP tag is enough to cause this.', 'ow-consent' ); break; case 'owco_ledger_write_refused': $out['cause'] = __( 'The choice could not be written into the consent register, so it was refused instead of being reported as saved with no proof behind it. Either the register table is missing, or the database turned the insert down.', 'ow-consent' ); $out['fix'] = __( 'Open the "Audit ledger" tab. If it reports the table as missing, deactivate and reactivate the plugin so the tables are rebuilt. If the table is there, the database user is most likely not allowed to write to it and only the host can grant that.', 'ow-consent' ); break; case 'owco_consent_failed': $out['cause'] = __( 'The site accepted the request but could not apply the choice, and refused it rather than claiming a success it could not deliver.', 'ow-consent' ); $out['fix'] = __( 'The underlying failure is in the server error log at the times listed below. Read it there, or ask your host for that extract.', 'ow-consent' ); break; case 'owco_bad_categories': case 'owco_bad_param': $out['cause'] = __( 'The choice reached this site in a shape the endpoint does not accept. Something between the browser and WordPress rewrote the request: a security module, a script-optimisation plugin, or a cached copy of the banner script older than the plugin itself.', 'ow-consent' ); $out['fix'] = __( 'Empty every cache, the CDN included, and load the site once. If the refusals continue, disable request-filtering and script-optimisation modules one at a time until they stop.', 'ow-consent' ); break; case 'owco_forbidden': case 'rest_forbidden': $out['cause'] = __( 'WordPress refused the request before this plugin ever saw it. On most sites that means a security plugin, or a rule on the server, that blocks anonymous POST requests to the REST API.', 'ow-consent' ); $out['fix'] = __( 'Allow anonymous POST requests to the addresses below, in your security plugin and — if your host runs one — in the server firewall as well.', 'ow-consent' ); $out['hint'] = '/wp-json/owco/v1/'; break; case 'rest_no_route': $out['cause'] = __( 'The address the banner writes to does not exist as far as WordPress is concerned: the REST API is switched off, or the permalink rules that route it were never written.', 'ow-consent' ); $out['fix'] = __( 'Open Settings — Permalinks and save once, without changing anything, to rewrite the rules. If a plugin closes the REST API to visitors who are not logged in, it has to make an exception for this address.', 'ow-consent' ); $out['hint'] = '/wp-json/owco/v1/consent'; break; default: $out['cause'] = __( 'This site refused the write with the code shown above, which is not one of the causes described here. It comes from WordPress itself, or from another plugin answering in place of this one.', 'ow-consent' ); $out['fix'] = __( 'Send this panel, code and all, to whoever maintains the site. The code, the HTTP status and the two addresses in the table below are the whole diagnosis — nothing has to be reproduced in a browser.', 'ow-consent' ); break; } return $out; } /** * The same, for the events that did NOT refuse the write. * * @param string $code Code, already sanitized. * @return array{cause:string,fix:string} */ protected static function refusal_note_explanation( $code ) { switch ( (string) $code ) { case 'rest_cookie_invalid_nonce': return array( 'cause' => __( 'WordPress rejected the login token that came with the choice as belonging to somebody else. The plugin overrode that verdict, so the choice was still recorded — but a visitor receiving another visitor login token means a cache in front of this site is storing and re-serving pages that were built for a signed-in person. That affects far more than the cookie banner.', 'ow-consent' ), 'fix' => __( 'Configure the page cache and the CDN never to store a response that carries a login cookie, and empty both once. If the site is behind a proxy that adds its own caching, it has to be told the same thing.', 'ow-consent' ), ); case 'owco_headers_already_sent': return array( 'cause' => __( 'The choice was recorded, but this site had already started sending the page by the time the consent cookie had to be written, so the server could not set it and the browser had to do it instead. A stray blank line or a warning printed by a theme or another plugin is the usual culprit.', 'ow-consent' ), 'fix' => __( 'On a staging copy, switch debugging on to find what is printed before the page headers, or deactivate plugins one at a time until it stops. Until then, a visitor whose browser refuses that fallback is asked again on every page.', 'ow-consent' ), ); case 'owco_cookie_not_persisted': return array( 'cause' => __( 'What visitors see: they accept or refuse, the banner goes away, and on the very next page it is back and they are asked again — over and over, as if nothing had been recorded. Something was: every one of those choices is in the register, and this site can evidence them. What is missing is on the visitor side. The address people are browsing is not the address WordPress is configured with, so the banner sends the choice to a different address from the one in the address bar. A browser treats that as another site and throws away the cookie that comes back from it, and it is that cookie that remembers the choice from one page to the next.', 'ow-consent' ), 'fix' => __( 'Make the two addresses the same, in Settings — General. The comparison below is the entire answer: one of those lines is different from the others, and until it is not, every visitor is asked again on every page.', 'ow-consent' ), ); default: return array( 'cause' => __( 'The choice was recorded, but the endpoint noted something worth reporting about the way the request arrived.', 'ow-consent' ), 'fix' => __( 'Send this panel, code and all, to whoever maintains the site.', 'ow-consent' ), ); } } /** * The rolling record of refused public writes, normalised for display. * * OWCO_REST::refusal_log() already sanitises what it returns; everything is cast * again here because the record is a stored option that may have been written * by a different version of the plugin. Guarded rather than called outright so * that a build whose REST unit predates the record degrades to printing nothing, * instead of fataling on the one screen that has to open when a site is already * misbehaving. * * @return array[] Rows of time/code/status/origin/host/token, newest first. */ protected static function write_refusals() { if ( ! class_exists( 'OWCO_REST' ) || ! method_exists( 'OWCO_REST', 'refusal_log' ) ) { return array(); } $raw = OWCO_REST::refusal_log(); if ( ! is_array( $raw ) ) { return array(); } $rows = array(); foreach ( $raw as $entry ) { if ( ! is_array( $entry ) || ! isset( $entry['code'] ) ) { continue; } $code = sanitize_key( (string) $entry['code'] ); if ( '' === $code ) { continue; } $rows[] = array( 'time' => ( isset( $entry['at'] ) && is_numeric( $entry['at'] ) ) ? max( 0, (int) $entry['at'] ) : 0, 'code' => $code, 'status' => ( isset( $entry['status'] ) && is_numeric( $entry['status'] ) ) ? max( 0, (int) $entry['status'] ) : 0, 'origin' => isset( $entry['origin'] ) ? self::refusal_host( $entry['origin'] ) : '', 'host' => isset( $entry['host'] ) ? self::refusal_host( $entry['host'] ) : '', 'token' => ! empty( $entry['token'] ), ); } // The record is written newest first and read back in that order; sorting here // as well means a hand-edited or migrated option cannot mislabel the window. usort( $rows, function ( $a, $b ) { if ( $a['time'] === $b['time'] ) { return 0; } return ( $a['time'] < $b['time'] ) ? 1 : -1; } ); return $rows; } /** * Render a refusal timestamp in the site timezone. * * @param int $stamp Unix timestamp. * @return string */ protected static function refusal_time_display( $stamp ) { $stamp = (int) $stamp; if ( $stamp <= 0 ) { return '—'; } return (string) wp_date( 'Y-m-d H:i', $stamp ); } /** * A host name safe to print: bare host, lower case, bounded, nothing else. * * The value originates in a request header, so it is attacker-controlled text. * It is escaped again at print time; it is reduced to host characters here so * that nothing but an address can ever be displayed as one. * * @param string|int|float|bool|null $value Raw value, host name or full URL. * @return string */ protected static function refusal_host( $value ) { if ( null === $value || is_bool( $value ) || is_array( $value ) ) { return ''; } $value = trim( (string) $value ); if ( '' === $value ) { return ''; } if ( false !== strpos( $value, '//' ) ) { $parsed = (string) wp_parse_url( $value, PHP_URL_HOST ); if ( '' !== $parsed ) { $value = $parsed; } } $value = (string) preg_replace( '/[^A-Za-z0-9._:\[\]-]/', '', $value ); return substr( strtolower( $value ), 0, 253 ); } protected static function compliance_diagnostic() { $issues = array(); if ( ! OWCO_Core::setting( 'banner_enabled' ) ) { $issues[] = array( 'level' => 'bad', 'message' => __( 'The banner is disabled: no consent is being collected.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'banner_reject_all' ) ) { $issues[] = array( 'level' => 'bad', 'message' => __( 'GDPR/CNIL: the "Reject all" button is mandatory and must be as visible as "Accept all".', 'ow-consent' ) ); } if ( OWCO_Core::setting( 'banner_close_x' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'The close cross is considered by the CNIL as an ambiguous implicit refusal. Disable it.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'legal_company_name' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'Company name missing — required to generate policies. See the "Legal identity" tab.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'legal_company_reg_number' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'Company registration number missing (RCS / SIREN / BCE-KBO) — mandatory in the legal notice. See the "Legal identity" tab.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'legal_host_name' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'Hosting provider missing — its identity is mandatory in the legal notice (LCEN art. 6-III). See the "Legal identity" tab.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'dsar_enabled' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'The DSAR portal is disabled: visitors cannot exercise their rights through the site.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'dsar_email' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'No DPO email configured: you will not be notified about DSAR requests.', 'ow-consent' ) ); } if ( ! OWCO_Core::setting( 'ledger_enabled' ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'The consent ledger is disabled: you keep no proof that consent was given (GDPR art. 7.1).', 'ow-consent' ) ); } // The first banner layer must carry a route to the information (EDPB 5/2020 §62). // `link_privacy_policy` falls back to the WordPress privacy page; when neither // exists, the banner ships with no link to anything at all. if ( '' === trim( (string) OWCO_Core::setting( 'link_privacy_policy' ) ) && '' === (string) get_privacy_policy_url() ) { $issues[] = array( 'level' => 'bad', 'message' => __( 'No privacy policy is linked: the banner gives the visitor no route to any information about the processing. Choose or generate a page under "Compliance" → "Policy links".', 'ow-consent' ) ); } if ( '' === trim( (string) OWCO_Core::setting( 'link_cookie_policy' ) ) ) { $issues[] = array( 'level' => 'warn', 'message' => __( 'No cookie policy is linked: nothing points the visitor at the detailed list of trackers. Choose or generate a page under "Compliance" → "Policy links".', 'ow-consent' ) ); } $dpa_issue = self::dpa_authority_issue(); if ( ! empty( $dpa_issue ) ) { $issues[] = $dpa_issue; } if ( ! is_ssl() ) { $issues[] = array( 'level' => 'bad', 'message' => __( 'The site is not on HTTPS — a consent cookie without HTTPS is not legally valid.', 'ow-consent' ) ); } // Scanner: uncategorized trackers. Guarded, because a failed activation leaves the // table missing and the dashboard must still render. if ( self::table_ready( OWCO_TABLE_SCANNER ) ) { $scanner = OWCO_Scanner::findings_summary(); $unknown = isset( $scanner['unknown'] ) ? (int) $scanner['unknown'] : 0; if ( $unknown > 0 ) { $issues[] = array( 'level' => 'warn', 'message' => sprintf( /* translators: %d: number of un-categorized trackers */ _n( 'Scanner: %d uncategorized tracker detected. Classify it on the "Scanner" tab so it is blocked before consent.', 'Scanner: %d uncategorized trackers detected. Classify them on the "Scanner" tab so they are blocked before consent.', $unknown, 'ow-consent' ), $unknown ), ); } } // DSAR requests that already blew the art. 12.3 deadline. $dsar = self::dsar_stats(); if ( $dsar['overdue'] > 0 ) { $issues[] = array( 'level' => 'bad', 'message' => sprintf( /* translators: %d: number of data-subject requests past their legal deadline */ _n( '%d data-subject request has passed its legal deadline. See the "DSAR requests" tab.', '%d data-subject requests have passed their legal deadline. See the "DSAR requests" tab.', $dsar['overdue'], 'ow-consent' ), $dsar['overdue'] ), ); } // Catalogue rules that can never fire: the blocker silently skips them. $broken = self::count_broken_catalog_rules(); if ( $broken > 0 ) { $issues[] = array( 'level' => 'warn', 'message' => sprintf( /* translators: %d: number of unusable blocking rules */ _n( '%d blocking rule cannot fire and is skipped at runtime. Repair it on the "Tracker catalogue" tab.', '%d blocking rules cannot fire and are skipped at runtime. Repair them on the "Tracker catalogue" tab.', $broken, 'ow-consent' ), $broken ), ); } return $issues; } /** * Diagnostic entry for a hand-typed supervisory authority that the active compliance * profile contradicts. * * `legal_dpa_authority` is honoured verbatim by OWCO_Policies as soon as it differs * from the shipped default, and it survives a change of compliance profile: a value * typed while the site was on GDPR keeps naming the CNIL in a POPIA or PIPL document * whose own prose describes a different regulator. Rather than stop honouring the * administrator's choice, the mismatch is surfaced here with a one-click reset. * * @return array Issue entry for compliance_diagnostic(), or an empty array. */ protected static function dpa_authority_issue() { $chosen = trim( (string) OWCO_Core::setting( 'legal_dpa_authority' ) ); if ( '' === $chosen ) { return array(); } $defaults = OWCO_Core::default_settings(); $shipped = isset( $defaults['legal_dpa_authority'] ) ? trim( (string) $defaults['legal_dpa_authority'] ) : ''; // Still the shipped value: OWCO_Policies already derives the authority itself. if ( $chosen === $shipped ) { return array(); } $derived = self::derived_dpa_authority(); if ( '' === $derived || 0 === strcasecmp( $chosen, $derived ) ) { return array(); } return array( 'level' => 'warn', 'message' => sprintf( /* translators: 1: authority name typed by the administrator, 2: name of the active compliance profile, 3: authority the plugin would derive from that profile */ __( 'The supervisory authority is set by hand to "%1$s", while the active framework (%2$s) designates %3$s. Every generated document prints the value you typed, so it contradicts its own text.', 'ow-consent' ), $chosen, self::profile_label( (string) OWCO_Core::setting( 'compliance_profile' ) ), $derived ), 'action' => 'reset_dpa_authority', /* translators: %s: name of the supervisory authority the plugin would derive */ 'action_label' => sprintf( __( 'Use %s instead', 'ow-consent' ), $derived ), ); } /** * The authority OWCO_Policies would name if `legal_dpa_authority` had never been * customised — i.e. the one the profile and `legal_country` imply. * * The mapping itself is deliberately NOT copied here: OWCO_Policies::authority_info() * is the single authoritative table (fix contract §7), and it is only reachable with * the setting blanked. Blanking is done through the documented `owco_settings` filter * and undone immediately; the settings memo is keyed on the callback set, so both the * probe and the settings afterwards are correct. * * @return string Authority name, or '' when none can be named. */ protected static function derived_dpa_authority() { if ( ! class_exists( 'OWCO_Policies' ) || ! method_exists( 'OWCO_Policies', 'dpa_authority' ) ) { return ''; } $lang = method_exists( 'OWCO_Policies', 'resolve_language' ) ? (string) OWCO_Policies::resolve_language() : 'fr'; $probe = array( __CLASS__, 'filter_blank_dpa_authority' ); add_filter( 'owco_settings', $probe, 9999 ); $derived = (string) OWCO_Policies::dpa_authority( '', '', $lang ); remove_filter( 'owco_settings', $probe, 9999 ); $derived = trim( $derived ); // When nothing is mapped, authority_info() answers a lowercase generic phrase // ("the competent supervisory authority"). That is not a name and must never be // offered as a replacement value. return ( '' !== $derived && preg_match( '/^\p{Lu}/u', $derived ) ) ? $derived : ''; } /** * `owco_settings` callback used only by derived_dpa_authority(), for the length of one * call. Public because add_filter() needs a callable. * * @param mixed $settings Effective settings array. * @return mixed */ public static function filter_blank_dpa_authority( $settings ) { if ( is_array( $settings ) ) { $settings['legal_dpa_authority'] = ''; } return $settings; } /** * How many catalogue rules the matcher would refuse. * * Capped: only the candidates SQL can narrow down are re-checked in PHP, so a large * catalogue does not turn the dashboard into a regex benchmark. * * @return int */ protected static function count_broken_catalog_rules() { global $wpdb; if ( ! self::table_ready( OWCO_TABLE_SCRIPTS ) ) { return 0; } $table = OWCO_TABLE_SCRIPTS; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- plugin-owned table, read on an admin screen; it must reflect the catalogue as it is now, so there is nothing to cache. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pattern, pattern_type, match_target, category FROM %i WHERE pattern_type = 'regex' OR ( match_target = 'url' AND pattern_type = 'host' AND ( CHAR_LENGTH(pattern) < 6 OR pattern NOT LIKE %s ) ) LIMIT 200", $table, '%.%' ), ARRAY_A ); $broken = 0; foreach ( (array) $rows as $row ) { if ( '' !== self::catalog_row_problem( $row ) ) { $broken++; } } return $broken; } /** * Short translated name of a compliance profile. * * Framework acronyms stay as-is; the country/jurisdiction wording around them is * translatable, so a localised admin never sees a raw database slug. * * @param string $p Profile slug. * @return string */ protected static function profile_label( $p ) { $labels = self::profile_labels(); $p = (string) $p; if ( isset( $labels[ $p ] ) ) { return $labels[ $p ]; } return '' !== $p ? $p : __( 'Not set', 'ow-consent' ); } /** * The compliance profiles offered by the plugin, translated, in menu order. * * @return array
*/
wp_kses_post( __( 'Every consent is sealed with a %s taken over the row itself plus the hash of the row before it. Editing, deleting or reordering a row breaks that link, which is what makes the ledger usable as proof under GDPR art. 7(1).', 'ow-consent' ) ),
'HMAC-' . esc_html( strtoupper( $algo ) ) . ''
);
?>
| # | |||||||
|---|---|---|---|---|---|---|---|
|
|
→ |
[owco_dsar_form]
ID ) ); ?>
·
post_status ) );
?>
ID, OWCO_Policies::EDITED_META, true ) : '';
if ( '' !== $edited ) :
// Only link when a revision really exists: revision.php with anything
// else than a revision id answers an error screen.
$revisions_url = '';
$last_revision = wp_get_post_revisions( $page->ID, array( 'numberposts' => 1, 'fields' => 'ids' ) );
if ( ! empty( $last_revision ) ) {
$revisions_url = (string) get_edit_post_link( (int) reset( $last_revision ) );
}
?>
__( 'All', 'ow-consent' ), // Glyph concatenated outside the gettext call so it cannot be lost in translation. 'unknown' => '⚠ ' . __( 'Uncategorized', 'ow-consent' ), ); foreach ( OWCO_Core::categories() as $filter_cat ) { $cat_links[ $filter_cat ] = OWCO_Core::category_label( $filter_cat ); } foreach ( $cat_links as $val => $lbl ) : $link_args = array_diff_key( $carry, array( 'filter' => '', 'paged' => '' ) ); if ( '' !== $val ) { $link_args['filter'] = $val; } $url = self::tab_url( 'scanner', $link_args ); $style = ( $filter === $val ) ? 'background:#0f172a;color:#fff;' : ''; ?>
__( 'Uncategorized', 'ow-consent' ) ); foreach ( OWCO_Core::categories() as $opt_slug ) { $cat_options[ $opt_slug ] = OWCO_Core::category_label( $opt_slug ); } $kind_labels = self::scanner_kind_labels(); ?> in a