Make WordPress Core

source: tags/6.9/src/wp-includes/pluggable.php

Last change on this file was 61201, checked in by westonruter, 6 weeks ago

Mail: Improve multipart message handling in wp_mail().

This improves how wp_mail() handles Content-Type headers for multipart messages, preventing cases where the header could be duplicated.

Developed in https://github.com/WordPress/wordpress-develop/pull/9500

Props SirLouen, gitlost, rmccue, westi, MattyRob, bgermann, nacin, SergeyBiryukov, dd32, MikeHansenMe, Kleor, kitchin, JeffMatson, abcd95, westonruter, christinecooper, JohnVieth, dawidadach, imokweb, ayeshrajans, lakshyajeet, tusharbharti, sajjad67.
Fixes #15448.

  • Property svn:eol-style set to native
File size: 124.5 KB
Line 
1<?php
2/**
3 * These functions can be replaced via plugins. If plugins do not redefine these
4 * functions, then these will be used instead.
5 *
6 * @package WordPress
7 */
8
9if ( ! function_exists( 'wp_set_current_user' ) ) :
10        /**
11         * Changes the current user by ID or name.
12         *
13         * Set $id to null and specify a name if you do not know a user's ID.
14         *
15         * Some WordPress functionality is based on the current user and not based on
16         * the signed in user. Therefore, it opens the ability to edit and perform
17         * actions on users who aren't signed in.
18         *
19         * @since 2.0.3
20         *
21         * @global WP_User $current_user The current user object which holds the user data.
22         *
23         * @param int|null $id   User ID.
24         * @param string   $name User's username.
25         * @return WP_User Current user User object.
26         */
27        function wp_set_current_user( $id, $name = '' ) {
28                global $current_user;
29
30                // If `$id` matches the current user, there is nothing to do.
31                if ( isset( $current_user )
32                && ( $current_user instanceof WP_User )
33                && ( $id === $current_user->ID )
34                && ( null !== $id )
35                ) {
36                        return $current_user;
37                }
38
39                $current_user = new WP_User( $id, $name );
40
41                setup_userdata( $current_user->ID );
42
43                /**
44                 * Fires after the current user is set.
45                 *
46                 * @since 2.0.1
47                 */
48                do_action( 'set_current_user' );
49
50                return $current_user;
51        }
52endif;
53
54if ( ! function_exists( 'wp_get_current_user' ) ) :
55        /**
56         * Retrieves the current user object.
57         *
58         * Will set the current user, if the current user is not set. The current user
59         * will be set to the logged-in person. If no user is logged-in, then it will
60         * set the current user to 0, which is invalid and won't have any permissions.
61         *
62         * @since 2.0.3
63         *
64         * @see _wp_get_current_user()
65         * @global WP_User $current_user Checks if the current user is set.
66         *
67         * @return WP_User Current WP_User instance.
68         */
69        function wp_get_current_user() {
70                return _wp_get_current_user();
71        }
72endif;
73
74if ( ! function_exists( 'get_userdata' ) ) :
75        /**
76         * Retrieves user info by user ID.
77         *
78         * @since 0.71
79         *
80         * @param int $user_id User ID
81         * @return WP_User|false WP_User object on success, false on failure.
82         */
83        function get_userdata( $user_id ) {
84                return get_user_by( 'id', $user_id );
85        }
86endif;
87
88if ( ! function_exists( 'get_user_by' ) ) :
89        /**
90         * Retrieves user info by a given field.
91         *
92         * @since 2.8.0
93         * @since 4.4.0 Added 'ID' as an alias of 'id' for the `$field` parameter.
94         *
95         * @global WP_User $current_user The current user object which holds the user data.
96         *
97         * @param string     $field The field to retrieve the user with. id | ID | slug | email | login.
98         * @param int|string $value A value for $field. A user ID, slug, email address, or login name.
99         * @return WP_User|false WP_User object on success, false on failure.
100         */
101        function get_user_by( $field, $value ) {
102                $userdata = WP_User::get_data_by( $field, $value );
103
104                if ( ! $userdata ) {
105                        return false;
106                }
107
108                $user = new WP_User();
109                $user->init( $userdata );
110
111                return $user;
112        }
113endif;
114
115if ( ! function_exists( 'cache_users' ) ) :
116        /**
117         * Retrieves info for user lists to prevent multiple queries by get_userdata().
118         *
119         * @since 3.0.0
120         *
121         * @global wpdb $wpdb WordPress database abstraction object.
122         *
123         * @param int[] $user_ids User ID numbers list
124         */
125        function cache_users( $user_ids ) {
126                global $wpdb;
127
128                update_meta_cache( 'user', $user_ids );
129
130                $clean = _get_non_cached_ids( $user_ids, 'users' );
131
132                if ( empty( $clean ) ) {
133                        return;
134                }
135
136                $list = implode( ',', $clean );
137
138                $users = $wpdb->get_results( "SELECT * FROM $wpdb->users WHERE ID IN ($list)" );
139
140                foreach ( $users as $user ) {
141                        update_user_caches( $user );
142                }
143        }
144endif;
145
146if ( ! function_exists( 'wp_mail' ) ) :
147        /**
148         * Sends an email, similar to PHP's mail function.
149         *
150         * A true return value does not automatically mean that the user received the
151         * email successfully. It just only means that the method used was able to
152         * process the request without any errors.
153         *
154         * The default content type is `text/plain` which does not allow using HTML.
155         * However, you can set the content type of the email by using the
156         * {@see 'wp_mail_content_type'} filter.
157         *
158         * The default charset is based on the charset used on the blog. The charset can
159         * be set using the {@see 'wp_mail_charset'} filter.
160         *
161         * When using the `$embeds` parameter to embed images for use in HTML emails,
162         * reference the embedded file in your HTML with a `cid:` URL whose value
163         * matches the file's Content-ID. By default, the Content-ID (`cid`) used for
164         * each embedded file is the key in the embeds array, unless modified via the
165         * {@see 'wp_mail_embed_args'} filter. For example:
166         *
167         * `<img src="cid:0" alt="Logo">`
168         * `<img src="cid:my-image" alt="Image">`
169         *
170         * You may also customize the Content-ID for each file by using the
171         * {@see 'wp_mail_embed_args'} filter and setting the `cid` value.
172         *
173         * @since 1.2.1
174         * @since 5.5.0 is_email() is used for email validation,
175         *              instead of PHPMailer's default validator.
176         * @since 6.9.0 Added $embeds parameter.
177         * @since 6.9.0 Improved Content-Type header handling for multipart messages.
178         *
179         * @global PHPMailer\PHPMailer\PHPMailer $phpmailer
180         *
181         * @param string|string[] $to          Array or comma-separated list of email addresses to send message.
182         * @param string          $subject     Email subject.
183         * @param string          $message     Message contents.
184         * @param string|string[] $headers     Optional. Additional headers.
185         * @param string|string[] $attachments Optional. Paths to files to attach.
186         * @param string|string[] $embeds      Optional. Paths to files to embed.
187         * @return bool Whether the email was sent successfully.
188         */
189        function wp_mail( $to, $subject, $message, $headers = '', $attachments = array(), $embeds = array() ) {
190                // Compact the input, apply the filters, and extract them back out.
191
192                /**
193                 * Filters the wp_mail() arguments.
194                 *
195                 * @since 2.2.0
196                 *
197                 * @param array $args {
198                 *     Array of the `wp_mail()` arguments.
199                 *
200                 *     @type string|string[] $to          Array or comma-separated list of email addresses to send message.
201                 *     @type string          $subject     Email subject.
202                 *     @type string          $message     Message contents.
203                 *     @type string|string[] $headers     Additional headers.
204                 *     @type string|string[] $attachments Paths to files to attach.
205                 *     @type string|string[] $embeds      Paths to files to embed.
206                 * }
207                 */
208                $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments', 'embeds' ) );
209
210                /**
211                 * Filters whether to preempt sending an email.
212                 *
213                 * Returning a non-null value will short-circuit {@see wp_mail()}, returning
214                 * that value instead. A boolean return value should be used to indicate whether
215                 * the email was successfully sent.
216                 *
217                 * @since 5.7.0
218                 *
219                 * @param null|bool $return Short-circuit return value.
220                 * @param array     $atts {
221                 *     Array of the `wp_mail()` arguments.
222                 *
223                 *     @type string|string[] $to          Array or comma-separated list of email addresses to send message.
224                 *     @type string          $subject     Email subject.
225                 *     @type string          $message     Message contents.
226                 *     @type string|string[] $headers     Additional headers.
227                 *     @type string|string[] $attachments Paths to files to attach.
228                 *     @type string|string[] $embeds      Paths to files to embed.
229                 * }
230                 */
231                $pre_wp_mail = apply_filters( 'pre_wp_mail', null, $atts );
232
233                if ( null !== $pre_wp_mail ) {
234                        return $pre_wp_mail;
235                }
236
237                if ( isset( $atts['to'] ) ) {
238                        $to = $atts['to'];
239                }
240
241                if ( ! is_array( $to ) ) {
242                        $to = explode( ',', $to );
243                }
244
245                if ( isset( $atts['subject'] ) ) {
246                        $subject = $atts['subject'];
247                }
248
249                if ( isset( $atts['message'] ) ) {
250                        $message = $atts['message'];
251                }
252
253                if ( isset( $atts['headers'] ) ) {
254                        $headers = $atts['headers'];
255                }
256
257                if ( isset( $atts['attachments'] ) ) {
258                        $attachments = $atts['attachments'];
259                }
260
261                if ( ! is_array( $attachments ) ) {
262                        $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
263                }
264
265                if ( isset( $atts['embeds'] ) ) {
266                        $embeds = $atts['embeds'];
267                }
268
269                if ( ! is_array( $embeds ) ) {
270                        $embeds = explode( "\n", str_replace( "\r\n", "\n", $embeds ) );
271                }
272
273                global $phpmailer;
274
275                // (Re)create it, if it's gone missing.
276                if ( ! ( $phpmailer instanceof PHPMailer\PHPMailer\PHPMailer ) ) {
277                        require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
278                        require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
279                        require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
280                        require_once ABSPATH . WPINC . '/class-wp-phpmailer.php';
281                        $phpmailer = new WP_PHPMailer( true );
282
283                        $phpmailer::$validator = static function ( $email ) {
284                                return (bool) is_email( $email );
285                        };
286                }
287
288                // Headers.
289                $cc       = array();
290                $bcc      = array();
291                $reply_to = array();
292
293                if ( empty( $headers ) ) {
294                        $headers = array();
295                } else {
296                        if ( ! is_array( $headers ) ) {
297                                /*
298                                 * Explode the headers out, so this function can take
299                                 * both string headers and an array of headers.
300                                 */
301                                $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
302                        } else {
303                                $tempheaders = $headers;
304                        }
305                        $headers = array();
306
307                        // If it's actually got contents.
308                        if ( ! empty( $tempheaders ) ) {
309                                // Iterate through the raw headers.
310                                foreach ( (array) $tempheaders as $header ) {
311                                        if ( ! str_contains( $header, ':' ) ) {
312                                                if ( false !== stripos( $header, 'boundary=' ) ) {
313                                                        $parts    = preg_split( '/boundary=/i', trim( $header ) );
314                                                        $boundary = trim( str_replace( array( "'", '"' ), '', $parts[1] ) );
315                                                }
316                                                continue;
317                                        }
318                                        // Explode them out.
319                                        list( $name, $content ) = explode( ':', trim( $header ), 2 );
320
321                                        // Cleanup crew.
322                                        $name    = trim( $name );
323                                        $content = trim( $content );
324
325                                        switch ( strtolower( $name ) ) {
326                                                // Mainly for legacy -- process a "From:" header if it's there.
327                                                case 'from':
328                                                        $bracket_pos = strpos( $content, '<' );
329                                                        if ( false !== $bracket_pos ) {
330                                                                // Text before the bracketed email is the "From" name.
331                                                                if ( $bracket_pos > 0 ) {
332                                                                        $from_name = substr( $content, 0, $bracket_pos );
333                                                                        $from_name = str_replace( '"', '', $from_name );
334                                                                        $from_name = trim( $from_name );
335                                                                }
336
337                                                                $from_email = substr( $content, $bracket_pos + 1 );
338                                                                $from_email = str_replace( '>', '', $from_email );
339                                                                $from_email = trim( $from_email );
340
341                                                                // Avoid setting an empty $from_email.
342                                                        } elseif ( '' !== trim( $content ) ) {
343                                                                $from_email = trim( $content );
344                                                        }
345                                                        break;
346                                                case 'content-type':
347                                                        if ( str_contains( $content, ';' ) ) {
348                                                                list( $type, $charset_content ) = explode( ';', $content );
349                                                                $content_type                   = trim( $type );
350                                                                if ( false !== stripos( $charset_content, 'charset=' ) ) {
351                                                                        $charset = trim( str_replace( array( 'charset=', '"' ), '', $charset_content ) );
352                                                                } elseif ( false !== stripos( $charset_content, 'boundary=' ) ) {
353                                                                        $boundary = trim( str_replace( array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset_content ) );
354                                                                        $charset  = '';
355                                                                        if ( preg_match( '~^multipart/(\S+)~', $content_type, $matches ) ) {
356                                                                                $content_type = 'multipart/' . strtolower( $matches[1] ) . '; boundary="' . $boundary . '"';
357                                                                        }
358                                                                }
359
360                                                                // Avoid setting an empty $content_type.
361                                                        } elseif ( '' !== trim( $content ) ) {
362                                                                $content_type = trim( $content );
363                                                        }
364                                                        break;
365                                                case 'cc':
366                                                        $cc = array_merge( (array) $cc, explode( ',', $content ) );
367                                                        break;
368                                                case 'bcc':
369                                                        $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
370                                                        break;
371                                                case 'reply-to':
372                                                        $reply_to = array_merge( (array) $reply_to, explode( ',', $content ) );
373                                                        break;
374                                                default:
375                                                        // Add it to our grand headers array.
376                                                        $headers[ trim( $name ) ] = trim( $content );
377                                                        break;
378                                        }
379                                }
380                        }
381                }
382
383                // Empty out the values that may be set.
384                $phpmailer->clearAllRecipients();
385                $phpmailer->clearAttachments();
386                $phpmailer->clearCustomHeaders();
387                $phpmailer->clearReplyTos();
388                $phpmailer->Body    = '';
389                $phpmailer->AltBody = '';
390
391                /*
392                 * Reset encoding to 8-bit, as it may have been automatically downgraded
393                 * to 7-bit by PHPMailer (based on the body contents) in a previous call
394                 * to wp_mail().
395                 *
396                 * See https://core.trac.wordpress.org/ticket/33972
397                 */
398                $phpmailer->Encoding = PHPMailer\PHPMailer\PHPMailer::ENCODING_8BIT;
399
400                // Set "From" name and email.
401
402                // If we don't have a name from the input headers.
403                if ( ! isset( $from_name ) ) {
404                        $from_name = 'WordPress';
405                }
406
407                /*
408                 * If we don't have an email from the input headers, default to wordpress@$sitename
409                 * Some hosts will block outgoing mail from this address if it doesn't exist,
410                 * but there's no easy alternative. Defaulting to admin_email might appear to be
411                 * another option, but some hosts may refuse to relay mail from an unknown domain.
412                 * See https://core.trac.wordpress.org/ticket/5007.
413                 */
414                if ( ! isset( $from_email ) ) {
415                        // Get the site domain and get rid of www.
416                        $sitename   = wp_parse_url( network_home_url(), PHP_URL_HOST );
417                        $from_email = 'wordpress@';
418
419                        if ( null !== $sitename ) {
420                                if ( str_starts_with( $sitename, 'www.' ) ) {
421                                        $sitename = substr( $sitename, 4 );
422                                }
423
424                                $from_email .= $sitename;
425                        }
426                }
427
428                /**
429                 * Filters the email address to send from.
430                 *
431                 * @since 2.2.0
432                 *
433                 * @param string $from_email Email address to send from.
434                 */
435                $from_email = apply_filters( 'wp_mail_from', $from_email );
436
437                /**
438                 * Filters the name to associate with the "from" email address.
439                 *
440                 * @since 2.3.0
441                 *
442                 * @param string $from_name Name associated with the "from" email address.
443                 */
444                $from_name = apply_filters( 'wp_mail_from_name', $from_name );
445
446                try {
447                        $phpmailer->setFrom( $from_email, $from_name );
448                } catch ( PHPMailer\PHPMailer\Exception $e ) {
449                        $mail_error_data                             = compact( 'to', 'subject', 'message', 'headers', 'attachments' );
450                        $mail_error_data['phpmailer_exception_code'] = $e->getCode();
451
452                        /** This filter is documented in wp-includes/pluggable.php */
453                        do_action( 'wp_mail_failed', new WP_Error( 'wp_mail_failed', $e->getMessage(), $mail_error_data ) );
454
455                        return false;
456                }
457
458                // Set mail's subject and body.
459                $phpmailer->Subject = $subject;
460                $phpmailer->Body    = $message;
461
462                // Set destination addresses, using appropriate methods for handling addresses.
463                $address_headers = compact( 'to', 'cc', 'bcc', 'reply_to' );
464
465                foreach ( $address_headers as $address_header => $addresses ) {
466                        if ( empty( $addresses ) ) {
467                                continue;
468                        }
469
470                        foreach ( (array) $addresses as $address ) {
471                                try {
472                                        // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>".
473                                        $recipient_name = '';
474
475                                        if ( preg_match( '/(.*)<(.+)>/', $address, $matches ) ) {
476                                                if ( count( $matches ) === 3 ) {
477                                                        $recipient_name = $matches[1];
478                                                        $address        = $matches[2];
479                                                }
480                                        }
481
482                                        switch ( $address_header ) {
483                                                case 'to':
484                                                        $phpmailer->addAddress( $address, $recipient_name );
485                                                        break;
486                                                case 'cc':
487                                                        $phpmailer->addCC( $address, $recipient_name );
488                                                        break;
489                                                case 'bcc':
490                                                        $phpmailer->addBCC( $address, $recipient_name );
491                                                        break;
492                                                case 'reply_to':
493                                                        $phpmailer->addReplyTo( $address, $recipient_name );
494                                                        break;
495                                        }
496                                } catch ( PHPMailer\PHPMailer\Exception $e ) {
497                                        continue;
498                                }
499                        }
500                }
501
502                // Set to use PHP's mail().
503                $phpmailer->isMail();
504
505                // Set Content-Type and charset.
506
507                // If we don't have a Content-Type from the input headers.
508                if ( ! isset( $content_type ) ) {
509                        $content_type = 'text/plain';
510                }
511
512                /**
513                 * Filters the wp_mail() content type.
514                 *
515                 * @since 2.3.0
516                 *
517                 * @param string $content_type Default wp_mail() content type.
518                 */
519                $content_type = apply_filters( 'wp_mail_content_type', $content_type );
520
521                $phpmailer->ContentType = $content_type;
522
523                // Set whether it's plaintext, depending on $content_type.
524                if ( 'text/html' === $content_type ) {
525                        $phpmailer->isHTML( true );
526                }
527
528                // If we don't have a charset from the input headers.
529                if ( ! isset( $charset ) ) {
530                        $charset = get_bloginfo( 'charset' );
531                }
532
533                /**
534                 * Filters the default wp_mail() charset.
535                 *
536                 * @since 2.3.0
537                 *
538                 * @param string $charset Default email charset.
539                 */
540                $phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
541
542                // Set custom headers.
543                if ( ! empty( $headers ) ) {
544                        foreach ( (array) $headers as $name => $content ) {
545                                // Only add custom headers not added automatically by PHPMailer.
546                                if ( ! in_array( $name, array( 'MIME-Version', 'X-Mailer' ), true ) ) {
547                                        try {
548                                                $phpmailer->addCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
549                                        } catch ( PHPMailer\PHPMailer\Exception $e ) {
550                                                continue;
551                                        }
552                                }
553                        }
554                }
555
556                if ( ! empty( $attachments ) ) {
557                        foreach ( $attachments as $filename => $attachment ) {
558                                $filename = is_string( $filename ) ? $filename : '';
559
560                                try {
561                                        $phpmailer->addAttachment( $attachment, $filename );
562                                } catch ( PHPMailer\PHPMailer\Exception $e ) {
563                                        continue;
564                                }
565                        }
566                }
567
568                if ( ! empty( $embeds ) ) {
569                        foreach ( $embeds as $key => $embed_path ) {
570                                /**
571                                 * Filters the arguments for PHPMailer's addEmbeddedImage() method.
572                                 *
573                                 * @since 6.9.0
574                                 *
575                                 * @param array $args {
576                                 *     An array of arguments for `addEmbeddedImage()`.
577                                 *     @type string $path        The path to the file.
578                                 *     @type string $cid         The Content-ID of the image. Default: The key in the embeds array.
579                                 *     @type string $name        The filename of the image.
580                                 *     @type string $encoding    The encoding of the image. Default: 'base64'.
581                                 *     @type string $type        The MIME type of the image. Default: empty string, which lets PHPMailer auto-detect.
582                                 *     @type string $disposition The disposition of the image. Default: 'inline'.
583                                 * }
584                                 */
585                                $embed_args = apply_filters(
586                                        'wp_mail_embed_args',
587                                        array(
588                                                'path'        => $embed_path,
589                                                'cid'         => (string) $key,
590                                                'name'        => basename( $embed_path ),
591                                                'encoding'    => 'base64',
592                                                'type'        => '',
593                                                'disposition' => 'inline',
594                                        )
595                                );
596
597                                try {
598                                        $phpmailer->addEmbeddedImage(
599                                                $embed_args['path'],
600                                                $embed_args['cid'],
601                                                $embed_args['name'],
602                                                $embed_args['encoding'],
603                                                $embed_args['type'],
604                                                $embed_args['disposition']
605                                        );
606                                } catch ( PHPMailer\PHPMailer\Exception $e ) {
607                                        continue;
608                                }
609                        }
610                }
611
612                /**
613                 * Fires after PHPMailer is initialized.
614                 *
615                 * @since 2.2.0
616                 *
617                 * @param PHPMailer $phpmailer The PHPMailer instance (passed by reference).
618                 */
619                do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );
620
621                $mail_data = compact( 'to', 'subject', 'message', 'headers', 'attachments' );
622
623                // Send!
624                try {
625                        $send = $phpmailer->send();
626
627                        /**
628                         * Fires after PHPMailer has successfully sent an email.
629                         *
630                         * The firing of this action does not necessarily mean that the recipient(s) received the
631                         * email successfully. It only means that the `send` method above was able to
632                         * process the request without any errors.
633                         *
634                         * @since 5.9.0
635                         *
636                         * @param array $mail_data {
637                         *     An array containing the email recipient(s), subject, message, headers, and attachments.
638                         *
639                         *     @type string[] $to          Email addresses to send message.
640                         *     @type string   $subject     Email subject.
641                         *     @type string   $message     Message contents.
642                         *     @type string[] $headers     Additional headers.
643                         *     @type string[] $attachments Paths to files to attach.
644                         *     @type string[] $embeds      Paths to files to embed.
645                         * }
646                         */
647                        do_action( 'wp_mail_succeeded', $mail_data );
648
649                        return $send;
650                } catch ( PHPMailer\PHPMailer\Exception $e ) {
651                        $mail_data['phpmailer_exception_code'] = $e->getCode();
652
653                        /**
654                         * Fires after a PHPMailer\PHPMailer\Exception is caught.
655                         *
656                         * @since 4.4.0
657                         *
658                         * @param WP_Error $error A WP_Error object with the PHPMailer\PHPMailer\Exception message, and an array
659                         *                        containing the mail recipient, subject, message, headers, and attachments.
660                         */
661                        do_action( 'wp_mail_failed', new WP_Error( 'wp_mail_failed', $e->getMessage(), $mail_data ) );
662
663                        return false;
664                }
665        }
666endif;
667
668if ( ! function_exists( 'wp_authenticate' ) ) :
669        /**
670         * Authenticates a user, confirming the login credentials are valid.
671         *
672         * @since 2.5.0
673         * @since 4.5.0 `$username` now accepts an email address.
674         *
675         * @param string $username User's username or email address.
676         * @param string $password User's password.
677         * @return WP_User|WP_Error WP_User object if the credentials are valid,
678         *                          otherwise WP_Error.
679         */
680        function wp_authenticate(
681                $username,
682                #[\SensitiveParameter]
683                $password
684        ) {
685                $username = sanitize_user( $username );
686                $password = trim( $password );
687
688                /**
689                 * Filters whether a set of user login credentials are valid.
690                 *
691                 * A WP_User object is returned if the credentials authenticate a user.
692                 * WP_Error or null otherwise.
693                 *
694                 * @since 2.8.0
695                 * @since 4.5.0 `$username` now accepts an email address.
696                 *
697                 * @param null|WP_User|WP_Error $user     WP_User if the user is authenticated.
698                 *                                        WP_Error or null otherwise.
699                 * @param string                $username Username or email address.
700                 * @param string                $password User password.
701                 */
702                $user = apply_filters( 'authenticate', null, $username, $password );
703
704                if ( null === $user || false === $user ) {
705                        /*
706                         * TODO: What should the error message be? (Or would these even happen?)
707                         * Only needed if all authentication handlers fail to return anything.
708                         */
709                        $user = new WP_Error( 'authentication_failed', __( '<strong>Error:</strong> Invalid username, email address or incorrect password.' ) );
710                }
711
712                $ignore_codes = array( 'empty_username', 'empty_password' );
713
714                if ( is_wp_error( $user ) && ! in_array( $user->get_error_code(), $ignore_codes, true ) ) {
715                        $error = $user;
716
717                        /**
718                         * Fires after a user login has failed.
719                         *
720                         * @since 2.5.0
721                         * @since 4.5.0 The value of `$username` can now be an email address.
722                         * @since 5.4.0 The `$error` parameter was added.
723                         *
724                         * @param string   $username Username or email address.
725                         * @param WP_Error $error    A WP_Error object with the authentication failure details.
726                         */
727                        do_action( 'wp_login_failed', $username, $error );
728                }
729
730                return $user;
731        }
732endif;
733
734if ( ! function_exists( 'wp_logout' ) ) :
735        /**
736         * Logs the current user out.
737         *
738         * @since 2.5.0
739         */
740        function wp_logout() {
741                $user_id = get_current_user_id();
742
743                wp_destroy_current_session();
744                wp_clear_auth_cookie();
745                wp_set_current_user( 0 );
746
747                /**
748                 * Fires after a user is logged out.
749                 *
750                 * @since 1.5.0
751                 * @since 5.5.0 Added the `$user_id` parameter.
752                 *
753                 * @param int $user_id ID of the user that was logged out.
754                 */
755                do_action( 'wp_logout', $user_id );
756        }
757endif;
758
759if ( ! function_exists( 'wp_validate_auth_cookie' ) ) :
760        /**
761         * Validates authentication cookie.
762         *
763         * The checks include making sure that the authentication cookie is set and
764         * pulling in the contents (if $cookie is not used).
765         *
766         * Makes sure the cookie is not expired. Verifies the hash in cookie is what is
767         * should be and compares the two.
768         *
769         * @since 2.5.0
770         *
771         * @global int $login_grace_period
772         *
773         * @param string $cookie Optional. If used, will validate contents instead of cookie's.
774         * @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
775         *                       Note: This does *not* default to 'auth' like other cookie functions.
776         * @return int|false User ID if valid cookie, false if invalid.
777         */
778        function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) {
779                $cookie_elements = wp_parse_auth_cookie( $cookie, $scheme );
780                if ( ! $cookie_elements ) {
781                        /**
782                         * Fires if an authentication cookie is malformed.
783                         *
784                         * @since 2.7.0
785                         *
786                         * @param string $cookie Malformed auth cookie.
787                         * @param string $scheme Authentication scheme. Values include 'auth', 'secure_auth',
788                         *                       or 'logged_in'.
789                         */
790                        do_action( 'auth_cookie_malformed', $cookie, $scheme );
791                        return false;
792                }
793
794                $scheme     = $cookie_elements['scheme'];
795                $username   = $cookie_elements['username'];
796                $hmac       = $cookie_elements['hmac'];
797                $token      = $cookie_elements['token'];
798                $expiration = $cookie_elements['expiration'];
799
800                $expired = (int) $expiration;
801
802                // Allow a grace period for POST and Ajax requests.
803                if ( wp_doing_ajax() || 'POST' === $_SERVER['REQUEST_METHOD'] ) {
804                        $expired += HOUR_IN_SECONDS;
805                }
806
807                // Quick check to see if an honest cookie has expired.
808                if ( $expired < time() ) {
809                        /**
810                         * Fires once an authentication cookie has expired.
811                         *
812                         * @since 2.7.0
813                         *
814                         * @param string[] $cookie_elements {
815                         *     Authentication cookie components. None of the components should be assumed
816                         *     to be valid as they come directly from a client-provided cookie value.
817                         *
818                         *     @type string $username   User's username.
819                         *     @type string $expiration The time the cookie expires as a UNIX timestamp.
820                         *     @type string $token      User's session token used.
821                         *     @type string $hmac       The security hash for the cookie.
822                         *     @type string $scheme     The cookie scheme to use.
823                         * }
824                         */
825                        do_action( 'auth_cookie_expired', $cookie_elements );
826                        return false;
827                }
828
829                $user = get_user_by( 'login', $username );
830                if ( ! $user ) {
831                        /**
832                         * Fires if a bad username is entered in the user authentication process.
833                         *
834                         * @since 2.7.0
835                         *
836                         * @param string[] $cookie_elements {
837                         *     Authentication cookie components. None of the components should be assumed
838                         *     to be valid as they come directly from a client-provided cookie value.
839                         *
840                         *     @type string $username   User's username.
841                         *     @type string $expiration The time the cookie expires as a UNIX timestamp.
842                         *     @type string $token      User's session token used.
843                         *     @type string $hmac       The security hash for the cookie.
844                         *     @type string $scheme     The cookie scheme to use.
845                         * }
846                         */
847                        do_action( 'auth_cookie_bad_username', $cookie_elements );
848                        return false;
849                }
850
851                if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
852                        // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
853                        $pass_frag = substr( $user->user_pass, 8, 4 );
854                } else {
855                        // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
856                        $pass_frag = substr( $user->user_pass, -4 );
857                }
858
859                $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
860
861                $hash = hash_hmac( 'sha256', $username . '|' . $expiration . '|' . $token, $key );
862
863                if ( ! hash_equals( $hash, $hmac ) ) {
864                        /**
865                         * Fires if a bad authentication cookie hash is encountered.
866                         *
867                         * @since 2.7.0
868                         *
869                         * @param string[] $cookie_elements {
870                         *     Authentication cookie components. None of the components should be assumed
871                         *     to be valid as they come directly from a client-provided cookie value.
872                         *
873                         *     @type string $username   User's username.
874                         *     @type string $expiration The time the cookie expires as a UNIX timestamp.
875                         *     @type string $token      User's session token used.
876                         *     @type string $hmac       The security hash for the cookie.
877                         *     @type string $scheme     The cookie scheme to use.
878                         * }
879                         */
880                        do_action( 'auth_cookie_bad_hash', $cookie_elements );
881                        return false;
882                }
883
884                $manager = WP_Session_Tokens::get_instance( $user->ID );
885                if ( ! $manager->verify( $token ) ) {
886                        /**
887                         * Fires if a bad session token is encountered.
888                         *
889                         * @since 4.0.0
890                         *
891                         * @param string[] $cookie_elements {
892                         *     Authentication cookie components. None of the components should be assumed
893                         *     to be valid as they come directly from a client-provided cookie value.
894                         *
895                         *     @type string $username   User's username.
896                         *     @type string $expiration The time the cookie expires as a UNIX timestamp.
897                         *     @type string $token      User's session token used.
898                         *     @type string $hmac       The security hash for the cookie.
899                         *     @type string $scheme     The cookie scheme to use.
900                         * }
901                         */
902                        do_action( 'auth_cookie_bad_session_token', $cookie_elements );
903                        return false;
904                }
905
906                // Ajax/POST grace period set above.
907                if ( $expiration < time() ) {
908                        $GLOBALS['login_grace_period'] = 1;
909                }
910
911                /**
912                 * Fires once an authentication cookie has been validated.
913                 *
914                 * @since 2.7.0
915                 *
916                 * @param string[] $cookie_elements {
917                 *     Authentication cookie components.
918                 *
919                 *     @type string $username   User's username.
920                 *     @type string $expiration The time the cookie expires as a UNIX timestamp.
921                 *     @type string $token      User's session token used.
922                 *     @type string $hmac       The security hash for the cookie.
923                 *     @type string $scheme     The cookie scheme to use.
924                 * }
925                 * @param WP_User  $user            User object.
926                 */
927                do_action( 'auth_cookie_valid', $cookie_elements, $user );
928
929                return $user->ID;
930        }
931endif;
932
933if ( ! function_exists( 'wp_generate_auth_cookie' ) ) :
934        /**
935         * Generates authentication cookie contents.
936         *
937         * @since 2.5.0
938         * @since 4.0.0 The `$token` parameter was added.
939         *
940         * @param int    $user_id    User ID.
941         * @param int    $expiration The time the cookie expires as a UNIX timestamp.
942         * @param string $scheme     Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
943         *                           Default 'auth'.
944         * @param string $token      User's session token to use for this cookie.
945         * @return string Authentication cookie contents. Empty string if user does not exist.
946         */
947        function wp_generate_auth_cookie( $user_id, $expiration, $scheme = 'auth', $token = '' ) {
948                $user = get_userdata( $user_id );
949                if ( ! $user ) {
950                        return '';
951                }
952
953                if ( ! $token ) {
954                        $manager = WP_Session_Tokens::get_instance( $user_id );
955                        $token   = $manager->create( $expiration );
956                }
957
958                if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
959                        // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
960                        $pass_frag = substr( $user->user_pass, 8, 4 );
961                } else {
962                        // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
963                        $pass_frag = substr( $user->user_pass, -4 );
964                }
965
966                $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
967
968                $hash = hash_hmac( 'sha256', $user->user_login . '|' . $expiration . '|' . $token, $key );
969
970                $cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash;
971
972                /**
973                 * Filters the authentication cookie.
974                 *
975                 * @since 2.5.0
976                 * @since 4.0.0 The `$token` parameter was added.
977                 *
978                 * @param string $cookie     Authentication cookie.
979                 * @param int    $user_id    User ID.
980                 * @param int    $expiration The time the cookie expires as a UNIX timestamp.
981                 * @param string $scheme     Cookie scheme used. Accepts 'auth', 'secure_auth', or 'logged_in'.
982                 * @param string $token      User's session token used.
983                 */
984                return apply_filters( 'auth_cookie', $cookie, $user_id, $expiration, $scheme, $token );
985        }
986endif;
987
988if ( ! function_exists( 'wp_parse_auth_cookie' ) ) :
989        /**
990         * Parses a cookie into its components.
991         *
992         * @since 2.7.0
993         * @since 4.0.0 The `$token` element was added to the return value.
994         *
995         * @param string $cookie Authentication cookie.
996         * @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
997         * @return string[]|false {
998         *     Authentication cookie components. None of the components should be assumed
999         *     to be valid as they come directly from a client-provided cookie value. If
1000         *     the cookie value is malformed, false is returned.
1001         *
1002         *     @type string $username   User's username.
1003         *     @type string $expiration The time the cookie expires as a UNIX timestamp.
1004         *     @type string $token      User's session token used.
1005         *     @type string $hmac       The security hash for the cookie.
1006         *     @type string $scheme     The cookie scheme to use.
1007         * }
1008         */
1009        function wp_parse_auth_cookie( $cookie = '', $scheme = '' ) {
1010                if ( empty( $cookie ) ) {
1011                        switch ( $scheme ) {
1012                                case 'auth':
1013                                        $cookie_name = AUTH_COOKIE;
1014                                        break;
1015                                case 'secure_auth':
1016                                        $cookie_name = SECURE_AUTH_COOKIE;
1017                                        break;
1018                                case 'logged_in':
1019                                        $cookie_name = LOGGED_IN_COOKIE;
1020                                        break;
1021                                default:
1022                                        if ( is_ssl() ) {
1023                                                $cookie_name = SECURE_AUTH_COOKIE;
1024                                                $scheme      = 'secure_auth';
1025                                        } else {
1026                                                $cookie_name = AUTH_COOKIE;
1027                                                $scheme      = 'auth';
1028                                        }
1029                        }
1030
1031                        if ( empty( $_COOKIE[ $cookie_name ] ) ) {
1032                                return false;
1033                        }
1034                        $cookie = $_COOKIE[ $cookie_name ];
1035                }
1036
1037                $cookie_elements = explode( '|', $cookie );
1038                if ( count( $cookie_elements ) !== 4 ) {
1039                        return false;
1040                }
1041
1042                list( $username, $expiration, $token, $hmac ) = $cookie_elements;
1043
1044                return compact( 'username', 'expiration', 'token', 'hmac', 'scheme' );
1045        }
1046endif;
1047
1048if ( ! function_exists( 'wp_set_auth_cookie' ) ) :
1049        /**
1050         * Sets the authentication cookies for a given user ID.
1051         *
1052         * The `$remember` parameter controls cookie persistence:
1053         * - If true, the cookie is persistent (default 14 days, filterable via {@see 'auth_cookie_expiration'}).
1054         * - If false, the cookie is a browser session cookie (expires when the browser closes).
1055         *   Internally, {@see 'auth_cookie_expiration'} is still applied, to expire the login after
1056         *   two days or when the browser is closed, whichever occurs first.
1057         *
1058         * @since 2.5.0
1059         * @since 4.3.0 Added the `$token` parameter.
1060         *
1061         * @param int         $user_id  User ID.
1062         * @param bool        $remember Whether to remember the user.
1063         * @param bool|string $secure   Whether the auth cookie should only be sent over HTTPS. Default is an empty
1064         *                              string which means the value of `is_ssl()` will be used.
1065         * @param string      $token    Optional. User's session token to use for this cookie.
1066         */
1067        function wp_set_auth_cookie( $user_id, $remember = false, $secure = '', $token = '' ) {
1068                if ( $remember ) {
1069                        /**
1070                         * Filters the duration of the authentication cookie expiration period.
1071                         *
1072                         * @since 2.8.0
1073                         *
1074                         * @param int  $length   Duration of the expiration period in seconds.
1075                         * @param int  $user_id  User ID.
1076                         * @param bool $remember Whether to remember the user login. Default false.
1077                         */
1078                        $expiration = time() + apply_filters( 'auth_cookie_expiration', 14 * DAY_IN_SECONDS, $user_id, $remember );
1079
1080                        /*
1081                         * Ensure the browser will continue to send the cookie after the expiration time is reached.
1082                         * Needed for the login grace period in wp_validate_auth_cookie().
1083                         */
1084                        $expire = $expiration + ( 12 * HOUR_IN_SECONDS );
1085                } else {
1086                        /** This filter is documented in wp-includes/pluggable.php */
1087                        $expiration = time() + apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user_id, $remember );
1088                        $expire     = 0;
1089                }
1090
1091                if ( '' === $secure ) {
1092                        $secure = is_ssl();
1093                }
1094
1095                // Front-end cookie is secure when the auth cookie is secure and the site's home URL uses HTTPS.
1096                $secure_logged_in_cookie = $secure && 'https' === parse_url( get_option( 'home' ), PHP_URL_SCHEME );
1097
1098                /**
1099                 * Filters whether the auth cookie should only be sent over HTTPS.
1100                 *
1101                 * @since 3.1.0
1102                 *
1103                 * @param bool $secure  Whether the cookie should only be sent over HTTPS.
1104                 * @param int  $user_id User ID.
1105                 */
1106                $secure = apply_filters( 'secure_auth_cookie', $secure, $user_id );
1107
1108                /**
1109                 * Filters whether the logged in cookie should only be sent over HTTPS.
1110                 *
1111                 * @since 3.1.0
1112                 *
1113                 * @param bool $secure_logged_in_cookie Whether the logged in cookie should only be sent over HTTPS.
1114                 * @param int  $user_id                 User ID.
1115                 * @param bool $secure                  Whether the auth cookie should only be sent over HTTPS.
1116                 */
1117                $secure_logged_in_cookie = apply_filters( 'secure_logged_in_cookie', $secure_logged_in_cookie, $user_id, $secure );
1118
1119                if ( $secure ) {
1120                        $auth_cookie_name = SECURE_AUTH_COOKIE;
1121                        $scheme           = 'secure_auth';
1122                } else {
1123                        $auth_cookie_name = AUTH_COOKIE;
1124                        $scheme           = 'auth';
1125                }
1126
1127                if ( '' === $token ) {
1128                        $manager = WP_Session_Tokens::get_instance( $user_id );
1129                        $token   = $manager->create( $expiration );
1130                }
1131
1132                $auth_cookie      = wp_generate_auth_cookie( $user_id, $expiration, $scheme, $token );
1133                $logged_in_cookie = wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token );
1134
1135                /**
1136                 * Fires immediately before the authentication cookie is set.
1137                 *
1138                 * @since 2.5.0
1139                 * @since 4.9.0 The `$token` parameter was added.
1140                 *
1141                 * @param string $auth_cookie Authentication cookie value.
1142                 * @param int    $expire      The time the login grace period expires as a UNIX timestamp.
1143                 *                            Default is 12 hours past the cookie's expiration time.
1144                 * @param int    $expiration  The time when the authentication cookie expires as a UNIX timestamp.
1145                 *                            Default is 14 days from now.
1146                 * @param int    $user_id     User ID.
1147                 * @param string $scheme      Authentication scheme. Values include 'auth' or 'secure_auth'.
1148                 * @param string $token       User's session token to use for this cookie.
1149                 */
1150                do_action( 'set_auth_cookie', $auth_cookie, $expire, $expiration, $user_id, $scheme, $token );
1151
1152                /**
1153                 * Fires immediately before the logged-in authentication cookie is set.
1154                 *
1155                 * @since 2.6.0
1156                 * @since 4.9.0 The `$token` parameter was added.
1157                 *
1158                 * @param string $logged_in_cookie The logged-in cookie value.
1159                 * @param int    $expire           The time the login grace period expires as a UNIX timestamp.
1160                 *                                 Default is 12 hours past the cookie's expiration time.
1161                 * @param int    $expiration       The time when the logged-in authentication cookie expires as a UNIX timestamp.
1162                 *                                 Default is 14 days from now.
1163                 * @param int    $user_id          User ID.
1164                 * @param string $scheme           Authentication scheme. Default 'logged_in'.
1165                 * @param string $token            User's session token to use for this cookie.
1166                 */
1167                do_action( 'set_logged_in_cookie', $logged_in_cookie, $expire, $expiration, $user_id, 'logged_in', $token );
1168
1169                /**
1170                 * Allows preventing auth cookies from actually being sent to the client.
1171                 *
1172                 * @since 4.7.4
1173                 * @since 6.2.0 The `$expire`, `$expiration`, `$user_id`, `$scheme`, and `$token` parameters were added.
1174                 *
1175                 * @param bool   $send       Whether to send auth cookies to the client. Default true.
1176                 * @param int    $expire     The time the login grace period expires as a UNIX timestamp.
1177                 *                           Default is 12 hours past the cookie's expiration time. Zero when clearing cookies.
1178                 * @param int    $expiration The time when the logged-in authentication cookie expires as a UNIX timestamp.
1179                 *                           Default is 14 days from now. Zero when clearing cookies.
1180                 * @param int    $user_id    User ID. Zero when clearing cookies.
1181                 * @param string $scheme     Authentication scheme. Values include 'auth' or 'secure_auth'.
1182                 *                           Empty string when clearing cookies.
1183                 * @param string $token      User's session token to use for this cookie. Empty string when clearing cookies.
1184                 */
1185                if ( ! apply_filters( 'send_auth_cookies', true, $expire, $expiration, $user_id, $scheme, $token ) ) {
1186                        return;
1187                }
1188
1189                setcookie( $auth_cookie_name, $auth_cookie, $expire, PLUGINS_COOKIE_PATH, COOKIE_DOMAIN, $secure, true );
1190                setcookie( $auth_cookie_name, $auth_cookie, $expire, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, $secure, true );
1191                setcookie( LOGGED_IN_COOKIE, $logged_in_cookie, $expire, COOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true );
1192                if ( COOKIEPATH !== SITECOOKIEPATH ) {
1193                        setcookie( LOGGED_IN_COOKIE, $logged_in_cookie, $expire, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true );
1194                }
1195        }
1196endif;
1197
1198if ( ! function_exists( 'wp_clear_auth_cookie' ) ) :
1199        /**
1200         * Removes all of the cookies associated with authentication.
1201         *
1202         * @since 2.5.0
1203         */
1204        function wp_clear_auth_cookie() {
1205                /**
1206                 * Fires just before the authentication cookies are cleared.
1207                 *
1208                 * @since 2.7.0
1209                 */
1210                do_action( 'clear_auth_cookie' );
1211
1212                /** This filter is documented in wp-includes/pluggable.php */
1213                if ( ! apply_filters( 'send_auth_cookies', true, 0, 0, 0, '', '' ) ) {
1214                        return;
1215                }
1216
1217                // Auth cookies.
1218                setcookie( AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN );
1219                setcookie( SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN );
1220                setcookie( AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, PLUGINS_COOKIE_PATH, COOKIE_DOMAIN );
1221                setcookie( SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, PLUGINS_COOKIE_PATH, COOKIE_DOMAIN );
1222                setcookie( LOGGED_IN_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
1223                setcookie( LOGGED_IN_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
1224
1225                // Settings cookies.
1226                setcookie( 'wp-settings-' . get_current_user_id(), ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH );
1227                setcookie( 'wp-settings-time-' . get_current_user_id(), ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH );
1228
1229                // Old cookies.
1230                setcookie( AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
1231                setcookie( AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
1232                setcookie( SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
1233                setcookie( SECURE_AUTH_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
1234
1235                // Even older cookies.
1236                setcookie( USER_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
1237                setcookie( PASS_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
1238                setcookie( USER_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
1239                setcookie( PASS_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
1240
1241                // Post password cookie.
1242                setcookie( 'wp-postpass_' . COOKIEHASH, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
1243        }
1244endif;
1245
1246if ( ! function_exists( 'is_user_logged_in' ) ) :
1247        /**
1248         * Determines whether the current visitor is a logged in user.
1249         *
1250         * For more information on this and similar theme functions, check out
1251         * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
1252         * Conditional Tags} article in the Theme Developer Handbook.
1253         *
1254         * @since 2.0.0
1255         *
1256         * @return bool True if user is logged in, false if not logged in.
1257         */
1258        function is_user_logged_in() {
1259                $user = wp_get_current_user();
1260
1261                return $user->exists();
1262        }
1263endif;
1264
1265if ( ! function_exists( 'auth_redirect' ) ) :
1266        /**
1267         * Checks if a user is logged in, if not it redirects them to the login page.
1268         *
1269         * When this code is called from a page, it checks to see if the user viewing the page is logged in.
1270         * If the user is not logged in, they are redirected to the login page. The user is redirected
1271         * in such a way that, upon logging in, they will be sent directly to the page they were originally
1272         * trying to access.
1273         *
1274         * @since 1.5.0
1275         */
1276        function auth_redirect() {
1277                $secure = ( is_ssl() || force_ssl_admin() );
1278
1279                /**
1280                 * Filters whether to use a secure authentication redirect.
1281                 *
1282                 * @since 3.1.0
1283                 *
1284                 * @param bool $secure Whether to use a secure authentication redirect. Default false.
1285                 */
1286                $secure = apply_filters( 'secure_auth_redirect', $secure );
1287
1288                // If https is required and request is http, redirect.
1289                if ( $secure && ! is_ssl() && str_contains( $_SERVER['REQUEST_URI'], 'wp-admin' ) ) {
1290                        if ( str_starts_with( $_SERVER['REQUEST_URI'], 'http' ) ) {
1291                                wp_redirect( set_url_scheme( $_SERVER['REQUEST_URI'], 'https' ) );
1292                                exit;
1293                        } else {
1294                                wp_redirect( 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
1295                                exit;
1296                        }
1297                }
1298
1299                /**
1300                 * Filters the authentication redirect scheme.
1301                 *
1302                 * @since 2.9.0
1303                 *
1304                 * @param string $scheme Authentication redirect scheme. Default empty.
1305                 */
1306                $scheme = apply_filters( 'auth_redirect_scheme', '' );
1307
1308                $user_id = wp_validate_auth_cookie( '', $scheme );
1309                if ( $user_id ) {
1310                        /**
1311                         * Fires before the authentication redirect.
1312                         *
1313                         * @since 2.8.0
1314                         *
1315                         * @param int $user_id User ID.
1316                         */
1317                        do_action( 'auth_redirect', $user_id );
1318
1319                        // If the user wants ssl but the session is not ssl, redirect.
1320                        if ( ! $secure && get_user_option( 'use_ssl', $user_id ) && str_contains( $_SERVER['REQUEST_URI'], 'wp-admin' ) ) {
1321                                if ( str_starts_with( $_SERVER['REQUEST_URI'], 'http' ) ) {
1322                                        wp_redirect( set_url_scheme( $_SERVER['REQUEST_URI'], 'https' ) );
1323                                        exit;
1324                                } else {
1325                                        wp_redirect( 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
1326                                        exit;
1327                                }
1328                        }
1329
1330                        return; // The cookie is good, so we're done.
1331                }
1332
1333                // The cookie is no good, so force login.
1334                nocache_headers();
1335
1336                if ( str_contains( $_SERVER['REQUEST_URI'], '/options.php' ) && wp_get_referer() ) {
1337                        $redirect = wp_get_referer();
1338                } else {
1339                        $redirect = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
1340                }
1341
1342                $login_url = wp_login_url( $redirect, true );
1343
1344                wp_redirect( $login_url );
1345                exit;
1346        }
1347endif;
1348
1349if ( ! function_exists( 'check_admin_referer' ) ) :
1350        /**
1351         * Ensures intent by verifying that a user was referred from another admin page with the correct security nonce.
1352         *
1353         * This function ensures the user intends to perform a given action, which helps protect against clickjacking style
1354         * attacks. It verifies intent, not authorization, therefore it does not verify the user's capabilities. This should
1355         * be performed with `current_user_can()` or similar.
1356         *
1357         * If the nonce value is invalid, the function will exit with an "Are You Sure?" style message.
1358         *
1359         * @since 1.2.0
1360         * @since 2.5.0 The `$query_arg` parameter was added.
1361         *
1362         * @param int|string $action    The nonce action.
1363         * @param string     $query_arg Optional. Key to check for nonce in `$_REQUEST`. Default '_wpnonce'.
1364         * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago,
1365         *                   2 if the nonce is valid and generated between 12-24 hours ago.
1366         *                   False if the nonce is invalid.
1367         */
1368        function check_admin_referer( $action = -1, $query_arg = '_wpnonce' ) {
1369                if ( -1 === $action ) {
1370                        _doing_it_wrong( __FUNCTION__, __( 'You should specify an action to be verified by using the first parameter.' ), '3.2.0' );
1371                }
1372
1373                $adminurl = strtolower( admin_url() );
1374                $referer  = strtolower( wp_get_referer() );
1375                $result   = isset( $_REQUEST[ $query_arg ] ) ? wp_verify_nonce( $_REQUEST[ $query_arg ], $action ) : false;
1376
1377                /**
1378                 * Fires once the admin request has been validated or not.
1379                 *
1380                 * @since 1.5.1
1381                 *
1382                 * @param string    $action The nonce action.
1383                 * @param false|int $result False if the nonce is invalid, 1 if the nonce is valid and generated between
1384                 *                          0-12 hours ago, 2 if the nonce is valid and generated between 12-24 hours ago.
1385                 */
1386                do_action( 'check_admin_referer', $action, $result );
1387
1388                if ( ! $result && ! ( -1 === $action && str_starts_with( $referer, $adminurl ) ) ) {
1389                        wp_nonce_ays( $action );
1390                        die();
1391                }
1392
1393                return $result;
1394        }
1395endif;
1396
1397if ( ! function_exists( 'check_ajax_referer' ) ) :
1398        /**
1399         * Verifies the Ajax request to prevent processing requests external of the blog.
1400         *
1401         * @since 2.0.3
1402         *
1403         * @param int|string   $action    Action nonce.
1404         * @param false|string $query_arg Optional. Key to check for the nonce in `$_REQUEST` (since 2.5). If false,
1405         *                                `$_REQUEST` values will be evaluated for '_ajax_nonce', and '_wpnonce'
1406         *                                (in that order). Default false.
1407         * @param bool         $stop      Optional. Whether to stop early when the nonce cannot be verified.
1408         *                                Default true.
1409         * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago,
1410         *                   2 if the nonce is valid and generated between 12-24 hours ago.
1411         *                   False if the nonce is invalid.
1412         */
1413        function check_ajax_referer( $action = -1, $query_arg = false, $stop = true ) {
1414                if ( -1 === $action ) {
1415                        _doing_it_wrong( __FUNCTION__, __( 'You should specify an action to be verified by using the first parameter.' ), '4.7.0' );
1416                }
1417
1418                $nonce = '';
1419
1420                if ( $query_arg && isset( $_REQUEST[ $query_arg ] ) ) {
1421                        $nonce = $_REQUEST[ $query_arg ];
1422                } elseif ( isset( $_REQUEST['_ajax_nonce'] ) ) {
1423                        $nonce = $_REQUEST['_ajax_nonce'];
1424                } elseif ( isset( $_REQUEST['_wpnonce'] ) ) {
1425                        $nonce = $_REQUEST['_wpnonce'];
1426                }
1427
1428                $result = wp_verify_nonce( $nonce, $action );
1429
1430                /**
1431                 * Fires once the Ajax request has been validated or not.
1432                 *
1433                 * @since 2.1.0
1434                 *
1435                 * @param string    $action The Ajax nonce action.
1436                 * @param false|int $result False if the nonce is invalid, 1 if the nonce is valid and generated between
1437                 *                          0-12 hours ago, 2 if the nonce is valid and generated between 12-24 hours ago.
1438                 */
1439                do_action( 'check_ajax_referer', $action, $result );
1440
1441                if ( $stop && false === $result ) {
1442                        if ( wp_doing_ajax() ) {
1443                                wp_die( -1, 403 );
1444                        } else {
1445                                die( '-1' );
1446                        }
1447                }
1448
1449                return $result;
1450        }
1451endif;
1452
1453if ( ! function_exists( 'wp_redirect' ) ) :
1454        /**
1455         * Redirects to another page.
1456         *
1457         * Note: wp_redirect() does not exit automatically, and should almost always be
1458         * followed by a call to `exit;`:
1459         *
1460         *     wp_redirect( $url );
1461         *     exit;
1462         *
1463         * Exiting can also be selectively manipulated by using wp_redirect() as a conditional
1464         * in conjunction with the {@see 'wp_redirect'} and {@see 'wp_redirect_status'} filters:
1465         *
1466         *     if ( wp_redirect( $url ) ) {
1467         *         exit;
1468         *     }
1469         *
1470         * @since 1.5.1
1471         * @since 5.1.0 The `$x_redirect_by` parameter was added.
1472         * @since 5.4.0 On invalid status codes, wp_die() is called.
1473         *
1474         * @global bool $is_IIS
1475         *
1476         * @param string       $location      The path or URL to redirect to.
1477         * @param int          $status        Optional. HTTP response status code to use. Default '302' (Moved Temporarily).
1478         * @param string|false $x_redirect_by Optional. The application doing the redirect or false to omit. Default 'WordPress'.
1479         * @return bool False if the redirect was canceled, true otherwise.
1480         */
1481        function wp_redirect( $location, $status = 302, $x_redirect_by = 'WordPress' ) {
1482                global $is_IIS;
1483
1484                /**
1485                 * Filters the redirect location.
1486                 *
1487                 * @since 2.1.0
1488                 *
1489                 * @param string $location The path or URL to redirect to.
1490                 * @param int    $status   The HTTP response status code to use.
1491                 */
1492                $location = apply_filters( 'wp_redirect', $location, $status );
1493
1494                /**
1495                 * Filters the redirect HTTP response status code to use.
1496                 *
1497                 * @since 2.3.0
1498                 *
1499                 * @param int    $status   The HTTP response status code to use.
1500                 * @param string $location The path or URL to redirect to.
1501                 */
1502                $status = apply_filters( 'wp_redirect_status', $status, $location );
1503
1504                if ( ! $location ) {
1505                        return false;
1506                }
1507
1508                if ( $status < 300 || 399 < $status ) {
1509                        wp_die( __( 'HTTP redirect status code must be a redirection code, 3xx.' ) );
1510                }
1511
1512                $location = wp_sanitize_redirect( $location );
1513
1514                if ( ! $is_IIS && 'cgi-fcgi' !== PHP_SAPI ) {
1515                        status_header( $status ); // This causes problems on IIS and some FastCGI setups.
1516                }
1517
1518                /**
1519                 * Filters the X-Redirect-By header.
1520                 *
1521                 * Allows applications to identify themselves when they're doing a redirect.
1522                 *
1523                 * @since 5.1.0
1524                 *
1525                 * @param string|false $x_redirect_by The application doing the redirect or false to omit the header.
1526                 * @param int          $status        Status code to use.
1527                 * @param string       $location      The path to redirect to.
1528                 */
1529                $x_redirect_by = apply_filters( 'x_redirect_by', $x_redirect_by, $status, $location );
1530                if ( is_string( $x_redirect_by ) ) {
1531                        header( "X-Redirect-By: $x_redirect_by" );
1532                }
1533
1534                header( "Location: $location", true, $status );
1535
1536                return true;
1537        }
1538endif;
1539
1540if ( ! function_exists( 'wp_sanitize_redirect' ) ) :
1541        /**
1542         * Sanitizes a URL for use in a redirect.
1543         *
1544         * @since 2.3.0
1545         *
1546         * @param string $location The path to redirect to.
1547         * @return string Redirect-sanitized URL.
1548         */
1549        function wp_sanitize_redirect( $location ) {
1550                // Encode spaces.
1551                $location = str_replace( ' ', '%20', $location );
1552
1553                $regex    = '/
1554                (
1555                        (?: [\xC2-\xDF][\x80-\xBF]        # double-byte sequences   110xxxxx 10xxxxxx
1556                        |   \xE0[\xA0-\xBF][\x80-\xBF]    # triple-byte sequences   1110xxxx 10xxxxxx * 2
1557                        |   [\xE1-\xEC][\x80-\xBF]{2}
1558                        |   \xED[\x80-\x9F][\x80-\xBF]
1559                        |   [\xEE-\xEF][\x80-\xBF]{2}
1560                        |   \xF0[\x90-\xBF][\x80-\xBF]{2} # four-byte sequences   11110xxx 10xxxxxx * 3
1561                        |   [\xF1-\xF3][\x80-\xBF]{3}
1562                        |   \xF4[\x80-\x8F][\x80-\xBF]{2}
1563                ){1,40}                              # ...one or more times
1564                )/x';
1565                $location = preg_replace_callback( $regex, '_wp_sanitize_utf8_in_redirect', $location );
1566                $location = preg_replace( '|[^a-z0-9-~+_.?#=&;,/:%!*\[\]()@]|i', '', $location );
1567                $location = wp_kses_no_null( $location );
1568
1569                // Remove %0D and %0A from location.
1570                $strip = array( '%0d', '%0a', '%0D', '%0A' );
1571                return _deep_replace( $strip, $location );
1572        }
1573
1574        /**
1575         * URL encodes UTF-8 characters in a URL.
1576         *
1577         * @ignore
1578         * @since 4.2.0
1579         * @access private
1580         *
1581         * @see wp_sanitize_redirect()
1582         *
1583         * @param array $matches RegEx matches against the redirect location.
1584         * @return string URL-encoded version of the first RegEx match.
1585         */
1586        function _wp_sanitize_utf8_in_redirect( $matches ) {
1587                return urlencode( $matches[0] );
1588        }
1589endif;
1590
1591if ( ! function_exists( 'wp_safe_redirect' ) ) :
1592        /**
1593         * Performs a safe (local) redirect, using wp_redirect().
1594         *
1595         * Checks whether the $location is using an allowed host, if it has an absolute
1596         * path. A plugin can therefore set or remove allowed host(s) to or from the
1597         * list.
1598         *
1599         * If the host is not allowed, then the redirect defaults to wp-admin on the siteurl
1600         * instead. This prevents malicious redirects which redirect to another host,
1601         * but only used in a few places.
1602         *
1603         * Note: wp_safe_redirect() does not exit automatically, and should almost always be
1604         * followed by a call to `exit;`:
1605         *
1606         *     wp_safe_redirect( $url );
1607         *     exit;
1608         *
1609         * Exiting can also be selectively manipulated by using wp_safe_redirect() as a conditional
1610         * in conjunction with the {@see 'wp_redirect'} and {@see 'wp_redirect_status'} filters:
1611         *
1612         *     if ( wp_safe_redirect( $url ) ) {
1613         *         exit;
1614         *     }
1615         *
1616         * @since 2.3.0
1617         * @since 5.1.0 The return value from wp_redirect() is now passed on, and the `$x_redirect_by` parameter was added.
1618         *
1619         * @param string       $location      The path or URL to redirect to.
1620         * @param int          $status        Optional. HTTP response status code to use. Default '302' (Moved Temporarily).
1621         * @param string|false $x_redirect_by Optional. The application doing the redirect or false to omit. Default 'WordPress'.
1622         * @return bool False if the redirect was canceled, true otherwise.
1623         */
1624        function wp_safe_redirect( $location, $status = 302, $x_redirect_by = 'WordPress' ) {
1625
1626                // Need to look at the URL the way it will end up in wp_redirect().
1627                $location = wp_sanitize_redirect( $location );
1628
1629                /**
1630                 * Filters the redirect fallback URL for when the provided redirect is not safe (local).
1631                 *
1632                 * @since 4.3.0
1633                 *
1634                 * @param string $fallback_url The fallback URL to use by default.
1635                 * @param int    $status       The HTTP response status code to use.
1636                 */
1637                $fallback_url = apply_filters( 'wp_safe_redirect_fallback', admin_url(), $status );
1638
1639                $location = wp_validate_redirect( $location, $fallback_url );
1640
1641                return wp_redirect( $location, $status, $x_redirect_by );
1642        }
1643endif;
1644
1645if ( ! function_exists( 'wp_validate_redirect' ) ) :
1646        /**
1647         * Validates a URL for use in a redirect.
1648         *
1649         * Checks whether the $location is using an allowed host, if it has an absolute
1650         * path. A plugin can therefore set or remove allowed host(s) to or from the
1651         * list.
1652         *
1653         * If the host is not allowed, then the redirect is to $fallback_url supplied.
1654         *
1655         * @since 2.8.1
1656         *
1657         * @param string $location     The redirect to validate.
1658         * @param string $fallback_url The value to return if $location is not allowed.
1659         * @return string Redirect-sanitized URL.
1660         */
1661        function wp_validate_redirect( $location, $fallback_url = '' ) {
1662                $location = wp_sanitize_redirect( trim( $location, " \t\n\r\0\x08\x0B" ) );
1663                // Browsers will assume 'http' is your protocol, and will obey a redirect to a URL starting with '//'.
1664                if ( str_starts_with( $location, '//' ) ) {
1665                        $location = 'http:' . $location;
1666                }
1667
1668                /*
1669                 * In PHP 5 parse_url() may fail if the URL query part contains 'http://'.
1670                 * See https://bugs.php.net/bug.php?id=38143
1671                 */
1672                $cut  = strpos( $location, '?' );
1673                $test = $cut ? substr( $location, 0, $cut ) : $location;
1674
1675                $lp = parse_url( $test );
1676
1677                // Give up if malformed URL.
1678                if ( false === $lp ) {
1679                        return $fallback_url;
1680                }
1681
1682                // Allow only 'http' and 'https' schemes. No 'data:', etc.
1683                if ( isset( $lp['scheme'] ) && ! ( 'http' === $lp['scheme'] || 'https' === $lp['scheme'] ) ) {
1684                        return $fallback_url;
1685                }
1686
1687                if ( ! isset( $lp['host'] ) && ! empty( $lp['path'] ) && '/' !== $lp['path'][0] ) {
1688                        $path = '';
1689                        if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
1690                                $path = dirname( parse_url( 'http://placeholder' . $_SERVER['REQUEST_URI'], PHP_URL_PATH ) . '?' );
1691                                $path = wp_normalize_path( $path );
1692                        }
1693                        $location = '/' . ltrim( $path . '/', '/' ) . $location;
1694                }
1695
1696                /*
1697                 * Reject if certain components are set but host is not.
1698                 * This catches URLs like https:host.com for which parse_url() does not set the host field.
1699                 */
1700                if ( ! isset( $lp['host'] ) && ( isset( $lp['scheme'] ) || isset( $lp['user'] ) || isset( $lp['pass'] ) || isset( $lp['port'] ) ) ) {
1701                        return $fallback_url;
1702                }
1703
1704                // Reject malformed components parse_url() can return on odd inputs.
1705                foreach ( array( 'user', 'pass', 'host' ) as $component ) {
1706                        if ( isset( $lp[ $component ] ) && strpbrk( $lp[ $component ], ':/?#@' ) ) {
1707                                return $fallback_url;
1708                        }
1709                }
1710
1711                $wpp = parse_url( home_url() );
1712
1713                /**
1714                 * Filters the list of allowed hosts to redirect to.
1715                 *
1716                 * @since 2.3.0
1717                 *
1718                 * @param string[] $hosts An array of allowed host names.
1719                 * @param string   $host  The host name of the redirect destination; empty string if not set.
1720                 */
1721                $allowed_hosts = (array) apply_filters( 'allowed_redirect_hosts', array( $wpp['host'] ), isset( $lp['host'] ) ? $lp['host'] : '' );
1722
1723                if ( isset( $lp['host'] ) && ( ! in_array( $lp['host'], $allowed_hosts, true ) && strtolower( $wpp['host'] ) !== $lp['host'] ) ) {
1724                        $location = $fallback_url;
1725                }
1726
1727                return $location;
1728        }
1729endif;
1730
1731if ( ! function_exists( 'wp_notify_postauthor' ) ) :
1732        /**
1733         * Notifies an author (and/or others) of a comment/trackback/pingback on a post.
1734         *
1735         * @since 1.0.0
1736         *
1737         * @param int|WP_Comment $comment_id Comment ID or WP_Comment object.
1738         * @param string         $deprecated Not used.
1739         * @return bool True on completion. False if no email addresses were specified.
1740         */
1741        function wp_notify_postauthor( $comment_id, $deprecated = null ) {
1742                if ( null !== $deprecated ) {
1743                        _deprecated_argument( __FUNCTION__, '3.8.0' );
1744                }
1745
1746                $comment = get_comment( $comment_id );
1747                if ( empty( $comment ) || empty( $comment->comment_post_ID ) ) {
1748                        return false;
1749                }
1750
1751                $post   = get_post( $comment->comment_post_ID );
1752                $author = get_userdata( $post->post_author );
1753
1754                // Who to notify? By default, just the post author, but others can be added.
1755                $emails = array();
1756                if ( $author ) {
1757                        $emails[] = $author->user_email;
1758                }
1759
1760                /**
1761                 * Filters the list of email addresses to receive a comment notification.
1762                 *
1763                 * By default, only post authors are notified of comments. This filter allows
1764                 * others to be added.
1765                 *
1766                 * @since 3.7.0
1767                 *
1768                 * @param string[] $emails     An array of email addresses to receive a comment notification.
1769                 * @param string   $comment_id The comment ID as a numeric string.
1770                 */
1771                $emails = apply_filters( 'comment_notification_recipients', $emails, $comment->comment_ID );
1772                $emails = array_filter( $emails );
1773
1774                // If there are no addresses to send the comment to, bail.
1775                if ( ! count( $emails ) ) {
1776                        return false;
1777                }
1778
1779                // Facilitate unsetting below without knowing the keys.
1780                $emails = array_flip( $emails );
1781
1782                /**
1783                 * Filters whether to notify comment authors of their comments on their own posts.
1784                 *
1785                 * By default, comment authors aren't notified of their comments on their own
1786                 * posts. This filter allows you to override that.
1787                 *
1788                 * @since 3.8.0
1789                 *
1790                 * @param bool   $notify     Whether to notify the post author of their own comment.
1791                 *                           Default false.
1792                 * @param string $comment_id The comment ID as a numeric string.
1793                 */
1794                $notify_author = apply_filters( 'comment_notification_notify_author', false, $comment->comment_ID );
1795
1796                // The comment was left by the author.
1797                if ( $author && ! $notify_author && (int) $comment->user_id === (int) $post->post_author ) {
1798                        unset( $emails[ $author->user_email ] );
1799                }
1800
1801                // The author moderated a comment on their own post.
1802                if ( $author && ! $notify_author && get_current_user_id() === (int) $post->post_author ) {
1803                        unset( $emails[ $author->user_email ] );
1804                }
1805
1806                // The post author is no longer a member of the blog.
1807                if ( $author && ! $notify_author && ! user_can( $post->post_author, 'read_post', $post->ID ) ) {
1808                        unset( $emails[ $author->user_email ] );
1809                }
1810
1811                // If there's no email to send the comment to, bail, otherwise flip array back around for use below.
1812                if ( ! count( $emails ) ) {
1813                        return false;
1814                } else {
1815                        $emails = array_flip( $emails );
1816                }
1817
1818                $comment_author_domain = '';
1819                if ( WP_Http::is_ip_address( $comment->comment_author_IP ) ) {
1820                        $comment_author_domain = gethostbyaddr( $comment->comment_author_IP );
1821                }
1822
1823                /*
1824                 * The blogname option is escaped with esc_html() on the way into the database in sanitize_option().
1825                 * We want to reverse this for the plain text arena of emails.
1826                 */
1827                $blogname        = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
1828                $comment_content = wp_specialchars_decode( $comment->comment_content );
1829
1830                $wp_email = 'wordpress@' . preg_replace( '#^www\.#', '', wp_parse_url( network_home_url(), PHP_URL_HOST ) );
1831
1832                if ( '' === $comment->comment_author ) {
1833                        $from = "From: \"$blogname\" <$wp_email>";
1834                        if ( '' !== $comment->comment_author_email ) {
1835                                $reply_to = "Reply-To: $comment->comment_author_email";
1836                        }
1837                } else {
1838                        $from = "From: \"$comment->comment_author\" <$wp_email>";
1839                        if ( '' !== $comment->comment_author_email ) {
1840                                $reply_to = "Reply-To: \"$comment->comment_author_email\" <$comment->comment_author_email>";
1841                        }
1842                }
1843
1844                $message_headers = "$from\n"
1845                . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
1846
1847                if ( isset( $reply_to ) ) {
1848                        $message_headers .= $reply_to . "\n";
1849                }
1850
1851                /**
1852                 * Filters the comment notification email headers.
1853                 *
1854                 * @since 1.5.2
1855                 *
1856                 * @param string $message_headers Headers for the comment notification email.
1857                 * @param string $comment_id      Comment ID as a numeric string.
1858                 */
1859                $message_headers = apply_filters( 'comment_notification_headers', $message_headers, $comment->comment_ID );
1860
1861                foreach ( $emails as $email ) {
1862                        $user = get_user_by( 'email', $email );
1863
1864                        if ( $user ) {
1865                                $switched_locale = switch_to_user_locale( $user->ID );
1866                        } else {
1867                                $switched_locale = switch_to_locale( get_locale() );
1868                        }
1869
1870                        switch ( $comment->comment_type ) {
1871                                case 'trackback':
1872                                        /* translators: %s: Post title. */
1873                                        $notify_message = sprintf( __( 'New trackback on your post "%s"' ), $post->post_title ) . "\r\n";
1874                                        /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */
1875                                        $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
1876                                        /* translators: %s: Trackback/pingback/comment author URL. */
1877                                        $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
1878                                        /* translators: %s: Comment text. */
1879                                        $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
1880                                        $notify_message .= __( 'You can see all trackbacks on this post here:' ) . "\r\n";
1881                                        /* translators: Trackback notification email subject. 1: Site title, 2: Post title. */
1882                                        $subject = sprintf( __( '[%1$s] Trackback: "%2$s"' ), $blogname, $post->post_title );
1883                                        break;
1884
1885                                case 'pingback':
1886                                        /* translators: %s: Post title. */
1887                                        $notify_message = sprintf( __( 'New pingback on your post "%s"' ), $post->post_title ) . "\r\n";
1888                                        /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */
1889                                        $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
1890                                        /* translators: %s: Trackback/pingback/comment author URL. */
1891                                        $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
1892                                        /* translators: %s: Comment text. */
1893                                        $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
1894                                        $notify_message .= __( 'You can see all pingbacks on this post here:' ) . "\r\n";
1895                                        /* translators: Pingback notification email subject. 1: Site title, 2: Post title. */
1896                                        $subject = sprintf( __( '[%1$s] Pingback: "%2$s"' ), $blogname, $post->post_title );
1897                                        break;
1898
1899                                case 'note':
1900                                        /* translators: %s: Post title. */
1901                                        $notify_message = sprintf( __( 'New note on your post "%s"' ), $post->post_title ) . "\r\n";
1902                                        /* translators: 1: Note author's name, 2: Note author's IP address, 3: Note author's hostname. */
1903                                        $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
1904                                        /* translators: %s: Note author email. */
1905                                        $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
1906                                        /* translators: %s: Note text. */
1907                                        $notify_message .= sprintf( __( 'Note: %s' ), "\r\n" . ( empty( $comment_content ) ? __( 'resolved/reopened' ) : $comment_content ) ) . "\r\n\r\n";
1908                                        $notify_message .= __( 'You can see all notes on this post here:' ) . "\r\n";
1909                                        /* translators: Note notification email subject. 1: Site title, 2: Post title. */
1910                                        $subject = sprintf( __( '[%1$s] Note: "%2$s"' ), $blogname, $post->post_title );
1911                                        break;
1912
1913                                default: // Comments.
1914                                        /* translators: %s: Post title. */
1915                                        $notify_message = sprintf( __( 'New comment on your post "%s"' ), $post->post_title ) . "\r\n";
1916                                        /* translators: 1: Comment author's name, 2: Comment author's IP address, 3: Comment author's hostname. */
1917                                        $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
1918                                        /* translators: %s: Comment author email. */
1919                                        $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
1920                                        /* translators: %s: Trackback/pingback/comment author URL. */
1921                                        $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
1922
1923                                        if ( $comment->comment_parent && user_can( $post->post_author, 'edit_comment', $comment->comment_parent ) ) {
1924                                                /* translators: Comment moderation. %s: Parent comment edit URL. */
1925                                                $notify_message .= sprintf( __( 'In reply to: %s' ), admin_url( "comment.php?action=editcomment&c={$comment->comment_parent}#wpbody-content" ) ) . "\r\n";
1926                                        }
1927
1928                                        /* translators: %s: Comment text. */
1929                                        $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
1930                                        $notify_message .= __( 'You can see all comments on this post here:' ) . "\r\n";
1931                                        /* translators: Comment notification email subject. 1: Site title, 2: Post title. */
1932                                        $subject = sprintf( __( '[%1$s] Comment: "%2$s"' ), $blogname, $post->post_title );
1933                                        break;
1934                        }
1935
1936                        /* translators: %s: Comment URL. */
1937                        if ( 'note' === $comment->comment_type ) {
1938                                $notify_message .= get_edit_post_link( $comment->comment_post_ID, 'url' ) . "\r\n";
1939                        } else {
1940                                $notify_message .= get_permalink( $comment->comment_post_ID ) . "#comments\r\n\r\n";
1941                                $notify_message .= sprintf( __( 'Permalink: %s' ), get_comment_link( $comment ) ) . "\r\n";
1942                        }
1943
1944                        if ( 'note' !== $comment->comment_type && user_can( $post->post_author, 'edit_comment', $comment->comment_ID ) ) {
1945                                if ( EMPTY_TRASH_DAYS ) {
1946                                        /* translators: Comment moderation. %s: Comment action URL. */
1947                                        $notify_message .= sprintf( __( 'Trash it: %s' ), admin_url( "comment.php?action=trash&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n";
1948                                } else {
1949                                        /* translators: Comment moderation. %s: Comment action URL. */
1950                                        $notify_message .= sprintf( __( 'Delete it: %s' ), admin_url( "comment.php?action=delete&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n";
1951                                }
1952                                /* translators: Comment moderation. %s: Comment action URL. */
1953                                $notify_message .= sprintf( __( 'Spam it: %s' ), admin_url( "comment.php?action=spam&c={$comment->comment_ID}#wpbody-content" ) ) . "\r\n";
1954                        }
1955
1956                        /**
1957                         * Filters the comment notification email text.
1958                         *
1959                         * @since 1.5.2
1960                         *
1961                         * @param string $notify_message The comment notification email text.
1962                         * @param string $comment_id     Comment ID as a numeric string.
1963                         */
1964                        $notify_message = apply_filters( 'comment_notification_text', $notify_message, $comment->comment_ID );
1965
1966                        /**
1967                         * Filters the comment notification email subject.
1968                         *
1969                         * @since 1.5.2
1970                         *
1971                         * @param string $subject    The comment notification email subject.
1972                         * @param string $comment_id Comment ID as a numeric string.
1973                         */
1974                        $subject = apply_filters( 'comment_notification_subject', $subject, $comment->comment_ID );
1975
1976                        wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers );
1977
1978                        if ( $switched_locale ) {
1979                                restore_previous_locale();
1980                        }
1981                }
1982
1983                return true;
1984        }
1985endif;
1986
1987if ( ! function_exists( 'wp_notify_moderator' ) ) :
1988        /**
1989         * Notifies the moderator of the site about a new comment that is awaiting approval.
1990         *
1991         * @since 1.0.0
1992         *
1993         * @global wpdb $wpdb WordPress database abstraction object.
1994         *
1995         * Uses the {@see 'notify_moderator'} filter to determine whether the site moderator
1996         * should be notified, overriding the site setting.
1997         *
1998         * @param int $comment_id Comment ID.
1999         * @return true Always returns true.
2000         */
2001        function wp_notify_moderator( $comment_id ) {
2002                global $wpdb;
2003
2004                $maybe_notify = get_option( 'moderation_notify' );
2005
2006                /**
2007                 * Filters whether to send the site moderator email notifications, overriding the site setting.
2008                 *
2009                 * @since 4.4.0
2010                 *
2011                 * @param bool $maybe_notify Whether to notify blog moderator.
2012                 * @param int  $comment_id   The ID of the comment for the notification.
2013                 */
2014                $maybe_notify = apply_filters( 'notify_moderator', $maybe_notify, $comment_id );
2015
2016                if ( ! $maybe_notify ) {
2017                        return true;
2018                }
2019
2020                $comment = get_comment( $comment_id );
2021                $post    = get_post( $comment->comment_post_ID );
2022                $user    = get_userdata( $post->post_author );
2023                // Send to the administration and to the post author if the author can modify the comment.
2024                $emails = array( get_option( 'admin_email' ) );
2025                if ( $user && user_can( $user->ID, 'edit_comment', $comment_id ) && ! empty( $user->user_email ) ) {
2026                        if ( 0 !== strcasecmp( $user->user_email, get_option( 'admin_email' ) ) ) {
2027                                $emails[] = $user->user_email;
2028                        }
2029                }
2030
2031                $comment_author_domain = '';
2032                if ( WP_Http::is_ip_address( $comment->comment_author_IP ) ) {
2033                        $comment_author_domain = gethostbyaddr( $comment->comment_author_IP );
2034                }
2035
2036                $comments_waiting = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_approved = '0'" );
2037
2038                /*
2039                 * The blogname option is escaped with esc_html() on the way into the database in sanitize_option().
2040                 * We want to reverse this for the plain text arena of emails.
2041                 */
2042                $blogname        = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
2043                $comment_content = wp_specialchars_decode( $comment->comment_content );
2044
2045                $message_headers = '';
2046
2047                /**
2048                 * Filters the list of recipients for comment moderation emails.
2049                 *
2050                 * @since 3.7.0
2051                 *
2052                 * @param string[] $emails     List of email addresses to notify for comment moderation.
2053                 * @param int      $comment_id Comment ID.
2054                 */
2055                $emails = apply_filters( 'comment_moderation_recipients', $emails, $comment_id );
2056
2057                /**
2058                 * Filters the comment moderation email headers.
2059                 *
2060                 * @since 2.8.0
2061                 *
2062                 * @param string $message_headers Headers for the comment moderation email.
2063                 * @param int    $comment_id      Comment ID.
2064                 */
2065                $message_headers = apply_filters( 'comment_moderation_headers', $message_headers, $comment_id );
2066
2067                foreach ( $emails as $email ) {
2068                        $user = get_user_by( 'email', $email );
2069
2070                        if ( $user ) {
2071                                $switched_locale = switch_to_user_locale( $user->ID );
2072                        } else {
2073                                $switched_locale = switch_to_locale( get_locale() );
2074                        }
2075
2076                        switch ( $comment->comment_type ) {
2077                                case 'trackback':
2078                                        /* translators: %s: Post title. */
2079                                        $notify_message  = sprintf( __( 'A new trackback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
2080                                        $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
2081                                        /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */
2082                                        $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
2083                                        /* translators: %s: Trackback/pingback/comment author URL. */
2084                                        $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
2085                                        $notify_message .= __( 'Trackback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n";
2086                                        break;
2087
2088                                case 'pingback':
2089                                        /* translators: %s: Post title. */
2090                                        $notify_message  = sprintf( __( 'A new pingback on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
2091                                        $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
2092                                        /* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */
2093                                        $notify_message .= sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
2094                                        /* translators: %s: Trackback/pingback/comment author URL. */
2095                                        $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
2096                                        $notify_message .= __( 'Pingback excerpt: ' ) . "\r\n" . $comment_content . "\r\n\r\n";
2097                                        break;
2098
2099                                default: // Comments.
2100                                        /* translators: %s: Post title. */
2101                                        $notify_message  = sprintf( __( 'A new comment on the post "%s" is waiting for your approval' ), $post->post_title ) . "\r\n";
2102                                        $notify_message .= get_permalink( $comment->comment_post_ID ) . "\r\n\r\n";
2103                                        /* translators: 1: Comment author's name, 2: Comment author's IP address, 3: Comment author's hostname. */
2104                                        $notify_message .= sprintf( __( 'Author: %1$s (IP address: %2$s, %3$s)' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
2105                                        /* translators: %s: Comment author email. */
2106                                        $notify_message .= sprintf( __( 'Email: %s' ), $comment->comment_author_email ) . "\r\n";
2107                                        /* translators: %s: Trackback/pingback/comment author URL. */
2108                                        $notify_message .= sprintf( __( 'URL: %s' ), $comment->comment_author_url ) . "\r\n";
2109
2110                                        if ( $comment->comment_parent ) {
2111                                                /* translators: Comment moderation. %s: Parent comment edit URL. */
2112                                                $notify_message .= sprintf( __( 'In reply to: %s' ), admin_url( "comment.php?action=editcomment&c={$comment->comment_parent}#wpbody-content" ) ) . "\r\n";
2113                                        }
2114
2115                                        /* translators: %s: Comment text. */
2116                                        $notify_message .= sprintf( __( 'Comment: %s' ), "\r\n" . $comment_content ) . "\r\n\r\n";
2117                                        break;
2118                        }
2119
2120                        /* translators: Comment moderation. %s: Comment action URL. */
2121                        $notify_message .= sprintf( __( 'Approve it: %s' ), admin_url( "comment.php?action=approve&c={$comment_id}#wpbody-content" ) ) . "\r\n";
2122
2123                        if ( EMPTY_TRASH_DAYS ) {
2124                                /* translators: Comment moderation. %s: Comment action URL. */
2125                                $notify_message .= sprintf( __( 'Trash it: %s' ), admin_url( "comment.php?action=trash&c={$comment_id}#wpbody-content" ) ) . "\r\n";
2126                        } else {
2127                                /* translators: Comment moderation. %s: Comment action URL. */
2128                                $notify_message .= sprintf( __( 'Delete it: %s' ), admin_url( "comment.php?action=delete&c={$comment_id}#wpbody-content" ) ) . "\r\n";
2129                        }
2130
2131                        /* translators: Comment moderation. %s: Comment action URL. */
2132                        $notify_message .= sprintf( __( 'Spam it: %s' ), admin_url( "comment.php?action=spam&c={$comment_id}#wpbody-content" ) ) . "\r\n";
2133
2134                        $notify_message .= sprintf(
2135                                /* translators: Comment moderation. %s: Number of comments awaiting approval. */
2136                                _n(
2137                                        'Currently %s comment is waiting for approval. Please visit the moderation panel:',
2138                                        'Currently %s comments are waiting for approval. Please visit the moderation panel:',
2139                                        $comments_waiting
2140                                ),
2141                                number_format_i18n( $comments_waiting )
2142                        ) . "\r\n";
2143                        $notify_message .= admin_url( 'edit-comments.php?comment_status=moderated#wpbody-content' ) . "\r\n";
2144
2145                        /* translators: Comment moderation notification email subject. 1: Site title, 2: Post title. */
2146                        $subject = sprintf( __( '[%1$s] Please moderate: "%2$s"' ), $blogname, $post->post_title );
2147
2148                        /**
2149                         * Filters the comment moderation email text.
2150                         *
2151                         * @since 1.5.2
2152                         *
2153                         * @param string $notify_message Text of the comment moderation email.
2154                         * @param int    $comment_id     Comment ID.
2155                         */
2156                        $notify_message = apply_filters( 'comment_moderation_text', $notify_message, $comment_id );
2157
2158                        /**
2159                         * Filters the comment moderation email subject.
2160                         *
2161                         * @since 1.5.2
2162                         *
2163                         * @param string $subject    Subject of the comment moderation email.
2164                         * @param int    $comment_id Comment ID.
2165                         */
2166                        $subject = apply_filters( 'comment_moderation_subject', $subject, $comment_id );
2167
2168                        wp_mail( $email, wp_specialchars_decode( $subject ), $notify_message, $message_headers );
2169
2170                        if ( $switched_locale ) {
2171                                restore_previous_locale();
2172                        }
2173                }
2174
2175                return true;
2176        }
2177endif;
2178
2179if ( ! function_exists( 'wp_password_change_notification' ) ) :
2180        /**
2181         * Notifies the blog admin of a user changing password, normally via email.
2182         *
2183         * @since 2.7.0
2184         *
2185         * @param WP_User $user User object.
2186         */
2187        function wp_password_change_notification( $user ) {
2188                /*
2189                 * Send a copy of password change notification to the admin,
2190                 * but check to see if it's the admin whose password we're changing, and skip this.
2191                 */
2192                if ( 0 !== strcasecmp( $user->user_email, get_option( 'admin_email' ) ) ) {
2193
2194                        $admin_user = get_user_by( 'email', get_option( 'admin_email' ) );
2195
2196                        if ( $admin_user ) {
2197                                $switched_locale = switch_to_user_locale( $admin_user->ID );
2198                        } else {
2199                                $switched_locale = switch_to_locale( get_locale() );
2200                        }
2201
2202                        /* translators: %s: User name. */
2203                        $message = sprintf( __( 'Password changed for user: %s' ), $user->user_login ) . "\r\n";
2204                        /*
2205                         * The blogname option is escaped with esc_html() on the way into the database in sanitize_option().
2206                         * We want to reverse this for the plain text arena of emails.
2207                         */
2208                        $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
2209
2210                        $wp_password_change_notification_email = array(
2211                                'to'      => get_option( 'admin_email' ),
2212                                /* translators: Password change notification email subject. %s: Site title. */
2213                                'subject' => __( '[%s] Password Changed' ),
2214                                'message' => $message,
2215                                'headers' => '',
2216                        );
2217
2218                        /**
2219                         * Filters the contents of the password change notification email sent to the site admin.
2220                         *
2221                         * @since 4.9.0
2222                         *
2223                         * @param array   $wp_password_change_notification_email {
2224                         *     Used to build wp_mail().
2225                         *
2226                         *     @type string $to      The intended recipient - site admin email address.
2227                         *     @type string $subject The subject of the email.
2228                         *     @type string $message The body of the email.
2229                         *     @type string $headers The headers of the email.
2230                         * }
2231                         * @param WP_User $user     User object for user whose password was changed.
2232                         * @param string  $blogname The site title.
2233                         */
2234                        $wp_password_change_notification_email = apply_filters( 'wp_password_change_notification_email', $wp_password_change_notification_email, $user, $blogname );
2235
2236                        wp_mail(
2237                                $wp_password_change_notification_email['to'],
2238                                wp_specialchars_decode( sprintf( $wp_password_change_notification_email['subject'], $blogname ) ),
2239                                $wp_password_change_notification_email['message'],
2240                                $wp_password_change_notification_email['headers']
2241                        );
2242
2243                        if ( $switched_locale ) {
2244                                restore_previous_locale();
2245                        }
2246                }
2247        }
2248endif;
2249
2250if ( ! function_exists( 'wp_new_user_notification' ) ) :
2251        /**
2252         * Emails login credentials to a newly-registered user.
2253         *
2254         * A new user registration notification is also sent to admin email.
2255         *
2256         * @since 2.0.0
2257         * @since 4.3.0 The `$plaintext_pass` parameter was changed to `$notify`.
2258         * @since 4.3.1 The `$plaintext_pass` parameter was deprecated. `$notify` added as a third parameter.
2259         * @since 4.6.0 The `$notify` parameter accepts 'user' for sending notification only to the user created.
2260         *
2261         * @param int    $user_id    User ID.
2262         * @param null   $deprecated Not used (argument deprecated).
2263         * @param string $notify     Optional. Type of notification that should happen. Accepts 'admin' or an empty
2264         *                           string (admin only), 'user', or 'both' (admin and user). Default empty.
2265         */
2266        function wp_new_user_notification( $user_id, $deprecated = null, $notify = '' ) {
2267                if ( null !== $deprecated ) {
2268                        _deprecated_argument( __FUNCTION__, '4.3.1' );
2269                }
2270
2271                // Accepts only 'user', 'admin' , 'both' or default '' as $notify.
2272                if ( ! in_array( $notify, array( 'user', 'admin', 'both', '' ), true ) ) {
2273                        return;
2274                }
2275
2276                $user = get_userdata( $user_id );
2277
2278                /*
2279                 * The blogname option is escaped with esc_html() on the way into the database in sanitize_option().
2280                 * We want to reverse this for the plain text arena of emails.
2281                 */
2282                $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
2283
2284                /**
2285                 * Filters whether the admin is notified of a new user registration.
2286                 *
2287                 * @since 6.1.0
2288                 *
2289                 * @param bool    $send Whether to send the email. Default true.
2290                 * @param WP_User $user User object for new user.
2291                 */
2292                $send_notification_to_admin = apply_filters( 'wp_send_new_user_notification_to_admin', true, $user );
2293
2294                if ( 'user' !== $notify && true === $send_notification_to_admin ) {
2295
2296                        $admin_user = get_user_by( 'email', get_option( 'admin_email' ) );
2297
2298                        if ( $admin_user ) {
2299                                $switched_locale = switch_to_user_locale( $admin_user->ID );
2300                        } else {
2301                                $switched_locale = switch_to_locale( get_locale() );
2302                        }
2303
2304                        /* translators: %s: Site title. */
2305                        $message = sprintf( __( 'New user registration on your site %s:' ), $blogname ) . "\r\n\r\n";
2306                        /* translators: %s: User login. */
2307                        $message .= sprintf( __( 'Username: %s' ), $user->user_login ) . "\r\n\r\n";
2308                        /* translators: %s: User email address. */
2309                        $message .= sprintf( __( 'Email: %s' ), $user->user_email ) . "\r\n";
2310
2311                        $wp_new_user_notification_email_admin = array(
2312                                'to'      => get_option( 'admin_email' ),
2313                                /* translators: New user registration notification email subject. %s: Site title. */
2314                                'subject' => __( '[%s] New User Registration' ),
2315                                'message' => $message,
2316                                'headers' => '',
2317                        );
2318
2319                        /**
2320                         * Filters the contents of the new user notification email sent to the site admin.
2321                         *
2322                         * @since 4.9.0
2323                         *
2324                         * @param array   $wp_new_user_notification_email_admin {
2325                         *     Used to build wp_mail().
2326                         *
2327                         *     @type string $to      The intended recipient - site admin email address.
2328                         *     @type string $subject The subject of the email.
2329                         *     @type string $message The body of the email.
2330                         *     @type string $headers The headers of the email.
2331                         * }
2332                         * @param WP_User $user     User object for new user.
2333                         * @param string  $blogname The site title.
2334                         */
2335                        $wp_new_user_notification_email_admin = apply_filters( 'wp_new_user_notification_email_admin', $wp_new_user_notification_email_admin, $user, $blogname );
2336
2337                        wp_mail(
2338                                $wp_new_user_notification_email_admin['to'],
2339                                wp_specialchars_decode( sprintf( $wp_new_user_notification_email_admin['subject'], $blogname ) ),
2340                                $wp_new_user_notification_email_admin['message'],
2341                                $wp_new_user_notification_email_admin['headers']
2342                        );
2343
2344                        if ( $switched_locale ) {
2345                                restore_previous_locale();
2346                        }
2347                }
2348
2349                /**
2350                 * Filters whether the user is notified of their new user registration.
2351                 *
2352                 * @since 6.1.0
2353                 *
2354                 * @param bool    $send Whether to send the email. Default true.
2355                 * @param WP_User $user User object for new user.
2356                 */
2357                $send_notification_to_user = apply_filters( 'wp_send_new_user_notification_to_user', true, $user );
2358
2359                // `$deprecated` was pre-4.3 `$plaintext_pass`. An empty `$plaintext_pass` didn't sent a user notification.
2360                if ( 'admin' === $notify || true !== $send_notification_to_user || ( empty( $deprecated ) && empty( $notify ) ) ) {
2361                        return;
2362                }
2363
2364                $key = get_password_reset_key( $user );
2365                if ( is_wp_error( $key ) ) {
2366                        return;
2367                }
2368
2369                $switched_locale = switch_to_user_locale( $user_id );
2370
2371                /* translators: %s: User login. */
2372                $message  = sprintf( __( 'Username: %s' ), $user->user_login ) . "\r\n\r\n";
2373                $message .= __( 'To set your password, visit the following address:' ) . "\r\n\r\n";
2374
2375                /*
2376                 * Since some user login names end in a period, this could produce ambiguous URLs that
2377                 * end in a period. To avoid the ambiguity, ensure that the login is not the last query
2378                 * arg in the URL. If moving it to the end, a trailing period will need to be escaped.
2379                 *
2380                 * @see https://core.trac.wordpress.org/tickets/42957
2381                 */
2382                $message .= network_site_url( 'wp-login.php?login=' . rawurlencode( $user->user_login ) . "&key=$key&action=rp", 'login' ) . "\r\n\r\n";
2383
2384                $message .= wp_login_url() . "\r\n";
2385
2386                $wp_new_user_notification_email = array(
2387                        'to'      => $user->user_email,
2388                        /* translators: Login details notification email subject. %s: Site title. */
2389                        'subject' => __( '[%s] Login Details' ),
2390                        'message' => $message,
2391                        'headers' => '',
2392                );
2393
2394                /**
2395                 * Filters the contents of the new user notification email sent to the new user.
2396                 *
2397                 * @since 4.9.0
2398                 *
2399                 * @param array   $wp_new_user_notification_email {
2400                 *     Used to build wp_mail().
2401                 *
2402                 *     @type string $to      The intended recipient - New user email address.
2403                 *     @type string $subject The subject of the email.
2404                 *     @type string $message The body of the email.
2405                 *     @type string $headers The headers of the email.
2406                 * }
2407                 * @param WP_User $user     User object for new user.
2408                 * @param string  $blogname The site title.
2409                 */
2410                $wp_new_user_notification_email = apply_filters( 'wp_new_user_notification_email', $wp_new_user_notification_email, $user, $blogname );
2411
2412                wp_mail(
2413                        $wp_new_user_notification_email['to'],
2414                        wp_specialchars_decode( sprintf( $wp_new_user_notification_email['subject'], $blogname ) ),
2415                        $wp_new_user_notification_email['message'],
2416                        $wp_new_user_notification_email['headers']
2417                );
2418
2419                if ( $switched_locale ) {
2420                        restore_previous_locale();
2421                }
2422        }
2423endif;
2424
2425if ( ! function_exists( 'wp_nonce_tick' ) ) :
2426        /**
2427         * Returns the time-dependent variable for nonce creation.
2428         *
2429         * A nonce has a lifespan of two ticks. Nonces in their second tick may be
2430         * updated, e.g. by autosave.
2431         *
2432         * @since 2.5.0
2433         * @since 6.1.0 Added `$action` argument.
2434         *
2435         * @param string|int $action Optional. The nonce action. Default -1.
2436         * @return float Float value rounded up to the next highest integer.
2437         */
2438        function wp_nonce_tick( $action = -1 ) {
2439                /**
2440                 * Filters the lifespan of nonces in seconds.
2441                 *
2442                 * @since 2.5.0
2443                 * @since 6.1.0 Added `$action` argument to allow for more targeted filters.
2444                 *
2445                 * @param int        $lifespan Lifespan of nonces in seconds. Default 86,400 seconds, or one day.
2446                 * @param string|int $action   The nonce action, or -1 if none was provided.
2447                 */
2448                $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS, $action );
2449
2450                return ceil( time() / ( $nonce_life / 2 ) );
2451        }
2452endif;
2453
2454if ( ! function_exists( 'wp_verify_nonce' ) ) :
2455        /**
2456         * Verifies that a correct security nonce was used with time limit.
2457         *
2458         * A nonce is valid for between 12 and 24 hours (by default).
2459         *
2460         * @since 2.0.3
2461         *
2462         * @param string     $nonce  Nonce value that was used for verification, usually via a form field.
2463         * @param string|int $action Should give context to what is taking place and be the same when nonce was created.
2464         * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago,
2465         *                   2 if the nonce is valid and generated between 12-24 hours ago.
2466         *                   False if the nonce is invalid.
2467         */
2468        function wp_verify_nonce( $nonce, $action = -1 ) {
2469                $nonce = (string) $nonce;
2470                $user  = wp_get_current_user();
2471                $uid   = (int) $user->ID;
2472                if ( ! $uid ) {
2473                        /**
2474                         * Filters whether the user who generated the nonce is logged out.
2475                         *
2476                         * @since 3.5.0
2477                         *
2478                         * @param int        $uid    ID of the nonce-owning user.
2479                         * @param string|int $action The nonce action, or -1 if none was provided.
2480                         */
2481                        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
2482                }
2483
2484                if ( empty( $nonce ) ) {
2485                        return false;
2486                }
2487
2488                $token = wp_get_session_token();
2489                $i     = wp_nonce_tick( $action );
2490
2491                // Nonce generated 0-12 hours ago.
2492                $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
2493                if ( hash_equals( $expected, $nonce ) ) {
2494                        return 1;
2495                }
2496
2497                // Nonce generated 12-24 hours ago.
2498                $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
2499                if ( hash_equals( $expected, $nonce ) ) {
2500                        return 2;
2501                }
2502
2503                /**
2504                 * Fires when nonce verification fails.
2505                 *
2506                 * @since 4.4.0
2507                 *
2508                 * @param string     $nonce  The invalid nonce.
2509                 * @param string|int $action The nonce action.
2510                 * @param WP_User    $user   The current user object.
2511                 * @param string     $token  The user's session token.
2512                 */
2513                do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token );
2514
2515                // Invalid nonce.
2516                return false;
2517        }
2518endif;
2519
2520if ( ! function_exists( 'wp_create_nonce' ) ) :
2521        /**
2522         * Creates a cryptographic token tied to a specific action, user, user session,
2523         * and window of time.
2524         *
2525         * @since 2.0.3
2526         * @since 4.0.0 Session tokens were integrated with nonce creation.
2527         *
2528         * @param string|int $action Scalar value to add context to the nonce.
2529         * @return string The token.
2530         */
2531        function wp_create_nonce( $action = -1 ) {
2532                $user = wp_get_current_user();
2533                $uid  = (int) $user->ID;
2534                if ( ! $uid ) {
2535                        /** This filter is documented in wp-includes/pluggable.php */
2536                        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
2537                }
2538
2539                $token = wp_get_session_token();
2540                $i     = wp_nonce_tick( $action );
2541
2542                return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
2543        }
2544endif;
2545
2546if ( ! function_exists( 'wp_salt' ) ) :
2547        /**
2548         * Returns a salt to add to hashes.
2549         *
2550         * Salts are created using secret keys. Secret keys are located in two places:
2551         * in the database and in the wp-config.php file. The secret key in the database
2552         * is randomly generated and will be appended to the secret keys in wp-config.php.
2553         *
2554         * The secret keys in wp-config.php should be updated to strong, random keys to maximize
2555         * security. Below is an example of how the secret key constants are defined.
2556         * Do not paste this example directly into wp-config.php. Instead, have a
2557         * {@link https://api.wordpress.org/secret-key/1.1/salt/ secret key created} just
2558         * for you.
2559         *
2560         *     define('AUTH_KEY',         ' Xakm<o xQy rw4EMsLKM-?!T+,PFF})H4lzcW57AF0U@N@< >M%G4Yt>f`z]MON');
2561         *     define('SECURE_AUTH_KEY',  'LzJ}op]mr|6+![P}Ak:uNdJCJZd>(Hx.-Mh#Tz)pCIU#uGEnfFz|f ;;eU%/U^O~');
2562         *     define('LOGGED_IN_KEY',    '|i|Ux`9<p-h$aFf(qnT:sDO:D1P^wZ$$/Ra@miTJi9G;ddp_<q}6H1)o|a +&JCM');
2563         *     define('NONCE_KEY',        '%:R{[P|,s.KuMltH5}cI;/k<Gx~j!f0I)m_sIyu+&NJZ)-iO>z7X>QYR0Z_XnZ@|');
2564         *     define('AUTH_SALT',        'eZyT)-Naw]F8CwA*VaW#q*|.)g@o}||wf~@C-YSt}(dh_r6EbI#A,y|nU2{B#JBW');
2565         *     define('SECURE_AUTH_SALT', '!=oLUTXh,QW=H `}`L|9/^4-3 STz},T(w}W<I`.JjPi)<Bmf1v,HpGe}T1:Xt7n');
2566         *     define('LOGGED_IN_SALT',   '+XSqHc;@Q*K_b|Z?NC[3H!!EONbh.n<+=uKR:>*c(u`g~EJBf#8u#R{mUEZrozmm');
2567         *     define('NONCE_SALT',       'h`GXHhD>SLWVfg1(1(N{;.V!MoE(SfbA_ksP@&`+AycHcAV$+?@3q+rxV{%^VyKT');
2568         *
2569         * Salting passwords helps against tools which has stored hashed values of
2570         * common dictionary strings. The added values makes it harder to crack.
2571         *
2572         * @since 2.5.0
2573         *
2574         * @link https://api.wordpress.org/secret-key/1.1/salt/ Create secrets for wp-config.php
2575         *
2576         * @param string $scheme Authentication scheme (auth, secure_auth, logged_in, nonce).
2577         * @return string Salt value
2578         */
2579        function wp_salt( $scheme = 'auth' ) {
2580                static $cached_salts = array();
2581                if ( isset( $cached_salts[ $scheme ] ) ) {
2582                        /**
2583                         * Filters the WordPress salt.
2584                         *
2585                         * @since 2.5.0
2586                         *
2587                         * @param string $cached_salt Cached salt for the given scheme.
2588                         * @param string $scheme      Authentication scheme. Values include 'auth',
2589                         *                            'secure_auth', 'logged_in', and 'nonce'.
2590                         */
2591                        return apply_filters( 'salt', $cached_salts[ $scheme ], $scheme );
2592                }
2593
2594                static $duplicated_keys;
2595                if ( null === $duplicated_keys ) {
2596                        $duplicated_keys = array();
2597
2598                        foreach ( array( 'AUTH', 'SECURE_AUTH', 'LOGGED_IN', 'NONCE', 'SECRET' ) as $first ) {
2599                                foreach ( array( 'KEY', 'SALT' ) as $second ) {
2600                                        if ( ! defined( "{$first}_{$second}" ) ) {
2601                                                continue;
2602                                        }
2603                                        $value                     = constant( "{$first}_{$second}" );
2604                                        $duplicated_keys[ $value ] = isset( $duplicated_keys[ $value ] );
2605                                }
2606                        }
2607
2608                        $duplicated_keys['put your unique phrase here'] = true;
2609
2610                        /*
2611                         * translators: This string should only be translated if wp-config-sample.php is localized.
2612                         * You can check the localized release package or
2613                         * https://i18n.svn.wordpress.org/<locale code>/branches/<wp version>/dist/wp-config-sample.php
2614                         */
2615                        $duplicated_keys[ __( 'put your unique phrase here' ) ] = true;
2616                }
2617
2618                /*
2619                 * Determine which options to prime.
2620                 *
2621                 * If the salt keys are undefined, use a duplicate value or the
2622                 * default `put your unique phrase here` value the salt will be
2623                 * generated via `wp_generate_password()` and stored as a site
2624                 * option. These options will be primed to avoid repeated
2625                 * database requests for undefined salts.
2626                 */
2627                $options_to_prime = array();
2628                foreach ( array( 'auth', 'secure_auth', 'logged_in', 'nonce' ) as $key ) {
2629                        foreach ( array( 'key', 'salt' ) as $second ) {
2630                                $const = strtoupper( "{$key}_{$second}" );
2631                                if ( ! defined( $const ) || true === $duplicated_keys[ constant( $const ) ] ) {
2632                                        $options_to_prime[] = "{$key}_{$second}";
2633                                }
2634                        }
2635                }
2636
2637                if ( ! empty( $options_to_prime ) ) {
2638                        /*
2639                         * Also prime `secret_key` used for undefined salting schemes.
2640                         *
2641                         * If the scheme is unknown, the default value for `secret_key` will be
2642                         * used too for the salt. This should rarely happen, so the option is only
2643                         * primed if other salts are undefined.
2644                         *
2645                         * At this point of execution it is known that a database call will be made
2646                         * to prime salts, so the `secret_key` option can be primed regardless of the
2647                         * constants status.
2648                         */
2649                        $options_to_prime[] = 'secret_key';
2650                        wp_prime_site_option_caches( $options_to_prime );
2651                }
2652
2653                $values = array(
2654                        'key'  => '',
2655                        'salt' => '',
2656                );
2657                if ( defined( 'SECRET_KEY' ) && SECRET_KEY && empty( $duplicated_keys[ SECRET_KEY ] ) ) {
2658                        $values['key'] = SECRET_KEY;
2659                }
2660                if ( 'auth' === $scheme && defined( 'SECRET_SALT' ) && SECRET_SALT && empty( $duplicated_keys[ SECRET_SALT ] ) ) {
2661                        $values['salt'] = SECRET_SALT;
2662                }
2663
2664                if ( in_array( $scheme, array( 'auth', 'secure_auth', 'logged_in', 'nonce' ), true ) ) {
2665                        foreach ( array( 'key', 'salt' ) as $type ) {
2666                                $const = strtoupper( "{$scheme}_{$type}" );
2667                                if ( defined( $const ) && constant( $const ) && empty( $duplicated_keys[ constant( $const ) ] ) ) {
2668                                        $values[ $type ] = constant( $const );
2669                                } elseif ( ! $values[ $type ] ) {
2670                                        $values[ $type ] = get_site_option( "{$scheme}_{$type}" );
2671                                        if ( ! $values[ $type ] ) {
2672                                                $values[ $type ] = wp_generate_password( 64, true, true );
2673                                                update_site_option( "{$scheme}_{$type}", $values[ $type ] );
2674                                        }
2675                                }
2676                        }
2677                } else {
2678                        if ( ! $values['key'] ) {
2679                                $values['key'] = get_site_option( 'secret_key' );
2680                                if ( ! $values['key'] ) {
2681                                        $values['key'] = wp_generate_password( 64, true, true );
2682                                        update_site_option( 'secret_key', $values['key'] );
2683                                }
2684                        }
2685                        $values['salt'] = hash_hmac( 'md5', $scheme, $values['key'] );
2686                }
2687
2688                $cached_salts[ $scheme ] = $values['key'] . $values['salt'];
2689
2690                /** This filter is documented in wp-includes/pluggable.php */
2691                return apply_filters( 'salt', $cached_salts[ $scheme ], $scheme );
2692        }
2693endif;
2694
2695if ( ! function_exists( 'wp_hash' ) ) :
2696        /**
2697         * Gets the hash of the given string.
2698         *
2699         * The default algorithm is md5 but can be changed to any algorithm supported by
2700         * `hash_hmac()`. Use the `hash_hmac_algos()` function to check the supported
2701         * algorithms.
2702         *
2703         * @since 2.0.3
2704         * @since 6.8.0 The `$algo` parameter was added.
2705         *
2706         * @throws InvalidArgumentException if the hashing algorithm is not supported.
2707         *
2708         * @param string $data   Plain text to hash.
2709         * @param string $scheme Authentication scheme (auth, secure_auth, logged_in, nonce).
2710         * @param string $algo   Hashing algorithm to use. Default: 'md5'.
2711         * @return string Hash of $data.
2712         */
2713        function wp_hash( $data, $scheme = 'auth', $algo = 'md5' ) {
2714                $salt = wp_salt( $scheme );
2715
2716                // Ensure the algorithm is supported by the hash_hmac function.
2717                if ( ! in_array( $algo, hash_hmac_algos(), true ) ) {
2718                        throw new InvalidArgumentException(
2719                                sprintf(
2720                                        /* translators: 1: Name of a cryptographic hash algorithm. 2: List of supported algorithms. */
2721                                        __( 'Unsupported hashing algorithm: %1$s. Supported algorithms are: %2$s' ),
2722                                        $algo,
2723                                        implode( ', ', hash_hmac_algos() )
2724                                )
2725                        );
2726                }
2727
2728                return hash_hmac( $algo, $data, $salt );
2729        }
2730endif;
2731
2732if ( ! function_exists( 'wp_hash_password' ) ) :
2733        /**
2734         * Creates a hash of a plain text password.
2735         *
2736         * For integration with other applications, this function can be overwritten to
2737         * instead use the other package password hashing algorithm.
2738         *
2739         * @since 2.5.0
2740         * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
2741         *
2742         * @global PasswordHash $wp_hasher phpass object.
2743         *
2744         * @param string $password Plain text user password to hash.
2745         * @return string The hash string of the password.
2746         */
2747        function wp_hash_password(
2748                #[\SensitiveParameter]
2749                $password
2750        ) {
2751                global $wp_hasher;
2752
2753                if ( ! empty( $wp_hasher ) ) {
2754                        return $wp_hasher->HashPassword( trim( $password ) );
2755                }
2756
2757                if ( strlen( $password ) > 4096 ) {
2758                        return '*';
2759                }
2760
2761                /**
2762                 * Filters the hashing algorithm to use in the password_hash() and password_needs_rehash() functions.
2763                 *
2764                 * The default is the value of the `PASSWORD_BCRYPT` constant which means bcrypt is used.
2765                 *
2766                 * **Important:** The only password hashing algorithm that is guaranteed to be available across PHP
2767                 * installations is bcrypt. If you use any other algorithm you must make sure that it is available on
2768                 * the server. The `password_algos()` function can be used to check which hashing algorithms are available.
2769                 *
2770                 * The hashing options can be controlled via the {@see 'wp_hash_password_options'} filter.
2771                 *
2772                 * Other available constants include:
2773                 *
2774                 * - `PASSWORD_ARGON2I`
2775                 * - `PASSWORD_ARGON2ID`
2776                 * - `PASSWORD_DEFAULT`
2777                 *
2778                 * The values of the algorithm constants are strings in PHP 7.4+ and integers in PHP 7.3 and earlier.
2779                 *
2780                 * @since 6.8.0
2781                 *
2782                 * @param string|int $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant.
2783                 */
2784                $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
2785
2786                /**
2787                 * Filters the options passed to the password_hash() and password_needs_rehash() functions.
2788                 *
2789                 * The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'}
2790                 * filter. You must ensure that the options are appropriate for the algorithm in use.
2791                 *
2792                 * The values of the algorithm constants are strings in PHP 7.4+ and integers in PHP 7.3 and earlier.
2793                 *
2794                 * @since 6.8.0
2795                 *
2796                 * @param array      $options   Array of options to pass to the password hashing functions.
2797                 *                              By default this is an empty array which means the default
2798                 *                              options will be used.
2799                 * @param string|int $algorithm The hashing algorithm in use.
2800                 */
2801                $options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
2802
2803                // Algorithms other than bcrypt don't need to use pre-hashing.
2804                if ( PASSWORD_BCRYPT !== $algorithm ) {
2805                        return password_hash( $password, $algorithm, $options );
2806                }
2807
2808                // Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation.
2809                $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) );
2810
2811                // Add a prefix to facilitate distinguishing vanilla bcrypt hashes.
2812                return '$wp' . password_hash( $password_to_hash, $algorithm, $options );
2813        }
2814endif;
2815
2816if ( ! function_exists( 'wp_check_password' ) ) :
2817        /**
2818         * Checks a plaintext password against a hashed password.
2819         *
2820         * Note that this function may be used to check a value that is not a user password.
2821         * A plugin may use this function to check a password of a different type, and there
2822         * may not always be a user ID associated with the password.
2823         *
2824         * For integration with other applications, this function can be overwritten to
2825         * instead use the other package password hashing algorithm.
2826         *
2827         * @since 2.5.0
2828         * @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A
2829         *              password that wasn't hashed with bcrypt will be checked with phpass.
2830         *
2831         * @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying
2832         *                                 passwords that were hashed with phpass.
2833         *
2834         * @param string     $password Plaintext password.
2835         * @param string     $hash     Hash of the password to check against.
2836         * @param string|int $user_id  Optional. ID of a user associated with the password.
2837         * @return bool False, if the $password does not match the hashed password.
2838         */
2839        function wp_check_password(
2840                #[\SensitiveParameter]
2841                $password,
2842                $hash,
2843                $user_id = ''
2844        ) {
2845                global $wp_hasher;
2846
2847                if ( strlen( $hash ) <= 32 ) {
2848                        // Check the hash using md5 regardless of the current hashing mechanism.
2849                        $check = hash_equals( $hash, md5( $password ) );
2850                } elseif ( ! empty( $wp_hasher ) ) {
2851                        // Check the password using the overridden hasher.
2852                        $check = $wp_hasher->CheckPassword( $password, $hash );
2853                } elseif ( strlen( $password ) > 4096 ) {
2854                        // Passwords longer than 4096 characters are not supported.
2855                        $check = false;
2856                } elseif ( str_starts_with( $hash, '$wp' ) ) {
2857                        // Check the password using the current prefixed hash.
2858                        $password_to_verify = base64_encode( hash_hmac( 'sha384', $password, 'wp-sha384', true ) );
2859                        $check              = password_verify( $password_to_verify, substr( $hash, 3 ) );
2860                } elseif ( str_starts_with( $hash, '$P$' ) ) {
2861                        // Check the password using phpass.
2862                        require_once ABSPATH . WPINC . '/class-phpass.php';
2863                        $check = ( new PasswordHash( 8, true ) )->CheckPassword( $password, $hash );
2864                } else {
2865                        // Check the password using compat support for any non-prefixed hash.
2866                        $check = password_verify( $password, $hash );
2867                }
2868
2869                /**
2870                 * Filters whether the plaintext password matches the hashed password.
2871                 *
2872                 * @since 2.5.0
2873                 * @since 6.8.0 Passwords are now hashed with bcrypt by default.
2874                 *              Old passwords may still be hashed with phpass or md5.
2875                 *
2876                 * @param bool       $check    Whether the passwords match.
2877                 * @param string     $password The plaintext password.
2878                 * @param string     $hash     The hashed password.
2879                 * @param string|int $user_id  Optional ID of a user associated with the password.
2880                 *                             Can be empty.
2881                 */
2882                return apply_filters( 'check_password', $check, $password, $hash, $user_id );
2883        }
2884endif;
2885
2886if ( ! function_exists( 'wp_password_needs_rehash' ) ) :
2887        /**
2888         * Checks whether a password hash needs to be rehashed.
2889         *
2890         * Passwords are hashed with bcrypt using the default cost. A password hashed in a prior version
2891         * of WordPress may still be hashed with phpass and will need to be rehashed. If the default cost
2892         * or algorithm is changed in PHP or WordPress then a password hashed in a previous version will
2893         * need to be rehashed.
2894         *
2895         * Note that, just like wp_check_password(), this function may be used to check a value that is
2896         * not a user password. A plugin may use this function to check a password of a different type,
2897         * and there may not always be a user ID associated with the password.
2898         *
2899         * @since 6.8.0
2900         *
2901         * @global PasswordHash $wp_hasher phpass object.
2902         *
2903         * @param string     $hash    Hash of a password to check.
2904         * @param string|int $user_id Optional. ID of a user associated with the password.
2905         * @return bool Whether the hash needs to be rehashed.
2906         */
2907        function wp_password_needs_rehash( $hash, $user_id = '' ) {
2908                global $wp_hasher;
2909
2910                if ( ! empty( $wp_hasher ) ) {
2911                        return false;
2912                }
2913
2914                /** This filter is documented in wp-includes/pluggable.php */
2915                $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
2916
2917                /** This filter is documented in wp-includes/pluggable.php */
2918                $options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
2919
2920                $prefixed = str_starts_with( $hash, '$wp' );
2921
2922                if ( ( PASSWORD_BCRYPT === $algorithm ) && ! $prefixed ) {
2923                        // If bcrypt is in use and the hash is not prefixed then it needs to be rehashed.
2924                        $needs_rehash = true;
2925                } else {
2926                        // Otherwise check the hash minus its prefix if necessary.
2927                        $hash_to_check = $prefixed ? substr( $hash, 3 ) : $hash;
2928                        $needs_rehash  = password_needs_rehash( $hash_to_check, $algorithm, $options );
2929                }
2930
2931                /**
2932                 * Filters whether the password hash needs to be rehashed.
2933                 *
2934                 * @since 6.8.0
2935                 *
2936                 * @param bool       $needs_rehash Whether the password hash needs to be rehashed.
2937                 * @param string     $hash         The password hash.
2938                 * @param string|int $user_id      Optional. ID of a user associated with the password.
2939                 */
2940                return apply_filters( 'password_needs_rehash', $needs_rehash, $hash, $user_id );
2941        }
2942endif;
2943
2944if ( ! function_exists( 'wp_generate_password' ) ) :
2945        /**
2946         * Generates a random password drawn from the defined set of characters.
2947         *
2948         * Uses wp_rand() to create passwords with far less predictability
2949         * than similar native PHP functions like `rand()` or `mt_rand()`.
2950         *
2951         * @since 2.5.0
2952         *
2953         * @param int  $length              Optional. The length of password to generate. Default 12.
2954         * @param bool $special_chars       Optional. Whether to include standard special characters.
2955         *                                  Default true.
2956         * @param bool $extra_special_chars Optional. Whether to include other special characters.
2957         *                                  Used when generating secret keys and salts. Default false.
2958         * @return string The random password.
2959         */
2960        function wp_generate_password( $length = 12, $special_chars = true, $extra_special_chars = false ) {
2961                $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
2962                if ( $special_chars ) {
2963                        $chars .= '!@#$%^&*()';
2964                }
2965                if ( $extra_special_chars ) {
2966                        $chars .= '-_ []{}<>~`+=,.;:/?|';
2967                }
2968
2969                $password = '';
2970                for ( $i = 0; $i < $length; $i++ ) {
2971                        $password .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
2972                }
2973
2974                /**
2975                 * Filters the randomly-generated password.
2976                 *
2977                 * @since 3.0.0
2978                 * @since 5.3.0 Added the `$length`, `$special_chars`, and `$extra_special_chars` parameters.
2979                 *
2980                 * @param string $password            The generated password.
2981                 * @param int    $length              The length of password to generate.
2982                 * @param bool   $special_chars       Whether to include standard special characters.
2983                 * @param bool   $extra_special_chars Whether to include other special characters.
2984                 */
2985                return apply_filters( 'random_password', $password, $length, $special_chars, $extra_special_chars );
2986        }
2987endif;
2988
2989if ( ! function_exists( 'wp_rand' ) ) :
2990        /**
2991         * Generates a random non-negative number.
2992         *
2993         * @since 2.6.2
2994         * @since 4.4.0 Uses PHP7 random_int() or the random_compat library if available.
2995         * @since 6.1.0 Returns zero instead of a random number if both `$min` and `$max` are zero.
2996         *
2997         * @global string $rnd_value
2998         *
2999         * @param int $min Optional. Lower limit for the generated number.
3000         *                 Accepts positive integers or zero. Defaults to 0.
3001         * @param int $max Optional. Upper limit for the generated number.
3002         *                 Accepts positive integers. Defaults to 4294967295.
3003         * @return int A random non-negative number between min and max.
3004         */
3005        function wp_rand( $min = null, $max = null ) {
3006                global $rnd_value;
3007
3008                /*
3009                 * Some misconfigured 32-bit environments (Entropy PHP, for example)
3010                 * truncate integers larger than PHP_INT_MAX to PHP_INT_MAX rather than overflowing them to floats.
3011                 */
3012                $max_random_number = 3000000000 === 2147483647 ? (float) '4294967295' : 4294967295; // 4294967295 = 0xffffffff
3013
3014                if ( null === $min ) {
3015                        $min = 0;
3016                }
3017
3018                if ( null === $max ) {
3019                        $max = $max_random_number;
3020                }
3021
3022                // We only handle ints, floats are truncated to their integer value.
3023                $min = (int) $min;
3024                $max = (int) $max;
3025
3026                // Use PHP's CSPRNG, or a compatible method.
3027                static $use_random_int_functionality = true;
3028                if ( $use_random_int_functionality ) {
3029                        try {
3030                                // wp_rand() can accept arguments in either order, PHP cannot.
3031                                $_max = max( $min, $max );
3032                                $_min = min( $min, $max );
3033                                $val  = random_int( $_min, $_max );
3034                                if ( false !== $val ) {
3035                                        return absint( $val );
3036                                } else {
3037                                        $use_random_int_functionality = false;
3038                                }
3039                        } catch ( Error $e ) {
3040                                $use_random_int_functionality = false;
3041                        } catch ( Exception $e ) {
3042                                $use_random_int_functionality = false;
3043                        }
3044                }
3045
3046                /*
3047                 * Reset $rnd_value after 14 uses.
3048                 * 32 (md5) + 40 (sha1) + 40 (sha1) / 8 = 14 random numbers from $rnd_value.
3049                 */
3050                if ( strlen( $rnd_value ) < 8 ) {
3051                        if ( defined( 'WP_SETUP_CONFIG' ) ) {
3052                                static $seed = '';
3053                        } else {
3054                                $seed = get_transient( 'random_seed' );
3055                        }
3056                        $rnd_value  = md5( uniqid( microtime() . mt_rand(), true ) . $seed );
3057                        $rnd_value .= sha1( $rnd_value );
3058                        $rnd_value .= sha1( $rnd_value . $seed );
3059                        $seed       = md5( $seed . $rnd_value );
3060                        if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) {
3061                                set_transient( 'random_seed', $seed );
3062                        }
3063                }
3064
3065                // Take the first 8 digits for our value.
3066                $value = substr( $rnd_value, 0, 8 );
3067
3068                // Strip the first eight, leaving the remainder for the next call to wp_rand().
3069                $rnd_value = substr( $rnd_value, 8 );
3070
3071                $value = abs( hexdec( $value ) );
3072
3073                // Reduce the value to be within the min - max range.
3074                $value = $min + ( $max - $min + 1 ) * $value / ( $max_random_number + 1 );
3075
3076                return abs( (int) $value );
3077        }
3078endif;
3079
3080if ( ! function_exists( 'wp_set_password' ) ) :
3081        /**
3082         * Updates the user's password with a new hashed one.
3083         *
3084         * For integration with other applications, this function can be overwritten to
3085         * instead use the other package password checking algorithm.
3086         *
3087         * Please note: This function should be used sparingly and is really only meant for single-time
3088         * application. Leveraging this improperly in a plugin or theme could result in an endless loop
3089         * of password resets if precautions are not taken to ensure it does not execute on every page load.
3090         *
3091         * @since 2.5.0
3092         * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
3093         *
3094         * @global wpdb $wpdb WordPress database abstraction object.
3095         *
3096         * @param string $password The plaintext new user password.
3097         * @param int    $user_id  User ID.
3098         */
3099        function wp_set_password(
3100                #[\SensitiveParameter]
3101                $password,
3102                $user_id
3103        ) {
3104                global $wpdb;
3105
3106                $old_user_data = get_userdata( $user_id );
3107
3108                $hash = wp_hash_password( $password );
3109                $wpdb->update(
3110                        $wpdb->users,
3111                        array(
3112                                'user_pass'           => $hash,
3113                                'user_activation_key' => '',
3114                        ),
3115                        array( 'ID' => $user_id )
3116                );
3117
3118                clean_user_cache( $user_id );
3119
3120                /**
3121                 * Fires after the user password is set.
3122                 *
3123                 * @since 6.2.0
3124                 * @since 6.7.0 The `$old_user_data` parameter was added.
3125                 *
3126                 * @param string  $password      The plaintext password just set.
3127                 * @param int     $user_id       The ID of the user whose password was just set.
3128                 * @param WP_User $old_user_data Object containing user's data prior to update.
3129                 */
3130                do_action( 'wp_set_password', $password, $user_id, $old_user_data );
3131        }
3132endif;
3133
3134if ( ! function_exists( 'get_avatar' ) ) :
3135        /**
3136         * Retrieves the avatar `<img>` tag for a user, email address, MD5 hash, comment, or post.
3137         *
3138         * @since 2.5.0
3139         * @since 4.2.0 Added the optional `$args` parameter.
3140         * @since 5.5.0 Added the `loading` argument.
3141         * @since 6.1.0 Added the `decoding` argument.
3142         * @since 6.3.0 Added the `fetchpriority` argument.
3143         *
3144         * @param mixed  $id_or_email   The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash,
3145         *                              user email, WP_User object, WP_Post object, or WP_Comment object.
3146         * @param int    $size          Optional. Height and width of the avatar in pixels. Default 96.
3147         * @param string $default_value URL for the default image or a default type. Accepts:
3148         *                              - '404' (return a 404 instead of a default image)
3149         *                              - 'retro' (a 8-bit arcade-style pixelated face)
3150         *                              - 'robohash' (a robot)
3151         *                              - 'monsterid' (a monster)
3152         *                              - 'wavatar' (a cartoon face)
3153         *                              - 'identicon' (the "quilt", a geometric pattern)
3154         *                              - 'initials' (initials based avatar with background color)
3155         *                              - 'color' (generated background color)
3156         *                              - 'mystery', 'mm', or 'mysteryman' (The Oyster Man)
3157         *                              - 'blank' (transparent GIF)
3158         *                              - 'gravatar_default' (the Gravatar logo)
3159         *                              Default is the value of the 'avatar_default' option,
3160         *                              with a fallback of 'mystery'.
3161         * @param string $alt           Optional. Alternative text to use in the avatar image tag.
3162         *                              Default empty.
3163         * @param array  $args {
3164         *     Optional. Extra arguments to retrieve the avatar.
3165         *
3166         *     @type int          $height        Display height of the avatar in pixels. Defaults to $size.
3167         *     @type int          $width         Display width of the avatar in pixels. Defaults to $size.
3168         *     @type bool         $force_default Whether to always show the default image, never the Gravatar.
3169         *                                       Default false.
3170         *     @type string       $rating        What rating to display avatars up to. Accepts:
3171         *                                       - 'G' (suitable for all audiences)
3172         *                                       - 'PG' (possibly offensive, usually for audiences 13 and above)
3173         *                                       - 'R' (intended for adult audiences above 17)
3174         *                                       - 'X' (even more mature than above)
3175         *                                       Default is the value of the 'avatar_rating' option.
3176         *     @type string       $scheme        URL scheme to use. See set_url_scheme() for accepted values.
3177         *                                       Default null.
3178         *     @type array|string $class         Array or string of additional classes to add to the img element.
3179         *                                       Default null.
3180         *     @type bool         $force_display Whether to always show the avatar - ignores the show_avatars option.
3181         *                                       Default false.
3182         *     @type string       $loading       Value for the `loading` attribute.
3183         *                                       Default null.
3184         *     @type string       $fetchpriority Value for the `fetchpriority` attribute.
3185         *                                       Default null.
3186         *     @type string       $decoding      Value for the `decoding` attribute.
3187         *                                       Default null.
3188         *     @type string       $extra_attr    HTML attributes to insert in the IMG element. Is not sanitized.
3189         *                                       Default empty.
3190         * }
3191         * @return string|false `<img>` tag for the user's avatar. False on failure.
3192         */
3193        function get_avatar( $id_or_email, $size = 96, $default_value = '', $alt = '', $args = null ) {
3194                $defaults = array(
3195                        // get_avatar_data() args.
3196                        'size'          => 96,
3197                        'height'        => null,
3198                        'width'         => null,
3199                        'default'       => get_option( 'avatar_default', 'mystery' ),
3200                        'force_default' => false,
3201                        'rating'        => get_option( 'avatar_rating' ),
3202                        'scheme'        => null,
3203                        'alt'           => '',
3204                        'class'         => null,
3205                        'force_display' => false,
3206                        'loading'       => null,
3207                        'fetchpriority' => null,
3208                        'decoding'      => null,
3209                        'extra_attr'    => '',
3210                );
3211
3212                if ( empty( $args ) ) {
3213                        $args = array();
3214                }
3215
3216                $args['size']    = (int) $size;
3217                $args['default'] = $default_value;
3218                $args['alt']     = $alt;
3219
3220                $args = wp_parse_args( $args, $defaults );
3221
3222                if ( empty( $args['height'] ) ) {
3223                        $args['height'] = $args['size'];
3224                }
3225                if ( empty( $args['width'] ) ) {
3226                        $args['width'] = $args['size'];
3227                }
3228
3229                // Update args with loading optimized attributes.
3230                $loading_optimization_attr = wp_get_loading_optimization_attributes( 'img', $args, 'get_avatar' );
3231
3232                $args = array_merge( $args, $loading_optimization_attr );
3233
3234                if ( is_object( $id_or_email ) && isset( $id_or_email->comment_ID ) ) {
3235                        $id_or_email = get_comment( $id_or_email );
3236                }
3237
3238                /**
3239                 * Allows the HTML for a user's avatar to be returned early.
3240                 *
3241                 * Returning a non-null value will effectively short-circuit get_avatar(), passing
3242                 * the value through the {@see 'get_avatar'} filter and returning early.
3243                 *
3244                 * @since 4.2.0
3245                 *
3246                 * @param string|null $avatar      HTML for the user's avatar. Default null.
3247                 * @param mixed       $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash,
3248                 *                                 user email, WP_User object, WP_Post object, or WP_Comment object.
3249                 * @param array       $args        Arguments passed to get_avatar_url(), after processing.
3250                 */
3251                $avatar = apply_filters( 'pre_get_avatar', null, $id_or_email, $args );
3252
3253                if ( ! is_null( $avatar ) ) {
3254                        /** This filter is documented in wp-includes/pluggable.php */
3255                        return apply_filters( 'get_avatar', $avatar, $id_or_email, $args['size'], $args['default'], $args['alt'], $args );
3256                }
3257
3258                if ( ! $args['force_display'] && ! get_option( 'show_avatars' ) ) {
3259                        return false;
3260                }
3261
3262                $url2x = get_avatar_url( $id_or_email, array_merge( $args, array( 'size' => $args['size'] * 2 ) ) );
3263
3264                $args = get_avatar_data( $id_or_email, $args );
3265
3266                $url = $args['url'];
3267
3268                if ( ! $url || is_wp_error( $url ) ) {
3269                        return false;
3270                }
3271
3272                $class = array( 'avatar', 'avatar-' . (int) $args['size'], 'photo' );
3273
3274                if ( ! $args['found_avatar'] || $args['force_default'] ) {
3275                        $class[] = 'avatar-default';
3276                }
3277
3278                if ( $args['class'] ) {
3279                        if ( is_array( $args['class'] ) ) {
3280                                $class = array_merge( $class, $args['class'] );
3281                        } else {
3282                                $class[] = $args['class'];
3283                        }
3284                }
3285
3286                // Add `loading`, `fetchpriority`, and `decoding` attributes.
3287                $extra_attr = $args['extra_attr'];
3288
3289                if ( in_array( $args['loading'], array( 'lazy', 'eager' ), true )
3290                        && ! preg_match( '/\bloading\s*=/', $extra_attr )
3291                ) {
3292                        if ( ! empty( $extra_attr ) ) {
3293                                $extra_attr .= ' ';
3294                        }
3295
3296                        $extra_attr .= "loading='{$args['loading']}'";
3297                }
3298
3299                if ( in_array( $args['fetchpriority'], array( 'high', 'low', 'auto' ), true )
3300                        && ! preg_match( '/\bfetchpriority\s*=/', $extra_attr )
3301                ) {
3302                        if ( ! empty( $extra_attr ) ) {
3303                                $extra_attr .= ' ';
3304                        }
3305
3306                        $extra_attr .= "fetchpriority='{$args['fetchpriority']}'";
3307                }
3308
3309                if ( in_array( $args['decoding'], array( 'async', 'sync', 'auto' ), true )
3310                        && ! preg_match( '/\bdecoding\s*=/', $extra_attr )
3311                ) {
3312                        if ( ! empty( $extra_attr ) ) {
3313                                $extra_attr .= ' ';
3314                        }
3315
3316                        $extra_attr .= "decoding='{$args['decoding']}'";
3317                }
3318
3319                $avatar = sprintf(
3320                        "<img alt='%s' src='%s' srcset='%s' class='%s' height='%d' width='%d' %s/>",
3321                        esc_attr( $args['alt'] ),
3322                        esc_url( $url ),
3323                        esc_url( $url2x ) . ' 2x',
3324                        esc_attr( implode( ' ', $class ) ),
3325                        (int) $args['height'],
3326                        (int) $args['width'],
3327                        $extra_attr
3328                );
3329
3330                /**
3331                 * Filters the HTML for a user's avatar.
3332                 *
3333                 * @since 2.5.0
3334                 * @since 4.2.0 Added the `$args` parameter.
3335                 *
3336                 * @param string $avatar        HTML for the user's avatar.
3337                 * @param mixed  $id_or_email   The avatar to retrieve. Accepts a user ID, Gravatar MD5 hash,
3338                 *                              user email, WP_User object, WP_Post object, or WP_Comment object.
3339                 * @param int    $size          Height and width of the avatar in pixels.
3340                 * @param string $default_value URL for the default image or a default type. Accepts:
3341                 *                              - '404' (return a 404 instead of a default image)
3342                 *                              - 'retro' (a 8-bit arcade-style pixelated face)
3343                 *                              - 'robohash' (a robot)
3344                 *                              - 'monsterid' (a monster)
3345                 *                              - 'wavatar' (a cartoon face)
3346                 *                              - 'identicon' (the "quilt", a geometric pattern)
3347                 *                              - 'mystery', 'mm', or 'mysteryman' (The Oyster Man)
3348                 *                              - 'blank' (transparent GIF)
3349                 *                              - 'gravatar_default' (the Gravatar logo)
3350                 * @param string $alt           Alternative text to use in the avatar image tag.
3351                 * @param array  $args          Arguments passed to get_avatar_data(), after processing.
3352                 */
3353                return apply_filters( 'get_avatar', $avatar, $id_or_email, $args['size'], $args['default'], $args['alt'], $args );
3354        }
3355endif;
3356
3357if ( ! function_exists( 'wp_text_diff' ) ) :
3358        /**
3359         * Displays a human readable HTML representation of the difference between two strings.
3360         *
3361         * The Diff is available for getting the changes between versions. The output is
3362         * HTML, so the primary use is for displaying the changes. If the two strings
3363         * are equivalent, then an empty string will be returned.
3364         *
3365         * @since 2.6.0
3366         *
3367         * @see wp_parse_args() Used to change defaults to user defined settings.
3368         * @uses Text_Diff
3369         * @uses WP_Text_Diff_Renderer_Table
3370         *
3371         * @param string       $left_string  "old" (left) version of string.
3372         * @param string       $right_string "new" (right) version of string.
3373         * @param string|array $args {
3374         *     Associative array of options to pass to WP_Text_Diff_Renderer_Table().
3375         *
3376         *     @type string $title           Titles the diff in a manner compatible
3377         *                                   with the output. Default empty.
3378         *     @type string $title_left      Change the HTML to the left of the title.
3379         *                                   Default empty.
3380         *     @type string $title_right     Change the HTML to the right of the title.
3381         *                                   Default empty.
3382         *     @type bool   $show_split_view True for split view (two columns), false for
3383         *                                   un-split view (single column). Default true.
3384         * }
3385         * @return string Empty string if strings are equivalent or HTML with differences.
3386         */
3387        function wp_text_diff( $left_string, $right_string, $args = null ) {
3388                $defaults = array(
3389                        'title'           => '',
3390                        'title_left'      => '',
3391                        'title_right'     => '',
3392                        'show_split_view' => true,
3393                );
3394                $args     = wp_parse_args( $args, $defaults );
3395
3396                if ( ! class_exists( 'WP_Text_Diff_Renderer_Table', false ) ) {
3397                        require ABSPATH . WPINC . '/wp-diff.php';
3398                }
3399
3400                $left_string  = normalize_whitespace( $left_string );
3401                $right_string = normalize_whitespace( $right_string );
3402
3403                $left_lines  = explode( "\n", $left_string );
3404                $right_lines = explode( "\n", $right_string );
3405                $text_diff   = new Text_Diff( $left_lines, $right_lines );
3406                $renderer    = new WP_Text_Diff_Renderer_Table( $args );
3407                $diff        = $renderer->render( $text_diff );
3408
3409                if ( ! $diff ) {
3410                        return '';
3411                }
3412
3413                $is_split_view       = ! empty( $args['show_split_view'] );
3414                $is_split_view_class = $is_split_view ? ' is-split-view' : '';
3415
3416                $r = "<table class='diff$is_split_view_class'>\n";
3417
3418                if ( $args['title'] ) {
3419                        $r .= "<caption class='diff-title'>$args[title]</caption>\n";
3420                }
3421
3422                if ( $args['title_left'] || $args['title_right'] ) {
3423                        $r .= '<thead>';
3424                }
3425
3426                if ( $args['title_left'] || $args['title_right'] ) {
3427                        $th_or_td_left  = empty( $args['title_left'] ) ? 'td' : 'th';
3428                        $th_or_td_right = empty( $args['title_right'] ) ? 'td' : 'th';
3429
3430                        $r .= "<tr class='diff-sub-title'>\n";
3431                        $r .= "\t<$th_or_td_left>$args[title_left]</$th_or_td_left>\n";
3432                        if ( $is_split_view ) {
3433                                $r .= "\t<$th_or_td_right>$args[title_right]</$th_or_td_right>\n";
3434                        }
3435                        $r .= "</tr>\n";
3436                }
3437
3438                if ( $args['title_left'] || $args['title_right'] ) {
3439                        $r .= "</thead>\n";
3440                }
3441
3442                $r .= "<tbody>\n$diff\n</tbody>\n";
3443                $r .= '</table>';
3444
3445                return $r;
3446        }
3447endif;
Note: See TracBrowser for help on using the repository browser.