관리-도구
편집 파일: class-wpcode-library.php
<?php /** * Load snippets from the wpcode.com snippet library. * * @package WPCode */ /** * Class WPCode_Library. */ class WPCode_Library { /** * Key for storing snippets in the cache. * * @var string */ protected $cache_key = 'snippets'; /** * Library endpoint for loading all data. * * @var string */ protected $all_snippets_endpoint = 'get'; /** * The key for storing individual snippets. * * @var string */ protected $snippet_key = 'snippets/snippet'; /** * The base cache folder for this class. * * @var string */ protected $cache_folder = 'library'; /** * The data. * * @var array */ protected $data; /** * The default time to live for libary items that are cached. * * @var int */ protected $ttl = DAY_IN_SECONDS; /** * Key for transient used to store already installed snippets. * * @var string */ protected $used_snippets_transient_key = 'wpcode_used_library_snippets'; /** * Array of snippet ids that were already loaded from the library. * * @var array */ protected $library_snippets; /** * Meta Key used for storing the library id. * * @var string */ protected $snippet_library_id_meta_key = '_wpcode_library_id'; /** * Total number of snippets in the library atm. * * @var int */ protected $snippets_count; /** * Snippets grouped by library username. * * @var array */ protected $snippets_by_username = array(); /** * Array of library usernames. * * @var array */ protected $library_usernames = array(); /** * Constructor. */ public function __construct() { $this->hooks(); } /** * Class-specific hooks. * * @return void */ protected function hooks() { add_action( 'trash_wpcode', array( $this, 'clear_used_snippets' ) ); add_action( 'transition_post_status', array( $this, 'clear_used_snippets_untrash' ), 10, 3 ); add_action( 'wpcode_library_api_auth_connected', array( $this, 'delete_cache' ) ); add_action( 'wpcode_library_api_auth_connected', array( $this, 'get_data_delayed' ), 15 ); add_action( 'wpcode_library_api_auth_deleted', array( $this, 'delete_cache' ) ); } /** * Wait for the file cache to be cleared before loading the data. * * @return void */ public function get_data_delayed() { // Wait for the cache to be cleared. add_action( 'shutdown', array( $this, 'get_data' ) ); } /** * Grab all the available categories from the library. * * @return array */ public function get_data() { if ( ! isset( $this->data ) ) { $this->data = $this->load_data(); } return $this->data; } /** * Get the number of snippets in the library. * * @return int */ public function get_snippets_count() { if ( ! isset( $this->snippets_count ) ) { $this->snippets_count = 0; $data = $this->get_data(); if ( ! empty( $data['snippets'] ) ) { $this->snippets_count = count( $data['snippets'] ); } } return $this->snippets_count; } /** * Grab data from the cache. * * @param string $key The key used to grab from cache. * @param int $ttl The time to live for cached data, defaults to class ttl. * * @return array|false */ public function get_from_cache( $key, $ttl = 0 ) { if ( empty( $ttl ) ) { $ttl = $this->ttl; } $data = wpcode()->file_cache->get( $this->cache_folder . '/' . $key, $ttl ); if ( isset( $data['error'] ) && isset( $data['time'] ) ) { if ( $data['time'] + 10 * MINUTE_IN_SECONDS < time() ) { return false; } else { return $this->get_empty_array(); } } return $data; } /** * Load the library data either from the server or from cache. * * @return array */ public function load_data() { $this->data = $this->get_from_cache( $this->cache_key ); if ( false === $this->data ) { $this->data = $this->get_from_server(); } $this->maybe_add_usernames_data(); return $this->data; } /** * Get data from the server. * * @return array */ protected function get_from_server() { $data = $this->process_response( $this->make_request( $this->all_snippets_endpoint ) ); if ( empty( $data['snippets'] ) ) { return $this->save_temporary_response_fail( $this->cache_key ); } $this->save_to_cache( $this->cache_key, $data ); return $data; } /** * Generic request handler with support for authentication. * * @param string $endpoint The API endpoint to load data from. * @param string $method The method used for the request (GET, POST, etc). * @param array $data The data to pass in the body for POST-like requests. * * @return string */ public function make_request( $endpoint = '', $method = 'GET', $data = array() ) { $args = array( 'method' => $method, 'timeout' => 10, ); if ( wpcode()->library_auth->has_auth() ) { $args['headers'] = $this->get_authenticated_headers(); } if ( ! empty( $data ) ) { $args['body'] = $data; } $url = add_query_arg( array( 'site' => rawurlencode( site_url() ), 'version' => WPCODE_VERSION, ), wpcode()->library_auth->get_api_url( $endpoint ) ); $response = wp_remote_request( $url, $args ); $response_code = wp_remote_retrieve_response_code( $response ); if ( $response_code > 299 ) { // Temporary error so cache for just 10 minutes and then try again. return ''; } return wp_remote_retrieve_body( $response ); } /** * Get the headers for making an authenticated request. * * @return array */ public function get_authenticated_headers() { // Build the headers of the request. return array( 'Content-Type' => 'application/x-www-form-urlencoded', 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0', 'Pragma' => 'no-cache', 'Expires' => 0, 'Origin' => site_url(), 'WPCode-Referer' => site_url(), 'WPCode-Sender' => 'WordPress', 'WPCode-Site' => esc_attr( get_option( 'blogname' ) ), 'WPCode-Version' => esc_attr( WPCODE_VERSION ), 'WPCode-Client-Id' => wpcode()->library_auth->get_client_id(), 'X-WPCode-ApiKey' => wpcode()->library_auth->get_auth_key(), ); } /** * When we can't fetch from the server we save a temporary error => true file to avoid * subsequent requests for a while. Returns a properly formatted array for frontend output. * * @param string $key The key used for storing the data in the cache. * * @return array */ public function save_temporary_response_fail( $key ) { $data = array( 'error' => true, 'time' => time(), ); $this->save_to_cache( $key, $data ); return $this->get_empty_array(); } /** * Get an empty array for a consistent response. * * @return array[] */ public function get_empty_array() { return array( 'categories' => array(), 'snippets' => array(), ); } /** * Save to cache. * * @param string $key The key used to store the data in the cache. * @param array|mixed $data The data that will be stored. * * @return void */ public function save_to_cache( $key, $data ) { wpcode()->file_cache->set( $this->cache_folder . '/' . $key, $data ); } /** * Generic handler for grabbing data by slug. Either all categories or the category slug. * * @param string $data Response body from server. * * @return array */ public function process_response( $data ) { $response = json_decode( $data, true ); if ( ! isset( $response['status'] ) || 'success' !== $response['status'] ) { return $this->get_empty_array(); } return $response['data']; } /** * Get a cache key for a specific snippet id. * * @param int $id The snippet id. * * @return string */ public function get_snippet_cache_key( $id ) { return $this->snippet_key . '_' . $id; } /** * Create a new snippet by the library id. * This grabs the snippet by its id from the snippet library site and creates * a new snippet on the current site using the response. * * @param int $library_id The id of the snippet on the library site. * * @return false|WPCode_Snippet */ public function create_new_snippet( $library_id ) { $snippet_data = $this->grab_snippet_from_api( $library_id ); if ( ! $snippet_data ) { return false; } $snippet_data = apply_filters( 'wpcode_library_import_snippet_data', $snippet_data ); $snippet = wpcode_get_snippet( $snippet_data ); $snippet->save(); delete_transient( $this->used_snippets_transient_key ); return $snippet; } /** * Grab a snippet data from the API. * * @param int $library_id The id of the snippet in the Library api. * * @return array|array[]|false */ public function grab_snippet_from_api( $library_id ) { $snippet_request = $this->make_request( 'get/' . $library_id ); $snippet_data = $this->process_response( $snippet_request ); if ( empty( $snippet_data ) ) { return false; } return $snippet_data; } /** * Get all the snippets that were created from the library, by library ID. * Results are cached in a transient. * * @return array */ public function get_used_library_snippets() { if ( isset( $this->library_snippets ) ) { return $this->library_snippets; } $snippets_from_library = get_transient( $this->used_snippets_transient_key ); if ( false === $snippets_from_library ) { $snippets_from_library = array(); $args = array( 'post_type' => wpcode_get_post_type(), 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => $this->snippet_library_id_meta_key, 'compare' => 'EXISTS', ), ), 'fields' => 'ids', 'post_status' => 'any', 'nopaging' => true, ); $snippets = get_posts( $args ); foreach ( $snippets as $snippet_id ) { $snippets_from_library[ $this->get_snippet_library_id( $snippet_id ) ] = $snippet_id; } set_transient( $this->used_snippets_transient_key, $snippets_from_library ); } $this->library_snippets = $snippets_from_library; return $this->library_snippets; } /** * Grab the library id from the snippet by snippet id. * * @param int $snippet_id The snippet id. * * @return int */ public function get_snippet_library_id( $snippet_id ) { return absint( get_post_meta( $snippet_id, '_wpcode_library_id', true ) ); } /** * When a snippet is trashed, clear the used snippets transients * for this class instance to avoid confusion in the library. * * @return void */ public function clear_used_snippets() { delete_transient( $this->used_snippets_transient_key ); } /** * Clear used snippets also when a snippet is un-trashed. * * @param string $new_status The new post status. * @param string $old_status The old post status. * @param WP_Post $post The post object. * * @return void */ public function clear_used_snippets_untrash( $new_status, $old_status, $post ) { if ( 'wpcode' !== $post->post_type || 'trash' !== $old_status ) { return; } $this->clear_used_snippets(); } /** * Delete the file cache for the snippets library. * * @return void */ public function delete_cache() { wpcode()->file_cache->delete( $this->cache_folder . '/' . $this->cache_key ); if ( isset( $this->data ) ) { unset( $this->data ); } } /** * Makes a request to the snippet library API to grab a public snippet by its hash. * * @param string $hash The hash used to identify the snippet on the library server. * @param string $auth The unique user hash used to authenticate the request on the library. * * @return array */ public function get_public_snippet( $hash, $auth ) { // Let's use transients for hashes to avoid unnecessary requests. $transient_key = 'wpcode_public_snippet_' . $hash . '_' . $auth; $snippet_data = get_transient( $transient_key ); if ( false === $snippet_data ) { $snippet_request = $this->make_request( 'public/' . $hash, 'POST', array( 'auth' => $auth, ) ); $snippet_data = json_decode( $snippet_request, true ); // Transient for 1 minute if error otherwise 30 minutes. $timeout = ! isset( $snippet_data['status'] ) || 'error' === $snippet_data['status'] ? 60 : 30 * 60; set_transient( $transient_key, $snippet_data, $timeout ); } return $snippet_data; } /** * Get snippets by username. * * @param string $username The username to grab data for. * @param string $version The version of the library to grab data for. * * @return array */ public function get_snippets_by_username( $username, $version = '' ) { $username = sanitize_key( $username ); if ( empty( $version ) ) { // Let's grab the version from the registered username if no version is explicitly passed. $version = $this->get_version_by_username( $username ); } if ( ! isset( $this->snippets_by_username[ $username ] ) ) { $this->load_snippets_by_username( $username, $version ); } return $this->snippets_by_username[ $username ]; } /** * Grab the version from the registered username array. * * @param string $username The username to grab version for. * * @return string */ public function get_version_by_username( $username ) { return isset( $this->library_usernames[ $username ] ) ? $this->library_usernames[ $username ]['version'] : ''; } /** * Load snippets in the current instance, either from cache or from the server. * * @param string $username The username to grab data for. * @param string $version The version of the plugin/theme to grab data for. * * @return array */ private function load_snippets_by_username( $username, $version ) { $this->snippets_by_username[ $username ] = $this->get_from_cache( 'profile_' . $username ); if ( false === $this->snippets_by_username[ $username ] ) { $this->snippets_by_username[ $username ] = $this->get_from_server_by_username( $username ); } // Let's filter the loaded data to make sure no snippets aimed at older versions are loaded. $this->snippets_by_username[ $username ] = $this->filter_snippets_by_version( $this->snippets_by_username[ $username ], $version ); return $this->data; } /** * Go through all the snippets and if they have a maximum version set, remove them if the current version is higher. * * @param array $profile_data The snippets to filter. * @param string $version The version to filter by. * * @return array */ public function filter_snippets_by_version( $profile_data, $version ) { // If we have no version, we can't filter anything. if ( empty( $version ) || empty( $profile_data['snippets'] ) ) { return $profile_data; } $filtered_snippets = array(); foreach ( $profile_data['snippets'] as $snippet ) { if ( empty( $snippet['max_version'] ) || version_compare( $version, $snippet['max_version'], '<=' ) ) { $filtered_snippets[] = $snippet; } } $profile_data['snippets'] = $filtered_snippets; return $profile_data; } /** * Grab data from the WPCode library by username. * * @param string $username The username to grab data for. * * @return array|array[] */ private function get_from_server_by_username( $username ) { $data = $this->process_response( $this->make_request( 'profile/' . $username ) ); if ( empty( $data['snippets'] ) ) { return $this->save_temporary_response_fail( 'profile_' . $username ); } $this->save_to_cache( 'profile_' . $username, $data ); return $data; } /** * Get a list of usernames that we should attempt to load data from the library for. * * @return array */ public function get_library_usernames() { return $this->library_usernames; } /** * Add a method to allow other plugins to register usernames to load data for. * * @param string $username The public username on the WPCode Library. * @param string $label The label to display in the WPCode library view. * @param string $max_version The plugin/theme version, used for excluding snippets aimed at older plugin/theme versions. * * @return void */ public function register_library_username( $username, $label = '', $max_version = '' ) { $username = sanitize_key( $username ); if ( empty( $label ) ) { $label = $username; } $this->library_usernames[ $username ] = array( 'label' => $label, 'version' => $max_version, ); } /** * If there are usernames to load data for, add them to the data array. * * @return void */ public function maybe_add_usernames_data() { $usernames = $this->get_library_usernames(); if ( empty( $usernames ) ) { return; } foreach ( $usernames as $username => $data ) { $snippets = $this->get_snippets_by_username( $username, $data['version'] ); if ( ! empty( $snippets['snippets'] ) ) { $this->data['categories'][] = array( 'slug' => $username, 'name' => $data['label'], 'count' => count( $snippets['snippets'] ), ); // Append snippets to the $this->data['snippets'] array. $this->data['snippets'] = array_merge( $this->data['snippets'], $snippets['snippets'] ); } } } /** * Get the URL to edit a snippet. * * @param int $snippet_id The snippet id. * * @return string */ public function get_edit_snippet_url( $snippet_id ) { return add_query_arg( array( 'page' => 'wpcode-snippet-manager', 'snippet_id' => absint( $snippet_id ), ), admin_url( 'admin.php' ) ); } /** * Get a direct link to install a snippet by its library URL. * * @param int $snippet_library_id The snippet ID on the WPCode library. * * @return string */ public function get_install_snippet_url( $snippet_library_id ) { return wp_nonce_url( add_query_arg( array( 'snippet_library_id' => absint( $snippet_library_id ), 'page' => 'wpcode-library', ), admin_url( 'admin.php' ) ), 'wpcode_add_from_library' ); } /** * Get just the snippets from usernames. * * @return array */ public function get_username_snippets() { $usernames = $this->get_library_usernames(); $snippets = array(); $categories = array(); foreach ( $usernames as $username => $data ) { $username_snippets = $this->get_snippets_by_username( $username, $data['version'] ); if ( ! empty( $username_snippets['snippets'] ) ) { $categories[] = array( 'slug' => $username, 'name' => $data['label'], 'count' => count( $username_snippets['snippets'] ), ); // Append snippets to the $this->data['snippets'] array. $snippets = array_merge( $snippets, $username_snippets['snippets'] ); } } return array( 'categories' => $categories, 'snippets' => $snippets, ); } }