query( $wpdb->prepare( "INSERT INTO %i (url, url_hash, hits, referrer, user_agent, first_seen, last_seen) VALUES (%s, %s, 1, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE hits = hits + 1, last_seen = VALUES(last_seen), referrer = IF(VALUES(referrer) <> '', VALUES(referrer), referrer), user_agent = VALUES(user_agent)", $table, $url, Url::hash( $url ), $referrer, $agent, $now, $now ) ); } /** files browsers and bots ask for on every site: noise in the log */ private static function ignored( string $url ): bool { return (bool) preg_match( '#^/(favicon\.ico|apple-touch-icon[^/]*\.png|robots\.txt|\.well-known/|wp-content/cache/)#', $url ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- rb_redirection_ is this plugin's prefix (constants, options, tables, functions and hooks); Plugin Check only guesses rbRedirection from the namespace || (bool) apply_filters( 'rb_redirection_ignore_404', false, $url ); } /** @return array{items: object[], total: int} */ public static function query( array $args ): array { global $wpdb; $t = Install::log_table(); $like = empty( $args['search'] ) ? '' : '%' . $wpdb->esc_like( (string) $args['search'] ) . '%'; $order = in_array( $args['orderby'] ?? '', array( 'hits', 'last_seen', 'url' ), true ) ? $args['orderby'] : 'last_seen'; $dir = ( $args['order'] ?? 'desc' ) === 'asc' ? 'ASC' : 'DESC'; $per = max( 1, min( 5000, (int) ( $args['per_page'] ?? 20 ) ) ); $offset = max( 0, ( (int) ( $args['page'] ?? 1 ) - 1 ) * $per ); // no search: the empty $like switches the LIKE off; the order column comes from the list above $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE (%s = '' OR url LIKE %s)", $t, $like, $like ) ); $items = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE (%s = '' OR url LIKE %s) ORDER BY CASE WHEN %s = 'ASC' THEN %i END ASC, CASE WHEN %s = 'DESC' THEN %i END DESC, id DESC LIMIT %d OFFSET %d", $t, $like, $like, $dir, $order, $dir, $order, $per, $offset ) ); // whether each address has a redirect now foreach ( $items as $item ) { $item->redirect_id = 0; $match = Redirects::match( (string) $item->url ); if ( $match ) { $item->redirect_id = (int) $match['row']->id; } } return array( 'items' => $items, 'total' => $total ); } public static function delete( array $ids ): int { global $wpdb; $ids = array_filter( array_map( 'absint', $ids ) ); if ( ! $ids ) { return 0; } $in = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $in is only %d placeholders, one per id return (int) $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE id IN ({$in})", Install::log_table(), ...$ids ) ); } public static function clear(): int { global $wpdb; return (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', Install::log_table() ) ); } /** daily: forget addresses not seen for "log404Days" days */ public static function clean(): int { global $wpdb; $days = (int) Settings::get( 'log404Days' ); $cut = gmdate( 'Y-m-d H:i:s', time() - max( 1, $days ) * DAY_IN_SECONDS ); return (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE last_seen < %s', Install::log_table(), $cut ) ); } }