【问题标题】:How do I safely join relative url segments?如何安全地加入相对 url 段?
【发布时间】:2012-01-17 19:49:53
【问题描述】:

我正在尝试找到一种将部分 url 路径段连接在一起的可靠方法。有没有快速的方法来做到这一点?

我尝试了以下方法:

puts URI::join('resource/', '/edit', '12?option=test')

我希望:

resource/edit/12?option=test

但我得到了错误:

`merge': both URI are relative (URI::BadURIError)

我过去曾为此使用过File.join(),但将文件库用于 url 似乎有些不对劲。

【问题讨论】:

  • “将文件库用于 url 似乎有些不对劲”,这是正确的。 File.join 对操作系统很敏感,会根据操作系统更改用作分隔符的字符。那会给你带来不好的结果。

标签: ruby uri


【解决方案1】:

URI 的 api 不是很好。

URI::join 仅当第一个以协议的绝对 uri 开始时才有效,而后面的以正确的方式是相对的...除非我尝试这样做,甚至无法做到工作。

这至少不会出错,但是为什么会跳过中间组件呢?

 URI::join('http://somewhere.com/resource', './edit', '12?option=test') 

我认为也许 URI 有点糟糕。它在实例上缺少重要的 api,例如实例 #join 或相对于基本 uri 进行评估的方法,这是您所期望的。这有点糟糕。

我认为您将不得不自己编写它。或者只是使用 File.join 和其他文件路径方法,在测试所有你能想到的边缘情况之后,以确保它符合你的期望/期望。

edit 2016 年 12 月 9 日我发现 addressable gem 做得很好。

base = Addressable::URI.parse("http://example.com")
base + "foo.html"
# => #<Addressable::URI:0x3ff9964aabe4 URI:http://example.com/foo.html>

base = Addressable::URI.parse("http://example.com/path/to/file.html")
base + "relative_file.xml"
# => #<Addressable::URI:0x3ff99648bc80 URI:http://example.com/path/to/relative_file.xml>

base = Addressable::URI.parse("https://example.com/path")
base + "//newhost/somewhere.jpg"
# => #<Addressable::URI:0x3ff9960c9ebc URI:https://newhost/somewhere.jpg>

base = Addressable::URI.parse("http://example.com/path/subpath/file.html")
base + "../up-one-level.html"
=> #<Addressable::URI:0x3fe13ec5e928 URI:http://example.com/path/up-one-level.html>

【讨论】:

  • 如果你需要String版本的uri,不要忘记做to_s,否则你会得到URI::HTTP对象
  • Addresable 如果有多个,也会跳过中间段。例如(Addressable::URI.parse("http://example.com") + "abc" + "xyz").to_s 生成 http://example.com/xyz。在我对这个问题的最初回答 (stackoverflow.com/a/9067748/345034) 多年后,我仍然认为 File.join 是加入 URL 段的最安全方式。 :D
【解决方案2】:

将uri作为URI::Generic或其子类

uri.path += '/123' 

享受吧!

06/25/2016 更新适用于持怀疑态度的人

require 'uri'
uri = URI('http://ioffe.net/boris')
uri.path += '/123'
p uri

输出

 <URI::HTTP:0x2341a58 URL:http://ioffe.net/boris/123>

Run me

【讨论】:

  • 这会将整个路径组件替换为“/123”,它不会扩展现有路径。
  • 这是我得到的:uri = URI("httpx://example.com/mypath/"); uri+= '/123' => #<:https href="https://example.com/123" rel="nofollow" target="_blank">example.com/123>
  • @AneilMallavarapu 你应该考虑你做错了什么。在这种情况下,您应该使用 url.path 而不是 url。在否决合法答案之前,请仔细查看我提供的示例。
【解决方案3】:

问题是resource/是相对于当前目录的,而/edit由于前导斜杠是指顶级目录。如果不知道edit 包含resource,就不可能加入这两个目录。

如果您正在寻找纯字符串操作,只需从所有部分删除前导或尾随斜杠,然后将它们与/ 结合起来。

【讨论】:

  • 好吧,除非您不在域根目录下工作,在这种情况下,第一部分的前导 / 会有所作为。
  • 这很容易强制使用空字符串作为第一个参数。
【解决方案4】:

使用 URI.join 的方法是:

URI.join('http://example.com', '/foo/', 'bar')

注意尾部的斜杠。您可以在此处找到完整的文档:

http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-join

【讨论】:

  • 如果你需要自己管理前导和尾随斜线,那么首先使用这种方法有什么意义呢?
  • 在这种情况下,您将获得一个 URI::HTTP 对象。尽管您有一个有效的观点,但当我需要一个字符串并且它会处理斜杠时,我通常使用File.join('http://example.com', '/foo/', 'bar')。 PS:我只在基于 linux 的系统和服务器上工作,所以我不会遇到上述文件分隔符问题。
【解决方案5】:

正如您所注意到的,URI::join 不会将路径与重复的斜杠组合在一起,因此它不适合该部分。

事实证明,实现这一点不需要大量的 Ruby 代码:

module GluePath

  def self.join(*paths, separator: '/')
    paths = paths.compact.reject(&:empty?)
    last = paths.length - 1
    paths.each_with_index.map { |path, index|
      _expand(path, index, last, separator)
    }.join
  end

  def self._expand(path, current, last, separator)
    if path.start_with?(separator) && current != 0
      path = path[1..-1]
    end

    unless path.end_with?(separator) || current == last
      path = [path, separator]
    end

    path
  end
end

该算法处理连续斜杠,保留开始和结束斜杠,并忽略 nil 和空字符串。

puts GluePath::join('resource/', '/edit', '12?option=test')

输出

resource/edit/12?option=test

【讨论】:

  • 实际上,File.join 在 Windows 上不使用反斜杠分隔符。它使用正斜杠 (/),就像在 Linux 上一样。
  • 如果对他人有帮助,starts_with?ends_with? 可能需要分别为 start_with?end_with?
  • 编辑了答案以与 Ruby 标准库兼容,没有 Active Support 扩展。
【解决方案6】:

使用此代码:

File.join('resource/', '/edit', '12?option=test').
     gsub(File::SEPARATOR, '/').
     sub(/^\//, '')
# => resource/edit/12?option=test

空字符串示例:

File.join('', '/edit', '12?option=test').
     gsub(File::SEPARATOR, '/').
     sub(/^\//, '')
# => edit/12?option=test

如果可能的话,或者使用它来使用像resource/edit/12?option=test 这样的段,其中http: 只是获取有效URI 的占位符。这对我有用。

URI.
  join('http:', 'resource/', 'edit/', '12?option=test').
  path.
  sub(/^\//, '')
# => "resource/edit/12"

【讨论】:

  • 我不相信这将适用于 Windows 机器,其中分隔符是反斜杠。
  • 确实如此。您需要设置分隔符。
【解决方案7】:

您可以使用File.join('resource/', '/edit', '12?option=test')

【讨论】:

  • 这是一个幸运的巧合,但打电话来表达你的意图并不是正确的方法。
  • 这不会在 Windows 上使用 \ 吗?
【解决方案8】:

未优化的解决方案。请注意,它不考虑查询参数。它只处理路径。

class URL
  def self.join(*str)
    str.map { |path|
      new_path = path
      # Check the first character
      if path[0] == "/"
        new_path = new_path[1..-1]
      end

      # Check the last character
      if path[-1] != "/"
        new_path += "/"
      end

      new_path
    }.join
  end
end

【讨论】:

    【解决方案9】:

    这个问题已经有近十年的历史了,但似乎没有完美的解决方案。

    少数已发布的答案无法处理多个//,例如类似path = path[1..-1] if path.start_with?('/')

    简单地调用File.join(*paths) 的答案似乎是公认的“Ruby 方式”,但在您传递URI 对象的情况下它们会失败,例如File.join(URI.join('some/path')) 失败并显示 TypeError: no implicit conversion of URI::Generic into String

    以下是我最终使用的:

    module UrlHelper
      def self.join(*paths)
        # yes, Ruby's stdlib really does lack a functional join method for URLs
        File.join(*paths.map(&:to_s))
      end
    end
    

    【讨论】:

      【解决方案10】:

      我改进了@Maximo Mussini 的脚本,使其能够正常运行:

      SmartURI.join('http://example.com/subpath', 'hello', query: { token: secret })
      => "http://example.com/subpath/hello?token=secret"
      

      https://gist.github.com/zernel/0f10c71f5a9e044653c1a65c6c5ad697

      require 'uri'
      
      module SmartURI
        SEPARATOR = '/'
      
        def self.join(*paths, query: nil)
          paths = paths.compact.reject(&:empty?)
          last = paths.length - 1
          url = paths.each_with_index.map { |path, index|
            _expand(path, index, last)
          }.join
          if query.nil?
            return url
          elsif query.is_a? Hash
            return url + "?#{URI.encode_www_form(query.to_a)}"
          else
            raise "Unexpected input type for query: #{query}, it should be a hash."
          end
        end
      
        def self._expand(path, current, last)
          if path.starts_with?(SEPARATOR) && current != 0
            path = path[1..-1]
          end
      
          unless path.ends_with?(SEPARATOR) || current == last
            path = [path, SEPARATOR]
          end
      
          path
        end
      end
      

      【讨论】:

        【解决方案11】:

        你可以用这个:

        URI.join('http://exemple.com', '/a/', 'b/', 'c/', 'd')
        => #<URI::HTTP http://exemple.com/a/b/c/d>
        URI.join('http://exemple.com', '/a/', 'b/', 'c/', 'd').to_s
        => "http://exemple.com/a/b/c/d"
        

        见:http://ruby-doc.org/stdlib-2.4.1/libdoc/uri/rdoc/URI.html#method-c-join-label-Synopsis

        【讨论】:

          【解决方案12】:

          我对@9​​87654325@ 的理解是它像网络浏览器一样思考。

          要评估它,请将您的 mental Web 浏览器指向第一个参数,然后继续单击链接,直到浏览到最后一个参数。

          例如,URI::join('http://example.com/resource/', '/edit', '12?option=test'),您可以这样浏览:

          如果第一个链接是/edit/(带有斜杠)或/edit/foo,那么下一个链接将与/edit/ 相关,而不是/

          这个页面可能比我解释得更好:Why is URI.join so counterintuitive?

          【讨论】:

            【解决方案13】:

            这是我对这个问题的简单看法,只是将所有路径段分开并再次将它们连接在一起。这仅在您只使用相对路径段时才有效,但如果您只想这样做,这很方便。

            def join_paths *paths
              paths.map{|p| p.split('/')}
                   .flatten
                   .reject(&:empty?)
                   .compact
                   .join('/')
            end
            

            那么你可以这样使用它:

            join_paths 'foo/', '/bar', 'a/b/c', 'd' #=> "foo/bar/a/b/c/d"
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2011-02-10
              • 1970-01-01
              • 2011-01-03
              • 2013-01-23
              • 1970-01-01
              • 2010-12-21
              • 2017-07-08
              相关资源
              最近更新 更多