| [ 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 * Get a remote page title 976 * 977 * This function returns a string: either the page title as defined in HTML, or the URL if not found 978 * The function tries to convert funky characters found in titles to UTF8, from the detected charset. 979 * Charset in use is guessed from HTML meta tag, or if not found, from server's 'content-type' response. 980 * 981 * @since 1.5 982 * @param string $url URL 983 * @return string Title (sanitized) or the URL if no title found 984 */ 985 function yourls_get_remote_title(string $url ): string { 986 // Allow plugins to short-circuit the whole function 987 $pre = yourls_apply_filter( 'shunt_get_remote_title', yourls_shunt_default(), $url ); 988 if ( yourls_shunt_default() !== $pre ) { 989 return $pre; 990 } 991 992 $url = yourls_sanitize_url( $url ); 993 994 // Only deal with http(s):// 995 if ( !in_array( yourls_get_protocol( $url ), [ 'http://', 'https://' ] ) ) { 996 return $url; 997 } 998 999 // When an unauthenticated visitor triggers the fetch, don't let them use the server to reach 1000 // hosts they cannot reach themselves. 1001 $ssrf_options = []; 1002 if ( yourls_restrict_remote_title_fetch() ) { 1003 $host = parse_url( $url, PHP_URL_HOST ); 1004 if ( !is_string( $host ) || yourls_host_is_local( $host ) ) { 1005 yourls_debug_log( 'Remote title fetch denied on non public host: ' . $url ); 1006 return $url; 1007 } 1008 // The initial host is public, now make sure every redirect hop is too 1009 $ssrf_options = yourls_http_options_no_local_redirect(); 1010 } 1011 1012 $title = $charset = false; 1013 1014 $max_bytes = yourls_apply_filter( 'get_remote_title_max_byte', 32768 ); // limit data fetching to 32K in order to find a <title> tag 1015 1016 $response = yourls_http_get( $url, [], [], array_merge( [ 'max_bytes' => $max_bytes ], $ssrf_options ) ); // can be a Request object or an error string 1017 if ( is_string( $response ) ) { 1018 return $url; 1019 } 1020 1021 // Page content. No content? Return the URL 1022 $content = $response->body; 1023 if ( !$content ) { 1024 return $url; 1025 } 1026 1027 // look for <title>. No title found? Return the URL 1028 if ( preg_match( '/<title>(.*?)<\/title>/is', $content, $found ) ) { 1029 $title = $found[ 1 ]; 1030 unset( $found ); 1031 } 1032 if ( !$title ) { 1033 return $url; 1034 } 1035 1036 // Now we have a title. We'll try to get proper utf8 from it. 1037 1038 // Get charset as (and if) defined by the HTML meta tag. We should match 1039 // <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 1040 // or <meta charset='utf-8'> and all possible variations: see https://gist.github.com/ozh/7951236 1041 if ( preg_match( '/<meta[^>]*charset\s*=["\' ]*([a-zA-Z0-9\-_]+)/is', $content, $found ) ) { 1042 if ( yourls_is_valid_charset( $found[ 1 ] ) ) { 1043 $charset = $found[ 1 ]; 1044 } 1045 unset( $found ); 1046 } 1047 if ( empty( $charset ) ) { 1048 // No charset found in HTML. Get charset as (and if) defined by the server response 1049 $_charset = current( $response->headers->getValues( 'content-type' ) ); 1050 if ( preg_match( '/charset=(\S+)/', $_charset, $found ) ) { 1051 $_charset = trim( $found[ 1 ], ';' ); 1052 if ( yourls_is_valid_charset( $_charset ) ) { 1053 $charset = $_charset; 1054 } 1055 unset( $found ); 1056 } 1057 } 1058 1059 // Conversion to utf-8 if what we have is not utf8 already 1060 if ( strtolower( $charset ) != 'utf-8' && function_exists( 'mb_convert_encoding' ) ) { 1061 // We use @ to remove warnings because mb_ functions are easily bitching about illegal chars 1062 if ( $charset ) { 1063 $title = @mb_convert_encoding( $title, 'UTF-8', $charset ); 1064 } 1065 else { 1066 $title = @mb_convert_encoding( $title, 'UTF-8' ); 1067 } 1068 } 1069 1070 // Remove HTML entities 1071 $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' ); 1072 1073 // Strip out evil things 1074 $title = yourls_sanitize_title( $title, $url ); 1075 1076 return (string)yourls_apply_filter( 'get_remote_title', $title, $url ); 1077 } 1078 1079 /** 1080 * Is supported charset encoding for conversion. 1081 * 1082 * @return bool 1083 */ 1084 function yourls_is_valid_charset( $charset ) { 1085 if ( ! function_exists( 'mb_list_encodings' ) ) { 1086 return false; // Okay to return false if mb_list_encodings() is not available since we won't be able to convert the charset. 1087 } 1088 $charset = strtolower( $charset ); 1089 $charsets = array_map( 'strtolower', mb_list_encodings() ); 1090 1091 return in_array( $charset, $charsets ); 1092 } 1093 1094 /** 1095 * Quick UA check for mobile devices. 1096 * 1097 * @return bool 1098 */ 1099 function yourls_is_mobile_device() { 1100 // Strings searched 1101 $mobiles = [ 1102 'android', 'blackberry', 'blazer', 1103 'compal', 'elaine', 'fennec', 'hiptop', 1104 'iemobile', 'iphone', 'ipod', 'ipad', 1105 'iris', 'kindle', 'opera mobi', 'opera mini', 1106 'palm', 'phone', 'pocket', 'psp', 'symbian', 1107 'treo', 'wap', 'windows ce', 'windows phone' 1108 ]; 1109 1110 // Current user-agent 1111 $current = strtolower( $_SERVER['HTTP_USER_AGENT'] ); 1112 1113 // Check and return 1114 $is_mobile = ( str_replace( $mobiles, '', $current ) != $current ); 1115 return (bool)yourls_apply_filter( 'is_mobile_device', $is_mobile ); 1116 } 1117 1118 /** 1119 * Get request in YOURLS base (eg in 'http://sho.rt/yourls/abcd' get 'abdc') 1120 * 1121 * With no parameter passed, this function will guess current page and consider 1122 * it is the requested page. 1123 * For testing purposes, parameters can be passed. 1124 * 1125 * @since 1.5 1126 * @param string $yourls_site Optional, YOURLS installation URL (default to constant YOURLS_SITE) 1127 * @param string $uri Optional, page requested (default to $_SERVER['REQUEST_URI'] eg '/yourls/abcd' ) 1128 * @return string Request relative to YOURLS base (eg 'abdc') 1129 */ 1130 function yourls_get_request(string $yourls_site = '', string $uri = ''): string { 1131 // Allow plugins to short-circuit the whole function 1132 $pre = yourls_apply_filter( 'shunt_get_request', yourls_shunt_default() ); 1133 if ( yourls_shunt_default() !== $pre ) { 1134 return $pre; 1135 } 1136 1137 yourls_do_action( 'pre_get_request', $yourls_site, $uri ); 1138 1139 // Default values 1140 if ( '' === $yourls_site ) { 1141 $yourls_site = yourls_get_yourls_site(); 1142 } 1143 if ( '' === $uri ) { 1144 $uri = $_SERVER[ 'REQUEST_URI' ]; 1145 } 1146 1147 // Even though the config sample states YOURLS_SITE should be set without trailing slash... 1148 $yourls_site = rtrim( $yourls_site, '/' ); 1149 1150 // Now strip the YOURLS_SITE path part out of the requested URI, and get the request relative to YOURLS base 1151 // +---------------------------+-------------------------+---------------------+--------------+ 1152 // | if we request | and YOURLS is hosted on | YOURLS path part is | "request" is | 1153 // +---------------------------+-------------------------+---------------------+--------------+ 1154 // | http://sho.rt/abc | http://sho.rt | / | abc | 1155 // | https://SHO.rt/subdir/abc | https://shor.rt/subdir/ | /subdir/ | abc | 1156 // +---------------------------+-------------------------+---------------------+--------------+ 1157 // and so on. You can find various test cases in tests/tests/utilities/GetRequestTest.php 1158 1159 // Take only the URL_PATH part of YOURLS_SITE (ie "https://sho.rt:1337/path/to/yourls" -> "/path/to/yourls") 1160 $yourls_site = parse_url( $yourls_site, PHP_URL_PATH ).'/'; 1161 1162 // Strip path part from request if exists 1163 $request = $uri; 1164 if (str_starts_with($uri, $yourls_site)) { 1165 $request = ltrim( substr( $uri, strlen( $yourls_site ) ), '/' ); 1166 } 1167 1168 // 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 1169 // If request is a simple keyword, strip query string and suspicious traversal attempts 1170 // Note that in a real case use, this shouldn't happen since the server resolves the path before the request reaches YOURLS, 1171 // ie https://github.com/ozh/../YOURLS/ resolves to https://github.com/YOURLS/ 1172 if ( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) { 1173 $request = current( explode( '?', $request ) ); 1174 $request = str_replace( [ '../', '..\\' ], '', $request, $count ); 1175 if ( $count > 0 ) { 1176 $request = trim( $request, '/' ); 1177 } 1178 } 1179 1180 $request = yourls_sanitize_url( $request ); 1181 1182 return (string)yourls_apply_filter( 'get_request', $request ); 1183 } 1184 1185 /** 1186 * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP. 1187 * 1188 * We also strip $_COOKIE from $_REQUEST to allow our lazy using $_REQUEST without 3rd party cookie interfering. 1189 * See #3383 for explanation. 1190 * 1191 * @since 1.5.1 1192 * @return void 1193 */ 1194 function yourls_fix_request_uri() { 1195 1196 $default_server_values = [ 1197 'SERVER_SOFTWARE' => '', 1198 'REQUEST_URI' => '', 1199 ]; 1200 $_SERVER = array_merge( $default_server_values, $_SERVER ); 1201 1202 // Make $_REQUEST with only $_GET and $_POST, not $_COOKIE. See #3383. 1203 $_REQUEST = array_merge( $_GET, $_POST ); 1204 1205 // Fix for IIS when running with PHP ISAPI 1206 if ( empty( $_SERVER[ 'REQUEST_URI' ] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER[ 'SERVER_SOFTWARE' ] ) ) ) { 1207 1208 // IIS Mod-Rewrite 1209 if ( isset( $_SERVER[ 'HTTP_X_ORIGINAL_URL' ] ) ) { 1210 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_ORIGINAL_URL' ]; 1211 } 1212 // IIS Isapi_Rewrite 1213 elseif ( isset( $_SERVER[ 'HTTP_X_REWRITE_URL' ] ) ) { 1214 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_REWRITE_URL' ]; 1215 } 1216 else { 1217 // Use ORIG_PATH_INFO if there is no PATH_INFO 1218 if ( !isset( $_SERVER[ 'PATH_INFO' ] ) && isset( $_SERVER[ 'ORIG_PATH_INFO' ] ) ) { 1219 $_SERVER[ 'PATH_INFO' ] = $_SERVER[ 'ORIG_PATH_INFO' ]; 1220 } 1221 1222 // Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice) 1223 if ( isset( $_SERVER[ 'PATH_INFO' ] ) ) { 1224 if ( $_SERVER[ 'PATH_INFO' ] == $_SERVER[ 'SCRIPT_NAME' ] ) { 1225 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'PATH_INFO' ]; 1226 } 1227 else { 1228 $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'SCRIPT_NAME' ].$_SERVER[ 'PATH_INFO' ]; 1229 } 1230 } 1231 1232 // Append the query string if it exists and isn't null 1233 if ( !empty( $_SERVER[ 'QUERY_STRING' ] ) ) { 1234 $_SERVER[ 'REQUEST_URI' ] .= '?'.$_SERVER[ 'QUERY_STRING' ]; 1235 } 1236 } 1237 } 1238 } 1239 1240 /** 1241 * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP. 1242 * 1243 * @return void 1244 */ 1245 function yourls_check_maintenance_mode() { 1246 $dot_file = YOURLS_ABSPATH . '/.maintenance' ; 1247 1248 if ( !file_exists( $dot_file ) || yourls_is_upgrading() || yourls_is_installing() ) { 1249 return; 1250 } 1251 1252 global $maintenance_start; 1253 yourls_include_file_sandbox( $dot_file ); 1254 // If the $maintenance_start timestamp is older than 10 minutes, don't die. 1255 if ( ( time() - $maintenance_start ) >= 600 ) { 1256 return; 1257 } 1258 1259 // Use any /user/maintenance.php file 1260 $file = YOURLS_USERDIR . '/maintenance.php'; 1261 if(file_exists($file)) { 1262 if(yourls_include_file_sandbox( $file ) == true) { 1263 die(); 1264 } 1265 } 1266 1267 // Or use the default messages 1268 $title = yourls__('Service temporarily unavailable'); 1269 $message = yourls__('Our service is currently undergoing scheduled maintenance.') . "</p>\n<p>" . 1270 yourls__('Things should not last very long, thank you for your patience and please excuse the inconvenience'); 1271 yourls_die( $message, $title, 503 ); 1272 } 1273 1274 /** 1275 * Check if a URL protocol is allowed 1276 * 1277 * Checks a URL against a list of whitelisted protocols. Protocols must be defined with 1278 * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid 1279 * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either 1280 * 1281 * @since 1.6 1282 * @see yourls_get_protocol() 1283 * 1284 * @param string $url URL to be checked 1285 * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols 1286 * @return bool true if protocol allowed, false otherwise 1287 */ 1288 function yourls_is_allowed_protocol(string $url, array $protocols = [] ): bool { 1289 if ( empty( $protocols ) ) { 1290 global $yourls_allowedprotocols; 1291 // KSES globals are normally populated on the 'plugins_loaded' action. This can run 1292 // earlier though (eg yourls_die() on a DB connection error, before plugins load), so 1293 // make sure the allowed protocols are available. 1294 if ( ! is_array( $yourls_allowedprotocols ) ) { 1295 yourls_kses_init(); 1296 } 1297 $protocols = $yourls_allowedprotocols; 1298 } 1299 1300 return yourls_apply_filter( 'is_allowed_protocol', in_array( yourls_get_protocol( $url ), $protocols ), $url, $protocols ); 1301 } 1302 1303 /** 1304 * Get protocol from a URL (eg mailto:, http:// ...) 1305 * 1306 * What we liberally call a "protocol" in YOURLS is the scheme name + colon + double slashes if present of a URI. Examples: 1307 * "something://blah" -> "something://" 1308 * "something:blah" -> "something:" 1309 * "something:/blah" -> "something:" 1310 * 1311 * Unit Tests for this function are located in tests/format/urls.php 1312 * 1313 * @since 1.6 1314 * 1315 * @param string $url URL to be check 1316 * @return string Protocol, with slash slash if applicable. Empty string if no protocol 1317 */ 1318 function yourls_get_protocol( $url ) { 1319 /* 1320 http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax 1321 The scheme name consists of a sequence of characters beginning with a letter and followed by any 1322 combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are 1323 case-insensitive, the canonical form is lowercase and documents that specify schemes must do so 1324 with lowercase letters. It is followed by a colon (":"). 1325 */ 1326 preg_match( '!^[a-zA-Z][a-zA-Z0-9+.-]+:(//)?!', $url, $matches ); 1327 return (string)yourls_apply_filter( 'get_protocol', isset( $matches[0] ) ? $matches[0] : '', $url ); 1328 } 1329 1330 /** 1331 * Get relative URL (eg 'abc' from 'http://sho.rt/abc') 1332 * 1333 * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is 1334 * or return empty string if $strict is true 1335 * 1336 * @since 1.6 1337 * @param string $url URL to relativize 1338 * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string 1339 * @return string URL 1340 */ 1341 function yourls_get_relative_url( $url, $strict = true ) { 1342 $url = yourls_sanitize_url( $url ); 1343 1344 // Remove protocols to make it easier 1345 $noproto_url = str_replace( 'https:', 'http:', $url ); 1346 $noproto_site = str_replace( 'https:', 'http:', yourls_get_yourls_site() ); 1347 1348 // Trim URL from YOURLS root URL : if no modification made, URL wasn't relative 1349 $_url = str_replace( $noproto_site.'/', '', $noproto_url ); 1350 if ( $_url == $noproto_url ) { 1351 $_url = ( $strict ? '' : $url ); 1352 } 1353 return yourls_apply_filter( 'get_relative_url', $_url, $url ); 1354 } 1355 1356 /** 1357 * Marks a function as deprecated and informs that it has been used. Stolen from WP. 1358 * 1359 * There is a hook deprecated_function that will be called that can be used 1360 * to get the backtrace up to what file and function called the deprecated 1361 * function. 1362 * 1363 * The current behavior is to trigger a user error if YOURLS_DEBUG is true. 1364 * 1365 * This function is to be used in every function that is deprecated. 1366 * 1367 * @since 1.6 1368 * 1369 * @param string $function The function that was called 1370 * @param string $version The version of WordPress that deprecated the function 1371 * @param string $replacement Optional. The function that should have been called 1372 * @return void 1373 */ 1374 function yourls_deprecated_function( $function, $version, $replacement = null ) { 1375 1376 yourls_do_action( 'deprecated_function', $function, $replacement, $version ); 1377 1378 // Allow plugin to filter the output error trigger 1379 if ( yourls_get_debug_mode() && yourls_apply_filter( 'deprecated_function_trigger_error', true ) ) { 1380 if ( ! is_null( $replacement ) ) 1381 trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) ); 1382 else 1383 trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.'), $function, $version ) ); 1384 } 1385 } 1386 1387 /** 1388 * Explode a URL in an array of ( 'protocol' , 'slashes if any', 'rest of the URL' ) 1389 * 1390 * Some hosts trip up when a query string contains 'http://' - see http://git.io/j1FlJg 1391 * The idea is that instead of passing the whole URL to a bookmarklet, eg index.php?u=http://blah.com, 1392 * we pass it by pieces to fool the server, eg index.php?proto=http:&slashes=//&rest=blah.com 1393 * 1394 * Known limitation: this won't work if the rest of the URL itself contains 'http://', for example 1395 * if rest = blah.com/file.php?url=http://foo.com 1396 * 1397 * Sample returns: 1398 * 1399 * with 'mailto:[email protected]?subject=hey' : 1400 * array( 'protocol' => 'mailto:', 'slashes' => '', 'rest' => '[email protected]?subject=hey' ) 1401 * 1402 * with 'http://example.com/blah.html' : 1403 * array( 'protocol' => 'http:', 'slashes' => '//', 'rest' => 'example.com/blah.html' ) 1404 * 1405 * @since 1.7 1406 * @param string $url URL to be parsed 1407 * @param array $array Optional, array of key names to be used in returned array 1408 * @return array|false false if no protocol found, array of ('protocol' , 'slashes', 'rest') otherwise 1409 */ 1410 function yourls_get_protocol_slashes_and_rest( $url, $array = [ 'protocol', 'slashes', 'rest' ] ) { 1411 $proto = yourls_get_protocol( $url ); 1412 1413 if ( !$proto or count( $array ) != 3 ) { 1414 return false; 1415 } 1416 1417 list( $null, $rest ) = explode( $proto, $url, 2 ); 1418 1419 list( $proto, $slashes ) = explode( ':', $proto ); 1420 1421 return [ 1422 $array[ 0 ] => $proto.':', 1423 $array[ 1 ] => $slashes, 1424 $array[ 2 ] => $rest 1425 ]; 1426 } 1427 1428 /** 1429 * Set URL scheme (HTTP or HTTPS) to a URL 1430 * 1431 * @since 1.7.1 1432 * @param string $url URL 1433 * @param string $scheme scheme, either 'http' or 'https' 1434 * @return string URL with chosen scheme 1435 */ 1436 function yourls_set_url_scheme( $url, $scheme = '' ) { 1437 if ( in_array( $scheme, [ 'http', 'https' ] ) ) { 1438 $url = preg_replace( '!^[a-zA-Z0-9+.-]+://!', $scheme.'://', $url ); 1439 } 1440 return $url; 1441 } 1442 1443 /** 1444 * Tell if there is a new YOURLS version 1445 * 1446 * This function checks, if needed, if there's a new version of YOURLS and, if applicable, displays 1447 * an update notice. 1448 * 1449 * @since 1.7.3 1450 * @return void 1451 */ 1452 function yourls_tell_if_new_version() { 1453 yourls_debug_log( 'Check for new version: '.( yourls_maybe_check_core_version() ? 'yes' : 'no' ) ); 1454 yourls_new_core_version_notice(YOURLS_VERSION); 1455 } 1456 1457 /** 1458 * File include sandbox 1459 * 1460 * Attempt to include a PHP file, fail with an error message if the file isn't valid PHP code. 1461 * This function does not check first if the file exists : depending on use case, you may check first. 1462 * 1463 * @since 1.9.2 1464 * @param string $file filename (full path) 1465 * @return string|bool string if error, true if success 1466 */ 1467 function yourls_include_file_sandbox($file) { 1468 try { 1469 if (is_readable( $file )) { 1470 require_once $file; 1471 yourls_debug_log("loaded $file"); 1472 return true; 1473 } 1474 } catch ( \Throwable $e ) { 1475 yourls_debug_log("could not load $file"); 1476 return sprintf("%s (%s : %s)", $e->getMessage() , $e->getFile() , $e->getLine() ); 1477 } 1478 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Sat Aug 8 05:10:51 2026 | Cross-referenced by PHPXref 0.7.1 |