Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.86% covered (warning)
75.86%
22 / 29
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Share_Destination
75.86% covered (warning)
75.86%
22 / 29
50.00% covered (danger)
50.00%
1 / 2
16.76
0.00% covered (danger)
0.00%
0 / 1
 get_endpoint
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
13
 get_prompt_url
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Share URLs for sharing buttons that send readers to an address they enter, such as a Mastodon instance.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes;
11
12/**
13 * Builds an endpoint URL on a site a reader names, and the prompt URL to ask them again.
14 *
15 * @since 0.6.0
16 */
17final class Share_Destination {
18
19    /**
20     * Rewrite http:// addresses to https://.
21     *
22     * @var bool
23     */
24    public const UPGRADE_HTTP = true;
25
26    /**
27     * Keep http:// addresses as entered.
28     *
29     * @var bool
30     */
31    public const KEEP_HTTP = false;
32
33    /**
34     * Build an endpoint URL on the site at the address a reader entered.
35     *
36     * @since 0.6.0
37     *
38     * @param string $address      Address the reader entered, with or without a scheme.
39     * @param string $path         Path to append to the address, e.g. 'share'.
40     * @param bool   $upgrade_http self::UPGRADE_HTTP or self::KEEP_HTTP.
41     * @return string|null Endpoint URL without a query string, or null if the address is not usable.
42     */
43    public static function get_endpoint( string $address, string $path, bool $upgrade_http ): ?string {
44        // Pasted addresses can carry invisible characters, like a zero-width space, that trim() misses.
45        $address = preg_replace( '/^[\s\p{Cf}]+|[\s\p{Cf}]+$/u', '', $address ) ?? trim( $address );
46        if ( ! preg_match( '#^[a-z][a-z0-9+.-]*://#i', $address ) ) {
47            $address = 'https://' . $address;
48        }
49
50        $parts = wp_parse_url( $address );
51        if ( ! is_array( $parts ) || empty( $parts['host'] ) || empty( $parts['scheme'] ) ) {
52            return null;
53        }
54
55        $scheme = strtolower( $parts['scheme'] );
56        if ( 'https' !== $scheme && 'http' !== $scheme ) {
57            return null;
58        }
59        if ( $upgrade_http ) {
60            $scheme = 'https';
61        }
62
63        // In https://example.com@example.org the host is example.org, not the name the reader sees first.
64        if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
65            return null;
66        }
67
68        // IDNA treats these full-width and ideographic full stops as dots, as in mstdn。jp.
69        $host = str_replace( array( "\u{3002}", "\u{FF0E}", "\u{FF61}" ), '.', $parts['host'] );
70        if ( ! preg_match( '/^[\p{L}\p{M}\p{N}.-]+$/u', $host ) ) {
71            return null;
72        }
73
74        $url = $scheme . '://' . $host;
75        if ( isset( $parts['port'] ) ) {
76            $url .= ':' . $parts['port'];
77        }
78        $url .= rtrim( $parts['path'] ?? '', '/' ) . '/' . ltrim( $path, '/' );
79
80        $url = esc_url_raw( $url, array( $scheme ) );
81
82        return '' === $url ? null : $url;
83    }
84
85    /**
86     * URL of a service's address prompt for a post, where a reader can try another address.
87     *
88     * @since 0.6.0
89     *
90     * @param int    $post_id Post ID.
91     * @param string $service Sharing service short name, e.g. 'mastodon'.
92     * @return string
93     */
94    public static function get_prompt_url( int $post_id, string $service ): string {
95        return add_query_arg(
96            array(
97                'share' => $service,
98                'nb'    => 1,
99            ),
100            get_permalink( $post_id )
101        );
102    }
103}