├── .formatter.exs ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── lib └── plug │ ├── redirect.ex │ └── redirect │ └── route.ex ├── mix.exs └── test ├── plug ├── redirect │ └── route_test.exs └── redirect_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "mix.exs", 4 | ".iex.exs", 5 | ".formatter.exs", 6 | "{apps,config,lib,test}/**/*.{ex,exs}" 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | mix.lock 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - 1.7.0 5 | 6 | otp_release: 7 | - 21.0 8 | 9 | env: MIX_ENV=test 10 | 11 | sudo: false # faster builds 12 | 13 | notifications: 14 | email: false 15 | 16 | script: 17 | - mix compile --warnings-as-errors 18 | - mix test 19 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | PlugRedirect 2 | Copyright © 2016 - Present Louis Pilfold - MIT Licence 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PlugRedirect 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/lpil/plug-redirect.svg?branch=master)](https://travis-ci.org/lpil/plug-redirect) 5 | [![Hex version](https://img.shields.io/hexpm/v/plug_redirect.svg "Hex version")](https://hex.pm/packages/plug_redirect) 6 | [![Hex downloads](https://img.shields.io/hexpm/dt/plug_redirect.svg "Hex downloads")](https://hex.pm/packages/plug_redirect) 7 | 8 | A plug builder for redirecting requests. 9 | 10 | 11 | ## Usage 12 | 13 | Add PlugRedirect to your Mix dependencies 14 | 15 | ```elixir 16 | # mix.exs 17 | def deps do 18 | [ 19 | {:plug_redirect, "~> 1.0"}, 20 | ] 21 | end 22 | ``` 23 | 24 | Fetch it: 25 | 26 | ``` 27 | mix deps.get 28 | ``` 29 | 30 | Create a new module and specify your redirects like so: 31 | 32 | ```elixir 33 | defmodule MyApp.Redirector do 34 | use Plug.Redirect 35 | 36 | # Argument #1 is the path to redirect from 37 | # Argument #2 is the path to redirect to 38 | redirect "/ada", "/lovelace" 39 | 40 | # An HTTP status code can also be specified 41 | redirect "/grace", "/hopper", status: 302 42 | 43 | # Segements prefixed with a colon will match anything 44 | redirect "/blog/:anything", "/blog-closed" 45 | 46 | # Variable segments can be interpolated into the destination 47 | redirect "/users/:name", "/profile/:name" 48 | end 49 | ``` 50 | 51 | This compiles into a plug that you can insert into your application's 52 | plug middleware stack. 53 | 54 | In Phoenix, insert the plug in your web application Endpoint. 55 | 56 | ```elixir 57 | defmodule MyApp.Endpoint do 58 | use Phoenix.Endpoint, otp_app: :my_app 59 | 60 | plug Plug.RequestId 61 | plug Plug.Logger 62 | plug Plug.MethodOverride 63 | plug Plug.Head 64 | 65 | plug MyApp.Redirector # Insert your redirector anywhere before the router 66 | plug MyApp.Router 67 | end 68 | ``` 69 | 70 | 71 | # LICENCE 72 | 73 | ``` 74 | PlugRedirect 75 | Copyright © 2016 - Present Louis Pilfold - MIT Licence 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining 78 | a copy of this software and associated documentation files (the "Software"), 79 | to deal in the Software without restriction, including without limitation 80 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 81 | and/or sell copies of the Software, and to permit persons to whom the 82 | Software is furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included 85 | in all copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 89 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 90 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 91 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 92 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 93 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 94 | ``` 95 | -------------------------------------------------------------------------------- /lib/plug/redirect.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Redirect do 2 | @moduledoc """ 3 | A plug builder for redirecting requests. 4 | """ 5 | 6 | alias Plug.Redirect.Route 7 | 8 | @redirect_codes [301, 302, 303, 307, 308] 9 | 10 | defmacro __using__(_opts) do 11 | quote do 12 | import unquote(__MODULE__), only: [redirect: 3, redirect: 2] 13 | @before_compile unquote(__MODULE__) 14 | def init(opts), do: opts 15 | end 16 | end 17 | 18 | defmacro __before_compile__(_env) do 19 | quote do 20 | def call(conn, _opts), do: conn 21 | end 22 | end 23 | 24 | @doc """ 25 | Specify a redirect. 26 | 27 | The first argument is the request to match upon for the redirect. 28 | 29 | The second argument is the location to redirect the request to. 30 | 31 | ## Options 32 | 33 | * `:status` - The HTTP status code to use for the redirect. 34 | """ 35 | defmacro redirect(from, to, options \\ [{:status, 301}]) 36 | 37 | defmacro redirect(from, to, [{:status, status}]) 38 | when status in @redirect_codes do 39 | from_segments = from |> Route.to_path_info_ast() |> Enum.filter(&(&1 != "")) 40 | to_segments = to |> Route.to_path_info_ast() 41 | 42 | quote do 43 | def call(%Plug.Conn{path_info: unquote(from_segments)} = conn, _opts) do 44 | to = unquote(to_segments) |> Enum.join("/") 45 | 46 | conn 47 | |> Plug.Conn.put_resp_header("location", to) 48 | |> Plug.Conn.resp(unquote(status), "You are being redirected.") 49 | |> Plug.Conn.halt() 50 | end 51 | end 52 | end 53 | 54 | defmacro redirect(status, from, to) when is_integer(status) do 55 | quote do 56 | Plug.Redirect.redirect(unquote(from), unquote(to), status: unquote(status)) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/plug/redirect/route.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Redirect.Route do 2 | @moduledoc """ 3 | Helpers for transforming route strings to ASTs that can be used in 4 | redirection functions. 5 | """ 6 | 7 | def to_path_info_ast(path) do 8 | path 9 | |> String.split("/") 10 | |> Enum.map(&segment_to_var/1) 11 | end 12 | 13 | defp segment_to_var(":") do 14 | ":" 15 | end 16 | 17 | defp segment_to_var(":" <> segment) do 18 | var_name = String.to_atom(segment) 19 | {var_name, [], __MODULE__} 20 | end 21 | 22 | defp segment_to_var(segment) do 23 | segment 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugRedirect.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :plug_redirect, 7 | version: "1.0.0", 8 | elixir: "~> 1.0", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | name: "PlugRedirect", 13 | source_url: "https://github.com/lpil/plug-redirect", 14 | description: "A plug builder for redirecting requests.", 15 | package: [ 16 | maintainers: ["Louis Pilfold"], 17 | licenses: ["MIT"], 18 | links: %{"GitHub" => "https://github.com/lpil/plug-redirect"} 19 | ] 20 | ] 21 | end 22 | 23 | def application do 24 | [extra_applications: []] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:plug, ">= 1.0.0"}, 30 | {:mix_test_watch, ">= 0.0.0", only: :dev}, 31 | {:ex_doc, ">= 0.0.0", only: :dev} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/plug/redirect/route_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.Redirect.RouteTest do 2 | use ExUnit.Case 3 | 4 | alias Plug.Redirect.Route 5 | 6 | test "to_path_info_ast" do 7 | ast = Route.to_path_info_ast("/foo/bar") 8 | assert ast == ["", "foo", "bar"] 9 | end 10 | 11 | test "to_path_info_ast with variable section" do 12 | ast = Route.to_path_info_ast("/foo/:bar") 13 | assert ["", "foo", {:bar, [], _}] = ast 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/plug/redirect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.RedirectTest do 2 | use ExUnit.Case 3 | use Plug.Test 4 | 5 | defmodule MyPlug do 6 | use Plug.Redirect 7 | 8 | redirect("/foo/bar", "/go/here", status: 301) 9 | redirect("/jump/up", "/get/down", status: 302) 10 | redirect("/ra/wavy", "/by/droid", status: 303) 11 | redirect("/rock/on", "/roll/out", status: 307) 12 | 13 | redirect("/no/status", "/301/default") 14 | 15 | redirect("/blog/:slug", "/no-more-blog") 16 | redirect("/users/:slug", "/profile/:slug") 17 | redirect("/other/:slug", "http://somewhere.com/profile/:slug") 18 | 19 | # Old API 20 | redirect(301, "/old/foo/bar", "/go/here") 21 | redirect(302, "/old/jump/up", "/get/down") 22 | redirect(303, "/old/ra/wavy", "/by/droid") 23 | redirect(307, "/old/rock/on", "/roll/out") 24 | end 25 | 26 | @opts MyPlug.init([]) 27 | @methods ~w(get head post put delete trace options connect patch)a 28 | 29 | for method <- @methods do 30 | test "it passes through when no redirects match a #{method}" do 31 | conn = unquote(method) |> conn("/hello") 32 | result = conn |> MyPlug.call(@opts) 33 | assert conn == result 34 | end 35 | end 36 | 37 | test "it can perform 301 redirects" do 38 | conn = get("/foo/bar") 39 | assert_redirect(conn, 301, "/go/here") 40 | end 41 | 42 | test "it can perform 302 redirects" do 43 | conn = get("/jump/up") 44 | assert_redirect(conn, 302, "/get/down") 45 | end 46 | 47 | test "it can perform 303 redirects" do 48 | conn = get("/ra/wavy") 49 | assert_redirect(conn, 303, "/by/droid") 50 | end 51 | 52 | test "it can perform 307 redirects" do 53 | conn = get("/rock/on") 54 | assert_redirect(conn, 307, "/roll/out") 55 | end 56 | 57 | describe "backwards compatibility with old API" do 58 | test "it can perform 301 redirects" do 59 | conn = get("/old/foo/bar") 60 | assert_redirect(conn, 301, "/go/here") 61 | end 62 | 63 | test "it can perform 302 redirects" do 64 | conn = get("/old/jump/up") 65 | assert_redirect(conn, 302, "/get/down") 66 | end 67 | 68 | test "it can perform 303 redirects" do 69 | conn = get("/old/ra/wavy") 70 | assert_redirect(conn, 303, "/by/droid") 71 | end 72 | 73 | test "it can perform 307 redirects" do 74 | conn = get("/old/rock/on") 75 | assert_redirect(conn, 307, "/roll/out") 76 | end 77 | end 78 | 79 | test "when given no status it defaults to 301" do 80 | conn = get("/no/status") 81 | assert_redirect(conn, 301, "/301/default") 82 | end 83 | 84 | test "variable sections can exist" do 85 | conn = get("/blog/some-article") 86 | assert_redirect(conn, 301, "/no-more-blog") 87 | conn = get("/blog/another-article") 88 | assert_redirect(conn, 301, "/no-more-blog") 89 | end 90 | 91 | test "other hosts can be redirected to" do 92 | conn = get("/other/louis") 93 | assert_redirect(conn, 301, "http://somewhere.com/profile/louis") 94 | end 95 | 96 | defp get(path) do 97 | :get |> conn(path) |> MyPlug.call(@opts) 98 | end 99 | 100 | defp assert_redirect(conn, code, to) do 101 | assert conn.state == :set 102 | assert conn.status == code 103 | assert Plug.Conn.get_resp_header(conn, "location") == [to] 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------