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


Generated: Sat Jul 11 05:10:54 2026 Cross-referenced by PHPXref 0.7.1