Skip to content

Feature/cookie#136

Closed
guidotripaldi wants to merge 7 commits intogrych:masterfrom
guidotripaldi:feature/cookie
Closed

Feature/cookie#136
guidotripaldi wants to merge 7 commits intogrych:masterfrom
guidotripaldi:feature/cookie

Conversation

@guidotripaldi
Copy link
Copy Markdown
Contributor

No description provided.

* `decrypt` - `Boolean`, Decrypt the value, default `false`. When `true`, implies `decode` = `true`

Examples:
TODO
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO found

"""
def cookie!(socket, key, options \\ []) do
cookies(socket)
|> extract_cookie(key, options)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a function call when a pipeline is only one function long

cookies = "a=foo; message=Hello, World!; c=bar"
encoded_cookies = "a=foo; map=eyJtZXNzYWdlIjoiSGVsbG8sIFdvcmxkISJ9; c=bar"


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no more than 1 consecutive blank lines.

use ExUnit.Case, ascync: true
import Drab.Utils


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no more than 1 consecutive blank lines.


defp extract_cookie_value(cookie, key) do
Regex.run(~r/#{key}=(.*)/, cookie)
|> case do
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a function call when a pipeline is only one function long

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are spaces around operators most of the time, but not here.

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are spaces around operators most of the time, but not here.

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are spaces around operators most of the time, but not here.


test "retrieve a cookie" do
socket = drab_socket()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no trailing white-space at the end of a line.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no trailing white-space at the end of a line.

|> case do
[_, value] -> value
_ -> ""
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no trailing white-space at the end of a line.

Drab.Live.Crypto.decrypt(value)
end

end # Module No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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};'")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not intuitive, that it deletes all the other cookies. It should set the existing one, and leave all other untouched.

Copy link
Copy Markdown
Contributor Author

@guidotripaldi guidotripaldi May 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure to understand what do you mean. Why do you say that the other cookies are deleted?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide a spec for all public functions

{:ok, result}
"""
def cookies(socket) do
exec_js(socket, "document.cookie")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I'll rename the original as raw_cookies/1 and I'll write the new code under cookies/1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 """
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would hide this from the public API, as the only usage is with cookie?

Copy link
Copy Markdown
Contributor Author

@guidotripaldi guidotripaldi May 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean only extract_cookie

* `key` - `String`, The cookie name

### Options

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be documented.

@grych
Copy link
Copy Markdown
Owner

grych commented May 30, 2018

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 "tz=Europe%2FCopenhagen; _octo=GH1.1.1424226204.1506678902" = $1

@grych
Copy link
Copy Markdown
Owner

grych commented May 30, 2018

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:

set_cookie name, value, encode: Drab.Coder.Encrypt
get_cookie name, decode: Drab.Coder.URL

where every Drab.Coder must have encode and decode functions (it might implement Drab.Coder behaviour)

  • Drab.Coder.Encrypt - encrypt + base64 - takes any erlang term
  • Drab.Coder.Base64 - base 64 only - takes any erlang term
  • Drab.Coder.URL - urlencoder - takes only string

So later we might easly add new coders etc.

@grych
Copy link
Copy Markdown
Owner

grych commented May 30, 2018

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:

do_something_with_string string, encode: Drab.Coder.URL
do_something_with_string string, encode: 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:

set_cookie cookie, value, encode: true

@grych
Copy link
Copy Markdown
Owner

grych commented May 30, 2018

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
exec_js!(socket, "document.cookie")
end


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no more than 1 consecutive blank lines.

|> extract_cookies_maps()
end


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a function call when a pipeline is only one function long

@sourcelevel-bot
Copy link
Copy Markdown

Ebert has finished reviewing this Pull Request and has found:

  • 7 possible new issues (including those that may have been commented here).
  • 7 fixed issues! 🎉

You can see more details about this review at https://ebertapp.io/github/grych/drab/pulls/136.

Copy link
Copy Markdown
Owner

@grych grych left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shrink the API

iex> Drab.Browser.raw_cookies(socket)
{:ok, result}
"""
def raw_cookies(socket) do
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, less is more! :)

@guidotripaldi
Copy link
Copy Markdown
Contributor Author

guidotripaldi commented May 31, 2018

General comment: shouldn't we encode/decode cookies with URLEncode by default?

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.

@guidotripaldi
Copy link
Copy Markdown
Contributor Author

guidotripaldi commented May 31, 2018

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"]
end

we 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"]
end

Furthermore, 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.

@guidotripaldi
Copy link
Copy Markdown
Contributor Author

guidotripaldi commented May 31, 2018

Or even we could do it bright and shine, leaving the user what coder/decoder to use.
[...] where every Drab.Coder must have encode and decode functions (it might implement Drab.Coder behaviour)
[...] 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)

Absolutely! This is the right way to go. Elegant and consistent!
it is certainly good to manage the encode actions, but as stated before I don't know if it is good to encode by default the cookies.

In brief, my (yet mumbling) view is:

  • for cookies, default is plain cookies, programmer choose if/when/how encoded them;
  • for drab_handler calls, default is to transparently encode/decode the passed value; programmer can set a default in the config and/or specify the encode preferred method for every call

@grych
Copy link
Copy Markdown
Owner

grych commented May 31, 2018

for cookies, default is plain cookies, programmer choose if/when/how encoded them;

set_cookie cookie, value # no encoding
set_cookie cookie, value, encode: true # default &Drab.Coder.encode/1
set_cookie cookie, value, encode: Drab.Coder.URL # calling Drab.Coder.URL.encode
set_cookie cookie, value, encode: Any.Other.Module # calling Any.Other.Module.encode

The similar for decoding.

for drab_handler calls, default is to transparently encode/decode the passed value; programmer can set a default in the config and/or specify the encode preferred method for every call

Sorry, I don't get, could you please elaborate?

@grych
Copy link
Copy Markdown
Owner

grych commented May 31, 2018

I am going to create this Drab.Coder now, please hold on.

@guidotripaldi
Copy link
Copy Markdown
Contributor Author

guidotripaldi commented May 31, 2018

for drab_handler calls, default is to transparently encode/decode the passed value; programmer can set a default in the config and/or specify the encode preferred method for every call

Sorry, I don't get, could you please elaborate?

it is not about cookies, it was referring to my previous comment:

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. [...]

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)

@grych
Copy link
Copy Markdown
Owner

grych commented May 31, 2018

Got it.

With the new API you would be able to do:

<% encoded_value = Drab.Coder.encode %{question: "foo", answer: 42} %>
<button  drab='click:check_answer("<%= encoded_value %>")'>Check answer</button>

and

defhandler check_answer(socket, sender, value) do
  decoded_value = Drab.Coder.decode(value)
        
  question = decoded_value[:question]
  answer = decoded_value[:answer]
end

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 :)

@guidotripaldi
Copy link
Copy Markdown
Contributor Author

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"]
end

It does make sense for you?

@grych
Copy link
Copy Markdown
Owner

grych commented May 31, 2018

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.

@grych grych mentioned this pull request May 31, 2018
@grych
Copy link
Copy Markdown
Owner

grych commented May 31, 2018

I have created coders, #137

@guidotripaldi
Copy link
Copy Markdown
Contributor Author

Hmm I believe it is overcomplicating.
IMO in this case explicitness is better, also because in 99% case you don't need the encoding.

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.

@guidotripaldi guidotripaldi deleted the feature/cookie branch June 4, 2018 05:57
@guidotripaldi guidotripaldi restored the feature/cookie branch June 4, 2018 05:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants