PluginProbe
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP / trunk
BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP vtrunk
3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 3.0.0 2.4.13 2.4.12 2.4.11 2.4.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 All 110 releases
betterlinks / includes / Link.php

Link.php in BetterLinks – Link Shortener, Link Cloaking, Redirects, Affiliate Link Manager & MCP trunk, at includes/Link.php

175 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace BetterLinks;
3 if ( ! defined( 'ABSPATH' ) ) { exit; }
4
5 use BetterLinks\Link\Utils;
6 use DeviceDetector\DeviceDetector;
7
8 class Link extends Utils {
9 public function __construct() {
10 // HEAD must resolve the same as GET: uptime monitors, link-preview crawlers
11 // (Slack/WhatsApp/iMessage) and CDN health probes issue HEAD first, and if
12 // it 404s they report the link as broken. The redirect path below sends the
13 // status + Location via wp_redirect() and exits, so HEAD naturally gets the
14 // headers with no body; dispatch_redirect() skips click tracking for HEAD.
15 if ( ! is_admin() && isset( $_SERVER['REQUEST_METHOD'] ) && in_array( strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'HEAD' ), true ) ) {
16 add_action( 'init', array( $this, 'run_redirect' ), 0 );
17 add_action( 'betterlinks_quick_link_creation', array( $this, 'quick_link_creation' ) );
18 add_action( 'betterlinks_prevent_unwanted_cle', array( $this, 'prevent_unwanted_cle' ) );
19 // $this->run_redirect();
20 }
21 }
22
23 /**
24 * Redirects short links to the destination url
25 */
26 public function run_redirect() {
27 // Quick Link Creation Functionality
28 do_action( 'betterlinks_quick_link_creation' );
29
30 // Note: Using sanitize_text_field for $_SERVER['REQUEST_URI'] may not handle redirects properly when short URLs contain non-ASCII characters (e.g., Chinese).
31 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore
32 $request_uri = stripslashes( rawurldecode( $request_uri ) );
33 $request_uri = substr( $request_uri, strlen( wp_parse_url( site_url( '/' ), PHP_URL_PATH ) ) );
34 $param = explode( '?', $request_uri, 2 );
35 $data = $this->get_slug_raw( rtrim( current( $param ), '/' ) );
36
37 $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''; // phpcs:ignore
38 $dd = new DeviceDetector( $user_agent );
39 $dd->parse();
40
41 $data['is_bot'] = $dd->isBot();
42 if ( empty( $data['target_url'] ) || ! apply_filters( 'betterlinks/pre_before_redirect', $data ) ) {
43 // password protection logics
44 do_action( 'betterlinkspro/admin/check_password_protection', $request_uri, $data );
45
46 if ( empty( $data['target_url'] ) || ! apply_filters( 'betterlinks/pre_before_redirect', $data ) ) { // phpcs:ignore
47 return false;
48 }
49 }
50 $data = apply_filters( 'betterlinks/link/before_dispatch_redirect', $data ); // phpcs:ignore.
51 if ( empty( $data ) ) {
52 return false;
53 }
54
55 do_action( 'betterlinks/before_redirect', $data ); // phpcs:ignore.
56 $this->dispatch_redirect( $data, next( $param ) );
57 }
58
59 /**
60 * Legacy Quick Link Creation transport: `?action=btl_cle&api_key=…` on any
61 * front-end URL.
62 *
63 * Kept for the Chrome extension and bookmarklets that predate the REST
64 * endpoint, but the credential itself has been replaced. The old key was
65 * `md5( AUTH_KEY )`: not bound to a user, never expiring, and only revocable
66 * by rotating a wp-config secret (which logs everyone out). It is now a
67 * plugin-issued token — see {@see \BetterLinks\CLEToken}.
68 *
69 * Prefer `POST /wp-json/betterlinks/v1/quick-link` with an
70 * `Authorization: Bearer` header, which keeps the credential out of the URL
71 * entirely (browser history, referrers, proxy and access logs).
72 *
73 * @return void
74 */
75 public function quick_link_creation() {
76 global $betterlinks_settings;
77
78 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- authenticated via the API token check below.
79 if ( ! isset( $_GET['action'], $_GET['api_key'] ) ) {
80 return;
81 }
82
83 if ( sanitize_text_field( wp_unslash( $_GET['action'] ) ) !== 'btl_cle' ) {
84 return;
85 }
86
87 $presented = sanitize_text_field( wp_unslash( $_GET['api_key'] ) );
88 $acting_user = $this->resolve_cle_user( $presented );
89
90 if ( ! $acting_user ) {
91 return;
92 }
93
94 $target_url = isset( $_GET['target_url'] ) ? sanitize_url( wp_unslash( $_GET['target_url'] ) ) : '';
95
96 do_action( 'betterlinks_prevent_unwanted_cle' );
97 $title = isset( $_GET['title'] ) ? sanitize_text_field( wp_unslash( $_GET['title'] ) ) : ''; // geting title from document obj, instead of fetching
98 // phpcs:enable WordPress.Security.NonceVerification.Recommended
99
100 if ( empty( $betterlinks_settings['cle']['enable_cle'] ) ) {
101 return;
102 }
103
104 if ( empty( $title ) ) {
105 $title = ( new Helper() )->fetch_target_url( $target_url );
106 }
107
108 if ( ! empty( $title ) ) {
109 // Only assume the token owner's identity at the point of the write, and
110 // only on a path that exits straight afterwards — never leave the rest
111 // of an ordinary front-end request running as that user.
112 $previous_user = get_current_user_id();
113 wp_set_current_user( $acting_user );
114
115 // Capability is resolved after the switch: Pro answers its delegated
116 // role filter from the current user, so it cannot be evaluated for an
117 // arbitrary id beforehand.
118 if ( ! CLEToken::current_user_can_create() ) {
119 wp_set_current_user( $previous_user );
120
121 return;
122 }
123
124 $this->create_new_link( $title, $target_url, $betterlinks_settings );
125 }
126 }
127
128 /**
129 * Resolve the user a legacy CLE request acts as.
130 *
131 * Accepts a plugin-issued token first. The deprecated `md5( AUTH_KEY )` value
132 * is only honoured while {@see CLEToken::legacy_key_allowed()} is true, which
133 * covers existing sites for one release and is off for everyone else.
134 *
135 * Returns the identity only — the capability check happens after the user
136 * switch, immediately before the write.
137 *
138 * @param string $presented Key from the query string.
139 * @return int User id, or 0 when the request is not authenticated.
140 */
141 private function resolve_cle_user( $presented ) {
142 if ( '' === $presented ) {
143 return 0;
144 }
145
146 $record = CLEToken::verify( $presented );
147
148 if ( $record ) {
149 return (int) $record['user_id'];
150 }
151
152 if ( ! CLEToken::legacy_key_allowed() || ! defined( 'AUTH_KEY' ) ) {
153 return 0;
154 }
155
156 if ( ! hash_equals( md5( AUTH_KEY ), $presented ) ) {
157 return 0;
158 }
159
160 // The legacy key carries no user identity. Attribute the insert to the
161 // site's oldest administrator rather than running with no user at all.
162 $admins = get_users(
163 array(
164 'role' => 'administrator',
165 'orderby' => 'ID',
166 'order' => 'ASC',
167 'number' => 1,
168 'fields' => 'ID',
169 )
170 );
171
172 return ! empty( $admins ) ? (int) $admins[0] : 0;
173 }
174 }
175