$atts['to'], 'subject' => $atts['subject'], 'message' => $atts['message'], 'headers' => isset( $atts['headers'] ) ? $atts['headers'] : '', 'attachments' => $files, 'attempts' => 0, 'created' => time(), ), '', false ); if ( ! $saved ) { self::remove_files( $files ); return $short; } wp_schedule_single_event( time(), self::HOOK, array( $id ) ); // A safety net: two requests writing WP-Cron's list at once can lose an event. if ( ! wp_next_scheduled( self::SWEEP ) ) { wp_schedule_single_event( time() + 2 * MINUTE_IN_SECONDS, self::SWEEP ); } self::$queued = true; return true; } /** * For a form sent with JavaScript: the address the visitor's browser * calls once it shows the answer, so the emails leave at once without * the visitor waiting for them. '' when nothing waits or WP-Cron is off. */ public static function browser_kick() { if ( ! self::$queued || ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) { return ''; } self::$queued = false; // The browser takes care of it. return site_url( 'wp-cron.php' ); } /** Otherwise WP-Cron is started at the end of the request. */ public static function kick() { if ( self::$queued && function_exists( 'spawn_cron' ) && ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) { spawn_cron(); } } /** WP-Cron: sends one email that was put aside. */ public static function send( $id ) { $id = preg_replace( '/[^a-z0-9]/', '', (string) $id ); $job = get_option( self::PREFIX . $id ); if ( ! is_array( $job ) ) { return; } // add_option() fails when the row exists: only one run gets past here. $lock = self::PREFIX . 'lock_' . $id; if ( ! add_option( $lock, time(), '', false ) ) { if ( (int) get_option( $lock ) > time() - 10 * MINUTE_IN_SECONDS ) { return; } update_option( $lock, time(), false ); // A run that died half-way: take over. } self::$deferring = false; $sent = wp_mail( $job['to'], $job['subject'], $job['message'], $job['headers'], $job['attachments'] ); ++$job['attempts']; if ( $sent || $job['attempts'] >= self::ATTEMPTS ) { delete_option( self::PREFIX . $id ); delete_option( $lock ); self::remove_files( $job['attachments'] ); if ( ! $sent ) { $failed = get_option( self::FAILED, array() ); $failed = is_array( $failed ) ? $failed : array(); update_option( self::FAILED, array( 'count' => ( isset( $failed['count'] ) ? (int) $failed['count'] : 0 ) + 1, 'time' => time(), 'subject' => (string) $job['subject'], 'error' => Mammoth_Mail::last_error(), ), false ); } return; } update_option( self::PREFIX . $id, $job, false ); delete_option( $lock ); wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS * $job['attempts'], self::HOOK, array( $id ) ); } /** Sends what waits for more than a minute with no sending planned. */ public static function sweep() { global $wpdb; $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name NOT LIKE %s ORDER BY option_id ASC LIMIT 50", $wpdb->esc_like( self::PREFIX ) . '%', $wpdb->esc_like( self::PREFIX . 'lock_' ) . '%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the queue itself. $left = false; foreach ( (array) $names as $name ) { $id = substr( $name, strlen( self::PREFIX ) ); $job = get_option( $name ); if ( ! is_array( $job ) || wp_next_scheduled( self::HOOK, array( $id ) ) ) { $left = $left || is_array( $job ); continue; } if ( (int) $job['created'] > time() - MINUTE_IN_SECONDS ) { $left = true; continue; } self::send( $id ); } if ( $left && ! wp_next_scheduled( self::SWEEP ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, self::SWEEP ); } } /** Emails waiting right now. */ public static function waiting() { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name NOT LIKE %s", $wpdb->esc_like( self::PREFIX ) . '%', $wpdb->esc_like( self::PREFIX . 'lock_' ) . '%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- a count for the settings screen. } public static function failed() { $failed = get_option( self::FAILED, array() ); return is_array( $failed ) ? $failed : array(); } public static function clear_failed() { delete_option( self::FAILED ); } /* ---------------------------------------------------------------- files */ private static function folder( $id ) { $upload = wp_upload_dir( null, false ); $base = trailingslashit( $upload['basedir'] ) . self::DIR; if ( ! is_dir( $base ) && ! wp_mkdir_p( $base ) ) { return ''; } // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- tiny guard files in our own folder. if ( ! file_exists( $base . '/.htaccess' ) && false === file_put_contents( $base . '/.htaccess', "Require all denied\nDeny from all\n" ) ) { return ''; } if ( ! file_exists( $base . '/index.php' ) ) { file_put_contents( $base . '/index.php', "