| [ 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 * Anything that is not a valid IP is considered non-public. 258 * 259 * @since 1.10.5 260 * @param string $ip IP address, v4 or v6 261 * @return bool true if the address is not public, or not an IP at all 262 */ 263 function yourls_ip_is_local(string $ip): bool { 264 // Not an IP at all: fail closed 265 if( filter_var( $ip, FILTER_VALIDATE_IP ) === false ) { 266 return true; 267 } 268 269 /* An IPv4-mapped IPv6 address ('::ffff:127.0.0.1', ie 10 null bytes, 2 xFF bytes, then the 270 * IPv4) is an IPv4 in disguise and is routed as such, so check the IPv4 it wraps instead. 271 * PHP only started rejecting these with FILTER_FLAG_NO_RES_RANGE in 8.3: on 8.1 and 8.2, 272 * '[::ffff:127.0.0.1]' would otherwise pass for a public address, and so would every other 273 * local IPv4 written that way. 274 * Same treatment for the deprecated IPv4-compatible form ('::127.0.0.1', 12 null bytes then 275 * the IPv4), hence testing the 10 first bytes only. '::' and '::1' match too and unwrap to 276 * 0.0.0.0 and 0.0.0.1, both reserved: still non-public, as they should be. 277 */ 278 $packed = inet_pton( $ip ); 279 if( strlen( $packed ) === 16 && substr( $packed, 0, 10 ) === str_repeat( "\0", 10 ) ) { 280 $ip = inet_ntop( substr( $packed, 12 ) ); 281 } 282 283 // FILTER_FLAG_NO_PRIV_RANGE covers 10/8, 172.16/12, 192.168/16 and fc00::/7 284 // FILTER_FLAG_NO_RES_RANGE covers 0/8, 127/8, 169.254/16 (cloud metadata), 240/4, ::, ::1 and fe80::/10 285 return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false; 286 } 287 288 /** 289 * Check if a host points to a non-public address (loopback, private, reserved or link-local) 290 * 291 * Accepts either a host name or an IP literal. A host name is resolved first, and considered 292 * local as soon as one of its addresses is not public. A host that cannot be resolved is 293 * considered local too. 294 * 295 * Known limitation: this does not protect against DNS rebinding (attacker controlling a DNS server 296 * with 0s TTL refresh, where evil-url.com could point to 1.2.3.4 (public) and then the next second point 297 * 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. 298 * 299 * @since 1.10.5 300 * @param string $host Host name or IP address. IPv6 literals can be bracketed or not. 301 * @return bool true if the host is not a public address or cannot be resolved 302 */ 303 function yourls_host_is_local(string $host): bool { 304 // Allow plugins to short-circuit the whole function 305 $pre = yourls_apply_filter( 'shunt_host_is_local', yourls_shunt_default(), $host ); 306 if ( yourls_shunt_default() !== $pre ) { 307 return $pre; 308 } 309 310 $host = trim( (string)$host ); 311 312 // parse_url() keeps IPv6 hosts bracketed ('[::1]'). Unbracket, otherwise the literal is not 313 // recognized as an IP and we needlessly hand it over to the resolver. 314 if( strlen( $host ) > 2 && $host[0] === '[' && substr( $host, -1 ) === ']' ) { 315 $host = substr( $host, 1, -1 ); 316 } 317 318 if( $host === '' ) { 319 $is_local = true; 320 } 321 322 // IP literal: no DNS involved, check it as is 323 elseif( filter_var( $host, FILTER_VALIDATE_IP ) !== false ) { 324 $is_local = yourls_ip_is_local( $host ); 325 } 326 327 else { 328 $ips = yourls_resolve_host( $host ); 329 330 // Unresolvable host: fail closed 331 $is_local = empty( $ips ); 332 333 foreach( $ips as $ip ) { 334 if( yourls_ip_is_local( $ip ) ) { 335 $is_local = true; 336 break; 337 } 338 } 339 } 340 341 return (bool)yourls_apply_filter( 'host_is_local', $is_local, $host ); 342 } 343 344 /** 345 * Check if the destination of a remote title fetch must be restricted to public addresses 346 * 347 * We want to avoid the situation where a public install of YOURLS is used to fetch titles from internal hosts, 348 * and potentially leak information about them (SSRF and port scan / service discovery) 349 * On a private install, the user is authenticated and (hopefully) trusted. 350 * 351 * Public install can be: YOURLS_PRIVATE set to false, or having a public interface on top of a regular private 352 * install. Checking constant YOURLS_USER covers both cases at once. 353 * 354 * A one-liner plugin disables the filtering entirely (say, public install on a private network): 355 * // Disable the restriction on remote title fetches (allow internal hosts) 356 * yourls_add_filter( 'restrict_remote_title_fetch', 'yourls_return_false' ); 357 * 358 * @since 1.10.5 359 * @return bool true if the fetch destination must be restricted to public addresses 360 */ 361 function yourls_restrict_remote_title_fetch(): bool { 362 return (bool)yourls_apply_filter( 'restrict_remote_title_fetch', !defined( 'YOURLS_USER' ) ); 363 } 364 365 /** 366 * HTTP request options that make a request fail when it is redirected to a non-public host 367 * 368 * Redirects are still followed: dropping them would break 'http -> https', 'example.com -> 369 * www.example.com', URL shorteners, or any legit 30x redirect. Instead, every hop 370 * is checked before it is requested. 371 * 372 * Meant to be merged into the $options of a single yourls_http_*() call, not to be added to 373 * yourls_http_default_options() -- other requests (core version check, plugins) are not 374 * triggered by an untrusted party. 375 * 376 * @since 1.10.5 377 * @return array Options to pass to yourls_http_get() 378 */ 379 function yourls_http_options_no_local_redirect(): array { 380 $hooks = new \WpOrg\Requests\Hooks(); 381 $hooks->register( 'requests.before_redirect', 'yourls_http_abort_local_redirect' ); 382 383 return array( 384 'hooks' => $hooks, 385 'redirects' => 3, 386 ); 387 } 388 389 /** 390 * Callback on the 'requests.before_redirect' hook: abort if the redirect target is not public 391 * 392 * The exception thrown is a \WpOrg\Requests\Exception and not a plain \Exception, because this 393 * is what yourls_http_request() catches -- anything else would escape and fatal. 394 * 395 * @since 1.10.5 396 * @param string $location URL the request is about to be redirected to 397 * @return void 398 * @throws \WpOrg\Requests\Exception When the redirect target is a non public host 399 */ 400 function yourls_http_abort_local_redirect(string $location): void { 401 $host = parse_url( $location, PHP_URL_HOST ); 402 403 if( !is_string( $host ) || yourls_host_is_local( $host ) ) { 404 throw new \WpOrg\Requests\Exception( 'Redirect to a non public host: ' . $location, 'yourls.local_redirect', $location ); 405 } 406 } 407 408 /** 409 * Perform a HTTP request, return response object 410 * 411 * @since 1.7 412 * @param string $type HTTP request type (GET, POST) 413 * @param string $url URL to request 414 * @param array $headers Extra headers to send with the request 415 * @param array $data Data to send either as a query string for GET requests, or in the body for POST requests 416 * @param array $options Options for the request (see /includes/Requests/Requests.php:request()) 417 * @return object WpOrg\Requests\Response object 418 */ 419 function yourls_http_request( $type, $url, $headers, $data, $options ) { 420 421 // Allow plugins to short-circuit the whole function 422 $pre = yourls_apply_filter( 'shunt_yourls_http_request', yourls_shunt_default(), $type, $url, $headers, $data, $options ); 423 if ( yourls_shunt_default() !== $pre ) { 424 return $pre; 425 } 426 427 $options = array_merge( yourls_http_default_options(), $options ); 428 429 if( yourls_http_get_proxy() && !yourls_send_through_proxy( $url ) ) { 430 unset( $options['proxy'] ); 431 } 432 433 // filter everything 434 $type = yourls_apply_filter('http_request_type', $type); 435 $url = yourls_apply_filter('http_request_url', $url); 436 $headers = yourls_apply_filter('http_request_headers', $headers); 437 $data = yourls_apply_filter('http_request_data', $data); 438 $options = yourls_apply_filter('http_request_options', $options); 439 440 try { 441 $result = Requests::request( $url, $headers, $data, $type, $options ); 442 } catch( \WpOrg\Requests\Exception $e ) { 443 $result = yourls_debug_log( $e->getMessage() . ' (' . $type . ' on ' . $url . ')' ); 444 }; 445 446 return $result; 447 } 448 449 /** 450 * Return funky user agent string 451 * 452 * @since 1.5 453 * @return string UA string 454 */ 455 function yourls_http_user_agent() { 456 return yourls_apply_filter( 'http_user_agent', 'YOURLS v'.YOURLS_VERSION.' +http://yourls.org/ (running on '.yourls_get_yourls_site().')' ); 457 } 458 459 /** 460 * Check api.yourls.org if there's a newer version of YOURLS 461 * 462 * This function collects various stats to help us improve YOURLS. See the blog post about it: 463 * http://blog.yourls.org/2014/01/on-yourls-1-7-and-api-yourls-org/ 464 * Results of requests sent to api.yourls.org are stored in option 'core_version_checks' and is an object 465 * with the following properties: 466 * - failed_attempts : number of consecutive failed attempts 467 * - last_attempt : time() of last attempt 468 * - last_result : content retrieved from api.yourls.org during previous check 469 * - version_checked : installed YOURLS version that was last checked 470 * 471 * @since 1.7 472 * @return mixed JSON data if api.yourls.org successfully requested, false otherwise 473 */ 474 function yourls_check_core_version() { 475 476 global $yourls_user_passwords; 477 478 $checks = yourls_get_option( 'core_version_checks' ); 479 480 // Invalidate check data when YOURLS version changes 481 if ( is_object( $checks ) && YOURLS_VERSION != $checks->version_checked ) { 482 $checks = false; 483 } 484 485 if( !is_object( $checks ) ) { 486 $checks = new stdClass; 487 $checks->failed_attempts = 0; 488 $checks->last_attempt = 0; 489 $checks->last_result = ''; 490 $checks->version_checked = YOURLS_VERSION; 491 } 492 493 // Total number of links and clicks 494 list( $total_urls, $total_clicks ) = array_values(yourls_get_db_stats()); 495 496 // The collection of stuff to report 497 $stuff = array( 498 // Globally uniquish site identifier 499 // This uses const YOURLS_SITE and not yourls_get_yourls_site() to prevent creating another id for an already known install 500 'md5' => md5( YOURLS_SITE . YOURLS_ABSPATH ), 501 502 // Install information 503 'failed_attempts' => $checks->failed_attempts, 504 'yourls_site' => defined( 'YOURLS_SITE' ) ? yourls_get_yourls_site() : 'unknown', 505 'yourls_version' => defined( 'YOURLS_VERSION' ) ? YOURLS_VERSION : 'unknown', 506 'php_version' => PHP_VERSION, 507 'mysql_version' => yourls_get_db('read-check_core_version')->mysql_version(), 508 'locale' => yourls_get_locale(), 509 510 // custom DB driver if any, and useful common PHP extensions 511 'db_driver' => defined( 'YOURLS_DB_DRIVER' ) ? YOURLS_DB_DRIVER : 'unset', 512 'db_ext_pdo' => extension_loaded( 'PDO' ) ? 1 : 0, 513 'db_ext_mysql' => extension_loaded( 'mysql' ) ? 1 : 0, 514 'db_ext_mysqli' => extension_loaded( 'mysqli' ) ? 1 : 0, 515 'ext_curl' => extension_loaded( 'curl' ) ? 1 : 0, 516 517 // Config information 518 'yourls_private' => defined( 'YOURLS_PRIVATE' ) && YOURLS_PRIVATE ? 1 : 0, 519 'yourls_unique' => defined( 'YOURLS_UNIQUE_URLS' ) && YOURLS_UNIQUE_URLS ? 1 : 0, 520 'yourls_url_convert' => defined( 'YOURLS_URL_CONVERT' ) ? YOURLS_URL_CONVERT : 'unknown', 521 522 // Usage information 523 'num_users' => count( $yourls_user_passwords ), 524 'num_active_plugins' => yourls_has_active_plugins(), 525 'num_pages' => defined( 'YOURLS_PAGEDIR' ) ? count( (array) glob( YOURLS_PAGEDIR .'/*.php') ) : 0, 526 'num_links' => $total_urls, 527 'num_clicks' => $total_clicks, 528 ); 529 530 $stuff = yourls_apply_filter( 'version_check_stuff', $stuff ); 531 532 // Send it in 533 $url = 'http://api.yourls.org/core/version/1.1/'; 534 if( yourls_can_http_over_ssl() ) { 535 $url = yourls_set_url_scheme($url, 'https'); 536 } 537 $req = yourls_http_post( $url, array(), $stuff ); 538 539 $checks->last_attempt = time(); 540 $checks->version_checked = YOURLS_VERSION; 541 542 // Unexpected results ? 543 if( is_string( $req ) or !$req->success ) { 544 $checks->failed_attempts = $checks->failed_attempts + 1; 545 yourls_update_option( 'core_version_checks', $checks ); 546 if( is_string($req) ) { 547 yourls_debug_log('Version check failed: ' . $req); 548 } 549 return false; 550 } 551 552 // Parse response 553 $json = json_decode( trim( $req->body ) ); 554 555 if( yourls_validate_core_version_response($json) ) { 556 // All went OK - mark this down 557 $checks->failed_attempts = 0; 558 $checks->last_result = $json; 559 yourls_update_option( 'core_version_checks', $checks ); 560 561 return $json; 562 } 563 564 // Request returned actual result, but not what we expected 565 return false; 566 } 567 568 /** 569 * Make sure response from api.yourls.org is valid 570 * 571 * 1) we should get a json object with two following properties: 572 * 'latest' => a string representing a YOURLS version number, eg '1.2.3' 573 * 'zipurl' => a string for a zip package URL, from github, eg 'https://api.github.com/repos/YOURLS/YOURLS/zipball/1.2.3' 574 * 2) 'latest' and version extracted from 'zipurl' should match 575 * 3) the object should not contain any other key 576 * 577 * @since 1.7.7 578 * @param object $json JSON object to check 579 * @return bool true if seems legit, false otherwise 580 */ 581 function yourls_validate_core_version_response($json) { 582 return ( 583 yourls_validate_core_version_response_keys($json) 584 && $json->latest === yourls_sanitize_version($json->latest) 585 && $json->zipurl === yourls_sanitize_url($json->zipurl) 586 && $json->latest === yourls_get_version_from_zipball_url($json->zipurl) 587 && yourls_is_valid_github_repo_url($json->zipurl) 588 ); 589 } 590 591 /** 592 * Get version number from Github zipball URL (last part of URL, really) 593 * 594 * @since 1.8.3 595 * @param string $zipurl eg 'https://api.github.com/repos/YOURLS/YOURLS/zipball/1.2.3' 596 * @return string 597 */ 598 function yourls_get_version_from_zipball_url($zipurl) { 599 $version = ''; 600 $parts = explode('/', parse_url(yourls_sanitize_url($zipurl), PHP_URL_PATH) ?? ''); 601 // expect at least 1 slash in path, return last part 602 if( count($parts) > 1 ) { 603 $version = end($parts); 604 } 605 return $version; 606 } 607 608 /** 609 * Check if URL is from YOURLS/YOURLS repo on github 610 * 611 * @since 1.8.3 612 * @param string $url URL to check 613 * @return bool 614 */ 615 function yourls_is_valid_github_repo_url($url) { 616 $url = yourls_sanitize_url($url); 617 return ( 618 join('.',array_slice(explode('.', parse_url($url, PHP_URL_HOST) ?? ''), -2, 2)) === 'github.com' 619 // explodes on '.' (['api','github','com']) and keeps the last two elements 620 // to make sure domain is either github.com or one of its subdomain (api.github.com for instance) 621 // TODO: keep an eye on Github API to make sure it doesn't change some day to another domain (githubapi.com, ...) 622 && substr( parse_url($url, PHP_URL_PATH), 0, 21 ) === '/repos/YOURLS/YOURLS/' 623 // make sure path starts with '/repos/YOURLS/YOURLS/' 624 ); 625 } 626 627 /** 628 * Check if object has only expected keys 'latest' and 'zipurl' containing strings 629 * 630 * @since 1.8.3 631 * @param object $json 632 * @return bool 633 */ 634 function yourls_validate_core_version_response_keys($json) { 635 $keys = array('latest', 'zipurl'); 636 return ( 637 count(array_diff(array_keys((array)$json), $keys)) === 0 638 && isset($json->latest) 639 && isset($json->zipurl) 640 && is_string($json->latest) 641 && is_string($json->zipurl) 642 ); 643 } 644 645 /** 646 * Determine if we want to check for a newer YOURLS version (and check if applicable) 647 * 648 * Currently checks are performed every 24h and only when someone is visiting an admin page. 649 * In the future (1.8?) maybe check with cronjob emulation instead. 650 * 651 * @since 1.7 652 * @return bool true if a check was needed and successfully performed, false otherwise 653 */ 654 function yourls_maybe_check_core_version() { 655 // Allow plugins to short-circuit the whole function 656 $pre = yourls_apply_filter( 'shunt_maybe_check_core_version', yourls_shunt_default() ); 657 if ( yourls_shunt_default() !== $pre ) { 658 return $pre; 659 } 660 661 if (yourls_skip_version_check()) { 662 return false; 663 } 664 665 if (!yourls_is_admin()) { 666 return false; 667 } 668 669 $checks = yourls_get_option( 'core_version_checks' ); 670 671 /* We don't want to check if : 672 - last_result is set (a previous check was performed) 673 - and it was less than 24h ago (or less than 2h ago if it wasn't successful) 674 - and version checked matched version running 675 Otherwise, we want to check. 676 */ 677 if( !empty( $checks->last_result ) 678 AND 679 ( 680 ( $checks->failed_attempts == 0 && ( ( time() - $checks->last_attempt ) < 24 * 3600 ) ) 681 OR 682 ( $checks->failed_attempts > 0 && ( ( time() - $checks->last_attempt ) < 2 * 3600 ) ) 683 ) 684 AND ( $checks->version_checked == YOURLS_VERSION ) 685 ) 686 return false; 687 688 // We want to check if there's a new version 689 $new_check = yourls_check_core_version(); 690 691 // Could not check for a new version, and we don't have ancient data 692 if( false == $new_check && !isset( $checks->last_result->latest ) ) 693 return false; 694 695 return true; 696 } 697 698 /** 699 * Check if user setting for skipping version check is set 700 * 701 * @since 1.8.2 702 * @return bool 703 */ 704 function yourls_skip_version_check() { 705 return yourls_apply_filter('skip_version_check', defined('YOURLS_NO_VERSION_CHECK') && YOURLS_NO_VERSION_CHECK); 706 } 707 708 /** 709 * Check if server can perform HTTPS requests, return bool 710 * 711 * @since 1.7.1 712 * @return bool whether the server can perform HTTP requests over SSL 713 */ 714 function yourls_can_http_over_ssl() { 715 $ssl_curl = $ssl_socket = false; 716 717 if( function_exists( 'curl_exec' ) ) { 718 $curl_version = curl_version(); 719 $ssl_curl = ( $curl_version['features'] & CURL_VERSION_SSL ); 720 } 721 722 if( function_exists( 'stream_socket_client' ) ) { 723 $ssl_socket = extension_loaded( 'openssl' ) && function_exists( 'openssl_x509_parse' ); 724 } 725 726 return ( $ssl_curl OR $ssl_socket ); 727 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Sun Aug 9 05:10:27 2026 | Cross-referenced by PHPXref 0.7.1 |