Repository: phoenixframework/phoenix
Pull request #2511 (2017-09-21 21:12:07)

Revert "add the context module name in phx.gen.schema run"
Per @chrismccord [here](https://github.com/phoenixframework/phoenix/commit/88eddbf55a8a489883e02e4265bafaa441643bc6#commitcomment-24471061): The namespacing/context ideas come later in the context guide; using them here without explanation is confusing, and adding an explanation would be premature since we tackle this later.

This reverts commit 88eddbf55a8a489883e02e4265bafaa441643bc6.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78521217f..53b3e0f5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 * Enhancements
   * [Controller] Support partial file downloads with `:offset` and `:length` options to `send_download/3`
+  * [Logger] Add whitelist support to `filter_parameters` logger configuration, via new `:keep` tuple format
 
 ## 1.3.0 (2017-07-28)
 
diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js
index 9ee8a7643..387663c0c 100644
--- a/assets/js/phoenix.js
+++ b/assets/js/phoenix.js
@@ -180,6 +180,7 @@
  * @module phoenix
  */
 
+const global = typeof(self) !== "undefined" ? self : window
 const VSN = "2.0.0"
 const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
 const DEFAULT_TIMEOUT = 10000
@@ -688,7 +689,7 @@ export class Socket {
     this.sendBuffer           = []
     this.ref                  = 0
     this.timeout              = opts.timeout || DEFAULT_TIMEOUT
-    this.transport            = opts.transport || window.WebSocket || LongPoll
+    this.transport            = opts.transport || global.WebSocket || LongPoll
     this.defaultEncoder       = Serializer.encode
     this.defaultDecoder       = Serializer.decode
     if(this.transport !== LongPoll){
@@ -1034,18 +1035,13 @@ export class LongPoll {
 export class Ajax {
 
   static request(method, endPoint, accept, body, timeout, ontimeout, callback){
-    if(typeof window !== 'undefined'){
-      if(window.XDomainRequest){
-        let req = new XDomainRequest() // IE8, IE9
-        this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
-      } else {
-        let req = window.XMLHttpRequest ?
-                    new window.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
-                    new ActiveXObject("Microsoft.XMLHTTP") // IE6, IE5
-        this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)
-      }
+    if(global.XDomainRequest){
+      let req = new XDomainRequest() // IE8, IE9
+      this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
     } else {
-      let req = new XMLHttpRequest(); // tvOS support
+      let req = global.XMLHttpRequest ?
+                  new global.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
+                  new ActiveXObject("Microsoft.XMLHTTP") // IE6, IE5
       this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)
     }
   }
@@ -1241,4 +1237,4 @@ class Timer {
       this.callback()
     }, this.timerCalc(this.tries + 1))
   }
-}
\ No newline at end of file
+}
diff --git a/guides/docs/contexts.md b/guides/docs/contexts.md
index f82b562b5..134e5759e 100644
--- a/guides/docs/contexts.md
+++ b/guides/docs/contexts.md
@@ -951,7 +951,7 @@ With our new plugs in place, we can now modify our `create`, `edit`, `update`, a
   end
 ```
 
-We modified the `create` action to grab our `current_author` from the connection assigns, which was placed there by our `authenticate_user` plug in the router. We then passed our current author into `CMS.create_page` where it will be used to associate the author to the new page. Next, we changed the `update` action to pass the `conn.assigns.page` into `CMS.update_page/2`, rather than fetching it directly in the action. Since our `authorize_page` plug already fetched the page and placed it into the assigns, we can simply reference it here in the action. Similarly, we updated the `delete` action to pass the `conn.assigns.page` into the `CMS` rather than fetching the page in the action.
+We modified the `create` action to grab our `current_author` from the connection assigns, which was placed there by our `require_existing_author` plug. We then passed our current author into `CMS.create_page` where it will be used to associate the author to the new page. Next, we changed the `update` action to pass the `conn.assigns.page` into `CMS.update_page/2`, rather than fetching it directly in the action. Since our `authorize_page` plug already fetched the page and placed it into the assigns, we can simply reference it here in the action. Similarly, we updated the `delete` action to pass the `conn.assigns.page` into the `CMS` rather than fetching the page in the action.
 
 To complete the web changes, let's display the author when showing a page. First, open up `lib/hello_web/views/cms/page_view.ex` and add a helper function to handle formatting the author's name:
 
diff --git a/guides/docs/ecto.md b/guides/docs/ecto.md
index 446cffc89..3f9986db2 100644
--- a/guides/docs/ecto.md
+++ b/guides/docs/ecto.md
@@ -27,11 +27,11 @@ ALTER USER postgres WITH SUPERUSER;
 Now that we have Ecto and Postgres installed and configured, the easiest way to use Ecto is to generate an Ecto *schema* through the `phx.gen.schema` task. Ecto schemas are a way for us to specify how Elixir data types map to and from external sources, such as database tables. Let's generate a `User` schema with `name`, `email`, `bio`, and `number_of_pets` fields.
 
 ```console
-$ mix phx.gen.schema Accounts.User users name:string email:string \
+$ mix phx.gen.schema User users name:string email:string \
 bio:string number_of_pets:integer
 
-* creating ./lib/hello/accounts/user.ex
-* creating priv/repo/migrations/20170523151118_create_users.exs
+* creating ./lib/hello/user.ex
+* creating priv/repo/migrations/20170523151118_create_user.exs
 
 Remember to update your repository by running migrations:
 
@@ -162,10 +162,10 @@ Ecto schemas are responsible for mapping Elixir values to external data sources,
 Here's the `User` schema that Phoenix generated for us.
 
 ```elixir
-defmodule Hello.Accounts.User do
+defmodule Hello.User do
   use Ecto.Schema
   import Ecto.Changeset
-  alias Hello.Accounts.User
+  alias Hello.User
 
 
   schema "users" do
@@ -206,13 +206,13 @@ Right now, we have two transformations in our pipeline. In the first call, we in
 `cast/3` first takes a struct, then the parameters (the proposed updates), and then the final field is the list of columns to be updated. `cast/3` also will only take fields that exist in the schema.
 Next, `validate_required/3` checks that this list of fields is present in the changeset that `cast/3` returns. By default with the generator, all fields are required.
 
-We can verify this functionality in iex. Let's fire up our application inside `iex` by running `iex -S mix`. In order to minimize typing and make this easier to read, let's alias our `Hello.Accounts.User` struct.
+We can verify this functionality in iex. Let's fire up our application inside `iex` by running `iex -S mix`. In order to minimize typing and make this easier to read, let's alias our `Hello.User` struct.
 
 ```console
 $ iex -S mix
 
-iex> alias Hello.Accounts.User
-Hello.Accounts.User
+iex> alias Hello.User
+Hello.User
 ```
 
 Next, let's build a changeset from our schema with an empty `User` struct, and an empty map of parameters.
@@ -225,7 +225,7 @@ iex> changeset = User.changeset(%User{}, %{})
   email: {"can't be blank", [validation: :required]},
   bio: {"can't be blank", [validation: :required]},
   number_of_pets: {"can't be blank", [validation: :required]}],
- data: #Hello.Accounts.User<>, valid?: false>
+ data: #Hello.User<>, valid?: false>
 ```
 
 Once we have a changeset, we can check it if it is valid.
@@ -288,7 +288,7 @@ iex> changeset = User.changeset(%User{}, params)
 #Ecto.Changeset<action: nil,
  changes: %{bio: "An example to all", email: "joe@example.com",
    name: "Joe Example", number_of_pets: 5}, errors: [],
- data: #Hello.Accounts.User<>, valid?: true>
+ data: #Hello.User<>, valid?: true>
 ```
 
 Our new changeset is valid.
@@ -373,13 +373,13 @@ We've talked a lot about migrations and data-storage, but we haven't yet persist
 Let's head back over to IEx with `iex -S mix`, and insert a couple of users to the database.
 
 ```console
-iex> alias Hello.{Repo, Accounts.User}
-[Hello.Repo, Hello.Accounts.User]
+iex> alias Hello.{Repo, User}
+[Hello.Repo, Hello.User]
 iex> Repo.insert(%User{email: "user1@example.com"})
 [debug] QUERY OK db=4.6ms
 INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", {{2017, 5, 23}, {19, 6, 4, 822044}}, {{2017, 5, 23}, {19, 6, 4, 822055}}]
 {:ok,
- %Hello.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+ %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil, email: "user1@example.com", id: 3,
   inserted_at: ~N[2017-05-23 19:06:04.822044], name: nil, number_of_pets: nil,
   updated_at: ~N[2017-05-23 19:06:04.822055]}}
@@ -388,7 +388,7 @@ iex> Repo.insert(%User{email: "user2@example.com"})
 [debug] QUERY OK db=5.1ms
 INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", {{2017, 5, 23}, {19, 6, 8, 452545}}, {{2017, 5, 23}, {19, 6, 8, 452556}}]
 {:ok,
- %Hello.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+ %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil, email: "user2@example.com", id: 4,
   inserted_at: ~N[2017-05-23 19:06:08.452545], name: nil, number_of_pets: nil,
   updated_at: ~N[2017-05-23 19:06:08.452556]}}
@@ -400,11 +400,11 @@ We started by aliasing our `User` and `Repo` modules for easy access. Next, we c
 iex> Repo.all(User)
 [debug] QUERY OK source="users" db=2.7ms
 SELECT u0."id", u0."bio", u0."email", u0."name", u0."number_of_pets", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
-[%Hello.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+[%Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil, email: "user1@example.com", id: 3,
   inserted_at: ~N[2017-05-23 19:06:04.822044], name: nil, number_of_pets: nil,
   updated_at: ~N[2017-05-23 19:06:04.822055]},
- %Hello.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+ %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil, email: "user2@example.com", id: 4,
   inserted_at: ~N[2017-05-23 19:06:08.452545], name: nil, number_of_pets: nil,
   updated_at: ~N[2017-05-23 19:06:08.452556]}]
diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex
index 3fbf0085c..6d4a1cd6c 100644
--- a/lib/phoenix/logger.ex
+++ b/lib/phoenix/logger.ex
@@ -15,6 +15,15 @@ defmodule Phoenix.Logger do
   case sensitive.
 
   Phoenix's default is `["password"]`.
+
+  Phoenix can filter all parameters by default and selectively keep
+  parameters. This can be configured like so:
+
+      config :phoenix, :filter_parameters, {:keep, ["id", "order"]}
+
+  With the configuration above, Phoenix will filter all parameters,
+  except those that match exactly `id` or `order`. If a kept parameter
+  matches, all parameters nested under that one will also be kept.
   """
   require Logger
   import Phoenix.Controller
@@ -48,22 +57,41 @@ defmodule Phoenix.Logger do
 
   @doc false
   def filter_values(values, params \\ Application.get_env(:phoenix, :filter_parameters))
-  def filter_values(%{__struct__: mod} = struct, _filter_params) when is_atom(mod) do
+  def filter_values(values, {:discard, params}), do: discard_values(values, params)
+  def filter_values(values, {:keep, params}), do: keep_values(values, params)
+  def filter_values(values, params), do: discard_values(values, params)
+
+  defp discard_values(%{__struct__: mod} = struct, _params) when is_atom(mod) do
     struct
   end
-  def filter_values(%{} = map, filter_params) do
+  defp discard_values(%{} = map, params) do
     Enum.into map, %{}, fn {k, v} ->
-      if is_binary(k) and String.contains?(k, filter_params) do
+      if is_binary(k) and String.contains?(k, params) do
         {k, "[FILTERED]"}
       else
-        {k, filter_values(v, filter_params)}
+        {k, discard_values(v, params)}
+      end
+    end
+  end
+  defp discard_values([_|_] = list, params) do
+    Enum.map(list, &discard_values(&1, params))
+  end
+  defp discard_values(other, _params), do: other
+
+  defp keep_values(%{__struct__: mod}, _params) when is_atom(mod), do: "[FILTERED]"
+  defp keep_values(%{} = map, params) do
+    Enum.into map, %{}, fn {k, v} ->
+      if is_binary(k) and k in params do
+        {k, discard_values(v, [])}
+      else
+        {k, keep_values(v, params)}
       end
     end
   end
-  def filter_values([_|_] = list, filter_params) do
-    Enum.map(list, &filter_values(&1, filter_params))
+  defp keep_values([_|_] = list, params) do
+    Enum.map(list, &keep_values(&1, params))
   end
-  def filter_values(other, _filter_params), do: other
+  defp keep_values(_other, _params), do: "[FILTERED]"
 
   defp params(%Plug.Conn.Unfetched{}), do: "[UNFETCHED]"
   defp params(params) do
diff --git a/priv/static/phoenix.js b/priv/static/phoenix.js
index 331a1c0f9..3e3958df9 100644
--- a/priv/static/phoenix.js
+++ b/priv/static/phoenix.js
@@ -201,6 +201,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
  * @module phoenix
  */
 
+var global = typeof self !== "undefined" ? self : window;
 var VSN = "2.0.0";
 var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
 var DEFAULT_TIMEOUT = 10000;
@@ -891,7 +892,7 @@ var Socket = exports.Socket = function () {
     this.sendBuffer = [];
     this.ref = 0;
     this.timeout = opts.timeout || DEFAULT_TIMEOUT;
-    this.transport = opts.transport || window.WebSocket || LongPoll;
+    this.transport = opts.transport || global.WebSocket || LongPoll;
     this.defaultEncoder = Serializer.encode;
     this.defaultDecoder = Serializer.decode;
     if (this.transport !== LongPoll) {
@@ -1410,18 +1411,13 @@ var Ajax = exports.Ajax = function () {
   _createClass(Ajax, null, [{
     key: "request",
     value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
-      if (typeof window !== 'undefined') {
-        if (window.XDomainRequest) {
-          var req = new XDomainRequest(); // IE8, IE9
-          this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
-        } else {
-          var _req = window.XMLHttpRequest ? new window.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
-          new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
-          this.xhrRequest(_req, method, endPoint, accept, body, timeout, ontimeout, callback);
-        }
+      if (global.XDomainRequest) {
+        var req = new XDomainRequest(); // IE8, IE9
+        this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
       } else {
-        var _req2 = new XMLHttpRequest(); // tvOS support
-        this.xhrRequest(_req2, method, endPoint, accept, body, timeout, ontimeout, callback);
+        var _req = global.XMLHttpRequest ? new global.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
+        new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
+        this.xhrRequest(_req, method, endPoint, accept, body, timeout, ontimeout, callback);
       }
     }
   }, {


Test patch:
diff --git a/test/phoenix/logger_test.exs b/test/phoenix/logger_test.exs
index a454000f2..23dddfd1c 100644
--- a/test/phoenix/logger_test.exs
+++ b/test/phoenix/logger_test.exs
@@ -72,32 +72,67 @@ defmodule Phoenix.LoggerTest do
     assert output =~ "Parameters: [UNFETCHED]"
   end
 
-  test "filter_values" do
-    assert Phoenix.Logger.filter_values(%{"foo" => "bar", "password" => "should_not_show"}, ["password"]) ==
-           %{"foo" => "bar", "password" => "[FILTERED]"}
-  end
+  describe "filter_values/2 with discard strategy" do
+    test "in top level map" do
+      values = %{"foo" => "bar", "password" => "should_not_show"}
+      assert Phoenix.Logger.filter_values(values, ["password"]) ==
+            %{"foo" => "bar", "password" => "[FILTERED]"}
+    end
 
-  test "filter_values when a map has secret key" do
-    assert Phoenix.Logger.filter_values(%{"foo" => "bar", "map" => %{"password" => "should_not_show"}}, ["password"]) ==
-           %{"foo" => "bar", "map" => %{"password" => "[FILTERED]"}}
-  end
+    test "when a map has secret key" do
+      values = %{"foo" => "bar", "map" => %{"password" => "should_not_show"}}
+      assert Phoenix.Logger.filter_values(values, ["password"]) ==
+            %{"foo" => "bar", "map" => %{"password" => "[FILTERED]"}}
+    end
 
-  test "filter_values when a list has a map with secret" do
-    assert Phoenix.Logger.filter_values(%{"foo" => "bar", "list" => [%{"password" => "should_not_show"}]}, ["password"]) ==
-           %{"foo" => "bar", "list" => [%{"password" => "[FILTERED]"}]}
-  end
+    test "when a list has a map with secret" do
+      values = %{"foo" => "bar", "list" => [%{"password" => "should_not_show"}]}
+      assert Phoenix.Logger.filter_values(values, ["password"]) ==
+            %{"foo" => "bar", "list" => [%{"password" => "[FILTERED]"}]}
+    end
 
-  test "filter_values does not filter structs" do
-    assert Phoenix.Logger.filter_values(%{"foo" => "bar", "file" => %Plug.Upload{}}, ["password"]) ==
-           %{"foo" => "bar", "file" => %Plug.Upload{}}
+    test "does not filter structs" do
+      values = %{"foo" => "bar", "file" => %Plug.Upload{}}
+      assert Phoenix.Logger.filter_values(values, ["password"]) ==
+            %{"foo" => "bar", "file" => %Plug.Upload{}}
+
+      values = %{"foo" => "bar", "file" => %{__struct__: "s"}}
+      assert Phoenix.Logger.filter_values(values, ["password"]) ==
+            %{"foo" => "bar", "file" => %{:__struct__ => "s"}}
+    end
 
-    assert Phoenix.Logger.filter_values(%{"foo" => "bar", "file" => %{__struct__: "s"}}, ["password"]) ==
-           %{"foo" => "bar", "file" => %{:__struct__ => "s"}}
+    test "does not fail on atomic keys" do
+      values = %{:foo => "bar", "password" => "should_not_show"}
+      assert Phoenix.Logger.filter_values(values, ["password"]) ==
+            %{:foo => "bar", "password" => "[FILTERED]"}
+    end
   end
 
-  test "filter_values does not fail on atomic keys" do
-    assert Phoenix.Logger.filter_values(%{:foo => "bar", "password" => "should_not_show"}, ["password"]) ==
-           %{:foo => "bar", "password" => "[FILTERED]"}
+  describe "filter_values/2 with keep strategy" do
+    test "discards values not specified in params" do
+      values = %{"foo" => "bar", "password" => "abc123", "file" => %Plug.Upload{}}
+      assert Phoenix.Logger.filter_values(values, {:keep, []}) ==
+            %{"foo" => "[FILTERED]", "password" => "[FILTERED]", "file" => "[FILTERED]"}
+    end
+
+    test "keeps values that are specified in params" do
+      values = %{"foo" => "bar", "password" => "abc123", "file" => %Plug.Upload{}}
+      assert Phoenix.Logger.filter_values(values, {:keep, ["foo", "file"]}) ==
+            %{"foo" => "bar", "password" => "[FILTERED]", "file" => %Plug.Upload{}}
+    end
+
+    test "keeps all values under keys that are kept" do
+      values = %{"foo" => %{"bar" => 1, "baz" => 2}}
+      assert Phoenix.Logger.filter_values(values, {:keep, ["foo"]}) ==
+            %{"foo" => %{"bar" => 1, "baz" => 2}}
+    end
+
+    test "only filters leaf values" do
+      values = %{"foo" => %{"bar" => 1, "baz" => 2}, "ids" => [1, 2]}
+      assert Phoenix.Logger.filter_values(values, {:keep, []}) ==
+            %{"foo" => %{"bar" => "[FILTERED]", "baz" => "[FILTERED]"},
+              "ids" => ["[FILTERED]", "[FILTERED]"]}
+    end
   end
 
   test "logs phoenix_channel_join as configured by the channel" do