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 ( empty( $this->data ) || ! is_array( $this->data ) ) {
$this->data = $this->get_from_server();
}
// Enforce shape BEFORE maybe_add_usernames_data() so that method's
// `$this->data['categories'][] = ...` and `array_merge($this->data['snippets'], ...)`
// can't fatal on a partial-shape cache payload like {"snippets": null}.
if ( ! is_array( $this->data ) ) {
$this->data = $this->get_empty_array();
}
if ( ! isset( $this->data['categories'] ) || ! is_array( $this->data['categories'] ) ) {
$this->data['categories'] = array();
}
if ( ! isset( $this->data['snippets'] ) || ! is_array( $this->data['snippets'] ) ) {
$this->data['snippets'] = array();
}
$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 ( ! is_array( $response ) || ! isset( $response['status'] ) || 'success' !== $response['status'] ) {
return array();
}
if ( ! isset( $response['data'] ) || ! is_array( $response['data'] ) ) {
return 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_id = $snippet->save();
// Save the version information if available in the API response.
if ( ! empty( $snippet_data['version'] ) ) {
update_post_meta( $snippet_id, '_wpcode_snippet_version', $snippet_data['version'] );
}
// Save the originating library author if available in the API response.
if ( ! empty( $snippet_data['username'] ) ) {
update_post_meta( $snippet_id, '_wpcode_library_author', sanitize_key( $snippet_data['username'] ) );
}
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 ) );
}
/**
* Resolve which library username (author) a snippet came from.
*
* Returns the persisted `_wpcode_library_author` meta if present. For
* snippets imported before this meta key was tracked, falls back to the
* (per-request memoized) reverse lookup map and backfills the meta when
* a match is found. Returns the generic 'wpcode' marker when a snippet
* is library-linked but its origin can't be identified — without
* persisting it, so a later call in the same request (or a later
* render) can re-attempt once registered-author data becomes available.
*
* @param int $snippet_id The local snippet id.
*
* @return string The library username slug, or empty string if not from the library.
*/
public function get_snippet_author( $snippet_id ) {
$library_id = $this->get_snippet_library_id( $snippet_id );
if ( empty( $library_id ) ) {
return '';
}
$stored = get_post_meta( $snippet_id, '_wpcode_library_author', true );
if ( ! empty( $stored ) ) {
return $stored;
}
$map = $this->get_library_author_lookup_map();
if ( isset( $map[ $library_id ] ) ) {
update_post_meta( $snippet_id, '_wpcode_library_author', $map[ $library_id ] );
return $map[ $library_id ];
}
return 'wpcode';
}
/**
* Reverse map of library_id => registered author username, built once
* per request so backfill scans don't repeat per row.
*
* @return array
*/
protected function get_library_author_lookup_map() {
static $map = null;
if ( null !== $map ) {
return $map;
}
$map = array();
foreach ( $this->get_library_usernames() as $username => $data ) {
$version = isset( $data['version'] ) ? $data['version'] : '';
$snippets = $this->get_snippets_by_username( $username, $version );
foreach ( ( isset( $snippets['snippets'] ) ? $snippets['snippets'] : array() ) as $snippet ) {
if ( isset( $snippet['library_id'] ) ) {
$map[ absint( $snippet['library_id'] ) ] = $username;
}
}
}
return $map;
}
/**
* Get the human readable label for a library author username.
*
* @param string $username The library username slug.
*
* @return string
*/
public function get_author_label( $username ) {
if ( 'wpcode' === $username ) {
return __( 'WPCode', 'insert-headers-and-footers' );
}
$usernames = $this->get_library_usernames();
return isset( $usernames[ $username ]['label'] ) ? $usernames[ $username ]['label'] : $username;
}
/**
* 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 ( empty( $this->snippets_by_username[ $username ] ) || ! is_array( $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,
);
}
/**
* Check if a snippet has an update available by comparing with cached data.
*
* @param int $snippet_id The snippet ID.
* @param string $library_id The library ID.
*
* @return bool|array False if no update, array with version info if update available.
*/
public function check_snippet_update( $snippet_id, $library_id ) {
// Get current version from post meta.
$current_version = get_post_meta( $snippet_id, '_wpcode_snippet_version', true );
// For library snippets, get from library cache.
$cached_data = $this->get_data();
$library_snippet = null;
if ( ! empty( $cached_data['snippets'] ) ) {
foreach ( $cached_data['snippets'] as $snippet ) {
if ( isset( $snippet['library_id'] ) && absint( $snippet['library_id'] ) === absint( $library_id ) ) {
$library_snippet = $snippet;
break;
}
}
}
if ( ! $library_snippet || empty( $library_snippet['version'] ) ) {
return false;
}
$latest_version = $library_snippet['version'];
// If either version is empty, set it to 1.0.0.
if ( empty( $current_version ) ) {
$current_version = '1.0.0';
}
if ( empty( $latest_version ) ) {
$latest_version = '1.0.0';
}
// If latest version is greater than current version, update is available.
if ( version_compare( $latest_version, $current_version, '>' ) ) {
return array(
'current_version' => $current_version,
'latest_version' => $latest_version,
);
}
return false;
}
/**
* Update a snippet from the library.
*
* @param int $snippet_id The ID of the snippet to update.
* @param int $library_id The ID of the library snippet to fetch.
*
* @return array|false Array with success data or false on failure.
*/
public function update_snippet_from_library( $snippet_id, $library_id ) {
// Get snippet data from library.
$library_snippet = $this->grab_snippet_from_api( $library_id );
if ( ! $library_snippet ) {
return false;
}
// Update snippet.
$library_snippet['id'] = $snippet_id;
$snippet = wpcode_get_snippet( $library_snippet );
$result = $snippet->save();
if ( ! $result ) {
return false;
}
// Update local version metadata.
if ( ! empty( $library_snippet['version'] ) ) {
update_post_meta( $snippet_id, '_wpcode_snippet_version', $library_snippet['version'] );
}
// Persist the originating library author from the fresh API response.
// This is a backfill opportunity for snippets imported before that meta
// key existed: a user clicking "Update Available" hits the same API
// endpoint as a new import, so the username is available here too.
if ( ! empty( $library_snippet['username'] ) ) {
update_post_meta( $snippet_id, '_wpcode_library_author', sanitize_key( $library_snippet['username'] ) );
}
return array(
'success' => true,
'version' => ! empty( $library_snippet['version'] ) ? $library_snippet['version'] : '',
);
}
/**
* Search snippets in the library by keyword.
*
* @param string $keyword The keyword to search for.
*
* @return array Array of matching snippets.
*/
public function search_snippets( $keyword ) {
$data = $this->get_data();
$all_snippets = isset( $data['snippets'] ) ? $data['snippets'] : array();
$results = array();
if ( empty( $all_snippets ) || ! is_array( $all_snippets ) ) {
return $results;
}
foreach ( $all_snippets as $snippet ) {
$found = false;
// Search in title.
if ( isset( $snippet['title'] ) && stripos( $snippet['title'], $keyword ) !== false ) {
$found = true;
}
// Search in description/note.
if ( ! $found && isset( $snippet['note'] ) && stripos( $snippet['note'], $keyword ) !== false ) {
$found = true;
}
// Search in tags.
if ( ! $found && isset( $snippet['tags'] ) && is_array( $snippet['tags'] ) ) {
foreach ( $snippet['tags'] as $tag ) {
if ( stripos( $tag, $keyword ) !== false ) {
$found = true;
break;
}
}
}
if ( $found ) {
$results[] = $snippet;
}
}
return $results;
}
/**
* Get the list of snippets that have updates available.
* Checks on the fly using cached data.
*
* @return array
*/
public function get_snippets_with_updates() {
// Get all snippets with library IDs.
$library_snippets = $this->get_used_library_snippets();
$snippets_with_updates = array();
// Check library snippets.
foreach ( $library_snippets as $library_id => $snippet_id ) {
$update_info = $this->check_snippet_update( $snippet_id, $library_id );
if ( $update_info ) {
$snippets_with_updates[] = $snippet_id;
}
}
/**
* Filter the list of snippets with updates.
* This allows other classes to add their snippets with updates to the results.
*
* @param array $snippets_with_updates The list of snippets with updates.
*/
return apply_filters( 'wpcode_snippets_with_updates', $snippets_with_updates );
}
}