├── 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 |
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 |
4 | Unpublish
5 | Adds a setting under Publish in the WordPress post editor screen to allow unpublishing a post at a given date.
6 | |
7 |
8 |
9 |
10 | A Human Made project.
11 | |
12 |
13 |
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 |
--------------------------------------------------------------------------------