'generated-log', 'plural' => 'generated-logs', 'ajax' => true, 'screen' => WP_Helper::get_wp_screen(), ) ); self::$table_name = 'php_error_logs'; } /** * Displays the search box. * * @since 1.1.0 * * @param string $text The 'submit' button label. * @param string $input_id ID attribute value for the search input field. */ public function search_box( $text, $input_id ) { if ( empty( $_REQUEST[ self::SEARCH_INPUT ] ) && ! $this->has_items() ) { return; } $input_id = $input_id . '-search-input'; ?> '', // to display the checkbox. 'timestamp' => esc_html( sprintf( /* translators: %s: UTC offset */ __( 'Time (%s)', '0-day-analytics' ), WP_Helper::get_timezone_location() ) ), 'severity' => __( 'Severity', '0-day-analytics' ), 'message' => __( 'Message', '0-day-analytics' ), 'plugin_theme' => __( 'Source', '0-day-analytics' ), ); self::$admin_columns = \array_merge( $admin_fields, $columns ); } return self::$admin_columns; } /** * Returns the table name. * * @since 1.1.0 */ public static function get_table_name(): string { return self::$table_name; } /** * Prepares the list of items for displaying. * * Query, filter data, handle sorting, and pagination, and any other data-manipulation required prior to rendering * * @since 1.1.0 */ public function prepare_items() { $columns = $this->get_columns(); $hidden = array(); // $sortable = $this->get_sortable_columns(); $sortable = array(); $this->_column_headers = array( $columns, $hidden, $sortable ); $this->fetch_table_data(); $hidden = \get_user_option( 'manage' . WP_Helper::get_wp_screen()->id . 'columnshidden', false ); if ( ! $hidden ) { $hidden = array(); } $this->_column_headers = array( self::$columns, $hidden, $sortable ); } /** * Returns the currently hidden column headers for the current user * * @return array * * @since 1.1.0 */ public static function get_hidden_columns() { return array_filter( (array) \get_user_option( 'manage' . Settings::get_main_menu_page_hook() . 'columnshidden', false ) ); } /** * Get a list of sortable columns. The format is: * 'internal-name' => 'orderby' * or * 'internal-name' => array( 'orderby', true ). * * The second format will make the initial sorting order be descending * * @since 1.1.0 * * @return array */ public function get_sortable_columns() { $first6_columns = array_keys( self::get_column_names() ); $sortable_columns = array(); unset( $first6_columns[0], $first6_columns[9] ); // id column. // data column. /* * Actual sorting still needs to be done by prepare_items. * specify which columns should have the sort icon. * * The second bool param sets the colum sort order - true ASC, false - DESC or unsorted. */ foreach ( $first6_columns as $value ) { $sortable_columns[ $value ] = array( $value, false ); } return $sortable_columns; } /** * Text displayed when no user data is available. * * @since 1.1.0 * * @return void */ public function no_items() { $debug_off = ( ! defined( 'WP_DEBUG' ) || ! \WP_DEBUG ); $debug_log_off = ( ! defined( 'WP_DEBUG_LOG' ) || ! \WP_DEBUG_LOG ); $settings_url = \esc_url( \admin_url( 'admin.php?page=' . Settings::SETTINGS_MENU_SLUG ) ); if ( $debug_off || $debug_log_off ) { ?>

WP_DEBUG' ); } else { printf( /* translators: %s: WP_DEBUG_LOG constant name */ \esc_html__( 'The %s constant is not enabled. Errors are not being written to a log file.', '0-day-analytics' ), 'WP_DEBUG_LOG' ); } ?>

', '' ); ?>

get_error_code() ) { $severity = 'notice'; } return array( array( 'message' => Error_Log::get_last_error()->get_error_message(), 'severity' => $severity, 'timestamp' => time(), ), ); } $items = ( ! $items ) ? self::get_screen_option_per_page() : $items; // Resolve time-range cutoff timestamp. $time_cutoff = 0; if ( ! empty( self::$query_args['time_range'] ) ) { $range = self::$query_args['time_range']; $now = time(); switch ( $range ) { case 'last_hour': $time_cutoff = $now - HOUR_IN_SECONDS; break; case 'today': $time_cutoff = strtotime( 'today midnight' ); break; case 'last_24h': $time_cutoff = $now - DAY_IN_SECONDS; break; case 'last_7d': $time_cutoff = $now - ( 7 * DAY_IN_SECONDS ); break; } } // Determine if post-read filtering will be applied (search or plugin filter). $has_search_filter = ! empty( $_REQUEST[ self::SEARCH_INPUT ] ) && is_string( $_REQUEST[ self::SEARCH_INPUT ] ); $has_plugin_filter = ! empty( self::$query_args ) && isset( self::$query_args['plugin_filter'] ) && ! empty( self::$query_args['plugin_filter'] ) && -1 !== (int) self::$query_args['plugin_filter']; $has_post_filter = $has_search_filter || $has_plugin_filter; // When post-read filters are active, read more items to compensate for filtering. $read_items = $has_post_filter ? $items * 3 : $items; $target_count = $items; // Safety limit: maximum total read iterations to prevent runaway reads. $max_read_passes = $has_post_filter ? 5 : 1; $read_pass = 0; while ( $read_pass < $max_read_passes ) { ++$read_pass; $disabled = Settings::get_disabled_severities(); $remaining = $read_items; while ( $remaining > 0 ) { $result = Reverse_Line_Reader::read_file_from_end( $file, function( $line, $pos ) use ( &$collected_items, &$errors, &$position, $read_items, $write_temp, $time_cutoff ) { $position = $pos; // Prevent memory exhaustion from extremely large multi-line errors. if ( count( $collected_items ) > 500 ) { // Reasonable limit for stack traces. $collected_items = array_slice( $collected_items, -100 ); // Keep last 100 items. } // Flag that holds the status of the error - are there more lines to read or not. $more_to_error = false; // Check if this is the last line, and if not try to parse the line. if ( ! empty( $line ) && null !== Log_Line_Parser::parse_entry_with_stack_trace( $line ) ) { $parsed_data = Log_Line_Parser::parse_php_error_log_stack_line( $line ); if ( \is_array( $parsed_data ) && isset( $parsed_data['message'] ) ) { if ( ! empty( $collected_items ) ) { $parsed_data['sub_items'] = $collected_items; if ( isset( $collected_items[0]['message'] ) ) { $parsed_data['message'] = $collected_items[0]['message'] . "\n" . $parsed_data['message']; unset( $collected_items[0] ); } $collected_items = array(); } // Time-range cutoff: stop reading if entry is older than cutoff. if ( $time_cutoff > 0 && isset( $parsed_data['timestamp'] ) && $parsed_data['timestamp'] < $time_cutoff ) { return array( 'close' => true, 'line_done' => true, 'no_flush' => true, ); } $errors[] = $parsed_data; $more_to_error = false; // Prevent memory exhaustion from too many errors. if ( count( $errors ) >= $read_items * 2 ) { // Allow some buffer. return array( 'close' => true, 'line_done' => true, 'no_flush' => false, ); } } elseif ( \is_array( $parsed_data ) ) { if ( isset( $parsed_data['call'] ) && str_starts_with( trim( $parsed_data['call'] ), 'made by' ) ) { $collected_items[] = array( 'message' => $parsed_data['call'], ); } else { $collected_items[] = $parsed_data; } $more_to_error = true; } } elseif ( ! empty( $line ) ) { $more_to_error = true; } $is_excluded = false; // Empty lines should not consume a pagination slot. if ( empty( $line ) && ! $more_to_error ) { $is_excluded = true; } if ( ! $more_to_error && ! $is_excluded ) { $done = false; if ( isset( $parsed_data['severity'] ) && \in_array( $parsed_data['severity'], Settings::get_disabled_severities(), true ) ) { $is_excluded = true; $done = true; // Error is removed - no need to check it again. } // If only one severity is selected - remove all others (otherwise plugin shows other non-defined / custom severities coming from unknown or 3rd party sources). if ( ! $done && isset( $parsed_data['severity'] ) && count( Settings::get_enabled_severities() ) === 1 ) { $enabled = Settings::get_enabled_severities(); if ( reset( $enabled ) !== $parsed_data['severity'] ) { $is_excluded = true; } } } return array( 'line_done' => ! $more_to_error, 'close' => false, 'no_flush' => $is_excluded, ); }, $remaining, $position, $write_temp ); if ( ! empty( $disabled ) ) { $last_error = end( $errors ); $done = false; if ( isset( $last_error['severity'] ) && \in_array( $last_error['severity'], $disabled, true ) ) { // Remove the last error if it is disabled. array_pop( $errors ); $done = true; // Error is removed - no need to check it again. } // If only one severity is selected - remove all others (otherwise plugin shows other non-defined / custom severities coming from unknown or 3rd party sources). if ( ! $done && isset( $last_error['severity'] ) && count( Settings::get_enabled_severities() ) === 1 ) { $enabled = Settings::get_enabled_severities(); if ( reset( $enabled ) !== $last_error['severity'] ) { array_pop( $errors ); } } } $last_error = end( $errors ); // Add plugin data but only if that is not request for the first error. if ( ! empty( $last_error ) && ! $first_only ) { $plugin = Plugin_Theme_Helper::get_plugin_from_file_path( $last_error['message'] ); if ( $plugin ) { $errors[ \array_key_last( $errors ) ]['plugin'] = $plugin; } } if ( $first_only && ! empty( $errors ) ) { // If we only want the first item, return it. Log_Line_Parser::store_last_parsed_timestamp(); return array( reset( $errors ) ); } if ( false === $result ) { break 2; } } // Apply post-read filters and check if we have enough results. // If not, continue reading in the next pass. if ( $has_post_filter && $read_pass < $max_read_passes ) { $filtered = $errors; if ( $has_plugin_filter ) { $pf = self::$query_args['plugin_filter']; $filtered = array_filter( $filtered, function ( $error ) use ( $pf ) { return isset( $error['plugin'] ) && ( false !== strpos( $error['plugin'], $pf ) ); } ); } if ( $has_search_filter ) { $s = \sanitize_text_field( \wp_unslash( $_REQUEST[ self::SEARCH_INPUT ] ) ); $filtered = array_filter( $filtered, function ( $event ) use ( $s ) { $ar_as_string = ( isset( $event['sub_items'] ) && is_array( $event['sub_items'] ) ) ? \wp_json_encode( $event['sub_items'] ) : ''; return ( false !== strpos( $event['message'], $s ) ) || ( false !== strpos( $ar_as_string, $s ) ); } ); } if ( count( $filtered ) >= $target_count ) { // We have enough matching results, stop reading. break; } // Not enough matching results yet — read another batch. $read_items = $items * 2; } else { break; } } Log_Line_Parser::store_last_parsed_timestamp(); // Apply time-range cutoff to collected errors (for entries at the boundary). if ( $time_cutoff > 0 ) { $errors = array_filter( $errors, function ( $error ) use ( $time_cutoff ) { return ! isset( $error['timestamp'] ) || $error['timestamp'] >= $time_cutoff; } ); } if ( $has_plugin_filter ) { $s = self::$query_args['plugin_filter']; $errors = array_filter( $errors, function ( $error ) use ( $s ) { if ( isset( $error['plugin'] ) ) { return ( false !== strpos( $error['plugin'], $s ) ); } else { return false; } } ); } if ( $has_search_filter ) { $s = \sanitize_text_field( \wp_unslash( $_REQUEST[ self::SEARCH_INPUT ] ) ); $errors = array_filter( $errors, function ( $event ) use ( $s ) { $ar_as_string = ( isset( $event['sub_items'] ) && is_array( $event['sub_items'] ) ) ? \wp_json_encode( $event['sub_items'] ) : ''; return ( false !== strpos( $event['message'], $s ) ) || ( false !== strpos( $ar_as_string, $s ) ); } ); } // Trim to requested count after filtering. if ( count( $errors ) > $target_count ) { $errors = array_slice( $errors, 0, $target_count ); } self::$items_collected = $errors; $errors = null; unset( $errors ); return self::$items_collected; } /** * Render a column when no column specific method exists. * * Use that method for common rendering and separate columns logic in different methods. See below. * * @param array $item - Array with the current row values. * @param string $column_name - The name of the currently processed column. * * @return mixed * * @since 1.1.0 */ public function column_default( $item, $column_name ) { return self::format_column_value( $item, $column_name ); } /** * Render a column when no column specific method exists. * * Use that method for common rendering and separate columns logic in different methods. See below. * * @param array $item - Array with the current row values. * @param string $column_name - The name of the currently processed column. * * @return mixed * * @since 1.1.0 */ public static function format_column_value( $item, $column_name ) { switch ( $column_name ) { case 'severity': if ( isset( $item['severity'] ) && ! empty( $item['severity'] ) ) { if ( isset( Settings::get_option( 'severities' )[ $item['severity'] ] ) ) { return '' . \esc_html( $item['severity'] ) . ''; } else { return '' . \esc_html( $item['severity'] ) . ''; } } else { return '' . \esc_html( 'not set' ) . ''; } break; case 'timestamp': if ( 1 === $item['timestamp'] ) { return sprintf( ' %s', \esc_html__( 'Immediately', '0-day-analytics' ), ); } $time_format = 'g:i a'; $event_datetime_utc = \gmdate( 'Y-m-d H:i:s', $item['timestamp'] ); $timezone_local = \wp_timezone(); $event_local = \get_date_from_gmt( $event_datetime_utc, 'Y-m-d' ); $today_local = ( new \DateTimeImmutable( 'now', $timezone_local ) )->format( 'Y-m-d' ); $tomorrow_local = ( new \DateTimeImmutable( 'tomorrow', $timezone_local ) )->format( 'Y-m-d' ); $yesterday_local = ( new \DateTimeImmutable( 'yesterday', $timezone_local ) )->format( 'Y-m-d' ); // If the offset of the date of the event is different from the offset of the site, add a marker. if ( \get_date_from_gmt( $event_datetime_utc, 'P' ) !== get_date_from_gmt( 'now', 'P' ) ) { $time_format .= ' (P)'; } $event_time_local = \get_date_from_gmt( $event_datetime_utc, $time_format ); if ( $event_local === $today_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Today at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $tomorrow_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Tomorrow at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $yesterday_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Yesterday at %s', '0-day-analytics' ), $event_time_local, ); } else { $date = sprintf( /* translators: 1: Date, 2: Time */ __( '%1$s at %2$s', '0-day-analytics' ), \get_date_from_gmt( $event_datetime_utc, 'F jS' ), $event_time_local, ); } $time = sprintf( '', \esc_attr( gmdate( 'c', $item['timestamp'] ) ), \esc_html( $date ) ); $until = $item['timestamp'] - time(); if ( $until < 0 ) { $ago = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( '%s ago', '0-day-analytics' ), WP_Helper::interval( abs( $until ) ) ); return sprintf( ' %s
%s', esc_html( $ago ), $time, ); } elseif ( 0 === $until ) { $in = __( 'Now', '0-day-analytics' ); } else { $in = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( 'In %s', '0-day-analytics' ), WP_Helper::interval( $until ), ); } return sprintf( ' %s
%s', \esc_html( $in ), $time, ); case 'message': $message = '
'; $message .= '' . \esc_html( $item[ $column_name ] ) . ''; if ( isset( $item['sub_items'] ) && ! empty( $item['sub_items'] ) ) { $message .= '
'; $reversed_details = \array_reverse( $item['sub_items'] ); $message .= ''; } return $message; case 'plugin_theme': $source_link = ''; $query_array = array( '_wpnonce' => \wp_create_nonce( 'source-view' ), 'action' => 'log_source_view', ); if ( isset( $item['error_file'] ) && ! empty( $item['error_file'] ) ) { $query_array['error_file'] = $item['error_file']; if ( isset( $item['error_line'] ) && ! empty( $item['error_line'] ) ) { $query_array['error_line'] = $item['error_line']; } $query_array['TB_iframe'] = 'true'; $view_url = \esc_url_raw( \add_query_arg( $query_array, \admin_url( 'admin-ajax.php' ) ) ); $title = __( 'Viewing: ', '0-day-analytics' ) . $item['error_file']; $source_link = '
' . __( 'view error source', '0-day-analytics' ) . '
'; } $message = \esc_html( $item['message'] ); if ( isset( $item['plugin'] ) && ! empty( $item['plugin'] ) ) { return __( 'Plugin: ', '0-day-analytics' ) . '' . \esc_html( Plugin_Theme_Helper::get_sources()['plugins'][ $item['plugin'] ]['Name'] ) . '
' . \__( 'Current version: ', '0-day-analytics' ) . \esc_html( Plugin_Theme_Helper::get_sources()['plugins'][ $item['plugin'] ]['Version'] ) . $source_link; } $theme = Plugin_Theme_Helper::get_theme_from_file_path( $message ); if ( $theme ) { $name = $theme->get( 'Name' ); $version = $theme->get( 'Version' ); $version = ( ! empty( $version ) ) ? '
' . __( 'Current version: ', '0-day-analytics' ) . \esc_html( $version ) : '
' . __( 'Unknown version', '0-day-analytics' ); $name = ( ( ! empty( $name ) ) ? \esc_html( $name ) : __( 'Unknown theme', '0-day-analytics' ) ) . $version; $parent = $theme->parent(); // ( 'parent_theme' ); if ( $parent ) { $parent = $theme->parent()->get( 'Name' ); $parent_version = $theme->parent()->get( 'Version' ); $parent_version = ( ! empty( $parent_version ) ) ? \esc_html( $parent_version ) : __( 'Unknown version', '0-day-analytics' ); $parent = ( ! empty( $parent ) ) ? '
' . __( 'Parent theme: ', '0-day-analytics' ) . \esc_html( $parent ) . '
' . __( 'Parent Current Version: ', '0-day-analytics' ) . $parent_version . '
' : ''; } $name .= (string) $parent; return __( 'Theme: ', '0-day-analytics' ) . '' . ( $name ) . '' . $source_link; } if ( false !== \mb_strpos( $message, ABSPATH . WPINC . \DIRECTORY_SEPARATOR ) ) { return ' ' . __( 'WP Core', '0-day-analytics' ) . $source_link; } $admin_path = str_replace( \get_home_url( 1 ) . '/', ABSPATH, \network_admin_url() ); if ( false !== \mb_strpos( $message, $admin_path ) ) { return ' ' . __( 'WP Admin Core', '0-day-analytics' ) . $source_link; } if ( isset( $item['source'] ) && 'wp_error' === $item['source'] ) { return __( 'WP Error thrown', '0-day-analytics' ); } if ( isset( $item['source'] ) ) { return \esc_html( $item['source'] ) . $source_link; } else { return ''; } default: return isset( $item[ $column_name ] ) ? \esc_html( $item[ $column_name ] ) : __( 'Column "', '0-day-analytics' ) . \esc_html( $column_name ) . __( '" not found', '0-day-analytics' ); } } /** * Get value for checkbox column. * * The special 'cb' column * * @param object $item - A row's data. * * @return string Text to be placed inside the column < td > . * * @since 1.1.0 */ protected function column_cb( $item ) { return; return sprintf( '' . '' ); } /** * Returns an associative array containing the bulk actions. * * @since 1.1.0 * * @return array */ public function get_bulk_actions() { return array(); } /** * Process actions triggered by the user. * * @since 1.1.0 */ public function handle_table_actions() { if ( ! isset( $_REQUEST[ self::$table_name ] ) ) { return; } /** * Note: Table bulk_actions can be identified by checking $_REQUEST['action'] and $_REQUEST['action2']. * * Action - is set if checkbox from top-most select-all is set, otherwise returns -1 * Action2 - is set if checkbox the bottom-most select-all checkbox is set, otherwise returns -1 */ } /** * Returns translated text for per page option * * @return string * * @since 2.3.0 */ private static function get_screen_per_page_title(): string { return __( 'Number of errors to read', '0-day-analytics' ); } /** * Table navigation. * * @param string $which - Position of the nav. * * @since 1.1.0 */ public function extra_tablenav( $which ) { // If the position is not top then render. // Show site alerts widget. // NOTE: this is shown when the filter IS NOT true. $log_file = Error_Log::extract_file_name( Error_Log::autodetect() ); if ( null !== Error_Log::get_last_error() ) { if ( 'top' === $which ) { if ( 'wp_debug_off' === Error_Log::get_last_error()->get_error_code() ) { ?>

get_error_message(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

get_error_code() ) { ?>

get_error_message(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

get_error_code() ) { ?>

get_error_message(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

get_error_code() ) { ?>

get_error_message(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

0 ) { ?>

' . \esc_html( $severity ) . ' '; } ?>

|
\__( '- Time range -', '0-day-analytics' ), 'last_hour' => \__( 'Last hour', '0-day-analytics' ), 'today' => \__( 'Today', '0-day-analytics' ), 'last_24h' => \__( 'Last 24 hours', '0-day-analytics' ), 'last_7d' => \__( 'Last 7 days', '0-day-analytics' ), ); ?>
$severity ) { ?> >

'; $this->single_row_columns( $item ); echo ''; } /** * Generates the table navigation above or below the table * * @param string $which - Holds info about the top and bottom navigation. * * @since 1.1.0 */ public function display_tablenav( $which ) { if ( 'top' === $which ) { \wp_nonce_field( 'advan-plugin-data', 'advanced-analytics-security', false ); } $this->extra_tablenav( $which ); if ( 'top' !== $which && ! empty( self::$items_collected ) ) { ?>
format( 'Y-m-d' ); $tomorrow_local = ( new \DateTimeImmutable( 'tomorrow', $timezone_local ) )->format( 'Y-m-d' ); $yesterday_local = ( new \DateTimeImmutable( 'yesterday', $timezone_local ) )->format( 'Y-m-d' ); // If the offset of the date of the event is different from the offset of the site, add a marker. if ( \get_date_from_gmt( $event_datetime_utc, 'P' ) !== get_date_from_gmt( 'now', 'P' ) ) { $time_format .= ' (P)'; } $event_time_local = \get_date_from_gmt( $event_datetime_utc, $time_format ); if ( $event_local === $today_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Today at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $tomorrow_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Tomorrow at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $yesterday_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Yesterday at %s', '0-day-analytics' ), $event_time_local, ); } else { $date = sprintf( /* translators: 1: Date, 2: Time */ __( '%1$s at %2$s', '0-day-analytics' ), \get_date_from_gmt( $event_datetime_utc, 'F jS' ), $event_time_local, ); } $time = sprintf( '', \esc_attr( gmdate( 'c', $event['timestamp'] ) ), \esc_html( $date ) ); $until = $event['timestamp'] - time(); if ( $until < 0 ) { $ago = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( '%s ago', '0-day-analytics' ), WP_Helper::interval( abs( $until ) ) ); $in = sprintf( ' %s ', esc_html( $ago ), ); } elseif ( 0 === $until ) { $in = __( 'Now', '0-day-analytics' ); } else { $in = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( 'In %s', '0-day-analytics' ), WP_Helper::interval( $until ), ); } $classes = ''; if ( isset( $event['severity'] ) && ! empty( $event['severity'] ) ) { $classes .= ' ' . $event['severity']; } $style = ''; if ( isset( Settings::get_option( 'severities' )[ $event['severity'] ] ) ) { $style = 'border-left: 5px solid ' . \esc_attr( Settings::get_option( 'severities' )[ $event['severity'] ]['color'] ) . ' !important;'; } $response = array( 'event' => $event, 'classes' => $classes, 'in' => $in, 'style' => $style, ); return \rest_ensure_response( $response ); } return \rest_ensure_response( array( 'message' => ADVAN_NAME . __( ': Error log - no logs to report.', '0-day-analytics' ), ) ); } /** * Sets the severity status. * * @param \WP_REST_Request $request - The request object. * * @return \WP_REST_Response|\WP_Error * * @since 1.9.5.1 */ public static function set_severity_status( \WP_REST_Request $request ) { // Require appropriate capability for changing severity visibility. if ( ! \current_user_can( 'manage_options' ) ) { return new \WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ), array( 'status' => 403 ) ); } $severity = $request->get_param( 'severity_name' ); $status = $request->get_param( 'status' ); if ( ! in_array( $severity, array_keys( Settings::get_option( 'severities' ) ), true ) ) { return new \WP_Error( 'invalid_severity', __( 'Invalid severity name.', '0-day-analytics' ), array( 'status' => 400 ) ); } if ( 'enable' === $status ) { Settings::enable_severity( $severity ); } elseif ( 'disable' === $status ) { Settings::disable_severity( $severity ); } else { return new \WP_Error( 'invalid_status', __( 'Invalid status.', '0-day-analytics' ), array( 'status' => 400 ) ); } return rest_ensure_response( array( 'success' => true, ) ); } /** * Set showing single sevetiry. * * @param \WP_REST_Request $request - The request object. * * @return \WP_REST_Response|\WP_Error * * @since 2.8.1 */ public static function set_single_severity( \WP_REST_Request $request ) { // Require appropriate capability for changing severity visibility. if ( ! \current_user_can( 'manage_options' ) ) { return new \WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to perform this action.', '0-day-analytics' ), array( 'status' => 403 ) ); } $selected_severity = $request->get_param( 'severity_name' ); if ( ! in_array( $selected_severity, array_keys( Settings::get_current_options()['severities'] ), true ) && 'none_selected' !== $selected_severity ) { return new \WP_Error( 'invalid_severity', __( 'Invalid severity name.', '0-day-analytics' ), array( 'status' => 400 ) ); } if ( 'none_selected' === $selected_severity ) { foreach ( array_keys( Settings::get_current_options()['severities'] )as $severity ) { Settings::enable_severity( $severity ); } } else { foreach ( array_keys( Settings::get_current_options()['severities'] )as $severity ) { if ( $selected_severity === $severity ) { Settings::enable_severity( $severity ); } else { Settings::disable_severity( $severity ); } } } return rest_ensure_response( array( 'success' => true, ) ); } /** * Extracts data and send it to Notification if there is something to report * * @return array * * @since 1.9.3 */ public static function get_notification_data(): array { $events = self::get_error_items( false, false, true ); $event = reset( $events ); $events = null; unset( $events ); $data = array(); if ( $event && ! empty( $event ) ) { $last_transient_reported_data = (int) \get_site_transient( self::NOTIFICATION_TRANSIENT ); if ( $last_transient_reported_data < $event['timestamp'] ) { \set_site_transient( self::NOTIFICATION_TRANSIENT, $event['timestamp'] ); $time_format = 'g:i a'; $event_datetime_utc = \gmdate( 'Y-m-d H:i:s', $event['timestamp'] ); $timezone_local = \wp_timezone(); $event_local = \get_date_from_gmt( $event_datetime_utc, 'Y-m-d' ); $today_local = ( new \DateTimeImmutable( 'now', $timezone_local ) )->format( 'Y-m-d' ); $tomorrow_local = ( new \DateTimeImmutable( 'tomorrow', $timezone_local ) )->format( 'Y-m-d' ); $yesterday_local = ( new \DateTimeImmutable( 'yesterday', $timezone_local ) )->format( 'Y-m-d' ); // If the offset of the date of the event is different from the offset of the site, add a marker. if ( \get_date_from_gmt( $event_datetime_utc, 'P' ) !== get_date_from_gmt( 'now', 'P' ) ) { $time_format .= ' (P)'; } $event_time_local = \get_date_from_gmt( $event_datetime_utc, $time_format ); if ( $event_local === $today_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Today at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $tomorrow_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Tomorrow at %s', '0-day-analytics' ), $event_time_local, ); } elseif ( $event_local === $yesterday_local ) { $date = sprintf( /* translators: %s: Time */ __( 'Yesterday at %s', '0-day-analytics' ), $event_time_local, ); } else { $date = sprintf( /* translators: 1: Date, 2: Time */ __( '%1$s at %2$s', '0-day-analytics' ), \get_date_from_gmt( $event_datetime_utc, 'F jS' ), $event_time_local, ); } $time = sprintf( '', \esc_attr( gmdate( 'c', $event['timestamp'] ) ), \esc_html( $date ) ); $until = $event['timestamp'] - time(); if ( $until < 0 ) { $ago = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( '%s ago', '0-day-analytics' ), WP_Helper::interval( abs( $until ) ) ); $in = sprintf( ' %s ', esc_html( $ago ), ); } elseif ( 0 === $until ) { $in = __( 'Now', '0-day-analytics' ); } else { $in = sprintf( /* translators: %s: Time period, for example "8 minutes" */ __( 'In %s', '0-day-analytics' ), WP_Helper::interval( $until ), ); } $base = 'base'; $base .= '64_en'; $base .= 'code'; // Escape severity and message to avoid injecting markup from log content into notifications. $data['body'] = \esc_html( $event['severity'] ) . ' ' . \esc_html( \html_entity_decode( (string) $event['message'] ) ); $data['title'] = $in; $data['icon'] = 'data:image/svg+xml;base64,' . $base( file_get_contents( \ADVAN_PLUGIN_ROOT . 'assets/icon.svg' ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $data['url'] = Miscellaneous::get_error_log_page_link(); } } else { \set_site_transient( self::NOTIFICATION_TRANSIENT, time() ); } return $data; } /** * Determines whether the table has items to display or not * * @since 2.8.1 * * @return bool */ public function has_items() { return ! empty( self::$items_collected ); } /** * Generates the list table rows. * * @return void * * @since 2.8.1 */ public function display_rows() { foreach ( self::$items_collected as $item ) { $this->single_row( $item ); } } /** * Adds a cron job for truncating the records in the requests table * * @param array $crons - The array with all the crons associated with the plugin. * * @return array * * @since 2.8.2 */ public static function add_cron_job( $crons ) { return static::add_standard_cron_job( $crons, 'advana_error_log_clear', ADVAN_PREFIX . 'error_log_clear', array( __CLASS__, 'truncate_error_log' ) ); } /** * Truncates the requests table from CRON job * * @return void * * @since 2.8.2 */ public static function truncate_error_log() { // Suppress the output which plugin generates during internal operations. \ob_start(); Error_Log::truncate_and_keep_errors(); \ob_clean(); } /** * Returns an array of CSS class names for the table. * * @return array Array of class names. * * @since 1.4.0 */ public function get_table_classes() { return array( 'widefat', 'fixed', 'striped', 'table-view-list', $this->_args['plural'], ); } } }