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