Index: src/wp-includes/link-template.php
===================================================================
--- src/wp-includes/link-template.php	(revision 37680)
+++ src/wp-includes/link-template.php	(working copy)
@@ -3539,9 +3539,63 @@
 }
 
 /**
+ * Returns the canonical URL for a post.
+ *
+ * When the post is the same as the current requested page the function will handle the
+ * pagination arguments too.
+ *
+ * @since 4.6.0
+ *
+ * @param int|WP_Post $post Optional. Post ID or object. Default is global `$post`.
+ * @return string|false The canonical URL, or false if the post does not exist or has not
+ *                      been published yet.
+ */
+function wp_get_canonical_url( $post = null ) {
+	$post = get_post( $post );
+
+	if ( ! $post ) {
+		return false;
+	}
+
+	if ( 'publish' !== $post->post_status ) {
+		return false;
+	}
+
+	$canonical_url = get_permalink( $post );
+
+	// If a canonical is being generated for the current page, make sure it has pagination if needed.
+	if ( $post->ID === get_queried_object_id() ) {
+		$page = get_query_var( 'page', 0 );
+		if ( $page >= 2 ) {
+			if ( '' == get_option( 'permalink_structure' ) ) {
+				$canonical_url = add_query_arg( 'page', $page, $canonical_url );
+			} else {
+				$canonical_url = trailingslashit( $canonical_url ) . user_trailingslashit( $page, 'single_paged' );
+			}
+		}
+
+		$cpage = get_query_var( 'cpage' );
+		if ( $cpage ) {
+			$canonical_url = get_comments_pagenum_link( $cpage );
+		}
+	}
+
+	/**
+	 * Filters the canonical URL for a post.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string  $string The post's canonical URL.
+	 * @param WP_Post $post   Post object.
+	 */
+	return apply_filters( 'get_canonical_url', $canonical_url, $post );
+}
+
+/**
  * Outputs rel=canonical for singular queries.
  *
  * @since 2.9.0
+ * @since 4.6.0 Adjusted to use wp_get_canonical_url()
  */
 function rel_canonical() {
 	if ( ! is_singular() ) {
@@ -3548,26 +3602,17 @@
 		return;
 	}
 
-	if ( ! $id = get_queried_object_id() ) {
+	$id = get_queried_object_id();
+
+	if ( 0 === $id ) {
 		return;
 	}
 
-	$url = get_permalink( $id );
+	$url = wp_get_canonical_url( $id );
 
-	$page = get_query_var( 'page' );
-	if ( $page >= 2 ) {
-		if ( '' == get_option( 'permalink_structure' ) ) {
-			$url = add_query_arg( 'page', $page, $url );
-		} else {
-			$url = trailingslashit( $url ) . user_trailingslashit( $page, 'single_paged' );
-		}
+	if ( ! empty( $url ) ) {
+		echo '<link rel="canonical" href="' . esc_url( $url ) . '" />' . "\n";
 	}
-
-	$cpage = get_query_var( 'cpage' );
-	if ( $cpage ) {
-		$url = get_comments_pagenum_link( $cpage );
-	}
-	echo '<link rel="canonical" href="' . esc_url( $url ) . "\" />\n";
 }
 
 /**
Index: tests/phpunit/tests/link/getCanonicalURL.php
===================================================================
--- tests/phpunit/tests/link/getCanonicalURL.php	(revision 0)
+++ tests/phpunit/tests/link/getCanonicalURL.php	(working copy)
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @group canonical
+ * @group foo
+ */
+
+class Tests_GetCanonicalURL extends WP_UnitTestCase {
+	public static $post_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$post_id = $factory->post->create( array(
+			'post_status' => 'publish',
+		) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		wp_delete_post( self::$post_id, true );
+	}
+
+	/**
+	 * Test for a non existing post.
+	 */
+	public function test_non_existing_post() {
+		$this->assertFalse( wp_get_canonical_url( -1 ) );
+	}
+
+	/**
+	 * Test for a page that is not published.
+	 */
+	public function test_post_status() {
+		$post_id = self::factory()->post->create( array(
+			'post_status' => 'draft',
+		) );
+
+		$this->assertFalse( wp_get_canonical_url( $post_id ) );
+	}
+
+	/**
+	 * Test for a page that is not the queried object.
+	 */
+	public function test_non_current_page() {
+		$this->assertEquals( get_permalink( self::$post_id ), wp_get_canonical_url( self::$post_id ) );
+	}
+
+	/**
+	 * Test non permalink structure page usage.
+	 */
+	public function test_paged() {
+		$link = add_query_arg( array(
+			'page' => 2,
+			'foo'  => 'bar',
+		), get_permalink( self::$post_id ) );
+
+		$this->go_to( $link );
+
+		$expected = add_query_arg( array(
+			'page' => 2,
+		), get_permalink( self::$post_id ) );
+
+		$this->assertEquals( $expected, wp_get_canonical_url( self::$post_id ) );
+	}
+
+	/**
+	 * Test permalink structure page usage.
+	 */
+	public function test_paged_permalink_structure() {
+		$this->set_permalink_structure( '/%postname%/' );
+		$page = 2;
+
+		$link = add_query_arg( array(
+			'page' => $page,
+			'foo'  => 'bar',
+		), get_permalink( self::$post_id ) );
+
+		$this->go_to( $link );
+
+		$expected = trailingslashit( get_permalink( self::$post_id ) ) . user_trailingslashit( $page, 'single_paged' );
+
+		$this->assertEquals( $expected, wp_get_canonical_url( self::$post_id ) );
+	}
+
+	/**
+	 * Test usage of cpage.
+	 */
+	public function test_cpage() {
+		$link = add_query_arg( array(
+			'cpage' => 2,
+		), get_permalink( self::$post_id ) );
+
+		$this->go_to( $link );
+
+		$expected = $link . '#comments';
+
+		$this->assertEquals( $expected , wp_get_canonical_url( self::$post_id ) );
+	}
+
+	/**
+	 * Test calling of filter.
+	 */
+	public function test_get_canonical_url_filter() {
+		add_filter( 'get_canonical_url', array( $this, 'canonical_url_filter' ) );
+		$canonical_url = wp_get_canonical_url( self::$post_id );
+		remove_filter( 'get_canonical_url', array( $this, 'canonical_url_filter' ) );
+
+		$this->assertEquals( $this->canonical_url_filter(), $canonical_url );
+	}
+
+	/**
+	 * Filter callback for testing of filter usage.
+	 *
+	 * @return string
+	 */
+	public function canonical_url_filter() {
+		return 'http://canonical.example.org/';
+	}
+}
