├── css └── unpublish.css ├── inc └── templates │ └── unpublish-ui.tpl.php ├── js └── unpublish.js ├── readme.md └── unpublish.php /css/unpublish.css: -------------------------------------------------------------------------------- 1 | #unpublish-timestamp:before { 2 | color: #82878c; 3 | top: -1px; 4 | margin-right: .25em; 5 | } 6 | 7 | .unpublish-timestamp-wrap input, 8 | .unpublish-timestamp-wrap select { 9 | font-size: 12px; 10 | padding: 1px; 11 | background-color: #fff; 12 | color: #32373c; 13 | outline: none; 14 | -webkit-transition: 0.05s border-color ease-in-out; 15 | transition: 0.05s border-color ease-in-out; 16 | } 17 | 18 | #unpublish-mm { 19 | height: 21px; 20 | line-height: 14px; 21 | padding: 0; 22 | vertical-align: top; 23 | } 24 | 25 | #unpublish-jj, 26 | #unpublish-hh, 27 | #unpublish-mn { 28 | width: 2em; 29 | } 30 | -------------------------------------------------------------------------------- /inc/templates/unpublish-ui.tpl.php: -------------------------------------------------------------------------------- 1 |
2 | %s', 'unpublish' ), $unpublish_date ); // xss ok ?> 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 26 | , 33 | @ 34 | 38 | : 39 | 43 |

44 | 45 | 46 | 47 |

48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /js/unpublish.js: -------------------------------------------------------------------------------- 1 | /* global unpublish */ 2 | 3 | (function( $ ) { 4 | var $editLink = $( '.edit-unpublish-timestamp' ); 5 | var $fieldset = $( '#unpublish-timestampdiv' ); 6 | var $stamp = $( '#unpublish-timestamp strong' ); 7 | var originalStamp = $stamp.text(); 8 | 9 | var hideFieldset = function() { 10 | $fieldset.slideUp( 'fast' ); 11 | } 12 | 13 | var showFieldset = function() { 14 | $fieldset.slideDown( 'fast' ); 15 | } 16 | 17 | var validate = function() { 18 | var attemptedDate, originalDate, 19 | now = new Date(), 20 | aa = $('#unpublish-aa').val(), 21 | mm = $('#unpublish-mm').val(), 22 | jj = $('#unpublish-jj').val(), 23 | hh = $('#unpublish-hh').val(), 24 | mn = $('#unpublish-mn').val(); 25 | 26 | // The unpublish date has just been cleared. 27 | if ( aa === '' && mm === '' && jj === '' && hh === '' && mn === '' ) { 28 | return true; 29 | } 30 | 31 | attemptedDate = new Date( aa, mm - 1, jj, hh, mn ); 32 | originalDate = new Date( 33 | $( '.unpublish-aa-orig' ).val(), 34 | $( '.unpublish-mm-orig' ).val() -1, 35 | $( '.unpublish-jj-orig' ).val(), 36 | $( '.unpublish-hh-orig' ).val(), 37 | $( '.unpublish-mn-orig' ).val() 38 | ); 39 | 40 | // No change made. 41 | if ( attemptedDate.getTime() === originalDate.getTime() ) { 42 | $stamp.html( originalStamp ); 43 | 44 | return true; 45 | } 46 | 47 | if ( attemptedDate.getFullYear() != aa 48 | || (1 + attemptedDate.getMonth()) != mm 49 | || attemptedDate.getDate() != jj 50 | || attemptedDate.getMinutes() != mn 51 | || attemptedDate <= now 52 | ) { 53 | return false; 54 | } 55 | 56 | $stamp.html( 57 | unpublish.dateFormat 58 | .replace( '%1$s', $( 'option[value="' + mm + '"]', '#unpublish-mm' ).attr( 'data-text' ) ) 59 | .replace( '%2$s', parseInt( jj, 10 ) ) 60 | .replace( '%3$s', aa ) 61 | .replace( '%4$s', ( '00' + hh ).slice( -2 ) ) 62 | .replace( '%5$s', ( '00' + mn ).slice( -2 ) ) 63 | ); 64 | 65 | return true; 66 | } 67 | 68 | $editLink.on( 'click', function( e ) { 69 | e.preventDefault(); 70 | $editLink.hide(); 71 | showFieldset(); 72 | }); 73 | 74 | $( '.cancel-unpublish-timestamp' ).on( 'click', function( e ) { 75 | e.preventDefault(); 76 | $editLink.show(); 77 | hideFieldset(); 78 | 79 | $stamp.html( originalStamp ); 80 | 81 | $fieldset.find( ':input' ).each( function( i, input ) { 82 | var $input = $( input ); 83 | var originalVal = $( '.' + $input.attr( 'name' ) + '-orig' ).val(); 84 | 85 | $input.val( originalVal ); 86 | }); 87 | }); 88 | 89 | $( '.clear-unpublish-timestamp' ).on( 'click', function( e ) { 90 | e.preventDefault(); 91 | $fieldset.find( ':input' ).val( '' ); 92 | $stamp.html( '—' ); 93 | }); 94 | 95 | $( '.save-unpublish-timestamp' ).on( 'click', function( e ) { 96 | e.preventDefault(); 97 | 98 | var isValid = validate(); 99 | 100 | if ( isValid ) { 101 | $fieldset.removeClass( 'form-invalid' ); 102 | $editLink.show(); 103 | hideFieldset(); 104 | } else { 105 | $fieldset.addClass( 'form-invalid' ); 106 | } 107 | }); 108 | })( jQuery ); 109 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 12 | 13 |
4 | Unpublish
5 | Adds a setting under Publish in the WordPress post editor screen to allow unpublishing a post at a given date. 6 |
10 | A Human Made project. 11 |
14 | 15 | Have you ever wished you could set a post to be unpublished automatically at a preset date? 16 | Well now you can! 17 | 18 | Introducing **Unpublish** 19 | ========== 20 | 21 | This plugin does one thing : it adds a setting to the post editor screen under the **Publish** section which allows you to enter a date by which the post will not be published anymore. 22 | 23 | Installation 24 | ========== 25 | 26 | 1. Upload the `unpublish` folder to the `/wp-content/plugins/` directory or just upload the ZIP package via `Plugins > Add New > Upload` in your WP Admin. 27 | 1. Activate the plugin through the`Plugins` menu in WordPress. 28 | 1. Place `add_post_type_support( 'post', 'unpublish' );` in your theme's functions.php for example. 29 | 30 | Frequently Asked Questions 31 | ========== 32 | 33 | ## Does it work with any post type? 34 | Yes, as long as you modify the code accordingly: 35 | 36 | $types = array( 'post', 'page' ); 37 | foreach( $types as $type ) { 38 | add_post_type_support( $type, 'unpublish' );` 39 | } 40 | 41 | Changelog 42 | ======= 43 | = 1.3 = 44 | * Fix bug where unpublished posts can not be restored from trash. Props [ginaliao](https://github.com/ginaliao). 45 | 46 | = 1.2 = 47 | * Protect meta key 48 | 49 | = 1.1 = 50 | * Move unpublish field to appear immediatly below the publish date. 51 | * Alter schedule as post meta updates. 52 | * Match formatting of unpublish and publish time. 53 | * Ensure schedule is removed when posts are trashed. 54 | 55 | Credits 56 | ======= 57 | Created by [Human Made](https://hmn.md/) for high volume and large-scale sites. Thanks to all our [contributors](https://github.com/humanmade/Unpublish/graphs/contributors). 58 | 59 | Interested in joining in on the fun? [Join us, and become human!](https://hmn.md/is/hiring/) 60 | -------------------------------------------------------------------------------- /unpublish.php: -------------------------------------------------------------------------------- 1 | $method(); 30 | } 31 | } 32 | } 33 | return self::$instance; 34 | } 35 | 36 | private function __construct() { 37 | /** Prevent the class from being loaded more than once **/ 38 | } 39 | 40 | /** 41 | * Set up variables associated with the plugin 42 | */ 43 | private function setup_variables() { 44 | $this->file = __FILE__; 45 | $this->basename = plugin_basename( $this->file ); 46 | $this->plugin_dir = plugin_dir_path( $this->file ); 47 | $this->plugin_url = plugin_dir_url( $this->file ); 48 | $this->cron_frequency = 'twicedaily'; 49 | } 50 | 51 | /** 52 | * Set up action associated with the plugin 53 | */ 54 | private function setup_actions() { 55 | 56 | add_action( 'load-post.php', array( self::$instance, 'action_load_customizations' ) ); 57 | add_action( 'load-post-new.php', array( self::$instance, 'action_load_customizations' ) ); 58 | add_action( 'added_post_meta', array( self::$instance, 'update_schedule' ), 10, 4 ); 59 | add_action( 'updated_post_meta', array( self::$instance, 'update_schedule' ), 10, 4 ); 60 | add_action( 'deleted_post_meta', array( self::$instance, 'remove_schedule' ), 10, 3 ); 61 | add_action( 'trashed_post', array( self::$instance, 'unschedule_unpublish' ) ); 62 | add_action( 'untrashed_post', array( self::$instance, 'reschedule_unpublish' ) ); 63 | add_action( self::$cron_key, array( self::$instance, 'unpublish_post' ) ); 64 | add_filter( 'is_protected_meta', array( self::$instance, 'protect_meta_key' ), 10, 3 ); 65 | 66 | if ( wp_next_scheduled( self::$deprecated_cron_key ) ) { 67 | add_action( self::$deprecated_cron_key, array( self::$instance, 'unpublish_content' ) ); 68 | } 69 | } 70 | 71 | /** 72 | * Load any / all customizations to the admin 73 | */ 74 | public function action_load_customizations() { 75 | 76 | $post_type = get_current_screen()->post_type; 77 | if ( post_type_supports( $post_type, self::$supports_key ) ) { 78 | add_action( 'post_submitbox_misc_actions', array( self::$instance, 'render_unpublish_ui' ), 1 ); 79 | add_action( 'admin_enqueue_scripts', array( self::$instance, 'enqueue_scripts_styles' ) ); 80 | add_action( 'save_post_' . $post_type, array( self::$instance, 'action_save_unpublish_timestamp' ) ); 81 | } 82 | 83 | } 84 | 85 | /** 86 | * Get month names 87 | * 88 | * global WP_Locale $wp_locale 89 | * 90 | * @return array Array of month names. 91 | */ 92 | protected function get_month_names() { 93 | global $wp_locale; 94 | 95 | $month_names = []; 96 | 97 | for ( $i = 1; $i < 13; $i = $i + 1 ) { 98 | $month_num = zeroise( $i, 2 ); 99 | $month_text = $wp_locale->get_month_abbrev( $wp_locale->get_month( $i ) ); 100 | $month_names[] = array( 101 | 'value' => $month_num, 102 | 'text' => $month_text, 103 | 'label' => sprintf( _x( '%1$s-%2$s', 'month number-name', 'unpublish' ), $month_num, $month_text ), 104 | ); 105 | } 106 | 107 | return $month_names; 108 | } 109 | 110 | /** 111 | * Get post unpublish timestamp 112 | * 113 | * @param int $post_id Post ID. 114 | * @return string Timestamp. 115 | */ 116 | private function get_unpublish_timestamp( $post_id ) { 117 | return get_post_meta( $post_id, self::$post_meta_key, true ); 118 | } 119 | 120 | /** 121 | * Render the UI for changing the unpublish time of a post 122 | */ 123 | public function render_unpublish_ui() { 124 | 125 | $unpublish_timestamp = $this->get_unpublish_timestamp( get_the_ID() ); 126 | if ( ! empty( $unpublish_timestamp ) ) { 127 | $local_timestamp = strtotime( get_date_from_gmt( date( 'Y-m-d H:i:s', $unpublish_timestamp ) ) ); 128 | /* translators: Unpublish box date format, see https://secure.php.net/date */ 129 | $datetime_format = __( 'M j, Y @ H:i', 'unpublish' ); 130 | $unpublish_date = date_i18n( $datetime_format, $local_timestamp ); 131 | $date_parts = array( 132 | 'jj' => date( 'd', $local_timestamp ), 133 | 'mm' => date( 'm', $local_timestamp ), 134 | 'aa' => date( 'Y', $local_timestamp ), 135 | 'hh' => date( 'H', $local_timestamp ), 136 | 'mn' => date( 'i', $local_timestamp ), 137 | ); 138 | } else { 139 | $unpublish_date = '—'; 140 | $date_parts = array( 141 | 'jj' => '', 142 | 'mm' => '', 143 | 'aa' => '', 144 | 'hh' => '', 145 | 'mn' => '', 146 | ); 147 | } 148 | 149 | $vars = array( 150 | 'unpublish_date' => $unpublish_date, 151 | 'month_names' => $this->get_month_names(), 152 | 'date_parts' => $date_parts, 153 | 'date_units' => array( 'aa', 'mm', 'jj', 'hh', 'mn' ), 154 | ); 155 | 156 | echo $this->get_view( 'unpublish-ui', $vars ); // xss ok 157 | } 158 | 159 | /** 160 | * Enqueue scripts & styles 161 | */ 162 | public function enqueue_scripts_styles() { 163 | wp_enqueue_style( 'unpublish', plugins_url( 'css/unpublish.css', __FILE__ ), array(), '0.1-alpha' ); 164 | wp_enqueue_script( 'unpublish', plugins_url( 'js/unpublish.js', __FILE__ ), array( 'jquery' ), '0.1-alpha', true ); 165 | wp_localize_script( 'unpublish', 'unpublish', array( 166 | /* translators: 1: month, 2: day, 3: year, 4: hour, 5: minute */ 167 | 'dateFormat' => __( '%1$s %2$s, %3$s @ %4$s:%5$s', 'unpublish' ), 168 | ) ); 169 | } 170 | 171 | /** 172 | * Add schedule 173 | * 174 | * @param int $meta_id ID of updated metadata entry. 175 | * @param int $object_id Object ID. 176 | * @param string $meta_key Meta key. 177 | * @param mixed $meta_value Meta value. 178 | */ 179 | public function update_schedule( $meta_id, $object_id, $meta_key, $meta_value ) { 180 | if ( self::$post_meta_key !== $meta_key ) { 181 | return; 182 | } 183 | 184 | if ( $meta_value ) { 185 | $this->schedule_unpublish( $object_id, $meta_value ); 186 | } else { 187 | $this->unschedule_unpublish( $object_id ); 188 | } 189 | } 190 | 191 | /** 192 | * Remove schedule 193 | * 194 | * @param array $meta_ids An array of deleted metadata entry IDs. 195 | * @param int $object_id Object ID. 196 | * @param string $meta_key Meta key. 197 | */ 198 | public function remove_schedule( $meta_ids, $object_id, $meta_key ) { 199 | if ( self::$post_meta_key === $meta_key ) { 200 | $this->unschedule_unpublish( $object_id ); 201 | } 202 | } 203 | 204 | /** 205 | * Save the unpublish time for a given post 206 | */ 207 | public function action_save_unpublish_timestamp( $post_id ) { 208 | if ( ! isset( $_POST['unpublish-nonce'] ) || ! wp_verify_nonce( $_POST['unpublish-nonce'], 'unpublish' ) ) { 209 | return; 210 | } 211 | 212 | if ( ! post_type_supports( get_post_type( $post_id ), self::$supports_key ) ) { 213 | return; 214 | } 215 | 216 | if ( ! current_user_can( 'edit_post', $post_id ) ) { 217 | return; 218 | } 219 | 220 | $units = array( 'aa', 'mm', 'jj', 'hh', 'mn' ); 221 | $units_count = count( $units ); 222 | $date_parts = []; 223 | 224 | foreach ( $units as $unit ) { 225 | $key = sprintf( 'unpublish-%s', $unit ); 226 | $date_parts[ $unit ] = $_POST[ $key ]; 227 | } 228 | 229 | $date_parts = array_filter( $date_parts ); 230 | 231 | // The unpublish date has just been cleared. 232 | if ( empty( $date_parts ) ) { 233 | delete_post_meta( $post_id, self::$post_meta_key ); 234 | return; 235 | } 236 | 237 | // Bail if one of the fields is empty. 238 | if ( count( $date_parts ) !== $units_count ) { 239 | return; 240 | } 241 | 242 | $unpublish_date = vsprintf( '%04d-%02d-%02d %02d:%02d:00', $date_parts ); 243 | $valid_date = wp_checkdate( $date_parts['mm'], $date_parts['jj'], $date_parts['aa'], $unpublish_date ); 244 | 245 | if ( ! $valid_date ) { 246 | return; 247 | } 248 | 249 | $timestamp = strtotime( get_gmt_from_date( $unpublish_date ) ); 250 | 251 | update_post_meta( $post_id, self::$post_meta_key, $timestamp ); 252 | } 253 | 254 | /** 255 | * Unpublish post 256 | * 257 | * Invoked by cron 'unpublish_post_cron' event. 258 | * 259 | * @param int $post_id Post ID. 260 | */ 261 | public function unpublish_post( $post_id ) { 262 | $unpublish_timestamp = (int) $this->get_unpublish_timestamp( $post_id ); 263 | 264 | if ( $unpublish_timestamp > time() ) { 265 | $this->schedule_unpublish( $post_id, $unpublish_timestamp ); 266 | return; 267 | } 268 | 269 | wp_trash_post( $post_id ); 270 | } 271 | 272 | /** 273 | * Unschedule unpublishing post 274 | * 275 | * @param int $post_id Post ID. 276 | */ 277 | public function unschedule_unpublish( $post_id ) { 278 | wp_clear_scheduled_hook( self::$cron_key, array( $post_id ) ); 279 | } 280 | 281 | /** 282 | * Schedule unpublishing post 283 | * 284 | * @param int $post_id Post ID. 285 | * @param int $timestamp Timestamp. 286 | */ 287 | public function schedule_unpublish( $post_id, $timestamp ) { 288 | $this->unschedule_unpublish( $post_id ); 289 | 290 | if ( $timestamp > current_time( 'timestamp', true ) ) { 291 | wp_schedule_single_event( $timestamp, self::$cron_key, array( $post_id ) ); 292 | } 293 | } 294 | 295 | /** 296 | * Reschedule unpublishing post 297 | * 298 | * @param int $post_id Post ID. 299 | */ 300 | public function reschedule_unpublish( $post_id ) { 301 | $timestamp = $this->get_unpublish_timestamp( $post_id ); 302 | 303 | if ( $timestamp ) { 304 | $this->schedule_unpublish( $post_id, $timestamp ); 305 | } 306 | } 307 | 308 | /** 309 | * Unpublish any content that needs unpublishing 310 | */ 311 | public function unpublish_content() { 312 | global $_wp_post_type_features; 313 | 314 | $post_types = array(); 315 | foreach ( $_wp_post_type_features as $post_type => $features ) { 316 | if ( ! empty( $features[ self::$supports_key ] ) ) { 317 | $post_types[] = $post_type; 318 | } 319 | } 320 | 321 | $args = array( 322 | 'fields' => 'ids', 323 | 'post_type' => $post_types, 324 | 'post_status' => 'any', 325 | 'posts_per_page' => 40, 326 | 'meta_query' => array( 327 | array( 328 | 'meta_key' => self::$post_meta_key, 329 | 'meta_value' => current_time( 'timestamp' ), 330 | 'compare' => '<', 331 | 'type' => 'NUMERIC', 332 | ), 333 | array( 334 | 'meta_key' => self::$post_meta_key, 335 | 'meta_value' => current_time( 'timestamp' ), 336 | 'compare' => 'EXISTS', 337 | ), 338 | ), 339 | ); 340 | $query = new WP_Query( $args ); 341 | 342 | if ( $query->have_posts() ) { 343 | foreach ( $query->posts as $post_id ) { 344 | wp_trash_post( $post_id ); 345 | } 346 | } else { 347 | // There are no posts scheduled to unpublish, we can safely remove the old cron. 348 | wp_clear_scheduled_hook( self::$deprecated_cron_key ); 349 | } 350 | } 351 | 352 | /** 353 | * Protect meta key so it doesn't show up on Custom Fields meta box 354 | * 355 | * @param bool $protected Whether the key is protected. Default false. 356 | * @param string $meta_key Meta key. 357 | * @param string $meta_type Meta type. 358 | * 359 | * @return bool 360 | */ 361 | public function protect_meta_key( $protected, $meta_key, $meta_type ) { 362 | if ( $meta_key === self::$post_meta_key && 'post' === $meta_type ) { 363 | $protected = true; 364 | } 365 | 366 | return $protected; 367 | } 368 | 369 | /** 370 | * Get a given view (if it exists) 371 | * 372 | * @param string $view The slug of the view 373 | * @return string 374 | */ 375 | public function get_view( $view, $vars = array() ) { 376 | 377 | if ( isset( $this->template_dir ) ) { 378 | $template_dir = $this->template_dir; 379 | } else { 380 | $template_dir = $this->plugin_dir . '/inc/templates/'; 381 | } 382 | 383 | $view_file = $template_dir . $view . '.tpl.php'; 384 | if ( ! file_exists( $view_file ) ) { 385 | return ''; 386 | } 387 | 388 | extract( $vars, EXTR_SKIP ); 389 | ob_start(); 390 | include $view_file; 391 | return ob_get_clean(); 392 | } 393 | } 394 | 395 | /** 396 | * Load the plugin 397 | */ 398 | function unpublish() { 399 | return Unpublish::get_instance(); 400 | } 401 | add_action( 'plugins_loaded', 'unpublish' ); 402 | --------------------------------------------------------------------------------