Skip to content

Instantly share code, notes, and snippets.

@raamdev
Last active February 28, 2020 17:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raamdev/5bc8be73ba16a725674854e6f33b91d7 to your computer and use it in GitHub Desktop.
Save raamdev/5bc8be73ba16a725674854e6f33b91d7 to your computer and use it in GitHub Desktop.
<?php
/**
* Plugin Name: Bit.ly
* Version: 1.2
* Author: Micah Ernst, Bradford Campeau-Laurion (Alley Interactive)
* Description: Uses bit.ly API to get shortened url for a post on publish and saves url as meta data. Based on TIME.com's Bit.ly plugin.
*/
if ( defined( 'WP_CLI' ) && WP_CLI )
require dirname( __FILE__ ) . '/class-wp-cli.php';
class Bitly {
// storing a copy of the api credentials
var $options;
// default post types
var $post_types = array( 'post', 'page' );
/**
* Set our options and some hooks
*/
function __construct() {
$this->options = $this->get_options();
add_action( 'admin_menu', array( $this, 'admin_menu') );
add_action( 'init', array( $this, 'init' ), 99 ); // run later after post_types have been registered
// only hook into the save_post hook if api credentials have been specified
if( !empty( $this->options['access_token'] ) || ( !empty( $this->options['api_login'] ) && !empty( $this->options['api_key'] ) ) ) {
add_action( 'save_post', array( $this, 'save_post' ), 50, 2 );
}
}
/**
* Add our post type support and allow post types to be filtered
*/
function init() {
// allow other post types to be supported
$this->post_types = (array) apply_filters( 'bitly_post_types', $this->post_types );
// default supported post types
foreach( $this->post_types as $post_type ) {
add_post_type_support( $post_type, 'bitly' );
}
}
/**
* Respond to save_post hook and generate a Bitly url
*
* This happens on save_post rather than publish_post because other plugins
* may have dependencies on the Bitly url before the post is published - for example,
* Publicize generates the Publicize message on save (regardless of status) and
* relies on Bitly being generated to correctly create the message body
*
* Bitly link is only generated if it doesn't already exist
*
* @param int $post_id
* @param object $post
*/
function save_post( $post_id, $post ) {
if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
return;
if ( ! bitly_is_url_generation_enabled() )
return;
// only save short urls for the following post types
if( !post_type_supports( $post->post_type, 'bitly' ) )
return;
// all good, lets make a url
$this->generate_bitly_url( $post_id );
}
/**
* Checks the post's status and creates a bitly url if it's publishing for the first time
*
* @deprecated Deprecated since 1.1, in favor of save_post()
*
* @param int $post_id
* @param object $post
*/
function publish_post( $post_id, $post ) {
return $this->save_post( $post_id, $post );
}
/**
* Create a bitly url if one doesnt already exist for the passed post id
*
* @param int $post_id
*
* @return mixed
*/
function generate_bitly_url( $post_id ) {
$bitly_url = bitly_get_url( $post_id );
if( empty( $bitly_url ) ) {
$permalink = get_permalink( $post_id );
$shortlink = $this->shortlink_for_url( $permalink, $post_id );
if ( $shortlink )
update_post_meta( $post_id, 'bitly_url', $shortlink );
}
return false;
}
/**
* Create a bitly url if one doesnt already exist for the current blog
*
* @return mixed
*/
function generate_bitly_blog_url() {
$bitly_blog_url = bitly_get_blog_url();
if( empty( $bitly_blog_url ) ) {
$shortlink = $this->shortlink_for_url( home_url() );
if ( $shortlink )
update_option( 'bitly_siteurl', $shortlink );
}
return false;
}
function shortlink_for_url( $url, $post_id = null ) {
// Bitly API v4
if( !empty( $this->options['access_token'] ) ) {
$rest_url = 'https://api-ssl.bitly.com/v4/shorten';
$headers = array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->options['access_token']
);
$req_args = array(
'headers' => $headers,
'body' => json_encode(array(
'long_url' => $url
))
);
// Allow API credentials and other options to be switched
$req_args = (array) apply_filters( 'bitly_request_args', $req_args, $post_id );
$response = wp_remote_post( $rest_url, $req_args );
if( ! is_wp_error( $response ) ) {
$json = json_decode( wp_remote_retrieve_body( $response ) );
if( isset( $json->link ) )
return $json->link;
}
}
// Fallback: Deprecated Bitly API v3
elseif ( !empty( $this->options['api_login'] ) && !empty( $this->options['api_key'] ) ) {
$params = array(
'login' => $this->options['api_login'],
'apiKey' => $this->options['api_key'],
'longUrl' => $url,
'format' => 'json',
);
// Allow API credentials and other options to be switched
$params = (array) apply_filters( 'bitly_http_options', $params, $post_id );
$params = http_build_query( $params );
$rest_url = 'https://api-ssl.bitly.com/v3/shorten?' . $params;
$response = wp_remote_get( $rest_url );
// if we get a valid response, save the url as meta data for this post
if( ! is_wp_error( $response ) ) {
$json = json_decode( wp_remote_retrieve_body( $response ) );
if( isset( $json->data->url ) )
return $json->data->url;
}
}
return false;
}
/**
* Wrapper function to get our bitly options
*/
function get_options() {
return wp_parse_args( get_option('bitly_settings'), array(
'access_token' => '',
'api_login' => '',
'api_key' => ''
));
}
/**
* Register a submenu page and the settings fields we'll use on that page
*/
function admin_menu() {
// reg our section
add_settings_section( 'v4_api', 'API Credentials', '__return_false', 'bitly-options' );
add_settings_section( 'v3_api', 'API Credentials (DEPRECATED v3)', '__return_false', 'bitly-options' );
// Get login/key
$access_token = isset( $this->options['access_token'] ) ? $this->options['access_token'] : '';
$login = isset( $this->options['api_login'] ) ? $this->options['api_login'] : '';
$key = isset( $this->options['api_key'] ) ? $this->options['api_key'] : '';
// create an api login and key field
add_settings_field( 'bitly_access_token', 'Access Token', array( $this, 'textfield' ), 'bitly-options', 'v4_api', array(
'name' => 'bitly_settings[access_token]',
'value' => $access_token,
));
add_settings_field( 'bitly_api_login', 'API Login (DEPRECATED)', array( $this, 'textfield' ), 'bitly-options', 'v3_api', array(
'name' => 'bitly_settings[api_login]',
'value' => $login,
));
add_settings_field( 'bitly_api_key', 'API Key (DEPRECATED)', array( $this, 'textfield' ), 'bitly-options', 'v3_api', array(
'name' => 'bitly_settings[api_key]',
'value' => $key,
));
// set our validation callback
register_setting( 'bitly_settings', 'bitly_settings', array( $this, 'validate_settings' ) );
// create a sub menu page within settings menu page
add_submenu_page( 'options-general.php', 'Bit.ly Settings', 'Bit.ly', 'edit_theme_options', 'bitly-settings', array( $this, 'settings_page' ) );
}
/**
* Builds a simple text field
*/
function textfield( $args ) {
$args = wp_parse_args( $args, [ 'name' => null, 'value' => null ] );
?>
<input type="text" name="<?php echo esc_attr( $args['name'] ); ?>" value="<?php echo esc_attr( $args['value'] ); ?>" class="regular-text"/>
<?php
}
/**
* Sanitize the values the user entered on our settings page
*/
function validate_settings( $input ) {
$output = array();
$output['access_token'] = sanitize_text_field( $input['access_token'] );
$output['api_login'] = sanitize_text_field( $input['api_login'] );
$output['api_key'] = sanitize_text_field( $input['api_key'] );
return $output;
}
/**
* Build the html for our settings screen
*/
function settings_page() {
?>
<div class="wrap">
<div id="icon-options-general" class="icon32"><br></div>
<h2>Bit.ly Settings</h2>
<form action="options.php" method="post">
<?php
wp_nonce_field( 'bitly_settings', 'bitly_settings_nonce', false );
settings_fields( 'bitly_settings' );
do_settings_sections( 'bitly-options' );
?>
<p class="submit">
<input type="submit" name="submit" class="button-primary" value="Save Changes"/>
</p>
</form>
</div>
<?php
}
}
$bitly = new Bitly();
/**
* Helper function to get the short url for a post
*
* @param int $post_id
* @return string $url
*/
function bitly_get_url( $post_id = null ) {
$post_id = empty( $post_id ) ? get_the_ID() : $post_id;
return get_post_meta( $post_id, 'bitly_url', true );
}
/**
* Helper function to get the short url for a blog
*
* @return string $url
*/
function bitly_get_blog_url() {
return get_option( 'bitly_siteurl' );
}
/**
* Generate short_url for use in bitly_process_posts()
*/
function bitly_generate_short_url( $post_id ) {
global $bitly;
if ( ! bitly_is_url_generation_enabled() )
return false;
if ( is_object( $bitly ) && is_callable( array( $bitly, 'generate_bitly_url' ) ) )
return call_user_func( array( $bitly, 'generate_bitly_url' ), $post_id );
return false;
}
/**
* Generate a short url for the current blog's homepage
*/
function bitly_generate_blog_short_url() {
global $bitly;
if ( ! bitly_is_url_generation_enabled() )
return false;
if ( is_object( $bitly ) && is_callable( array( $bitly, 'generate_bitly_blog_url' ) ) )
return call_user_func( array( $bitly, 'generate_bitly_blog_url' ) );
return false;
}
/**
* Filter to replace the default shortlink
*/
function bitly_shortlink( $shortlink, $id, $context ) {
if ( 'post' == $context || ( 'query' == $context && is_single() ) ) {
if ( 'query' == $context )
$id = get_queried_object_id();
$bitly = bitly_get_url( $id );
if( $bitly ) $shortlink = esc_url( $bitly );
} elseif ( 'query' == $context && ( is_home() || is_front_page() ) ) {
$bitly = bitly_get_blog_url();
if ( $bitly ) $shortlink = esc_url( $bitly );
}
return $shortlink;
}
add_filter( 'pre_get_shortlink', 'bitly_shortlink', 10, 3 );
/**
* Helper to get available post types
*/
function bitly_get_post_types() {
global $bitly;
if( is_object( $bitly ) )
return $bitly->post_types;
else
return array( 'post', 'page' );
}
/**
* Cron to process all of the posts that don't have bitly urls
*/
function bitly_process_posts( $hourly_limit = null ) {
global $wpdb;
if ( ! bitly_is_url_generation_enabled() ) {
bitly_log( 'Bit.ly backfill is currently disabled via code' );
return;
}
// Check if we should even be running this
$bitly_processed = get_option( 'bitly_processed' );
if ( ! empty( $bitly_processed ) ) {
bitly_log( "All bit.ly URLs were processed. Run reset_process_status if you think this is in error and try again." );
return;
}
// Use the default limit if one was not set
if ( empty( $hourly_limit ) || ! is_numeric( $hourly_limit ) )
$hourly_limit = apply_filters( 'bitly_hourly_limit', 100 );
// Generate a shortlink for the homepage, if it doesn't exist
$blog_shortlink = bitly_get_blog_url();
if ( ! $blog_shortlink ) {
bitly_log( "Set short URL for blog" );
bitly_generate_blog_short_url();
}
$post_type_sql = "";
// get the post types that are supported
$post_types = bitly_get_post_types();
// build the sql for querying post types
if( count( $post_types ) ) {
foreach( $post_types as $post_type ) {
$sanitized_post_types[] = $wpdb->prepare( '%s', $post_type );
}
$post_type_sql = sprintf( '%s IN ( %s )', "$wpdb->posts.post_type", implode( ',', $sanitized_post_types ) );
}
// Only do the query if there's post_type sql
if( ! empty( $post_type_sql ) ) {
bitly_log( "Starting to process posts without bit.ly short URLs with a limit of {$hourly_limit}" );
// Get $limit published posts that don't have a bitly url
// Only query for a maximum of 100 posts at a time
$processed = 0;
$per_page = 100;
do {
$query = "
SELECT $wpdb->posts.ID
FROM $wpdb->posts
LEFT JOIN $wpdb->postmeta ON ( $wpdb->posts.ID = $wpdb->postmeta.post_id AND $wpdb->postmeta.meta_key = 'bitly_url' )
WHERE 1=1
AND ( $post_type_sql )
AND ( $wpdb->posts.post_status = 'publish' )
AND ( $wpdb->postmeta.post_id IS NULL )
GROUP BY $wpdb->posts.ID
ORDER BY $wpdb->posts.post_date DESC
LIMIT $per_page
";
// Get the posts
$posts = $wpdb->get_results( $query );
// Increment the counter
$processed += count( $posts );
// This could be empty if there was no $post_type_sql
if ( ! empty( $posts ) ) {
// Process these posts
foreach( $posts as $p ) {
bitly_log( "Generating short_url for post ID {$p->ID}" );
bitly_generate_short_url( $p->ID );
}
} else {
// Kill our scheduled event
bitly_log( "No posts were found. Killing the event." );
bitly_processed();
}
bitly_log( "Processed {$processed} posts" );
sleep( 2 );
} while ( count( $posts ) && $processed < $hourly_limit );
} else {
// Kill our scheduled event
bitly_log( "No bit.ly post types were found. Killing the event." );
bitly_processed();
}
// If $per_page isn't equal to the number of posts found on the last run, we should disable this forever
if ( $per_page != count( $posts ) ) {
bitly_log( "All bit.ly posts were processed. Killing the event." );
bitly_processed();
}
}
// Enable backfill for posts that don't have a bitly url
add_action( 'init', 'bitly_init_post_backfill' );
function bitly_init_post_backfill() {
if ( ! bitly_is_url_generation_enabled() )
return;
add_action( 'bitly_hourly_hook', 'bitly_process_posts' );
$bitly_processed = get_option( 'bitly_processed' );
$blog_shortlink = get_option( 'bitly_siteurl' );
if ( ( ! $bitly_processed || ! $blog_shortlink ) && ! wp_next_scheduled( 'bitly_hourly_hook' ) )
wp_schedule_event( time() + 30, 'hourly', 'bitly_hourly_hook' );
}
/**
* Should we actually generate bit.ly urls for posts?
*
* Generation can be disabled for dev environments, where generating bit.ly urls would create links
* to a dev address.
*
* @return bool Boolean indicating if bit.ly urls should be generated
*/
function bitly_is_url_generation_enabled() {
return (bool) apply_filters( 'bitly_enable_url_generation', true );
}
/**
* Disables the backfill process from running in the future because no URLs remain
*
* @return void
*/
function bitly_processed() {
update_option( 'bitly_processed', 1 );
wp_clear_scheduled_hook( 'bitly_hourly_hook' );
}
/**
* Helper function to log output if the backfill is being executed from a CLI script
*
* @param string $message
* @return void
*/
function bitly_log( $message ) {
if ( defined( 'WP_CLI' ) && WP_CLI )
WP_CLI::line( $message );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment