' . esc_html__( 'Akismet Setup' , 'akismet') . '
' . + '' . esc_html__( 'Akismet filters out spam, so you can focus on more important things.' , 'akismet') . '
' . + '' . esc_html__( 'On this page, you are able to set up the Akismet plugin.' , 'akismet') . '
', + ) + ); + + $current_screen->add_help_tab( + array( + 'id' => 'setup-signup', + 'title' => __( 'New to Akismet' , 'akismet'), + 'content' => + '' . esc_html__( 'Akismet Setup' , 'akismet') . '
' . + '' . esc_html__( 'You need to enter an API key to activate the Akismet service on your site.' , 'akismet') . '
' . + '' . sprintf( __( 'Sign up for an account on %s to get an API Key.' , 'akismet'), 'Akismet.com' ) . '
', + ) + ); + + $current_screen->add_help_tab( + array( + 'id' => 'setup-manual', + 'title' => __( 'Enter an API Key' , 'akismet'), + 'content' => + '' . esc_html__( 'Akismet Setup' , 'akismet') . '
' . + '' . esc_html__( 'If you already have an API key' , 'akismet') . '
' . + '' . esc_html__( 'Akismet Stats' , 'akismet') . '
' . + '' . esc_html__( 'Akismet filters out spam, so you can focus on more important things.' , 'akismet') . '
' . + '' . esc_html__( 'On this page, you are able to view stats on spam filtered on your site.' , 'akismet') . '
', + ) + ); + } + else { + //configuration page + $current_screen->add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview' , 'akismet'), + 'content' => + '' . esc_html__( 'Akismet Configuration' , 'akismet') . '
' . + '' . esc_html__( 'Akismet filters out spam, so you can focus on more important things.' , 'akismet') . '
' . + '' . esc_html__( 'On this page, you are able to update your Akismet settings and view spam stats.' , 'akismet') . '
', + ) + ); + + $current_screen->add_help_tab( + array( + 'id' => 'settings', + 'title' => __( 'Settings' , 'akismet'), + 'content' => + '' . esc_html__( 'Akismet Configuration' , 'akismet') . '
' . + ( Akismet::predefined_api_key() ? '' : '' . esc_html__( 'API Key' , 'akismet') . ' - ' . esc_html__( 'Enter/remove an API key.' , 'akismet') . '
' ) . + '' . esc_html__( 'Comments' , 'akismet') . ' - ' . esc_html__( 'Show the number of approved comments beside each comment author in the comments list page.' , 'akismet') . '
' . + '' . esc_html__( 'Strictness' , 'akismet') . ' - ' . esc_html__( 'Choose to either discard the worst spam automatically or to always put all spam in spam folder.' , 'akismet') . '
', + ) + ); + + if ( ! Akismet::predefined_api_key() ) { + $current_screen->add_help_tab( + array( + 'id' => 'account', + 'title' => __( 'Account' , 'akismet'), + 'content' => + '' . esc_html__( 'Akismet Configuration' , 'akismet') . '
' . + '' . esc_html__( 'Subscription Type' , 'akismet') . ' - ' . esc_html__( 'The Akismet subscription plan' , 'akismet') . '
' . + '' . esc_html__( 'Status' , 'akismet') . ' - ' . esc_html__( 'The subscription status - active, cancelled or suspended' , 'akismet') . '
', + ) + ); + } + } + } + + // Help Sidebar + $current_screen->set_help_sidebar( + '' . esc_html__( 'For more information:' , 'akismet') . '
' . + '' . esc_html__( 'Akismet FAQ' , 'akismet') . '
' . + '' . esc_html__( 'Akismet Support' , 'akismet') . '
' + ); + } + + public static function enter_api_key() { + if ( ! current_user_can( 'manage_options' ) ) { + die( __( 'Cheatin’ uh?', 'akismet' ) ); + } + + if ( !wp_verify_nonce( $_POST['_wpnonce'], self::NONCE ) ) + return false; + + foreach( array( 'akismet_strictness', 'akismet_show_user_comments_approved' ) as $option ) { + update_option( $option, isset( $_POST[$option] ) && (int) $_POST[$option] == 1 ? '1' : '0' ); + } + + if ( ! empty( $_POST['akismet_comment_form_privacy_notice'] ) ) { + self::set_form_privacy_notice_option( $_POST['akismet_comment_form_privacy_notice'] ); + } else { + self::set_form_privacy_notice_option( 'hide' ); + } + + if ( Akismet::predefined_api_key() ) { + return false; //shouldn't have option to save key if already defined + } + + $new_key = preg_replace( '/[^a-f0-9]/i', '', $_POST['key'] ); + $old_key = Akismet::get_api_key(); + + if ( empty( $new_key ) ) { + if ( !empty( $old_key ) ) { + delete_option( 'wordpress_api_key' ); + self::$notices[] = 'new-key-empty'; + } + } + elseif ( $new_key != $old_key ) { + self::save_key( $new_key ); + } + + return true; + } + + public static function save_key( $api_key ) { + $key_status = Akismet::verify_key( $api_key ); + + if ( $key_status == 'valid' ) { + $akismet_user = self::get_akismet_user( $api_key ); + + if ( $akismet_user ) { + if ( in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ) ) ) + update_option( 'wordpress_api_key', $api_key ); + + if ( $akismet_user->status == 'active' ) + self::$notices['status'] = 'new-key-valid'; + elseif ( $akismet_user->status == 'notice' ) + self::$notices['status'] = $akismet_user; + else + self::$notices['status'] = $akismet_user->status; + } + else + self::$notices['status'] = 'new-key-invalid'; + } + elseif ( in_array( $key_status, array( 'invalid', 'failed' ) ) ) + self::$notices['status'] = 'new-key-'.$key_status; + } + + public static function dashboard_stats() { + if ( did_action( 'rightnow_end' ) ) { + return; // We already displayed this info in the "Right Now" section + } + + if ( !$count = get_option('akismet_spam_count') ) + return; + + global $submenu; + + echo ''.sprintf( _n( + 'Akismet has protected your site from %3$s spam comment.', + 'Akismet has protected your site from %3$s spam comments.', + $count + , 'akismet'), 'https://akismet.com/wordpress/', esc_url( add_query_arg( array( 'page' => 'akismet-admin' ), admin_url( isset( $submenu['edit-comments.php'] ) ? 'edit-comments.php' : 'edit.php' ) ) ), number_format_i18n($count) ).'
'; + } + + // WP 2.5+ + public static function rightnow_stats() { + if ( $count = get_option('akismet_spam_count') ) { + $intro = sprintf( _n( + 'Akismet has protected your site from %2$s spam comment already. ', + 'Akismet has protected your site from %2$s spam comments already. ', + $count + , 'akismet'), 'https://akismet.com/wordpress/', number_format_i18n( $count ) ); + } else { + $intro = sprintf( __('Akismet blocks spam from getting to your blog. ', 'akismet'), 'https://akismet.com/wordpress/' ); + } + + $link = add_query_arg( array( 'comment_status' => 'spam' ), admin_url( 'edit-comments.php' ) ); + + if ( $queue_count = self::get_spam_count() ) { + $queue_text = sprintf( _n( + 'There’s %1$s comment in your spam queue right now.', + 'There are %1$s comments in your spam queue right now.', + $queue_count + , 'akismet'), number_format_i18n( $queue_count ), esc_url( $link ) ); + } else { + $queue_text = sprintf( __( "There’s nothing in your spam queue at the moment." , 'akismet'), esc_url( $link ) ); + } + + $text = $intro . 'wp_check_comment_disallowed_list' : 'wp_blacklist_check'
+ );
+ break;
+ case 'report-spam':
+ if ( isset( $row['user'] ) ) {
+ /* translators: The placeholder is a username. */
+ $message = esc_html( sprintf( __( '%s reported this comment as spam.', 'akismet' ), $row['user'] ) );
+ } else if ( ! $message ) {
+ $message = esc_html( __( 'This comment was reported as spam.', 'akismet' ) );
+ }
+ break;
+ case 'report-ham':
+ if ( isset( $row['user'] ) ) {
+ /* translators: The placeholder is a username. */
+ $message = esc_html( sprintf( __( '%s reported this comment as not spam.', 'akismet' ), $row['user'] ) );
+ } else if ( ! $message ) {
+ $message = esc_html( __( 'This comment was reported as not spam.', 'akismet' ) );
+ }
+ break;
+ case 'cron-retry-spam':
+ $message = esc_html( __( 'Akismet caught this comment as spam during an automatic retry.', 'akismet' ) );
+ break;
+ case 'cron-retry-ham':
+ $message = esc_html( __( 'Akismet cleared this comment during an automatic retry.', 'akismet' ) );
+ break;
+ case 'check-error':
+ if ( isset( $row['meta'], $row['meta']['response'] ) ) {
+ /* translators: The placeholder is an error response returned by the API server. */
+ $message = sprintf( esc_html( __( 'Akismet was unable to check this comment (response: %s) but will automatically retry later.', 'akismet' ) ), '' . esc_html( $row['meta']['response'] ) . '' );
+ } else {
+ $message = esc_html( __( 'Akismet was unable to check this comment but will automatically retry later.', 'akismet' ) );
+ }
+ break;
+ case 'recheck-error':
+ if ( isset( $row['meta'], $row['meta']['response'] ) ) {
+ /* translators: The placeholder is an error response returned by the API server. */
+ $message = sprintf( esc_html( __( 'Akismet was unable to recheck this comment (response: %s).', 'akismet' ) ), '' . esc_html( $row['meta']['response'] ) . '' );
+ } else {
+ $message = esc_html( __( 'Akismet was unable to recheck this comment.', 'akismet' ) );
+ }
+ break;
+ case 'webhook-spam':
+ $message = esc_html( __( 'Akismet caught this comment as spam and updated its status via webhook.', 'akismet' ) );
+ break;
+ case 'webhook-ham':
+ $message = esc_html( __( 'Akismet cleared this comment and updated its status via webhook.', 'akismet' ) );
+ break;
+ case 'webhook-spam-noaction':
+ $message = esc_html( __( 'Akismet determined this comment was spam during a recheck. It did not update the comment status because it had already been modified by another user or plugin.', 'akismet' ) );
+ break;
+ case 'webhook-ham-noaction':
+ $message = esc_html( __( 'Akismet cleared this comment during a recheck. It did not update the comment status because it had already been modified by another user or plugin.', 'akismet' ) );
+ break;
+ case 'akismet-skipped':
+ $message = esc_html( __( 'This comment was not sent to Akismet when it was submitted because it was caught by something else.', 'akismet' ) );
+ break;
+ case 'akismet-skipped-disallowed':
+ $message = esc_html( __( 'This comment was not sent to Akismet when it was submitted because it was caught by the comment disallowed list.', 'akismet' ) );
+ break;
+ default:
+ if ( preg_match( '/^status-changed/', $row['event'] ) ) {
+ // Half of these used to be saved without the dash after 'status-changed'.
+ // See https://plugins.trac.wordpress.org/changeset/1150658/akismet/trunk
+ $new_status = preg_replace( '/^status-changed-?/', '', $row['event'] );
+ /* translators: The placeholder is a short string (like 'spam' or 'approved') denoting the new comment status. */
+ $message = sprintf( esc_html( __( 'Comment status was changed to %s', 'akismet' ) ), '' . esc_html( $new_status ) . '' );
+ } else if ( preg_match( '/^status-/', $row['event'] ) ) {
+ $new_status = preg_replace( '/^status-/', '', $row['event'] );
+
+ if ( isset( $row['user'] ) ) {
+ /* translators: %1$s is a username; %2$s is a short string (like 'spam' or 'approved') denoting the new comment status. */
+ $message = sprintf( esc_html( __( '%1$s changed the comment status to %2$s.', 'akismet' ) ), $row['user'], '' . esc_html( $new_status ) . '' );
+ }
+ }
+ break;
+ }
+ }
+
+ if ( ! empty( $message ) ) {
+ echo ''; + + if ( isset( $row['time'] ) ) { + $time = gmdate( 'D d M Y @ h:i:s a', (int) $row['time'] ) . ' GMT'; + + /* translators: The placeholder is an amount of time, like "7 seconds" or "3 days" returned by the function human_time_diff(). */ + $time_html = '' . sprintf( esc_html__( '%s ago', 'akismet' ), human_time_diff( $row['time'] ) ) . ''; + + echo sprintf( + /* translators: %1$s is a human-readable time difference, like "3 hours ago", and %2$s is an already-translated phrase describing how a comment's status changed, like "This comment was reported as spam." */ + esc_html( __( '%1$s - %2$s', 'akismet' ) ), + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $time_html, + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $message + ); // esc_html() is done above so that we can use HTML in $message. + } else { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $message; // esc_html() is done above so that we can use HTML in $message. + } + + echo '
'; + } + } + } else { + echo ''; + echo esc_html( __( 'No comment history.', 'akismet' ) ); + echo '
'; + } + } + + public static function plugin_action_links( $links, $file ) { + if ( $file == plugin_basename( plugin_dir_url( __FILE__ ) . '/akismet.php' ) ) { + $links[] = ''.esc_html__( 'Settings' , 'akismet').''; + } + + return $links; + } + + // Total spam in queue + // get_option( 'akismet_spam_count' ) is the total caught ever + public static function get_spam_count( $type = false ) { + global $wpdb; + + if ( !$type ) { // total + $count = wp_cache_get( 'akismet_spam_count', 'widget' ); + if ( false === $count ) { + $count = wp_count_comments(); + $count = $count->spam; + wp_cache_set( 'akismet_spam_count', $count, 'widget', 3600 ); + } + return $count; + } elseif ( 'comments' == $type || 'comment' == $type ) { // comments + $type = ''; + } + + return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(comment_ID) FROM {$wpdb->comments} WHERE comment_approved = 'spam' AND comment_type = %s", $type ) ); + } + + // Check connectivity between the WordPress blog and Akismet's servers. + // Returns an associative array of server IP addresses, where the key is the IP address, and value is true (available) or false (unable to connect). + public static function check_server_ip_connectivity() { + + $servers = $ips = array(); + + // Some web hosts may disable this function + if ( function_exists( 'gethostbynamel' ) ) { + + $ips = gethostbynamel( 'rest.akismet.com' ); + if ( $ips && is_array($ips) && count($ips) ) { + $api_key = Akismet::get_api_key(); + + foreach ( $ips as $ip ) { + $response = Akismet::verify_key( $api_key, $ip ); + // even if the key is invalid, at least we know we have connectivity + if ( $response == 'valid' || $response == 'invalid' ) + $servers[$ip] = 'connected'; + else + $servers[$ip] = $response ? $response : 'unable to connect'; + } + } + } + + return $servers; + } + + // Simpler connectivity check + public static function check_server_connectivity($cache_timeout = 86400) { + + $debug = array(); + $debug[ 'PHP_VERSION' ] = PHP_VERSION; + $debug[ 'WORDPRESS_VERSION' ] = $GLOBALS['wp_version']; + $debug[ 'AKISMET_VERSION' ] = AKISMET_VERSION; + $debug[ 'AKISMET__PLUGIN_DIR' ] = AKISMET__PLUGIN_DIR; + $debug[ 'SITE_URL' ] = site_url(); + $debug[ 'HOME_URL' ] = home_url(); + + $servers = get_option('akismet_available_servers'); + if ( (time() - get_option('akismet_connectivity_time') < $cache_timeout) && $servers !== false ) { + $servers = self::check_server_ip_connectivity(); + update_option('akismet_available_servers', $servers); + update_option('akismet_connectivity_time', time()); + } + + if ( wp_http_supports( array( 'ssl' ) ) ) { + $response = wp_remote_get( 'https://rest.akismet.com/1.1/test' ); + } + else { + $response = wp_remote_get( 'http://rest.akismet.com/1.1/test' ); + } + + $debug[ 'gethostbynamel' ] = function_exists('gethostbynamel') ? 'exists' : 'not here'; + $debug[ 'Servers' ] = $servers; + $debug[ 'Test Connection' ] = $response; + + Akismet::log( $debug ); + + if ( $response && 'connected' == wp_remote_retrieve_body( $response ) ) + return true; + + return false; + } + + // Check the server connectivity and store the available servers in an option. + public static function get_server_connectivity($cache_timeout = 86400) { + return self::check_server_connectivity( $cache_timeout ); + } + + /** + * Find out whether any comments in the Pending queue have not yet been checked by Akismet. + * + * @return bool + */ + public static function are_any_comments_waiting_to_be_checked() { + return !! get_comments( array( + // Exclude comments that are not pending. This would happen if someone manually approved or spammed a comment + // that was waiting to be checked. The akismet_error meta entry will eventually be removed by the cron recheck job. + 'status' => 'hold', + + // This is the commentmeta that is saved when a comment couldn't be checked. + 'meta_key' => 'akismet_error', + + // We only need to know whether at least one comment is waiting for a check. + 'number' => 1, + ) ); + } + + public static function get_page_url( $page = 'config' ) { + + $args = array( 'page' => 'akismet-key-config' ); + + if ( $page == 'stats' ) { + $args = array( 'page' => 'akismet-key-config', 'view' => 'stats' ); + } elseif ( $page == 'delete_key' ) { + $args = array( 'page' => 'akismet-key-config', 'view' => 'start', 'action' => 'delete-key', '_wpnonce' => wp_create_nonce( self::NONCE ) ); + } elseif ( $page === 'init' ) { + $args = array( 'page' => 'akismet-key-config', 'view' => 'start' ); + } + + return add_query_arg( $args, menu_page_url( 'akismet-key-config', false ) ); + } + + public static function get_akismet_user( $api_key ) { + $akismet_user = false; + + $request_args = array( + 'key' => $api_key, + 'blog' => get_option( 'home' ), + ); + + $request_args = apply_filters( 'akismet_request_args', $request_args, 'get-subscription' ); + + $subscription_verification = Akismet::http_post( Akismet::build_query( $request_args ), 'get-subscription' ); + + if ( ! empty( $subscription_verification[1] ) ) { + if ( 'invalid' !== $subscription_verification[1] ) { + $akismet_user = json_decode( $subscription_verification[1] ); + } + } + + return $akismet_user; + } + + public static function get_stats( $api_key ) { + $stat_totals = array(); + + foreach( array( '6-months', 'all' ) as $interval ) { + $request_args = array( + 'blog' => get_option( 'home' ), + 'key' => $api_key, + 'from' => $interval, + ); + + $request_args = apply_filters( 'akismet_request_args', $request_args, 'get-stats' ); + + $response = Akismet::http_post( Akismet::build_query( $request_args ), 'get-stats' ); + + if ( ! empty( $response[1] ) ) { + $data = json_decode( $response[1] ); + /* + * The json decoded response should be an object. If it's not an object, something's wrong, and the data + * shouldn't be added to the stats_totals array. + */ + if ( is_object( $data ) ) { + $stat_totals[ $interval ] = $data; + } + } + } + + return $stat_totals; + } + + public static function verify_wpcom_key( $api_key, $user_id, $extra = array() ) { + $request_args = array_merge( + array( + 'user_id' => $user_id, + 'api_key' => $api_key, + 'get_account_type' => 'true', + ), + $extra + ); + + $request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-wpcom-key' ); + + $akismet_account = Akismet::http_post( Akismet::build_query( $request_args ), 'verify-wpcom-key' ); + + if ( ! empty( $akismet_account[1] ) ) + $akismet_account = json_decode( $akismet_account[1] ); + + Akismet::log( compact( 'akismet_account' ) ); + + return $akismet_account; + } + + public static function connect_jetpack_user() { + + if ( $jetpack_user = self::get_jetpack_user() ) { + if ( isset( $jetpack_user['user_id'] ) && isset( $jetpack_user['api_key'] ) ) { + $akismet_user = self::verify_wpcom_key( $jetpack_user['api_key'], $jetpack_user['user_id'], array( 'action' => 'connect_jetpack_user' ) ); + + if ( is_object( $akismet_user ) ) { + self::save_key( $akismet_user->api_key ); + return in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ) ); + } + } + } + + return false; + } + + public static function display_alert() { + Akismet::view( 'notice', array( + 'type' => 'alert', + 'code' => (int) get_option( 'akismet_alert_code' ), + 'msg' => get_option( 'akismet_alert_msg' ) + ) ); + } + + public static function get_usage_limit_alert_data() { + return array( + 'type' => 'usage-limit', + 'code' => (int) get_option( 'akismet_alert_code' ), + 'msg' => get_option( 'akismet_alert_msg' ), + 'api_calls' => get_option( 'akismet_alert_api_calls' ), + 'usage_limit' => get_option( 'akismet_alert_usage_limit' ), + 'upgrade_plan' => get_option( 'akismet_alert_upgrade_plan' ), + 'upgrade_url' => get_option( 'akismet_alert_upgrade_url' ), + 'upgrade_type' => get_option( 'akismet_alert_upgrade_type' ), + 'upgrade_via_support' => get_option( 'akismet_alert_upgrade_via_support' ) === 'true', + ); + } + + public static function display_usage_limit_alert() { + Akismet::view( 'notice', self::get_usage_limit_alert_data() ); + } + + public static function display_spam_check_warning() { + Akismet::fix_scheduled_recheck(); + + if ( wp_next_scheduled('akismet_schedule_cron_recheck') > time() && self::are_any_comments_waiting_to_be_checked() ) { + /* + * The 'akismet_display_cron_disabled_notice' filter can be used to control whether the WP-Cron disabled notice is displayed. + */ + if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON && apply_filters( 'akismet_display_cron_disabled_notice', true ) ) { + Akismet::view( 'notice', array( 'type' => 'spam-check-cron-disabled' ) ); + } else { + /* translators: The Akismet configuration page URL. */ + $link_text = apply_filters( 'akismet_spam_check_warning_link_text', sprintf( __( 'Please check your Akismet configuration and contact your web host if problems persist.', 'akismet' ), esc_url( self::get_page_url() ) ) ); + Akismet::view( 'notice', array( 'type' => 'spam-check', 'link_text' => $link_text ) ); + } + } + } + + public static function display_api_key_warning() { + Akismet::view( 'notice', array( 'type' => 'plugin' ) ); + } + + public static function display_page() { + if ( !Akismet::get_api_key() || ( isset( $_GET['view'] ) && $_GET['view'] == 'start' ) ) + self::display_start_page(); + elseif ( isset( $_GET['view'] ) && $_GET['view'] == 'stats' ) + self::display_stats_page(); + else + self::display_configuration_page(); + } + + public static function display_start_page() { + if ( isset( $_GET['action'] ) ) { + if ( $_GET['action'] == 'delete-key' ) { + if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], self::NONCE ) ) + delete_option( 'wordpress_api_key' ); + } + } + + if ( $api_key = Akismet::get_api_key() && ( empty( self::$notices['status'] ) || 'existing-key-invalid' != self::$notices['status'] ) ) { + self::display_configuration_page(); + return; + } + + //the user can choose to auto connect their API key by clicking a button on the akismet done page + //if jetpack, get verified api key by using connected wpcom user id + //if no jetpack, get verified api key by using an akismet token + + $akismet_user = false; + + if ( isset( $_GET['token'] ) && preg_match('/^(\d+)-[0-9a-f]{20}$/', $_GET['token'] ) ) + $akismet_user = self::verify_wpcom_key( '', '', array( 'token' => $_GET['token'] ) ); + elseif ( $jetpack_user = self::get_jetpack_user() ) + $akismet_user = self::verify_wpcom_key( $jetpack_user['api_key'], $jetpack_user['user_id'] ); + + if ( isset( $_GET['action'] ) ) { + if ( $_GET['action'] == 'save-key' ) { + if ( is_object( $akismet_user ) ) { + self::save_key( $akismet_user->api_key ); + self::display_configuration_page(); + return; + } + } + } + + Akismet::view( 'start', compact( 'akismet_user' ) ); + + /* + // To see all variants when testing. + $akismet_user->status = 'no-sub'; + Akismet::view( 'start', compact( 'akismet_user' ) ); + $akismet_user->status = 'cancelled'; + Akismet::view( 'start', compact( 'akismet_user' ) ); + $akismet_user->status = 'suspended'; + Akismet::view( 'start', compact( 'akismet_user' ) ); + $akismet_user->status = 'other'; + Akismet::view( 'start', compact( 'akismet_user' ) ); + $akismet_user = false; + */ + } + + public static function display_stats_page() { + Akismet::view( 'stats' ); + } + + public static function display_configuration_page() { + $api_key = Akismet::get_api_key(); + $akismet_user = self::get_akismet_user( $api_key ); + + if ( ! $akismet_user ) { + // This could happen if the user's key became invalid after it was previously valid and successfully set up. + self::$notices['status'] = 'existing-key-invalid'; + self::display_start_page(); + return; + } + + $stat_totals = self::get_stats( $api_key ); + + // If unset, create the new strictness option using the old discard option to determine its default. + // If the old option wasn't set, default to discarding the blatant spam. + if ( get_option( 'akismet_strictness' ) === false ) { + add_option( 'akismet_strictness', ( get_option( 'akismet_discard_month' ) === 'false' ? '0' : '1' ) ); + } + + // Sync the local "Total spam blocked" count with the authoritative count from the server. + if ( isset( $stat_totals['all'], $stat_totals['all']->spam ) ) { + update_option( 'akismet_spam_count', $stat_totals['all']->spam ); + } + + $notices = array(); + + if ( empty( self::$notices ) ) { + if ( ! empty( $stat_totals['all'] ) && isset( $stat_totals['all']->time_saved ) && $akismet_user->status == 'active' && $akismet_user->account_type == 'free-api-key' ) { + + $time_saved = false; + + if ( $stat_totals['all']->time_saved > 1800 ) { + $total_in_minutes = round( $stat_totals['all']->time_saved / 60 ); + $total_in_hours = round( $total_in_minutes / 60 ); + $total_in_days = round( $total_in_hours / 8 ); + $cleaning_up = __( 'Cleaning up spam takes time.' , 'akismet'); + + if ( $total_in_days > 1 ) + $time_saved = $cleaning_up . ' ' . sprintf( _n( 'Akismet has saved you %s day!', 'Akismet has saved you %s days!', $total_in_days, 'akismet' ), number_format_i18n( $total_in_days ) ); + elseif ( $total_in_hours > 1 ) + $time_saved = $cleaning_up . ' ' . sprintf( _n( 'Akismet has saved you %d hour!', 'Akismet has saved you %d hours!', $total_in_hours, 'akismet' ), $total_in_hours ); + elseif ( $total_in_minutes >= 30 ) + $time_saved = $cleaning_up . ' ' . sprintf( _n( 'Akismet has saved you %d minute!', 'Akismet has saved you %d minutes!', $total_in_minutes, 'akismet' ), $total_in_minutes ); + } + + $notices[] = array( 'type' => 'active-notice', 'time_saved' => $time_saved ); + } + } + + if ( !isset( self::$notices['status'] ) && in_array( $akismet_user->status, array( 'cancelled', 'suspended', 'missing', 'no-sub' ) ) ) { + $notices[] = array( 'type' => $akismet_user->status ); + } + + $alert_code = get_option( 'akismet_alert_code' ); + if ( isset( Akismet::$limit_notices[ $alert_code ] ) ) { + $notices[] = self::get_usage_limit_alert_data(); + } elseif ( $alert_code > 0 ) { + $notices[] = array( + 'type' => 'alert', + 'code' => (int) get_option( 'akismet_alert_code' ), + 'msg' => get_option( 'akismet_alert_msg' ), + ); + } + + /* + * To see all variants when testing. + * + * You may also want to comment out the akismet_view_arguments filter in Akismet::view() + * to ensure that you can see all of the notices (e.g. suspended, active-notice). + */ + // $notices[] = array( 'type' => 'active-notice', 'time_saved' => 'Cleaning up spam takes time. Akismet has saved you 1 minute!' ); + // $notices[] = array( 'type' => 'plugin' ); + // $notices[] = array( 'type' => 'notice', 'notice_header' => 'This is the notice header.', 'notice_text' => 'This is the notice text.' ); + // $notices[] = array( 'type' => 'missing-functions' ); + // $notices[] = array( 'type' => 'servers-be-down' ); + // $notices[] = array( 'type' => 'active-dunning' ); + // $notices[] = array( 'type' => 'cancelled' ); + // $notices[] = array( 'type' => 'suspended' ); + // $notices[] = array( 'type' => 'missing' ); + // $notices[] = array( 'type' => 'no-sub' ); + // $notices[] = array( 'type' => 'new-key-valid' ); + // $notices[] = array( 'type' => 'new-key-invalid' ); + // $notices[] = array( 'type' => 'existing-key-invalid' ); + // $notices[] = array( 'type' => 'new-key-failed' ); + // $notices[] = array( 'type' => 'usage-limit', 'api_calls' => '15000', 'usage_limit' => '10000', 'upgrade_plan' => 'Enterprise', 'upgrade_url' => 'https://akismet.com/account/', 'code' => 10502 ); + // $notices[] = array( 'type' => 'spam-check', 'link_text' => 'Link text.' ); + // $notices[] = array( 'type' => 'spam-check-cron-disabled' ); + // $notices[] = array( 'type' => 'alert', 'code' => 123 ); + + Akismet::log( compact( 'stat_totals', 'akismet_user' ) ); + Akismet::view( 'config', compact( 'api_key', 'akismet_user', 'stat_totals', 'notices' ) ); + } + + public static function display_notice() { + global $hook_suffix; + + if ( in_array( $hook_suffix, array( 'jetpack_page_akismet-key-config', 'settings_page_akismet-key-config' ) ) ) { + // This page manages the notices and puts them inline where they make sense. + return; + } + + // To see notice variants while testing. + // Akismet::view( 'notice', array( 'type' => 'spam-check-cron-disabled' ) ); + // Akismet::view( 'notice', array( 'type' => 'spam-check' ) ); + // Akismet::view( 'notice', array( 'type' => 'alert', 'code' => 123, 'msg' => 'Message' ) ); + + if ( in_array( $hook_suffix, array( 'edit-comments.php' ) ) && (int) get_option( 'akismet_alert_code' ) > 0 ) { + Akismet::verify_key( Akismet::get_api_key() ); //verify that the key is still in alert state + + $alert_code = get_option( 'akismet_alert_code' ); + if ( isset( Akismet::$limit_notices[ $alert_code ] ) ) { + self::display_usage_limit_alert(); + } elseif ( $alert_code > 0 ) { + self::display_alert(); + } + } + elseif ( ( 'plugins.php' === $hook_suffix || 'edit-comments.php' === $hook_suffix ) && ! Akismet::get_api_key() ) { + // Show the "Set Up Akismet" banner on the comments and plugin pages if no API key has been set. + self::display_api_key_warning(); + } + elseif ( $hook_suffix == 'edit-comments.php' && wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) { + self::display_spam_check_warning(); + } + + if ( isset( $_GET['akismet_recheck_complete'] ) ) { + $recheck_count = (int) $_GET['recheck_count']; + $spam_count = (int) $_GET['spam_count']; + + if ( $recheck_count === 0 ) { + $message = __( 'There were no comments to check. Akismet will only check comments awaiting moderation.', 'akismet' ); + } + else { + $message = sprintf( _n( 'Akismet checked %s comment.', 'Akismet checked %s comments.', $recheck_count, 'akismet' ), number_format( $recheck_count ) ); + $message .= ' '; + + if ( $spam_count === 0 ) { + $message .= __( 'No comments were caught as spam.', 'akismet' ); + } + else { + $message .= sprintf( _n( '%s comment was caught as spam.', '%s comments were caught as spam.', $spam_count, 'akismet' ), number_format( $spam_count ) ); + } + } + + echo '' . esc_html( $message ) . '
' . esc_html( __( 'Akismet could not recheck your comments for spam.', 'akismet' ) ) . '
+ + +
+ + + + + + 'FIRST_MONTH_OVER_LIMIT', + 10502 => 'SECOND_MONTH_OVER_LIMIT', + 10504 => 'THIRD_MONTH_APPROACHING_LIMIT', + 10508 => 'THIRD_MONTH_OVER_LIMIT', + 10516 => 'FOUR_PLUS_MONTHS_OVER_LIMIT', + ); + + private static $last_comment = ''; + private static $initiated = false; + private static $prevent_moderation_email_for_these_comments = array(); + private static $last_comment_result = null; + private static $comment_as_submitted_allowed_keys = array( 'blog' => '', 'blog_charset' => '', 'blog_lang' => '', 'blog_ua' => '', 'comment_agent' => '', 'comment_author' => '', 'comment_author_IP' => '', 'comment_author_email' => '', 'comment_author_url' => '', 'comment_content' => '', 'comment_date_gmt' => '', 'comment_tags' => '', 'comment_type' => '', 'guid' => '', 'is_test' => '', 'permalink' => '', 'reporter' => '', 'site_domain' => '', 'submit_referer' => '', 'submit_uri' => '', 'user_ID' => '', 'user_agent' => '', 'user_id' => '', 'user_ip' => '' ); + + public static function init() { + if ( ! self::$initiated ) { + self::init_hooks(); + } + } + + /** + * Initializes WordPress hooks + */ + private static function init_hooks() { + self::$initiated = true; + + add_action( 'wp_insert_comment', array( 'Akismet', 'auto_check_update_meta' ), 10, 2 ); + add_filter( 'preprocess_comment', array( 'Akismet', 'auto_check_comment' ), 1 ); + add_filter( 'rest_pre_insert_comment', array( 'Akismet', 'rest_auto_check_comment' ), 1 ); + + add_action( 'comment_form', array( 'Akismet', 'load_form_js' ) ); + add_action( 'do_shortcode_tag', array( 'Akismet', 'load_form_js_via_filter' ), 10, 4 ); + + add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments' ) ); + add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_old_comments_meta' ) ); + add_action( 'akismet_scheduled_delete', array( 'Akismet', 'delete_orphaned_commentmeta' ) ); + add_action( 'akismet_schedule_cron_recheck', array( 'Akismet', 'cron_recheck' ) ); + + add_action( 'comment_form', array( 'Akismet', 'add_comment_nonce' ), 1 ); + add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) ); + add_filter( 'script_loader_tag', array( 'Akismet', 'set_form_js_async' ), 10, 3 ); + + add_filter( 'comment_moderation_recipients', array( 'Akismet', 'disable_moderation_emails_if_unreachable' ), 1000, 2 ); + add_filter( 'pre_comment_approved', array( 'Akismet', 'last_comment_status' ), 10, 2 ); + + add_action( 'transition_comment_status', array( 'Akismet', 'transition_comment_status' ), 10, 3 ); + + // Run this early in the pingback call, before doing a remote fetch of the source uri + add_action( 'xmlrpc_call', array( 'Akismet', 'pre_check_pingback' ), 10, 3 ); + + // Jetpack compatibility + add_filter( 'jetpack_options_whitelist', array( 'Akismet', 'add_to_jetpack_options_whitelist' ) ); + add_filter( 'jetpack_contact_form_html', array( 'Akismet', 'inject_custom_form_fields' ) ); + add_filter( 'jetpack_contact_form_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) ); + + // Gravity Forms + add_filter( 'gform_get_form_filter', array( 'Akismet', 'inject_custom_form_fields' ) ); + add_filter( 'gform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ) ); + + // Contact Form 7 + add_filter( 'wpcf7_form_elements', array( 'Akismet', 'append_custom_form_fields' ) ); + add_filter( 'wpcf7_akismet_parameters', array( 'Akismet', 'prepare_custom_form_values' ) ); + + // Formidable Forms + add_filter( 'frm_filter_final_form', array( 'Akismet', 'inject_custom_form_fields' ) ); + add_filter( 'frm_akismet_values', array( 'Akismet', 'prepare_custom_form_values' ) ); + + // Fluent Forms + /* + * The Fluent Forms hook names were updated in version 5.0.0. The last version that supported + * the original hook names was 4.3.25, and version 4.3.25 was tested up to WordPress version 6.1. + * + * The legacy hooks are fired before the new hooks. See + * https://github.com/fluentform/fluentform/commit/cc45341afcae400f217470a7bbfb15efdd80454f + * + * The legacy Fluent Forms hooks will be removed when Akismet no longer supports WordPress version 6.1. + * This will provide compatibility with previous versions of Fluent Forms for a reasonable amount of time. + */ + add_filter( 'fluentform_form_element_start', array( 'Akismet', 'output_custom_form_fields' ) ); + add_filter( 'fluentform_akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 ); + // Current Fluent Form hooks. + add_filter( 'fluentform/form_element_start', array( 'Akismet', 'output_custom_form_fields' ) ); + add_filter( 'fluentform/akismet_fields', array( 'Akismet', 'prepare_custom_form_values' ), 10, 2 ); + + add_action( 'update_option_wordpress_api_key', array( 'Akismet', 'updated_option' ), 10, 2 ); + add_action( 'add_option_wordpress_api_key', array( 'Akismet', 'added_option' ), 10, 2 ); + + add_action( 'comment_form_after', array( 'Akismet', 'display_comment_form_privacy_notice' ) ); + } + + public static function get_api_key() { + return apply_filters( 'akismet_get_api_key', defined('WPCOM_API_KEY') ? constant('WPCOM_API_KEY') : get_option('wordpress_api_key') ); + } + + /** + * Exchange the API key for a token that can only be used to access stats pages. + * + * @return string + */ + public static function get_access_token() { + static $access_token = null; + + if ( is_null( $access_token ) ) { + $request_args = array( 'api_key' => self::get_api_key() ); + + $request_args = apply_filters( 'akismet_request_args', $request_args, 'token' ); + + $response = self::http_post( self::build_query( $request_args ), 'token' ); + + $access_token = $response[1]; + } + + return $access_token; + } + + public static function check_key_status( $key, $ip = null ) { + $request_args = array( + 'key' => $key, + 'blog' => get_option( 'home' ), + ); + + $request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-key' ); + + return self::http_post( self::build_query( $request_args ), 'verify-key', $ip ); + } + + public static function verify_key( $key, $ip = null ) { + // Shortcut for obviously invalid keys. + if ( strlen( $key ) != 12 ) { + return 'invalid'; + } + + $response = self::check_key_status( $key, $ip ); + + if ( $response[1] != 'valid' && $response[1] != 'invalid' ) + return 'failed'; + + return $response[1]; + } + + public static function deactivate_key( $key ) { + $request_args = array( + 'key' => $key, + 'blog' => get_option( 'home' ), + ); + + $request_args = apply_filters( 'akismet_request_args', $request_args, 'deactivate' ); + + $response = self::http_post( self::build_query( $request_args ), 'deactivate' ); + + if ( $response[1] != 'deactivated' ) + return 'failed'; + + return $response[1]; + } + + /** + * Add the akismet option to the Jetpack options management whitelist. + * + * @param array $options The list of whitelisted option names. + * @return array The updated whitelist + */ + public static function add_to_jetpack_options_whitelist( $options ) { + $options[] = 'wordpress_api_key'; + return $options; + } + + /** + * When the akismet option is updated, run the registration call. + * + * This should only be run when the option is updated from the Jetpack/WP.com + * API call, and only if the new key is different than the old key. + * + * @param mixed $old_value The old option value. + * @param mixed $value The new option value. + */ + public static function updated_option( $old_value, $value ) { + // Not an API call + if ( ! class_exists( 'WPCOM_JSON_API_Update_Option_Endpoint' ) ) { + return; + } + // Only run the registration if the old key is different. + if ( $old_value !== $value ) { + self::verify_key( $value ); + } + } + + /** + * Treat the creation of an API key the same as updating the API key to a new value. + * + * @param mixed $option_name Will always be "wordpress_api_key", until something else hooks in here. + * @param mixed $value The option value. + */ + public static function added_option( $option_name, $value ) { + if ( 'wordpress_api_key' === $option_name ) { + return self::updated_option( '', $value ); + } + } + + public static function rest_auto_check_comment( $commentdata ) { + return self::auto_check_comment( $commentdata, 'rest_api' ); + } + + /** + * Check a comment for spam. + * + * @param array $commentdata + * @param string $context What kind of request triggered this comment check? Possible values are 'default', 'rest_api', and 'xml-rpc'. + * @return array|WP_Error Either the $commentdata array with additional entries related to its spam status + * or a WP_Error, if it's a REST API request and the comment should be discarded. + */ + public static function auto_check_comment( $commentdata, $context = 'default' ) { + // If no key is configured, then there's no point in doing any of this. + if ( ! self::get_api_key() ) { + return $commentdata; + } + + self::$last_comment_result = null; + + // Skip the Akismet check if the comment matches the Disallowed Keys list. + if ( function_exists( 'wp_check_comment_disallowed_list' ) ) { + $comment_author = isset( $commentdata['comment_author'] ) ? $commentdata['comment_author'] : ''; + $comment_author_email = isset( $commentdata['comment_author_email'] ) ? $commentdata['comment_author_email'] : ''; + $comment_author_url = isset( $commentdata['comment_author_url'] ) ? $commentdata['comment_author_url'] : ''; + $comment_content = isset( $commentdata['comment_content'] ) ? $commentdata['comment_content'] : ''; + $comment_author_ip = isset( $commentdata['comment_author_IP'] ) ? $commentdata['comment_author_IP'] : ''; + $comment_agent = isset( $commentdata['comment_agent'] ) ? $commentdata['comment_agent'] : ''; + + if ( wp_check_comment_disallowed_list( $comment_author, $comment_author_email, $comment_author_url, $comment_content, $comment_author_ip, $comment_agent ) ) { + self::set_last_comment( $commentdata ); + return $commentdata; + } + } + + $comment = $commentdata; + + $comment['user_ip'] = self::get_ip_address(); + $comment['user_agent'] = self::get_user_agent(); + $comment['referrer'] = self::get_referer(); + $comment['blog'] = get_option( 'home' ); + $comment['blog_lang'] = get_locale(); + $comment['blog_charset'] = get_option('blog_charset'); + $comment['permalink'] = get_permalink( $comment['comment_post_ID'] ); + + if ( ! empty( $comment['user_ID'] ) ) { + $comment['user_role'] = Akismet::get_user_roles( $comment['user_ID'] ); + } + + /** See filter documentation in init_hooks(). */ + $akismet_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); + $comment['akismet_comment_nonce'] = 'inactive'; + if ( $akismet_nonce_option == 'true' || $akismet_nonce_option == '' ) { + $comment['akismet_comment_nonce'] = 'failed'; + if ( isset( $_POST['akismet_comment_nonce'] ) && wp_verify_nonce( $_POST['akismet_comment_nonce'], 'akismet_comment_nonce_' . $comment['comment_post_ID'] ) ) + $comment['akismet_comment_nonce'] = 'passed'; + + // comment reply in wp-admin + if ( isset( $_POST['_ajax_nonce-replyto-comment'] ) && check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' ) ) + $comment['akismet_comment_nonce'] = 'passed'; + + } + + if ( self::is_test_mode() ) + $comment['is_test'] = 'true'; + + foreach( $_POST as $key => $value ) { + if ( is_string( $value ) ) + $comment["POST_{$key}"] = $value; + } + + foreach ( $_SERVER as $key => $value ) { + if ( ! is_string( $value ) ) { + continue; + } + + if ( preg_match( "/^HTTP_COOKIE/", $key ) ) { + continue; + } + + // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need. + if ( preg_match( "/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/", $key ) ) { + $comment[ "$key" ] = $value; + } + } + + $post = get_post( $comment['comment_post_ID'] ); + + if ( ! is_null( $post ) ) { + // $post can technically be null, although in the past, it's always been an indicator of another plugin interfering. + $comment[ 'comment_post_modified_gmt' ] = $post->post_modified_gmt; + + // Tags and categories are important context in which to consider the comment. + $comment['comment_context'] = array(); + + $tag_names = wp_get_post_tags( $post->ID, array( 'fields' => 'names' ) ); + + if ( $tag_names && ! is_wp_error( $tag_names ) ) { + foreach ( $tag_names as $tag_name ) { + $comment['comment_context'][] = $tag_name; + } + } + + $category_names = wp_get_post_categories( $post->ID, array( 'fields' => 'names' ) ); + + if ( $category_names && ! is_wp_error( $category_names ) ) { + foreach ( $category_names as $category_name ) { + $comment['comment_context'][] = $category_name; + } + } + } + + /** + * Filter the data that is used to generate the request body for the API call. + * + * @since 5.3.1 + * + * @param array $comment An array of request data. + * @param string $endpoint The API endpoint being requested. + */ + $comment = apply_filters( 'akismet_request_args', $comment, 'comment-check' ); + + $response = self::http_post( self::build_query( $comment ), 'comment-check' ); + + do_action( 'akismet_comment_check_response', $response ); + + $commentdata['comment_as_submitted'] = array_intersect_key( $comment, self::$comment_as_submitted_allowed_keys ); + + // Also include any form fields we inject into the comment form, like ak_js + foreach ( $_POST as $key => $value ) { + if ( is_string( $value ) && strpos( $key, 'ak_' ) === 0 ) { + $commentdata['comment_as_submitted'][ 'POST_' . $key ] = $value; + } + } + + $commentdata['akismet_result'] = $response[1]; + + if ( isset( $response[0]['x-akismet-pro-tip'] ) ) + $commentdata['akismet_pro_tip'] = $response[0]['x-akismet-pro-tip']; + + if ( isset( $response[0]['x-akismet-guid'] ) ) { + $commentdata['akismet_guid'] = $response[0]['x-akismet-guid']; + } + + if ( isset( $response[0]['x-akismet-error'] ) ) { + // An error occurred that we anticipated (like a suspended key) and want the user to act on. + // Send to moderation. + self::$last_comment_result = '0'; + } + else if ( 'true' == $response[1] ) { + // akismet_spam_count will be incremented later by comment_is_spam() + self::$last_comment_result = 'spam'; + + $discard = ( isset( $commentdata['akismet_pro_tip'] ) && $commentdata['akismet_pro_tip'] === 'discard' && self::allow_discard() ); + + do_action( 'akismet_spam_caught', $discard ); + + if ( $discard ) { + // The spam is obvious, so we're bailing out early. + // akismet_result_spam() won't be called so bump the counter here + if ( $incr = apply_filters( 'akismet_spam_count_incr', 1 ) ) { + update_option( 'akismet_spam_count', get_option( 'akismet_spam_count' ) + $incr ); + } + + if ( 'rest_api' === $context ) { + return new WP_Error( 'akismet_rest_comment_discarded', __( 'Comment discarded.', 'akismet' ) ); + } else if ( 'xml-rpc' === $context ) { + // If this is a pingback that we're pre-checking, the discard behavior is the same as the normal spam response behavior. + return $commentdata; + } else { + // Redirect back to the previous page, or failing that, the post permalink, or failing that, the homepage of the blog. + $redirect_to = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ( $post ? get_permalink( $post ) : home_url() ); + wp_safe_redirect( esc_url_raw( $redirect_to ) ); + die(); + } + } + else if ( 'rest_api' === $context ) { + // The way the REST API structures its calls, we can set the comment_approved value right away. + $commentdata['comment_approved'] = 'spam'; + } + } + + // if the response is neither true nor false, hold the comment for moderation and schedule a recheck + if ( 'true' != $response[1] && 'false' != $response[1] ) { + if ( !current_user_can('moderate_comments') ) { + // Comment status should be moderated + self::$last_comment_result = '0'; + } + + if ( ! wp_next_scheduled( 'akismet_schedule_cron_recheck' ) ) { + wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' ); + do_action( 'akismet_scheduled_recheck', 'invalid-response-' . $response[1] ); + } + + self::$prevent_moderation_email_for_these_comments[] = $commentdata; + } + + // Delete old comments daily + if ( ! wp_next_scheduled( 'akismet_scheduled_delete' ) ) { + wp_schedule_event( time(), 'daily', 'akismet_scheduled_delete' ); + } + + self::set_last_comment( $commentdata ); + self::fix_scheduled_recheck(); + + return $commentdata; + } + + public static function get_last_comment() { + return self::$last_comment; + } + + public static function set_last_comment( $comment ) { + if ( is_null( $comment ) ) { + self::$last_comment = null; + } + else { + // We filter it here so that it matches the filtered comment data that we'll have to compare against later. + // wp_filter_comment expects comment_author_IP + self::$last_comment = wp_filter_comment( + array_merge( + array( 'comment_author_IP' => self::get_ip_address() ), + $comment + ) + ); + } + } + + // this fires on wp_insert_comment. we can't update comment_meta when auto_check_comment() runs + // because we don't know the comment ID at that point. + public static function auto_check_update_meta( $id, $comment ) { + // wp_insert_comment() might be called in other contexts, so make sure this is the same comment + // as was checked by auto_check_comment + if ( is_object( $comment ) && !empty( self::$last_comment ) && is_array( self::$last_comment ) ) { + if ( self::matches_last_comment( $comment ) ) { + load_plugin_textdomain( 'akismet' ); + + // normal result: true or false + if ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'true' ) { + update_comment_meta( $comment->comment_ID, 'akismet_result', 'true' ); + self::update_comment_history( $comment->comment_ID, '', 'check-spam' ); + if ( $comment->comment_approved != 'spam' ) { + self::update_comment_history( + $comment->comment_ID, + '', + 'status-changed-' . $comment->comment_approved + ); + } + } elseif ( isset( self::$last_comment['akismet_result'] ) && self::$last_comment['akismet_result'] == 'false' ) { + update_comment_meta( $comment->comment_ID, 'akismet_result', 'false' ); + self::update_comment_history( $comment->comment_ID, '', 'check-ham' ); + // Status could be spam or trash, depending on the WP version and whether this change applies: + // https://core.trac.wordpress.org/changeset/34726 + if ( $comment->comment_approved == 'spam' || $comment->comment_approved == 'trash' ) { + if ( function_exists( 'wp_check_comment_disallowed_list' ) ) { + if ( wp_check_comment_disallowed_list( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent ) ) { + self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' ); + } else { + self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved ); + } + } else { + self::update_comment_history( $comment->comment_ID, '', 'status-changed-' . $comment->comment_approved ); + } + } + } elseif ( ! isset( self::$last_comment['akismet_result'] ) ) { + // akismet_result isn't set, so the comment wasn't sent to Akismet. + update_comment_meta( $comment->comment_ID, 'akismet_skipped', 'true' ); + $caught_by_disallowed_list = false; + + if ( function_exists( 'wp_check_comment_disallowed_list' ) ) { + $caught_by_disallowed_list = wp_check_comment_disallowed_list( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent ); + } + + if ( $caught_by_disallowed_list ) { + self::update_comment_history( $comment->comment_ID, '', 'wp-disallowed' ); + self::update_comment_history( $comment->comment_ID, '', 'akismet-skipped-disallowed' ); + } else { + // Add a generic skipped history item. + self::update_comment_history( $comment->comment_ID, '', 'akismet-skipped' ); + } + } else { + // abnormal result: error + update_comment_meta( $comment->comment_ID, 'akismet_error', time() ); + self::update_comment_history( + $comment->comment_ID, + '', + 'check-error', + array( 'response' => substr( self::$last_comment['akismet_result'], 0, 50 ) ) + ); + } + + // record the complete original data as submitted for checking + if ( isset( self::$last_comment['comment_as_submitted'] ) ) { + update_comment_meta( $comment->comment_ID, 'akismet_as_submitted', self::$last_comment['comment_as_submitted'] ); + } + + if ( isset( self::$last_comment['akismet_pro_tip'] ) ) { + update_comment_meta( $comment->comment_ID, 'akismet_pro_tip', self::$last_comment['akismet_pro_tip'] ); + } + + if ( isset( self::$last_comment['akismet_guid'] ) ) { + update_comment_meta( $comment->comment_ID, 'akismet_guid', self::$last_comment['akismet_guid'] ); + } + } + } + } + + public static function delete_old_comments() { + global $wpdb; + + /** + * Determines how many comments will be deleted in each batch. + * + * @param int The default, as defined by AKISMET_DELETE_LIMIT. + */ + $delete_limit = apply_filters( 'akismet_delete_comment_limit', defined( 'AKISMET_DELETE_LIMIT' ) ? AKISMET_DELETE_LIMIT : 10000 ); + $delete_limit = max( 1, intval( $delete_limit ) ); + + /** + * Determines how many days a comment will be left in the Spam queue before being deleted. + * + * @param int The default number of days. + */ + $delete_interval = apply_filters( 'akismet_delete_comment_interval', 15 ); + $delete_interval = max( 1, intval( $delete_interval ) ); + + while ( $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->comments} WHERE DATE_SUB(NOW(), INTERVAL %d DAY) > comment_date_gmt AND comment_approved = 'spam' LIMIT %d", $delete_interval, $delete_limit ) ) ) { + if ( empty( $comment_ids ) ) + return; + + $wpdb->queries = array(); + + $comments = array(); + + foreach ( $comment_ids as $comment_id ) { + $comments[ $comment_id ] = get_comment( $comment_id ); + + do_action( 'delete_comment', $comment_id, $comments[ $comment_id ] ); + do_action( 'akismet_batch_delete_count', __FUNCTION__ ); + } + + // Prepared as strings since comment_id is an unsigned BIGINT, and using %d will constrain the value to the maximum signed BIGINT. + $format_string = implode( ', ', array_fill( 0, is_countable( $comment_ids ) ? count( $comment_ids ) : 0, '%s' ) ); + + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->comments} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ( " . $format_string . " )", $comment_ids ) ); + + foreach ( $comment_ids as $comment_id ) { + do_action( 'deleted_comment', $comment_id, $comments[ $comment_id ] ); + unset( $comments[ $comment_id ] ); + } + + clean_comment_cache( $comment_ids ); + do_action( 'akismet_delete_comment_batch', is_countable( $comment_ids ) ? count( $comment_ids ) : 0 ); + } + + if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->comments ) ) // lucky number + $wpdb->query("OPTIMIZE TABLE {$wpdb->comments}"); + } + + public static function delete_old_comments_meta() { + global $wpdb; + + $interval = apply_filters( 'akismet_delete_commentmeta_interval', 15 ); + + # enforce a minimum of 1 day + $interval = absint( $interval ); + if ( $interval < 1 ) + $interval = 1; + + // akismet_as_submitted meta values are large, so expire them + // after $interval days regardless of the comment status + while ( $comment_ids = $wpdb->get_col( $wpdb->prepare( "SELECT m.comment_id FROM {$wpdb->commentmeta} as m INNER JOIN {$wpdb->comments} as c USING(comment_id) WHERE m.meta_key = 'akismet_as_submitted' AND DATE_SUB(NOW(), INTERVAL %d DAY) > c.comment_date_gmt LIMIT 10000", $interval ) ) ) { + if ( empty( $comment_ids ) ) + return; + + $wpdb->queries = array(); + + foreach ( $comment_ids as $comment_id ) { + delete_comment_meta( $comment_id, 'akismet_as_submitted' ); + do_action( 'akismet_batch_delete_count', __FUNCTION__ ); + } + + do_action( 'akismet_delete_commentmeta_batch', is_countable( $comment_ids ) ? count( $comment_ids ) : 0 ); + } + + if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number + $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}"); + } + + // Clear out comments meta that no longer have corresponding comments in the database + public static function delete_orphaned_commentmeta() { + global $wpdb; + + $last_meta_id = 0; + $start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true ); + $max_exec_time = max( ini_get('max_execution_time') - 5, 3 ); + + while ( $commentmeta_results = $wpdb->get_results( $wpdb->prepare( "SELECT m.meta_id, m.comment_id, m.meta_key FROM {$wpdb->commentmeta} as m LEFT JOIN {$wpdb->comments} as c USING(comment_id) WHERE c.comment_id IS NULL AND m.meta_id > %d ORDER BY m.meta_id LIMIT 1000", $last_meta_id ) ) ) { + if ( empty( $commentmeta_results ) ) + return; + + $wpdb->queries = array(); + + $commentmeta_deleted = 0; + + foreach ( $commentmeta_results as $commentmeta ) { + if ( 'akismet_' == substr( $commentmeta->meta_key, 0, 8 ) ) { + delete_comment_meta( $commentmeta->comment_id, $commentmeta->meta_key ); + do_action( 'akismet_batch_delete_count', __FUNCTION__ ); + $commentmeta_deleted++; + } + + $last_meta_id = $commentmeta->meta_id; + } + + do_action( 'akismet_delete_commentmeta_batch', $commentmeta_deleted ); + + // If we're getting close to max_execution_time, quit for this round. + if ( microtime(true) - $start_time > $max_exec_time ) + return; + } + + if ( apply_filters( 'akismet_optimize_table', ( mt_rand(1, 5000) == 11), $wpdb->commentmeta ) ) // lucky number + $wpdb->query("OPTIMIZE TABLE {$wpdb->commentmeta}"); + } + + // how many approved comments does this author have? + public static function get_user_comments_approved( $user_id, $comment_author_email, $comment_author, $comment_author_url ) { + global $wpdb; + + /** + * Which comment types should be ignored when counting a user's approved comments? + * + * Some plugins add entries to the comments table that are not actual + * comments that could have been checked by Akismet. Allow these comments + * to be excluded from the "approved comment count" query in order to + * avoid artificially inflating the approved comment count. + * + * @param array $comment_types An array of comment types that won't be considered + * when counting a user's approved comments. + * + * @since 4.2.2 + */ + $excluded_comment_types = apply_filters( 'akismet_excluded_comment_types', array() ); + + $comment_type_where = ''; + + if ( is_array( $excluded_comment_types ) && ! empty( $excluded_comment_types ) ) { + $excluded_comment_types = array_unique( $excluded_comment_types ); + + foreach ( $excluded_comment_types as $excluded_comment_type ) { + $comment_type_where .= $wpdb->prepare( ' AND comment_type <> %s ', $excluded_comment_type ); + } + } + + if ( ! empty( $user_id ) ) { + return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE user_id = %d AND comment_approved = 1" . $comment_type_where, $user_id ) ); + } + + if ( ! empty( $comment_author_email ) ) { + return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_author_email = %s AND comment_author = %s AND comment_author_url = %s AND comment_approved = 1" . $comment_type_where, $comment_author_email, $comment_author, $comment_author_url ) ); + } + + return 0; + } + + /** + * Get the full comment history for a given comment, as an array in reverse chronological order. + * Each entry will have an 'event', a 'time', and possible a 'message' member (if the entry is old enough). + * Some entries will also have a 'user' or 'meta' member. + * + * @param int $comment_id The relevant comment ID. + * @return array|bool An array of history events, or false if there is no history. + */ + public static function get_comment_history( $comment_id ) { + $history = get_comment_meta( $comment_id, 'akismet_history', false ); + if ( empty( $history ) || empty( $history[ 0 ] ) ) { + return false; + } + + /* + // To see all variants when testing. + $history[] = array( 'time' => 445856401, 'message' => 'Old versions of Akismet stored the message as a literal string in the commentmeta.', 'event' => null ); + $history[] = array( 'time' => 445856402, 'event' => 'recheck-spam' ); + $history[] = array( 'time' => 445856403, 'event' => 'check-spam' ); + $history[] = array( 'time' => 445856404, 'event' => 'recheck-ham' ); + $history[] = array( 'time' => 445856405, 'event' => 'check-ham' ); + $history[] = array( 'time' => 445856406, 'event' => 'wp-blacklisted' ); + $history[] = array( 'time' => 445856406, 'event' => 'wp-disallowed' ); + $history[] = array( 'time' => 445856407, 'event' => 'report-spam' ); + $history[] = array( 'time' => 445856408, 'event' => 'report-spam', 'user' => 'sam' ); + $history[] = array( 'message' => 'sam reported this comment as spam (hardcoded message).', 'time' => 445856400, 'event' => 'report-spam', 'user' => 'sam' ); + $history[] = array( 'time' => 445856409, 'event' => 'report-ham', 'user' => 'sam' ); + $history[] = array( 'message' => 'sam reported this comment as ham (hardcoded message).', 'time' => 445856400, 'event' => 'report-ham', 'user' => 'sam' ); // + $history[] = array( 'time' => 445856410, 'event' => 'cron-retry-spam' ); + $history[] = array( 'time' => 445856411, 'event' => 'cron-retry-ham' ); + $history[] = array( 'time' => 445856412, 'event' => 'check-error' ); // + $history[] = array( 'time' => 445856413, 'event' => 'check-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) ); + $history[] = array( 'time' => 445856414, 'event' => 'recheck-error' ); // Should not generate a message. + $history[] = array( 'time' => 445856415, 'event' => 'recheck-error', 'meta' => array( 'response' => 'The server was taking a nap.' ) ); + $history[] = array( 'time' => 445856416, 'event' => 'status-changedtrash' ); + $history[] = array( 'time' => 445856417, 'event' => 'status-changedspam' ); + $history[] = array( 'time' => 445856418, 'event' => 'status-changedhold' ); + $history[] = array( 'time' => 445856419, 'event' => 'status-changedapprove' ); + $history[] = array( 'time' => 445856420, 'event' => 'status-changed-trash' ); + $history[] = array( 'time' => 445856421, 'event' => 'status-changed-spam' ); + $history[] = array( 'time' => 445856422, 'event' => 'status-changed-hold' ); + $history[] = array( 'time' => 445856423, 'event' => 'status-changed-approve' ); + $history[] = array( 'time' => 445856424, 'event' => 'status-trash', 'user' => 'sam' ); + $history[] = array( 'time' => 445856425, 'event' => 'status-spam', 'user' => 'sam' ); + $history[] = array( 'time' => 445856426, 'event' => 'status-hold', 'user' => 'sam' ); + $history[] = array( 'time' => 445856427, 'event' => 'status-approve', 'user' => 'sam' ); + $history[] = array( 'time' => 445856427, 'event' => 'webhook-spam' ); + $history[] = array( 'time' => 445856427, 'event' => 'webhook-ham' ); + $history[] = array( 'time' => 445856427, 'event' => 'webhook-spam-noaction' ); + $history[] = array( 'time' => 445856427, 'event' => 'webhook-ham-noaction' ); + */ + + usort( $history, array( 'Akismet', '_cmp_time' ) ); + return $history; + } + + /** + * Log an event for a given comment, storing it in comment_meta. + * + * @param int $comment_id The ID of the relevant comment. + * @param string $message The string description of the event. No longer used. + * @param string $event The event code. + * @param array $meta Metadata about the history entry. e.g., the user that reported or changed the status of a given comment. + */ + public static function update_comment_history( $comment_id, $message, $event=null, $meta=null ) { + global $current_user; + + $user = ''; + + $event = array( + 'time' => self::_get_microtime(), + 'event' => $event, + ); + + if ( is_object( $current_user ) && isset( $current_user->user_login ) ) { + $event['user'] = $current_user->user_login; + } + + if ( ! empty( $meta ) ) { + $event['meta'] = $meta; + } + + // $unique = false so as to allow multiple values per comment + $r = add_comment_meta( $comment_id, 'akismet_history', $event, false ); + } + + public static function check_db_comment( $id, $recheck_reason = 'recheck_queue' ) { + global $wpdb; + + if ( ! self::get_api_key() ) { + return new WP_Error( 'akismet-not-configured', __( 'Akismet is not configured. Please enter an API key.', 'akismet' ) ); + } + + $c = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $id ), ARRAY_A ); + + if ( ! $c ) { + return new WP_Error( 'invalid-comment-id', __( 'Comment not found.', 'akismet' ) ); + } + + $c['user_ip'] = $c['comment_author_IP']; + $c['user_agent'] = $c['comment_agent']; + $c['referrer'] = ''; + $c['blog'] = get_option( 'home' ); + $c['blog_lang'] = get_locale(); + $c['blog_charset'] = get_option('blog_charset'); + $c['permalink'] = get_permalink($c['comment_post_ID']); + $c['recheck_reason'] = $recheck_reason; + + $c['user_role'] = ''; + if ( ! empty( $c['user_ID'] ) ) { + $c['user_role'] = Akismet::get_user_roles( $c['user_ID'] ); + } + + if ( self::is_test_mode() ) + $c['is_test'] = 'true'; + + $c = apply_filters( 'akismet_request_args', $c, 'comment-check' ); + + $response = self::http_post( self::build_query( $c ), 'comment-check' ); + + if ( ! empty( $response[1] ) ) { + return $response[1]; + } + + return false; + } + + public static function recheck_comment( $id, $recheck_reason = 'recheck_queue' ) { + add_comment_meta( $id, 'akismet_rechecking', true ); + + $api_response = self::check_db_comment( $id, $recheck_reason ); + + if ( is_wp_error( $api_response ) ) { + // Invalid comment ID. + } + else if ( 'true' === $api_response ) { + wp_set_comment_status( $id, 'spam' ); + update_comment_meta( $id, 'akismet_result', 'true' ); + delete_comment_meta( $id, 'akismet_error' ); + delete_comment_meta( $id, 'akismet_delayed_moderation_email' ); + Akismet::update_comment_history( $id, '', 'recheck-spam' ); + } + elseif ( 'false' === $api_response ) { + update_comment_meta( $id, 'akismet_result', 'false' ); + delete_comment_meta( $id, 'akismet_error' ); + delete_comment_meta( $id, 'akismet_delayed_moderation_email' ); + Akismet::update_comment_history( $id, '', 'recheck-ham' ); + } + else { + // abnormal result: error + update_comment_meta( $id, 'akismet_result', 'error' ); + Akismet::update_comment_history( + $id, + '', + 'recheck-error', + array( 'response' => substr( $api_response, 0, 50 ) ) + ); + } + + delete_comment_meta( $id, 'akismet_rechecking' ); + + return $api_response; + } + + public static function transition_comment_status( $new_status, $old_status, $comment ) { + + if ( $new_status == $old_status ) + return; + + if ( 'spam' === $new_status || 'spam' === $old_status ) { + // Clear the cache of the "X comments in your spam queue" count on the dashboard. + wp_cache_delete( 'akismet_spam_count', 'widget' ); + } + + # we don't need to record a history item for deleted comments + if ( $new_status == 'delete' ) + return; + + if ( !current_user_can( 'edit_post', $comment->comment_post_ID ) && !current_user_can( 'moderate_comments' ) ) + return; + + if ( defined('WP_IMPORTING') && WP_IMPORTING == true ) + return; + + // if this is present, it means the status has been changed by a re-check, not an explicit user action + if ( get_comment_meta( $comment->comment_ID, 'akismet_rechecking' ) ) + return; + + if ( function_exists( 'getallheaders' ) ) { + $request_headers = getallheaders(); + + foreach ( $request_headers as $header => $value ) { + if ( strtolower( $header ) == 'x-akismet-webhook' ) { + // This change is due to a webhook request. + return; + } + } + } + + // Assumption alert: + // We want to submit comments to Akismet only when a moderator explicitly spams or approves it - not if the status + // is changed automatically by another plugin. Unfortunately WordPress doesn't provide an unambiguous way to + // determine why the transition_comment_status action was triggered. And there are several different ways by which + // to spam and unspam comments: bulk actions, ajax, links in moderation emails, the dashboard, and perhaps others. + // We'll assume that this is an explicit user action if certain POST/GET variables exist. + if ( + // status=spam: Marking as spam via the REST API or... + // status=unspam: I'm not sure. Maybe this used to be used instead of status=approved? Or the UI for removing from spam but not approving has been since removed?... + // status=approved: Unspamming via the REST API (Calypso) or... + ( isset( $_POST['status'] ) && in_array( $_POST['status'], array( 'spam', 'unspam', 'approved', ) ) ) + // spam=1: Clicking "Spam" underneath a comment in wp-admin and allowing the AJAX request to happen. + || ( isset( $_POST['spam'] ) && (int) $_POST['spam'] == 1 ) + // unspam=1: Clicking "Not Spam" underneath a comment in wp-admin and allowing the AJAX request to happen. Or, clicking "Undo" after marking something as spam. + || ( isset( $_POST['unspam'] ) && (int) $_POST['unspam'] == 1 ) + // comment_status=spam/unspam: It's unclear where this is happening. + || ( isset( $_POST['comment_status'] ) && in_array( $_POST['comment_status'], array( 'spam', 'unspam' ) ) ) + // action=spam: Choosing "Mark as Spam" from the Bulk Actions dropdown in wp-admin (or the "Spam it" link in notification emails). + // action=unspam: Choosing "Not Spam" from the Bulk Actions dropdown in wp-admin. + // action=spamcomment: Following the "Spam" link below a comment in wp-admin (not allowing AJAX request to happen). + // action=unspamcomment: Following the "Not Spam" link below a comment in wp-admin (not allowing AJAX request to happen). + || ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'spam', 'unspam', 'spamcomment', 'unspamcomment', ) ) ) + // action=editedcomment: Editing a comment via wp-admin (and possibly changing its status). + || ( isset( $_POST['action'] ) && in_array( $_POST['action'], array( 'editedcomment' ) ) ) + // for=jetpack: Moderation via the WordPress app, Calypso, anything powered by the Jetpack connection. + || ( isset( $_GET['for'] ) && ( 'jetpack' == $_GET['for'] ) && ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) ) + // Certain WordPress.com API requests + || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) + // WordPress.org REST API requests + || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) + ) { + if ( $new_status == 'spam' && ( $old_status == 'approved' || $old_status == 'unapproved' || !$old_status ) ) { + return self::submit_spam_comment( $comment->comment_ID ); + } elseif ( $old_status == 'spam' && ( $new_status == 'approved' || $new_status == 'unapproved' ) ) { + return self::submit_nonspam_comment( $comment->comment_ID ); + } + } + + self::update_comment_history( $comment->comment_ID, '', 'status-' . $new_status ); + } + + public static function submit_spam_comment( $comment_id ) { + global $wpdb, $current_user, $current_site; + + $comment_id = (int) $comment_id; + + $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ), ARRAY_A ); + + if ( ! $comment ) { + // it was deleted + return; + } + + if ( 'spam' != $comment['comment_approved'] ) { + return; + } + + self::update_comment_history( $comment_id, '', 'report-spam' ); + + // If the user hasn't configured Akismet, there's nothing else to do at this point. + if ( ! self::get_api_key() ) { + return; + } + + // use the original version stored in comment_meta if available + $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) ); + + if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) { + $comment = array_merge( $comment, $as_submitted ); + } + + $comment['blog'] = get_option( 'home' ); + $comment['blog_lang'] = get_locale(); + $comment['blog_charset'] = get_option( 'blog_charset' ); + $comment['permalink'] = get_permalink( $comment['comment_post_ID'] ); + + if ( is_object( $current_user ) ) { + $comment['reporter'] = $current_user->user_login; + } + + if ( is_object( $current_site ) ) { + $comment['site_domain'] = $current_site->domain; + } + + $comment['user_role'] = ''; + if ( ! empty( $comment['user_ID'] ) ) { + $comment['user_role'] = self::get_user_roles( $comment['user_ID'] ); + } + + if ( self::is_test_mode() ) { + $comment['is_test'] = 'true'; + } + + $post = get_post( $comment['comment_post_ID'] ); + + if ( ! is_null( $post ) ) { + $comment['comment_post_modified_gmt'] = $post->post_modified_gmt; + } + + $comment = apply_filters( 'akismet_request_args', $comment, 'submit-spam' ); + + $response = self::http_post( self::build_query( $comment ), 'submit-spam' ); + + update_comment_meta( $comment_id, 'akismet_user_result', 'true' ); + + if ( $comment['reporter'] ) { + update_comment_meta( $comment_id, 'akismet_user', $comment['reporter'] ); + } + + do_action('akismet_submit_spam_comment', $comment_id, $response[1]); + } + + public static function submit_nonspam_comment( $comment_id ) { + global $wpdb, $current_user, $current_site; + + $comment_id = (int) $comment_id; + + $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID = %d", $comment_id ), ARRAY_A ); + + if ( ! $comment ) { + // it was deleted + return; + } + + self::update_comment_history( $comment_id, '', 'report-ham' ); + + // If the user hasn't configured Akismet, there's nothing else to do at this point. + if ( ! self::get_api_key() ) { + return; + } + + // use the original version stored in comment_meta if available + $as_submitted = self::sanitize_comment_as_submitted( get_comment_meta( $comment_id, 'akismet_as_submitted', true ) ); + + if ( $as_submitted && is_array( $as_submitted ) && isset( $as_submitted['comment_content'] ) ) { + $comment = array_merge( $comment, $as_submitted ); + } + + $comment['blog'] = get_option( 'home' ); + $comment['blog_lang'] = get_locale(); + $comment['blog_charset'] = get_option( 'blog_charset' ); + $comment['permalink'] = get_permalink( $comment['comment_post_ID'] ); + $comment['user_role'] = ''; + + if ( is_object( $current_user ) ) { + $comment['reporter'] = $current_user->user_login; + } + + if ( is_object( $current_site ) ) { + $comment['site_domain'] = $current_site->domain; + } + + if ( ! empty( $comment['user_ID'] ) ) { + $comment['user_role'] = self::get_user_roles( $comment['user_ID'] ); + } + + if ( self::is_test_mode() ) { + $comment['is_test'] = 'true'; + } + + $post = get_post( $comment['comment_post_ID'] ); + + if ( ! is_null( $post ) ) { + $comment['comment_post_modified_gmt'] = $post->post_modified_gmt; + } + + $comment = apply_filters( 'akismet_request_args', $comment, 'submit-ham' ); + + $response = self::http_post( self::build_query( $comment ), 'submit-ham' ); + + update_comment_meta( $comment_id, 'akismet_user_result', 'false' ); + + if ( $comment['reporter'] ) { + update_comment_meta( $comment_id, 'akismet_user', $comment['reporter'] ); + } + + do_action('akismet_submit_nonspam_comment', $comment_id, $response[1]); + } + + public static function cron_recheck() { + global $wpdb; + + $api_key = self::get_api_key(); + + $status = self::verify_key( $api_key ); + if ( get_option( 'akismet_alert_code' ) || $status == 'invalid' ) { + // since there is currently a problem with the key, reschedule a check for 6 hours hence + wp_schedule_single_event( time() + 21600, 'akismet_schedule_cron_recheck' ); + do_action( 'akismet_scheduled_recheck', 'key-problem-' . get_option( 'akismet_alert_code' ) . '-' . $status ); + return false; + } + + delete_option('akismet_available_servers'); + + $comment_errors = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error' LIMIT 100" ); + + load_plugin_textdomain( 'akismet' ); + + foreach ( (array) $comment_errors as $comment_id ) { + // if the comment no longer exists, or is too old, remove the meta entry from the queue to avoid getting stuck + $comment = get_comment( $comment_id ); + + if ( + ! $comment // Comment has been deleted + || strtotime( $comment->comment_date_gmt ) < strtotime( "-15 days" ) // Comment is too old. + || $comment->comment_approved !== "0" // Comment is no longer in the Pending queue + ) { + delete_comment_meta( $comment_id, 'akismet_error' ); + delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' ); + continue; + } + + add_comment_meta( $comment_id, 'akismet_rechecking', true ); + $status = self::check_db_comment( $comment_id, 'retry' ); + + $event = ''; + if ( $status == 'true' ) { + $event = 'cron-retry-spam'; + } elseif ( $status == 'false' ) { + $event = 'cron-retry-ham'; + } + + // If we got back a legit response then update the comment history + // other wise just bail now and try again later. No point in + // re-trying all the comments once we hit one failure. + if ( !empty( $event ) ) { + delete_comment_meta( $comment_id, 'akismet_error' ); + self::update_comment_history( $comment_id, '', $event ); + update_comment_meta( $comment_id, 'akismet_result', $status ); + // make sure the comment status is still pending. if it isn't, that means the user has already moved it elsewhere. + $comment = get_comment( $comment_id ); + if ( $comment && 'unapproved' == wp_get_comment_status( $comment_id ) ) { + if ( $status == 'true' ) { + wp_spam_comment( $comment_id ); + } elseif ( $status == 'false' ) { + // comment is good, but it's still in the pending queue. depending on the moderation settings + // we may need to change it to approved. + if ( check_comment($comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent, $comment->comment_type) ) + wp_set_comment_status( $comment_id, 1 ); + else if ( get_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ) ) + wp_notify_moderator( $comment_id ); + } + } + + delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' ); + } else { + // If this comment has been pending moderation for longer than MAX_DELAY_BEFORE_MODERATION_EMAIL, + // send a moderation email now. + if ( ( intval( gmdate( 'U' ) ) - strtotime( $comment->comment_date_gmt ) ) < self::MAX_DELAY_BEFORE_MODERATION_EMAIL ) { + delete_comment_meta( $comment_id, 'akismet_delayed_moderation_email' ); + wp_notify_moderator( $comment_id ); + } + + delete_comment_meta( $comment_id, 'akismet_rechecking' ); + wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' ); + do_action( 'akismet_scheduled_recheck', 'check-db-comment-' . $status ); + return; + } + delete_comment_meta( $comment_id, 'akismet_rechecking' ); + } + + $remaining = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->commentmeta} WHERE meta_key = 'akismet_error'" ); + if ( $remaining && !wp_next_scheduled('akismet_schedule_cron_recheck') ) { + wp_schedule_single_event( time() + 1200, 'akismet_schedule_cron_recheck' ); + do_action( 'akismet_scheduled_recheck', 'remaining' ); + } + } + + public static function fix_scheduled_recheck() { + $future_check = wp_next_scheduled( 'akismet_schedule_cron_recheck' ); + if ( !$future_check ) { + return; + } + + if ( get_option( 'akismet_alert_code' ) > 0 ) { + return; + } + + $check_range = time() + 1200; + if ( $future_check > $check_range ) { + wp_clear_scheduled_hook( 'akismet_schedule_cron_recheck' ); + wp_schedule_single_event( time() + 300, 'akismet_schedule_cron_recheck' ); + do_action( 'akismet_scheduled_recheck', 'fix-scheduled-recheck' ); + } + } + + public static function add_comment_nonce( $post_id ) { + /** + * To disable the Akismet comment nonce, add a filter for the 'akismet_comment_nonce' tag + * and return any string value that is not 'true' or '' (empty string). + * + * Don't return boolean false, because that implies that the 'akismet_comment_nonce' option + * has not been set and that Akismet should just choose the default behavior for that + * situation. + */ + + if ( ! self::get_api_key() ) { + return; + } + + $akismet_comment_nonce_option = apply_filters( 'akismet_comment_nonce', get_option( 'akismet_comment_nonce' ) ); + + if ( $akismet_comment_nonce_option == 'true' || $akismet_comment_nonce_option == '' ) { + echo ''; + } + } + + public static function is_test_mode() { + return defined('AKISMET_TEST_MODE') && AKISMET_TEST_MODE; + } + + public static function allow_discard() { + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) + return false; + if ( is_user_logged_in() ) + return false; + + return ( get_option( 'akismet_strictness' ) === '1' ); + } + + public static function get_ip_address() { + return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null; + } + + /** + * Do these two comments, without checking the comment_ID, "match"? + * + * @param mixed $comment1 A comment object or array. + * @param mixed $comment2 A comment object or array. + * @return bool Whether the two comments should be treated as the same comment. + */ + private static function comments_match( $comment1, $comment2 ) { + $comment1 = (array) $comment1; + $comment2 = (array) $comment2; + + // Set default values for these strings that we check in order to simplify + // the checks and avoid PHP warnings. + if ( ! isset( $comment1['comment_author'] ) ) { + $comment1['comment_author'] = ''; + } + + if ( ! isset( $comment2['comment_author'] ) ) { + $comment2['comment_author'] = ''; + } + + if ( ! isset( $comment1['comment_author_email'] ) ) { + $comment1['comment_author_email'] = ''; + } + + if ( ! isset( $comment2['comment_author_email'] ) ) { + $comment2['comment_author_email'] = ''; + } + + $comments_match = ( + isset( $comment1['comment_post_ID'], $comment2['comment_post_ID'] ) + && intval( $comment1['comment_post_ID'] ) == intval( $comment2['comment_post_ID'] ) + && ( + // The comment author length max is 255 characters, limited by the TINYTEXT column type. + // If the comment author includes multibyte characters right around the 255-byte mark, they + // may be stripped when the author is saved in the DB, so a 300+ char author may turn into + // a 253-char author when it's saved, not 255 exactly. The longest possible character is + // theoretically 6 bytes, so we'll only look at the first 248 bytes to be safe. + substr( $comment1['comment_author'], 0, 248 ) == substr( $comment2['comment_author'], 0, 248 ) + || substr( stripslashes( $comment1['comment_author'] ), 0, 248 ) == substr( $comment2['comment_author'], 0, 248 ) + || substr( $comment1['comment_author'], 0, 248 ) == substr( stripslashes( $comment2['comment_author'] ), 0, 248 ) + // Certain long comment author names will be truncated to nothing, depending on their encoding. + || ( ! $comment1['comment_author'] && strlen( $comment2['comment_author'] ) > 248 ) + || ( ! $comment2['comment_author'] && strlen( $comment1['comment_author'] ) > 248 ) + ) + && ( + // The email max length is 100 characters, limited by the VARCHAR(100) column type. + // Same argument as above for only looking at the first 93 characters. + substr( $comment1['comment_author_email'], 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 ) + || substr( stripslashes( $comment1['comment_author_email'] ), 0, 93 ) == substr( $comment2['comment_author_email'], 0, 93 ) + || substr( $comment1['comment_author_email'], 0, 93 ) == substr( stripslashes( $comment2['comment_author_email'] ), 0, 93 ) + // Very long emails can be truncated and then stripped if the [0:100] substring isn't a valid address. + || ( ! $comment1['comment_author_email'] && strlen( $comment2['comment_author_email'] ) > 100 ) + || ( ! $comment2['comment_author_email'] && strlen( $comment1['comment_author_email'] ) > 100 ) + ) + ); + + return $comments_match; + } + + // Does the supplied comment match the details of the one most recently stored in self::$last_comment? + public static function matches_last_comment( $comment ) { + return self::comments_match( self::$last_comment, $comment ); + } + + private static function get_user_agent() { + return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null; + } + + private static function get_referer() { + return isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null; + } + + // return a comma-separated list of role names for the given user + public static function get_user_roles( $user_id ) { + $comment_user = null; + $roles = false; + + if ( !class_exists('WP_User') ) + return false; + + if ( $user_id > 0 ) { + $comment_user = new WP_User( $user_id ); + if ( isset( $comment_user->roles ) ) + $roles = implode( ',', $comment_user->roles ); + } + + if ( is_multisite() && is_super_admin( $user_id ) ) { + if ( empty( $roles ) ) { + $roles = 'super_admin'; + } else { + $comment_user->roles[] = 'super_admin'; + $roles = implode( ',', $comment_user->roles ); + } + } + + return $roles; + } + + // filter handler used to return a spam result to pre_comment_approved + public static function last_comment_status( $approved, $comment ) { + if ( is_null( self::$last_comment_result ) ) { + // We didn't have reason to store the result of the last check. + return $approved; + } + + // Only do this if it's the correct comment + if ( ! self::matches_last_comment( $comment ) ) { + self::log( "comment_is_spam mismatched comment, returning unaltered $approved" ); + return $approved; + } + + if ( 'trash' === $approved ) { + // If the last comment we checked has had its approval set to 'trash', + // then it failed the comment blacklist check. Let that blacklist override + // the spam check, since users have the (valid) expectation that when + // they fill out their blacklists, comments that match it will always + // end up in the trash. + return $approved; + } + + // bump the counter here instead of when the filter is added to reduce the possibility of overcounting + if ( $incr = apply_filters('akismet_spam_count_incr', 1) ) + update_option( 'akismet_spam_count', get_option('akismet_spam_count') + $incr ); + + return self::$last_comment_result; + } + + /** + * If Akismet is temporarily unreachable, we don't want to "spam" the blogger with + * moderation emails for comments that will be automatically cleared or spammed on + * the next retry. + * + * For comments that will be rechecked later, empty the list of email addresses that + * the moderation email would be sent to. + * + * @param array $emails An array of email addresses that the moderation email will be sent to. + * @param int $comment_id The ID of the relevant comment. + * @return array An array of email addresses that the moderation email will be sent to. + */ + public static function disable_moderation_emails_if_unreachable( $emails, $comment_id ) { + if ( ! empty( self::$prevent_moderation_email_for_these_comments ) && ! empty( $emails ) ) { + $comment = get_comment( $comment_id ); + + if ( $comment ) { + foreach ( self::$prevent_moderation_email_for_these_comments as $possible_match ) { + if ( self::comments_match( $possible_match, $comment ) ) { + update_comment_meta( $comment_id, 'akismet_delayed_moderation_email', true ); + return array(); + } + } + } + } + + return $emails; + } + + public static function _cmp_time( $a, $b ) { + return $a['time'] > $b['time'] ? -1 : 1; + } + + public static function _get_microtime() { + $mtime = explode( ' ', microtime() ); + return $mtime[1] + $mtime[0]; + } + + /** + * Make a POST request to the Akismet API. + * + * @param string $request The body of the request. + * @param string $path The path for the request. + * @param string $ip The specific IP address to hit. + * @return array A two-member array consisting of the headers and the response body, both empty in the case of a failure. + */ + public static function http_post( $request, $path, $ip=null ) { + + $akismet_ua = sprintf( 'WordPress/%s | Akismet/%s', $GLOBALS['wp_version'], constant( 'AKISMET_VERSION' ) ); + $akismet_ua = apply_filters( 'akismet_ua', $akismet_ua ); + + $host = self::API_HOST; + $api_key = self::get_api_key(); + + if ( $api_key ) { + $request = add_query_arg( 'api_key', $api_key, $request ); + } + + $http_host = $host; + // use a specific IP if provided + // needed by Akismet_Admin::check_server_connectivity() + if ( $ip && long2ip( ip2long( $ip ) ) ) { + $http_host = $ip; + } + + $http_args = array( + 'body' => $request, + 'headers' => array( + 'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ), + 'Host' => $host, + 'User-Agent' => $akismet_ua, + ), + 'httpversion' => '1.0', + 'timeout' => 15 + ); + + $akismet_url = $http_akismet_url = "http://{$http_host}/1.1/{$path}"; + + /** + * Try SSL first; if that fails, try without it and don't try it again for a while. + */ + + $ssl = $ssl_failed = false; + + // Check if SSL requests were disabled fewer than X hours ago. + $ssl_disabled = get_option( 'akismet_ssl_disabled' ); + + if ( $ssl_disabled && $ssl_disabled < ( time() - 60 * 60 * 24 ) ) { // 24 hours + $ssl_disabled = false; + delete_option( 'akismet_ssl_disabled' ); + } + else if ( $ssl_disabled ) { + do_action( 'akismet_ssl_disabled' ); + } + + if ( ! $ssl_disabled && ( $ssl = wp_http_supports( array( 'ssl' ) ) ) ) { + $akismet_url = set_url_scheme( $akismet_url, 'https' ); + + do_action( 'akismet_https_request_pre' ); + } + + $response = wp_remote_post( $akismet_url, $http_args ); + + Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) ); + + if ( $ssl && is_wp_error( $response ) ) { + do_action( 'akismet_https_request_failure', $response ); + + // Intermittent connection problems may cause the first HTTPS + // request to fail and subsequent HTTP requests to succeed randomly. + // Retry the HTTPS request once before disabling SSL for a time. + $response = wp_remote_post( $akismet_url, $http_args ); + + Akismet::log( compact( 'akismet_url', 'http_args', 'response' ) ); + + if ( is_wp_error( $response ) ) { + $ssl_failed = true; + + do_action( 'akismet_https_request_failure', $response ); + + do_action( 'akismet_http_request_pre' ); + + // Try the request again without SSL. + $response = wp_remote_post( $http_akismet_url, $http_args ); + + Akismet::log( compact( 'http_akismet_url', 'http_args', 'response' ) ); + } + } + + if ( is_wp_error( $response ) ) { + do_action( 'akismet_request_failure', $response ); + + return array( '', '' ); + } + + if ( $ssl_failed ) { + // The request failed when using SSL but succeeded without it. Disable SSL for future requests. + update_option( 'akismet_ssl_disabled', time() ); + + do_action( 'akismet_https_disabled' ); + } + + $simplified_response = array( $response['headers'], $response['body'] ); + + $alert_code_check_paths = array( + 'verify-key', + 'comment-check', + 'get-stats', + ); + + if ( in_array( $path, $alert_code_check_paths ) ) { + self::update_alert( $simplified_response ); + } + + return $simplified_response; + } + + // given a response from an API call like check_key_status(), update the alert code options if an alert is present. + public static function update_alert( $response ) { + $alert_option_prefix = 'akismet_alert_'; + $alert_header_prefix = 'x-akismet-alert-'; + $alert_header_names = array( + 'code', + 'msg', + 'api-calls', + 'usage-limit', + 'upgrade-plan', + 'upgrade-url', + 'upgrade-type', + 'upgrade-via-support', + ); + + foreach ( $alert_header_names as $alert_header_name ) { + $value = null; + if ( isset( $response[0][ $alert_header_prefix . $alert_header_name ] ) ) { + $value = $response[0][ $alert_header_prefix . $alert_header_name ]; + } + + $option_name = $alert_option_prefix . str_replace( '-', '_', $alert_header_name ); + if ( $value != get_option( $option_name ) ) { + if ( ! $value ) { + delete_option( $option_name ); + } else { + update_option( $option_name, $value ); + } + } + } + } + + /** + * Mark akismet-frontend.js as deferred. Because nothing depends on it, it can run at any time + * after it's loaded, and the browser won't have to wait for it to load to continue + * parsing the rest of the page. + */ + public static function set_form_js_async( $tag, $handle, $src ) { + if ( 'akismet-frontend' !== $handle ) { + return $tag; + } + + return preg_replace( '/^'; + } + + $fields .= ''; + + return $fields; + } + + public static function output_custom_form_fields( $post_id ) { + if ( 'fluentform/form_element_start' === current_filter() && did_action( 'fluentform_form_element_start' ) ) { + // Already did this via the legacy filter. + return; + } + + // phpcs:ignore WordPress.Security.EscapeOutput + echo self::get_akismet_form_fields(); + } + + public static function inject_custom_form_fields( $html ) { + $html = str_replace( '', self::get_akismet_form_fields() . '', $html ); + + return $html; + } + + public static function append_custom_form_fields( $html ) { + $html .= self::get_akismet_form_fields(); + + return $html; + } + + /** + * Ensure that any Akismet-added form fields are included in the comment-check call. + * + * @param array $form + * @param array $data Some plugins will supply the POST data via the filter, since they don't + * read it directly from $_POST. + * @return array $form + */ + public static function prepare_custom_form_values( $form, $data = null ) { + if ( 'fluentform/akismet_fields' === current_filter() && did_filter( 'fluentform_akismet_fields' ) ) { + // Already updated the form fields via the legacy filter. + return $form; + } + + if ( is_null( $data ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $data = $_POST; + } + + $prefix = 'ak_'; + + // Contact Form 7 uses _wpcf7 as a prefix to know which fields to exclude from comment_content. + if ( 'wpcf7_akismet_parameters' === current_filter() ) { + $prefix = '_wpcf7_ak_'; + } + + foreach ( $data as $key => $val ) { + if ( 0 === strpos( $key, $prefix ) ) { + $form[ 'POST_ak_' . substr( $key, strlen( $prefix ) ) ] = $val; + } + } + + return $form; + } + + private static function bail_on_activation( $message, $deactivate = true ) { +?> + + + + + + + + + + + $plugin ) { + if ( $plugin === $akismet ) { + $plugins[$i] = false; + $update = true; + } + } + + if ( $update ) { + update_option( 'active_plugins', array_filter( $plugins ) ); + } + } + exit; + } + + public static function view( $name, array $args = array() ) { + $args = apply_filters( 'akismet_view_arguments', $args, $name ); + + foreach ( $args as $key => $val ) { + $$key = $val; + } + + load_plugin_textdomain( 'akismet' ); + + $file = AKISMET__PLUGIN_DIR . 'views/'. $name . '.php'; + + include( $file ); + } + + /** + * Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook() + * @static + */ + public static function plugin_activation() { + if ( version_compare( $GLOBALS['wp_version'], AKISMET__MINIMUM_WP_VERSION, '<' ) ) { + load_plugin_textdomain( 'akismet' ); + + $message = ''.sprintf(esc_html__( 'Akismet %s requires WordPress %s or higher.' , 'akismet'), AKISMET_VERSION, AKISMET__MINIMUM_WP_VERSION ).' '.sprintf(__('Please upgrade WordPress to a current version, or downgrade to version 2.4 of the Akismet plugin.', 'akismet'), 'https://codex.wordpress.org/Upgrading_WordPress', 'https://wordpress.org/extend/plugins/akismet/download/'); + + Akismet::bail_on_activation( $message ); + } elseif ( ! empty( $_SERVER['SCRIPT_NAME'] ) && false !== strpos( $_SERVER['SCRIPT_NAME'], '/wp-admin/plugins.php' ) ) { + add_option( 'Activated_Akismet', true ); + } + } + + /** + * Removes all connection options + * @static + */ + public static function plugin_deactivation( ) { + self::deactivate_key( self::get_api_key() ); + + // Remove any scheduled cron jobs. + $akismet_cron_events = array( + 'akismet_schedule_cron_recheck', + 'akismet_scheduled_delete', + ); + + foreach ( $akismet_cron_events as $akismet_cron_event ) { + $timestamp = wp_next_scheduled( $akismet_cron_event ); + + if ( $timestamp ) { + wp_unschedule_event( $timestamp, $akismet_cron_event ); + } + } + } + + /** + * Essentially a copy of WP's build_query but one that doesn't expect pre-urlencoded values. + * + * @param array $args An array of key => value pairs + * @return string A string ready for use as a URL query string. + */ + public static function build_query( $args ) { + return _http_build_query( $args, '', '&' ); + } + + /** + * Log debugging info to the error log. + * + * Enabled when WP_DEBUG_LOG is enabled (and WP_DEBUG, since according to + * core, "WP_DEBUG_DISPLAY and WP_DEBUG_LOG perform no function unless + * WP_DEBUG is true), but can be disabled via the akismet_debug_log filter. + * + * @param mixed $akismet_debug The data to log. + */ + public static function log( $akismet_debug ) { + if ( apply_filters( 'akismet_debug_log', defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG && defined( 'AKISMET_DEBUG' ) && AKISMET_DEBUG ) ) { + error_log( print_r( compact( 'akismet_debug' ), true ) ); + } + } + + /** + * Check pingbacks for spam before they're saved to the DB. + * + * @param string $method The XML-RPC method that was called. + * @param array $args This and the $server arg are marked as optional since plugins might still be + * calling do_action( 'xmlrpc_action', [...] ) without the arguments that were added in WP 5.7. + * @param wp_xmlrpc_server $server + */ + public static function pre_check_pingback( $method, $args = array(), $server = null ) { + if ( $method !== 'pingback.ping' ) { + return; + } + + /* + * $args looks like this: + * + * Array + * ( + * [0] => http://www.example.net/?p=1 // Site that created the pingback. + * [1] => https://www.example.com/?p=2 // Post being pingback'd on this site. + * ) + */ + + if ( ! is_null( $server ) && ! empty( $args[1] ) ) { + $is_multicall = false; + $multicall_count = 0; + + if ( 'system.multicall' === $server->message->methodName ) { + $is_multicall = true; + $multicall_count = is_countable( $server->message->params ) ? count( $server->message->params ) : 0; + } + + $post_id = url_to_postid( $args[1] ); + + // If pingbacks aren't open on this post, we'll still check whether this request is part of a potential DDOS, + // but indicate to the server that pingbacks are indeed closed so we don't include this request in the user's stats, + // since the user has already done their part by disabling pingbacks. + $pingbacks_closed = false; + + $post = get_post( $post_id ); + + if ( ! $post || ! pings_open( $post ) ) { + $pingbacks_closed = true; + } + + $comment = array( + 'comment_author_url' => $args[0], + 'comment_post_ID' => $post_id, + 'comment_author' => '', + 'comment_author_email' => '', + 'comment_content' => '', + 'comment_type' => 'pingback', + 'akismet_pre_check' => '1', + 'comment_pingback_target' => $args[1], + 'pingbacks_closed' => $pingbacks_closed ? '1' : '0', + 'is_multicall' => $is_multicall, + 'multicall_count' => $multicall_count, + ); + + $comment = self::auto_check_comment( $comment, 'xml-rpc' ); + + if ( isset( $comment['akismet_result'] ) && 'true' == $comment['akismet_result'] ) { + // Sad: tightly coupled with the IXR classes. Unfortunately the action provides no context and no way to return anything. + $server->error( new IXR_Error( 0, 'Invalid discovery target' ) ); + + // Also note that if this was part of a multicall, a spam result will prevent the subsequent calls from being executed. + // This is probably fine, but it raises the bar for what should be acceptable as a false positive. + } + } + } + + /** + * Ensure that we are loading expected scalar values from akismet_as_submitted commentmeta. + * + * @param mixed $meta_value + * @return mixed + */ + private static function sanitize_comment_as_submitted( $meta_value ) { + if ( empty( $meta_value ) ) { + return $meta_value; + } + + $meta_value = (array) $meta_value; + + foreach ( $meta_value as $key => $value ) { + if ( ! is_scalar( $value ) ) { + unset( $meta_value[ $key ] ); + } else { + // These can change, so they're not explicitly listed in comment_as_submitted_allowed_keys. + if ( strpos( $key, 'POST_ak_' ) === 0 ) { + continue; + } + + if ( ! isset( self::$comment_as_submitted_allowed_keys[ $key ] ) ) { + unset( $meta_value[ $key ] ); + } + } + } + + return $meta_value; + } + + public static function predefined_api_key() { + if ( defined( 'WPCOM_API_KEY' ) ) { + return true; + } + + return apply_filters( 'akismet_predefined_api_key', false ); + } + + /** + * Controls the display of a privacy related notice underneath the comment form using the `akismet_comment_form_privacy_notice` option and filter respectively. + * Default is top not display the notice, leaving the choice to site admins, or integrators. + */ + public static function display_comment_form_privacy_notice() { + if ( 'display' !== apply_filters( 'akismet_comment_form_privacy_notice', get_option( 'akismet_comment_form_privacy_notice', 'hide' ) ) ) { + return; + } + echo apply_filters( + 'akismet_comment_form_privacy_notice_markup', + '' + ); + } + + public static function load_form_js() { + if ( + ! is_admin() + && ( ! function_exists( 'amp_is_request' ) || ! amp_is_request() ) + && self::get_api_key() + ) { + wp_register_script( 'akismet-frontend', plugin_dir_url( __FILE__ ) . '_inc/akismet-frontend.js', array(), filemtime( plugin_dir_path( __FILE__ ) . '_inc/akismet-frontend.js' ), true ); + wp_enqueue_script( 'akismet-frontend' ); + } + } + + /** + * Add the form JavaScript when we detect that a supported form shortcode is being parsed. + */ + public static function load_form_js_via_filter( $return_value, $tag, $attr, $m ) { + if ( in_array( $tag, array( 'contact-form', 'gravityform', 'contact-form-7', 'formidable', 'fluentform' ) ) ) { + self::load_form_js(); + } + + return $return_value; + } + + /** + * Was the last entry in the comment history created by Akismet? + * + * @param int $comment_id The ID of the comment. + * @return bool + */ + public static function last_comment_status_change_came_from_akismet( $comment_id ) { + $history = self::get_comment_history( $comment_id ); + + if ( empty( $history ) ) { + return false; + } + + $most_recent_history_event = $history[0]; + + if ( ! isset( $most_recent_history_event['event'] ) ) { + return false; + } + + $akismet_history_events = array( + 'check-error', + 'cron-retry-ham', + 'cron-retry-spam', + 'check-ham', + 'check-spam', + 'recheck-error', + 'recheck-ham', + 'recheck-spam', + 'webhook-ham', + 'webhook-spam', + ); + + if ( in_array( $most_recent_history_event['event'], $akismet_history_events ) ) { + return true; + } + + return false; + } +} diff --git a/plugins/akismet/index.php b/plugins/akismet/index.php new file mode 100644 index 0000000..cf879a5 --- /dev/null +++ b/plugins/akismet/index.php @@ -0,0 +1,2 @@ + + + ++ + + + +
+ +
%s %s
', + __( 'Quote from Hello Dolly song, by Jerry Herman:' ), + $lang, + $chosen + ); +} + +// Now we set that function up to execute when the admin_notices action is called. +add_action( 'admin_notices', 'hello_dolly' ); + +// We need some CSS to position the paragraph. +function dolly_css() { + echo " + + "; +} + +add_action( 'admin_head', 'dolly_css' ); diff --git a/plugins/index.php b/plugins/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/plugins/index.php @@ -0,0 +1,2 @@ + .wp-block-button__link:not(.has-text-color, .has-background):hover { + background-color: var(--wp--preset--color--contrast-2, var(--wp--preset--color--contrast, transparent)); + color: var(--wp--preset--color--base); + border-color: var(--wp--preset--color--contrast-2, var(--wp--preset--color--contrast, currentColor)); +} diff --git a/themes/twentytwentyfour/assets/fonts/cardo/LICENSE.txt b/themes/twentytwentyfour/assets/fonts/cardo/LICENSE.txt new file mode 100644 index 0000000..ed707cd --- /dev/null +++ b/themes/twentytwentyfour/assets/fonts/cardo/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright (c) 2002-2011, David J. Perry (hospes02@scholarsfonts.net) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/themes/twentytwentyfour/assets/fonts/cardo/cardo_italic_400.woff2 b/themes/twentytwentyfour/assets/fonts/cardo/cardo_italic_400.woff2 new file mode 100644 index 0000000..46905a8 Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/cardo/cardo_italic_400.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/cardo/cardo_normal_400.woff2 b/themes/twentytwentyfour/assets/fonts/cardo/cardo_normal_400.woff2 new file mode 100644 index 0000000..536d7a5 Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/cardo/cardo_normal_400.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/cardo/cardo_normal_700.woff2 b/themes/twentytwentyfour/assets/fonts/cardo/cardo_normal_700.woff2 new file mode 100644 index 0000000..2d280ef Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/cardo/cardo_normal_700.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/instrument-sans/InstrumentSans-Italic-VariableFont_wdth,wght.woff2 b/themes/twentytwentyfour/assets/fonts/instrument-sans/InstrumentSans-Italic-VariableFont_wdth,wght.woff2 new file mode 100644 index 0000000..f5f2861 Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/instrument-sans/InstrumentSans-Italic-VariableFont_wdth,wght.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/instrument-sans/InstrumentSans-VariableFont_wdth,wght.woff2 b/themes/twentytwentyfour/assets/fonts/instrument-sans/InstrumentSans-VariableFont_wdth,wght.woff2 new file mode 100644 index 0000000..02203d6 Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/instrument-sans/InstrumentSans-VariableFont_wdth,wght.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/instrument-sans/OFL.txt b/themes/twentytwentyfour/assets/fonts/instrument-sans/OFL.txt new file mode 100644 index 0000000..56e04aa --- /dev/null +++ b/themes/twentytwentyfour/assets/fonts/instrument-sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/themes/twentytwentyfour/assets/fonts/inter/Inter-VariableFont_slnt,wght.woff2 b/themes/twentytwentyfour/assets/fonts/inter/Inter-VariableFont_slnt,wght.woff2 new file mode 100644 index 0000000..350bbbc Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/inter/Inter-VariableFont_slnt,wght.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/inter/LICENSE.txt b/themes/twentytwentyfour/assets/fonts/inter/LICENSE.txt new file mode 100644 index 0000000..b525cbf --- /dev/null +++ b/themes/twentytwentyfour/assets/fonts/inter/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/themes/twentytwentyfour/assets/fonts/jost/Jost-Italic-VariableFont_wght.woff2 b/themes/twentytwentyfour/assets/fonts/jost/Jost-Italic-VariableFont_wght.woff2 new file mode 100644 index 0000000..184c920 Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/jost/Jost-Italic-VariableFont_wght.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/jost/Jost-VariableFont_wght.woff2 b/themes/twentytwentyfour/assets/fonts/jost/Jost-VariableFont_wght.woff2 new file mode 100644 index 0000000..eedcc3f Binary files /dev/null and b/themes/twentytwentyfour/assets/fonts/jost/Jost-VariableFont_wght.woff2 differ diff --git a/themes/twentytwentyfour/assets/fonts/jost/OFL.txt b/themes/twentytwentyfour/assets/fonts/jost/OFL.txt new file mode 100644 index 0000000..85b6b99 --- /dev/null +++ b/themes/twentytwentyfour/assets/fonts/jost/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Jost Project Authors (https://github.com/indestructible-type) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/themes/twentytwentyfour/assets/images/abstract-geometric-art.webp b/themes/twentytwentyfour/assets/images/abstract-geometric-art.webp new file mode 100644 index 0000000..4b7fd1a Binary files /dev/null and b/themes/twentytwentyfour/assets/images/abstract-geometric-art.webp differ diff --git a/themes/twentytwentyfour/assets/images/angular-roof.webp b/themes/twentytwentyfour/assets/images/angular-roof.webp new file mode 100644 index 0000000..c9b8d7a Binary files /dev/null and b/themes/twentytwentyfour/assets/images/angular-roof.webp differ diff --git a/themes/twentytwentyfour/assets/images/art-gallery.webp b/themes/twentytwentyfour/assets/images/art-gallery.webp new file mode 100644 index 0000000..34b458b Binary files /dev/null and b/themes/twentytwentyfour/assets/images/art-gallery.webp differ diff --git a/themes/twentytwentyfour/assets/images/building-exterior.webp b/themes/twentytwentyfour/assets/images/building-exterior.webp new file mode 100644 index 0000000..936ecf1 Binary files /dev/null and b/themes/twentytwentyfour/assets/images/building-exterior.webp differ diff --git a/themes/twentytwentyfour/assets/images/green-staircase.webp b/themes/twentytwentyfour/assets/images/green-staircase.webp new file mode 100644 index 0000000..1da8272 Binary files /dev/null and b/themes/twentytwentyfour/assets/images/green-staircase.webp differ diff --git a/themes/twentytwentyfour/assets/images/hotel-facade.webp b/themes/twentytwentyfour/assets/images/hotel-facade.webp new file mode 100644 index 0000000..a076a58 Binary files /dev/null and b/themes/twentytwentyfour/assets/images/hotel-facade.webp differ diff --git a/themes/twentytwentyfour/assets/images/icon-message.webp b/themes/twentytwentyfour/assets/images/icon-message.webp new file mode 100644 index 0000000..f6e5e24 Binary files /dev/null and b/themes/twentytwentyfour/assets/images/icon-message.webp differ diff --git a/themes/twentytwentyfour/assets/images/museum.webp b/themes/twentytwentyfour/assets/images/museum.webp new file mode 100644 index 0000000..df42352 Binary files /dev/null and b/themes/twentytwentyfour/assets/images/museum.webp differ diff --git a/themes/twentytwentyfour/assets/images/tourist-and-building.webp b/themes/twentytwentyfour/assets/images/tourist-and-building.webp new file mode 100644 index 0000000..285e37c Binary files /dev/null and b/themes/twentytwentyfour/assets/images/tourist-and-building.webp differ diff --git a/themes/twentytwentyfour/assets/images/windows.webp b/themes/twentytwentyfour/assets/images/windows.webp new file mode 100644 index 0000000..39a4e41 Binary files /dev/null and b/themes/twentytwentyfour/assets/images/windows.webp differ diff --git a/themes/twentytwentyfour/functions.php b/themes/twentytwentyfour/functions.php new file mode 100644 index 0000000..8536cb8 --- /dev/null +++ b/themes/twentytwentyfour/functions.php @@ -0,0 +1,206 @@ + 'arrow-icon-details', + 'label' => __( 'Arrow icon', 'twentytwentyfour' ), + /* + * Styles for the custom Arrow icon style of the Details block + */ + 'inline_style' => ' + .is-style-arrow-icon-details { + padding-top: var(--wp--preset--spacing--10); + padding-bottom: var(--wp--preset--spacing--10); + } + + .is-style-arrow-icon-details summary { + list-style-type: "\2193\00a0\00a0\00a0"; + } + + .is-style-arrow-icon-details[open]>summary { + list-style-type: "\2192\00a0\00a0\00a0"; + }', + ) + ); + register_block_style( + 'core/post-terms', + array( + 'name' => 'pill', + 'label' => __( 'Pill', 'twentytwentyfour' ), + /* + * Styles variation for post terms + * https://github.com/WordPress/gutenberg/issues/24956 + */ + 'inline_style' => ' + .is-style-pill a, + .is-style-pill span:not([class], [data-rich-text-placeholder]) { + display: inline-block; + background-color: var(--wp--preset--color--base-2); + padding: 0.375rem 0.875rem; + border-radius: var(--wp--preset--spacing--20); + } + + .is-style-pill a:hover { + background-color: var(--wp--preset--color--contrast-3); + }', + ) + ); + register_block_style( + 'core/list', + array( + 'name' => 'checkmark-list', + 'label' => __( 'Checkmark', 'twentytwentyfour' ), + /* + * Styles for the custom checkmark list block style + * https://github.com/WordPress/gutenberg/issues/51480 + */ + 'inline_style' => ' + ul.is-style-checkmark-list { + list-style-type: "\2713"; + } + + ul.is-style-checkmark-list li { + padding-inline-start: 1ch; + }', + ) + ); + register_block_style( + 'core/navigation-link', + array( + 'name' => 'arrow-link', + 'label' => __( 'With arrow', 'twentytwentyfour' ), + /* + * Styles for the custom arrow nav link block style + */ + 'inline_style' => ' + .is-style-arrow-link .wp-block-navigation-item__label:after { + content: "\2197"; + padding-inline-start: 0.25rem; + vertical-align: middle; + text-decoration: none; + display: inline-block; + }', + ) + ); + register_block_style( + 'core/heading', + array( + 'name' => 'asterisk', + 'label' => __( 'With asterisk', 'twentytwentyfour' ), + 'inline_style' => " + .is-style-asterisk:before { + content: ''; + width: 1.5rem; + height: 3rem; + background: var(--wp--preset--color--contrast-2, currentColor); + clip-path: path('M11.93.684v8.039l5.633-5.633 1.216 1.23-5.66 5.66h8.04v1.737H13.2l5.701 5.701-1.23 1.23-5.742-5.742V21h-1.737v-8.094l-5.77 5.77-1.23-1.217 5.743-5.742H.842V9.98h8.162l-5.701-5.7 1.23-1.231 5.66 5.66V.684h1.737Z'); + display: block; + } + + /* Hide the asterisk if the heading has no content, to avoid using empty headings to display the asterisk only, which is an A11Y issue */ + .is-style-asterisk:empty:before { + content: none; + } + + .is-style-asterisk:-moz-only-whitespace:before { + content: none; + } + + .is-style-asterisk.has-text-align-center:before { + margin: 0 auto; + } + + .is-style-asterisk.has-text-align-right:before { + margin-left: auto; + } + + .rtl .is-style-asterisk.has-text-align-left:before { + margin-right: auto; + }", + ) + ); + } +endif; + +add_action( 'init', 'twentytwentyfour_block_styles' ); + +/** + * Enqueue block stylesheets. + */ + +if ( ! function_exists( 'twentytwentyfour_block_stylesheets' ) ) : + /** + * Enqueue custom block stylesheets + * + * @since Twenty Twenty-Four 1.0 + * @return void + */ + function twentytwentyfour_block_stylesheets() { + /** + * The wp_enqueue_block_style() function allows us to enqueue a stylesheet + * for a specific block. These will only get loaded when the block is rendered + * (both in the editor and on the front end), improving performance + * and reducing the amount of data requested by visitors. + * + * See https://make.wordpress.org/core/2021/12/15/using-multiple-stylesheets-per-block/ for more info. + */ + wp_enqueue_block_style( + 'core/button', + array( + 'handle' => 'twentytwentyfour-button-style-outline', + 'src' => get_parent_theme_file_uri( 'assets/css/button-outline.css' ), + 'ver' => wp_get_theme( get_template() )->get( 'Version' ), + 'path' => get_parent_theme_file_path( 'assets/css/button-outline.css' ), + ) + ); + } +endif; + +add_action( 'init', 'twentytwentyfour_block_stylesheets' ); + +/** + * Register pattern categories. + */ + +if ( ! function_exists( 'twentytwentyfour_pattern_categories' ) ) : + /** + * Register pattern categories + * + * @since Twenty Twenty-Four 1.0 + * @return void + */ + function twentytwentyfour_pattern_categories() { + + register_block_pattern_category( + 'twentytwentyfour_page', + array( + 'label' => _x( 'Pages', 'Block pattern category', 'twentytwentyfour' ), + 'description' => __( 'A collection of full page layouts.', 'twentytwentyfour' ), + ) + ); + } +endif; + +add_action( 'init', 'twentytwentyfour_pattern_categories' ); diff --git a/themes/twentytwentyfour/parts/footer.html b/themes/twentytwentyfour/parts/footer.html new file mode 100644 index 0000000..c21388e --- /dev/null +++ b/themes/twentytwentyfour/parts/footer.html @@ -0,0 +1 @@ + diff --git a/themes/twentytwentyfour/parts/header.html b/themes/twentytwentyfour/parts/header.html new file mode 100644 index 0000000..2fa483b --- /dev/null +++ b/themes/twentytwentyfour/parts/header.html @@ -0,0 +1,26 @@ + +
+
+
+ Études Articles per month.', 'Feature for pricing level', 'twentytwentyfour' ) ); ?>
+ + + +
+
+
+ Études app for iOS and Android.', 'Feature for pricing level', 'twentytwentyfour' ) ); ?>
+
Études Articles per month.', 'Feature for pricing level', 'twentytwentyfour' ) ); ?>
+ + + +Études app for iOS and Android.', 'Feature for pricing level', 'twentytwentyfour' ) ); ?>
+ +Études Articles.', 'Feature for pricing level', 'twentytwentyfour' ) ); ?>
+ + + +Études app for iOS and Android', 'Feature for pricing level', 'twentytwentyfour' ) ); ?>
+ +
+
+ + WordPress'; + echo sprintf( + /* Translators: Designed with WordPress */ + esc_html__( 'Designed with %1$s', 'twentytwentyfour' ), + $wordpress_link + ); + ?> +
+ +
+ + WordPress'; + echo sprintf( + /* Translators: Designed with WordPress */ + esc_html__( 'Designed with %1$s', 'twentytwentyfour' ), + $wordpress_link + ); + ?> +
+ ++ WordPress'; + echo sprintf( + /* Translators: Designed with WordPress */ + esc_html__( 'Designed with %1$s', 'twentytwentyfour' ), + $wordpress_link + ); + ?> +
+ +
+
+ —
+ + + + + + + + + + +
+
+ + +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + + + +
+
+ Études is not confined to the past—we are passionate about the cutting edge designs shaping our world today.', 'twentytwentyfour' ) ); ?>
+ + + + + +
+ +
+ + + + + ++ WordPress' + ) + ?> +
+ ++ +
+ + + diff --git a/themes/twentytwentythree/patterns/post-meta.php b/themes/twentytwentythree/patterns/post-meta.php new file mode 100644 index 0000000..a1a9226 --- /dev/null +++ b/themes/twentytwentythree/patterns/post-meta.php @@ -0,0 +1,77 @@ + + + + + + ++ +
+ + + + + ++ +
+ + + ++ +
+ + + ++ +
+ + + +' . esc_html__( 'About us', 'twentytwentytwo' ) . '
+ + + +' . esc_html__( 'We are a rogue collective of bird watchers. We’ve been known to sneak through fences, climb perimeter walls, and generally trespass in order to observe the rarest of birds.', 'twentytwentytwo' ) . '
+ + + + + + +' . esc_html__( 'About us', 'twentytwentytwo' ) . '
+ + + +' . esc_html__( 'We are a rogue collective of bird watchers. We’ve been known to sneak through fences, climb perimeter walls, and generally trespass in order to observe the rarest of birds.', 'twentytwentytwo' ) . '
+' . esc_html__( 'Latest posts', 'twentytwentytwo' ) . '
+ + +' . esc_html__( 'Categories', 'twentytwentytwo' ) . '
+ + +' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . esc_html__( '© Site Title', 'twentytwentytwo' ) . '
+' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . + sprintf( + /* Translators: WordPress link. */ + esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), + 'WordPress' + ) . '
+' . esc_html__( '© Site Title', 'twentytwentytwo' ) . '
+ ) . '/assets/images/divider-white.png)
 ) . '/assets/images/divider-black.png)
' . esc_html__( 'Latest posts', 'twentytwentytwo' ) . '
+ + + + ) . '/assets/images/bird-on-gray.jpg)
' . esc_html__( 'Hummingbird', 'twentytwentytwo' ) . '
+ + + +' . esc_html__( 'A beautiful bird featuring a surprising set of color feathers.', 'twentytwentytwo' ) . '
+ ) . '/assets/images/icon-binoculars.png)
' . esc_html__( 'Jesús Rodriguez, Doug Stilton, Emery Driscoll, Megan Perry, Rowan Price, Angelo Tso, Edward Stilton, Amy Jensen, Boston Bell, Shay Ford, Lee Cunningham, Evelynn Ray, Landen Reese, Ewan Hart, Jenna Chan, Phoenix Murray, Mel Saunders, Aldo Davidson, Zain Hall.', 'twentytwentytwo' ) . '
+ + + + + + + + + ) . '/assets/images/ducks.jpg)
 ) . '/assets/images/flight-path-on-salmon.jpg)
' . esc_html__( 'May 14th, 2022, 6 PM', 'twentytwentytwo' ) . '
+' . wp_kses_post( __( 'The Vintagé Theater
245 Arden Rd.
Gardenville, NH', 'twentytwentytwo' ) ) . '
' . esc_html__( 'May 16th, 2022, 6 PM', 'twentytwentytwo' ) . '
+' . wp_kses_post( __( 'The Swell Theater
120 River Rd.
Rainfall, NH', 'twentytwentytwo' ) ) . '
' . esc_html__( 'May 18th, 2022, 7 PM', 'twentytwentytwo' ) . '
+' . wp_kses_post( __( 'The Vintagé Theater
245 Arden Rd.
Gardenville, NH', 'twentytwentytwo' ) ) . '
' . esc_html__( 'May 20th, 2022, 6 PM', 'twentytwentytwo' ) . '
+' . wp_kses_post( __( 'The Swell Theater
120 River Rd.
Rainfall, NH', 'twentytwentytwo' ) ) . '
' . esc_html__( 'Help support our growing community by joining at the Pigeon level. Your support will help pay our writers, and you’ll get access to our exclusive newsletter.', 'twentytwentytwo' ) . '
+ + + + + + + + +' . esc_html__( 'Join at the Sparrow level and become a member of our flock! You’ll receive our newsletter, plus a bird pin that you can wear with pride when you’re out in nature.', 'twentytwentytwo' ) . '
+ + + + + + + + +' . esc_html__( 'Play a leading role for our community by joining at the Falcon level. This level earns you a seat on our board, where you can help plan future birdwatching expeditions.', 'twentytwentytwo' ) . '
+ + + + + + + + + ) . '/assets/images/bird-on-salmon.jpg)
 ) . '/assets/images/bird-on-green.jpg)
' . wp_kses_post( __( 'May 14th, 2022 @ 7:00PM
The Vintagé Theater,
245 Arden Rd.
Gardenville, NH', 'twentytwentytwo' ) ) . '
' . esc_html__( 'Featuring', 'twentytwentytwo' ) . '
+' . wp_kses_post( __( 'Jesús Rodriguez
Doug Stilton
Emery Driscoll
Megan Perry
Rowan Price', 'twentytwentytwo' ) ) . '
' . wp_kses_post( __( 'Angelo Tso
Edward Stilton
Amy Jensen
Boston Bell
Shay Ford', 'twentytwentytwo' ) ) . '
' . esc_html__( 'A film about hobbyist bird watchers, a catalog of different birds, paired with the noises they make. Each bird is listed by their scientific name so things seem more official.', 'twentytwentytwo' ) . '
+ ) . '/assets/images/flight-path-on-gray-a.jpg)
' . esc_html__( 'A film about hobbyist bird watchers, a catalog of different birds, paired with the noises they make. Each bird is listed by their scientific name so things seem more official.', 'twentytwentytwo' ) . '
+ + + + + + + + + ) . '/assets/images/ducks.jpg)
 ) . '/assets/images/flight-path-on-gray-c.jpg)
 ) . '/assets/images/flight-path-on-transparent-c.png)
 ) . '/assets/images/flight-path-on-transparent-d.png)
' . esc_html__( 'This page could not be found. Maybe try a search?', 'twentytwentytwo' ) . '
+ + ', +); diff --git a/themes/twentytwentytwo/inc/patterns/hidden-bird.php b/themes/twentytwentytwo/inc/patterns/hidden-bird.php new file mode 100644 index 0000000..cfc9db5 --- /dev/null +++ b/themes/twentytwentytwo/inc/patterns/hidden-bird.php @@ -0,0 +1,14 @@ + __( 'Heading and bird image', 'twentytwentytwo' ), + 'inserter' => false, + 'content' => ' + ) . '/assets/images/flight-path-on-transparent-d.png)
 ) . '/assets/images/flight-path-on-transparent-c.png)
 ) . '/assets/images/flight-path-on-gray-b.jpg)
 ) . '/assets/images/icon-bird.jpg)
 ) . '/assets/images/icon-bird.jpg)
' . esc_html__( 'A podcast about birds', 'twentytwentytwo' ) . '
+ + + + + + + + + ) . '/assets/images/bird-on-salmon.jpg)
' . esc_html__( 'Oh hello. My name’s Doug, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show on Tuesday evenings at 11PM EDT.', 'twentytwentytwo' ) . '
+ + + + + + + + + ) . '/assets/images/bird-on-black.jpg)
' . esc_html__( 'Oh hello. My name’s Emery, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show on Tuesday evenings at 11PM EDT.', 'twentytwentytwo' ) . '
+ + + + + + + + +' . esc_html__( 'Oh hello. My name’s Jesús, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show on Tuesday evenings at 11PM EDT.', 'twentytwentytwo' ) . '
+ + + + + + + + +' . esc_html__( 'Oh hello. My name’s Edvard, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show every Tuesday evening at 11PM EDT. Listen in sometime!', 'twentytwentytwo' ) . '
+ + + + + + + + + + + ) . '/assets/images/flight-path-on-transparent-b.png)
' . wp_kses_post( __( 'Oh hello. My name’s Angelo, and I operate this blog. I was born in Portland, but I currently live in upstate New York. You may recognize me from publications with names like Eagle Beagle and Mourning Dive. I write for a living.
I usually use this blog to catalog extensive lists of birds and other things that I find interesting. If you find an error with one of my lists, please keep it to yourself.
If that’s not your cup of tea, I definitely recommend this tea. It’s my favorite.', 'twentytwentytwo' ) ) . '
' . wp_kses_post( __( 'May 14th, 2022 @ 7:00PM
The Vintagé Theater,
245 Arden Rd.
Gardenville, NH', 'twentytwentytwo' ) ) . '
 ) . '/assets/images/flight-path-on-transparent-a.png)
' . esc_html__( 'Oh hello. My name’s Angelo, and you’ve found your way to my blog. I write about a range of topics, but lately I’ve been sharing my hopes for next year.', 'twentytwentytwo' ) . '
+' . esc_html__( 'WELCOME', 'twentytwentytwo' ) . '
+' . wp_kses_post( __( 'Oh hello. My name’s Angelo, and I operate this blog. I was born in Portland, but I currently live in upstate New York. You may recognize me from publications with names like Eagle Beagle and Mourning Dive. I write for a living.
I usually use this blog to catalog extensive lists of birds and other things that I find interesting. If you find an error with one of my lists, please keep it to yourself.
If that’s not your cup of tea, I definitely recommend this tea. It’s my favorite.', 'twentytwentytwo' ) ) . '
' . esc_html__( 'POSTS', 'twentytwentytwo' ) . '
+ ) . '/assets/images/flight-path-on-salmon.jpg)
 ) . '/assets/images/bird-on-salmon.jpg)
 ) . '/assets/images/icon-binoculars.png)
' . esc_html__( 'February, 12 2021', 'twentytwentytwo' ) . '
+ + + + + + + +' . wp_kses_post( __( 'The Grand Theater
154 Eastern Avenue
Maryland NY, 12345', 'twentytwentytwo' ) ) . '