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