Gestionnaire de fichiers - Editer - /home/wwgoat/public_html/blog/inc.tar
Arrière
block-styles.php 0000644 00000004611 14720673703 0007703 0 ustar 00 <?php /** * Block Styles * * @link https://developer.wordpress.org/reference/functions/register_block_style/ * * @package WordPress * @subpackage Twenty_Twenty_One * @since Twenty Twenty-One 1.0 */ if ( function_exists( 'register_block_style' ) ) { /** * Register block styles. * * @since Twenty Twenty-One 1.0 * * @return void */ function twenty_twenty_one_register_block_styles() { // Columns: Overlap. register_block_style( 'core/columns', array( 'name' => 'twentytwentyone-columns-overlap', 'label' => esc_html__( 'Overlap', 'twentytwentyone' ), ) ); // Cover: Borders. register_block_style( 'core/cover', array( 'name' => 'twentytwentyone-border', 'label' => esc_html__( 'Borders', 'twentytwentyone' ), ) ); // Group: Borders. register_block_style( 'core/group', array( 'name' => 'twentytwentyone-border', 'label' => esc_html__( 'Borders', 'twentytwentyone' ), ) ); // Image: Borders. register_block_style( 'core/image', array( 'name' => 'twentytwentyone-border', 'label' => esc_html__( 'Borders', 'twentytwentyone' ), ) ); // Image: Frame. register_block_style( 'core/image', array( 'name' => 'twentytwentyone-image-frame', 'label' => esc_html__( 'Frame', 'twentytwentyone' ), ) ); // Latest Posts: Dividers. register_block_style( 'core/latest-posts', array( 'name' => 'twentytwentyone-latest-posts-dividers', 'label' => esc_html__( 'Dividers', 'twentytwentyone' ), ) ); // Latest Posts: Borders. register_block_style( 'core/latest-posts', array( 'name' => 'twentytwentyone-latest-posts-borders', 'label' => esc_html__( 'Borders', 'twentytwentyone' ), ) ); // Media & Text: Borders. register_block_style( 'core/media-text', array( 'name' => 'twentytwentyone-border', 'label' => esc_html__( 'Borders', 'twentytwentyone' ), ) ); // Separator: Thick. register_block_style( 'core/separator', array( 'name' => 'twentytwentyone-separator-thick', 'label' => esc_html__( 'Thick', 'twentytwentyone' ), ) ); // Social icons: Dark gray color. register_block_style( 'core/social-links', array( 'name' => 'twentytwentyone-social-icons-color', 'label' => esc_html__( 'Dark gray', 'twentytwentyone' ), ) ); } add_action( 'init', 'twenty_twenty_one_register_block_styles' ); } template-tags.php 0000644 00000056027 14720673703 0010047 0 ustar 00 <?php /** * Custom template tags for this theme. * * @package WordPress * @subpackage Twenty_Twenty * @since Twenty Twenty 1.0 */ /** * Table of Contents: * Logo & Description * Comments * Post Meta * Menus * Classes * Archives * Miscellaneous */ /** * Logo & Description */ /** * Displays the site logo, either text or image. * * @since Twenty Twenty 1.0 * * @param array $args Arguments for displaying the site logo either as an image or text. * @param bool $display Display or return the HTML. * @return string Compiled HTML based on our arguments. */ function twentytwenty_site_logo( $args = array(), $display = true ) { $logo = get_custom_logo(); $site_title = get_bloginfo( 'name' ); $contents = ''; $classname = ''; $defaults = array( 'logo' => '%1$s<span class="screen-reader-text">%2$s</span>', 'logo_class' => 'site-logo', 'title' => '<a href="%1$s">%2$s</a>', 'title_class' => 'site-title', 'home_wrap' => '<h1 class="%1$s">%2$s</h1>', 'single_wrap' => '<div class="%1$s faux-heading">%2$s</div>', 'condition' => ( is_front_page() || is_home() ) && ! is_page(), ); $args = wp_parse_args( $args, $defaults ); /** * Filters the arguments for `twentytwenty_site_logo()`. * * @since Twenty Twenty 1.0 * * @param array $args Parsed arguments. * @param array $defaults Function's default arguments. */ $args = apply_filters( 'twentytwenty_site_logo_args', $args, $defaults ); if ( has_custom_logo() ) { $contents = sprintf( $args['logo'], $logo, esc_html( $site_title ) ); $classname = $args['logo_class']; } else { $contents = sprintf( $args['title'], esc_url( get_home_url( null, '/' ) ), esc_html( $site_title ) ); $classname = $args['title_class']; } $wrap = $args['condition'] ? 'home_wrap' : 'single_wrap'; $html = sprintf( $args[ $wrap ], $classname, $contents ); /** * Filters the arguments for `twentytwenty_site_logo()`. * * @since Twenty Twenty 1.0 * * @param string $html Compiled HTML based on our arguments. * @param array $args Parsed arguments. * @param string $classname Class name based on current view, home or single. * @param string $contents HTML for site title or logo. */ $html = apply_filters( 'twentytwenty_site_logo', $html, $args, $classname, $contents ); if ( ! $display ) { return $html; } echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Displays the site description. * * @since Twenty Twenty 1.0 * * @param bool $display Display or return the HTML. * @return string The HTML to display. */ function twentytwenty_site_description( $display = true ) { $description = get_bloginfo( 'description' ); if ( ! $description ) { return; } $wrapper = '<div class="site-description">%s</div><!-- .site-description -->'; $html = sprintf( $wrapper, esc_html( $description ) ); /** * Filters the HTML for the site description. * * @since Twenty Twenty 1.0 * * @param string $html The HTML to display. * @param string $description Site description via `bloginfo()`. * @param string $wrapper The format used in case you want to reuse it in a `sprintf()`. */ $html = apply_filters( 'twentytwenty_site_description', $html, $description, $wrapper ); if ( ! $display ) { return $html; } echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Comments */ /** * Checks if the specified comment is written by the author of the post commented on. * * @since Twenty Twenty 1.0 * * @param object $comment Comment data. * @return bool */ function twentytwenty_is_comment_by_post_author( $comment = null ) { if ( is_object( $comment ) && $comment->user_id > 0 ) { $user = get_userdata( $comment->user_id ); $post = get_post( $comment->comment_post_ID ); if ( ! empty( $user ) && ! empty( $post ) ) { return $comment->user_id === $post->post_author; } } return false; } /** * Filters comment reply link to not JS scroll. * * Filter the comment reply link to add a class indicating it should not use JS slow-scroll, as it * makes it scroll to the wrong position on the page. * * @since Twenty Twenty 1.0 * * @param string $link Link to the top of the page. * @return string Link to the top of the page. */ function twentytwenty_filter_comment_reply_link( $link ) { $link = str_replace( 'class=\'', 'class=\'do-not-scroll ', $link ); return $link; } add_filter( 'comment_reply_link', 'twentytwenty_filter_comment_reply_link' ); /** * Post Meta */ /** * Retrieves and displays the post meta. * * If it's a single post, outputs the post meta values specified in the Customizer settings. * * @since Twenty Twenty 1.0 * * @param int $post_id The ID of the post for which the post meta should be output. * @param string $location Which post meta location to output – single or preview. */ function twentytwenty_the_post_meta( $post_id = null, $location = 'single-top' ) { echo twentytwenty_get_post_meta( $post_id, $location ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in twentytwenty_get_post_meta(). } /** * Filters the edit post link to add an icon and use the post meta structure. * * @since Twenty Twenty 1.0 * * @param string $link Anchor tag for the edit link. * @param int $post_id Post ID. * @param string $text Anchor text. */ function twentytwenty_edit_post_link( $link, $post_id, $text ) { if ( is_admin() ) { return $link; } $edit_url = get_edit_post_link( $post_id ); if ( ! $edit_url ) { return; } $text = sprintf( wp_kses( /* translators: %s: Post title. Only visible to screen readers. */ __( 'Edit <span class="screen-reader-text">%s</span>', 'twentytwenty' ), array( 'span' => array( 'class' => array(), ), ) ), get_the_title( $post_id ) ); return '<div class="post-meta-wrapper post-meta-edit-link-wrapper"><ul class="post-meta"><li class="post-edit meta-wrapper"><span class="meta-icon">' . twentytwenty_get_theme_svg( 'edit' ) . '</span><span class="meta-text"><a href="' . esc_url( $edit_url ) . '">' . $text . '</a></span></li></ul><!-- .post-meta --></div><!-- .post-meta-wrapper -->'; } add_filter( 'edit_post_link', 'twentytwenty_edit_post_link', 10, 3 ); /** * Retrieves the post meta. * * @since Twenty Twenty 1.0 * * @param int $post_id The ID of the post. * @param string $location The location where the meta is shown. */ function twentytwenty_get_post_meta( $post_id = null, $location = 'single-top' ) { // Require post ID. if ( ! $post_id ) { return; } /** * Filters post types array. * * This filter can be used to hide post meta information of post, page or custom post type * registered by child themes or plugins. * * @since Twenty Twenty 1.0 * * @param array Array of post types. */ $disallowed_post_types = apply_filters( 'twentytwenty_disallowed_post_types_for_meta_output', array( 'page' ) ); // Check whether the post type is allowed to output post meta. if ( in_array( get_post_type( $post_id ), $disallowed_post_types, true ) ) { return; } $post_meta_wrapper_classes = ''; $post_meta_classes = ''; // Get the post meta settings for the location specified. if ( 'single-top' === $location ) { /** * Filters post meta info visibility. * * Use this filter to hide post meta information like Author, Post date, Comments, Is sticky status. * * @since Twenty Twenty 1.0 * * @param array $args { * @type string $author * @type string $post-date * @type string $comments * @type string $sticky * } */ $post_meta = apply_filters( 'twentytwenty_post_meta_location_single_top', array( 'author', 'post-date', 'comments', 'sticky', ) ); $post_meta_wrapper_classes = ' post-meta-single post-meta-single-top'; } elseif ( 'single-bottom' === $location ) { /** * Filters post tags visibility. * * Use this filter to hide post tags. * * @since Twenty Twenty 1.0 * * @param array $args { * @type string $tags * } */ $post_meta = apply_filters( 'twentytwenty_post_meta_location_single_bottom', array( 'tags', ) ); $post_meta_wrapper_classes = ' post-meta-single post-meta-single-bottom'; } // If the post meta setting has the value 'empty', it's explicitly empty and the default post meta shouldn't be output. if ( $post_meta && ! in_array( 'empty', $post_meta, true ) ) { // Make sure we don't output an empty container. $has_meta = false; $the_post = get_post( $post_id ); setup_postdata( $the_post ); ob_start(); ?> <div class="post-meta-wrapper<?php echo esc_attr( $post_meta_wrapper_classes ); ?>"> <ul class="post-meta<?php echo esc_attr( $post_meta_classes ); ?>"> <?php /** * Fires before post meta HTML display. * * Allow output of additional post meta info to be added by child themes and plugins. * * @since Twenty Twenty 1.0 * @since Twenty Twenty 1.1 Added the `$post_meta` and `$location` parameters. * * @param int $post_id Post ID. * @param array $post_meta An array of post meta information. * @param string $location The location where the meta is shown. * Accepts 'single-top' or 'single-bottom'. */ do_action( 'twentytwenty_start_of_post_meta_list', $post_id, $post_meta, $location ); // Author. if ( post_type_supports( get_post_type( $post_id ), 'author' ) && in_array( 'author', $post_meta, true ) ) { $has_meta = true; ?> <li class="post-author meta-wrapper"> <span class="meta-icon"> <span class="screen-reader-text"> <?php /* translators: Hidden accessibility text. */ _e( 'Post author', 'twentytwenty' ); ?> </span> <?php twentytwenty_the_theme_svg( 'user' ); ?> </span> <span class="meta-text"> <?php printf( /* translators: %s: Author name. */ __( 'By %s', 'twentytwenty' ), '<a href="' . esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ) . '">' . esc_html( get_the_author_meta( 'display_name' ) ) . '</a>' ); ?> </span> </li> <?php } // Post date. if ( in_array( 'post-date', $post_meta, true ) ) { $has_meta = true; ?> <li class="post-date meta-wrapper"> <span class="meta-icon"> <span class="screen-reader-text"> <?php /* translators: Hidden accessibility text. */ _e( 'Post date', 'twentytwenty' ); ?> </span> <?php twentytwenty_the_theme_svg( 'calendar' ); ?> </span> <span class="meta-text"> <a href="<?php the_permalink(); ?>"><?php the_time( get_option( 'date_format' ) ); ?></a> </span> </li> <?php } // Categories. if ( in_array( 'categories', $post_meta, true ) && has_category() ) { $has_meta = true; ?> <li class="post-categories meta-wrapper"> <span class="meta-icon"> <span class="screen-reader-text"> <?php /* translators: Hidden accessibility text. */ _e( 'Categories', 'twentytwenty' ); ?> </span> <?php twentytwenty_the_theme_svg( 'folder' ); ?> </span> <span class="meta-text"> <?php _ex( 'In', 'A string that is output before one or more categories', 'twentytwenty' ); ?> <?php the_category( ', ' ); ?> </span> </li> <?php } // Tags. if ( in_array( 'tags', $post_meta, true ) && has_tag() ) { $has_meta = true; ?> <li class="post-tags meta-wrapper"> <span class="meta-icon"> <span class="screen-reader-text"> <?php /* translators: Hidden accessibility text. */ _e( 'Tags', 'twentytwenty' ); ?> </span> <?php twentytwenty_the_theme_svg( 'tag' ); ?> </span> <span class="meta-text"> <?php the_tags( '', ', ', '' ); ?> </span> </li> <?php } // Comments link. if ( in_array( 'comments', $post_meta, true ) && ! post_password_required() && ( comments_open() || get_comments_number() ) ) { $has_meta = true; ?> <li class="post-comment-link meta-wrapper"> <span class="meta-icon"> <?php twentytwenty_the_theme_svg( 'comment' ); ?> </span> <span class="meta-text"> <?php comments_popup_link(); ?> </span> </li> <?php } // Sticky. if ( in_array( 'sticky', $post_meta, true ) && is_sticky() ) { $has_meta = true; ?> <li class="post-sticky meta-wrapper"> <span class="meta-icon"> <?php twentytwenty_the_theme_svg( 'bookmark' ); ?> </span> <span class="meta-text"> <?php _e( 'Sticky post', 'twentytwenty' ); ?> </span> </li> <?php } /** * Fires after post meta HTML display. * * Allow output of additional post meta info to be added by child themes and plugins. * * @since Twenty Twenty 1.0 * @since Twenty Twenty 1.1 Added the `$post_meta` and `$location` parameters. * * @param int $post_id Post ID. * @param array $post_meta An array of post meta information. * @param string $location The location where the meta is shown. * Accepts 'single-top' or 'single-bottom'. */ do_action( 'twentytwenty_end_of_post_meta_list', $post_id, $post_meta, $location ); ?> </ul><!-- .post-meta --> </div><!-- .post-meta-wrapper --> <?php wp_reset_postdata(); $meta_output = ob_get_clean(); // If there is meta to output, return it. if ( $has_meta && $meta_output ) { return $meta_output; } } } /** * Menus */ /** * Filters classes of wp_list_pages items to match menu items. * * Filter the class applied to wp_list_pages() items with children to match the menu class, to simplify. * styling of sub levels in the fallback. Only applied if the match_menu_classes argument is set. * * @since Twenty Twenty 1.0 * * @param string[] $css_class An array of CSS classes to be applied to each list item. * @param WP_Post $page Page data object. * @param int $depth Depth of page, used for padding. * @param array $args An array of arguments. * @return array CSS class names. */ function twentytwenty_filter_wp_list_pages_item_classes( $css_class, $page, $depth, $args ) { // Only apply to wp_list_pages() calls with match_menu_classes set to true. $match_menu_classes = isset( $args['match_menu_classes'] ); if ( ! $match_menu_classes ) { return $css_class; } // Add current menu item class. if ( in_array( 'current_page_item', $css_class, true ) ) { $css_class[] = 'current-menu-item'; } // Add menu item has children class. if ( in_array( 'page_item_has_children', $css_class, true ) ) { $css_class[] = 'menu-item-has-children'; } return $css_class; } add_filter( 'page_css_class', 'twentytwenty_filter_wp_list_pages_item_classes', 10, 4 ); /** * Adds a Sub Nav Toggle to the Expanded Menu and Mobile Menu. * * @since Twenty Twenty 1.0 * * @param stdClass $args An object of wp_nav_menu() arguments. * @param WP_Post $item Menu item data object. * @return stdClass An object of wp_nav_menu() arguments. */ function twentytwenty_add_sub_toggles_to_main_menu( $args, $item ) { // Add sub menu toggles to the Expanded Menu with toggles. if ( isset( $args->show_toggles ) && $args->show_toggles ) { // Wrap the menu item link contents in a div, used for positioning. $args->before = '<div class="ancestor-wrapper">'; $args->after = ''; // Add a toggle to items with children. if ( in_array( 'menu-item-has-children', $item->classes, true ) ) { $toggle_target_string = '.menu-modal .menu-item-' . $item->ID . ' > .sub-menu'; $toggle_duration = twentytwenty_toggle_duration(); // Add the sub menu toggle. $args->after .= '<button class="toggle sub-menu-toggle fill-children-current-color" data-toggle-target="' . $toggle_target_string . '" data-toggle-type="slidetoggle" data-toggle-duration="' . absint( $toggle_duration ) . '" aria-expanded="false"><span class="screen-reader-text">' . /* translators: Hidden accessibility text. */ __( 'Show sub menu', 'twentytwenty' ) . '</span>' . twentytwenty_get_theme_svg( 'chevron-down' ) . '</button>'; } // Close the wrapper. $args->after .= '</div><!-- .ancestor-wrapper -->'; // Add sub menu icons to the primary menu without toggles. } elseif ( 'primary' === $args->theme_location ) { if ( in_array( 'menu-item-has-children', $item->classes, true ) ) { $args->after = '<span class="icon"></span>'; } else { $args->after = ''; } } return $args; } add_filter( 'nav_menu_item_args', 'twentytwenty_add_sub_toggles_to_main_menu', 10, 2 ); /** * Displays SVG icons in social links menu. * * @since Twenty Twenty 1.0 * * @param string $item_output The menu item's starting HTML output. * @param WP_Post $item Menu item data object. * @param int $depth Depth of the menu. Used for padding. * @param stdClass $args An object of wp_nav_menu() arguments. * @return string The menu item output with social icon. */ function twentytwenty_nav_menu_social_icons( $item_output, $item, $depth, $args ) { // Change SVG icon inside social links menu if there is supported URL. if ( 'social' === $args->theme_location ) { $svg = TwentyTwenty_SVG_Icons::get_social_link_svg( $item->url ); if ( empty( $svg ) ) { $svg = twentytwenty_get_theme_svg( 'link' ); } $item_output = str_replace( $args->link_after, '</span>' . $svg, $item_output ); } return $item_output; } add_filter( 'walker_nav_menu_start_el', 'twentytwenty_nav_menu_social_icons', 10, 4 ); /** * Classes */ /** * Adds 'no-js' class. * * If we're missing JavaScript support, the HTML element will have a 'no-js' class. * * @since Twenty Twenty 1.0 */ function twentytwenty_no_js_class() { ?> <script>document.documentElement.className = document.documentElement.className.replace( 'no-js', 'js' );</script> <?php } add_action( 'wp_head', 'twentytwenty_no_js_class' ); /** * Adds conditional body classes. * * @since Twenty Twenty 1.0 * * @global WP_Post $post Global post object. * * @param array $classes Classes added to the body tag. * @return array Classes added to the body tag. */ function twentytwenty_body_classes( $classes ) { global $post; $post_type = isset( $post ) ? $post->post_type : false; // Check whether we're singular. if ( is_singular() ) { $classes[] = 'singular'; } // Check whether the current page should have an overlay header. if ( is_page_template( array( 'templates/template-cover.php' ) ) ) { $classes[] = 'overlay-header'; } // Check whether the current page has full-width content. if ( is_page_template( array( 'templates/template-full-width.php' ) ) ) { $classes[] = 'has-full-width-content'; } // Check for enabled search. if ( true === get_theme_mod( 'enable_header_search', true ) ) { $classes[] = 'enable-search-modal'; } // Check for post thumbnail. if ( is_singular() && has_post_thumbnail() ) { $classes[] = 'has-post-thumbnail'; } elseif ( is_singular() ) { $classes[] = 'missing-post-thumbnail'; } // Check whether we're in the customizer preview. if ( is_customize_preview() ) { $classes[] = 'customizer-preview'; } // Check if posts have single pagination. if ( is_single() && ( get_next_post() || get_previous_post() ) ) { $classes[] = 'has-single-pagination'; } else { $classes[] = 'has-no-pagination'; } // Check if we're showing comments. if ( $post && ( ( 'post' === $post_type || comments_open() || get_comments_number() ) && ! post_password_required() ) ) { $classes[] = 'showing-comments'; } else { $classes[] = 'not-showing-comments'; } // Check if avatars are visible. $classes[] = get_option( 'show_avatars' ) ? 'show-avatars' : 'hide-avatars'; // Slim page template class names (class = name - file suffix). if ( is_page_template() ) { $classes[] = basename( get_page_template_slug(), '.php' ); } // Check for the elements output in the top part of the footer. $has_footer_menu = has_nav_menu( 'footer' ); $has_social_menu = has_nav_menu( 'social' ); $has_sidebar_1 = is_active_sidebar( 'sidebar-1' ); $has_sidebar_2 = is_active_sidebar( 'sidebar-2' ); // Add a class indicating whether those elements are output. if ( $has_footer_menu || $has_social_menu || $has_sidebar_1 || $has_sidebar_2 ) { $classes[] = 'footer-top-visible'; } else { $classes[] = 'footer-top-hidden'; } // Get header/footer background color. $header_footer_background = get_theme_mod( 'header_footer_background_color', '#ffffff' ); $header_footer_background = strtolower( '#' . ltrim( $header_footer_background, '#' ) ); // Get content background color. $background_color = get_theme_mod( 'background_color', 'f5efe0' ); $background_color = strtolower( '#' . ltrim( $background_color, '#' ) ); // Add extra class if main background and header/footer background are the same color. if ( $background_color === $header_footer_background ) { $classes[] = 'reduced-spacing'; } return $classes; } add_filter( 'body_class', 'twentytwenty_body_classes' ); /** * Archives */ /** * Filters the archive title and styles the word before the first colon. * * @since Twenty Twenty 1.0 * * @param string $title Current archive title. * @return string Current archive title. */ function twentytwenty_get_the_archive_title( $title ) { /** * Filters the regular expression used to style the word before the first colon. * * @since Twenty Twenty 1.0 * * @param array $regex An array of regular expression pattern and replacement. */ $regex = apply_filters( 'twentytwenty_get_the_archive_title_regex', array( 'pattern' => '/(\A[^\:]+\:)/', 'replacement' => '<span class="color-accent">$1</span>', ) ); if ( empty( $regex ) ) { return $title; } return preg_replace( $regex['pattern'], $regex['replacement'], $title ); } add_filter( 'get_the_archive_title', 'twentytwenty_get_the_archive_title' ); /** * Miscellaneous */ /** * Toggles animation duration in milliseconds. * * @since Twenty Twenty 1.0 * * @return int Duration in milliseconds */ function twentytwenty_toggle_duration() { /** * Filters the animation duration/speed used usually for submenu toggles. * * @since Twenty Twenty 1.0 * * @param int $duration Duration in milliseconds. */ $duration = apply_filters( 'twentytwenty_toggle_duration', 250 ); return $duration; } /** * Gets unique ID. * * This is a PHP implementation of Underscore's uniqueId method. A static variable * contains an integer that is incremented with each call. This number is returned * with the optional prefix. As such the returned value is not universally unique, * but it is unique across the life of the PHP process. * * @since Twenty Twenty 1.0 * * @see wp_unique_id() Themes requiring WordPress 5.0.3 and greater should use this instead. * * @param string $prefix Prefix for the returned ID. * @return string Unique ID. */ function twentytwenty_unique_id( $prefix = '' ) { static $id_counter = 0; if ( function_exists( 'wp_unique_id' ) ) { return wp_unique_id( $prefix ); } return $prefix . (string) ++$id_counter; } template-functions.php 0000644 00000042434 14720673703 0011116 0 ustar 00 <?php /** * Functions which enhance the theme by hooking into WordPress * * @package WordPress * @subpackage Twenty_Twenty_One * @since Twenty Twenty-One 1.0 */ /** * Adds custom classes to the array of body classes. * * @since Twenty Twenty-One 1.0 * * @param array $classes Classes for the body element. * @return array */ function twenty_twenty_one_body_classes( $classes ) { // Helps detect if JS is enabled or not. $classes[] = 'no-js'; // Adds `singular` to singular pages, and `hfeed` to all other pages. $classes[] = is_singular() ? 'singular' : 'hfeed'; // Add a body class if main navigation is active. if ( has_nav_menu( 'primary' ) ) { $classes[] = 'has-main-navigation'; } // Add a body class if there are no footer widgets. if ( ! is_active_sidebar( 'sidebar-1' ) ) { $classes[] = 'no-widgets'; } return $classes; } add_filter( 'body_class', 'twenty_twenty_one_body_classes' ); /** * Adds custom class to the array of posts classes. * * @since Twenty Twenty-One 1.0 * * @param array $classes An array of CSS classes. * @return array */ function twenty_twenty_one_post_classes( $classes ) { $classes[] = 'entry'; return $classes; } add_filter( 'post_class', 'twenty_twenty_one_post_classes', 10, 3 ); /** * Add a pingback url auto-discovery header for single posts, pages, or attachments. * * @since Twenty Twenty-One 1.0 * * @return void */ function twenty_twenty_one_pingback_header() { if ( is_singular() && pings_open() ) { echo '<link rel="pingback" href="', esc_url( get_bloginfo( 'pingback_url' ) ), '">'; } } add_action( 'wp_head', 'twenty_twenty_one_pingback_header' ); /** * Remove the `no-js` class from body if JS is supported. * * @since Twenty Twenty-One 1.0 * * @return void */ function twenty_twenty_one_supports_js() { echo '<script>document.body.classList.remove("no-js");</script>'; } add_action( 'wp_footer', 'twenty_twenty_one_supports_js' ); /** * Changes comment form default fields. * * @since Twenty Twenty-One 1.0 * * @param array $defaults The form defaults. * @return array */ function twenty_twenty_one_comment_form_defaults( $defaults ) { // Adjust height of comment form. $defaults['comment_field'] = preg_replace( '/rows="\d+"/', 'rows="5"', $defaults['comment_field'] ); return $defaults; } add_filter( 'comment_form_defaults', 'twenty_twenty_one_comment_form_defaults' ); /** * Determines if post thumbnail can be displayed. * * @since Twenty Twenty-One 1.0 * * @return bool */ function twenty_twenty_one_can_show_post_thumbnail() { /** * Filters whether post thumbnail can be displayed. * * @since Twenty Twenty-One 1.0 * * @param bool $show_post_thumbnail Whether to show post thumbnail. */ return apply_filters( 'twenty_twenty_one_can_show_post_thumbnail', ! post_password_required() && ! is_attachment() && has_post_thumbnail() ); } /** * Returns the size for avatars used in the theme. * * @since Twenty Twenty-One 1.0 * * @return int */ function twenty_twenty_one_get_avatar_size() { return 60; } /** * Creates continue reading text. * * @since Twenty Twenty-One 1.0 */ function twenty_twenty_one_continue_reading_text() { $continue_reading = sprintf( /* translators: %s: Post title. Only visible to screen readers. */ esc_html__( 'Continue reading %s', 'twentytwentyone' ), the_title( '<span class="screen-reader-text">', '</span>', false ) ); return $continue_reading; } /** * Creates the continue reading link for excerpt. * * @since Twenty Twenty-One 1.0 */ function twenty_twenty_one_continue_reading_link_excerpt() { if ( ! is_admin() ) { return '… <a class="more-link" href="' . esc_url( get_permalink() ) . '">' . twenty_twenty_one_continue_reading_text() . '</a>'; } } // Filter the excerpt more link. add_filter( 'excerpt_more', 'twenty_twenty_one_continue_reading_link_excerpt' ); /** * Creates the continue reading link. * * @since Twenty Twenty-One 1.0 */ function twenty_twenty_one_continue_reading_link() { if ( ! is_admin() ) { return '<div class="more-link-container"><a class="more-link" href="' . esc_url( get_permalink() ) . '#more-' . esc_attr( get_the_ID() ) . '">' . twenty_twenty_one_continue_reading_text() . '</a></div>'; } } // Filter the content more link. add_filter( 'the_content_more_link', 'twenty_twenty_one_continue_reading_link' ); if ( ! function_exists( 'twenty_twenty_one_post_title' ) ) { /** * Adds a title to posts and pages that are missing titles. * * @since Twenty Twenty-One 1.0 * * @param string $title The title. * @return string */ function twenty_twenty_one_post_title( $title ) { return '' === $title ? esc_html_x( 'Untitled', 'Added to posts and pages that are missing titles', 'twentytwentyone' ) : $title; } } add_filter( 'the_title', 'twenty_twenty_one_post_title' ); /** * Gets the SVG code for a given icon. * * @since Twenty Twenty-One 1.0 * * @param string $group The icon group. * @param string $icon The icon. * @param int $size The icon size in pixels. * @return string */ function twenty_twenty_one_get_icon_svg( $group, $icon, $size = 24 ) { return Twenty_Twenty_One_SVG_Icons::get_svg( $group, $icon, $size ); } /** * Changes the default navigation arrows to svg icons * * @since Twenty Twenty-One 1.0 * * @param string $calendar_output The generated HTML of the calendar. * @return string */ function twenty_twenty_one_change_calendar_nav_arrows( $calendar_output ) { $calendar_output = str_replace( '« ', is_rtl() ? twenty_twenty_one_get_icon_svg( 'ui', 'arrow_right' ) : twenty_twenty_one_get_icon_svg( 'ui', 'arrow_left' ), $calendar_output ); $calendar_output = str_replace( ' »', is_rtl() ? twenty_twenty_one_get_icon_svg( 'ui', 'arrow_left' ) : twenty_twenty_one_get_icon_svg( 'ui', 'arrow_right' ), $calendar_output ); return $calendar_output; } add_filter( 'get_calendar', 'twenty_twenty_one_change_calendar_nav_arrows' ); /** * Get custom CSS. * * Return CSS for non-latin language, if available, or null * * @since Twenty Twenty-One 1.0 * * @param string $type Whether to return CSS for the "front-end", "block-editor", or "classic-editor". * @return string */ function twenty_twenty_one_get_non_latin_css( $type = 'front-end' ) { // Fetch site locale. $locale = get_bloginfo( 'language' ); /** * Filters the fallback fonts for non-latin languages. * * @since Twenty Twenty-One 1.0 * * @param array $font_family An array of locales and font families. */ $font_family = apply_filters( 'twenty_twenty_one_get_localized_font_family_types', array( // Arabic. 'ar' => array( 'Tahoma', 'Arial', 'sans-serif' ), 'ary' => array( 'Tahoma', 'Arial', 'sans-serif' ), 'azb' => array( 'Tahoma', 'Arial', 'sans-serif' ), 'ckb' => array( 'Tahoma', 'Arial', 'sans-serif' ), 'fa-IR' => array( 'Tahoma', 'Arial', 'sans-serif' ), 'haz' => array( 'Tahoma', 'Arial', 'sans-serif' ), 'ps' => array( 'Tahoma', 'Arial', 'sans-serif' ), // Chinese Simplified (China) - Noto Sans SC. 'zh-CN' => array( '\'PingFang SC\'', '\'Helvetica Neue\'', '\'Microsoft YaHei New\'', '\'STHeiti Light\'', 'sans-serif' ), // Chinese Traditional (Taiwan) - Noto Sans TC. 'zh-TW' => array( '\'PingFang TC\'', '\'Helvetica Neue\'', '\'Microsoft YaHei New\'', '\'STHeiti Light\'', 'sans-serif' ), // Chinese (Hong Kong) - Noto Sans HK. 'zh-HK' => array( '\'PingFang HK\'', '\'Helvetica Neue\'', '\'Microsoft YaHei New\'', '\'STHeiti Light\'', 'sans-serif' ), // Cyrillic. 'bel' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'bg-BG' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'kk' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'mk-MK' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'mn' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'ru-RU' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'sah' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'sr-RS' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'tt-RU' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), 'uk' => array( '\'Helvetica Neue\'', 'Helvetica', '\'Segoe UI\'', 'Arial', 'sans-serif' ), // Devanagari. 'bn-BD' => array( 'Arial', 'sans-serif' ), 'hi-IN' => array( 'Arial', 'sans-serif' ), 'mr' => array( 'Arial', 'sans-serif' ), 'ne-NP' => array( 'Arial', 'sans-serif' ), // Greek. 'el' => array( '\'Helvetica Neue\', Helvetica, Arial, sans-serif' ), // Gujarati. 'gu' => array( 'Arial', 'sans-serif' ), // Hebrew. 'he-IL' => array( '\'Arial Hebrew\'', 'Arial', 'sans-serif' ), // Japanese. 'ja' => array( 'sans-serif' ), // Korean. 'ko-KR' => array( '\'Apple SD Gothic Neo\'', '\'Malgun Gothic\'', '\'Nanum Gothic\'', 'Dotum', 'sans-serif' ), // Thai. 'th' => array( '\'Sukhumvit Set\'', '\'Helvetica Neue\'', 'Helvetica', 'Arial', 'sans-serif' ), // Vietnamese. 'vi' => array( '\'Libre Franklin\'', 'sans-serif' ), ) ); // Return if the selected language has no fallback fonts. if ( empty( $font_family[ $locale ] ) ) { return ''; } /** * Filters the elements to apply fallback fonts to. * * @since Twenty Twenty-One 1.0 * * @param array $elements An array of elements for "front-end", "block-editor", or "classic-editor". */ $elements = apply_filters( 'twenty_twenty_one_get_localized_font_family_elements', array( 'front-end' => array( 'body', 'input', 'textarea', 'button', '.button', '.faux-button', '.wp-block-button__link', '.wp-block-file__button', '.has-drop-cap:not(:focus)::first-letter', '.entry-content .wp-block-archives', '.entry-content .wp-block-categories', '.entry-content .wp-block-cover-image', '.entry-content .wp-block-latest-comments', '.entry-content .wp-block-latest-posts', '.entry-content .wp-block-pullquote', '.entry-content .wp-block-quote.is-large', '.entry-content .wp-block-quote.is-style-large', '.entry-content .wp-block-archives *', '.entry-content .wp-block-categories *', '.entry-content .wp-block-latest-posts *', '.entry-content .wp-block-latest-comments *', '.entry-content p', '.entry-content ol', '.entry-content ul', '.entry-content dl', '.entry-content dt', '.entry-content cite', '.entry-content figcaption', '.entry-content .wp-caption-text', '.comment-content p', '.comment-content ol', '.comment-content ul', '.comment-content dl', '.comment-content dt', '.comment-content cite', '.comment-content figcaption', '.comment-content .wp-caption-text', '.widget_text p', '.widget_text ol', '.widget_text ul', '.widget_text dl', '.widget_text dt', '.widget-content .rssSummary', '.widget-content cite', '.widget-content figcaption', '.widget-content .wp-caption-text' ), 'block-editor' => array( '.editor-styles-wrapper > *', '.editor-styles-wrapper p', '.editor-styles-wrapper ol', '.editor-styles-wrapper ul', '.editor-styles-wrapper dl', '.editor-styles-wrapper dt', '.editor-post-title__block .editor-post-title__input', '.editor-styles-wrapper .wp-block h1', '.editor-styles-wrapper .wp-block h2', '.editor-styles-wrapper .wp-block h3', '.editor-styles-wrapper .wp-block h4', '.editor-styles-wrapper .wp-block h5', '.editor-styles-wrapper .wp-block h6', '.editor-styles-wrapper .has-drop-cap:not(:focus)::first-letter', '.editor-styles-wrapper cite', '.editor-styles-wrapper figcaption', '.editor-styles-wrapper .wp-caption-text' ), 'classic-editor' => array( 'body#tinymce.wp-editor', 'body#tinymce.wp-editor p', 'body#tinymce.wp-editor ol', 'body#tinymce.wp-editor ul', 'body#tinymce.wp-editor dl', 'body#tinymce.wp-editor dt', 'body#tinymce.wp-editor figcaption', 'body#tinymce.wp-editor .wp-caption-text', 'body#tinymce.wp-editor .wp-caption-dd', 'body#tinymce.wp-editor cite', 'body#tinymce.wp-editor table' ), ) ); // Return if the specified type doesn't exist. if ( empty( $elements[ $type ] ) ) { return ''; } // Include file if function doesn't exist. if ( ! function_exists( 'twenty_twenty_one_generate_css' ) ) { require_once get_theme_file_path( 'inc/custom-css.php' ); // phpcs:ignore WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound } // Return the specified styles. return twenty_twenty_one_generate_css( // @phpstan-ignore-line. implode( ',', $elements[ $type ] ), 'font-family', implode( ',', $font_family[ $locale ] ), null, null, false ); } /** * Print the first instance of a block in the content, and then break away. * * @since Twenty Twenty-One 1.0 * * @param string $block_name The full block type name, or a partial match. * Example: `core/image`, `core-embed/*`. * @param string|null $content The content to search in. Use null for get_the_content(). * @param int $instances How many instances of the block will be printed (max). Default 1. * @return bool Returns true if a block was located & printed, otherwise false. */ function twenty_twenty_one_print_first_instance_of_block( $block_name, $content = null, $instances = 1 ) { $instances_count = 0; $blocks_content = ''; if ( ! $content ) { $content = get_the_content(); } // Parse blocks in the content. $blocks = parse_blocks( $content ); // Loop blocks. foreach ( $blocks as $block ) { // Confidence check. if ( ! isset( $block['blockName'] ) ) { continue; } // Check if this the block matches the $block_name. $is_matching_block = false; // If the block ends with *, try to match the first portion. if ( '*' === $block_name[-1] ) { $is_matching_block = 0 === strpos( $block['blockName'], rtrim( $block_name, '*' ) ); } else { $is_matching_block = $block_name === $block['blockName']; } if ( $is_matching_block ) { // Increment count. ++$instances_count; // Add the block HTML. $blocks_content .= render_block( $block ); // Break the loop if the $instances count was reached. if ( $instances_count >= $instances ) { break; } } } if ( $blocks_content ) { /** This filter is documented in wp-includes/post-template.php */ echo apply_filters( 'the_content', $blocks_content ); // phpcs:ignore WordPress.Security.EscapeOutput return true; } return false; } /** * Retrieve protected post password form content. * * @since Twenty Twenty-One 1.0 * @since Twenty Twenty-One 1.4 Corrected parameter name for `$output`, * added the `$post` parameter. * * @param string $output The password form HTML output. * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string HTML content for password form for password protected post. */ function twenty_twenty_one_password_form( $output, $post = 0 ) { $post = get_post( $post ); $label = 'pwbox-' . ( empty( $post->ID ) ? wp_rand() : $post->ID ); $output = '<p class="post-password-message">' . esc_html__( 'This content is password protected. Please enter a password to view.', 'twentytwentyone' ) . '</p> <form action="' . esc_url( site_url( 'wp-login.php?action=postpass', 'login_post' ) ) . '" class="post-password-form" method="post"> <label class="post-password-form__label" for="' . esc_attr( $label ) . '">' . esc_html_x( 'Password', 'Post password form', 'twentytwentyone' ) . '</label><input class="post-password-form__input" name="post_password" id="' . esc_attr( $label ) . '" type="password" spellcheck="false" size="20" /><input type="submit" class="post-password-form__submit" name="' . esc_attr_x( 'Submit', 'Post password form', 'twentytwentyone' ) . '" value="' . esc_attr_x( 'Enter', 'Post password form', 'twentytwentyone' ) . '" /></form> '; return $output; } add_filter( 'the_password_form', 'twenty_twenty_one_password_form', 10, 2 ); /** * Filters the list of attachment image attributes. * * @since Twenty Twenty-One 1.0 * * @param string[] $attr Array of attribute values for the image markup, keyed by attribute name. * See wp_get_attachment_image(). * @param WP_Post $attachment Image attachment post. * @param string|int[] $size Requested image size. Can be any registered image size name, or * an array of width and height values in pixels (in that order). * @return string[] The filtered attributes for the image markup. */ function twenty_twenty_one_get_attachment_image_attributes( $attr, $attachment, $size ) { if ( is_admin() ) { return $attr; } if ( isset( $attr['class'] ) && false !== strpos( $attr['class'], 'custom-logo' ) ) { return $attr; } $width = false; $height = false; if ( is_array( $size ) ) { $width = (int) $size[0]; $height = (int) $size[1]; } elseif ( $attachment && is_object( $attachment ) && $attachment->ID ) { $meta = wp_get_attachment_metadata( $attachment->ID ); if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) { $width = (int) $meta['width']; $height = (int) $meta['height']; } } if ( $width && $height ) { // Add style. $attr['style'] = isset( $attr['style'] ) ? $attr['style'] : ''; $attr['style'] = 'width:100%;height:' . round( 100 * $height / $width, 2 ) . '%;max-width:' . $width . 'px;' . $attr['style']; } return $attr; } add_filter( 'wp_get_attachment_image_attributes', 'twenty_twenty_one_get_attachment_image_attributes', 10, 3 ); back-compat.php 0000644 00000004654 14720673703 0007460 0 ustar 00 <?php /** * Back compat functionality * * Prevents the theme from running on WordPress versions prior to 5.3, * since this theme is not meant to be backward compatible beyond that and * relies on many newer functions and markup changes introduced in 5.3. * * @package WordPress * @subpackage Twenty_Twenty_One * @since Twenty Twenty-One 1.0 */ /** * Display upgrade notice on theme switch. * * @since Twenty Twenty-One 1.0 * * @return void */ function twenty_twenty_one_switch_theme() { add_action( 'admin_notices', 'twenty_twenty_one_upgrade_notice' ); } add_action( 'after_switch_theme', 'twenty_twenty_one_switch_theme' ); /** * Adds a message for unsuccessful theme switch. * * Prints an update nag after an unsuccessful attempt to switch to * the theme on WordPress versions prior to 5.3. * * @since Twenty Twenty-One 1.0 * * @global string $wp_version WordPress version. * * @return void */ function twenty_twenty_one_upgrade_notice() { echo '<div class="error"><p>'; printf( /* translators: %s: WordPress Version. */ esc_html__( 'This theme requires WordPress 5.3 or newer. You are running version %s. Please upgrade.', 'twentytwentyone' ), esc_html( $GLOBALS['wp_version'] ) ); echo '</p></div>'; } /** * Prevents the Customizer from being loaded on WordPress versions prior to 5.3. * * @since Twenty Twenty-One 1.0 * * @global string $wp_version WordPress version. * * @return void */ function twenty_twenty_one_customize() { wp_die( sprintf( /* translators: %s: WordPress Version. */ esc_html__( 'This theme requires WordPress 5.3 or newer. You are running version %s. Please upgrade.', 'twentytwentyone' ), esc_html( $GLOBALS['wp_version'] ) ), '', array( 'back_link' => true, ) ); } add_action( 'load-customize.php', 'twenty_twenty_one_customize' ); /** * Prevents the Theme Preview from being loaded on WordPress versions prior to 5.3. * * @since Twenty Twenty-One 1.0 * * @global string $wp_version WordPress version. * * @return void */ function twenty_twenty_one_preview() { if ( isset( $_GET['preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification wp_die( sprintf( /* translators: %s: WordPress Version. */ esc_html__( 'This theme requires WordPress 5.3 or newer. You are running version %s. Please upgrade.', 'twentytwentyone' ), esc_html( $GLOBALS['wp_version'] ) ) ); } } add_action( 'template_redirect', 'twenty_twenty_one_preview' ); block-patterns.php 0000644 00000023305 14720673703 0010221 0 ustar 00 <?php /** * Block Patterns * * @link https://developer.wordpress.org/reference/functions/register_block_pattern/ * @link https://developer.wordpress.org/reference/functions/register_block_pattern_category/ * * @package WordPress * @subpackage Twenty_Twenty * @since Twenty Twenty 1.6 */ /** * Register Block Pattern Category. */ if ( function_exists( 'register_block_pattern_category' ) ) { register_block_pattern_category( 'twentytwenty', array( 'label' => esc_html__( 'Twenty Twenty', 'twentytwenty' ) ) ); } /** * Register Block Patterns. */ if ( function_exists( 'register_block_pattern' ) ) { // Call to Action. register_block_pattern( 'twentytwenty/call-to-action', array( 'title' => esc_html__( 'Call to Action', 'twentytwenty' ), 'categories' => array( 'twentytwenty' ), 'viewportWidth' => 1400, 'content' => implode( '', array( '<!-- wp:group {"align":"wide","style":{"color":{"background":"#ffffff"}}} -->', '<div class="wp-block-group alignwide has-background" style="background-color:#ffffff"><div class="wp-block-group__inner-container"><!-- wp:group -->', '<div class="wp-block-group"><div class="wp-block-group__inner-container"><!-- wp:heading {"align":"center"} -->', '<h2 class="has-text-align-center">' . esc_html__( 'Support the Museum and Get Exclusive Offers', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"align":"center"} -->', '<p class="has-text-align-center">' . esc_html__( 'Members get access to exclusive exhibits and sales. Our memberships cost $99.99 and are billed annually.', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"align":"center","className":"is-style-outline"} -->', '<div class="wp-block-button aligncenter is-style-outline"><a class="wp-block-button__link" href="#">' . esc_html__( 'Become a Member', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div></div>', '<!-- /wp:group --></div></div>', '<!-- /wp:group -->', ) ), ) ); // Double Call to Action. register_block_pattern( 'twentytwenty/double-call-to-action', array( 'title' => esc_html__( 'Double Call to Action', 'twentytwenty' ), 'categories' => array( 'twentytwenty' ), 'viewportWidth' => 1400, 'content' => implode( '', array( '<!-- wp:columns {"align":"wide"} -->', '<div class="wp-block-columns alignwide"><!-- wp:column -->', '<div class="wp-block-column"><!-- wp:group {"style":{"color":{"background":"#ffffff"}}} -->', '<div class="wp-block-group has-background" style="background-color:#ffffff"><div class="wp-block-group__inner-container"><!-- wp:heading {"align":"center"} -->', '<h2 class="has-text-align-center">' . esc_html__( 'The Museum', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"align":"center"} -->', '<p class="has-text-align-center">' . esc_html__( 'Award-winning exhibitions featuring internationally-renowned artists.', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:buttons {"align":"center"} -->', '<div class="wp-block-buttons aligncenter"><!-- wp:button {"className":"is-style-outline"} -->', '<div class="wp-block-button is-style-outline"><a class="wp-block-button__link">' . esc_html__( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div>', '<!-- /wp:buttons --></div></div>', '<!-- /wp:group --></div>', '<!-- /wp:column -->', '<!-- wp:column -->', '<div class="wp-block-column"><!-- wp:group {"style":{"color":{"background":"#ffffff"}}} -->', '<div class="wp-block-group has-background" style="background-color:#ffffff"><div class="wp-block-group__inner-container"><!-- wp:heading {"align":"center"} -->', '<h2 class="has-text-align-center">' . esc_html__( 'The Store', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"align":"center"} -->', '<p class="has-text-align-center">' . esc_html__( 'An awe-inspiring collection of books, prints, and gifts from our exhibitions.', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:buttons {"align":"center"} -->', '<div class="wp-block-buttons aligncenter"><!-- wp:button {"className":"is-style-outline"} -->', '<div class="wp-block-button is-style-outline"><a class="wp-block-button__link">' . esc_html__( 'Shop Now', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div>', '<!-- /wp:buttons --></div></div>', '<!-- /wp:group --></div>', '<!-- /wp:column --></div>', '<!-- /wp:columns -->', ) ), ) ); // Event Details. register_block_pattern( 'twentytwenty/event-details', array( 'title' => esc_html__( 'Event Details', 'twentytwenty' ), 'categories' => array( 'twentytwenty' ), 'viewportWidth' => 1400, 'content' => implode( '', array( '<!-- wp:group {"align":"wide","backgroundColor":"primary"} -->', '<div class="wp-block-group alignwide has-primary-background-color has-background"><div class="wp-block-group__inner-container"><!-- wp:columns -->', '<div class="wp-block-columns"><!-- wp:column -->', '<div class="wp-block-column"><!-- wp:paragraph {"align":"center","textColor":"background","fontSize":"large"} -->', '<p class="has-text-align-center has-background-color has-text-color has-large-font-size">' . wp_kses_post( __( '<em>Dates</em><br>Aug 1 — Dec 1', 'twentytwenty' ) ) . '</p>', '<!-- /wp:paragraph --></div>', '<!-- /wp:column -->', '<!-- wp:column -->', '<div class="wp-block-column"><!-- wp:paragraph {"align":"center","textColor":"background","fontSize":"large"} -->', '<p class="has-text-align-center has-background-color has-text-color has-large-font-size">' . wp_kses_post( __( '<em>Location</em><br>Exhibit Hall B', 'twentytwenty' ) ) . '</p>', '<!-- /wp:paragraph --></div>', '<!-- /wp:column -->', '<!-- wp:column -->', '<div class="wp-block-column"><!-- wp:paragraph {"align":"center","textColor":"background","fontSize":"large"} -->', '<p class="has-text-align-center has-background-color has-text-color has-large-font-size">' . wp_kses_post( __( '<em>Price</em><br>Included', 'twentytwenty' ) ) . '</p>', '<!-- /wp:paragraph --></div>', '<!-- /wp:column --></div>', '<!-- /wp:columns --></div></div>', '<!-- /wp:group -->', ) ), ) ); // Featured Content. register_block_pattern( 'twentytwenty/featured-content', array( 'title' => esc_html__( 'Featured Content', 'twentytwenty' ), 'categories' => array( 'twentytwenty' ), 'viewportWidth' => 1400, 'content' => implode( '', array( '<!-- wp:columns {"align":"wide"} -->', '<div class="wp-block-columns alignwide"><!-- wp:column -->', '<div class="wp-block-column"><!-- wp:image {"sizeSlug":"full"} -->', '<figure class="wp-block-image size-full"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/2020-three-quarters-1.png" alt="' . esc_attr__( 'Abstract Rectangles', 'twentytwenty' ) . '"/></figure>', '<!-- /wp:image -->', '<!-- wp:heading -->', '<h2>' . esc_html__( 'Works and Days', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"fontSize":"larger"} -->', '<p class="has-larger-font-size">' . esc_html__( 'August 1 — December 1', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"align":"left","className":"is-style-outline"} -->', '<div class="wp-block-button alignleft is-style-outline"><a class="wp-block-button__link" href="#">' . esc_html__( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div>', '<!-- /wp:column -->', '<!-- wp:column -->', '<div class="wp-block-column"><!-- wp:image {sizeSlug":"full"} -->', '<figure class="wp-block-image size-full"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/2020-three-quarters-2.png" alt="' . esc_attr__( 'Abstract Rectangles', 'twentytwenty' ) . '"/></figure>', '<!-- /wp:image -->', '<!-- wp:heading -->', '<h2>' . esc_html__( 'The Life I Deserve', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"fontSize":"larger"} -->', '<p class="has-larger-font-size">' . esc_html__( 'August 1 — December 1', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"align":"left","className":"is-style-outline"} -->', '<div class="wp-block-button alignleft is-style-outline"><a class="wp-block-button__link" href="#">' . esc_html__( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div>', '<!-- /wp:column --></div>', '<!-- /wp:columns -->', ) ), ) ); // Introduction. register_block_pattern( 'twentytwenty/introduction', array( 'title' => esc_html__( 'Introduction', 'twentytwenty' ), 'categories' => array( 'twentytwenty' ), 'viewportWidth' => 1400, 'content' => implode( '', array( '<!-- wp:heading {"align":"center"} -->', '<h2 class="has-text-align-center">' . esc_html__( 'The Premier Destination for Modern Art in Sweden', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"dropCap":true} -->', '<p class="has-drop-cap">' . esc_html__( 'With seven floors of striking architecture, UMoMA shows exhibitions of international contemporary art, sometimes along with art historical retrospectives. Existential, political, and philosophical issues are intrinsic to our program. As visitor, you are invited to guided tours artist talks, lectures, film screenings, and other events with free admission.', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', ) ), ) ); } custom-css.php 0000644 00000022115 14720673703 0007367 0 ustar 00 <?php /** * Twenty Twenty Custom CSS * * @package WordPress * @subpackage Twenty_Twenty * @since Twenty Twenty 1.0 */ if ( ! function_exists( 'twentytwenty_generate_css' ) ) { /** * Generate CSS. * * @since Twenty Twenty 1.0 * * @param string $selector The CSS selector. * @param string $style The CSS style. * @param string $value The CSS value. * @param string $prefix The CSS prefix. * @param string $suffix The CSS suffix. * @param bool $display Print the styles. */ function twentytwenty_generate_css( $selector, $style, $value, $prefix = '', $suffix = '', $display = true ) { $return = ''; /* * Bail early if we have no $selector elements or properties and $value. */ if ( ! $value || ! $selector ) { return; } $return = sprintf( '%s { %s: %s; }', $selector, $style, $prefix . $value . $suffix ); if ( $display ) { echo $return; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to double check this, but for now, we want to pass PHPCS ;) } return $return; } } if ( ! function_exists( 'twentytwenty_get_customizer_css' ) ) { /** * Get CSS Built from Customizer Options. * Build CSS reflecting colors, fonts and other options set in the Customizer, and return them for output. * * @since Twenty Twenty 1.0 * * @param string $type Whether to return CSS for the "front-end", "block-editor", or "classic-editor". */ function twentytwenty_get_customizer_css( $type = 'front-end' ) { // Get variables. $body = sanitize_hex_color( twentytwenty_get_color_for_area( 'content', 'text' ) ); $body_default = '#000000'; $secondary = sanitize_hex_color( twentytwenty_get_color_for_area( 'content', 'secondary' ) ); $secondary_default = '#6d6d6d'; $borders = sanitize_hex_color( twentytwenty_get_color_for_area( 'content', 'borders' ) ); $borders_default = '#dcd7ca'; $accent = sanitize_hex_color( twentytwenty_get_color_for_area( 'content', 'accent' ) ); $accent_default = '#cd2653'; // Header. $header_footer_background = sanitize_hex_color( twentytwenty_get_color_for_area( 'header-footer', 'background' ) ); $header_footer_background_default = '#ffffff'; // Cover. $cover = sanitize_hex_color( get_theme_mod( 'cover_template_overlay_text_color' ) ); $cover_default = '#ffffff'; // Background. $background = sanitize_hex_color_no_hash( get_theme_mod( 'background_color' ) ); $background_default = 'f5efe0'; ob_start(); /* * Note – Styles are applied in this order: * 1. Element specific * 2. Helper classes * * This enables all helper classes to overwrite base element styles, * meaning that any color classes applied in the block editor will * have a higher priority than the base element styles. */ // Front-End Styles. if ( 'front-end' === $type ) { // Auto-calculated colors. $elements_definitions = twentytwenty_get_elements_array(); foreach ( $elements_definitions as $context => $props ) { foreach ( $props as $key => $definitions ) { foreach ( $definitions as $property => $elements ) { /* * If we don't have an elements array or it is empty * then skip this iteration early; */ if ( ! is_array( $elements ) || empty( $elements ) ) { continue; } $val = twentytwenty_get_color_for_area( $context, $key ); if ( $val ) { twentytwenty_generate_css( implode( ',', $elements ), $property, $val ); } } } } if ( $cover && $cover !== $cover_default ) { twentytwenty_generate_css( '.overlay-header .header-inner', 'color', $cover ); twentytwenty_generate_css( '.cover-header .entry-header *', 'color', $cover ); } // Block Editor Styles. } elseif ( 'block-editor' === $type ) { // Colors. // Accent color. if ( $accent && $accent !== $accent_default ) { twentytwenty_generate_css( ':root .has-accent-color, .editor-styles-wrapper a, .editor-styles-wrapper .has-drop-cap:not(:focus)::first-letter, .editor-styles-wrapper .wp-block-button.is-style-outline .wp-block-button__link, .editor-styles-wrapper .wp-block-pullquote::before, .editor-styles-wrapper .wp-block-file .wp-block-file__textlink', 'color', $accent ); twentytwenty_generate_css( '.editor-styles-wrapper .wp-block-quote', 'border-color', $accent, '' ); twentytwenty_generate_css( '.has-accent-background-color, .editor-styles-wrapper .wp-block-button__link, .editor-styles-wrapper .wp-block-file__button', 'background-color', $accent ); } // Background color. if ( $background && $background !== $background_default ) { twentytwenty_generate_css( '.editor-styles-wrapper', 'background-color', '#' . $background ); twentytwenty_generate_css( '.has-background.has-primary-background-color:not(.has-text-color),.has-background.has-primary-background-color *:not(.has-text-color),.has-background.has-accent-background-color:not(.has-text-color),.has-background.has-accent-background-color *:not(.has-text-color)', 'color', '#' . $background ); } // Borders color. if ( $borders && $borders !== $borders_default ) { twentytwenty_generate_css( '.editor-styles-wrapper .wp-block-code, .editor-styles-wrapper pre, .editor-styles-wrapper .wp-block-preformatted pre, .editor-styles-wrapper .wp-block-verse pre, .editor-styles-wrapper fieldset, .editor-styles-wrapper .wp-block-table, .editor-styles-wrapper .wp-block-table *, .editor-styles-wrapper .wp-block-table.is-style-stripes, .editor-styles-wrapper .wp-block-latest-posts.is-grid li', 'border-color', $borders ); twentytwenty_generate_css( '.editor-styles-wrapper .wp-block-table caption, .editor-styles-wrapper .wp-block-table.is-style-stripes tbody tr:nth-child(odd)', 'background-color', $borders ); } // Text color. if ( $body && $body !== $body_default ) { twentytwenty_generate_css( 'html .editor-styles-wrapper, .editor-post-title__block .editor-post-title__input, .editor-post-title__block .editor-post-title__input:focus', 'color', $body ); } // Secondary color. if ( $secondary && $secondary !== $secondary_default ) { twentytwenty_generate_css( '.editor-styles-wrapper figcaption, .editor-styles-wrapper cite, .editor-styles-wrapper .wp-block-quote__citation, .editor-styles-wrapper .wp-block-quote cite, .editor-styles-wrapper .wp-block-quote footer, .editor-styles-wrapper .wp-block-pullquote__citation, .editor-styles-wrapper .wp-block-pullquote cite, .editor-styles-wrapper .wp-block-pullquote footer, .editor-styles-wrapper ul.wp-block-archives li, .editor-styles-wrapper ul.wp-block-categories li, .editor-styles-wrapper ul.wp-block-latest-posts li, .editor-styles-wrapper ul.wp-block-categories__list li, .editor-styles-wrapper .wp-block-latest-comments time, .editor-styles-wrapper .wp-block-latest-posts time', 'color', $secondary ); } // Header Footer Background Color. if ( $header_footer_background && $header_footer_background !== $header_footer_background_default ) { twentytwenty_generate_css( '.editor-styles-wrapper .wp-block-pullquote::before', 'background-color', $header_footer_background ); } } elseif ( 'classic-editor' === $type ) { // Colors. // Accent color. if ( $accent && $accent !== $accent_default ) { twentytwenty_generate_css( 'body#tinymce.wp-editor.content a, body#tinymce.wp-editor.content a:focus, body#tinymce.wp-editor.content a:hover', 'color', $accent ); twentytwenty_generate_css( 'body#tinymce.wp-editor.content blockquote, body#tinymce.wp-editor.content .wp-block-quote', 'border-color', $accent, '', ' !important' ); twentytwenty_generate_css( 'body#tinymce.wp-editor.content button, body#tinymce.wp-editor.content .faux-button, body#tinymce.wp-editor.content .wp-block-button__link, body#tinymce.wp-editor.content .wp-block-file__button, body#tinymce.wp-editor.content input[type=\'button\'], body#tinymce.wp-editor.content input[type=\'reset\'], body#tinymce.wp-editor.content input[type=\'submit\']', 'background-color', $accent ); } // Background color. if ( $background && $background !== $background_default ) { twentytwenty_generate_css( 'body#tinymce.wp-editor.content', 'background-color', '#' . $background ); } // Text color. if ( $body && $body !== $body_default ) { twentytwenty_generate_css( 'body#tinymce.wp-editor.content', 'color', $body ); } // Secondary color. if ( $secondary && $secondary !== $secondary_default ) { twentytwenty_generate_css( 'body#tinymce.wp-editor.content hr:not(.is-style-dots), body#tinymce.wp-editor.content cite, body#tinymce.wp-editor.content figcaption, body#tinymce.wp-editor.content .wp-caption-text, body#tinymce.wp-editor.content .wp-caption-dd, body#tinymce.wp-editor.content .gallery-caption', 'color', $secondary ); } // Borders color. if ( $borders && $borders !== $borders_default ) { twentytwenty_generate_css( 'body#tinymce.wp-editor.content pre, body#tinymce.wp-editor.content hr, body#tinymce.wp-editor.content fieldset,body#tinymce.wp-editor.content input, body#tinymce.wp-editor.content textarea', 'border-color', $borders ); } } // Return the results. return ob_get_clean(); } } menu-functions.php 0000644 00000007257 14720673703 0010253 0 ustar 00 <?php /** * Functions and filters related to the menus. * * Makes the default WordPress navigation use an HTML structure similar * to the Navigation block. * * @link https://make.wordpress.org/themes/2020/07/06/printing-navigation-block-html-from-a-legacy-menu-in-themes/ * * @package WordPress * @subpackage Twenty_Twenty_One * @since Twenty Twenty-One 1.0 */ /** * Add a button to top-level menu items that has sub-menus. * An icon is added using CSS depending on the value of aria-expanded. * * @since Twenty Twenty-One 1.0 * * @param string $output Nav menu item start element. * @param object $item Nav menu item. * @param int $depth Depth. * @param object $args Nav menu args. * @return string Nav menu item start element. */ function twenty_twenty_one_add_sub_menu_toggle( $output, $item, $depth, $args ) { if ( 'primary' === $args->theme_location && 0 === $depth && in_array( 'menu-item-has-children', $item->classes, true ) ) { // Add toggle button. $output .= '<button class="sub-menu-toggle" aria-expanded="false" onClick="twentytwentyoneExpandSubMenu(this)">'; $output .= '<span class="icon-plus">' . twenty_twenty_one_get_icon_svg( 'ui', 'plus', 18 ) . '</span>'; $output .= '<span class="icon-minus">' . twenty_twenty_one_get_icon_svg( 'ui', 'minus', 18 ) . '</span>'; /* translators: Hidden accessibility text. */ $output .= '<span class="screen-reader-text">' . esc_html__( 'Open menu', 'twentytwentyone' ) . '</span>'; $output .= '</button>'; } return $output; } add_filter( 'walker_nav_menu_start_el', 'twenty_twenty_one_add_sub_menu_toggle', 10, 4 ); /** * Detects the social network from a URL and returns the SVG code for its icon. * * @since Twenty Twenty-One 1.0 * * @param string $uri Social link. * @param int $size The icon size in pixels. * @return string */ function twenty_twenty_one_get_social_link_svg( $uri, $size = 24 ) { return Twenty_Twenty_One_SVG_Icons::get_social_link_svg( $uri, $size ); } /** * Displays SVG icons in the footer navigation. * * @since Twenty Twenty-One 1.0 * * @param string $item_output The menu item's starting HTML output. * @param WP_Post $item Menu item data object. * @param int $depth Depth of the menu. Used for padding. * @param stdClass $args An object of wp_nav_menu() arguments. * @return string The menu item output with social icon. */ function twenty_twenty_one_nav_menu_social_icons( $item_output, $item, $depth, $args ) { // Change SVG icon inside social links menu if there is supported URL. if ( 'footer' === $args->theme_location ) { $svg = twenty_twenty_one_get_social_link_svg( $item->url, 24 ); if ( ! empty( $svg ) ) { $item_output = str_replace( $args->link_before, $svg, $item_output ); } } return $item_output; } add_filter( 'walker_nav_menu_start_el', 'twenty_twenty_one_nav_menu_social_icons', 10, 4 ); /** * Filters the arguments for a single nav menu item. * * @since Twenty Twenty-One 1.0 * * @param stdClass $args An object of wp_nav_menu() arguments. * @param WP_Post $item Menu item data object. * @param int $depth Depth of menu item. Used for padding. * @return stdClass */ function twenty_twenty_one_add_menu_description_args( $args, $item, $depth ) { if ( '</span>' !== $args->link_after ) { $args->link_after = ''; } if ( 0 === $depth && isset( $item->description ) && $item->description ) { // The extra <span> element is here for styling purposes: Allows the description to not be underlined on hover. $args->link_after = '<p class="menu-item-description"><span>' . $item->description . '</span></p>'; } return $args; } add_filter( 'nav_menu_item_args', 'twenty_twenty_one_add_menu_description_args', 10, 3 ); starter-content.php 0000644 00000027145 14720673703 0010433 0 ustar 00 <?php /** * Twenty Twenty Starter Content * * @link https://make.wordpress.org/core/2016/11/30/starter-content-for-themes-in-4-7/ * * @package WordPress * @subpackage Twenty_Twenty * @since Twenty Twenty 1.0 */ /** * Function to return the array of starter content for the theme. * * Passes it through the `twentytwenty_starter_content` filter before returning. * * @since Twenty Twenty 1.0 * * @return array A filtered array of args for the starter_content. */ function twentytwenty_get_starter_content() { // Define and register starter content to showcase the theme on new sites. $starter_content = array( 'widgets' => array( // Place one core-defined widgets in the first footer widget area. 'sidebar-1' => array( 'text_about', ), // Place one core-defined widgets in the second footer widget area. 'sidebar-2' => array( 'text_business_info', ), ), // Create the custom image attachments used as post thumbnails for pages. 'attachments' => array( 'image-opening' => array( 'post_title' => _x( 'The New UMoMA Opens its Doors', 'Theme starter content', 'twentytwenty' ), 'file' => 'assets/images/2020-landscape-1.png', // URL relative to the template directory. ), ), // Specify the core-defined pages to create and add custom thumbnails to some of them. 'posts' => array( 'front' => array( 'post_type' => 'page', 'post_title' => __( 'The New UMoMA Opens its Doors', 'twentytwenty' ), // Use the above featured image with the predefined about page. 'thumbnail' => '{{image-opening}}', 'post_content' => implode( '', array( '<!-- wp:group {"align":"wide"} -->', '<div class="wp-block-group alignwide"><div class="wp-block-group__inner-container"><!-- wp:heading {"align":"center"} -->', '<h2 class="has-text-align-center">' . __( 'The premier destination for modern art in Northern Sweden. Open from 10 AM to 6 PM every day during the summer months.', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading --></div></div>', '<!-- /wp:group -->', '<!-- wp:columns {"align":"wide"} -->', '<div class="wp-block-columns alignwide"><!-- wp:column -->', '<div class="wp-block-column"><!-- wp:group -->', '<div class="wp-block-group"><div class="wp-block-group__inner-container">', '<!-- wp:image {"align":"full","id":37,"sizeSlug":"full"} -->', '<figure class="wp-block-image alignfull size-full"><img src="' . get_theme_file_uri() . '/assets/images/2020-three-quarters-1.png" alt="" class="wp-image-37"/></figure>', '<!-- /wp:image -->', '<!-- wp:heading {"level":3} -->', '<h3>' . __( 'Works and Days', 'twentytwenty' ) . '</h3>', '<!-- /wp:heading -->', '<!-- wp:paragraph -->', '<p>' . __( 'August 1 -- December 1', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"className":"is-style-outline"} -->', '<div class="wp-block-button is-style-outline"><a class="wp-block-button__link" href="https://make.wordpress.org/core/2019/09/27/block-editor-theme-related-updates-in-wordpress-5-3/">' . __( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div></div>', '<!-- /wp:group -->', '<!-- wp:group -->', '<div class="wp-block-group"><div class="wp-block-group__inner-container">', '<!-- wp:image {"align":"full","id":37,"sizeSlug":"full"} -->', '<figure class="wp-block-image alignfull size-full"><img src="' . get_theme_file_uri() . '/assets/images/2020-three-quarters-3.png" alt="" class="wp-image-37"/></figure>', '<!-- /wp:image -->', '<!-- wp:heading {"level":3} -->', '<h3>' . __( 'Theatre of Operations', 'twentytwenty' ) . '</h3>', '<!-- /wp:heading -->', '<!-- wp:paragraph -->', '<p>' . __( 'October 1 -- December 1', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"className":"is-style-outline"} -->', '<div class="wp-block-button is-style-outline"><a class="wp-block-button__link" href="https://make.wordpress.org/core/2019/09/27/block-editor-theme-related-updates-in-wordpress-5-3/">' . __( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div></div>', '<!-- /wp:group --></div>', '<!-- /wp:column -->', '<!-- wp:column -->', '<div class="wp-block-column"><!-- wp:group -->', '<div class="wp-block-group"><div class="wp-block-group__inner-container">', '<!-- wp:image {"align":"full","id":37,"sizeSlug":"full"} -->', '<figure class="wp-block-image alignfull size-full"><img src="' . get_theme_file_uri() . '/assets/images/2020-three-quarters-2.png" alt="" class="wp-image-37"/></figure>', '<!-- /wp:image -->', '<!-- wp:heading {"level":3} -->', '<h3>' . __( 'The Life I Deserve', 'twentytwenty' ) . '</h3>', '<!-- /wp:heading -->', '<!-- wp:paragraph -->', '<p>' . __( 'August 1 -- December 1', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"className":"is-style-outline"} -->', '<div class="wp-block-button is-style-outline"><a class="wp-block-button__link" href="https://make.wordpress.org/core/2019/09/27/block-editor-theme-related-updates-in-wordpress-5-3/">' . __( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div></div>', '<!-- /wp:group -->', '<!-- wp:group -->', '<div class="wp-block-group"><div class="wp-block-group__inner-container">', '<!-- wp:image {"align":"full","id":37,"sizeSlug":"full"} -->', '<figure class="wp-block-image alignfull size-full"><img src="' . get_theme_file_uri() . '/assets/images/2020-three-quarters-4.png" alt="" class="wp-image-37"/></figure>', '<!-- /wp:image -->', '<!-- wp:heading {"level":3} -->', '<h3>' . __( 'From Signac to Matisse', 'twentytwenty' ) . '</h3>', '<!-- /wp:heading -->', '<!-- wp:paragraph -->', '<p>' . __( 'October 1 -- December 1', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"className":"is-style-outline"} -->', '<div class="wp-block-button is-style-outline"><a class="wp-block-button__link" href="https://make.wordpress.org/core/2019/09/27/block-editor-theme-related-updates-in-wordpress-5-3/">' . __( 'Read More', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div></div>', '<!-- /wp:group --></div>', '<!-- /wp:column --></div>', '<!-- /wp:columns -->', '<!-- wp:image {"align":"full","id":37,"sizeSlug":"full"} -->', '<figure class="wp-block-image alignfull size-full"><img src="' . get_theme_file_uri() . '/assets/images/2020-landscape-2.png" alt="" class="wp-image-37"/></figure>', '<!-- /wp:image -->', '<!-- wp:group {"align":"wide"} -->', '<div class="wp-block-group alignwide"><div class="wp-block-group__inner-container"><!-- wp:heading {"align":"center","textColor":"accent"} -->', '<h2 class="has-accent-color has-text-align-center">' . __( '“Cyborgs, as the philosopher Donna Haraway established, are not reverent. They do not remember the cosmos.”', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading --></div></div>', '<!-- /wp:group -->', '<!-- wp:paragraph {"dropCap":true} -->', '<p class="has-drop-cap">' . __( 'With seven floors of striking architecture, UMoMA shows exhibitions of international contemporary art, sometimes along with art historical retrospectives. Existential, political and philosophical issues are intrinsic to our programme. As visitor you are invited to guided tours artist talks, lectures, film screenings and other events with free admission', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:paragraph -->', '<p>' . __( 'The exhibitions are produced by UMoMA in collaboration with artists and museums around the world and they often attract international attention. UMoMA has received a Special Commendation from the European Museum of the Year, and was among the top candidates for the Swedish Museum of the Year Award as well as for the Council of Europe Museum Prize.', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:paragraph -->', '<p></p>', '<!-- /wp:paragraph -->', '<!-- wp:group {"customBackgroundColor":"#ffffff","align":"wide"} -->', '<div class="wp-block-group alignwide has-background" style="background-color:#ffffff"><div class="wp-block-group__inner-container"><!-- wp:group -->', '<div class="wp-block-group"><div class="wp-block-group__inner-container"><!-- wp:heading {"align":"center"} -->', '<h2 class="has-text-align-center">' . __( 'Become a Member and Get Exclusive Offers!', 'twentytwenty' ) . '</h2>', '<!-- /wp:heading -->', '<!-- wp:paragraph {"align":"center"} -->', '<p class="has-text-align-center">' . __( 'Members get access to exclusive exhibits and sales. Our memberships cost $99.99 and are billed annually.', 'twentytwenty' ) . '</p>', '<!-- /wp:paragraph -->', '<!-- wp:button {"align":"center"} -->', '<div class="wp-block-button aligncenter"><a class="wp-block-button__link" href="https://make.wordpress.org/core/2019/09/27/block-editor-theme-related-updates-in-wordpress-5-3/">' . __( 'Join the Club', 'twentytwenty' ) . '</a></div>', '<!-- /wp:button --></div></div>', '<!-- /wp:group --></div></div>', '<!-- /wp:group -->', '<!-- wp:gallery {"ids":[39,38],"align":"wide"} -->', '<figure class="wp-block-gallery alignwide columns-2 is-cropped"><ul class="blocks-gallery-grid"><li class="blocks-gallery-item"><figure><img src="' . get_theme_file_uri() . '/assets/images/2020-square-2.png" alt="" data-id="39" data-full-url="' . get_theme_file_uri() . '/assets/images/2020-square-2.png" data-link="assets/images/2020-square-2/" class="wp-image-39"/></figure></li><li class="blocks-gallery-item"><figure><img src="' . get_theme_file_uri() . '/assets/images/2020-square-1.png" alt="" data-id="38" data-full-url="' . get_theme_file_uri() . '/assets/images/2020-square-1.png" data-link="' . get_theme_file_uri() . '/assets/images/2020-square-1/" class="wp-image-38"/></figure></li></ul></figure>', '<!-- /wp:gallery -->', ) ), ), 'about', 'contact', 'blog', ), // Default to a static front page and assign the front and posts pages. 'options' => array( 'show_on_front' => 'page', 'page_on_front' => '{{front}}', 'page_for_posts' => '{{blog}}', ), // Set up nav menus for each of the two areas registered in the theme. 'nav_menus' => array( // Assign a menu to the "primary" location. 'primary' => array( 'name' => __( 'Primary', 'twentytwenty' ), 'items' => array( 'link_home', // Note that the core "home" page is actually a link in case a static front page is not used. 'page_about', 'page_blog', 'page_contact', ), ), // This replicates primary just to demonstrate the expanded menu. 'expanded' => array( 'name' => __( 'Primary', 'twentytwenty' ), 'items' => array( 'link_home', // Note that the core "home" page is actually a link in case a static front page is not used. 'page_about', 'page_blog', 'page_contact', ), ), // Assign a menu to the "social" location. 'social' => array( 'name' => __( 'Social Links Menu', 'twentytwenty' ), 'items' => array( 'link_yelp', 'link_facebook', 'link_twitter', 'link_instagram', 'link_email', ), ), ), ); /** * Filters Twenty Twenty array of starter content. * * @since Twenty Twenty 1.0 * * @param array $starter_content Array of starter content. */ return apply_filters( 'twentytwenty_starter_content', $starter_content ); } class-wpseo-replace-vars.php 0000644 00000146711 14720701066 0012114 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 1.5.4 */ // Avoid direct calls to this file. if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * Class: WPSEO_Replace_Vars. * * This class implements the replacing of `%%variable_placeholders%%` with their real value based on the current * requested page/post/cpt/etc in text strings. */ class WPSEO_Replace_Vars { /** * Default post/page/cpt information. * * @var array */ protected $defaults = [ 'ID' => '', 'name' => '', 'post_author' => '', 'post_content' => '', 'post_date' => '', 'post_excerpt' => '', 'post_modified' => '', 'post_title' => '', 'taxonomy' => '', 'term_id' => '', 'term404' => '', ]; /** * Current post/page/cpt information. * * @var stdClass */ protected $args; /** * Help texts for use in WPSEO -> Search appearance tabs. * * @var array */ protected static $help_texts = []; /** * Register of additional variable replacements registered by other plugins/themes. * * @var array */ protected static $external_replacements = []; /** * Setup the help texts and external replacements as statics so they will be available to all instances. * * @return void */ public static function setup_statics_once() { if ( self::$help_texts === [] ) { self::set_basic_help_texts(); self::set_advanced_help_texts(); } if ( self::$external_replacements === [] ) { /** * Action: 'wpseo_register_extra_replacements' - Allows for registration of additional * variables to replace. */ do_action( 'wpseo_register_extra_replacements' ); } } /** * Register new replacement %%variables%%. * For use by other plugins/themes to register extra variables. * * @see wpseo_register_var_replacement() for a usage example. * * @param string $var_to_replace The name of the variable to replace, i.e. '%%var%%'. * Note: the surrounding %% are optional. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable. * Uses the same format as add_filter/add_action function parameter and * should *return* the replacement value. DON'T echo it. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'. * @param string $help_text Help text to be added to the help tab for this variable. * * @return bool Whether the replacement function was succesfully registered. */ public static function register_replacement( $var_to_replace, $replace_function, $type = 'advanced', $help_text = '' ) { $success = false; if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) { $var_to_replace = self::remove_var_delimiter( $var_to_replace ); if ( preg_match( '`^[A-Z0-9_-]+$`i', $var_to_replace ) === false ) { trigger_error( esc_html__( 'A replacement variable can only contain alphanumeric characters, an underscore or a dash. Try renaming your variable.', 'wordpress-seo' ), E_USER_WARNING ); } elseif ( strpos( $var_to_replace, 'cf_' ) === 0 || strpos( $var_to_replace, 'ct_' ) === 0 ) { trigger_error( esc_html__( 'A replacement variable can not start with "%%cf_" or "%%ct_" as these are reserved for the WPSEO standard variable variables for custom fields and custom taxonomies. Try making your variable name unique.', 'wordpress-seo' ), E_USER_WARNING ); } elseif ( ! method_exists( self::class, 'retrieve_' . $var_to_replace ) ) { if ( $var_to_replace !== '' && ! isset( self::$external_replacements[ $var_to_replace ] ) ) { self::$external_replacements[ $var_to_replace ] = $replace_function; $replacement_variable = new WPSEO_Replacement_Variable( $var_to_replace, $var_to_replace, $help_text ); self::register_help_text( $type, $replacement_variable ); $success = true; } else { trigger_error( esc_html__( 'A replacement variable with the same name has already been registered. Try making your variable name unique.', 'wordpress-seo' ), E_USER_WARNING ); } } else { trigger_error( esc_html__( 'You cannot overrule a WPSEO standard variable replacement by registering a variable with the same name. Use the "wpseo_replacements" filter instead to adjust the replacement value.', 'wordpress-seo' ), E_USER_WARNING ); } } return $success; } /** * Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt/etc. * * @param string $text The string to replace the variables in. * @param array $args The object some of the replacement values might come from, * could be a post, taxonomy or term. * @param array $omit Variables that should not be replaced by this function. * * @return string */ public function replace( $text, $args, $omit = [] ) { $text = wp_strip_all_tags( $text ); // Let's see if we can bail super early. if ( strpos( $text, '%%' ) === false ) { return YoastSEO()->helpers->string->standardize_whitespace( $text ); } $args = (array) $args; if ( isset( $args['post_content'] ) && ! empty( $args['post_content'] ) ) { $args['post_content'] = YoastSEO()->helpers->string->strip_shortcode( $args['post_content'] ); } if ( isset( $args['post_excerpt'] ) && ! empty( $args['post_excerpt'] ) ) { $args['post_excerpt'] = YoastSEO()->helpers->string->strip_shortcode( $args['post_excerpt'] ); } $this->args = (object) wp_parse_args( $args, $this->defaults ); // Clean $omit array. if ( is_array( $omit ) && $omit !== [] ) { $omit = array_map( [ self::class, 'remove_var_delimiter' ], $omit ); } $replacements = []; if ( preg_match_all( '`%%([^%]+(%%single)?)%%?`iu', $text, $matches ) ) { $replacements = $this->set_up_replacements( $matches, $omit ); } /** * Filter: 'wpseo_replacements' - Allow customization of the replacements before they are applied. * * @param array $replacements The replacements. * @param array $args The object some of the replacement values might come from, * could be a post, taxonomy or term. */ $replacements = apply_filters( 'wpseo_replacements', $replacements, $this->args ); // Do the actual replacements. if ( is_array( $replacements ) && $replacements !== [] ) { $text = str_replace( array_keys( $replacements ), // Make sure to exclude replacement values that are arrays e.g. coming from a custom field serialized value. array_filter( array_values( $replacements ), 'is_scalar' ), $text ); } /** * Filter: 'wpseo_replacements_final' - Allow overruling of whether or not to remove placeholders * which didn't yield a replacement. * * @example <code>add_filter( 'wpseo_replacements_final', '__return_false' );</code> * * @param bool $final */ if ( apply_filters( 'wpseo_replacements_final', true ) === true && ( isset( $matches[1] ) && is_array( $matches[1] ) ) ) { // Remove non-replaced variables. $remove = array_diff( $matches[1], $omit ); // Make sure the $omit variables do not get removed. $remove = array_map( [ self::class, 'add_var_delimiter' ], $remove ); $text = str_replace( $remove, '', $text ); } // Undouble separators which have nothing between them, i.e. where a non-replaced variable was removed. if ( isset( $replacements['%%sep%%'] ) && ( is_string( $replacements['%%sep%%'] ) && $replacements['%%sep%%'] !== '' ) ) { $q_sep = preg_quote( $replacements['%%sep%%'], '`' ); $text = preg_replace( '`' . $q_sep . '(?:\s*' . $q_sep . ')*`u', $replacements['%%sep%%'], $text ); } // Remove superfluous whitespace. $text = YoastSEO()->helpers->string->standardize_whitespace( $text ); return $text; } /** * Register a new replacement variable if it has not been registered already. * * @param string $var_to_replace The name of the variable to replace, i.e. '%%var%%'. * Note: the surrounding %% are optional. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable. * Uses the same format as add_filter/add_action function parameter and * should *return* the replacement value. DON'T echo it. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'. * @param string $help_text Help text to be added to the help tab for this variable. * * @return bool `true` if the replace var has been registered, `false` if not. */ public function safe_register_replacement( $var_to_replace, $replace_function, $type = 'advanced', $help_text = '' ) { if ( ! $this->has_been_registered( $var_to_replace ) ) { return self::register_replacement( $var_to_replace, $replace_function, $type, $help_text ); } return false; } /** * Checks whether the given replacement variable has already been registered or not. * * @param string $replacement_variable The replacement variable to check, including the variable delimiter (e.g. `%%var%%`). * * @return bool `true` if the replacement variable has already been registered. */ public function has_been_registered( $replacement_variable ) { $replacement_variable = self::remove_var_delimiter( $replacement_variable ); return isset( self::$external_replacements[ $replacement_variable ] ); } /** * Returns the list of hidden replace vars. * * E.g. the replace vars that should work, but are not advertised. * * @return string[] The list of hidden replace vars. */ public function get_hidden_replace_vars() { return [ 'currentdate', 'currentyear', 'currentmonth', 'currentday', 'post_year', 'post_month', 'post_day', 'author_first_name', 'author_last_name', 'permalink', 'post_content', 'category_title', ]; } /** * Retrieve the replacements for the variables found. * * @param array $matches Variables found in the original string - regex result. * @param array $omit Variables that should not be replaced by this function. * * @return array Retrieved replacements - this might be a smaller array as some variables * may not yield a replacement in certain contexts. */ private function set_up_replacements( $matches, $omit ) { $replacements = []; // @todo Figure out a way to deal with external functions starting with cf_/ct_. foreach ( $matches[1] as $k => $var ) { // Don't set up replacements which should be omitted. if ( in_array( $var, $omit, true ) ) { continue; } // Deal with variable variable names first. if ( strpos( $var, 'cf_' ) === 0 ) { $replacement = $this->retrieve_cf_custom_field_name( $var ); } elseif ( strpos( $var, 'ct_desc_' ) === 0 ) { $replacement = $this->retrieve_ct_desc_custom_tax_name( $var ); } elseif ( strpos( $var, 'ct_' ) === 0 ) { $single = ( isset( $matches[2][ $k ] ) && $matches[2][ $k ] !== '' ); $replacement = $this->retrieve_ct_custom_tax_name( $var, $single ); } // Deal with non-variable variable names. elseif ( method_exists( $this, 'retrieve_' . $var ) ) { $method_name = 'retrieve_' . $var; $replacement = $this->$method_name(); } // Deal with externally defined variable names. elseif ( isset( self::$external_replacements[ $var ] ) && ! is_null( self::$external_replacements[ $var ] ) ) { $replacement = call_user_func( self::$external_replacements[ $var ], $var, $this->args ); } // Replacement retrievals can return null if no replacement can be determined, root those outs. if ( isset( $replacement ) ) { $var = self::add_var_delimiter( $var ); $replacements[ $var ] = $replacement; } unset( $replacement, $single, $method_name ); } return $replacements; } /* *********************** BASIC VARIABLES ************************** */ /** * Retrieve the post/cpt categories (comma separated) for use as replacement string. * * @return string|null */ private function retrieve_category() { $replacement = null; if ( ! empty( $this->args->ID ) ) { $cat = $this->get_terms( $this->args->ID, 'category' ); if ( $cat !== '' ) { return $cat; } } if ( isset( $this->args->cat_name ) && ! empty( $this->args->cat_name ) ) { $replacement = $this->args->cat_name; } return $replacement; } /** * Retrieve the category description for use as replacement string. * * @return string|null */ private function retrieve_category_description() { return $this->retrieve_term_description(); } /** * Retrieve the date of the post/page/cpt for use as replacement string. * * @return string|null */ private function retrieve_date() { $replacement = null; if ( $this->args->post_date !== '' ) { // Returns a string. $replacement = YoastSEO()->helpers->date->format_translated( $this->args->post_date, get_option( 'date_format' ) ); } elseif ( get_query_var( 'day' ) && get_query_var( 'day' ) !== '' ) { // Returns a string. $replacement = get_the_date(); } elseif ( single_month_title( ' ', false ) && single_month_title( ' ', false ) !== '' ) { // Returns a string. $replacement = single_month_title( ' ', false ); } elseif ( get_query_var( 'year' ) !== '' ) { // Returns an integer, let's cast to string. $replacement = (string) get_query_var( 'year' ); } return $replacement; } /** * Retrieve the post/page/cpt excerpt for use as replacement string. * The excerpt will be auto-generated if it does not exist. * * @return string|null */ private function retrieve_excerpt() { $replacement = null; $locale = get_locale(); // Japanese doesn't have a jp_JP variant in WP. $limit = ( $locale === 'ja' ) ? 80 : 156; // The check `post_password_required` is because excerpt must be hidden for a post with a password. if ( ! empty( $this->args->ID ) && ! post_password_required( $this->args->ID ) ) { if ( $this->args->post_excerpt !== '' ) { $replacement = wp_strip_all_tags( $this->args->post_excerpt ); } elseif ( $this->args->post_content !== '' ) { $content = strip_shortcodes( $this->args->post_content ); $content = wp_strip_all_tags( $content ); if ( mb_strlen( $content ) <= $limit ) { return $content; } $replacement = wp_html_excerpt( $content, $limit ); // Check if the description has space and trim the auto-generated string to a word boundary. if ( strrpos( $replacement, ' ' ) ) { $replacement = substr( $replacement, 0, strrpos( $replacement, ' ' ) ); } } } return $replacement; } /** * Retrieve the post/page/cpt excerpt for use as replacement string (without auto-generation). * * @return string|null */ private function retrieve_excerpt_only() { $replacement = null; // The check `post_password_required` is because excerpt must be hidden for a post with a password. if ( ! empty( $this->args->ID ) && $this->args->post_excerpt !== '' && ! post_password_required( $this->args->ID ) ) { $replacement = wp_strip_all_tags( $this->args->post_excerpt ); } return $replacement; } /** * Retrieve the title of the parent page of the current page/cpt for use as replacement string. * Only applicable for hierarchical post types. * * @todo Check: shouldn't this use $this->args as well ? * * @return string|null */ private function retrieve_parent_title() { $replacement = null; if ( ! empty( $this->args->ID ) ) { $parent_id = wp_get_post_parent_id( $this->args->ID ); if ( $parent_id ) { $replacement = get_the_title( $parent_id ); } } return $replacement; } /** * Retrieve the current search phrase for use as replacement string. * * @return string|null */ private function retrieve_searchphrase() { $replacement = null; $search = get_query_var( 's' ); if ( $search !== '' ) { $replacement = esc_html( $search ); } return $replacement; } /** * Retrieve the separator for use as replacement string. * * @return string Retrieves the title separator. */ private function retrieve_sep() { return YoastSEO()->helpers->options->get_title_separator(); } /** * Retrieve the site's tag line / description for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string|null */ private function retrieve_sitedesc() { static $replacement; if ( ! isset( $replacement ) ) { $description = wp_strip_all_tags( get_bloginfo( 'description' ) ); if ( $description !== '' ) { $replacement = $description; } } return $replacement; } /** * Retrieve the site's name for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string|null */ private function retrieve_sitename() { static $replacement; if ( ! isset( $replacement ) ) { $sitename = YoastSEO()->helpers->site->get_site_name(); if ( $sitename !== '' ) { $replacement = $sitename; } } return $replacement; } /** * Retrieve the current tag/tags for use as replacement string. * * @return string|null */ private function retrieve_tag() { $replacement = null; if ( ! empty( $this->args->ID ) ) { $tags = $this->get_terms( $this->args->ID, 'post_tag' ); if ( $tags !== '' ) { $replacement = $tags; } } return $replacement; } /** * Retrieve the tag description for use as replacement string. * * @return string|null */ private function retrieve_tag_description() { return $this->retrieve_term_description(); } /** * Retrieve the term description for use as replacement string. * * @return string|null */ private function retrieve_term_description() { $replacement = null; if ( ! empty( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) { $term_desc = get_term_field( 'description', $this->args->term_id, $this->args->taxonomy ); if ( $term_desc !== '' ) { $replacement = wp_strip_all_tags( $term_desc ); } } return $replacement; } /** * Retrieve the term name for use as replacement string. * * @return string|null */ private function retrieve_term_title() { $replacement = null; if ( ! empty( $this->args->taxonomy ) && ! empty( $this->args->name ) ) { $replacement = $this->args->name; } return $replacement; } /** * Retrieve the title of the post/page/cpt for use as replacement string. * * @return string|null */ private function retrieve_title() { $replacement = null; if ( is_string( $this->args->post_title ) && $this->args->post_title !== '' ) { $replacement = $this->args->post_title; } return $replacement; } /** * Retrieve primary category for use as replacement string. * * @return bool|int|null */ private function retrieve_primary_category() { $primary_category = null; if ( ! empty( $this->args->ID ) ) { $wpseo_primary_category = new WPSEO_Primary_Term( 'category', $this->args->ID ); $term_id = $wpseo_primary_category->get_primary_term(); $term = get_term( $term_id ); if ( ! is_wp_error( $term ) && ! empty( $term ) ) { $primary_category = $term->name; } } return $primary_category; } /** * Retrieve the string generated by get_the_archive_title(). * * @return string|null */ private function retrieve_archive_title() { return get_the_archive_title(); } /* *********************** ADVANCED VARIABLES ************************** */ /** * Determine the page numbering of the current post/page/cpt. * * @param string $request Either 'nr'|'max' - whether to return the page number or the max number of pages. * * @return int|null */ private function determine_pagenumbering( $request = 'nr' ) { global $wp_query, $post; $max_num_pages = null; $page_number = null; $max_num_pages = 1; if ( ! is_singular() ) { $page_number = get_query_var( 'paged' ); if ( $page_number === 0 || $page_number === '' ) { $page_number = 1; } if ( ! empty( $wp_query->max_num_pages ) ) { $max_num_pages = $wp_query->max_num_pages; } } else { $page_number = get_query_var( 'page' ); if ( $page_number === 0 || $page_number === '' ) { $page_number = 1; } if ( isset( $post->post_content ) ) { $max_num_pages = ( substr_count( $post->post_content, '<!--nextpage-->' ) + 1 ); } } $return = null; switch ( $request ) { case 'nr': $return = $page_number; break; case 'max': $return = $max_num_pages; break; } return $return; } /** * Determine the post type names for the current post/page/cpt. * * @param string $request Either 'single'|'plural' - whether to return the single or plural form. * * @return string|null */ private function determine_pt_names( $request = 'single' ) { global $wp_query; $pt_single = null; $pt_plural = null; $post_type = ''; if ( isset( $wp_query->query_vars['post_type'] ) && ( ( is_string( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] !== '' ) || ( is_array( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] !== [] ) ) ) { $post_type = $wp_query->query_vars['post_type']; } elseif ( isset( $this->args->post_type ) && ( is_string( $this->args->post_type ) && $this->args->post_type !== '' ) ) { $post_type = $this->args->post_type; } else { // Make it work in preview mode. $post = $wp_query->get_queried_object(); if ( $post instanceof WP_Post ) { $post_type = $post->post_type; } } if ( is_array( $post_type ) ) { $post_type = reset( $post_type ); } if ( $post_type !== '' ) { $pt = get_post_type_object( $post_type ); $pt_single = $pt->name; $pt_plural = $pt->name; if ( isset( $pt->labels->singular_name ) ) { $pt_single = $pt->labels->singular_name; } if ( isset( $pt->labels->name ) ) { $pt_plural = $pt->labels->name; } } $return = null; switch ( $request ) { case 'single': $return = $pt_single; break; case 'plural': $return = $pt_plural; break; } return $return; } /** * Retrieve the attachment caption for use as replacement string. * * @return string|null */ private function retrieve_caption() { return $this->retrieve_excerpt_only(); } /** * Retrieve a post/page/cpt's custom field value for use as replacement string. * * @param string $var_to_replace The complete variable to replace which includes the name of * the custom field which value is to be retrieved. * * @return string|null */ private function retrieve_cf_custom_field_name( $var_to_replace ) { $replacement = null; if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) { $field = substr( $var_to_replace, 3 ); if ( ! empty( $this->args->ID ) ) { // Post meta can be arrays and in this case we need to exclude them. $name = get_post_meta( $this->args->ID, $field, true ); if ( $name !== '' && ! is_array( $name ) ) { $replacement = $name; } } elseif ( ! empty( $this->args->term_id ) ) { $name = get_term_meta( $this->args->term_id, $field, true ); if ( $name !== '' ) { $replacement = $name; } } } return $replacement; } /** * Retrieve a post/page/cpt's custom taxonomies for use as replacement string. * * @param string $var_to_replace The complete variable to replace which includes the name of * the custom taxonomy which value(s) is to be retrieved. * @param bool $single Whether to retrieve only the first or all values for the taxonomy. * * @return string|null */ private function retrieve_ct_custom_tax_name( $var_to_replace, $single = false ) { $replacement = null; if ( ( is_string( $var_to_replace ) && $var_to_replace !== '' ) && ! empty( $this->args->ID ) ) { $tax = substr( $var_to_replace, 3 ); $name = $this->get_terms( $this->args->ID, $tax, $single ); if ( $name !== '' ) { $replacement = $name; } } return $replacement; } /** * Retrieve a post/page/cpt's custom taxonomies description for use as replacement string. * * @param string $var_to_replace The complete variable to replace which includes the name of * the custom taxonomy which description is to be retrieved. * * @return string|null */ private function retrieve_ct_desc_custom_tax_name( $var_to_replace ) { $replacement = null; if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) { $tax = substr( $var_to_replace, 8 ); if ( ! empty( $this->args->ID ) ) { $terms = get_the_terms( $this->args->ID, $tax ); if ( is_array( $terms ) && $terms !== [] ) { $term = current( $terms ); $term_desc = get_term_field( 'description', $term->term_id, $tax ); if ( $term_desc !== '' ) { $replacement = wp_strip_all_tags( $term_desc ); } } } } return $replacement; } /** * Retrieve the current date for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The formatted current date. */ private function retrieve_currentdate() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( get_option( 'date_format' ) ); } return $replacement; } /** * Retrieve the current day for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The current day. */ private function retrieve_currentday() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( 'j' ); } return $replacement; } /** * Retrieve the current month for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The current month. */ private function retrieve_currentmonth() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( 'F' ); } return $replacement; } /** * Retrieve the current time for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The formatted current time. */ private function retrieve_currenttime() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( get_option( 'time_format' ) ); } return $replacement; } /** * Retrieve the current year for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The current year. */ private function retrieve_currentyear() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( 'Y' ); } return $replacement; } /** * Retrieve the post/page/cpt's focus keyword for use as replacement string. * * @return string|null */ private function retrieve_focuskw() { // Retrieve focuskw from a Post. if ( ! empty( $this->args->ID ) ) { $focus_kw = WPSEO_Meta::get_value( 'focuskw', $this->args->ID ); if ( $focus_kw !== '' ) { return $focus_kw; } return null; } // Retrieve focuskw from a Term. if ( ! empty( $this->args->term_id ) ) { $focus_kw = WPSEO_Taxonomy_Meta::get_term_meta( $this->args->term_id, $this->args->taxonomy, 'focuskw' ); if ( $focus_kw !== '' ) { return $focus_kw; } } return null; } /** * Retrieve the post/page/cpt ID for use as replacement string. * * @return string|null */ private function retrieve_id() { $replacement = null; if ( ! empty( $this->args->ID ) ) { // The post/page/cpt ID is an integer, let's cast to string. $replacement = (string) $this->args->ID; } return $replacement; } /** * Retrieve the post/page/cpt modified time for use as replacement string. * * @return string|null */ private function retrieve_modified() { $replacement = null; if ( ! empty( $this->args->post_modified ) ) { $replacement = YoastSEO()->helpers->date->format_translated( $this->args->post_modified, get_option( 'date_format' ) ); } return $replacement; } /** * Retrieve the post/page/cpt author's "nice name" for use as replacement string. * * @return string|null */ private function retrieve_name() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $name = get_the_author_meta( 'display_name', $user_id ); if ( $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post/page/cpt author's users description for use as a replacement string. * * @return string|null */ private function retrieve_user_description() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $description = get_the_author_meta( 'description', $user_id ); if ( $description !== '' ) { $replacement = $description; } return $replacement; } /** * Retrieve the current page number with context (i.e. 'page 2 of 4') for use as replacement string. * * @return string */ private function retrieve_page() { $replacement = null; $max = $this->determine_pagenumbering( 'max' ); $nr = $this->determine_pagenumbering( 'nr' ); $sep = $this->retrieve_sep(); if ( $max > 1 && $nr > 1 ) { /* translators: 1: current page number, 2: total number of pages. */ $replacement = sprintf( $sep . ' ' . __( 'Page %1$d of %2$d', 'wordpress-seo' ), $nr, $max ); } return $replacement; } /** * Retrieve the current page number for use as replacement string. * * @return string|null */ private function retrieve_pagenumber() { $replacement = null; $nr = $this->determine_pagenumbering( 'nr' ); if ( isset( $nr ) && $nr > 0 ) { $replacement = (string) $nr; } return $replacement; } /** * Retrieve the current page total for use as replacement string. * * @return string|null */ private function retrieve_pagetotal() { $replacement = null; $max = $this->determine_pagenumbering( 'max' ); if ( isset( $max ) && $max > 0 ) { $replacement = (string) $max; } return $replacement; } /** * Retrieve the post type plural label for use as replacement string. * * @return string|null */ private function retrieve_pt_plural() { $replacement = null; $name = $this->determine_pt_names( 'plural' ); if ( isset( $name ) && $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post type single label for use as replacement string. * * @return string|null */ private function retrieve_pt_single() { $replacement = null; $name = $this->determine_pt_names( 'single' ); if ( isset( $name ) && $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the slug which caused the 404 for use as replacement string. * * @return string|null */ private function retrieve_term404() { $replacement = null; if ( $this->args->term404 !== '' ) { $replacement = sanitize_text_field( str_replace( '-', ' ', $this->args->term404 ) ); } else { $error_request = get_query_var( 'pagename' ); if ( $error_request !== '' ) { $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) ); } else { $error_request = get_query_var( 'name' ); if ( $error_request !== '' ) { $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) ); } } } return $replacement; } /** * Retrieve the post/page/cpt author's user id for use as replacement string. * * @return string */ private function retrieve_userid() { // The user ID is an integer, let's cast to string. $replacement = ! empty( $this->args->post_author ) ? (string) $this->args->post_author : (string) get_query_var( 'author' ); return $replacement; } /** * Retrieve the post/page/cpt's published year for use as replacement string. * * @return string|null */ private function retrieve_post_year() { if ( empty( $this->args->ID ) ) { return null; } return get_the_date( 'Y', $this->args->ID ); } /** * Retrieve the post/page/cpt's published month for use as replacement string. * * @return string|null */ private function retrieve_post_month() { if ( empty( $this->args->ID ) ) { return null; } return get_the_date( 'F', $this->args->ID ); } /** * Retrieve the post/page/cpt's published day for use as replacement string. * * @return string|null */ private function retrieve_post_day() { if ( empty( $this->args->ID ) ) { return null; } return get_the_date( 'd', $this->args->ID ); } /** * Retrieve the post/page/cpt author's first name for use as replacement string. * * @return string|null */ private function retrieve_author_first_name() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $name = get_the_author_meta( 'first_name', $user_id ); if ( $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post/page/cpt author's last name for use as replacement string. * * @return string|null */ private function retrieve_author_last_name() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $name = get_the_author_meta( 'last_name', $user_id ); if ( $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post/page/cpt permalink for use as replacement string. * * @return string|null */ private function retrieve_permalink() { if ( empty( $this->args->ID ) ) { return null; } return get_permalink( $this->args->ID ); } /** * Retrieve the post/page/cpt content for use as replacement string. * * @return string|null */ private function retrieve_post_content() { $replacement = null; // The check `post_password_required` is because content must be hidden for a post with a password. if ( ! empty( $this->args->ID ) && $this->args->post_content !== '' && ! post_password_required( $this->args->ID ) ) { $content = strip_shortcodes( $this->args->post_content ); $replacement = wp_strip_all_tags( $content ); } return $replacement; } /** * Retrieve the current or first category title. To be used for import data from AIOSEO. * The code derives from AIOSEO's way of dealing with that var, so we can ensure 100% seamless transition. * * @return string|null */ private function retrieve_category_title() { if ( empty( $this->args ) || empty( $this->args->ID ) ) { return null; } $post_id = $this->args->ID; $post = get_post( $post_id ); $taxonomies = get_object_taxonomies( $post, 'objects' ); foreach ( $taxonomies as $taxonomy_slug => $taxonomy ) { if ( ! $taxonomy->hierarchical ) { continue; } $post_terms = get_the_terms( $post_id, $taxonomy_slug ); if ( is_array( $post_terms ) && count( $post_terms ) > 0 ) { // AiOSEO takes the name of whatever the first hierarchical taxonomy is. $term = $post_terms[0]; if ( $term ) { return $term->name; } } } return null; } /* *********************** HELP TEXT RELATED ************************** */ /** * Set the help text for a user/plugin/theme defined extra variable. * * @param string $type Type of variable: 'basic' or 'advanced'. * @param WPSEO_Replacement_Variable $replacement_variable The replacement variable to register. * * @return void */ private static function register_help_text( $type, WPSEO_Replacement_Variable $replacement_variable ) { $identifier = $replacement_variable->get_variable(); if ( ( is_string( $type ) && in_array( $type, [ 'basic', 'advanced' ], true ) ) && ( $identifier !== '' && ! isset( self::$help_texts[ $type ][ $identifier ] ) ) ) { self::$help_texts[ $type ][ $identifier ] = $replacement_variable; } } /** * Generates a list of replacement variables based on the help texts. * * @return array List of replace vars. */ public function get_replacement_variables_with_labels() { self::setup_statics_once(); $custom_variables = []; foreach ( array_merge( WPSEO_Custom_Fields::get_custom_fields(), WPSEO_Custom_Taxonomies::get_custom_taxonomies() ) as $custom_variable ) { $custom_variables[ $custom_variable ] = new WPSEO_Replacement_Variable( $custom_variable, $this->get_label( $custom_variable ), '' ); } $replacement_variables = array_filter( array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] ), [ $this, 'is_not_prefixed' ], ARRAY_FILTER_USE_KEY ); $hidden = $this->get_hidden_replace_vars(); return array_values( array_map( static function ( WPSEO_Replacement_Variable $replacement_variable ) use ( $hidden ) { $name = $replacement_variable->get_variable(); return [ 'name' => $name, 'value' => '', 'label' => $replacement_variable->get_label(), 'hidden' => in_array( $name, $hidden, true ), ]; }, array_merge( $replacement_variables, $custom_variables ) ) ); } /** * Generates a list of replacement variables based on the help texts. * * @return array List of replace vars. */ public function get_replacement_variables_list() { self::setup_statics_once(); $replacement_variables = array_merge( $this->get_replacement_variables(), WPSEO_Custom_Fields::get_custom_fields(), WPSEO_Custom_Taxonomies::get_custom_taxonomies() ); return array_map( [ $this, 'format_replacement_variable' ], $replacement_variables ); } /** * Creates a merged associative array of both the basic and advanced help texts. * * @return array Array with the replacement variables. */ private function get_replacement_variables() { $help_texts = array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] ); return array_filter( array_keys( $help_texts ), [ $this, 'is_not_prefixed' ] ); } /** * Checks whether the replacement variable contains a `ct_` or `cf_` prefix, because they follow different logic. * * @param string $replacement_variable The replacement variable. * * @return bool True when the replacement variable is not prefixed. */ private function is_not_prefixed( $replacement_variable ) { $prefixes = [ 'cf_', 'ct_' ]; $prefix = $this->get_prefix( $replacement_variable ); return ! in_array( $prefix, $prefixes, true ); } /** * Strip the prefix from a replacement variable name. * * @param string $replacement_variable The replacement variable. * * @return string The replacement variable name without the prefix. */ private function strip_prefix( $replacement_variable ) { return substr( $replacement_variable, 3 ); } /** * Gets the prefix from a replacement variable name. * * @param string $replacement_variable The replacement variable. * * @return string The prefix of the replacement variable. */ private function get_prefix( $replacement_variable ) { return substr( $replacement_variable, 0, 3 ); } /** * Strips 'desc_' if present, and appends ' description' at the end. * * @param string $label The replacement variable. * * @return string The altered replacement variable name. */ private function handle_description( $label ) { if ( strpos( $label, 'desc_' ) === 0 ) { return substr( $label, 5 ) . ' description'; } return $label; } /** * Creates a label for prefixed replacement variables that matches the format in the editors. * * @param string $replacement_variable The replacement variable. * * @return string The replacement variable label. */ private function get_label( $replacement_variable ) { $prefix = $this->get_prefix( $replacement_variable ); if ( $prefix === 'cf_' ) { return $this->strip_prefix( $replacement_variable ) . ' (custom field)'; } if ( $prefix === 'ct_' ) { $label = $this->strip_prefix( $replacement_variable ); $label = $this->handle_description( $label ); return ucfirst( $label . ' (custom taxonomy)' ); } if ( $prefix === 'pt_' ) { if ( $replacement_variable === 'pt_single' ) { return 'Post type (singular)'; } return 'Post type (plural)'; } return ''; } /** * Formats the replacement variables. * * @param string $replacement_variable The replacement variable to format. * * @return array The formatted replacement variable. */ private function format_replacement_variable( $replacement_variable ) { return [ 'name' => $replacement_variable, 'value' => '', 'label' => $this->get_label( $replacement_variable ), ]; } /** * Set/translate the help texts for the WPSEO standard basic variables. * * @return void */ private static function set_basic_help_texts() { /* translators: %s: wp_title() function. */ $separator_description = __( 'The separator defined in your theme\'s %s tag.', 'wordpress-seo' ); $separator_description = sprintf( $separator_description, // '<code>wp_title()</code>' 'wp_title()' ); $replacement_variables = [ new WPSEO_Replacement_Variable( 'date', __( 'Date', 'wordpress-seo' ), __( 'Replaced with the date of the post/page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'title', __( 'Title', 'wordpress-seo' ), __( 'Replaced with the title of the post/page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'parent_title', __( 'Parent title', 'wordpress-seo' ), __( 'Replaced with the title of the parent page of the current page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'archive_title', __( 'Archive title', 'wordpress-seo' ), __( 'Replaced with the normal title for an archive generated by WordPress', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'sitename', __( 'Site title', 'wordpress-seo' ), __( 'The site\'s name', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'sitedesc', __( 'Tagline', 'wordpress-seo' ), __( 'The site\'s tagline', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'excerpt', __( 'Excerpt', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (or auto-generated if it does not exist)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'excerpt_only', __( 'Excerpt only', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (without auto-generation)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'tag', __( 'Tag', 'wordpress-seo' ), __( 'Replaced with the current tag/tags', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'category', __( 'Category', 'wordpress-seo' ), __( 'Replaced with the post categories (comma separated)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'primary_category', __( 'Primary category', 'wordpress-seo' ), __( 'Replaced with the primary category of the post/page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'category_description', __( 'Category description', 'wordpress-seo' ), __( 'Replaced with the category description', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'tag_description', __( 'Tag description', 'wordpress-seo' ), __( 'Replaced with the tag description', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term_description', __( 'Term description', 'wordpress-seo' ), __( 'Replaced with the term description', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term_title', __( 'Term title', 'wordpress-seo' ), __( 'Replaced with the term name', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'searchphrase', __( 'Search phrase', 'wordpress-seo' ), __( 'Replaced with the current search phrase', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term_hierarchy', __( 'Term hierarchy', 'wordpress-seo' ), __( 'Replaced with the term ancestors hierarchy', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'sep', __( 'Separator', 'wordpress-seo' ), $separator_description ), new WPSEO_Replacement_Variable( 'currentdate', __( 'Current date', 'wordpress-seo' ), __( 'Replaced with the current date', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'currentyear', __( 'Current year', 'wordpress-seo' ), __( 'Replaced with the current year', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'currentmonth', __( 'Current month', 'wordpress-seo' ), __( 'Replaced with the current month', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'currentday', __( 'Current day', 'wordpress-seo' ), __( 'Replaced with the current day', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_year', __( 'Post year', 'wordpress-seo' ), __( 'Replaced with the year the post was published', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_month', __( 'Post month', 'wordpress-seo' ), __( 'Replaced with the month the post was published', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_day', __( 'Post day', 'wordpress-seo' ), __( 'Replaced with the day the post was published', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'author_first_name', __( 'Author first name', 'wordpress-seo' ), __( 'Replaced with the first name of the author', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'author_last_name', __( 'Author last name', 'wordpress-seo' ), __( 'Replaced with the last name of the author', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'permalink', __( 'Permalink', 'wordpress-seo' ), __( 'Replaced with the permalink', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_content', __( 'Post Content', 'wordpress-seo' ), __( 'Replaced with the post content', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'category_title', __( 'Category Title', 'wordpress-seo' ), __( 'Current or first category title', 'wordpress-seo' ) ), ]; foreach ( $replacement_variables as $replacement_variable ) { self::register_help_text( 'basic', $replacement_variable ); } } /** * Set/translate the help texts for the WPSEO standard advanced variables. * * @return void */ private static function set_advanced_help_texts() { $replacement_variables = [ new WPSEO_Replacement_Variable( 'pt_single', __( 'Post type (singular)', 'wordpress-seo' ), __( 'Replaced with the content type single label', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'pt_plural', __( 'Post type (plural)', 'wordpress-seo' ), __( 'Replaced with the content type plural label', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'modified', __( 'Modified', 'wordpress-seo' ), __( 'Replaced with the post/page modified time', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'id', __( 'ID', 'wordpress-seo' ), __( 'Replaced with the post/page ID', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'name', __( 'Name', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'nicename\'', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'user_description', __( 'User description', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'Biographical Info\'', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'page', __( 'Page', 'wordpress-seo' ), __( 'Replaced with the current page number with context (i.e. page 2 of 4)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'pagetotal', __( 'Pagetotal', 'wordpress-seo' ), __( 'Replaced with the current page total', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'pagenumber', __( 'Pagenumber', 'wordpress-seo' ), __( 'Replaced with the current page number', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'caption', __( 'Caption', 'wordpress-seo' ), __( 'Attachment caption', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'focuskw', __( 'Focus keyword', 'wordpress-seo' ), __( 'Replaced with the posts focus keyphrase', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term404', __( 'Term404', 'wordpress-seo' ), __( 'Replaced with the slug which caused the 404', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'cf_<custom-field-name>', '<custom-field-name> ' . __( '(custom field)', 'wordpress-seo' ), __( 'Replaced with a posts custom field value', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'ct_<custom-tax-name>', '<custom-tax-name> ' . __( '(custom taxonomy)', 'wordpress-seo' ), __( 'Replaced with a posts custom taxonomies, comma separated.', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'ct_desc_<custom-tax-name>', '<custom-tax-name> ' . __( 'description (custom taxonomy)', 'wordpress-seo' ), __( 'Replaced with a custom taxonomies description', 'wordpress-seo' ) ), ]; foreach ( $replacement_variables as $replacement_variable ) { self::register_help_text( 'advanced', $replacement_variable ); } } /* *********************** GENERAL HELPER METHODS ************************** */ /** * Remove the '%%' delimiters from a variable string. * * @param string $text Variable string to be cleaned. * * @return string */ private static function remove_var_delimiter( $text ) { return trim( $text, '%' ); } /** * Add the '%%' delimiters to a variable string. * * @param string $text Variable string to be delimited. * * @return string */ private static function add_var_delimiter( $text ) { return '%%' . $text . '%%'; } /** * Retrieve a post's terms, comma delimited. * * @param int $id ID of the post to get the terms for. * @param string $taxonomy The taxonomy to get the terms for this post from. * @param bool $return_single If true, return the first term. * * @return string Either a single term or a comma delimited string of terms. */ public function get_terms( $id, $taxonomy, $return_single = false ) { $output = ''; // If we're on a specific tag, category or taxonomy page, use that. if ( ! empty( $this->args->term_id ) ) { $output = $this->args->name; } elseif ( ! empty( $id ) && ! empty( $taxonomy ) ) { $terms = get_the_terms( $id, $taxonomy ); if ( is_array( $terms ) && $terms !== [] ) { foreach ( $terms as $term ) { if ( $return_single ) { $output = $term->name; break; } else { $output .= $term->name . ', '; } } $output = rtrim( trim( $output ), ',' ); } } unset( $terms, $term ); /** * Allows filtering of the terms list used to replace %%category%%, %%tag%% * and %%ct_<custom-tax-name>%% variables. * * @param string $output Comma-delimited string containing the terms. * @param string $taxonomy The taxonomy of the terms. */ return apply_filters( 'wpseo_terms', $output, $taxonomy ); } /** * Gets a taxonomy term hierarchy including the term to get the parents for. * * @return string */ private function get_term_hierarchy() { if ( ! is_taxonomy_hierarchical( $this->args->taxonomy ) ) { return ''; } $separator = ' ' . $this->retrieve_sep() . ' '; $args = [ 'format' => 'name', 'separator' => $separator, 'link' => false, 'inclusive' => true, ]; return rtrim( get_term_parents_list( $this->args->term_id, $this->args->taxonomy, $args ), $separator ); } /** * Retrieves the term ancestors hierarchy. * * @return string|null The term ancestors hierarchy. */ private function retrieve_term_hierarchy() { $replacement = null; if ( ! empty( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) { $hierarchy = $this->get_term_hierarchy(); if ( $hierarchy !== '' ) { $replacement = esc_html( $hierarchy ); } } return $replacement; } } wpseo-functions-deprecated.php 0000644 00000000104 14720701066 0012514 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Deprecated */ interface-wpseo-wordpress-ajax-integration.php 0000644 00000000446 14720701066 0015647 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * An interface for registering AJAX integrations with WordPress. */ interface WPSEO_WordPress_AJAX_Integration { /** * Registers all AJAX hooks to WordPress. * * @return void */ public function register_ajax_hooks(); } wpseo-functions.php 0000644 00000022002 14720701066 0010417 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } if ( ! function_exists( 'yoast_breadcrumb' ) ) { /** * Template tag for breadcrumbs. * * @param string $before What to show before the breadcrumb. * @param string $after What to show after the breadcrumb. * @param bool $display Whether to display the breadcrumb (true) or return it (false). * * @return string */ function yoast_breadcrumb( $before = '', $after = '', $display = true ) { $breadcrumbs_enabled = current_theme_supports( 'yoast-seo-breadcrumbs' ); if ( ! $breadcrumbs_enabled ) { $breadcrumbs_enabled = WPSEO_Options::get( 'breadcrumbs-enable', false ); } if ( $breadcrumbs_enabled ) { return WPSEO_Breadcrumbs::breadcrumb( $before, $after, $display ); } } } if ( ! function_exists( 'yoast_get_primary_term_id' ) ) { /** * Get the primary term ID. * * @param string $taxonomy Optional. The taxonomy to get the primary term ID for. Defaults to category. * @param int|WP_Post|null $post Optional. Post to get the primary term ID for. * * @return bool|int */ function yoast_get_primary_term_id( $taxonomy = 'category', $post = null ) { $post = get_post( $post ); $primary_term = new WPSEO_Primary_Term( $taxonomy, $post->ID ); return $primary_term->get_primary_term(); } } if ( ! function_exists( 'yoast_get_primary_term' ) ) { /** * Get the primary term name. * * @param string $taxonomy Optional. The taxonomy to get the primary term for. Defaults to category. * @param int|WP_Post|null $post Optional. Post to get the primary term for. * * @return string Name of the primary term. */ function yoast_get_primary_term( $taxonomy = 'category', $post = null ) { $primary_term_id = yoast_get_primary_term_id( $taxonomy, $post ); $term = get_term( $primary_term_id ); if ( ! is_wp_error( $term ) && ! empty( $term ) ) { return $term->name; } return ''; } } /** * Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt. * * @param string $text The string to replace the variables in. * @param object $args The object some of the replacement values might come from, * could be a post, taxonomy or term. * @param array $omit Variables that should not be replaced by this function. * * @return string */ function wpseo_replace_vars( $text, $args, $omit = [] ) { $replacer = new WPSEO_Replace_Vars(); return $replacer->replace( $text, $args, $omit ); } /** * Register a new variable replacement. * * This function is for use by other plugins/themes to easily add their own additional variables to replace. * This function should be called from a function on the 'wpseo_register_extra_replacements' action hook. * The use of this function is preferred over the older 'wpseo_replacements' filter as a way to add new replacements. * The 'wpseo_replacements' filter should still be used to adjust standard WPSEO replacement values. * The function can not be used to replace standard WPSEO replacement value functions and will thrown a warning * if you accidently try. * To avoid conflicts with variables registered by WPSEO and other themes/plugins, try and make the * name of your variable unique. Variable names also can not start with "%%cf_" or "%%ct_" as these are reserved * for the standard WPSEO variable variables 'cf_<custom-field-name>', 'ct_<custom-tax-name>' and * 'ct_desc_<custom-tax-name>'. * The replacement function will be passed the undelimited name (i.e. stripped of the %%) of the variable * to replace in case you need it. * * Example code: * <code> * <?php * function retrieve_var1_replacement( $var1 ) { * return 'your replacement value'; * } * * function register_my_plugin_extra_replacements() { * wpseo_register_var_replacement( '%%myvar1%%', 'retrieve_var1_replacement', 'advanced', 'this is a help text for myvar1' ); * wpseo_register_var_replacement( 'myvar2', array( 'class', 'method_name' ), 'basic', 'this is a help text for myvar2' ); * } * add_action( 'wpseo_register_extra_replacements', 'register_my_plugin_extra_replacements' ); * ?> * </code> * * @since 1.5.4 * * @param string $replacevar_name The name of the variable to replace, i.e. '%%var%%'. * Note: the surrounding %% are optional, name can only contain [A-Za-z0-9_-]. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable. * Uses the same format as add_filter/add_action function parameter and * should *return* the replacement value. DON'T echo it. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'. * @param string $help_text Help text to be added to the help tab for this variable. * * @return bool Whether the replacement function was successfully registered. */ function wpseo_register_var_replacement( $replacevar_name, $replace_function, $type = 'advanced', $help_text = '' ) { return WPSEO_Replace_Vars::register_replacement( $replacevar_name, $replace_function, $type, $help_text ); } /** * WPML plugin support: Set titles for custom types / taxonomies as translatable. * * It adds new keys to a wpml-config.xml file for a custom post type title, metadesc, * title-ptarchive and metadesc-ptarchive fields translation. * Documentation: http://wpml.org/documentation/support/language-configuration-files/ * * @global $sitepress * * @param array $config WPML configuration data to filter. * * @return array */ function wpseo_wpml_config( $config ) { global $sitepress; if ( ( is_array( $config ) && isset( $config['wpml-config']['admin-texts']['key'] ) ) && ( is_array( $config['wpml-config']['admin-texts']['key'] ) && $config['wpml-config']['admin-texts']['key'] !== [] ) ) { $admin_texts = $config['wpml-config']['admin-texts']['key']; foreach ( $admin_texts as $k => $val ) { if ( $val['attr']['name'] === 'wpseo_titles' ) { $translate_cp = array_keys( $sitepress->get_translatable_documents() ); if ( is_array( $translate_cp ) && $translate_cp !== [] ) { foreach ( $translate_cp as $post_type ) { $admin_texts[ $k ]['key'][]['attr']['name'] = 'title-' . $post_type; $admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-' . $post_type; $admin_texts[ $k ]['key'][]['attr']['name'] = 'title-ptarchive-' . $post_type; $admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-ptarchive-' . $post_type; $translate_tax = $sitepress->get_translatable_taxonomies( false, $post_type ); if ( is_array( $translate_tax ) && $translate_tax !== [] ) { foreach ( $translate_tax as $taxonomy ) { $admin_texts[ $k ]['key'][]['attr']['name'] = 'title-tax-' . $taxonomy; $admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-tax-' . $taxonomy; } } } } break; } } $config['wpml-config']['admin-texts']['key'] = $admin_texts; } return $config; } add_filter( 'icl_wpml_config_array', 'wpseo_wpml_config' ); if ( ! function_exists( 'ctype_digit' ) ) { /** * Emulate PHP native ctype_digit() function for when the ctype extension would be disabled *sigh*. * Only emulates the behaviour for when the input is a string, does not handle integer input as ascii value. * * @param string $text String input to validate. * * @return bool */ function ctype_digit( $text ) { $return = false; if ( ( is_string( $text ) && $text !== '' ) && preg_match( '`^\d+$`', $text ) === 1 ) { $return = true; } return $return; } } /** * Makes sure the taxonomy meta is updated when a taxonomy term is split. * * @link https://make.wordpress.org/core/2015/02/16/taxonomy-term-splitting-in-4-2-a-developer-guide/ Article explaining the taxonomy term splitting in WP 4.2. * * @param string $old_term_id Old term id of the taxonomy term that was splitted. * @param string $new_term_id New term id of the taxonomy term that was splitted. * @param string $term_taxonomy_id Term taxonomy id for the taxonomy that was affected. * @param string $taxonomy The taxonomy that the taxonomy term was splitted for. * * @return void */ function wpseo_split_shared_term( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { $tax_meta = get_option( 'wpseo_taxonomy_meta', [] ); if ( ! empty( $tax_meta[ $taxonomy ][ $old_term_id ] ) ) { $tax_meta[ $taxonomy ][ $new_term_id ] = $tax_meta[ $taxonomy ][ $old_term_id ]; unset( $tax_meta[ $taxonomy ][ $old_term_id ] ); update_option( 'wpseo_taxonomy_meta', $tax_meta ); } } add_action( 'split_shared_term', 'wpseo_split_shared_term', 10, 4 ); /** * Get all WPSEO related capabilities. * * @since 8.3 * @return array */ function wpseo_get_capabilities() { if ( ! did_action( 'wpseo_register_capabilities' ) ) { do_action( 'wpseo_register_capabilities' ); } return WPSEO_Capability_Manager_Factory::get()->get_capabilities(); } index.php 0000644 00000000046 14720701066 0006367 0 ustar 00 <?php /** * Nothing to see here. */ class-upgrade.php 0000644 00000155517 14720701066 0010030 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internal */ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Helpers\Taxonomy_Helper; use Yoast\WP\SEO\Integrations\Cleanup_Integration; use Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher; /** * This code handles the option upgrades. */ class WPSEO_Upgrade { /** * The taxonomy helper. * * @var Taxonomy_Helper */ private $taxonomy_helper; /** * Class constructor. */ public function __construct() { $this->taxonomy_helper = YoastSEO()->helpers->taxonomy; $version = WPSEO_Options::get( 'version' ); WPSEO_Options::maybe_set_multisite_defaults( false ); $routines = [ '1.5.0' => 'upgrade_15', '2.0' => 'upgrade_20', '2.1' => 'upgrade_21', '2.2' => 'upgrade_22', '2.3' => 'upgrade_23', '3.0' => 'upgrade_30', '3.3' => 'upgrade_33', '3.6' => 'upgrade_36', '4.0' => 'upgrade_40', '4.4' => 'upgrade_44', '4.7' => 'upgrade_47', '4.9' => 'upgrade_49', '5.0' => 'upgrade_50', '5.5' => 'upgrade_55', '6.3' => 'upgrade_63', '7.0-RC0' => 'upgrade_70', '7.1-RC0' => 'upgrade_71', '7.3-RC0' => 'upgrade_73', '7.4-RC0' => 'upgrade_74', '7.5.3' => 'upgrade_753', '7.7-RC0' => 'upgrade_77', '7.7.2-RC0' => 'upgrade_772', '9.0-RC0' => 'upgrade_90', '10.0-RC0' => 'upgrade_100', '11.1-RC0' => 'upgrade_111', // Reset notifications because we removed the AMP Glue plugin notification. '12.1-RC0' => 'clean_all_notifications', '12.3-RC0' => 'upgrade_123', '12.4-RC0' => 'upgrade_124', '12.8-RC0' => 'upgrade_128', '13.2-RC0' => 'upgrade_132', '14.0.3-RC0' => 'upgrade_1403', '14.1-RC0' => 'upgrade_141', '14.2-RC0' => 'upgrade_142', '14.5-RC0' => 'upgrade_145', '14.9-RC0' => 'upgrade_149', '15.1-RC0' => 'upgrade_151', '15.3-RC0' => 'upgrade_153', '15.5-RC0' => 'upgrade_155', '15.7-RC0' => 'upgrade_157', '15.9.1-RC0' => 'upgrade_1591', '16.2-RC0' => 'upgrade_162', '16.5-RC0' => 'upgrade_165', '17.2-RC0' => 'upgrade_172', '17.7.1-RC0' => 'upgrade_1771', '17.9-RC0' => 'upgrade_179', '18.3-RC3' => 'upgrade_183', '18.6-RC0' => 'upgrade_186', '18.9-RC0' => 'upgrade_189', '19.1-RC0' => 'upgrade_191', '19.3-RC0' => 'upgrade_193', '19.6-RC0' => 'upgrade_196', '19.11-RC0' => 'upgrade_1911', '20.2-RC0' => 'upgrade_202', '20.5-RC0' => 'upgrade_205', '20.7-RC0' => 'upgrade_207', '20.8-RC0' => 'upgrade_208', '22.6-RC0' => 'upgrade_226', ]; array_walk( $routines, [ $this, 'run_upgrade_routine' ], $version ); if ( version_compare( $version, '12.5-RC0', '<' ) ) { /* * We have to run this by hook, because otherwise: * - the theme support check isn't available. * - the notification center notifications are not filled yet. */ add_action( 'init', [ $this, 'upgrade_125' ] ); } /** * Filter: 'wpseo_run_upgrade' - Runs the upgrade hook which are dependent on Yoast SEO. * * @param string $version The current version of Yoast SEO */ do_action( 'wpseo_run_upgrade', $version ); $this->finish_up( $version ); } /** * Runs the upgrade routine. * * @param string $routine The method to call. * @param string $version The new version. * @param string $current_version The current set version. * * @return void */ protected function run_upgrade_routine( $routine, $version, $current_version ) { if ( version_compare( $current_version, $version, '<' ) ) { $this->$routine( $current_version ); } } /** * Adds a new upgrade history entry. * * @param string $current_version The old version from which we are upgrading. * @param string $new_version The version we are upgrading to. * * @return void */ protected function add_upgrade_history( $current_version, $new_version ) { $upgrade_history = new WPSEO_Upgrade_History(); $upgrade_history->add( $current_version, $new_version, array_keys( WPSEO_Options::$options ) ); } /** * Runs the needed cleanup after an update, setting the DB version to latest version, flushing caches etc. * * @param string|null $previous_version The previous version. * * @return void */ protected function finish_up( $previous_version = null ) { if ( $previous_version ) { WPSEO_Options::set( 'previous_version', $previous_version, 'wpseo' ); } WPSEO_Options::set( 'version', WPSEO_VERSION, 'wpseo' ); // Just flush rewrites, always, to at least make them work after an upgrade. add_action( 'shutdown', 'flush_rewrite_rules' ); // Flush the sitemap cache. WPSEO_Sitemaps_Cache::clear(); // Make sure all our options always exist - issue #1245. WPSEO_Options::ensure_options_exist(); } /** * Run the Yoast SEO 1.5 upgrade routine. * * @param string $version Current plugin version. * * @return void */ private function upgrade_15( $version ) { // Clean up options and meta. WPSEO_Options::clean_up( null, $version ); WPSEO_Meta::clean_up(); } /** * Moves options that moved position in WPSEO 2.0. * * @return void */ private function upgrade_20() { /** * Clean up stray wpseo_ms options from the options table, option should only exist in the sitemeta table. * This could have been caused in many version of Yoast SEO, so deleting it for everything below 2.0. */ delete_option( 'wpseo_ms' ); $wpseo = $this->get_option_from_database( 'wpseo' ); $this->save_option_setting( $wpseo, 'pinterestverify' ); // Re-save option to trigger sanitization. $this->cleanup_option_data( 'wpseo' ); } /** * Detects if taxonomy terms were split and updates the corresponding taxonomy meta's accordingly. * * @return void */ private function upgrade_21() { $taxonomies = get_option( 'wpseo_taxonomy_meta', [] ); if ( ! empty( $taxonomies ) ) { foreach ( $taxonomies as $taxonomy => $tax_metas ) { foreach ( $tax_metas as $term_id => $tax_meta ) { if ( function_exists( 'wp_get_split_term' ) ) { $new_term_id = wp_get_split_term( $term_id, $taxonomy ); if ( $new_term_id !== false ) { $taxonomies[ $taxonomy ][ $new_term_id ] = $taxonomies[ $taxonomy ][ $term_id ]; unset( $taxonomies[ $taxonomy ][ $term_id ] ); } } } } update_option( 'wpseo_taxonomy_meta', $taxonomies ); } } /** * Performs upgrade functions to Yoast SEO 2.2. * * @return void */ private function upgrade_22() { // Unschedule our tracking. wp_clear_scheduled_hook( 'yoast_tracking' ); $this->cleanup_option_data( 'wpseo' ); } /** * Schedules upgrade function to Yoast SEO 2.3. * * @return void */ private function upgrade_23() { add_action( 'wp', [ $this, 'upgrade_23_query' ], 90 ); add_action( 'admin_head', [ $this, 'upgrade_23_query' ], 90 ); } /** * Performs upgrade query to Yoast SEO 2.3. * * @return void */ public function upgrade_23_query() { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: executed only during the upgrade routine. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Reason: executed only during the upgrade routine. $wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_sitemap-include&meta_value=never&order=ASC' ); if ( ! empty( $wp_query->posts ) ) { $options = get_option( 'wpseo_xml' ); $excluded_posts = []; if ( $options['excluded-posts'] !== '' ) { $excluded_posts = explode( ',', $options['excluded-posts'] ); } foreach ( $wp_query->posts as $post ) { if ( ! in_array( (string) $post->ID, $excluded_posts, true ) ) { $excluded_posts[] = $post->ID; } } // Updates the meta value. $options['excluded-posts'] = implode( ',', $excluded_posts ); // Update the option. update_option( 'wpseo_xml', $options ); } // Remove the meta fields. delete_post_meta_by_key( '_yoast_wpseo_sitemap-include' ); } /** * Performs upgrade functions to Yoast SEO 3.0. * * @return void */ private function upgrade_30() { // Remove the meta fields for sitemap prio. delete_post_meta_by_key( '_yoast_wpseo_sitemap-prio' ); } /** * Performs upgrade functions to Yoast SEO 3.3. * * @return void */ private function upgrade_33() { // Notification dismissals have been moved to User Meta instead of global option. delete_option( Yoast_Notification_Center::STORAGE_KEY ); } /** * Performs upgrade functions to Yoast SEO 3.6. * * @return void */ protected function upgrade_36() { global $wpdb; // Between 3.2 and 3.4 the sitemap options were saved with autoloading enabled. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i LIKE %s AND autoload IN ("on", "yes")', [ $wpdb->options, 'option_name', 'wpseo_sitemap_%' ] ) ); } /** * Removes the about notice when its still in the database. * * @return void */ private function upgrade_40() { $center = Yoast_Notification_Center::get(); $center->remove_notification_by_id( 'wpseo-dismiss-about' ); } /** * Moves the content-analysis-active and keyword-analysis-acive options from wpseo-titles to wpseo. * * @return void */ private function upgrade_44() { $wpseo_titles = $this->get_option_from_database( 'wpseo_titles' ); $this->save_option_setting( $wpseo_titles, 'content-analysis-active', 'content_analysis_active' ); $this->save_option_setting( $wpseo_titles, 'keyword-analysis-active', 'keyword_analysis_active' ); // Remove irrelevant content from the option. $this->cleanup_option_data( 'wpseo_titles' ); } /** * Renames the meta name for the cornerstone content. It was a public meta field and it has to be private. * * @return void */ private function upgrade_47() { global $wpdb; // The meta key has to be private, so prefix it. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->postmeta . ' SET meta_key = %s WHERE meta_key = "yst_is_cornerstone"', WPSEO_Cornerstone_Filter::META_NAME ) ); } /** * Removes the 'wpseo-dismiss-about' notice for every user that still has it. * * @return void */ protected function upgrade_49() { global $wpdb; /* * Using a filter to remove the notification for the current logged in user. The notification center is * initializing the notifications before the upgrade routine has been executedd and is saving the stored * notifications on shutdown. This causes the returning notification. By adding this filter the shutdown * routine on the notification center will remove the notification. */ add_filter( 'yoast_notifications_before_storage', [ $this, 'remove_about_notice' ] ); $meta_key = $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $usermetas = $wpdb->get_results( $wpdb->prepare( ' SELECT %i, %i FROM %i WHERE %i = %s AND %i LIKE %s ', [ 'user_id', 'meta_value', $wpdb->usermeta, 'meta_key', $meta_key, 'meta_value', '%wpseo-dismiss-about%' ] ), ARRAY_A ); if ( empty( $usermetas ) ) { return; } foreach ( $usermetas as $usermeta ) { $notifications = maybe_unserialize( $usermeta['meta_value'] ); foreach ( $notifications as $notification_key => $notification ) { if ( ! empty( $notification['options']['id'] ) && $notification['options']['id'] === 'wpseo-dismiss-about' ) { unset( $notifications[ $notification_key ] ); } } update_user_option( $usermeta['user_id'], Yoast_Notification_Center::STORAGE_KEY, array_values( $notifications ) ); } } /** * Removes the wpseo-dismiss-about notice from a list of notifications. * * @param Yoast_Notification[] $notifications The notifications to filter. * * @return Yoast_Notification[] The filtered list of notifications. Excluding the wpseo-dismiss-about notification. */ public function remove_about_notice( $notifications ) { foreach ( $notifications as $notification_key => $notification ) { if ( $notification->get_id() === 'wpseo-dismiss-about' ) { unset( $notifications[ $notification_key ] ); } } return $notifications; } /** * Adds the yoast_seo_links table to the database. * * @return void */ protected function upgrade_50() { global $wpdb; // Deletes the post meta value, which might created in the RC. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = '_yst_content_links_processed'", [ $wpdb->postmeta, 'meta_key' ] ) ); } /** * Register new capabilities and roles. * * @return void */ private function upgrade_55() { // Register roles. do_action( 'wpseo_register_roles' ); WPSEO_Role_Manager_Factory::get()->add(); // Register capabilities. do_action( 'wpseo_register_capabilities' ); WPSEO_Capability_Manager_Factory::get()->add(); } /** * Removes some no longer used options for noindexing subpages and for meta keywords and its associated templates. * * @return void */ private function upgrade_63() { $this->cleanup_option_data( 'wpseo_titles' ); } /** * Perform the 7.0 upgrade, moves settings around, deletes several options. * * @return void */ private function upgrade_70() { $wpseo_permalinks = $this->get_option_from_database( 'wpseo_permalinks' ); $wpseo_xml = $this->get_option_from_database( 'wpseo_xml' ); $wpseo_rss = $this->get_option_from_database( 'wpseo_rss' ); $wpseo = $this->get_option_from_database( 'wpseo' ); $wpseo_internallinks = $this->get_option_from_database( 'wpseo_internallinks' ); // Move some permalink settings, then delete the option. $this->save_option_setting( $wpseo_permalinks, 'redirectattachment', 'disable-attachment' ); $this->save_option_setting( $wpseo_permalinks, 'stripcategorybase' ); // Move one XML sitemap setting, then delete the option. $this->save_option_setting( $wpseo_xml, 'enablexmlsitemap', 'enable_xml_sitemap' ); // Move the RSS settings to the search appearance settings, then delete the RSS option. $this->save_option_setting( $wpseo_rss, 'rssbefore' ); $this->save_option_setting( $wpseo_rss, 'rssafter' ); $this->save_option_setting( $wpseo, 'company_logo' ); $this->save_option_setting( $wpseo, 'company_name' ); $this->save_option_setting( $wpseo, 'company_or_person' ); $this->save_option_setting( $wpseo, 'person_name' ); // Remove the website name and altername name as we no longer need them. $this->cleanup_option_data( 'wpseo' ); // All the breadcrumbs settings have moved to the search appearance settings. foreach ( array_keys( $wpseo_internallinks ) as $key ) { $this->save_option_setting( $wpseo_internallinks, $key ); } // Convert hidden metabox options to display metabox options. $title_options = get_option( 'wpseo_titles' ); foreach ( $title_options as $key => $value ) { if ( strpos( $key, 'hideeditbox-tax-' ) === 0 ) { $taxonomy = substr( $key, strlen( 'hideeditbox-tax-' ) ); WPSEO_Options::set( 'display-metabox-tax-' . $taxonomy, ! $value ); continue; } if ( strpos( $key, 'hideeditbox-' ) === 0 ) { $post_type = substr( $key, strlen( 'hideeditbox-' ) ); WPSEO_Options::set( 'display-metabox-pt-' . $post_type, ! $value ); continue; } } // Cleanup removed options. delete_option( 'wpseo_xml' ); delete_option( 'wpseo_permalinks' ); delete_option( 'wpseo_rss' ); delete_option( 'wpseo_internallinks' ); // Remove possibly present plugin conflict notice for plugin that was removed from the list of conflicting plugins. $yoast_plugin_conflict = WPSEO_Plugin_Conflict::get_instance(); $yoast_plugin_conflict->clear_error( 'header-footer/plugin.php' ); // Moves the user meta for excluding from the XML sitemap to a noindex. global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( "UPDATE $wpdb->usermeta SET meta_key = 'wpseo_noindex_author' WHERE meta_key = 'wpseo_excludeauthorsitemap'" ); } /** * Perform the 7.1 upgrade. * * @return void */ private function upgrade_71() { $this->cleanup_option_data( 'wpseo_social' ); // Move the breadcrumbs setting and invert it. $title_options = $this->get_option_from_database( 'wpseo_titles' ); if ( array_key_exists( 'breadcrumbs-blog-remove', $title_options ) ) { WPSEO_Options::set( 'breadcrumbs-display-blog-page', ! $title_options['breadcrumbs-blog-remove'] ); $this->cleanup_option_data( 'wpseo_titles' ); } } /** * Perform the 7.3 upgrade. * * @return void */ private function upgrade_73() { global $wpdb; // We've moved the cornerstone checkbox to our proper namespace. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_yoast_wpseo_is_cornerstone' WHERE meta_key = '_yst_is_cornerstone'" ); // Remove the previous Whip dismissed message, as this is a new one regarding PHP 5.2. delete_option( 'whip_dismiss_timestamp' ); } /** * Performs the 7.4 upgrade. * * @return void */ protected function upgrade_74() { $this->remove_sitemap_validators(); } /** * Performs the 7.5.3 upgrade. * * When upgrading purging media is potentially relevant. * * @return void */ private function upgrade_753() { // Only when attachments are not disabled. if ( WPSEO_Options::get( 'disable-attachment' ) === true ) { return; } // Only when attachments are not no-indexed. if ( WPSEO_Options::get( 'noindex-attachment' ) === true ) { return; } // Set purging relevancy. WPSEO_Options::set( 'is-media-purge-relevant', true ); } /** * Performs the 7.7 upgrade. * * @return void */ private function upgrade_77() { // Remove all OpenGraph content image cache. $this->delete_post_meta( '_yoast_wpseo_post_image_cache' ); } /** * Performs the 7.7.2 upgrade. * * @return void */ private function upgrade_772() { if ( YoastSEO()->helpers->woocommerce->is_active() ) { $this->migrate_woocommerce_archive_setting_to_shop_page(); } } /** * Performs the 9.0 upgrade. * * @return void */ protected function upgrade_90() { global $wpdb; // Invalidate all sitemap cache transients. WPSEO_Sitemaps_Cache_Validator::cleanup_database(); // Removes all scheduled tasks for hitting the sitemap index. wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i LIKE %s', [ $wpdb->options, 'option_name', 'wpseo_sitemap_%' ] ) ); } /** * Performs the 10.0 upgrade. * * @return void */ private function upgrade_100() { // Removes recalibration notifications. $this->clean_all_notifications(); // Removes recalibration options. WPSEO_Options::clean_up( 'wpseo' ); delete_option( 'wpseo_recalibration_beta_mailinglist_subscription' ); } /** * Performs the 11.1 upgrade. * * @return void */ private function upgrade_111() { // Set company_or_person to company when it's an invalid value. $company_or_person = WPSEO_Options::get( 'company_or_person', '' ); if ( ! in_array( $company_or_person, [ 'company', 'person' ], true ) ) { WPSEO_Options::set( 'company_or_person', 'company' ); } } /** * Performs the 12.3 upgrade. * * Removes the about notice when its still in the database. * * @return void */ private function upgrade_123() { $plugins = [ 'yoast-seo-premium', 'video-seo-for-wordpress-seo-by-yoast', 'yoast-news-seo', 'local-seo-for-yoast-seo', 'yoast-woocommerce-seo', 'yoast-acf-analysis', ]; $center = Yoast_Notification_Center::get(); foreach ( $plugins as $plugin ) { $center->remove_notification_by_id( 'wpseo-outdated-yoast-seo-plugin-' . $plugin ); } } /** * Performs the 12.4 upgrade. * * Removes the Google plus defaults from the database. * * @return void */ private function upgrade_124() { $this->cleanup_option_data( 'wpseo_social' ); } /** * Performs the 12.5 upgrade. * * @return void */ public function upgrade_125() { // Disables the force rewrite title when the theme supports it through WordPress. if ( WPSEO_Options::get( 'forcerewritetitle', false ) && current_theme_supports( 'title-tag' ) ) { WPSEO_Options::set( 'forcerewritetitle', false ); } global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i = %s', [ $wpdb->usermeta, 'meta_key', 'wp_yoast_promo_hide_premium_upsell_admin_block' ] ) ); // Removes the WordPress update notification, because it is no longer necessary when WordPress 5.3 is released. $center = Yoast_Notification_Center::get(); $center->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' ); } /** * Performs the 12.8 upgrade. * * @return void */ private function upgrade_128() { // Re-save wpseo to make sure bf_banner_2019_dismissed key is gone. $this->cleanup_option_data( 'wpseo' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-page_comments-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' ); } /** * Performs the 13.2 upgrade. * * @return void */ private function upgrade_132() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-tagline-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-permalink-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-onpageorg' ); // Transfers the onpage option value to the ryte option. $ryte_option = get_option( 'wpseo_ryte' ); $onpage_option = get_option( 'wpseo_onpage' ); if ( ! $ryte_option && $onpage_option ) { update_option( 'wpseo_ryte', $onpage_option ); delete_option( 'wpseo_onpage' ); } // Changes onpage_indexability to ryte_indexability. $wpseo_option = get_option( 'wpseo' ); if ( isset( $wpseo_option['onpage_indexability'] ) && ! isset( $wpseo_option['ryte_indexability'] ) ) { $wpseo_option['ryte_indexability'] = $wpseo_option['onpage_indexability']; unset( $wpseo_option['onpage_indexability'] ); update_option( 'wpseo', $wpseo_option ); } if ( wp_next_scheduled( 'wpseo_ryte_fetch' ) ) { wp_clear_scheduled_hook( 'wpseo_ryte_fetch' ); } /* * Re-register capabilities to add the new `view_site_health_checks` * capability to the SEO Manager role. */ do_action( 'wpseo_register_capabilities' ); WPSEO_Capability_Manager_Factory::get()->add(); } /** * Perform the 14.0.3 upgrade. * * @return void */ private function upgrade_1403() { WPSEO_Options::set( 'ignore_indexation_warning', false ); } /** * Performs the 14.1 upgrade. * * @return void */ private function upgrade_141() { /* * These notifications are retrieved from storage on the `init` hook with * priority 1. We need to remove them after they're retrieved. */ add_action( 'init', [ $this, 'remove_notifications_for_141' ] ); add_action( 'init', [ $this, 'clean_up_private_taxonomies_for_141' ] ); $this->reset_permalinks_of_attachments_for_141(); } /** * Performs the 14.2 upgrade. * * Removes the yoast-acf-analysis notice when it's still in the database. * * @return void */ private function upgrade_142() { add_action( 'init', [ $this, 'remove_acf_notification_for_142' ] ); } /** * Performs the 14.5 upgrade. * * @return void */ private function upgrade_145() { add_action( 'init', [ $this, 'set_indexation_completed_option_for_145' ] ); } /** * Performs the 14.9 upgrade. * * @return void */ private function upgrade_149() { $version = get_option( 'wpseo_license_server_version', 2 ); WPSEO_Options::set( 'license_server_version', $version ); delete_option( 'wpseo_license_server_version' ); } /** * Performs the 15.1 upgrade. * * @return void */ private function upgrade_151() { $this->set_home_url_for_151(); $this->move_indexables_indexation_reason_for_151(); add_action( 'init', [ $this, 'set_permalink_structure_option_for_151' ] ); add_action( 'init', [ $this, 'store_custom_taxonomy_slugs_for_151' ] ); } /** * Performs the 15.3 upgrade. * * @return void */ private function upgrade_153() { WPSEO_Options::set( 'category_base_url', get_option( 'category_base' ) ); WPSEO_Options::set( 'tag_base_url', get_option( 'tag_base' ) ); // Rename a couple of options. $indexation_started_value = WPSEO_Options::get( 'indexation_started' ); WPSEO_Options::set( 'indexing_started', $indexation_started_value ); $indexables_indexing_completed_value = WPSEO_Options::get( 'indexables_indexation_completed' ); WPSEO_Options::set( 'indexables_indexing_completed', $indexables_indexing_completed_value ); } /** * Performs the 15.5 upgrade. * * @return void */ private function upgrade_155() { // Unset the fbadminapp value in the wpseo_social option. $wpseo_social_option = get_option( 'wpseo_social' ); if ( isset( $wpseo_social_option['fbadminapp'] ) ) { unset( $wpseo_social_option['fbadminapp'] ); update_option( 'wpseo_social', $wpseo_social_option ); } } /** * Performs the 15.7 upgrade. * * @return void */ private function upgrade_157() { add_action( 'init', [ $this, 'remove_plugin_updated_notification_for_157' ] ); } /** * Performs the 15.9.1 upgrade routine. * * @return void */ private function upgrade_1591() { $enabled_auto_updates = get_option( 'auto_update_plugins' ); $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class ); $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', [], $enabled_auto_updates ); } /** * Performs the 16.2 upgrade routine. * * @return void */ private function upgrade_162() { $enabled_auto_updates = get_site_option( 'auto_update_plugins' ); $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class ); $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] ); } /** * Performs the 16.5 upgrade. * * @return void */ private function upgrade_165() { add_action( 'init', [ $this, 'copy_og_settings_from_social_to_titles' ], 99 ); // Run after the WPSEO_Options::enrich_defaults method which has priority 99. add_action( 'init', [ $this, 'reset_og_settings_to_default_values' ], 100 ); } /** * Performs the 17.2 upgrade. Cleans out any unnecessary indexables. See $cleanup_integration->get_cleanup_tasks() to see what will be cleaned out. * * @return void */ private function upgrade_172() { wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' ); wp_unschedule_hook( 'wpseo_cleanup_indexables' ); if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 17.7.1 upgrade routine. * * @return void */ private function upgrade_1771() { $enabled_auto_updates = get_site_option( 'auto_update_plugins' ); $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class ); $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] ); } /** * Performs the 17.9 upgrade routine. * * @return void */ private function upgrade_179() { WPSEO_Options::set( 'wincher_integration_active', true ); } /** * Performs the 18.3 upgrade routine. * * @return void */ private function upgrade_183() { $this->delete_post_meta( 'yoast-structured-data-blocks-images-cache' ); } /** * Performs the 18.6 upgrade routine. * * @return void */ private function upgrade_186() { if ( is_multisite() ) { WPSEO_Options::set( 'allow_wincher_integration_active', false ); } } /** * Performs the 18.9 upgrade routine. * * @return void */ private function upgrade_189() { // Make old users not get the Installation Success page after upgrading. WPSEO_Options::set( 'should_redirect_after_install_free', false ); // We're adding a hardcoded time here, so that in the future we can be able to identify whether the user did see the Installation Success page or not. // If they did, they wouldn't have this hardcoded value in that option, but rather (roughly) the timestamp of the moment they saw it. WPSEO_Options::set( 'activation_redirect_timestamp_free', 1652258756 ); // Transfer the Social URLs. $other = []; $other[] = WPSEO_Options::get( 'instagram_url' ); $other[] = WPSEO_Options::get( 'linkedin_url' ); $other[] = WPSEO_Options::get( 'myspace_url' ); $other[] = WPSEO_Options::get( 'pinterest_url' ); $other[] = WPSEO_Options::get( 'youtube_url' ); $other[] = WPSEO_Options::get( 'wikipedia_url' ); WPSEO_Options::set( 'other_social_urls', array_values( array_unique( array_filter( $other ) ) ) ); // Transfer the progress of the old Configuration Workout. $workout_data = WPSEO_Options::get( 'workouts_data' ); $old_conf_progress = ( $workout_data['configuration']['finishedSteps'] ?? [] ); if ( in_array( 'optimizeSeoData', $old_conf_progress, true ) && in_array( 'siteRepresentation', $old_conf_progress, true ) ) { // If completed ‘SEO optimization’ and ‘Site representation’ step, we assume the workout was completed. $configuration_finished_steps = [ 'siteRepresentation', 'socialProfiles', 'personalPreferences', ]; WPSEO_Options::set( 'configuration_finished_steps', $configuration_finished_steps ); } } /** * Performs the 19.1 upgrade routine. * * @return void */ private function upgrade_191() { if ( is_multisite() ) { WPSEO_Options::set( 'allow_remove_feed_post_comments', true ); } } /** * Performs the 19.3 upgrade routine. * * @return void */ private function upgrade_193() { if ( empty( get_option( 'wpseo_premium', [] ) ) ) { WPSEO_Options::set( 'enable_index_now', true ); WPSEO_Options::set( 'enable_link_suggestions', true ); } } /** * Performs the 19.6 upgrade routine. * * @return void */ private function upgrade_196() { WPSEO_Options::set( 'ryte_indexability', false ); WPSEO_Options::set( 'allow_ryte_indexability', false ); wp_clear_scheduled_hook( 'wpseo_ryte_fetch' ); } /** * Performs the 19.11 upgrade routine. * * @return void */ private function upgrade_1911() { add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_post_types' ] ); add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_taxonomies' ] ); $this->deduplicate_unindexed_indexable_rows(); $this->remove_indexable_rows_for_disabled_authors_archive(); if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 20.2 upgrade routine. * * @return void */ private function upgrade_202() { if ( WPSEO_Options::get( 'disable-attachment', true ) ) { $attachment_cleanup_helper = YoastSEO()->helpers->attachment_cleanup; $attachment_cleanup_helper->remove_attachment_indexables( true ); $attachment_cleanup_helper->clean_attachment_links_from_target_indexable_ids( true ); } $this->clean_unindexed_indexable_rows_with_no_object_id(); if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { // This schedules the cleanup routine cron again, since in combination of premium cleans up the prominent words table. We also want to cleanup possible orphaned hierarchies from the above cleanups. wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 20.5 upgrade routine. * * @return void */ private function upgrade_205() { if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 20.7 upgrade routine. * Removes the metadata related to the settings page introduction modal for all the users. * Also, schedules another cleanup scheduled action. * * @return void */ private function upgrade_207() { add_action( 'shutdown', [ $this, 'delete_user_introduction_meta' ] ); } /** * Performs the 20.8 upgrade routine. * Schedules another cleanup scheduled action. * * @return void */ private function upgrade_208() { if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 22.6 upgrade routine. * Schedules another cleanup scheduled action, but starting from the last cleanup action we just added (if there aren't any running cleanups already). * * @return void */ private function upgrade_226() { if ( get_option( Cleanup_Integration::CURRENT_TASK_OPTION ) === false ) { $cleanup_integration = YoastSEO()->classes->get( Cleanup_Integration::class ); $cleanup_integration->start_cron_job( 'clean_selected_empty_usermeta', DAY_IN_SECONDS ); } } /** * Sets the home_url option for the 15.1 upgrade routine. * * @return void */ protected function set_home_url_for_151() { $home_url = WPSEO_Options::get( 'home_url' ); if ( empty( $home_url ) ) { WPSEO_Options::set( 'home_url', get_home_url() ); } } /** * Moves the `indexables_indexation_reason` option to the * renamed `indexing_reason` option. * * @return void */ protected function move_indexables_indexation_reason_for_151() { $reason = WPSEO_Options::get( 'indexables_indexation_reason', '' ); WPSEO_Options::set( 'indexing_reason', $reason ); } /** * Checks if the indexable indexation is completed. * If so, sets the `indexables_indexation_completed` option to `true`, * else to `false`. * * @return void */ public function set_indexation_completed_option_for_145() { WPSEO_Options::set( 'indexables_indexation_completed', YoastSEO()->helpers->indexing->get_limited_filtered_unindexed_count( 1 ) === 0 ); } /** * Cleans up the private taxonomies from the indexables table for the upgrade routine to 14.1. * * @return void */ public function clean_up_private_taxonomies_for_141() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // Clean up indexables of private taxonomies. $private_taxonomies = get_taxonomies( [ 'public' => false ], 'names' ); if ( empty( $private_taxonomies ) ) { return; } $replacements = array_merge( [ Model::get_table_name( 'Indexable' ), 'object_type', 'object_sub_type' ], $private_taxonomies ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'term' AND %i IN (" . implode( ', ', array_fill( 0, count( $private_taxonomies ), '%s' ) ) . ')', $replacements ) ); $wpdb->show_errors = $show_errors; } /** * Resets the permalinks of attachments to `null` in the indexable table for the upgrade routine to 14.1. * * @return void */ private function reset_permalinks_of_attachments_for_141() { global $wpdb; // If migrations haven't been completed succesfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // Reset the permalinks of the attachments in the indexable table. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "UPDATE %i SET %i = NULL WHERE %i = 'post' AND %i = 'attachment'", [ Model::get_table_name( 'Indexable' ), 'permalink', 'object_type', 'object_sub_type' ] ) ); $wpdb->show_errors = $show_errors; } /** * Removes notifications from the Notification center for the 14.1 upgrade. * * @return void */ public function remove_notifications_for_141() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-recalculate' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-blog-public-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-links-table-not-accessible' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-post-type-archive-notification' ); } /** * Removes the wpseo-suggested-plugin-yoast-acf-analysis notification from the Notification center for the 14.2 upgrade. * * @return void */ public function remove_acf_notification_for_142() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-suggested-plugin-yoast-acf-analysis' ); } /** * Removes the wpseo-plugin-updated notification from the Notification center for the 15.7 upgrade. * * @return void */ public function remove_plugin_updated_notification_for_157() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-plugin-updated' ); } /** * Removes all notifications saved in the database under 'wp_yoast_notifications'. * * @return void */ private function clean_all_notifications() { global $wpdb; delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY, '', true ); } /** * Removes the post meta fields for a given meta key. * * @param string $meta_key The meta key. * * @return void */ private function delete_post_meta( $meta_key ) { global $wpdb; $deleted = $wpdb->delete( $wpdb->postmeta, [ 'meta_key' => $meta_key ], [ '%s' ] ); if ( $deleted ) { wp_cache_set( 'last_changed', microtime(), 'posts' ); } } /** * Removes all sitemap validators. * * This should be executed on every upgrade routine until we have removed the sitemap caching in the database. * * @return void */ private function remove_sitemap_validators() { global $wpdb; // Remove all sitemap validators. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i LIKE %s', [ $wpdb->options, 'option_name', 'wpseo_sitemap%validator%' ] ) ); } /** * Retrieves the option value directly from the database. * * @param string $option_name Option to retrieve. * * @return int|string|bool|float|array<string|int|bool|float> The content of the option if exists, otherwise an empty array. */ protected function get_option_from_database( $option_name ) { global $wpdb; // Load option directly from the database, to avoid filtering and sanitization. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $results = $wpdb->get_results( $wpdb->prepare( 'SELECT %i FROM %i WHERE %i = %s', [ 'option_value', $wpdb->options, 'option_name', $option_name ] ), ARRAY_A ); if ( ! empty( $results ) ) { return maybe_unserialize( $results[0]['option_value'] ); } return []; } /** * Cleans the option to make sure only relevant settings are there. * * @param string $option_name Option name save. * * @return void */ protected function cleanup_option_data( $option_name ) { $data = get_option( $option_name, [] ); if ( ! is_array( $data ) || $data === [] ) { return; } /* * Clean up the option by re-saving it. * * The option framework will remove any settings that are not configured * for this option, removing any migrated settings. */ update_option( $option_name, $data ); } /** * Saves an option setting to where it should be stored. * * @param int|string|bool|float|array<string|int|bool|float> $source_data The option containing the value to be migrated. * @param string $source_setting Name of the key in the "from" option. * @param string|null $target_setting Name of the key in the "to" option. * * @return void */ protected function save_option_setting( $source_data, $source_setting, $target_setting = null ) { if ( $target_setting === null ) { $target_setting = $source_setting; } if ( isset( $source_data[ $source_setting ] ) ) { WPSEO_Options::set( $target_setting, $source_data[ $source_setting ] ); } } /** * Migrates WooCommerce archive settings to the WooCommerce Shop page meta-data settings. * * If no Shop page is defined, nothing will be migrated. * * @return void */ private function migrate_woocommerce_archive_setting_to_shop_page() { $shop_page_id = wc_get_page_id( 'shop' ); if ( $shop_page_id === -1 ) { return; } $title = WPSEO_Meta::get_value( 'title', $shop_page_id ); if ( empty( $title ) ) { $option_title = WPSEO_Options::get( 'title-ptarchive-product' ); WPSEO_Meta::set_value( 'title', $option_title, $shop_page_id ); WPSEO_Options::set( 'title-ptarchive-product', '' ); } $meta_description = WPSEO_Meta::get_value( 'metadesc', $shop_page_id ); if ( empty( $meta_description ) ) { $option_metadesc = WPSEO_Options::get( 'metadesc-ptarchive-product' ); WPSEO_Meta::set_value( 'metadesc', $option_metadesc, $shop_page_id ); WPSEO_Options::set( 'metadesc-ptarchive-product', '' ); } $bc_title = WPSEO_Meta::get_value( 'bctitle', $shop_page_id ); if ( empty( $bc_title ) ) { $option_bctitle = WPSEO_Options::get( 'bctitle-ptarchive-product' ); WPSEO_Meta::set_value( 'bctitle', $option_bctitle, $shop_page_id ); WPSEO_Options::set( 'bctitle-ptarchive-product', '' ); } $noindex = WPSEO_Meta::get_value( 'meta-robots-noindex', $shop_page_id ); if ( $noindex === '0' ) { $option_noindex = WPSEO_Options::get( 'noindex-ptarchive-product' ); WPSEO_Meta::set_value( 'meta-robots-noindex', $option_noindex, $shop_page_id ); WPSEO_Options::set( 'noindex-ptarchive-product', false ); } } /** * Stores the initial `permalink_structure` option. * * @return void */ public function set_permalink_structure_option_for_151() { WPSEO_Options::set( 'permalink_structure', get_option( 'permalink_structure' ) ); } /** * Stores the initial slugs of custom taxonomies. * * @return void */ public function store_custom_taxonomy_slugs_for_151() { $taxonomies = $this->taxonomy_helper->get_custom_taxonomies(); $custom_taxonomies = []; foreach ( $taxonomies as $taxonomy ) { $slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy ); $custom_taxonomies[ $taxonomy ] = $slug; } WPSEO_Options::set( 'custom_taxonomy_slugs', $custom_taxonomies ); } /** * Copies the frontpage social settings to the titles options. * * @return void */ public function copy_og_settings_from_social_to_titles() { $wpseo_social = get_option( 'wpseo_social' ); $wpseo_titles = get_option( 'wpseo_titles' ); $copied_options = []; // Reset to the correct default value. $copied_options['open_graph_frontpage_title'] = '%%sitename%%'; $options = [ 'og_frontpage_title' => 'open_graph_frontpage_title', 'og_frontpage_desc' => 'open_graph_frontpage_desc', 'og_frontpage_image' => 'open_graph_frontpage_image', 'og_frontpage_image_id' => 'open_graph_frontpage_image_id', ]; foreach ( $options as $social_option => $titles_option ) { if ( ! empty( $wpseo_social[ $social_option ] ) ) { $copied_options[ $titles_option ] = $wpseo_social[ $social_option ]; } } $wpseo_titles = array_merge( $wpseo_titles, $copied_options ); update_option( 'wpseo_titles', $wpseo_titles ); } /** * Reset the social options with the correct default values. * * @return void */ public function reset_og_settings_to_default_values() { $wpseo_titles = get_option( 'wpseo_titles' ); $updated_options = []; $updated_options['social-title-author-wpseo'] = '%%name%%'; $updated_options['social-title-archive-wpseo'] = '%%date%%'; /* translators: %s expands to the name of a post type (plural). */ $post_type_archive_default = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' ); /* translators: %s expands to the variable used for term title. */ $term_archive_default = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' ); $post_type_objects = get_post_types( [ 'public' => true ], 'objects' ); if ( $post_type_objects ) { foreach ( $post_type_objects as $pt ) { // Post types. if ( isset( $wpseo_titles[ 'social-title-' . $pt->name ] ) ) { $updated_options[ 'social-title-' . $pt->name ] = '%%title%%'; } // Post type archives. if ( isset( $wpseo_titles[ 'social-title-ptarchive-' . $pt->name ] ) ) { $updated_options[ 'social-title-ptarchive-' . $pt->name ] = $post_type_archive_default; } } } $taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' ); if ( $taxonomy_objects ) { foreach ( $taxonomy_objects as $tax ) { if ( isset( $wpseo_titles[ 'social-title-tax-' . $tax->name ] ) ) { $updated_options[ 'social-title-tax-' . $tax->name ] = $term_archive_default; } } } $wpseo_titles = array_merge( $wpseo_titles, $updated_options ); update_option( 'wpseo_titles', $wpseo_titles ); } /** * Removes all indexables for posts that are not publicly viewable. * This method should be called after init, because post_types can still be registered. * * @return void */ public function remove_indexable_rows_for_non_public_post_types() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; $indexable_table = Model::get_table_name( 'Indexable' ); $included_post_types = YoastSEO()->helpers->post_type->get_indexable_post_types(); if ( empty( $included_post_types ) ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'post' AND %i IS NOT NULL", [ $indexable_table, 'object_type', 'object_sub_type' ] ) ); } else { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'post' AND %i IS NOT NULL AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_post_types ), '%s' ) ) . ' )', array_merge( [ $indexable_table, 'object_type', 'object_sub_type', 'object_sub_type' ], $included_post_types ) ) ); } $wpdb->show_errors = $show_errors; } /** * Removes all indexables for terms that are not publicly viewable. * This method should be called after init, because taxonomies can still be registered. * * @return void */ public function remove_indexable_rows_for_non_public_taxonomies() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; $indexable_table = Model::get_table_name( 'Indexable' ); $included_taxonomies = YoastSEO()->helpers->taxonomy->get_indexable_taxonomies(); if ( empty( $included_taxonomies ) ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'term' AND %i IS NOT NULL", [ $indexable_table, 'object_type', 'object_sub_type' ] ) ); } else { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'term' AND %i IS NOT NULL AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_taxonomies ), '%s' ) ) . ' )', array_merge( [ $indexable_table, 'object_type', 'object_sub_type', 'object_sub_type' ], $included_taxonomies ) ) ); } $wpdb->show_errors = $show_errors; } /** * De-duplicates indexables that have more than one "unindexed" rows for the same object. Keeps the newest indexable. * * @return void */ protected function deduplicate_unindexed_indexable_rows() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $duplicates = $wpdb->get_results( $wpdb->prepare( " SELECT MAX(id) as newest_id, object_id, object_type FROM %i WHERE post_status = 'unindexed' AND object_type IN ( 'term', 'post', 'user' ) GROUP BY object_id, object_type HAVING count(*) > 1", [ Model::get_table_name( 'Indexable' ) ] ), ARRAY_A ); if ( empty( $duplicates ) ) { $wpdb->show_errors = $show_errors; return; } // Users, terms and posts may share the same object_id. So delete them in separate, more performant, queries. $delete_queries = [ $this->get_indexable_deduplication_query_for_type( 'post', $duplicates, $wpdb ), $this->get_indexable_deduplication_query_for_type( 'term', $duplicates, $wpdb ), $this->get_indexable_deduplication_query_for_type( 'user', $duplicates, $wpdb ), ]; foreach ( $delete_queries as $delete_query ) { if ( ! empty( $delete_query ) ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. $wpdb->query( $delete_query ); // phpcs:enable } } $wpdb->show_errors = $show_errors; } /** * Cleans up "unindexed" indexable rows when appropriate, aka when there's no object ID even though it should. * * @return void */ protected function clean_unindexed_indexable_rows_with_no_object_id() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'unindexed' AND %i NOT IN ( 'home-page', 'date-archive', 'post-type-archive', 'system-page' ) AND %i IS NULL", [ Model::get_table_name( 'Indexable' ), 'post_status', 'object_type', 'object_id' ] ) ); $wpdb->show_errors = $show_errors; } /** * Removes all user indexable rows when the author archive is disabled. * * @return void */ protected function remove_indexable_rows_for_disabled_authors_archive() { global $wpdb; if ( ! YoastSEO()->helpers->author_archive->are_disabled() ) { return; } // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'user'", [ Model::get_table_name( 'Indexable' ), 'object_type' ] ) ); $wpdb->show_errors = $show_errors; } /** * Creates a query for de-duplicating indexables for a particular type. * * @param string $object_type The object type to deduplicate. * @param string|array<array<int,int,string>> $duplicates The result of the duplicate query. * @param wpdb $wpdb The wpdb object. * * @return string The query that removes all but one duplicate for each object of the object type. */ protected function get_indexable_deduplication_query_for_type( $object_type, $duplicates, $wpdb ) { $filtered_duplicates = array_filter( $duplicates, static function ( $duplicate ) use ( $object_type ) { return $duplicate['object_type'] === $object_type; } ); if ( empty( $filtered_duplicates ) ) { return ''; } $object_ids = wp_list_pluck( $filtered_duplicates, 'object_id' ); $newest_indexable_ids = wp_list_pluck( $filtered_duplicates, 'newest_id' ); $replacements = array_merge( [ Model::get_table_name( 'Indexable' ), 'object_id' ], array_values( $object_ids ), array_values( $newest_indexable_ids ) ); $replacements[] = $object_type; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. return $wpdb->prepare( 'DELETE FROM %i WHERE %i IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' ) AND id NOT IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' ) AND object_type = %s', $replacements ); } /** * Removes the settings' introduction modal data for users. * * @return void */ public function delete_user_introduction_meta() { delete_metadata( 'user', 0, '_yoast_settings_introduction', '', true ); } } class-wpseo-statistics.php 0000644 00000002643 14720701066 0011715 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class that generates interesting statistics about things. */ class WPSEO_Statistics { /** * Returns the post count for a certain SEO rank. * * @todo Merge/DRY this with the logic virtually the same in WPSEO_Metabox::column_sort_orderby(). * * @param WPSEO_Rank $rank The SEO rank to get the post count for. * * @return int */ public function get_post_count( $rank ) { if ( $rank->get_rank() === WPSEO_Rank::NO_FOCUS ) { $posts = [ 'meta_query' => [ 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'focuskw', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], ], ]; } elseif ( $rank->get_rank() === WPSEO_Rank::NO_INDEX ) { $posts = [ 'meta_key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'meta_value' => '1', 'compare' => '=', ]; } else { $posts = [ 'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'meta_value' => [ $rank->get_starting_score(), $rank->get_end_score() ], 'meta_compare' => 'BETWEEN', 'meta_type' => 'NUMERIC', ]; } $posts['fields'] = 'ids'; $posts['post_status'] = 'publish'; if ( current_user_can( 'edit_others_posts' ) === false ) { $posts['author'] = get_current_user_id(); } $posts = new WP_Query( $posts ); return (int) $posts->found_posts; } } class-wpseo-meta.php 0000644 00000104220 14720701066 0010443 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 1.5.0 */ use Yoast\WP\SEO\Config\Schema_Types; use Yoast\WP\SEO\Helpers\Schema\Article_Helper; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * This class implements defaults and value validation for all WPSEO Post Meta values. * * Some guidelines: * - To update a meta value, you can just use update_post_meta() with the full (prefixed) meta key * or the convenience method WPSEO_Meta::set_value() with the internal key. * All updates will be automatically validated. * Meta values will only be saved to the database if they are *not* the same as the default to * keep database load low. * - To retrieve a WPSEO meta value, you **must** use WPSEO_Meta::get_value() which will always return a * string value, either the saved value or the default. * This method can also retrieve a complete set of WPSEO meta values for one specific post, see * the method documentation for the parameters. * * {@internal Unfortunately there isn't a filter available to hook into before returning the results * for get_post_meta(), get_post_custom() and the likes. That would have been the * preferred solution.}} * * {@internal All WP native get_meta() results get cached internally, so no need to cache locally.}} * {@internal Use $key when the key is the WPSEO internal name (without prefix), $meta_key when it * includes the prefix.}} */ class WPSEO_Meta { /** * Prefix for all WPSEO meta values in the database. * * {@internal If at any point this would change, quite apart from an upgrade routine, * this also will need to be changed in the wpml-config.xml file.}} * * @var string */ public static $meta_prefix = '_yoast_wpseo_'; /** * Prefix for all WPSEO meta value form field names and ids. * * @var string */ public static $form_prefix = 'yoast_wpseo_'; /** * Allowed length of the meta description. * * @var int */ public static $meta_length = 156; /** * Reason the meta description is not the default length. * * @var string */ public static $meta_length_reason = ''; /** * Meta box field definitions for the meta box form. * * {@internal * - Titles, help texts, description text and option labels are added via a translate_meta_boxes() method * in the relevant child classes (WPSEO_Metabox and WPSEO_Social_admin) as they are only needed there. * - Beware: even though the meta keys are divided into subsets, they still have to be uniquely named!}} * * @var array * Array format: * (required) 'type' => (string) field type. i.e. text / textarea / checkbox / * radio / select / multiselect / upload etc. * (recommended) 'default_value' => (string|array) default value for the field. * IMPORTANT: * - if the field has options, the default has to be the * key of one of the options. * - if the field is a text field, the default **has** to be * an empty string as otherwise the user can't save * an empty value/delete the meta value. * - if the field is a checkbox, the only valid values * are 'on' or 'off'. * (semi-required) 'options' => (array) options for used with (multi-)select and radio * fields, required if that's the field type. * key = (string) value which will be saved to db. * value = (string) text label for the option. * (optional) 'autocomplete' => (bool) whether autocomplete is on for text fields, * defaults to true. * (optional) 'class' => (string) classname(s) to add to the actual <input> tag. * (optional) 'rows' => (int) number of rows for a textarea, defaults to 3. * (optional) 'serialized' => (bool) whether the value is expected to be serialized, * i.e. an array or object, defaults to false. * Currently only used by add-on plugins. */ public static $meta_fields = [ 'general' => [ 'focuskw' => [ 'type' => 'hidden', 'title' => '', ], 'title' => [ 'type' => 'hidden', 'default_value' => '', ], 'metadesc' => [ 'type' => 'hidden', 'default_value' => '', 'class' => 'metadesc', 'rows' => 2, ], 'linkdex' => [ 'type' => 'hidden', 'default_value' => '0', ], 'content_score' => [ 'type' => 'hidden', 'default_value' => '0', ], 'inclusive_language_score' => [ 'type' => 'hidden', 'default_value' => '0', ], 'is_cornerstone' => [ 'type' => 'hidden', 'default_value' => 'false', ], ], 'advanced' => [ 'meta-robots-noindex' => [ 'type' => 'hidden', 'default_value' => '0', // = post-type default. 'options' => [ '0' => '', // Post type default. '2' => '', // Index. '1' => '', // No-index. ], ], 'meta-robots-nofollow' => [ 'type' => 'hidden', 'default_value' => '0', // = follow. 'options' => [ '0' => '', // Follow. '1' => '', // No-follow. ], ], 'meta-robots-adv' => [ 'type' => 'hidden', 'default_value' => '', 'options' => [ 'noimageindex' => '', 'noarchive' => '', 'nosnippet' => '', ], ], 'bctitle' => [ 'type' => 'hidden', 'default_value' => '', ], 'canonical' => [ 'type' => 'hidden', 'default_value' => '', ], 'redirect' => [ 'type' => 'url', 'default_value' => '', ], ], 'social' => [], 'schema' => [ 'schema_page_type' => [ 'type' => 'hidden', 'options' => Schema_Types::PAGE_TYPES, ], 'schema_article_type' => [ 'type' => 'hidden', 'hide_on_pages' => true, 'options' => Schema_Types::ARTICLE_TYPES, ], ], /* Fields we should validate & save, but not show on any form. */ 'non_form' => [ 'linkdex' => [ 'type' => null, 'default_value' => '0', ], ], ]; /** * Helper property - reverse index of the definition array. * * Format: [full meta key including prefix] => array * ['subset'] => (string) primary index * ['key'] => (string) internal key * * @var array */ public static $fields_index = []; /** * Helper property - array containing only the defaults in the format: * [full meta key including prefix] => (string) default value * * @var array */ public static $defaults = []; /** * Helper property to define the social network meta field definitions - networks. * * @var array */ private static $social_networks = [ 'opengraph' => 'opengraph', 'twitter' => 'twitter', ]; /** * Helper property to define the social network meta field definitions - fields and their type. * * @var array */ private static $social_fields = [ 'title' => 'hidden', 'description' => 'hidden', 'image' => 'hidden', 'image-id' => 'hidden', ]; /** * Register our actions and filters. * * @return void */ public static function init() { foreach ( self::$social_networks as $option => $network ) { if ( WPSEO_Options::get( $option, false, [ 'wpseo_social' ] ) === true ) { foreach ( self::$social_fields as $box => $type ) { self::$meta_fields['social'][ $network . '-' . $box ] = [ 'type' => $type, 'default_value' => '', ]; } } } unset( $option, $network, $box, $type ); /** * Allow add-on plugins to register their meta fields for management by this class. * Calls to add_filter() must be made before plugins_loaded prio 14. */ $extra_fields = apply_filters( 'add_extra_wpseo_meta_fields', [] ); if ( is_array( $extra_fields ) ) { self::$meta_fields = self::array_merge_recursive_distinct( $extra_fields, self::$meta_fields ); } unset( $extra_fields ); foreach ( self::$meta_fields as $subset => $field_group ) { foreach ( $field_group as $key => $field_def ) { register_meta( 'post', self::$meta_prefix . $key, [ 'sanitize_callback' => [ self::class, 'sanitize_post_meta' ] ] ); // Set the $fields_index property for efficiency. self::$fields_index[ self::$meta_prefix . $key ] = [ 'subset' => $subset, 'key' => $key, ]; // Set the $defaults property for efficiency. if ( isset( $field_def['default_value'] ) ) { self::$defaults[ self::$meta_prefix . $key ] = $field_def['default_value']; } else { // Meta will always be a string, so let's make the meta meta default also a string. self::$defaults[ self::$meta_prefix . $key ] = ''; } } } unset( $subset, $field_group, $key, $field_def ); self::filter_schema_article_types(); add_filter( 'update_post_metadata', [ self::class, 'remove_meta_if_default' ], 10, 5 ); add_filter( 'add_post_metadata', [ self::class, 'dont_save_meta_if_default' ], 10, 4 ); } /** * Retrieve the meta box form field definitions for the given tab and post type. * * @param string $tab Tab for which to retrieve the field definitions. * @param string $post_type Post type of the current post. * * @return array Array containing the meta box field definitions. */ public static function get_meta_field_defs( $tab, $post_type = 'post' ) { if ( ! isset( self::$meta_fields[ $tab ] ) ) { return []; } $field_defs = self::$meta_fields[ $tab ]; switch ( $tab ) { case 'non-form': // Prevent non-form fields from being passed to forms. $field_defs = []; break; case 'advanced': global $post; if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) { return []; } $post_type = ''; if ( isset( $post->post_type ) ) { $post_type = $post->post_type; } elseif ( ! isset( $post->post_type ) && isset( $_GET['post_type'] ) ) { $post_type = sanitize_text_field( $_GET['post_type'] ); } if ( $post_type === '' ) { return []; } /* Don't show the breadcrumb title field if breadcrumbs aren't enabled. */ if ( WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ) ) { unset( $field_defs['bctitle'] ); } if ( empty( $post->ID ) || ( ! empty( $post->ID ) && self::get_value( 'redirect', $post->ID ) === '' ) ) { unset( $field_defs['redirect'] ); } break; case 'schema': if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) { return []; } $field_defs['schema_page_type']['default'] = WPSEO_Options::get( 'schema-page-type-' . $post_type ); $article_helper = new Article_Helper(); if ( $article_helper->is_article_post_type( $post_type ) ) { $default_schema_article_type = WPSEO_Options::get( 'schema-article-type-' . $post_type ); /** This filter is documented in inc/options/class-wpseo-option-titles.php */ $allowed_article_types = apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ); if ( ! array_key_exists( $default_schema_article_type, $allowed_article_types ) ) { $default_schema_article_type = WPSEO_Options::get_default( 'wpseo_titles', 'schema-article-type-' . $post_type ); } $field_defs['schema_article_type']['default'] = $default_schema_article_type; } else { unset( $field_defs['schema_article_type'] ); } break; } /** * Filter the WPSEO metabox form field definitions for a tab. * {tab} can be 'general', 'advanced' or 'social'. * * @param array $field_defs Metabox form field definitions. * @param string $post_type Post type of the post the metabox is for, defaults to 'post'. * * @return array */ return apply_filters( 'wpseo_metabox_entries_' . $tab, $field_defs, $post_type ); } /** * Validate the post meta values. * * @param mixed $meta_value The new value. * @param string $meta_key The full meta key (including prefix). * * @return string Validated meta value. */ public static function sanitize_post_meta( $meta_value, $meta_key ) { $field_def = self::$meta_fields[ self::$fields_index[ $meta_key ]['subset'] ][ self::$fields_index[ $meta_key ]['key'] ]; $clean = self::$defaults[ $meta_key ]; switch ( true ) { case ( $meta_key === self::$meta_prefix . 'linkdex' ): $int = WPSEO_Utils::validate_int( $meta_value ); if ( $int !== false && $int >= 0 ) { $clean = strval( $int ); // Convert to string to make sure default check works. } break; case ( $field_def['type'] === 'checkbox' ): // Only allow value if it's one of the predefined options. if ( in_array( $meta_value, [ 'on', 'off' ], true ) ) { $clean = $meta_value; } break; case ( $field_def['type'] === 'select' || $field_def['type'] === 'radio' ): // Only allow value if it's one of the predefined options. if ( isset( $field_def['options'][ $meta_value ] ) ) { $clean = $meta_value; } break; case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'meta-robots-adv' ): $clean = self::validate_meta_robots_adv( $meta_value ); break; case ( $field_def['type'] === 'url' || $meta_key === self::$meta_prefix . 'canonical' ): // Validate as url(-part). $url = WPSEO_Utils::sanitize_url( $meta_value ); if ( $url !== '' ) { $clean = $url; } break; case ( $field_def['type'] === 'upload' && in_array( $meta_key, [ self::$meta_prefix . 'opengraph-image', self::$meta_prefix . 'twitter-image' ], true ) ): // Validate as url. $url = WPSEO_Utils::sanitize_url( $meta_value, [ 'http', 'https', 'ftp', 'ftps' ] ); if ( $url !== '' ) { $clean = $url; } break; case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'is_cornerstone' ): $clean = $meta_value; /* * This used to be a checkbox, then became a hidden input. * To make sure the value remains consistent, we cast 'true' to '1'. */ if ( $meta_value === 'true' ) { $clean = '1'; } break; case ( $field_def['type'] === 'hidden' && isset( $field_def['options'] ) ): // Only allow value if it's one of the predefined options. if ( isset( $field_def['options'][ $meta_value ] ) ) { $clean = $meta_value; } break; case ( $field_def['type'] === 'textarea' ): if ( is_string( $meta_value ) ) { // Remove line breaks and tabs. // @todo [JRF => Yoast] Verify that line breaks and the likes aren't allowed/recommended in meta header fields. $meta_value = str_replace( [ "\n", "\r", "\t", ' ' ], ' ', $meta_value ); $clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) ); } break; case ( $field_def['type'] === 'multiselect' ): $clean = $meta_value; break; case ( $field_def['type'] === 'text' ): default: if ( is_string( $meta_value ) ) { $clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) ); } break; } $clean = apply_filters( 'wpseo_sanitize_post_meta_' . $meta_key, $clean, $meta_value, $field_def, $meta_key ); return $clean; } /** * Validate a meta-robots-adv meta value. * * @todo [JRF => Yoast] Verify that this logic for the prioritisation is correct. * * @param array|string $meta_value The value to validate. * * @return string Clean value. */ public static function validate_meta_robots_adv( $meta_value ) { $clean = self::$meta_fields['advanced']['meta-robots-adv']['default_value']; $options = self::$meta_fields['advanced']['meta-robots-adv']['options']; if ( is_string( $meta_value ) ) { $meta_value = explode( ',', $meta_value ); } if ( is_array( $meta_value ) && $meta_value !== [] ) { $meta_value = array_map( 'trim', $meta_value ); // Individual selected entries. $cleaning = []; foreach ( $meta_value as $value ) { if ( isset( $options[ $value ] ) ) { $cleaning[] = $value; } } if ( $cleaning !== [] ) { $clean = implode( ',', $cleaning ); } unset( $cleaning, $value ); } return $clean; } /** * Prevent saving of default values and remove potential old value from the database if replaced by a default. * * @param bool $check The current status to allow updating metadata for the given type. * @param int $object_id ID of the current object for which the meta is being updated. * @param string $meta_key The full meta key (including prefix). * @param string $meta_value New meta value. * @param string $prev_value The old meta value. * * @return bool|null True = stop saving, null = continue saving. */ public static function remove_meta_if_default( $check, $object_id, $meta_key, $meta_value, $prev_value = '' ) { /* If it's one of our meta fields, check against default. */ if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) { if ( $prev_value !== '' ) { delete_post_meta( $object_id, $meta_key, $prev_value ); } else { delete_post_meta( $object_id, $meta_key ); } return true; // Stop saving the value. } return $check; // Go on with the normal execution (update) in meta.php. } /** * Prevent adding of default values to the database. * * @param bool $check The current status to allow adding metadata for the given type. * @param int $object_id ID of the current object for which the meta is being added. * @param string $meta_key The full meta key (including prefix). * @param string $meta_value New meta value. * * @return bool|null True = stop saving, null = continue saving. */ public static function dont_save_meta_if_default( $check, $object_id, $meta_key, $meta_value ) { /* If it's one of our meta fields, check against default. */ if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) { return true; // Stop saving the value. } return $check; // Go on with the normal execution (add) in meta.php. } /** * Is the given meta value the same as the default value ? * * @param string $meta_key The full meta key (including prefix). * @param mixed $meta_value The value to check. * * @return bool */ public static function meta_value_is_default( $meta_key, $meta_value ) { return ( isset( self::$defaults[ $meta_key ] ) && $meta_value === self::$defaults[ $meta_key ] ); } /** * Get a custom post meta value. * * Returns the default value if the meta value has not been set. * * {@internal Unfortunately there isn't a filter available to hook into before returning * the results for get_post_meta(), get_post_custom() and the likes. That * would have been the preferred solution.}} * * @param string $key Internal key of the value to get (without prefix). * @param int $postid Post ID of the post to get the value for. * * @return string All 'normal' values returned from get_post_meta() are strings. * Objects and arrays are possible, but not used by this plugin * and therefore discarted (except when the special 'serialized' field def * value is set to true - only used by add-on plugins for now). * Will return the default value if no value was found. * Will return empty string if no default was found (not one of our keys) or * if the post does not exist. */ public static function get_value( $key, $postid = 0 ) { global $post; $postid = absint( $postid ); if ( $postid === 0 ) { if ( ( isset( $post ) && is_object( $post ) ) && ( isset( $post->post_status ) && $post->post_status !== 'auto-draft' ) ) { $postid = $post->ID; } else { return ''; } } $custom = get_post_custom( $postid ); // Array of strings or empty array. $table_key = self::$meta_prefix . $key; // Populate the field_def using the field_index lookup array. $field_def = []; if ( isset( self::$fields_index[ $table_key ] ) ) { $field_def = self::$meta_fields[ self::$fields_index[ $table_key ]['subset'] ][ self::$fields_index[ $table_key ]['key'] ]; } // Check if we have a custom post meta entry. if ( isset( $custom[ $table_key ][0] ) ) { $unserialized = maybe_unserialize( $custom[ $table_key ][0] ); // Check if it is already unserialized. if ( $custom[ $table_key ][0] === $unserialized ) { return $custom[ $table_key ][0]; } // Check whether we need to unserialize it. if ( isset( $field_def['serialized'] ) && $field_def['serialized'] === true ) { // Ok, serialize value expected/allowed. return $unserialized; } } // Meta was either not found or found, but object/array while not allowed to be. if ( isset( self::$defaults[ self::$meta_prefix . $key ] ) ) { // Update the default value to the current post type. switch ( $key ) { case 'schema_page_type': case 'schema_article_type': return ''; } return self::$defaults[ self::$meta_prefix . $key ]; } /* * Shouldn't ever happen, means not one of our keys as there will always be a default available * for all our keys. */ return ''; } /** * Update a meta value for a post. * * @param string $key The internal key of the meta value to change (without prefix). * @param mixed $meta_value The value to set the meta to. * @param int $post_id The ID of the post to change the meta for. * * @return bool Whether the value was changed. */ public static function set_value( $key, $meta_value, $post_id ) { /* * Slash the data, because `update_metadata` will unslash it and we have already unslashed it. * Related issue: https://github.com/Yoast/YoastSEO.js/issues/2158 */ $meta_value = wp_slash( $meta_value ); return update_post_meta( $post_id, self::$meta_prefix . $key, $meta_value ); } /** * Deletes a meta value for a post. * * @param string $key The internal key of the meta value to change (without prefix). * @param int $post_id The ID of the post to delete the meta for. * * @return bool Whether the delete was successful or not. */ public static function delete( $key, $post_id ) { return delete_post_meta( $post_id, self::$meta_prefix . $key ); } /** * Used for imports, this functions imports the value of $old_metakey into $new_metakey for those post * where no WPSEO meta data has been set. * Optionally deletes the $old_metakey values. * * @param string $old_metakey The old key of the meta value. * @param string $new_metakey The new key, usually the WPSEO meta key (including prefix). * @param bool $delete_old Whether to delete the old meta key/value-sets. * * @return void */ public static function replace_meta( $old_metakey, $new_metakey, $delete_old = false ) { global $wpdb; /* * Get only those rows where no wpseo meta values exist for the same post * (with the exception of linkdex as that will be set independently of whether the post has been edited). * * {@internal Query is pretty well optimized this way.}} */ $query = $wpdb->prepare( " SELECT `a`.* FROM {$wpdb->postmeta} AS a WHERE `a`.`meta_key` = %s AND NOT EXISTS ( SELECT DISTINCT `post_id` , count( `meta_id` ) AS count FROM {$wpdb->postmeta} AS b WHERE `a`.`post_id` = `b`.`post_id` AND `meta_key` LIKE %s AND `meta_key` <> %s GROUP BY `post_id` ) ;", $old_metakey, $wpdb->esc_like( self::$meta_prefix . '%' ), self::$meta_prefix . 'linkdex' ); $oldies = $wpdb->get_results( $query ); if ( is_array( $oldies ) && $oldies !== [] ) { foreach ( $oldies as $old ) { update_post_meta( $old->post_id, $new_metakey, $old->meta_value ); } } // Delete old keys. if ( $delete_old === true ) { delete_post_meta_by_key( $old_metakey ); } } /** * General clean-up of the saved meta values. * - Remove potentially lingering old meta keys; * - Remove all default and invalid values. * * @return void */ public static function clean_up() { global $wpdb; /* * Clean up '_yoast_wpseo_meta-robots'. * * Retrieve all '_yoast_wpseo_meta-robots' meta values and convert if no new values found. * * {@internal Query is pretty well optimized this way.}} * * @todo [JRF => Yoast] Find out all possible values which the old '_yoast_wpseo_meta-robots' could contain * to convert the data correctly. */ $query = $wpdb->prepare( " SELECT `a`.* FROM {$wpdb->postmeta} AS a WHERE `a`.`meta_key` = %s AND NOT EXISTS ( SELECT DISTINCT `post_id` , count( `meta_id` ) AS count FROM {$wpdb->postmeta} AS b WHERE `a`.`post_id` = `b`.`post_id` AND ( `meta_key` = %s OR `meta_key` = %s ) GROUP BY `post_id` ) ;", self::$meta_prefix . 'meta-robots', self::$meta_prefix . 'meta-robots-noindex', self::$meta_prefix . 'meta-robots-nofollow' ); $oldies = $wpdb->get_results( $query ); if ( is_array( $oldies ) && $oldies !== [] ) { foreach ( $oldies as $old ) { $old_values = explode( ',', $old->meta_value ); foreach ( $old_values as $value ) { if ( $value === 'noindex' ) { update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-noindex', 1 ); } elseif ( $value === 'nofollow' ) { update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-nofollow', 1 ); } } } } unset( $query, $oldies, $old, $old_values, $value ); // Delete old keys. delete_post_meta_by_key( self::$meta_prefix . 'meta-robots' ); /* * Remove all default values and (most) invalid option values. * Invalid option values for the multiselect (meta-robots-adv) field will be dealt with seperately. * * {@internal Some of the defaults have changed in v1.5, but as the defaults will * be removed and new defaults will now automatically be passed when no * data found, this update is automatic (as long as we remove the old * values which we do in the below routine).}} * * {@internal Unfortunately we can't use the normal delete_meta() with key/value combination * as '' (empty string) values will be ignored and would result in all metas * with that key being deleted, not just the empty fields. * Still, the below implementation is largely based on the delete_meta() function.}} */ $query = []; foreach ( self::$meta_fields as $subset => $field_group ) { foreach ( $field_group as $key => $field_def ) { if ( ! isset( $field_def['default_value'] ) ) { continue; } if ( isset( $field_def['options'] ) && is_array( $field_def['options'] ) && $field_def['options'] !== [] ) { $valid = $field_def['options']; // Remove the default value from the valid options. unset( $valid[ $field_def['default_value'] ] ); $valid = array_keys( $valid ); $query[] = $wpdb->prepare( "( meta_key = %s AND meta_value NOT IN ( '" . implode( "','", esc_sql( $valid ) ) . "' ) )", self::$meta_prefix . $key ); unset( $valid ); } elseif ( is_string( $field_def['default_value'] ) && $field_def['default_value'] !== '' ) { $query[] = $wpdb->prepare( '( meta_key = %s AND meta_value = %s )', self::$meta_prefix . $key, $field_def['default_value'] ); } else { $query[] = $wpdb->prepare( "( meta_key = %s AND meta_value = '' )", self::$meta_prefix . $key ); } } } unset( $subset, $field_group, $key, $field_def ); $query = "SELECT meta_id FROM {$wpdb->postmeta} WHERE " . implode( ' OR ', $query ) . ';'; $meta_ids = $wpdb->get_col( $query ); if ( is_array( $meta_ids ) && $meta_ids !== [] ) { // WP native action. do_action( 'delete_post_meta', $meta_ids, null, null, null ); $query = "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN( " . implode( ',', $meta_ids ) . ' )'; $count = $wpdb->query( $query ); if ( $count ) { foreach ( $meta_ids as $object_id ) { wp_cache_delete( $object_id, 'post_meta' ); } // WP native action. do_action( 'deleted_post_meta', $meta_ids, null, null, null ); } } unset( $query, $meta_ids, $count, $object_id ); /* * Deal with the multiselect (meta-robots-adv) field. * * Removes invalid option combinations, such as 'none,noarchive'. * * Default values have already been removed, so we should have a small result set and * (hopefully) even smaller set of invalid results. */ $query = $wpdb->prepare( "SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s", self::$meta_prefix . 'meta-robots-adv' ); $oldies = $wpdb->get_results( $query ); if ( is_array( $oldies ) && $oldies !== [] ) { foreach ( $oldies as $old ) { $clean = self::validate_meta_robots_adv( $old->meta_value ); if ( $clean !== $old->meta_value ) { if ( $clean !== self::$meta_fields['advanced']['meta-robots-adv']['default_value'] ) { update_metadata_by_mid( 'post', $old->meta_id, $clean ); } else { delete_metadata_by_mid( 'post', $old->meta_id ); } } } } unset( $query, $oldies, $old, $clean ); do_action( 'wpseo_meta_clean_up' ); } /** * Recursively merge a variable number of arrays, using the left array as base, * giving priority to the right array. * * Difference with native array_merge_recursive(): * array_merge_recursive converts values with duplicate keys to arrays rather than * overwriting the value in the first array with the duplicate value in the second array. * * array_merge_recursive_distinct does not change the data types of the values in the arrays. * Matching keys' values in the second array overwrite those in the first array, as is the * case with array_merge. * * Freely based on information found on http://www.php.net/manual/en/function.array-merge-recursive.php * * {@internal Should be moved to a general utility class.}} * * @return array */ public static function array_merge_recursive_distinct() { $arrays = func_get_args(); if ( count( $arrays ) < 2 ) { if ( $arrays === [] ) { return []; } else { return $arrays[0]; } } $merged = array_shift( $arrays ); foreach ( $arrays as $array ) { foreach ( $array as $key => $value ) { if ( is_array( $value ) && ( isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) ) { $merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value ); } else { $merged[ $key ] = $value; } } unset( $key, $value ); } return $merged; } /** * Counts the total of all the keywords being used for posts except the given one. * * @param string $keyword The keyword to be counted. * @param int $post_id The id of the post to which the keyword belongs. * * @return array */ public static function keyword_usage( $keyword, $post_id ) { if ( empty( $keyword ) ) { return []; } /** * The indexable repository. * * @var Indexable_Repository */ $repository = YoastSEO()->classes->get( Indexable_Repository::class ); $post_ids = $repository->query() ->select( 'object_id' ) ->where( 'primary_focus_keyword', $keyword ) ->where( 'object_type', 'post' ) ->where_not_equal( 'object_id', $post_id ) ->where_not_equal( 'post_status', 'trash' ) ->limit( 2 ) // Limit to 2 results to save time and resources. ->find_array(); // Get object_id from each subarray in $post_ids. $post_ids = ( is_array( $post_ids ) ) ? array_column( $post_ids, 'object_id' ) : []; /* * If Premium is installed, get the additional keywords as well. * We only check for the additional keywords if we've not already found two. * In that case there's no use for an additional query as we already know * that the keyword has been used multiple times before. */ if ( count( $post_ids ) < 2 ) { /** * Allows enhancing the array of posts' that share their focus keywords with the post's focus keywords. * * @param array $post_ids The array of posts' ids that share their related keywords with the post. * @param string $keyword The keyword to search for. * @param int $post_id The id of the post the keyword is associated to. */ $post_ids = apply_filters( 'wpseo_posts_for_focus_keyword', $post_ids, $keyword, $post_id ); } return $post_ids; } /** * Returns the post types for the given post ids. * * @param array $post_ids The post ids to get the post types for. * * @return array The post types. */ public static function post_types_for_ids( $post_ids ) { /** * The indexable repository. * * @var Indexable_Repository */ $repository = YoastSEO()->classes->get( Indexable_Repository::class ); // Check if post ids is not empty. if ( ! empty( $post_ids ) ) { // Get the post subtypes for the posts that share the keyword. $post_types = $repository->query() ->select( 'object_sub_type' ) ->where_in( 'object_id', $post_ids ) ->find_array(); // Get object_sub_type from each subarray in $post_ids. $post_types = array_column( $post_types, 'object_sub_type' ); } else { $post_types = []; } return $post_types; } /** * Filter the schema article types. * * @return void */ public static function filter_schema_article_types() { /** This filter is documented in inc/options/class-wpseo-option-titles.php */ self::$meta_fields['schema']['schema_article_type']['options'] = apply_filters( 'wpseo_schema_article_types', self::$meta_fields['schema']['schema_article_type']['options'] ); } } class-wpseo-custom-taxonomies.php 0000644 00000003134 14720701066 0013215 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Custom_Taxonomies. */ class WPSEO_Custom_Taxonomies { /** * Custom taxonomies cache. * * @var array */ protected static $custom_taxonomies = null; /** * Gets the names of the custom taxonomies, prepends 'ct_' and 'ct_desc', and returns them in an array. * * @return array The custom taxonomy prefixed names. */ public static function get_custom_taxonomies() { // Use cached value if available. if ( ! is_null( self::$custom_taxonomies ) ) { return self::$custom_taxonomies; } self::$custom_taxonomies = []; $args = [ 'public' => true, '_builtin' => false, ]; $custom_taxonomies = get_taxonomies( $args, 'names', 'and' ); if ( is_array( $custom_taxonomies ) ) { foreach ( $custom_taxonomies as $custom_taxonomy ) { array_push( self::$custom_taxonomies, self::add_custom_taxonomies_prefix( $custom_taxonomy ), self::add_custom_taxonomies_description_prefix( $custom_taxonomy ) ); } } return self::$custom_taxonomies; } /** * Adds the ct_ prefix to a taxonomy. * * @param string $taxonomy The taxonomy to prefix. * * @return string The prefixed taxonomy. */ private static function add_custom_taxonomies_prefix( $taxonomy ) { return 'ct_' . $taxonomy; } /** * Adds the ct_desc_ prefix to a taxonomy. * * @param string $taxonomy The taxonomy to prefix. * * @return string The prefixed taxonomy. */ private static function add_custom_taxonomies_description_prefix( $taxonomy ) { return 'ct_desc_' . $taxonomy; } } class-wpseo-content-images.php 0000644 00000005174 14720701066 0012442 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Content_Images. */ class WPSEO_Content_Images { /** * Retrieves images from the post content. * * @param int $post_id The post ID. * @param WP_Post|null $post The post object. * * @return array An array of images found in this post. */ public function get_images( $post_id, $post = null ) { return $this->get_images_from_content( $this->get_post_content( $post_id, $post ) ); } /** * Grabs the images from the content. * * @param string $content The post content string. * * @return array An array of image URLs. */ public function get_images_from_content( $content ) { if ( ! is_string( $content ) ) { return []; } $content_images = $this->get_img_tags_from_content( $content ); $images = array_map( [ $this, 'get_img_tag_source' ], $content_images ); $images = array_filter( $images ); $images = array_unique( $images ); $images = array_values( $images ); // Reset the array keys. return $images; } /** * Gets the image tags from a given content string. * * @param string $content The content to search for image tags. * * @return array An array of `<img>` tags. */ private function get_img_tags_from_content( $content ) { if ( strpos( $content, '<img' ) === false ) { return []; } preg_match_all( '`<img [^>]+>`', $content, $matches ); if ( isset( $matches[0] ) ) { return $matches[0]; } return []; } /** * Retrieves the image URL from an image tag. * * @param string $image Image HTML element. * * @return string|bool The image URL on success, false on failure. */ private function get_img_tag_source( $image ) { preg_match( '`src=(["\'])(.*?)\1`', $image, $matches ); if ( isset( $matches[2] ) && filter_var( $matches[2], FILTER_VALIDATE_URL ) ) { return $matches[2]; } return false; } /** * Retrieves the post content we want to work with. * * @param int $post_id The post ID. * @param WP_Post|array|null $post The post. * * @return string The content of the supplied post. */ private function get_post_content( $post_id, $post ) { if ( $post === null ) { $post = get_post( $post_id ); } if ( $post === null ) { return ''; } /** * Filter: 'wpseo_pre_analysis_post_content' - Allow filtering the content before analysis. * * @param string $post_content The Post content string. * @param WP_Post $post The current post. */ $content = apply_filters( 'wpseo_pre_analysis_post_content', $post->post_content, $post ); if ( ! is_string( $content ) ) { $content = ''; } return $content; } } class-addon-manager.php 0000644 00000064105 14720701066 0011066 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Inc */ use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; /** * Represents the addon manager. */ class WPSEO_Addon_Manager { /** * Holds the name of the transient. * * @var string */ public const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information'; /** * Holds the name of the transient. * * @var string */ public const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick'; /** * Holds the slug for YoastSEO free. * * @var string */ public const FREE_SLUG = 'yoast-seo-wordpress'; /** * Holds the slug for YoastSEO Premium. * * @var string */ public const PREMIUM_SLUG = 'yoast-seo-wordpress-premium'; /** * Holds the slug for Yoast News. * * @var string */ public const NEWS_SLUG = 'yoast-seo-news'; /** * Holds the slug for Video. * * @var string */ public const VIDEO_SLUG = 'yoast-seo-video'; /** * Holds the slug for WooCommerce. * * @var string */ public const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce'; /** * Holds the slug for Local. * * @var string */ public const LOCAL_SLUG = 'yoast-seo-local'; /** * The expected addon data. * * @var array */ protected static $addons = [ 'wp-seo-premium.php' => self::PREMIUM_SLUG, 'wpseo-news.php' => self::NEWS_SLUG, 'video-seo.php' => self::VIDEO_SLUG, 'wpseo-woocommerce.php' => self::WOOCOMMERCE_SLUG, 'local-seo.php' => self::LOCAL_SLUG, ]; /** * The addon data for the shortlinks. * * @var array */ private $addon_details = [ self::PREMIUM_SLUG => [ 'name' => 'Yoast SEO Premium', 'short_link_activation' => 'https://yoa.st/13j', 'short_link_renewal' => 'https://yoa.st/4ey', ], self::NEWS_SLUG => [ 'name' => 'Yoast News SEO', 'short_link_activation' => 'https://yoa.st/4xq', 'short_link_renewal' => 'https://yoa.st/4xv', ], self::WOOCOMMERCE_SLUG => [ 'name' => 'Yoast WooCommerce SEO', 'short_link_activation' => 'https://yoa.st/4xs', 'short_link_renewal' => 'https://yoa.st/4xx', ], self::VIDEO_SLUG => [ 'name' => 'Yoast Video SEO', 'short_link_activation' => 'https://yoa.st/4xr', 'short_link_renewal' => 'https://yoa.st/4xw', ], self::LOCAL_SLUG => [ 'name' => 'Yoast Local SEO', 'short_link_activation' => 'https://yoa.st/4xp', 'short_link_renewal' => 'https://yoa.st/4xu', ], ]; /** * Holds the site information data. * * @var stdClass */ private $site_information; /** * Hooks into WordPress. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this, 'validate_addons' ], 15 ); add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] ); add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 ); add_action( 'plugins_loaded', [ $this, 'register_expired_messages' ], 10 ); } /** * Registers "expired subscription" warnings to the update messages of our addons. * * @return void */ public function register_expired_messages() { foreach ( array_keys( $this->get_installed_addons() ) as $plugin_file ) { add_action( 'in_plugin_update_message-' . $plugin_file, [ $this, 'expired_subscription_warning' ], 10, 2 ); } } /** * Gets the subscriptions for current site. * * @return stdClass The subscriptions. */ public function get_subscriptions() { return $this->get_site_information()->subscriptions; } /** * Provides a list of addon filenames. * * @return string[] List of addon filenames with their slugs. */ public function get_addon_filenames() { return self::$addons; } /** * Finds the plugin file. * * @param string $plugin_slug The plugin slug to search. * * @return bool|string Plugin file when installed, False when plugin isn't installed. */ public function get_plugin_file( $plugin_slug ) { $plugins = $this->get_plugins(); $plugin_files = array_keys( $plugins ); $target_plugin_file = array_search( $plugin_slug, $this->get_addon_filenames(), true ); if ( ! $target_plugin_file ) { return false; } foreach ( $plugin_files as $plugin_file ) { if ( strpos( $plugin_file, $target_plugin_file ) !== false ) { return $plugin_file; } } return false; } /** * Retrieves the subscription for the given slug. * * @param string $slug The plugin slug to retrieve. * * @return stdClass|false Subscription data when found, false when not found. */ public function get_subscription( $slug ) { foreach ( $this->get_subscriptions() as $subscription ) { if ( $subscription->product->slug === $slug ) { return $subscription; } } return false; } /** * Retrieves a list of (subscription) slugs by the active addons. * * @return array The slugs. */ public function get_subscriptions_for_active_addons() { $active_addons = array_keys( $this->get_active_addons() ); $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons ); $subscriptions = []; foreach ( $subscription_slugs as $subscription_slug ) { $subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug ); } return $subscriptions; } /** * Retrieves a list of versions for each addon. * * @return array The addon versions. */ public function get_installed_addons_versions() { $addon_versions = []; foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) { $addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version']; } return $addon_versions; } /** * Retrieves the plugin information from the subscriptions. * * @param stdClass|false $data The result object. Default false. * @param string $action The type of information being requested from the Plugin Installation API. * @param stdClass $args Plugin API arguments. * * @return object Extended plugin data. */ public function get_plugin_information( $data, $action, $args ) { if ( $action !== 'plugin_information' ) { return $data; } if ( ! isset( $args->slug ) ) { return $data; } $subscription = $this->get_subscription( $args->slug ); if ( ! $subscription ) { return $data; } $data = $this->convert_subscription_to_plugin( $subscription, null, true ); if ( $this->has_subscription_expired( $subscription ) ) { unset( $data->package, $data->download_link ); } return $data; } /** * Retrieves information from MyYoast about which addons are connected to the current site. * * @return stdClass The list of addons activated for this site. */ public function get_myyoast_site_information() { if ( $this->site_information === null ) { $this->site_information = $this->get_site_information_transient(); } if ( $this->site_information ) { return $this->site_information; } $this->site_information = $this->request_current_sites(); if ( $this->site_information ) { $this->site_information = $this->map_site_information( $this->site_information ); $this->set_site_information_transient( $this->site_information ); return $this->site_information; } return $this->get_site_information_default(); } /** * Checks if the subscription for the given slug is valid. * * @param string $slug The plugin slug to retrieve. * * @return bool True when the subscription is valid. */ public function has_valid_subscription( $slug ) { $subscription = $this->get_subscription( $slug ); // An non-existing subscription is never valid. if ( ! $subscription ) { return false; } return ! $this->has_subscription_expired( $subscription ); } /** * Checks if there are addon updates. * * @param stdClass|mixed $data The current data for update_plugins. * * @return stdClass Extended data for update_plugins. */ public function check_for_updates( $data ) { global $wp_version; if ( empty( $data ) ) { return $data; } // We have to figure out if we're safe to upgrade the add-ons, based on what the latest Yoast Free requirements for the WP version is. $yoast_free_data = $this->extract_yoast_data( $data ); foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) { $subscription_slug = $this->get_slug_by_plugin_file( $plugin_file ); $subscription = $this->get_subscription( $subscription_slug ); if ( ! $subscription ) { continue; } $plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file ); // Let's assume for now that it will get added in the 'no_update' key that we'll return to the WP API. $is_no_update = true; // If the add-on's version is the latest, we have to do no further checks. if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) { // If we haven't retrieved the Yoast Free requirements for the WP version yet, do nothing. The next run will probably get us that information. if ( is_null( $plugin_data->requires ) ) { continue; } if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) { // The add-on has an available update *and* the Yoast Free requirements for the WP version are also met, so go ahead and show the upgrade info to the user. $is_no_update = false; $data->response[ $plugin_file ] = $plugin_data; if ( $this->has_subscription_expired( $subscription ) ) { unset( $data->response[ $plugin_file ]->package, $data->response[ $plugin_file ]->download_link ); } } } if ( $is_no_update ) { // Still convert subscription when no updates is available. $data->no_update[ $plugin_file ] = $plugin_data; if ( $this->has_subscription_expired( $subscription ) ) { unset( $data->no_update[ $plugin_file ]->package, $data->no_update[ $plugin_file ]->download_link ); } } } return $data; } /** * Extracts Yoast SEO Free's data from the wp.org API response. * * @param object $data The wp.org API response. * * @return object Yoast Free's data from wp.org. */ protected function extract_yoast_data( $data ) { if ( isset( $data->response[ WPSEO_BASENAME ] ) ) { return $data->response[ WPSEO_BASENAME ]; } if ( isset( $data->no_update[ WPSEO_BASENAME ] ) ) { return $data->no_update[ WPSEO_BASENAME ]; } return (object) []; } /** * If the plugin is lacking an active subscription, throw a warning. * * @param array $plugin_data The data for the plugin in this row. * * @return void */ public function expired_subscription_warning( $plugin_data ) { $subscription = $this->get_subscription( $plugin_data['slug'] ); if ( $subscription && $this->has_subscription_expired( $subscription ) ) { $addon_link = ( isset( $this->addon_details[ $plugin_data['slug'] ] ) ) ? $this->addon_details[ $plugin_data['slug'] ]['short_link_renewal'] : $this->addon_details[ self::PREMIUM_SLUG ]['short_link_renewal']; $sale_copy = ''; if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2023-promotion' ) ) { $sale_copy = sprintf( /* translators: %1$s is a <br> tag. */ esc_html__( '%1$s Now with 30%% Black Friday Discount!', 'wordpress-seo' ), '<br>' ); } echo '<br><br>'; echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> ' . sprintf( /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */ esc_html__( '%1$s can\'t be updated because your product subscription is expired. %2$sRenew your product subscription%3$s to get updates again and use all the features of %1$s.', 'wordpress-seo' ), esc_html( $plugin_data['name'] ), '<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">', '</a>' ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above. . $sale_copy . '</strong>'; } } /** * Checks if there are any installed addons. * * @return bool True when there are installed Yoast addons. */ public function has_installed_addons() { $installed_addons = $this->get_installed_addons(); return ! empty( $installed_addons ); } /** * Checks if the plugin is installed and activated in WordPress. * * @param string $slug The class' slug. * * @return bool True when installed and activated. */ public function is_installed( $slug ) { $slug_to_class_map = [ static::PREMIUM_SLUG => 'WPSEO_Premium', static::NEWS_SLUG => 'WPSEO_News', static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO', static::VIDEO_SLUG => 'WPSEO_Video_Sitemap', static::LOCAL_SLUG => 'WPSEO_Local_Core', ]; if ( ! isset( $slug_to_class_map[ $slug ] ) ) { return false; } return class_exists( $slug_to_class_map[ $slug ] ); } /** * Validates the addons and show a notice for the ones that are invalid. * * @return void */ public function validate_addons() { $notification_center = Yoast_Notification_Center::get(); if ( $notification_center === null ) { return; } foreach ( $this->addon_details as $slug => $addon_info ) { $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] ); // Add a notification when the installed plugin isn't activated in My Yoast. if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) { $notification_center->add_notification( $notification ); continue; } $notification_center->remove_notification( $notification ); } } /** * Removes the site information transients. * * @codeCoverageIgnore * * @return void */ public function remove_site_information_transients() { delete_transient( self::SITE_INFORMATION_TRANSIENT ); delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK ); } /** * Creates an instance of Yoast_Notification. * * @param string $product_name The product to create the notification for. * @param string $short_link The short link for the addon notification. * * @return Yoast_Notification The created notification. */ protected function create_notification( $product_name, $short_link ) { $notification_options = [ 'type' => Yoast_Notification::ERROR, 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ), 'capabilities' => 'wpseo_manage_options', ]; return new Yoast_Notification( sprintf( /* translators: %1$s expands to a strong tag, %2$s expands to the product name, %3$s expands to a closing strong tag, %4$s expands to an a tag. %5$s expands to MyYoast, %6$s expands to a closing a tag, %7$s expands to the product name */ __( '%1$s %2$s isn\'t working as expected %3$s and you are not receiving updates or support! Make sure to %4$s activate your product subscription in %5$s%6$s to unlock all the features of %7$s.', 'wordpress-seo' ), '<strong>', $product_name, '</strong>', '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">', 'MyYoast', '</a>', $product_name ), $notification_options ); } /** * Checks whether a plugin expiry date has been passed. * * @param stdClass $subscription Plugin subscription. * * @return bool Has the plugin expired. */ protected function has_subscription_expired( $subscription ) { return ( strtotime( $subscription->expiry_date ) - time() ) < 0; } /** * Converts a subscription to plugin based format. * * @param stdClass $subscription The subscription to convert. * @param stdClass|null $yoast_free_data The Yoast Free's data. * @param bool $plugin_info Whether we're in the plugin information modal. * @param string $plugin_file The plugin filename. * * @return stdClass The converted subscription. */ protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) { $changelog = ''; if ( isset( $subscription->product->changelog ) ) { // We need to replace h2's and h3's with h4's because the styling expects that. $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) ); $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) ); } // If we're running this because we want to just show the plugin info in the version details modal, we can fallback to the Yoast Free constants, since that modal will not be accessible anyway in the event that the new Free version increases those constants. $defaults = [ // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future. 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null, ]; return (object) [ 'new_version' => ( $subscription->product->version ?? '' ), 'name' => $subscription->product->name, 'slug' => $subscription->product->slug, 'plugin' => $plugin_file, 'url' => $subscription->product->store_url, 'last_update' => $subscription->product->last_updated, 'homepage' => $subscription->product->store_url, 'download_link' => $subscription->product->download, 'package' => $subscription->product->download, 'sections' => [ 'changelog' => $changelog, 'support' => $this->get_support_section(), ], 'icons' => [ '2x' => $this->get_icon( $subscription->product->slug ), ], 'update_supported' => true, 'banners' => $this->get_banners( $subscription->product->slug ), // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults. 'tested' => YOAST_SEO_WP_TESTED, 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ), 'requires_php' => YOAST_SEO_PHP_REQUIRED, ]; } /** * Returns the plugin's icon URL. * * @param string $slug The plugin slug. * * @return string The icon URL for this plugin. */ protected function get_icon( $slug ) { switch ( $slug ) { case self::LOCAL_SLUG: return 'https://yoa.st/local-seo-icon'; case self::NEWS_SLUG: return 'https://yoa.st/news-seo-icon'; case self::PREMIUM_SLUG: return 'https://yoa.st/yoast-seo-icon'; case self::VIDEO_SLUG: return 'https://yoa.st/video-seo-icon'; case self::WOOCOMMERCE_SLUG: return 'https://yoa.st/woo-seo-icon'; } } /** * Return an array of plugin banner URLs. * * @param string $slug The plugin slug. * * @return string[] */ protected function get_banners( $slug ) { switch ( $slug ) { case self::LOCAL_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-local', 'low' => 'https://yoa.st/yoast-seo-banner-low-local', ]; case self::NEWS_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-news', 'low' => 'https://yoa.st/yoast-seo-banner-low-news', ]; case self::PREMIUM_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-premium', 'low' => 'https://yoa.st/yoast-seo-banner-low-premium', ]; case self::VIDEO_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-video', 'low' => 'https://yoa.st/yoast-seo-banner-low-video', ]; case self::WOOCOMMERCE_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-woo', 'low' => 'https://yoa.st/yoast-seo-banner-low-woo', ]; } } /** * Checks if the given plugin_file belongs to a Yoast addon. * * @param string $plugin_file Path to the plugin. * * @return bool True when plugin file is for a Yoast addon. */ protected function is_yoast_addon( $plugin_file ) { return $this->get_slug_by_plugin_file( $plugin_file ) !== ''; } /** * Retrieves the addon slug by given plugin file path. * * @param string $plugin_file The file path to the plugin. * * @return string The slug when found or empty string when not. */ protected function get_slug_by_plugin_file( $plugin_file ) { $addons = self::$addons; // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations. if ( YoastSEO()->helpers->product->is_premium() ) { $addons['wp-seo.php'] = self::FREE_SLUG; } foreach ( $addons as $addon => $addon_slug ) { if ( strpos( $plugin_file, $addon ) !== false ) { return $addon_slug; } } return ''; } /** * Retrieves the installed Yoast addons. * * @return array The installed plugins. */ protected function get_installed_addons() { return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY ); } /** * Retrieves a list of active addons. * * @return array The active addons. */ protected function get_active_addons() { return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY ); } /** * Retrieves the current sites from the API. * * @codeCoverageIgnore * * @return bool|stdClass Object when request is successful. False if not. */ protected function request_current_sites() { $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' ); if ( $api_request->fire() ) { return $api_request->get_response(); } return $this->get_site_information_default(); } /** * Retrieves the transient value with the site information. * * @codeCoverageIgnore * * @return stdClass|false The transient value. */ protected function get_site_information_transient() { global $pagenow; // Force re-check on license & dashboard pages. $current_page = null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing and thus no need to sanitize. $current_page = wp_unslash( $_GET['page'] ); } // Check whether the licenses are valid or whether we need to show notifications. $quick = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' ); // Also do a fresh request on Plugins & Core Update pages. $quick = $quick || $pagenow === 'plugins.php'; $quick = $quick || $pagenow === 'update-core.php'; if ( $quick ) { return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK ); } return get_transient( self::SITE_INFORMATION_TRANSIENT ); } /** * Sets the site information transient. * * @codeCoverageIgnore * * @param stdClass $site_information The site information to save. * * @return void */ protected function set_site_information_transient( $site_information ) { set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS ); set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 ); } /** * Retrieves all installed WordPress plugins. * * @codeCoverageIgnore * * @return array The plugins. */ protected function get_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return get_plugins(); } /** * Checks if the given plugin file belongs to an active plugin. * * @codeCoverageIgnore * * @param string $plugin_file The file path to the plugin. * * @return bool True when plugin is active. */ protected function is_plugin_active( $plugin_file ) { return is_plugin_active( $plugin_file ); } /** * Returns an object with no subscriptions. * * @codeCoverageIgnore * * @return stdClass Site information. */ protected function get_site_information_default() { return (object) [ 'url' => WPSEO_Utils::get_home_url(), 'subscriptions' => [], ]; } /** * Maps the plugin API response. * * @param object $site_information Site information as received from the API. * * @return stdClass Mapped site information. */ protected function map_site_information( $site_information ) { return (object) [ 'url' => $site_information->url, 'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ), ]; } /** * Maps a plugin subscription. * * @param object $subscription Subscription information as received from the API. * * @return stdClass Mapped subscription. */ protected function map_subscription( $subscription ) { // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties. return (object) [ 'renewal_url' => $subscription->renewalUrl, 'expiry_date' => $subscription->expiryDate, 'product' => (object) [ 'version' => $subscription->product->version, 'name' => $subscription->product->name, 'slug' => $subscription->product->slug, 'last_updated' => $subscription->product->lastUpdated, 'store_url' => $subscription->product->storeUrl, // Ternary operator is necessary because download can be undefined. 'download' => ( $subscription->product->download ?? null ), 'changelog' => $subscription->product->changelog, ], ]; // phpcs:enable } /** * Retrieves the site information. * * @return stdClass The site information. */ private function get_site_information() { if ( ! $this->has_installed_addons() ) { return $this->get_site_information_default(); } return $this->get_myyoast_site_information(); } /** * Retrieves the contents for the support section. * * @return string The support section content. */ protected function get_support_section() { return '<h4>' . __( 'Need support?', 'wordpress-seo' ) . '</h4>' . '<p>' /* translators: 1: expands to <a> that refers to the help page, 2: </a> closing tag. */ . sprintf( __( 'You can probably find an answer to your question in our %1$shelp center%2$s.', 'wordpress-seo' ), '<a href="https://yoast.com/help/">', '</a>' ) . ' ' /* translators: %s expands to a mailto support link. */ . sprintf( __( 'If you still need support and have an active subscription for this product, please email %s.', 'wordpress-seo' ), '<a href="mailto:support@yoast.com">support@yoast.com</a>' ) . '</p>'; } } class-upgrade-history.php 0000644 00000006066 14720701066 0011521 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internal */ /** * This class handles storing the current options for future reference. * * This should only be used during an upgrade routine. */ class WPSEO_Upgrade_History { /** * Option to use to store/retrieve data from. * * @var string */ protected $option_name = 'wpseo_upgrade_history'; /** * WPSEO_Upgrade_History constructor. * * @param string|null $option_name Optional. Custom option to use to store/retrieve history from. */ public function __construct( $option_name = null ) { if ( $option_name !== null ) { $this->option_name = $option_name; } } /** * Retrieves the content of the history items currently stored. * * @return array<array<string>> The contents of the history option. */ public function get() { $data = get_option( $this->get_option_name(), [] ); if ( ! is_array( $data ) ) { return []; } return $data; } /** * Adds a new history entry in the storage. * * @param string $old_version The version we are upgrading from. * @param string $new_version The version we are upgrading to. * @param array<string> $option_names The options that need to be stored. * * @return void */ public function add( $old_version, $new_version, array $option_names ) { $option_data = []; if ( $option_names !== [] ) { $option_data = $this->get_options_data( $option_names ); } // Retrieve current history. $data = $this->get(); // Add new entry. $data[ time() ] = [ 'options' => $option_data, 'old_version' => $old_version, 'new_version' => $new_version, ]; // Store the data. $this->set( $data ); } /** * Retrieves the data for the specified option names from the database. * * @param array<string> $option_names The option names to retrieve. * * @return array<int|string|bool|float,array<string|int|bool|float>> The retrieved data. */ protected function get_options_data( array $option_names ) { $wpdb = $this->get_wpdb(); $results = $wpdb->get_results( $wpdb->prepare( ' SELECT %i, %i FROM ' . $wpdb->options . ' WHERE %i IN ( ' . implode( ',', array_fill( 0, count( $option_names ), '%s' ) ) . ' ) ', array_merge( [ 'option_value', 'option_name', 'option_name' ], $option_names ) ), ARRAY_A ); $data = []; foreach ( $results as $result ) { $data[ $result['option_name'] ] = maybe_unserialize( $result['option_value'] ); } return $data; } /** * Stores the new history state. * * @param array<array<string>> $data The data to store. * * @return void */ protected function set( array $data ) { // This should not be autoloaded! update_option( $this->get_option_name(), $data, false ); } /** * Retrieves the WPDB object. * * @return wpdb The WPDB object to use. */ protected function get_wpdb() { global $wpdb; return $wpdb; } /** * Retrieves the option name to store the history in. * * @return string The option name to store the history in. */ protected function get_option_name() { return $this->option_name; } } class-wpseo-custom-fields.php 0000644 00000003346 14720701066 0012302 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Custom_Fields. */ class WPSEO_Custom_Fields { /** * Custom fields cache. * * @var array */ protected static $custom_fields = null; /** * Retrieves the custom field names as an array. * * @link WordPress core: wp-admin/includes/template.php. Reused query from it. * * @return array The custom fields. */ public static function get_custom_fields() { global $wpdb; // Use cached value if available. if ( ! is_null( self::$custom_fields ) ) { return self::$custom_fields; } self::$custom_fields = []; /** * Filters the number of custom fields to retrieve for the drop-down * in the Custom Fields meta box. * * @param int $limit Number of custom fields to retrieve. Default 30. */ $limit = apply_filters( 'postmeta_form_limit', 30 ); $sql = "SELECT DISTINCT meta_key FROM $wpdb->postmeta WHERE meta_key NOT BETWEEN '_' AND '_z' AND SUBSTRING(meta_key, 1, 1) != '_' LIMIT %d"; $fields = $wpdb->get_col( $wpdb->prepare( $sql, $limit ) ); /** * Filters the custom fields that are auto-completed and replaced as replacement variables * in the meta box and sidebar. * * @param string[] $fields The custom field names. */ $fields = apply_filters( 'wpseo_replacement_variables_custom_fields', $fields ); if ( is_array( $fields ) ) { self::$custom_fields = array_map( [ 'WPSEO_Custom_Fields', 'add_custom_field_prefix' ], $fields ); } return self::$custom_fields; } /** * Adds the cf_ prefix to a field. * * @param string $field The field to prefix. * * @return string The prefixed field. */ private static function add_custom_field_prefix( $field ) { return 'cf_' . $field; } } class-wpseo-installation.php 0000644 00000002163 14720701066 0012221 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 3.6 */ /** * This class checks if the wpseo option doesn't exists. In the case it doesn't it will set a property that is * accessible via a method to check if the installation is fresh. */ class WPSEO_Installation { /** * Checks if Yoast SEO is installed for the first time. */ public function __construct() { $is_first_install = $this->is_first_install(); if ( $is_first_install && WPSEO_Utils::is_api_available() ) { add_action( 'wpseo_activate', [ $this, 'set_first_install_options' ] ); } } /** * When the option doesn't exist, it should be a new install. * * @return bool */ private function is_first_install() { return ( get_option( 'wpseo' ) === false ); } /** * Sets the options on first install for showing the installation notice and disabling of the settings pages. * * @return void */ public function set_first_install_options() { $options = get_option( 'wpseo' ); $options['show_onboarding_notice'] = true; $options['first_activated_on'] = time(); update_option( 'wpseo', $options ); } } class-wpseo-utils.php 0000644 00000071040 14720701066 0010660 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 1.8.0 */ use Yoast\WP\SEO\Integrations\Feature_Flag_Integration; /** * Group of utility methods for use by WPSEO. * All methods are static, this is just a sort of namespacing class wrapper. */ class WPSEO_Utils { /** * Whether the PHP filter extension is enabled. * * @since 1.8.0 * * @var bool */ public static $has_filters; /** * Check whether file editing is allowed for the .htaccess and robots.txt files. * * {@internal current_user_can() checks internally whether a user is on wp-ms and adjusts accordingly.}} * * @since 1.8.0 * * @return bool */ public static function allow_system_file_edit() { $allowed = true; if ( current_user_can( 'edit_files' ) === false ) { $allowed = false; } /** * Filter: 'wpseo_allow_system_file_edit' - Allow developers to change whether the editing of * .htaccess and robots.txt is allowed. * * @param bool $allowed Whether file editing is allowed. */ return apply_filters( 'wpseo_allow_system_file_edit', $allowed ); } /** * Check if the web server is running on Apache or compatible (LiteSpeed). * * @since 1.8.0 * * @return bool */ public static function is_apache() { if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) { return false; } $software = sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ); return stripos( $software, 'apache' ) !== false || stripos( $software, 'litespeed' ) !== false; } /** * Check if the web server is running on Nginx. * * @since 1.8.0 * * @return bool */ public static function is_nginx() { if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) { return false; } $software = sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ); return stripos( $software, 'nginx' ) !== false; } /** * Check whether a url is relative. * * @since 1.8.0 * * @param string $url URL string to check. * * @return bool */ public static function is_url_relative( $url ) { return YoastSEO()->helpers->url->is_relative( $url ); } /** * Recursively trim whitespace round a string value or of string values within an array. * Only trims strings to avoid typecasting a variable (to string). * * @since 1.8.0 * * @param mixed $value Value to trim or array of values to trim. * * @return mixed Trimmed value or array of trimmed values. */ public static function trim_recursive( $value ) { if ( is_string( $value ) ) { $value = trim( $value ); } elseif ( is_array( $value ) ) { $value = array_map( [ self::class, 'trim_recursive' ], $value ); } return $value; } /** * Emulate the WP native sanitize_text_field function in a %%variable%% safe way. * * Sanitize a string from user input or from the db. * * - Check for invalid UTF-8; * - Convert single < characters to entity; * - Strip all tags; * - Remove line breaks, tabs and extra white space; * - Strip octets - BUT DO NOT REMOVE (part of) VARIABLES WHICH WILL BE REPLACED. * * @link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/formatting.php for the original. * * @since 1.8.0 * * @param string $value String value to sanitize. * * @return string */ public static function sanitize_text_field( $value ) { $filtered = wp_check_invalid_utf8( $value ); if ( strpos( $filtered, '<' ) !== false ) { $filtered = wp_pre_kses_less_than( $filtered ); // This will strip extra whitespace for us. $filtered = wp_strip_all_tags( $filtered, true ); } else { $filtered = trim( preg_replace( '`[\r\n\t ]+`', ' ', $filtered ) ); } $found = false; while ( preg_match( '`[^%](%[a-f0-9]{2})`i', $filtered, $match ) ) { $filtered = str_replace( $match[1], '', $filtered ); $found = true; } unset( $match ); if ( $found ) { // Strip out the whitespace that may now exist after removing the octets. $filtered = trim( preg_replace( '` +`', ' ', $filtered ) ); } /** * Filter a sanitized text field string. * * @since WP 2.9.0 * * @param string $filtered The sanitized string. * @param string $str The string prior to being sanitized. */ return apply_filters( 'sanitize_text_field', $filtered, $value ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter. } /** * Sanitize a url for saving to the database. * Not to be confused with the old native WP function. * * @since 1.8.0 * * @param string $value String URL value to sanitize. * @param array $allowed_protocols Optional set of allowed protocols. * * @return string */ public static function sanitize_url( $value, $allowed_protocols = [ 'http', 'https' ] ) { $url = ''; $parts = wp_parse_url( $value ); if ( isset( $parts['scheme'], $parts['host'] ) ) { $url = $parts['scheme'] . '://'; if ( isset( $parts['user'] ) ) { $url .= rawurlencode( $parts['user'] ); $url .= isset( $parts['pass'] ) ? ':' . rawurlencode( $parts['pass'] ) : ''; $url .= '@'; } $parts['host'] = preg_replace( '`[^a-z0-9-.:\[\]\\x80-\\xff]`', '', strtolower( $parts['host'] ) ); $url .= $parts['host'] . ( isset( $parts['port'] ) ? ':' . intval( $parts['port'] ) : '' ); } if ( isset( $parts['path'] ) && strpos( $parts['path'], '/' ) === 0 ) { $path = explode( '/', wp_strip_all_tags( $parts['path'] ) ); $path = self::sanitize_encoded_text_field( $path ); $url .= str_replace( '%40', '@', implode( '/', $path ) ); } if ( ! $url ) { return ''; } if ( isset( $parts['query'] ) ) { wp_parse_str( $parts['query'], $parsed_query ); $parsed_query = array_combine( self::sanitize_encoded_text_field( array_keys( $parsed_query ) ), self::sanitize_encoded_text_field( array_values( $parsed_query ) ) ); $url = add_query_arg( $parsed_query, $url ); } if ( isset( $parts['fragment'] ) ) { $url .= '#' . self::sanitize_encoded_text_field( $parts['fragment'] ); } if ( strpos( $url, '%' ) !== false ) { $url = preg_replace_callback( '`%[a-fA-F0-9]{2}`', static function ( $octects ) { return strtolower( $octects[0] ); }, $url ); } return esc_url_raw( $url, $allowed_protocols ); } /** * Decode, sanitize and encode the array of strings or the string. * * @since 13.3 * * @param array|string $value The value to sanitize and encode. * * @return array|string The sanitized value. */ public static function sanitize_encoded_text_field( $value ) { if ( is_array( $value ) ) { return array_map( [ self::class, 'sanitize_encoded_text_field' ], $value ); } return rawurlencode( sanitize_text_field( rawurldecode( $value ) ) ); } /** * Validate a value as boolean. * * @since 1.8.0 * * @param mixed $value Value to validate. * * @return bool */ public static function validate_bool( $value ) { if ( ! isset( self::$has_filters ) ) { self::$has_filters = extension_loaded( 'filter' ); } if ( self::$has_filters ) { return filter_var( $value, FILTER_VALIDATE_BOOLEAN ); } else { return self::emulate_filter_bool( $value ); } } /** * Cast a value to bool. * * @since 1.8.0 * * @param mixed $value Value to cast. * * @return bool */ public static function emulate_filter_bool( $value ) { $true = [ '1', 'true', 'True', 'TRUE', 'y', 'Y', 'yes', 'Yes', 'YES', 'on', 'On', 'ON', ]; $false = [ '0', 'false', 'False', 'FALSE', 'n', 'N', 'no', 'No', 'NO', 'off', 'Off', 'OFF', ]; if ( is_bool( $value ) ) { return $value; } elseif ( is_int( $value ) && ( $value === 0 || $value === 1 ) ) { return (bool) $value; } elseif ( ( is_float( $value ) && ! is_nan( $value ) ) && ( $value === (float) 0 || $value === (float) 1 ) ) { return (bool) $value; } elseif ( is_string( $value ) ) { $value = trim( $value ); if ( in_array( $value, $true, true ) ) { return true; } elseif ( in_array( $value, $false, true ) ) { return false; } else { return false; } } return false; } /** * Validate a value as integer. * * @since 1.8.0 * * @param mixed $value Value to validate. * * @return int|bool Int or false in case of failure to convert to int. */ public static function validate_int( $value ) { if ( ! isset( self::$has_filters ) ) { self::$has_filters = extension_loaded( 'filter' ); } if ( self::$has_filters ) { return filter_var( $value, FILTER_VALIDATE_INT ); } else { return self::emulate_filter_int( $value ); } } /** * Cast a value to integer. * * @since 1.8.0 * * @param mixed $value Value to cast. * * @return int|bool */ public static function emulate_filter_int( $value ) { if ( is_int( $value ) ) { return $value; } elseif ( is_float( $value ) ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. if ( (int) $value == $value && ! is_nan( $value ) ) { return (int) $value; } else { return false; } } elseif ( is_string( $value ) ) { $value = trim( $value ); if ( $value === '' ) { return false; } elseif ( ctype_digit( $value ) ) { return (int) $value; } elseif ( strpos( $value, '-' ) === 0 && ctype_digit( substr( $value, 1 ) ) ) { return (int) $value; } else { return false; } } return false; } /** * Clears the WP or W3TC cache depending on which is used. * * @since 1.8.0 * * @return void */ public static function clear_cache() { if ( function_exists( 'w3tc_flush_posts' ) ) { w3tc_flush_posts(); } elseif ( function_exists( 'wp_cache_clear_cache' ) ) { wp_cache_clear_cache(); } } /** * Clear rewrite rules. * * @since 1.8.0 * * @return void */ public static function clear_rewrites() { update_option( 'rewrite_rules', '' ); } /** * Do simple reliable math calculations without the risk of wrong results. * * In the rare case that the bcmath extension would not be loaded, it will return the normal calculation results. * * @link http://floating-point-gui.de/ * @link http://php.net/language.types.float.php See the big red warning. * * @since 1.5.0 * @since 1.8.0 Moved from stand-alone function to this class. * * @param mixed $number1 Scalar (string/int/float/bool). * @param string $action Calculation action to execute. Valid input: * '+' or 'add' or 'addition', * '-' or 'sub' or 'subtract', * '*' or 'mul' or 'multiply', * '/' or 'div' or 'divide', * '%' or 'mod' or 'modulus' * '=' or 'comp' or 'compare'. * @param mixed $number2 Scalar (string/int/float/bool). * @param bool $round Whether or not to round the result. Defaults to false. * Will be disregarded for a compare operation. * @param int $decimals Decimals for rounding operation. Defaults to 0. * @param int $precision Calculation precision. Defaults to 10. * * @return mixed Calculation Result or false if either or the numbers isn't scalar or * an invalid operation was passed. * - For compare the result will always be an integer. * - For all other operations, the result will either be an integer (preferred) * or a float. */ public static function calc( $number1, $action, $number2, $round = false, $decimals = 0, $precision = 10 ) { static $bc; if ( ! is_scalar( $number1 ) || ! is_scalar( $number2 ) ) { return false; } if ( ! isset( $bc ) ) { $bc = extension_loaded( 'bcmath' ); } if ( $bc ) { $number1 = number_format( $number1, 10, '.', '' ); $number2 = number_format( $number2, 10, '.', '' ); } $result = null; $compare = false; switch ( $action ) { case '+': case 'add': case 'addition': $result = ( $bc ) ? bcadd( $number1, $number2, $precision ) /* string */ : ( $number1 + $number2 ); break; case '-': case 'sub': case 'subtract': $result = ( $bc ) ? bcsub( $number1, $number2, $precision ) /* string */ : ( $number1 - $number2 ); break; case '*': case 'mul': case 'multiply': $result = ( $bc ) ? bcmul( $number1, $number2, $precision ) /* string */ : ( $number1 * $number2 ); break; case '/': case 'div': case 'divide': if ( $bc ) { $result = bcdiv( $number1, $number2, $precision ); // String, or NULL if right_operand is 0. } elseif ( $number2 != 0 ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( $number1 / $number2 ); } if ( ! isset( $result ) ) { $result = 0; } break; case '%': case 'mod': case 'modulus': if ( $bc ) { $result = bcmod( $number1, $number2 ); // String, or NULL if modulus is 0. } elseif ( $number2 != 0 ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( $number1 % $number2 ); } if ( ! isset( $result ) ) { $result = 0; } break; case '=': case 'comp': case 'compare': $compare = true; if ( $bc ) { $result = bccomp( $number1, $number2, $precision ); // Returns int 0, 1 or -1. } else { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( $number1 == $number2 ) ? 0 : ( ( $number1 > $number2 ) ? 1 : -1 ); } break; } if ( isset( $result ) ) { if ( $compare === false ) { if ( $round === true ) { $result = round( floatval( $result ), $decimals ); if ( $decimals === 0 ) { $result = (int) $result; } } else { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( intval( $result ) == $result ) ? intval( $result ) : floatval( $result ); } } return $result; } return false; } /** * Trim whitespace and NBSP (Non-breaking space) from string. * * @since 2.0.0 * * @param string $text String input to trim. * * @return string */ public static function trim_nbsp_from_string( $text ) { $find = [ ' ', chr( 0xC2 ) . chr( 0xA0 ) ]; $text = str_replace( $find, ' ', $text ); $text = trim( $text ); return $text; } /** * Check if a string is a valid datetime. * * @since 2.0.0 * * @param string $datetime String input to check as valid input for DateTime class. * * @return bool */ public static function is_valid_datetime( $datetime ) { return YoastSEO()->helpers->date->is_valid_datetime( $datetime ); } /** * Format the URL to be sure it is okay for using as a redirect url. * * This method will parse the URL and combine them in one string. * * @since 2.3.0 * * @param string $url URL string. * * @return mixed */ public static function format_url( $url ) { $parsed_url = wp_parse_url( $url ); $formatted_url = ''; if ( ! empty( $parsed_url['path'] ) ) { $formatted_url = $parsed_url['path']; } // Prepend a slash if first char != slash. if ( stripos( $formatted_url, '/' ) !== 0 ) { $formatted_url = '/' . $formatted_url; } // Append 'query' string if it exists. if ( ! empty( $parsed_url['query'] ) ) { $formatted_url .= '?' . $parsed_url['query']; } return apply_filters( 'wpseo_format_admin_url', $formatted_url ); } /** * Retrieves the sitename. * * @since 3.0.0 * * @return string */ public static function get_site_name() { return YoastSEO()->helpers->site->get_site_name(); } /** * Check if the current opened page is a Yoast SEO page. * * @since 3.0.0 * * @return bool */ public static function is_yoast_seo_page() { return YoastSEO()->helpers->current_page->is_yoast_seo_page(); } /** * Check if the current opened page belongs to Yoast SEO Free. * * @since 3.3.0 * * @param string $current_page The current page the user is on. * * @return bool */ public static function is_yoast_seo_free_page( $current_page ) { $yoast_seo_free_pages = [ 'wpseo_dashboard', 'wpseo_tools', 'wpseo_search_console', 'wpseo_licenses', ]; return in_array( $current_page, $yoast_seo_free_pages, true ); } /** * Determine if Yoast SEO is in development mode? * * Inspired by JetPack (https://github.com/Automattic/jetpack/blob/master/class.jetpack.php#L1383-L1406). * * @since 3.0.0 * * @return bool */ public static function is_development_mode() { $development_mode = false; if ( defined( 'YOAST_ENVIRONMENT' ) && YOAST_ENVIRONMENT === 'development' ) { $development_mode = true; } elseif ( defined( 'WPSEO_DEBUG' ) ) { $development_mode = WPSEO_DEBUG; } elseif ( site_url() && strpos( site_url(), '.' ) === false ) { $development_mode = true; } /** * Filter the Yoast SEO development mode. * * @since 3.0 * * @param bool $development_mode Is Yoast SEOs development mode active. */ return apply_filters( 'yoast_seo_development_mode', $development_mode ); } /** * Retrieve home URL with proper trailing slash. * * @since 3.3.0 * * @param string $path Path relative to home URL. * @param string|null $scheme Scheme to apply. * * @return string Home URL with optional path, appropriately slashed if not. */ public static function home_url( $path = '', $scheme = null ) { return YoastSEO()->helpers->url->home( $path, $scheme ); } /** * Checks if the WP-REST-API is available. * * @since 3.6 * @since 3.7 Introduced the $minimum_version parameter. * * @param string $minimum_version The minimum version the API should be. * * @return bool Returns true if the API is available. */ public static function is_api_available( $minimum_version = '2.0' ) { return ( defined( 'REST_API_VERSION' ) && version_compare( REST_API_VERSION, $minimum_version, '>=' ) ); } /** * Determine whether or not the metabox should be displayed for a post type. * * @param string|null $post_type Optional. The post type to check the visibility of the metabox for. * * @return bool Whether or not the metabox should be displayed. */ protected static function display_post_type_metabox( $post_type = null ) { if ( ! isset( $post_type ) ) { $post_type = get_post_type(); } if ( ! isset( $post_type ) || ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) ) { return false; } if ( $post_type === 'attachment' && WPSEO_Options::get( 'disable-attachment' ) ) { return false; } return apply_filters( 'wpseo_enable_editor_features_' . $post_type, WPSEO_Options::get( 'display-metabox-pt-' . $post_type ) ); } /** * Determine whether or not the metabox should be displayed for a taxonomy. * * @param string|null $taxonomy Optional. The post type to check the visibility of the metabox for. * * @return bool Whether or not the metabox should be displayed. */ protected static function display_taxonomy_metabox( $taxonomy = null ) { if ( ! isset( $taxonomy ) || ! in_array( $taxonomy, get_taxonomies( [ 'public' => true ], 'names' ), true ) ) { return false; } return WPSEO_Options::get( 'display-metabox-tax-' . $taxonomy ); } /** * Determines whether the metabox is active for the given identifier and type. * * @param string $identifier The identifier to check for. * @param string $type The type to check for. * * @return bool Whether or not the metabox is active. */ public static function is_metabox_active( $identifier, $type ) { if ( $type === 'post_type' ) { return self::display_post_type_metabox( $identifier ); } if ( $type === 'taxonomy' ) { return self::display_taxonomy_metabox( $identifier ); } return false; } /** * Determines whether the plugin is active for the entire network. * * @return bool Whether the plugin is network-active. */ public static function is_plugin_network_active() { return YoastSEO()->helpers->url->is_plugin_network_active(); } /** * Gets the type of the current post. * * @return string The post type, or an empty string. */ public static function get_post_type() { $wp_screen = get_current_screen(); if ( $wp_screen !== null && ! empty( $wp_screen->post_type ) ) { return $wp_screen->post_type; } return ''; } /** * Gets the type of the current page. * * @return string Returns 'post' if the current page is a post edit page. Taxonomy in other cases. */ public static function get_page_type() { global $pagenow; if ( WPSEO_Metabox::is_post_edit( $pagenow ) ) { return 'post'; } return 'taxonomy'; } /** * Getter for the Adminl10n array. Applies the wpseo_admin_l10n filter. * * @return array The Adminl10n array. */ public static function get_admin_l10n() { $post_type = self::get_post_type(); $page_type = self::get_page_type(); $label_object = false; $no_index = false; if ( $page_type === 'post' ) { $label_object = get_post_type_object( $post_type ); $no_index = WPSEO_Options::get( 'noindex-' . $post_type, false ); } else { $label_object = WPSEO_Taxonomy::get_labels(); $wp_screen = get_current_screen(); if ( $wp_screen !== null && ! empty( $wp_screen->taxonomy ) ) { $taxonomy_slug = $wp_screen->taxonomy; $no_index = WPSEO_Options::get( 'noindex-tax-' . $taxonomy_slug, false ); } } $wpseo_admin_l10n = [ 'displayAdvancedTab' => WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) || ! WPSEO_Options::get( 'disableadvanced_meta' ), 'noIndex' => (bool) $no_index, 'isPostType' => (bool) get_post_type(), 'postType' => get_post_type(), 'postTypeNamePlural' => ( $page_type === 'post' ) ? $label_object->label : $label_object->name, 'postTypeNameSingular' => ( $page_type === 'post' ) ? $label_object->labels->singular_name : $label_object->singular_name, 'isBreadcrumbsDisabled' => WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ), 'isAiFeatureActive' => (bool) WPSEO_Options::get( 'enable_ai_generator' ), ]; $additional_entries = apply_filters( 'wpseo_admin_l10n', [] ); if ( is_array( $additional_entries ) ) { $wpseo_admin_l10n = array_merge( $wpseo_admin_l10n, $additional_entries ); } return $wpseo_admin_l10n; } /** * Retrieves the analysis worker log level. Defaults to errors only. * * Uses bool YOAST_SEO_DEBUG as flag to enable logging. Off equals ERROR. * Uses string YOAST_SEO_DEBUG_ANALYSIS_WORKER as log level for the Analysis * Worker. Defaults to INFO. * Can be: TRACE, DEBUG, INFO, WARN or ERROR. * * @return string The log level to use. */ public static function get_analysis_worker_log_level() { if ( defined( 'YOAST_SEO_DEBUG' ) && YOAST_SEO_DEBUG ) { return defined( 'YOAST_SEO_DEBUG_ANALYSIS_WORKER' ) ? YOAST_SEO_DEBUG_ANALYSIS_WORKER : 'INFO'; } return 'ERROR'; } /** * Returns the unfiltered home URL. * * In case WPML is installed, returns the original home_url and not the WPML version. * In case of a multisite setup we return the network_home_url. * * @codeCoverageIgnore * * @return string The home url. */ public static function get_home_url() { return YoastSEO()->helpers->url->network_safe_home_url(); } /** * Prepares data for outputting as JSON. * * @param array $data The data to format. * * @return false|string The prepared JSON string. */ public static function format_json_encode( $data ) { $flags = ( JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( self::is_development_mode() ) { $flags = ( $flags | JSON_PRETTY_PRINT ); /** * Filter the Yoast SEO development mode. * * @param array $data Allows filtering of the JSON data for debug purposes. */ $data = apply_filters( 'wpseo_debug_json_data', $data ); } // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.FoundWithAdditionalParams -- This is the definition of format_json_encode. return wp_json_encode( $data, $flags ); } /** * Extends the allowed post tags with accessibility-related attributes. * * @codeCoverageIgnore * * @param array $allowed_post_tags The allowed post tags. * * @return array The allowed tags including post tags, input tags and select tags. */ public static function extend_kses_post_with_a11y( $allowed_post_tags ) { static $a11y_tags; if ( isset( $a11y_tags ) === false ) { $a11y_tags = [ 'button' => [ 'aria-expanded' => true, 'aria-controls' => true, ], 'div' => [ 'tabindex' => true, ], // Below are attributes that are needed for backwards compatibility (WP < 5.1). 'span' => [ 'aria-hidden' => true, ], 'input' => [ 'aria-describedby' => true, ], 'select' => [ 'aria-describedby' => true, ], 'textarea' => [ 'aria-describedby' => true, ], ]; // Add the global allowed attributes to each html element. $a11y_tags = array_map( '_wp_add_global_attributes', $a11y_tags ); } return array_merge_recursive( $allowed_post_tags, $a11y_tags ); } /** * Extends the allowed post tags with input, select and option tags. * * @codeCoverageIgnore * * @param array $allowed_post_tags The allowed post tags. * * @return array The allowed tags including post tags, input tags, select tags and option tags. */ public static function extend_kses_post_with_forms( $allowed_post_tags ) { static $input_tags; if ( isset( $input_tags ) === false ) { $input_tags = [ 'input' => [ 'accept' => true, 'accesskey' => true, 'align' => true, 'alt' => true, 'autocomplete' => true, 'autofocus' => true, 'checked' => true, 'contenteditable' => true, 'dirname' => true, 'disabled' => true, 'draggable' => true, 'dropzone' => true, 'form' => true, 'formaction' => true, 'formenctype' => true, 'formmethod' => true, 'formnovalidate' => true, 'formtarget' => true, 'height' => true, 'hidden' => true, 'lang' => true, 'list' => true, 'max' => true, 'maxlength' => true, 'min' => true, 'multiple' => true, 'name' => true, 'pattern' => true, 'placeholder' => true, 'readonly' => true, 'required' => true, 'size' => true, 'spellcheck' => true, 'src' => true, 'step' => true, 'tabindex' => true, 'translate' => true, 'type' => true, 'value' => true, 'width' => true, /* * Below are attributes that are needed for backwards compatibility (WP < 5.1). * They are used for the social media image in the metabox. * These can be removed once we move to the React versions of the social previews. */ 'data-target' => true, 'data-target-id' => true, ], 'select' => [ 'accesskey' => true, 'autofocus' => true, 'contenteditable' => true, 'disabled' => true, 'draggable' => true, 'dropzone' => true, 'form' => true, 'hidden' => true, 'lang' => true, 'multiple' => true, 'name' => true, 'onblur' => true, 'onchange' => true, 'oncontextmenu' => true, 'onfocus' => true, 'oninput' => true, 'oninvalid' => true, 'onreset' => true, 'onsearch' => true, 'onselect' => true, 'onsubmit' => true, 'required' => true, 'size' => true, 'spellcheck' => true, 'tabindex' => true, 'translate' => true, ], 'option' => [ 'class' => true, 'disabled' => true, 'id' => true, 'label' => true, 'selected' => true, 'value' => true, ], ]; // Add the global allowed attributes to each html element. $input_tags = array_map( '_wp_add_global_attributes', $input_tags ); } return array_merge_recursive( $allowed_post_tags, $input_tags ); } /** * Gets an array of enabled features. * * @return string[] The array of enabled features. */ public static function retrieve_enabled_features() { /** * The feature flag integration. * * @var Feature_Flag_Integration $feature_flag_integration; */ $feature_flag_integration = YoastSEO()->classes->get( Feature_Flag_Integration::class ); return $feature_flag_integration->get_enabled_features(); } } class-rewrite.php 0000644 00000017121 14720701066 0010046 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Frontend */ /** * This code handles the category rewrites. */ class WPSEO_Rewrite { /** * Class constructor. */ public function __construct() { add_filter( 'query_vars', [ $this, 'query_vars' ] ); add_filter( 'term_link', [ $this, 'no_category_base' ], 10, 3 ); add_filter( 'request', [ $this, 'request' ] ); add_filter( 'category_rewrite_rules', [ $this, 'category_rewrite_rules_wrapper' ] ); add_action( 'created_category', [ $this, 'schedule_flush' ] ); add_action( 'edited_category', [ $this, 'schedule_flush' ] ); add_action( 'delete_category', [ $this, 'schedule_flush' ] ); } /** * Trigger a rewrite_rule flush on shutdown. * * @since 1.2.8 * * @return void */ public function schedule_flush() { if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) { add_action( 'shutdown', 'flush_rewrite_rules' ); } } /** * Override the category link to remove the category base. * * @param string $link Term link, overridden by the function for categories. * @param WP_Term $term Unused, term object. * @param string $taxonomy Taxonomy slug. * * @return string */ public function no_category_base( $link, $term, $taxonomy ) { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return $link; } if ( $taxonomy !== 'category' ) { return $link; } $category_base = get_option( 'category_base' ); if ( empty( $category_base ) ) { $category_base = 'category'; } /* * Remove initial slash, if there is one (we remove the trailing slash * in the regex replacement and don't want to end up short a slash). */ if ( substr( $category_base, 0, 1 ) === '/' ) { $category_base = substr( $category_base, 1 ); } $category_base .= '/'; return preg_replace( '`' . preg_quote( $category_base, '`' ) . '`u', '', $link, 1 ); } /** * Update the query vars with the redirect var when stripcategorybase is active. * * @param array<string> $query_vars Main query vars to filter. * * @return array<string> The query vars. */ public function query_vars( $query_vars ) { if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) { $query_vars[] = 'wpseo_category_redirect'; } return $query_vars; } /** * Checks whether the redirect needs to be created. * * @param array<string> $query_vars Query vars to check for existence of redirect var. * * @return array<string> The query vars. */ public function request( $query_vars ) { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return $query_vars; } if ( ! isset( $query_vars['wpseo_category_redirect'] ) ) { return $query_vars; } $this->redirect( $query_vars['wpseo_category_redirect'] ); return []; } /** * Wrapper for the category_rewrite_rules() below, so we can add the $rules param in a BC way. * * @param array<string> $rules Rewrite rules generated for the current permastruct, keyed by their regex pattern. * * @return array<string> The category rewrite rules. */ public function category_rewrite_rules_wrapper( $rules ) { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return $rules; } return $this->category_rewrite_rules(); } /** * This function taken and only slightly adapted from WP No Category Base plugin by Saurabh Gupta. * * @return array<string> The category rewrite rules. */ public function category_rewrite_rules() { global $wp_rewrite; $category_rewrite = []; $taxonomy = get_taxonomy( 'category' ); $permalink_structure = get_option( 'permalink_structure' ); $blog_prefix = ''; if ( strpos( $permalink_structure, '/blog/' ) === 0 ) { if ( ( is_multisite() && ! is_subdomain_install() ) || is_main_site() || is_main_network() ) { $blog_prefix = 'blog/'; } } $categories = get_categories( [ 'hide_empty' => false ] ); if ( is_array( $categories ) && $categories !== [] ) { foreach ( $categories as $category ) { $category_nicename = $category->slug; if ( $category->parent === $category->cat_ID ) { // Recursive recursion. $category->parent = 0; } elseif ( $taxonomy->rewrite['hierarchical'] !== false && $category->parent !== 0 ) { $parents = get_category_parents( $category->parent, false, '/', true ); if ( ! is_wp_error( $parents ) ) { $category_nicename = $parents . $category_nicename; } unset( $parents ); } $category_rewrite = $this->add_category_rewrites( $category_rewrite, $category_nicename, $blog_prefix, $wp_rewrite->pagination_base ); // Adds rules for the uppercase encoded URIs. $category_nicename_filtered = $this->convert_encoded_to_upper( $category_nicename ); if ( $category_nicename_filtered !== $category_nicename ) { $category_rewrite = $this->add_category_rewrites( $category_rewrite, $category_nicename_filtered, $blog_prefix, $wp_rewrite->pagination_base ); } } unset( $categories, $category, $category_nicename, $category_nicename_filtered ); } // Redirect support from Old Category Base. $old_base = $wp_rewrite->get_category_permastruct(); $old_base = str_replace( '%category%', '(.+)', $old_base ); $old_base = trim( $old_base, '/' ); $category_rewrite[ $old_base . '$' ] = 'index.php?wpseo_category_redirect=$matches[1]'; return $category_rewrite; } /** * Adds required category rewrites rules. * * @param array<string> $rewrites The current set of rules. * @param string $category_name Category nicename. * @param string $blog_prefix Multisite blog prefix. * @param string $pagination_base WP_Query pagination base. * * @return array<string> The added set of rules. */ protected function add_category_rewrites( $rewrites, $category_name, $blog_prefix, $pagination_base ) { $rewrite_name = $blog_prefix . '(' . $category_name . ')'; global $wp_rewrite; $feed_regex = '(' . implode( '|', $wp_rewrite->feeds ) . ')'; $rewrites[ $rewrite_name . '/(?:feed/)?' . $feed_regex . '/?$' ] = 'index.php?category_name=$matches[1]&feed=$matches[2]'; $rewrites[ $rewrite_name . '/' . $pagination_base . '/?([0-9]{1,})/?$' ] = 'index.php?category_name=$matches[1]&paged=$matches[2]'; $rewrites[ $rewrite_name . '/?$' ] = 'index.php?category_name=$matches[1]'; return $rewrites; } /** * Walks through category nicename and convert encoded parts * into uppercase using $this->encode_to_upper(). * * @param string $name The encoded category URI string. * * @return string The convered URI string. */ protected function convert_encoded_to_upper( $name ) { // Checks if name has any encoding in it. if ( strpos( $name, '%' ) === false ) { return $name; } $names = explode( '/', $name ); $names = array_map( [ $this, 'encode_to_upper' ], $names ); return implode( '/', $names ); } /** * Converts the encoded URI string to uppercase. * * @param string $encoded The encoded string. * * @return string The uppercased string. */ public function encode_to_upper( $encoded ) { if ( strpos( $encoded, '%' ) === false ) { return $encoded; } return strtoupper( $encoded ); } /** * Redirect the "old" category URL to the new one. * * @codeCoverageIgnore * * @param string $category_redirect The category page to redirect to. * @return void */ protected function redirect( $category_redirect ) { $catlink = trailingslashit( get_option( 'home' ) ) . user_trailingslashit( $category_redirect, 'category' ); wp_safe_redirect( $catlink, 301, 'Yoast SEO' ); exit; } } class-wpseo-shortlinker.php 0000644 00000002154 14720701066 0012064 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * Helps with creating shortlinks in the plugin. */ class WPSEO_Shortlinker { /** * Builds a URL to use in the plugin as shortlink. * * @param string $url The URL to build upon. * * @return string The final URL. */ public function build_shortlink( $url ) { return YoastSEO()->helpers->short_link->build( $url ); } /** * Returns a version of the URL with a utm_content with the current version. * * @param string $url The URL to build upon. * * @return string The final URL. */ public static function get( $url ) { return YoastSEO()->helpers->short_link->get( $url ); } /** * Echoes a version of the URL with a utm_content with the current version. * * @param string $url The URL to build upon. * * @return void */ public static function show( $url ) { YoastSEO()->helpers->short_link->show( $url ); } /** * Gets the shortlink's query params. * * @return array The shortlink's query params. */ public static function get_query_params() { return YoastSEO()->helpers->short_link->get_query_params(); } } class-wpseo-image-utils.php 0000644 00000036135 14720701066 0011746 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Image_Utils. */ class WPSEO_Image_Utils { /** * Find an attachment ID for a given URL. * * @param string $url The URL to find the attachment for. * * @return int The found attachment ID, or 0 if none was found. */ public static function get_attachment_by_url( $url ) { /* * As get_attachment_by_url won't work on resized versions of images, * we strip out the size part of an image URL. */ $url = preg_replace( '/(.*)-\d+x\d+\.(jpg|png|gif)$/', '$1.$2', $url ); static $uploads; if ( $uploads === null ) { $uploads = wp_get_upload_dir(); } // Don't try to do this for external URLs. if ( strpos( $url, $uploads['baseurl'] ) !== 0 ) { return 0; } if ( function_exists( 'wpcom_vip_attachment_url_to_postid' ) ) { // @codeCoverageIgnoreStart -- We can't test this properly. return (int) wpcom_vip_attachment_url_to_postid( $url ); // @codeCoverageIgnoreEnd -- The rest we _can_ test. } return self::attachment_url_to_postid( $url ); } /** * Implements the attachment_url_to_postid with use of WP Cache. * * @param string $url The attachment URL for which we want to know the Post ID. * * @return int The Post ID belonging to the attachment, 0 if not found. */ protected static function attachment_url_to_postid( $url ) { $cache_key = sprintf( 'yoast_attachment_url_post_id_%s', md5( $url ) ); // Set the ID based on the hashed URL in the cache. $id = wp_cache_get( $cache_key ); if ( $id === 'not_found' ) { return 0; } // ID is found in cache, return. if ( $id !== false ) { return $id; } // Note: We use the WP COM version if we can, see above. $id = attachment_url_to_postid( $url ); if ( empty( $id ) ) { /** * If no ID was found, maybe we're dealing with a scaled big image. So, let's try that. * * @see https://core.trac.wordpress.org/ticket/51058 */ $id = self::get_scaled_image_id( $url ); } if ( empty( $id ) ) { wp_cache_set( $cache_key, 'not_found', '', ( 12 * HOUR_IN_SECONDS + wp_rand( 0, ( 4 * HOUR_IN_SECONDS ) ) ) ); return 0; } // We have the Post ID, but it's not in the cache yet. We do that here and return. wp_cache_set( $cache_key, $id, '', ( 24 * HOUR_IN_SECONDS + wp_rand( 0, ( 12 * HOUR_IN_SECONDS ) ) ) ); return $id; } /** * Tries getting the ID of a potentially scaled image. * * @param string $url The URL of the image. * * @return int|false The ID of the image or false for failure. */ protected static function get_scaled_image_id( $url ) { $path_parts = pathinfo( $url ); if ( isset( $path_parts['dirname'], $path_parts['filename'], $path_parts['extension'] ) ) { $scaled_url = trailingslashit( $path_parts['dirname'] ) . $path_parts['filename'] . '-scaled.' . $path_parts['extension']; return attachment_url_to_postid( $scaled_url ); } return false; } /** * Retrieves the image data. * * @param array $image Image array with URL and metadata. * @param int $attachment_id Attachment ID. * * @return false|array { * Array of image data * * @type string $alt Image's alt text. * @type string $path Path of image. * @type int $width Width of image. * @type int $height Height of image. * @type string $type Image's MIME type. * @type string $size Image's size. * @type string $url Image's URL. * @type int $filesize The file size in bytes, if already set. * } */ public static function get_data( $image, $attachment_id ) { if ( ! is_array( $image ) ) { return false; } // Deals with non-set keys and values being null or false. if ( empty( $image['width'] ) || empty( $image['height'] ) ) { return false; } $image['id'] = $attachment_id; $image['alt'] = self::get_alt_tag( $attachment_id ); $image['pixels'] = ( (int) $image['width'] * (int) $image['height'] ); if ( ! isset( $image['type'] ) ) { $image['type'] = get_post_mime_type( $attachment_id ); } /** * Filter: 'wpseo_image_data' - Filter image data. * * Elements with keys not listed in the section will be discarded. * * @param array $image_data { * Array of image data * * @type int id Image's ID as an attachment. * @type string alt Image's alt text. * @type string path Image's path. * @type int width Width of image. * @type int height Height of image. * @type int pixels Number of pixels in the image. * @type string type Image's MIME type. * @type string size Image's size. * @type string url Image's URL. * @type int filesize The file size in bytes, if already set. * } * @param int $attachment_id Attachment ID. */ $image = apply_filters( 'wpseo_image_data', $image, $attachment_id ); // Keep only the keys we need, and nothing else. return array_intersect_key( $image, array_flip( [ 'id', 'alt', 'path', 'width', 'height', 'pixels', 'type', 'size', 'url', 'filesize' ] ) ); } /** * Checks a size version of an image to see if it's not too heavy. * * @param array $image Image to check the file size of. * * @return bool True when the image is within limits, false if not. */ public static function has_usable_file_size( $image ) { if ( ! is_array( $image ) || $image === [] ) { return false; } /** * Filter: 'wpseo_image_image_weight_limit' - Determines what the maximum weight * (in bytes) of an image is allowed to be, default is 2 MB. * * @param int $max_bytes The maximum weight (in bytes) of an image. */ $max_size = apply_filters( 'wpseo_image_image_weight_limit', 2097152 ); // We cannot check without a path, so assume it's fine. if ( ! isset( $image['path'] ) ) { return true; } return ( self::get_file_size( $image ) <= $max_size ); } /** * Find the right version of an image based on size. * * @param int $attachment_id Attachment ID. * @param string|array $size Size name, or array of width and height in pixels (e.g [800,400]). * * @return array|false Returns an array with image data on success, false on failure. */ public static function get_image( $attachment_id, $size ) { $image = false; if ( $size === 'full' ) { $image = self::get_full_size_image_data( $attachment_id ); } if ( ! $image ) { $image = image_get_intermediate_size( $attachment_id, $size ); } if ( ! is_array( $image ) ) { $image_src = wp_get_attachment_image_src( $attachment_id, $size ); if ( is_array( $image_src ) && isset( $image_src[1] ) && isset( $image_src[2] ) ) { $image = []; $image['url'] = $image_src[0]; $image['width'] = $image_src[1]; $image['height'] = $image_src[2]; $image['size'] = 'full'; } } if ( ! $image ) { return false; } if ( ! isset( $image['size'] ) ) { $image['size'] = $size; } return self::get_data( $image, $attachment_id ); } /** * Returns the image data for the full size image. * * @param int $attachment_id Attachment ID. * * @return array|false Array when there is a full size image. False if not. */ protected static function get_full_size_image_data( $attachment_id ) { $image = wp_get_attachment_metadata( $attachment_id ); if ( ! is_array( $image ) ) { return false; } $image['url'] = wp_get_attachment_image_url( $attachment_id, 'full' ); $image['path'] = get_attached_file( $attachment_id ); $image['size'] = 'full'; return $image; } /** * Finds the full file path for a given image file. * * @param string $path The relative file path. * * @return string The full file path. */ public static function get_absolute_path( $path ) { static $uploads; if ( $uploads === null ) { $uploads = wp_get_upload_dir(); } // Add the uploads basedir if the path does not start with it. if ( empty( $uploads['error'] ) && strpos( $path, $uploads['basedir'] ) !== 0 ) { return $uploads['basedir'] . DIRECTORY_SEPARATOR . ltrim( $path, DIRECTORY_SEPARATOR ); } return $path; } /** * Get the relative path of the image. * * @param string $img Image URL. * * @return string The expanded image URL. */ public static function get_relative_path( $img ) { if ( $img[0] !== '/' ) { return $img; } // If it's a relative URL, it's relative to the domain, not necessarily to the WordPress install, we // want to preserve domain name and URL scheme (http / https) though. $parsed_url = wp_parse_url( home_url() ); $img = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $img; return $img; } /** * Get the image file size. * * @param array $image An image array object. * * @return int The file size in bytes. */ public static function get_file_size( $image ) { if ( isset( $image['filesize'] ) ) { return $image['filesize']; } if ( ! isset( $image['path'] ) ) { return 0; } // If the file size for the file is over our limit, we're going to go for a smaller version. if ( function_exists( 'wp_filesize' ) ) { return wp_filesize( self::get_absolute_path( $image['path'] ) ); } return file_exists( $image['path'] ) ? (int) filesize( $image['path'] ) : 0; } /** * Returns the different image variations for consideration. * * @param int $attachment_id The attachment to return the variations for. * * @return array The different variations possible for this attachment ID. */ public static function get_variations( $attachment_id ) { $variations = []; foreach ( self::get_sizes() as $size ) { $variation = self::get_image( $attachment_id, $size ); // The get_image function returns false if the size doesn't exist for this attachment. if ( $variation ) { $variations[] = $variation; } } return $variations; } /** * Check original size of image. If original image is too small, return false, else return true. * * Filters a list of variations by a certain set of usable dimensions. * * @param array $usable_dimensions { * The parameters to check against. * * @type int $min_width Minimum width of image. * @type int $max_width Maximum width of image. * @type int $min_height Minimum height of image. * @type int $max_height Maximum height of image. * } * @param array $variations The variations that should be considered. * * @return array Whether a variation is fit for display or not. */ public static function filter_usable_dimensions( $usable_dimensions, $variations ) { $filtered = []; foreach ( $variations as $variation ) { $dimensions = $variation; if ( self::has_usable_dimensions( $dimensions, $usable_dimensions ) ) { $filtered[] = $variation; } } return $filtered; } /** * Filters a list of variations by (disk) file size. * * @param array $variations The variations to consider. * * @return array The validations that pass the required file size limits. */ public static function filter_usable_file_size( $variations ) { foreach ( $variations as $variation ) { // We return early to prevent measuring the file size of all the variations. if ( self::has_usable_file_size( $variation ) ) { return [ $variation ]; } } return []; } /** * Retrieve the internal WP image file sizes. * * @return array An array of image sizes. */ public static function get_sizes() { /** * Filter: 'wpseo_image_sizes' - Determines which image sizes we'll loop through to get an appropriate image. * * @param array<string> $sizes The array of image sizes to loop through. */ return apply_filters( 'wpseo_image_sizes', [ 'full', 'large', 'medium_large' ] ); } /** * Grabs an image alt text. * * @param int $attachment_id The attachment ID. * * @return string The image alt text. */ public static function get_alt_tag( $attachment_id ) { return (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); } /** * Checks whether an img sizes up to the parameters. * * @param array $dimensions The image values. * @param array $usable_dimensions The parameters to check against. * * @return bool True if the image has usable measurements, false if not. */ private static function has_usable_dimensions( $dimensions, $usable_dimensions ) { foreach ( [ 'width', 'height' ] as $param ) { $minimum = $usable_dimensions[ 'min_' . $param ]; $maximum = $usable_dimensions[ 'max_' . $param ]; $current = $dimensions[ $param ]; if ( ( $current < $minimum ) || ( $current > $maximum ) ) { return false; } } return true; } /** * Gets the post's first usable content image. Null if none is available. * * @param int|null $post_id The post id. * * @return string|null The image URL. */ public static function get_first_usable_content_image_for_post( $post_id = null ) { $post = get_post( $post_id ); // We know get_post() returns the post or null. if ( ! $post ) { return null; } $image_finder = new WPSEO_Content_Images(); $images = $image_finder->get_images( $post->ID, $post ); return self::get_first_image( $images ); } /** * Gets the term's first usable content image. Null if none is available. * * @param int $term_id The term id. * * @return string|null The image URL. */ public static function get_first_content_image_for_term( $term_id ) { $term_description = term_description( $term_id ); // We know term_description() returns a string which may be empty. if ( $term_description === '' ) { return null; } $image_finder = new WPSEO_Content_Images(); $images = $image_finder->get_images_from_content( $term_description ); return self::get_first_image( $images ); } /** * Retrieves an attachment ID for an image uploaded in the settings. * * Due to self::get_attachment_by_url returning 0 instead of false. * 0 is also a possibility when no ID is available. * * @param string $setting The setting the image is stored in. * * @return int|bool The attachment id, or false or 0 if no ID is available. */ public static function get_attachment_id_from_settings( $setting ) { $image_id = WPSEO_Options::get( $setting . '_id', false ); if ( $image_id ) { return $image_id; } $image = WPSEO_Options::get( $setting, false ); if ( $image ) { // There is not an option to put a URL in an image field in the settings anymore, only to upload it through the media manager. // This means an attachment always exists, so doing this is only needed once. $image_id = self::get_attachment_by_url( $image ); } // Only store a new ID if it is not 0, to prevent an update loop. if ( $image_id ) { WPSEO_Options::set( $setting . '_id', $image_id ); } return $image_id; } /** * Retrieves the first possible image url from an array of images. * * @param array $images The array to extract image url from. * * @return string|null The extracted image url when found, null when not found. */ protected static function get_first_image( $images ) { if ( ! is_array( $images ) ) { return null; } $images = array_filter( $images ); if ( empty( $images ) ) { return null; } return reset( $images ); } } date-helper.php 0000644 00000003330 14720701066 0007451 0 ustar 00 <?php /** * Date helper class. * * @package WPSEO\Internals */ /** * Class WPSEO_Date_Helper * * Note: Move this class with namespace to the src/helpers directory and add a class_alias for BC. */ class WPSEO_Date_Helper { /** * Formats a given date in UTC TimeZone format. * * @param string $date String representing the date / time. * @param string $format The format that the passed date should be in. * * @return string The formatted date. */ public function format( $date, $format = DATE_W3C ) { return YoastSEO()->helpers->date->format( $date, $format ); } /** * Formats the given timestamp to the needed format. * * @param int $timestamp The timestamp to use for the formatting. * @param string $format The format that the passed date should be in. * * @return string The formatted date. */ public function format_timestamp( $timestamp, $format = DATE_W3C ) { return YoastSEO()->helpers->date->format_timestamp( $timestamp, $format ); } /** * Formats a given date in UTC TimeZone format and translate it to the set language. * * @param string $date String representing the date / time. * @param string $format The format that the passed date should be in. * * @return string The formatted and translated date. */ public function format_translated( $date, $format = DATE_W3C ) { return YoastSEO()->helpers->date->format_translated( $date, $format ); } /** * Check if a string is a valid datetime. * * @param string $datetime String input to check as valid input for DateTime class. * * @return bool True when datatime is valid. */ public function is_valid_datetime( $datetime ) { return YoastSEO()->helpers->date->is_valid_datetime( $datetime ); } } class-yoast-dynamic-rewrites.php 0000644 00000012361 14720701066 0013011 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class containing an alternative rewrite rules API for handling them dynamically without requiring flushing rules. */ class Yoast_Dynamic_Rewrites implements WPSEO_WordPress_Integration { /** * Additional rewrite rules with high priority. * * @var array */ protected $extra_rules_top = []; /** * Additional rewrite rules with low priority. * * @var array */ protected $extra_rules_bottom = []; /** * Main instance holder. * * @var self|null */ protected static $instance = null; /** * WP_Rewrite instance to use. * * @var WP_Rewrite */ public $wp_rewrite; /** * Gets the main instance of the class. * * @return self Dynamic rewrites main instance. */ public static function instance() { if ( self::$instance === null ) { self::$instance = new self(); self::$instance->register_hooks(); } return self::$instance; } /** * Constructor. * * Sets the WP_Rewrite instance to use. * * @param WP_Rewrite|null $rewrite Optional. WP_Rewrite instance to use. Default is the $wp_rewrite global. * @throws RuntimeException Throws an exception if the $wp_rewrite global is not set. */ public function __construct( $rewrite = null ) { if ( ! $rewrite ) { if ( empty( $GLOBALS['wp_rewrite'] ) ) { /* translators: 1: PHP class name, 2: PHP variable name */ throw new RuntimeException( sprintf( __( 'The %1$s class must not be instantiated before the %2$s global is set.', 'wordpress-seo' ), self::class, '$wp_rewrite' ) ); } $rewrite = $GLOBALS['wp_rewrite']; } $this->wp_rewrite = $rewrite; } /** * Registers all necessary hooks with WordPress. * * @return void */ public function register_hooks() { add_action( 'init', [ $this, 'trigger_dynamic_rewrite_rules_hook' ], 1 ); add_filter( 'option_rewrite_rules', [ $this, 'filter_rewrite_rules_option' ] ); add_filter( 'sanitize_option_rewrite_rules', [ $this, 'sanitize_rewrite_rules_option' ] ); } /** * Adds a dynamic rewrite rule that transforms a URL structure to a set of query vars. * * Rules registered with this method are applied dynamically and do not require the rewrite rules * to be flushed in order to become active, which is a benefit over the regular WordPress core API. * Note however that the dynamic application only works for rules that correspond to index.php. * Non-WordPress rewrite rules still require flushing. * * Any value in the $after parameter that isn't 'bottom' will result in the rule * being placed at the top of the rewrite rules. * * @param string $regex Regular expression to match request against. * @param string|array $query The corresponding query vars for this rewrite rule. * @param string $priority Optional. Priority of the new rule. Accepts 'top' * or 'bottom'. Default 'bottom'. * * @return void */ public function add_rule( $regex, $query, $priority = 'bottom' ) { if ( is_array( $query ) ) { $query = add_query_arg( $query, 'index.php' ); } $this->wp_rewrite->add_rule( $regex, $query, $priority ); // Do not further handle external rules. if ( substr( $query, 0, strlen( $this->wp_rewrite->index . '?' ) ) !== $this->wp_rewrite->index . '?' ) { return; } if ( $priority === 'bottom' ) { $this->extra_rules_bottom[ $regex ] = $query; return; } $this->extra_rules_top[ $regex ] = $query; } /** * Triggers the hook on which rewrite rules should be added. * * This allows for a more specific point in time from the generic `init` hook where this is * otherwise handled. * * @return void */ public function trigger_dynamic_rewrite_rules_hook() { /** * Fires when the plugin's dynamic rewrite rules should be added. * * @param self $dynamic_rewrites Dynamic rewrites handler instance. Use its `add_rule()` method * to add dynamic rewrite rules. */ do_action( 'yoast_add_dynamic_rewrite_rules', $this ); } /** * Filters the rewrite rules option to dynamically add additional rewrite rules. * * @param array|string $rewrite_rules Array of rewrite rule $regex => $query pairs, or empty string * if currently not set. * * @return array|string Filtered value of $rewrite_rules. */ public function filter_rewrite_rules_option( $rewrite_rules ) { // Do not add extra rewrite rules if the rules need to be flushed. if ( empty( $rewrite_rules ) ) { return $rewrite_rules; } return array_merge( $this->extra_rules_top, $rewrite_rules, $this->extra_rules_bottom ); } /** * Sanitizes the rewrite rules option prior to writing it to the database. * * This method ensures that the dynamic rewrite rules do not become part of the actual option. * * @param array|string $rewrite_rules Array pf rewrite rule $regex => $query pairs, or empty string * in order to unset. * * @return array|string Filtered value of $rewrite_rules before writing the option. */ public function sanitize_rewrite_rules_option( $rewrite_rules ) { if ( empty( $rewrite_rules ) ) { return $rewrite_rules; } return array_diff_key( $rewrite_rules, $this->extra_rules_top, $this->extra_rules_bottom ); } } language-utils.php 0000644 00000002441 14720701066 0010202 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 5.9.0 */ /** * Group of language utility methods for use by WPSEO. * All methods are static, this is just a sort of namespacing class wrapper. */ class WPSEO_Language_Utils { /** * Returns the language part of a given locale, defaults to english when the $locale is empty. * * @param string|null $locale The locale to get the language of. * * @return string The language part of the locale. */ public static function get_language( $locale = null ) { $language = 'en'; if ( empty( $locale ) || ! is_string( $locale ) ) { return $language; } $locale_parts = explode( '_', $locale ); if ( ! empty( $locale_parts[0] ) && ( strlen( $locale_parts[0] ) === 2 || strlen( $locale_parts[0] ) === 3 ) ) { $language = $locale_parts[0]; } return $language; } /** * Returns the full name for the sites' language. * * @return string The language name. */ public static function get_site_language_name() { require_once ABSPATH . 'wp-admin/includes/translation-install.php'; $translations = wp_get_available_translations(); $locale = get_locale(); $language = isset( $translations[ $locale ] ) ? $translations[ $locale ]['native_name'] : 'English (US)'; return $language; } } exceptions/class-myyoast-bad-request-exception.php 0000644 00000000273 14720701066 0016461 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class WPSEO_MyYoast_Bad_Request_Exception. */ class WPSEO_MyYoast_Bad_Request_Exception extends Exception { } exceptions/class-myyoast-invalid-json-exception.php 0000644 00000000327 14720701066 0016642 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class WPSEO_MyYoast_Invalid_JSON_Exception. */ class WPSEO_MyYoast_Invalid_JSON_Exception extends WPSEO_MyYoast_Bad_Request_Exception { } options/class-wpseo-option-titles.php 0000644 00000104506 14720701066 0014031 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ use Yoast\WP\SEO\Config\Schema_Types; /** * Option: wpseo_titles. */ class WPSEO_Option_Titles extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_titles'; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * {@internal Note: Some of the default values are added via the translate_defaults() method.}} * * @var string[] */ protected $defaults = [ // Form fields. 'forcerewritetitle' => false, 'separator' => 'sc-dash', 'title-home-wpseo' => '%%sitename%% %%page%% %%sep%% %%sitedesc%%', // Text field. 'title-author-wpseo' => '', // Text field. 'title-archive-wpseo' => '%%date%% %%page%% %%sep%% %%sitename%%', // Text field. 'title-search-wpseo' => '', // Text field. 'title-404-wpseo' => '', // Text field. 'social-title-author-wpseo' => '%%name%%', // Text field. 'social-title-archive-wpseo' => '%%date%%', // Text field. 'social-description-author-wpseo' => '', // Text area. 'social-description-archive-wpseo' => '', // Text area. 'social-image-url-author-wpseo' => '', // Hidden input field. 'social-image-url-archive-wpseo' => '', // Hidden input field. 'social-image-id-author-wpseo' => 0, // Hidden input field. 'social-image-id-archive-wpseo' => 0, // Hidden input field. 'metadesc-home-wpseo' => '', // Text area. 'metadesc-author-wpseo' => '', // Text area. 'metadesc-archive-wpseo' => '', // Text area. 'rssbefore' => '', // Text area. 'rssafter' => '', // Text area. 'noindex-author-wpseo' => false, 'noindex-author-noposts-wpseo' => true, 'noindex-archive-wpseo' => true, 'disable-author' => false, 'disable-date' => false, 'disable-post_format' => false, 'disable-attachment' => true, 'breadcrumbs-404crumb' => '', // Text field. 'breadcrumbs-display-blog-page' => true, 'breadcrumbs-boldlast' => false, 'breadcrumbs-archiveprefix' => '', // Text field. 'breadcrumbs-enable' => true, 'breadcrumbs-home' => '', // Text field. 'breadcrumbs-prefix' => '', // Text field. 'breadcrumbs-searchprefix' => '', // Text field. 'breadcrumbs-sep' => '»', // Text field. 'website_name' => '', 'person_name' => '', 'person_logo' => '', 'person_logo_id' => 0, 'alternate_website_name' => '', 'company_logo' => '', 'company_logo_id' => 0, 'company_logo_meta' => false, 'person_logo_meta' => false, 'company_name' => '', 'company_alternate_name' => '', 'company_or_person' => 'company', 'company_or_person_user_id' => false, 'stripcategorybase' => false, 'open_graph_frontpage_title' => '%%sitename%%', // Text field. 'open_graph_frontpage_desc' => '', // Text field. 'open_graph_frontpage_image' => '', // Text field. 'open_graph_frontpage_image_id' => 0, 'publishing_principles_id' => 0, 'ownership_funding_info_id' => 0, 'actionable_feedback_policy_id' => 0, 'corrections_policy_id' => 0, 'ethics_policy_id' => 0, 'diversity_policy_id' => 0, 'diversity_staffing_report_id' => 0, 'org-description' => '', 'org-email' => '', 'org-phone' => '', 'org-legal-name' => '', 'org-founding-date' => '', 'org-number-employees' => '', 'org-vat-id' => '', 'org-tax-id' => '', 'org-iso' => '', 'org-duns' => '', 'org-leicode' => '', 'org-naics' => '', /* * Uses enrich_defaults to add more along the lines of: * - 'title-' . $pt->name => ''; // Text field. * - 'metadesc-' . $pt->name => ''; // Text field. * - 'noindex-' . $pt->name => false; * - 'display-metabox-pt-' . $pt->name => false; * * - 'title-ptarchive-' . $pt->name => ''; // Text field. * - 'metadesc-ptarchive-' . $pt->name => ''; // Text field. * - 'bctitle-ptarchive-' . $pt->name => ''; // Text field. * - 'noindex-ptarchive-' . $pt->name => false; * * - 'title-tax-' . $tax->name => '''; // Text field. * - 'metadesc-tax-' . $tax->name => ''; // Text field. * - 'noindex-tax-' . $tax->name => false; * - 'display-metabox-tax-' . $tax->name => false; * * - 'schema-page-type-' . $pt->name => 'WebPage'; * - 'schema-article-type-' . $pt->name => 'Article'; */ ]; /** * Used for "caching" during pageload. * * @var string[] */ protected $enriched_defaults = null; /** * Array of variable option name patterns for the option. * * @var string[] */ protected $variable_array_key_patterns = [ 'title-', 'metadesc-', 'noindex-', 'display-metabox-pt-', 'bctitle-ptarchive-', 'post_types-', 'taxonomy-', 'schema-page-type-', 'schema-article-type-', 'social-title-', 'social-description-', 'social-image-url-', 'social-image-id-', 'org-', ]; /** * Array of sub-options which should not be overloaded with multi-site defaults. * * @var string[] */ public $ms_exclude = [ 'forcerewritetitle', ]; /** * Add the actions and filters for the option. * * @todo [JRF => testers] Check if the extra actions below would run into problems if an option * is updated early on and if so, change the call to schedule these for a later action on add/update * instead of running them straight away. */ protected function __construct() { parent::__construct(); add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] ); add_action( 'init', [ $this, 'end_of_init' ], 999 ); add_action( 'registered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_action( 'unregistered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_action( 'registered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_action( 'unregistered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); } /** * Make sure we can recognize the right action for the double cleaning. * * @return void */ public function end_of_init() { do_action( 'wpseo_double_clean_titles' ); } /** * Get the singleton instance of this class. * * @return self */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Get the available separator options. * * @return string[] */ public function get_separator_options() { $separators = wp_list_pluck( self::get_separator_option_list(), 'option' ); /** * Allow altering the array with separator options. * * @param array $separator_options Array with the separator options. */ $filtered_separators = apply_filters( 'wpseo_separator_options', $separators ); if ( is_array( $filtered_separators ) && $filtered_separators !== [] ) { $separators = array_merge( $separators, $filtered_separators ); } return $separators; } /** * Get the available separator options aria-labels. * * @return string[] Array with the separator options aria-labels. */ public function get_separator_options_for_display() { $separators = $this->get_separator_options(); $separator_list = self::get_separator_option_list(); $separator_options = []; foreach ( $separators as $key => $label ) { $aria_label = ( $separator_list[ $key ]['label'] ?? '' ); $separator_options[ $key ] = [ 'label' => $label, 'aria_label' => $aria_label, ]; } return $separator_options; } /** * Translate strings used in the option defaults. * * @return void */ public function translate_defaults() { /* translators: 1: Author name; 2: Site name. */ $this->defaults['title-author-wpseo'] = sprintf( __( '%1$s, Author at %2$s', 'wordpress-seo' ), '%%name%%', '%%sitename%%' ) . ' %%page%% '; /* translators: %s expands to the search phrase. */ $this->defaults['title-search-wpseo'] = sprintf( __( 'You searched for %s', 'wordpress-seo' ), '%%searchphrase%%' ) . ' %%page%% %%sep%% %%sitename%%'; $this->defaults['title-404-wpseo'] = __( 'Page not found', 'wordpress-seo' ) . ' %%sep%% %%sitename%%'; /* translators: 1: link to post; 2: link to blog. */ $this->defaults['rssafter'] = sprintf( __( 'The post %1$s appeared first on %2$s.', 'wordpress-seo' ), '%%POSTLINK%%', '%%BLOGLINK%%' ); $this->defaults['breadcrumbs-404crumb'] = __( 'Error 404: Page not found', 'wordpress-seo' ); $this->defaults['breadcrumbs-archiveprefix'] = __( 'Archives for', 'wordpress-seo' ); $this->defaults['breadcrumbs-home'] = __( 'Home', 'wordpress-seo' ); $this->defaults['breadcrumbs-searchprefix'] = __( 'You searched for', 'wordpress-seo' ); } /** * Add dynamically created default options based on available post types and taxonomies. * * @return void */ public function enrich_defaults() { $enriched_defaults = $this->enriched_defaults; if ( $enriched_defaults !== null ) { $this->defaults += $enriched_defaults; return; } $enriched_defaults = []; /* * Retrieve all the relevant post type and taxonomy arrays. * * WPSEO_Post_Type::get_accessible_post_types() should *not* be used here. * These are the defaults and can be prepared for any public post type. */ $post_type_objects = get_post_types( [ 'public' => true ], 'objects' ); if ( $post_type_objects ) { /* translators: %s expands to the name of a post type (plural). */ $archive = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' ); foreach ( $post_type_objects as $pt ) { $enriched_defaults[ 'title-' . $pt->name ] = '%%title%% %%page%% %%sep%% %%sitename%%'; // Text field. $enriched_defaults[ 'metadesc-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'noindex-' . $pt->name ] = false; $enriched_defaults[ 'display-metabox-pt-' . $pt->name ] = true; $enriched_defaults[ 'post_types-' . $pt->name . '-maintax' ] = 0; // Select box. $enriched_defaults[ 'schema-page-type-' . $pt->name ] = 'WebPage'; $enriched_defaults[ 'schema-article-type-' . $pt->name ] = ( $pt->name === 'post' ) ? 'Article' : 'None'; if ( $pt->name !== 'attachment' ) { $enriched_defaults[ 'social-title-' . $pt->name ] = '%%title%%'; // Text field. $enriched_defaults[ 'social-description-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'social-image-url-' . $pt->name ] = ''; // Hidden input field. $enriched_defaults[ 'social-image-id-' . $pt->name ] = 0; // Hidden input field. } // Custom post types that have archives. if ( ! $pt->_builtin && WPSEO_Post_Type::has_archive( $pt ) ) { $enriched_defaults[ 'title-ptarchive-' . $pt->name ] = $archive . ' %%page%% %%sep%% %%sitename%%'; // Text field. $enriched_defaults[ 'metadesc-ptarchive-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'bctitle-ptarchive-' . $pt->name ] = ''; // Text field. $enriched_defaults[ 'noindex-ptarchive-' . $pt->name ] = false; $enriched_defaults[ 'social-title-ptarchive-' . $pt->name ] = $archive; // Text field. $enriched_defaults[ 'social-description-ptarchive-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'social-image-url-ptarchive-' . $pt->name ] = ''; // Hidden input field. $enriched_defaults[ 'social-image-id-ptarchive-' . $pt->name ] = 0; // Hidden input field. } } } $taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' ); if ( $taxonomy_objects ) { /* translators: %s expands to the variable used for term title. */ $archives = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' ); foreach ( $taxonomy_objects as $tax ) { $enriched_defaults[ 'title-tax-' . $tax->name ] = $archives . ' %%page%% %%sep%% %%sitename%%'; // Text field. $enriched_defaults[ 'metadesc-tax-' . $tax->name ] = ''; // Text area. $enriched_defaults[ 'display-metabox-tax-' . $tax->name ] = true; $enriched_defaults[ 'noindex-tax-' . $tax->name ] = ( $tax->name === 'post_format' ); $enriched_defaults[ 'social-title-tax-' . $tax->name ] = $archives; // Text field. $enriched_defaults[ 'social-description-tax-' . $tax->name ] = ''; // Text area. $enriched_defaults[ 'social-image-url-tax-' . $tax->name ] = ''; // Hidden input field. $enriched_defaults[ 'social-image-id-tax-' . $tax->name ] = 0; // Hidden input field. $enriched_defaults[ 'taxonomy-' . $tax->name . '-ptparent' ] = 0; // Select box;. } } $this->enriched_defaults = $enriched_defaults; $this->defaults += $enriched_defaults; } /** * Invalidates enrich_defaults() cache. * * Called from actions: * - (un)registered_post_type * - (un)registered_taxonomy * * @return void */ public function invalidate_enrich_defaults_cache() { $this->enriched_defaults = null; } /** * Validate the option. * * @param string[] $dirty New value for the option. * @param string[] $clean Clean value for the option, normally the defaults. * @param string[] $old Old value of the option. * * @return string[] Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { $allowed_post_types = $this->get_allowed_post_types(); foreach ( $clean as $key => $value ) { $switch_key = $this->get_switch_key( $key ); switch ( $switch_key ) { // Only ever set programmatically, so no reason for intense validation. case 'company_logo_meta': case 'person_logo_meta': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } break; /* Breadcrumbs text fields. */ case 'breadcrumbs-404crumb': case 'breadcrumbs-archiveprefix': case 'breadcrumbs-home': case 'breadcrumbs-prefix': case 'breadcrumbs-searchprefix': case 'breadcrumbs-sep': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = wp_kses_post( $dirty[ $key ] ); } break; /* * Text fields. */ /* * Covers: * 'title-home-wpseo', 'title-author-wpseo', 'title-archive-wpseo', // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code. * 'title-search-wpseo', 'title-404-wpseo' * 'title-' . $pt->name * 'title-ptarchive-' . $pt->name * 'title-tax-' . $tax->name * 'social-title-' . $pt->name * 'social-title-ptarchive-' . $pt->name * 'social-title-tax-' . $tax->name * 'social-title-author-wpseo', 'social-title-archive-wpseo' * 'open_graph_frontpage_title' */ case 'org-': case 'website_name': case 'alternate_website_name': case 'title-': case 'social-title-': case 'open_graph_frontpage_title': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] ); } break; case 'company_or_person': if ( isset( $dirty[ $key ] ) ) { if ( in_array( $dirty[ $key ], [ 'company', 'person' ], true ) ) { $clean[ $key ] = $dirty[ $key ]; } else { $defaults = $this->get_defaults(); $clean[ $key ] = $defaults['company_or_person']; } } break; /* * Covers: * 'company_logo', 'person_logo' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code. */ case 'company_logo': case 'person_logo': case 'open_graph_frontpage_image': // When a logo changes, we need to ditch the caches we have for it. unset( $clean[ $switch_key . '_id' ] ); unset( $clean[ $switch_key . '_meta' ] ); $this->validate_url( $key, $dirty, $old, $clean ); break; /* * Covers: * 'social-image-url-' . $pt->name * 'social-image-url-ptarchive-' . $pt->name * 'social-image-url-tax-' . $tax->name * 'social-image-url-author-wpseo', 'social-image-url-archive-wpseo' */ case 'social-image-url-': $this->validate_url( $key, $dirty, $old, $clean ); break; /* * Covers: * 'metadesc-home-wpseo', 'metadesc-author-wpseo', 'metadesc-archive-wpseo' * 'metadesc-' . $pt->name * 'metadesc-ptarchive-' . $pt->name * 'metadesc-tax-' . $tax->name * and also: * 'bctitle-ptarchive-' . $pt->name * 'social-description-' . $pt->name * 'social-description-ptarchive-' . $pt->name * 'social-description-tax-' . $tax->name * 'social-description-author-wpseo', 'social-description-archive-wpseo' * 'open_graph_frontpage_desc' */ case 'metadesc-': case 'bctitle-ptarchive-': case 'company_name': case 'company_alternate_name': case 'person_name': case 'social-description-': case 'open_graph_frontpage_desc': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] ); } break; /* * Covers: 'rssbefore', 'rssafter' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code. */ case 'rssbefore': case 'rssafter': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = wp_kses_post( $dirty[ $key ] ); } break; /* 'post_types-' . $pt->name . '-maintax' fields. */ case 'post_types-': $post_type = str_replace( [ 'post_types-', '-maintax' ], '', $key ); $taxonomies = get_object_taxonomies( $post_type, 'names' ); if ( isset( $dirty[ $key ] ) ) { if ( $taxonomies !== [] && in_array( $dirty[ $key ], $taxonomies, true ) ) { $clean[ $key ] = $dirty[ $key ]; } elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) { $clean[ $key ] = 0; } elseif ( sanitize_title_with_dashes( $dirty[ $key ] ) === $dirty[ $key ] ) { // Allow taxonomies which may not be registered yet. $clean[ $key ] = $dirty[ $key ]; } else { if ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] ); } /* * @todo [JRF => whomever] Maybe change the untranslated $pt name in the * error message to the nicely translated label ? */ add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-id for the error message box. /* translators: %s expands to a post type. */ sprintf( __( 'Please select a valid taxonomy for post type "%s"', 'wordpress-seo' ), $post_type ), // The error message. 'error' // Message type. ); } } elseif ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] ); } unset( $taxonomies, $post_type ); break; /* 'taxonomy-' . $tax->name . '-ptparent' fields. */ case 'taxonomy-': if ( isset( $dirty[ $key ] ) ) { if ( $allowed_post_types !== [] && in_array( $dirty[ $key ], $allowed_post_types, true ) ) { $clean[ $key ] = $dirty[ $key ]; } elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) { $clean[ $key ] = 0; } elseif ( sanitize_key( $dirty[ $key ] ) === $dirty[ $key ] ) { // Allow taxonomies which may not be registered yet. $clean[ $key ] = $dirty[ $key ]; } else { if ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_key( $old[ $key ] ); } /* * @todo [JRF =? whomever] Maybe change the untranslated $tax name in the * error message to the nicely translated label ? */ $tax = str_replace( [ 'taxonomy-', '-ptparent' ], '', $key ); add_settings_error( $this->group_name, // Slug title of the setting. '_' . $tax, // Suffix-ID for the error message box. /* translators: %s expands to a taxonomy slug. */ sprintf( __( 'Please select a valid post type for taxonomy "%s"', 'wordpress-seo' ), $tax ), // The error message. 'error' // Message type. ); unset( $tax ); } } elseif ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_key( $old[ $key ] ); } break; /* * Covers: * 'company_or_person_user_id' * 'company_logo_id', 'person_logo_id', 'open_graph_frontpage_image_id' * 'social-image-id-' . $pt->name * 'social-image-id-ptarchive-' . $pt->name * 'social-image-id-tax-' . $tax->name * 'social-image-id-author-wpseo', 'social-image-id-archive-wpseo' */ case 'company_or_person_user_id': case 'company_logo_id': case 'person_logo_id': case 'social-image-id-': case 'open_graph_frontpage_image_id': case 'publishing_principles_id': case 'ownership_funding_info_id': case 'actionable_feedback_policy_id': case 'corrections_policy_id': case 'ethics_policy_id': case 'diversity_policy_id': case 'diversity_staffing_report_id': if ( isset( $dirty[ $key ] ) ) { $int = WPSEO_Utils::validate_int( $dirty[ $key ] ); if ( $int !== false && $int >= 0 ) { $clean[ $key ] = $int; } } elseif ( isset( $old[ $key ] ) ) { $int = WPSEO_Utils::validate_int( $old[ $key ] ); if ( $int !== false && $int >= 0 ) { $clean[ $key ] = $int; } } break; /* Separator field - Radio. */ case 'separator': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { // Get separator fields. $separator_fields = $this->get_separator_options(); // Check if the given separator exists. if ( isset( $separator_fields[ $dirty[ $key ] ] ) ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'schema-page-type-': if ( isset( $dirty[ $key ] ) && is_string( $dirty[ $key ] ) ) { if ( array_key_exists( $dirty[ $key ], Schema_Types::PAGE_TYPES ) ) { $clean[ $key ] = $dirty[ $key ]; } else { $defaults = $this->get_defaults(); $post_type = str_replace( $switch_key, '', $key ); $clean[ $key ] = $defaults[ $switch_key . $post_type ]; } } break; case 'schema-article-type-': if ( isset( $dirty[ $key ] ) && is_string( $dirty[ $key ] ) ) { /** * Filter: 'wpseo_schema_article_types' - Allow developers to filter the available article types. * * Make sure when you filter this to also filter `wpseo_schema_article_types_labels`. * * @param array $schema_article_types The available schema article types. */ if ( array_key_exists( $dirty[ $key ], apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ) ) ) { $clean[ $key ] = $dirty[ $key ]; } else { $defaults = $this->get_defaults(); $post_type = str_replace( $switch_key, '', $key ); $clean[ $key ] = $defaults[ $switch_key . $post_type ]; } } break; /* * Boolean fields. */ /* * Covers: * 'noindex-author-wpseo', 'noindex-author-noposts-wpseo', 'noindex-archive-wpseo' * 'noindex-' . $pt->name * 'noindex-ptarchive-' . $pt->name * 'noindex-tax-' . $tax->name * 'forcerewritetitle': * 'noodp': * 'noydir': * 'disable-author': * 'disable-date': * 'disable-post_format'; * 'noindex-' * 'display-metabox-pt-' * 'display-metabox-pt-'. $pt->name * 'display-metabox-tax-' * 'display-metabox-tax-' . $tax->name * 'breadcrumbs-display-blog-page' * 'breadcrumbs-boldlast' * 'breadcrumbs-enable' * 'stripcategorybase' */ default: $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; } } return $clean; } /** * Retrieve a list of the allowed post types as breadcrumb parent for a taxonomy. * Helper method for validation. * * {@internal Don't make static as new types may still be registered.}} * * @return string[] */ protected function get_allowed_post_types() { $allowed_post_types = []; /* * WPSEO_Post_Type::get_accessible_post_types() should *not* be used here. */ $post_types = get_post_types( [ 'public' => true ], 'objects' ); if ( get_option( 'show_on_front' ) === 'page' && get_option( 'page_for_posts' ) > 0 ) { $allowed_post_types[] = 'post'; } if ( is_array( $post_types ) && $post_types !== [] ) { foreach ( $post_types as $type ) { if ( WPSEO_Post_Type::has_archive( $type ) ) { $allowed_post_types[] = $type->name; } } } return $allowed_post_types; } /** * Clean a given option value. * * @param string[] $option_value Old (not merged with defaults or filtered) option value to clean according to the rules for this option. * @param string[]|null $current_version Optional. Version from which to upgrade, if not set, version specific upgrades will be disregarded. * @param string[]|null $all_old_option_values Optional. Only used when importing old options to have access to the real old values, in contrast to the saved ones. * * @return string[] Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { static $original = null; // Double-run this function to ensure renaming of the taxonomy options will work. if ( ! isset( $original ) && has_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] ) === false ) { add_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] ); $original = $option_value; } /* * Move options from very old option to this one. * * {@internal Don't rename to the 'current' names straight away as that would prevent * the rename/unset combi below from working.}} * * @todo [JRF] Maybe figure out a smarter way to deal with this. */ $old_option = null; if ( isset( $all_old_option_values ) ) { // Ok, we have an import. if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) { $old_option = $all_old_option_values['wpseo_indexation']; } } else { $old_option = get_option( 'wpseo_indexation' ); } if ( is_array( $old_option ) && $old_option !== [] ) { $move = [ 'noindexauthor' => 'noindex-author', 'disableauthor' => 'disable-author', 'noindexdate' => 'noindex-archive', 'noindexcat' => 'noindex-category', 'noindextag' => 'noindex-post_tag', 'noindexpostformat' => 'noindex-post_format', ]; foreach ( $move as $old => $new ) { if ( isset( $old_option[ $old ] ) && ! isset( $option_value[ $new ] ) ) { $option_value[ $new ] = $old_option[ $old ]; } } unset( $move, $old, $new ); } unset( $old_option ); // Fix wrongness created by buggy version 1.2.2. if ( isset( $option_value['title-home'] ) && $option_value['title-home'] === '%%sitename%% - %%sitedesc%% - 12345' ) { $option_value['title-home-wpseo'] = '%%sitename%% - %%sitedesc%%'; } /* * Renaming these options to avoid ever overwritting these if a (bloody stupid) user / * programmer would use any of the following as a custom post type or custom taxonomy: * 'home', 'author', 'archive', 'search', '404', 'subpages'. * * Similarly, renaming the tax options to avoid a custom post type and a taxonomy * with the same name occupying the same option. */ $rename = [ 'title-home' => 'title-home-wpseo', 'title-author' => 'title-author-wpseo', 'title-archive' => 'title-archive-wpseo', 'title-search' => 'title-search-wpseo', 'title-404' => 'title-404-wpseo', 'metadesc-home' => 'metadesc-home-wpseo', 'metadesc-author' => 'metadesc-author-wpseo', 'metadesc-archive' => 'metadesc-archive-wpseo', 'noindex-author' => 'noindex-author-wpseo', 'noindex-archive' => 'noindex-archive-wpseo', ]; foreach ( $rename as $old => $new ) { if ( isset( $option_value[ $old ] ) && ! isset( $option_value[ $new ] ) ) { $option_value[ $new ] = $option_value[ $old ]; unset( $option_value[ $old ] ); } } unset( $rename, $old, $new ); /* * {@internal This clean-up action can only be done effectively once the taxonomies * and post_types have been registered, i.e. at the end of the init action.}} */ if ( ( isset( $original ) && current_filter() === 'wpseo_double_clean_titles' ) || did_action( 'wpseo_double_clean_titles' ) > 0 ) { $rename = [ 'title-' => 'title-tax-', 'metadesc-' => 'metadesc-tax-', 'noindex-' => 'noindex-tax-', 'tax-hideeditbox-' => 'hideeditbox-tax-', ]; $taxonomy_names = get_taxonomies( [ 'public' => true ], 'names' ); $post_type_names = get_post_types( [ 'public' => true ], 'names' ); $defaults = $this->get_defaults(); if ( $taxonomy_names !== [] ) { foreach ( $taxonomy_names as $tax ) { foreach ( $rename as $old_prefix => $new_prefix ) { if ( ( isset( $original[ $old_prefix . $tax ] ) && ! isset( $original[ $new_prefix . $tax ] ) ) && ( ! isset( $option_value[ $new_prefix . $tax ] ) || ( isset( $option_value[ $new_prefix . $tax ] ) && $option_value[ $new_prefix . $tax ] === $defaults[ $new_prefix . $tax ] ) ) ) { $option_value[ $new_prefix . $tax ] = $original[ $old_prefix . $tax ]; /* * Check if there is a cpt with the same name as the tax, * if so, we should make sure that the old setting hasn't been removed. */ if ( ! isset( $post_type_names[ $tax ] ) && isset( $option_value[ $old_prefix . $tax ] ) ) { unset( $option_value[ $old_prefix . $tax ] ); } elseif ( isset( $post_type_names[ $tax ] ) && ! isset( $option_value[ $old_prefix . $tax ] ) ) { $option_value[ $old_prefix . $tax ] = $original[ $old_prefix . $tax ]; } if ( $old_prefix === 'tax-hideeditbox-' ) { unset( $option_value[ $old_prefix . $tax ] ); } } } } } unset( $rename, $taxonomy_names, $post_type_names, $defaults, $tax, $old_prefix, $new_prefix ); } return $option_value; } /** * Make sure that any set option values relating to post_types and/or taxonomies are retained, * even when that post_type or taxonomy may not yet have been registered. * * {@internal Overrule the abstract class version of this to make sure one extra renamed * variable key does not get removed. IMPORTANT: keep this method in line with * the parent on which it is based!}} * * @param string[] $dirty Original option as retrieved from the database. * @param string[] $clean Filtered option where any options which shouldn't be in our option * have already been removed and any options which weren't set * have been set to their defaults. * * @return string[] */ protected function retain_variable_keys( $dirty, $clean ) { if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) { // Add the extra pattern. $patterns = $this->variable_array_key_patterns; $patterns[] = 'tax-hideeditbox-'; /** * Allow altering the array with variable array key patterns. * * @param array $patterns Array with the variable array key patterns. */ $patterns = apply_filters( 'wpseo_option_titles_variable_array_key_patterns', $patterns ); foreach ( $dirty as $key => $value ) { // Do nothing if already in filtered option array. if ( isset( $clean[ $key ] ) ) { continue; } foreach ( $patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { $clean[ $key ] = $value; break; } } } } return $clean; } /** * Retrieves a list of separator options. * * @return string[] An array of the separator options. */ protected static function get_separator_option_list() { $separators = [ 'sc-dash' => [ 'option' => '-', 'label' => __( 'Dash', 'wordpress-seo' ), ], 'sc-ndash' => [ 'option' => '–', 'label' => __( 'En dash', 'wordpress-seo' ), ], 'sc-mdash' => [ 'option' => '—', 'label' => __( 'Em dash', 'wordpress-seo' ), ], 'sc-colon' => [ 'option' => ':', 'label' => __( 'Colon', 'wordpress-seo' ), ], 'sc-middot' => [ 'option' => '·', 'label' => __( 'Middle dot', 'wordpress-seo' ), ], 'sc-bull' => [ 'option' => '•', 'label' => __( 'Bullet', 'wordpress-seo' ), ], 'sc-star' => [ 'option' => '*', 'label' => __( 'Asterisk', 'wordpress-seo' ), ], 'sc-smstar' => [ 'option' => '⋆', 'label' => __( 'Low asterisk', 'wordpress-seo' ), ], 'sc-pipe' => [ 'option' => '|', 'label' => __( 'Vertical bar', 'wordpress-seo' ), ], 'sc-tilde' => [ 'option' => '~', 'label' => __( 'Small tilde', 'wordpress-seo' ), ], 'sc-laquo' => [ 'option' => '«', 'label' => __( 'Left angle quotation mark', 'wordpress-seo' ), ], 'sc-raquo' => [ 'option' => '»', 'label' => __( 'Right angle quotation mark', 'wordpress-seo' ), ], 'sc-lt' => [ 'option' => '>', 'label' => __( 'Less than sign', 'wordpress-seo' ), ], 'sc-gt' => [ 'option' => '<', 'label' => __( 'Greater than sign', 'wordpress-seo' ), ], ]; /** * Allows altering the separator options array. * * @param array $separators Array with the separator options. */ $separator_list = apply_filters( 'wpseo_separator_option_list', $separators ); if ( ! is_array( $separator_list ) ) { return $separators; } return $separator_list; } } options/class-wpseo-option.php 0000644 00000067762 14720701066 0012543 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * This abstract class and its concrete classes implement defaults and value validation for * all WPSEO options and subkeys within options. * * Some guidelines: * [Retrieving options] * - Use the normal get_option() to retrieve an option. You will receive a complete array for the option. * Any subkeys which were not set, will have their default values in place. * - In other words, you will normally not have to check whether a subkey isset() as they will *always* be set. * They will also *always* be of the correct variable type. * The only exception to this are the options with variable option names based on post_type or taxonomy * as those will not always be available before the taxonomy/post_type is registered. * (they will be available if a value was set, they won't be if it wasn't as the class won't know * that a default needs to be injected). * * [Updating/Adding options] * - For multisite site_options, please use the WPSEO_Options::update_site_option() method. * - For normal options, use the normal add/update_option() functions. As long as the classes here * are instantiated, validation for all options and their subkeys will be automatic. * - On (successful) update of a couple of options, certain related actions will be run automatically. * Some examples: * - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly * - on change of wpseo and wpseo_title, some caches will be cleared * * [Important information about add/updating/changing these classes] * - Make sure that option array key names are unique across options. The WPSEO_Options::get_all() * method merges most options together. If any of them have non-unique names, even if they * are in a different option, they *will* overwrite each other. * - When you add a new array key in an option: make sure you add proper defaults and add the key * to the validation routine in the proper place or add a new validation case. * You don't need to do any upgrading as any option returned will always be merged with the * defaults, so new options will automatically be available. * If the default value is a string which need translating, add this to the concrete class * translate_defaults() method. * - When you remove an array key from an option: if it's important that the option is really removed, * add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run. * This will re-save the option and automatically remove the array key no longer in existence. * - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run. * - When you change the default for an option sub-key, make sure you verify that the validation routine will * still work the way it should. * Example: changing a default from '' (empty string) to 'text' with a validation routine with tests * for an empty string will prevent a user from saving an empty string as the real value. So the * test for '' with the validation routine would have to be removed in that case. * - If an option needs specific actions different from defined in this abstract class, you can just overrule * a method by defining it in the concrete class. * * @todo [JRF => testers] Double check that validation will not cause errors when called * from upgrade routine (some of the WP functions may not yet be available). */ abstract class WPSEO_Option { /** * Prefix for override option keys that allow or disallow the option key of the same name. * * @var string */ public const ALLOW_KEY_PREFIX = 'allow_'; /** * Option name - MUST be set in concrete class and set to public. * * @var string */ protected $option_name; /** * Option group name for use in settings forms. * * Will be set automagically if not set in concrete class (i.e. * if it conforms to the normal pattern 'yoast' . $option_name . 'options', * only set in concrete class if it doesn't). * * @var string */ public $group_name; /** * Whether to include the option in the return for WPSEO_Options::get_all(). * * Also determines which options are copied over for ms_(re)set_blog(). * * @var bool */ public $include_in_all = true; /** * Whether this option is only for when the install is multisite. * * @var bool */ public $multisite_only = false; /** * Array of defaults for the option - MUST be set in concrete class. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array */ protected $defaults; /** * Array of variable option name patterns for the option - if any. * * Set this when the option contains array keys which vary based on post_type * or taxonomy. * * @var array */ protected $variable_array_key_patterns; /** * Array of sub-options which should not be overloaded with multi-site defaults. * * @var array */ public $ms_exclude = []; /** * Name for an option higher in the hierarchy to override setting access. * * @var string */ protected $override_option_name; /** * Instance of this class. * * @var WPSEO_Option */ protected static $instance; /* *********** INSTANTIATION METHODS *********** */ /** * Add all the actions and filters for the option. */ protected function __construct() { /* Add filters which get applied to the get_options() results. */ $this->add_default_filters(); // Return defaults if option not set. $this->add_option_filters(); // Merge with defaults if option *is* set. if ( $this->multisite_only !== true ) { /** * The option validation routines remove the default filters to prevent failing * to insert an option if it's new. Let's add them back afterwards. */ add_action( 'add_option', [ $this, 'add_default_filters_if_same_option' ] ); // Adding back after INSERT. add_action( 'update_option', [ $this, 'add_default_filters_if_same_option' ] ); add_filter( 'pre_update_option', [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 ); // Refills the cache when the option has been updated. add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 10 ); } elseif ( is_multisite() ) { /* * The option validation routines remove the default filters to prevent failing * to insert an option if it's new. Let's add them back afterwards. * * For site_options, this method is not foolproof as these actions are not fired * on an insert/update failure. Please use the WPSEO_Options::update_site_option() method * for updating site options to make sure the filters are in place. */ add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] ); add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] ); add_filter( 'pre_update_site_option_' . $this->option_name, [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 ); // Refills the cache when the option has been updated. add_action( 'update_site_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 1, 0 ); } /* * Make sure the option will always get validated, independently of register_setting() * (only available on back-end). */ add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] ); /* Register our option for the admin pages */ add_action( 'admin_init', [ $this, 'register_setting' ] ); /* Set option group name if not given */ if ( ! isset( $this->group_name ) || $this->group_name === '' ) { $this->group_name = 'yoast_' . $this->option_name . '_options'; } /* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */ if ( method_exists( $this, 'translate_defaults' ) ) { add_action( 'init', [ $this, 'translate_defaults' ], 2 ); } /** * Enrich defaults once custom post types and taxonomies have been registered * which is normally done on the init action. * * @todo [JRF/testers] Verify that none of the options which are only available after * enrichment are used before the enriching. */ if ( method_exists( $this, 'enrich_defaults' ) ) { add_action( 'init', [ $this, 'enrich_defaults' ], 99 ); } } /* * All concrete classes *must* contain the get_instance method. * * {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}} * * ``` * abstract protected static function get_instance(); * ``` * --------------- * * Concrete classes *may* contain a translate_defaults method. * ``` * abstract public function translate_defaults(); * ``` * --------------- * * Concrete classes *may* contain an enrich_defaults method to add additional defaults once * all post_types and taxonomies have been registered. * * ``` * abstract public function enrich_defaults(); * ``` */ /* *********** METHODS INFLUENCING get_option() *********** */ /** * Add filters to make sure that the option default is returned if the option is not set. * * @return void */ public function add_default_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) { add_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } } /** * Adds back the default filters that were removed during validation if the option was changed. * Checks if this option was changed to prevent constantly checking if filters are present. * * @param string $option_name The option name. * * @return void */ public function add_default_filters_if_same_option( $option_name ) { if ( $option_name === $this->option_name ) { $this->add_default_filters(); } } /** * Adds back the default filters that were removed during validation if the option was not changed. * This is because in that case the latter actions are not called and thus the filters are never * added back. * * @param mixed $value The current value. * @param string $option_name The option name. * @param mixed $old_value The old value. * * @return string The current value. */ public function add_default_filters_if_not_changed( $value, $option_name, $old_value ) { if ( $option_name !== $this->option_name ) { return $value; } if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) { $this->add_default_filters(); } return $value; } /** * Validate webmaster tools & Pinterest verification strings. * * @param string $key Key to check, by type of service. * @param array $dirty Dirty data with the new values. * @param array $old Old data. * @param array $clean Clean data by reference, normally the default values. * * @return void */ public function validate_verification_string( $key, $dirty, $old, &$clean ) { if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $meta = $dirty[ $key ]; if ( strpos( $meta, 'content=' ) ) { // Make sure we only have the real key, not a complete meta tag. preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match ); if ( isset( $match[2] ) ) { $meta = $match[2]; } unset( $match ); } $meta = sanitize_text_field( $meta ); if ( $meta !== '' ) { $regex = '`^[A-Fa-f0-9_-]+$`'; switch ( $key ) { case 'googleverify': case 'baiduverify': $regex = '`^[A-Za-z0-9_-]+$`'; break; case 'msverify': case 'pinterestverify': case 'yandexverify': break; } if ( preg_match( $regex, $meta ) ) { $clean[ $key ] = $meta; } else { // Restore the previous value, if any. if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) { $clean[ $key ] = $old[ $key ]; } Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $meta ); } } } } /** * Validates an option as a valid URL. Prints out a WordPress settings error * notice if the URL is invalid. * * @param string $key Key to check, by type of URL setting. * @param array $dirty Dirty data with the new values. * @param array $old Old data. * @param array $clean Clean data by reference, normally the default values. * * @return void */ public function validate_url( $key, $dirty, $old, &$clean ) { if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $submitted_url = trim( $dirty[ $key ] ); $validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL ); if ( $validated_url === false ) { // Restore the previous URL value, if any. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) { $url = WPSEO_Utils::sanitize_url( $old[ $key ] ); if ( $url !== '' ) { $clean[ $key ] = $url; } } Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url ); return; } // The URL format is valid, let's sanitize it. $url = WPSEO_Utils::sanitize_url( $validated_url ); if ( $url !== '' ) { $clean[ $key ] = $url; } } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { remove_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } /** * Get the enriched default value for an option. * * Checks if the concrete class contains an enrich_defaults() method and if so, runs it. * * {@internal The enrich_defaults method is used to set defaults for variable array keys * in an option, such as array keys depending on post_types and/or taxonomies.}} * * @return array */ public function get_defaults() { if ( method_exists( $this, 'translate_defaults' ) ) { $this->translate_defaults(); } if ( method_exists( $this, 'enrich_defaults' ) ) { $this->enrich_defaults(); } return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name ); } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) { add_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { remove_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ); } /** * Merge an option with its default values. * * This method should *not* be called directly!!! It is only meant to filter the get_option() results. * * @param mixed $options Option value. * * @return mixed Option merged with the defaults for that option. */ public function get_option( $options = null ) { $filtered = $this->array_filter_merge( $options ); /* * If the option contains variable option keys, make sure we don't remove those settings * - even if the defaults are not complete yet. * Unfortunately this means we also won't be removing the settings for post types or taxonomies * which are no longer in the WP install, but rather that than the other way around. */ if ( isset( $this->variable_array_key_patterns ) ) { $filtered = $this->retain_variable_keys( $options, $filtered ); } return $filtered; } /* *********** METHODS influencing add_option(), update_option() and saving from admin pages. *********** */ /** * Register (whitelist) the option for the configuration pages. * The validation callback is already registered separately on the sanitize_option hook, * so no need to double register. * * @return void */ public function register_setting() { if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) { return; } if ( $this->multisite_only === true ) { $network_settings_api = Yoast_Network_Settings_API::get(); if ( $network_settings_api->meets_requirements() ) { $network_settings_api->register_setting( $this->group_name, $this->option_name ); } return; } register_setting( $this->group_name, $this->option_name ); } /** * Validate the option. * * @param mixed $option_value The unvalidated new value for the option. * * @return array Validated new value for the option. */ public function validate( $option_value ) { $clean = $this->get_defaults(); /* Return the defaults if the new value is empty. */ if ( ! is_array( $option_value ) || $option_value === [] ) { return $clean; } $option_value = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $option_value ); $old = $this->get_original_option(); if ( ! is_array( $old ) ) { $old = []; } $old = array_merge( $clean, $old ); $clean = $this->validate_option( $option_value, $clean, $old ); // Prevent updates to variables that are disabled via the override option. $clean = $this->prevent_disabled_options_update( $clean, $old ); /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */ if ( isset( $this->variable_array_key_patterns ) ) { $clean = $this->retain_variable_keys( $option_value, $clean ); } $this->remove_default_filters(); return $clean; } /** * Checks whether a specific option key is disabled. * * This is determined by whether an override option is available with a key that equals the given key prefixed * with 'allow_'. * * @param string $key Option key. * * @return bool True if option key is disabled, false otherwise. */ public function is_disabled( $key ) { $override_option = $this->get_override_option(); if ( empty( $override_option ) ) { return false; } return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ]; } /** * All concrete classes must contain a validate_option() method which validates all * values within the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. */ abstract protected function validate_option( $dirty, $clean, $old ); /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */ /** * Retrieve the real old value (unmerged with defaults). * * @return array|bool The original option value (which can be false if the option doesn't exist). */ protected function get_original_option() { $this->remove_default_filters(); $this->remove_option_filters(); // Get (unvalidated) array, NOT merged with defaults. if ( $this->multisite_only !== true ) { $option_value = get_option( $this->option_name ); } else { $option_value = get_site_option( $this->option_name ); } $this->add_option_filters(); $this->add_default_filters(); return $option_value; } /** * Add the option if it doesn't exist for some strange reason. * * @uses WPSEO_Option::get_original_option() * * @return void */ public function maybe_add_option() { if ( $this->get_original_option() === false ) { if ( $this->multisite_only !== true ) { update_option( $this->option_name, $this->get_defaults() ); } else { $this->update_site_option( $this->get_defaults() ); } } } /** * Update a site_option. * * {@internal This special method is only needed for multisite options, but very needed indeed there. * The order in which certain functions and hooks are run is different between * get_option() and get_site_option() which means in practice that the removing * of the default filters would be done too late and the re-adding of the default * filters might not be done at all. * Aka: use the WPSEO_Options::update_site_option() method (which calls this method) * for safely adding/updating multisite options.}} * * @param mixed $value The new value for the option. * * @return bool Whether the update was successful. */ public function update_site_option( $value ) { if ( $this->multisite_only === true && is_multisite() ) { $this->remove_default_filters(); $result = update_site_option( $this->option_name, $value ); $this->add_default_filters(); return $result; } else { return false; } } /** * Retrieve the real old value (unmerged with defaults), clean and re-save the option. * * @uses WPSEO_Option::get_original_option() * @uses WPSEO_Option::import() * * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version-specific upgrades will be disregarded. * * @return void */ public function clean( $current_version = null ) { $option_value = $this->get_original_option(); $this->import( $option_value, $current_version ); } /** * Clean and re-save the option. * * @uses clean_option() method from concrete class if it exists. * * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe * something along the lines of: * -> add them to a property in this class * -> if that property isset at the end of the routine and add_settings_error function does not exist, * save as transient (or update the transient if one already exists) * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it * once the admin has dismissed the message (add ajax function) * Important: all validation routines which add_settings_errors would need to be changed for this to work * * @param array $option_value Option value to be imported. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version-specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to * have access to the real old values, in contrast to * the saved ones. * * @return void */ public function import( $option_value, $current_version = null, $all_old_option_values = null ) { if ( $option_value === false ) { $option_value = $this->get_defaults(); } elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) { $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values ); } /* * Save the cleaned value - validation will take care of cleaning out array keys which * should no longer be there. */ if ( $this->multisite_only !== true ) { update_option( $this->option_name, $option_value ); } else { $this->update_site_option( $this->option_name, $option_value ); } } /** * Returns the variable array key patterns for an options class. * * @return array */ public function get_patterns() { return (array) $this->variable_array_key_patterns; } /** * Retrieves the option name. * * @return string The set option name. */ public function get_option_name() { return $this->option_name; } /* * Concrete classes *may* contain a clean_option method which will clean out old/renamed * values within the option. * * ``` * abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null ); * ``` */ /* *********** HELPER METHODS for internal use. *********** */ /** * Helper method - Combines a fixed array of default values with an options array * while filtering out any keys which are not in the defaults array. * * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation * removes any invalid keys on save. * * @param array|null $options Optional. Current options. If not set, the option defaults * for the $option_key will be returned. * * @return array Combined and filtered options array. */ protected function array_filter_merge( $options = null ) { $defaults = $this->get_defaults(); if ( ! isset( $options ) || $options === false || $options === [] ) { return $defaults; } $options = (array) $options; /* $filtered = array(); if ( $defaults !== array() ) { foreach ( $defaults as $key => $default_value ) { // @todo should this walk through array subkeys ? $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value ); } } */ $filtered = array_merge( $defaults, $options ); return $filtered; } /** * Sets updated values for variables that are disabled via the override option back to their previous values. * * @param array $updated Updated option value. * @param array $old Old option value. * * @return array Updated option value, with all disabled variables set to their old values. */ protected function prevent_disabled_options_update( $updated, $old ) { $override_option = $this->get_override_option(); if ( empty( $override_option ) ) { return $updated; } /* * This loop could as well call `is_disabled( $key )` for each iteration, * however this would be worse performance-wise. */ foreach ( $old as $key => $value ) { if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) { $updated[ $key ] = $old[ $key ]; } } return $updated; } /** * Retrieves the value of the override option, if available. * * An override option contains values that may determine access to certain sub-variables * of this option. * * Only regular options in multisite can have override options, which in that case * would be network options. * * @return array Override option value, or empty array if unavailable. */ protected function get_override_option() { if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) { return []; } return get_site_option( $this->override_option_name, [] ); } /** * Make sure that any set option values relating to post_types and/or taxonomies are retained, * even when that post_type or taxonomy may not yet have been registered. * * {@internal The wpseo_titles concrete class overrules this method. Make sure that any * changes applied here, also get ported to that version.}} * * @param array $dirty Original option as retrieved from the database. * @param array $clean Filtered option where any options which shouldn't be in our option * have already been removed and any options which weren't set * have been set to their defaults. * * @return array */ protected function retain_variable_keys( $dirty, $clean ) { if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) { foreach ( $dirty as $key => $value ) { // Do nothing if already in filtered options. if ( isset( $clean[ $key ] ) ) { continue; } foreach ( $this->variable_array_key_patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { $clean[ $key ] = $value; break; } } } } return $clean; } /** * Check whether a given array key conforms to one of the variable array key patterns for this option. * * @used-by validate_option() methods for options with variable array keys. * * @param string $key Array key to check. * * @return string Pattern if it conforms, original array key if it doesn't or if the option * does not have variable array keys. */ protected function get_switch_key( $key ) { if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === [] ) ) { return $key; } foreach ( $this->variable_array_key_patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { return $pattern; } } return $key; } } options/class-wpseo-options.php 0000644 00000042040 14720701066 0012704 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Overall Option Management class. * * Instantiates all the options and offers a number of utility methods to work with the options. */ class WPSEO_Options { /** * The option values. * * @var array|null */ protected static $option_values = null; /** * Options this class uses. * * @var array Array format: (string) option_name => (string) name of concrete class for the option. */ public static $options = [ 'wpseo' => 'WPSEO_Option_Wpseo', 'wpseo_titles' => 'WPSEO_Option_Titles', 'wpseo_social' => 'WPSEO_Option_Social', 'wpseo_ms' => 'WPSEO_Option_MS', 'wpseo_taxonomy_meta' => 'WPSEO_Taxonomy_Meta', ]; /** * Array of instantiated option objects. * * @var array */ protected static $option_instances = []; /** * Array with the option names. * * @var array */ protected static $option_names = []; /** * Instance of this class. * * @var WPSEO_Options */ protected static $instance; /** * Instantiate all the WPSEO option management classes. */ protected function __construct() { $this->register_hooks(); foreach ( static::$options as $option_class ) { static::register_option( call_user_func( [ $option_class, 'get_instance' ] ) ); } } /** * Register our hooks. * * @return void */ public function register_hooks() { add_action( 'registered_taxonomy', [ $this, 'clear_cache' ] ); add_action( 'unregistered_taxonomy', [ $this, 'clear_cache' ] ); add_action( 'registered_post_type', [ $this, 'clear_cache' ] ); add_action( 'unregistered_post_type', [ $this, 'clear_cache' ] ); } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( static::$instance instanceof self ) ) { static::$instance = new self(); } return static::$instance; } /** * Registers an option to the options list. * * @param WPSEO_Option $option_instance Instance of the option. * * @return void */ public static function register_option( WPSEO_Option $option_instance ) { $option_name = $option_instance->get_option_name(); if ( $option_instance->multisite_only && ! static::is_multisite() ) { unset( static::$options[ $option_name ], static::$option_names[ $option_name ] ); return; } $is_already_registered = array_key_exists( $option_name, static::$options ); if ( ! $is_already_registered ) { static::$options[ $option_name ] = get_class( $option_instance ); } if ( $option_instance->include_in_all === true ) { static::$option_names[ $option_name ] = $option_name; } static::$option_instances[ $option_name ] = $option_instance; if ( ! $is_already_registered ) { static::clear_cache(); } } /** * Get the group name of an option for use in the settings form. * * @param string $option_name The option for which you want to retrieve the option group name. * * @return string|bool */ public static function get_group_name( $option_name ) { if ( isset( static::$option_instances[ $option_name ] ) ) { return static::$option_instances[ $option_name ]->group_name; } return false; } /** * Get a specific default value for an option. * * @param string $option_name The option for which you want to retrieve a default. * @param string $key The key within the option who's default you want. * * @return mixed */ public static function get_default( $option_name, $key ) { if ( isset( static::$option_instances[ $option_name ] ) ) { $defaults = static::$option_instances[ $option_name ]->get_defaults(); if ( isset( $defaults[ $key ] ) ) { return $defaults[ $key ]; } } return null; } /** * Update a site_option. * * @param string $option_name The option name of the option to save. * @param mixed $value The new value for the option. * * @return bool */ public static function update_site_option( $option_name, $value ) { if ( is_multisite() && isset( static::$option_instances[ $option_name ] ) ) { return static::$option_instances[ $option_name ]->update_site_option( $value ); } return false; } /** * Get the instantiated option instance. * * @param string $option_name The option for which you want to retrieve the instance. * * @return object|bool */ public static function get_option_instance( $option_name ) { if ( isset( static::$option_instances[ $option_name ] ) ) { return static::$option_instances[ $option_name ]; } return false; } /** * Retrieve an array of the options which should be included in get_all() and reset(). * * @return array Array of option names. */ public static function get_option_names() { $option_names = array_values( static::$option_names ); if ( $option_names === [] ) { foreach ( static::$option_instances as $option_name => $option_object ) { if ( $option_object->include_in_all === true ) { $option_names[] = $option_name; } } } /** * Filter: wpseo_options - Allow developers to change the option name to include. * * @param array $option_names The option names to include in get_all and reset(). */ return apply_filters( 'wpseo_options', $option_names ); } /** * Retrieve all the options for the SEO plugin in one go. * * @param array<string> $specific_options The option groups of the option you want to get. * * @return array Array combining the values of all the options. */ public static function get_all( $specific_options = [] ) { $option_names = ( empty( $specific_options ) ) ? static::get_option_names() : $specific_options; static::$option_values = static::get_options( $option_names ); return static::$option_values; } /** * Retrieve one or more options for the SEO plugin. * * @param array $option_names An array of option names of the options you want to get. * * @return array Array combining the values of the requested options. */ public static function get_options( array $option_names ) { $options = []; $option_names = array_filter( $option_names, 'is_string' ); foreach ( $option_names as $option_name ) { if ( isset( static::$option_instances[ $option_name ] ) ) { $option = static::get_option( $option_name ); if ( $option !== null ) { $options = array_merge( $options, $option ); } } } return $options; } /** * Retrieve a single option for the SEO plugin. * * @param string $option_name The name of the option you want to get. * * @return array Array containing the requested option. */ public static function get_option( $option_name ) { $option = null; if ( is_string( $option_name ) && ! empty( $option_name ) ) { if ( isset( static::$option_instances[ $option_name ] ) ) { if ( static::$option_instances[ $option_name ]->multisite_only !== true ) { $option = get_option( $option_name ); } else { $option = get_site_option( $option_name ); } } } return $option; } /** * Retrieve a single field from any option for the SEO plugin. Keys are always unique. * * @param string $key The key it should return. * @param mixed $default_value The default value that should be returned if the key isn't set. * @param array<string> $option_groups The option groups to retrieve the option from. * * @return mixed Returns value if found, $default_value if not. */ public static function get( $key, $default_value = null, $option_groups = [] ) { if ( ! isset( static::$option_values[ $key ] ) ) { static::prime_cache( $option_groups ); } if ( isset( static::$option_values[ $key ] ) ) { return static::$option_values[ $key ]; } return $default_value; } /** * Resets the cache to null. * * @return void */ public static function clear_cache() { static::$option_values = null; } /** * Primes our cache. * * @param array<string> $option_groups The option groups to prime the cache with. * * @return void */ private static function prime_cache( $option_groups = [] ) { static::$option_values = static::get_all( $option_groups ); static::$option_values = static::add_ms_option( static::$option_values ); } /** * Retrieve a single field from an option for the SEO plugin. * * @param string $key The key to set. * @param mixed $value The value to set. * @param string $option_group The lookup table which represents the option_group where the key is stored. * * @return mixed|null Returns value if found, $default if not. */ public static function set( $key, $value, $option_group = '' ) { $lookup_table = static::get_lookup_table( $option_group ); if ( isset( $lookup_table[ $key ] ) ) { return static::save_option( $lookup_table[ $key ], $key, $value ); } $patterns = static::get_pattern_table(); foreach ( $patterns as $pattern => $option ) { if ( strpos( $key, $pattern ) === 0 ) { return static::save_option( $option, $key, $value ); } } static::$option_values[ $key ] = $value; } /** * Get an option only if it's been auto-loaded. * * @param string $option The option to retrieve. * @param mixed $default_value A default value to return. * * @return mixed */ public static function get_autoloaded_option( $option, $default_value = false ) { $value = wp_cache_get( $option, 'options' ); if ( $value === false ) { $passed_default = func_num_args() > 1; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter. return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default ); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter. return apply_filters( "option_{$option}", maybe_unserialize( $value ), $option ); } /** * Run the clean up routine for one or all options. * * @param array|string|null $option_name Optional. the option you want to clean or an array of * option names for the options you want to clean. * If not set, all options will be cleaned. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * * @return void */ public static function clean_up( $option_name = null, $current_version = null ) { if ( isset( $option_name ) && is_string( $option_name ) && $option_name !== '' ) { if ( isset( static::$option_instances[ $option_name ] ) ) { static::$option_instances[ $option_name ]->clean( $current_version ); } } elseif ( isset( $option_name ) && is_array( $option_name ) && $option_name !== [] ) { foreach ( $option_name as $option ) { if ( isset( static::$option_instances[ $option ] ) ) { static::$option_instances[ $option ]->clean( $current_version ); } } unset( $option ); } else { foreach ( static::$option_instances as $instance ) { $instance->clean( $current_version ); } unset( $instance ); // If we've done a full clean-up, we can safely remove this really old option. delete_option( 'wpseo_indexation' ); } } /** * Check that all options exist in the database and add any which don't. * * @return void */ public static function ensure_options_exist() { foreach ( static::$option_instances as $instance ) { $instance->maybe_add_option(); } } /** * Initialize some options on first install/activate/reset. * * @return void */ public static function initialize() { /* Force WooThemes to use Yoast SEO data. */ if ( function_exists( 'woo_version_init' ) ) { update_option( 'seo_woo_use_third_party_data', 'true' ); } } /** * Reset all options to their default values and rerun some tests. * * @return void */ public static function reset() { if ( ! is_multisite() ) { $option_names = static::get_option_names(); if ( is_array( $option_names ) && $option_names !== [] ) { foreach ( $option_names as $option_name ) { delete_option( $option_name ); update_option( $option_name, get_option( $option_name ) ); } } unset( $option_names ); } else { // Reset MS blog based on network default blog setting. static::reset_ms_blog( get_current_blog_id() ); } static::initialize(); } /** * Initialize default values for a new multisite blog. * * @param bool $force_init Whether to always do the initialization routine (title/desc test). * * @return void */ public static function maybe_set_multisite_defaults( $force_init = false ) { $option = get_option( 'wpseo' ); if ( is_multisite() ) { if ( $option['ms_defaults_set'] === false ) { static::reset_ms_blog( get_current_blog_id() ); static::initialize(); } elseif ( $force_init === true ) { static::initialize(); } } } /** * Reset all options for a specific multisite blog to their default values based upon a * specified default blog if one was chosen on the network page or the plugin defaults if it was not. * * @param int|string $blog_id Blog id of the blog for which to reset the options. * * @return void */ public static function reset_ms_blog( $blog_id ) { if ( is_multisite() ) { $options = get_site_option( 'wpseo_ms' ); $option_names = static::get_option_names(); if ( is_array( $option_names ) && $option_names !== [] ) { $base_blog_id = $blog_id; if ( $options['defaultblog'] !== '' && $options['defaultblog'] !== 0 ) { $base_blog_id = $options['defaultblog']; } foreach ( $option_names as $option_name ) { delete_blog_option( $blog_id, $option_name ); $new_option = get_blog_option( $base_blog_id, $option_name ); /* Remove sensitive, theme dependent and site dependent info. */ if ( isset( static::$option_instances[ $option_name ] ) && static::$option_instances[ $option_name ]->ms_exclude !== [] ) { foreach ( static::$option_instances[ $option_name ]->ms_exclude as $key ) { unset( $new_option[ $key ] ); } } if ( $option_name === 'wpseo' ) { $new_option['ms_defaults_set'] = true; } update_blog_option( $blog_id, $option_name, $new_option ); } } } } /** * Saves the option to the database. * * @param string $wpseo_options_group_name The name for the wpseo option group in the database. * @param string $option_name The name for the option to set. * @param mixed $option_value The value for the option. * * @return bool Returns true if the option is successfully saved in the database. */ public static function save_option( $wpseo_options_group_name, $option_name, $option_value ) { $options = static::get_option( $wpseo_options_group_name ); $options[ $option_name ] = $option_value; if ( isset( static::$option_instances[ $wpseo_options_group_name ] ) && static::$option_instances[ $wpseo_options_group_name ]->multisite_only === true ) { static::update_site_option( $wpseo_options_group_name, $options ); } else { update_option( $wpseo_options_group_name, $options ); } // Check if everything got saved properly. $saved_option = static::get_option( $wpseo_options_group_name ); // Clear our cache. static::clear_cache(); return $saved_option[ $option_name ] === $options[ $option_name ]; } /** * Adds the multisite options to the option stack if relevant. * * @param array $option The currently present options settings. * * @return array Options possibly including multisite. */ protected static function add_ms_option( $option ) { if ( ! is_multisite() ) { return $option; } $ms_option = static::get_option( 'wpseo_ms' ); if ( $ms_option === null ) { return $option; } return array_merge( $option, $ms_option ); } /** * Checks if installation is multisite. * * @return bool True when is multisite. */ protected static function is_multisite() { static $is_multisite; if ( $is_multisite === null ) { $is_multisite = is_multisite(); } return $is_multisite; } /** * Retrieves a lookup table to find in which option_group a key is stored. * * @param string $option_group The option_group where the key is stored. * * @return array The lookup table. */ private static function get_lookup_table( $option_group = '' ) { $lookup_table = []; $option_groups = ( $option_group === '' ) ? static::$options : [ $option_group => static::$options[ $option_group ] ]; foreach ( array_keys( $option_groups ) as $option_name ) { $full_option = static::get_option( $option_name ); foreach ( $full_option as $key => $value ) { $lookup_table[ $key ] = $option_name; } } return $lookup_table; } /** * Retrieves a lookup table to find in which option_group a key is stored. * * @return array The lookup table. */ private static function get_pattern_table() { $pattern_table = []; foreach ( static::$options as $option_name => $option_class ) { $instance = call_user_func( [ $option_class, 'get_instance' ] ); foreach ( $instance->get_patterns() as $key ) { $pattern_table[ $key ] = $option_name; } } return $pattern_table; } } options/class-wpseo-taxonomy-meta.php 0000644 00000042543 14720701066 0014023 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo_taxonomy_meta. */ class WPSEO_Taxonomy_Meta extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_taxonomy_meta'; /** * Whether to include the option in the return for WPSEO_Options::get_all(). * * @var bool */ public $include_in_all = false; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * {@internal Important: in contrast to most defaults, the below array format is * very bare. The real option is in the format [taxonomy_name][term_id][...] * where [...] is any of the $defaults_per_term options shown below. * This is of course taken into account in the below methods.}} * * @var array */ protected $defaults = []; /** * Option name - same as $option_name property, but now also available to static methods. * * @var string */ public static $name; /** * Array of defaults for individual taxonomy meta entries. * * @var array */ public static $defaults_per_term = [ 'wpseo_title' => '', 'wpseo_desc' => '', 'wpseo_canonical' => '', 'wpseo_bctitle' => '', 'wpseo_noindex' => 'default', 'wpseo_focuskw' => '', 'wpseo_linkdex' => '', 'wpseo_content_score' => '', 'wpseo_inclusive_language_score' => '', 'wpseo_focuskeywords' => '[]', 'wpseo_keywordsynonyms' => '[]', 'wpseo_is_cornerstone' => '0', // Social fields. 'wpseo_opengraph-title' => '', 'wpseo_opengraph-description' => '', 'wpseo_opengraph-image' => '', 'wpseo_opengraph-image-id' => '', 'wpseo_twitter-title' => '', 'wpseo_twitter-description' => '', 'wpseo_twitter-image' => '', 'wpseo_twitter-image-id' => '', ]; /** * Available index options. * * Used for form generation and input validation. * * {@internal Labels (translation) added on admin_init via WPSEO_Taxonomy::translate_meta_options().}} * * @var array */ public static $no_index_options = [ 'default' => '', 'index' => '', 'noindex' => '', ]; /** * Add the actions and filters for the option. * * @todo [JRF => testers] Check if the extra actions below would run into problems if an option * is updated early on and if so, change the call to schedule these for a later action on add/update * instead of running them straight away. */ protected function __construct() { parent::__construct(); self::$name = $this->option_name; } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); self::$name = self::$instance->option_name; } return self::$instance; } /** * Add extra default options received from a filter. * * @return void */ public function enrich_defaults() { $extra_defaults_per_term = apply_filters( 'wpseo_add_extra_taxmeta_term_defaults', [] ); if ( is_array( $extra_defaults_per_term ) ) { self::$defaults_per_term = array_merge( $extra_defaults_per_term, self::$defaults_per_term ); } } /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { /* * Prevent complete validation (which can be expensive when there are lots of terms) * if only one item has changed and has already been validated. */ if ( isset( $dirty['wpseo_already_validated'] ) && $dirty['wpseo_already_validated'] === true ) { unset( $dirty['wpseo_already_validated'] ); return $dirty; } foreach ( $dirty as $taxonomy => $terms ) { /* Don't validate taxonomy - may not be registered yet and we don't want to remove valid ones. */ if ( is_array( $terms ) && $terms !== [] ) { foreach ( $terms as $term_id => $meta_data ) { /* Only validate term if the taxonomy exists. */ if ( taxonomy_exists( $taxonomy ) && get_term_by( 'id', $term_id, $taxonomy ) === false ) { /* Is this term id a special case ? */ if ( has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) { $clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id ); } continue; } if ( is_array( $meta_data ) && $meta_data !== [] ) { /* Validate meta data. */ $old_meta = self::get_term_meta( $term_id, $taxonomy ); $meta_data = self::validate_term_meta_data( $meta_data, $old_meta ); if ( $meta_data !== [] ) { $clean[ $taxonomy ][ $term_id ] = $meta_data; } } // Deal with special cases (for when taxonomy doesn't exist yet). if ( ! isset( $clean[ $taxonomy ][ $term_id ] ) && has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) { $clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id ); } } } } return $clean; } /** * Validate the meta data for one individual term and removes default values (no need to save those). * * @param array $meta_data New values. * @param array $old_meta The original values. * * @return array Validated and filtered value. */ public static function validate_term_meta_data( $meta_data, $old_meta ) { $clean = self::$defaults_per_term; $meta_data = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $meta_data ); if ( ! is_array( $meta_data ) || $meta_data === [] ) { return $clean; } foreach ( $clean as $key => $value ) { switch ( $key ) { case 'wpseo_noindex': if ( isset( $meta_data[ $key ] ) ) { if ( isset( self::$no_index_options[ $meta_data[ $key ] ] ) ) { $clean[ $key ] = $meta_data[ $key ]; } } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_canonical': if ( isset( $meta_data[ $key ] ) && $meta_data[ $key ] !== '' ) { $url = WPSEO_Utils::sanitize_url( $meta_data[ $key ] ); if ( $url !== '' ) { $clean[ $key ] = $url; } unset( $url ); } break; case 'wpseo_bctitle': if ( isset( $meta_data[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] ); } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_keywordsynonyms': if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) { // The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation. $input = json_decode( $meta_data[ $key ], true ); $sanitized = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $input ); $clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized ); } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_focuskeywords': if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) { // The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation. $input = json_decode( $meta_data[ $key ], true ); // This data has two known keys: `keyword` and `score`. $sanitized = []; foreach ( $input as $entry ) { $sanitized[] = [ 'keyword' => WPSEO_Utils::sanitize_text_field( $entry['keyword'] ), 'score' => WPSEO_Utils::sanitize_text_field( $entry['score'] ), ]; } $clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized ); } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_focuskw': case 'wpseo_title': case 'wpseo_desc': case 'wpseo_linkdex': default: if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] ); } if ( $key === 'wpseo_focuskw' ) { $search = [ '<', '>', '`', '<', '>', '`', ]; $clean[ $key ] = str_replace( $search, '', $clean[ $key ] ); } break; } $clean[ $key ] = apply_filters( 'wpseo_sanitize_tax_meta_' . $key, $clean[ $key ], ( $meta_data[ $key ] ?? null ), ( $old_meta[ $key ] ?? null ) ); } // Only save the non-default values. return array_diff_assoc( $clean, self::$defaults_per_term ); } /** * Clean a given option value. * - Convert old option values to new * - Fixes strings which were escaped (should have been sanitized - escaping is for output) * * @param array $option_value Old (not merged with defaults or filtered) option value to * clean according to the rules for this option. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to have * access to the real old values, in contrast to the saved ones. * * @return array Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { /* Clean up old values and remove empty arrays. */ if ( is_array( $option_value ) && $option_value !== [] ) { foreach ( $option_value as $taxonomy => $terms ) { if ( is_array( $terms ) && $terms !== [] ) { foreach ( $terms as $term_id => $meta_data ) { if ( ! is_array( $meta_data ) || $meta_data === [] ) { // Remove empty term arrays. unset( $option_value[ $taxonomy ][ $term_id ] ); } else { foreach ( $meta_data as $key => $value ) { switch ( $key ) { case 'noindex': if ( $value === 'on' ) { // Convert 'on' to 'noindex'. $option_value[ $taxonomy ][ $term_id ][ $key ] = 'noindex'; } break; case 'canonical': case 'wpseo_bctitle': case 'wpseo_title': case 'wpseo_desc': case 'wpseo_linkdex': // @todo [JRF => whomever] Needs checking, I don't have example data [JRF]. if ( $value !== '' ) { // Fix incorrectly saved (encoded) canonical urls and texts. $option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( stripslashes( $value ), ENT_QUOTES ); } break; default: // @todo [JRF => whomever] Needs checking, I don't have example data [JRF]. if ( $value !== '' ) { // Fix incorrectly saved (escaped) text strings. $option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( $value, ENT_QUOTES ); } break; } } } } } else { // Remove empty taxonomy arrays. unset( $option_value[ $taxonomy ] ); } } } return $option_value; } /** * Retrieve a taxonomy term's meta value(s). * * @param mixed $term Term to get the meta value for * either (string) term name, (int) term id or (object) term. * @param string $taxonomy Name of the taxonomy to which the term is attached. * @param string|null $meta Optional. Meta value to get (without prefix). * * @return mixed Value for the $meta if one is given, might be the default. * If no meta is given, an array of all the meta data for the term. * False if the term does not exist or the $meta provided is invalid. */ public static function get_term_meta( $term, $taxonomy, $meta = null ) { /* Figure out the term id. */ if ( is_int( $term ) ) { $term = get_term_by( 'id', $term, $taxonomy ); } elseif ( is_string( $term ) ) { $term = get_term_by( 'slug', $term, $taxonomy ); } if ( is_object( $term ) && isset( $term->term_id ) ) { $term_id = $term->term_id; } else { return false; } $tax_meta = self::get_term_tax_meta( $term_id, $taxonomy ); /* * Either return the complete array or a single value from it or false if the value does not exist * (shouldn't happen after merge with defaults, indicates typo in request). */ if ( ! isset( $meta ) ) { return $tax_meta; } if ( isset( $tax_meta[ 'wpseo_' . $meta ] ) ) { return $tax_meta[ 'wpseo_' . $meta ]; } return false; } /** * Get the current queried object and return the meta value. * * @param string $meta The meta field that is needed. * * @return mixed */ public static function get_meta_without_term( $meta ) { $term = $GLOBALS['wp_query']->get_queried_object(); if ( ! $term || empty( $term->taxonomy ) ) { return false; } return self::get_term_meta( $term, $term->taxonomy, $meta ); } /** * Saving the values for the given term_id. * * @param int $term_id ID of the term to save data for. * @param string $taxonomy The taxonomy the term belongs to. * @param array $meta_values The values that will be saved. * * @return void */ public static function set_values( $term_id, $taxonomy, array $meta_values ) { /* Validate the post values */ $old = self::get_term_meta( $term_id, $taxonomy ); $clean = self::validate_term_meta_data( $meta_values, $old ); self::save_clean_values( $term_id, $taxonomy, $clean ); } /** * Setting a single value to the term meta. * * @param int $term_id ID of the term to save data for. * @param string $taxonomy The taxonomy the term belongs to. * @param string $meta_key The target meta key to store the value in. * @param string $meta_value The value of the target meta key. * * @return void */ public static function set_value( $term_id, $taxonomy, $meta_key, $meta_value ) { if ( substr( strtolower( $meta_key ), 0, 6 ) !== 'wpseo_' ) { $meta_key = 'wpseo_' . $meta_key; } self::set_values( $term_id, $taxonomy, [ $meta_key => $meta_value ] ); } /** * Find the keyword usages in the metas for the taxonomies/terms. * * @param string $keyword The keyword to look for. * @param string $current_term_id The current term id. * @param string $current_taxonomy The current taxonomy name. * * @return array */ public static function get_keyword_usage( $keyword, $current_term_id, $current_taxonomy ) { $tax_meta = self::get_tax_meta(); $found = []; // @todo Check for terms of all taxonomies, not only the current taxonomy. foreach ( $tax_meta as $taxonomy_name => $terms ) { foreach ( $terms as $term_id => $meta_values ) { $is_current = ( $current_taxonomy === $taxonomy_name && (string) $current_term_id === (string) $term_id ); if ( ! $is_current && ! empty( $meta_values['wpseo_focuskw'] ) && $meta_values['wpseo_focuskw'] === $keyword ) { $found[] = $term_id; } } } return [ $keyword => $found ]; } /** * Saving the values for the given term_id. * * @param int $term_id ID of the term to save data for. * @param string $taxonomy The taxonomy the term belongs to. * @param array $clean Array with clean values. * * @return void */ private static function save_clean_values( $term_id, $taxonomy, array $clean ) { $tax_meta = self::get_tax_meta(); /* Add/remove the result to/from the original option value. */ if ( $clean !== [] ) { $tax_meta[ $taxonomy ][ $term_id ] = $clean; } else { unset( $tax_meta[ $taxonomy ][ $term_id ] ); if ( isset( $tax_meta[ $taxonomy ] ) && $tax_meta[ $taxonomy ] === [] ) { unset( $tax_meta[ $taxonomy ] ); } } // Prevent complete array validation. $tax_meta['wpseo_already_validated'] = true; self::save_tax_meta( $tax_meta ); } /** * Getting the meta from the options. * * @return void|array */ private static function get_tax_meta() { return get_option( self::$name ); } /** * Saving the tax meta values to the database. * * @param array $tax_meta Array with the meta values for taxonomy. * * @return void */ private static function save_tax_meta( $tax_meta ) { update_option( self::$name, $tax_meta ); } /** * Getting the taxonomy meta for the given term_id and taxonomy. * * @param int $term_id The id of the term. * @param string $taxonomy Name of the taxonomy to which the term is attached. * * @return array */ private static function get_term_tax_meta( $term_id, $taxonomy ) { $tax_meta = self::get_tax_meta(); /* If we have data for the term, merge with defaults for complete array, otherwise set defaults. */ if ( isset( $tax_meta[ $taxonomy ][ $term_id ] ) ) { return array_merge( self::$defaults_per_term, $tax_meta[ $taxonomy ][ $term_id ] ); } return self::$defaults_per_term; } } options/class-wpseo-option-wpseo.php 0000644 00000052034 14720701066 0013660 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo. */ class WPSEO_Option_Wpseo extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo'; /** * Array of defaults for the option. * * {@internal Shouldn't be requested directly, use $this->get_defaults();}} * * @var array */ protected $defaults = [ // Non-form fields, set via (ajax) function. 'tracking' => null, 'toggled_tracking' => false, 'license_server_version' => false, 'ms_defaults_set' => false, 'ignore_search_engines_discouraged_notice' => false, 'indexing_first_time' => true, 'indexing_started' => null, 'indexing_reason' => '', 'indexables_indexing_completed' => false, 'index_now_key' => '', // Non-form field, should only be set via validation routine. 'version' => '', // Leave default as empty to ensure activation/upgrade works. 'previous_version' => '', // Form fields. 'disableadvanced_meta' => true, 'enable_headless_rest_endpoints' => true, 'ryte_indexability' => false, 'baiduverify' => '', // Text field. 'googleverify' => '', // Text field. 'msverify' => '', // Text field. 'yandexverify' => '', 'site_type' => '', // List of options. 'has_multiple_authors' => '', 'environment_type' => '', 'content_analysis_active' => true, 'keyword_analysis_active' => true, 'inclusive_language_analysis_active' => false, 'enable_admin_bar_menu' => true, 'enable_cornerstone_content' => true, 'enable_xml_sitemap' => true, 'enable_text_link_counter' => true, 'enable_index_now' => true, 'enable_ai_generator' => true, 'ai_enabled_pre_default' => false, 'show_onboarding_notice' => false, 'first_activated_on' => false, 'myyoast-oauth' => [ 'config' => [ 'clientId' => null, 'secret' => null, ], 'access_tokens' => [], ], 'semrush_integration_active' => true, 'semrush_tokens' => [], 'semrush_country_code' => 'us', 'permalink_structure' => '', 'home_url' => '', 'dynamic_permalinks' => false, 'category_base_url' => '', 'tag_base_url' => '', 'custom_taxonomy_slugs' => [], 'enable_enhanced_slack_sharing' => true, 'enable_metabox_insights' => true, 'enable_link_suggestions' => true, 'algolia_integration_active' => false, 'import_cursors' => [], 'workouts_data' => [ 'configuration' => [ 'finishedSteps' => [] ] ], 'configuration_finished_steps' => [], 'dismiss_configuration_workout_notice' => false, 'dismiss_premium_deactivated_notice' => false, 'importing_completed' => [], 'wincher_integration_active' => true, 'wincher_tokens' => [], 'wincher_automatically_add_keyphrases' => false, 'wincher_website_id' => '', 'first_time_install' => false, 'should_redirect_after_install_free' => false, 'activation_redirect_timestamp_free' => 0, 'remove_feed_global' => false, 'remove_feed_global_comments' => false, 'remove_feed_post_comments' => false, 'remove_feed_authors' => false, 'remove_feed_categories' => false, 'remove_feed_tags' => false, 'remove_feed_custom_taxonomies' => false, 'remove_feed_post_types' => false, 'remove_feed_search' => false, 'remove_atom_rdf_feeds' => false, 'remove_shortlinks' => false, 'remove_rest_api_links' => false, 'remove_rsd_wlw_links' => false, 'remove_oembed_links' => false, 'remove_generator' => false, 'remove_emoji_scripts' => false, 'remove_powered_by_header' => false, 'remove_pingback_header' => false, 'clean_campaign_tracking_urls' => false, 'clean_permalinks' => false, 'clean_permalinks_extra_variables' => '', 'search_cleanup' => false, 'search_cleanup_emoji' => false, 'search_cleanup_patterns' => false, 'search_character_limit' => 50, 'deny_search_crawling' => false, 'deny_wp_json_crawling' => false, 'deny_adsbot_crawling' => false, 'deny_ccbot_crawling' => false, 'deny_google_extended_crawling' => false, 'deny_gptbot_crawling' => false, 'redirect_search_pretty_urls' => false, 'least_readability_ignore_list' => [], 'least_seo_score_ignore_list' => [], 'most_linked_ignore_list' => [], 'least_linked_ignore_list' => [], 'indexables_page_reading_list' => [ false, false, false, false, false ], 'indexables_overview_state' => 'dashboard-not-visited', 'last_known_public_post_types' => [], 'last_known_public_taxonomies' => [], 'last_known_no_unindexed' => [], 'new_post_types' => [], 'new_taxonomies' => [], 'show_new_content_type_notification' => false, ]; /** * Sub-options which should not be overloaded with multi-site defaults. * * @var array */ public $ms_exclude = [ 'ignore_search_engines_discouraged_notice', /* Privacy. */ 'baiduverify', 'googleverify', 'msverify', 'yandexverify', ]; /** * Possible values for the site_type option. * * @var array */ protected $site_types = [ '', 'blog', 'shop', 'news', 'smallBusiness', 'corporateOther', 'personalOther', ]; /** * Possible environment types. * * @var array */ protected $environment_types = [ '', 'local', 'production', 'staging', 'development', ]; /** * Possible has_multiple_authors options. * * @var array */ protected $has_multiple_authors_options = [ '', true, false, ]; /** * Name for an option higher in the hierarchy to override setting access. * * @var string */ protected $override_option_name = 'wpseo_ms'; /** * Add the actions and filters for the option. * * @todo [JRF => testers] Check if the extra actions below would run into problems if an option * is updated early on and if so, change the call to schedule these for a later action on add/update * instead of running them straight away. */ protected function __construct() { parent::__construct(); /** * Filter: 'wpseo_enable_tracking' - Enables the data tracking of Yoast SEO Premium. * * @param string $is_enabled The enabled state. Default is false. */ $this->defaults['tracking'] = apply_filters( 'wpseo_enable_tracking', false ); /* Clear the cache on update/add. */ add_action( 'add_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] ); add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] ); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); /** * Filter the `wpseo` option defaults. * * @param array $defaults Array the defaults for the `wpseo` option attributes. */ $this->defaults = apply_filters( 'wpseo_option_wpseo_defaults', $this->defaults ); } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { parent::add_option_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook(); if ( has_filter( $hookname, $callback ) === false ) { add_filter( $hookname, $callback, $priority ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { parent::remove_option_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook(); remove_filter( $hookname, $callback, $priority ); } /** * Add filters to make sure that the option default is returned if the option is not set. * * @return void */ public function add_default_filters() { parent::add_default_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook(); if ( has_filter( $hookname, $callback ) === false ) { add_filter( $hookname, $callback, $priority ); } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { parent::remove_default_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook(); remove_filter( $hookname, $callback, $priority ); } /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { case 'version': $clean[ $key ] = WPSEO_VERSION; break; case 'previous_version': case 'semrush_country_code': case 'license_server_version': case 'home_url': case 'index_now_key': case 'wincher_website_id': case 'clean_permalinks_extra_variables': case 'indexables_overview_state': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'indexing_reason': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = sanitize_text_field( $dirty[ $key ] ); } break; /* Verification strings. */ case 'baiduverify': case 'googleverify': case 'msverify': case 'yandexverify': $this->validate_verification_string( $key, $dirty, $old, $clean ); break; /* * Boolean dismiss warnings - not fields - may not be in form * (and don't need to be either as long as the default is false). */ case 'ignore_search_engines_discouraged_notice': case 'ms_defaults_set': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::validate_bool( $dirty[ $key ] ); } elseif ( isset( $old[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::validate_bool( $old[ $key ] ); } break; case 'site_type': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->site_types, true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'environment_type': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->environment_types, true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'has_multiple_authors': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->has_multiple_authors_options, true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'first_activated_on': case 'indexing_started': case 'activation_redirect_timestamp_free': $clean[ $key ] = false; if ( isset( $dirty[ $key ] ) ) { if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'tracking': $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : null ); break; case 'myyoast_oauth': case 'semrush_tokens': case 'custom_taxonomy_slugs': case 'wincher_tokens': case 'workouts_data': case 'configuration_finished_steps': case 'least_readability_ignore_list': case 'least_seo_score_ignore_list': case 'most_linked_ignore_list': case 'least_linked_ignore_list': case 'indexables_page_reading_list': case 'last_known_public_post_types': case 'last_known_public_taxonomies': case 'new_post_types': case 'new_taxonomies': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( ! is_array( $items ) ) { $items = json_decode( $dirty[ $key ], true ); } if ( is_array( $items ) ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'permalink_structure': case 'category_base_url': case 'tag_base_url': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = sanitize_option( $key, $dirty[ $key ] ); } break; case 'search_character_limit': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = (int) $dirty[ $key ]; } break; case 'import_cursors': case 'importing_completed': if ( isset( $dirty[ $key ] ) && is_array( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'last_known_no_unindexed': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( is_array( $items ) ) { foreach ( $items as $item_key => $item ) { if ( ! is_string( $item_key ) || ! is_numeric( $item ) ) { unset( $items[ $item_key ] ); } } $clean[ $key ] = $items; } } break; /* * Boolean (checkbox) fields. * * Covers: * 'disableadvanced_meta' * 'enable_headless_rest_endpoints' * 'yoast_tracking' * 'dynamic_permalinks' * 'indexing_first_time' * 'first_time_install' * 'remove_feed_global' * 'remove_feed_global_comments' * 'remove_feed_post_comments' * 'remove_feed_authors' * 'remove_feed_categories' * 'remove_feed_tags' * 'remove_feed_custom_taxonomies' * 'remove_feed_post_types' * 'remove_feed_search' * 'remove_atom_rdf_feeds' * 'remove_shortlinks' * 'remove_rest_api_links' * 'remove_rsd_wlw_links' * 'remove_oembed_links' * 'remove_generator' * 'remove_emoji_scripts' * 'remove_powered_by_header' * 'remove_pingback_header' * 'clean_campaign_tracking_urls' * 'clean_permalinks' * 'clean_permalinks_extra_variables' * 'search_cleanup' * 'search_cleanup_emoji' * 'search_cleanup_patterns' * 'deny_wp_json_crawling' * 'deny_adsbot_crawling' * 'deny_ccbot_crawling' * 'deny_google_extended_crawling' * 'deny_gptbot_crawling' * 'redirect_search_pretty_urls' * 'should_redirect_after_install_free' * 'show_new_content_type_notification' * and most of the feature variables. */ default: $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; } } return $clean; } /** * Verifies that the feature variables are turned off if the network is configured so. * * @param mixed $options Value of the option to be returned. Typically an array. * * @return mixed Filtered $options value. */ public function verify_features_against_network( $options = [] ) { if ( ! is_array( $options ) || empty( $options ) ) { return $options; } // For the feature variables, set their values to off in case they are disabled. $feature_vars = [ 'disableadvanced_meta' => false, 'ryte_indexability' => false, 'content_analysis_active' => false, 'keyword_analysis_active' => false, 'inclusive_language_analysis_active' => false, 'enable_admin_bar_menu' => false, 'enable_cornerstone_content' => false, 'enable_xml_sitemap' => false, 'enable_text_link_counter' => false, 'enable_metabox_insights' => false, 'enable_link_suggestions' => false, 'enable_headless_rest_endpoints' => false, 'tracking' => false, 'enable_enhanced_slack_sharing' => false, 'semrush_integration_active' => false, 'wincher_integration_active' => false, 'remove_feed_global' => false, 'remove_feed_global_comments' => false, 'remove_feed_post_comments' => false, 'enable_index_now' => false, 'enable_ai_generator' => false, 'remove_feed_authors' => false, 'remove_feed_categories' => false, 'remove_feed_tags' => false, 'remove_feed_custom_taxonomies' => false, 'remove_feed_post_types' => false, 'remove_feed_search' => false, 'remove_atom_rdf_feeds' => false, 'remove_shortlinks' => false, 'remove_rest_api_links' => false, 'remove_rsd_wlw_links' => false, 'remove_oembed_links' => false, 'remove_generator' => false, 'remove_emoji_scripts' => false, 'remove_powered_by_header' => false, 'remove_pingback_header' => false, 'clean_campaign_tracking_urls' => false, 'clean_permalinks' => false, 'search_cleanup' => false, 'search_cleanup_emoji' => false, 'search_cleanup_patterns' => false, 'redirect_search_pretty_urls' => false, 'algolia_integration_active' => false, ]; // We can reuse this logic from the base class with the above defaults to parse with the correct feature values. $options = $this->prevent_disabled_options_update( $options, $feature_vars ); return $options; } /** * Gets the filter hook name and callback for adjusting the retrieved option value * against the network-allowed features. * * @return array Array where the first item is the hook name, the second is the hook callback, * and the third is the hook priority. */ protected function get_verify_features_option_filter_hook() { return [ "option_{$this->option_name}", [ $this, 'verify_features_against_network' ], 11, ]; } /** * Gets the filter hook name and callback for adjusting the default option value against the network-allowed features. * * @return array Array where the first item is the hook name, the second is the hook callback, * and the third is the hook priority. */ protected function get_verify_features_default_option_filter_hook() { return [ "default_option_{$this->option_name}", [ $this, 'verify_features_against_network' ], 11, ]; } /** * Clean a given option value. * * @param array $option_value Old (not merged with defaults or filtered) option value to * clean according to the rules for this option. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to have * access to the real old values, in contrast to the saved ones. * * @return array Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { // Deal with value change from text string to boolean. $value_change = [ 'ignore_search_engines_discouraged_notice', ]; $target_values = [ 'ignore', 'done', ]; foreach ( $value_change as $key ) { if ( isset( $option_value[ $key ] ) && in_array( $option_value[ $key ], $target_values, true ) ) { $option_value[ $key ] = true; } } return $option_value; } } options/class-wpseo-option-ms.php 0000644 00000022345 14720701066 0013144 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Site option for Multisite installs only * * Overloads a number of methods of the abstract class to ensure the use of the correct site_option * WP functions. */ class WPSEO_Option_MS extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_ms'; /** * Option group name for use in settings forms. * * @var string */ public $group_name = 'yoast_wpseo_multisite_options'; /** * Whether to include the option in the return for WPSEO_Options::get_all(). * * @var bool */ public $include_in_all = false; /** * Whether this option is only for when the install is multisite. * * @var bool */ public $multisite_only = true; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array */ protected $defaults = []; /** * Available options for the 'access' setting. Used for input validation. * * {@internal Important: Make sure the options added to the array here are in line * with the keys for the options set for the select box in the * admin/pages/network.php file.}} * * @var array */ public static $allowed_access_options = [ 'admin', 'superadmin', ]; /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Only run parent constructor in multisite context. */ public function __construct() { $allow_prefix = self::ALLOW_KEY_PREFIX; $this->defaults = [ 'access' => 'admin', 'defaultblog' => '', // Numeric blog ID or empty. "{$allow_prefix}disableadvanced_meta" => true, "{$allow_prefix}ryte_indexability" => false, "{$allow_prefix}content_analysis_active" => true, "{$allow_prefix}keyword_analysis_active" => true, "{$allow_prefix}inclusive_language_analysis_active" => true, "{$allow_prefix}enable_admin_bar_menu" => true, "{$allow_prefix}enable_cornerstone_content" => true, "{$allow_prefix}enable_xml_sitemap" => true, "{$allow_prefix}enable_text_link_counter" => true, "{$allow_prefix}enable_headless_rest_endpoints" => true, "{$allow_prefix}enable_metabox_insights" => true, "{$allow_prefix}enable_link_suggestions" => true, "{$allow_prefix}tracking" => true, "{$allow_prefix}enable_enhanced_slack_sharing" => true, "{$allow_prefix}semrush_integration_active" => true, "{$allow_prefix}wincher_integration_active" => false, "{$allow_prefix}remove_feed_global" => true, "{$allow_prefix}remove_feed_global_comments" => true, "{$allow_prefix}remove_feed_post_comments" => true, "{$allow_prefix}enable_index_now" => true, "{$allow_prefix}enable_ai_generator" => true, "{$allow_prefix}remove_feed_authors" => true, "{$allow_prefix}remove_feed_categories" => true, "{$allow_prefix}remove_feed_tags" => true, "{$allow_prefix}remove_feed_custom_taxonomies" => true, "{$allow_prefix}remove_feed_post_types" => true, "{$allow_prefix}remove_feed_search" => true, "{$allow_prefix}remove_atom_rdf_feeds" => true, "{$allow_prefix}remove_shortlinks" => true, "{$allow_prefix}remove_rest_api_links" => true, "{$allow_prefix}remove_rsd_wlw_links" => true, "{$allow_prefix}remove_oembed_links" => true, "{$allow_prefix}remove_generator" => true, "{$allow_prefix}remove_emoji_scripts" => true, "{$allow_prefix}remove_powered_by_header" => true, "{$allow_prefix}remove_pingback_header" => true, "{$allow_prefix}clean_campaign_tracking_urls" => true, "{$allow_prefix}clean_permalinks" => true, "{$allow_prefix}search_cleanup" => true, "{$allow_prefix}search_cleanup_emoji" => true, "{$allow_prefix}search_cleanup_patterns" => true, "{$allow_prefix}redirect_search_pretty_urls" => true, "{$allow_prefix}algolia_integration_active" => true, ]; if ( is_multisite() ) { parent::__construct(); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); } } /** * Add filters to make sure that the option default is returned if the option is not set * * @return void */ public function add_default_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) { add_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { remove_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) { add_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { remove_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ); } /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages *********** */ /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { case 'access': if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], self::$allowed_access_options, true ) ) { $clean[ $key ] = $dirty[ $key ]; } elseif ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. /* translators: %1$s expands to the option name and %2$sexpands to Yoast SEO */ sprintf( __( '%1$s is not a valid choice for who should be allowed access to the %2$s settings. Value reset to the default.', 'wordpress-seo' ), esc_html( sanitize_text_field( $dirty[ $key ] ) ), 'Yoast SEO' ), // The error message. 'error' // Message type. ); } break; case 'defaultblog': if ( isset( $dirty[ $key ] ) && ( $dirty[ $key ] !== '' && $dirty[ $key ] !== '-' ) ) { $int = WPSEO_Utils::validate_int( $dirty[ $key ] ); if ( $int !== false && $int > 0 ) { // Check if a valid blog number has been received. $exists = get_blog_details( $int, false ); if ( $exists && $exists->deleted === '0' ) { $clean[ $key ] = $int; } elseif ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. esc_html__( 'The default blog setting must be the numeric blog id of the blog you want to use as default.', 'wordpress-seo' ) . '<br>' . sprintf( /* translators: %s is the ID number of a blog. */ esc_html__( 'This must be an existing blog. Blog %s does not exist or has been marked as deleted.', 'wordpress-seo' ), '<strong>' . esc_html( sanitize_text_field( $dirty[ $key ] ) ) . '</strong>' ), // The error message. 'error' // Message type. ); } unset( $exists ); } elseif ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. esc_html__( 'The default blog setting must be the numeric blog id of the blog you want to use as default.', 'wordpress-seo' ) . '<br>' . esc_html__( 'No numeric value was received.', 'wordpress-seo' ), // The error message. 'error' // Message type. ); } unset( $int ); } break; default: $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; } } return $clean; } } options/class-wpseo-option-social.php 0000644 00000022153 14720701066 0013774 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo_social. */ class WPSEO_Option_Social extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_social'; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array */ protected $defaults = [ // Form fields. 'facebook_site' => '', // Text field. 'instagram_url' => '', 'linkedin_url' => '', 'myspace_url' => '', 'og_default_image' => '', // Text field. 'og_default_image_id' => '', 'og_frontpage_title' => '', // Text field. 'og_frontpage_desc' => '', // Text field. 'og_frontpage_image' => '', // Text field. 'og_frontpage_image_id' => '', 'opengraph' => true, 'pinterest_url' => '', 'pinterestverify' => '', 'twitter' => true, 'twitter_site' => '', // Text field. 'twitter_card_type' => 'summary_large_image', 'youtube_url' => '', 'wikipedia_url' => '', 'other_social_urls' => [], 'mastodon_url' => '', ]; /** * Array of sub-options which should not be overloaded with multi-site defaults. * * @var array */ public $ms_exclude = [ /* Privacy. */ 'pinterestverify', ]; /** * Array of allowed twitter card types. * * While we only have the options summary and summary_large_image in the * interface now, we might change that at some point. * * {@internal Uncomment any of these to allow them in validation *and* automatically * add them as a choice in the options page.}} * * @var array */ public static $twitter_card_types = [ 'summary_large_image' => '', // 'summary' => '', // 'photo' => '', // 'gallery' => '', // 'app' => '', // 'player' => '', // 'product' => '', ]; /** * Add the actions and filters for the option. */ protected function __construct() { parent::__construct(); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Translate/set strings used in the option defaults. * * @return void */ public function translate_defaults() { self::$twitter_card_types['summary_large_image'] = 'Summary with large image'; } /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { /* Text fields. */ case 'og_frontpage_desc': case 'og_frontpage_title': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] ); } break; case 'og_default_image_id': case 'og_frontpage_image_id': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = (int) $dirty[ $key ]; if ( $dirty[ $key ] === '' ) { $clean[ $key ] = $dirty[ $key ]; } } break; /* URL text fields - no ftp allowed. */ case 'facebook_site': case 'instagram_url': case 'linkedin_url': case 'myspace_url': case 'pinterest_url': case 'og_default_image': case 'og_frontpage_image': case 'youtube_url': case 'wikipedia_url': case 'mastodon_url': $this->validate_url( $key, $dirty, $old, $clean ); break; case 'pinterestverify': $this->validate_verification_string( $key, $dirty, $old, $clean ); break; /* Twitter user name. */ case 'twitter_site': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $twitter_id = $this->validate_twitter_id( $dirty[ $key ] ); if ( $twitter_id ) { $clean[ $key ] = $twitter_id; } elseif ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) { $twitter_id = sanitize_text_field( ltrim( $old[ $key ], '@' ) ); if ( preg_match( '`^[A-Za-z0-9_]{1,25}$`', $twitter_id ) ) { $clean[ $key ] = $twitter_id; } } unset( $twitter_id ); Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] ); } break; case 'twitter_card_type': if ( isset( $dirty[ $key ], self::$twitter_card_types[ $dirty[ $key ] ] ) && $dirty[ $key ] !== '' ) { $clean[ $key ] = $dirty[ $key ]; } break; /* Boolean fields. */ case 'opengraph': case 'twitter': $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; /* Array fields. */ case 'other_social_urls': if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( ! is_array( $items ) ) { $items = json_decode( $dirty[ $key ], true ); } if ( is_array( $items ) ) { foreach ( $items as $item_key => $item ) { $validated_url = $this->validate_social_url( $item ); if ( $validated_url === false ) { // Restore the previous URL values, if any. $old_urls = ( isset( $old[ $key ] ) ) ? $old[ $key ] : []; foreach ( $old_urls as $old_item_key => $old_url ) { if ( $old_url !== '' ) { $url = WPSEO_Utils::sanitize_url( $old_url ); if ( $url !== '' ) { $clean[ $key ][ $old_item_key ] = $url; } } } break; } // The URL format is valid, let's sanitize it. $url = WPSEO_Utils::sanitize_url( $validated_url ); if ( $url !== '' ) { $clean[ $key ][ $item_key ] = $url; } } } } break; } } return $clean; } /** * Validates a social URL. * * @param string $url The url to be validated. * * @return string|false The validated URL or false if the URL is not valid. */ public function validate_social_url( $url ) { $validated_url = filter_var( WPSEO_Utils::sanitize_url( trim( $url ) ), FILTER_VALIDATE_URL ); return $validated_url; } /** * Validates a twitter id. * * @param string $twitter_id The twitter id to be validated. * @param bool $strip_at_sign Whether or not to strip the `@` sign. * * @return string|false The validated twitter id or false if it is not valid. */ public function validate_twitter_id( $twitter_id, $strip_at_sign = true ) { $twitter_id = ( $strip_at_sign ) ? sanitize_text_field( ltrim( $twitter_id, '@' ) ) : sanitize_text_field( $twitter_id ); /* * From the Twitter documentation about twitter screen names: * Typically a maximum of 15 characters long, but some historical accounts * may exist with longer names. * A username can only contain alphanumeric characters (letters A-Z, numbers 0-9) * with the exception of underscores. * * @link https://support.twitter.com/articles/101299-why-can-t-i-register-certain-usernames */ if ( preg_match( '`^[A-Za-z0-9_]{1,25}$`', $twitter_id ) ) { return $twitter_id; } if ( preg_match( '`^http(?:s)?://(?:www\.)?(?:twitter|x)\.com/(?P<handle>[A-Za-z0-9_]{1,25})/?$`', $twitter_id, $matches ) ) { return $matches['handle']; } return false; } /** * Clean a given option value. * * @param array $option_value Old (not merged with defaults or filtered) option value to * clean according to the rules for this option. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to have * access to the real old values, in contrast to the saved ones. * * @return array Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { /* Move options from very old option to this one. */ $old_option = null; if ( isset( $all_old_option_values ) ) { // Ok, we have an import. if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) { $old_option = $all_old_option_values['wpseo_indexation']; } } else { $old_option = get_option( 'wpseo_indexation' ); } if ( is_array( $old_option ) && $old_option !== [] ) { $move = [ 'opengraph', ]; foreach ( $move as $key ) { if ( isset( $old_option[ $key ] ) && ! isset( $option_value[ $key ] ) ) { $option_value[ $key ] = $old_option[ $key ]; } } unset( $move, $key ); } unset( $old_option ); return $option_value; } } sitemaps/class-sitemaps-renderer.php 0000644 00000022551 14720701066 0013646 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Renders XML output for sitemaps. */ class WPSEO_Sitemaps_Renderer { /** * XSL stylesheet for styling a sitemap for web browsers. * * @var string */ protected $stylesheet = ''; /** * Holds the get_bloginfo( 'charset' ) value to reuse for performance. * * @var string */ protected $charset = 'UTF-8'; /** * Holds charset of output, might be converted. * * @var string */ protected $output_charset = 'UTF-8'; /** * If data encoding needs to be converted for output. * * @var bool */ protected $needs_conversion = false; /** * Set up object properties. */ public function __construct() { $stylesheet_url = preg_replace( '/(^http[s]?:)/', '', $this->get_xsl_url() ); $this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . esc_url( $stylesheet_url ) . '"?>'; $this->charset = get_bloginfo( 'charset' ); $this->output_charset = $this->charset; if ( $this->charset !== 'UTF-8' && function_exists( 'mb_list_encodings' ) && in_array( $this->charset, mb_list_encodings(), true ) ) { $this->output_charset = 'UTF-8'; } $this->needs_conversion = $this->output_charset !== $this->charset; } /** * Builds the sitemap index. * * @param array<string> $links Set of sitemaps index links. * * @return string */ public function get_index( $links ) { $xml = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; foreach ( $links as $link ) { $xml .= $this->sitemap_index_url( $link ); } /** * Filter to append sitemaps to the index. * * @param string $index String to append to sitemaps index, defaults to empty. */ $xml .= apply_filters( 'wpseo_sitemap_index', '' ); $xml .= '</sitemapindex>'; return $xml; } /** * Builds the sitemap. * * @param array<string> $links Set of sitemap links. * @param string $type Sitemap type. * @param int $current_page Current sitemap page number. * * @return string */ public function get_sitemap( $links, $type, $current_page ) { $urlset = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" ' . 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd ' . 'http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" ' . 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; /** * Filters the `urlset` for all sitemaps. * * @param string $urlset The output for the sitemap's `urlset`. */ $urlset = apply_filters( 'wpseo_sitemap_urlset', $urlset ); /** * Filters the `urlset` for a sitemap by type. * * @param string $urlset The output for the sitemap's `urlset`. */ $xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset ); foreach ( $links as $url ) { $xml .= $this->sitemap_url( $url ); } /** * Filter to add extra URLs to the XML sitemap by type. * * Only runs for the first page, not on all. * * @param string $content String content to add, defaults to empty. */ if ( $current_page === 1 ) { $xml .= apply_filters( "wpseo_sitemap_{$type}_content", '' ); } $xml .= '</urlset>'; return $xml; } /** * Produce final XML output with debug information. * * @param string $sitemap Sitemap XML. * * @return string */ public function get_output( $sitemap ) { $output = '<?xml version="1.0" encoding="' . esc_attr( $this->output_charset ) . '"?>'; if ( $this->stylesheet ) { /** * Filter the stylesheet URL for the XML sitemap. * * @param string $stylesheet Stylesheet URL. */ $output .= apply_filters( 'wpseo_stylesheet_url', $this->stylesheet ) . "\n"; } $output .= $sitemap; $output .= "\n<!-- XML Sitemap generated by Yoast SEO -->"; return $output; } /** * Get charset for the output. * * @return string */ public function get_output_charset() { return $this->output_charset; } /** * Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet. * * @param string $stylesheet Full XML-stylesheet declaration. * * @return void */ public function set_stylesheet( $stylesheet ) { $this->stylesheet = $stylesheet; } /** * Build the `<sitemap>` tag for a given URL. * * @param array<string> $url Array of parts that make up this entry. * * @return string */ protected function sitemap_index_url( $url ) { $date = null; if ( ! empty( $url['lastmod'] ) ) { $date = YoastSEO()->helpers->date->format( $url['lastmod'] ); } $url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false ); $output = "\t<sitemap>\n"; $output .= "\t\t<loc>" . $url['loc'] . "</loc>\n"; $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n"; $output .= "\t</sitemap>\n"; return $output; } /** * Build the `<url>` tag for a given URL. * * Public access for backwards compatibility reasons. * * @param array<string> $url Array of parts that make up this entry. * * @return string */ public function sitemap_url( $url ) { $date = null; if ( ! empty( $url['mod'] ) ) { // Create a DateTime object date in the correct timezone. $date = YoastSEO()->helpers->date->format( $url['mod'] ); } $output = "\t<url>\n"; $output .= "\t\t<loc>" . $this->encode_and_escape( $url['loc'] ) . "</loc>\n"; $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n"; if ( empty( $url['images'] ) ) { $url['images'] = []; } foreach ( $url['images'] as $img ) { if ( empty( $img['src'] ) ) { continue; } $output .= "\t\t<image:image>\n"; $output .= "\t\t\t<image:loc>" . $this->encode_and_escape( $img['src'] ) . "</image:loc>\n"; $output .= "\t\t</image:image>\n"; } unset( $img ); $output .= "\t</url>\n"; /** * Filters the output for the sitemap URL tag. * * @param string $output The output for the sitemap url tag. * @param array $url The sitemap URL array on which the output is based. */ return apply_filters( 'wpseo_sitemap_url', $output, $url ); } /** * Ensure the URL is encoded per RFC3986 and correctly escaped for use in an XML sitemap. * * This method works around a two quirks in esc_url(): * 1. `esc_url()` leaves schema-relative URLs alone, while according to the sitemap specs, * the URL must always begin with a protocol. * 2. `esc_url()` escapes ampersands as `&` instead of the more common `&`. * According to the specs, `&` should be used, and even though this shouldn't * really make a difference in practice, to quote Jono: "I'd be nervous about & * given how many weird and wonderful things eat sitemaps", so better safe than sorry. * * @link https://www.sitemaps.org/protocol.html#xmlTagDefinitions * @link https://www.sitemaps.org/protocol.html#escaping * @link https://developer.wordpress.org/reference/functions/esc_url/ * * @param string $url URL to encode and escape. * * @return string */ protected function encode_and_escape( $url ) { $url = $this->encode_url_rfc3986( $url ); $url = esc_url( $url ); $url = str_replace( '&', '&', $url ); $url = str_replace( ''', ''', $url ); if ( strpos( $url, '//' ) === 0 ) { // Schema-relative URL for which esc_url() does not add a scheme. $url = 'http:' . $url; } return $url; } /** * Apply some best effort conversion to comply with RFC3986. * * @param string $url URL to encode. * * @return string */ protected function encode_url_rfc3986( $url ) { if ( filter_var( $url, FILTER_VALIDATE_URL ) ) { return $url; } $path = wp_parse_url( $url, PHP_URL_PATH ); if ( ! empty( $path ) && $path !== '/' ) { $encoded_path = explode( '/', $path ); // First decode the path, to prevent double encoding. $encoded_path = array_map( 'rawurldecode', $encoded_path ); $encoded_path = array_map( 'rawurlencode', $encoded_path ); $encoded_path = implode( '/', $encoded_path ); $url = str_replace( $path, $encoded_path, $url ); } $query = wp_parse_url( $url, PHP_URL_QUERY ); if ( ! empty( $query ) ) { parse_str( $query, $parsed_query ); $parsed_query = http_build_query( $parsed_query, '', '&', PHP_QUERY_RFC3986 ); $url = str_replace( $query, $parsed_query, $url ); } return $url; } /** * Retrieves the XSL URL that should be used in the current environment * * When home_url and site_url are not the same, the home_url should be used. * This is because the XSL needs to be served from the same domain, protocol and port * as the XML file that is loading it. * * @return string The XSL URL that needs to be used. */ protected function get_xsl_url() { if ( home_url() !== site_url() ) { return apply_filters( 'wpseo_sitemap_public_url', home_url( 'main-sitemap.xsl' ) ); } /* * Fallback to circumvent a cross-domain security problem when the XLS file is * loaded from a different (sub)domain. */ if ( strpos( plugins_url(), home_url() ) !== 0 ) { return home_url( 'main-sitemap.xsl' ); } return plugin_dir_url( WPSEO_FILE ) . 'css/main-sitemap.xsl'; } } sitemaps/class-author-sitemap-provider.php 0000644 00000013122 14720701066 0015001 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap provider for author archives. */ class WPSEO_Author_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { // If the author archives have been disabled, we don't do anything. if ( WPSEO_Options::get( 'disable-author', false ) || WPSEO_Options::get( 'noindex-author-wpseo', false ) ) { return false; } return $type === 'author'; } /** * Get the links for the sitemap index. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { if ( ! $this->handles_type( 'author' ) ) { return []; } // @todo Consider doing this less often / when necessary. R. $this->update_user_meta(); $has_exclude_filter = has_filter( 'wpseo_sitemap_exclude_author' ); $query_arguments = []; if ( ! $has_exclude_filter ) { // We only need full users if legacy filter(s) hooked to exclusion logic. R. $query_arguments['fields'] = 'ID'; } $users = $this->get_users( $query_arguments ); if ( $has_exclude_filter ) { $users = $this->exclude_users( $users ); $users = wp_list_pluck( $users, 'ID' ); } if ( empty( $users ) ) { return []; } $index = []; $user_pages = array_chunk( $users, $max_entries ); foreach ( $user_pages as $page_counter => $users_page ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); $user_id = array_shift( $users_page ); // Time descending, first user on page is most recently updated. $user = get_user_by( 'id', $user_id ); $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $current_page . '.xml' ), 'lastmod' => ( $user->_yoast_wpseo_profile_updated ) ? YoastSEO()->helpers->date->format_timestamp( $user->_yoast_wpseo_profile_updated ) : null, ]; } return $index; } /** * Retrieve users, taking account of all necessary exclusions. * * @param array $arguments Arguments to add. * * @return array */ protected function get_users( $arguments = [] ) { global $wpdb; $defaults = [ 'capability' => [ 'edit_posts' ], 'meta_key' => '_yoast_wpseo_profile_updated', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'meta_query' => [ 'relation' => 'AND', [ 'key' => $wpdb->get_blog_prefix() . 'user_level', 'value' => '0', 'compare' => '!=', ], [ 'relation' => 'OR', [ 'key' => 'wpseo_noindex_author', 'value' => 'on', 'compare' => '!=', ], [ 'key' => 'wpseo_noindex_author', 'compare' => 'NOT EXISTS', ], ], ], ]; if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) { unset( $defaults['capability'] ); // Otherwise it cancels out next argument. $defaults['has_published_posts'] = YoastSEO()->helpers->author_archive->get_author_archive_post_types(); } return get_users( array_merge( $defaults, $arguments ) ); } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = []; if ( ! $this->handles_type( 'author' ) ) { return $links; } $user_criteria = [ 'offset' => ( ( $current_page - 1 ) * $max_entries ), 'number' => $max_entries, ]; $users = $this->get_users( $user_criteria ); // Throw an exception when there are no users in the sitemap. if ( count( $users ) === 0 ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $users = $this->exclude_users( $users ); if ( empty( $users ) ) { $users = []; } $time = time(); foreach ( $users as $user ) { $author_link = get_author_posts_url( $user->ID ); if ( empty( $author_link ) ) { continue; } $mod = $time; if ( isset( $user->_yoast_wpseo_profile_updated ) ) { $mod = $user->_yoast_wpseo_profile_updated; } $url = [ 'loc' => $author_link, 'mod' => date( DATE_W3C, $mod ), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ]; /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'user', $user ); if ( ! empty( $url ) ) { $links[] = $url; } } return $links; } /** * Update any users that don't have last profile update timestamp. * * @return int Count of users updated. */ protected function update_user_meta() { $user_criteria = [ 'capability' => [ 'edit_posts' ], 'meta_query' => [ [ 'key' => '_yoast_wpseo_profile_updated', 'compare' => 'NOT EXISTS', ], ], ]; $users = get_users( $user_criteria ); $time = time(); foreach ( $users as $user ) { update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', $time ); } return count( $users ); } /** * Wrap legacy filter to deduplicate calls. * * @param array $users Array of user objects to filter. * * @return array */ protected function exclude_users( $users ) { /** * Filter the authors, included in XML sitemap. * * @param array $users Array of user objects to filter. */ return apply_filters( 'wpseo_sitemap_exclude_author', $users ); } } sitemaps/interface-sitemap-provider.php 0000644 00000001443 14720701066 0014337 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap Provider interface. */ interface WPSEO_Sitemap_Provider { /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ); /** * Get set of sitemaps index link data. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ); /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array */ public function get_sitemap_links( $type, $max_entries, $current_page ); } sitemaps/class-sitemap-image-parser.php 0000644 00000027264 14720701066 0014237 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Parses images from the given post. */ class WPSEO_Sitemap_Image_Parser { /** * Holds the home_url() value to speed up loops. * * @var string */ protected $home_url = ''; /** * Holds site URL hostname. * * @var string */ protected $host = ''; /** * Holds site URL protocol. * * @var string */ protected $scheme = 'http'; /** * Cached set of attachments for multiple posts. * * @var array */ protected $attachments = []; /** * Holds blog charset value for use in DOM parsing. * * @var string */ protected $charset = 'UTF-8'; /** * Set up URL properties for reuse. */ public function __construct() { $this->home_url = home_url(); $parsed_home = wp_parse_url( $this->home_url ); if ( ! empty( $parsed_home['host'] ) ) { $this->host = str_replace( 'www.', '', $parsed_home['host'] ); } if ( ! empty( $parsed_home['scheme'] ) ) { $this->scheme = $parsed_home['scheme']; } $this->charset = esc_attr( get_bloginfo( 'charset' ) ); } /** * Get set of image data sets for the given post. * * @param object $post Post object to get images for. * * @return array */ public function get_images( $post ) { $images = []; if ( ! is_object( $post ) ) { return $images; } $thumbnail_id = get_post_thumbnail_id( $post->ID ); if ( $thumbnail_id ) { $src = $this->get_absolute_url( $this->image_url( $thumbnail_id ) ); $images[] = $this->get_image_item( $post, $src ); } /** * Filter: 'wpseo_sitemap_content_before_parse_html_images' - Filters the post content * before it is parsed for images. * * @param string $content The raw/unprocessed post content. */ $content = apply_filters( 'wpseo_sitemap_content_before_parse_html_images', $post->post_content ); $unfiltered_images = $this->parse_html_images( $content ); foreach ( $unfiltered_images as $image ) { $images[] = $this->get_image_item( $post, $image['src'] ); } foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) { $src = $this->get_absolute_url( $this->image_url( $attachment->ID ) ); $images[] = $this->get_image_item( $post, $src ); } if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) { $src = $this->get_absolute_url( $this->image_url( $post->ID ) ); $images[] = $this->get_image_item( $post, $src ); } foreach ( $images as $key => $image ) { if ( empty( $image['src'] ) ) { unset( $images[ $key ] ); } } /** * Filter images to be included for the post in XML sitemap. * * @param array $images Array of image items. * @param int $post_id ID of the post. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages', $images, $post->ID ); if ( isset( $image_list ) && is_array( $image_list ) ) { $images = $image_list; } return $images; } /** * Get the images in the term description. * * @param object $term Term to get images from description for. * * @return array */ public function get_term_images( $term ) { $images = $this->parse_html_images( $term->description ); foreach ( $this->parse_galleries( $term->description ) as $attachment ) { $images[] = [ 'src' => $this->get_absolute_url( $this->image_url( $attachment->ID ) ), ]; } /** * Filter images to be included for the term in XML sitemap. * * @param array $image_list Array of image items. * @param int $term_id ID of the post. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages_term', $images, $term->term_id ); if ( isset( $image_list ) && is_array( $image_list ) ) { $images = $image_list; } return $images; } /** * Parse `<img />` tags in content. * * @param string $content Content string to parse. * * @return array */ private function parse_html_images( $content ) { $images = []; if ( ! class_exists( 'DOMDocument' ) ) { return $images; } if ( empty( $content ) ) { return $images; } // Prevent DOMDocument from bubbling warnings about invalid HTML. libxml_use_internal_errors( true ); $post_dom = new DOMDocument(); $post_dom->loadHTML( '<?xml encoding="' . $this->charset . '">' . $content ); // Clear the errors, so they don't get kept in memory. libxml_clear_errors(); /** * Image attribute. * * @var DOMElement $img */ foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { $src = $img->getAttribute( 'src' ); if ( empty( $src ) ) { continue; } $class = $img->getAttribute( 'class' ); if ( // This detects WP-inserted images, which we need to upsize. R. ! empty( $class ) && ( strpos( $class, 'size-full' ) === false ) && preg_match( '|wp-image-(?P<id>\d+)|', $class, $matches ) && get_post_status( $matches['id'] ) ) { $query_params = wp_parse_url( $src, PHP_URL_QUERY ); $src = $this->image_url( $matches['id'] ); if ( $query_params ) { $src .= '?' . $query_params; } } $src = $this->get_absolute_url( $src ); if ( strpos( $src, $this->host ) === false ) { continue; } if ( $src !== esc_url( $src, null, 'attribute' ) ) { continue; } $images[] = [ 'src' => $src, ]; } return $images; } /** * Parse gallery shortcodes in a given content. * * @param string $content Content string. * @param int $post_id Optional. ID of post being parsed. * * @return array Set of attachment objects. */ protected function parse_galleries( $content, $post_id = 0 ) { $attachments = []; $galleries = $this->get_content_galleries( $content ); foreach ( $galleries as $gallery ) { $id = $post_id; if ( ! empty( $gallery['id'] ) ) { $id = intval( $gallery['id'] ); } // Forked from core gallery_shortcode() to have exact same logic. R. if ( ! empty( $gallery['ids'] ) ) { $gallery['include'] = $gallery['ids']; } $gallery_attachments = $this->get_gallery_attachments( $id, $gallery ); $attachments = array_merge( $attachments, $gallery_attachments ); } return array_unique( $attachments, SORT_REGULAR ); } /** * Retrieves galleries from the passed content. * * Forked from core to skip executing shortcodes for performance. * * @param string $content Content to parse for shortcodes. * * @return array A list of arrays, each containing gallery data. */ protected function get_content_galleries( $content ) { $galleries = []; if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) { return $galleries; } foreach ( $matches as $shortcode ) { $attributes = shortcode_parse_atts( $shortcode[3] ); if ( $attributes === '' ) { // Valid shortcode without any attributes. R. $attributes = []; } $galleries[] = $attributes; } return $galleries; } /** * Get image item array with filters applied. * * @param WP_Post $post Post object for the context. * @param string $src Image URL. * * @return array */ protected function get_image_item( $post, $src ) { $image = []; /** * Filter image URL to be included in XML sitemap for the post. * * @param string $src Image URL. * @param object $post Post object. */ $image['src'] = apply_filters( 'wpseo_xml_sitemap_img_src', $src, $post ); /** * Filter image data to be included in XML sitemap for the post. * * @param array $image { * Array of image data. * * @type string $src Image URL. * } * * @param object $post Post object. */ return apply_filters( 'wpseo_xml_sitemap_img', $image, $post ); } /** * Get attached image URL with filters applied. Adapted from core for speed. * * @param int $post_id ID of the post. * * @return string */ private function image_url( $post_id ) { static $uploads; if ( empty( $uploads ) ) { $uploads = wp_upload_dir(); } if ( $uploads['error'] !== false ) { return ''; } $file = get_post_meta( $post_id, '_wp_attached_file', true ); if ( empty( $file ) ) { return ''; } // Check that the upload base exists in the file location. if ( strpos( $file, $uploads['basedir'] ) === 0 ) { $src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); } elseif ( strpos( $file, 'wp-content/uploads' ) !== false ) { $src = $uploads['baseurl'] . substr( $file, ( strpos( $file, 'wp-content/uploads' ) + 18 ) ); } else { // It's a newly uploaded file, therefore $file is relative to the baseurl. $src = $uploads['baseurl'] . '/' . $file; } return apply_filters( 'wp_get_attachment_url', $src, $post_id ); } /** * Make absolute URL for domain or protocol-relative one. * * @param string $src URL to process. * * @return string */ protected function get_absolute_url( $src ) { if ( empty( $src ) || ! is_string( $src ) ) { return $src; } if ( YoastSEO()->helpers->url->is_relative( $src ) === true ) { if ( $src[0] !== '/' ) { return $src; } // The URL is relative, we'll have to make it absolute. return $this->home_url . $src; } if ( strpos( $src, 'http' ) !== 0 ) { // Protocol relative URL, we add the scheme as the standard requires a protocol. return $this->scheme . ':' . $src; } return $src; } /** * Returns the attachments for a gallery. * * @param int $id The post ID. * @param array $gallery The gallery config. * * @return array The selected attachments. */ protected function get_gallery_attachments( $id, $gallery ) { // When there are attachments to include. if ( ! empty( $gallery['include'] ) ) { return $this->get_gallery_attachments_for_included( $gallery['include'] ); } // When $id is empty, just return empty array. if ( empty( $id ) ) { return []; } return $this->get_gallery_attachments_for_parent( $id, $gallery ); } /** * Returns the attachments for the given ID. * * @param int $id The post ID. * @param array $gallery The gallery config. * * @return array The selected attachments. */ protected function get_gallery_attachments_for_parent( $id, $gallery ) { $query = [ 'posts_per_page' => -1, 'post_parent' => $id, ]; // When there are posts that should be excluded from result set. if ( ! empty( $gallery['exclude'] ) ) { $query['post__not_in'] = wp_parse_id_list( $gallery['exclude'] ); } return $this->get_attachments( $query ); } /** * Returns an array with attachments for the post IDs that will be included. * * @param array $included_ids Array with IDs to include. * * @return array The found attachments. */ protected function get_gallery_attachments_for_included( $included_ids ) { $ids_to_include = wp_parse_id_list( $included_ids ); $attachments = $this->get_attachments( [ 'posts_per_page' => count( $ids_to_include ), 'post__in' => $ids_to_include, ] ); $gallery_attachments = []; foreach ( $attachments as $val ) { $gallery_attachments[ $val->ID ] = $val; } return $gallery_attachments; } /** * Returns the attachments. * * @param array $args Array with query args. * * @return array The found attachments. */ protected function get_attachments( $args ) { $default_args = [ 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', // Defaults taken from function get_posts. 'orderby' => 'date', 'order' => 'DESC', 'meta_key' => '', 'meta_value' => '', 'suppress_filters' => true, 'ignore_sticky_posts' => true, 'no_found_rows' => true, ]; $args = wp_parse_args( $args, $default_args ); $get_attachments = new WP_Query(); return $get_attachments->query( $args ); } } sitemaps/class-sitemaps-cache.php 0000644 00000020423 14720701066 0013077 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Handles sitemaps caching and invalidation. * * @since 3.2 */ class WPSEO_Sitemaps_Cache { /** * Holds the options that, when updated, should cause the cache to clear. * * @var array */ protected static $cache_clear = []; /** * Mirror of enabled status for static calls. * * @var bool */ protected static $is_enabled = false; /** * Holds the flag to clear all cache. * * @var bool */ protected static $clear_all = false; /** * Holds the array of types to clear. * * @var array */ protected static $clear_types = []; /** * Hook methods for invalidation on necessary events. */ public function __construct() { add_action( 'init', [ $this, 'init' ] ); add_action( 'deleted_term_relationships', [ self::class, 'invalidate' ] ); add_action( 'update_option', [ self::class, 'clear_on_option_update' ] ); add_action( 'edited_terms', [ self::class, 'invalidate_helper' ], 10, 2 ); add_action( 'clean_term_cache', [ self::class, 'invalidate_helper' ], 10, 2 ); add_action( 'clean_object_term_cache', [ self::class, 'invalidate_helper' ], 10, 2 ); add_action( 'user_register', [ self::class, 'invalidate_author' ] ); add_action( 'delete_user', [ self::class, 'invalidate_author' ] ); add_action( 'shutdown', [ self::class, 'clear_queued' ] ); } /** * Setup context for static calls. * * @return void */ public function init() { self::$is_enabled = $this->is_enabled(); } /** * If cache is enabled. * * @since 3.2 * * @return bool */ public function is_enabled() { /** * Filter if XML sitemap transient cache is enabled. * * @param bool $unsigned Enable cache or not, defaults to true. */ return apply_filters( 'wpseo_enable_xml_sitemap_transient_caching', false ); } /** * Retrieve the sitemap page from cache. * * @since 3.2 * * @param string $type Sitemap type. * @param int $page Page number to retrieve. * * @return string|bool */ public function get_sitemap( $type, $page ) { $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); if ( $transient_key === false ) { return false; } return get_transient( $transient_key ); } /** * Get the sitemap that is cached. * * @param string $type Sitemap type. * @param int $page Page number to retrieve. * * @return WPSEO_Sitemap_Cache_Data|null Null on no cache found otherwise object containing sitemap and meta data. */ public function get_sitemap_data( $type, $page ) { $sitemap = $this->get_sitemap( $type, $page ); if ( empty( $sitemap ) ) { return null; } /* * Unserialize Cache Data object as is_serialized() doesn't recognize classes in C format. * This work-around should no longer be needed once the minimum PHP version has gone up to PHP 7.4, * as the `WPSEO_Sitemap_Cache_Data` class uses O format serialization in PHP 7.4 and higher. * * @link https://wiki.php.net/rfc/custom_object_serialization */ if ( is_string( $sitemap ) && strpos( $sitemap, 'C:24:"WPSEO_Sitemap_Cache_Data"' ) === 0 ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Can't be avoided due to how WP stores options. $sitemap = unserialize( $sitemap ); } // What we expect it to be if it is set. if ( $sitemap instanceof WPSEO_Sitemap_Cache_Data_Interface ) { return $sitemap; } return null; } /** * Store the sitemap page from cache. * * @since 3.2 * * @param string $type Sitemap type. * @param int $page Page number to store. * @param string $sitemap Sitemap body to store. * @param bool $usable Is this a valid sitemap or a cache of an invalid sitemap. * * @return bool */ public function store_sitemap( $type, $page, $sitemap, $usable = true ) { $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); if ( $transient_key === false ) { return false; } $status = ( $usable ) ? WPSEO_Sitemap_Cache_Data::OK : WPSEO_Sitemap_Cache_Data::ERROR; $sitemap_data = new WPSEO_Sitemap_Cache_Data(); $sitemap_data->set_sitemap( $sitemap ); $sitemap_data->set_status( $status ); return set_transient( $transient_key, $sitemap_data, DAY_IN_SECONDS ); } /** * Delete cache transients for index and specific type. * * Always deletes the main index sitemaps cache, as that's always invalidated by any other change. * * @since 1.5.4 * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache() to method in this class. * * @param string $type Sitemap type to invalidate. * * @return void */ public static function invalidate( $type ) { self::clear( [ $type ] ); } /** * Helper to invalidate in hooks where type is passed as second argument. * * @since 3.2 * * @param int $unused Unused term ID value. * @param string $type Taxonomy to invalidate. * * @return void */ public static function invalidate_helper( $unused, $type ) { if ( WPSEO_Options::get( 'noindex-' . $type ) === false || WPSEO_Options::get( 'noindex-tax-' . $type ) === false ) { self::invalidate( $type ); } } /** * Invalidate sitemap cache for authors. * * @param int $user_id User ID. * * @return bool True if the sitemap was properly invalidated. False otherwise. */ public static function invalidate_author( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( $user === false ) { return false; } if ( current_action() === 'user_register' ) { update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() ); } if ( empty( $user->roles ) || in_array( 'subscriber', $user->roles, true ) ) { return false; } self::invalidate( 'author' ); return true; } /** * Invalidate sitemap cache for the post type of a post. * * Don't invalidate for revisions. * * @since 1.5.4 * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache_on_save_post() to method in this class. * * @param int $post_id Post ID to invalidate type for. * * @return void */ public static function invalidate_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ) { return; } self::invalidate( get_post_type( $post_id ) ); } /** * Delete cache transients for given sitemaps types or all by default. * * @since 1.8.0 * @since 3.2 Moved from WPSEO_Utils to this class. * * @param array $types Set of sitemap types to delete cache transients for. * * @return void */ public static function clear( $types = [] ) { if ( ! self::$is_enabled ) { return; } // No types provided, clear all. if ( empty( $types ) ) { self::$clear_all = true; return; } // Always invalidate the index sitemap as well. if ( ! in_array( WPSEO_Sitemaps::SITEMAP_INDEX_TYPE, $types, true ) ) { array_unshift( $types, WPSEO_Sitemaps::SITEMAP_INDEX_TYPE ); } foreach ( $types as $type ) { if ( ! in_array( $type, self::$clear_types, true ) ) { self::$clear_types[] = $type; } } } /** * Invalidate storage for cache types queued to clear. * * @return void */ public static function clear_queued() { if ( self::$clear_all ) { WPSEO_Sitemaps_Cache_Validator::invalidate_storage(); self::$clear_all = false; self::$clear_types = []; return; } foreach ( self::$clear_types as $type ) { WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type ); } self::$clear_types = []; } /** * Adds a hook that when given option is updated, the cache is cleared. * * @since 3.2 * * @param string $option Option name. * @param string $type Sitemap type. * * @return void */ public static function register_clear_on_option_update( $option, $type = '' ) { self::$cache_clear[ $option ] = $type; } /** * Clears the transient cache when a given option is updated, if that option has been registered before. * * @since 3.2 * * @param string $option The option name that's being updated. * * @return void */ public static function clear_on_option_update( $option ) { if ( array_key_exists( $option, self::$cache_clear ) ) { if ( empty( self::$cache_clear[ $option ] ) ) { // Clear all caches. self::clear(); } else { // Clear specific provided type(s). $types = (array) self::$cache_clear[ $option ]; self::clear( $types ); } } } } sitemaps/class-sitemap-cache-data.php 0000644 00000011376 14720701066 0013632 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap Cache Data object, manages sitemap data stored in cache. */ class WPSEO_Sitemap_Cache_Data implements Serializable, WPSEO_Sitemap_Cache_Data_Interface { /** * Sitemap XML data. * * @var string */ private $sitemap = ''; /** * Status of the sitemap, usable or not. * * @var string */ private $status = self::UNKNOWN; /** * Set the sitemap XML data * * @param string $sitemap XML Content of the sitemap. * * @return void */ public function set_sitemap( $sitemap ) { if ( ! is_string( $sitemap ) ) { $sitemap = ''; } $this->sitemap = $sitemap; /* * Empty sitemap is not usable. */ if ( ! empty( $sitemap ) ) { $this->set_status( self::OK ); } else { $this->set_status( self::ERROR ); } } /** * Set the status of the sitemap, is it usable. * * @param bool|string $usable Is the sitemap usable or not. * * @return void */ public function set_status( $usable ) { if ( $usable === self::OK ) { $this->status = self::OK; return; } if ( $usable === self::ERROR ) { $this->status = self::ERROR; $this->sitemap = ''; return; } $this->status = self::UNKNOWN; } /** * Is the sitemap usable. * * @return bool True if usable, False if bad or unknown. */ public function is_usable() { return $this->status === self::OK; } /** * Get the XML content of the sitemap. * * @return string The content of the sitemap. */ public function get_sitemap() { return $this->sitemap; } /** * Get the status of the sitemap. * * @return string Status of the sitemap, 'ok'/'error'/'unknown'. */ public function get_status() { return $this->status; } /** * String representation of object. * * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} * * @link https://www.php.net/language.oop5.magic#object.serialize * @link https://wiki.php.net/rfc/custom_object_serialization * * @since 17.8.0 * * @return array The data to be serialized. */ public function __serialize() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound $data = [ 'status' => $this->status, 'xml' => $this->sitemap, ]; return $data; } /** * Constructs the object. * * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} * * @link https://www.php.net/language.oop5.magic#object.serialize * @link https://wiki.php.net/rfc/custom_object_serialization * * @since 17.8.0 * * @param array $data The unserialized data to use to (re)construct the object. * * @return void */ public function __unserialize( $data ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound $this->set_sitemap( $data['xml'] ); $this->set_status( $data['status'] ); } /** * String representation of object. * * {@internal The magic methods take precedence over the Serializable interface. * This means that in practice, this method will now only be called on PHP < 7.4. * For PHP 7.4 and higher, the magic methods will be used instead.} * * {@internal The Serializable interface is being phased out, in favour of the magic methods. * This method should be deprecated and removed and the class should no longer * implement the `Serializable` interface. * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} * * @link http://php.net/manual/en/serializable.serialize.php * @link https://wiki.php.net/rfc/phase_out_serializable * * @since 5.1.0 * * @return string The string representation of the object or null in C-format. */ public function serialize() { return serialize( $this->__serialize() ); } /** * Constructs the object. * * {@internal The magic methods take precedence over the Serializable interface. * This means that in practice, this method will now only be called on PHP < 7.4. * For PHP 7.4 and higher, the magic methods will be used instead.} * * {@internal The Serializable interface is being phased out, in favour of the magic methods. * This method should be deprecated and removed and the class should no longer * implement the `Serializable` interface. * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} * * @link http://php.net/manual/en/serializable.unserialize.php * @link https://wiki.php.net/rfc/phase_out_serializable * * @since 5.1.0 * * @param string $data The string representation of the object in C or O-format. * * @return void */ public function unserialize( $data ) { $data = unserialize( $data ); $this->__unserialize( $data ); } } sitemaps/class-sitemaps.php 0000644 00000040503 14720701066 0012037 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Class WPSEO_Sitemaps. * * @todo This class could use a general description with some explanation on sitemaps. OR. */ class WPSEO_Sitemaps { /** * Sitemap index identifier. * * @var string */ public const SITEMAP_INDEX_TYPE = '1'; /** * Content of the sitemap to output. * * @var string */ protected $sitemap = ''; /** * Flag to indicate if this is an invalid or empty sitemap. * * @var bool */ public $bad_sitemap = false; /** * Whether or not the XML sitemap was served from a transient or not. * * @var bool */ private $transient = false; /** * HTTP protocol to use in headers. * * @since 3.2 * * @var string */ protected $http_protocol = 'HTTP/1.1'; /** * Holds the n variable. * * @var int */ private $current_page = 1; /** * The sitemaps router. * * @since 3.2 * * @var WPSEO_Sitemaps_Router */ public $router; /** * The sitemap renderer. * * @since 3.2 * * @var WPSEO_Sitemaps_Renderer */ public $renderer; /** * The sitemap cache. * * @since 3.2 * * @var WPSEO_Sitemaps_Cache */ public $cache; /** * The sitemap providers. * * @since 3.2 * * @var WPSEO_Sitemap_Provider[] */ public $providers; /** * Class constructor. */ public function __construct() { add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] ); add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 ); add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 ); add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] ); $this->router = new WPSEO_Sitemaps_Router(); $this->renderer = new WPSEO_Sitemaps_Renderer(); $this->cache = new WPSEO_Sitemaps_Cache(); if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) { $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); } } /** * Initialize sitemap providers classes. * * @since 5.3 * * @return void */ public function init_sitemaps_providers() { $this->providers = [ new WPSEO_Post_Type_Sitemap_Provider(), new WPSEO_Taxonomy_Sitemap_Provider(), new WPSEO_Author_Sitemap_Provider(), ]; $external_providers = apply_filters( 'wpseo_sitemaps_providers', [] ); foreach ( $external_providers as $provider ) { if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) { $this->providers[] = $provider; } } } /** * Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets. * * @return void */ public function reduce_query_load() { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return; } $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); $extension = substr( $request_uri, -4 ); if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) { remove_all_actions( 'widgets_init' ); } } /** * Register your own sitemap. Call this during 'init'. * * @param string $name The name of the sitemap. * @param callback $building_function Function to build your sitemap. * @param string $rewrite Optional. Regular expression to match your sitemap with. * * @return void */ public function register_sitemap( $name, $building_function, $rewrite = '' ) { add_action( 'wpseo_do_sitemap_' . $name, $building_function ); if ( $rewrite ) { Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' ); } } /** * Register your own XSL file. Call this during 'init'. * * @since 1.4.23 * * @param string $name The name of the XSL file. * @param callback $building_function Function to build your XSL file. * @param string $rewrite Optional. Regular expression to match your sitemap with. * * @return void */ public function register_xsl( $name, $building_function, $rewrite = '' ) { add_action( 'wpseo_xsl_' . $name, $building_function ); if ( $rewrite ) { Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' ); } } /** * Set the sitemap current page to allow creating partial sitemaps with WP-CLI * in a one-off process. * * @param int $current_page The part that should be generated. * * @return void */ public function set_n( $current_page ) { if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) { $this->current_page = intval( $current_page ); } } /** * Set the sitemap content to display after you have generated it. * * @param string $sitemap The generated sitemap to output. * * @return void */ public function set_sitemap( $sitemap ) { $this->sitemap = $sitemap; } /** * Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests. * * @param bool $is_bad Is this a bad request. True or false. * * @return void */ public function set_bad_sitemap( $is_bad ) { $this->bad_sitemap = (bool) $is_bad; } /** * Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML. * * @since 1.4.16 * * @return void */ public function sitemap_close() { remove_all_actions( 'wp_footer' ); die(); } /** * Hijack requests for potential sitemaps and XSL files. * * @param WP_Query $query Main query instance. * * @return void */ public function redirect( $query ) { if ( ! $query->is_main_query() ) { return; } $yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' ); if ( ! empty( $yoast_sitemap_xsl ) ) { /* * This is a method to provide the XSL via the home_url. * Needed when the site_url and home_url are not the same. * Loading the XSL needs to come from the same domain, protocol and port as the XML. * * Whenever home_url and site_url are the same, the file can be loaded directly. */ $this->xsl_output( $yoast_sitemap_xsl ); $this->sitemap_close(); return; } $type = get_query_var( 'sitemap' ); if ( empty( $type ) ) { return; } if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) { wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' ); exit; } $this->set_n( get_query_var( 'sitemap_n' ) ); if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) { $this->build_sitemap( $type ); } if ( $this->bad_sitemap ) { $query->set_404(); status_header( 404 ); return; } $this->output(); $this->sitemap_close(); } /** * Try to get the sitemap from cache. * * @param string $type Sitemap type. * @param int $page_number The page number to retrieve. * * @return bool If the sitemap has been retrieved from cache. */ private function get_sitemap_from_cache( $type, $page_number ) { $this->transient = false; if ( $this->cache->is_enabled() !== true ) { return false; } /** * Fires before the attempt to retrieve XML sitemap from the transient cache. * * @param WPSEO_Sitemaps $sitemaps Sitemaps object. */ do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this ); $sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number ); // No cache was found, refresh it because cache is enabled. if ( empty( $sitemap_cache_data ) ) { return $this->refresh_sitemap_cache( $type, $page_number ); } // Cache object was found, parse information. $this->transient = true; $this->sitemap = $sitemap_cache_data->get_sitemap(); $this->bad_sitemap = ! $sitemap_cache_data->is_usable(); return true; } /** * Build and save sitemap to cache. * * @param string $type Sitemap type. * @param int $page_number The page number to save to. * * @return bool */ private function refresh_sitemap_cache( $type, $page_number ) { $this->set_n( $page_number ); $this->build_sitemap( $type ); return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap ); } /** * Attempts to build the requested sitemap. * * Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy. * * @param string $type The requested sitemap's identifier. * * @return void */ public function build_sitemap( $type ) { /** * Filter the type of sitemap to build. * * @param string $type Sitemap type, determined by the request. */ $type = apply_filters( 'wpseo_build_sitemap_post_type', $type ); if ( $type === '1' ) { $this->build_root_map(); return; } $entries_per_page = $this->get_entries_per_page(); foreach ( $this->providers as $provider ) { if ( ! $provider->handles_type( $type ) ) { continue; } try { $links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page ); } catch ( OutOfBoundsException $exception ) { $this->bad_sitemap = true; return; } $this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page ); return; } if ( has_action( 'wpseo_do_sitemap_' . $type ) ) { /** * Fires custom handler, if hooked to generate sitemap for the type. */ do_action( 'wpseo_do_sitemap_' . $type ); return; } $this->bad_sitemap = true; } /** * Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types. * * @return void */ public function build_root_map() { $links = []; $entries_per_page = $this->get_entries_per_page(); foreach ( $this->providers as $provider ) { $links = array_merge( $links, $provider->get_index_links( $entries_per_page ) ); } /** * Filter the sitemap links array before the index sitemap is built. * * @param array $links Array of sitemap links */ $links = apply_filters( 'wpseo_sitemap_index_links', $links ); if ( empty( $links ) ) { $this->bad_sitemap = true; $this->sitemap = ''; return; } $this->sitemap = $this->renderer->get_index( $links ); } /** * Spits out the XSL for the XML sitemap. * * @since 1.4.13 * * @param string $type Type to output. * * @return void */ public function xsl_output( $type ) { if ( $type !== 'main' ) { /** * Fires for the output of XSL for XML sitemaps, other than type "main". */ do_action( 'wpseo_xsl_' . $type ); return; } header( $this->http_protocol . ' 200 OK', true, 200 ); // Prevent the search engines from indexing the XML Sitemap. header( 'X-Robots-Tag: noindex, follow', true ); header( 'Content-Type: text/xml' ); // Make the browser cache this file properly. $expires = YEAR_IN_SECONDS; header( 'Pragma: public' ); header( 'Cache-Control: max-age=' . $expires ); header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' ); // Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043. readfile( WPSEO_PATH . 'css/main-sitemap.xsl' ); } /** * Spit out the generated sitemap. * * @return void */ public function output() { $this->send_headers(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document. echo $this->renderer->get_output( $this->sitemap ); } /** * Makes a request to the sitemap index to cache it before the arrival of the search engines. * * @return void */ public function hit_sitemap_index() { if ( ! $this->cache->is_enabled() ) { return; } wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ); } /** * Get the GMT modification date for the last modified post in the post type. * * @since 3.2 * * @param string|array $post_types Post type or array of types. * @param bool $return_all Flag to return array of values. * * @return string|array|false */ public static function get_last_modified_gmt( $post_types, $return_all = false ) { global $wpdb; static $post_type_dates = null; if ( ! is_array( $post_types ) ) { $post_types = [ $post_types ]; } foreach ( $post_types as $post_type ) { if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R. $post_type_dates = null; break; } } if ( is_null( $post_type_dates ) ) { $post_type_dates = []; $post_type_names = WPSEO_Post_Type::get_accessible_post_types(); if ( ! empty( $post_type_names ) ) { $post_statuses = array_map( 'esc_sql', self::get_post_statuses() ); $replacements = array_merge( [ 'post_type', 'post_modified_gmt', 'date', $wpdb->posts, 'post_status', ], $post_statuses, [ 'post_type' ], array_keys( $post_type_names ), [ 'post_type', 'date', ] ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $dates = $wpdb->get_results( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT %i, MAX(%i) AS %i FROM %i WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i IN (' . implode( ', ', array_fill( 0, count( $post_type_names ), '%s' ) ) . ') GROUP BY %i ORDER BY %i DESC ', $replacements ) ); foreach ( $dates as $obj ) { $post_type_dates[ $obj->post_type ] = $obj->date; } } } $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) ); if ( count( $dates ) > 0 ) { if ( $return_all ) { return $dates; } return max( $dates ); } return false; } /** * Get the modification date for the last modified post in the post type. * * @param array $post_types Post types to get the last modification date for. * * @return string */ public function get_last_modified( $post_types ) { return YoastSEO()->helpers->date->format( self::get_last_modified_gmt( $post_types ) ); } /** * Get the maximum number of entries per XML sitemap. * * @return int The maximum number of entries. */ protected function get_entries_per_page() { /** * Filter the maximum number of entries per XML sitemap. * * After changing the output of the filter, make sure that you disable and enable the * sitemaps to make sure the value is picked up for the sitemap cache. * * @param int $entries The maximum number of entries per XML sitemap. */ $entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 ); return $entries; } /** * Get post statuses for post_type or the root sitemap. * * @since 10.2 * * @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap. * * @return array List of post statuses. */ public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) { /** * Filter post status list for sitemap query for the post type. * * @param array $post_statuses Post status list, defaults to array( 'publish' ). * @param string $type Post type or SITEMAP_INDEX_TYPE. */ $post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type ); if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) { $post_statuses = [ 'publish' ]; } if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' ) && ! in_array( 'inherit', $post_statuses, true ) ) { $post_statuses[] = 'inherit'; } return $post_statuses; } /** * Sends all the required HTTP Headers. * * @return void */ private function send_headers() { if ( headers_sent() ) { return; } $headers = [ $this->http_protocol . ' 200 OK' => 200, // Prevent the search engines from indexing the XML Sitemap. 'X-Robots-Tag: noindex, follow' => '', 'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '', ]; /** * Filter the HTTP headers we send before an XML sitemap. * * @param array $headers The HTTP headers we're going to send out. */ $headers = apply_filters( 'wpseo_sitemap_http_headers', $headers ); foreach ( $headers as $header => $status ) { if ( is_numeric( $status ) ) { header( $header, true, $status ); continue; } header( $header, true ); } } } sitemaps/interface-sitemap-cache-data.php 0000644 00000002315 14720701066 0014456 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Cache Data interface. */ interface WPSEO_Sitemap_Cache_Data_Interface { /** * Status for normal, usable sitemap. * * @var string */ public const OK = 'ok'; /** * Status for unusable sitemap. * * @var string */ public const ERROR = 'error'; /** * Status for unusable sitemap because it cannot be identified. * * @var string */ public const UNKNOWN = 'unknown'; /** * Set the content of the sitemap. * * @param string $sitemap The XML content of the sitemap. * * @return void */ public function set_sitemap( $sitemap ); /** * Set the status of the sitemap. * * @param bool|string $usable True/False or 'ok'/'error' for status. * * @return void */ public function set_status( $usable ); /** * Builds the sitemap. * * @return string The XML content of the sitemap. */ public function get_sitemap(); /** * Get the status of this sitemap. * * @return string Status 'ok', 'error' or 'unknown'. */ public function get_status(); /** * Is the sitemap content usable ? * * @return bool True if the sitemap is usable, False if not. */ public function is_usable(); } sitemaps/class-post-type-sitemap-provider.php 0000644 00000046607 14720701066 0015461 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ use Yoast\WP\SEO\Models\SEO_Links; /** * Sitemap provider for author archives. */ class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Holds image parser instance. * * @var WPSEO_Sitemap_Image_Parser */ protected static $image_parser; /** * Holds the parsed home url. * * @var array */ protected static $parsed_home_url; /** * Determines whether images should be included in the XML sitemap. * * @var bool */ private $include_images; /** * Set up object properties for data reuse. */ public function __construct() { add_action( 'save_post', [ $this, 'save_post' ] ); /** * Filter - Allows excluding images from the XML sitemap. * * @param bool $include True to include, false to exclude. */ $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); } /** * Get the Image Parser. * * @return WPSEO_Sitemap_Image_Parser */ protected function get_image_parser() { if ( ! isset( self::$image_parser ) ) { self::$image_parser = new WPSEO_Sitemap_Image_Parser(); } return self::$image_parser; } /** * Gets the parsed home url. * * @return array The home url, as parsed by wp_parse_url. */ protected function get_parsed_home_url() { if ( ! isset( self::$parsed_home_url ) ) { self::$parsed_home_url = wp_parse_url( home_url() ); } return self::$parsed_home_url; } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { return post_type_exists( $type ); } /** * Retrieves the sitemap links. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { global $wpdb; $post_types = WPSEO_Post_Type::get_accessible_post_types(); $post_types = array_filter( $post_types, [ $this, 'is_valid_post_type' ] ); $last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true ); $index = []; foreach ( $post_types as $post_type ) { $total_count = $this->get_post_type_count( $post_type ); if ( $total_count === 0 ) { continue; } $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $all_dates = []; if ( $max_pages > 1 ) { $all_dates = version_compare( $wpdb->db_version(), '8.0', '>=' ) ? $this->get_all_dates_using_with_clause( $post_type, $max_entries ) : $this->get_all_dates( $post_type, $max_entries ); } for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); $date = false; if ( empty( $current_page ) || $current_page === $max_pages ) { if ( ! empty( $last_modified_times[ $post_type ] ) ) { $date = $last_modified_times[ $post_type ]; } } else { $date = $all_dates[ $page_counter ]; } $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ]; } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = []; $post_type = $type; if ( ! $this->is_valid_post_type( $post_type ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $steps = min( 100, $max_entries ); $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; $total = ( $offset + $max_entries ); $post_type_entries = $this->get_post_type_count( $post_type ); if ( $total > $post_type_entries ) { $total = $post_type_entries; } if ( $current_page === 1 ) { $links = array_merge( $links, $this->get_first_links( $post_type ) ); } // If total post type count is lower than the offset, an invalid page is requested. if ( $post_type_entries < $offset ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } if ( $post_type_entries === 0 ) { return $links; } $posts_to_exclude = $this->get_excluded_posts( $type ); while ( $total > $offset ) { $posts = $this->get_posts( $post_type, $steps, $offset ); $offset += $steps; if ( empty( $posts ) ) { continue; } foreach ( $posts as $post ) { if ( in_array( $post->ID, $posts_to_exclude, true ) ) { continue; } if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $post->ID ) === '1' ) { continue; } $url = $this->get_url( $post ); if ( ! isset( $url['loc'] ) ) { continue; } /** * Filter URL entry before it gets added to the sitemap. * * @param array $url Array of URL parts. * @param string $type URL type. * @param object $post Data object for the URL. */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'post', $post ); if ( ! empty( $url ) ) { $links[] = $url; } } unset( $post, $url ); } return $links; } /** * Check for relevant post type before invalidation. * * @param int $post_id Post ID to possibly invalidate for. * * @return void */ public function save_post( $post_id ) { if ( $this->is_valid_post_type( get_post_type( $post_id ) ) ) { WPSEO_Sitemaps_Cache::invalidate_post( $post_id ); } } /** * Check if post type should be present in sitemaps. * * @param string $post_type Post type string to check for. * * @return bool */ public function is_valid_post_type( $post_type ) { if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) || ! WPSEO_Post_Type::is_post_type_indexable( $post_type ) ) { return false; } /** * Filter decision if post type is excluded from the XML sitemap. * * @param bool $exclude Default false. * @param string $post_type Post type name. */ if ( apply_filters( 'wpseo_sitemap_exclude_post_type', false, $post_type ) ) { return false; } return true; } /** * Retrieves a list with the excluded post ids. * * @param string $post_type Post type. * * @return array Array with post ids to exclude. */ protected function get_excluded_posts( $post_type ) { $excluded_posts_ids = []; $page_on_front_id = ( $post_type === 'page' ) ? (int) get_option( 'page_on_front' ) : 0; if ( $page_on_front_id > 0 ) { $excluded_posts_ids[] = $page_on_front_id; } /** * Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude. * * @param array $posts_to_exclude The posts to exclude. */ $excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', $excluded_posts_ids ); if ( ! is_array( $excluded_posts_ids ) ) { $excluded_posts_ids = []; } $excluded_posts_ids = array_map( 'intval', $excluded_posts_ids ); $page_for_posts_id = ( $post_type === 'page' ) ? (int) get_option( 'page_for_posts' ) : 0; if ( $page_for_posts_id > 0 ) { $excluded_posts_ids[] = $page_for_posts_id; } return array_unique( $excluded_posts_ids ); } /** * Get count of posts for post type. * * @param string $post_type Post type to retrieve count for. * * @return int */ protected function get_post_type_count( $post_type ) { global $wpdb; /** * Filter JOIN query part for type count of post type. * * @param string $join SQL part, defaults to empty string. * @param string $post_type Post type name. */ $join_filter = apply_filters( 'wpseo_typecount_join', '', $post_type ); /** * Filter WHERE query part for type count of post type. * * @param string $where SQL part, defaults to empty string. * @param string $post_type Post type name. */ $where_filter = apply_filters( 'wpseo_typecount_where', '', $post_type ); $where = $this->get_sql_where_clause( $post_type ); $sql = " SELECT COUNT({$wpdb->posts}.ID) FROM {$wpdb->posts} {$join_filter} {$where} {$where_filter} "; return (int) $wpdb->get_var( $sql ); } /** * Produces set of links to prepend at start of first sitemap page. * * @param string $post_type Post type to produce links for. * * @return array */ protected function get_first_links( $post_type ) { $links = []; $archive_url = false; if ( $post_type === 'page' ) { $page_on_front_id = (int) get_option( 'page_on_front' ); if ( $page_on_front_id > 0 ) { $front_page = $this->get_url( get_post( $page_on_front_id ) ); } if ( empty( $front_page ) ) { $front_page = [ 'loc' => YoastSEO()->helpers->url->home(), ]; } // Deprecated, kept for backwards data compat. R. $front_page['chf'] = 'daily'; $front_page['pri'] = 1; $images = ( $front_page['images'] ?? [] ); /** * Filter images to be included for the term in XML sitemap. * * @param array $images Array of image items. * @return array $image_list Array of image items. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages_front_page', $images ); if ( is_array( $image_list ) ) { $front_page['images'] = $image_list; } $links[] = $front_page; } elseif ( $post_type !== 'page' ) { /** * Filter the URL Yoast SEO uses in the XML sitemap for this post type archive. * * @param string $archive_url The URL of this archive * @param string $post_type The post type this archive is for. */ $archive_url = apply_filters( 'wpseo_sitemap_post_type_archive_link', $this->get_post_type_archive_link( $post_type ), $post_type ); } if ( $archive_url ) { $links[] = [ 'loc' => $archive_url, 'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ]; } /** * Filters the first post type links. * * @param array $links The first post type links. * @param string $post_type The post type this archive is for. */ return apply_filters( 'wpseo_sitemap_post_type_first_links', $links, $post_type ); } /** * Get URL for a post type archive. * * @since 5.3 * * @param string $post_type Post type. * * @return string|bool URL or false if it should be excluded. */ protected function get_post_type_archive_link( $post_type ) { $pt_archive_page_id = -1; if ( $post_type === 'post' ) { if ( get_option( 'show_on_front' ) === 'posts' ) { return YoastSEO()->helpers->url->home(); } $pt_archive_page_id = (int) get_option( 'page_for_posts' ); // Post archive should be excluded if posts page isn't set. if ( $pt_archive_page_id <= 0 ) { return false; } } if ( ! $this->is_post_type_archive_indexable( $post_type, $pt_archive_page_id ) ) { return false; } return get_post_type_archive_link( $post_type ); } /** * Determines whether a post type archive is indexable. * * @since 11.5 * * @param string $post_type Post type. * @param int $archive_page_id The page id. * * @return bool True when post type archive is indexable. */ protected function is_post_type_archive_indexable( $post_type, $archive_page_id = -1 ) { if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) { return false; } /** * Filter the page which is dedicated to this post type archive. * * @since 9.3 * * @param string $archive_page_id The post_id of the page. * @param string $post_type The post type this archive is for. */ $archive_page_id = (int) apply_filters( 'wpseo_sitemap_page_for_post_type_archive', $archive_page_id, $post_type ); if ( $archive_page_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $archive_page_id ) === '1' ) { return false; } return true; } /** * Retrieve set of posts with optimized query routine. * * @param string $post_type Post type to retrieve. * @param int $count Count of posts to retrieve. * @param int $offset Starting offset. * * @return object[] */ protected function get_posts( $post_type, $count, $offset ) { global $wpdb; static $filters = []; if ( ! isset( $filters[ $post_type ] ) ) { // Make sure you're wpdb->preparing everything you throw into this!! $filters[ $post_type ] = [ /** * Filter JOIN query part for the post type. * * @param string $join SQL part, defaults to false. * @param string $post_type Post type name. */ 'join' => apply_filters( 'wpseo_posts_join', false, $post_type ), /** * Filter WHERE query part for the post type. * * @param string $where SQL part, defaults to false. * @param string $post_type Post type name. */ 'where' => apply_filters( 'wpseo_posts_where', false, $post_type ), ]; } $join_filter = $filters[ $post_type ]['join']; $where_filter = $filters[ $post_type ]['where']; $where = $this->get_sql_where_clause( $post_type ); /* * Optimized query per this thread: * {@link http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion}. * Also see {@link http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/}. */ $sql = " SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_status, post_modified_gmt, post_date, post_date_gmt FROM ( SELECT {$wpdb->posts}.ID FROM {$wpdb->posts} {$join_filter} {$where} {$where_filter} ORDER BY {$wpdb->posts}.post_modified ASC LIMIT %d OFFSET %d ) o JOIN {$wpdb->posts} l ON l.ID = o.ID "; $posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) ); $post_ids = []; foreach ( $posts as $post_index => $post ) { $post->post_type = $post_type; $sanitized_post = sanitize_post( $post, 'raw' ); $posts[ $post_index ] = new WP_Post( $sanitized_post ); $post_ids[] = $sanitized_post->ID; } update_meta_cache( 'post', $post_ids ); return $posts; } /** * Constructs an SQL where clause for a given post type. * * @param string $post_type Post type slug. * * @return string */ protected function get_sql_where_clause( $post_type ) { global $wpdb; $join = ''; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $status_where = "{$wpdb->posts}.post_status IN ('" . implode( "','", $post_statuses ) . "')"; // Based on WP_Query->get_posts(). R. if ( $post_type === 'attachment' ) { $join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) "; $parent_statuses = array_diff( $post_statuses, [ 'inherit' ] ); $status_where = "p2.post_status IN ('" . implode( "','", $parent_statuses ) . "') AND p2.post_password = ''"; } $where_clause = " {$join} WHERE {$status_where} AND {$wpdb->posts}.post_type = %s AND {$wpdb->posts}.post_password = '' AND {$wpdb->posts}.post_date != '0000-00-00 00:00:00' "; return $wpdb->prepare( $where_clause, $post_type ); } /** * Produce array of URL parts for given post object. * * @param object $post Post object to get URL parts for. * * @return array|bool */ protected function get_url( $post ) { $url = []; /** * Filter the URL Yoast SEO uses in the XML sitemap. * * Note that only absolute local URLs are allowed as the check after this removes external URLs. * * @param string $url URL to use in the XML sitemap * @param object $post Post object for the URL. */ $url['loc'] = apply_filters( 'wpseo_xml_sitemap_post_url', get_permalink( $post ), $post ); $link_type = YoastSEO()->helpers->url->get_link_type( wp_parse_url( $url['loc'] ), $this->get_parsed_home_url() ); /* * Do not include external URLs. * * {@link https://wordpress.org/plugins/page-links-to/} can rewrite permalinks to external URLs. */ if ( $link_type === SEO_Links::TYPE_EXTERNAL ) { return false; } $modified = max( $post->post_modified_gmt, $post->post_date_gmt ); if ( $modified !== '0000-00-00 00:00:00' ) { $url['mod'] = $modified; } $url['chf'] = 'daily'; // Deprecated, kept for backwards data compat. R. $canonical = WPSEO_Meta::get_value( 'canonical', $post->ID ); if ( $canonical !== '' && $canonical !== $url['loc'] ) { /* * Let's assume that if a canonical is set for this page and it's different from * the URL of this post, that page is either already in the XML sitemap OR is on * an external site, either way, we shouldn't include it here. */ return false; } unset( $canonical ); $url['pri'] = 1; // Deprecated, kept for backwards data compat. R. if ( $this->include_images ) { $url['images'] = $this->get_image_parser()->get_images( $post ); } return $url; } /** * Get all dates for a post type by using the WITH clause for performance. * * @param string $post_type Post type to retrieve dates for. * @param int $max_entries Maximum number of entries to retrieve. * * @return array Array of dates. */ private function get_all_dates_using_with_clause( $post_type, $max_entries ) { global $wpdb; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $replacements = array_merge( [ 'ordering', 'post_modified_gmt', $wpdb->posts, 'type_status_date', 'post_status', ], $post_statuses, [ 'post_type', $post_type, 'post_modified_gmt', 'post_modified_gmt', 'ordering', $max_entries, ] ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_col( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' WITH %i AS (SELECT ROW_NUMBER() OVER (ORDER BY %i) AS n, post_modified_gmt FROM %i USE INDEX ( %i ) WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i = %s ORDER BY %i) SELECT %i FROM %i WHERE MOD(n, %d) = 0; ', $replacements ) ); } /** * Get all dates for a post type. * * @param string $post_type Post type to retrieve dates for. * @param int $max_entries Maximum number of entries to retrieve. * * @return array Array of dates. */ private function get_all_dates( $post_type, $max_entries ) { global $wpdb; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $replacements = array_merge( [ 'post_modified_gmt', $wpdb->posts, 'type_status_date', 'post_status', ], $post_statuses, [ 'post_type', $post_type, $max_entries, 'post_modified_gmt', ] ); return $wpdb->get_col( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT %i FROM ( SELECT @rownum:=0 ) init JOIN %i USE INDEX( %i ) WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i = %s AND ( @rownum:=@rownum+1 ) %% %d = 0 ORDER BY %i ASC ', $replacements ) ); } } sitemaps/class-sitemaps-admin.php 0000644 00000005736 14720701066 0013136 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\XML Sitemaps */ /** * Class that handles the Admin side of XML sitemaps. */ class WPSEO_Sitemaps_Admin { /** * Post_types that are being imported. * * @var array */ private $importing_post_types = []; /** * Class constructor. */ public function __construct() { add_action( 'transition_post_status', [ $this, 'status_transition' ], 10, 3 ); add_action( 'admin_footer', [ $this, 'status_transition_bulk_finished' ] ); WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo_titles', '' ); WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo', '' ); } /** * Hooked into transition_post_status. Will initiate search engine pings * if the post is being published, is a post type that a sitemap is built for * and is a post that is included in sitemaps. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * * @return void */ public function status_transition( $new_status, $old_status, $post ) { if ( $new_status !== 'publish' ) { return; } if ( defined( 'WP_IMPORTING' ) ) { $this->status_transition_bulk( $new_status, $old_status, $post ); return; } $post_type = get_post_type( $post ); wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. } /** * Notify Google of the updated sitemap. * * @deprecated 22.0 * @codeCoverageIgnore * * @return void */ public function ping_search_engines() { _deprecated_function( __METHOD__, 'Yoast SEO 22.0' ); } /** * While bulk importing, just save unique post_types. * * When importing is done, if we have a post_type that is saved in the sitemap * try to ping the search engines. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * * @return void */ private function status_transition_bulk( $new_status, $old_status, $post ) { $this->importing_post_types[] = get_post_type( $post ); $this->importing_post_types = array_unique( $this->importing_post_types ); } /** * After import finished, walk through imported post_types and update info. * * @return void */ public function status_transition_bulk_finished() { if ( ! defined( 'WP_IMPORTING' ) ) { return; } if ( empty( $this->importing_post_types ) ) { return; } $ping_search_engines = false; foreach ( $this->importing_post_types as $post_type ) { wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. // Just have the cache deleted for nav_menu_item. if ( $post_type === 'nav_menu_item' ) { continue; } if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === false ) { $ping_search_engines = true; } } // Nothing to do. if ( $ping_search_engines === false ) { return; } if ( WP_CACHE ) { do_action( 'wpseo_hit_sitemap_index' ); } } } sitemaps/class-taxonomy-sitemap-provider.php 0000644 00000022263 14720701066 0015363 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap provider for author archives. */ class WPSEO_Taxonomy_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Holds image parser instance. * * @var WPSEO_Sitemap_Image_Parser */ protected static $image_parser; /** * Determines whether images should be included in the XML sitemap. * * @var bool */ private $include_images; /** * Set up object properties for data reuse. */ public function __construct() { /** * Filter - Allows excluding images from the XML sitemap. * * @param bool $include True to include, false to exclude. */ $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { $taxonomy = get_taxonomy( $type ); if ( $taxonomy === false || ! $this->is_valid_taxonomy( $taxonomy->name ) || ! $taxonomy->public ) { return false; } return true; } /** * Retrieves the links for the sitemap. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' ); if ( empty( $taxonomies ) ) { return []; } $taxonomy_names = array_filter( array_keys( $taxonomies ), [ $this, 'is_valid_taxonomy' ] ); $taxonomies = array_intersect_key( $taxonomies, array_flip( $taxonomy_names ) ); // Retrieve all the taxonomies and their terms so we can do a proper count on them. /** * Filter the setting of excluding empty terms from the XML sitemap. * * @param bool $exclude Defaults to true. * @param array $taxonomy_names Array of names for the taxonomies being processed. */ $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, $taxonomy_names ); $all_taxonomies = []; foreach ( $taxonomy_names as $taxonomy_name ) { /** * Filter the setting of excluding empty terms from the XML sitemap for a specific taxonomy. * * @param bool $exclude Defaults to the sitewide setting. * @param string $taxonomy_name The name of the taxonomy being processed. */ $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy_name ); $term_args = [ 'taxonomy' => $taxonomy_name, 'hide_empty' => $hide_empty_tax, 'fields' => 'ids', ]; $taxonomy_terms = get_terms( $term_args ); if ( count( $taxonomy_terms ) > 0 ) { $all_taxonomies[ $taxonomy_name ] = $taxonomy_terms; } } $index = []; foreach ( $taxonomies as $tax_name => $tax ) { if ( ! isset( $all_taxonomies[ $tax_name ] ) ) { // No eligible terms found. continue; } $total_count = ( isset( $all_taxonomies[ $tax_name ] ) ) ? count( $all_taxonomies[ $tax_name ] ) : 1; $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $last_modified_gmt = WPSEO_Sitemaps::get_last_modified_gmt( $tax->object_type ); for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); if ( ! is_array( $tax->object_type ) || count( $tax->object_type ) === 0 ) { continue; } $terms = array_splice( $all_taxonomies[ $tax_name ], 0, $max_entries ); if ( ! $terms ) { continue; } $args = [ 'post_type' => $tax->object_type, 'tax_query' => [ [ 'taxonomy' => $tax_name, 'terms' => $terms, ], ], 'orderby' => 'modified', 'order' => 'DESC', 'posts_per_page' => 1, ]; $query = new WP_Query( $args ); if ( $query->have_posts() ) { $date = $query->posts[0]->post_modified_gmt; } else { $date = $last_modified_gmt; } $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( $tax_name . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ]; } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { global $wpdb; $links = []; if ( ! $this->handles_type( $type ) ) { return $links; } $taxonomy = get_taxonomy( $type ); $steps = $max_entries; $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, [ $taxonomy->name ] ); /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy->name ); $terms = get_terms( [ 'taxonomy' => $taxonomy->name, 'hide_empty' => $hide_empty_tax, 'update_term_meta_cache' => false, 'offset' => $offset, 'number' => $steps, ] ); // If there are no terms fetched for this range, we are on an invalid page. if ( empty( $terms ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses() ); $replacements = array_merge( [ 'post_modified_gmt', $wpdb->posts, $wpdb->term_relationships, 'object_id', 'ID', $wpdb->term_taxonomy, 'term_taxonomy_id', 'term_taxonomy_id', 'taxonomy', 'term_id', 'post_status', ], $post_statuses, [ 'post_password' ] ); /** * Filter: 'wpseo_exclude_from_sitemap_by_term_ids' - Allow excluding terms by ID. * * @param array $terms_to_exclude The terms to exclude. */ $terms_to_exclude = apply_filters( 'wpseo_exclude_from_sitemap_by_term_ids', [] ); foreach ( $terms as $term ) { if ( in_array( $term->term_id, $terms_to_exclude, true ) ) { continue; } $url = []; $tax_noindex = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'noindex' ); if ( $tax_noindex === 'noindex' ) { continue; } $canonical = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' ); $url['loc'] = get_term_link( $term, $term->taxonomy ); if ( is_string( $canonical ) && $canonical !== '' && $canonical !== $url['loc'] ) { continue; } $current_replacements = $replacements; array_splice( $current_replacements, 9, 0, $term->taxonomy ); array_splice( $current_replacements, 11, 0, $term->term_id ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $url['mod'] = $wpdb->get_var( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT MAX(p.%i) AS lastmod FROM %i AS p INNER JOIN %i AS term_rel ON term_rel.%i = p.%i INNER JOIN %i AS term_tax ON term_tax.%i = term_rel.%i AND term_tax.%i = %s AND term_tax.%i = %d WHERE p.%i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' ", $current_replacements ) ); if ( $this->include_images ) { $url['images'] = $this->get_image_parser()->get_term_images( $term ); } // Deprecated, kept for backwards data compat. R. $url['chf'] = 'daily'; $url['pri'] = 1; /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'term', $term ); if ( ! empty( $url ) ) { $links[] = $url; } } return $links; } /** * Check if taxonomy by name is valid to appear in sitemaps. * * @param string $taxonomy_name Taxonomy name to check. * * @return bool */ public function is_valid_taxonomy( $taxonomy_name ) { if ( WPSEO_Options::get( "noindex-tax-{$taxonomy_name}" ) === true ) { return false; } if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu', 'wp_pattern_category' ], true ) ) { return false; } if ( $taxonomy_name === 'post_format' && WPSEO_Options::get( 'disable-post_format', false ) ) { return false; } /** * Filter to exclude the taxonomy from the XML sitemap. * * @param bool $exclude Defaults to false. * @param string $taxonomy_name Name of the taxonomy to exclude.. */ if ( apply_filters( 'wpseo_sitemap_exclude_taxonomy', false, $taxonomy_name ) ) { return false; } return true; } /** * Get the Image Parser. * * @return WPSEO_Sitemap_Image_Parser */ protected function get_image_parser() { if ( ! isset( self::$image_parser ) ) { self::$image_parser = new WPSEO_Sitemap_Image_Parser(); } return self::$image_parser; } } sitemaps/class-sitemaps-router.php 0000644 00000010707 14720701066 0013360 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ use Yoast\WP\SEO\Conditionals\Deactivating_Yoast_Seo_Conditional; /** * Rewrite setup and handling for sitemaps functionality. */ class WPSEO_Sitemaps_Router { /** * Sets up init logic. */ public function __construct() { // If we add rewrite rules during the plugin's deactivation, the flush_rewrite_rules that we perform afterwards won't properly flush those new rules. if ( YoastSEO()->classes->get( Deactivating_Yoast_Seo_Conditional::class )->is_met() ) { return; } add_action( 'yoast_add_dynamic_rewrite_rules', [ $this, 'add_rewrite_rules' ] ); add_filter( 'query_vars', [ $this, 'add_query_vars' ] ); add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ] ); add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 ); } /** * Adds rewrite routes for sitemaps. * * @param Yoast_Dynamic_Rewrites $dynamic_rewrites Dynamic rewrites handler instance. * * @return void */ public function add_rewrite_rules( $dynamic_rewrites ) { $dynamic_rewrites->add_rule( 'sitemap_index\.xml$', 'index.php?sitemap=1', 'top' ); $dynamic_rewrites->add_rule( '([^/]+?)-sitemap([0-9]+)?\.xml$', 'index.php?sitemap=$matches[1]&sitemap_n=$matches[2]', 'top' ); $dynamic_rewrites->add_rule( '([a-z]+)?-?sitemap\.xsl$', 'index.php?yoast-sitemap-xsl=$matches[1]', 'top' ); } /** * Adds query variables for sitemaps. * * @param array<string> $query_vars List of query variables to filter. * * @return array<string> Filtered query variables. */ public function add_query_vars( $query_vars ) { $query_vars[] = 'sitemap'; $query_vars[] = 'sitemap_n'; $query_vars[] = 'yoast-sitemap-xsl'; return $query_vars; } /** * Sets up rewrite rules. * * @deprecated 21.8 * @codeCoverageIgnore * * @return void */ public function init() { _deprecated_function( __METHOD__, 'Yoast SEO 21.8' ); } /** * Stop trailing slashes on sitemap.xml URLs. * * @param string $redirect The redirect URL currently determined. * * @return bool|string */ public function redirect_canonical( $redirect ) { if ( get_query_var( 'sitemap' ) || get_query_var( 'yoast-sitemap-xsl' ) ) { return false; } return $redirect; } /** * Redirects sitemap.xml to sitemap_index.xml. * * @return void */ public function template_redirect() { if ( ! $this->needs_sitemap_index_redirect() ) { return; } YoastSEO()->helpers->redirect->do_safe_redirect( home_url( '/sitemap_index.xml' ), 301, 'Yoast SEO' ); } /** * Checks whether the current request needs to be redirected to sitemap_index.xml. * * @global WP_Query $wp_query Current query. * * @return bool True if redirect is needed, false otherwise. */ public function needs_sitemap_index_redirect() { global $wp_query; $protocol = 'http://'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! empty( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) === 'on' ) { $protocol = 'https://'; } $domain = ''; if ( isset( $_SERVER['SERVER_NAME'] ) ) { $domain = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ); } $path = ''; if ( isset( $_SERVER['REQUEST_URI'] ) ) { $path = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); } // Due to different environment configurations, we need to check both SERVER_NAME and HTTP_HOST. $check_urls = [ $protocol . $domain . $path ]; if ( ! empty( $_SERVER['HTTP_HOST'] ) ) { $check_urls[] = $protocol . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . $path; } return $wp_query->is_404 && in_array( home_url( '/sitemap.xml' ), $check_urls, true ); } /** * Create base URL for the sitemap. * * @param string $page Page to append to the base URL. * * @return string base URL (incl page) */ public static function get_base_url( $page ) { global $wp_rewrite; $base = $wp_rewrite->using_index_permalinks() ? 'index.php/' : '/'; /** * Filter the base URL of the sitemaps. * * @param string $base The string that should be added to home_url() to make the full base URL. */ $base = apply_filters( 'wpseo_sitemaps_base_url', $base ); /* * Get the scheme from the configured home URL instead of letting WordPress * determine the scheme based on the requested URI. */ return home_url( $base . $page, wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME ) ); } } sitemaps/class-sitemaps-cache-validator.php 0000644 00000022661 14720701066 0015070 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Handles storage keys for sitemaps caching and invalidation. * * @since 3.2 */ class WPSEO_Sitemaps_Cache_Validator { /** * Prefix of the transient key for sitemap caches. * * @var string */ public const STORAGE_KEY_PREFIX = 'yst_sm_'; /** * Name of the option that holds the global validation value. * * @var string */ public const VALIDATION_GLOBAL_KEY = 'wpseo_sitemap_cache_validator_global'; /** * The format which creates the key of the option that holds the type validation value. * * @var string */ public const VALIDATION_TYPE_KEY_FORMAT = 'wpseo_sitemap_%s_cache_validator'; /** * Get the cache key for a certain type and page. * * A type of cache would be something like 'page', 'post' or 'video'. * * Example key format for sitemap type "post", page 1: wpseo_sitemap_post_1:akfw3e_23azBa . * * @since 3.2 * * @param string|null $type The type to get the key for. Null or self::SITEMAP_INDEX_TYPE for index cache. * @param int $page The page of cache to get the key for. * * @return bool|string The key where the cache is stored on. False if the key could not be generated. */ public static function get_storage_key( $type = null, $page = 1 ) { // Using SITEMAP_INDEX_TYPE for sitemap index cache. $type = is_null( $type ) ? WPSEO_Sitemaps::SITEMAP_INDEX_TYPE : $type; $global_cache_validator = self::get_validator(); $type_cache_validator = self::get_validator( $type ); $prefix = self::STORAGE_KEY_PREFIX; $postfix = sprintf( '_%d:%s_%s', $page, $global_cache_validator, $type_cache_validator ); try { $type = self::truncate_type( $type, $prefix, $postfix ); } catch ( OutOfBoundsException $exception ) { // Maybe do something with the exception, for now just mark as invalid. return false; } // Build key. $full_key = $prefix . $type . $postfix; return $full_key; } /** * If the type is over length make sure we compact it so we don't have any database problems. * * When there are more 'extremely long' post types, changes are they have variations in either the start or ending. * Because of this, we cut out the excess in the middle which should result in less chance of collision. * * @since 3.2 * * @param string $type The type of sitemap to be used. * @param string $prefix The part before the type in the cache key. Only the length is used. * @param string $postfix The part after the type in the cache key. Only the length is used. * * @return string The type with a safe length to use * * @throws OutOfRangeException When there is less than 15 characters of space for a key that is originally longer. */ public static function truncate_type( $type, $prefix = '', $postfix = '' ) { /* * This length has been restricted by the database column length of 64 in the past. * The prefix added by WordPress is '_transient_' because we are saving to a transient. * We need to use a timeout on the transient, otherwise the values get autoloaded, this adds * another restriction to the length. */ $max_length = 45; // 64 - 19 ('_transient_timeout_') $max_length -= strlen( $prefix ); $max_length -= strlen( $postfix ); if ( strlen( $type ) > $max_length ) { if ( $max_length < 15 ) { /* * If this happens the most likely cause is a page number that is too high. * * So this would not happen unintentionally. * Either by trying to cause a high server load, finding backdoors or misconfiguration. */ throw new OutOfRangeException( __( 'Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.', 'wordpress-seo' ) ); } $half = ( $max_length / 2 ); $first_part = substr( $type, 0, ( ceil( $half ) - 1 ) ); $last_part = substr( $type, ( 1 - floor( $half ) ) ); $type = $first_part . '..' . $last_part; } return $type; } /** * Invalidate sitemap cache. * * @since 3.2 * * @param string|null $type The type to get the key for. Null for all caches. * * @return void */ public static function invalidate_storage( $type = null ) { // Global validator gets cleared when no type is provided. $old_validator = null; // Get the current type validator. if ( ! is_null( $type ) ) { $old_validator = self::get_validator( $type ); } // Refresh validator. self::create_validator( $type ); if ( ! wp_using_ext_object_cache() ) { // Clean up current cache from the database. self::cleanup_database( $type, $old_validator ); } // External object cache pushes old and unretrieved items out by itself so we don't have to do anything for that. } /** * Cleanup invalidated database cache. * * @since 3.2 * * @param string|null $type The type of sitemap to clear cache for. * @param string|null $validator The validator to clear cache of. * * @return void */ public static function cleanup_database( $type = null, $validator = null ) { global $wpdb; if ( is_null( $type ) ) { // Clear all cache if no type is provided. $like = sprintf( '%s%%', self::STORAGE_KEY_PREFIX ); } else { // Clear type cache for all type keys. $like = sprintf( '%1$s%2$s_%%', self::STORAGE_KEY_PREFIX, $type ); } /* * Add slashes to the LIKE "_" single character wildcard. * * We can't use `esc_like` here because we need the % in the query. */ $where = []; $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) ); $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) ); // Delete transients. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $wpdb->query( $wpdb->prepare( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. 'DELETE FROM %i WHERE ' . implode( ' OR ', array_fill( 0, count( $where ), '%s' ) ), array_merge( [ $wpdb->options ], $where ) ) ); wp_cache_delete( 'alloptions', 'options' ); } /** * Get the current cache validator. * * Without the type the global validator is returned. * This can invalidate -all- keys in cache at once. * * With the type parameter the validator for that specific type can be invalidated. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return string|null The validator for the supplied type. */ public static function get_validator( $type = '' ) { $key = self::get_validator_key( $type ); $current = get_option( $key, null ); if ( ! is_null( $current ) ) { return $current; } if ( self::create_validator( $type ) ) { return self::get_validator( $type ); } return null; } /** * Get the cache validator option key for the specified type. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return string Validator to be used to generate the cache key. */ public static function get_validator_key( $type = '' ) { if ( empty( $type ) ) { return self::VALIDATION_GLOBAL_KEY; } return sprintf( self::VALIDATION_TYPE_KEY_FORMAT, $type ); } /** * Refresh the cache validator value. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return bool True if validator key has been saved as option. */ public static function create_validator( $type = '' ) { $key = self::get_validator_key( $type ); // Generate new validator. $microtime = microtime(); // Remove space. list( $milliseconds, $seconds ) = explode( ' ', $microtime ); // Transients are purged every 24h. $seconds = ( $seconds % DAY_IN_SECONDS ); $milliseconds = intval( substr( $milliseconds, 2, 3 ), 10 ); // Combine seconds and milliseconds and convert to integer. $validator = intval( $seconds . '' . $milliseconds, 10 ); // Apply base 61 encoding. $compressed = self::convert_base10_to_base61( $validator ); return update_option( $key, $compressed, false ); } /** * Encode to base61 format. * * This is base64 (numeric + alpha + alpha upper case) without the 0. * * @since 3.2 * * @param int $base10 The number that has to be converted to base 61. * * @return string Base 61 converted string. * * @throws InvalidArgumentException When the input is not an integer. */ public static function convert_base10_to_base61( $base10 ) { if ( ! is_int( $base10 ) ) { throw new InvalidArgumentException( __( 'Expected an integer as input.', 'wordpress-seo' ) ); } // Characters that will be used in the conversion. $characters = '123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $length = strlen( $characters ); $remainder = $base10; $output = ''; do { // Building from right to left in the result. $index = ( $remainder % $length ); // Prepend the character to the output. $output = $characters[ $index ] . $output; // Determine the remainder after removing the applied number. $remainder = floor( $remainder / $length ); // Keep doing it until we have no remainder left. } while ( $remainder ); return $output; } } class-wpseo-admin-bar-menu.php 0000644 00000064773 14720701066 0012333 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Score_Icon_Helper; use Yoast\WP\SEO\Integrations\Support_Integration; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter; use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * Class for the Yoast SEO admin bar menu. */ class WPSEO_Admin_Bar_Menu implements WPSEO_WordPress_Integration { /** * The identifier used for the menu. * * @var string */ public const MENU_IDENTIFIER = 'wpseo-menu'; /** * The identifier used for the Keyword Research submenu. * * @var string */ public const KEYWORD_RESEARCH_SUBMENU_IDENTIFIER = 'wpseo-kwresearch'; /** * The identifier used for the Analysis submenu. * * @var string */ public const ANALYSIS_SUBMENU_IDENTIFIER = 'wpseo-analysis'; /** * The identifier used for the Settings submenu. * * @var string */ public const SETTINGS_SUBMENU_IDENTIFIER = 'wpseo-settings'; /** * The identifier used for the Network Settings submenu. * * @var string */ public const NETWORK_SETTINGS_SUBMENU_IDENTIFIER = 'wpseo-network-settings'; /** * Asset manager instance. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Holds the Score_Icon_Helper instance. * * @var Score_Icon_Helper */ protected $indexable_repository; /** * Holds the Score_Icon_Helper instance. * * @var Score_Icon_Helper */ protected $score_icon_helper; /** * Holds the Product_Helper instance. * * @var Product_Helper */ protected $product_helper; /** * Holds the shortlinker instance. * * @var WPSEO_Shortlinker */ protected $shortlinker; /** * Whether SEO Score is enabled. * * @var bool */ protected $is_seo_enabled = null; /** * Whether readability is enabled. * * @var bool */ protected $is_readability_enabled = null; /** * The indexable for the current WordPress page, if found. * * @var bool|Indexable */ protected $current_indexable = null; /** * Constructs the WPSEO_Admin_Bar_Menu. * * @param WPSEO_Admin_Asset_Manager|null $asset_manager Optional. Asset manager to use. * @param Indexable_Repository|null $indexable_repository Optional. The Indexable_Repository. * @param Score_Icon_Helper|null $score_icon_helper Optional. The Score_Icon_Helper. * @param Product_Helper|null $product_helper Optional. The product helper. * @param WPSEO_Shortlinker|null $shortlinker The shortlinker. */ public function __construct( ?WPSEO_Admin_Asset_Manager $asset_manager = null, ?Indexable_Repository $indexable_repository = null, ?Score_Icon_Helper $score_icon_helper = null, ?Product_Helper $product_helper = null, ?WPSEO_Shortlinker $shortlinker = null ) { if ( ! $asset_manager ) { $asset_manager = new WPSEO_Admin_Asset_Manager(); } if ( ! $indexable_repository ) { $indexable_repository = YoastSEO()->classes->get( Indexable_Repository::class ); } if ( ! $score_icon_helper ) { $score_icon_helper = YoastSEO()->helpers->score_icon; } if ( ! $product_helper ) { $product_helper = YoastSEO()->helpers->product; } if ( ! $shortlinker ) { $shortlinker = new WPSEO_Shortlinker(); } $this->product_helper = $product_helper; $this->asset_manager = $asset_manager; $this->indexable_repository = $indexable_repository; $this->score_icon_helper = $score_icon_helper; $this->shortlinker = $shortlinker; } /** * Gets whether SEO score is enabled, with cache applied. * * @return bool True if SEO score is enabled, false otherwise. */ protected function get_is_seo_enabled() { if ( is_null( $this->is_seo_enabled ) ) { $this->is_seo_enabled = ( new WPSEO_Metabox_Analysis_SEO() )->is_enabled(); } return $this->is_seo_enabled; } /** * Gets whether readability is enabled, with cache applied. * * @return bool True if readability is enabled, false otherwise. */ protected function get_is_readability_enabled() { if ( is_null( $this->is_readability_enabled ) ) { $this->is_readability_enabled = ( new WPSEO_Metabox_Analysis_Readability() )->is_enabled(); } return $this->is_readability_enabled; } /** * Returns the indexable for the current WordPress page, with cache applied. * * @return bool|Indexable The indexable, false if none could be found. */ protected function get_current_indexable() { if ( is_null( $this->current_indexable ) ) { $this->current_indexable = $this->indexable_repository->for_current_page(); } return $this->current_indexable; } /** * Adds the admin bar menu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ public function add_menu( WP_Admin_Bar $wp_admin_bar ) { // On block editor pages, the admin bar only shows on mobile, where having this menu icon is not very helpful. if ( is_admin() ) { $screen = get_current_screen(); if ( isset( $screen ) && $screen->is_block_editor() ) { return; } } // If the current user can't write posts, this is all of no use, so let's not output an admin menu. if ( ! current_user_can( 'edit_posts' ) ) { return; } $this->add_root_menu( $wp_admin_bar ); /** * Adds a submenu item in the top of the adminbar. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * @param string $menu_identifier The menu identifier. */ do_action( 'wpseo_add_adminbar_submenu', $wp_admin_bar, self::MENU_IDENTIFIER ); if ( ! is_admin() ) { if ( is_singular() || is_tag() || is_tax() || is_category() ) { $is_seo_enabled = $this->get_is_seo_enabled(); $is_readability_enabled = $this->get_is_readability_enabled(); $indexable = $this->get_current_indexable(); if ( $is_seo_enabled ) { $focus_keyword = ( ! is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) || is_null( $indexable->primary_focus_keyword ) ) ? __( 'not set', 'wordpress-seo' ) : $indexable->primary_focus_keyword; $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-seo-focus-keyword', 'title' => __( 'Focus keyphrase: ', 'wordpress-seo' ) . '<span class="wpseo-focus-keyword">' . $focus_keyword . '</span>', 'meta' => [ 'tabindex' => '0' ], ] ); $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-seo-score', 'title' => __( 'SEO score', 'wordpress-seo' ) . ': ' . $this->score_icon_helper->for_seo( $indexable, 'adminbar-sub-menu-score' ) ->present(), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( $is_readability_enabled ) { $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-readability-score', 'title' => __( 'Readability', 'wordpress-seo' ) . ': ' . $this->score_icon_helper->for_readability( $indexable->readability_score, 'adminbar-sub-menu-score' ) ->present(), 'meta' => [ 'tabindex' => '0' ], ] ); } if ( ! $this->product_helper->is_premium() ) { $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-frontend-inspector', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-frontend-inspector' ), 'title' => __( 'Front-end SEO inspector', 'wordpress-seo' ) . new Premium_Badge_Presenter( 'wpseo-frontend-inspector-badge' ), 'meta' => [ 'tabindex' => '0', 'target' => '_blank', ], ] ); } } $this->add_analysis_submenu( $wp_admin_bar ); $this->add_seo_tools_submenu( $wp_admin_bar ); $this->add_how_to_submenu( $wp_admin_bar ); $this->add_get_help_submenu( $wp_admin_bar ); } if ( ! is_admin() || is_blog_admin() ) { $this->add_settings_submenu( $wp_admin_bar ); } elseif ( is_network_admin() ) { $this->add_network_settings_submenu( $wp_admin_bar ); } if ( ! $this->product_helper->is_premium() ) { $this->add_premium_link( $wp_admin_bar ); } } /** * Enqueues admin bar assets. * * @return void */ public function enqueue_assets() { if ( ! is_admin_bar_showing() ) { return; } // If the current user can't write posts, this is all of no use, so let's not output an admin menu. if ( ! current_user_can( 'edit_posts' ) ) { return; } $this->asset_manager->register_assets(); $this->asset_manager->enqueue_style( 'adminbar' ); } /** * Registers the hooks. * * @return void */ public function register_hooks() { if ( ! $this->meets_requirements() ) { return; } add_action( 'admin_bar_menu', [ $this, 'add_menu' ], 95 ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Checks whether the requirements to use this class are met. * * @return bool True if requirements are met, false otherwise. */ public function meets_requirements() { if ( is_network_admin() ) { return WPSEO_Utils::is_plugin_network_active(); } if ( WPSEO_Options::get( 'enable_admin_bar_menu' ) !== true ) { return false; } return ! is_admin() || is_blog_admin(); } /** * Adds the admin bar root menu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_root_menu( WP_Admin_Bar $wp_admin_bar ) { $title = $this->get_title(); $score = ''; $settings_url = ''; $counter = ''; $notification_popup = ''; $notification_count = 0; $post = $this->get_singular_post(); if ( $post ) { $score = $this->get_post_score( $post ); } $term = $this->get_singular_term(); if ( $term ) { $score = $this->get_term_score( $term ); } $can_manage_options = $this->can_manage_options(); if ( $can_manage_options ) { $settings_url = $this->get_settings_page_url(); } if ( empty( $score ) && ! is_network_admin() && $can_manage_options ) { $notification_center = Yoast_Notification_Center::get(); $notification_count = $notification_center->get_notification_count(); $counter = $this->get_notification_counter( $notification_count ); $notification_popup = $this->get_notification_popup(); } $admin_bar_menu_args = [ 'id' => self::MENU_IDENTIFIER, 'title' => $title . $score . $counter . $notification_popup, 'href' => $settings_url, 'meta' => [ 'tabindex' => ! empty( $settings_url ) ? false : '0' ], ]; $wp_admin_bar->add_menu( $admin_bar_menu_args ); if ( $notification_count > 0 ) { $admin_bar_menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-notifications', 'title' => __( 'Notifications', 'wordpress-seo' ) . $counter, 'href' => $settings_url, 'meta' => [ 'tabindex' => ! empty( $settings_url ) ? false : '0' ], ]; $wp_admin_bar->add_menu( $admin_bar_menu_args ); } } /** * Adds the admin bar analysis submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_analysis_submenu( WP_Admin_Bar $wp_admin_bar ) { try { $url = YoastSEO()->meta->for_current_page()->canonical; } catch ( Exception $e ) { // This is not the type of error we can handle here. return; } if ( ! $url ) { return; } $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => self::ANALYSIS_SUBMENU_IDENTIFIER, 'title' => __( 'Analyze this page', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); $encoded_url = rawurlencode( $url ); $submenu_items = [ [ 'id' => 'wpseo-inlinks', 'title' => __( 'Check links to this URL', 'wordpress-seo' ), 'href' => 'https://search.google.com/search-console/links/drilldown?resource_id=' . rawurlencode( get_option( 'siteurl' ) ) . '&type=EXTERNAL&target=' . $encoded_url . '&domain=', ], [ 'id' => 'wpseo-structureddata', 'title' => __( 'Google Rich Results Test', 'wordpress-seo' ), 'href' => 'https://search.google.com/test/rich-results?url=' . $encoded_url, ], [ 'id' => 'wpseo-facebookdebug', 'title' => __( 'Facebook Debugger', 'wordpress-seo' ), 'href' => '//developers.facebook.com/tools/debug/?q=' . $encoded_url, ], [ 'id' => 'wpseo-pagespeed', 'title' => __( 'Google Page Speed Test', 'wordpress-seo' ), 'href' => '//developers.google.com/speed/pagespeed/insights/?url=' . $encoded_url, ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, self::ANALYSIS_SUBMENU_IDENTIFIER ); } /** * Adds the admin bar tools submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_seo_tools_submenu( WP_Admin_Bar $wp_admin_bar ) { $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-sub-tools', 'title' => __( 'SEO Tools', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); $submenu_items = [ [ 'id' => 'wpseo-semrush', 'title' => 'Semrush', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-semrush' ), ], [ 'id' => 'wpseo-wincher', 'title' => 'Wincher', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-wincher' ), ], [ 'id' => 'wpseo-google-trends', 'title' => 'Google trends', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-gtrends' ), ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, 'wpseo-sub-tools' ); } /** * Adds the admin bar How To submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_how_to_submenu( WP_Admin_Bar $wp_admin_bar ) { $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-sub-howto', 'title' => __( 'How to', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); $submenu_items = [ [ 'id' => 'wpseo-learn-seo', 'title' => __( 'Learn more SEO', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-learn-more-seo' ), ], [ 'id' => 'wpseo-improve-blogpost', 'title' => __( 'Improve your blog post', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-improve-blog-post' ), ], [ 'id' => 'wpseo-write-better-content', 'title' => __( 'Write better content', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-write-better' ), ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, 'wpseo-sub-howto' ); } /** * Adds the admin bar How To submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_get_help_submenu( WP_Admin_Bar $wp_admin_bar ) { $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-sub-get-help', 'title' => __( 'Help', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; if ( current_user_can( Support_Integration::CAPABILITY ) ) { $menu_args['href'] = admin_url( 'admin.php?page=' . Support_Integration::PAGE ); $wp_admin_bar->add_menu( $menu_args ); return; } $wp_admin_bar->add_menu( $menu_args ); $submenu_items = [ [ 'id' => 'wpseo-yoast-help', 'title' => __( 'Yoast.com help section', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-yoast-help' ), ], [ 'id' => 'wpseo-premium-support', 'title' => __( 'Yoast Premium support', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-premium-support' ), ], [ 'id' => 'wpseo-wp-support-forums', 'title' => __( 'WordPress.org support forums', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-wp-support-forums' ), ], [ 'id' => 'wpseo-learn-seo-2', 'title' => __( 'Learn more SEO', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-learn-more-seo-help' ), ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, 'wpseo-sub-get-help' ); } /** * Adds the admin bar How To submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_premium_link( WP_Admin_Bar $wp_admin_bar ) { $sale_percentage = ''; if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) { $sale_percentage = sprintf( '<span class="admin-bar-premium-promotion">%1$s</span>', esc_html__( '30% OFF', 'wordpress-seo' ) ); } $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-get-premium', // Circumvent an issue in the WP admin bar API in order to pass `data` attributes. See https://core.trac.wordpress.org/ticket/38636. 'title' => sprintf( '<a href="%1$s" target="_blank" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2" style="padding:0;">%2$s » %3$s</a>', esc_url( $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-get-premium' ) ), esc_html__( 'Get Yoast SEO Premium', 'wordpress-seo' ), $sale_percentage ), 'meta' => [ 'tabindex' => '0', ], ] ); } /** * Adds the admin bar settings submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_settings_submenu( WP_Admin_Bar $wp_admin_bar ) { if ( ! $this->can_manage_options() ) { return; } $admin_menu = new WPSEO_Admin_Menu( new WPSEO_Menu() ); $submenu_pages = $admin_menu->get_submenu_pages(); $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => self::SETTINGS_SUBMENU_IDENTIFIER, 'title' => __( 'SEO Settings', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); foreach ( $submenu_pages as $submenu_page ) { if ( ! current_user_can( $submenu_page[3] ) ) { continue; } // Don't add the Google Search Console menu item. if ( $submenu_page[4] === 'wpseo_search_console' ) { continue; } $id = 'wpseo-' . str_replace( '_', '-', str_replace( 'wpseo_', '', $submenu_page[4] ) ); if ( $id === 'wpseo-dashboard' ) { $id = 'wpseo-general'; } $menu_args = [ 'parent' => self::SETTINGS_SUBMENU_IDENTIFIER, 'id' => $id, 'title' => $submenu_page[2], 'href' => admin_url( 'admin.php?page=' . rawurlencode( $submenu_page[4] ) ), ]; $wp_admin_bar->add_menu( $menu_args ); } } /** * Adds the admin bar network settings submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_network_settings_submenu( WP_Admin_Bar $wp_admin_bar ) { if ( ! $this->can_manage_options() ) { return; } $network_admin_menu = new WPSEO_Network_Admin_Menu( new WPSEO_Menu() ); $submenu_pages = $network_admin_menu->get_submenu_pages(); $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER, 'title' => __( 'SEO Settings', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); foreach ( $submenu_pages as $submenu_page ) { if ( ! current_user_can( $submenu_page[3] ) ) { continue; } $id = 'wpseo-' . str_replace( '_', '-', str_replace( 'wpseo_', '', $submenu_page[4] ) ); if ( $id === 'wpseo-dashboard' ) { $id = 'wpseo-general'; } $menu_args = [ 'parent' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER, 'id' => $id, 'title' => $submenu_page[2], 'href' => network_admin_url( 'admin.php?page=' . rawurlencode( $submenu_page[4] ) ), ]; $wp_admin_bar->add_menu( $menu_args ); } } /** * Gets the menu title markup. * * @return string Admin bar title markup. */ protected function get_title() { return '<div id="yoast-ab-icon" class="ab-item yoast-logo svg"><span class="screen-reader-text">' . __( 'SEO', 'wordpress-seo' ) . '</span></div>'; } /** * Gets the current post if in a singular post context. * * @global string $pagenow Current page identifier. * @global WP_Post|null $post Current post object, or null if none available. * * @return WP_Post|null Post object, or null if not in singular context. */ protected function get_singular_post() { global $pagenow, $post; if ( ! is_singular() && ( ! is_blog_admin() || ! WPSEO_Metabox::is_post_edit( $pagenow ) ) ) { return null; } if ( ! isset( $post ) || ! is_object( $post ) || ! $post instanceof WP_Post ) { return null; } return $post; } /** * Gets the focus keyword for a given post. * * @param WP_Post $post Post object to get its focus keyword. * * @return string Focus keyword, or empty string if none available. */ protected function get_post_focus_keyword( $post ) { if ( ! is_object( $post ) || ! property_exists( $post, 'ID' ) ) { return ''; } /** * Filter: 'wpseo_use_page_analysis' Determines if the analysis should be enabled. * * @param bool $enabled Determines if the analysis should be enabled. */ if ( apply_filters( 'wpseo_use_page_analysis', true ) !== true ) { return ''; } return WPSEO_Meta::get_value( 'focuskw', $post->ID ); } /** * Gets the score for a given post. * * @param WP_Post $post Post object to get its score. * * @return string Score markup, or empty string if none available. */ protected function get_post_score( $post ) { if ( ! is_object( $post ) || ! property_exists( $post, 'ID' ) ) { return ''; } if ( apply_filters( 'wpseo_use_page_analysis', true ) !== true ) { return ''; } return $this->get_score_icon(); } /** * Gets the current term if in a singular term context. * * @global string $pagenow Current page identifier. * @global WP_Query $wp_query Current query object. * @global WP_Term|null $tag Current term object, or null if none available. * * @return WP_Term|null Term object, or null if not in singular context. */ protected function get_singular_term() { global $pagenow, $wp_query, $tag; if ( is_category() || is_tag() || is_tax() ) { return $wp_query->get_queried_object(); } if ( WPSEO_Taxonomy::is_term_edit( $pagenow ) && ! WPSEO_Taxonomy::is_term_overview( $pagenow ) && isset( $tag ) && is_object( $tag ) && ! is_wp_error( $tag ) ) { return get_term( $tag->term_id ); } return null; } /** * Gets the score for a given term. * * @param WP_Term $term Term object to get its score. * * @return string Score markup, or empty string if none available. */ protected function get_term_score( $term ) { if ( ! is_object( $term ) || ! property_exists( $term, 'term_id' ) || ! property_exists( $term, 'taxonomy' ) ) { return ''; } return $this->get_score_icon(); } /** * Create the score icon. * * @return string The score icon, or empty string. */ protected function get_score_icon() { $is_seo_enabled = $this->get_is_seo_enabled(); $is_readability_enabled = $this->get_is_readability_enabled(); $indexable = $this->get_current_indexable(); if ( $is_seo_enabled ) { return $this->score_icon_helper->for_seo( $indexable, 'adminbar-seo-score' )->present(); } if ( $is_readability_enabled ) { return $this->score_icon_helper->for_readability( $indexable->readability_score, 'adminbar-seo-score' ) ->present(); } return ''; } /** * Gets the URL to the main admin settings page. * * @return string Admin settings page URL. */ protected function get_settings_page_url() { return self_admin_url( 'admin.php?page=' . WPSEO_Admin::PAGE_IDENTIFIER ); } /** * Gets the notification counter if in a valid context. * * @param int $notification_count Number of notifications. * * @return string Notification counter markup, or empty string if not available. */ protected function get_notification_counter( $notification_count ) { /* translators: Hidden accessibility text; %s: number of notifications. */ $counter_screen_reader_text = sprintf( _n( '%s notification', '%s notifications', $notification_count, 'wordpress-seo' ), number_format_i18n( $notification_count ) ); return sprintf( ' <div class="wp-core-ui wp-ui-notification yoast-issue-counter%s"><span class="yoast-issues-count" aria-hidden="true">%d</span><span class="screen-reader-text">%s</span></div>', ( $notification_count ) ? '' : ' wpseo-no-adminbar-notifications', $notification_count, $counter_screen_reader_text ); } /** * Gets the notification popup if in a valid context. * * @return string Notification popup markup, or empty string if not available. */ protected function get_notification_popup() { $notification_center = Yoast_Notification_Center::get(); $new_notifications = $notification_center->get_new_notifications(); $new_notifications_count = count( $new_notifications ); if ( ! $new_notifications_count ) { return ''; } $notification = sprintf( _n( 'There is a new notification.', 'There are new notifications.', $new_notifications_count, 'wordpress-seo' ), $new_notifications_count ); return '<div class="yoast-issue-added">' . $notification . '</div>'; } /** * Checks whether the current user can manage options in the current context. * * @return bool True if capabilities are sufficient, false otherwise. */ protected function can_manage_options() { return ( is_network_admin() && current_user_can( 'wpseo_manage_network_options' ) ) || ( ! is_network_admin() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ); } /** * Add submenu items to a menu item. * * @param array $submenu_items Submenu items array. * @param WP_Admin_Bar $wp_admin_bar Admin bar object. * @param string $parent_id Parent menu item ID. * * @return void */ protected function add_submenu_items( array $submenu_items, WP_Admin_Bar $wp_admin_bar, $parent_id ) { foreach ( $submenu_items as $menu_item ) { $menu_args = [ 'parent' => $parent_id, 'id' => $menu_item['id'], 'title' => $menu_item['title'], 'href' => $menu_item['href'], 'meta' => [ 'target' => '_blank' ], ]; $wp_admin_bar->add_menu( $menu_args ); } } } class-wpseo-replacement-variable.php 0000644 00000002537 14720701066 0013607 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 7.7 */ /** * Class WPSEO_Replacement_Variable. * * This class stores the data of a single snippet variable. */ class WPSEO_Replacement_Variable { /** * The variable to use. * * @var string */ protected $variable; /** * The label of the replacement variable. * * @var string */ protected $label; /** * The description of the replacement variable. * * @var string */ protected $description; /** * WPSEO_Replacement_Variable constructor. * * @param string $variable The variable that is replaced. * @param string $label The label of the replacement variable. * @param string $description The description of the replacement variable. */ public function __construct( $variable, $label, $description ) { $this->variable = $variable; $this->label = $label; $this->description = $description; } /** * Returns the variable to use. * * @return string */ public function get_variable() { return $this->variable; } /** * Returns the label of the replacement variable. * * @return string */ public function get_label() { return $this->label; } /** * Returns the description of the replacement variable. * * @return string */ public function get_description() { return $this->description; } } class-post-type.php 0000644 00000007622 14720701066 0010336 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Inc */ /** * Represents the post type utils. */ class WPSEO_Post_Type { /** * Returns an array with the accessible post types. * * An accessible post type is a post type that is public and isn't set as no-index (robots). * * @return array Array with all the accessible post_types. */ public static function get_accessible_post_types() { return YoastSEO()->helpers->post_type->get_accessible_post_types(); } /** * Returns whether the passed post type is considered accessible. * * @param string $post_type The post type to check. * * @return bool Whether or not the post type is considered accessible. */ public static function is_post_type_accessible( $post_type ) { return in_array( $post_type, self::get_accessible_post_types(), true ); } /** * Checks if the request post type is public and indexable. * * @param string $post_type_name The name of the post type to lookup. * * @return bool True when post type is set to index. */ public static function is_post_type_indexable( $post_type_name ) { return YoastSEO()->helpers->post_type->is_indexable( $post_type_name ); } /** * Filters the attachment post type from an array with post_types. * * @param array $post_types The array to filter the attachment post type from. * * @return array The filtered array. */ public static function filter_attachment_post_type( array $post_types ) { if ( WPSEO_Options::get( 'disable-attachment' ) === true ) { unset( $post_types['attachment'] ); } return $post_types; } /** * Checks if the post type is enabled in the REST API. * * @param string $post_type The post type to check. * * @return bool Whether or not the post type is available in the REST API. */ public static function is_rest_enabled( $post_type ) { $post_type_object = get_post_type_object( $post_type ); if ( $post_type_object === null ) { return false; } return $post_type_object->show_in_rest === true; } /** * Checks if the current post type has an archive. * * Context: The has_archive value can be a string or a boolean. In most case it will be a boolean, * but it can be defined as a string. When it is a string the archive_slug will be overwritten to * define another endpoint. * * @param WP_Post_Type $post_type The post type object. * * @return bool True whether the post type has an archive. */ public static function has_archive( $post_type ) { return YoastSEO()->helpers->post_type->has_archive( $post_type ); } /** * Checks if the Yoast Metabox has been enabled for the post type. * * @param string $post_type The post type name. * * @return bool True whether the metabox is enabled. */ public static function has_metabox_enabled( $post_type ) { return WPSEO_Options::get( 'display-metabox-pt-' . $post_type, false ); } /* ********************* DEPRECATED METHODS ********************* */ /** * Removes the notification related to the post types which have been made public. * * @deprecated 20.10 * @codeCoverageIgnore * * @return void */ public static function remove_post_types_made_public_notification() { _deprecated_function( __METHOD__, 'Yoast SEO 20.10', 'Content_Type_Visibility_Dismiss_Notifications::dismiss_notifications' ); $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'post-types-made-public' ); } /** * Removes the notification related to the taxonomies which have been made public. * * @deprecated 20.10 * @codeCoverageIgnore * * @return void */ public static function remove_taxonomies_made_public_notification() { _deprecated_function( __METHOD__, 'Yoast SEO 20.10', 'Content_Type_Visibility_Dismiss_Notifications::dismiss_notifications' ); $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'taxonomies-made-public' ); } } class-wpseo-primary-term.php 0000644 00000003313 14720701066 0012146 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * Represents a post's primary term. */ class WPSEO_Primary_Term { /** * Taxonomy name for the term. * * @var string */ protected $taxonomy_name; /** * Post ID for the term. * * @var int */ protected $post_ID; /** * The taxonomy this term is part of. * * @param string $taxonomy_name Taxonomy name for the term. * @param int $post_id Post ID for the term. */ public function __construct( $taxonomy_name, $post_id ) { $this->taxonomy_name = $taxonomy_name; $this->post_ID = $post_id; } /** * Returns the primary term ID. * * @return int|bool */ public function get_primary_term() { $primary_term = get_post_meta( $this->post_ID, WPSEO_Meta::$meta_prefix . 'primary_' . $this->taxonomy_name, true ); if ( ! $primary_term ) { return false; } $terms = $this->get_terms(); if ( ! in_array( (int) $primary_term, wp_list_pluck( $terms, 'term_id' ), true ) ) { $primary_term = false; } $primary_term = (int) $primary_term; return ( $primary_term ) ? ( $primary_term ) : false; } /** * Sets the new primary term ID. * * @param int $new_primary_term New primary term ID. * * @return void */ public function set_primary_term( $new_primary_term ) { update_post_meta( $this->post_ID, WPSEO_Meta::$meta_prefix . 'primary_' . $this->taxonomy_name, $new_primary_term ); } /** * Get the terms for the current post ID. * When $terms is not an array, set $terms to an array. * * @return array */ protected function get_terms() { $terms = get_the_terms( $this->post_ID, $this->taxonomy_name ); if ( ! is_array( $terms ) ) { $terms = []; } return $terms; } } wpseo-non-ajax-functions.php 0000644 00000003071 14720701066 0012135 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * Initializes the admin bar. * * @return void */ function wpseo_initialize_admin_bar() { $admin_bar_menu = new WPSEO_Admin_Bar_Menu(); $admin_bar_menu->register_hooks(); } add_action( 'wp_loaded', 'wpseo_initialize_admin_bar' ); /** * Allows editing of the meta fields through weblog editors like Marsedit. * * @param array $required_capabilities Capabilities that must all be true to allow action. * @param array $capabilities Array of capabilities to be checked, unused here. * @param array $args List of arguments for the specific capabilities to be checked. * * @return array Filtered capabilities. */ function allow_custom_field_edits( $required_capabilities, $capabilities, $args ) { if ( ! in_array( $args[0], [ 'edit_post_meta', 'add_post_meta' ], true ) ) { return $required_capabilities; } // If this is provided, it is the post ID. if ( empty( $args[2] ) ) { return $required_capabilities; } // If this is provided, it is the custom field. if ( empty( $args[3] ) ) { return $required_capabilities; } // If the meta key is part of the plugin, grant capabilities accordingly. if ( strpos( $args[3], WPSEO_Meta::$meta_prefix ) === 0 && current_user_can( 'edit_post', $args[2] ) ) { $required_capabilities[ $args[0] ] = true; } return $required_capabilities; } add_filter( 'user_has_cap', 'allow_custom_field_edits', 0, 3 ); class-wpseo-rank.php 0000644 00000016266 14720701066 0010464 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Holder for SEO Rank information. */ class WPSEO_Rank { /** * Constant used for determining a bad SEO rating. * * @var string */ public const BAD = 'bad'; /** * Constant used for determining an OK SEO rating. * * @var string */ public const OK = 'ok'; /** * Constant used for determining a good SEO rating. * * @var string */ public const GOOD = 'good'; /** * Constant used for determining that no focus keyphrase is set. * * @var string */ public const NO_FOCUS = 'na'; /** * Constant used for determining that this content is not indexed. * * @var string */ public const NO_INDEX = 'noindex'; /** * All possible ranks. * * @var array */ protected static $ranks = [ self::BAD, self::OK, self::GOOD, self::NO_FOCUS, self::NO_INDEX, ]; /** * Holds the translation from seo score slug to actual score range. * * @var array */ protected static $ranges = [ self::NO_FOCUS => [ 'start' => 0, 'end' => 0, ], self::BAD => [ 'start' => 1, 'end' => 40, ], self::OK => [ 'start' => 41, 'end' => 70, ], self::GOOD => [ 'start' => 71, 'end' => 100, ], ]; /** * The current rank. * * @var int */ protected $rank; /** * WPSEO_Rank constructor. * * @param int $rank The actual rank. */ public function __construct( $rank ) { if ( ! in_array( $rank, self::$ranks, true ) ) { $rank = self::BAD; } $this->rank = $rank; } /** * Returns the saved rank for this rank. * * @return string */ public function get_rank() { return $this->rank; } /** * Returns a CSS class for this rank. * * @return string */ public function get_css_class() { $labels = [ self::NO_FOCUS => 'na', self::NO_INDEX => 'noindex', self::BAD => 'bad', self::OK => 'ok', self::GOOD => 'good', ]; return $labels[ $this->rank ]; } /** * Returns a label for this rank. * * @return string */ public function get_label() { $labels = [ self::NO_FOCUS => __( 'Not available', 'wordpress-seo' ), self::NO_INDEX => __( 'No index', 'wordpress-seo' ), self::BAD => __( 'Needs improvement', 'wordpress-seo' ), self::OK => __( 'OK', 'wordpress-seo' ), self::GOOD => __( 'Good', 'wordpress-seo' ), ]; return $labels[ $this->rank ]; } /** * Returns an inclusive language label for this rank. * The only difference with get_label above is that we return "Potentially non-inclusive" for an OK rank. * * @return string */ public function get_inclusive_language_label() { if ( $this->rank === self::OK ) { return __( 'Potentially non-inclusive', 'wordpress-seo' ); } return $this->get_label(); } /** * Returns a label for use in a drop down. * * @return mixed */ public function get_drop_down_label() { $labels = [ self::NO_FOCUS => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'No Focus Keyphrase', 'wordpress-seo' ) ), self::BAD => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'Needs improvement', 'wordpress-seo' ) ), self::OK => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'OK', 'wordpress-seo' ) ), self::GOOD => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ) ), self::NO_INDEX => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'Post Noindexed', 'wordpress-seo' ) ), ]; return $labels[ $this->rank ]; } /** * Gets the drop down labels for the readability score. * * @return string The readability rank label. */ public function get_drop_down_readability_labels() { $labels = [ self::BAD => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'Needs improvement', 'wordpress-seo' ) ), self::OK => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'OK', 'wordpress-seo' ) ), self::GOOD => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ) ), ]; return $labels[ $this->rank ]; } /** * Gets the drop down labels for the inclusive language score. * * @return string The inclusive language rank label. */ public function get_drop_down_inclusive_language_labels() { $labels = [ self::BAD => sprintf( /* translators: %s expands to the inclusive language score */ __( 'Inclusive language: %s', 'wordpress-seo' ), __( 'Needs improvement', 'wordpress-seo' ) ), self::OK => sprintf( /* translators: %s expands to the inclusive language score */ __( 'Inclusive language: %s', 'wordpress-seo' ), __( 'Potentially non-inclusive', 'wordpress-seo' ) ), self::GOOD => sprintf( /* translators: %s expands to the inclusive language score */ __( 'Inclusive language: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ) ), ]; return $labels[ $this->rank ]; } /** * Get the starting score for this rank. * * @return int The start score. */ public function get_starting_score() { // No index does not have a starting score. if ( $this->rank === self::NO_INDEX ) { return -1; } return self::$ranges[ $this->rank ]['start']; } /** * Get the ending score for this rank. * * @return int The end score. */ public function get_end_score() { // No index does not have an end score. if ( $this->rank === self::NO_INDEX ) { return -1; } return self::$ranges[ $this->rank ]['end']; } /** * Returns a rank for a specific numeric score. * * @param int $score The score to determine a rank for. * * @return self */ public static function from_numeric_score( $score ) { // Set up the default value. $rank = new self( self::BAD ); foreach ( self::$ranges as $rank_index => $range ) { if ( $range['start'] <= $score && $score <= $range['end'] ) { $rank = new self( $rank_index ); break; } } return $rank; } /** * Returns a list of all possible SEO Ranks. * * @return WPSEO_Rank[] */ public static function get_all_ranks() { return array_map( [ 'WPSEO_Rank', 'create_rank' ], self::$ranks ); } /** * Returns a list of all possible Readability Ranks. * * @return WPSEO_Rank[] */ public static function get_all_readability_ranks() { return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD ] ); } /** * Returns a list of all possible Inclusive Language Ranks. * * @return WPSEO_Rank[] */ public static function get_all_inclusive_language_ranks() { return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD ] ); } /** * Converts a numeric rank into a WPSEO_Rank object, for use in functional array_* functions. * * @param string $rank SEO Rank. * * @return WPSEO_Rank */ private static function create_rank( $rank ) { return new self( $rank ); } } interface-wpseo-wordpress-integration.php 0000644 00000000534 14720701066 0014724 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ if ( ! interface_exists( 'WPSEO_WordPress_Integration' ) ) { /** * An interface for registering integrations with WordPress. */ interface WPSEO_WordPress_Integration { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks(); } } class-my-yoast-api-request.php 0000644 00000011432 14720701066 0012403 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Inc */ /** * Handles requests to MyYoast. */ class WPSEO_MyYoast_Api_Request { /** * The Request URL. * * @var string */ protected $url; /** * The request parameters. * * @var array */ protected $args = [ 'method' => 'GET', 'timeout' => 5, 'headers' => [ 'Accept-Encoding' => '*', 'Expect' => '', ], ]; /** * Contains the fetched response. * * @var stdClass */ protected $response; /** * Contains the error message when request went wrong. * * @var string */ protected $error_message = ''; /** * Constructor. * * @codeCoverageIgnore * * @param string $url The request url. * @param array $args The request arguments. */ public function __construct( $url, array $args = [] ) { $this->url = 'https://my.yoast.com/api/' . $url; $this->args = wp_parse_args( $args, $this->args ); } /** * Fires the request. * * @return bool True when request is successful. */ public function fire() { try { $response = $this->do_request( $this->url, $this->args ); $this->response = $this->decode_response( $response ); return true; } catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request_exception ) { $this->error_message = $bad_request_exception->getMessage(); return false; } } /** * Retrieves the error message. * * @return string The set error message. */ public function get_error_message() { return $this->error_message; } /** * Retrieves the response. * * @return stdClass The response object. */ public function get_response() { return $this->response; } /** * Performs the request using WordPress internals. * * @codeCoverageIgnore * * @param string $url The request URL. * @param array $request_arguments The request arguments. * * @return string The retrieved body. * @throws WPSEO_MyYoast_Bad_Request_Exception When request is invalid. */ protected function do_request( $url, $request_arguments ) { $request_arguments = $this->enrich_request_arguments( $request_arguments ); $response = wp_remote_request( $url, $request_arguments ); if ( is_wp_error( $response ) ) { throw new WPSEO_MyYoast_Bad_Request_Exception( $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); // Do nothing, response code is okay. if ( $response_code === 200 || strpos( $response_code, '200' ) !== false ) { return wp_remote_retrieve_body( $response ); } throw new WPSEO_MyYoast_Bad_Request_Exception( esc_html( $response_message ), (int) $response_code ); } /** * Decodes the JSON encoded response. * * @param string $response The response to decode. * * @return stdClass The json decoded response. * @throws WPSEO_MyYoast_Invalid_JSON_Exception When decoded string is not a JSON object. */ protected function decode_response( $response ) { $response = json_decode( $response ); if ( ! is_object( $response ) ) { throw new WPSEO_MyYoast_Invalid_JSON_Exception( esc_html__( 'No JSON object was returned.', 'wordpress-seo' ) ); } return $response; } /** * Checks if MyYoast tokens are allowed and adds the token to the request body. * * When tokens are disallowed it will add the url to the request body. * * @param array $request_arguments The arguments to enrich. * * @return array The enriched arguments. */ protected function enrich_request_arguments( array $request_arguments ) { $request_arguments = wp_parse_args( $request_arguments, [ 'headers' => [] ] ); $addon_version_headers = $this->get_installed_addon_versions(); foreach ( $addon_version_headers as $addon => $version ) { $request_arguments['headers'][ $addon . '-version' ] = $version; } $request_body = $this->get_request_body(); if ( $request_body !== [] ) { $request_arguments['body'] = $request_body; } return $request_arguments; } /** * Retrieves the request body based on URL or access token support. * * @codeCoverageIgnore * * @return array The request body. */ public function get_request_body() { return [ 'url' => WPSEO_Utils::get_home_url() ]; } /** * Wraps the get current user id function. * * @codeCoverageIgnore * * @return int The user id. */ protected function get_current_user_id() { return get_current_user_id(); } /** * Retrieves the installed addons as http headers. * * @codeCoverageIgnore * * @return array The installed addon versions. */ protected function get_installed_addon_versions() { $addon_manager = new WPSEO_Addon_Manager(); return $addon_manager->get_installed_addons_versions(); } } svg-icons.php 0000644 00000003611 14720702275 0007174 0 ustar 00 <?php /** * Twenty Twenty SVG Icon helper functions * * @package WordPress * @subpackage Twenty_Twenty * @since Twenty Twenty 1.0 */ if ( ! function_exists( 'twentytwenty_the_theme_svg' ) ) { /** * Output and Get Theme SVG. * Output and get the SVG markup for an icon in the TwentyTwenty_SVG_Icons class. * * @since Twenty Twenty 1.0 * * @param string $svg_name The name of the icon. * @param string $group The group the icon belongs to. * @param string $color Color code. */ function twentytwenty_the_theme_svg( $svg_name, $group = 'ui', $color = '' ) { echo twentytwenty_get_theme_svg( $svg_name, $group, $color ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in twentytwenty_get_theme_svg(). } } if ( ! function_exists( 'twentytwenty_get_theme_svg' ) ) { /** * Get information about the SVG icon. * * @since Twenty Twenty 1.0 * * @param string $svg_name The name of the icon. * @param string $group The group the icon belongs to. * @param string $color Color code. */ function twentytwenty_get_theme_svg( $svg_name, $group = 'ui', $color = '' ) { // Make sure that only our allowed tags and attributes are included. $svg = wp_kses( TwentyTwenty_SVG_Icons::get_svg( $svg_name, $group, $color ), array( 'svg' => array( 'class' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'aria-hidden' => true, 'role' => true, 'focusable' => true, ), 'path' => array( 'fill' => true, 'fill-rule' => true, 'd' => true, 'transform' => true, ), 'polygon' => array( 'fill' => true, 'fill-rule' => true, 'points' => true, 'transform' => true, 'focusable' => true, ), ) ); if ( ! $svg ) { return false; } return $svg; } } class-envato-market-github.php 0000644 00000023435 14720702445 0012431 0 ustar 00 <?php /** * Envato Market Github class. * * @package Envato_Market */ if ( ! class_exists( 'Envato_Market_Github' ) ) : /** * Creates the connection between Github to install & update the Envato Market plugin. * * @class Envato_Market_Github * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Github { /** * Action nonce. * * @type string */ const AJAX_ACTION = 'envato_market_dismiss_notice'; /** * The single class instance. * * @since 1.0.0 * @access private * * @var object */ private static $_instance = null; /** * The API URL. * * @since 1.0.0 * @access private * * @var string */ private static $api_url = 'https://envato.github.io/wp-envato-market/dist/update-check.json'; /** * The Envato_Market_Items Instance * * Ensures only one instance of this class exists in memory at any one time. * * @see Envato_Market_Github() * @uses Envato_Market_Github::init_actions() Setup hooks and actions. * * @since 1.0.0 * @static * @return object The one true Envato_Market_Github. * @codeCoverageIgnore */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); self::$_instance->init_actions(); } return self::$_instance; } /** * A dummy constructor to prevent this class from being loaded more than once. * * @see Envato_Market_Github::instance() * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function __construct() { /* We do nothing here! */ } /** * You cannot clone this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __clone() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * You cannot unserialize instances of this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * Setup the actions and filters. * * @uses add_action() To add actions. * @uses add_filter() To add filters. * * @since 1.0.0 */ public function init_actions() { // Bail outside of the WP Admin panel. if ( ! is_admin() ) { return; } add_filter( 'http_request_args', array( $this, 'update_check' ), 5, 2 ); add_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 ); add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'update_plugins' ) ); add_filter( 'pre_set_transient_update_plugins', array( $this, 'update_plugins' ) ); add_filter( 'site_transient_update_plugins', array( $this, 'update_state' ) ); add_filter( 'transient_update_plugins', array( $this, 'update_state' ) ); add_action( 'admin_notices', array( $this, 'notice' ) ); add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'dismiss_notice' ) ); } /** * Check Github for an update. * * @since 1.0.0 * * @return false|object */ public function api_check() { $raw_response = wp_remote_get( self::$api_url ); if ( is_wp_error( $raw_response ) ) { return false; } if ( ! empty( $raw_response['body'] ) ) { $raw_body = json_decode( $raw_response['body'], true ); if ( $raw_body ) { return (object) $raw_body; } } return false; } /** * Disables requests to the wp.org repository for Envato Market. * * @since 1.0.0 * * @param array $request An array of HTTP request arguments. * @param string $url The request URL. * @return array */ public function update_check( $request, $url ) { // Plugin update request. if ( false !== strpos( $url, '//api.wordpress.org/plugins/update-check/1.1/' ) ) { // Decode JSON so we can manipulate the array. $data = json_decode( $request['body']['plugins'] ); // Remove the Envato Market. unset( $data->plugins->{'envato-market/envato-market.php'} ); // Encode back into JSON and update the response. $request['body']['plugins'] = wp_json_encode( $data ); } return $request; } /** * API check. * * @since 1.0.0 * * @param bool $api Always false. * @param string $action The API action being performed. * @param object $args Plugin arguments. * @return mixed $api The plugin info or false. */ public function plugins_api( $api, $action, $args ) { if ( isset( $args->slug ) && 'envato-market' === $args->slug ) { $api_check = $this->api_check(); if ( is_object( $api_check ) ) { $api = $api_check; } } return $api; } /** * Update check. * * @since 1.0.0 * * @param object $transient The pre-saved value of the `update_plugins` site transient. * @return object */ public function update_plugins( $transient ) { $state = $this->state(); if ( 'activated' === $state ) { $api_check = $this->api_check(); if ( is_object( $api_check ) && version_compare( envato_market()->get_version(), $api_check->version, '<' ) ) { $transient->response['envato-market/envato-market.php'] = (object) array( 'slug' => 'envato-market', 'plugin' => 'envato-market/envato-market.php', 'new_version' => $api_check->version, 'url' => 'https://github.com/envato/wp-envato-market', 'package' => $api_check->download_link, ); } } return $transient; } /** * Set the plugin state. * * @since 1.0.0 * * @return string */ public function state() { $option = 'envato_market_state'; $active_plugins = apply_filters( 'active_plugins', get_option( 'active_plugins' ) ); // We also have to check network activated plugins. Otherwise this plugin won't update on multisite. $active_sitewide_plugins = get_site_option( 'active_sitewide_plugins' ); if ( ! is_array( $active_plugins ) ) { $active_plugins = array(); } if ( ! is_array( $active_sitewide_plugins ) ) { $active_sitewide_plugins = array(); } $active_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) ); if ( in_array( 'envato-market/envato-market.php', $active_plugins ) ) { $state = 'activated'; update_option( $option, $state ); } else { $state = 'install'; update_option( $option, $state ); foreach ( array_keys( get_plugins() ) as $plugin ) { if ( strpos( $plugin, 'envato-market.php' ) !== false ) { $state = 'deactivated'; update_option( $option, $state ); } } } return $state; } /** * Force the plugin state to be updated. * * @since 1.0.0 * * @param object $transient The saved value of the `update_plugins` site transient. * @return object */ public function update_state( $transient ) { $state = $this->state(); return $transient; } /** * Admin notices. * * @since 1.0.0 * * @return string */ public function notice() { $screen = get_current_screen(); $slug = 'envato-market'; $state = get_option( 'envato_market_state' ); $notice = get_option( self::AJAX_ACTION ); if ( empty( $state ) ) { $state = $this->state(); } if ( 'activated' === $state || 'update-core' === $screen->id || 'update' === $screen->id || 'plugins' === $screen->id && isset( $_GET['action'] ) && 'delete-selected' === $_GET['action'] || 'dismissed' === $notice ) { return; } if ( 'deactivated' === $state ) { $activate_url = add_query_arg( array( 'action' => 'activate', 'plugin' => urlencode( "$slug/$slug.php" ), '_wpnonce' => urlencode( wp_create_nonce( "activate-plugin_$slug/$slug.php" ) ), ), self_admin_url( 'plugins.php' ) ); $message = sprintf( esc_html__( '%1$sActivate the Envato Market plugin%2$s to get updates for your ThemeForest and CodeCanyon items.', 'envato-market' ), '<a href="' . esc_url( $activate_url ) . '">', '</a>' ); } elseif ( 'install' === $state ) { $install_url = add_query_arg( array( 'action' => 'install-plugin', 'plugin' => $slug, ), self_admin_url( 'update.php' ) ); $message = sprintf( esc_html__( '%1$sInstall the Envato Market plugin%2$s to get updates for your ThemeForest and CodeCanyon items.', 'envato-market' ), '<a href="' . esc_url( wp_nonce_url( $install_url, 'install-plugin_' . $slug ) ) . '">', '</a>' ); } if ( isset( $message ) ) { ?> <div class="updated envato-market-notice notice is-dismissible"> <p><?php echo wp_kses_post( $message ); ?></p> </div> <script> jQuery( document ).ready( function( $ ) { $( document ).on( 'click', '.envato-market-notice .notice-dismiss', function() { $.ajax( { url: ajaxurl, data: { action: '<?php echo self::AJAX_ACTION; ?>', nonce: '<?php echo wp_create_nonce( self::AJAX_ACTION ); ?>' } } ); } ); } ); </script> <?php } } /** * Dismiss admin notice. * * @since 1.0.0 */ public function dismiss_notice() { if ( ! check_ajax_referer( self::AJAX_ACTION, 'nonce', false ) ) { status_header( 400 ); wp_send_json_error( 'bad_nonce' ); } elseif ( ! current_user_can( 'update_plugins' ) ) { wp_send_json_error( array( 'message' => __( 'User not allowed to update items.', 'envato-market' ) ) ); } update_option( self::AJAX_ACTION, 'dismissed' ); wp_send_json_success(); } } if ( ! function_exists( 'envato_market_github' ) ) : /** * Envato_Market_Github Instance * * @since 1.0.0 * * @return Envato_Market_Github */ function envato_market_github() { return Envato_Market_Github::instance(); } endif; /** * Loads the main instance of Envato_Market_Github * * @since 1.0.0 */ add_action( 'after_setup_theme', 'envato_market_github', 99 ); endif; class-envato-market.php 0000644 00000024235 14720702445 0011150 0 ustar 00 <?php /** * Envato Market class. * * @package Envato_Market */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'Envato_Market' ) ) : /** * It's the main class that does all the things. * * @class Envato_Market * @version 1.0.0 * @since 1.0.0 */ final class Envato_Market { /** * The single class instance. * * @since 1.0.0 * @access private * * @var object */ private static $_instance = null; /** * Plugin data. * * @since 1.0.0 * @access private * * @var object */ private $data; /** * The slug. * * @since 1.0.0 * @access private * * @var string */ private $slug; /** * The version number. * * @since 1.0.0 * @access private * * @var string */ private $version; /** * The web URL to the plugin directory. * * @since 1.0.0 * @access private * * @var string */ private $plugin_url; /** * The server path to the plugin directory. * * @since 1.0.0 * @access private * * @var string */ private $plugin_path; /** * The web URL to the plugin admin page. * * @since 1.0.0 * @access private * * @var string */ private $page_url; /** * The setting option name. * * @since 1.0.0 * @access private * * @var string */ private $option_name; private $envato_api_domain; private $envato_api_headers; /** * Main Envato_Market Instance * * Ensures only one instance of this class exists in memory at any one time. * * @see Envato_Market() * @uses Envato_Market::init_globals() Setup class globals. * @uses Envato_Market::init_includes() Include required files. * @uses Envato_Market::init_actions() Setup hooks and actions. * * @since 1.0.0 * @static * @return Envato_Market. * @codeCoverageIgnore */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); self::$_instance->init_globals(); self::$_instance->init_includes(); self::$_instance->init_actions(); } return self::$_instance; } /** * A dummy constructor to prevent this class from being loaded more than once. * * @see Envato_Market::instance() * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function __construct() { /* We do nothing here! */ } /** * You cannot clone this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __clone() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * You cannot unserialize instances of this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * Setup the class globals. * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function init_globals() { $this->data = new stdClass(); $this->version = ENVATO_MARKET_VERSION; $this->slug = 'envato-market'; $this->option_name = self::sanitize_key( $this->slug ); $this->plugin_url = ENVATO_MARKET_URI; $this->plugin_path = ENVATO_MARKET_PATH; $this->page_url = ENVATO_MARKET_NETWORK_ACTIVATED ? network_admin_url( 'admin.php?page=' . $this->slug ) : admin_url( 'admin.php?page=' . $this->slug ); $this->data->admin = true; if ( defined('ENVATO_LOCAL_DEVELOPMENT') ) { $this->envato_api_domain = ENVATO_API_DOMAIN; $this->envato_api_headers = ENVATO_API_HEADERS; } else { $this->envato_api_headers = [ 'Authorization' => 'Bearer ' . $this->get_option( 'token' ) ]; $this->envato_api_domain = 'https://api.envato.com'; } } /** * Include required files. * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function init_includes() { require $this->plugin_path . '/inc/admin/class-envato-market-admin.php'; require $this->plugin_path . '/inc/admin/functions.php'; require $this->plugin_path . '/inc/class-envato-market-api.php'; require $this->plugin_path . '/inc/class-envato-market-items.php'; require $this->plugin_path . '/inc/class-envato-market-github.php'; } /** * Setup the hooks, actions and filters. * * @uses add_action() To add actions. * @uses add_filter() To add filters. * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function init_actions() { // Activate plugin. register_activation_hook( ENVATO_MARKET_CORE_FILE, array( $this, 'activate' ) ); // Deactivate plugin. register_deactivation_hook( ENVATO_MARKET_CORE_FILE, array( $this, 'deactivate' ) ); // Load the textdomain. add_action( 'init', array( $this, 'load_textdomain' ) ); // Load OAuth. add_action( 'init', array( $this, 'admin' ) ); // Load Upgrader. add_action( 'init', array( $this, 'items' ) ); } /** * Activate plugin. * * @since 1.0.0 * @codeCoverageIgnore */ public function activate() { self::set_plugin_state( true ); } /** * Deactivate plugin. * * @since 1.0.0 * @codeCoverageIgnore */ public function deactivate() { self::set_plugin_state( false ); } /** * Loads the plugin's translated strings. * * @since 1.0.0 * @codeCoverageIgnore */ public function load_textdomain() { load_plugin_textdomain( 'envato-market', false, ENVATO_MARKET_PATH . 'languages/' ); } /** * Sanitize data key. * * @since 1.0.0 * @access private * * @param string $key An alpha numeric string to sanitize. * @return string */ private function sanitize_key( $key ) { return preg_replace( '/[^A-Za-z0-9\_]/i', '', str_replace( array( '-', ':' ), '_', $key ) ); } /** * Recursively converts data arrays to objects. * * @since 1.0.0 * @access private * * @param array $array An array of data. * @return object */ private function convert_data( $array ) { foreach ( (array) $array as $key => $value ) { if ( is_array( $value ) ) { $array[ $key ] = self::convert_data( $value ); } } return (object) $array; } /** * Set the `is_plugin_active` option. * * This setting helps determine context. Since the plugin can be included in your theme root you * might want to hide the admin UI when the plugin is not activated and implement your own. * * @since 1.0.0 * @access private * * @param bool $value Whether or not the plugin is active. */ private function set_plugin_state( $value ) { self::set_option( 'is_plugin_active', $value ); } /** * Set option value. * * @since 1.0.0 * * @param string $name Option name. * @param mixed $option Option data. */ public function set_option( $name, $option ) { $options = self::get_options(); $name = self::sanitize_key( $name ); $options[ $name ] = esc_html( $option ); $this->set_options( $options ); } /** * Set option. * * @since 2.0.0 * * @param mixed $options Option data. */ public function set_options( $options ) { ENVATO_MARKET_NETWORK_ACTIVATED ? update_site_option( $this->option_name, $options ) : update_option( $this->option_name, $options ); } /** * Return the option settings array. * * @since 1.0.0 */ public function get_options() { return ENVATO_MARKET_NETWORK_ACTIVATED ? get_site_option( $this->option_name, array() ) : get_option( $this->option_name, array() ); } /** * Return a value from the option settings array. * * @since 1.0.0 * * @param string $name Option name. * @param mixed $default The default value if nothing is set. * @return mixed */ public function get_option( $name, $default = '' ) { $options = self::get_options(); $name = self::sanitize_key( $name ); return isset( $options[ $name ] ) ? $options[ $name ] : $default; } /** * Set data. * * @since 1.0.0 * * @param string $key Unique object key. * @param mixed $data Any kind of data. */ public function set_data( $key, $data ) { if ( ! empty( $key ) ) { if ( is_array( $data ) ) { $data = self::convert_data( $data ); } $key = self::sanitize_key( $key ); // @codingStandardsIgnoreStart $this->data->$key = $data; // @codingStandardsIgnoreEnd } } /** * Get data. * * @since 1.0.0 * * @param string $key Unique object key. * @return string|object */ public function get_data( $key ) { return isset( $this->data->$key ) ? $this->data->$key : ''; } /** * Return the plugin slug. * * @since 1.0.0 * * @return string */ public function get_slug() { return $this->slug; } /** * Return the plugin version number. * * @since 1.0.0 * * @return string */ public function get_version() { return $this->version; } /** * Return the plugin URL. * * @since 1.0.0 * * @return string */ public function get_plugin_url() { return $this->plugin_url; } /** * Return the plugin path. * * @since 1.0.0 * * @return string */ public function get_plugin_path() { return $this->plugin_path; } /** * Return the plugin page URL. * * @since 1.0.0 * * @return string */ public function get_page_url() { return $this->page_url; } /** * Return the option settings name. * * @since 1.0.0 * * @return string */ public function get_option_name() { return $this->option_name; } /** * Admin UI class. * * @since 1.0.0 * * @return Envato_Market_Admin */ public function admin() { return Envato_Market_Admin::instance(); } /** * Envato API class. * * @since 1.0.0 * * @return Envato_Market_API */ public function api() { return Envato_Market_API::instance(); } /** * Items class. * * @since 1.0.0 * * @return Envato_Market_Items */ public function items() { return Envato_Market_Items::instance(); } public function get_envato_api_domain() { return $this->envato_api_domain; } public function get_envato_api_headers() { return $this->envato_api_headers; } } endif; class-envato-market-items.php 0000644 00000041660 14720702445 0012270 0 ustar 00 <?php /** * Items class. * * @package Envato_Market */ if ( ! class_exists( 'Envato_Market_Items' ) ) : /** * Creates the theme & plugin arrays & injects API results. * * @class Envato_Market_Items * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Items { /** * The single class instance. * * @since 1.0.0 * @access private * * @var object */ private static $_instance = null; /** * Premium themes. * * @since 1.0.0 * @access private * * @var array */ private static $themes = array(); /** * Premium plugins. * * @since 1.0.0 * @access private * * @var array */ private static $plugins = array(); /** * WordPress plugins. * * @since 1.0.0 * @access private * * @var array */ private static $wp_plugins = array(); /** * The Envato_Market_Items Instance * * Ensures only one instance of this class exists in memory at any one time. * * @see Envato_Market_Items() * @uses Envato_Market_Items::init_actions() Setup hooks and actions. * * @since 1.0.0 * @static * @return object The one true Envato_Market_Items. * @codeCoverageIgnore */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); self::$_instance->init_actions(); } return self::$_instance; } /** * A dummy constructor to prevent this class from being loaded more than once. * * @see Envato_Market_Items::instance() * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function __construct() { /* We do nothing here! */ } /** * You cannot clone this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __clone() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * You cannot unserialize instances of this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * Setup the hooks, actions and filters. * * @uses add_action() To add actions. * @uses add_filter() To add filters. * * @since 1.0.0 */ public function init_actions() { // Check for theme & plugin updates. add_filter( 'http_request_args', array( $this, 'update_check' ), 5, 2 ); // Inject plugin updates into the response array. add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'update_plugins' ), 5, 1 ); add_filter( 'pre_set_transient_update_plugins', array( $this, 'update_plugins' ), 5, 1 ); // Inject theme updates into the response array. add_filter( 'pre_set_site_transient_update_themes', array( $this, 'update_themes' ), 1, 99999 ); add_filter( 'pre_set_transient_update_themes', array( $this, 'update_themes' ), 1, 99999 ); // Inject plugin information into the API calls. add_filter( 'plugins_api', array( $this, 'plugins_api' ), 10, 3 ); // Rebuild the saved theme data. add_action( 'after_switch_theme', array( $this, 'rebuild_themes' ) ); // Rebuild the saved plugin data. add_action( 'activated_plugin', array( $this, 'rebuild_plugins' ) ); add_action( 'deactivated_plugin', array( $this, 'rebuild_plugins' ) ); } /** * Get the premium plugins list. * * @since 1.0.0 * * @param string $group The plugin group. Options are 'purchased', 'active', 'installed', or 'install'. * @return array */ public function plugins( $group = '' ) { if ( ! empty( $group ) ) { if ( isset( self::$plugins[ $group ] ) ) { return self::$plugins[ $group ]; } else { return array(); } } return self::$plugins; } /** * Get the premium themes list. * * @since 1.0.0 * * @param string $group The theme group. Options are 'purchased', 'active', 'installed', or 'install'. * @return array */ public function themes( $group = '' ) { if ( ! empty( $group ) ) { if ( isset( self::$themes[ $group ] ) ) { return self::$themes[ $group ]; } else { return array(); } } return self::$themes; } /** * Get the list of WordPress plugins * * @since 1.0.0 * * @param bool $flush Forces a cache flush. Default is 'false'. * @return array */ public function wp_plugins( $flush = false ) { if ( empty( self::$wp_plugins ) || true === $flush ) { wp_cache_set( 'plugins', false, 'plugins' ); self::$wp_plugins = get_plugins(); } return self::$wp_plugins; } /** * Disables requests to the wp.org repository for premium themes. * * @since 1.0.0 * * @param array $request An array of HTTP request arguments. * @param string $url The request URL. * @return array */ public function update_check( $request, $url ) { // Theme update request. if ( false !== strpos( $url, '//api.wordpress.org/themes/update-check/1.1/' ) ) { /** * Excluded theme slugs that should never ping the WordPress API. * We don't need the extra http requests for themes we know are premium. */ self::set_themes(); $installed = self::$themes['installed']; // Decode JSON so we can manipulate the array. $data = json_decode( $request['body']['themes'] ); // Remove the excluded themes. foreach ( $installed as $slug => $id ) { unset( $data->themes->$slug ); } // Encode back into JSON and update the response. $request['body']['themes'] = wp_json_encode( $data ); } // Plugin update request. if ( false !== strpos( $url, '//api.wordpress.org/plugins/update-check/1.1/' ) ) { /** * Excluded theme slugs that should never ping the WordPress API. * We don't need the extra http requests for themes we know are premium. */ self::set_plugins(); $installed = self::$plugins['installed']; // Decode JSON so we can manipulate the array. $data = json_decode( $request['body']['plugins'] ); // Remove the excluded themes. foreach ( $installed as $slug => $id ) { unset( $data->plugins->$slug ); } // Encode back into JSON and update the response. $request['body']['plugins'] = wp_json_encode( $data ); } return $request; } /** * Inject update data for premium themes. * * @since 1.0.0 * * @param object $transient The pre-saved value of the `update_themes` site transient. * @return object */ public function update_themes( $transient ) { // Process premium theme updates. if ( isset( $transient->checked ) ) { self::set_themes( true ); $installed = array_merge( self::$themes['active'], self::$themes['installed'] ); foreach ( $installed as $slug => $premium ) { $theme = wp_get_theme( $slug ); if ( $theme->exists() && version_compare( $theme->get( 'Version' ), $premium['version'], '<' ) ) { $transient->response[ $slug ] = array( 'theme' => $slug, 'new_version' => $premium['version'], 'url' => $premium['url'], 'package' => envato_market()->api()->deferred_download( $premium['id'] ), ); } } } return $transient; } /** * Inject update data for premium plugins. * * @since 1.0.0 * * @param object $transient The pre-saved value of the `update_plugins` site transient. * @return object */ public function update_plugins( $transient ) { self::set_plugins( true ); // Process premium plugin updates. $installed = array_merge( self::$plugins['active'], self::$plugins['installed'] ); $plugins = self::wp_plugins(); foreach ( $installed as $plugin => $premium ) { if ( isset( $plugins[ $plugin ] ) && version_compare( $plugins[ $plugin ]['Version'], $premium['version'], '<' ) ) { $_plugin = array( 'slug' => dirname( $plugin ), 'plugin' => $plugin, 'new_version' => $premium['version'], 'url' => $premium['url'], 'package' => envato_market()->api()->deferred_download( $premium['id'] ), ); $transient->response[ $plugin ] = (object) $_plugin; } } return $transient; } /** * Inject API data for premium plugins. * * @since 1.0.0 * * @param bool $response Always false. * @param string $action The API action being performed. * @param object $args Plugin arguments. * @return bool|object $response The plugin info or false. */ public function plugins_api( $response, $action, $args ) { self::set_plugins( true ); // Process premium theme updates. if ( 'plugin_information' === $action && isset( $args->slug ) ) { $installed = array_merge( self::$plugins['active'], self::$plugins['installed'] ); foreach ( $installed as $slug => $plugin ) { if ( dirname( $slug ) === $args->slug ) { $response = new stdClass(); $response->slug = $args->slug; $response->plugin = $slug; $response->plugin_name = $plugin['name']; $response->name = $plugin['name']; $response->version = $plugin['version']; $response->author = $plugin['author']; $response->homepage = $plugin['url']; $response->requires = $plugin['requires']; $response->tested = $plugin['tested']; $response->downloaded = $plugin['number_of_sales']; $response->last_updated = $plugin['updated_at']; $response->sections = array( 'description' => $plugin['description'] ); $response->banners['low'] = $plugin['landscape_url']; $response->rating = ! empty( $plugin['rating'] ) && ! empty( $plugin['rating']['rating'] ) && $plugin['rating']['rating'] > 0 ? $plugin['rating']['rating'] / 5 * 100 : 0; $response->num_ratings = ! empty( $plugin['rating'] ) && ! empty( $plugin['rating']['count'] ) ? $plugin['rating']['count'] : 0; $response->download_link = envato_market()->api()->deferred_download( $plugin['id'] ); break; } } } return $response; } /** * Set the list of themes * * @since 1.0.0 * * @param bool $forced Forces an API request. Default is 'false'. * @param bool $use_cache Attempts to rebuild from the cache before making an API request. */ public function set_themes( $forced = false, $use_cache = false ) { $themes_transient = get_site_transient( envato_market()->get_option_name() . '_themes' ); self::$themes = is_array($themes_transient) ? $themes_transient : array(); if ( empty(self::$themes) || true === $forced ) { $themes = envato_market()->api()->themes(); foreach ( envato_market()->get_option( 'items', array() ) as $item ) { if ( empty( $item ) ) { continue; } if ( 'theme' === $item['type'] ) { $request_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $item['token'], ), ); $request = envato_market()->api()->item( $item['id'], $request_args ); if ( false !== $request ) { $themes[] = $request; } } } self::process_themes( $themes ); } elseif ( true === $use_cache ) { self::process_themes( self::$themes['purchased'] ); } } /** * Set the list of plugins * * @since 1.0.0 * * @param bool $forced Forces an API request. Default is 'false'. * @param bool $use_cache Attempts to rebuild from the cache before making an API request. * @param array $args Used to remove or add a plugin during activate and deactivate routines. */ public function set_plugins( $forced = false, $use_cache = false, $args = array() ) { $plugins_transient = get_site_transient( envato_market()->get_option_name() . '_plugins' ); self::$plugins = is_array($plugins_transient) ? $plugins_transient : array(); if ( empty(self::$plugins) || true === $forced ) { $plugins = envato_market()->api()->plugins(); foreach ( envato_market()->get_option( 'items', array() ) as $item ) { if ( empty( $item ) ) { continue; } if ( 'plugin' === $item['type'] ) { $request_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $item['token'], ), ); $request = envato_market()->api()->item( $item['id'], $request_args ); if ( false !== $request ) { $plugins[] = $request; } } } self::process_plugins( $plugins, $args ); } elseif ( true === $use_cache ) { self::process_plugins( self::$plugins['purchased'], $args ); } } /** * Rebuild the themes array using the cache value if possible. * * @since 1.0.0 * * @param mixed $filter Any data being filtered. * @return mixed */ public function rebuild_themes( $filter ) { self::set_themes( false, true ); return $filter; } /** * Rebuild the plugins array using the cache value if possible. * * @since 1.0.0 * * @param string $plugin The plugin to add or remove. */ public function rebuild_plugins( $plugin ) { $remove = ( 'deactivated_plugin' === current_filter() ) ? true : false; self::set_plugins( false, true, array( 'plugin' => $plugin, 'remove' => $remove, ) ); } /** * Normalizes a string to do a value check against. * * Strip all HTML tags including script and style & then decode the * HTML entities so `&` will equal `&` in the value check and * finally lower case the entire string. This is required becuase some * themes & plugins add a link to the Author field or ambersands to the * names, or change the case of their files or names, which will not match * the saved value in the database causing a false negative. * * @since 1.0.0 * * @param string $string The string to normalize. * @return string */ public function normalize( $string ) { return strtolower( html_entity_decode( wp_strip_all_tags( $string ) ) ); } /** * Process the themes and save the transient. * * @since 1.0.0 * * @param array $purchased The purchased themes array. */ private function process_themes( $purchased ) { if ( is_wp_error( $purchased ) ) { $purchased = array(); } $current = wp_get_theme()->get_template(); $active = array(); $installed = array(); $install = $purchased; if ( ! empty( $purchased ) ) { foreach ( wp_get_themes() as $theme ) { /** * WP_Theme object. * * @var WP_Theme $theme */ $template = $theme->get_template(); $title = $theme->get( 'Name' ); $author = $theme->get( 'Author' ); foreach ( $install as $key => $value ) { if ( $this->normalize( $value['name'] ) === $this->normalize( $title ) && $this->normalize( $value['author'] ) === $this->normalize( $author ) ) { $installed[ $template ] = $value; unset( $install[ $key ] ); } } } } if ( isset( $installed[ $current ] ) ) { $active[ $current ] = $installed[ $current ]; unset( $installed[ $current ] ); } self::$themes['purchased'] = array_unique( $purchased, SORT_REGULAR ); self::$themes['active'] = array_unique( $active, SORT_REGULAR ); self::$themes['installed'] = array_unique( $installed, SORT_REGULAR ); self::$themes['install'] = array_unique( array_values( $install ), SORT_REGULAR ); set_site_transient( envato_market()->get_option_name() . '_themes', self::$themes, HOUR_IN_SECONDS ); } /** * Process the plugins and save the transient. * * @since 1.0.0 * * @param array $purchased The purchased plugins array. * @param array $args Used to remove or add a plugin during activate and deactivate routines. */ private function process_plugins( $purchased, $args = array() ) { if ( is_wp_error( $purchased ) ) { $purchased = array(); } $active = array(); $installed = array(); $install = $purchased; if ( ! empty( $purchased ) ) { foreach ( self::wp_plugins( true ) as $slug => $plugin ) { foreach ( $install as $key => $value ) { if ( $this->normalize( $value['name'] ) === $this->normalize( $plugin['Name'] ) && $this->normalize( $value['author'] ) === $this->normalize( $plugin['Author'] ) && file_exists( WP_PLUGIN_DIR . '/' . $slug ) ) { $installed[ $slug ] = $value; unset( $install[ $key ] ); } } } } foreach ( $installed as $slug => $plugin ) { $condition = false; if ( ! empty( $args ) && $slug === $args['plugin'] ) { if ( true === $args['remove'] ) { continue; } $condition = true; } if ( $condition || is_plugin_active( $slug ) ) { $active[ $slug ] = $plugin; unset( $installed[ $slug ] ); } } self::$plugins['purchased'] = array_unique( $purchased, SORT_REGULAR ); self::$plugins['active'] = array_unique( $active, SORT_REGULAR ); self::$plugins['installed'] = array_unique( $installed, SORT_REGULAR ); self::$plugins['install'] = array_unique( array_values( $install ), SORT_REGULAR ); set_site_transient( envato_market()->get_option_name() . '_plugins', self::$plugins, HOUR_IN_SECONDS ); } } endif; admin/view/callback/section/oauth.php 0000644 00000002737 14720702445 0013655 0 ustar 00 <?php /** * OAuth section * * @package Envato_Market * @since 1.0.0 */ ?> <p> <?php printf( esc_html__( 'This area enables WordPress Theme & Plugin updates from Envato Market. Read more about how this process works at %s.', 'envato-market' ), '<a href="https://envato.com/market-plugin/" target="_blank">' . esc_html__( 'envato.com', 'envato-market' ) . '</a>' ); ?> </p> <p> <?php esc_html_e( 'Please follow the steps below:', 'envato-market' ); ?> </p> <ol> <li><?php printf( esc_html__( 'Generate an Envato API Personal Token by %s.', 'envato-market' ), '<a href="' . envato_market()->admin()->get_generate_token_url() . '" target="_blank">' . esc_html__( 'clicking this link', 'envato-market' ) . '</a>' ); ?></li> <li><?php esc_html_e( 'Name the token eg “My WordPress site”.', 'envato-market' ); ?></li> <li><?php esc_html_e( 'Ensure the following permissions are enabled:', 'envato-market' ); ?> <ul> <li><?php esc_html_e( 'View and search Envato sites', 'envato-market' ); ?></li> <li><?php esc_html_e( 'Download your purchased items', 'envato-market' ); ?></li> <li><?php esc_html_e( 'List purchases you\'ve made', 'envato-market' ); ?></li> </ul> </li> <li><?php esc_html_e( 'Copy the token into the box below.', 'envato-market' ); ?></li> <li><?php esc_html_e( 'Click the "Save Changes" button.', 'envato-market' ); ?></li> <li><?php esc_html_e( 'A list of purchased Themes & Plugins from Envato Market will appear.', 'envato-market' ); ?></li> </ol> admin/view/callback/section/items.php 0000644 00000001032 14720702445 0013641 0 ustar 00 <?php /** * Items section * * @package Envato_Market * @since 1.0.0 */ ?> <p><?php esc_html_e( 'Add Envato Market themes & plugins using multiple OAuth tokens. This is especially useful when an item has been purchased on behalf of a third-party. This works similarly to the global OAuth Personal Token, but for individual items and additionally requires the Envato Market item ID.', 'envato-market' ); ?></p> <p><?php esc_html_e( 'Warning: These tokens can be revoked by the account holder at any time.', 'envato-market' ); ?></p> admin/view/callback/setting/token.php 0000644 00000000612 14720702445 0013654 0 ustar 00 <?php /** * Token setting * * @package Envato_Market * @since 1.0.0 */ ?> <input type="text" name="<?php echo esc_attr( envato_market()->get_option_name() ); ?>[token]" class="widefat" value="<?php echo esc_html( envato_market()->get_option( 'token' ) ); ?>" autocomplete="off"> <p class="description"><?php esc_html_e( 'Enter your Envato API Personal Token.', 'envato-market' ); ?></p> admin/view/callback/setting/items.php 0000644 00000003531 14720702445 0013660 0 ustar 00 <?php /** * Items setting * * @package Envato_Market * @since 1.0.0 */ $items = envato_market()->get_option( 'items', array() ); ?> <ul id="envato-market-items"> <?php if ( ! empty( $items ) ) { foreach ( $items as $key => $item ) { if ( empty( $item['name'] ) || empty( $item['token'] ) || empty( $item['id'] ) || empty( $item['type'] ) || empty( $item['authorized'] ) ) { continue; } $class = 'success' === $item['authorized'] ? 'is-authorized' : 'not-authorized'; echo ' <li data-id="' . esc_attr( $item['id'] ) . '" class="' . esc_attr( $class ) . '"> <span class="item-name">' . esc_html__( 'ID', 'envato-market' ) . ': ' . esc_html( $item['id'] ) . ' - ' . esc_html( $item['name'] ) . '</span> <button class="item-delete dashicons dashicons-dismiss"> <span class="screen-reader-text">' . esc_html__( 'Delete', 'envato-market' ) . '</span> </button> <input type="hidden" name="' . esc_attr( envato_market()->get_option_name() ) . '[items][' . esc_attr( $key ) . '][name]" value="' . esc_html( $item['name'] ) . '" /> <input type="hidden" name="' . esc_attr( envato_market()->get_option_name() ) . '[items][' . esc_attr( $key ) . '][token]" value="' . esc_html( $item['token'] ) . '" /> <input type="hidden" name="' . esc_attr( envato_market()->get_option_name() ) . '[items][' . esc_attr( $key ) . '][id]" value="' . esc_html( $item['id'] ) . '" /> <input type="hidden" name="' . esc_attr( envato_market()->get_option_name() ) . '[items][' . esc_attr( $key ) . '][type]" value="' . esc_html( $item['type'] ) . '" /> <input type="hidden" name="' . esc_attr( envato_market()->get_option_name() ) . '[items][' . esc_attr( $key ) . '][authorized]" value="' . esc_html( $item['authorized'] ) . '" /> </li>'; } } ?> </ul> <button class="button add-envato-market-item"><?php esc_html_e( 'Add Item', 'envato-market' ); ?></button> admin/view/callback/admin.php 0000644 00000002011 14720702445 0012142 0 ustar 00 <?php /** * Admin UI * * @package Envato_Market * @since 1.0.0 */ if ( isset( $_GET['action'] ) ) { $id = ! empty( $_GET['id'] ) ? absint( trim( $_GET['id'] ) ) : ''; if ( 'install-plugin' === $_GET['action'] ) { Envato_Market_Admin::install_plugin( $id ); } elseif ( 'install-theme' === $_GET['action'] ) { Envato_Market_Admin::install_theme( $id ); } } else { add_thickbox(); ?> <div class="wrap about-wrap full-width-layout"> <?php Envato_Market_Admin::render_intro_partial(); ?> <?php Envato_Market_Admin::render_tabs_partial(); ?> <form method="POST" action="<?php echo esc_url( ENVATO_MARKET_NETWORK_ACTIVATED ? network_admin_url( 'edit.php?action=envato_market_network_settings' ) : admin_url( 'options.php' ) ); ?>"> <?php Envato_Market_Admin::render_themes_panel_partial(); ?> <?php Envato_Market_Admin::render_plugins_panel_partial(); ?> <?php Envato_Market_Admin::render_settings_panel_partial(); ?> <?php Envato_Market_Admin::render_help_panel_partial(); ?> </form> </div> <?php } admin/view/notice/error-missing-zip.php 0000644 00000000645 14720702445 0014212 0 ustar 00 <?php /** * Error notice * * @package Envato_Market * @since 2.0.1 */ ?> <div class="notice notice-error is-dismissible"> <p><?php printf( esc_html__( 'Failed to locate the package file for this item. Please contact the item author for support, or install/upgrade the item manually from the %s.', 'envato-market' ), '<a href="https://themeforest.net/downloads" target="_blank">downloads page</a>' ); ?></p> </div> admin/view/notice/error-single-use.php 0000644 00000000424 14720702445 0014007 0 ustar 00 <?php /** * Error notice * * @package Envato_Market * @since 1.0.0 */ ?> <div class="notice notice-error is-dismissible"> <p><?php esc_html_e( 'One or more Single Use OAuth Personal Tokens could not be verified and should be removed.', 'envato-market' ); ?></p> </div> admin/view/notice/success.php 0000644 00000000353 14720702445 0012256 0 ustar 00 <?php /** * Success notice * * @package Envato_Market * @since 1.0.0 */ ?> <div class="notice notice-success is-dismissible"> <p><?php esc_html_e( 'Your OAuth Personal Token has been verified.', 'envato-market' ); ?></p> </div> admin/view/notice/error.php 0000644 00000000513 14720702445 0011735 0 ustar 00 <?php /** * Error notice * * @package Envato_Market * @since 1.0.0 */ ?> <div class="notice notice-error is-dismissible"> <p><?php esc_html_e( 'The OAuth Personal Token could not be verified. Please check that the Token has been entered correctly and has the minimum required permissions.', 'envato-market' ); ?></p> </div> admin/view/notice/error-http.php 0000644 00000000741 14720702445 0012715 0 ustar 00 <?php /** * Error notice * * @package Envato_Market * @since 2.0.1 */ ?> <div class="notice notice-error is-dismissible"> <p><?php esc_html_e( 'Failed to connect to the Envato API. Please contact the hosting providier with this message: "The Envato Market WordPress plugin requires TLS version 1.2 or above, please confirm if this hosting account supports TLS version 1.2 and allows connections from WordPress to the host api.envato.com".', 'envato-market' ); ?></p> </div> admin/view/notice/success-no-items.php 0000644 00000000457 14720702445 0014014 0 ustar 00 <?php /** * Success notice * * @package Envato_Market * @since 1.0.0 */ ?> <div class="notice notice-success is-dismissible"> <p><?php esc_html_e( 'Your OAuth Personal Token has been verified. However, there are no WordPress downloadable items in your account.', 'envato-market' ); ?></p> </div> admin/view/notice/success-single-use.php 0000644 00000000367 14720702445 0014334 0 ustar 00 <?php /** * Success notice * * @package Envato_Market * @since 1.0.0 */ ?> <div class="notice notice-success is-dismissible"> <p><?php esc_html_e( 'All Single Use OAuth Personal Tokens have been verified.', 'envato-market' ); ?></p> </div> admin/view/notice/error-permissions.php 0000644 00000001054 14720702445 0014307 0 ustar 00 <?php /** * Error notice * * @package Envato_Market * @since 2.0.1 */ ?> <div class="notice notice-error is-dismissible"> <p><?php printf( esc_html__( 'Incorrect token permissions, please generate another token or fix the permissions on the existing token.' ) ); ?></p> <p><?php printf( esc_html__( 'Please ensure only the following permissions are enabled: ', 'envato-market' ) ); ?></p> <ol> <?php foreach ( $this->get_required_permissions() as $permission ) { ?> <li><?php echo esc_html( $permission ); ?></li> <?php } ?> </ol> </div> admin/view/notice/error-details.php 0000644 00000000466 14720702445 0013367 0 ustar 00 <?php /** * Error details * * @package Envato_Market * @since 2.0.2 */ ?> <div class="notice notice-error is-dismissible"> <p><?php printf( '<strong>Additional Error Details:</strong><br/>%s.<br/> %s <br/> %s', esc_html( $title ), esc_html( $message ), esc_html( json_encode( $data ) ) ); ?></p> </div> admin/view/partials/plugins.php 0000644 00000000726 14720702445 0012631 0 ustar 00 <?php /** * Plugins panel partial * * @package Envato_Market * @since 1.0.0 */ $plugins = envato_market()->items()->plugins( 'purchased' ); ?> <div id="plugins" class="panel <?php echo empty( $plugins ) ? 'hidden' : ''; ?>"> <div class="envato-market-blocks"> <?php if ( ! empty( $plugins ) ) { envato_market_plugins_column( 'active' ); envato_market_plugins_column( 'installed' ); envato_market_plugins_column( 'install' ); } ?> </div> </div> admin/view/partials/intro.php 0000644 00000001671 14720702445 0012303 0 ustar 00 <?php /** * Intro partial * * @package Envato_Market * @since 1.0.0 */ ?> <div class="col"> <h1 class="about-title"><img class="about-logo" src="<?php echo envato_market()->get_plugin_url(); ?>images/envato-market-logo.svg" alt="Envato Market"><sup><?php echo esc_html( envato_market()->get_version() ); ?></sup></h1> <p><?php esc_html_e( 'Welcome!', 'envato-market' ); ?></p> <p><?php esc_html_e( 'This plugin can install WordPress themes and plugins purchased from ThemeForest & CodeCanyon by connecting with the Envato Market API using a secure OAuth personal token. Once your themes & plugins are installed WordPress will periodically check for updates, so keeping your items up to date is as simple as a few clicks.', 'envato-market' ); ?></p> <p><strong><?php printf( esc_html__( 'Find out more at %1$senvato.com%2$s.', 'envato-market' ), '<a href="https://envato.com/market-plugin/" target="_blank">', '</a>' ); ?></strong></p> </div> admin/view/partials/help.php 0000644 00000005201 14720702445 0012071 0 ustar 00 <?php /** * Help panel partial * * @package Envato_Market * @since 2.0.1 */ ?> <div id="help" class="panel"> <div class="envato-market-blocks"> <div class="envato-market-block"> <h3>Troubleshooting:</h3> <p>If you’re having trouble with the plugin, please</p> <ol> <li>Confirm the old <code>Envato Toolkit</code> plugin is not installed.</li> <li>Confirm the latest version of WordPress is installed.</li> <li>Confirm the latest version of the <a href="https://envato.com/market-plugin/" target="_blank">Envato Market</a> plugin is installed.</li> <li>Try creating a new API token has from the <a href="<?php echo envato_market()->admin()->get_generate_token_url(); ?>" target="_blank">build.envato.com</a> website - ensure only the following permissions have been granted <ul> <li>View and search Envato sites</li> <li>Download your purchased items</li> <li>List purchases you've made</li> </ul> </li> <li>Check with the hosting provider to ensure the API connection to <code>api.envato.com</code> is not blocked.</li> <li>Check with the hosting provider that the minimum TLS version is 1.2 or above on the server.</li> <li>If you can’t see your items - check with the item author to confirm the Theme or Plugin is compatible with the Envato Market plugin.</li> <li>Confirm your Envato account is still active and the items are still visible from <a href="https://themeforest.net/downloads" target="_blank">your downloads page</a>.</li> <li>Note - if an item has been recently updated, it may take up to 24 hours for the latest version to appear in the Envato Market plugin.</li> </ol> </div> <div class="envato-market-block"> <h3>Health Check:</h3> <div class="envato-market-healthcheck"> Problem starting healthcheck. Please check javascript console for errors. </div> <h3>Support:</h3> <p>The Envato Market plugin is maintained - we ensure it works best on the latest version of WordPress and on a modern hosting platform, however we can’t guarantee it’ll work on all WordPress sites or hosting environments.</p> <p>If you’ve tried all the troubleshooting steps and you’re still unable to get the Envato Market plugin to work on your site/hosting, at this time, our advice is to remove the Envato Market plugin and instead visit the Downloads section of ThemeForest/CodeCanyon to download the latest version of your items.</p> <p>If you’re having trouble with a specific item from ThemeForest or CodeCanyon, it’s best you browse to the Theme or Plugin item page, visit the ‘support’ tab and follow the next steps. </p> </div> </div> </div> admin/view/partials/tabs.php 0000644 00000002724 14720702445 0012101 0 ustar 00 <?php /** * Tabs partial * * @package Envato_Market * @since 1.0.0 */ $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; $themes = envato_market()->items()->themes( 'purchased' ); $plugins = envato_market()->items()->plugins( 'purchased' ); ?> <h2 class="nav-tab-wrapper"> <?php // Themes tab. $theme_class = array(); if ( ! empty( $themes ) ) { if ( empty( $tab ) ) { $tab = 'themes'; } if ( 'themes' === $tab ) { $theme_class[] = 'nav-tab-active'; } } else { $theme_class[] = 'hidden'; } echo '<a href="#themes" data-id="theme" class="nav-tab ' . esc_attr( implode( ' ', $theme_class ) ) . '">' . esc_html__( 'Themes', 'envato-market' ) . '</a>'; // Plugins tab. $plugin_class = array(); if ( ! empty( $plugins ) ) { if ( empty( $tab ) ) { $tab = 'plugins'; } if ( 'plugins' === $tab ) { $plugin_class[] = 'nav-tab-active'; } } else { $plugin_class[] = 'hidden'; } echo '<a href="#plugins" data-id="plugin" class="nav-tab ' . esc_attr( implode( ' ', $plugin_class ) ) . '">' . esc_html__( 'Plugins', 'envato-market' ) . '</a>'; // Settings tab. echo '<a href="#settings" class="nav-tab ' . esc_attr( 'settings' === $tab || empty( $tab ) ? 'nav-tab-active' : '' ) . '">' . esc_html__( 'Settings', 'envato-market' ) . '</a>'; // Help tab. echo '<a href="#help" class="nav-tab ' . esc_attr( 'help' === $tab ? 'nav-tab-active' : '' ) . '">' . esc_html__( 'Help', 'envato-market' ) . '</a>'; ?> </h2> admin/view/partials/settings.php 0000644 00000001760 14720702445 0013007 0 ustar 00 <?php /** * Settings panel partial * * @package Envato_Market * @since 1.0.0 */ $token = envato_market()->get_option( 'token' ); $items = envato_market()->get_option( 'items', array() ); ?> <div id="settings" class="panel"> <div class="envato-market-blocks"> <?php settings_fields( envato_market()->get_slug() ); ?> <?php Envato_Market_Admin::do_settings_sections( envato_market()->get_slug(), 2 ); ?> </div> <p class="submit"> <input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Save Changes', 'envato-market' ); ?>" /> <?php if ( ( '' !== $token || ! empty( $items ) ) && 10 !== has_action( 'admin_notices', array( $this, 'error_notice' ) ) ) { ?> <a href="<?php echo esc_url( add_query_arg( array( 'authorization' => 'check' ), envato_market()->get_page_url() ) ); ?>" class="button button-secondary auth-check-button" style="margin:0 5px"><?php esc_html_e( 'Test API Connection', 'envato-market' ); ?></a> <?php } ?> </p> </div> admin/view/partials/themes.php 0000644 00000000715 14720702445 0012433 0 ustar 00 <?php /** * Themes panel partial * * @package Envato_Market * @since 1.0.0 */ $themes = envato_market()->items()->themes( 'purchased' ); ?> <div id="themes" class="panel <?php echo empty( $themes ) ? 'hidden' : ''; ?>"> <div class="envato-market-blocks"> <?php if ( ! empty( $themes ) ) { envato_market_themes_column( 'active' ); envato_market_themes_column( 'installed' ); envato_market_themes_column( 'install' ); } ?> </div> </div> admin/class-envato-market-theme-installer-skin.php 0000644 00000011106 14720702445 0016266 0 ustar 00 <?php /** * Upgrader skin classes. * * @package Envato_Market */ // Include the WP_Upgrader_Skin class. if ( ! class_exists( 'WP_Upgrader_Skin', false ) ) : include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skins.php'; endif; if ( ! class_exists( 'Envato_Market_Theme_Installer_Skin' ) ) : /** * Theme Installer Skin. * * @class Envato_Market_Theme_Installer_Skin * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Theme_Installer_Skin extends Theme_Installer_Skin { /** * Modify the install actions. * * @since 1.0.0 */ public function after() { if ( empty( $this->upgrader->result['destination_name'] ) ) { return; } $theme_info = $this->upgrader->theme_info(); if ( empty( $theme_info ) ) { return; } $name = $theme_info->display( 'Name' ); $stylesheet = $this->upgrader->result['destination_name']; $template = $theme_info->get_template(); $activate_link = add_query_arg( array( 'action' => 'activate', 'template' => urlencode( $template ), 'stylesheet' => urlencode( $stylesheet ), ), admin_url( 'themes.php' ) ); $activate_link = wp_nonce_url( $activate_link, 'switch-theme_' . $stylesheet ); $install_actions = array(); if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { $install_actions['preview'] = '<a href="' . wp_customize_url( $stylesheet ) . '" class="hide-if-no-customize load-customize"><span aria-hidden="true">' . __( 'Live Preview', 'envato-market' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Live Preview “%s”', 'envato-market' ), $name ) . '</span></a>'; } if ( is_multisite() ) { if ( current_user_can( 'manage_network_themes' ) ) { $install_actions['network_enable'] = '<a href="' . esc_url( network_admin_url( wp_nonce_url( 'themes.php?action=enable&theme=' . urlencode( $stylesheet ) . '&paged=1&s', 'enable-theme_' . $stylesheet ) ) ) . '" target="_parent">' . __( 'Network Enable', 'envato-market' ) . '</a>'; } } $install_actions['activate'] = '<a href="' . esc_url( $activate_link ) . '" class="activatelink"><span aria-hidden="true">' . __( 'Activate', 'envato-market' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Activate “%s”', 'envato-market' ), $name ) . '</span></a>'; $install_actions['themes_page'] = '<a href="' . esc_url( admin_url( 'admin.php?page=' . envato_market()->get_slug() . '&tab=themes' ) ) . '" target="_parent">' . __( 'Return to Theme Installer', 'envato-market' ) . '</a>'; if ( ! $this->result || is_wp_error( $this->result ) || is_multisite() || ! current_user_can( 'switch_themes' ) ) { unset( $install_actions['activate'], $install_actions['preview'] ); } if ( ! empty( $install_actions ) ) { $this->feedback( implode( ' | ', $install_actions ) ); } } } endif; if ( ! class_exists( 'Envato_Market_Plugin_Installer_Skin' ) ) : /** * Plugin Installer Skin. * * @class Envato_Market_Plugin_Installer_Skin * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Plugin_Installer_Skin extends Plugin_Installer_Skin { /** * Modify the install actions. * * @since 1.0.0 */ public function after() { $plugin_file = $this->upgrader->plugin_info(); $install_actions = array(); if ( current_user_can( 'activate_plugins' ) ) { $install_actions['activate_plugin'] = '<a href="' . esc_url( wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ) ) . '" target="_parent">' . __( 'Activate Plugin', 'envato-market' ) . '</a>'; } if ( is_multisite() ) { unset( $install_actions['activate_plugin'] ); if ( current_user_can( 'manage_network_plugins' ) ) { $install_actions['network_activate'] = '<a href="' . esc_url( network_admin_url( wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ) ) ) . '" target="_parent">' . __( 'Network Activate', 'envato-market' ) . '</a>'; } } $install_actions['plugins_page'] = '<a href="' . esc_url( admin_url( 'admin.php?page=' . envato_market()->get_slug() . '&tab=plugins' ) ) . '" target="_parent">' . __( 'Return to Plugin Installer', 'envato-market' ) . '</a>'; if ( ! $this->result || is_wp_error( $this->result ) ) { unset( $install_actions['activate_plugin'], $install_actions['site_activate'], $install_actions['network_activate'] ); } if ( ! empty( $install_actions ) ) { $this->feedback( implode( ' | ', $install_actions ) ); } } } endif; admin/class-envato-market-theme-upgrader.php 0000644 00000003723 14720702445 0015146 0 ustar 00 <?php /** * Theme Upgrader class. * * @package Envato_Market */ // Include the WP_Upgrader class. if ( ! class_exists( 'WP_Upgrader', false ) ) : include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; endif; if ( ! class_exists( 'Envato_Market_Theme_Upgrader' ) ) : /** * Extends the WordPress Theme_Upgrader class. * * This class makes modifications to the strings during install & upgrade. * * @class Envato_Market_Plugin_Upgrader * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Theme_Upgrader extends Theme_Upgrader { /** * Initialize the upgrade strings. * * @since 1.0.0 */ public function upgrade_strings() { parent::upgrade_strings(); $this->strings['downloading_package'] = __( 'Downloading the Envato Market upgrade package…', 'envato-market' ); } /** * Initialize the install strings. * * @since 1.0.0 */ public function install_strings() { parent::install_strings(); $this->strings['downloading_package'] = __( 'Downloading the Envato Market install package…', 'envato-market' ); } } endif; if ( ! class_exists( 'Envato_Market_Plugin_Upgrader' ) ) : /** * Extends the WordPress Plugin_Upgrader class. * * This class makes modifications to the strings during install & upgrade. * * @class Envato_Market_Plugin_Upgrader * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Plugin_Upgrader extends Plugin_Upgrader { /** * Initialize the upgrade strings. * * @since 1.0.0 */ public function upgrade_strings() { parent::upgrade_strings(); $this->strings['downloading_package'] = __( 'Downloading the Envato Market upgrade package…', 'envato-market' ); } /** * Initialize the install strings. * * @since 1.0.0 */ public function install_strings() { parent::install_strings(); $this->strings['downloading_package'] = __( 'Downloading the Envato Market install package…', 'envato-market' ); } } endif; admin/class-envato-market-admin.php 0000644 00000162261 14720702445 0013330 0 ustar 00 <?php /** * Admin UI class. * * @package Envato_Market */ if ( ! class_exists( 'Envato_Market_Admin' ) && class_exists( 'Envato_Market' ) ) : /** * Creates an admin page to save the Envato API OAuth token. * * @class Envato_Market_Admin * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_Admin { /** * Action nonce. * * @type string */ const AJAX_ACTION = 'envato_market'; /** * The single class instance. * * @since 1.0.0 * @access private * * @var object */ private static $_instance = null; /** * Main Envato_Market_Admin Instance * * Ensures only one instance of this class exists in memory at any one time. * * @return object The one true Envato_Market_Admin. * @codeCoverageIgnore * @uses Envato_Market_Admin::init_actions() Setup hooks and actions. * * @since 1.0.0 * @static * @see Envato_Market_Admin() */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); self::$_instance->init_actions(); } return self::$_instance; } /** * A dummy constructor to prevent this class from being loaded more than once. * * @see Envato_Market_Admin::instance() * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function __construct() { /* We do nothing here! */ } /** * You cannot clone this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __clone() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * You cannot unserialize instances of this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * Setup the hooks, actions and filters. * * @uses add_action() To add actions. * @uses add_filter() To add filters. * * @since 1.0.0 */ public function init_actions() { // @codeCoverageIgnoreStart if ( false === envato_market()->get_data( 'admin' ) && false === envato_market()->get_option( 'is_plugin_active' ) ) { // Turns the UI off if allowed. return; } // @codeCoverageIgnoreEnd // Deferred Download. add_action( 'upgrader_package_options', array( $this, 'maybe_deferred_download' ), 9 ); // Add pre download filter to help with 3rd party plugin integration. add_filter( 'upgrader_pre_download', array( $this, 'upgrader_pre_download' ), 2, 4 ); // Add item AJAX handler. add_action( 'wp_ajax_' . self::AJAX_ACTION . '_add_item', array( $this, 'ajax_add_item' ) ); // Remove item AJAX handler. add_action( 'wp_ajax_' . self::AJAX_ACTION . '_remove_item', array( $this, 'ajax_remove_item' ) ); // Health check AJAX handler add_action( 'wp_ajax_' . self::AJAX_ACTION . '_healthcheck', array( $this, 'ajax_healthcheck' ) ); // Maybe delete the site transients. add_action( 'init', array( $this, 'maybe_delete_transients' ), 11 ); // Add the menu. add_action( 'admin_menu', array( $this, 'add_menu_page' ) ); // Register the settings. add_action( 'admin_init', array( $this, 'register_settings' ) ); // We may need to redirect after an item is enabled. add_action( 'current_screen', array( $this, 'maybe_redirect' ) ); // Add authorization notices. add_action( 'current_screen', array( $this, 'add_notices' ) ); // Set the API values. add_action( 'current_screen', array( $this, 'set_items' ) ); // Hook to verify the API token before saving it. add_filter( 'pre_update_option_' . envato_market()->get_option_name(), array( $this, 'check_api_token_before_saving', ), 9, 3 ); add_filter( 'pre_update_site_option_' . envato_market()->get_option_name(), array( $this, 'check_api_token_before_saving', ), 9, 3 ); // When network enabled, add the network options menu. add_action( 'network_admin_menu', array( $this, 'add_menu_page' ) ); // Ability to make use of the Settings API when in multisite mode. add_action( 'network_admin_edit_envato_market_network_settings', array( $this, 'save_network_settings' ) ); } /** * This runs before we save the Envato Market options array. * If the token has changed then we set a transient so we can do the update check. * * @param array $value The option to save. * @param array $old_value The old option value. * @param array $option Serialized option value. * * @return array $value The updated option value. * @since 2.0.1 */ public function check_api_token_before_saving( $value, $old_value, $option ) { if ( ! empty( $value['token'] ) && ( empty( $old_value['token'] ) || $old_value['token'] != $value['token'] || isset( $_POST['envato_market'] ) ) ) { set_site_transient( envato_market()->get_option_name() . '_check_token', $value['token'], HOUR_IN_SECONDS ); } return $value; } /** * Defers building the API download url until the last responsible moment to limit file requests. * * Filter the package options before running an update. * * @param array $options { * Options used by the upgrader. * * @type string $package Package for update. * @type string $destination Update location. * @type bool $clear_destination Clear the destination resource. * @type bool $clear_working Clear the working resource. * @type bool $abort_if_destination_exists Abort if the Destination directory exists. * @type bool $is_multi Whether the upgrader is running multiple times. * @type array $hook_extra Extra hook arguments. * } * @since 1.0.0 */ public function maybe_deferred_download( $options ) { $package = $options['package']; if ( false !== strrpos( $package, 'deferred_download' ) && false !== strrpos( $package, 'item_id' ) ) { parse_str( parse_url( $package, PHP_URL_QUERY ), $vars ); if ( $vars['item_id'] ) { $args = $this->set_bearer_args( $vars['item_id'] ); $options['package'] = envato_market()->api()->download( $vars['item_id'], $args ); } } return $options; } /** * We want to stop certain popular 3rd party scripts from blocking the update process by * adjusting the plugin name slightly so the 3rd party plugin checks stop. * * Currently works for: Visual Composer. * * @param string $reply Package URL. * @param string $package Package URL. * @param object $updater Updater Object. * * @return string $reply New Package URL. * @since 2.0.0 */ public function upgrader_pre_download( $reply, $package, $updater ) { if ( strpos( $package, 'marketplace.envato.com/short-dl' ) !== false ) { if ( isset( $updater->skin->plugin_info ) && ! empty( $updater->skin->plugin_info['Name'] ) ) { $updater->skin->plugin_info['Name'] = $updater->skin->plugin_info['Name'] . '.'; } else { $updater->skin->plugin_info = array( 'Name' => 'Name', ); } } return $reply; } /** * Returns the bearer arguments for a request with a single use API Token. * * @param int $id The item ID. * * @return array * @since 1.0.0 */ public function set_bearer_args( $id ) { $token = ''; $args = array(); foreach ( envato_market()->get_option( 'items', array() ) as $item ) { if ( absint( $item['id'] ) === absint( $id ) ) { $token = $item['token']; break; } } if ( ! empty( $token ) ) { $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $token, ), ); } return $args; } /** * Maybe delete the site transients. * * @since 1.0.0 * @codeCoverageIgnore */ public function maybe_delete_transients() { if ( isset( $_POST[ envato_market()->get_option_name() ] ) ) { // Nonce check. if ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( $_POST['_wpnonce'], envato_market()->get_slug() . '-options' ) ) { wp_die( __( 'You do not have sufficient permissions to delete transients.', 'envato-market' ) ); } self::delete_transients(); } elseif ( ! envato_market()->get_option( 'installed_version', 0 ) || version_compare( envato_market()->get_version(), envato_market()->get_option( 'installed_version', 0 ), '<' ) ) { // When the plugin updates we want to delete transients. envato_market()->set_option( 'installed_version', envato_market()->get_version() ); self::delete_transients(); } } /** * Delete the site transients. * * @since 1.0.0 * @access private */ private function delete_transients() { delete_site_transient( envato_market()->get_option_name() . '_themes' ); delete_site_transient( envato_market()->get_option_name() . '_plugins' ); } /** * Prints out all settings sections added to a particular settings page in columns. * * @param string $page The slug name of the page whos settings sections you want to output. * @param int $columns The number of columns in each row. * * @since 1.0.0 * * @global array $wp_settings_sections Storage array of all settings sections added to admin pages * @global array $wp_settings_fields Storage array of settings fields and info about their pages/sections */ public static function do_settings_sections( $page, $columns = 2 ) { global $wp_settings_sections, $wp_settings_fields; // @codeCoverageIgnoreStart if ( ! isset( $wp_settings_sections[ $page ] ) ) { return; } // @codeCoverageIgnoreEnd foreach ( (array) $wp_settings_sections[ $page ] as $section ) { // @codeCoverageIgnoreStart if ( ! isset( $wp_settings_fields ) || ! isset( $wp_settings_fields[ $page ] ) || ! isset( $wp_settings_fields[ $page ][ $section['id'] ] ) ) { continue; } // @codeCoverageIgnoreEnd // Set the column class. $class = 'envato-market-block'; ?> <div class="<?php echo esc_attr( $class ); ?>"> <?php if ( ! empty( $section['title'] ) ) { echo '<h3>' . esc_html( $section['title'] ) . '</h3>' . "\n"; } if ( ! empty( $section['callback'] ) ) { call_user_func( $section['callback'], $section ); } ?> <table class="form-table"> <?php do_settings_fields( $page, $section['id'] ); ?> </table> </div> <?php } } /** * Adds the menu. * * @since 1.0.0 */ public function add_menu_page() { if ( ENVATO_MARKET_NETWORK_ACTIVATED && ! is_super_admin() ) { // we do not want to show a menu item for people who do not have permission. return; } $svg_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><path fill="black" d="M39.137058 70.157119c1.685122 0 3.051217-1.365967 3.051217-3.051217 0-1.685122-1.366095-3.051217-3.051217-3.051217-1.685121 0-3.051217 1.366095-3.051217 3.051217 0 1.68525 1.366096 3.051217 3.051217 3.051217zm17.560977-23.85614-17.212984 1.84103c-.321858.03862-.47635-.373356-.231738-.566471l16.852503-13.118945c1.094318-.901204 1.789532-2.291632 1.493422-3.785054-.296109-2.291632-2.188636-3.785054-4.570388-3.47607L34.721548 29.87333c-.321858.0515-.502099-.360481-.231738-.566471l18.139936-13.852782c3.579064-2.780856 3.875174-8.2524479.592219-11.4324082-2.986845-2.9868582-7.763223-2.8838635-10.737194.1029947L13.24716 33.864373c-1.094318 1.197313-1.596417 2.780856-1.287433 4.480268.502099 2.690736 3.17996 4.480268 5.870696 3.978169l15.758184-3.218583c.347607-.06437.527847.38623.231738.579345L16.337 50.871367c-2.188636 1.390428-3.17996 3.875175-2.484746 6.359921.695214 3.282955 3.978169 5.175482 7.158129 4.377273l26.134897-6.437166c.296109-.07725.514973.270361.321858.502099l-4.081164 5.033864c-1.094318 1.390428.695214 3.282955 2.188637 2.188637l13.42793-11.033304c2.381751-1.982647.798208-5.870696-2.291632-5.574586z"/></svg>'; $page = add_menu_page( __( 'Envato Market', 'envato-market' ), __( 'Envato Market', 'envato-market' ), 'manage_options', envato_market()->get_slug(), array( $this, 'render_admin_callback', ), 'data:image/svg+xml;base64,' . base64_encode($svg_icon) ); // Enqueue admin CSS. add_action( 'admin_print_styles-' . $page, array( $this, 'admin_enqueue_style' ) ); // Enqueue admin JavaScript. add_action( 'admin_print_scripts-' . $page, array( $this, 'admin_enqueue_script' ) ); // Add Underscore.js templates. add_action( 'admin_footer-' . $page, array( $this, 'render_templates' ) ); } /** * Enqueue admin css. * * @since 1.0.0 */ public function admin_enqueue_style() { $file_url = envato_market()->get_plugin_url() . 'css/envato-market' . ( is_rtl() ? '-rtl' : '' ) . '.css'; wp_enqueue_style( envato_market()->get_slug(), $file_url, array( 'wp-jquery-ui-dialog' ), envato_market()->get_version() ); } /** * Enqueue admin script. * * @since 1.0.0 */ public function admin_enqueue_script() { $min = ( WP_DEBUG ? '' : '.min' ); $slug = envato_market()->get_slug(); $version = envato_market()->get_version(); $plugin_url = envato_market()->get_plugin_url(); wp_enqueue_script( $slug, $plugin_url . 'js/envato-market' . $min . '.js', array( 'jquery', 'jquery-ui-dialog', 'wp-util', ), $version, true ); wp_enqueue_script( $slug . '-updates', $plugin_url . 'js/updates' . $min . '.js', array( 'jquery', 'updates', 'wp-a11y', 'wp-util', ), $version, true ); // Script data array. $exports = array( 'nonce' => wp_create_nonce( self::AJAX_ACTION ), 'action' => self::AJAX_ACTION, 'i18n' => array( 'save' => __( 'Save', 'envato-market' ), 'remove' => __( 'Remove', 'envato-market' ), 'cancel' => __( 'Cancel', 'envato-market' ), 'error' => __( 'An unknown error occurred. Try again.', 'envato-market' ), ), ); // Export data to JS. wp_scripts()->add_data( $slug, 'data', sprintf( 'var _envatoMarket = %s;', wp_json_encode( $exports ) ) ); } /** * Underscore (JS) templates for dialog windows. * * @codeCoverageIgnore */ public function render_templates() { ?> <script type="text/html" id="tmpl-envato-market-auth-check-button"> <a href="<?php echo esc_url( add_query_arg( array( 'authorization' => 'check' ), envato_market()->get_page_url() ) ); ?>" class="button button-secondary auth-check-button" style="margin:0 5px"><?php esc_html_e( 'Test API Connection', 'envato-market' ); ?></a> </script> <script type="text/html" id="tmpl-envato-market-item"> <li data-id="{{ data.id }}"> <span class="item-name"><?php esc_html_e( 'ID', 'envato-market' ); ?> : {{ data.id }} - {{ data.name }}</span> <button class="item-delete dashicons dashicons-dismiss"> <span class="screen-reader-text"><?php esc_html_e( 'Delete', 'envato-market' ); ?></span> </button> <input type="hidden" name="<?php echo esc_attr( envato_market()->get_option_name() ); ?>[items][{{ data.key }}][name]" value="{{ data.name }}"/> <input type="hidden" name="<?php echo esc_attr( envato_market()->get_option_name() ); ?>[items][{{ data.key }}][token]" value="{{ data.token }}"/> <input type="hidden" name="<?php echo esc_attr( envato_market()->get_option_name() ); ?>[items][{{ data.key }}][id]" value="{{ data.id }}"/> <input type="hidden" name="<?php echo esc_attr( envato_market()->get_option_name() ); ?>[items][{{ data.key }}][type]" value="{{ data.type }}"/> <input type="hidden" name="<?php echo esc_attr( envato_market()->get_option_name() ); ?>[items][{{ data.key }}][authorized]" value="{{ data.authorized }}"/> </li> </script> <script type="text/html" id="tmpl-envato-market-dialog-remove"> <div id="envato-market-dialog-remove" title="<?php esc_html_e( 'Remove Item', 'envato-market' ); ?>"> <p><?php esc_html_e( 'You are about to remove the connection between the Envato Market API and this item. You cannot undo this action.', 'envato-market' ); ?></p> </div> </script> <script type="text/html" id="tmpl-envato-market-dialog-form"> <div id="envato-market-dialog-form" title="<?php esc_html_e( 'Add Item', 'envato-market' ); ?>"> <form> <fieldset> <label for="token"><?php esc_html_e( 'Token', 'envato-market' ); ?></label> <input type="text" name="token" class="widefat" value=""/> <p class="description"><?php esc_html_e( 'Enter the Envato API Personal Token.', 'envato-market' ); ?></p> <label for="id"><?php esc_html_e( 'Item ID', 'envato-market' ); ?></label> <input type="text" name="id" class="widefat" value=""/> <p class="description"><?php esc_html_e( 'Enter the Envato Item ID.', 'envato-market' ); ?></p> <input type="submit" tabindex="-1" style="position:absolute; top:-5000px"/> </fieldset> </form> </div> </script> <script type="text/html" id="tmpl-envato-market-dialog-error"> <div class="notice notice-error"> <p>{{ data.message }}</p> </div> </script> <script type="text/html" id="tmpl-envato-market-card"> <div class="envato-market-block" data-id="{{ data.id }}"> <div class="envato-card {{ data.type }}"> <div class="envato-card-top"> <a href="{{ data.url }}" class="column-icon"> <img src="{{ data.thumbnail_url }}"/> </a> <div class="column-name"> <h4> <a href="{{ data.url }}">{{ data.name }}</a> <span class="version" aria-label="<?php esc_attr_e( 'Version %s', 'envato-market' ); ?>"><?php esc_html_e( 'Version', 'envato-market' ); ?> {{ data.version }}</span> </h4> </div> <div class="column-description"> <div class="description"> <p>{{ data.description }}</p> </div> <p class="author"> <cite><?php esc_html_e( 'By', 'envato-market' ); ?> {{ data.author }}</cite> </p> </div> </div> <div class="envato-card-bottom"> <div class="column-actions"> <a href="{{{ data.install }}}" class="button button-primary"> <span aria-hidden="true"><?php esc_html_e( 'Install', 'envato-market' ); ?></span> <span class="screen-reader-text"><?php esc_html_e( 'Install', 'envato-market' ); ?> {{ data.name }}</span> </a> </div> </div> </div> </div> </script> <?php } /** * Registers the settings. * * @since 1.0.0 */ public function register_settings() { // Setting. register_setting( envato_market()->get_slug(), envato_market()->get_option_name() ); // OAuth section. add_settings_section( envato_market()->get_option_name() . '_oauth_section', __( 'Getting Started (Simple)', 'envato-market' ), array( $this, 'render_oauth_section_callback' ), envato_market()->get_slug() ); // Token setting. add_settings_field( 'token', __( 'Token', 'envato-market' ), array( $this, 'render_token_setting_callback' ), envato_market()->get_slug(), envato_market()->get_option_name() . '_oauth_section' ); // Items section. add_settings_section( envato_market()->get_option_name() . '_items_section', __( 'Single Item Tokens (Advanced)', 'envato-market' ), array( $this, 'render_items_section_callback' ), envato_market()->get_slug() ); // Items setting. add_settings_field( 'items', __( 'Envato Market Items', 'envato-market' ), array( $this, 'render_items_setting_callback' ), envato_market()->get_slug(), envato_market()->get_option_name() . '_items_section' ); } /** * Redirect after the enable action runs. * * @since 1.0.0 * @codeCoverageIgnore */ public function maybe_redirect() { if ( $this->are_we_on_settings_page() ) { if ( ! empty( $_GET['action'] ) && 'install-theme' === $_GET['action'] && ! empty( $_GET['enabled'] ) ) { wp_safe_redirect( esc_url( envato_market()->get_page_url() ) ); exit; } } } /** * Add authorization notices. * * @since 1.0.0 */ public function add_notices() { if ( $this->are_we_on_settings_page() ) { // @codeCoverageIgnoreStart if ( get_site_transient( envato_market()->get_option_name() . '_check_token' ) || ( isset( $_GET['authorization'] ) && 'check' === $_GET['authorization'] ) ) { delete_site_transient( envato_market()->get_option_name() . '_check_token' ); self::authorization_redirect(); } // @codeCoverageIgnoreEnd // Get the option array. $option = envato_market()->get_options(); // Display success/error notices. if ( ! empty( $option['notices'] ) ) { self::delete_transients(); // Show succes notice. if ( isset( $option['notices']['success'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_success_notice', ) ); } // Show succes no-items notice. if ( isset( $option['notices']['success-no-items'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_success_no_items_notice', ) ); } // Show single-use succes notice. if ( isset( $option['notices']['success-single-use'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_success_single_use_notice', ) ); } // Show error notice. if ( isset( $option['notices']['error'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_error_notice', ) ); } // Show invalid permissions error notice. if ( isset( $option['notices']['error-permissions'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_error_permissions', ) ); } // Show single-use error notice. if ( isset( $option['notices']['error-single-use'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_error_single_use_notice', ) ); } // Show missing zip notice. if ( isset( $option['notices']['missing-package-zip'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_error_missing_zip', ) ); } // Show missing http connection error. if ( isset( $option['notices']['http_error'] ) ) { add_action( ( ENVATO_MARKET_NETWORK_ACTIVATED ? 'network_' : '' ) . 'admin_notices', array( $this, 'render_error_http', ) ); } // Update the saved data so the notice disappears on the next page load. unset( $option['notices'] ); envato_market()->set_options( $option ); } } } /** * Set the API values. * * @since 1.0.0 */ public function set_items() { if ( $this->are_we_on_settings_page() ) { envato_market()->items()->set_themes(); envato_market()->items()->set_plugins(); } } /** * Check if we're on the settings page. * * @since 2.0.0 * @access private */ private function are_we_on_settings_page() { return 'toplevel_page_' . envato_market()->get_slug() === get_current_screen()->id || 'toplevel_page_' . envato_market()->get_slug() . '-network' === get_current_screen()->id; } /** * Check for authorization and redirect. * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function authorization_redirect() { self::authorization(); wp_safe_redirect( esc_url( envato_market()->get_page_url() . '#settings' ) ); exit; } /** * Set the Envato API authorization value. * * @since 1.0.0 */ public function authorization() { // Get the option array. $option = envato_market()->get_options(); $option['notices'] = array(); // Check for global token. if ( envato_market()->get_option( 'token' ) || envato_market()->api()->token ) { $notice = 'success'; $scope_check = $this->authorize_token_permissions(); if ( 'http_error' === $scope_check ) { $notice = 'http_error'; } elseif ( 'error' === $this->authorize_total_items() || 'error' === $scope_check ) { $notice = 'error'; } else { if ( 'missing-permissions' == $scope_check ) { $notice = 'error-permissions'; } elseif ( 'too-many-permissions' === $scope_check ) { $notice = 'error-permissions'; } else { $themes_notice = $this->authorize_themes(); $plugins_notice = $this->authorize_plugins(); if ( 'error' === $themes_notice || 'error' === $plugins_notice ) { $notice = 'error'; } elseif ( 'success-no-themes' === $themes_notice && 'success-no-plugins' === $plugins_notice ) { $notice = 'success-no-items'; } } } $option['notices'][ $notice ] = true; } // Check for single-use token. if ( ! empty( $option['items'] ) ) { $failed = false; foreach ( $option['items'] as $key => $item ) { if ( empty( $item['name'] ) || empty( $item['token'] ) || empty( $item['id'] ) || empty( $item['type'] ) || empty( $item['authorized'] ) ) { continue; } $request_args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $item['token'], ), ); // Uncached API response with single-use token. $response = envato_market()->api()->item( $item['id'], $request_args ); if ( ! is_wp_error( $response ) && isset( $response['id'] ) ) { $option['items'][ $key ]['authorized'] = 'success'; } else { if ( is_wp_error( $response ) ) { $this->store_additional_error_debug_information( 'Unable to query single item ID ' . $item['id'], $response->get_error_message(), $response->get_error_data() ); } $failed = true; $option['items'][ $key ]['authorized'] = 'failed'; } } if ( true === $failed ) { $option['notices']['error-single-use'] = true; } else { $option['notices']['success-single-use'] = true; } } // Set the option array. if ( ! empty( $option['notices'] ) ) { envato_market()->set_options( $option ); } } /** * Check that themes are authorized. * * @return bool * @since 1.0.0 */ public function authorize_total_items() { $domain = envato_market()->get_envato_api_domain(); $path = envato_market()->api()->api_path_for('total-items'); $url = $domain . $path; $response = envato_market()->api()->request( $url ); $notice = 'success'; if ( is_wp_error( $response ) ) { $notice = 'error'; $this->store_additional_error_debug_information( 'Failed to query total number of items in API response', $response->get_error_message(), $response->get_error_data() ); } elseif ( ! isset( $response['total-items'] ) ) { $notice = 'error'; $this->store_additional_error_debug_information( 'Incorrect response from API when querying total items' ); } return $notice; } /** * Get the required API permissions for this plugin to work. * * @single 2.0.1 * * @return array */ public function get_required_permissions() { return apply_filters( 'envato_market_required_permissions', array( 'default' => 'View and search Envato sites', 'purchase:download' => 'Download your purchased items', 'purchase:list' => 'List purchases you\'ve made', ) ); } /** * Return the URL a user needs to click to generate a personal token. * * @single 2.0.1 * * @return string The full URL to request a token. */ public function get_generate_token_url() { return 'https://build.envato.com/create-token/?' . implode( '&', array_map( function ( $val ) { return $val . '=t'; }, array_keys( $this->get_required_permissions() ) ) ); } /** * Check that themes are authorized. * * @return bool * @since 1.0.0 */ public function authorize_token_permissions() { if ( defined('ENVATO_LOCAL_DEVELOPMENT') ) { return 'success'; } $notice = 'success'; $response = envato_market()->api()->request( 'https://api.envato.com/whoami' ); if ( is_wp_error( $response ) && ( $response->get_error_code() === 'http_error' || $response->get_error_code() == 500 ) ) { $this->store_additional_error_debug_information( 'An error occured checking token permissions', $response->get_error_message(), $response->get_error_data() ); $notice = 'http_error'; } elseif ( is_wp_error( $response ) || ! isset( $response['scopes'] ) || ! is_array( $response['scopes'] ) ) { $this->store_additional_error_debug_information( 'No scopes found in API response message', $response->get_error_message(), $response->get_error_data() ); $notice = 'error'; } else { $minimum_scopes = $this->get_required_permissions(); $maximum_scopes = array( 'default' => 'Default' ) + $minimum_scopes; foreach ( $minimum_scopes as $required_scope => $required_scope_name ) { if ( ! in_array( $required_scope, $response['scopes'] ) ) { // The scope minimum required scope doesn't exist. $this->store_additional_error_debug_information( 'Could not find required API permission scope in output.', $required_scope ); $notice = 'missing-permissions'; } } foreach ( $response['scopes'] as $scope ) { if ( ! isset( $maximum_scopes[ $scope ] ) ) { // The available scope is outside our maximum bounds. $this->store_additional_error_debug_information( 'Found too many permissions on token.', $scope ); $notice = 'too-many-permissions'; } } } return $notice; } /** * Check that themes or plugins are authorized and downloadable. * * @param string $type The filter type, either 'themes' or 'plugins'. Default 'themes'. * * @return bool|null * @since 1.0.0 */ public function authorize_items( $type = 'themes' ) { $domain = envato_market()->get_envato_api_domain(); $path = envato_market()->api()->api_path_for('list-purchases'); $api_url = $domain . $path . '?filter_by=wordpress-' . $type; $response = envato_market()->api()->request( $api_url ); $notice = 'success'; if ( is_wp_error( $response ) ) { $notice = 'error'; $this->store_additional_error_debug_information( 'Error listing buyer purchases.', $response->get_error_message(), $response->get_error_data() ); } elseif ( empty( $response ) ) { $notice = 'error'; $this->store_additional_error_debug_information( 'Empty API result listing buyer purchases' ); } elseif ( empty( $response['results'] ) ) { $notice = 'success-no-' . $type; } else { shuffle( $response['results'] ); $item = array_shift( $response['results'] ); if ( ! isset( $item['item']['id'] ) || ! envato_market()->api()->download( $item['item']['id'] ) ) { $this->store_additional_error_debug_information( 'Failed to find the correct item format in API response' ); $notice = 'error'; } } return $notice; } /** * Check that themes are authorized. * * @return bool * @since 1.0.0 */ public function authorize_themes() { return $this->authorize_items( 'themes' ); } /** * Check that plugins are authorized. * * @return bool * @since 1.0.0 */ public function authorize_plugins() { return $this->authorize_items( 'plugins' ); } /** * Install plugin. * * @param string $plugin The plugin item ID. * * @since 1.0.0 * @codeCoverageIgnore */ public function install_plugin( $plugin ) { if ( ! current_user_can( 'install_plugins' ) ) { $msg = ' <div class="wrap"> <h1>' . __( 'Installing Plugin...', 'envato-market' ) . '</h1> <p>' . __( 'You do not have sufficient permissions to install plugins on this site.', 'envato-market' ) . '</p> <a href="' . esc_url( 'admin.php?page=' . envato_market()->get_slug() . '&tab=plugins' ) . '">' . __( 'Return to Plugin Installer', 'envato-market' ) . '</a> </div>'; wp_die( $msg ); } check_admin_referer( 'install-plugin_' . $plugin ); envato_market()->items()->set_plugins( true ); $install = envato_market()->items()->plugins( 'install' ); $api = new stdClass(); foreach ( $install as $value ) { if ( absint( $value['id'] ) === absint( $plugin ) ) { $api->name = $value['name']; $api->version = $value['version']; } } $array_api = (array) $api; if ( empty( $array_api ) ) { $msg = ' <div class="wrap"> <h1>' . __( 'Installing Plugin...', 'envato-market' ) . '</h1> <p>' . __( 'An error occurred, please check that the item ID is correct.', 'envato-market' ) . '</p> <a href="' . esc_url( 'admin.php?page=' . envato_market()->get_slug() . '&tab=plugins' ) . '">' . __( 'Return to Plugin Installer', 'envato-market' ) . '</a> </div>'; wp_die( $msg ); } $title = sprintf( __( 'Installing Plugin: %s', 'envato-market' ), esc_html( $api->name . ' ' . $api->version ) ); $nonce = 'install-plugin_' . $plugin; $url = 'admin.php?page=' . envato_market()->get_slug() . '&action=install-plugin&plugin=' . urlencode( $plugin ); $type = 'web'; // Install plugin type, From Web or an Upload. $api->download_link = envato_market()->api()->download( $plugin, $this->set_bearer_args( $plugin ) ); // Must have the upgrader & skin. require envato_market()->get_plugin_path() . '/inc/admin/class-envato-market-theme-upgrader.php'; require envato_market()->get_plugin_path() . '/inc/admin/class-envato-market-theme-installer-skin.php'; $upgrader = new Envato_Market_Plugin_Upgrader( new Envato_Market_Plugin_Installer_Skin( compact( 'title', 'url', 'nonce', 'plugin', 'api' ) ) ); $upgrader->install( $api->download_link ); } /** * Install theme. * * @param string $theme The theme item ID. * * @since 1.0.0 * @codeCoverageIgnore */ public function install_theme( $theme ) { if ( ! current_user_can( 'install_themes' ) ) { $msg = ' <div class="wrap"> <h1>' . __( 'Installing Theme...', 'envato-market' ) . '</h1> <p>' . __( 'You do not have sufficient permissions to install themes on this site.', 'envato-market' ) . '</p> <a href="' . esc_url( 'admin.php?page=' . envato_market()->get_slug() . '&tab=themes' ) . '">' . __( 'Return to Theme Installer', 'envato-market' ) . '</a> </div>'; wp_die( $msg ); } check_admin_referer( 'install-theme_' . $theme ); envato_market()->items()->set_themes( true ); $install = envato_market()->items()->themes( 'install' ); $api = new stdClass(); foreach ( $install as $value ) { if ( absint( $value['id'] ) === absint( $theme ) ) { $api->name = $value['name']; $api->version = $value['version']; } } $array_api = (array) $api; if ( empty( $array_api ) ) { $msg = ' <div class="wrap"> <h1>' . __( 'Installing Theme...', 'envato-market' ) . '</h1> <p>' . __( 'An error occurred, please check that the item ID is correct.', 'envato-market' ) . '</p> <a href="' . esc_url( 'admin.php?page=' . envato_market()->get_slug() . '&tab=themes' ) . '">' . __( 'Return to Plugin Installer', 'envato-market' ) . '</a> </div>'; wp_die( $msg ); } wp_enqueue_script( 'customize-loader' ); $title = sprintf( __( 'Installing Theme: %s', 'envato-market' ), esc_html( $api->name . ' ' . $api->version ) ); $nonce = 'install-theme_' . $theme; $url = 'admin.php?page=' . envato_market()->get_slug() . '&action=install-theme&theme=' . urlencode( $theme ); $type = 'web'; // Install theme type, From Web or an Upload. $api->download_link = envato_market()->api()->download( $theme, $this->set_bearer_args( $theme ) ); // Must have the upgrader & skin. require_once envato_market()->get_plugin_path() . '/inc/admin/class-envato-market-theme-upgrader.php'; require_once envato_market()->get_plugin_path() . '/inc/admin/class-envato-market-theme-installer-skin.php'; $upgrader = new Envato_Market_Theme_Upgrader( new Envato_Market_Theme_Installer_Skin( compact( 'title', 'url', 'nonce', 'api' ) ) ); $upgrader->install( $api->download_link ); } /** * AJAX handler for adding items that use a non global token. * * @since 1.0.0 * @codeCoverageIgnore */ public function ajax_add_item() { if ( ! check_ajax_referer( self::AJAX_ACTION, 'nonce', false ) ) { status_header( 400 ); wp_send_json_error( 'bad_nonce' ); } elseif ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) { status_header( 405 ); wp_send_json_error( 'bad_method' ); } elseif ( empty( $_POST['token'] ) ) { wp_send_json_error( array( 'message' => __( 'The Token is missing.', 'envato-market' ) ) ); } elseif ( empty( $_POST['id'] ) ) { wp_send_json_error( array( 'message' => __( 'The Item ID is missing.', 'envato-market' ) ) ); } elseif ( ! current_user_can( 'install_themes' ) || ! current_user_can( 'install_plugins' ) ) { wp_send_json_error( array( 'message' => __( 'User not allowed to install items.', 'envato-market' ) ) ); } $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $_POST['token'], ), ); $request = envato_market()->api()->item( $_POST['id'], $args ); if ( false === $request ) { wp_send_json_error( array( 'message' => __( 'The Token or Item ID is incorrect.', 'envato-market' ) ) ); } if ( false === envato_market()->api()->download( $_POST['id'], $args ) ) { wp_send_json_error( array( 'message' => __( 'The item cannot be downloaded.', 'envato-market' ) ) ); } if ( isset( $request['number_of_sales'] ) ) { $type = 'plugin'; } else { $type = 'theme'; } if ( isset( $type ) ) { $response = array( 'name' => $request['name'], 'token' => $_POST['token'], 'id' => $_POST['id'], 'type' => $type, 'authorized' => 'success', ); $options = get_option( envato_market()->get_option_name(), array() ); if ( ! empty( $options['items'] ) ) { $options['items'] = array_values( $options['items'] ); $key = count( $options['items'] ); } else { $options['items'] = array(); $key = 0; } $options['items'][] = $response; envato_market()->set_options( $options ); // Rebuild the theme cache. if ( 'theme' === $type ) { envato_market()->items()->set_themes( true, false ); $install_link = add_query_arg( array( 'page' => envato_market()->get_slug(), 'action' => 'install-theme', 'id' => $_POST['id'], ), self_admin_url( 'admin.php' ) ); $request['install'] = wp_nonce_url( $install_link, 'install-theme_' . $_POST['id'] ); } // Rebuild the plugin cache. if ( 'plugin' === $type ) { envato_market()->items()->set_plugins( true, false ); $install_link = add_query_arg( array( 'page' => envato_market()->get_slug(), 'action' => 'install-plugin', 'id' => $_POST['id'], ), self_admin_url( 'admin.php' ) ); $request['install'] = wp_nonce_url( $install_link, 'install-plugin_' . $_POST['id'] ); } $response['key'] = $key; $response['item'] = $request; wp_send_json_success( $response ); } wp_send_json_error( array( 'message' => __( 'An unknown error occurred.', 'envato-market' ) ) ); } /** * AJAX handler for removing items that use a non global token. * * @since 1.0.0 * @codeCoverageIgnore */ public function ajax_remove_item() { if ( ! check_ajax_referer( self::AJAX_ACTION, 'nonce', false ) ) { status_header( 400 ); wp_send_json_error( 'bad_nonce' ); } elseif ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) { status_header( 405 ); wp_send_json_error( 'bad_method' ); } elseif ( empty( $_POST['id'] ) ) { wp_send_json_error( array( 'message' => __( 'The Item ID is missing.', 'envato-market' ) ) ); } elseif ( ! current_user_can( 'delete_plugins' ) || ! current_user_can( 'delete_themes' ) ) { wp_send_json_error( array( 'message' => __( 'User not allowed to update items.', 'envato-market' ) ) ); } $options = get_option( envato_market()->get_option_name(), array() ); $type = ''; foreach ( $options['items'] as $key => $item ) { if ( $item['id'] === $_POST['id'] ) { $type = $item['type']; unset( $options['items'][ $key ] ); break; } } $options['items'] = array_values( $options['items'] ); envato_market()->set_options( $options ); // Rebuild the theme cache. if ( 'theme' === $type ) { envato_market()->items()->set_themes( true, false ); } // Rebuild the plugin cache. if ( 'plugin' === $type ) { envato_market()->items()->set_plugins( true, false ); } wp_send_json_success(); } /** * AJAX handler for performing a healthcheck of the current website. * * @since 2.0.6 * @codeCoverageIgnore */ public function ajax_healthcheck() { if ( ! check_ajax_referer( self::AJAX_ACTION, 'nonce', false ) ) { status_header( 400 ); wp_send_json_error( 'bad_nonce' ); } elseif ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) { status_header( 405 ); wp_send_json_error( 'bad_method' ); } elseif ( ! current_user_can( 'install_themes' ) || ! current_user_can( 'install_plugins' ) ) { wp_send_json_error( array( 'message' => __( 'User not allowed to install items.', 'envato-market' ) ) ); } $limits = $this->get_server_limits(); wp_send_json_success( array( 'limits' => $limits ) ); } /** * AJAX handler for performing a healthcheck of the current website. * * @since 2.0.6 * @codeCoverageIgnore */ public function get_server_limits() { $limits = []; // Check memory limit is > 256 M try { $memory_limit = wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) ); $memory_limit_desired = 256; $memory_limit_ok = $memory_limit < 0 || $memory_limit >= $memory_limit_desired * 1024 * 1024; $memory_limit_in_mb = $memory_limit < 0 ? 'Unlimited' : floor( $memory_limit / ( 1024 * 1024 ) ) . 'M'; $limits['memory_limit'] = [ 'title' => 'PHP Memory Limit', 'ok' => $memory_limit_ok, 'message' => $memory_limit_ok ? "is ok at {$memory_limit_in_mb}." : "{$memory_limit_in_mb} may be too small. If you are having issues please set your PHP memory limit to at least 256M - or ask your hosting provider to do this if you're unsure." ]; } catch ( \Exception $e ) { $limits['memory_limit'] = [ 'title' => 'PHP Memory Limit', 'ok' => false, 'message' => 'Failed to check memory limit. If you are having issues please ask hosting provider to raise the memory limit for you.' ]; } // Check upload size. try { $upload_size_desired = 80; $upload_max_filesize = wp_max_upload_size(); $upload_max_filesize_ok = $upload_max_filesize < 0 || $upload_max_filesize >= $upload_size_desired * 1024 * 1024; $upload_max_filesize_in_mb = $upload_max_filesize < 0 ? 'Unlimited' : floor( $upload_max_filesize / ( 1024 * 1024 ) ) . 'M'; $limits['upload'] = [ 'ok' => $upload_max_filesize_ok, 'title' => 'PHP Upload Limits', 'message' => $upload_max_filesize_ok ? "is ok at $upload_max_filesize_in_mb." : "$upload_max_filesize_in_mb may be too small. If you are having issues please set your PHP upload limits to at least {$upload_size_desired}M - or ask your hosting provider to do this if you're unsure.", ]; } catch ( \Exception $e ) { $limits['upload'] = [ 'title' => 'PHP Upload Limits', 'ok' => false, 'message' => 'Failed to check upload limit. If you are having issues please ask hosting provider to raise the upload limit for you.' ]; } // Check max_input_vars. try { $max_input_vars = ini_get( 'max_input_vars' ); $max_input_vars_desired = 1000; $max_input_vars_ok = $max_input_vars < 0 || $max_input_vars >= $max_input_vars_desired; $limits['max_input_vars'] = [ 'ok' => $max_input_vars_ok, 'title' => 'PHP Max Input Vars', 'message' => $max_input_vars_ok ? "is ok at $max_input_vars." : "$max_input_vars may be too small. If you are having issues please set your PHP max input vars to at least $max_input_vars_desired - or ask your hosting provider to do this if you're unsure.", ]; } catch ( \Exception $e ) { $limits['max_input_vars'] = [ 'title' => 'PHP Max Input Vars', 'ok' => false, 'message' => 'Failed to check input vars limit. If you are having issues please ask hosting provider to raise the input vars limit for you.' ]; } // Check max_execution_time. try { $max_execution_time = ini_get( 'max_execution_time' ); $max_execution_time_desired = 60; $max_execution_time_ok = $max_execution_time <= 0 || $max_execution_time >= $max_execution_time_desired; $limits['max_execution_time'] = [ 'ok' => $max_execution_time_ok, 'title' => 'PHP Execution Time', 'message' => $max_execution_time_ok ? "PHP execution time limit is ok at {$max_execution_time}." : "$max_execution_time is too small. Please set your PHP max execution time to at least $max_execution_time_desired - or ask your hosting provider to do this if you're unsure.", ]; } catch ( \Exception $e ) { $limits['max_execution_time'] = [ 'title' => 'PHP Execution Time', 'ok' => false, 'message' => 'Failed to check PHP execution time limit. Please ask hosting provider to raise this limit for you.' ]; } // Check various hostname connectivity. $hosts_to_check = array( array( 'hostname' => 'envato.github.io', 'url' => 'https://envato.github.io/wp-envato-market/dist/update-check.json', 'title' => 'Plugin Update API', ), array( 'hostname' => 'api.envato.com', 'url' => 'https://api.envato.com/ping', 'title' => 'Envato Market API', ), array( 'hostname' => 'marketplace.envato.com', 'url' => 'https://marketplace.envato.com/robots.txt', 'title' => 'Download API', ), ); foreach ( $hosts_to_check as $host ) { try { $response = wp_remote_get( $host['url'], [ 'user-agent' => 'WordPress - Envato Market ' . envato_market()->get_version(), 'timeout' => 5, ] ); $response_code = wp_remote_retrieve_response_code( $response ); if ( $response && ! is_wp_error( $response ) && $response_code === 200 ) { $limits[ $host['hostname'] ] = [ 'ok' => true, 'title' => $host['title'], 'message' => 'Connected ok.', ]; } else { $limits[ $host['hostname'] ] = [ 'ok' => false, 'title' => $host['title'], 'message' => "Connection failed. Status '$response_code'. Please ensure PHP is allowed to connect to the host '" . $host['hostname'] . "' - or ask your hosting provider to do this if you’re unsure. " . ( is_wp_error( $response ) ? $response->get_error_message() : '' ), ]; } } catch ( \Exception $e ) { $limits[ $host['hostname'] ] = [ 'ok' => true, 'title' => $host['title'], 'message' => "Connection failed. Please contact the hosting provider and ensure PHP is allowed to connect to the host '" . $host['hostname'] . "'. " . $e->getMessage(), ]; } } // Check authenticated API request if ( !defined('ENVATO_LOCAL_DEVELOPMENT') ) { $response = envato_market()->api()->request( 'https://api.envato.com/whoami' ); if ( is_wp_error( $response ) ) { $limits['authentication'] = [ 'ok' => false, 'title' => 'Envato API Authentication', 'message' => "Not currently authenticated with the Envato API. Please add your API token. " . $response->get_error_message(), ]; } elseif ( ! isset( $response['scopes'] ) ) { $limits['authentication'] = [ 'ok' => false, 'title' => 'Envato API Authentication', 'message' => "Missing API permissions. Please re-create your Envato API token with the correct permissions. ", ]; } else { $minimum_scopes = $this->get_required_permissions(); $maximum_scopes = array( 'default' => 'Default' ) + $minimum_scopes; $missing_scopes = array(); $additional_scopes = array(); foreach ( $minimum_scopes as $required_scope => $required_scope_name ) { if ( ! in_array( $required_scope, $response['scopes'] ) ) { // The scope minimum required scope doesn't exist. $missing_scopes [] = $required_scope; } } foreach ( $response['scopes'] as $scope ) { if ( ! isset( $maximum_scopes[ $scope ] ) ) { // The available scope is outside our maximum bounds. $additional_scopes [] = $scope; } } $limits['authentication'] = [ 'ok' => true, 'title' => 'Envato API Authentication', 'message' => "Authenticated successfully with correct scopes: " . implode( ', ', $response['scopes'] ), ]; } } $debug_enabled = defined( 'WP_DEBUG' ) && WP_DEBUG; $limits['wp_debug'] = [ 'ok' => ! $debug_enabled, 'title' => 'WP Debug', 'message' => $debug_enabled ? 'If you’re on a production website, it’s best to set WP_DEBUG to false, please ask your hosting provider to do this if you’re unsure.' : 'WP Debug is disabled, all ok.', ]; $zip_archive_installed = class_exists( '\ZipArchive' ); $limits['zip_archive'] = [ 'ok' => $zip_archive_installed, 'title' => 'ZipArchive Support', 'message' => $zip_archive_installed ? 'ZipArchive is available.' : 'ZipArchive is not available. If you have issues installing or updating items please ask your hosting provider to enable ZipArchive.', ]; $php_version_ok = version_compare( PHP_VERSION, '7.0', '>=' ); $limits['php_version'] = [ 'ok' => $php_version_ok, 'title' => 'PHP Version', 'message' => $php_version_ok ? 'PHP version is ok at ' . PHP_VERSION . '.' : 'Please ask the hosting provider to upgrade your PHP version to at least 7.0 or above.', ]; require_once( ABSPATH . 'wp-admin/includes/file.php' ); $current_filesystem_method = get_filesystem_method(); if ( $current_filesystem_method !== 'direct' ) { $limits['filesystem_method'] = [ 'ok' => false, 'title' => 'WordPress Filesystem', 'message' => 'Please enable WordPress FS_METHOD direct - or ask your hosting provider to do this if you’re unsure.', ]; } $wp_upload_dir = wp_upload_dir(); $upload_base_dir = $wp_upload_dir['basedir']; $upload_base_dir_writable = is_writable( $upload_base_dir ); $limits['wp_content_writable'] = [ 'ok' => $upload_base_dir_writable, 'title' => 'WordPress File Permissions', 'message' => $upload_base_dir_writable ? 'is ok.' : 'Please set correct WordPress PHP write permissions for the wp-content directory - or ask your hosting provider to do this if you’re unsure.', ]; $active_plugins = get_option( 'active_plugins' ); $active_plugins_ok = count( $active_plugins ) < 15; if ( ! $active_plugins_ok ) { $limits['active_plugins'] = [ 'ok' => false, 'title' => 'Active Plugins', 'message' => 'Please try to reduce the number of active plugins on your WordPress site, as this will slow things down.', ]; } return $limits; } /** * Admin page callback. * * @since 1.0.0 */ public function render_admin_callback() { require( envato_market()->get_plugin_path() . 'inc/admin/view/callback/admin.php' ); } /** * OAuth section callback. * * @since 1.0.0 */ public function render_oauth_section_callback() { require( envato_market()->get_plugin_path() . 'inc/admin/view/callback/section/oauth.php' ); } /** * Items section callback. * * @since 1.0.0 */ public function render_items_section_callback() { require( envato_market()->get_plugin_path() . 'inc/admin/view/callback/section/items.php' ); } /** * Token setting callback. * * @since 1.0.0 */ public function render_token_setting_callback() { require( envato_market()->get_plugin_path() . 'inc/admin/view/callback/setting/token.php' ); } /** * Items setting callback. * * @since 1.0.0 */ public function render_items_setting_callback() { require( envato_market()->get_plugin_path() . 'inc/admin/view/callback/setting/items.php' ); } /** * Intro * * @since 1.0.0 */ public function render_intro_partial() { require( envato_market()->get_plugin_path() . 'inc/admin/view/partials/intro.php' ); } /** * Tabs * * @since 1.0.0 */ public function render_tabs_partial() { require( envato_market()->get_plugin_path() . 'inc/admin/view/partials/tabs.php' ); } /** * Settings panel * * @since 1.0.0 */ public function render_settings_panel_partial() { require( envato_market()->get_plugin_path() . 'inc/admin/view/partials/settings.php' ); } /** * Help panel * * @since 2.0.1 */ public function render_help_panel_partial() { require( envato_market()->get_plugin_path() . 'inc/admin/view/partials/help.php' ); } /** * Themes panel * * @since 1.0.0 */ public function render_themes_panel_partial() { require( envato_market()->get_plugin_path() . 'inc/admin/view/partials/themes.php' ); } /** * Plugins panel * * @since 1.0.0 */ public function render_plugins_panel_partial() { require( envato_market()->get_plugin_path() . 'inc/admin/view/partials/plugins.php' ); } /** * Success notice. * * @since 1.0.0 */ public function render_success_notice() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/success.php' ); } /** * Success no-items notice. * * @since 1.0.0 */ public function render_success_no_items_notice() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/success-no-items.php' ); } /** * Success single-use notice. * * @since 1.0.0 */ public function render_success_single_use_notice() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/success-single-use.php' ); } /** * Error details. * * @since 2.0.2 */ public function render_additional_error_details() { $error_details = get_site_transient( envato_market()->get_option_name() . '_error_information' ); if ( $error_details && ! empty( $error_details['title'] ) ) { extract( $error_details ); require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/error-details.php' ); } } /** * Error notice. * * @since 1.0.0 */ public function render_error_notice() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/error.php' ); $this->render_additional_error_details(); } /** * Permission error notice. * * @since 2.0.1 */ public function render_error_permissions() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/error-permissions.php' ); $this->render_additional_error_details(); } /** * Error single-use notice. * * @since 1.0.0 */ public function render_error_single_use_notice() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/error-single-use.php' ); $this->render_additional_error_details(); } /** * Error missing zip. * * @since 2.0.1 */ public function render_error_missing_zip() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/error-missing-zip.php' ); $this->render_additional_error_details(); } /** * Error http * * @since 2.0.1 */ public function render_error_http() { require( envato_market()->get_plugin_path() . 'inc/admin/view/notice/error-http.php' ); $this->render_additional_error_details(); } /** * Use the Settings API when in network mode. * * This allows us to make use of the same WordPress Settings API when displaying the menu item in network mode. * * @since 2.0.0 */ public function save_network_settings() { check_admin_referer( envato_market()->get_slug() . '-options' ); global $new_whitelist_options; $options = $new_whitelist_options[ envato_market()->get_slug() ]; foreach ( $options as $option ) { if ( isset( $_POST[ $option ] ) ) { update_site_option( $option, $_POST[ $option ] ); } else { delete_site_option( $option ); } } wp_redirect( envato_market()->get_page_url() ); exit; } /** * Store additional error information in transient so users can self debug. * * @since 2.0.2 */ public function store_additional_error_debug_information( $title, $message = '', $data = [] ) { set_site_transient( envato_market()->get_option_name() . '_error_information', [ 'title' => $title, 'message' => $message, 'data' => $data, ], 120 ); } } endif; admin/functions.php 0000644 00000040477 14720702445 0010376 0 ustar 00 <?php /** * Functions * * @package Envato_Market */ /** * Interate over the themes array and displays each theme. * * @since 1.0.0 * * @param string $group The theme group. Options are 'purchased', 'active', 'installed', or 'install'. */ function envato_market_themes_column( $group = 'install' ) { $premium = envato_market()->items()->themes( $group ); if ( empty( $premium ) ) { return; } foreach ( $premium as $slug => $theme ) : $name = $theme['name']; $author = $theme['author']; $version = $theme['version']; $description = $theme['description']; $url = $theme['url']; $author_url = $theme['author_url']; $theme['hasUpdate'] = false; if ( 'active' === $group || 'installed' === $group ) { $get_theme = wp_get_theme( $slug ); if ( $get_theme->exists() ) { $name = $get_theme->get( 'Name' ); $author = $get_theme->get( 'Author' ); $version = $get_theme->get( 'Version' ); $description = $get_theme->get( 'Description' ); $author_url = $get_theme->get( 'AuthorURI' ); if ( version_compare( $version, $theme['version'], '<' ) ) { $theme['hasUpdate'] = true; } } } // Setup the column CSS classes. $classes = array( 'envato-card', 'theme' ); if ( 'active' === $group ) { $classes[] = 'active'; } // Setup the update action links. $update_actions = array(); if ( true === $theme['hasUpdate'] ) { $classes[] = 'update'; $classes[] = 'envato-card-' . esc_attr( $slug ); if ( current_user_can( 'update_themes' ) ) { // Upgrade link. $upgrade_link = add_query_arg( array( 'action' => 'upgrade-theme', 'theme' => esc_attr( $slug ), ), self_admin_url( 'update.php' ) ); $update_actions['update'] = sprintf( '<a class="update-now" href="%1$s" aria-label="%2$s" data-name="%3$s %5$s" data-slug="%4$s" data-version="%5$s">%6$s</a>', wp_nonce_url( $upgrade_link, 'upgrade-theme_' . $slug ), esc_attr__( 'Update %s now', 'envato-market' ), esc_attr( $name ), esc_attr( $slug ), esc_attr( $theme['version'] ), esc_html__( 'Update Available', 'envato-market' ) ); $update_actions['details'] = sprintf( '<a href="%1$s" class="details" title="%2$s" target="_blank">%3$s</a>', esc_url( $url ), esc_attr( $name ), sprintf( __( 'View version %1$s details.', 'envato-market' ), $theme['version'] ) ); } } // Setup the action links. $actions = array(); if ( 'active' === $group && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { // Customize theme. $customize_url = admin_url( 'customize.php' ); $customize_url .= '?theme=' . urlencode( $slug ); $customize_url .= '&return=' . urlencode( envato_market()->get_page_url() . '#themes' ); $actions['customize'] = '<a href="' . esc_url( $customize_url ) . '" class="button button-primary load-customize hide-if-no-customize"><span aria-hidden="true">' . __( 'Customize', 'envato-market' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Customize “%s”', 'envato-market' ), $name ) . '</span></a>'; } elseif ( 'installed' === $group ) { $can_activate = true; // @codeCoverageIgnoreStart // Multisite needs special attention. if ( is_multisite() && ! $get_theme->is_allowed( 'both' ) && current_user_can( 'manage_sites' ) ) { $can_activate = false; if ( current_user_can( 'manage_network_themes' ) ) { $actions['network_enable'] = '<a href="' . esc_url( network_admin_url( wp_nonce_url( 'themes.php?action=enable&theme=' . urlencode( $slug ) . '&paged=1&s', 'enable-theme_' . $slug ) ) ) . '" class="button"><span aria-hidden="true">' . __( 'Network Enable', 'envato-market' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Network Enable “%s”', 'envato-market' ), $name ) . '</span></a>'; } } // @codeCoverageIgnoreEnd // Can activate theme. if ( $can_activate && current_user_can( 'switch_themes' ) ) { $activate_link = add_query_arg( array( 'action' => 'activate', 'stylesheet' => urlencode( $slug ), ), admin_url( 'themes.php' ) ); $activate_link = wp_nonce_url( $activate_link, 'switch-theme_' . $slug ); // Activate link. $actions['activate'] = '<a href="' . esc_url( $activate_link ) . '" class="button"><span aria-hidden="true">' . __( 'Activate', 'envato-market' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Activate “%s”', 'envato-market' ), $name ) . '</span></a>'; // Preview theme. if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { $preview_url = admin_url( 'customize.php' ); $preview_url .= '?theme=' . urlencode( $slug ); $preview_url .= '&return=' . urlencode( envato_market()->get_page_url() . '#themes' ); $actions['customize_preview'] = '<a href="' . esc_url( $preview_url ) . '" class="button button-primary load-customize hide-if-no-customize"><span aria-hidden="true">' . __( 'Live Preview', 'envato-market' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Live Preview “%s”', 'envato-market' ), $name ) . '</span></a>'; } } } elseif ( 'install' === $group && current_user_can( 'install_themes' ) ) { // Install link. $install_link = add_query_arg( array( 'page' => envato_market()->get_slug(), 'action' => 'install-theme', 'id' => $theme['id'], ), self_admin_url( 'admin.php' ) ); $actions['install'] = ' <a href="' . wp_nonce_url( $install_link, 'install-theme_' . $theme['id'] ) . '" class="button button-primary"> <span aria-hidden="true">' . __( 'Install', 'envato-market' ) . '</span> <span class="screen-reader-text">' . sprintf( __( 'Install %s', 'envato-market' ), $name ) . '</span> </a>'; } if ( 0 === strrpos( html_entity_decode( $author ), '<a ' ) ) { $author_link = $author; } else { $author_link = '<a href="' . esc_url( $author_url ) . '">' . esc_html( $author ) . '</a>'; } ?> <div class="envato-market-block" data-id="<?php echo esc_attr( $theme['id'] ); ?>"> <div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"> <div class="envato-card-top"> <a href="<?php echo esc_url( $url ); ?>" class="column-icon"> <img src="<?php echo esc_url( $theme['thumbnail_url'] ); ?>"/> </a> <div class="column-name"> <h4> <a href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( $name ); ?></a> <span class="version" aria-label="<?php esc_attr_e( 'Version %s', 'envato-market' ); ?>"> <?php echo esc_html( sprintf( __( 'Version %s', 'envato-market' ), $version ) ); ?> </span> </h4> </div> <div class="column-description"> <div class="description"> <?php echo wp_kses_post( wpautop( strip_tags( $description ) ) ); ?> </div> <p class="author"> <cite> <?php esc_html_e( 'By', 'envato-market' ); ?> <?php echo wp_kses_post( $author_link ); ?> </cite> </p> </div> <?php if ( ! empty( $update_actions ) ) { ?> <div class="column-update"> <?php echo implode( "\n", $update_actions ); ?> </div> <?php } ?> </div> <div class="envato-card-bottom"> <div class="column-rating"> <?php if ( ! empty( $theme['rating'] ) ) { if ( is_array( $theme['rating'] ) ) { $count = ! empty( $theme['rating']['count'] ) ? $theme['rating']['count'] : 0; $rating = ! empty( $theme['rating']['rating'] ) ? (int) $theme['rating']['rating'] : 0; wp_star_rating( array( 'rating' => $count > 0 ? ( $rating / 5 * 100 ) : 0, 'type' => 'percent', 'number' => $count, ) ); } else { wp_star_rating( array( 'rating' => $theme['rating'] > 0 ? ( $theme['rating'] / 5 * 100 ) : 0, 'type' => 'percent', ) ); } } ?> </div> <div class="column-actions"> <?php echo implode( "\n", $actions ); ?> </div> </div> </div> </div> <?php endforeach; } /** * Interate over the plugins array and displays each plugin. * * @since 1.0.0 * * @param string $group The plugin group. Options are 'purchased', 'active', 'installed', or 'install'. */ function envato_market_plugins_column( $group = 'install' ) { $premium = envato_market()->items()->plugins( $group ); if ( empty( $premium ) ) { return; } $plugins = envato_market()->items()->wp_plugins(); foreach ( $premium as $slug => $plugin ) : $name = $plugin['name']; $author = $plugin['author']; $version = $plugin['version']; $description = $plugin['description']; $url = $plugin['url']; $author_url = $plugin['author_url']; $plugin['hasUpdate'] = false; // Setup the column CSS classes. $classes = array( 'envato-card', 'plugin' ); if ( 'active' === $group ) { $classes[] = 'active'; } // Setup the update action links. $update_actions = array(); // Check for an update. if ( isset( $plugins[ $slug ] ) && version_compare( $plugins[ $slug ]['Version'], $plugin['version'], '<' ) ) { $plugin['hasUpdate'] = true; $classes[] = 'update'; $classes[] = 'envato-card-' . sanitize_key( dirname( $slug ) ); if ( current_user_can( 'update_plugins' ) ) { // Upgrade link. $upgrade_link = add_query_arg( array( 'action' => 'upgrade-plugin', 'plugin' => $slug, ), self_admin_url( 'update.php' ) ); // Details link. $details_link = add_query_arg( array( 'action' => 'upgrade-plugin', 'tab' => 'plugin-information', 'plugin' => dirname( $slug ), 'section' => 'changelog', 'TB_iframe' => 'true', 'width' => 640, 'height' => 662, ), self_admin_url( 'plugin-install.php' ) ); $update_actions['update'] = sprintf( '<a class="update-now" href="%1$s" aria-label="%2$s" data-name="%3$s %6$s" data-plugin="%4$s" data-slug="%5$s" data-version="%6$s">%7$s</a>', wp_nonce_url( $upgrade_link, 'upgrade-plugin_' . $slug ), esc_attr__( 'Update %s now', 'envato-market' ), esc_attr( $name ), esc_attr( $slug ), sanitize_key( dirname( $slug ) ), esc_attr( $version ), esc_html__( 'Update Available', 'envato-market' ) ); $update_actions['details'] = sprintf( '<a href="%1$s" class="thickbox details" title="%2$s">%3$s</a>', esc_url( $details_link ), esc_attr( $name ), sprintf( __( 'View version %1$s details.', 'envato-market' ), $version ) ); } } // Setup the action links. $actions = array(); if ( 'active' === $group ) { // Deactivate link. $deactivate_link = add_query_arg( array( 'action' => 'deactivate', 'plugin' => $slug, ), self_admin_url( 'plugins.php' ) ); $actions['deactivate'] = ' <a href="' . wp_nonce_url( $deactivate_link, 'deactivate-plugin_' . $slug ) . '" class="button"> <span aria-hidden="true">' . __( 'Deactivate', 'envato-market' ) . '</span> <span class="screen-reader-text">' . sprintf( __( 'Deactivate %s', 'envato-market' ), $name ) . '</span> </a>'; } elseif ( 'installed' === $group ) { if ( ! is_multisite() && current_user_can( 'delete_plugins' ) ) { // Delete link. $delete_link = add_query_arg( array( 'action' => 'delete-selected', 'checked[]' => $slug, ), self_admin_url( 'plugins.php' ) ); $actions['delete'] = ' <a href="' . wp_nonce_url( $delete_link, 'bulk-plugins' ) . '" class="button-delete"> <span aria-hidden="true">' . __( 'Delete', 'envato-market' ) . '</span> <span class="screen-reader-text">' . sprintf( __( 'Delete %s', 'envato-market' ), $name ) . '</span> </a>'; } if ( ! is_multisite() && current_user_can( 'activate_plugins' ) ) { // Activate link. $activate_link = add_query_arg( array( 'action' => 'activate', 'plugin' => $slug, ), self_admin_url( 'plugins.php' ) ); $actions['activate'] = ' <a href="' . wp_nonce_url( $activate_link, 'activate-plugin_' . $slug ) . '" class="button"> <span aria-hidden="true">' . __( 'Activate', 'envato-market' ) . '</span> <span class="screen-reader-text">' . sprintf( __( 'Activate %s', 'envato-market' ), $name ) . '</span> </a>'; } // @codeCoverageIgnoreStart // Multisite needs special attention. if ( is_multisite() ) { if ( current_user_can( 'manage_network_plugins' ) ) { $actions['network_activate'] = ' <a href="' . esc_url( network_admin_url( wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $slug ), 'activate-plugin_' . $slug ) ) ) . '" class="button"> <span aria-hidden="true">' . __( 'Network Activate', 'envato-market' ) . '</span> <span class="screen-reader-text">' . sprintf( __( 'Network Activate %s', 'envato-market' ), $name ) . '</span> </a>'; } } // @codeCoverageIgnoreEnd } elseif ( 'install' === $group && current_user_can( 'install_plugins' ) ) { // Install link. $install_link = add_query_arg( array( 'page' => envato_market()->get_slug(), 'action' => 'install-plugin', 'id' => $plugin['id'], ), self_admin_url( 'admin.php' ) ); $actions['install'] = ' <a href="' . wp_nonce_url( $install_link, 'install-plugin_' . $plugin['id'] ) . '" class="button button-primary"> <span aria-hidden="true">' . __( 'Install', 'envato-market' ) . '</span> <span class="screen-reader-text">' . sprintf( __( 'Install %s', 'envato-market' ), $name ) . '</span> </a>'; } if ( 0 === strrpos( html_entity_decode( $author ), '<a ' ) ) { $author_link = $author; } else { $author_link = '<a href="' . esc_url( $author_url ) . '">' . esc_html( $author ) . '</a>'; } ?> <div class="envato-market-block" data-id="<?php echo esc_attr( $plugin['id'] ); ?>"> <div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"> <div class="envato-card-top"> <a href="<?php echo esc_url( $url ); ?>" class="column-icon"> <img src="<?php echo esc_url( $plugin['thumbnail_url'] ); ?>"/> </a> <div class="column-name"> <h4> <a href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( $name ); ?></a> <span class="version" aria-label="<?php esc_attr_e( 'Version %s', 'envato-market' ); ?>"> <?php echo esc_html( sprintf( __( 'Version %s', 'envato-market' ), ( isset( $plugins[ $slug ] ) ? $plugins[ $slug ]['Version'] : $version ) ) ); ?> </span> </h4> </div> <div class="column-description"> <div class="description"> <?php echo wp_kses_post( wpautop( strip_tags( $description ) ) ); ?> </div> <p class="author"> <cite> <?php esc_html_e( 'By', 'envato-market' ); ?> <?php echo wp_kses_post( $author_link ); ?> </cite> </p> </div> <?php if ( ! empty( $update_actions ) ) { ?> <div class="column-update"> <?php echo implode( "\n", $update_actions ); ?> </div> <?php } ?> </div> <div class="envato-card-bottom"> <div class="column-rating"> <?php if ( ! empty( $plugin['rating'] ) ) { if ( is_array( $plugin['rating'] ) && ! empty( $plugin['rating']['count'] ) ) { wp_star_rating( array( 'rating' => $plugin['rating']['rating'] > 0 ? ( $plugin['rating']['rating'] / 5 * 100 ) : 0, 'type' => 'percent', 'number' => $plugin['rating']['count'], ) ); } else { wp_star_rating( array( 'rating' => $plugin['rating'] > 0 ? ( $plugin['rating'] / 5 * 100 ) : 0, 'type' => 'percent', ) ); } } ?> </div> <div class="column-actions"> <?php echo implode( "\n", $actions ); ?> </div> </div> </div> </div> <?php endforeach; } /** * A handy method for logging to the st_out / and or debug_log * Use: write_log("My variable is {$variable}") */ if (!function_exists('write_log') && defined('ENVATO_LOCAL_DEVELOPMENT')) { function write_log($log) { if (is_array($log) || is_object($log)) { error_log(print_r($log, true)); } else { error_log($log); } } } class-envato-market-api.php 0000644 00000034405 14720702445 0011717 0 ustar 00 <?php /** * Envato API class. * * @package Envato_Market */ if ( ! class_exists( 'Envato_Market_API' ) && class_exists( 'Envato_Market' ) ) : /** * Creates the Envato API connection. * * @class Envato_Market_API * @version 1.0.0 * @since 1.0.0 */ class Envato_Market_API { /** * The single class instance. * * @since 1.0.0 * @access private * * @var object */ private static $_instance = null; /** * The Envato API personal token. * * @since 1.0.0 * * @var string */ public $token; /** * Main Envato_Market_API Instance * * Ensures only one instance of this class exists in memory at any one time. * * @see Envato_Market_API() * @uses Envato_Market_API::init_globals() Setup class globals. * @uses Envato_Market_API::init_actions() Setup hooks and actions. * * @since 1.0.0 * @static * @return object The one true Envato_Market_API. * @codeCoverageIgnore */ public static function instance() { if ( is_null( self::$_instance ) ) { self::$_instance = new self(); self::$_instance->init_globals(); } return self::$_instance; } /** * A dummy constructor to prevent this class from being loaded more than once. * * @see Envato_Market_API::instance() * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function __construct() { /* We do nothing here! */ } /** * You cannot clone this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __clone() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * You cannot unserialize instances of this class. * * @since 1.0.0 * @codeCoverageIgnore */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'envato-market' ), '1.0.0' ); } /** * Setup the class globals. * * @since 1.0.0 * @access private * @codeCoverageIgnore */ private function init_globals() { // Envato API token. $this->token = envato_market()->get_option( 'token' ); } /** * Query the Envato API. * * @uses wp_remote_get() To perform an HTTP request. * * @since 1.0.0 * * @param string $url API request URL, including the request method, parameters, & file type. * @param array $args The arguments passed to `wp_remote_get`. * @return array|WP_Error The HTTP response. */ public function request( $url, $args = array() ) { $defaults = array( 'sslverify' => !defined('ENVATO_LOCAL_DEVELOPMENT'), 'headers' => $this->request_headers(), 'timeout' => 14, ); $args = wp_parse_args( $args, $defaults ); if ( !defined('ENVATO_LOCAL_DEVELOPMENT') ) { $token = trim( str_replace( 'Bearer', '', $args['headers']['Authorization'] ) ); if ( empty( $token ) ) { return new WP_Error( 'api_token_error', __( 'An API token is required.', 'envato-market' ) ); } } $debugging_information = [ 'request_url' => $url, ]; // Make an API request. $response = wp_remote_get( esc_url_raw( $url ), $args ); // Check the response code. $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); $debugging_information['response_code'] = $response_code; $debugging_information['response_cf_ray'] = wp_remote_retrieve_header( $response, 'cf-ray' ); $debugging_information['response_server'] = wp_remote_retrieve_header( $response, 'server' ); if ( ! empty( $response->errors ) && isset( $response->errors['http_request_failed'] ) ) { // API connectivity issue, inject notice into transient with more details. $option = envato_market()->get_options(); if ( empty( $option['notices'] ) ) { $option['notices'] = []; } $option['notices']['http_error'] = current( $response->errors['http_request_failed'] ); envato_market()->set_options( $option ); return new WP_Error( 'http_error', esc_html( current( $response->errors['http_request_failed'] ) ), $debugging_information ); } if ( 200 !== $response_code && ! empty( $response_message ) ) { return new WP_Error( $response_code, $response_message, $debugging_information ); } elseif ( 200 !== $response_code ) { return new WP_Error( $response_code, __( 'An unknown API error occurred.', 'envato-market' ), $debugging_information ); } else { $return = json_decode( wp_remote_retrieve_body( $response ), true ); if ( null === $return ) { return new WP_Error( 'api_error', __( 'An unknown API error occurred.', 'envato-market' ), $debugging_information ); } return $return; } } /** * Deferred item download URL. * * @since 1.0.0 * * @param int $id The item ID. * @return string. */ public function deferred_download( $id ) { if ( empty( $id ) ) { return ''; } $args = array( 'deferred_download' => true, 'item_id' => $id, ); return add_query_arg( $args, esc_url( envato_market()->get_page_url() ) ); } /** * Get the item download. * * @since 1.0.0 * * @param int $id The item ID. * @param array $args The arguments passed to `wp_remote_get`. * @return bool|array The HTTP response. */ public function download( $id, $args = array() ) { if ( empty( $id ) ) { return false; } $domain = envato_market()->get_envato_api_domain(); $path = $this->api_path_for('download'); $url = $domain . $path . '?item_id=' . $id . '&shorten_url=true'; $response = $this->request( $url, $args ); // @todo Find out which errors could be returned & handle them in the UI. if ( is_wp_error( $response ) || empty( $response ) || ! empty( $response['error'] ) ) { return false; } if ( ! empty( $response['wordpress_theme'] ) ) { return $response['wordpress_theme']; } if ( ! empty( $response['wordpress_plugin'] ) ) { return $response['wordpress_plugin']; } // Missing a WordPress theme and plugin, report an error. $option = envato_market()->get_options(); if ( ! isset( $option['notices'] ) ) { $option['notices'] = []; } $option['notices']['missing-package-zip'] = true; envato_market()->set_options( $option ); return false; } /** * Get an item by ID and type. * * @since 1.0.0 * * @param int $id The item ID. * @param array $args The arguments passed to `wp_remote_get`. * @return array The HTTP response. */ public function item( $id, $args = array() ) { $domain = envato_market()->get_envato_api_domain(); $path = $this->api_path_for('catalog-item'); $url = $domain . $path . '?id=' . $id; $response = $this->request( $url, $args ); if ( is_wp_error( $response ) || empty( $response ) ) { return false; } if ( ! empty( $response['wordpress_theme_metadata'] ) ) { return $this->normalize_theme( $response ); } if ( ! empty( $response['wordpress_plugin_metadata'] ) ) { return $this->normalize_plugin( $response ); } return false; } /** * Get the list of available themes. * * @since 1.0.0 * * @param array $args The arguments passed to `wp_remote_get`. * @return array The HTTP response. */ public function themes( $args = array() ) { $themes = array(); $domain = envato_market()->get_envato_api_domain(); $path = $this->api_path_for('list-purchases'); $url = $domain . $path . '?filter_by=wordpress-themes'; $response = $this->request( $url, $args ); if ( is_wp_error( $response ) || empty( $response ) || empty( $response['results'] ) ) { return $themes; } foreach ( $response['results'] as $theme ) { $themes[] = $this->normalize_theme( $theme['item'] ); } return $themes; } /** * Normalize a theme. * * @since 1.0.0 * * @param array $theme An array of API request values. * @return array A normalized array of values. */ public function normalize_theme( $theme ) { $normalized_theme = array( 'id' => $theme['id'], 'name' => ( ! empty( $theme['wordpress_theme_metadata']['theme_name'] ) ? $theme['wordpress_theme_metadata']['theme_name'] : '' ), 'author' => ( ! empty( $theme['wordpress_theme_metadata']['author_name'] ) ? $theme['wordpress_theme_metadata']['author_name'] : '' ), 'version' => ( ! empty( $theme['wordpress_theme_metadata']['version'] ) ? $theme['wordpress_theme_metadata']['version'] : '' ), 'description' => self::remove_non_unicode( strip_tags( $theme['wordpress_theme_metadata']['description'] ) ), 'url' => ( ! empty( $theme['url'] ) ? $theme['url'] : '' ), 'author_url' => ( ! empty( $theme['author_url'] ) ? $theme['author_url'] : '' ), 'thumbnail_url' => ( ! empty( $theme['thumbnail_url'] ) ? $theme['thumbnail_url'] : '' ), 'rating' => ( ! empty( $theme['rating'] ) ? $theme['rating'] : '' ), 'landscape_url' => '', ); // No main thumbnail in API response, so we grab it from the preview array. if ( empty( $normalized_theme['thumbnail_url'] ) && ! empty( $theme['previews'] ) && is_array( $theme['previews'] ) ) { foreach ( $theme['previews'] as $possible_preview ) { if ( ! empty( $possible_preview['landscape_url'] ) ) { $normalized_theme['landscape_url'] = $possible_preview['landscape_url']; break; } } } if ( empty( $normalized_theme['thumbnail_url'] ) && ! empty( $theme['previews'] ) && is_array( $theme['previews'] ) ) { foreach ( $theme['previews'] as $possible_preview ) { if ( ! empty( $possible_preview['icon_url'] ) ) { $normalized_theme['thumbnail_url'] = $possible_preview['icon_url']; break; } } } return $normalized_theme; } /** * Get the list of available plugins. * * @since 1.0.0 * * @param array $args The arguments passed to `wp_remote_get`. * @return array The HTTP response. */ public function plugins( $args = array() ) { $plugins = array(); $domain = envato_market()->get_envato_api_domain(); $path = $this->api_path_for('list-purchases'); $url = $domain . $path . '?filter_by=wordpress-plugins'; $response = $this->request( $url, $args ); if ( is_wp_error( $response ) || empty( $response ) || empty( $response['results'] ) ) { return $plugins; } foreach ( $response['results'] as $plugin ) { $plugins[] = $this->normalize_plugin( $plugin['item'] ); } return $plugins; } /** * Normalize a plugin. * * @since 1.0.0 * * @param array $plugin An array of API request values. * @return array A normalized array of values. */ public function normalize_plugin( $plugin ) { $requires = null; $tested = null; $versions = array(); // Set the required and tested WordPress version numbers. foreach ( $plugin['attributes'] as $k => $v ) { if ( ! empty( $v['name'] ) && 'compatible-software' === $v['name'] && ! empty( $v['value'] ) && is_array( $v['value'] ) ) { foreach ( $v['value'] as $version ) { $versions[] = str_replace( 'WordPress ', '', trim( $version ) ); } if ( ! empty( $versions ) ) { $requires = $versions[ count( $versions ) - 1 ]; $tested = $versions[0]; } break; } } $plugin_normalized = array( 'id' => $plugin['id'], 'name' => ( ! empty( $plugin['wordpress_plugin_metadata']['plugin_name'] ) ? $plugin['wordpress_plugin_metadata']['plugin_name'] : '' ), 'author' => ( ! empty( $plugin['wordpress_plugin_metadata']['author'] ) ? $plugin['wordpress_plugin_metadata']['author'] : '' ), 'version' => ( ! empty( $plugin['wordpress_plugin_metadata']['version'] ) ? $plugin['wordpress_plugin_metadata']['version'] : '' ), 'description' => self::remove_non_unicode( strip_tags( $plugin['wordpress_plugin_metadata']['description'] ) ), 'url' => ( ! empty( $plugin['url'] ) ? $plugin['url'] : '' ), 'author_url' => ( ! empty( $plugin['author_url'] ) ? $plugin['author_url'] : '' ), 'thumbnail_url' => ( ! empty( $plugin['thumbnail_url'] ) ? $plugin['thumbnail_url'] : '' ), 'landscape_url' => ( ! empty( $plugin['previews']['landscape_preview']['landscape_url'] ) ? $plugin['previews']['landscape_preview']['landscape_url'] : '' ), 'requires' => $requires, 'tested' => $tested, 'number_of_sales' => ( ! empty( $plugin['number_of_sales'] ) ? $plugin['number_of_sales'] : '' ), 'updated_at' => ( ! empty( $plugin['updated_at'] ) ? $plugin['updated_at'] : '' ), 'rating' => ( ! empty( $plugin['rating'] ) ? $plugin['rating'] : '' ), ); // No main thumbnail in API response, so we grab it from the preview array. if ( empty( $plugin_normalized['landscape_url'] ) && ! empty( $plugin['previews'] ) && is_array( $plugin['previews'] ) ) { foreach ( $plugin['previews'] as $possible_preview ) { if ( ! empty( $possible_preview['landscape_url'] ) ) { $plugin_normalized['landscape_url'] = $possible_preview['landscape_url']; break; } } } if ( empty( $plugin_normalized['thumbnail_url'] ) && ! empty( $plugin['previews'] ) && is_array( $plugin['previews'] ) ) { foreach ( $plugin['previews'] as $possible_preview ) { if ( ! empty( $possible_preview['icon_url'] ) ) { $plugin_normalized['thumbnail_url'] = $possible_preview['icon_url']; break; } } } return $plugin_normalized; } public function api_path_for( $path ) { if ( defined('ENVATO_LOCAL_DEVELOPMENT') ) { $paths = MONOLITH_API_PATHS; } else { $paths = array( 'download' => '/v2/market/buyer/download', 'catalog-item' => '/v2/market/catalog/item', 'list-purchases' => '/v2/market/buyer/list-purchases', 'total-items' => '/v1/market/total-items.json' ); } return $paths[$path]; } /** * Remove all non unicode characters in a string * * @since 1.0.0 * * @param string $retval The string to fix. * @return string */ static private function remove_non_unicode( $retval ) { return preg_replace( '/[\x00-\x1F\x80-\xFF]/', '', $retval ); } private function request_headers() { $user_agent = array('User-Agent' => 'WordPress - Envato Market ' . envato_market()->get_version()); $headers = array_merge($user_agent, envato_market()->get_envato_api_headers()); return $headers; } } endif; patterns/page-sidebar-poster.php 0000644 00000006543 14720703302 0012760 0 ustar 00 <?php /** * Poster with right sidebar block pattern */ return array( 'title' => __( 'Poster with right sidebar', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:columns {"align":"wide","style":{"spacing":{"blockGap":"5%"}}} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"70%"} --> <div class="wp-block-column" style="flex-basis:70%"> <!-- wp:heading {"level":1,"align":"wide","style":{"typography":{"fontSize":"clamp(3rem, 6vw, 4.5rem)"},"spacing":{"margin":{"bottom":"0px"}}}} --> <h1 class="alignwide" style="font-size:clamp(3rem, 6vw, 4.5rem);margin-bottom:0px">' . wp_kses_post( __( '<em>Flutter</em>, a collection of bird-related ephemera', 'twentytwentytwo' ) ) . '</h1> <!-- /wp:heading --></div> <!-- /wp:column --> <!-- wp:column {"width":""} --> <div class="wp-block-column"></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:columns {"align":"wide","style":{"spacing":{"blockGap":"5%"}}} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"70%","style":{"spacing":{"padding":{"bottom":"32px"}}}} --> <div class="wp-block-column" style="padding-bottom:32px;flex-basis:70%"><!-- wp:image {"width":984,"height":1426,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-salmon.jpg" alt="' . esc_attr__( 'Image of a bird on a branch', 'twentytwentytwo' ) . '" width="984" height="1426"/></figure> <!-- /wp:image --></div> <!-- /wp:column --> <!-- wp:column {"width":""} --> <div class="wp-block-column"><!-- wp:image {"width":100,"height":47,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/icon-binoculars.png" alt="' . esc_attr__( 'An icon representing binoculars.', 'twentytwentytwo' ) . '" width="100" height="47"/></figure> <!-- /wp:image --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:heading {"level":3,"fontSize":"large"} --> <h3 class="has-large-font-size"><em>' . esc_html__( 'Date', 'twentytwentytwo' ) . '</em></h3> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . esc_html__( 'February, 12 2021', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:heading {"level":3,"fontSize":"large"} --> <h3 class="has-large-font-size"><em>' . esc_html__( 'Location', 'twentytwentytwo' ) . '</em></h3> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . wp_kses_post( __( 'The Grand Theater<br>154 Eastern Avenue<br>Maryland NY, 12345', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/header-centered-logo-black-background.php 0000644 00000002463 14720703302 0016264 0 ustar 00 <?php /** * Header with centered logo and black background */ return array( 'title' => __( 'Header with centered logo and background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--small, 1.25rem)","top":"var(--wp--custom--spacing--small, 1.25rem)"}},"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"foreground","textColor":"background","layout":{"type":"flex","justifyContent":"center"}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:navigation-link {"isTopLevelLink":true} /--> <!-- wp:navigation-link {"isTopLevelLink":true} /--> <!-- wp:site-logo {"width":90} /--> <!-- wp:navigation-link {"isTopLevelLink":true} /--> <!-- wp:navigation-link {"isTopLevelLink":true} /--> <!-- /wp:navigation --></div> <!-- /wp:group -->', ); patterns/footer-query-title-citation.php 0000644 00000004200 14720703302 0014477 0 ustar 00 <?php /** * Footer with query, title, and citation */ return array( 'title' => __( 'Footer with query, title, and citation', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}},"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"primary","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background has-link-color" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:query {"query":{"perPage":3,"pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"exclude","inherit":false},"displayLayout":{"type":"flex","columns":3},"align":"wide"} --> <div class="wp-block-query alignwide"><!-- wp:post-template --> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"isLink":true} /--> <!-- /wp:post-template --></div> <!-- /wp:query --> <!-- wp:spacer --> <div style="height:100px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:site-title {"level":0} /--> <!-- wp:group {"layout":{"type":"flex","justifyContent":"right"}} --> <div class="wp-block-group"> <!-- wp:paragraph --> <p>' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/footer-navigation-copyright.php 0000644 00000002327 14720703302 0014560 0 ustar 00 <?php /** * Footer with navigation and copyright */ return array( 'title' => __( 'Footer with navigation and copyright', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"center"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --> <!-- wp:spacer {"height":50} --> <div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"16px"}}} --> <p class="has-text-align-center" style="font-size:16px">' . esc_html__( '© Site Title', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-logo-navigation-offset-tagline.php 0000644 00000003333 14720703302 0016345 0 ustar 00 <?php /** * Logo, navigation, and offset tagline Header block pattern */ return array( 'title' => __( 'Logo, navigation, and offset tagline Header', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--large, 8rem)"}}}} --> <div class="wp-block-group alignwide" style="padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:site-logo {"width":64} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --> <!-- wp:columns {"isStackedOnMobile":false,"align":"wide"} --> <div class="wp-block-columns alignwide is-not-stacked-on-mobile"><!-- wp:column {"width":"64px"} --> <div class="wp-block-column" style="flex-basis:64px"></div> <!-- /wp:column --> <!-- wp:column {"width":"380px"} --> <div class="wp-block-column" style="flex-basis:380px"><!-- wp:site-tagline {"fontSize":"small"} /--></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/page-sidebar-blog-posts-right.php 0000644 00000012123 14720703302 0014637 0 ustar 00 <?php /** * Blog posts with right sidebar block pattern */ return array( 'title' => __( 'Blog posts with right sidebar', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"2rem","top":"0px","right":"0px","left":"0px"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:0px;padding-right:0px;padding-bottom:2rem;padding-left:0px"><!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:site-logo {"width":64} /--></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:group --> <!-- wp:spacer {"height":64} --> <div style="height:64px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:columns {"align":"wide","style":{"spacing":{"margin":{"top":"0px","bottom":"0px"},"blockGap":"5%"},"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}}},"textColor":"foreground"} --> <div class="wp-block-columns alignwide has-foreground-color has-text-color has-link-color" style="margin-top:0px;margin-bottom:0px"><!-- wp:column {"width":"66.66%","style":{"spacing":{"padding":{"bottom":"6rem"}}}} --> <div class="wp-block-column" style="padding-bottom:6rem;flex-basis:66.66%"><!-- wp:query {"queryId":9,"query":{"perPage":"5","pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":false},"displayLayout":{"type":"list"},"layout":{"inherit":true}} --> <div class="wp-block-query"><!-- wp:post-template --> <!-- wp:post-title {"isLink":true,"style":{"spacing":{"margin":{"top":"0","bottom":"1rem"}},"typography":{"fontStyle":"normal","fontWeight":"300"},"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}}},"textColor":"foreground","fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))"} /--> <!-- wp:post-featured-image {"isLink":true} /--> <!-- wp:post-excerpt /--> <!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:post-date {"format":"F j, Y","style":{"typography":{"fontStyle":"normal","fontWeight":"400"}},"fontSize":"small"} /--> <!-- wp:post-terms {"term":"category","fontSize":"small"} /--> <!-- wp:post-terms {"term":"post_tag","fontSize":"small"} /--></div> <!-- /wp:group --> <!-- wp:spacer {"height":64} --> <div style="height:64px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- /wp:post-template --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column {"width":"33.33%"} --> <div class="wp-block-column" style="flex-basis:33.33%"><!-- wp:image {"width":768,"height":1160,"sizeSlug":"large","linkDestination":"none"} --> <figure class="wp-block-image size-large is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-salmon.jpg" alt="' . esc_attr__( 'Illustration of a flying bird.', 'twentytwentytwo' ) . '" width="768" height="1160"/></figure> <!-- /wp:image --> <!-- wp:spacer {"height":4} --> <div style="height:4px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-title {"isLink":false,"style":{"typography":{"fontStyle":"normal","fontWeight":"300","lineHeight":"1.2"}},"fontSize":"large","fontFamily":"source-serif-pro"} /--> <!-- wp:site-tagline /--> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:heading {"level":4,"fontSize":"large"} --> <h4 class="has-large-font-size"><em>' . esc_html__( 'Categories', 'twentytwentytwo' ) . '</em></h4> <!-- /wp:heading --> <!-- wp:tag-cloud {"taxonomy":"category","showTagCounts":true} /--> <!-- wp:heading {"level":4,"fontSize":"large"} --> <h4 class="has-large-font-size"><em>' . esc_html__( 'Tags', 'twentytwentytwo' ) . '</em></h4> <!-- /wp:heading --> <!-- wp:tag-cloud {"showTagCounts":true} /--></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/header-stacked.php 0000644 00000002721 14720703302 0011761 0 ustar 00 <?php /** * Logo and navigation header block pattern */ return array( 'title' => __( 'Logo and navigation header', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--large, 8rem)","top":"var(--wp--custom--spacing--small, 1.25rem)"}}}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:site-logo {"align":"center","width":128} /--> <!-- wp:spacer {"height":10} --> <div style="height:10px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-title {"textAlign":"center","style":{"typography":{"fontStyle":"normal","fontWeight":"400","textTransform":"uppercase"}}} /--> <!-- wp:spacer {"height":10} --> <div style="height:10px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"center"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-default.php 0000644 00000002401 14720703302 0011762 0 ustar 00 <?php /** * Default header block pattern */ return array( 'title' => __( 'Default header', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--large, 8rem)","top":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"> <!-- wp:site-logo {"width":64} /--> <!-- wp:site-title {"style":{"typography":{"fontStyle":"italic","fontWeight":"400"}}} /--></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/general-list-events.php 0000644 00000017625 14720703302 0013016 0 ustar 00 <?php /** * List of events block pattern */ return array( 'title' => __( 'List of events', 'twentytwentytwo' ), 'categories' => array( 'featured', 'text' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}},"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"primary","textColor":"background"} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:heading {"align":"wide","style":{"typography":{"fontSize":"clamp(3.25rem, 8vw, 6.25rem)","lineHeight":"1.15"},"spacing":{"margin":{"bottom":"2rem"}}}} --> <h2 class="alignwide" style="font-size:clamp(3.25rem, 8vw, 6.25rem);line-height:1.15;margin-bottom:2rem"><em>' . esc_html__( 'Speaker Series', 'twentytwentytwo' ) . '</em></h2> <!-- /wp:heading --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:separator {"color":"background","align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide has-text-color has-background has-background-background-color has-background-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center","width":"210px"} --> <div class="wp-block-column is-vertically-aligned-center" style="flex-basis:210px"><!-- wp:paragraph --> <p>' . esc_html__( 'May 14th, 2022, 6 PM', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size" id="jesus-rodriguez">' . esc_html__( 'Jesús Rodriguez', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'The Vintagé Theater<br>245 Arden Rd.<br>Gardenville, NH', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:separator {"color":"background","align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide has-text-color has-background has-background-background-color has-background-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center","width":"210px"} --> <div class="wp-block-column is-vertically-aligned-center" style="flex-basis:210px"><!-- wp:paragraph --> <p>' . esc_html__( 'May 16th, 2022, 6 PM', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size" id="jesus-rodriguez">' . esc_html__( 'Doug Stilton', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'The Swell Theater<br>120 River Rd.<br>Rainfall, NH', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:separator {"color":"background","align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide has-text-color has-background has-background-background-color has-background-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center","width":"210px"} --> <div class="wp-block-column is-vertically-aligned-center" style="flex-basis:210px"><!-- wp:paragraph --> <p>' . esc_html__( 'May 18th, 2022, 7 PM', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size" id="jesus-rodriguez">' . esc_html__( 'Amy Jensen', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'The Vintagé Theater<br>245 Arden Rd.<br>Gardenville, NH', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:separator {"color":"background","align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide has-text-color has-background has-background-background-color has-background-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center","width":"210px"} --> <div class="wp-block-column is-vertically-aligned-center" style="flex-basis:210px"><!-- wp:paragraph --> <p>' . esc_html__( 'May 20th, 2022, 6 PM', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size" id="jesus-rodriguez">' . esc_html__( 'Emery Driscoll', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'The Swell Theater<br>120 River Rd.<br>Rainfall, NH', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:separator {"color":"background","align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide has-text-color has-background has-background-background-color has-background-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:group {"align":"wide"} --> <div class="wp-block-group alignwide"><!-- wp:social-links {"iconColor":"background","iconColorValue":"var(--wp--preset--color--background)","className":"is-style-logos-only","layout":{"type":"flex","justifyContent":"right"}} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:group --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/query-grid.php 0000644 00000002556 14720703302 0011213 0 ustar 00 <?php /** * Grid of posts block pattern */ return array( 'title' => __( 'Grid of posts', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:query {"query":{"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":12},"displayLayout":{"type":"flex","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --> <!-- wp:separator {"align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide is-style-wide"/> <!-- /wp:separator --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query -->', ); patterns/page-layout-two-columns.php 0000644 00000007667 14720703302 0013647 0 ustar 00 <?php /** * Page layout with two columns. */ return array( 'title' => __( 'Page layout with two columns', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:var(--wp--custom--spacing--large, 8rem);"><!-- wp:heading {"level":1,"align":"wide","style":{"typography":{"fontSize":"clamp(4rem, 15vw, 12.5rem)","lineHeight":"1","fontWeight":"200"}}} --> <h1 class="alignwide" style="font-size:clamp(4rem, 15vw, 12.5rem);font-weight:200;line-height:1">' . wp_kses_post( __( '<em>Goldfinch </em><br><em>& Sparrow</em>', 'twentytwentytwo' ) ) . '</h1> <!-- /wp:heading --> <!-- wp:spacer {"height":50} --> <div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:group {"align":"wide","layout":{"inherit":false}} --> <div class="wp-block-group alignwide"><!-- wp:columns --> <div class="wp-block-columns"><!-- wp:column {"verticalAlignment":"center","width":"20%"} --> <div class="wp-block-column is-vertically-aligned-center" style="flex-basis:20%"><!-- wp:paragraph --> <p>' . esc_html__( 'WELCOME', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center","width":"80%"} --> <div class="wp-block-column is-vertically-aligned-center" style="flex-basis:80%"><!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator is-style-wide"/> <!-- /wp:separator --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'Oh hello. My name’s Angelo, and I operate this blog. I was born in Portland, but I currently live in upstate New York. You may recognize me from publications with names like <a href="#">Eagle Beagle</a> and <a href="#">Mourning Dive</a>. I write for a living.<br><br>I usually use this blog to catalog extensive lists of birds and other things that I find interesting. If you find an error with one of my lists, please keep it to yourself.<br><br>If that’s not your cup of tea, <a href="#">I definitely recommend this tea</a>. It’s my favorite.', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:spacer --> <div style="height:100px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator is-style-wide"/> <!-- /wp:separator --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:paragraph --> <p>' . esc_html__( 'POSTS', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:latest-posts /--></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/query-simple-blog.php 0000644 00000003534 14720703302 0012475 0 ustar 00 <?php /** * Simple blog posts block pattern */ return array( 'title' => __( 'Simple blog posts', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:query {"query":{"perPage":3,"pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":true,"perPage":10},"layout":{"inherit":true}} --> <div class="wp-block-query"><!-- wp:post-template --> <!-- wp:post-title {"isLink":true,"style":{"spacing":{"margin":{"top":"1rem","bottom":"1rem"}},"typography":{"fontStyle":"normal","fontWeight":"300"},"elements":{"link":{"color":{"text":"var:preset|color|primary"}}}},"textColor":"primary","fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))"} /--> <!-- wp:post-featured-image {"isLink":true} /--> <!-- wp:post-excerpt /--> <!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:post-date {"format":"F j, Y","style":{"typography":{"fontStyle":"normal","fontWeight":"400"}},"fontSize":"small"} /--> <!-- wp:post-terms {"term":"category","fontSize":"small"} /--> <!-- wp:post-terms {"term":"post_tag","fontSize":"small"} /--></div> <!-- /wp:group --> <!-- wp:spacer {"height":128} --> <div style="height:128px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- /wp:post-template --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query -->', ); patterns/header-centered-title-navigation-social.php 0000644 00000004267 14720703302 0016667 0 ustar 00 <?php /** * Centered header with navigation, social links, and salmon background block pattern */ return array( 'title' => __( 'Centered header with navigation, social links, and background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|primary"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"secondary","textColor":"primary","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-primary-color has-secondary-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem);"><!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"left"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:column --> <!-- wp:column {"width":""} --> <div class="wp-block-column"><!-- wp:site-title {"textAlign":"center","style":{"typography":{"textTransform":"uppercase","fontStyle":"normal","fontWeight":"700"}}} /--></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:social-links {"iconColor":"primary","iconColorValue":"var(--wp--custom--color--primary)","className":"is-style-logos-only","layout":{"type":"flex","justifyContent":"right"}} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/query-irregular-grid.php 0000644 00000020546 14720703302 0013204 0 ustar 00 <?php /** * Irregular grid of posts block pattern */ return array( 'title' => __( 'Irregular grid of posts', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:group {"align":"wide"} --> <div class="wp-block-group alignwide"><!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"1","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":64} --> <div style="height:64px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"2","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":128} --> <div style="height:128px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"3","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"4","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":96} --> <div style="height:96px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"5","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":160} --> <div style="height:160px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"6","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"7","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":160} --> <div style="height:160px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:query {"query":{"offset":"8","postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":"1"},"displayLayout":{"type":"list","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:spacer {"height":96} --> <div style="height:96px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/header-text-only-green-background.php 0000644 00000003116 14720703302 0015520 0 ustar 00 <?php /** * Text-only header with green background block pattern */ return array( 'title' => __( 'Text-only header with background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"primary","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide"><!-- wp:group --> <div class="wp-block-group"><!-- wp:site-title {"style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}},"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:site-tagline {"style":{"spacing":{"margin":{"top":"0.25em","bottom":"0px"}},"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/hidden-404.php 0000644 00000001741 14720703302 0010656 0 ustar 00 <?php /** * 404 content. */ return array( 'title' => __( '404 content', 'twentytwentytwo' ), 'inserter' => false, 'content' => '<!-- wp:heading {"style":{"typography":{"fontSize":"clamp(4rem, 40vw, 20rem)","fontWeight":"200","lineHeight":"1"}},"className":"has-text-align-center"} --> <h2 class="has-text-align-center" style="font-size:clamp(4rem, 40vw, 20rem);font-weight:200;line-height:1">' . esc_html( _x( '404', 'Error code for a webpage that is not found.', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --> <!-- wp:paragraph {"align":"center"} --> <p class="has-text-align-center">' . esc_html__( 'This page could not be found. Maybe try a search?', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:search {"label":"' . esc_html_x( 'Search', 'label', 'twentytwentytwo' ) . '","showLabel":false,"width":50,"widthUnit":"%","buttonText":"' . esc_html__( 'Search', 'twentytwentytwo' ) . '","buttonUseIcon":true,"align":"center"} /-->', ); patterns/page-about-media-right.php 0000644 00000005551 14720703302 0013335 0 ustar 00 <?php /** * About page with media on the right */ return array( 'title' => __( 'About page with media on the right', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:media-text {"align":"full","mediaPosition":"right","mediaLink":"' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-black.jpg","mediaType":"image","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"foreground","textColor":"background"} --> <div class="wp-block-media-text alignfull has-media-on-the-right is-stacked-on-mobile has-background-color has-foreground-background-color has-text-color has-background has-link-color"><figure class="wp-block-media-text__media"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-black.jpg" alt="' . esc_attr__( 'An image of a bird flying', 'twentytwentytwo' ) . '"/></figure><div class="wp-block-media-text__content"><!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-logo {"width":60} /--> <!-- wp:group {"style":{"spacing":{"padding":{"right":"min(8rem, 5vw)","top":"min(20rem, 20vw)"}}}} --> <div class="wp-block-group" style="padding-top:min(20rem, 20vw);padding-right:min(8rem, 5vw)"><!-- wp:heading {"style":{"typography":{"fontWeight":"300","lineHeight":"1.115","fontSize":"clamp(3rem, 6vw, 4.5rem)"}}} --> <h2 style="font-size:clamp(3rem, 6vw, 4.5rem);font-weight:300;line-height:1.115"><em>' . wp_kses_post( __( 'Emery<br>Driscoll', 'twentytwentytwo' ) ) . '</em></h2> <!-- /wp:heading --> <!-- wp:paragraph {"style":{"typography":{"lineHeight":"1.6"}}} --> <p style="line-height:1.6">' . esc_html__( 'Oh hello. My name’s Emery, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show on Tuesday evenings at 11PM EDT.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:social-links {"iconColor":"background","iconColorValue":"var(--wp--preset--color--foreground)","iconBackgroundColor":"foreground","iconBackgroundColorValue":"var(--wp--preset--color--background)"} --> <ul class="wp-block-social-links has-icon-color has-icon-background-color"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:group --></div> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:media-text -->', ); patterns/page-about-links-dark.php 0000644 00000007131 14720703302 0013176 0 ustar 00 <?php /** * About page links (dark) */ return array( 'title' => __( 'About page links (dark)', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages', 'buttons' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"10rem","bottom":"10rem"}}},"backgroundColor":"primary","textColor":"background","layout":{"inherit":false,"contentSize":"400px"}} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background has-link-color" style="padding-top:10rem;padding-bottom:10rem;"><!-- wp:group --> <div class="wp-block-group"> <!-- wp:image {"width":100,"height":100,"sizeSlug":"full","linkDestination":"none","className":"is-style-rounded"} --> <figure class="wp-block-image size-full is-resized is-style-rounded"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/icon-bird.jpg" alt="' . esc_attr__( 'Logo featuring a flying bird', 'twentytwentytwo' ) . '" width="100" height="100"/></figure> <!-- /wp:image --> <!-- wp:heading {"textAlign":"left","style":{"typography":{"fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))"}}} --> <h2 class="has-text-align-left" style="font-size:var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))">' . esc_html__( 'A trouble of hummingbirds', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:buttons {"contentJustification":"left"} --> <div class="wp-block-buttons is-content-justification-left"><!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-outline"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-outline"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Watch our videos', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-outline"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-outline"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Listen on iTunes Podcasts', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-outline"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-outline"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Listen on Spotify', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-outline"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-outline"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Support the show', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-outline"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-outline"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'About the hosts', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/general-subscribe.php 0000644 00000002715 14720703302 0012514 0 ustar 00 <?php /** * Subscribe callout block pattern */ return array( 'title' => __( 'Subscribe callout', 'twentytwentytwo' ), 'categories' => array( 'featured', 'buttons' ), 'content' => '<!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:heading --> <h2>' . wp_kses_post( __( 'Watch birds<br>from your inbox', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"fontSize":"medium"} --> <div class="wp-block-button has-custom-font-size has-medium-font-size"><a class="wp-block-button__link">' . esc_html__( 'Join our mailing list', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center","style":{"spacing":{"padding":{"top":"2rem","bottom":"2rem"}}}} --> <div class="wp-block-column is-vertically-aligned-center" style="padding-top:2rem;padding-bottom:2rem"><!-- wp:separator {"color":"primary","className":"is-style-wide"} --> <hr class="wp-block-separator has-text-color has-background has-primary-background-color has-primary-color is-style-wide"/> <!-- /wp:separator --></div> <!-- /wp:column --></div> <!-- /wp:columns -->', ); patterns/general-wide-image-intro-buttons.php 0000644 00000004625 14720703302 0015372 0 ustar 00 <?php /** * Wide image with introduction and buttons block pattern */ return array( 'title' => __( 'Wide image with introduction and buttons', 'twentytwentytwo' ), 'categories' => array( 'featured', 'columns' ), 'content' => '<!-- wp:group {"align":"wide"} --> <div class="wp-block-group alignwide"><!-- wp:image {"width":2100,"height":994,"sizeSlug":"large"} --> <figure class="wp-block-image size-large is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-gray-a.jpg" alt="' . esc_attr__( 'Illustration of a bird flying.', 'twentytwentytwo' ) . '" width="2100" height="994"/></figure> <!-- /wp:image --> <!-- wp:columns {"verticalAlignment":null} --> <div class="wp-block-columns"><!-- wp:column {"verticalAlignment":"bottom"} --> <div class="wp-block-column is-vertically-aligned-bottom"><!-- wp:heading {"style":{"typography":{"fontSize":"clamp(3.25rem, 8vw, 6.25rem)","lineHeight":"1.15"}}} --> <h2 style="font-size:clamp(3.25rem, 8vw, 6.25rem);line-height:1.15"><em>' . wp_kses_post( __( 'Welcome to<br>the Aviary', 'twentytwentytwo' ) ) . '</em></h2> <!-- /wp:heading --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"bottom","style":{"spacing":{"padding":{"bottom":"6rem"}}}} --> <div class="wp-block-column is-vertically-aligned-bottom" style="padding-bottom:6rem"><!-- wp:paragraph --> <p>' . esc_html__( 'A film about hobbyist bird watchers, a catalog of different birds, paired with the noises they make. Each bird is listed by their scientific name so things seem more official.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":20} --> <div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"className":"is-style-outline"} --> <div class="wp-block-button is-style-outline"><a class="wp-block-button__link">' . esc_html__( 'Learn More', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"className":"is-style-outline"} --> <div class="wp-block-button is-style-outline"><a class="wp-block-button__link">' . esc_html__( 'Buy Tickets', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/header-text-only-salmon-background.php 0000644 00000002645 14720703302 0015717 0 ustar 00 <?php /** * Text-only header with salmon background block pattern */ return array( 'title' => __( 'Text-only header with background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"secondary","textColor":"foreground","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-foreground-color has-secondary-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide"><!-- wp:site-title {"style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}},"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-centered-logo.php 0000644 00000003505 14720703302 0013073 0 ustar 00 <?php /** * Header with centered logo block pattern */ return array( 'title' => __( 'Header with centered logo', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"primary","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem);"><!-- wp:columns {"verticalAlignment":"center","align":"wide"} --> <div class="wp-block-columns alignwide are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:site-title {"style":{"typography":{"fontStyle":"normal","fontWeight":"400","textTransform":"uppercase"}}} /--></div> <!-- /wp:column --> <!-- wp:column {"width":"64px"} --> <div class="wp-block-column" style="flex-basis:64px"><!-- wp:site-logo {"width":64} /--></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center"} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/footer-social-copyright.php 0000644 00000003034 14720703302 0013667 0 ustar 00 <?php /** * Footer with social links and copyright */ return array( 'title' => __( 'Footer with social links and copyright', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:social-links {"iconColor":"foreground","iconColorValue":"var(--wp--preset--color--foreground)","iconBackgroundColor":"background","iconBackgroundColorValue":"var(--wp--preset--color--background)","layout":{"type":"flex","justifyContent":"center"}} --> <ul class="wp-block-social-links has-icon-color has-icon-background-color"><!-- wp:social-link {"url":"#","service":"facebook"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --> <!-- wp:spacer {"height":50} --> <div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"16px"}}} --> <p class="has-text-align-center" style="font-size:16px">' . esc_html__( '© Site Title', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/footer-title-tagline-social.php 0000644 00000003427 14720703302 0014427 0 ustar 00 <?php /** * Footer with title, tagline, and social links on a dark background */ return array( 'title' => __( 'Footer with title, tagline, and social links on a dark background', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"foreground","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:group --> <div class="wp-block-group"><!-- wp:site-title {"style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}},"typography":{"textTransform":"uppercase"}}} /--> <!-- wp:site-tagline {"style":{"spacing":{"margin":{"top":"0.25em","bottom":"0px"}},"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:group --> <!-- wp:social-links {"iconBackgroundColor":"foreground","iconBackgroundColorValue":"var(--wp--preset--color--foreground)","layout":{"type":"flex","justifyContent":"right"}} --> <ul class="wp-block-social-links has-icon-background-color"><!-- wp:social-link {"url":"#","service":"facebook"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/page-sidebar-grid-posts.php 0000644 00000010137 14720703302 0013531 0 ustar 00 <?php /** * Grid of posts with left sidebar block pattern */ return array( 'title' => __( 'Grid of posts with left sidebar', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:columns {"align":"wide","style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}}}} --> <div class="wp-block-columns alignwide" style="margin-top:0px;margin-bottom:0px"><!-- wp:column {"width":"30%"} --> <div class="wp-block-column" style="flex-basis:30%"><!-- wp:site-title {"isLink":false,"style":{"spacing":{"margin":{"top":"0px","bottom":"1rem"}},"typography":{"fontStyle":"italic","fontWeight":"300","lineHeight":"1.1"}},"fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))","fontFamily":"source-serif-pro"} /--> <!-- wp:site-tagline {"fontSize":"small"} /--> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:separator {"color":"foreground","className":"is-style-wide"} --> <hr class="wp-block-separator has-text-color has-background has-foreground-background-color has-foreground-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:navigation {"orientation":"vertical"} --> <!-- wp:page-list /--> <!-- /wp:navigation --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:separator {"color":"foreground","className":"is-style-wide"} --> <hr class="wp-block-separator has-text-color has-background has-foreground-background-color has-foreground-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-logo {"width":60} /--></div> <!-- /wp:column --> <!-- wp:column {"width":"70%"} --> <div class="wp-block-column" style="flex-basis:70%"><!-- wp:query {"query":{"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","inherit":false,"perPage":12},"displayLayout":{"type":"flex","columns":3},"layout":{"inherit":true}} --> <div class="wp-block-query"><!-- wp:post-template --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"200px"} /--> <!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group"><!-- wp:post-title {"isLink":true,"style":{"typography":{"fontStyle":"normal","fontWeight":"400"}},"fontSize":"small","fontFamily":"system-font"} /--> <!-- wp:post-date {"format":"m.d.y","style":{"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:group --> <!-- /wp:post-template --> <!-- wp:separator {"className":"alignwide is-style-wide"} --> <hr class="wp-block-separator alignwide is-style-wide"/> <!-- /wp:separator --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/query-image-grid.php 0000644 00000003573 14720703302 0012273 0 ustar 00 <?php /** * Grid of image posts block pattern */ return array( 'title' => __( 'Grid of image posts', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:query {"query":{"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","inherit":false,"perPage":12},"displayLayout":{"type":"flex","columns":3},"layout":{"inherit":true}} --> <div class="wp-block-query"><!-- wp:post-template --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"200px"} /--> <!-- wp:columns {"isStackedOnMobile":false,"style":{"spacing":{"blockGap":"0.5rem"}}} --> <div class="wp-block-columns is-not-stacked-on-mobile"><!-- wp:column --> <div class="wp-block-column"><!-- wp:post-title {"isLink":true,"style":{"typography":{"fontStyle":"normal","fontWeight":"400"},"spacing":{"margin":{"top":"0.2em"}}},"fontSize":"small","fontFamily":"system-font"} /--></div> <!-- /wp:column --> <!-- wp:column {"width":"4em"} --> <div class="wp-block-column" style="flex-basis:4em"><!-- wp:post-date {"textAlign":"right","format":"m.d.y","style":{"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- /wp:post-template --> <!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator alignwide is-style-wide"/> <!-- /wp:separator --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query -->', ); patterns/header-large-dark.php 0000644 00000005367 14720703302 0012365 0 ustar 00 <?php /** * Large header with dark background block pattern */ return array( 'title' => __( 'Large header with dark background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"0px","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"backgroundColor":"foreground","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:0px;padding-bottom:var(--wp--custom--spacing--large, 8rem);"><!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"0px","bottom":"0px"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:0px;padding-bottom:0px;"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:site-logo {"width":64} /--> <!-- wp:site-title {"style":{"typography":{"fontStyle":"italic","fontWeight":"400"}}} /--></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:group --> <!-- wp:heading {"align":"wide","style":{"typography":{"fontSize":"clamp(3.25rem, 8vw, 6.25rem)","lineHeight":"1.15"}}} --> <h2 class="alignwide" style="font-size:clamp(3.25rem, 8vw, 6.25rem);line-height:1.15">' . wp_kses_post( __( '<em>The Hatchery</em>: a blog about my adventures in bird watching', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:group --> <!-- wp:image {"align":"full","width":2400,"height":1020,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image alignfull size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-transparent-c.png" alt="' . esc_attr__( 'Illustration of a bird flying.', 'twentytwentytwo' ) . '" width="2400" height="1020"/></figure> <!-- /wp:image --></div> <!-- /wp:group --><!-- wp:spacer {"height":66} --> <div style="height:66px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer -->', ); patterns/header-image-background.php 0000644 00000004562 14720703302 0013547 0 ustar 00 <?php /** * Header with image background block pattern */ return array( 'title' => __( 'Header with image background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:cover {"url":"' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-gray-c.jpg","dimRatio":0,"focalPoint":{"x":"0.58","y":"0.58"},"minHeight":400,"contentPosition":"center center","align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}},"color":{}}} --> <div class="wp-block-cover alignfull" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem);min-height:400px"><span aria-hidden="true" class="has-background-dim-0 wp-block-cover__gradient-background has-background-dim"></span><img class="wp-block-cover__image-background" alt="' . esc_attr__( 'Illustration of a flying bird', 'twentytwentytwo' ) . '" src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-gray-c.jpg" style="object-position:58% 58%" data-object-fit="cover" data-object-position="58% 58%"/><div class="wp-block-cover__inner-container"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"0rem","top":"0px","right":"0px","left":"0px"}},"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}}},"textColor":"foreground","layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide has-foreground-color has-text-color has-link-color" style="padding-top:0px;padding-right:0px;padding-bottom:0rem;padding-left:0px"><!-- wp:site-title {"style":{"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --> <!-- wp:spacer {"height":500} --> <div style="height:500px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div></div> <!-- /wp:cover --></div> <!-- /wp:group -->', ); patterns/header-logo-navigation-gray-background.php 0000644 00000002630 14720703302 0016514 0 ustar 00 <?php /** * Logo and navigation header with gray background */ return array( 'title' => __( 'Logo and navigation header with background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"tertiary","textColor":"foreground","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-foreground-color has-tertiary-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"0rem","top":"0px","right":"0px","left":"0px"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:0px;padding-right:0px;padding-bottom:0rem;padding-left:0px"><!-- wp:site-logo {"width":64} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/page-about-simple-dark.php 0000644 00000006417 14720703302 0013355 0 ustar 00 <?php /** * Simple dark about page */ return array( 'title' => __( 'Simple dark about page', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:cover {"overlayColor":"foreground","minHeight":100,"minHeightUnit":"vh","contentPosition":"center center","align":"full","style":{"spacing":{"padding":{"top":"max(1.25rem, 8vw)","right":"max(1.25rem, 8vw)","bottom":"max(1.25rem, 8vw)","left":"max(1.25rem, 8vw)"}}}} --> <div class="wp-block-cover alignfull has-foreground-background-color has-background-dim" style="padding-top:max(1.25rem, 8vw);padding-right:max(1.25rem, 8vw);padding-bottom:max(1.25rem, 8vw);padding-left:max(1.25rem, 8vw);min-height:100vh"><div class="wp-block-cover__inner-container"><!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"},"overlayMenu":"always"} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --> <!-- wp:columns --> <div class="wp-block-columns"><!-- wp:column {"verticalAlignment":"bottom","width":"45%","style":{"spacing":{"padding":{"top":"12rem"}}}} --> <div class="wp-block-column is-vertically-aligned-bottom" style="padding-top:12rem;flex-basis:45%"><!-- wp:site-logo {"width":60} /--> <!-- wp:heading {"style":{"typography":{"fontWeight":"300","lineHeight":"1.115","fontSize":"clamp(3rem, 6vw, 4.5rem)"}}} --> <h2 style="font-size:clamp(3rem, 6vw, 4.5rem);font-weight:300;line-height:1.115"><em>' . wp_kses_post( __( 'Jesús<br>Rodriguez', 'twentytwentytwo' ) ) . '</em></h2> <!-- /wp:heading --> <!-- wp:paragraph {"style":{"typography":{"lineHeight":"1.6"}}} --> <p style="line-height:1.6">' . esc_html__( 'Oh hello. My name’s Jesús, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show on Tuesday evenings at 11PM EDT.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:social-links {"iconColor":"background","iconColorValue":"var(--wp--preset--color--foreground)","iconBackgroundColor":"foreground","iconBackgroundColorValue":"var(--wp--preset--color--background)"} --> <ul class="wp-block-social-links has-icon-color has-icon-background-color"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center","style":{"spacing":{"padding":{"top":"0rem","right":"0rem","bottom":"4rem","left":"0rem"}}}} --> <div class="wp-block-column is-vertically-aligned-center" style="padding-top:0rem;padding-right:0rem;padding-bottom:4rem;padding-left:0rem"><!-- wp:separator {"color":"background","className":"is-style-wide"} --> <hr class="wp-block-separator has-text-color has-background has-background-background-color has-background-color is-style-wide"/> <!-- /wp:separator --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div></div> <!-- /wp:cover -->', ); patterns/general-featured-posts.php 0000644 00000002162 14720703302 0013474 0 ustar 00 <?php /** * Featured posts block pattern */ return array( 'title' => __( 'Featured posts', 'twentytwentytwo' ), 'categories' => array( 'featured', 'query' ), 'content' => '<!-- wp:group {"align":"wide","layout":{"inherit":false}} --> <div class="wp-block-group alignwide"><!-- wp:paragraph {"style":{"typography":{"textTransform":"uppercase"}}} --> <p style="text-transform:uppercase">' . esc_html__( 'Latest posts', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:query {"query":{"perPage":3,"pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":false},"displayLayout":{"type":"flex","columns":3}} --> <div class="wp-block-query"><!-- wp:post-template --> <!-- wp:post-featured-image {"isLink":true,"width":"","height":"310px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --></div> <!-- /wp:group -->', ); patterns/header-image-background-overlay.php 0000644 00000004514 14720703302 0015223 0 ustar 00 <?php /** * Header with image background and overlay block pattern */ return array( 'title' => __( 'Header with image background and overlay', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:cover {"url":"' . esc_url( get_template_directory_uri() ) . '/assets/images/ducks.jpg","dimRatio":90,"overlayColor":"primary","focalPoint":{"x":"0.26","y":"0.34"},"minHeight":100,"minHeightUnit":"px","contentPosition":"center center","align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}},"color":{"duotone":["#000000","#ffffff"]}}} --> <div class="wp-block-cover alignfull" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem);min-height:100px"><span aria-hidden="true" class="has-primary-background-color has-background-dim-90 wp-block-cover__gradient-background has-background-dim"></span><img class="wp-block-cover__image-background" alt="' . esc_attr__( 'Painting of ducks in the water.', 'twentytwentytwo' ) . '" src="' . esc_url( get_template_directory_uri() ) . '/assets/images/ducks.jpg" style="object-position:26% 34%" data-object-fit="cover" data-object-position="26% 34%"/><div class="wp-block-cover__inner-container"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"0rem","top":"0px","right":"0px","left":"0px"}},"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"textColor":"background","layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide has-background-color has-text-color has-link-color" style="padding-top:0px;padding-right:0px;padding-bottom:0rem;padding-left:0px"><!-- wp:site-title {"style":{"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div></div> <!-- /wp:cover --></div> <!-- /wp:group -->', ); patterns/footer-logo.php 0000644 00000002035 14720703302 0011347 0 ustar 00 <?php /** * Default footer with logo */ return array( 'title' => __( 'Footer with logo and citation', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:site-logo {"width":60} /--> <!-- wp:paragraph {"align":"right"} --> <p class="has-text-align-right">' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-title-and-button.php 0000644 00000002300 14720703302 0013526 0 ustar 00 <?php /** * Title and button header block pattern */ return array( 'title' => __( 'Title and button header', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--large, 8rem)","top":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:site-title {"style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}},"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"},"overlayMenu":"always"} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/page-about-links.php 0000644 00000010344 14720703302 0012257 0 ustar 00 <?php /** * About page links */ return array( 'title' => __( 'About page links', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages', 'buttons' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"10rem","bottom":"10rem"}}},"layout":{"inherit":false,"contentSize":"400px"}} --> <div class="wp-block-group alignfull" style="padding-top:10rem;padding-bottom:10rem;"><!-- wp:image {"align":"center","width":100,"height":100,"sizeSlug":"full","linkDestination":"none","className":"is-style-rounded"} --> <div class="wp-block-image is-style-rounded"><figure class="aligncenter size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/icon-bird.jpg" alt="' . esc_attr__( 'Logo featuring a flying bird', 'twentytwentytwo' ) . '" width="100" height="100"/></figure></div> <!-- /wp:image --> <!-- wp:group --> <div class="wp-block-group"> <!-- wp:heading {"textAlign":"center","style":{"typography":{"fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))"}}} --> <h2 class="has-text-align-center" style="font-size:var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))">' . esc_html__( 'Swoop', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --> <!-- wp:paragraph {"align":"center"} --> <p class="has-text-align-center">' . esc_html__( 'A podcast about birds', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:buttons {"contentJustification":"left"} --> <div class="wp-block-buttons is-content-justification-left"><!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-fill"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-fill"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Watch our videos', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-fill"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-fill"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Listen on iTunes Podcasts', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-fill"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-fill"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Listen on Spotify', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-fill"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-fill"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'Support the show', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --> <!-- wp:button {"width":100,"style":{"border":{"radius":"6px"}},"className":"is-style-fill"} --> <div class="wp-block-button has-custom-width wp-block-button__width-100 is-style-fill"><a class="wp-block-button__link" style="border-radius:6px">' . esc_html__( 'About the hosts', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:group --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:social-links {"iconColor":"primary","iconColorValue":"var(--wp--preset--color--primary)","className":"is-style-logos-only","layout":{"type":"flex","justifyContent":"center"}} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"facebook"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:group -->', ); patterns/general-layered-images-with-duotone.php 0000644 00000002764 14720703302 0016053 0 ustar 00 <?php /** * Layered images with duotone block pattern */ return array( 'title' => __( 'Layered images with duotone', 'twentytwentytwo' ), 'categories' => array( 'featured', 'gallery' ), 'content' => '<!-- wp:cover {"url":"' . esc_url( get_template_directory_uri() ) . '/assets/images/ducks.jpg","dimRatio":0,"minHeight":666,"contentPosition":"center center","isDark":false,"align":"wide","style":{"spacing":{"padding":{"top":"1em","right":"1em","bottom":"1em","left":"1em"}},"color":{"duotone":["#000000","#FFFFFF"]}}} --> <div class="wp-block-cover alignwide is-light" style="padding-top:1em;padding-right:1em;padding-bottom:1em;padding-left:1em;min-height:666px"><span aria-hidden="true" class="has-background-dim-0 wp-block-cover__gradient-background has-background-dim"></span><img class="wp-block-cover__image-background" alt="' . esc_attr__( 'Painting of ducks in the water.', 'twentytwentytwo' ) . '" src="' . esc_url( get_template_directory_uri() ) . '/assets/images/ducks.jpg" data-object-fit="cover"/><div class="wp-block-cover__inner-container"><!-- wp:image {"align":"center","width":384,"height":580,"sizeSlug":"large"} --> <div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-salmon.jpg" alt="' . esc_attr__( 'Illustration of a flying bird.', 'twentytwentytwo' ) . '" width="384" height="580"/></figure></div> <!-- /wp:image --></div></div> <!-- /wp:cover -->', ); patterns/footer-default.php 0000644 00000002004 14720703302 0012027 0 ustar 00 <?php /** * Default footer */ return array( 'title' => __( 'Default footer', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:site-title {"level":0} /--> <!-- wp:paragraph {"align":"right"} --> <p class="has-text-align-right">' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-small-dark.php 0000644 00000004501 14720703302 0012370 0 ustar 00 <?php /** * Small header with dark background block pattern */ return array( 'title' => __( 'Small header with dark background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"0px","bottom":"0px"}}},"backgroundColor":"foreground","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:0px;padding-bottom:0px;"><!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"0px","bottom":"0px"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:0px;padding-bottom:0px;"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:site-logo {"width":64} /--> <!-- wp:site-title {"style":{"typography":{"fontStyle":"italic","fontWeight":"400"}}} /--></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group --> <!-- wp:image {"align":"wide","width":2000,"height":474,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image alignwide size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-transparent-d.png" alt="' . esc_attr__( 'Illustration of a bird flying.', 'twentytwentytwo' ) . '" width="2000" height="474"/></figure> <!-- /wp:image --></div> <!-- /wp:group --> <!-- wp:spacer {"height":66} --> <div style="height:66px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer -->', ); patterns/footer-blog.php 0000644 00000005306 14720703302 0011336 0 ustar 00 <?php /** * Blog footer */ return array( 'title' => __( 'Blog footer', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:paragraph {"style":{"typography":{"textTransform":"uppercase"}}} --> <p style="text-transform:uppercase">' . esc_html__( 'About us', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>' . esc_html__( 'We are a rogue collective of bird watchers. We’ve been known to sneak through fences, climb perimeter walls, and generally trespass in order to observe the rarest of birds.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:paragraph {"style":{"typography":{"textTransform":"uppercase"}}} --> <p style="text-transform:uppercase">' . esc_html__( 'Latest posts', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:latest-posts /--></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:paragraph {"style":{"typography":{"textTransform":"uppercase"}}} --> <p style="text-transform:uppercase">' . esc_html__( 'Categories', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:categories /--></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:spacer {"height":50} --> <div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:site-title {"level":0} /--> <!-- wp:paragraph {"align":"right"} --> <p class="has-text-align-right">' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/general-divider-light.php 0000644 00000001622 14720703302 0013262 0 ustar 00 <?php /** * Divider with image and color (light) block pattern */ return array( 'title' => __( 'Divider with image and color (light)', 'twentytwentytwo' ), 'categories' => array( 'featured' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"1rem","right":"0px","bottom":"1rem","left":"0px"}}},"backgroundColor":"secondary"} --> <div class="wp-block-group alignfull has-secondary-background-color has-background" style="padding-top:1rem;padding-right:0px;padding-bottom:1rem;padding-left:0px"><!-- wp:image {"id":473,"width":3001,"height":246,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/divider-black.png" alt="" class="wp-image-473" width="3001" height="246"/></figure> <!-- /wp:image --></div> <!-- /wp:group -->', ); patterns/footer-query-images-title-citation.php 0000644 00000004444 14720703302 0015754 0 ustar 00 <?php /** * Footer with query, featured images, title, and citation */ return array( 'title' => __( 'Footer with query, featured images, title, and citation', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}},"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"foreground","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:query {"query":{"perPage":3,"pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"exclude","inherit":false},"displayLayout":{"type":"flex","columns":3},"align":"wide"} --> <div class="wp-block-query alignwide"><!-- wp:post-template --> <!-- wp:post-featured-image {"isLink":true,"width":"100%","height":"318px"} /--> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --></div> <!-- /wp:query --> <!-- wp:spacer --> <div style="height:100px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:site-title {"level":0} /--> <!-- wp:group {"layout":{"type":"flex","justifyContent":"right"}} --> <div class="wp-block-group"> <!-- wp:paragraph --> <p>' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/general-video-header-details.php 0000644 00000005272 14720703302 0014513 0 ustar 00 <?php /** * Video with header and details block pattern */ return array( 'title' => __( 'Video with header and details', 'twentytwentytwo' ), 'categories' => array( 'featured', 'columns' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}},"elements":{"link":{"color":{"text":"var:preset|color|secondary"}}}},"backgroundColor":"foreground","textColor":"secondary"} --> <div class="wp-block-group alignfull has-secondary-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:heading {"level":1,"align":"wide","style":{"typography":{"fontSize":"clamp(3rem, 6vw, 4.5rem)"}}} --> <h1 class="alignwide" id="warble-a-film-about-hobbyist-bird-watchers-1" style="font-size:clamp(3rem, 6vw, 4.5rem)">' . wp_kses_post( __( '<em>Warble</em>, a film about <br>hobbyist bird watchers.', 'twentytwentytwo' ) ) . '</h1> <!-- /wp:heading --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:video {"align":"wide"} --> <figure class="wp-block-video alignwide"><video controls src="' . esc_url( get_template_directory_uri() ) . '/assets/videos/birds.mp4"></video></figure> <!-- /wp:video --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"50%"} --> <div class="wp-block-column" style="flex-basis:50%"><!-- wp:paragraph --> <p><strong>' . esc_html__( 'Featuring', 'twentytwentytwo' ) . '</strong></p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'Jesús Rodriguez<br>Doug Stilton<br>Emery Driscoll<br>Megan Perry<br>Rowan Price', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'Angelo Tso<br>Edward Stilton<br>Amy Jensen<br>Boston Bell<br>Shay Ford', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/page-about-media-left.php 0000644 00000005735 14720703302 0013156 0 ustar 00 <?php /** * About page with media on the left */ return array( 'title' => __( 'About page with media on the left', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:media-text {"align":"full","mediaType":"image","imageFill":true,"focalPoint":{"x":"0.63","y":"0.16"},"backgroundColor":"foreground","className":"alignfull is-image-fill has-background-color has-text-color has-background has-link-color"} --> <div class="wp-block-media-text alignfull is-stacked-on-mobile is-image-fill has-background-color has-text-color has-background has-link-color has-foreground-background-color has-background"><figure class="wp-block-media-text__media" style="background-image:url(' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-salmon.jpg);background-position:63% 16%"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-salmon.jpg" alt="' . esc_attr__( 'Image of a bird on a branch', 'twentytwentytwo' ) . '"/></figure><div class="wp-block-media-text__content"><!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-logo {"width":60} /--> <!-- wp:group {"style":{"spacing":{"padding":{"right":"min(8rem, 5vw)","top":"min(28rem, 28vw)"}}}} --> <div class="wp-block-group" style="padding-top:min(28rem, 28vw);padding-right:min(8rem, 5vw)"><!-- wp:heading {"style":{"typography":{"fontWeight":"300","lineHeight":"1.115","fontSize":"clamp(3rem, 6vw, 4.5rem)"}}} --> <h2 style="font-size:clamp(3rem, 6vw, 4.5rem);font-weight:300;line-height:1.115"><em>' . esc_html__( 'Doug', 'twentytwentytwo' ) . '<br>' . esc_html__( 'Stilton', 'twentytwentytwo' ) . '</em></h2> <!-- /wp:heading --> <!-- wp:paragraph {"style":{"typography":{"lineHeight":"1.6"}}} --> <p style="line-height:1.6">' . esc_html__( 'Oh hello. My name’s Doug, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show on Tuesday evenings at 11PM EDT.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:social-links {"iconColor":"background","iconColorValue":"var(--wp--preset--color--background)","iconBackgroundColor":"foreground","iconBackgroundColorValue":"var(--wp--preset--color--foreground)"} --> <ul class="wp-block-social-links has-icon-color has-icon-background-color"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:group --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div></div> <!-- /wp:media-text -->', ); patterns/footer-dark.php 0000644 00000002645 14720703302 0011337 0 ustar 00 <?php /** * Dark footer with title and citation */ return array( 'title' => __( 'Dark footer with title and citation', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"foreground","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide"><!-- wp:site-title {"level":0} /--> <!-- wp:paragraph {"align":"right"} --> <p class="has-text-align-right">' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-logo-navigation-social-black-background.php 0000644 00000003623 14720703302 0020101 0 ustar 00 <?php /** * Logo, navigation, and social links header with black background block pattern */ return array( 'title' => __( 'Logo, navigation, and social links header with background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|background"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"foreground","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"0rem","top":"0px","right":"0px","left":"0px"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:0px;padding-right:0px;padding-bottom:0rem;padding-left:0px"><!-- wp:site-logo {"width":64} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- wp:social-links {"iconColor":"background","iconColorValue":"var(--wp--preset--color--background)","className":"is-style-logos-only"} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"instagram"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--></ul> <!-- /wp:social-links --> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/general-video-trailer.php 0000644 00000003227 14720703302 0013300 0 ustar 00 <?php /** * Video trailer block pattern */ return array( 'title' => __( 'Video trailer', 'twentytwentytwo' ), 'categories' => array( 'featured', 'columns' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|foreground"}}},"spacing":{"padding":{"top":"6rem","bottom":"4rem"}}},"backgroundColor":"secondary","textColor":"foreground","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-foreground-color has-secondary-background-color has-text-color has-background has-link-color" style="padding-top:6rem;padding-bottom:4rem"><!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"33.33%"} --> <div class="wp-block-column" style="flex-basis:33.33%"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size" id="extended-trailer">' . esc_html__( 'Extended Trailer', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . esc_html__( 'A film about hobbyist bird watchers, a catalog of different birds, paired with the noises they make. Each bird is listed by their scientific name so things seem more official.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"width":"66.66%"} --> <div class="wp-block-column" style="flex-basis:66.66%"><!-- wp:video --> <figure class="wp-block-video"><video controls src="' . esc_url( get_template_directory_uri() ) . '/assets/videos/birds.mp4"></video></figure> <!-- /wp:video --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/query-text-grid.php 0000644 00000002456 14720703302 0012174 0 ustar 00 <?php /** * Text-based grid of posts block pattern */ return array( 'title' => __( 'Text-based grid of posts', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:query {"query":{"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","sticky":"","perPage":12},"displayLayout":{"type":"flex","columns":3},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:post-title {"isLink":true,"fontSize":"x-large"} /--> <!-- wp:post-excerpt /--> <!-- wp:post-date {"format":"F j, Y","isLink":true,"fontSize":"small"} /--> <!-- /wp:post-template --> <!-- wp:separator {"align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide is-style-wide"/> <!-- /wp:separator --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query -->', ); patterns/hidden-heading-and-bird.php 0000644 00000002620 14720703302 0013421 0 ustar 00 <?php /** * Heading and bird image * * This pattern is used only for translation * and to reference a dynamic image URL. It does * not appear in the inserter. */ return array( 'title' => __( 'Heading and bird image', 'twentytwentytwo' ), 'inserter' => false, 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"0px","bottom":"0px"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:0px;padding-bottom:0px;"><!-- wp:heading {"align":"wide","style":{"typography":{"fontSize":"var(--wp--custom--typography--font-size--colossal, clamp(3.25rem, 8vw, 6.25rem))","lineHeight":"1.15"}}} --> <h2 class="alignwide" style="font-size:var(--wp--custom--typography--font-size--colossal, clamp(3.25rem, 8vw, 6.25rem));line-height:1.15">' . wp_kses_post( __( '<em>The Hatchery</em>: a blog about my adventures in bird watching', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:group --> <!-- wp:image {"align":"full","width":2400,"height":1020,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image alignfull size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-transparent-c.png" alt="' . esc_attr__( 'Illustration of a bird flying.', 'twentytwentytwo' ) . '" width="2400" height="1020"/></figure> <!-- /wp:image -->', ); patterns/hidden-bird.php 0000644 00000001242 14720703302 0011263 0 ustar 00 <?php /** * Bird image * * This pattern is used only to reference a dynamic image URL. * It does not appear in the inserter. */ return array( 'title' => __( 'Heading and bird image', 'twentytwentytwo' ), 'inserter' => false, 'content' => '<!-- wp:image {"align":"wide","width":2000,"height":474,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image alignwide size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-transparent-d.png" alt="' . esc_attr__( 'Illustration of a bird flying.', 'twentytwentytwo' ) . '" width="2000" height="474"/></figure> <!-- /wp:image -->', ); patterns/general-image-with-caption.php 0000644 00000003106 14720703302 0014214 0 ustar 00 <?php /** * Image with caption block pattern */ return array( 'title' => __( 'Image with caption', 'twentytwentytwo' ), 'categories' => array( 'featured', 'columns', 'gallery' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"6rem","bottom":"6rem"}},"elements":{"link":{"color":{"text":"var:preset|color|background"}}}},"backgroundColor":"primary","textColor":"background","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background has-link-color" style="padding-top:6rem;padding-bottom:6rem"><!-- wp:media-text {"mediaId":202,"mediaLink":"' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-gray.jpg","mediaType":"image","verticalAlignment":"bottom","imageFill":false} --> <div class="wp-block-media-text alignwide is-stacked-on-mobile is-vertically-aligned-bottom"><figure class="wp-block-media-text__media"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-gray.jpg" alt="' . esc_attr__( 'Hummingbird illustration', 'twentytwentytwo' ) . '" class="wp-image-202 size-full"/></figure><div class="wp-block-media-text__content"><!-- wp:paragraph --> <p><strong>' . esc_html__( 'Hummingbird', 'twentytwentytwo' ) . '</strong></p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>' . esc_html__( 'A beautiful bird featuring a surprising set of color feathers.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div></div> <!-- /wp:media-text --></div> <!-- /wp:group -->', ); patterns/page-layout-image-text-and-video.php 0000644 00000007650 14720703302 0015260 0 ustar 00 <?php /** * Page layout with image, text and video. */ return array( 'title' => __( 'Page layout with image, text and video', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"backgroundColor":"primary","textColor":"background"} --> <div class="wp-block-group alignfull has-background-color has-primary-background-color has-text-color has-background" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"layout":{"inherit":true}} --> <div class="wp-block-group"><!-- wp:heading {"level":1,"align":"wide","style":{"typography":{"fontSize":"clamp(3rem, 6vw, 4.5rem)"}}} --> <h1 class="alignwide" style="font-size:clamp(3rem, 6vw, 4.5rem)">' . wp_kses_post( __( '<em>Warble</em>, a film about <br>hobbyist bird watchers.', 'twentytwentytwo' ) ) . '</h1> <!-- /wp:heading --> <!-- wp:spacer {"height":50} --> <div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"33.33%"} --> <div class="wp-block-column" style="flex-basis:33.33%"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size">' . esc_html__( 'Screening', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . wp_kses_post( __( 'May 14th, 2022 @ 7:00PM<br>The Vintagé Theater,<br>245 Arden Rd.<br>Gardenville, NH', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"backgroundColor":"secondary","textColor":"primary"} --> <div class="wp-block-button"><a class="wp-block-button__link has-primary-color has-secondary-background-color has-text-color has-background">' . esc_html__( 'Buy Tickets', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --> <!-- wp:column {"width":"66.66%"} --> <div class="wp-block-column" style="flex-basis:66.66%"></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group --> <!-- wp:image {"align":"full","width":2400,"height":1178,"style":{"color":{}}} --> <figure class="wp-block-image alignfull is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-transparent-a.png" alt="' . esc_attr__( 'An illustration of a bird in flight', 'twentytwentytwo' ) . '" width="2400" height="1178"/></figure> <!-- /wp:image --> <!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"33.33%"} --> <div class="wp-block-column" style="flex-basis:33.33%"><!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size">' . esc_html__( 'Extended Trailer', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . esc_html__( 'Oh hello. My name’s Angelo, and you’ve found your way to my blog. I write about a range of topics, but lately I’ve been sharing my hopes for next year.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --> <!-- wp:column {"width":"66.66%"} --> <div class="wp-block-column" style="flex-basis:66.66%"><!-- wp:video {"id":181} --> <figure class="wp-block-video"><video controls src="' . esc_url( get_template_directory_uri() ) . '/assets/videos/birds.mp4"></video></figure> <!-- /wp:video --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/query-default.php 0000644 00000004425 14720703302 0011707 0 ustar 00 <?php /** * Default posts block pattern */ return array( 'title' => __( 'Default posts', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:query {"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":""},"align":"wide","layout":{"inherit":true}} --> <div class="wp-block-query alignwide"><!-- wp:post-template {"align":"wide"} --> <!-- wp:group {"layout":{"inherit":true}} --> <div class="wp-block-group"><!-- wp:post-title {"isLink":true,"align":"wide","fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))"} /--> <!-- wp:post-featured-image {"isLink":true,"align":"wide","style":{"spacing":{"margin":{"top":"calc(1.75 * var(--wp--style--block-gap))"}}}} /--> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"650px"} --> <div class="wp-block-column" style="flex-basis:650px"><!-- wp:post-excerpt /--> <!-- wp:post-date {"isLink":true,"format":"F j, Y","style":{"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:column --> <!-- wp:column {"width":""} --> <div class="wp-block-column"></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:separator {"align":"wide","className":"is-style-wide"} --> <hr class="wp-block-separator alignwide is-style-wide"/> <!-- /wp:separator --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:group --> <!-- /wp:post-template --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query -->', ); patterns/header-title-navigation-social.php 0000644 00000003031 14720703302 0015064 0 ustar 00 <?php /** * Title, navigation, and social links header block pattern */ return array( 'title' => __( 'Title, navigation, and social links header', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--large, 8rem)","top":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:site-title {"style":{"typography":{"fontStyle":"italic","fontWeight":"400"}}} /--> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- wp:social-links {"iconColor":"foreground","iconColorValue":"var(--wp--preset--color--foreground)","className":"is-style-logos-only"} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"instagram"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--></ul> <!-- /wp:social-links --> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/page-sidebar-blog-posts.php 0000644 00000010704 14720703302 0013527 0 ustar 00 <?php /** * Blog posts with left sidebar block pattern */ return array( 'title' => __( 'Blog posts with left sidebar', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:columns {"align":"wide","style":{"spacing":{"margin":{"top":"0px","bottom":"0px"},"blockGap":"5%"},"elements":{"link":{"color":{"text":"var:preset|color|primary"}}}},"textColor":"primary"} --> <div class="wp-block-columns alignwide has-primary-color has-text-color has-link-color" style="margin-top:0px;margin-bottom:0px"><!-- wp:column {"width":"33.33%"} --> <div class="wp-block-column" style="flex-basis:33.33%"><!-- wp:cover {"overlayColor":"secondary","minHeight":400,"isDark":false} --> <div class="wp-block-cover is-light" style="min-height:400px"><span aria-hidden="true" class="has-secondary-background-color has-background-dim-100 wp-block-cover__gradient-background has-background-dim"></span><div class="wp-block-cover__inner-container"><!-- wp:site-logo {"align":"center","width":60} /--></div></div> <!-- /wp:cover --> <!-- wp:spacer {"height":40} --> <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-tagline {"fontSize":"small"} /--> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:separator {"color":"foreground","className":"is-style-wide"} --> <hr class="wp-block-separator has-text-color has-background has-foreground-background-color has-foreground-color is-style-wide"/> <!-- /wp:separator --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:navigation {"orientation":"vertical"} --> <!-- wp:page-list /--> <!-- /wp:navigation --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:separator {"color":"foreground","className":"is-style-wide"} --> <hr class="wp-block-separator has-text-color has-background has-foreground-background-color has-foreground-color is-style-wide"/> <!-- /wp:separator --></div> <!-- /wp:column --> <!-- wp:column {"width":"66.66%"} --> <div class="wp-block-column" style="flex-basis:66.66%"><!-- wp:query {"query":{"perPage":"5","pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":false},"layout":{"inherit":true}} --> <div class="wp-block-query"><!-- wp:post-template --> <!-- wp:post-title {"isLink":true,"style":{"spacing":{"margin":{"top":"0","bottom":"1rem"}},"typography":{"fontStyle":"normal","fontWeight":"300"},"elements":{"link":{"color":{"text":"var:preset|color|primary"}}}},"textColor":"primary","fontSize":"var(--wp--custom--typography--font-size--huge, clamp(2.25rem, 4vw, 2.75rem))"} /--> <!-- wp:post-featured-image {"isLink":true} /--> <!-- wp:post-excerpt /--> <!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:post-date {"format":"F j, Y","style":{"typography":{"fontStyle":"normal","fontWeight":"400"}},"fontSize":"small"} /--> <!-- wp:post-terms {"term":"category","fontSize":"small"} /--> <!-- wp:post-terms {"term":"post_tag","fontSize":"small"} /--></div> <!-- /wp:group --> <!-- wp:spacer {"height":128} --> <div style="height:128px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- /wp:post-template --> <!-- wp:query-pagination {"paginationArrow":"arrow","align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --> <!-- wp:query-pagination-previous {"fontSize":"small"} /--> <!-- wp:query-pagination-numbers /--> <!-- wp:query-pagination-next {"fontSize":"small"} /--> <!-- /wp:query-pagination --></div> <!-- /wp:query --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/query-large-titles.php 0000644 00000002517 14720703302 0012657 0 ustar 00 <?php /** * Large post titles block pattern */ return array( 'title' => __( 'Large post titles', 'twentytwentytwo' ), 'categories' => array( 'query' ), 'blockTypes' => array( 'core/query' ), 'content' => '<!-- wp:query {"query":{"pages":0,"offset":0,"postType":"post","categoryIds":[],"tagIds":[],"order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":false,"perPage":8},"align":"wide"} --> <div class="wp-block-query alignwide"><!-- wp:post-template --> <!-- wp:columns --> <div class="wp-block-columns"><!-- wp:column {"verticalAlignment":"top","width":"4em"} --> <div class="wp-block-column is-vertically-aligned-top" style="flex-basis:4em"><!-- wp:post-date {"format":"M j","fontSize":"small"} /--></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"center","width":""} --> <div class="wp-block-column is-vertically-aligned-center"><!-- wp:post-title {"isLink":true,"style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}},"typography":{"fontSize":"clamp(2.75rem, 6vw, 3.25rem)"}}} /--></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator is-style-wide"/> <!-- /wp:separator --> <!-- /wp:post-template --></div> <!-- /wp:query -->', ); patterns/general-pricing-table.php 0000644 00000011754 14720703302 0013256 0 ustar 00 <?php /** * Pricing table block pattern */ return array( 'title' => __( 'Pricing table', 'twentytwentytwo' ), 'categories' => array( 'featured', 'columns', 'buttons' ), 'content' => '<!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator is-style-wide"/> <!-- /wp:separator --> <!-- wp:heading {"style":{"typography":{"fontSize":"var(--wp--custom--typography--font-size--gigantic, clamp(2.75rem, 6vw, 3.25rem))","lineHeight":"0.5"}}} --> <h2 id="1" style="font-size:var(--wp--custom--typography--font-size--gigantic, clamp(2.75rem, 6vw, 3.25rem));line-height:0.5">' . esc_html( _x( '1', 'First item in a numbered list.', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --> <!-- wp:heading {"level":3,"fontSize":"x-large"} --> <h3 class="has-x-large-font-size" id="pigeon"><em>' . esc_html__( 'Pigeon', 'twentytwentytwo' ) . '</em></h3> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . esc_html__( 'Help support our growing community by joining at the Pigeon level. Your support will help pay our writers, and you’ll get access to our exclusive newsletter.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"backgroundColor":"foreground","width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link has-foreground-background-color has-background">' . esc_html__( '$25', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator is-style-wide"/> <!-- /wp:separator --> <!-- wp:heading {"style":{"typography":{"fontSize":"clamp(2.75rem, 6vw, 3.25rem)","lineHeight":"0.5"}}} --> <h2 id="2" style="font-size:clamp(2.75rem, 6vw, 3.25rem);line-height:0.5">' . esc_html( _x( '2', 'Second item in a numbered list.', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --> <!-- wp:heading {"level":3,"fontSize":"x-large"} --> <h3 class="has-x-large-font-size" id="sparrow"><meta charset="utf-8"><em>' . esc_html__( 'Sparrow', 'twentytwentytwo' ) . '</em></h3> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . esc_html__( 'Join at the Sparrow level and become a member of our flock! You’ll receive our newsletter, plus a bird pin that you can wear with pride when you’re out in nature.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"backgroundColor":"foreground","width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link has-foreground-background-color has-background">' . esc_html__( '$75', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:separator {"className":"is-style-wide"} --> <hr class="wp-block-separator is-style-wide"/> <!-- /wp:separator --> <!-- wp:heading {"style":{"typography":{"fontSize":"clamp(2.75rem, 6vw, 3.25rem)","lineHeight":"0.5"}}} --> <h2 id="3" style="font-size:clamp(2.75rem, 6vw, 3.25rem);line-height:0.5">' . esc_html( _x( '3', 'Third item in a numbered list.', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --> <!-- wp:heading {"level":3,"fontSize":"x-large"} --> <h3 class="has-x-large-font-size" id="falcon"><meta charset="utf-8"><em>' . esc_html__( 'Falcon', 'twentytwentytwo' ) . '</em></h3> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . esc_html__( 'Play a leading role for our community by joining at the Falcon level. This level earns you a seat on our board, where you can help plan future birdwatching expeditions.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"backgroundColor":"foreground","width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link has-foreground-background-color has-background">' . esc_html__( '$150', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --> <!-- wp:spacer {"height":16} --> <div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:column --></div> <!-- /wp:columns -->', ); patterns/header-text-only-with-tagline-black-background.php 0000644 00000003335 14720703302 0020071 0 ustar 00 <?php /** * Text-only header with tagline and black background block pattern */ return array( 'title' => __( 'Text-only header with tagline and background', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","style":{"elements":{"link":{"color":{"text":"var:preset|color|secondary"}}},"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"backgroundColor":"foreground","textColor":"secondary","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-secondary-color has-foreground-background-color has-text-color has-background has-link-color" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"0rem","top":"0px","right":"0px","left":"0px"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:0px;padding-right:0px;padding-bottom:0rem;padding-left:0px"><!-- wp:group {"layout":{"type":"flex","justifyContent":"left"}} --> <div class="wp-block-group"><!-- wp:site-title {"style":{"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:site-tagline {"style":{"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/footer-about-title-logo.php 0000644 00000003433 14720703302 0013601 0 ustar 00 <?php /** * Footer with text, title, and logo */ return array( 'title' => __( 'Footer with text, title, and logo', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"6rem"}}},"backgroundColor":"secondary","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-secondary-background-color has-background" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:6rem"><!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"width":"33%"} --> <div class="wp-block-column" style="flex-basis:33%"><!-- wp:paragraph {"style":{"typography":{"textTransform":"uppercase"}}} --> <p style="text-transform:uppercase">' . esc_html__( 'About us', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:paragraph {"style":{"fontSize":"small"} --> <p class="has-small-font-size">' . esc_html__( 'We are a rogue collective of bird watchers. We’ve been known to sneak through fences, climb perimeter walls, and generally trespass in order to observe the rarest of birds.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":180} --> <div style="height:180px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:site-title {"level":0} /--></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"bottom"} --> <div class="wp-block-column is-vertically-aligned-bottom"><!-- wp:site-logo {"align":"right","width":60} /--></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/general-large-list-names.php 0000644 00000004573 14720703302 0013703 0 ustar 00 <?php /** * Large list of names block pattern */ return array( 'title' => __( 'Large list of names', 'twentytwentytwo' ), 'categories' => array( 'featured', 'text' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"6rem","bottom":"6rem"}},"elements":{"link":{"color":{"text":"var:preset|color|primary"}}}},"backgroundColor":"tertiary","textColor":"primary","layout":{"inherit":true}} --> <div class="wp-block-group alignfull has-primary-color has-tertiary-background-color has-text-color has-background has-link-color" style="padding-top:6rem;padding-bottom:6rem"><!-- wp:group {"align":"wide"} --> <div class="wp-block-group alignwide"><!-- wp:image {"width":175,"height":82,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/icon-binoculars.png" alt="' . esc_attr__( 'An icon representing binoculars.', 'twentytwentytwo' ) . '" width="175" height="82"/></figure> <!-- /wp:image --></div> <!-- /wp:group --> <!-- wp:group {"align":"wide"} --> <div class="wp-block-group alignwide"><!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:paragraph {"style":{"typography":{"fontWeight":"300"}},"fontSize":"x-large"} --> <p class="has-x-large-font-size" style="font-weight:300">' . esc_html__( 'Jesús Rodriguez, Doug Stilton, Emery Driscoll, Megan Perry, Rowan Price, Angelo Tso, Edward Stilton, Amy Jensen, Boston Bell, Shay Ford, Lee Cunningham, Evelynn Ray, Landen Reese, Ewan Hart, Jenna Chan, Phoenix Murray, Mel Saunders, Aldo Davidson, Zain Hall.', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":32} --> <div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"backgroundColor":"primary","textColor":"background"} --> <div class="wp-block-button"><a class="wp-block-button__link has-background-color has-primary-background-color has-text-color has-background">' . esc_html__( 'Read more', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/header-with-tagline.php 0000644 00000003063 14720703302 0012737 0 ustar 00 <?php /** * Header with tagline block pattern */ return array( 'title' => __( 'Header with tagline', 'twentytwentytwo' ), 'categories' => array( 'header' ), 'blockTypes' => array( 'core/template-part/header' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"bottom":"var(--wp--custom--spacing--large, 8rem)","top":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--large, 8rem)"><!-- wp:group {"layout":{"type":"flex"}} --> <div class="wp-block-group"><!-- wp:site-logo {"width":64} /--> <!-- wp:group --> <div class="wp-block-group"><!-- wp:site-title {"style":{"spacing":{"margin":{"top":"0px","bottom":"0px"}},"typography":{"fontStyle":"normal","fontWeight":"700"}}} /--> <!-- wp:site-tagline {"style":{"spacing":{"margin":{"top":"0.25em","bottom":"0px"}},"typography":{"fontStyle":"italic","fontWeight":"400"}},"fontSize":"small"} /--></div> <!-- /wp:group --></div> <!-- /wp:group --> <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true,"justifyContent":"right"}} --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/footer-navigation.php 0000644 00000002250 14720703302 0012545 0 ustar 00 <?php /** * Footer with navigation and citation */ return array( 'title' => __( 'Footer with navigation and citation', 'twentytwentytwo' ), 'categories' => array( 'footer' ), 'blockTypes' => array( 'core/template-part/footer' ), 'content' => '<!-- wp:group {"align":"full","layout":{"inherit":true}} --> <div class="wp-block-group alignfull"><!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"flex","justifyContent":"space-between"}} --> <div class="wp-block-group alignwide" style="padding-top:4rem;padding-bottom:4rem"><!-- wp:navigation --> <!-- wp:page-list {"isNavigationChild":true,"showSubmenuIcon":true,"openSubmenusOnClick":false} /--> <!-- /wp:navigation --> <!-- wp:paragraph {"align":"right"} --> <p class="has-text-align-right">' . sprintf( /* Translators: WordPress link. */ esc_html__( 'Proudly powered by %s', 'twentytwentytwo' ), '<a href="' . esc_url( __( 'https://wordpress.org', 'twentytwentytwo' ) ) . '" rel="nofollow">WordPress</a>' ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:group --></div> <!-- /wp:group -->', ); patterns/general-divider-dark.php 0000644 00000001614 14720703302 0013075 0 ustar 00 <?php /** * Divider with image and color (dark) block pattern */ return array( 'title' => __( 'Divider with image and color (dark)', 'twentytwentytwo' ), 'categories' => array( 'featured' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"1rem","right":"0px","bottom":"1rem","left":"0px"}}},"backgroundColor":"primary"} --> <div class="wp-block-group alignfull has-primary-background-color has-background" style="padding-top:1rem;padding-right:0px;padding-bottom:1rem;padding-left:0px"><!-- wp:image {"id":473,"width":3001,"height":246,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/divider-white.png" alt="" class="wp-image-473" width="3001" height="246"/></figure> <!-- /wp:image --></div> <!-- /wp:group -->', ); patterns/page-about-solid-color.php 0000644 00000005340 14720703302 0013365 0 ustar 00 <?php /** * About page on solid color background */ return array( 'title' => __( 'About page on solid color background', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"1.25rem","right":"1.25rem","bottom":"1.25rem","left":"1.25rem"}}}} --> <div class="wp-block-group alignfull" style="padding-top:1.25rem;padding-right:1.25rem;padding-bottom:1.25rem;padding-left:1.25rem"><!-- wp:cover {"overlayColor":"secondary","minHeight":80,"minHeightUnit":"vh","isDark":false,"align":"full"} --> <div class="wp-block-cover alignfull is-light" style="min-height:80vh"><span aria-hidden="true" class="has-secondary-background-color has-background-dim-100 wp-block-cover__gradient-background has-background-dim"></span><div class="wp-block-cover__inner-container"><!-- wp:group {"layout":{"inherit":false,"contentSize":"400px"}} --> <div class="wp-block-group"><!-- wp:spacer {"height":64} --> <div style="height:64px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --><!-- wp:heading {"style":{"typography":{"lineHeight":"1","textTransform":"uppercase","fontSize":"clamp(2.75rem, 6vw, 3.25rem)"}}} --> <h2 id="edvard-smith" style="font-size:clamp(2.75rem, 6vw, 3.25rem);line-height:1;text-transform:uppercase">' . wp_kses_post( __( 'Edvard<br>Smith', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --> <!-- wp:spacer {"height":8} --> <div style="height:8px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:paragraph {"fontSize":"small"} --> <p class="has-small-font-size">' . esc_html__( 'Oh hello. My name’s Edvard, and you’ve found your way to my website. I’m an avid bird watcher, and I also broadcast my own radio show every Tuesday evening at 11PM EDT. Listen in sometime!', 'twentytwentytwo' ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":8} --> <div style="height:8px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:social-links {"iconColor":"foreground","iconColorValue":"var(--wp--preset--color--foreground)","className":"is-style-logos-only"} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --><!-- wp:spacer {"height":64} --> <div style="height:64px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --></div> <!-- /wp:group --></div></div> <!-- /wp:cover --></div> <!-- /wp:group -->', ); patterns/page-layout-image-and-text.php 0000644 00000005366 14720703302 0014156 0 ustar 00 <?php /** * Page layout with image and text. */ return array( 'title' => __( 'Page layout with image and text', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--large, 8rem)","bottom":"2rem"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--large, 8rem);padding-bottom:2rem"><!-- wp:heading {"align":"wide","style":{"typography":{"fontSize":"clamp(4rem, 8vw, 7.5rem)","lineHeight":"1.15","fontWeight":"300"}}} --> <h2 class="alignwide" style="font-size:clamp(4rem, 8vw, 7.5rem);font-weight:300;line-height:1.15">' . wp_kses_post( __( '<em>Watching Birds </em><br><em>in the Garden</em>', 'twentytwentytwo' ) ) . '</h2> <!-- /wp:heading --></div> <!-- /wp:group --> <!-- wp:image {"align":"full","width":2400,"height":1800,"style":{"color":{}}} --> <figure class="wp-block-image alignfull is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-transparent-b.png" alt="' . esc_attr_x( 'TBD', 'Short for to be determined', 'twentytwentytwo' ) . '" width="2400" height="1800"/></figure> <!-- /wp:image --> <!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"2rem","bottom":"var(--wp--custom--spacing--large, 8rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:2rem;padding-bottom:var(--wp--custom--spacing--large, 8rem)"> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"verticalAlignment":"bottom","style":{"spacing":{"padding":{"bottom":"1em"}}}} --> <div class="wp-block-column is-vertically-aligned-bottom" style="padding-bottom:1em"><!-- wp:site-logo {"width":60} /--></div> <!-- /wp:column --> <!-- wp:column {"verticalAlignment":"bottom"} --> <div class="wp-block-column is-vertically-aligned-bottom"><!-- wp:paragraph --> <p>' . wp_kses_post( __( 'Oh hello. My name’s Angelo, and I operate this blog. I was born in Portland, but I currently live in upstate New York. You may recognize me from publications with names like <a href="#">Eagle Beagle</a> and <a href="#">Mourning Dive</a>. I write for a living.<br><br>I usually use this blog to catalog extensive lists of birds and other things that I find interesting. If you find an error with one of my lists, please keep it to yourself.<br><br>If that’s not your cup of tea, <a href="#">I definitely recommend this tea</a>. It’s my favorite.', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --></div> <!-- /wp:column --></div> <!-- /wp:columns --></div> <!-- /wp:group -->', ); patterns/general-two-images-text.php 0000644 00000005275 14720703302 0013575 0 ustar 00 <?php /** * Two images with text block pattern */ return array( 'title' => __( 'Two images with text', 'twentytwentytwo' ), 'categories' => array( 'featured', 'columns', 'gallery' ), 'content' => '<!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column {"style":{"spacing":{"padding":{"top":"0rem","right":"0rem","bottom":"0rem","left":"0rem"}}}} --> <div class="wp-block-column" style="padding-top:0rem;padding-right:0rem;padding-bottom:0rem;padding-left:0rem"><!-- wp:image {"width":984,"height":1426,"sizeSlug":"large"} --> <figure class="wp-block-image size-large is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-salmon.jpg" alt="' . esc_attr__( 'Illustration of a bird sitting on a branch.', 'twentytwentytwo' ) . '" width="984" height="1426"/></figure> <!-- /wp:image --></div> <!-- /wp:column --> <!-- wp:column {"style":{"spacing":{"padding":{"top":"0rem","right":"0rem","bottom":"0rem","left":"0rem"}}}} --> <div class="wp-block-column" style="padding-top:0rem;padding-right:0rem;padding-bottom:0rem;padding-left:0rem"><!-- wp:image {"width":984,"height":1426,"sizeSlug":"large"} --> <figure class="wp-block-image size-large is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/bird-on-green.jpg" alt="' . esc_attr__( 'Illustration of a bird flying.', 'twentytwentytwo' ) . '" width="984" height="1426"/></figure> <!-- /wp:image --> <!-- wp:spacer {"height":30} --> <div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:heading {"fontSize":"x-large"} --> <h2 class="has-x-large-font-size" id="screening">' . esc_html__( 'SCREENING', 'twentytwentytwo' ) . '</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>' . wp_kses_post( __( 'May 14th, 2022 @ 7:00PM<br>The Vintagé Theater,<br>245 Arden Rd.<br>Gardenville, NH', 'twentytwentytwo' ) ) . '</p> <!-- /wp:paragraph --> <!-- wp:spacer {"height":8} --> <div style="height:8px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:spacer {"height":10} --> <div style="height:10px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"backgroundColor":"foreground"} --> <div class="wp-block-button"><a class="wp-block-button__link has-foreground-background-color has-background">' . esc_html__( 'Buy Tickets', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --></div> <!-- /wp:columns -->', ); patterns/page-about-large-image-and-buttons.php 0000644 00000010461 14720703302 0015545 0 ustar 00 <?php /** * About page with large image and buttons */ return array( 'title' => __( 'About page with large image and buttons', 'twentytwentytwo' ), 'categories' => array( 'twentytwentytwo_pages', 'buttons' ), 'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var(--wp--custom--spacing--small, 1.25rem)","bottom":"var(--wp--custom--spacing--small, 1.25rem)"}}},"layout":{"inherit":true}} --> <div class="wp-block-group alignfull" style="padding-top:var(--wp--custom--spacing--small, 1.25rem);padding-bottom:var(--wp--custom--spacing--small, 1.25rem)"><!-- wp:image {"align":"wide","width":2100,"height":1260,"sizeSlug":"full","linkDestination":"none"} --> <figure class="wp-block-image alignwide size-full is-resized"><img src="' . esc_url( get_template_directory_uri() ) . '/assets/images/flight-path-on-gray-b.jpg" alt="" width="2100" height="1260"/></figure> <!-- /wp:image --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link">' . esc_html__( 'Purchase my work', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link">' . esc_html__( 'Support my studio', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link">' . esc_html__( 'Take a class', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:columns {"align":"wide"} --> <div class="wp-block-columns alignwide"><!-- wp:column --> <div class="wp-block-column"><!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link">' . esc_html__( 'Read about me', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link">' . esc_html__( 'Learn about my process', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --> <!-- wp:column --> <div class="wp-block-column"><!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"width":100} --> <div class="wp-block-button has-custom-width wp-block-button__width-100"><a class="wp-block-button__link">' . esc_html__( 'Join my mailing list', 'twentytwentytwo' ) . '</a></div> <!-- /wp:button --></div> <!-- /wp:buttons --></div> <!-- /wp:column --></div> <!-- /wp:columns --> <!-- wp:spacer {"height":50} --> <div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div> <!-- /wp:spacer --> <!-- wp:social-links {"iconColor":"primary","iconColorValue":"var(--wp--preset--color--primary)","className":"is-style-logos-only","layout":{"type":"flex","justifyContent":"center"}} --> <ul class="wp-block-social-links has-icon-color is-style-logos-only"><!-- wp:social-link {"url":"#","service":"wordpress"} /--> <!-- wp:social-link {"url":"#","service":"facebook"} /--> <!-- wp:social-link {"url":"#","service":"twitter"} /--> <!-- wp:social-link {"url":"#","service":"instagram"} /--></ul> <!-- /wp:social-links --></div> <!-- /wp:group -->', );
| ver. 1.4 |
Github
|
.
| PHP 8.0.30 | Génération de la page: 0.19 |
proxy
|
phpinfo
|
Réglages