WordPress.org

Plugin Directory

Ticket #1440: grist-authors.php

File grist-authors.php, 17.9 KB (added by danielbachhuber, 2 years ago)

Possible example to follow: Grist's bylines plugin written by nacin

Line 
1<?php
2/* Plugin Name: Grist Authors
3 * Description: Handles a special 'Author' post type and co-authors for posts.
4 * Author: Andrew Nacin
5 * Author URI: http://andrewnacin.com/
6 */
7
8class Grist_Authors {
9
10        static function init() {
11                add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
12                add_action( 'init', array( __CLASS__, 'register_post_type' ) );
13                add_action( 'add_meta_boxes_author', array( __CLASS__, 'add_meta_boxes_author' ) );
14                add_action( 'admin_head-post-new.php', array( __CLASS__, 'header_inline' ) );
15                add_action( 'admin_head-post.php', array( __CLASS__, 'header_inline' ) );
16                add_action( 'admin_footer-post-new.php', array( __CLASS__, 'footer_inline' ) );
17                add_action( 'admin_footer-post.php', array( __CLASS__, 'footer_inline' ) );
18                add_action( 'save_post', array( __CLASS__, 'save_post_type_author' ), 10, 2 );
19                add_filter( 'request', array( __CLASS__, 'request' ) );
20                add_filter( 'post_row_actions', array( __CLASS__, 'post_row_actions' ), 10, 2 );
21                add_filter( 'page_row_actions', array( __CLASS__, 'post_row_actions' ), 10, 2 ); // while hierarchical = true
22                add_filter( 'bulk_actions-edit-author', array( __CLASS__, 'bulk_actions_author' ) );
23                add_filter( 'manage_post_posts_columns', array( __CLASS__, 'manage_post_posts_columns' ) );
24                add_filter( 'manage_author_posts_columns', array( __CLASS__, 'manage_author_posts_columns' ) );
25                add_action( 'manage_post_posts_custom_column', array( __CLASS__, 'manage_post_posts_custom_column' ), 10, 2 );
26                add_action( 'manage_author_posts_custom_column', array( __CLASS__, 'manage_author_posts_custom_column' ), 10, 2 );
27                add_action( 'add_meta_boxes_post', array( __CLASS__, 'add_meta_boxes_post' ) );
28                add_filter( 'wp_insert_post_data', array( __CLASS__, 'wp_insert_post_data' ), 10, 2 );
29        }
30
31        /**
32         * Forces author-style templates for the /author/ pages, even though these are a post type.
33         *
34         * author.php, archive.php, index.php is the template hierarchy.
35         */
36        static function single_template( $template ) {
37                if ( get_queried_object()->post_type == 'author' )
38                        return locate_template( array( 'author.php', 'archive.php', 'index.php' ) );
39                return $template;
40        }
41
42        /**
43         * Adds an 'Authors' column to after the 'title' field.
44         *
45         * The current 'author' column does not need to be removed, as it isn't there.
46         * remove_post_type_support() all standard author UI in our register_post_type() method.
47         */
48        static function manage_post_posts_columns( $columns ) {
49                $new_columns = array();
50                foreach ( $columns as $column_key => $column_name ) {
51                        $new_columns[ $column_key ] = $column_name;
52                        if ( $column_key == 'title' )
53                                $new_columns['grist_author'] = 'Authors';
54                }
55                return $new_columns;
56        }
57
58        /**
59         * Render the 'Authors' column.
60         *
61         * This caches all author data for all posts on the first run.
62         */
63        static function manage_post_posts_custom_column( $column, $post_id ) {
64                global $wp_query;
65
66                static $cached_users = false;
67
68                if ( ! $cached_users ) {
69                        $pids = array();
70                        foreach ( $wp_query->posts as $post ) {
71                                $pids[] = $post->ID;
72                        }
73                        $author_ids = array();
74                        foreach ( $pids as $pid ) {
75                                $author_ids = array_merge( $author_ids, (array) get_post_meta( $pid, '_grist_author_id', false ) );
76                        }
77                        // We don't care about the return value here, only that the posts end up
78                        // in the cache for future get_post() calls.
79                        get_posts( array( 'post_type' => 'author', 'include' => $author_ids, 'nopaging' => true ) );
80                        unset( $author_ids, $pid, $pids, $post );
81
82                        $cached_users = true;
83                }
84
85                switch ( $column ) {
86                        case 'grist_author' :
87                                $post = get_post( $post_id );
88                                $authors = get_post_meta( $post_id, '_grist_author_id', false );
89                                $output = array();
90                                foreach ( $authors as $author_id ) {
91                                        $author = get_post( $author_id );
92                                        if ( $author->ID == $post->post_author ) {
93                                                echo '<strong>' . esc_html( $author->post_title ) . '</strong><br />';
94                                        } else {
95                                                if ( $author->post_author ) {
96                                                        $output[] = '<a href="' . esc_url( add_query_arg( 'author', $author->post_author ) ) . '">' . esc_html( $author->post_title ) . '</a>';
97                                                } else {
98                                                        $output[] = esc_html( $author->post_title );
99                                                }
100                                        }
101                                }
102                                if ( $output )
103                                        echo implode( "<br />\n", $output );
104                                echo '<br />&nbsp;';
105                                break;
106                }
107        }
108
109        /**
110         * Adds 'Twitter' and 'Linked User Account' columns to the Authors (post type) list table.
111         */
112        static function manage_author_posts_columns( $columns ) {
113                unset( $columns['date'] );
114                $columns['twitter'] = 'Twitter';
115                $columns['user_account'] = 'Linked User Account';
116                return $columns;
117        }
118
119        /**
120         * Renders the 'Twitter' and 'Linked User Account' columns for the Authors (post type) list table.
121         */
122        static function manage_author_posts_custom_column( $column, $post_id ) {
123                switch ( $column ) {
124                        case 'twitter' :
125                                $twitter = get_post_meta( $post_id, '_grist_author_twitter', true );
126                                if ( $twitter )
127                                        echo '<a href="' . esc_url( 'http://twitter.com/' . $twitter ) . '">@' . esc_html( $twitter ) . '</a>';
128                                break;
129                        case 'user_account' :
130                                if ( ! get_the_author_meta( 'ID' ) )
131                                        break;
132                                // Provide direct links to the user on users.php if we can.
133                                if ( current_user_can( 'list_users' ) ) {
134                                        $user_link = add_query_arg( 's', urlencode( get_the_author_meta( 'user_login' ) ), admin_url( 'users.php' ) ) . '#user-' . get_the_author_meta( 'ID' );
135                                        echo '<a href="' . esc_url( $user_link ) . '">' . get_the_author() . '</a>';
136                                } else {
137                                        the_author();
138                                }
139                                break;
140                }
141        }
142
143        /**
144         * For authors (post type), no bulk actions.
145         */
146        static function bulk_actions_author( $actions ) {
147                return array();
148        }
149
150        /**
151         * For authors (post type), no quick edit, trash, or delete action links.
152         */
153        static function post_row_actions( $actions, $post ) {
154                if ( $post->post_type == 'author' )
155                        unset( $actions['inline hide-if-no-js'], $actions['trash'], $actions['delete'] );
156                return $actions;
157        }
158
159        /**
160         * If core's /author/$author/ rewrite rule gets hit, catch it and serve up the post type instead.
161         */
162        static function request( $qvs ) {
163                if ( ! is_admin() && isset( $qvs['author_name'] ) ) {
164                        $qvs['post_type'] = 'author';
165                        $qvs['name'] = $qvs['author_name'];
166                        unset( $qvs['author_name'] );
167                }
168                return $qvs;
169        }
170
171        /**
172         * Register our author post type and removes support for 'author' from the post post_type.
173         */
174        static function register_post_type() {
175                $labels = array(
176                        'name' => 'Authors',
177                        'singular_name' => 'Author',
178                        'add_new' => 'Add Author',
179                        'add_new_item' => 'Add New Author',
180                        'edit_item' => 'Edit Author',
181                        'new_item' => 'New Author',
182                        'view_item' => 'View Author',
183                        'search_items' => 'Search Authors',
184                        'not_found' => 'No author found',
185                        'not_found_in_trash' => 'Sorry, no authors found in the trash',
186                );
187
188                $args = array(
189                        'labels' => $labels,
190                        'public' => true,
191                        'show_ui' => true,
192                        'rewrite' => true,
193                        'query_var' => 'author_name',
194                        'has_archive' => false,
195                        'hierarchical' => true, // hack, so wp_dropdown_pages() works.
196                        'capability_type' => 'page',
197                        'map_meta_cap' => true,
198                        'supports' => array( 'title', 'editor', 'thumbnail' ),
199                        'show_in_menu' => 'users.php',
200                );
201
202                register_post_type( 'author', $args );
203                remove_post_type_support( 'post', 'author' );
204        }
205
206        /**
207         * Add a meta box to our author post type.
208         *
209         * This box ends up holding Twitter and WP.com fields.
210         */
211        static function add_meta_boxes_author( $post ) {
212                add_meta_box( 'grist_author_meta', 'Grist Author Meta', array( __CLASS__, 'render_meta_box_author' ), null, 'side', 'high' );
213        }
214
215        /**
216         * Adds a meta box to posts for co-author assignments.
217         */
218        static function add_meta_boxes_post( $post ) {
219                add_meta_box( 'grist_co_authors', 'Authors', array( __CLASS__, 'render_meta_box_post' ), null, 'side', 'low' );
220        }
221
222        /**
223         * Render the co-authors meta box.
224         */
225        static function render_meta_box_post( $post ) {
226                $authors = get_post_meta( $post->ID, '_grist_author_id', false );
227                $show_option_none = false;
228
229                if ( ! $authors ) {
230                        $authors = Grist_Authors::get_author_id_by_user( get_current_user_id() );
231                        if ( ! $authors )
232                                $show_option_none = '(no author)';
233                        $authors = array( $authors );
234                }
235                $authors[] = 0;
236                $primary = array_shift( $authors );
237
238                wp_dropdown_pages( array(
239                        'show_option_none' => $show_option_none,
240                        'selected' => $primary,
241                        'name' => 'grist_author_id[]',
242                        'id' => 'grist_author_id_0',
243                        'post_type' => 'author',
244                        'echo' => 1,
245                        'sort_column' => 'post_title',
246                ) );
247
248                $i = 1;
249                echo '<div class="grist-co-authors"><p>Co-authors:</p>';
250                foreach ( $authors as $author_id ) {
251                        echo '<div>';
252                        wp_dropdown_pages( array(
253                                'show_option_none' => '(no co-author)',
254                                'selected' => $author_id,
255                                'name' => 'grist_author_id[]',
256                                'id' => 'grist_author_id_' . $i,
257                                'post_type' => 'author',
258                                'echo' => 1,
259                                'sort_column' => 'post_title',
260                        ) );
261                        echo '<a href="#" class="author-remove">Remove</a>';
262                        echo '</div>';
263                        $i++;
264                }
265                echo '</div><p><a href="#" id="author-add">Add</a></p>';
266        }
267
268        /**
269         * Utility function fetches an author ID (post type) when given a WP.com user ID.
270         */
271        static function get_author_id_by_user( $user_id = null ) {
272                if ( empty( $user_id ) )
273                        $user_id = get_current_user_id();
274                $query = new WP_Query( array(
275                        'post_type' => 'author',
276                        'author' => $user_id,
277                        'post_status' => 'publish',
278                        'posts_per_page' => 1,
279                        'update_post_term_cache' => false,
280                        'update_post_meta_cache' => false,
281                        'no_found_rows' => true,
282                        'fields' => 'ids',
283                ) );
284                if ( isset( $query->posts[0] ) )
285                        return $query->posts[0];
286                return 0;
287        }
288
289        /**
290         * Renders the meta box for authors (post type) to hold Twitter and WP.com user information.
291         */
292        static function render_meta_box_author( $post ) {
293                $twitter = get_post_meta( $post->ID, '_grist_author_twitter', true );
294                echo '<p><label>Twitter Handle</label> <input style="width:200px" type="text" value="' . esc_attr( $twitter ) . '" name="grist_author[twitter]" /></p>';
295
296                echo '<p><label>WP.com User</label> ';
297                wp_dropdown_users( array(
298                        'show_option_none' => '(No corresponding user)',
299                        'name' => 'grist_author[author]',
300                        // If we're adding an author or if there is no post author (0), then use -1 (which is show_option_none).
301                        // We then take -1 on save and convert it back to 0. (#blamenacin)
302                        'selected' => 'auto-draft' == $post->post_status || ! $post->post_author ? -1 : $post->post_author,
303                ) );
304                echo '</p>';
305        }
306
307        /**
308         * Drops in some JS on the post edit screen to handle the co-authors UI.
309         */
310        static function footer_inline() {
311                $screen = get_current_screen();
312                if ( 'post' !== $screen->post_type )
313                        return;
314?>
315<script>
316jQuery(document).ready( function($) {
317        var coauthors = $('.grist-co-authors');
318        $('#author-add').click( function(e) {
319                e.preventDefault();
320                coauthors.find('select').first().parent().clone()
321                        .find('select').val('').end()
322                        .find('.author-remove').show().end()
323                        .appendTo( coauthors );
324        });
325        coauthors.delegate( '.author-remove', 'click', function(e) {
326                e.preventDefault();
327                if ( coauthors.find('select').length > 1 )
328                        $(this).parent().remove();
329                else
330                        $(this).prev().val('');
331        });
332});
333</script>
334<?php
335        }
336
337        /**
338         * Adds some information to the featured image box for authors (post type)
339         * that clarifies what size we're looking for.
340         */
341        static function admin_post_thumbnail_html( $content ) {
342                $content .= '<p>(50 pixels &times; 50 pixels, please.)</p>';
343                return $content;
344        }
345
346        /**
347         * Some elegant (and less elegant) hacks for the author edit screen.
348         *
349         *  - Turns off the visual editor, disables the upload/insert media button.
350         *  - Auto-corrects $parent_file and $submenu_file, though changes in 3.3 should make these redundant. (See #WP19125)
351         *  - Hides the delete link (it doesn't need to be prevented with a cap check, only discouraged).
352         *  - Fixes positioning and styling of the editor. Not sure if late changes in 3.3 made these unnecessary.
353         */
354        static function header_inline() {
355                $screen = get_current_screen();
356                switch ( $screen->post_type ) :
357                        case 'author' :
358                                add_filter( 'admin_post_thumbnail_html', array( __CLASS__, 'admin_post_thumbnail_html' ) );
359                                add_filter( 'user_can_richedit', '__return_false' );
360                                remove_action( 'media_buttons', 'media_buttons' );
361                                $GLOBALS['parent_file']  = 'users.php';
362                                $GLOBALS['submenu_file'] = "edit.php?post_type=author";
363                                echo '<style>.misc-pub-section, #delete-action { display: none } #content_resize { top: -2px !important } .wp-editor-container { background: #fff } </style>' . "\n";
364                                break;
365                endswitch;
366        }
367
368        /**
369         * Saving the author post type, specifically Twitter. The WP.com user linkage is handled by
370         * our wp_insert_post_data() method.
371         */
372        static function save_post_type_author( $post_id, $post ) {
373                if ( 'author' != $post->post_type )
374                        return;
375
376                if ( ! isset( $_POST['grist_author'] ) )
377                        return;
378
379                $twitter = $_POST['grist_author']['twitter'];
380                // Sanitize all kinds of possible inputs.
381                $twitter = str_replace( array( 'http://', 'https://', '#!', 'twitter.com', '/', '@' ), '', $twitter );
382                $twitter = sanitize_text_field( $twitter );
383                update_post_meta( $post->ID, '_grist_author_twitter', $twitter );
384        }
385
386        /**
387         * Saving the post and author post types, specifically stuff related to user IDs.
388         *
389         * We need to do this on wp_insert_post_data rather than save_post so have raw,
390         * unadulterated access to post_author.
391         */
392        static function wp_insert_post_data( $post, $args ) {
393                switch ( $post['post_type'] ) :
394                        case 'post' :
395                                // New posts have an ID, this just prevents this from running on auto-draft creation.
396                                if ( ! isset( $args['ID'] ) )
397                                        break;
398
399                                // If author data was submitted:
400                                if ( isset( $_POST['grist_author_id'] ) ) {
401                                        // Make the authors unique, remove the first one, and consider that the primary.
402                                        $ids = array_unique( array_filter( array_map( 'absint', $_POST['grist_author_id'] ) ) );
403                                        $primary = array_shift( $ids );
404                                        $author_object = get_post( $primary );
405
406                                        // If we have a primary author that has a corresponding WP.com user ID, then
407                                        // make the post's post_author keep the WP.com user ID. (Good karma.)
408                                        if ( $author_object && $author_object->post_author )
409                                                $post['post_author'] = $author_object->post_author;
410                                        else
411                                                $post['post_author'] = 0;
412
413                                        // Wipe them all out so we can re-add in order.
414                                        delete_post_meta( $args['ID'], '_grist_author_id' );
415                                        // Add the primary ID to meta, then the rest of them.
416                                        add_post_meta( $args['ID'], '_grist_author_id', $primary );
417                                        foreach ( $ids as $id ) {
418                                                add_post_meta( $args['ID'], '_grist_author_id', $id );
419                                        }
420                                } else {
421                                        // Okay, no author data was submitted.
422                                        // Figure out what is in the DB and do not lose the linked WP.com user.
423                                        // This happens, say, during a quick edit.
424                                        $from_db = get_post( $args['ID'] );
425                                        if ( $from_db )
426                                                $post['post_author'] = $from_db->post_author;
427                                        else
428                                                $post['post_author'] = 0;
429                                }
430                                break;
431                        case 'author' :
432                                // Don't run for auto-drafts. (New posts have IDs.)
433                                if ( ! isset( $args['ID'] ) )
434                                        break;
435
436                                // First, figure out what we have in the DB as the linked WP.com user.
437                                $from_db = get_post( $args['ID'] );
438                                if ( $from_db )
439                                        $post['post_author'] = intval( $from_db->post_author );
440                                else
441                                        $post['post_author'] = 0;
442
443                                // If data was passed on save, then use it. But if post_author was -1
444                                // (which is what the dropdowns use for nothing selected), we can't store
445                                // that in an unsigned int. Clarify we want 0 for no author.
446                                if ( isset( $_POST['grist_author'] ) ) {
447                                        $post['post_author'] = intval( $_POST['grist_author']['author'] );
448                                        if ( $post['post_author'] < 0 )
449                                                $post['post_author'] = 0;
450                                }
451                                break;
452                endswitch;
453                return $post;
454        }
455}
456// Go.
457Grist_Authors::init();
458
459
460
461/*
462TEMPLATE TAGS
463*/
464
465/**
466 * Grist Byline filtering.
467 *
468 * @param string $before  text before the author name
469 * @param string $after  text after the author name
470 * @param array|string $args optional attributes for the link
471 * @param string $prefix text before the link
472 * Use this template tag for bylines:
473 * <p class="byline"><?php grist_byline(); ?></p>
474 */
475function grist_byline( $before = '', $after = '', $args = array(), $prefix = 'By ' ) {
476
477        $author_ids = get_post_meta( get_the_ID(), '_grist_author_id', false );
478
479        if ( empty( $author_ids )  )
480                return;
481
482        if( !empty( $args ))
483        {
484                $args = _parse_html_attributes( $args );
485        }
486
487        $output = array();
488        foreach ( $author_ids as $author_id ) {
489                $author_name = $before . get_the_title( $author_id ) . $after;
490                $output[] = sprintf( '<a href="%s" title="Posts by %s" %s >%s</a>', esc_url( get_permalink( $author_id ) ), esc_attr( $author_name ), $args ,$author_name );
491        }
492        echo $prefix . wp_sprintf( '%l', $output );
493}
494
495function grist_the_author_bios( $before = '<div>', $after = '</div>' ) {
496        if ( get_post_type() == 'author' )
497                $author_ids = array( get_the_ID() );
498        else
499                $author_ids = get_post_meta( get_the_ID(), '_grist_author_id', false );
500
501        foreach ( $author_ids as $author_id ) {
502                $author = get_post( $author_id );
503
504                echo $before . apply_filters('the_content', $author->post_content) . $after . "\n\n";
505        }
506}
507
508function grist_has_multiple_authors() {
509                return 1 < count( (array) get_post_meta( get_the_ID(), '_grist_author_id', false ) );
510}
511
512function grist_get_the_author($post_id='') {
513        $id = ($post_id) ?: get_the_ID();
514        $author_id = 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
515
516        if( !$author_id)
517                return 'Grist';
518
519        return get_the_title( $author_id );
520}
521
522function grist_get_author_feed_link($post_id='') {
523        $id = ($post_id) ?: get_the_ID();
524        $author_id = 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
525        return user_trailingslashit( get_permalink( $author_id ) . '/feed' );
526}
527
528// grist_get_author_meta( 'twitter' )
529function grist_get_the_author_meta( $meta, $post_id='' ) {
530        $id = ($post_id) ?: get_the_ID();
531        $author_id = 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
532        return get_post_meta( $author_id, '_grist_author_' . $meta, true );
533}
534
535function grist_get_author_post_id($post_id='') {
536        $id = ($post_id) ?: get_the_ID();
537        return 'author' == get_post_type($id) ? $id : get_post_meta( $id, '_grist_author_id', true );
538}