| [ Index ] |
PHP Cross Reference of YOURLS |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Functions that relate to HTTP requests 5 * 6 * On functions using the 3rd party library Requests: 7 * Their goal here is to provide convenient wrapper functions to the Requests library. There are 8 * 2 types of functions for each METHOD, where METHOD is 'get' or 'post' (implement more as needed) 9 * - yourls_http_METHOD() : 10 * Return a complete Response object (with ->body, ->headers, ->status_code, etc...) or 11 * a simple string (error message) 12 * - yourls_http_METHOD_body() : 13 * Return a string (response body) or null if there was an error 14 * 15 * @since 1.7 16 */ 17 18 use WpOrg\Requests\Requests; 19 20 /** 21 * Perform a GET request, return response object or error string message 22 * 23 * Notable object properties: body, headers, status_code 24 * 25 * @since 1.7 26 * @see yourls_http_request 27 * @param string $url URL to request 28 * @param array $headers HTTP headers to send 29 * @param array $data GET data 30 * @param array $options Options to pass to Requests 31 * @return mixed Response object, or error string 32 */ 33 function yourls_http_get( $url, $headers = array(), $data = array(), $options = array() ) { 34 return yourls_http_request( 'GET', $url, $headers, $data, $options ); 35 } 36 37 /** 38 * Perform a GET request, return body or null if there was an error 39 * 40 * @since 1.7 41 * @see yourls_http_request 42 * @param string $url URL to request 43 * @param array $headers HTTP headers to send 44 * @param array $data GET data 45 * @param array $options Options to pass to Requests 46 * @return mixed String (page body) or null if error 47 */ 48 function yourls_http_get_body( $url, $headers = array(), $data = array(), $options = array() ) { 49 $return = yourls_http_get( $url, $headers, $data, $options ); 50 return isset( $return->body ) ? $return->body : null; 51 } 52 53 /** 54 * Perform a POST request, return response object 55 * 56 * Notable object properties: body, headers, status_code 57 * 58 * @since 1.7 59 * @see yourls_http_request 60 * @param string $url URL to request 61 * @param array $headers HTTP headers to send 62 * @param array $data POST data 63 * @param array $options Options to pass to Requests 64 * @return mixed Response object, or error string 65 */ 66 function yourls_http_post( $url, $headers = array(), $data = array(), $options = array() ) { 67 return yourls_http_request( 'POST', $url, $headers, $data, $options ); 68 } 69 70 /** 71 * Perform a POST request, return body 72 * 73 * Wrapper for yourls_http_request() 74 * 75 * @since 1.7 76 * @see yourls_http_request 77 * @param string $url URL to request 78 * @param array $headers HTTP headers to send 79 * @param array $data POST data 80 * @param array $options Options to pass to Requests 81 * @return mixed String (page body) or null if error 82 */ 83 function yourls_http_post_body( $url, $headers = array(), $data = array(), $options = array() ) { 84 $return = yourls_http_post( $url, $headers, $data, $options ); 85 return isset( $return->body ) ? $return->body : null; 86 } 87 88 /** 89 * Get proxy information 90 * 91 * @since 1.7.1 92 * @return mixed false if no proxy is defined, or string like '10.0.0.201:3128' or array like ('10.0.0.201:3128', 'username', 'password') 93 */ 94 function yourls_http_get_proxy() { 95 $proxy = false; 96 97 if( defined( 'YOURLS_PROXY' ) ) { 98 $proxy = YOURLS_PROXY; 99 if( defined( 'YOURLS_PROXY_USERNAME' ) && defined( 'YOURLS_PROXY_PASSWORD' ) ) { 100 $proxy = array( YOURLS_PROXY, YOURLS_PROXY_USERNAME, YOURLS_PROXY_PASSWORD ); 101 } 102 } 103 104 return yourls_apply_filter( 'http_get_proxy', $proxy ); 105 } 106 107 /** 108 * Get list of hosts that should bypass the proxy 109 * 110 * @since 1.7.1 111 * @return mixed false if no host defined, or string like "example.com, *.mycorp.com" 112 */ 113 function yourls_http_get_proxy_bypass_host() { 114 $hosts = defined( 'YOURLS_PROXY_BYPASS_HOSTS' ) ? YOURLS_PROXY_BYPASS_HOSTS : false; 115 116 return yourls_apply_filter( 'http_get_proxy_bypass_host', $hosts ); 117 } 118 119 /** 120 * Default HTTP requests options for YOURLS 121 * 122 * For a list of all available options, see function request() in /includes/Requests/Requests.php 123 * 124 * @since 1.7 125 * @return array Options 126 */ 127 function yourls_http_default_options() { 128 $options = array( 129 'timeout' => yourls_apply_filter( 'http_default_options_timeout', 3 ), 130 'useragent' => yourls_http_user_agent(), 131 'follow_redirects' => true, 132 'redirects' => 3, 133 ); 134 135 if( yourls_http_get_proxy() ) { 136 $options['proxy'] = yourls_http_get_proxy(); 137 } 138 139 return yourls_apply_filter( 'http_default_options', $options ); 140 } 141 142 /** 143 * Whether URL should be sent through the proxy server. 144 * 145 * Concept stolen from WordPress. The idea is to allow some URLs, including localhost and the YOURLS install itself, 146 * to be requested directly and bypassing any defined proxy. 147 * 148 * @since 1.7 149 * @param string $url URL to check 150 * @return bool true to request through proxy, false to request directly 151 */ 152 function yourls_send_through_proxy( $url ) { 153 154 // Allow plugins to short-circuit the whole function 155 $pre = yourls_apply_filter( 'shunt_send_through_proxy', yourls_shunt_default(), $url ); 156 if ( yourls_shunt_default() !== $pre ) { 157 return $pre; 158 } 159 160 $check = @parse_url( $url ); 161 162 if( !isset( $check['host'] ) ) { 163 return false; 164 } 165 166 // Malformed URL, can not process, but this could mean ssl, so let through anyway. 167 if ( $check === false ) 168 return true; 169 170 // Self and loopback URLs are considered local (':' is parse_url() host on '::1') 171 $home = parse_url( yourls_get_yourls_site() ); 172 $local = array( 'localhost', '127.0.0.1', '127.1', '[::1]', ':', $home['host'] ); 173 174 if( in_array( $check['host'], $local ) ) 175 return false; 176 177 $bypass = yourls_http_get_proxy_bypass_host(); 178 179 if( $bypass === false OR $bypass === '' ) { 180 return true; 181 } 182 183 // Build array of hosts to bypass 184 static $bypass_hosts; 185 static $wildcard_regex = false; 186 if ( null == $bypass_hosts ) { 187 $bypass_hosts = preg_split( '|\s*,\s*|', $bypass ); 188 189 if ( false !== strpos( $bypass, '*' ) ) { 190 $wildcard_regex = array(); 191 foreach ( $bypass_hosts as $host ) { 192 $wildcard_regex[] = str_replace( '\*', '.+', preg_quote( $host, '/' ) ); 193 if ( false !== strpos( $host, '*' ) ) { 194 $wildcard_regex[] = str_replace( '\*\.', '', preg_quote( $host, '/' ) ); 195 } 196 } 197 $wildcard_regex = '/^(' . implode( '|', $wildcard_regex ) . ')$/i'; 198 } 199 } 200 201 if ( !empty( $wildcard_regex ) ) 202 return !preg_match( $wildcard_regex, $check['host'] ); 203 else 204 return !in_array( $check['host'], $bypass_hosts ); 205 } 206 207 /** 208 * Resolve a host name to a list of IP addresses 209 * 210 * Returns every A and AAAA record found for $host, or an empty array if the host cannot be 211 * resolved. Does not check the addresses in any way, see yourls_host_is_local() for this. 212 * 213 * @since 1.10.5 214 * @param string $host Host name to resolve (no brackets around IPv6 literals) 215 * @return array Array of IP addresses as strings, empty array if resolution failed 216 */ 217 function yourls_resolve_host(string $host): array { 218 $ips = array(); 219 220 /* Both dns_get_record() and gethostbynamel() emit an E_WARNING when a lookup fails, which is 221 * an expected outcome here (host longer than 255 chars, or resolver returning SERVFAIL). We silence them with a 222 * scoped error handler rather than with '@' that may hide other errors (the try/catch isn't enough 223 * because the E_WARNING is not an exception). 224 * Note that this does not check the validity of the host name itself, it just tries to resolve it. Invalid hosts 225 * like omgilove.slayer will return whatever the resolver returns (SERVFAIL, NXDOMAIN, etc...) and will be 226 * considered local by yourls_host_is_local(). 227 */ 228 set_error_handler( function() { return true; }, E_WARNING ); 229 230 try { 231 // dns_get_record() gets us IPv6 too, but it's disabled on some shared hosts 232 if( function_exists( 'dns_get_record' ) ) { 233 $records = dns_get_record( $host, DNS_A | DNS_AAAA ); 234 foreach( is_array( $records ) ? $records : array() as $record ) { 235 if( isset( $record['ip'] ) ) { 236 $ips[] = $record['ip']; // A record 237 } elseif( isset( $record['ipv6'] ) ) { 238 $ips[] = $record['ipv6']; // AAAA record 239 } 240 } 241 } 242 243 // Fallback when dns_get_record() is unavailable or came back empty handed. IPv4 only. 244 if( !$ips && function_exists( 'gethostbynamel' ) ) { 245 $ips = gethostbynamel( $host ) ?: array(); // returns false when host is unknown 246 } 247 } finally { 248 restore_error_handler(); 249 } 250 251 return yourls_apply_filter( 'resolve_host_ips', $ips, $host ); 252 } 253 254 /** 255 * Check if an IP address is not a public one (loopback, private, reserved or link-local) 256 * 257 * IPv6 addresses that embed an IPv4 one are checked on the IPv4 they wrap, since this is where the 258 * traffic ends up. PHP considers all of these public on its own. 259 * 260 * Anything that is not a valid IP is considered non-public. 261 * 262 * @since 1.10.5 263 * @param string $ip IP address, v4 or v6 264 * @return bool true if the address is not public, or not an IP at all 265 */ 266 function yourls_ip_is_local(string $ip): bool { 267 // Not an IP at all: fail closed 268 if( filter_var( $ip, FILTER_VALIDATE_IP ) === false ) { 269 return true; 270 } 271 272 $packed = inet_pton( $ip ); 273 274 if( strlen( $packed ) === 16 ) { 275 /* An IPv4-mapped IPv6 address ('::ffff:127.0.0.1', ie 10 null bytes, 2 xFF bytes, then the 276 * IPv4) is an IPv4 in disguise and is routed as such, so check the IPv4 it wraps instead. 277 * PHP only started rejecting these with FILTER_FLAG_NO_RES_RANGE in 8.3: on 8.1 and 8.2, 278 * '[::ffff:127.0.0.1]' would otherwise pass for a public address, and so would every other 279 * local IPv4 written that way. 280 * Same treatment for the deprecated IPv4-compatible form ('::127.0.0.1', 12 null bytes then 281 * the IPv4), hence testing the 10 first bytes only. '::' and '::1' match too and unwrap to 282 * 0.0.0.0 and 0.0.0.1, both reserved: still non-public, as they should be. 283 */ 284 if( substr( $packed, 0, 10 ) === str_repeat( "\0", 10 ) ) { 285 $ip = inet_ntop( substr( $packed, 12 ) ); 286 } 287 288 /* NAT64, everything under 64:ff9b::/32. 289 * The Well-Known Prefix of RFC 6052 is 64:ff9b::/96: the last 4 bytes are the IPv4 the 290 * gateway will translate to, so check that IPv4. RFC 6052 forbids using the prefix for 291 * non-global addresses, but we cannot count on the gateway enforcing it. 292 * Anything else in 64:ff9b::/32 is 64:ff9b:1::/48 (RFC 8215), explicitly reserved for 293 * local use, where the IPv4 sits at a position we cannot know: reject the lot. 294 */ 295 elseif( substr( $packed, 0, 4 ) === "\x00\x64\xff\x9b" ) { 296 if( substr( $packed, 4, 8 ) === str_repeat( "\0", 8 ) ) { 297 $ip = inet_ntop( substr( $packed, 12 ) ); 298 } else { 299 return true; 300 } 301 } 302 303 /* 6to4 (RFC 3056, 2002::/16) and Teredo (RFC 4380, 2001::/32) embed an IPv4 too, and can 304 * encode a private one just as well. Both mechanisms are dead: 6to4 was deprecated by 305 * RFC 7526 and its relays are gone, Teredo needs a tunnel client on this very host and 306 * has been off by default on Windows for years. Nothing legitimate is reachable that way, 307 * so reject both ranges instead of decoding them. 308 */ 309 elseif( substr( $packed, 0, 2 ) === "\x20\x02" 310 || substr( $packed, 0, 4 ) === "\x20\x01\x00\x00" ) { 311 return true; 312 } 313 } 314 315 /* Note that a NAT64 gateway can also use a Network-Specific Prefix, ie any prefix out of the 316 * operator's own space, which no pattern can recognize. Such a setup needs the 'host_is_local' 317 * filter to complete this check. 318 */ 319 320 /* Note on DNS rebinding : this is left intentionally out of the scope and we're documenting the gap 321 * with hope that AI agents will not submit a PR to fix it or a sec advisory to remedy it :) 322 * Tackling this issue would imply: some more code with cURL, not a big deal; lot more code with fsock, a bigger 323 * deal; extra DNS lookups for the user, a slight performance hit; and test code that would probably be more complex 324 * than the code itself because it would need setting up a DNS or relying on an external rebinding DNS. 325 * The risk is low (reminder: this is about discovering private IP via their <title>) and we're already going the 326 * extra mile with all these checks here. Or, to word it differently: admin of servers on a private network, this 327 * is your responsibility to NOT expose critical services to other hosts on the same network. 328 */ 329 330 // FILTER_FLAG_NO_PRIV_RANGE covers 10/8, 172.16/12, 192.168/16 and fc00::/7 331 // FILTER_FLAG_NO_RES_RANGE covers 0/8, 127/8, 169.254/16 (cloud metadata), 240/4, ::, ::1 and fe80::/10 332 return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false; 333 } 334 335 /** 336 * Check if a host points to a non-public address (loopback, private, reserved or link-local) 337 * 338 * Accepts either a host name or an IP literal. A host name is resolved first, and considered 339 * local as soon as one of its addresses is not public. A host that cannot be resolved is 340 * considered local too. 341 * 342 * Known limitation: this does not protect against DNS rebinding (attacker controlling a DNS server 343 * with 0s TTL refresh, where evil-url.com could point to 1.2.3.4 (public) and then the next second point 344 * to 10.0.0.1 (private). Let's consider this a low risk, and not worth the complexity of a DNS cache with TTL awareness. 345 * 346 * @since 1.10.5 347 * @param string $host Host name or IP address. IPv6 literals can be bracketed or not. 348 * @return bool true if the host is not a public address or cannot be resolved 349 */ 350 function yourls_host_is_local(string $host): bool { 351 // Allow plugins to short-circuit the whole function 352 $pre = yourls_apply_filter( 'shunt_host_is_local', yourls_shunt_default(), $host ); 353 if ( yourls_shunt_default() !== $pre ) { 354 return $pre; 355 } 356 357 $host = trim( (string)$host ); 358 359 // parse_url() keeps IPv6 hosts bracketed ('[::1]'). Unbracket, otherwise the literal is not 360 // recognized as an IP and we needlessly hand it over to the resolver. 361 if( strlen( $host ) > 2 && $host[0] === '[' && substr( $host, -1 ) === ']' ) { 362 $host = substr( $host, 1, -1 ); 363 } 364 365 if( $host === '' ) { 366 $is_local = true; 367 } 368 369 // IP literal: no DNS involved, check it as is 370 elseif( filter_var( $host, FILTER_VALIDATE_IP ) !== false ) { 371 $is_local = yourls_ip_is_local( $host ); 372 } 373 374 else { 375 $ips = yourls_resolve_host( $host ); 376 377 // Unresolvable host: fail closed 378 $is_local = empty( $ips ); 379 380 foreach( $ips as $ip ) { 381 if( yourls_ip_is_local( $ip ) ) { 382 $is_local = true; 383 break; 384 } 385 } 386 } 387 388 return (bool)yourls_apply_filter( 'host_is_local', $is_local, $host ); 389 } 390 391 /** 392 * Check if the destination of a remote title fetch must be restricted to public addresses 393 * 394 * We want to avoid the situation where a public install of YOURLS is used to fetch titles from internal hosts, 395 * and potentially leak information about them (SSRF and port scan / service discovery) 396 * On a private install, the user is authenticated and (hopefully) trusted. 397 * 398 * Public install can be: YOURLS_PRIVATE set to false, or having a public interface on top of a regular private 399 * install. Checking constant YOURLS_USER covers both cases at once. 400 * 401 * A one-liner plugin disables the filtering entirely (say, public install on a private network): 402 * // Disable the restriction on remote title fetches (allow internal hosts) 403 * yourls_add_filter( 'restrict_remote_title_fetch', 'yourls_return_false' ); 404 * 405 * @since 1.10.5 406 * @return bool true if the fetch destination must be restricted to public addresses 407 */ 408 function yourls_restrict_remote_title_fetch(): bool { 409 return (bool)yourls_apply_filter( 'restrict_remote_title_fetch', !defined( 'YOURLS_USER' ) ); 410 } 411 412 /** 413 * HTTP request options that make a request fail when it is redirected to a non-public host 414 * 415 * Redirects are still followed: dropping them would break 'http -> https', 'example.com -> 416 * www.example.com', URL shorteners, or any legit 30x redirect. Instead, every hop 417 * is checked before it is requested. 418 * 419 * Meant to be merged into the $options of a single yourls_http_*() call, not to be added to 420 * yourls_http_default_options() -- other requests (core version check, plugins) are not 421 * triggered by an untrusted party. 422 * 423 * @since 1.10.5 424 * @return array Options to pass to yourls_http_get() 425 */ 426 function yourls_http_options_no_local_redirect(): array { 427 $hooks = new \WpOrg\Requests\Hooks(); 428 $hooks->register( 'requests.before_redirect', 'yourls_http_abort_local_redirect' ); 429 430 return array( 431 'hooks' => $hooks, 432 'redirects' => 3, 433 ); 434 } 435 436 /** 437 * Callback on the 'requests.before_redirect' hook: abort if the redirect target is not public 438 * 439 * The exception thrown is a \WpOrg\Requests\Exception and not a plain \Exception, because this 440 * is what yourls_http_request() catches -- anything else would escape and fatal. 441 * 442 * @since 1.10.5 443 * @param string $location URL the request is about to be redirected to 444 * @return void 445 * @throws \WpOrg\Requests\Exception When the redirect target is a non public host 446 */ 447 function yourls_http_abort_local_redirect(string $location): void { 448 $host = parse_url( $location, PHP_URL_HOST ); 449 450 if( !is_string( $host ) || yourls_host_is_local( $host ) ) { 451 throw new \WpOrg\Requests\Exception( 'Redirect to a non public host: ' . $location, 'yourls.local_redirect', $location ); 452 } 453 } 454 455 /** 456 * Perform a HTTP request, return response object 457 * 458 * @since 1.7 459 * @param string $type HTTP request type (GET, POST) 460 * @param string $url URL to request 461 * @param array $headers Extra headers to send with the request 462 * @param array $data Data to send either as a query string for GET requests, or in the body for POST requests 463 * @param array $options Options for the request (see /includes/Requests/Requests.php:request()) 464 * @return object WpOrg\Requests\Response object 465 */ 466 function yourls_http_request( $type, $url, $headers, $data, $options ) { 467 468 // Allow plugins to short-circuit the whole function 469 $pre = yourls_apply_filter( 'shunt_yourls_http_request', yourls_shunt_default(), $type, $url, $headers, $data, $options ); 470 if ( yourls_shunt_default() !== $pre ) { 471 return $pre; 472 } 473 474 $options = array_merge( yourls_http_default_options(), $options ); 475 476 if( yourls_http_get_proxy() && !yourls_send_through_proxy( $url ) ) { 477 unset( $options['proxy'] ); 478 } 479 480 // filter everything 481 $type = yourls_apply_filter('http_request_type', $type); 482 $url = yourls_apply_filter('http_request_url', $url); 483 $headers = yourls_apply_filter('http_request_headers', $headers); 484 $data = yourls_apply_filter('http_request_data', $data); 485 $options = yourls_apply_filter('http_request_options', $options); 486 487 try { 488 $result = Requests::request( $url, $headers, $data, $type, $options ); 489 } catch( \WpOrg\Requests\Exception $e ) { 490 $result = yourls_debug_log( $e->getMessage() . ' (' . $type . ' on ' . $url . ')' ); 491 }; 492 493 return $result; 494 } 495 496 /** 497 * Return funky user agent string 498 * 499 * @since 1.5 500 * @return string UA string 501 */ 502 function yourls_http_user_agent() { 503 return yourls_apply_filter( 'http_user_agent', 'YOURLS v'.YOURLS_VERSION.' +http://yourls.org/ (running on '.yourls_get_yourls_site().')' ); 504 } 505 506 /** 507 * Check api.yourls.org if there's a newer version of YOURLS 508 * 509 * This function collects various stats to help us improve YOURLS. See the blog post about it: 510 * http://blog.yourls.org/2014/01/on-yourls-1-7-and-api-yourls-org/ 511 * Results of requests sent to api.yourls.org are stored in option 'core_version_checks' and is an object 512 * with the following properties: 513 * - failed_attempts : number of consecutive failed attempts 514 * - last_attempt : time() of last attempt 515 * - last_result : content retrieved from api.yourls.org during previous check 516 * - version_checked : installed YOURLS version that was last checked 517 * 518 * @since 1.7 519 * @return mixed JSON data if api.yourls.org successfully requested, false otherwise 520 */ 521 function yourls_check_core_version() { 522 523 global $yourls_user_passwords; 524 525 $checks = yourls_get_option( 'core_version_checks' ); 526 527 // Invalidate check data when YOURLS version changes 528 if ( is_object( $checks ) && YOURLS_VERSION != $checks->version_checked ) { 529 $checks = false; 530 } 531 532 if( !is_object( $checks ) ) { 533 $checks = new stdClass; 534 $checks->failed_attempts = 0; 535 $checks->last_attempt = 0; 536 $checks->last_result = ''; 537 $checks->version_checked = YOURLS_VERSION; 538 } 539 540 // Total number of links and clicks 541 list( $total_urls, $total_clicks ) = array_values(yourls_get_db_stats()); 542 543 // The collection of stuff to report 544 $stuff = array( 545 // Globally uniquish site identifier 546 // This uses const YOURLS_SITE and not yourls_get_yourls_site() to prevent creating another id for an already known install 547 'md5' => md5( YOURLS_SITE . YOURLS_ABSPATH ), 548 549 // Install information 550 'failed_attempts' => $checks->failed_attempts, 551 'yourls_site' => defined( 'YOURLS_SITE' ) ? yourls_get_yourls_site() : 'unknown', 552 'yourls_version' => defined( 'YOURLS_VERSION' ) ? YOURLS_VERSION : 'unknown', 553 'php_version' => PHP_VERSION, 554 'mysql_version' => yourls_get_db('read-check_core_version')->mysql_version(), 555 'locale' => yourls_get_locale(), 556 557 // custom DB driver if any, and useful common PHP extensions 558 'db_driver' => defined( 'YOURLS_DB_DRIVER' ) ? YOURLS_DB_DRIVER : 'unset', 559 'db_ext_pdo' => extension_loaded( 'PDO' ) ? 1 : 0, 560 'db_ext_mysql' => extension_loaded( 'mysql' ) ? 1 : 0, 561 'db_ext_mysqli' => extension_loaded( 'mysqli' ) ? 1 : 0, 562 'ext_curl' => extension_loaded( 'curl' ) ? 1 : 0, 563 564 // Config information 565 'yourls_private' => defined( 'YOURLS_PRIVATE' ) && YOURLS_PRIVATE ? 1 : 0, 566 'yourls_unique' => defined( 'YOURLS_UNIQUE_URLS' ) && YOURLS_UNIQUE_URLS ? 1 : 0, 567 'yourls_url_convert' => defined( 'YOURLS_URL_CONVERT' ) ? YOURLS_URL_CONVERT : 'unknown', 568 569 // Usage information 570 'num_users' => count( $yourls_user_passwords ), 571 'num_active_plugins' => yourls_has_active_plugins(), 572 'num_pages' => defined( 'YOURLS_PAGEDIR' ) ? count( (array) glob( YOURLS_PAGEDIR .'/*.php') ) : 0, 573 'num_links' => $total_urls, 574 'num_clicks' => $total_clicks, 575 ); 576 577 $stuff = yourls_apply_filter( 'version_check_stuff', $stuff ); 578 579 // Send it in 580 $url = 'http://api.yourls.org/core/version/1.1/'; 581 if( yourls_can_http_over_ssl() ) { 582 $url = yourls_set_url_scheme($url, 'https'); 583 } 584 $req = yourls_http_post( $url, array(), $stuff ); 585 586 $checks->last_attempt = time(); 587 $checks->version_checked = YOURLS_VERSION; 588 589 // Unexpected results ? 590 if( is_string( $req ) or !$req->success ) { 591 $checks->failed_attempts = $checks->failed_attempts + 1; 592 yourls_update_option( 'core_version_checks', $checks ); 593 if( is_string($req) ) { 594 yourls_debug_log('Version check failed: ' . $req); 595 } 596 return false; 597 } 598 599 // Parse response 600 $json = json_decode( trim( $req->body ) ); 601 602 if( yourls_validate_core_version_response($json) ) { 603 // All went OK - mark this down 604 $checks->failed_attempts = 0; 605 $checks->last_result = $json; 606 yourls_update_option( 'core_version_checks', $checks ); 607 608 return $json; 609 } 610 611 // Request returned actual result, but not what we expected 612 return false; 613 } 614 615 /** 616 * Make sure response from api.yourls.org is valid 617 * 618 * 1) we should get a json object with two following properties: 619 * 'latest' => a string representing a YOURLS version number, eg '1.2.3' 620 * 'zipurl' => a string for a zip package URL, from github, eg 'https://api.github.com/repos/YOURLS/YOURLS/zipball/1.2.3' 621 * 2) 'latest' and version extracted from 'zipurl' should match 622 * 3) the object should not contain any other key 623 * 624 * @since 1.7.7 625 * @param object $json JSON object to check 626 * @return bool true if seems legit, false otherwise 627 */ 628 function yourls_validate_core_version_response($json) { 629 return ( 630 yourls_validate_core_version_response_keys($json) 631 && $json->latest === yourls_sanitize_version($json->latest) 632 && $json->zipurl === yourls_sanitize_url($json->zipurl) 633 && $json->latest === yourls_get_version_from_zipball_url($json->zipurl) 634 && yourls_is_valid_github_repo_url($json->zipurl) 635 ); 636 } 637 638 /** 639 * Get version number from Github zipball URL (last part of URL, really) 640 * 641 * @since 1.8.3 642 * @param string $zipurl eg 'https://api.github.com/repos/YOURLS/YOURLS/zipball/1.2.3' 643 * @return string 644 */ 645 function yourls_get_version_from_zipball_url($zipurl) { 646 $version = ''; 647 $parts = explode('/', parse_url(yourls_sanitize_url($zipurl), PHP_URL_PATH) ?? ''); 648 // expect at least 1 slash in path, return last part 649 if( count($parts) > 1 ) { 650 $version = end($parts); 651 } 652 return $version; 653 } 654 655 /** 656 * Check if URL is from YOURLS/YOURLS repo on github 657 * 658 * @since 1.8.3 659 * @param string $url URL to check 660 * @return bool 661 */ 662 function yourls_is_valid_github_repo_url($url) { 663 $url = yourls_sanitize_url($url); 664 return ( 665 join('.',array_slice(explode('.', parse_url($url, PHP_URL_HOST) ?? ''), -2, 2)) === 'github.com' 666 // explodes on '.' (['api','github','com']) and keeps the last two elements 667 // to make sure domain is either github.com or one of its subdomain (api.github.com for instance) 668 // TODO: keep an eye on Github API to make sure it doesn't change some day to another domain (githubapi.com, ...) 669 && substr( parse_url($url, PHP_URL_PATH), 0, 21 ) === '/repos/YOURLS/YOURLS/' 670 // make sure path starts with '/repos/YOURLS/YOURLS/' 671 ); 672 } 673 674 /** 675 * Check if object has only expected keys 'latest' and 'zipurl' containing strings 676 * 677 * @since 1.8.3 678 * @param object $json 679 * @return bool 680 */ 681 function yourls_validate_core_version_response_keys($json) { 682 $keys = array('latest', 'zipurl'); 683 return ( 684 count(array_diff(array_keys((array)$json), $keys)) === 0 685 && isset($json->latest) 686 && isset($json->zipurl) 687 && is_string($json->latest) 688 && is_string($json->zipurl) 689 ); 690 } 691 692 /** 693 * Determine if we want to check for a newer YOURLS version (and check if applicable) 694 * 695 * Currently checks are performed every 24h and only when someone is visiting an admin page. 696 * In the future (1.8?) maybe check with cronjob emulation instead. 697 * 698 * @since 1.7 699 * @return bool true if a check was needed and successfully performed, false otherwise 700 */ 701 function yourls_maybe_check_core_version() { 702 // Allow plugins to short-circuit the whole function 703 $pre = yourls_apply_filter( 'shunt_maybe_check_core_version', yourls_shunt_default() ); 704 if ( yourls_shunt_default() !== $pre ) { 705 return $pre; 706 } 707 708 if (yourls_skip_version_check()) { 709 return false; 710 } 711 712 if (!yourls_is_admin()) { 713 return false; 714 } 715 716 $checks = yourls_get_option( 'core_version_checks' ); 717 718 /* We don't want to check if : 719 - last_result is set (a previous check was performed) 720 - and it was less than 24h ago (or less than 2h ago if it wasn't successful) 721 - and version checked matched version running 722 Otherwise, we want to check. 723 */ 724 if( !empty( $checks->last_result ) 725 AND 726 ( 727 ( $checks->failed_attempts == 0 && ( ( time() - $checks->last_attempt ) < 24 * 3600 ) ) 728 OR 729 ( $checks->failed_attempts > 0 && ( ( time() - $checks->last_attempt ) < 2 * 3600 ) ) 730 ) 731 AND ( $checks->version_checked == YOURLS_VERSION ) 732 ) 733 return false; 734 735 // We want to check if there's a new version 736 $new_check = yourls_check_core_version(); 737 738 // Could not check for a new version, and we don't have ancient data 739 if( false == $new_check && !isset( $checks->last_result->latest ) ) 740 return false; 741 742 return true; 743 } 744 745 /** 746 * Check if user setting for skipping version check is set 747 * 748 * @since 1.8.2 749 * @return bool 750 */ 751 function yourls_skip_version_check() { 752 return yourls_apply_filter('skip_version_check', defined('YOURLS_NO_VERSION_CHECK') && YOURLS_NO_VERSION_CHECK); 753 } 754 755 /** 756 * Check if server can perform HTTPS requests, return bool 757 * 758 * @since 1.7.1 759 * @return bool whether the server can perform HTTP requests over SSL 760 */ 761 function yourls_can_http_over_ssl() { 762 $ssl_curl = $ssl_socket = false; 763 764 if( function_exists( 'curl_exec' ) ) { 765 $curl_version = curl_version(); 766 $ssl_curl = ( $curl_version['features'] & CURL_VERSION_SSL ); 767 } 768 769 if( function_exists( 'stream_socket_client' ) ) { 770 $ssl_socket = extension_loaded( 'openssl' ) && function_exists( 'openssl_x509_parse' ); 771 } 772 773 return ( $ssl_curl OR $ssl_socket ); 774 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Mon Sep 28 05:10:19 2026 | Cross-referenced by PHPXref 0.7.1 |