<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[43437] trunk: REST API: Declare user capabilities using JSON Hyper Schema's "targetSchema".</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/43437">43437</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/43437","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>pento</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2018-07-11 06:22:10 +0000 (Wed, 11 Jul 2018)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>REST API: Declare user capabilities using JSON Hyper Schema's "targetSchema".

There are a variety of operations a WordPress user can only perform if they have the correct capabilities. A REST API client should only display UI for one of these operations if the WordPress user can perform the operation.

Rather than requiring REST API clients to calculate whether to display UI based on potentially complicated combinations of user capabilities, `targetSchema` allows us to expose a single flag to show whether the corresponding UI should be displayed.

This change also includes flags on post objects for the following actions:

- `action-publish`: The current user can publish this post.
- `action-sticky`: The current user can make this post sticky, and the post type supports sticking.
- `action-assign-author': The current user can change the author on this post.
- `action-assign-{$taxonomy}`: The current user can assign terms from the "$taxonomy" taxonomy to this post.
- `action-create-{$taxonomy}`: The current user can create terms int the "$taxonomy" taxonomy.

Props TimothyBlynJacobs, danielbachhuber.
Fixes <a href="https://core.trac.wordpress.org/ticket/44287">#44287</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestattachmentscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestpostscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestattachmentscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestpostscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesrestapiendpointsclasswprestattachmentscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php 2018-07-09 13:44:53 UTC (rev 43436)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php   2018-07-11 06:22:10 UTC (rev 43437)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -368,11 +368,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $data = $this->filter_response_by_context( $data, $context );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $links = $response->get_links();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Wrap the data in a response object.
</span><span class="cx" style="display: block; padding: 0 10px">                $response = rest_ensure_response( $data );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $response->add_links( $links );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $response->add_links( $this->prepare_links( $post ) );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters an attachment returned from the REST API.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestpostscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php       2018-07-09 13:44:53 UTC (rev 43436)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php 2018-07-11 06:22:10 UTC (rev 43437)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1590,8 +1590,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // Wrap the data in a response object.
</span><span class="cx" style="display: block; padding: 0 10px">                $response = rest_ensure_response( $data );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $response->add_links( $this->prepare_links( $post ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $links = $this->prepare_links( $post );
+               $response->add_links( $links );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( ! empty( $links['self']['href'] ) ) {
+                       $actions = $this->get_available_actions( $post, $request );
+
+                       $self = $links['self']['href'];
+
+                       foreach ( $actions as $rel ) {
+                               $response->add_link( $rel, $self );
+                       }
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters the post data for a response.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1730,6 +1741,60 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Get the link relations available for the post and current user.
+        *
+        * @since 4.9.7
+        *
+        * @param WP_Post $post Post object.
+        * @param WP_REST_Request Request object.
+        *
+        * @return array List of link relations.
+        */
+       protected function get_available_actions( $post, $request ) {
+
+               if ( 'edit' !== $request['context'] ) {
+                       return array();
+               }
+
+               $rels = array();
+
+               $post_type = get_post_type_object( $post->post_type );
+
+               if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) {
+                       $rels[] = 'https://api.w.org/action-publish';
+               }
+
+               if ( 'post' === $post_type->name ) {
+                       if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) {
+                               $rels[] = 'https://api.w.org/action-sticky';
+                       }
+               }
+
+               if ( post_type_supports( $post_type->name, 'author' ) ) {
+                       if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
+                               $rels[] = 'https://api.w.org/action-assign-author';
+                       }
+               }
+
+               $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
+
+               foreach ( $taxonomies as $tax ) {
+                       $tax_base   = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
+                       $create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms;
+
+                       if ( current_user_can( $create_cap ) ) {
+                               $rels[] = 'https://api.w.org/action-create-' . $tax_base;
+                       }
+
+                       if ( current_user_can( $tax->cap->assign_terms ) ) {
+                               $rels[] = 'https://api.w.org/action-assign-' . $tax_base;
+                       }
+               }
+
+               return $rels;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Retrieves the post's schema, conforming to JSON Schema.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2069,10 +2134,126 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $schema_links = $this->get_schema_links();
+
+               if ( $schema_links ) {
+                       $schema['links'] = $schema_links;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return $this->add_additional_fields_schema( $schema );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Retrieve Link Description Objects that should be added to the Schema for the posts collection.
+        *
+        * @since 4.9.7
+        *
+        * @return array
+        */
+       protected function get_schema_links() {
+
+               $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
+
+               $links = array();
+
+               if ( 'attachment' !== $this->post_type ) {
+                       $links[] = array(
+                               'rel'          => 'https://api.w.org/action-publish',
+                               'title'        => __( 'The current user can publish this post.' ),
+                               'href'         => $href,
+                               'targetSchema' => array(
+                                       'type'       => 'object',
+                                       'properties' => array(
+                                               'status' => array(
+                                                       'type' => 'string',
+                                                       'enum' => array( 'publish', 'future' ),
+                                               ),
+                                       ),
+                               ),
+                       );
+               }
+
+               if ( 'post' === $this->post_type ) {
+                       $links[] = array(
+                               'rel'          => 'https://api.w.org/action-sticky',
+                               'title'        => __( 'The current user can sticky this post.' ),
+                               'href'         => $href,
+                               'targetSchema' => array(
+                                       'type'       => 'object',
+                                       'properties' => array(
+                                               'sticky' => array(
+                                                       'type' => 'boolean',
+                                               ),
+                                       ),
+                               ),
+                       );
+               }
+
+               if ( post_type_supports( $this->post_type, 'author' ) ) {
+                       $links[] = array(
+                               'rel'          => 'https://api.w.org/action-assign-author',
+                               'title'        => __( 'The current user can change the author on this post.' ),
+                               'href'         => $href,
+                               'targetSchema' => array(
+                                       'type'       => 'object',
+                                       'properties' => array(
+                                               'author' => array(
+                                                       'type' => 'integer',
+                                               ),
+                                       ),
+                               ),
+                       );
+               }
+
+               $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
+
+               foreach ( $taxonomies as $tax ) {
+                       $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
+
+                       /* translators: %s: taxonomy name */
+                       $assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name );
+                       /* translators: %s: taxonomy name */
+                       $create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name );
+
+                       $links[] = array(
+                               'rel'          => 'https://api.w.org/action-assign-' . $tax_base,
+                               'title'        => $assign_title,
+                               'href'         => $href,
+                               'targetSchema' => array(
+                                       'type'       => 'object',
+                                       'properties' => array(
+                                               $tax_base => array(
+                                                       'type'  => 'array',
+                                                       'items' => array(
+                                                               'type' => 'integer',
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                       );
+
+                       $links[] = array(
+                               'rel'          => 'https://api.w.org/action-create-' . $tax_base,
+                               'title'        => $create_title,
+                               'href'         => $href,
+                               'targetSchema' => array(
+                                       'type'       => 'object',
+                                       'properties' => array(
+                                               $tax_base => array(
+                                                       'type'  => 'array',
+                                                       'items' => array(
+                                                               'type' => 'integer',
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                       );
+               }
+
+               return $links;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Retrieves the query params for the posts collection.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.7.0
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestattachmentscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php        2018-07-09 13:44:53 UTC (rev 43436)
+++ trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php  2018-07-11 06:22:10 UTC (rev 43437)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1350,6 +1350,50 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function test_links_exist() {
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'self', $links );
+       }
+
+       public function test_publish_action_ldo_not_registered() {
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-publish' ) );
+
+               $this->assertCount( 0, $publish, 'LDO not found on schema.' );
+       }
+
+       public function test_publish_action_link_does_not_exists() {
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public function tearDown() {
</span><span class="cx" style="display: block; padding: 0 10px">                parent::tearDown();
</span><span class="cx" style="display: block; padding: 0 10px">                if ( file_exists( $this->test_file ) ) {
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestpostscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php      2018-07-09 13:44:53 UTC (rev 43436)
+++ trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php        2018-07-11 06:22:10 UTC (rev 43437)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3640,6 +3640,312 @@
</span><span class="cx" style="display: block; padding: 0 10px">                update_post_meta( $post->ID, 'my_custom_int', $value );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function test_publish_action_ldo_registered() {
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-publish' ) );
+
+               $this->assertCount( 1, $publish, 'LDO found on schema.' );
+       }
+
+       public function test_sticky_action_ldo_registered_for_posts() {
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-sticky' ) );
+
+               $this->assertCount( 1, $publish, 'LDO found on schema.' );
+       }
+
+       public function test_sticky_action_ldo_not_registered_for_non_posts() {
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-sticky' ) );
+
+               $this->assertCount( 0, $publish, 'LDO found on schema.' );
+       }
+
+       public function test_author_action_ldo_registered_for_post_types_with_author_support() {
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-assign-author' ) );
+
+               $this->assertCount( 1, $publish, 'LDO found on schema.' );
+       }
+
+       public function test_author_action_ldo_not_registered_for_post_types_without_author_support() {
+
+               remove_post_type_support( 'post', 'author' );
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-assign-author' ) );
+
+               $this->assertCount( 0, $publish, 'LDO found on schema.' );
+       }
+
+       public function test_term_action_ldos_registered() {
+
+               $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) );
+               $data     = $response->get_data();
+               $schema   = $data['schema'];
+
+               $this->assertArrayHasKey( 'links', $schema );
+               $rels = array_flip( wp_list_pluck( $schema['links'], 'rel' ) );
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-assign-categories', $rels );
+               $this->assertArrayHasKey( 'https://api.w.org/action-create-categories', $rels );
+               $this->assertArrayHasKey( 'https://api.w.org/action-assign-tags', $rels );
+               $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $rels );
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-post_format', $rels );
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-create-post_format', $rels );
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-nav_menu', $rels );
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-create-nav_menu', $rels );
+       }
+
+       public function test_action_links_only_available_in_edit_context() {
+
+               wp_set_current_user( self::$author_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'view' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links );
+       }
+
+       public function test_publish_action_link_exists_for_author() {
+
+               wp_set_current_user( self::$author_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-publish', $links );
+       }
+
+       public function test_publish_action_link_does_not_exist_for_contributor() {
+
+               wp_set_current_user( self::$contributor_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$contributor_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links );
+       }
+
+       public function test_sticky_action_exists_for_editor() {
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-sticky', $links );
+       }
+
+       public function test_sticky_action_does_not_exist_for_author() {
+
+               wp_set_current_user( self::$author_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links );
+       }
+
+       public function test_sticky_action_does_not_exist_for_non_post_posts() {
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->post->create(
+                       array(
+                               'post_author' => self::$author_id,
+                               'post_type'   => 'page',
+                       )
+               );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links );
+       }
+
+
+       public function test_assign_author_action_exists_for_editor() {
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-assign-author', $links );
+       }
+
+       public function test_assign_author_action_does_not_exist_for_author() {
+
+               wp_set_current_user( self::$author_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-author', $links );
+       }
+
+       public function test_assign_author_action_does_not_exist_for_post_types_without_author_support() {
+
+               remove_post_type_support( 'post', 'author' );
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->post->create();
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-author', $links );
+       }
+
+       public function test_create_term_action_exists_for_editor() {
+
+               wp_set_current_user( self::$editor_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-create-categories', $links );
+               $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $links );
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-create-post_format', $links );
+       }
+
+       public function test_create_term_action_non_hierarchical_exists_for_author() {
+
+               wp_set_current_user( self::$author_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $links );
+       }
+
+       public function test_create_term_action_hierarchical_does_not_exists_for_author() {
+
+               wp_set_current_user( self::$author_id );
+
+               $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayNotHasKey( 'https://api.w.org/action-create-categories', $links );
+       }
+
+       public function test_assign_term_action_exists_for_contributor() {
+
+               wp_set_current_user( self::$contributor_id );
+
+               $post = self::factory()->post->create(
+                       array(
+                               'post_author' => self::$contributor_id,
+                               'post_status' => 'draft',
+                       )
+               );
+               $this->assertGreaterThan( 0, $post );
+
+               $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+
+               $response = rest_get_server()->dispatch( $request );
+               $links    = $response->get_links();
+
+               $this->assertArrayHasKey( 'https://api.w.org/action-assign-categories', $links );
+               $this->assertArrayHasKey( 'https://api.w.org/action-assign-tags', $links );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public function tearDown() {
</span><span class="cx" style="display: block; padding: 0 10px">                _unregister_post_type( 'youseeeme' );
</span><span class="cx" style="display: block; padding: 0 10px">                if ( isset( $this->attachment_id ) ) {
</span></span></pre>
</div>
</div>

</body>
</html>