Conversation
lib/drab/utils.ex
Outdated
| * `decrypt` - `Boolean`, Decrypt the value, default `false`. When `true`, implies `decode` = `true` | ||
|
|
||
| Examples: | ||
| TODO |
| """ | ||
| def cookie!(socket, key, options \\ []) do | ||
| cookies(socket) | ||
| |> extract_cookie(key, options) |
There was a problem hiding this comment.
Use a function call when a pipeline is only one function long
test/drab/utils_test.exs
Outdated
| cookies = "a=foo; message=Hello, World!; c=bar" | ||
| encoded_cookies = "a=foo; map=eyJtZXNzYWdlIjoiSGVsbG8sIFdvcmxkISJ9; c=bar" | ||
|
|
||
|
|
There was a problem hiding this comment.
There should be no more than 1 consecutive blank lines.
test/drab/utils_test.exs
Outdated
| use ExUnit.Case, ascync: true | ||
| import Drab.Utils | ||
|
|
||
|
|
There was a problem hiding this comment.
There should be no more than 1 consecutive blank lines.
lib/drab/utils.ex
Outdated
|
|
||
| defp extract_cookie_value(cookie, key) do | ||
| Regex.run(~r/#{key}=(.*)/, cookie) | ||
| |> case do |
There was a problem hiding this comment.
Use a function call when a pipeline is only one function long
test/integration/browser_test.exs
Outdated
| assert "" == cookie(socket, "foo") | ||
|
|
||
| #write and retrieve a cookie | ||
| assert {:ok, _} = set_cookie(socket, "map", %{"message" => "Hello, World!"}, path: "/", max_age: 3*24*60*60, encode: true) |
There was a problem hiding this comment.
There are spaces around operators most of the time, but not here.
test/integration/browser_test.exs
Outdated
| assert "" == cookie(socket, "foo") | ||
|
|
||
| #write and retrieve a cookie | ||
| assert {:ok, _} = set_cookie(socket, "map", %{"message" => "Hello, World!"}, path: "/", max_age: 3*24*60*60, encode: true) |
There was a problem hiding this comment.
There are spaces around operators most of the time, but not here.
test/integration/browser_test.exs
Outdated
| assert "" == cookie(socket, "foo") | ||
|
|
||
| #write and retrieve a cookie | ||
| assert {:ok, _} = set_cookie(socket, "map", %{"message" => "Hello, World!"}, path: "/", max_age: 3*24*60*60, encode: true) |
There was a problem hiding this comment.
There are spaces around operators most of the time, but not here.
test/integration/browser_test.exs
Outdated
|
|
||
| test "retrieve a cookie" do | ||
| socket = drab_socket() | ||
|
|
There was a problem hiding this comment.
There should be no trailing white-space at the end of a line.
test/integration/browser_test.exs
Outdated
| assert {:ok, _} = set_cookie(socket, "map1", %{"message" => "Hello, World 1!"}, path: "/", encode: true) | ||
| assert {:ok, _} = set_cookie(socket, "map2", %{"message" => "Hello, World 2!"}, path: "/", encode: true) | ||
| assert {:ok, _} = set_cookie(socket, "map3", %{"message" => "Hello, World 3!"}, path: "/", encode: true) | ||
|
|
There was a problem hiding this comment.
There should be no trailing white-space at the end of a line.
lib/drab/utils.ex
Outdated
| |> case do | ||
| [_, value] -> value | ||
| _ -> "" | ||
| end |
There was a problem hiding this comment.
There should be no trailing white-space at the end of a line.
lib/drab/utils.ex
Outdated
| Drab.Live.Crypto.decrypt(value) | ||
| end | ||
|
|
||
| end # Module No newline at end of file |
There was a problem hiding this comment.
There should be a final \n at the end of each file.
| expires = cookie_expires(socket, max_age) | ||
|
|
||
| # Set cookie | ||
| exec_js(socket, "document.cookie='#{key}=#{encoded_value}; expires=#{expires}; path=#{path};'") |
There was a problem hiding this comment.
It is not intuitive, that it deletes all the other cookies. It should set the existing one, and leave all other untouched.
There was a problem hiding this comment.
I'm not sure to understand what do you mean. Why do you say that the other cookies are deleted?
There was a problem hiding this comment.
Forget about it, my mistake, I did not know that document.cookie = is clever enough to set the specific one only (I thought it is setting up the string with all the cookies). It is OK.
| {:ok, result} | ||
| """ | ||
| # @spec exec_js(Phoenix.Socket.t(), String.t(), Keyword.t()) :: result | ||
| def set_cookie(socket, key, value, options \\ []) do |
There was a problem hiding this comment.
Please provide a spec for all public functions
lib/drab/browser.ex
Outdated
| {:ok, result} | ||
| """ | ||
| def cookies(socket) do | ||
| exec_js(socket, "document.cookie") |
There was a problem hiding this comment.
It would be better to extract cookies to some nice Elixir structure, like map, where the cookie name is a key and the value is the cookie value.
There was a problem hiding this comment.
Yes. I'll rename the original as raw_cookies/1 and I'll write the new code under cookies/1
There was a problem hiding this comment.
Please try to take a look to the last PR to see if this is what you expected
| |> (&((decode || decrypt) && decode_js(&1) || &1)).() | ||
| end | ||
|
|
||
| @doc """ |
There was a problem hiding this comment.
I would hide this from the public API, as the only usage is with cookie?
There was a problem hiding this comment.
I leaved public because I found useful to have them available if for example you need to call them from within a Controller (as I do in one of my app), that's because I put them in Utils instead to just leave them as defp in browser.ex
| * `key` - `String`, The cookie name | ||
|
|
||
| ### Options | ||
|
|
|
General comment: shouldn't we encode/decode cookies with URLEncode by default? I think this is a standard, I can see even on this github page |
|
Or even we could do it bright and shine, leaving the user what coder/decoder to use. Please check the following API, we might spread it to the other parts of Drab later: where every
So later we might easly add new coders etc. |
|
For those coders, we should follow the pattern from Jason or Poison: encode, decode, encode!, decode!, so all the functions which should encode/decode data might take not only our coders, but also Jason: Also, we might have a default encoder (configurable in config.exs), which normally should be encrypt + base64 (for safety). Then the user might just do: |
|
Also (sorry for a comment spam ;) we should have Drab.Coder.encode and Drab.Coder.decode, which delegates to the default coder. So in your controller, you could call them without specifying the one. |
Now the cookies can be extracted raw, or converted to a list of maps
lib/drab/browser.ex
Outdated
| exec_js!(socket, "document.cookie") | ||
| end | ||
|
|
||
|
|
There was a problem hiding this comment.
There should be no more than 1 consecutive blank lines.
lib/drab/browser.ex
Outdated
| |> extract_cookies_maps() | ||
| end | ||
|
|
||
|
|
There was a problem hiding this comment.
There should be no more than 1 consecutive blank lines.
| @spec extract_cookies_maps(String.t()) :: Keyword.t() | ||
| def extract_cookies_maps(cookies) do | ||
| Regex.scan(~r/(^|\s)(.*?)=(.*?)(;|$)/, cookies) | ||
| |> case do |
There was a problem hiding this comment.
Use a function call when a pipeline is only one function long
|
Ebert has finished reviewing this Pull Request and has found:
You can see more details about this review at https://ebertapp.io/github/grych/drab/pulls/136. |
| iex> Drab.Browser.raw_cookies(socket) | ||
| {:ok, result} | ||
| """ | ||
| def raw_cookies(socket) do |
There was a problem hiding this comment.
I don't think we need to expose raw_cookies. Let's keep API smallest possible.
| ] | ||
| """ | ||
| @spec extract_cookies_maps(String.t()) :: Keyword.t() | ||
| def extract_cookies_maps(cookies) do |
There was a problem hiding this comment.
Again, let's not do this function part of the API. User already has an excellent function to read cookies, why to confuse him with more functions?
Public API should have: cookie, cookies and set_cookie.
There was a problem hiding this comment.
Yes, less is more! :)
I'm generally in favor to default encoding something when the format could be critical in term of errors between the storing and retrieving of the value. But in this case I think it should be left to the developer decision, as cookies aren't an opaque storage, but they are directly visible also to the final user, so the encoding format matter, for example when the gdpr privacy and beyond problematics are taken in account. Furthermore, when we extract a cookie, we cannot know in advance if it was encoded and how (the programmer might want to retrive a cookie written by others), so we cannot apply any default decode criteria. |
|
A case where default encoding maybe could be useful is when complex values have to be transferred from an .html.drab template to the backend by mean the third value of a Drab event handler call. For example, instead to having to explicitly encoded it in the template <% encoded_value = Drab.Utils.encode_param %{question: "foo", answer: 42} %>
<button drab='click:check_answer("<%= encoded_value %>")'>Check answer</button>and then having to remember to decode it in the commander defhandler check_answer(socket, sender, value) do
decoded_value = Drab.Utils.decode_value(value)
question = decoded_value["question"]
answer = decoded_value["answer"]
endwe maybe could just have an helper that takes the name of the event, the name of the handler and the parameter with the complex value to pass on and some optional option, that implicitly encode the value, mark it as encoded someway and then in Drab.Core recognizing that flag in the parameter, and transparently decode the value without an action by the programmer, something like <button drab=<%=drab_handler(:click, :check_answer, %{question: "foo", answer: "42"}, encrypted: true) %> >Check answer</button>defhandler check_answer(socket, sender, value) do
question = value["question"]
answer = value["answer"]
endFurthermore, if we give transparently encoding by default, we probably should give also security by design, for example encrypting by default all the communications between browser and the backend. |
Absolutely! This is the right way to go. Elegant and consistent! In brief, my (yet mumbling) view is:
|
The similar for decoding.
Sorry, I don't get, could you please elaborate? |
|
I am going to create this Drab.Coder now, please hold on. |
it is not about cookies, it was referring to my previous comment:
where I proposed to have a sort of a new helper to call Drab from templates, in which the value of the parameter is encoded by default (currently instead, is passed as a plain string, so encoded is manually needed every time the parameter is i.e. a struct) |
|
Got it. With the new API you would be able to do: and Or of course you could choose the other encoder than default (which will be cipher+base64) I am now working on it, should finish soon :) |
|
Yes, but what I mean is a suggestion to write an helper and to modify somewhere the Drab.Core, to avoid the necessity to call by hand Drab.Coder.encode/x, as per the example in my previous message: # helper in the template
<button drab=<%=drab_handler(:click, :check_answer, %{question: "foo", answer: "42"}, encrypted: true) %> >Check answer</button># in the Commander handler, automatic decoder of the incoming value
defhandler check_answer(socket, sender, value) do
question = value["question"]
answer = value["answer"]
endIt does make sense for you? |
|
Hmm I believe it is overcomplicating. IMO in this case explicitness is better, also because in 99% case you don't need the encoding. Automatic decoder in handler would also be problematic, as we need to differentiate between encoded and non-encoded values. |
|
I have created coders, #137 |
I understand your point of view. Probably mine is influenced by the fact that I often need to pass more than just one value to a Drab call, and then I would like to pass transparently any kind of value as I were calling a standard function, but, as you said, probably this is not the need of most of the audience. |
No description provided.