[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

/includes/ -> functions.php (source)

   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  }


Generated: Sat Sep 5 05:13:00 2026 Cross-referenced by PHPXref 0.7.1