| [ Index ] |
PHP Cross Reference of YOURLS |
[Summary view] [Print] [Text view]
1 <?php 2 /* 3 * YOURLS general functions 4 * 5 */ 6 7 /** 8 * Make an optimized regexp pattern from a string of characters 9 * 10 * @param string $string 11 * @return string 12 */ 13 function yourls_make_regexp_pattern( $string ) { 14 // Simple benchmarks show that regexp with smarter sequences (0-9, a-z, A-Z...) are not faster or slower than 0123456789 etc... 15 // add @ as an escaped character because @ is used as the regexp delimiter in yourls-loader.php 16 return preg_quote( $string, '@' ); 17 } 18 19 /** 20 * Get client IP Address. Returns a DB safe string. May not be a valid IP per se. 21 * 22 * By default, it trusts only REMOTE_ADDR. If the request comes from a proxy 23 * listed in the 'get_ip_trusted_proxies' filter, it looks for the real client 24 * IP in the headers, with precedence HTTP_X_FORWARDED_FOR > HTTP_CLIENT_IP > HTTP_VIA. 25 * 26 * @return string 27 */ 28 function yourls_get_IP(): string { 29 $ip = $_SERVER['REMOTE_ADDR'] ?? ''; 30 31 // Allow plugins to define a trusted proxy list, and if the request comes from a trusted proxy, look for the real IP in the headers 32 // Precedence: if set, HTTP_X_FORWARDED_FOR > HTTP_CLIENT_IP > HTTP_VIA > REMOTE_ADDR 33 $trusted_proxies = yourls_apply_filter('get_ip_trusted_proxies', []); 34 35 if ( !empty( $trusted_proxies ) && yourls_ip_is_in_ip_list( $ip, $trusted_proxies ) ) { 36 $headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_VIA']; 37 foreach ($headers as $header) { 38 if (!empty($_SERVER[$header])) { 39 $ip = $_SERVER[$header]; 40 break; 41 } 42 } 43 } 44 45 // If there are multiple IPs (e.g. in HTTP_X_FORWARDED_FOR), take the first one 46 $ip = explode(',', $ip)[0]; 47 48 return (string)yourls_apply_filter( 'get_IP', yourls_sanitize_ip( $ip ) ); 49 } 50 51 /** 52 * Check if an IP address matches a given IP or CIDR range (IPv4 and IPv6). 53 * 54 * @since 1.10.5 55 * @param string $ip IP address to check 56 * @param string $range Single IP or CIDR notation (e.g. '10.0.0.0/24' or '2400:cb00::/32') 57 * @return bool 58 */ 59 function yourls_ip_matches_range(string $ip, string $range ): bool { 60 if (!str_contains($range, '/')) { 61 return inet_pton( $ip ) === inet_pton( $range ); 62 } 63 64 list( $subnet, $bits ) = explode( '/', $range ); 65 $bits = (int) $bits; 66 67 $ip_bin = inet_pton( $ip ); 68 $subnet_bin = inet_pton( $subnet ); 69 70 if ( $ip_bin === false || $subnet_bin === false ) { 71 return false; 72 } 73 74 // Both must be same protocol (4 bytes for IPv4, 16 bytes for IPv6) 75 if ( strlen( $ip_bin ) !== strlen( $subnet_bin ) ) { 76 return false; 77 } 78 79 $mask = str_repeat( "\xff", (int) ( $bits / 8 ) ); 80 if ( $bits % 8 ) { 81 $mask .= chr( 0xff << ( 8 - $bits % 8 ) & 0xff ); 82 } 83 $mask = str_pad( $mask, strlen( $ip_bin ), "\x00" ); 84 85 return ( $ip_bin & $mask ) === ( $subnet_bin & $mask ); 86 } 87 88 /** 89 * Check if an IP address is in a list of IP (IPs or CIDR ranges), typically a list of trusted proxies. 90 * 91 * @param string $ip IP address to check 92 * @param array $proxies List of IPs or CIDR ranges 93 * @return bool 94 */ 95 function yourls_ip_is_in_ip_list(string $ip, array $proxies ): bool { 96 foreach ( $proxies as $range ) { 97 if ( yourls_ip_matches_range( $ip, $range ) ) { 98 return true; 99 } 100 } 101 return false; 102 } 103 104 /** 105 * Get next id a new link will have if no custom keyword provided 106 * 107 * @since 1.0 108 * @return int id of next link 109 */ 110 function yourls_get_next_decimal() { 111 return (int)yourls_apply_filter( 'get_next_decimal', (int)yourls_get_option( 'next_id' ) ); 112 } 113 114 /** 115 * Update id for next link with no custom keyword 116 * 117 * Note: this function relies upon yourls_update_option(), which will return either true or false 118 * depending upon if there has been an actual MySQL query updating the DB. 119 * In other words, this function may return false yet this would not mean it has functionally failed 120 * In other words I'm not sure if we really need this function to return something :face_with_eyes_looking_up: 121 * See issue 2621 for more on this. 122 * 123 * @since 1.0 124 * @param integer $int id for next link 125 * @return bool true or false depending on if there has been an actual MySQL query. See note above. 126 */ 127 function yourls_update_next_decimal( $int = 0 ) { 128 $int = ( $int == 0 ) ? yourls_get_next_decimal() + 1 : (int)$int ; 129 $update = yourls_update_option( 'next_id', $int ); 130 yourls_do_action( 'update_next_decimal', $int, $update ); 131 return $update; 132 } 133 134 /** 135 * Return XML output. 136 * 137 * @param array $array 138 * @return string 139 */ 140 function yourls_xml_encode( $array ) { 141 return (\Spatie\ArrayToXml\ArrayToXml::convert($array, '', true, 'UTF-8')); 142 } 143 144 /** 145 * Update click count on a short URL. Return 0/1 for error/success. 146 * 147 * @param string $keyword 148 * @param false|int $clicks 149 * @return int 0 or 1 for error/success 150 */ 151 function yourls_update_clicks( $keyword, $clicks = false ) { 152 // Allow plugins to short-circuit the whole function 153 $pre = yourls_apply_filter( 'shunt_update_clicks', yourls_shunt_default(), $keyword, $clicks ); 154 if ( yourls_shunt_default() !== $pre ) { 155 return $pre; 156 } 157 158 $keyword = yourls_sanitize_keyword( $keyword ); 159 $table = YOURLS_DB_TABLE_URL; 160 if ( $clicks !== false && is_int( $clicks ) && $clicks >= 0 ) { 161 $update = "UPDATE `$table` SET `clicks` = :clicks WHERE `keyword` = :keyword"; 162 $values = [ 'clicks' => $clicks, 'keyword' => $keyword ]; 163 $update_type = 'set'; 164 } else { 165 $update = "UPDATE `$table` SET `clicks` = clicks + 1 WHERE `keyword` = :keyword"; 166 $values = [ 'keyword' => $keyword ]; 167 $update_type = 'increment'; 168 } 169 170 $ydb = yourls_get_db('write-update_clicks'); 171 172 // Try and update click count. An error probably means a concurrency problem : just skip the update 173 try { 174 $result = $ydb->fetchAffected($update, $values); 175 } catch (Exception $e) { 176 $result = 0; 177 } 178 179 if ( $result ) { 180 if ( $ydb->has_infos($keyword) ) { 181 if ( $update_type === 'increment' ) { 182 $infos = $ydb->get_infos($keyword); 183 if ( isset( $infos['clicks'] ) ) { 184 $infos['clicks']++; 185 $ydb->set_infos($keyword, $infos); 186 } else { 187 $ydb->delete_infos($keyword); // We don't know why it's missing, so just purge the cache. 188 } 189 } elseif ( $update_type === 'set' ) { 190 $ydb->update_infos_if_exists($keyword, ['clicks' => $clicks]); 191 } 192 } 193 } 194 195 yourls_do_action( 'update_clicks', $keyword, $result, $clicks ); 196 197 return $result; 198 } 199 200 201 /** 202 * Return array of stats. (string)$filter is 'bottom', 'last', 'rand' or 'top'. (int)$limit is the number of links to return 203 * 204 * @param string $filter 'bottom', 'last', 'rand' or 'top' 205 * @param int $limit Number of links to return 206 * @param int $start Offset to start from 207 * @return array Array of links 208 */ 209 function yourls_get_stats($filter = 'top', $limit = 10, $start = 0) { 210 switch( $filter ) { 211 case 'bottom': 212 $sort_by = '`clicks`'; 213 $sort_order = 'asc'; 214 break; 215 case 'last': 216 $sort_by = '`timestamp`'; 217 $sort_order = 'desc'; 218 break; 219 case 'rand': 220 case 'random': 221 $sort_by = 'RAND()'; 222 $sort_order = ''; 223 break; 224 case 'top': 225 default: 226 $sort_by = '`clicks`'; 227 $sort_order = 'desc'; 228 break; 229 } 230 231 // Fetch links 232 $limit = intval( $limit ); 233 $start = intval( $start ); 234 if ( $limit > 0 ) { 235 236 $table_url = YOURLS_DB_TABLE_URL; 237 $results = yourls_get_db('read-get_stats')->fetchObjects( "SELECT * FROM `$table_url` WHERE 1=1 ORDER BY $sort_by $sort_order LIMIT $start, $limit;" ); 238 239 $return = []; 240 $i = 1; 241 242 foreach ( (array)$results as $res ) { 243 $return['links']['link_'.$i++] = [ 244 'shorturl' => yourls_link($res->keyword), 245 'url' => $res->url, 246 'title' => $res->title, 247 'timestamp'=> $res->timestamp, 248 'ip' => $res->ip, 249 'clicks' => $res->clicks, 250 ]; 251 } 252 } 253 254 $return['stats'] = yourls_get_db_stats(); 255 256 $return['statusCode'] = '200'; 257 258 return yourls_apply_filter( 'get_stats', $return, $filter, $limit, $start ); 259 } 260 261 /** 262 * Get total number of URLs and sum of clicks. Input: optional "AND WHERE" clause. Returns array 263 * 264 * The $where parameter will contain additional SQL arguments: 265 * $where['sql'] will concatenate SQL clauses: $where['sql'] = ' AND something = :value AND otherthing < :othervalue'; 266 * $where['binds'] will hold the (name => value) placeholder pairs: $where['binds'] = array('value' => $value, 'othervalue' => $value2) 267 * 268 * @param array $where See comment above 269 * @return array 270 */ 271 function yourls_get_db_stats( $where = [ 'sql' => '', 'binds' => [] ] ) { 272 $table_url = YOURLS_DB_TABLE_URL; 273 274 $totals = yourls_get_db('read-get_db_stats')->fetchObject( "SELECT COUNT(keyword) as count, SUM(clicks) as sum FROM `$table_url` WHERE 1=1 " . $where['sql'] , $where['binds'] ); 275 $return = [ 'total_links' => (int)$totals->count, 'total_clicks' => (int)$totals->sum ]; 276 277 return yourls_apply_filter( 'get_db_stats', $return, $where ); 278 } 279 280 /** 281 * Returns a sanitized a user agent string. Given what I found on http://www.user-agents.org/ it should be OK. 282 * 283 * @return string 284 */ 285 function yourls_get_user_agent() { 286 $ua = '-'; 287 288 if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { 289 $ua = strip_tags( html_entity_decode( $_SERVER['HTTP_USER_AGENT'] )); 290 $ua = preg_replace('![^0-9a-zA-Z\':., /{}\(\)\[\]\+@&\!\?;_\-=~\*\#]!', '', $ua ); 291 } 292 293 return yourls_apply_filter( 'get_user_agent', substr( $ua, 0, 255 ) ); 294 } 295 296 /** 297 * Returns the sanitized referrer submitted by the browser. 298 * 299 * @return string HTTP Referrer or 'direct' if no referrer was provided 300 */ 301 function yourls_get_referrer() { 302 $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? yourls_sanitize_url_safe( $_SERVER['HTTP_REFERER'] ) : 'direct'; 303 304 return yourls_apply_filter( 'get_referrer', substr( $referrer, 0, 200 ) ); 305 } 306 307 /** 308 * Redirect to another page 309 * 310 * YOURLS redirection, either to internal or external URLs. If headers have not been sent, redirection 311 * is achieved with PHP's header(). If headers have been sent already and we're not in a command line 312 * client, redirection occurs with Javascript. 313 * 314 * Note: yourls_redirect() does not exit automatically, and should almost always be followed by a call to exit() 315 * to prevent the script from continuing. 316 * 317 * @since 1.4 318 * @param string $location URL to redirect to 319 * @param int $code HTTP status code to send 320 * @return int 1 for header redirection, 2 for js redirection, 3 otherwise (CLI) 321 */ 322 function yourls_redirect( $location, $code = 301 ) { 323 yourls_do_action( 'pre_redirect', $location, $code ); 324 $location = yourls_apply_filter( 'redirect_location', $location, $code ); 325 $code = yourls_apply_filter( 'redirect_code', $code, $location ); 326 327 // Redirect, either properly if possible, or via Javascript otherwise 328 if( !headers_sent() ) { 329 yourls_status_header( $code ); 330 header( "Location: $location" ); 331 return 1; 332 } 333 334 // Headers sent : redirect with JS if not in CLI 335 if( php_sapi_name() !== 'cli') { 336 yourls_redirect_javascript( $location ); 337 return 2; 338 } 339 340 // We're in CLI 341 return 3; 342 } 343 344 /** 345 * Redirect to an existing short URL 346 * 347 * Redirect client to an existing short URL (no check performed) and execute misc tasks: update 348 * clicks for short URL, update logs, and send an X-Robots-Tag header to control indexing of a page. 349 * 350 * @since 1.7.3 351 * @param string $url 352 * @param string $keyword 353 * @return void 354 */ 355 function yourls_redirect_shorturl($url, $keyword) { 356 yourls_do_action( 'redirect_shorturl', $url, $keyword ); 357 358 // Attempt to update click count in main table 359 yourls_update_clicks( $keyword ); 360 361 // Update detailed log for stats 362 yourls_log_redirect( $keyword ); 363 364 // Send an X-Robots-Tag header 365 yourls_robots_tag_header(); 366 367 yourls_redirect( $url, 301 ); 368 } 369 370 /** 371 * Send an X-Robots-Tag header. See #3486 372 * 373 * @since 1.9.2 374 * @return void 375 */ 376 function yourls_robots_tag_header() { 377 // Allow plugins to short-circuit the whole function 378 $pre = yourls_apply_filter( 'shunt_robots_tag_header', yourls_shunt_default() ); 379 if ( yourls_shunt_default() !== $pre ) { 380 return $pre; 381 } 382 383 // By default, we're sending a 'noindex' header 384 $tag = yourls_apply_filter( 'robots_tag_header', 'noindex' ); 385 $replace = yourls_apply_filter( 'robots_tag_header_replace', true ); 386 if ( !headers_sent() ) { 387 header( "X-Robots-Tag: $tag", $replace ); 388 } 389 } 390 391 392 /** 393 * Send headers to explicitly tell browser not to cache content or redirection 394 * 395 * @since 1.7.10 396 * @return void 397 */ 398 function yourls_no_cache_headers() { 399 if( !headers_sent() ) { 400 header( 'Expires: Thu, 23 Mar 1972 07:00:00 GMT' ); 401 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' ); 402 header( 'Cache-Control: no-cache, must-revalidate, max-age=0' ); 403 header( 'Pragma: no-cache' ); 404 } 405 } 406 407 /** 408 * Send header to prevent display within a frame from another site (avoid clickjacking) 409 * 410 * This header makes it impossible for an external site to display YOURLS admin within a frame, 411 * which allows for clickjacking. 412 * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 413 * This said, the whole function is shuntable : legit uses of iframes should be still possible. 414 * 415 * @since 1.8.1 416 * @return void|mixed 417 */ 418 function yourls_no_frame_header() { 419 // Allow plugins to short-circuit the whole function 420 $pre = yourls_apply_filter( 'shunt_no_frame_header', yourls_shunt_default() ); 421 if ( yourls_shunt_default() !== $pre ) { 422 return $pre; 423 } 424 425 if( !headers_sent() ) { 426 header( 'X-Frame-Options: SAMEORIGIN' ); 427 } 428 } 429 430 /** 431 * Send a filterable content type header 432 * 433 * @since 1.7 434 * @param string $type content type ('text/html', 'application/json', ...) 435 * @return bool whether header was sent 436 */ 437 function yourls_content_type_header( $type ) { 438 yourls_do_action( 'content_type_header', $type ); 439 if( !headers_sent() ) { 440 $charset = yourls_apply_filter( 'content_type_header_charset', 'utf-8' ); 441 header( "Content-Type: $type; charset=$charset" ); 442 return true; 443 } 444 return false; 445 } 446 447 /** 448 * Set HTTP status header 449 * 450 * @since 1.4 451 * @param int $code status header code 452 * @return bool whether header was sent 453 */ 454 function yourls_status_header( $code = 200 ) { 455 yourls_do_action( 'status_header', $code ); 456 457 if( headers_sent() ) 458 return false; 459 460 $protocol = $_SERVER['SERVER_PROTOCOL']; 461 if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) 462 $protocol = 'HTTP/1.0'; 463 464 $code = intval( $code ); 465 $desc = yourls_get_HTTP_status( $code ); 466 467 @header ("$protocol $code $desc"); // This causes problems on IIS and some FastCGI setups 468 469 return true; 470 } 471 472 /** 473 * Redirect to another page using Javascript. 474 * Set optional (bool)$dontwait to false to force manual redirection (make sure a message has been read by user) 475 * 476 * @param string $location 477 * @param bool $dontwait 478 * @return void 479 */ 480 function yourls_redirect_javascript( $location, $dontwait = true ) { 481 yourls_do_action( 'pre_redirect_javascript', $location, $dontwait ); 482 $location = yourls_apply_filter( 'redirect_javascript', $location, $dontwait ); 483 if ( $dontwait ) { 484 $message = yourls_s( 'if you are not redirected after 10 seconds, please <a href="%s">click here</a>', $location ); 485 echo <<<REDIR 486 <script type="text/javascript"> 487 window.location="$location"; 488 </script> 489 <small>($message)</small> 490 REDIR; 491 } 492 else { 493 echo '<p>'.yourls_s( 'Please <a href="%s">click here</a>', $location ).'</p>'; 494 } 495 yourls_do_action( 'post_redirect_javascript', $location ); 496 } 497 498 /** 499 * Return an HTTP status code 500 * 501 * @param int $code 502 * @return string 503 */ 504 function yourls_get_HTTP_status( $code ) { 505 $code = intval( $code ); 506 $headers_desc = [ 507 100 => 'Continue', 508 101 => 'Switching Protocols', 509 102 => 'Processing', 510 511 200 => 'OK', 512 201 => 'Created', 513 202 => 'Accepted', 514 203 => 'Non-Authoritative Information', 515 204 => 'No Content', 516 205 => 'Reset Content', 517 206 => 'Partial Content', 518 207 => 'Multi-Status', 519 226 => 'IM Used', 520 521 300 => 'Multiple Choices', 522 301 => 'Moved Permanently', 523 302 => 'Found', 524 303 => 'See Other', 525 304 => 'Not Modified', 526 305 => 'Use Proxy', 527 306 => 'Reserved', 528 307 => 'Temporary Redirect', 529 530 400 => 'Bad Request', 531 401 => 'Unauthorized', 532 402 => 'Payment Required', 533 403 => 'Forbidden', 534 404 => 'Not Found', 535 405 => 'Method Not Allowed', 536 406 => 'Not Acceptable', 537 407 => 'Proxy Authentication Required', 538 408 => 'Request Timeout', 539 409 => 'Conflict', 540 410 => 'Gone', 541 411 => 'Length Required', 542 412 => 'Precondition Failed', 543 413 => 'Request Entity Too Large', 544 414 => 'Request-URI Too Long', 545 415 => 'Unsupported Media Type', 546 416 => 'Requested Range Not Satisfiable', 547 417 => 'Expectation Failed', 548 422 => 'Unprocessable Entity', 549 423 => 'Locked', 550 424 => 'Failed Dependency', 551 426 => 'Upgrade Required', 552 553 500 => 'Internal Server Error', 554 501 => 'Not Implemented', 555 502 => 'Bad Gateway', 556 503 => 'Service Unavailable', 557 504 => 'Gateway Timeout', 558 505 => 'HTTP Version Not Supported', 559 506 => 'Variant Also Negotiates', 560 507 => 'Insufficient Storage', 561 510 => 'Not Extended' 562 ]; 563 564 return $headers_desc[$code] ?? ''; 565 } 566 567 /** 568 * Log a redirect (for stats) 569 * 570 * This function does not check for the existence of a valid keyword, in order to save a query. Make sure the keyword 571 * exists before calling it. 572 * 573 * @since 1.4 574 * @param string $keyword short URL keyword 575 * @return mixed Result of the INSERT query (1 on success) 576 */ 577 function yourls_log_redirect( $keyword ) { 578 // Allow plugins to short-circuit the whole function 579 $pre = yourls_apply_filter( 'shunt_log_redirect', yourls_shunt_default(), $keyword ); 580 if ( yourls_shunt_default() !== $pre ) { 581 return $pre; 582 } 583 584 if (!yourls_do_log_redirect()) { 585 return true; 586 } 587 588 $table = YOURLS_DB_TABLE_LOG; 589 $ip = yourls_get_IP(); 590 $binds = [ 591 'now' => date( 'Y-m-d H:i:s' ), 592 'keyword' => yourls_sanitize_keyword($keyword), 593 'referrer' => substr( yourls_get_referrer(), 0, 200 ), 594 'ua' => substr(yourls_get_user_agent(), 0, 255), 595 'ip' => $ip, 596 'location' => yourls_geo_ip_to_countrycode($ip), 597 ]; 598 599 // Action to allow plugins to log the redirect in their own way. See #3990 600 yourls_do_action( 'log_redirect', $binds ); 601 602 // Try and log. An error probably means a concurrency problem : just skip the logging 603 try { 604 $result = yourls_get_db('write-log_redirect')->fetchAffected("INSERT INTO `$table` (click_time, shorturl, referrer, user_agent, ip_address, country_code) VALUES (:now, :keyword, :referrer, :ua, :ip, :location)", $binds ); 605 } catch (Exception $e) { 606 $result = 0; 607 } 608 609 return $result; 610 } 611 612 /** 613 * Check if we want to log redirects (for stats) 614 * 615 * Logs redirects unless YOURLS_NOSTATS is defined and true. Filterable. 616 * 617 * @return bool 618 */ 619 function yourls_do_log_redirect() { 620 $do_log = ( !defined( 'YOURLS_NOSTATS' ) || YOURLS_NOSTATS != true ); 621 return (bool)yourls_apply_filter( 'do_log_redirect', $do_log ); 622 } 623 624 /** 625 * Check if an upgrade is needed 626 * 627 * @return bool 628 */ 629 function yourls_upgrade_is_needed() { 630 // check YOURLS_DB_VERSION exist && match values stored in YOURLS_DB_TABLE_OPTIONS 631 list( $currentver, $currentsql ) = yourls_get_current_version_from_sql(); 632 if ( $currentsql < YOURLS_DB_VERSION ) { 633 return true; 634 } 635 636 // Check if YOURLS_VERSION exist && match value stored in YOURLS_DB_TABLE_OPTIONS, update DB if required 637 if ( $currentver < YOURLS_VERSION ) { 638 yourls_update_option( 'version', YOURLS_VERSION ); 639 } 640 641 return false; 642 } 643 644 /** 645 * Get current version & db version as stored in the options DB. Prior to 1.4 there's no option table. 646 * 647 * @return array 648 */ 649 function yourls_get_current_version_from_sql() { 650 $currentver = yourls_get_option( 'version' ); 651 $currentsql = yourls_get_option( 'db_version' ); 652 653 // Values if version is 1.3 654 if ( !$currentver ) { 655 $currentver = '1.3'; 656 } 657 if ( !$currentsql ) { 658 $currentsql = '100'; 659 } 660 661 return [ $currentver, $currentsql ]; 662 } 663 664 /** 665 * Determine if the current page is private 666 * 667 * @return bool 668 */ 669 function yourls_is_private() { 670 $private = defined( 'YOURLS_PRIVATE' ) && YOURLS_PRIVATE; 671 672 if ( $private ) { 673 674 // Allow overruling for particular pages: 675 676 // API 677 if ( yourls_is_API() && defined( 'YOURLS_PRIVATE_API' ) ) { 678 $private = YOURLS_PRIVATE_API; 679 } 680 // Stat pages 681 elseif ( yourls_is_infos() && defined( 'YOURLS_PRIVATE_INFOS' ) ) { 682 $private = YOURLS_PRIVATE_INFOS; 683 } 684 // Others future cases ? 685 } 686 687 return yourls_apply_filter( 'is_private', $private ); 688 } 689 690 /** 691 * Allow several short URLs for the same long URL ? 692 * 693 * @return bool 694 */ 695 function yourls_allow_duplicate_longurls() { 696 // special treatment if API to check for WordPress plugin requests 697 if ( yourls_is_API() && isset( $_REQUEST[ 'source' ] ) && $_REQUEST[ 'source' ] == 'plugin' ) { 698 return false; 699 } 700 701 return yourls_apply_filter('allow_duplicate_longurls', defined('YOURLS_UNIQUE_URLS') && !YOURLS_UNIQUE_URLS); 702 } 703 704 /** 705 * Get the flood delay in seconds, as maybe defined in config, filtered 706 * 707 * This is the minimum delay between two link creations from the same IP. 708 * Defaults to 15 when undefined. 709 * 710 * @since 1.10.5 711 * @return int Flood delay in seconds 712 */ 713 function yourls_get_flood_delay(): int { 714 $delay = defined( 'YOURLS_FLOOD_DELAY_SECONDS' ) ? (int) YOURLS_FLOOD_DELAY_SECONDS : 15; 715 return yourls_apply_filter( 'get_flood_delay', $delay ); 716 } 717 718 /** 719 * Get the list of IPs exempt from flood checking, as maybe defined in config, filtered 720 * 721 * @since 1.10.5 722 * @return array List of whitelisted IPs (empty array if none) 723 */ 724 function yourls_get_flood_ip_whitelist(): array { 725 $whitelist = defined( 'YOURLS_FLOOD_IP_WHITELIST' ) ? (string) YOURLS_FLOOD_IP_WHITELIST : ''; 726 $ips = array_filter( array_map( 'trim', explode( ',', $whitelist ) ) ); 727 728 $ips = yourls_apply_filter( 'get_flood_ip_whitelist', $ips ); 729 730 // Sanitize each IP, including any value added through the filter, drop empties and reindex 731 $ips = array_map( fn( $ip ) => yourls_sanitize_ip( trim( (string) $ip ) ), (array) $ips ); 732 return array_values( array_filter( $ips ) ); 733 } 734 735 /** 736 * Check if an IP shortens URL too fast to prevent DB flood. Return true, or die. 737 * 738 * @param string $ip 739 * @return bool|mixed|string 740 */ 741 function yourls_check_IP_flood(string $ip = '' ): mixed { 742 743 // Allow plugins to short-circuit the whole function 744 $pre = yourls_apply_filter( 'shunt_check_IP_flood', yourls_shunt_default(), $ip ); 745 if ( yourls_shunt_default() !== $pre ) { 746 return $pre; 747 } 748 749 yourls_do_action( 'pre_check_ip_flood', $ip ); // at this point $ip can be '', check it if your plugin hooks in here 750 751 // Raise white flag if installing or if no flood delay defined 752 $flood_delay = yourls_get_flood_delay(); 753 if( $flood_delay <= 0 || yourls_is_installing() ) 754 return true; 755 756 // Don't throttle logged in users 757 if( yourls_is_private() ) { 758 if( yourls_is_valid_user() === true ) 759 return true; 760 } 761 762 // Don't throttle whitelist IPs 763 if( in_array( $ip, yourls_get_flood_ip_whitelist() ) ) { 764 return true; 765 } 766 767 $ip = ( $ip ? yourls_sanitize_ip( $ip ) : yourls_get_IP() ); 768 769 yourls_do_action( 'check_ip_flood', $ip ); 770 771 $table = YOURLS_DB_TABLE_URL; 772 $lasttime = yourls_get_db('read-check_ip_flood')->fetchValue( "SELECT `timestamp` FROM $table WHERE `ip` = :ip ORDER BY `timestamp` DESC LIMIT 1", [ 'ip' => $ip ] ); 773 if( $lasttime ) { 774 $now = date( 'U' ); 775 $then = date( 'U', strtotime( $lasttime ) ); 776 if( ( $now - $then ) <= $flood_delay ) { 777 // Flood! 778 yourls_do_action( 'ip_flood', $ip, $now - $then ); 779 yourls_die( yourls__( 'Too many URLs added too fast. Slow down please.' ), yourls__( 'Too Many Requests' ), 429 ); 780 } 781 } 782 783 return true; 784 } 785 786 /** 787 * Check if YOURLS is installing 788 * 789 * @since 1.6 790 * @return bool 791 */ 792 function yourls_is_installing() { 793 return (bool)yourls_apply_filter( 'is_installing', defined( 'YOURLS_INSTALLING' ) && YOURLS_INSTALLING ); 794 } 795 796 /** 797 * Check if YOURLS is upgrading 798 * 799 * @since 1.6 800 * @return bool 801 */ 802 function yourls_is_upgrading() { 803 return (bool)yourls_apply_filter( 'is_upgrading', defined( 'YOURLS_UPGRADING' ) && YOURLS_UPGRADING ); 804 } 805 806 /** 807 * Check if YOURLS is installed 808 * 809 * Checks property $ydb->installed that is created by yourls_get_all_options() 810 * 811 * See inline comment for updating from 1.3 or prior. 812 * 813 * @return bool 814 */ 815 function yourls_is_installed() { 816 return (bool)yourls_apply_filter( 'is_installed', yourls_get_db('read-is_installed')->is_installed() ); 817 } 818 819 /** 820 * Set installed state 821 * 822 * @since 1.7.3 823 * @param bool $bool whether YOURLS is installed or not 824 * @return void 825 */ 826 function yourls_set_installed( $bool ) { 827 yourls_get_db('read-set_installed')->set_installed( $bool ); 828 } 829 830 /** 831 * Generate random string of (int)$length length and type $type (see function for details) 832 * 833 * @param int $length 834 * @param int $type 835 * @param string $charlist 836 * @return mixed|string 837 */ 838 function yourls_rnd_string ( $length = 5, $type = 0, $charlist = '' ) { 839 $length = intval( $length ); 840 841 // define possible characters 842 switch ( $type ) { 843 844 // no vowels to make no offending word, no 0/1/o/l to avoid confusion between letters & digits. Perfect for passwords. 845 case '1': 846 $possible = "23456789bcdfghjkmnpqrstvwxyz"; 847 break; 848 849 // Same, with lower + upper 850 case '2': 851 $possible = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ"; 852 break; 853 854 // all letters, lowercase 855 case '3': 856 $possible = "abcdefghijklmnopqrstuvwxyz"; 857 break; 858 859 // all letters, lowercase + uppercase 860 case '4': 861 $possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 862 break; 863 864 // all digits & letters lowercase 865 case '5': 866 $possible = "0123456789abcdefghijklmnopqrstuvwxyz"; 867 break; 868 869 // all digits & letters lowercase + uppercase 870 case '6': 871 $possible = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 872 break; 873 874 // custom char list, or comply to charset as defined in config 875 default: 876 case '0': 877 $possible = $charlist ? $charlist : yourls_get_shorturl_charset(); 878 break; 879 } 880 881 $str = substr( str_shuffle( $possible ), 0, $length ); 882 return yourls_apply_filter( 'rnd_string', $str, $length, $type, $charlist ); 883 } 884 885 /** 886 * Check if we're in API mode. 887 * 888 * @return bool 889 */ 890 function yourls_is_API() { 891 return (bool)yourls_apply_filter( 'is_API', defined( 'YOURLS_API' ) && YOURLS_API ); 892 } 893 894 /** 895 * Check if we're in Ajax mode. 896 * 897 * @return bool 898 */ 899 function yourls_is_Ajax() { 900 return (bool)yourls_apply_filter( 'is_Ajax', defined( 'YOURLS_AJAX' ) && YOURLS_AJAX ); 901 } 902 903 /** 904 * Check if we're in GO mode (yourls-go.php). 905 * 906 * @return bool 907 */ 908 function yourls_is_GO() { 909 return (bool)yourls_apply_filter( 'is_GO', defined( 'YOURLS_GO' ) && YOURLS_GO ); 910 } 911 912 /** 913 * Check if we're displaying stats infos (yourls-infos.php). Returns bool 914 * 915 * @return bool 916 */ 917 function yourls_is_infos() { 918 return (bool)yourls_apply_filter( 'is_infos', defined( 'YOURLS_INFOS' ) && YOURLS_INFOS ); 919 } 920 921 /** 922 * Check if we're in the admin area. Returns bool. Does not relate with user rights. 923 * 924 * @return bool 925 */ 926 function yourls_is_admin() { 927 return (bool)yourls_apply_filter( 'is_admin', defined( 'YOURLS_ADMIN' ) && YOURLS_ADMIN ); 928 } 929 930 /** 931 * Check if the server seems to be running on Windows. Not exactly sure how reliable this is. 932 * 933 * @return bool 934 */ 935 function yourls_is_windows() { 936 return defined( 'DIRECTORY_SEPARATOR' ) && DIRECTORY_SEPARATOR == '\\'; 937 } 938 939 /** 940 * Check if SSL is required. 941 * 942 * @return bool 943 */ 944 function yourls_needs_ssl() { 945 return (bool)yourls_apply_filter( 'needs_ssl', defined( 'YOURLS_ADMIN_SSL' ) && YOURLS_ADMIN_SSL ); 946 } 947 948 /** 949 * Check if SSL is used. Stolen from WP. 950 * 951 * @return bool 952 */ 953 function yourls_is_ssl() { 954 $is_ssl = false; 955 if ( isset( $_SERVER[ 'HTTPS' ] ) ) { 956 if ( 'on' == strtolower( $_SERVER[ 'HTTPS' ] ) ) { 957 $is_ssl = true; 958 } 959 if ( '1' == $_SERVER[ 'HTTPS' ] ) { 960 $is_ssl = true; 961 } 962 } 963 elseif ( isset( $_SERVER[ 'HTTP_X_FORWARDED_PROTO' ] ) ) { 964 if ( 'https' == strtolower( $_SERVER[ 'HTTP_X_FORWARDED_PROTO' ] ) ) { 965 $is_ssl = true; 966 } 967 } 968 elseif ( isset( $_SERVER[ 'SERVER_PORT' ] ) && ( '443' == $_SERVER[ 'SERVER_PORT' ] ) ) { 969 $is_ssl = true; 970 } 971 return (bool)yourls_apply_filter( 'is_ssl', $is_ssl ); 972 } 973 974 /** 975 * Build the HTTPS URL to redirect an admin request to, when SSL is required 976 * 977 * Scheme and host come from the configured YOURLS_SITE. Path and query string come from the request. 978 * 979 * @since 1.10.7 980 * @param string $request_uri Requested URI, typically $_SERVER['REQUEST_URI'] 981 * @return string HTTPS URL on the YOURLS host 982 */ 983 function yourls_get_ssl_redirect_url(string $request_uri): string { 984 $site = parse_url(yourls_get_yourls_site()); 985 $host = $site['host'] ?? ''; 986 if (isset($site['port'])) { 987 $host .= ':' . $site['port']; 988 } 989 990 // Keep only path and query, dropping any scheme or host found in the requested URI 991 $request = parse_url($request_uri); 992 $path = is_array($request) ? ($request['path'] ?? '') : ''; 993 if (!str_starts_with($path, '/')) { 994 $path = '/' . $path; 995 } 996 if (is_array($request) && isset($request['query'])) { 997 $path .= '?' . $request['query']; 998 } 999 1000 return yourls_apply_filter( 'get_ssl_redirect_url', 'https://' . $host . $path); 1001 } 1002 1003 /** 1004 * Get a remote page title 1005 * 1006 * This function returns a string: either the page title as defined in HTML, or the URL if not found 1007 * The function tries to convert funky characters found in titles to UTF8, from the detected charset. 1008 * Charset in use is guessed from HTML meta tag, or if not found, from server's 'content-type' response. 1009 * 1010 * @since 1.5 1011 * @param string $url URL 1012 * @return string Title (sanitized) or the URL if no title found 1013 */ 1014 function yourls_get_remote_title(string $url ): string { 1015 // Allow plugins to short-circuit the whole function 1016 $pre = yourls_apply_filter( 'shunt_get_remote_title', yourls_shunt_default(), $url ); 1017 if ( yourls_shunt_default() !== $pre ) { 1018 return $pre; 1019 } 1020 1021 $url = yourls_sanitize_url( $url ); 1022 1023 // Only deal with http(s):// 1024 if ( !in_array( yourls_get_protocol( $url ), [ 'http://', 'https://' ] ) ) { 1025 return $url; 1026 } 1027 1028 // When an unauthenticated visitor triggers the fetch, don't let them use the server to reach 1029 // hosts they cannot reach themselves. 1030 $ssrf_options = []; 1031 if ( yourls_restrict_remote_title_fetch() ) { 1032 $host = parse_url( $url, PHP_URL_HOST ); 1033 if ( !is_string( $host ) || yourls_host_is_local( $host ) ) { 1034 yourls_debug_log( 'Remote title fetch denied on non public host: ' . $url ); 1035 return $url; 1036 } 1037 // The initial host is public, now make sure every redirect hop is too 1038 $ssrf_options = yourls_http_options_no_local_redirect(); 1039 } 1040 1041 $title = $charset = false; 1042 1043 $max_bytes = yourls_apply_filter( 'get_remote_title_max_byte', 32768 ); // limit data fetching to 32K in order to find a <title> tag 1044 1045 $response = yourls_http_get( $url, [], [], array_merge( [ 'max_bytes' => $max_bytes ], $ssrf_options ) ); // can be a Request object or an error string 1046 if ( is_string( $response ) ) { 1047 return $url; 1048 } 1049 1050 // Page content. No content? Return the URL 1051 $content = $response->body; 1052 if ( !$content ) { 1053 return $url; 1054 } 1055 1056 // look for <title>, which can have attributes. No title found? Return the URL 1057 if ( preg_match( '/<title(?:\s[^>]*)?>(.*?)<\/title>/is', $content, $found ) ) { 1058 $title = $found[ 1 ]; 1059 unset( $found ); 1060 } 1061 if ( !$title ) { 1062 return $url; 1063 } 1064 1065 // Now we have a title. We'll try to get proper utf8 from it. 1066 1067 // Get charset as (and if) defined by the HTML meta tag. We should match 1068 // <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 1069 // or <meta charset='utf-8'> and all possible variations: see https://gist.github.com/ozh/7951236 1070 // A 'charset=' string can also appear where it does not declare anything, for instance in the 1071 // description of the page, so check each <meta> tag and to find if a charset is declared. 1072 if ( preg_match_all( '/<meta\s[^>]*>/is', $content, $metas ) ) { 1073 foreach ( $metas[ 0 ] as $meta ) { 1074 if ( preg_match( '/\bcontent\s*=\s*["\']?[^"\']*?charset\s*=["\' ]*([a-zA-Z0-9\-_]+)/is', $meta, $found ) ) { 1075 // A charset in the 'content' attribute only counts in a Content-Type declaration 1076 if ( !preg_match( '/\bhttp-equiv\s*=\s*["\']?content-type/is', $meta ) ) { 1077 unset( $found ); 1078 continue; 1079 } 1080 } 1081 elseif ( !preg_match( '/\bcharset\s*=["\' ]*([a-zA-Z0-9\-_]+)/is', $meta, $found ) ) { 1082 continue; 1083 } 1084 // First declaration wins, whether it is valid or not 1085 if ( yourls_is_valid_charset( $found[ 1 ] ) ) { 1086 $charset = $found[ 1 ]; 1087 } 1088 unset( $found ); 1089 break; 1090 } 1091 } 1092 if ( empty( $charset ) ) { 1093 // No charset found in HTML. Get charset as (and if) defined by the server response 1094 $_charset = current( (array)$response->headers->getValues( 'content-type' ) ); 1095 // The charset value can be a quoted string: 'text/html; charset="utf-8"' 1096 if ( preg_match( '/charset=(\S+)/', $_charset, $found ) ) { 1097 $_charset = trim( $found[ 1 ], ';"\'' ); 1098 if ( yourls_is_valid_charset( $_charset ) ) { 1099 $charset = $_charset; 1100 } 1101 unset( $found ); 1102 } 1103 } 1104 1105 // Conversion to utf-8 if what we have is not utf8 already. A page can also declare utf-8 and 1106 // still serve invalid utf-8, so in that case convert too, to replace the invalid characters. 1107 if ( function_exists( 'mb_convert_encoding' ) ) { 1108 if ( $charset && strtolower( $charset ) != 'utf-8' ) { 1109 $title = mb_convert_encoding( $title, 'UTF-8', $charset ); 1110 } 1111 elseif ( !mb_check_encoding( $title, 'UTF-8' ) ) { 1112 $title = mb_convert_encoding( $title, 'UTF-8' ); 1113 } 1114 } 1115 1116 // Remove HTML entities 1117 $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' ); 1118 1119 // Strip out evil things 1120 $title = yourls_sanitize_title( $title, $url ); 1121 1122 return (string)yourls_apply_filter( 'get_remote_title', $title, $url ); 1123 } 1124 1125 /** 1126 * Is supported charset encoding for conversion. 1127 * 1128 * @return bool 1129 */ 1130 function yourls_is_valid_charset( $charset ) { 1131 if ( ! function_exists( 'mb_list_encodings' ) ) { 1132 return false; // Okay to return false if mb_list_encodings() is not available since we won't be able to convert the charset. 1133 } 1134 $charset = strtolower( $charset ); 1135 $charsets = array_map( 'strtolower', mb_list_encodings() ); 1136 1137 return in_array( $charset, $charsets ); 1138 } 1139 1140 /** 1141 * Quick UA check for mobile devices. 1142 * 1143 * @return bool 1144 */ 1145 function yourls_is_mobile_device() { 1146 // Strings searched 1147 $mobiles = [ 1148 'android', 'blackberry', 'blazer', 1149 'compal', 'elaine', 'fennec', 'hiptop', 1150 'iemobile', 'iphone', 'ipod', 'ipad', 1151 'iris', 'kindle', 'opera mobi', 'opera mini', 1152 'palm', 'phone', 'pocket', 'psp', 'symbian', 1153 'treo', 'wap', 'windows ce', 'windows phone' 1154 ]; 1155 1156 // Current user-agent 1157 $current = strtolower( $_SERVER['HTTP_USER_AGENT'] ); 1158 1159 // Check and return 1160 $is_mobile = ( str_replace( $mobiles, '', $current ) != $current ); 1161 return (bool)yourls_apply_filter( 'is_mobile_device', $is_mobile ); 1162 } 1163 1164 /** 1165 * Get request in YOURLS base (eg in 'http://sho.rt/yourls/abcd' get 'abdc') 1166 * 1167 * With no parameter passed, this function will guess current page and consider 1168 * it is the requested page. 1169 * For testing purposes, parameters can be passed. 1170 * 1171 * @since 1.5 1172 * @param string $yourls_site Optional, YOURLS installation URL (default to constant YOURLS_SITE) 1173 * @param string $uri Optional, page requested (default to $_SERVER['REQUEST_URI'] eg '/yourls/abcd' ) 1174 * @return string Request relative to YOURLS base (eg 'abdc') 1175 */ 1176 function yourls_get_request(string $yourls_site = '', string $uri = ''): string { 1177 // Allow plugins to short-circuit the whole function 1178 $pre = yourls_apply_filter( 'shunt_get_request', yourls_shunt_default() ); 1179 if ( yourls_shunt_default() !== $pre ) { 1180 return $pre; 1181 } 1182 1183 yourls_do_action( 'pre_get_request', $yourls_site, $uri ); 1184 1185 // Default values 1186 if ( '' === $yourls_site ) { 1187 $yourls_site = yourls_get_yourls_site(); 1188 } 1189 if ( '' === $uri ) { 1190 $uri = $_SERVER[ 'REQUEST_URI' ]; 1191 } 1192 1193 // Even though the config sample states YOURLS_SITE should be set without trailing slash... 1194 $yourls_site = rtrim( $yourls_site, '/' ); 1195 1196 // Now strip the YOURLS_SITE path part out of the requested URI, and get the request relative to YOURLS base 1197 // +---------------------------+-------------------------+---------------------+--------------+ 1198 // | if we request | and YOURLS is hosted on | YOURLS path part is | "request" is | 1199 // +---------------------------+-------------------------+---------------------+--------------+ 1200 // | http://sho.rt/abc | http://sho.rt | / | abc | 1201 // | https://SHO.rt/subdir/abc | https://shor.rt/subdir/ | /subdir/ | abc | 1202 // +---------------------------+-------------------------+---------------------+--------------+ 1203 // and so on. You can find various test cases in tests/tests/utilities/GetRequestTest.php 1204 1205 // Take only the URL_PATH part of YOURLS_SITE (ie "https://sho.rt:1337/path/to/yourls" -> "/path/to/yourls") 1206 $yourls_site = parse_url( $yourls_site, PHP_URL_PATH ).'/'; 1207 1208 // Strip path part from request if exists 1209 $request = $uri; 1210 if (str_starts_with($uri, $yourls_site)) { 1211 $request = ltrim( substr( $uri, strlen( $yourls_site ) ), '/' ); 1212 } 1213 1214 // Request can be a full URL, ie https://sho.rt/http://site.com to "prefix n' shorten" a URL, see https://sho.rt/admin/tools.php 1215 // If request is a simple keyword, strip query string and suspicious traversal attempts 1216 // Note that in a real case use, this shouldn't happen since the server resolves the path before the request reaches YOURLS, 1217 // ie https://github.com/ozh/../YOURLS/ resolves to https://github.com/YOURLS/ 1218 if ( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) { 1219 $request = current( explode( '?', $request ) ); 1220 $request = str_replace( [ '../', '..\\' ], '', $request, $count ); 1221 if ( $count > 0 ) { 1222 $request = trim( $request, '/' ); 1223 } 1224 } 1225 1226 $request = yourls_sanitize_url( $request ); 1227 1228 return (string)yourls_apply_filter( 'get_request', $request ); 1229 } 1230 1231 /** 1232 * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP. 1233 * 1234 * We also strip $_COOKIE from $_REQUEST to allow our lazy using $_REQUEST without 3rd party cookie interfering. 1235 * See #3383 for explanation. 1236 * 1237 * @since 1.5.1 1238 * @return void 1239 */ 1240 function yourls_fix_request_uri() { 1241 1242 $default_server_values = [ 1243 'SERVER_SOFTWARE' => '', 1244 'REQUEST_URI' => '', 1245 ]; 1246 $_SERVER = array_merge( $default_server_values, $_SERVER ); 1247 1248 // Make $_REQUEST with only $_GET and $_POST, not $_COOKIE. See #3383. 1249 $_REQUEST = array_merge( $_GET, $_POST ); 1250 1251 // Fix for IIS when running with PHP ISAPI 1252 if ( empty( $_SERVER[ 'REQUEST_URI' ] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER[ 'SERVER_SOFTWARE' ] ) ) ) { 1253 1254 // IIS Mod-Rewrite 1255 if ( isset( $_SERVER[ 'HTTP_X_ORIGINAL_URL' ] ) ) { 1256 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_ORIGINAL_URL' ]; 1257 } 1258 // IIS Isapi_Rewrite 1259 elseif ( isset( $_SERVER[ 'HTTP_X_REWRITE_URL' ] ) ) { 1260 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_REWRITE_URL' ]; 1261 } 1262 else { 1263 // Use ORIG_PATH_INFO if there is no PATH_INFO 1264 if ( !isset( $_SERVER[ 'PATH_INFO' ] ) && isset( $_SERVER[ 'ORIG_PATH_INFO' ] ) ) { 1265 $_SERVER[ 'PATH_INFO' ] = $_SERVER[ 'ORIG_PATH_INFO' ]; 1266 } 1267 1268 // Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice) 1269 if ( isset( $_SERVER[ 'PATH_INFO' ] ) ) { 1270 if ( $_SERVER[ 'PATH_INFO' ] == $_SERVER[ 'SCRIPT_NAME' ] ) { 1271 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'PATH_INFO' ]; 1272 } 1273 else { 1274 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'SCRIPT_NAME' ].$_SERVER[ 'PATH_INFO' ]; 1275 } 1276 } 1277 1278 // Append the query string if it exists and isn't null 1279 if ( !empty( $_SERVER[ 'QUERY_STRING' ] ) ) { 1280 $_SERVER[ 'REQUEST_URI' ] .= '?'.$_SERVER[ 'QUERY_STRING' ]; 1281 } 1282 } 1283 } 1284 } 1285 1286 /** 1287 * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP. 1288 * 1289 * @return void 1290 */ 1291 function yourls_check_maintenance_mode() { 1292 $dot_file = YOURLS_ABSPATH . '/.maintenance' ; 1293 1294 if ( !file_exists( $dot_file ) || yourls_is_upgrading() || yourls_is_installing() ) { 1295 return; 1296 } 1297 1298 global $maintenance_start; 1299 yourls_include_file_sandbox( $dot_file ); 1300 // If the $maintenance_start timestamp is older than 10 minutes, don't die. 1301 if ( ( time() - $maintenance_start ) >= 600 ) { 1302 return; 1303 } 1304 1305 // Use any /user/maintenance.php file 1306 $file = YOURLS_USERDIR . '/maintenance.php'; 1307 if(file_exists($file)) { 1308 if(yourls_include_file_sandbox( $file ) == true) { 1309 die(); 1310 } 1311 } 1312 1313 // Or use the default messages 1314 $title = yourls__('Service temporarily unavailable'); 1315 $message = yourls__('Our service is currently undergoing scheduled maintenance.') . "</p>\n<p>" . 1316 yourls__('Things should not last very long, thank you for your patience and please excuse the inconvenience'); 1317 yourls_die( $message, $title, 503 ); 1318 } 1319 1320 /** 1321 * Check if a URL protocol is allowed 1322 * 1323 * Checks a URL against a list of whitelisted protocols. Protocols must be defined with 1324 * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid 1325 * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either 1326 * 1327 * @since 1.6 1328 * @see yourls_get_protocol() 1329 * 1330 * @param string $url URL to be checked 1331 * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols 1332 * @return bool true if protocol allowed, false otherwise 1333 */ 1334 function yourls_is_allowed_protocol(string $url, array $protocols = [] ): bool { 1335 if ( empty( $protocols ) ) { 1336 global $yourls_allowedprotocols; 1337 // KSES globals are normally populated on the 'plugins_loaded' action. This can run 1338 // earlier though (eg yourls_die() on a DB connection error, before plugins load), so 1339 // make sure the allowed protocols are available. 1340 if ( ! is_array( $yourls_allowedprotocols ) ) { 1341 yourls_kses_init(); 1342 } 1343 $protocols = $yourls_allowedprotocols; 1344 } 1345 1346 return yourls_apply_filter( 'is_allowed_protocol', in_array( yourls_get_protocol( $url ), $protocols ), $url, $protocols ); 1347 } 1348 1349 /** 1350 * Get protocol from a URL (eg mailto:, http:// ...) 1351 * 1352 * What we liberally call a "protocol" in YOURLS is the scheme name + colon + double slashes if present of a URI. Examples: 1353 * "something://blah" -> "something://" 1354 * "something:blah" -> "something:" 1355 * "something:/blah" -> "something:" 1356 * 1357 * Unit Tests for this function are located in tests/format/urls.php 1358 * 1359 * @since 1.6 1360 * 1361 * @param string $url URL to be check 1362 * @return string Protocol, with slash slash if applicable. Empty string if no protocol 1363 */ 1364 function yourls_get_protocol( $url ) { 1365 /* 1366 http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax 1367 The scheme name consists of a sequence of characters beginning with a letter and followed by any 1368 combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are 1369 case-insensitive, the canonical form is lowercase and documents that specify schemes must do so 1370 with lowercase letters. It is followed by a colon (":"). 1371 */ 1372 preg_match( '!^[a-zA-Z][a-zA-Z0-9+.-]+:(//)?!', $url, $matches ); 1373 return (string)yourls_apply_filter( 'get_protocol', isset( $matches[0] ) ? $matches[0] : '', $url ); 1374 } 1375 1376 /** 1377 * Get relative URL (eg 'abc' from 'http://sho.rt/abc') 1378 * 1379 * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is 1380 * or return empty string if $strict is true 1381 * 1382 * @since 1.6 1383 * @param string $url URL to relativize 1384 * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string 1385 * @return string URL 1386 */ 1387 function yourls_get_relative_url( $url, $strict = true ) { 1388 $url = yourls_sanitize_url( $url ); 1389 1390 // Remove protocols to make it easier 1391 $noproto_url = str_replace( 'https:', 'http:', $url ); 1392 $noproto_site = str_replace( 'https:', 'http:', yourls_get_yourls_site() ); 1393 1394 // Trim URL from YOURLS root URL : if no modification made, URL wasn't relative 1395 $_url = str_replace( $noproto_site.'/', '', $noproto_url ); 1396 if ( $_url == $noproto_url ) { 1397 $_url = ( $strict ? '' : $url ); 1398 } 1399 return yourls_apply_filter( 'get_relative_url', $_url, $url ); 1400 } 1401 1402 /** 1403 * Marks a function as deprecated and informs that it has been used. Stolen from WP. 1404 * 1405 * There is a hook deprecated_function that will be called that can be used 1406 * to get the backtrace up to what file and function called the deprecated 1407 * function. 1408 * 1409 * The current behavior is to trigger a user error if YOURLS_DEBUG is true. 1410 * 1411 * This function is to be used in every function that is deprecated. 1412 * 1413 * @since 1.6 1414 * 1415 * @param string $function The function that was called 1416 * @param string $version The version of WordPress that deprecated the function 1417 * @param string $replacement Optional. The function that should have been called 1418 * @return void 1419 */ 1420 function yourls_deprecated_function( $function, $version, $replacement = null ) { 1421 1422 yourls_do_action( 'deprecated_function', $function, $replacement, $version ); 1423 1424 // Allow plugin to filter the output error trigger 1425 if ( yourls_get_debug_mode() && yourls_apply_filter( 'deprecated_function_trigger_error', true ) ) { 1426 if ( ! is_null( $replacement ) ) 1427 trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) ); 1428 else 1429 trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.'), $function, $version ) ); 1430 } 1431 } 1432 1433 /** 1434 * Explode a URL in an array of ( 'protocol' , 'slashes if any', 'rest of the URL' ) 1435 * 1436 * Some hosts trip up when a query string contains 'http://' - see http://git.io/j1FlJg 1437 * The idea is that instead of passing the whole URL to a bookmarklet, eg index.php?u=http://blah.com, 1438 * we pass it by pieces to fool the server, eg index.php?proto=http:&slashes=//&rest=blah.com 1439 * 1440 * Known limitation: this won't work if the rest of the URL itself contains 'http://', for example 1441 * if rest = blah.com/file.php?url=http://foo.com 1442 * 1443 * Sample returns: 1444 * 1445 * with 'mailto:[email protected]?subject=hey' : 1446 * array( 'protocol' => 'mailto:', 'slashes' => '', 'rest' => '[email protected]?subject=hey' ) 1447 * 1448 * with 'http://example.com/blah.html' : 1449 * array( 'protocol' => 'http:', 'slashes' => '//', 'rest' => 'example.com/blah.html' ) 1450 * 1451 * @since 1.7 1452 * @param string $url URL to be parsed 1453 * @param array $array Optional, array of key names to be used in returned array 1454 * @return array|false false if no protocol found, array of ('protocol' , 'slashes', 'rest') otherwise 1455 */ 1456 function yourls_get_protocol_slashes_and_rest( $url, $array = [ 'protocol', 'slashes', 'rest' ] ) { 1457 $proto = yourls_get_protocol( $url ); 1458 1459 if ( !$proto or count( $array ) != 3 ) { 1460 return false; 1461 } 1462 1463 list( $null, $rest ) = explode( $proto, $url, 2 ); 1464 1465 list( $proto, $slashes ) = explode( ':', $proto ); 1466 1467 return [ 1468 $array[ 0 ] => $proto.':', 1469 $array[ 1 ] => $slashes, 1470 $array[ 2 ] => $rest 1471 ]; 1472 } 1473 1474 /** 1475 * Set URL scheme (HTTP or HTTPS) to a URL 1476 * 1477 * @since 1.7.1 1478 * @param string $url URL 1479 * @param string $scheme scheme, either 'http' or 'https' 1480 * @return string URL with chosen scheme 1481 */ 1482 function yourls_set_url_scheme( $url, $scheme = '' ) { 1483 if ( in_array( $scheme, [ 'http', 'https' ] ) ) { 1484 $url = preg_replace( '!^[a-zA-Z0-9+.-]+://!', $scheme.'://', $url ); 1485 } 1486 return $url; 1487 } 1488 1489 /** 1490 * Tell if there is a new YOURLS version 1491 * 1492 * This function checks, if needed, if there's a new version of YOURLS and, if applicable, displays 1493 * an update notice. 1494 * 1495 * @since 1.7.3 1496 * @return void 1497 */ 1498 function yourls_tell_if_new_version() { 1499 yourls_debug_log( 'Check for new version: '.( yourls_maybe_check_core_version() ? 'yes' : 'no' ) ); 1500 yourls_new_core_version_notice(YOURLS_VERSION); 1501 } 1502 1503 /** 1504 * File include sandbox 1505 * 1506 * Attempt to include a PHP file, fail with an error message if the file isn't valid PHP code. 1507 * This function does not check first if the file exists : depending on use case, you may check first. 1508 * 1509 * @since 1.9.2 1510 * @param string $file filename (full path) 1511 * @return string|bool string if error, true if success 1512 */ 1513 function yourls_include_file_sandbox($file) { 1514 try { 1515 if (is_readable( $file )) { 1516 require_once $file; 1517 yourls_debug_log("loaded $file"); 1518 return true; 1519 } 1520 } catch ( \Throwable $e ) { 1521 yourls_debug_log("could not load $file"); 1522 return sprintf("%s (%s : %s)", $e->getMessage() , $e->getFile() , $e->getLine() ); 1523 } 1524 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Mon Nov 9 05:10:31 2026 | Cross-referenced by PHPXref 0.7.1 |