friends = $friends; $this->register_hooks(); } /** * Register the WordPress hooks */ private function register_hooks() { add_filter( 'pre_get_posts', array( $this, 'friend_posts_query' ), 2 ); add_filter( 'pre_get_posts', array( $this, 'exclude_compose_format_from_feed' ), 10 ); add_filter( 'post_type_link', array( $this, 'friend_post_link' ), 10, 2 ); add_filter( 'friends_header_widget_title', array( $this, 'header_widget_title' ) ); add_filter( 'get_edit_post_link', array( $this, 'friend_post_edit_link' ) ); add_filter( 'template_include', array( $this, 'template_override' ) ); add_filter( 'wp_loaded', array( $this, 'add_rewrite_rule' ) ); add_filter( 'init', array( $this, 'register_friends_sidebar' ) ); add_action( 'init', array( $this, 'add_theme_supports' ) ); add_action( 'wp_ajax_friends_publish', array( $this, 'ajax_frontend_publish_post' ) ); add_action( 'wp_ajax_friends-mention-autocomplete', array( $this, 'ajax_mention_autocomplete' ) ); add_action( 'wp_ajax_friends-change-post-format', array( $this, 'ajax_change_post_format' ) ); add_action( 'wp_ajax_friends-load-next-page', array( $this, 'ajax_load_next_page' ) ); add_action( 'wp_ajax_friends-autocomplete', array( $this, 'ajax_autocomplete' ) ); add_action( 'wp_ajax_friends-set-widget-open-state', array( $this, 'ajax_set_widget_open_state' ) ); add_action( 'wp_ajax_friends-get-post-counts', array( $this, 'ajax_get_post_counts' ) ); add_action( 'friends_search_autocomplete', array( $this, 'autocomplete_user_search' ), 10, 2 ); add_action( 'wp_ajax_friends-star', array( $this, 'ajax_star_friend_user' ) ); add_action( 'wp_ajax_friends-move-to-folder', array( $this, 'ajax_move_to_folder' ) ); add_action( 'wp_ajax_friends-create-folder', array( $this, 'ajax_create_folder' ) ); add_action( 'wp_ajax_friends-load-comments', array( $this, 'ajax_load_comments' ) ); add_action( 'wp_ajax_friends-reblog', array( $this, 'wp_ajax_reblog' ) ); add_action( 'friends_post_footer_first', array( $this, 'reblog_button' ) ); add_filter( 'friends_reblog', array( get_called_class(), 'reblog' ), 10, 3 ); add_filter( 'friends_unreblog', array( get_called_class(), 'unreblog' ), 10, 2 ); add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 2 ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'template_redirect', array( $this, 'load_theme' ) ); add_action( 'admin_bar_menu', array( $this, 'admin_bar_theme_switcher' ), 100 ); add_action( 'customize_loaded_components', array( $this, 'ensure_widget_editing' ) ); add_action( 'friends_load_theme_default', array( $this, 'default_theme' ) ); add_action( 'friends_load_theme_block', array( $this, 'block_theme' ) ); add_action( 'friends_load_theme_google-reader', array( $this, 'google_reader_theme' ) ); add_action( 'friends_load_theme_mastodon', array( $this, 'mastodon_theme' ) ); add_action( 'friends_load_theme_twitter', array( $this, 'twitter_theme' ) ); add_action( 'friends_load_themes', array( $this, 'register_block_theme' ) ); add_action( 'friends_load_themes', array( $this, 'register_google_reader_theme' ) ); add_action( 'friends_load_themes', array( $this, 'register_mastodon_theme' ) ); add_action( 'friends_load_themes', array( $this, 'register_twitter_theme' ) ); add_action( 'init', array( $this, 'register_block_templates' ) ); add_action( 'friends_template_paths', array( $this, 'friends_template_paths' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_scripts' ), 99999 ); add_action( 'wp_footer', array( $this, 'dequeue_scripts' ) ); add_action( 'the_post', array( $this, 'the_post' ), 10, 2 ); add_action( 'parse_query', array( $this, 'parse_query' ) ); add_filter( 'body_class', array( $this, 'add_body_class' ) ); add_filter( 'block_type_metadata_settings', array( $this, 'block_type_metadata_settings' ), 15 ); add_filter( 'pre_render_block', array( $this, 'render_friends_template_part' ), 10, 2 ); add_filter( 'pre_get_block_file_template', array( $this, 'get_friends_block_file_template' ), 10, 3 ); add_filter( 'get_block_templates', array( $this, 'add_friends_template_parts' ), 10, 3 ); add_filter( 'tag_row_actions', array( $this, 'tag_row_actions' ), 10, 2 ); add_action( 'friends_after_header', array( $this, 'migration_notification' ) ); add_action( 'wp_ajax_friends_dismiss_migration_notification', array( $this, 'ajax_dismiss_migration_notification' ) ); add_filter( 'friends_override_author_name', array( $this, 'override_author_name' ), 10, 3 ); add_filter( 'friends_friend_posts_query_viewable', array( $this, 'expose_opml' ), 10, 2 ); add_filter( 'get_comment_link', array( $this, 'friend_post_comment_link' ), 10, 2 ); } /** * We're asking WordPress to handle the title for us. */ public function add_rewrite_rule() { $existing_rules = get_option( 'rewrite_rules' ); $needs_flush = false; $rules = array( '^friends/(.*)/(?:feed/)?(feed|rdf|rss|rss2|atom)/?$' => 'index.php?pagename=friends/$matches[1]&feed=$matches[2]', '^friends/feed/?$' => 'index.php?pagename=friends&feed=feed', '^friends/(.*)/(\d+)/?$' => 'index.php?pagename=friends/$matches[1]&page=$matches[2]', '^friends/(.*)' => 'index.php?pagename=friends/$matches[1]', ); foreach ( $rules as $rule => $rewrite ) { if ( empty( $existing_rules[ $rule ] ) ) { $needs_flush = true; } add_rewrite_rule( $rule, $rewrite, 'top' ); } if ( $needs_flush ) { flush_rewrite_rules(); } } /** * Run our add_theme_supports if on the frontend. */ public function add_theme_supports() { if ( ! Friends::on_frontend() ) { return; } $this->add_theme_support_title_tag(); $this->add_theme_support_admin_bar(); } /** * We're asking WordPress to handle the title for us. */ private function add_theme_support_title_tag() { add_theme_support( 'title-tag' ); add_filter( 'document_title_parts', array( $this, 'modify_page_title' ) ); } /** * Remove the margin-top on the friends page. */ private function add_theme_support_admin_bar() { add_theme_support( 'admin-bar', array( 'callback' => '__return_false', ) ); } /** * Modify the page title to be output. This function is only invoked on the friends page. * * @param array $title The title. * * @return array The modified title. */ public function modify_page_title( $title ) { if ( is_single() ) { $title['page'] = __( 'Friends', 'friends' ); $title['site'] = $this->author->display_name; } elseif ( $this->author instanceof User ) { $title['title'] = __( 'Friends', 'friends' ); $title['site'] = $this->author->display_name; } else { $title = array( 'site' => __( 'Friends', 'friends' ), ); } return $title; } /** * Registers the sidebar for the /friends page. */ public function register_friends_sidebar() { register_sidebar( array( 'name' => 'Friends Topbar', 'id' => 'friends-topbar', 'before_widget' => '
', 'after_widget' => '
', 'before_title' => '

', 'after_title' => '

', ) ); register_sidebar( array( 'name' => 'Friends Sidebar', 'id' => 'friends-sidebar', 'before_widget' => '
', 'after_widget' => '
', 'before_title' => '
', 'after_title' => '
', ) ); register_sidebar( array( 'name' => __( 'Friends Sidebar 2 (Twitter theme)', 'friends' ), 'id' => 'friends-sidebar-2', 'description' => __( 'Shown in the right column of the Twitter theme.', 'friends' ), 'before_widget' => '
', 'after_widget' => '
', 'before_title' => '
', 'after_title' => '
', ) ); if ( Friends::on_frontend() ) { add_action( 'customize_register', '__return_true' ); } } public static function ensure_widget_editing( $components ) { if ( ! is_array( $components ) ) { $components = array(); } $components[] = 'widgets'; return $components; } public function load_theme() { if ( ! is_user_logged_in() || ! Friends::on_frontend() ) { return; } // Handle theme switching via GET parameter. if ( isset( $_GET['friends_theme'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $new_theme = sanitize_text_field( wp_unslash( $_GET['friends_theme'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $themes = self::get_themes(); if ( isset( $themes[ $new_theme ] ) ) { update_user_option( get_current_user_id(), 'friends_frontend_theme', $new_theme ); } } $theme = 'default'; $default_theme = get_user_option( 'friends_frontend_theme', get_current_user_id() ); if ( $default_theme ) { $theme = $default_theme; } if ( $this->post_format ) { $post_type_theme = get_user_option( 'friends_frontend_theme_' . $this->post_format, get_current_user_id() ); if ( $post_type_theme ) { $theme = $post_type_theme; } } if ( ! has_action( 'friends_load_theme_' . $theme ) ) { $theme = 'default'; } $this->theme = $theme; do_action( 'friends_load_theme_' . $theme ); } public function friends_template_paths( $file_paths ) { $backup_file_paths = $file_paths; if ( has_filter( 'friends_template_paths_theme_' . $this->theme ) ) { $file_paths = apply_filters( 'friends_template_paths_theme_' . $this->theme, $file_paths ); } if ( empty( $file_paths ) ) { return $backup_file_paths; } return $file_paths; } /** * Get an asset version that changes when the local file changes. * * @param string $file Relative asset path. * @return string Asset version. */ private function get_asset_version( $file ) { $path = dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file; if ( file_exists( $path ) ) { return Friends::VERSION . '-' . filemtime( $path ); } return Friends::VERSION; } public function default_theme() { $handle = 'friends'; $file = 'friends.css'; $version = $this->get_asset_version( $file ); wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) ); $file = 'friends-default-theme.js'; wp_enqueue_script( 'friends-default-theme', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $this->get_asset_version( $file ), true ); } /** * Reference our script for the /friends page */ public function enqueue_scripts() { if ( ! is_user_logged_in() || ! Friends::on_frontend() ) { return; } global $wp_query; $handle = 'friends'; $file = 'friends.js'; $version = $this->get_asset_version( $file ); wp_enqueue_script( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'common', 'jquery', 'wp-util' ), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ), true ); $query_vars = wp_json_encode( $this->get_minimal_query_vars( $wp_query->query_vars ) ); $variables = array( 'emojis_json' => plugins_url( 'emojis.json', FRIENDS_PLUGIN_FILE ), 'ajax_url' => admin_url( 'admin-ajax.php' ), 'rest_base' => rest_url( 'friends/v1/' ), 'rest_nonce' => wp_create_nonce( 'wp_rest' ), 'text_link_expired' => __( 'The link has expired. A new link has been generated, please click it again.', 'friends' ), 'text_undo' => __( 'Undo' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 'text_trash_post' => __( 'Trash this post', 'friends' ), 'text_del_convers' => __( 'Do you really want to delete this conversation?', 'friends' ), 'text_no_more_posts' => __( 'No more posts available.', 'friends' ), 'text_checking_url' => __( 'Checking URL.', 'friends' ), 'text_refreshed' => __( 'Refreshed', 'friends' ), 'text_refreshing' => __( 'Refreshing', 'friends' ), 'text_compact_mode' => __( 'Compact mode', 'friends' ), 'text_expanded_mode' => __( 'Expanded mode', 'friends' ), 'text_loading_comments' => __( 'Loading comments...', 'friends' ), 'text_still_loading' => __( 'Still loading...', 'friends' ), 'message_delivery_nonce' => wp_create_nonce( 'friends-message-delivery-statuses' ), 'refresh_now' => isset( $_GET['refresh'] ) ? 'true' : 'false', // phpcs:ignore WordPress.Security.NonceVerification.Recommended 'query_vars' => $query_vars, 'qv_sign' => sha1( wp_salt( 'nonce' ) . $query_vars ), 'current_page' => get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1, 'max_page' => $wp_query->max_num_pages, ); // translators: %s is a user handle. $variables['text_confirm_delete_follower'] = __( 'Do you really want to delete the follower %s?', 'friends' ); $variables['text_new_folder'] = __( 'Folder name:', 'friends' ); $variables['text_loading'] = __( 'Loading...', 'friends' ); $variables['text_discovering'] = __( 'Discovering feeds...', 'friends' ); $variables['text_ready_to_follow'] = __( 'Ready', 'friends' ); $variables['text_follow'] = __( 'Follow', 'friends' ); $variables['text_follow_selected'] = __( 'Follow selected', 'friends' ); $variables['text_review_selected'] = __( 'Review selected', 'friends' ); $variables['text_skip'] = __( 'Skip', 'friends' ); $variables['text_error'] = __( 'An error occurred.', 'friends' ); $variables['text_display_name'] = __( 'Display Name', 'friends' ); $variables['text_username'] = __( 'Username', 'friends' ); $variables['text_post_format'] = __( 'Post Format', 'friends' ); $variables['text_feed_parser'] = __( 'Parser', 'friends' ); $variables['text_feed_metadata'] = __( 'Display feed metadata', 'friends' ); $variables['text_hide_feed_metadata'] = __( 'Hide feed metadata', 'friends' ); $variables['text_preview_feed'] = __( 'Preview feed', 'friends' ); $variables['text_hide_preview'] = __( 'Hide preview', 'friends' ); $variables['text_no_preview_items'] = __( 'No preview items were found.', 'friends' ); $variables['text_add_feed_url'] = __( 'Add feed URL', 'friends' ); $variables['text_feed_url'] = __( 'Feed URL', 'friends' ); $variables['text_custom_feed'] = __( 'Custom feed', 'friends' ); $variables['text_feed_url_required'] = __( 'Please enter a feed URL.', 'friends' ); $variables['text_show_more_feeds'] = __( 'Show more feeds', 'friends' ); $variables['text_show_unsupported_feeds'] = __( 'Show unsupported feeds', 'friends' ); $variables['text_no_supported_feeds'] = __( 'No supported feeds discovered.', 'friends' ); $variables['text_no_feed_selected'] = __( 'Please select at least one feed.', 'friends' ); $variables['text_opml_no_feeds'] = __( 'No feeds were found in the OPML file.', 'friends' ); $variables['text_opml_invalid'] = __( 'Could not parse the OPML file.', 'friends' ); $variables['text_opml_select_all'] = __( 'Select all', 'friends' ); $variables['text_opml_deselect_all'] = __( 'Deselect all', 'friends' ); $variables['text_opml_import_selected'] = __( 'Review selected', 'friends' ); $variables['text_opml_no_selected'] = __( 'Please select at least one feed.', 'friends' ); $variables['text_opml_no_feeds_for_url'] = __( 'No feeds discovered.', 'friends' ); $variables['post_formats'] = array_merge( array( 'autodetect' => __( 'Autodetect Post Format', 'friends' ) ), get_post_format_strings() ); $variables['registered_parsers'] = array_map( 'wp_strip_all_tags', $this->friends->feed->get_registered_parsers() ); wp_localize_script( 'friends', 'friends', $variables ); } public function dequeue_scripts() { if ( is_user_logged_in() && Friends::on_frontend() && 'block' !== $this->theme ) { // Dequeue theme styles so that they don't interact with the Friends frontend. $wp_styles = wp_styles(); $theme_root_path = wp_parse_url( get_theme_root_uri(), PHP_URL_PATH ); foreach ( $wp_styles->queue as $style ) { $src = $wp_styles->registered[ $style ]->src; $src_path = wp_parse_url( $src, PHP_URL_PATH ); $is_theme_style = $theme_root_path && $src_path && 0 === strpos( $src_path, trailingslashit( $theme_root_path ) ); if ( 'global-styles' === $style || $is_theme_style ) { wp_dequeue_style( $style ); } } } } public function migration_notification() { if ( ! Friends::on_frontend() ) { return; } if ( ! Friends::has_pending_migrations() ) { return; } ?>
friends->on_frontend() ) { $classes[] = 'friends-page'; $classes[] = 'off-canvas'; $classes[] = 'off-canvas-sidebar-show'; } return $classes; } public function register_google_reader_theme( Frontend $friends_frontend ) { $friends_frontend->register_theme( 'Google Reader', 'google-reader' ); add_filter( 'friends_theme_name', function ( $name, $slug ) { return 'google-reader' === $slug ? __( 'Google Reader', 'friends' ) : $name; }, 10, 2 ); } public function register_mastodon_theme( Frontend $friends_frontend ) { $friends_frontend->register_theme( 'Mastodon', 'mastodon' ); add_filter( 'friends_theme_name', function ( $name, $slug ) { return 'mastodon' === $slug ? __( 'Mastodon', 'friends' ) : $name; }, 10, 2 ); } public function mastodon_theme() { $handle = 'friends-mastodon'; $file = 'templates/mastodon/mastodon.css'; $version = $this->get_asset_version( $file ); wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) ); $file = 'templates/mastodon/mastodon.js'; wp_enqueue_script( 'friends-mastodon', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $this->get_asset_version( $file ), true ); wp_localize_script( 'friends-mastodon', 'friendsMastodon', array( 'mentionNonce' => wp_create_nonce( 'friends-mention-autocomplete' ), ) ); add_filter( 'friends_template_paths_theme_mastodon', function ( $file_paths ) { $file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/mastodon/'; return $file_paths; } ); } public function register_twitter_theme( Frontend $friends_frontend ) { $friends_frontend->register_theme( 'Twitter', 'twitter' ); add_filter( 'friends_theme_name', function ( $name, $slug ) { return 'twitter' === $slug ? __( 'Twitter', 'friends' ) : $name; }, 10, 2 ); } public function twitter_theme() { $handle = 'friends-twitter'; $file = 'templates/twitter/twitter.css'; $version = $this->get_asset_version( $file ); wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) ); $file = 'templates/twitter/twitter.js'; wp_enqueue_script( 'friends-twitter', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery', 'friends' ), $this->get_asset_version( $file ), true ); wp_localize_script( 'friends-twitter', 'friendsTwitter', array( 'mentionNonce' => wp_create_nonce( 'friends-mention-autocomplete' ), ) ); add_filter( 'friends_template_paths_theme_twitter', function ( $file_paths ) { $file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/twitter/'; return $file_paths; } ); } public function google_reader_theme() { $handle = 'friends-google-reader'; $file = 'templates/google-reader/google-reader.css'; $version = $this->get_asset_version( $file ); wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) ); $file = 'templates/google-reader/google-reader.js'; wp_enqueue_script( 'friends-google-reader', plugins_url( $file, FRIENDS_PLUGIN_FILE ), array( 'jquery' ), $this->get_asset_version( $file ), true ); add_filter( 'friends_template_paths_theme_google-reader', function ( $file_paths ) { // Add Google Reader templates at highest priority (lowest number), fall back to defaults. $file_paths[0] = FRIENDS_PLUGIN_DIR . 'templates/google-reader/'; return $file_paths; } ); } /** * Add a theme switcher submenu to the Friends admin bar menu. * * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance. */ public function admin_bar_theme_switcher( $wp_admin_bar ) { if ( ! is_user_logged_in() || ! Friends::on_frontend() ) { return; } $current_theme = get_user_option( 'friends_frontend_theme', get_current_user_id() ); if ( ! $current_theme ) { $current_theme = 'default'; } $themes = self::get_themes(); $wp_admin_bar->add_node( array( 'parent' => 'friends-menu', 'id' => 'friends-theme', 'title' => esc_html__( 'Theme', 'friends' ), 'href' => admin_url( 'admin.php?page=friends-settings' ), ) ); $wp_admin_bar->add_group( array( 'parent' => 'friends-theme', 'id' => 'friends-theme-list', ) ); foreach ( $themes as $slug => $name ) { $wp_admin_bar->add_node( array( 'parent' => 'friends-theme-list', 'id' => 'friends-theme-' . $slug, 'title' => ( $slug === $current_theme ? '✓ ' : '' ) . esc_html( $name ), 'href' => add_query_arg( 'friends_theme', $slug ), ) ); } } public function register_block_theme( Frontend $friends_frontend ) { $friends_frontend->register_theme( 'Block Theme', 'block' ); add_filter( 'friends_theme_name', function ( $name, $slug ) { return 'block' === $slug ? __( 'Block Theme', 'friends' ) : $name; }, 10, 2 ); } public function block_theme() { // No theme swap needed; templates are registered as plugin templates // and injected via template_override. $handle = 'friends-blocks'; $file = 'friends-blocks.css'; $version = $this->get_asset_version( $file ); wp_enqueue_style( $handle, plugins_url( $file, FRIENDS_PLUGIN_FILE ), array(), apply_filters( 'friends_debug_enqueue', $version, $handle, dirname( FRIENDS_PLUGIN_FILE ) . '/' . $file ) ); } /** * Register Friends block templates as plugin templates. */ public function register_block_templates() { if ( ! wp_is_block_theme() || ! class_exists( 'WP_Block_Templates_Registry' ) ) { return; } $registry = \WP_Block_Templates_Registry::get_instance(); $templates = array( 'friends//friends-index' => array( 'title' => __( 'Friends Feed', 'friends' ), 'content' => 'index', ), 'friends//friends-author-index' => array( 'title' => __( 'Friends Author Feed', 'friends' ), 'content' => 'friends-author-index', ), 'friends//friends-followers' => array( 'title' => __( 'Friends Followers', 'friends' ), 'content' => 'friends-followers', ), 'friends//friends-add-friend' => array( 'title' => __( 'Friends Add Friend', 'friends' ), 'content' => 'friends-add-friend', ), 'friends//friends-subscriptions' => array( 'title' => __( 'Friends Following', 'friends' ), 'content' => 'friends-subscriptions', ), 'friends//friends-single' => array( 'title' => __( 'Friends Single Post', 'friends' ), 'content' => 'single-friend_post_cache', ), ); foreach ( $templates as $name => $args ) { $file = FRIENDS_PLUGIN_DIR . 'themes/friends/templates/' . $args['content'] . '.html'; if ( file_exists( $file ) ) { $registry->register( $name, array( 'title' => $args['title'], 'content' => file_get_contents( $file ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ) ); } } } /** * Render template parts for the friends plugin. * * @param string|null $pre_render The pre-rendered content. * @param array $parsed_block The parsed block. * @return string|null The rendered content or null. */ public function render_friends_template_part( $pre_render, $parsed_block ) { if ( 'core/template-part' !== $parsed_block['blockName'] ) { return $pre_render; } $attrs = $parsed_block['attrs']; if ( ! isset( $attrs['theme'] ) || 'friends' !== $attrs['theme'] || ! isset( $attrs['slug'] ) ) { return $pre_render; } // Check for user-customized version in the database first. $custom_query = new \WP_Query( array( 'post_type' => 'wp_template_part', 'post_status' => 'publish', 'post_name__in' => array( $attrs['slug'] ), 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query array( 'taxonomy' => 'wp_theme', 'field' => 'name', 'terms' => 'friends', ), ), 'posts_per_page' => 1, 'no_found_rows' => true, ) ); if ( $custom_query->have_posts() ) { $content = $custom_query->posts[0]->post_content; } else { $file = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/' . $attrs['slug'] . '.html'; if ( ! file_exists( $file ) ) { return $pre_render; } $content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents } $tag = isset( $attrs['tagName'] ) ? $attrs['tagName'] : 'div'; return '<' . $tag . ' class="wp-block-template-part">' . do_blocks( $content ) . ''; } /** * Build a WP_Block_Template for a friends template part. * * @param string $slug The template part slug. * @param string $file The file path. * @return WP_Block_Template */ private function build_friends_template_part( $slug, $file ) { $template = new \WP_Block_Template(); $template->id = 'friends//' . $slug; $template->theme = 'friends'; $template->plugin = 'friends'; $template->slug = $slug; $template->source = 'plugin'; $template->origin = 'plugin'; $template->type = 'wp_template_part'; $template->title = ucwords( str_replace( '-', ' ', $slug ) ); $template->status = 'publish'; $template->has_theme_file = true; $template->is_custom = false; $template->content = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $template->area = WP_TEMPLATE_PART_AREA_UNCATEGORIZED; return $template; } /** * Resolve a friends template part by ID. * * @param WP_Block_Template|null $block_template The found block template. * @param string $id Template unique identifier. * @param string $template_type Template type. * @return WP_Block_Template|null */ public function get_friends_block_file_template( $block_template, $id, $template_type ) { if ( 'wp_template_part' !== $template_type ) { return $block_template; } $parts = explode( '//', $id, 2 ); if ( count( $parts ) < 2 || 'friends' !== $parts[0] ) { return $block_template; } $slug = $parts[1]; $file = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/' . $slug . '.html'; if ( ! file_exists( $file ) ) { return $block_template; } return $this->build_friends_template_part( $slug, $file ); } /** * Add friends template parts to block template queries. * * @param WP_Block_Template[] $query_result Array of found block templates. * @param array $query Arguments to retrieve templates. * @param string $template_type Template type. * @return WP_Block_Template[] */ public function add_friends_template_parts( $query_result, $query, $template_type ) { if ( 'wp_template_part' !== $template_type ) { return $query_result; } $parts_dir = FRIENDS_PLUGIN_DIR . 'themes/friends/parts/'; if ( ! is_dir( $parts_dir ) ) { return $query_result; } $slugs_to_include = isset( $query['slug__in'] ) ? $query['slug__in'] : array(); foreach ( glob( $parts_dir . '*.html' ) as $file ) { $slug = basename( $file, '.html' ); if ( $slugs_to_include && ! in_array( $slug, $slugs_to_include, true ) ) { continue; } // Don't add if already in results. $exists = false; foreach ( $query_result as $existing ) { if ( $existing->slug === $slug && 'friends' === $existing->theme ) { $exists = true; break; } } if ( ! $exists ) { $query_result[] = $this->build_friends_template_part( $slug, $file ); } } return $query_result; } public function block_type_metadata_settings( $settings ) { if ( ! Friends::on_frontend() || ! isset( $settings['name'] ) ) { return $settings; } if ( 'core/post-author-name' === $settings['name'] ) { $settings['render_callback'] = function ( $attributes, $content, $block ) { if ( isset( $block->context['postId'] ) ) { $author = User::get_post_author( get_post( $block->context['postId'] ) ); } else { return ''; } if ( empty( $author ) || is_wp_error( $author ) ) { return ''; } $author_name = $author->display_name; $author_name_html = apply_filters( 'friends_author_display_name_html', esc_html( $author_name ), $author_name, $author ); $override_author_name = apply_filters( 'friends_override_author_name', '', $author_name, $block->context['postId'] ); if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) { $author_name_html = sprintf( '%3$s', esc_url( $author->get_local_friends_page_url() ), esc_attr( $attributes['linkTarget'] ), $author_name_html ); } if ( $override_author_name && trim( str_replace( $override_author_name, '', $author_name ) ) === $author_name ) { $author_name_html .= ' – ' . esc_html( $override_author_name ); } $classes = array(); if ( isset( $attributes['textAlign'] ) ) { $classes[] = 'has-text-align-' . $attributes['textAlign']; } if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) { $classes[] = 'has-link-color'; } $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) ); return sprintf( '
%2$s
', $wrapper_attributes, wp_kses( $author_name_html, array_merge( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'class' => true, 'href' => true, 'target' => true, ), ) ) ) ); }; } return $settings; } public function tag_row_actions( $actions, $tag ) { $actions['view-friends'] = sprintf( '%s', esc_url( home_url( '/friends/tag/' . $tag->name ) ), __( 'View on your Friends page', 'friends' ) ); return $actions; } /** * Gets the minimal query variables. * * @param array $query_vars The query variables. * * @return array The minimal query variables. */ private function get_minimal_query_vars( $query_vars ) { return array_filter( array_intersect_key( $query_vars, array_flip( array( 'p', 'page_id', 'pagename', 'author', 'author__not_in', 'post_type', 'post_status', 'posts_per_page', 'order', 'tax_query', 's' ) ) ) ); } public function wp_ajax_reblog() { if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['post_id'] ) ) { wp_send_json_error( 'error' ); } check_ajax_referer( 'friends-reblog' ); $post = get_post( intval( $_POST['post_id'] ) ); if ( ! $post || ! Friends::check_url( $post->guid ) ) { wp_send_json_error( 'unknown-post', array( 'guid' => $post->guid ) ); } $reblog_post_id = get_post_meta( get_the_ID(), 'reblogged', true ); if ( $reblog_post_id ) { wp_send_json_success( array( 'post_id' => $reblog_post_id, 'redirect' => get_edit_post_link( $reblog_post_id, 'js' ), ) ); wp_send_json_error( 'already-reblogged', array( 'guid' => $post->guid ) ); return; } /** * Reblogs a post * * @param int|null $reblog_post_id The post ID of the reblogged post. Default null. * @param WP_Post $post The post object. */ $reblog_post_id = apply_filters( 'friends_reblog', null, $post, 'draft' ); if ( ! $reblog_post_id || is_wp_error( $reblog_post_id ) ) { wp_send_json_error( 'error' ); } wp_send_json_success( array( 'post_id' => $reblog_post_id, 'redirect' => get_edit_post_link( $reblog_post_id, 'js' ), ) ); } public function reblog_button() { Friends::template_loader()->get_template_part( 'frontend/parts/reblog-button', null, array() ); } public static function reblog( $ret, $post, $post_status = 'publish' ) { if ( ! $post ) { return $ret; } $post = get_post( $post ); if ( ! $post ) { return $ret; } $author = get_post_meta( $post->ID, 'author', true ); if ( ! $author ) { $friend = User::get_post_author( $post ); $author = $friend->display_name; } $reblog = '' . PHP_EOL . '

'; $reblog .= sprintf( // translators: %s is a link. __( 'Reblog via %s', 'friends' ), '' . esc_html( $author ) . '' ); $reblog_title = sprintf( // translators: %1$s is the author, %2$s is the post title. __( 'Reblog of %1$s: %2$s', 'friends' ), $author, $post->post_title ); $reblog .= PHP_EOL . '

' . PHP_EOL . '' . PHP_EOL; // Wrap the post content in a quote block. $reblog .= '' . PHP_EOL . '
'; $reblog .= $post->post_content; $reblog .= '
' . PHP_EOL . ''; $new_post = array( 'post_title' => $reblog_title, 'post_author' => get_current_user_id(), 'post_status' => $post_status, 'post_type' => 'post', 'post_format' => get_post_format( $post ), 'post_content' => $reblog, ); /** * Allows changing a reblog post before it gets posted. * * @param array $new_post A post array to be handed to wp_insert_post. * * Additionally, the array contains the post_format which can also be changed. * * Example: * ```php * add_filter( 'friends_reblog_pre_insert_post', function( $new_post ) { * $new_post['post_type'] = 'custom_post_type'; * $new_post['post_format'] = 'aside'; // always set the post_format to aside, regardless of the original post format. * return $new_post; * } ); * ``` */ $new_post = apply_filters( 'friends_reblog_pre_insert_post', $new_post ); $new_post_id = wp_insert_post( $new_post ); set_post_format( $new_post_id, $new_post['post_format'] ); update_post_meta( $new_post_id, 'reblog', $post->guid ); update_post_meta( $new_post_id, 'reblog_of', $post->ID ); update_post_meta( $post->ID, 'reblogged', $new_post_id ); update_post_meta( $post->ID, 'reblogged_by', $new_post['post_author'] ); return $new_post_id; } public static function unreblog( $ret, $post ) { if ( ! $post ) { return $ret; } $post = get_post( $post ); if ( ! $post ) { return $ret; } $reblogged = get_post_meta( $post->ID, 'reblogged', true ); if ( ! $reblogged ) { return $ret; } wp_trash_post( $reblogged ); return $reblogged; } /** * The Ajax function to autocomplete @mentions in the compose box. */ public function ajax_mention_autocomplete() { check_ajax_referer( 'friends-mention-autocomplete' ); if ( ! current_user_can( Friends::REQUIRED_ROLE ) ) { wp_send_json_error(); exit; } if ( ! isset( $_POST['q'] ) ) { wp_send_json_success( array() ); exit; } $q = sanitize_text_field( wp_unslash( $_POST['q'] ) ); $users = User_Query::search( '*' . $q . '*' ); $results = array(); foreach ( $users->get_results() as $friend ) { $handle = null; foreach ( $friend->get_feeds() as $feed ) { $ap_actor_id = $feed->get_ap_actor_id(); if ( $ap_actor_id && class_exists( '\Activitypub\Collection\Remote_Actors' ) ) { $acct = \Activitypub\Collection\Remote_Actors::get_acct( $ap_actor_id ); if ( $acct ) { $handle = $acct; break; } } } if ( ! $handle ) { $handle = $friend->user_login; if ( $friend->user_url ) { $host = wp_parse_url( $friend->user_url, PHP_URL_HOST ); if ( $host ) { $handle = $friend->user_login . '@' . $host; } } } $results[] = array( 'handle' => $handle, 'display_name' => $friend->display_name, 'avatar' => $friend->get_avatar_url(), ); } wp_send_json_success( $results ); } /** * The Ajax function to be called upon posting from /friends */ public function ajax_frontend_publish_post() { if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wpnonce'] ), 'friends_publish' ) ) { return false; } $p = array( 'post_type' => 'post', 'post_title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '', 'post_content' => isset( $_POST['content'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content'] ) ) : '', 'post_status' => isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '', 'post_format' => isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : '', ); if ( empty( $p['post_status'] ) ) { $p['post_status'] = 'publish'; } $result = 'empty'; if ( ! empty( $_POST['in_reply_to'] ) ) { $p['meta_input'] = array( 'activitypub_in_reply_to' => sanitize_text_field( wp_unslash( $_POST['in_reply_to'] ) ), ); } if ( ! empty( $p['post_content'] ) || ! empty( $p['post_title'] ) ) { $post_id = wp_insert_post( $p ); if ( ! empty( $p['post_format'] ) ) { set_post_format( $post_id, $p['post_format'] ); } $result = is_wp_error( $post_id ) ? 'error' : 'success'; } // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! empty( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) === 'xmlhttprequest' ) { echo esc_html( $result ); exit; } if ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { wp_safe_redirect( remove_query_arg( 'in_reply_to', add_query_arg( 'result', $result, $_SERVER['HTTP_REFERER'] ) ) ); exit; } wp_safe_redirect( home_url( '/friends/' ) ); exit; // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } /** * The Ajax function to change the post format from the Frontend. */ public function ajax_change_post_format() { $post_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0; $post_format = isset( $_POST['format'] ) ? sanitize_text_field( wp_unslash( $_POST['format'] ) ) : 'standard'; check_ajax_referer( "friends-change-post-format_$post_id" ); if ( ! current_user_can( Friends::REQUIRED_ROLE, $post_id ) ) { wp_send_json_error(); exit; } $post_formats = get_post_format_strings(); if ( ! isset( $post_formats[ $post_format ] ) ) { wp_send_json_error(); exit; } if ( ! set_post_format( $post_id, $post_format ) ) { wp_send_json_error(); exit; } wp_send_json_success(); } /** * Calculates an estimating read time. * * @param string $original_text The original text. * * @return float The read time in seconds. */ public static function calculate_read_time( $original_text ) { // from wp_trim_words(). $text = wp_strip_all_tags( $original_text ); /* * translators: If your word count is based on single characters (e.g. East Asian characters), * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'. * Do not translate into your own language. */ if ( strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ) ) { // phpcs:ignore WordPress.WP.I18n.MissingArgDomain $text = trim( preg_replace( "/[\n\r\t ]+/", ' ', $text ), ' ' ); preg_match_all( '/./u', $text, $words_array ); $words_array = array_shift( $words_array ); $words_per_minute = 500; } else { $words_array = preg_split( "/[\n\r\t ]+/", $text, -1, PREG_SPLIT_NO_EMPTY ); $words_per_minute = 200; } $additional_time = 0; $figures = substr_count( strtolower( $original_text ), ' __( 'Friends Default Theme', 'friends' ), ), self::$themes ); foreach ( $themes as $slug => $name ) { $themes[ $slug ] = apply_filters( 'friends_theme_name', $name, $slug ); } return $themes; } /** * Handles the post loop on the Friends page. */ public static function have_posts() { $friends = Friends::get_instance(); $known_author = $friends->frontend->author; $known_avatar = null; if ( $known_author instanceof User ) { $known_avatar = $known_author->get_avatar_url(); } while ( have_posts() ) { global $post; the_post(); $args = array( 'friends' => $friends, ); $args['item_friend_user'] = User::get_post_author( $post ); if ( $known_author ) { $args['friend_user'] = $known_author; $args['avatar'] = $known_avatar; } else { $args['friend_user'] = $args['item_friend_user']; $args['avatar'] = $args['friend_user']->get_avatar_url(); } $read_time = self::calculate_read_time( get_the_content() ); if ( $read_time >= 60 ) { $mins = ceil( $read_time / MINUTE_IN_SECONDS ); /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */ $args['read_time'] = sprintf( _n( '%s min', '%s mins', $mins ), $mins ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain } elseif ( $read_time > 20 ) { /* translators: Time difference between two dates, in minutes (min=minute). %s: Number of minutes. */ $args['read_time'] = _x( '< 1 min', 'reading time', 'friends' ); } Friends::template_loader()->get_template_part( 'frontend/parts/content', get_post_format(), $args ); } } /** * The Ajax function to load more posts for infinite scrolling. */ public function ajax_load_next_page() { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // phpcs:disable WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['query_vars'] ) || ! isset( $_POST['page'] ) || ! isset( $_POST['qv_sign'] ) ) { wp_send_json_error(); exit; } $query_vars = wp_unslash( $_POST['query_vars'] ); // We do have a nonce verification here. if ( sha1( wp_salt( 'nonce' ) . $query_vars ) !== $_POST['qv_sign'] ) { wp_send_json_error(); exit; } $query_vars = json_decode( $query_vars, true ); $query_vars['paged'] = intval( $_POST['page'] ) + 1; // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // phpcs:enable WordPress.Security.NonceVerification.Missing // We actually want to render the query loop, so we use query_posts. query_posts( $query_vars ); // phpcs:ignore WordPress.WP.DiscouragedFunctions.query_posts_query_posts ob_start(); if ( have_posts() ) { self::have_posts(); } else { esc_html_e( 'No further posts of your friends could were found.', 'friends' ); } $posts = ob_get_contents(); ob_end_clean(); wp_send_json_success( $posts ); } /** * The Ajax function to autocomplete search. */ public function ajax_autocomplete() { check_ajax_referer( 'friends-autocomplete' ); if ( ! isset( $_POST['q'] ) ) { wp_send_json_error(); exit; } $q = sanitize_text_field( wp_unslash( $_POST['q'] ) ); $results = array(); /** $result = ''; $result .= ''; $result .= 'Add friend'; $result .= ' '; $result .= esc_html( $q ); $result .= ''; $results[] = $result; */ $results = apply_filters( 'friends_search_autocomplete', $results, $q ); $result = ''; $result .= ''; $result .= 'Search for '; $result .= ' '; $result .= esc_html( $q ); $result .= ''; $results[] = $result; wp_send_json_success( '' ); } /** * The Add user search entries to the autocomplete results. * * @param array $results The autocomplete results. * @param string $q The query. * * @return array The autocomplete results. */ public function autocomplete_user_search( $results, $q ) { $users = User_Query::search( '*' . $q . '*' ); foreach ( $users->get_results() as $friend ) { $result = ''; $result .= ''; $result .= str_ireplace( $q, '' . $q . '', $friend->display_name ); $result .= ' '; $result .= str_ireplace( $q, '' . $q . '', $friend->user_login ); $result .= ''; $results[] = $result; } return $results; } /** * Filter comment links to use ActivityPub source URL in AJAX context. * * The ActivityPub plugin skips its filter when is_admin() is true, * but AJAX requests go through admin-ajax.php where is_admin() returns true. * * @param string $comment_link The comment permalink. * @param WP_Comment $comment The comment object. * @return string The filtered comment link. */ public function activitypub_comment_link( $comment_link, $comment ) { if ( ! $comment || 'comment' !== $comment->comment_type ) { return $comment_link; } $source_url = get_comment_meta( $comment->comment_ID, 'source_url', true ); if ( $source_url ) { return $source_url; } return $comment_link; } /** * The Ajax function to load comments. */ public function ajax_load_comments() { if ( ! isset( $_POST['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing wp_send_json_error(); exit; } $post_id = intval( $_POST['post_id'] ); check_ajax_referer( "comments-$post_id" ); $comments = apply_filters( 'friends_get_comments', get_comments( array( 'post_id' => $post_id, 'status' => 'approve', 'order' => 'ASC', ) ), $post_id ); $friend_user = User::get_post_author( get_post( $post_id ) ); $user_feed_url = get_post_meta( $post_id, 'feed_url', true ); $user_feed = $this->friends->feed->get_user_feed_by_url( $user_feed_url ); remove_all_filters( 'comment_form_before' ); remove_all_filters( 'comment_form_after' ); // Add filter to fix ActivityPub comment links in AJAX context. add_filter( 'get_comment_link', array( $this, 'activitypub_comment_link' ), 20, 2 ); if ( empty( $comments ) ) { $content = apply_filters( 'friends_no_comments_feed_available', __( 'No comments yet.', 'friends' ), $post_id, $friend_user, $user_feed ); } else { $template_loader = Friends::template_loader(); ob_start(); ?>
    'ol', 'short_ping' => true, 'avatar_size' => 24, ), $comments ); ?>
set_starred( $starred ); wp_send_json_success( array( 'starred' => $friend_user->get_starred(), ) ); } /** * Ajax handler to move a subscription to a folder. */ public function ajax_move_to_folder() { if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['friend_id'] ) ) { wp_send_json_error(); exit; } check_ajax_referer( 'friends-move-to-folder' ); $friend_id = sanitize_text_field( wp_unslash( $_POST['friend_id'] ) ); $folder_id = isset( $_POST['folder_id'] ) ? intval( $_POST['folder_id'] ) : 0; $friend_user = User::get_by_username( $friend_id ); if ( ! $friend_user || is_wp_error( $friend_user ) || ! ( $friend_user instanceof Subscription ) ) { wp_send_json_error( 'invalid-user' ); exit; } $result = $friend_user->move_to_folder( $folder_id ); wp_send_json_success( array( 'moved' => $result ) ); } /** * Ajax handler to create a new folder. */ public function ajax_create_folder() { if ( ! current_user_can( Friends::REQUIRED_ROLE ) || ! isset( $_POST['name'] ) ) { wp_send_json_error(); exit; } check_ajax_referer( 'friends-move-to-folder' ); $name = sanitize_text_field( wp_unslash( $_POST['name'] ) ); $parent_id = isset( $_POST['parent_id'] ) ? intval( $_POST['parent_id'] ) : 0; $folder = Subscription::create_folder( $name, $parent_id ); if ( is_wp_error( $folder ) ) { wp_send_json_error( $folder->get_error_message() ); exit; } wp_send_json_success( array( 'term_id' => $folder->term_id, 'name' => $folder->name, ) ); } /** * Ensure that untrashed friends posts go back to published. * * @param string $new_status The new status of the post being restored. * @param int $post_id The ID of the post being restored. */ public function untrash_post_status( $new_status, $post_id ) { if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ), true ) ) { return $new_status; } return 'publish'; } /** * Load the template for /friends * * @param string $template The original template intended to load. * @return string The new template to be loaded. */ public function template_override( $template ) { if ( ! Friends::on_frontend() ) { return $template; } global $args; $args = array( 'friends' => $this->friends, 'friend_user' => $this->author, 'post_format' => $this->post_format, ); if ( $this->template ) { global $wp_query; $wp_query->is_404 = false; status_header( 200 ); if ( 'block' === $this->theme ) { $block_template_content = $this->get_block_template_content_for( $this->template ); if ( false !== $block_template_content ) { global $_wp_current_template_content, $_wp_current_template_id; $_wp_current_template_content = $block_template_content; $_wp_current_template_id = get_stylesheet() . '//friends-' . basename( $this->template ); return ABSPATH . WPINC . '/template-canvas.php'; } } if ( 'frontend/index' === $this->template ) { $args['frontend_default_view'] = get_user_option( 'friends_frontend_default_view', get_current_user_id() ); } return Friends::template_loader()->get_template_part( $this->template, null, $args, false ); } if ( 'block' === $this->theme ) { global $wp_query; if ( $wp_query->is_single ) { $template_key = 'frontend/single'; $template_id = 'friends-single'; } elseif ( $this->author ) { $template_key = 'frontend/author-index'; $template_id = 'friends-author-index'; } else { $template_key = 'frontend/index'; $template_id = 'friends-index'; } $block_template_content = $this->get_block_template_content_for( $template_key ); if ( false !== $block_template_content ) { global $_wp_current_template_content, $_wp_current_template_id; $_wp_current_template_content = $block_template_content; $_wp_current_template_id = get_stylesheet() . '//' . $template_id; return ABSPATH . WPINC . '/template-canvas.php'; } } $args['frontend_default_view'] = get_user_option( 'friends_frontend_default_view', get_current_user_id() ); $args['blocks-everywhere'] = false; if ( isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['show_welcome'] = true; } return Friends::template_loader()->get_template_part( 'frontend/index', $this->post_format, $args, false ); } public function get_block_template_content_for( $template_path ) { $map = array( 'frontend/index' => 'index', 'frontend/author-index' => 'friends-author-index', 'frontend/single' => 'single-friend_post_cache', 'frontend/followers' => 'friends-followers', 'frontend/add-friend' => 'friends-add-friend', 'frontend/subscriptions' => 'friends-subscriptions', ); if ( ! isset( $map[ $template_path ] ) ) { return false; } $template = $map[ $template_path ]; if ( 'frontend/index' === $template_path && isset( $_GET['welcome'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $template = 'welcome'; } $file = FRIENDS_PLUGIN_DIR . 'themes/friends/templates/' . $template . '.html'; if ( ! file_exists( $file ) ) { return false; } return file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents } public function get_static_frontend_template( $path ) { global $friends_args; global $wp_query; $wp_query->is_singular = false; switch ( $path ) { case 'add-friend': $path = 'frontend/add-friend'; break; case 'subscriptions': wp_safe_redirect( home_url( '/friends/following/' ) ); exit; case 'following': $friends_args = array(); $path = 'frontend/subscriptions'; break; case 'messages': $friends_args = array( 'title' => __( 'Direct Messages', 'friends' ), 'no-bottom-margin' => true, ); $path = 'frontend/messages'; break; case 'followers': if ( ! class_exists( '\Activitypub\Collection\Followers' ) ) { return 'frontend/index'; } $friends_args = array(); if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $friends_args['only_mutual'] = true; $friends_args['title'] = __( 'Your Mutual Followers', 'friends' ); } else { $friends_args['only_mutual'] = false; $friends_args['title'] = __( 'Your Followers', 'friends' ); } $friends_args['user_id'] = get_current_user_id(); $path = 'frontend/followers'; break; case 'blog-followers': $friends_args = array(); if ( ! class_exists( '\Activitypub\Collection\Actors' ) ) { return 'frontend/index'; } if ( isset( $_GET['mutual'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $friends_args['only_mutual'] = true; $friends_args['title'] = __( 'Your Mutual Blog Followers', 'friends' ); } else { $friends_args['only_mutual'] = false; $friends_args['title'] = __( 'Your Blog Followers', 'friends' ); } $friends_args['user_id'] = \Activitypub\Collection\Actors::BLOG_USER_ID; $path = 'frontend/followers'; break; case 'mutual': if ( ! class_exists( '\Activitypub\Collection\Followers' ) ) { return 'frontend/index'; } $friends_args = array(); $friends_args['title'] = __( 'Friends', 'friends' ); $friends_args['filter'] = 'following'; $friends_args['user_id'] = get_current_user_id(); $path = 'frontend/followers'; break; default: return 'frontend/index'; } $wp_query->is_404 = false; return $path; } /** * Modify the Friends page title depending on context, for example add an author name or post format. * * @param string $title The original title. * * @return string The modified title. */ public function header_widget_title( $title ) { $title = '' . esc_html( $title ) . ''; if ( $this->author ) { $title .= ' » ' . esc_html( $this->author->display_name ) . ''; } if ( $this->post_format ) { $post_formats = get_post_format_strings(); $title .= ' » ' . $post_formats[ $this->post_format ]; } return $title; } /** * Output a link, potentially augmented with authication information. * * @param string $url The url. * @param string $text The link text. * @param array $html_attributes HTML attributes. * @param User $friend_user The friend user. */ public function link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) { echo wp_kses( self::get_link( $url, $text, $html_attributes, $friend_user ), array( 'a' => array( 'href' => array(), 'title' => array(), 'target' => array(), 'rel' => array(), 'class' => array(), 'style' => array(), 'data-nonce' => array(), 'data-cnonce' => array(), 'data-token' => array(), 'data-friend' => array(), 'data-id' => array(), 'data-url' => array(), ), 'span' => array( 'class' => array() ), ) ); } /** * Get a link, potentially augmented with authication information. * * @param string $url The url. * @param string $text The link text. * @param array $html_attributes HTML attributes. * @param User $friend_user The friend user. * * @return string The link. */ public static function get_link( $url, $text, array $html_attributes = array(), ?User $friend_user = null ) { if ( is_null( $friend_user ) ) { $friend_user = new User( get_the_author_meta( 'ID' ) ); } $link = ' $value ) { if ( ! in_array( $name, array( 'title', 'target', 'rel', 'class', 'style', 'data-nonce', 'data-cnonce', 'data-token', 'data-friend', 'data-id', 'data-url' ) ) ) { continue; } $link .= ' ' . $name . '="' . esc_attr( $value ) . '"'; } $link .= ' title="' . esc_attr( $text ) . '">'; if ( isset( $html_attributes['dashicon_front'] ) ) { $link .= ''; } $link .= '' . esc_html( $text ) . ''; if ( isset( $html_attributes['dashicon_back'] ) ) { $link .= ''; } $link .= ''; return $link; } public static function get_widget_open_state( $widget ) { static $state = null; if ( is_null( $state ) ) { $state = get_user_meta( get_current_user_id(), 'friends_widget_state', true ); if ( ! is_array( $state ) ) { $state = array(); } } return isset( $state[ $widget ] ) && is_string( $state[ $widget ] ) ? $state[ $widget ] : 'open'; } public function ajax_set_widget_open_state() { if ( ! isset( $_POST['widget'] ) || ! isset( $_POST['state'] ) ) { wp_send_json_error(); exit; } check_ajax_referer( 'friends_widget_state' ); $widget = sanitize_text_field( wp_unslash( $_POST['widget'] ) ); $open_state = sanitize_text_field( wp_unslash( $_POST['state'] ) ); $state = get_user_meta( get_current_user_id(), 'friends_widget_state', true ); if ( ! is_array( $state ) ) { $state = array(); } $state[ $widget ] = $open_state; update_user_meta( get_current_user_id(), 'friends_widget_state', $state ); wp_send_json_success(); } public function ajax_get_post_counts() { check_ajax_referer( 'friends_post_counts' ); $counts = array_fill_keys( get_post_format_slugs(), 0 ); foreach ( $this->friends->get_post_count_by_post_format( true ) as $post_format => $count ) { $counts[ $post_format ] = $this->friends->get_post_format_plural_string( $post_format, $count ); } $post_counts = $this->friends->get_post_count_by_post_status( true ); if ( isset( $post_counts->trash ) && $post_counts->trash ) { $counts['trash'] = sprintf( /* translators: %s is the number of hidden posts */_n( '%s hidden items', '%s hidden items', $post_counts->trash, 'friends' ), number_format_i18n( $post_counts->trash ) ); } else { $counts['trash'] = 0; } wp_send_json_success( $counts ); } /** * Don't show the edit link for friend posts. * * @param string $link The edit link. * @return string|bool The edit link or false. */ public function friend_post_edit_link( $link ) { global $post; if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) { if ( Friends::on_frontend() ) { $new_link = false; } else { $new_link = get_the_guid( $post ); } /** * Allow overriding the link for editing friend posts. * * By default on the Frontend, a post cannot be edited because $new_link is false. * * Example: * * ```php * add_filter( 'friend_post_edit_link', function( $link, $original_link ) { * if ( ! $link ) { * $link = $original_link; // always allow editing. * } * return $link; * }, 10, 2 ); * ``` */ return apply_filters( 'friend_post_edit_link', $new_link, $link ); } return $link; } /** * Link friend posts to the remote site. * * @param string $post_link The post's permalink. * @param \WP_Post $post The post in question. * @return string The overriden post link. */ public function friend_post_link( $post_link, \WP_Post $post ) { if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) { return get_the_guid( $post ); } return $post_link; } /** * Link friend post comments to the local friends page. * * @param string $link The comment permalink. * @param \WP_Comment $comment The comment object. * @return string The overriden comment link. */ public function friend_post_comment_link( $link, \WP_Comment $comment ) { // Don't override external ActivityPub comments - they should link to their source. if ( 'activitypub' === $comment->comment_type || 'activitypub' === get_comment_meta( $comment->comment_ID, 'protocol', true ) ) { return $link; } $post = get_post( $comment->comment_post_ID ); if ( $post && in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) { $friend_user = User::get_post_author( $post ); return $friend_user->get_local_friends_page_url( $post->ID ) . '#comment-' . $comment->comment_ID; } return $link; } /** * Potentially override the post author name with metadata. * * @param string $overridden_author_name The already overridden author name. * @param string $author_name The author name. * @param int $post_id The post id. * * @return string The modified author name. */ public function override_author_name( $overridden_author_name, $author_name, $post_id ) { if ( $overridden_author_name && $overridden_author_name !== $author_name ) { return $overridden_author_name; } $override_author_name = get_post_meta( $post_id, 'author', true ); if ( $override_author_name ) { return $override_author_name; } return $author_name; } public function expose_opml( $viewable, $pagename ) { if ( 'opml' === $pagename ) { return true; } return $viewable; } /** * Render the Friends OPML * * @param bool $only_public Only public feed URLs. */ protected function render_opml( $only_public = false ) { if ( ! \is_user_logged_in() ) { $only_public = true; $user_id = Friends::get_main_friend_user_id(); $user = new User( $user_id ); } else { $user = wp_get_current_user(); } // translators: %s is a name. $title = sprintf( __( '%s is following', 'friends' ), $user->display_name ); $filename = 'friends-'; if ( ! $only_public ) { $title = __( 'My Friends', 'friends' ); $filename .= 'private-'; } $filename .= $user->user_login . '.opml'; $feeds = array(); $users = array(); $friend_users = User_Query::all_associated_users(); foreach ( $friend_users->get_results() as $friend_user ) { $role = $friend_user->get_role_name( true, 9 ); if ( $only_public ) { $role = 'Feeds'; } if ( ! isset( $users[ $role ] ) ) { $users[ $role ] = array(); } $users[ $role ][] = $friend_user; } ksort( $users ); foreach ( $users as $role => $friend_users ) { foreach ( $friend_users as $friend_user ) { $user_feeds = $friend_user->get_active_feeds(); $need_local_feed = false; foreach ( $user_feeds as $feed ) { if ( $feed->get_parser() === Feed_Parser_SimplePie::SLUG ) { break; } switch ( $feed->get_mime_type() ) { case 'application/atom+xml': case 'application/atomxml': case 'application/rss+xml': case 'application/rssxml': break; default: $need_local_feed = true; break 2; } } if ( $need_local_feed && ! $only_public ) { $user_feeds = array_slice( $user_feeds, 0, 1 ); } $user = array( 'friend_user' => $friend_user, 'feeds' => array(), ); $html_url = $friend_user->user_url; foreach ( $user_feeds as $feed ) { $type = 'rss'; $feed_title = $friend_user->display_name; if ( ! $only_public ) { $html_url = $feed->get_local_html_url(); } if ( $need_local_feed ) { if ( $only_public ) { // Cannot create a public URL. continue; } $xml_url = $feed->get_local_url(); // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['auth'] ) ) { $xml_url = add_query_arg( 'auth', sanitize_text_field( wp_unslash( $_GET['auth'] ) ), $xml_url ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } } else { if ( $only_public ) { $xml_url = $feed->get_url(); } else { $xml_url = $feed->get_url(); } if ( 'application/atom+xml' === $feed->get_mime_type() ) { $type = 'atom'; } } $user['feeds'][] = array( 'xml_url' => $xml_url, 'html_url' => $html_url, 'title' => $feed_title, 'type' => $type, ); } if ( ! empty( $user['feeds'] ) ) { if ( ! isset( $feeds[ $role ] ) ) { $feeds[ $role ] = array(); } $feeds[ $role ][] = $user; } } } Friends::template_loader()->get_template_part( 'admin/opml', null, array( 'title' => $title, 'feeds' => $feeds, 'filename' => $filename, ) ); exit; } private function has_required_priviledges() { if ( Friends::is_main_user() ) { return true; } if ( is_multisite() ) { if ( is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) { return true; } if ( is_super_admin( get_current_user_id() ) ) { // Super admins would count as administrators. return false; } } if ( current_user_can( 'manage_options' ) ) { return true; } return false; } /** * Exclude the compose post format from the main RSS feed. * * @param \WP_Query $query The main query. * @return \WP_Query The modified main query. */ public function exclude_compose_format_from_feed( $query ) { if ( ! $query->is_main_query() || ! $query->is_feed() ) { return $query; } if ( ! get_option( 'friends_exclude_compose_format_from_feed' ) ) { return $query; } $format = get_option( 'friends_compose_post_format', 'status' ); if ( ! $format || 'standard' === $format ) { return $query; } $tax_query = (array) $query->get( 'tax_query' ); $tax_query[] = array( 'taxonomy' => 'post_format', 'field' => 'slug', 'terms' => array( 'post-format-' . $format ), 'operator' => 'NOT IN', ); $query->set( 'tax_query', $tax_query ); return $query; } /** * Modify the main query for the /friends page * * @param \WP_Query $query The main query. * @return \WP_Query The modified main query. */ public function friend_posts_query( $query ) { global $wp_query, $wp, $authordata; if ( $wp_query !== $query || $query->is_admin() || $query->is_home() ) { return $query; } $pagename = ''; if ( isset( $wp_query->query['pagename'] ) ) { $pagename = $wp_query->query['pagename']; } elseif ( isset( $wp_query->query['category_name'] ) ) { $pagename = $wp_query->query['category_name']; } elseif ( isset( $wp_query->query['name'] ) ) { $pagename = $wp_query->query['name']; } $pagename_parts = explode( '/', trim( $pagename, '/' ) ); $is_friends = array_shift( $pagename_parts ); if ( 'friends' !== $is_friends ) { return $query; } if ( isset( $pagename_parts[0] ) && 'settings' === $pagename_parts[0] ) { wp_safe_redirect( self_admin_url( 'admin.php?page=friends-settings' ) ); exit; } // Not available for the general public or friends. $viewable = $this->has_required_priviledges() && Friends::on_frontend(); if ( $query->is_feed() ) { // Feeds can be viewed through extra authentication. if ( isset( $wp_query->query['pagename'] ) ) { if ( apply_filters( 'friends_friend_feed_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) { $viewable = true; } } } if ( ! $viewable && isset( $wp_query->query['pagename'] ) ) { if ( apply_filters( 'friends_friend_posts_query_viewable', false, isset( $pagename_parts[0] ) ? $pagename_parts[0] : false ) ) { $viewable = true; } } $page_id = get_query_var( 'page' ); // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['share'] ) ) { $share_hash = hash( 'crc32b', apply_filters( 'friends_share_salt', wp_salt( 'nonce' ), $page_id ) . $page_id ); if ( $_GET['share'] === $share_hash ) { $viewable = true; } } if ( ! $viewable ) { if ( $query->is_feed() ) { status_header( 404 ); $query->set_404(); } elseif ( ! Friends::on_frontend() ) { if ( count( $pagename_parts ) > 0 ) { wp_safe_redirect( home_url( '/friends/' ) ); exit; } } elseif ( ! Friends::is_main_user() ) { wp_die( esc_html__( 'You are not allowed to view this page.', 'friends' ) ); } return $query; } // Don't let the Post Kinds plugin interfere with this query. remove_action( 'pre_get_posts', array( 'Kind_Taxonomy', 'kind_firehose_query' ), 99 ); switch_to_locale( get_user_locale() ); $tax_query = array(); $post_format = null; while ( $pagename_parts ) { $pagename_part = $pagename_parts[0]; $current_part = array_shift( $pagename_parts ); if ( 'reaction' === substr( $current_part, 0, 8 ) && strlen( $current_part ) > 8 ) { $reaction = Reactions::validate_emoji( substr( $current_part, 8 ) ); if ( ! $reaction ) { continue; } $this->reaction = $reaction; if ( ! empty( $tax_query ) ) { $tax_query['relation'] = 'AND'; } $tax_query[] = array( 'taxonomy' => 'friend-reaction-' . get_current_user_id(), 'field' => 'slug', 'terms' => array( substr( $current_part, 8 ) ), ); continue; } switch ( $current_part ) { case 'opml': return $this->render_opml( isset( $_REQUEST['public'] ) && boolval( $_REQUEST['public'] ) ); case 'type': $post_format = array_shift( $pagename_parts ); $tax_query = $this->friends->wp_query_get_post_format_tax_query( $tax_query, explode( ',', $post_format ) ); if ( $tax_query ) { $this->post_format = $post_format; } break; case 'tag': if ( empty( $pagename_parts ) && $page_id ) { // Support numeric tags. $this->tag = strval( $page_id ); $page_id = false; } else { $this->tag = array_shift( $pagename_parts ); } $tax_query = $this->friends->wp_query_get_post_tag_tax_query( $tax_query, $this->tag ); break; default: // Maybe an author. $author = User::get_by_username( $current_part ); if ( false === $author || is_wp_error( $author ) ) { if ( $query->is_feed() ) { status_header( 404 ); $query->set_404(); return $query; } $template = $this->get_static_frontend_template( $current_part ); if ( 'frontend/index' !== $template ) { $wp_query->is_404 = false; $query->is_404 = false; status_header( 200 ); add_filter( 'pre_handle_404', '__return_true' ); $this->template = $template; return $query; } wp_safe_redirect( home_url( '/friends/' ) ); exit; } $this->author = $author; break; } } $this->is_friends_page = true; $query->is_friends_page = true; $query->is_singular = false; $query->is_single = false; $query->is_category = false; $query->is_archive = false; $query->queried_object = null; $query->queried_object_id = null; $post_types = apply_filters( 'friends_frontend_post_types', array() ); if ( 'status' === $post_format ) { // Show your own posts on the status feed. $post_types[] = 'post'; } $query->set( 'post_type', $post_types ); $query->set( 'tax_query', $tax_query ); if ( ( isset( $_GET['share'] ) && $_GET['share'] === $share_hash ) || $viewable ) { $post_status = array( 'publish', 'private', 'future' ); if ( isset( $_GET['show-hidden'] ) ) { $post_status[] = 'trash'; } $query->set( 'post_status', $post_status ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended $query->is_page = false; $query->is_comment_feed = false; $query->set( 'pagename', null ); $query->set( 'category_name', null ); if ( get_option( 'posts_per_page' ) < 20 ) { $query->set( 'posts_per_page', 20 ); if ( 'collapsed' === get_user_option( 'friends_frontend_default_view', get_current_user_id() ) && 'status' === $post_format ) { $query->set( 'posts_per_page', 30 ); } } if ( $page_id ) { $query->set( 'page_id', $page_id ); if ( ! $this->author ) { $post = get_post( $page_id ); if ( $post ) { $author = User::get_post_author( $post ); if ( false !== $author ) { $this->author = $author; } } } $query->is_single = true; $query->is_singular = true; $wp->set_query_var( 'page', null ); $wp->set_query_var( 'page_id', $page_id ); } if ( $this->author ) { if ( $this->author instanceof User ) { $authordata = $this->author; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $this->author->modify_query_by_author( $query ); if ( ! $page_id ) { $query->is_author = true; } } else { $this->author = null; } } if ( ! $query->is_singular && ! $query->is_author ) { // This is the main friends page. $hide_from_friends_page = get_user_option( 'friends_hide_from_friends_page' ); $hide_from_friends_page = array_diff( array_map( 'intval', (array) $hide_from_friends_page ), array( 0 ) ); if ( $hide_from_friends_page ) { $query->set( 'author__not_in', $hide_from_friends_page ); } } // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['order'] ) ) { $order = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ); if ( in_array( $order, array( 'ASC', 'DESC' ), true ) ) { $query->set( 'order', $order ); } } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $query; } }