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


Generated: Sat Aug 8 05:10:51 2026 Cross-referenced by PHPXref 0.7.1