Compare commits

..

978 Commits

Author SHA1 Message Date
Benjamin-McRae-Tracsis
b24028200c
Actually export the new options (#1825)
* actually export the new options

* bump version number, update changelog
2023-11-28 15:04:23 -07:00
Benjamin-McRae-Tracsis
22c5e46d5c
Add an options data structure to allow fine-tuned control of what instances are generated for a route (#1819)
* remove read from the list of derived instances, partially closing #1773, #1203

* bump version

* adjusting a version bound because the next version breaks compilation

* make a RouteOpts type that allows for finer control over what instances are derived for a Route

* some lintings

* adjust versioning and changelog

* actually a more major version bump

* verified that export list is complete

* add @ since
2023-10-23 08:39:21 -06:00
Michael Snoyman
2b29a73a50
Merge pull request #1821 from yitz-zoomin/test-bare-get-params
Add addBareGetParam to yesod-test
2023-09-22 07:29:39 +03:00
Yitz Gale
26de905117 Use haddock @since notation 2023-09-19 17:50:20 +03:00
Yitz Gale
32b609e93f Add PR link to ChangeLog.md 2023-09-19 13:13:06 +00:00
Yitz Gale
8534caa05a Add addBareGetParam to yesod-test 2023-09-19 13:10:12 +00:00
Michael Snoyman
9471c75c9c
Merge pull request #1820 from Vekhir/patch-2
Support Aeson 2.2
2023-09-14 07:54:51 +03:00
Vekhir
c7c0176292
Update ChangeLog.md for 1.6.11.2 2023-09-14 06:04:05 +02:00
Vekhir
9795042cc7
Support Aeson 2.2 2023-09-14 06:00:14 +02:00
Michael Snoyman
11b7089436
Merge pull request #1818 from Vekhir/patch-1
Add attoparsec-aeson to support aeson-2.2
2023-09-14 05:48:31 +03:00
Vekhir
86247aa865 Add attoparsec-aeson to stack.yaml 2023-09-13 15:25:42 +02:00
Vekhir
a742ae5c16 Add attoparsec-aeson to support aeson-2.2
The module `Data.Aeson.Parser` is moved into attoparsec-aeson for aeson >=2.2.
For aeson <2.2, attoparsec-aeson is an empty package, since the module exists within aeson.
2023-09-11 15:31:53 +02:00
Michael Snoyman
0d10965e0f
Merge pull request #1817 from ciukstar/datetime-local-field
Add datetimeLocalField
2023-09-05 08:51:42 +03:00
ciukstar
3206cf4c73 Update the Changelog.md file with a link to PR 2023-08-31 03:55:54 +03:00
ciukstar
773c815b90 Add datetimeLocalField 2023-08-31 03:46:20 +03:00
Michael Snoyman
7a10dd3628
Merge pull request #1812 from jezen/master
Fix SubSub compilation for GHC >= 9.0.1
2023-07-26 06:39:48 -04:00
Jezen Thomas
4a3df62979
Fix SubSub compilation for GHC >= 9.0.1
Resolves #1811.

Related:

- https://stackoverflow.com/questions/73719275/evaluation-of-template-haskell-in-yesod?noredirect=1&lq=1

- https://github.com/yesodweb/yesodweb.com-content/pull/269
2023-07-26 12:21:31 +03:00
Michael Snoyman
b3416ec0a4
Merge pull request #1805 from AriFordsham/ari/subsites
Fix subsite-to-subsite dispatch
2023-07-13 06:23:41 +03:00
Ari Fordsham
48ee9f2134 Merge branch 'ari/subsubtest' into ari/subsites 2023-07-09 16:16:12 +03:00
Ari Fordsham
9ce822b8f7 SubSubTest 2023-07-09 16:05:01 +03:00
Michael Snoyman
393954d802
Merge pull request #1806 from yesodweb/no-newstack
Drop newstack
2023-07-02 08:56:44 +03:00
Michael Snoyman
f3f2ae112f Drop newstack 2023-07-02 08:35:55 +03:00
Ari Fordsham
038452fc17 Empty commit to trigger CI 2023-06-25 18:20:42 +03:00
Ari Fordsham
8be44a8cf4 Add changelog 2023-06-25 18:10:53 +03:00
Ari Fordsham
b0634b0d45 Works with subsite-with-static 2023-06-25 18:05:14 +03:00
Ari Fordsham
97b07380e5 Make changes 2023-06-25 16:30:17 +03:00
Sergiu Starciuc
197ecb409f
Add Romanian translation for yesod-form (#1801) 2023-05-10 11:14:51 +02:00
Michael Snoyman
ccfd77192e
Merge pull request #1797 from mixphix/no-star-is-type
No star is type
2023-03-01 08:22:13 +02:00
Melanie Phoenix
ee343e616e changelogs 2023-02-28 11:23:25 -05:00
Melanie Phoenix
ef58df42c6 bump versions 2023-02-28 11:18:52 -05:00
Melanie Phoenix
f6ea77118a no StarIsType 2023-02-28 11:07:01 -05:00
Michael Snoyman
c4e796248c
Merge pull request #1796 from TeofilC/yesod-core-transformers-0.6
Adapt to removal of ListT from transformers-0.6.0
2023-02-10 14:57:45 +02:00
Teo Camarasu
c35bdb1cd4 Adapt to removal of ListT from transformers-0.6.0
Co-authored-by: Ryan Scott <ryan.gl.scott@gmail.com>
2023-02-10 12:15:06 +00:00
Michael Snoyman
0fa3dbcab6
Merge pull request #1795 from TeofilC/monad-aform
yesod-form: Add Monad AForm instance for transformers >=0.6
2023-02-09 08:23:56 +02:00
Teo Camarasu
a6e420b42f yesod-form: bump version and add changelog message for #1795 2023-02-07 10:52:29 +00:00
Teo Camarasu
06fd5df137 yesod-form: Add Monad AForm instance for transformers >=0.6
This is required in order to have a MonadTrans instance
2023-02-07 10:51:51 +00:00
Michael Snoyman
66bed05d33
Merge pull request #1790 from ricky0123/fix-websocket-chat
Fix websocket examples
2022-11-20 09:36:14 +02:00
ricky
d8560042e7 fix websocket chat examples
.
2022-11-17 08:29:58 -06:00
Michael Snoyman
5880bd3119
Merge pull request #1785 from felixonmars/patch-1
Allow vector 0.13
2022-10-11 06:42:40 +03:00
Felix Yan
73db75b8cf
Allow vector 0.13
Builds fine and all tests pass.
2022-10-10 23:53:34 +03:00
Michael Snoyman
e3381d590f
Merge pull request #1783 from cblp/fix-radio
Fix according to Bootstrap 3 docs
2022-10-06 06:36:25 +03:00
Yuriy Syrovetskiy
cb874e3bbb fixup! Update Changelog 2022-10-05 22:53:21 +02:00
Yuriy Syrovetskiy
fbefa3ad37 Update Changelog 2022-10-05 20:32:49 +02:00
Yuriy Syrovetskiy
b841e8cf0b Fix according to Bootstrap 3 docs 2022-10-05 20:21:03 +02:00
Michael Snoyman
5ac0138697
Minor version bump on yesod-test 2022-09-22 09:48:42 +03:00
Michael Snoyman
f729d9bbb6
Merge pull request #1781 from eahlberg/add-by-selector-label-contain
Add bySelectorLabelContain to support testing inputs with the same label
2022-09-22 09:43:20 +03:00
Michael Snoyman
faa4105250
Merge branch 'add-by-selector-label-contain' of github.com:eahlberg/yesod into eahlberg-add-by-selector-label-contain 2022-09-22 06:49:33 +03:00
Michael Snoyman
486b871229
Merge pull request #1782 from yesodweb/support-stack-29
Remove unneeded invocation of Stack in CI
2022-09-22 06:48:14 +03:00
Michael Snoyman
bb74ef5f08
Remove unneeded invocation of Stack in CI 2022-09-22 06:39:22 +03:00
Eric Ahlberg
bca75573b8 Update changelog 2022-09-21 13:50:50 +02:00
Eric Ahlberg
6c2a20699a Refactor 2022-09-21 13:50:50 +02:00
Eric Ahlberg
bd86b4db7a Add bySelectorLabelContain 2022-09-21 13:50:50 +02:00
Eric Ahlberg
b28ee833d1 Add tests 2022-09-21 10:34:02 +02:00
Michael Snoyman
42050fb5c7
Remove bounds on fsnotify 2022-09-18 11:08:53 +03:00
Michael Snoyman
65adf9ba72
Merge pull request #1775 from SupercedeTech/add-with-option-more-flexible-radio-inputs
Add withRadioField a more flexible radio option renderer
2022-09-18 08:05:47 +03:00
Matt Parsons
26a195b8c7
Support GHC 9.4 (#1769)
* Support GHC 9.4

* tidy it on up

* ok tests pass again

* weird

* woo

* Changelog, cabal files

* fix for older cabal

* Drop MacOS from older resolvers

https://github.com/bravit/hid-examples/issues/7#issuecomment-781308838

* oops
2022-09-07 11:49:14 -06:00
Michael Snoyman
02a1a56dd7
Merge pull request #1778 from degustaf/master
Remove deprecated function from testing example
2022-08-26 05:59:57 +03:00
Derek Gustafson
7721b65f58
Remove deprecated function from testing example
byLabel has been deprecated, but is still used as an example.
2022-08-25 17:07:07 -04:00
Jappie Klooster
25f83fb73d Add withRadioField a more flexible radio option renderer
This re-expresses radioField into the new more flexible
function.
Which gives an adhoc example on how to use it as well.

This function passes the radio input to a callback function
to let said function decide how it should be rendered.
These changes allow you to make a radio table for example,
for selecting some row.

bump version number, add @since

add note on radioField

Update changelog
2022-08-17 10:28:42 +02:00
Michael Snoyman
337a9928f2
Merge pull request #1772 from SupercedeTech/make-exception-catching-configurable
Make catching exceptions configurable.
2022-07-20 18:04:39 +03:00
Jappie Klooster
69df01668a
Update yesod-core/src/Yesod/Core/Class/Yesod.hs
Co-authored-by: patrick brisbin <pbrisbin@gmail.com>
2022-07-20 15:23:29 +02:00
Jappie Klooster
dd2ba40873 be more explicit in changelog 2022-07-20 14:30:34 +02:00
Jappie Klooster
13db3db118 Add backwards compatibility for old unliftio 2022-07-20 14:14:14 +02:00
Jappie Klooster
dc4ee0f92c remove unsafeAsyncCatch 2022-07-20 14:07:30 +02:00
Jappie Klooster
01ccea46cc update docs, better names
rename catchBehvaior -> catchHandlerExceptions
rename shouldCatch -> catchHanlderExceptions
2022-07-20 12:43:09 +02:00
Jappie Klooster
5ac65db1bf Delete catchbevior and allow a user to provide a catch.
By default the one from unliftIO is used.
2022-07-20 12:32:48 +02:00
Jappie Klooster
d04c22e3d6 Rewrite default behavior into rethrow async exceptions 2022-07-20 11:55:44 +02:00
Jappie Klooster
964fa0db55 Fix dealing with timeout and add appropriate test
add comments for this nonobvious test
2022-07-14 21:55:00 +02:00
Jappie Klooster
27042c93ce change catchbehavior to get app be in io, make it abstract type 2022-07-07 12:06:56 +02:00
Jappie Klooster
710adc7329 don't patch but minor version bump isntead 2022-07-07 11:15:40 +02:00
Jappie Klooster
9648ccf79f add customapp to core.cabal 2022-07-06 22:43:19 +02:00
Jappie Klooster
827d9269b0 update changelog 2022-07-06 22:41:52 +02:00
Jappie Klooster
1487b121be Make catching exceptions configurable.
Fixes https://github.com/yesodweb/yesod/issues/1771

This is done by adding a function to Yesod
typeclass which can match on any exception
and tell the framework if it should rethrow
or not.

I used an overridable function because it seemed
more flexible then a whitelist.
A user can now for example choose to throw
everything, or catch everything as easily.

add docs

bump
2022-07-06 22:40:24 +02:00
Michael Snoyman
99c1fd49a3
Merge branch 'patch-1' of https://github.com/friedbrice/yesod 2022-05-11 14:44:27 +03:00
Michael Snoyman
50c439da56
Merge pull request #1768 from SupercedeTech/quote-in-test
Fix quote ' not matching in htmlContain* functions
2022-05-11 14:05:57 +03:00
Daniel P. Brice
b8de71c5ab
Update ChangeLog.md 2022-05-10 13:31:34 -07:00
Jappie Klooster
b88b1f430f Add link to PR 2022-05-10 16:27:28 -04:00
Daniel P. Brice
d5a194a7dd
Update yesod-test.cabal 2022-05-10 13:25:45 -07:00
Daniel P. Brice
8028f1defd
assertEq delegates to HUnit.assertEqual
HUnit.assertEqual gives a formatted diff, making it easier to see the differences between the two values at a glance.
2022-05-10 13:24:21 -07:00
Jappie Klooster
5f3e237c29 Bump version and add changes 2022-05-10 16:24:07 -04:00
Jappie Klooster
28fc2269b0 Fix quote ' not matching in any body
This sometimes occured in our code base when generating
names with the fakedata package, someone named o'conner
randomly fails a particular test.

Also add tests for the other matching function and fixed them.

Furthermore, I snuck in logging of the matches as well.
2022-05-10 16:20:35 -04:00
Michael Snoyman
0a273d5aae
Merge pull request #1766 from ivanbakel/fix-meta-typo
Fix typo in how description meta tags are laid out
2022-04-25 05:15:55 +03:00
Isaac van Bakel
032b906a73 Bump version to 1.6.23.1, update ChangeLog 2022-04-21 14:35:22 +01:00
Isaac van Bakel
1295f1c643 Fix typo in how description meta tags are laid out 2022-04-21 14:32:49 +01:00
Michael Snoyman
f338e519f2
Merge pull request #1765 from ivanbakel/idempotent-description
Add API for idempotent page description editing
2022-04-21 05:35:40 +03:00
Isaac van Bakel
04683ca58b Bump yesod-core version, update ChangeLog 2022-04-20 13:01:26 +01:00
Isaac van Bakel
b9fbdb3950 Add idempotent versions of setDescription API
`setDescription` and `setDescriptionI` present a similar API to
`setTitle` and `setTitleI`, but unlike those functions the description
functions are not idempotent - so calling them multiple times inserts
multiple `<meta/>` tags in HTML `<head/>`.

This adds explicitly idempotent versions of those functions which are
handled in a similar way to the title, so that calling them multiple
times has the effect of taking the final value specified.

Because the non-idempotent behaviour of setDescription is not obvious,
this also adds warnings for that behaviour to make it clear what the
effect of multiple calls will be. Unfortunately, setDescriptionIdemp
can't be made a drop-in replacement because developers may have defined
their own layouts which need to take pageDescription into account.
2022-04-20 12:54:23 +01:00
Isaac van Bakel
9c0b00190a Add test of setDescription idempotency
Like setTitle, this function should really be idempotent so developers
don't add multiple conflicting meta descriptions to the page. Unlike
setTitle, the function currently fails its idempotency test.
2022-04-20 12:01:34 +01:00
Michael Snoyman
4f962c9073
Merge pull request #1764 from parsonsmatt/matt/yesod-test-expose-sio
Expose SIO type
2022-04-18 09:09:09 +03:00
parsonsmatt
ef4178f4c8 Add runSIO, changelog, version bump 2022-04-14 08:50:41 -06:00
Michael Snoyman
b0e461c669
Merge pull request #1760 from parsonsmatt/matt/support-persistent-2.14
Support persistent-2.14
2022-04-14 05:53:19 +03:00
parsonsmatt
60d0748834 Expose SIO type 2022-04-13 16:27:01 -06:00
parsonsmatt
7bec27aa3c changelog link 2022-04-13 10:14:55 -06:00
parsonsmatt
d54c17ef27 changelog, version 2022-04-13 10:14:17 -06:00
parsonsmatt
5f71a49c0f Support persistent-2.14 2022-04-13 10:10:35 -06:00
Michael Snoyman
d831b9f108
Merge pull request #1756 from SupercedeTech/remove-sometimes-failing-test
Remove sometimes failing superfluous  test
2022-03-25 16:57:37 +03:00
Jappie Klooster
d54dbf5fd6 bump version number 2022-03-25 07:52:32 -04:00
Jappie Klooster
4daf1d2107 update changelog 2022-03-25 07:51:57 -04:00
Jappie Klooster
73f20b6285 Remove sometimes failing test
This test sometimes fails on nix builds.
I'm not sure why, but it should be superflous with
"thread killed = 500" test anyway.

They test both for async exceptions.
Just a different one.
2022-03-25 07:30:33 -04:00
Michael Snoyman
3d65a3bf16
Remove NumericUnderscores for older GHCs 2022-03-24 10:29:28 +02:00
Michael Snoyman
60111462de
Merge branch 'ghc-9.2-compat' of https://github.com/TeofilC/yesod 2022-03-24 05:25:39 +02:00
Michael Snoyman
53936c43a3
Merge branch 'fix-catch-async-exception-on-requst-threads' of https://github.com/SupercedeTech/yesod 2022-03-24 05:24:18 +02:00
Michael Snoyman
c74fc994ae
Merge pull request #1752 from lykahb/lykahb/content-void
Create instances for ToContent Void, ToTypedContent Void
2022-03-24 04:41:55 +02:00
Teo Camarasu
c6fab6f410 update yesod-bin changelog 2022-03-23 08:43:20 +00:00
Teo Camarasu
b117e5a4cd update yesod-core changelog 2022-03-23 08:43:20 +00:00
Teo Camarasu
87427c1290 bump yesod-bin 2022-03-23 08:43:20 +00:00
Teo Camarasu
3c2b50e08c bump yesod-core 2022-03-23 08:43:20 +00:00
Teo Camarasu
24d3ea9e53 Fix building yesod-bin with Cabal-3.6 2022-03-23 08:43:20 +00:00
Ryan Scott
9039df924d Allow building with template-haskell-2.18.0 2022-03-22 20:29:50 +00:00
Jappie Klooster
764fd94bc6 add changelog entry 2022-03-22 15:51:10 -04:00
Jappie Klooster
f48485e181 Bump version number 2022-03-22 15:46:47 -04:00
Jappie Klooster
5b96d94915 Fix it for async exceptions in the sessions as well 2022-03-22 15:45:20 -04:00
Jappie Klooster
e284a68a9f Remove the use of masks
I don't think these are neccisary.
If an exception get's delivered at these points,
we couldn't do anything about it anyway
2022-03-22 15:18:38 -04:00
Jappie Klooster
4c1719cb6e Disable the allocation limit within the test instead
I don't think we should add that to the function
seems odly specific
2022-03-22 15:15:49 -04:00
Jappie Klooster
eb7405765d Add async exception handling for basic runner. 2022-03-22 14:47:27 -04:00
Jappie Klooster
42abd9b666 add explicit exports 2022-03-22 14:20:46 -04:00
Jappie Klooster
08d37a1857 Add test showing the failures 2022-03-22 14:02:25 -04:00
Boris Lykah
7d44c38c91 Update changelog 2022-03-22 11:46:09 -06:00
Boris Lykah
8fb0cbb31a Bump version for yesod-core 2022-03-22 11:44:16 -06:00
Boris Lykah
d3808c3a97 Create instances for ToContent Void, ToTypedContent Void 2022-03-21 12:17:24 -06:00
Sergiu Starciuc
48d05fd6ab
Color field (#1748)
This PR adds a new colorField function to create an html color field (<input type="color">) as described at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/color
2022-03-02 17:49:09 +01:00
Michael Snoyman
5bd872be02
Merge pull request #1746 from yesodweb/enable-new-nightly
Enable new nightly
2022-02-11 07:11:30 +02:00
Michael Snoyman
b4b32cb341
Change yesod-auth version 2022-02-11 06:28:29 +02:00
Michael Snoyman
7af2cd04b6
Allow newer GHC 2022-02-11 06:01:35 +02:00
Michael Snoyman
6e7e7299ba
Update yesod for aeson 2 2022-02-11 06:00:02 +02:00
Michael Snoyman
3583fe2a03
Update yesod-bin for aeson 2 2022-02-11 05:55:17 +02:00
Michael Snoyman
385d17dd94
Support aeson 2 2022-02-11 05:28:01 +02:00
Michael Snoyman
2c498c14b2
Relax an upper bound 2022-02-11 05:10:14 +02:00
Michael Snoyman
863cdfa458
Enable a new nightly 2022-02-11 05:10:05 +02:00
Michael Snoyman
b147b272e2
Merge pull request #1745 from stevehartdata/docs-fix
Add missing documentation to 'warp'
2022-01-20 09:41:16 +02:00
Steve Hart
ee41ae000e Update changelog 2022-01-19 10:26:41 -05:00
Steve Hart
6b164c6007 Add missing documentation to 'warp' 2022-01-19 10:18:12 -05:00
Sibi Prabakaran
b54210cef2
Merge pull request #1742 from smichel17/patch-1
Fix yesod-auth README link & add yesod-auth-oauth2
2021-12-07 11:51:37 +05:30
smichel17
072659b770
Fix yesod-auth README link & add yesod-auth-oauth2
Fixes #1738
2021-12-06 18:13:09 +00:00
Michael Snoyman
f30f96ee41
Merge pull request #1741 from schoettl/yesod-auth-fixes
Fix German translations of AuthMessage
2021-11-16 10:31:59 +02:00
Jakob Schöttl
3f0bf09712 Fix German translations of AuthMessage 2021-11-15 10:25:30 +01:00
Michael Snoyman
e5f9376700
Merge pull request #1740 from stevemao/cleanup
update the examples to show how to cleanup resources once user discon…
2021-10-03 06:26:52 +03:00
Steve Mao
e6d2769408
update the examples to show how to cleanup resources once user disconnects 2021-10-02 17:22:58 +10:00
Michael Snoyman
9a59f0648c
Merge pull request #1739 from stevemao/multiple
add multiple channels example
2021-09-30 05:27:30 +03:00
Steve Mao
4ae578a1a1
add multiple channels example 2021-09-29 23:12:16 +10:00
Michael Snoyman
dfc270b0b2
Merge pull request #1737 from geraldus/ghc-9.0.1
Make yesod-auth buildable with GHC 9.0.1
2021-09-10 14:56:36 +03:00
Arthur Sakhievich Fayzrakhmanov
1a6ba6d099 Update Changelog 2021-09-10 13:35:15 +05:00
Arthur Sakhievich Fayzrakhmanov
67f846d324 Version bump 2021-09-10 11:37:13 +05:00
Arthur Sakhievich Fayzrakhmanov
814584d7d9 Apply stylish-haskell 2021-09-10 11:30:05 +05:00
Arthur Sakhievich Fayzrakhmanov
8f83462134 Fix GHC 9.0.1 build 2021-09-10 11:29:24 +05:00
Michael Snoyman
58311a3d93
Simplify matrix, disable nightly 2021-07-22 18:06:37 +03:00
Michael Snoyman
0d0fa77009
Merge pull request #1734 from googleson78/default-gen
Export defaultGen
2021-07-22 16:38:21 +03:00
Georgi Lyubenov
1f52a39aa2 Export defaultGen 2021-07-22 14:54:58 +03:00
Michael Snoyman
f3dd8cf204
Merge pull request #1731 from felixonmars/patch-1
Correct a typo in yesod-form's description
2021-07-08 12:56:04 +03:00
Felix Yan
e972a63a35
Correct a typo in yesod-form's description 2021-07-08 17:51:58 +08:00
Michael Snoyman
bffa6de813
Merge pull request #1730 from felixonmars/ghc9
Fix compatibility with template-haskell 2.17 for yesod
2021-06-30 18:45:03 +03:00
Felix Yan
44b1ea252c
Bump version and update Changelog 2021-06-30 18:11:33 +08:00
Felix Yan
189487914d
Fix compatibility with template-haskell 2.17 for yesod 2021-06-30 18:07:24 +08:00
Michael Snoyman
9edbc05827
Version bump for #1729 2021-06-27 12:19:01 +03:00
Felix Yan
a1e18c5b68
Fix compatibility with template-haskell 2.17 2021-06-25 10:54:55 +08:00
Michael Snoyman
81236a2832
Merge pull request #1728 from NorfairKing/breadcrumb-loop-detector
yesod-core: detect loops in breadcrumbs
2021-05-21 18:09:59 +03:00
Tom Sydney Kerckhove
2d0dab20a6 minor version bump and changelog entry 2021-05-21 17:09:10 +02:00
Tom Sydney Kerckhove
0db056534c breadcrumbs: guard refactor 2021-05-21 08:41:42 +02:00
Tom Sydney Kerckhove
884d937792 use ++ instead of <> to fix the build 2021-05-20 16:00:55 +02:00
Tom Sydney Kerckhove
59ef730317 yesod-core: refactor the loop detector to not use Just wrapping 2021-05-20 14:28:09 +02:00
Tom Sydney Kerckhove
96a940b60c yesod-core: test for looping breadcrumbs 2021-05-20 14:25:17 +02:00
Tom Sydney Kerckhove
d981c87c39 yesod-core: detect loops in breadcrumbs 2021-05-17 20:40:09 +02:00
Michael Snoyman
8a799d2768
Merge pull request #1726 from Smart-Hypercube-fork/master
Use secure entropy source to generate CSRF tokens
2021-05-11 11:05:37 +03:00
Hypercube
1cb0fc579c Change version number 2021-05-11 14:03:51 +08:00
Hypercube
5deabe53e8 Update changelog 2021-05-11 11:35:59 +08:00
Hypercube
b6215582d8 Use secure entropy source to generate CSRF tokens 2021-05-11 11:32:07 +08:00
Michael Snoyman
5d8566ad5c
Merge pull request #1724 from yesodweb/parsonsmatt-matt/support-persistent-2.13
Parsonsmatt matt/support persistent 2.13
2021-05-07 14:18:13 +03:00
Michael Snoyman
3ea97d21b8
Fix extra-deps 2021-05-07 13:08:39 +03:00
parsonsmatt
b3188d962e add to test 2021-05-06 07:35:37 -06:00
parsonsmatt
3d3fe3f5b6 fix version in changelog 2021-05-05 16:31:30 -06:00
parsonsmatt
d42354ae98 use hackage release 2021-05-05 15:17:46 -06:00
parsonsmatt
69735fc9c6 Add link to changelog, version bump 2021-05-05 12:33:24 -06:00
parsonsmatt
3224e8e6f1 Support persistent-2.13 2021-05-05 12:16:05 -06:00
Michael Snoyman
2f8036c61f
Version bump for bounds 2021-04-15 09:29:10 +03:00
Michael Snoyman
e064306ef3
Version bumps for bounds 2021-04-15 09:18:55 +03:00
Michael Snoyman
cf3d9db87d
Merge pull request #1722 from schoettl/selectFieldGrouped
Forms: selectFieldGrouped
2021-04-15 08:58:21 +03:00
Jakob Schöttl
73a85310c6 Relax version constraints for yesod-form 2021-04-14 13:46:53 +02:00
Jakob Schöttl
08b5150ac0 Fix typo 2021-04-14 13:46:03 +02:00
Jakob Schöttl
7ffff25326 Add some type annotations 2021-04-14 09:40:34 +02:00
Jakob Schöttl
e3a95bd92c Simplify code, fix linter warnings 2021-04-14 09:40:21 +02:00
Jakob Schöttl
848da5ff12 Bump version and fix old version comments 2021-04-14 09:39:13 +02:00
Jakob Schöttl
c6f44d47b9 Also export this helper 2021-04-13 22:22:26 +02:00
Jakob Schöttl
2998849e99 Fix comments 2021-04-13 22:16:29 +02:00
Jakob Schöttl
829b5af62c Fix implementation of instance Functor OptionList 2021-04-13 21:58:40 +02:00
Jakob Schöttl
993de7fa86 Add selectFieldGrouped 2021-04-13 09:57:50 +02:00
Jakob Schöttl
daf977fdb1 Use standard function forM_ 2021-04-10 11:42:04 +02:00
Michael Snoyman
21bfad3570
Merge pull request #1721 from yesodweb/pb/reorder-languages
Stop moving session language ahead of reqLangs
2021-04-09 06:04:58 +03:00
patrick brisbin
0c2a4ebc81
Bump minor, not patch 2021-04-08 10:07:18 -04:00
patrick brisbin
7875930c43
Version bump 2021-04-08 09:53:58 -04:00
patrick brisbin
dc2d5d9cd0
Stop moving session language ahead of reqLangs
Yesod.Core.Handler.languages checks first for a language set in the
user's session, prepending that value to YesodRequest{reqLangs}, so it
is respected above all else if present.

For context, reqLangs itself also includes the session, but just later
in line:

    langs' = catMaybes [ lookup langKey gets -- Query _LANG
                       , lookup langKey cookies     -- Cookie _LANG
                       , lookupText langKey session -- Session _LANG
                       ] ++ langs                    -- Accept-Language(s)

In #1720, it was raised that allowing the session (something implicitly
present for any request) to override a query parameter (something
explicitly given on that request) is surprising.

We decided (without knowing what order reqLangs was doing) that query,
cookie, session, accept was best and languages should be changed to do
that. Conveniently, this just makes languages equivalent to reqLangs, so
that is what this patch does.
2021-04-08 09:34:38 -04:00
Michael Snoyman
c59993ff28
Change cabal-version syntax 2021-04-03 22:37:00 +03:00
Michael Snoyman
b97d8d60b3
Fix changelog 2021-03-30 22:13:53 +03:00
Michael Snoyman
42eea68fb6
Support persistent 2.12 2021-03-30 21:48:10 +03:00
Michael Snoyman
f2657e7ee0
Merge pull request #1717 from Burtannia/devel-ssl
Devel SSL
2021-02-10 17:46:18 +02:00
James Burton
a068bbdb8c Simplified implementation of cert/key parser option 2021-02-10 13:54:22 +00:00
James Burton
4699479bbb Removed unused imports 2021-02-09 17:52:55 +00:00
James Burton
8d0866f08b Updated changelog 2021-02-09 17:41:49 +00:00
James Burton
818e8e3781 Parser now requires that both cert/key be provided or neither 2021-02-09 17:38:50 +00:00
James Burton
8a4fb790cf Revert "Fixed indentation"
This reverts commit 52cf633993.
2021-02-09 12:07:56 +00:00
James Burton
52cf633993 Fixed indentation 2021-02-08 22:42:26 +00:00
James Burton
045d05f7d6 Bumped version 2021-02-08 18:19:40 +00:00
James Burton
9f72790df9 Added options to pass SSL certificate and key to yesod devel 2021-02-08 17:59:49 +00:00
James Burton
1c471acfd5
Fixed bug when duplicating option tags (#1716) 2021-01-08 19:03:46 +00:00
Michael Snoyman
60350c6532
Merge pull request #1715 from yesodweb/multi-form-cabal-version
Bumped cabal version to >= 1.10
2020-12-22 05:05:25 +02:00
Burtannia
bb008df3bd Bumped cabal version to >= 1.10 2020-12-22 02:17:57 +00:00
Michael Snoyman
19bd528ac7
Version bump 2020-12-16 05:45:46 +02:00
Michael Snoyman
9cb8d2d369
Merge pull request #1705 from masaeedu/addmonadstate
Add MonadState instance for SIO
2020-12-16 05:32:31 +02:00
Asad Saeeduddin
63afa32fa0
Add MPTC extension 2020-12-15 21:34:16 -05:00
Asad Saeeduddin
7695803af5
Fix ambiguous get/put issue 2020-12-15 20:48:20 -05:00
Asad Saeeduddin
210c992601
Add MTL dependency 2020-12-15 19:59:28 -05:00
Asad Saeeduddin
a1e708107b
Add MonadState instance for SIO 2020-12-15 19:59:28 -05:00
Michael Snoyman
3015133b0e
Merge pull request #1713 from eahlberg/fix-cookie-test-example
Fix import in cookie example
2020-12-14 16:01:49 +02:00
Eric Ahlberg
383149c0af Update changelog 2020-12-14 14:30:08 +01:00
Eric Ahlberg
44895915ea Fix import in cookie example 2020-12-14 10:39:50 +01:00
Michael Snoyman
f52291d2c9
Forgot to update cabal file 2020-12-14 11:30:14 +02:00
Michael Snoyman
e4cd44a4c7
Fix test suite for latest wai-extra (fixes #1711) 2020-12-14 11:29:11 +02:00
Michael Snoyman
c6c2cd2252
Merge pull request #1710 from yesodweb/pb/has-callstack
Fix up missing HasCallStack
2020-12-02 08:37:52 +02:00
patrick brisbin
761dbc7753
Update yesod-test ChangeLog 2020-12-01 12:00:45 -05:00
patrick brisbin
cb06004044
yesod-test version bump 2020-12-01 11:57:46 -05:00
patrick brisbin
07d76095a7
Add missing HasCallStack
As far as I could tell, all of these functions call failure, or call
things that call failure.
2020-12-01 11:56:37 -05:00
patrick brisbin
24acd4e3b7
Add missing HasCallStack
Even though functions that use this one all have HasCallStack, the fact
that this function itself doesn't means that all errors are reported as
from this line anyway:

    Failures:

      ./Yesod/Test.hs:1571:28:
      1) ...

This should correct that.
2020-12-01 11:49:29 -05:00
Michael Snoyman
95dc598d4b
Merge pull request #1707 from yesodweb/multi-form-delete-button
Multi form delete button
2020-11-20 08:31:48 +02:00
Burtannia
c60430e69e Wrapper error class is now removed on copy 2020-11-20 02:51:34 +00:00
Burtannia
f2d3f3d8da Replaced JS string concatenation with rawJS 2020-11-20 02:47:19 +00:00
Michael Snoyman
3b306b39ba
Merge pull request #1709 from eahlberg/fix-open-graph-functions
Fix functions generating Open Graph metadata
2020-11-19 06:43:31 +02:00
Eric Ahlberg
fd049ec3b0 Update changelog 2020-11-18 19:37:39 +01:00
Eric Ahlberg
13039e567f Bump version 2020-11-18 19:28:36 +01:00
Eric Ahlberg
62479374cf Use property attribute instead of name 2020-11-18 19:22:17 +01:00
Burtannia
91c1a7fac7 Fixed name clashes when using more than one instance of a multi-field 2020-11-18 07:14:40 +00:00
Burtannia
2eec150289 Moved JS constant inside function 2020-11-18 05:16:12 +00:00
Burtannia
0f51f91334 Removed dependency on Semigroup 2020-11-17 22:12:59 +00:00
Burtannia
5c56320c39 Added PR to changelog 2020-11-17 21:20:43 +00:00
Burtannia
da3723d2c7 Bumped version number to 1.7.0 2020-11-17 21:17:34 +00:00
Burtannia
ee5b2e129d Updated changelog and readme 2020-11-17 21:02:14 +00:00
Burtannia
e619b8d6ff Updated since annotations in docs 2020-11-17 21:00:42 +00:00
James Burton
fcda22ec5c Clearing fields now triggers the change event 2020-11-16 00:54:33 +00:00
James Burton
1c742a83d3 Fixed tooltip styling in Bootstrap 4 2020-11-15 23:00:24 +00:00
James Burton
973461e70f Minor refactoring 2020-11-15 22:19:37 +00:00
James Burton
008b4af741 Reworked the field duplication code to be more robust and allow for fields with multiple elements like radio fields 2020-11-15 21:12:23 +00:00
James Burton
e209810b8c Removed strictness from MultiSettings fields that are only used in applicative forms 2020-11-15 17:45:47 +00:00
James Burton
0d0112b73b Updated documentation 2020-11-15 17:44:56 +00:00
Burtannia
7b327b3dcd Made MultiSettings fields strict 2020-11-14 00:51:34 +00:00
Burtannia
44f065c615 Updated docs 2020-11-14 00:37:18 +00:00
Burtannia
df0c61e364 Tooltip now shows in applicative multi-fields 2020-11-13 23:03:14 +00:00
Burtannia
2c1112c52c Fixed bootstrap styling 2020-11-13 22:48:56 +00:00
Burtannia
a3319f766a Error messages are now deleted with fields and are highlighted correctly 2020-11-13 22:25:44 +00:00
Burtannia
39ed1f6453 Added support for customising button contents 2020-11-13 20:18:50 +00:00
Burtannia
e18d0a771b Added delete button to Yesod.Form.Multi 2020-11-13 18:46:42 +00:00
Michael Snoyman
cdd6e28d5f
New cabal-version for Hackage 2020-11-08 12:52:23 +02:00
Michael Snoyman
3cfe814cba
cabal-version bump for Hackage 2020-11-08 12:49:42 +02:00
Michael Snoyman
0325a24826
Merge pull request #1703 from jeffhappily/update-docs-for-handlert
Replace HandlerT with HandlerFor in the documentation
2020-11-08 12:49:26 +02:00
Jeff Happily
29bb2053fd
Bump version and update changelog 2020-11-08 18:32:33 +08:00
Jeff Happily
de375e26de
Replace HandlerT with HandlerFor in the documentation 2020-11-08 10:55:01 +08:00
Michael Snoyman
6a40abf033
More Hackage appeasement 2020-11-06 09:15:40 +02:00
Michael Snoyman
3c65d49376
Hackage wants newer cabal-version 2020-11-06 09:07:06 +02:00
Michael Snoyman
e02f1dc780
Merge pull request #1702 from yesodweb/actions
Move to Github Actions
2020-11-06 09:11:15 +02:00
Michael Snoyman
2a280a0a4e
Move to actions, test persistent 2.11 2020-11-06 07:15:17 +02:00
Michael Snoyman
504b3c74cf
Merge branch 'master' of https://github.com/friedbrice/yesod 2020-11-06 06:24:17 +02:00
Daniel Brice
3a44d47acf link to PR in changelog 2020-11-05 18:06:52 -08:00
Michael Snoyman
a770fd2b63
Appease Hackage 2020-11-05 10:50:38 +02:00
Michael Snoyman
710d40d253
Merge pull request #1700 from Disco-Dave/yesod-websockets/remove-use-of-forkPingThread
Remove use of deprecated forkPingThread
2020-11-05 08:26:44 +02:00
David Burkett
c00d1e1aa8 Use forkPingThread if websockets is less than 0.12.6.0 2020-11-04 21:46:00 -05:00
David Burkett
da09fdc69a Updated the Changelog.md for yesod-websockets to include comment and link for PR 1700 2020-11-04 20:43:41 -05:00
David Burkett
ead5d3388f Remove the use of forkPingThread in Yesod.WebSockets and replaced it with withPingThread 2020-11-04 20:34:37 -05:00
Daniel Brice
46f8879a0a add language extensions (how did this compile before?) 2020-11-04 14:18:23 -08:00
Daniel Brice
0a338177fe bump persistent-template 2020-11-04 14:15:21 -08:00
Daniel Brice
41bcace5fc support persistent 2.11 2020-11-04 14:01:57 -08:00
Michael Snoyman
bcae4c99b1
Merge pull request #1698 from yesodweb/documentErrorResponse
Document ErrorResponse
2020-10-07 09:14:08 +03:00
Maximilian Tagher
24061e18bd .. 2020-10-06 10:41:53 -04:00
Maximilian Tagher
067914aac0 Document ErrorResponse 2020-10-06 10:41:03 -04:00
Michael Snoyman
de45bc0d27
Appease Hackage 2020-09-22 17:37:55 +03:00
Michael Snoyman
f6ac2b1d3a
Fix filepath in cabal file 2020-09-22 17:32:37 +03:00
Michael Snoyman
1b79db382d
Merge pull request #1697 from d86leader/master
Generate appropriate Handler and Widget synonyms for polymorphic sites
2020-09-22 17:30:26 +03:00
d86leader
22c59207c1 Update changelog and version 2020-09-22 15:59:18 +07:00
d86leader
e3528ad85d Add test for regression of mkYesod 2020-09-22 15:41:45 +07:00
d86leader
4c4584fde8 Fix incorrect code generation for polymorphic datatypes 2020-09-22 15:33:43 +07:00
d86leader
62b418a801 Add tests for mkYesod with polymorphic datatypes 2020-09-22 15:33:34 +07:00
Michael Snoyman
84ca72e1d0
Merge pull request #1695 from yesodweb/removeGHandlerFromDocs
Remove GHandler from handlerToIO docs
2020-09-21 05:29:19 +03:00
Maximilian Tagher
6e5fa23dc2 .. 2020-09-20 16:57:01 -04:00
Maximilian Tagher
f0db028ec0 .. 2020-09-20 16:54:44 -04:00
Maximilian Tagher
30f189a48c Remove GHandler from handlerToIO docs 2020-09-20 16:50:16 -04:00
Michael Snoyman
eb5f7a95cd
Merge pull request #1691 from dten/absolute-templates-path
Absolute templates path
2020-08-09 17:48:45 +03:00
David Hewson
8585893b1d bump yesod version 2020-08-09 13:45:16 +01:00
David Hewson
8069d42d90 use globFilePackage in widgetFileReload and widgetFileNoReload
this will mean that files can be build from another directory whilst still finding the package's templates
2020-08-07 13:23:17 +01:00
David Hewson
b73a95c8b6 globFilePackage which provides absolute globFile paths within package 2020-08-07 13:23:17 +01:00
Michael Snoyman
8845483c20
Merge pull request #1689 from felixonmars/skip-semigroups-for-ghc8
Drop dependency on semigroups
2020-08-02 13:28:11 +03:00
Felix Yan
672b82d510
Drop dependency on semigroups
They are not needed on GHC we support.
2020-08-02 17:30:19 +08:00
Michael Snoyman
b2c154b358
Merge pull request #1688 from yesodweb/fixAddHandler
Fix add-handler for new routes file path
2020-08-02 12:19:06 +03:00
Maximilian Tagher
9f1387968f .. 2020-07-30 14:30:08 -04:00
Maximilian Tagher
7cfda1d650 Fix add-handler for new routes file path
https://github.com/yesodweb/yesod/pull/1686 recommended `.yesodroutes` as the file extension for the routes file, and https://github.com/yesodweb/yesod-scaffold/pull/203 added that to the scaffolding. But yesod-bin assumes the file is just named `routes`.

This PR will check if the old path exists and use that, and if not use the new path.

I tested this on the yesod-scaffold repo with both the old and new filename
2020-07-30 14:26:22 -04:00
Maximilian Tagher
77e6c3e7c2
Recommend .yesodroutes file extension (#1686)
* Recommend .yesodroutes file extension

Closes https://github.com/yesodweb/yesod/issues/1685

* ..
2020-07-26 17:08:39 +03:00
Michael Snoyman
7964967ba8
One more sort for #1684 2020-07-04 19:09:50 +03:00
Michael Snoyman
084a3cefb5
Sort directory contents #1684 2020-07-02 19:20:03 +03:00
Michael Snoyman
f5a6ccb363
Appease Hackage 2020-06-29 05:36:16 +03:00
Michael Snoyman
4bfca1bd86
Changelog entry for #1683 2020-06-29 04:47:51 +03:00
Michael Snoyman
c5a56f74fd
Merge pull request #1683 from ncaq/fix-yesod-bin-terminal-kill
fixed: yesod-bin: when C-c, kill yesod-bin and children process
2020-06-30 08:41:51 +03:00
ncaq
b3ed4613e7 fixed: yesod-bin: when C-c, kill yesod-bin and children process
* deleted: `setDelegateCtlc True`
* added: `setCreateGroup True`

When you use a group,
the child process will be terminated when the parent process is terminated.
2020-06-29 17:14:52 +09:00
Maximilian Tagher
a1c6bc553c Update yesod cabal file for distribution
Hackage is now requiring a higher cabal file version, and with that comes a requirement to specify the language of each module
2020-06-26 13:01:43 -04:00
Maximilian Tagher
5cbcebf4db Bump yesod to 1.6.0.2 2020-06-26 12:55:43 -04:00
Maximilian Tagher
51b4bde6d9
Merge pull request #1658 from v0d1ch/1655_Replace-decodeFile-with-decodeFileEither
Replace decodeFile with decodeFileEither
2020-06-26 12:52:32 -04:00
Michael Snoyman
98afc13e92
Allow random 1.2 2020-06-24 10:34:43 +03:00
Michael Snoyman
d4a60baf77
Merge pull request #1680 from yesodweb/printBodyPreview
When statusIs fails, print a preview of the body
2020-06-22 08:59:32 +03:00
Maximilian Tagher
1d67e3a359 1.6.10 2020-06-21 20:27:35 -04:00
Maximilian Tagher
28e5b606b2 attempt to fix 8.2 2020-06-20 17:27:13 -04:00
Maximilian Tagher
4ddff42847 .. 2020-06-20 15:01:51 -04:00
Maximilian Tagher
f50d23ce49 .. 2020-06-20 15:01:26 -04:00
Maximilian Tagher
8f00e76257 .. 2020-06-20 14:56:19 -04:00
Maximilian Tagher
34927e3401 .. 2020-06-20 14:54:31 -04:00
Maximilian Tagher
2ddc63e66a When statusIs fails, print a preview of the body
My team makes frequent use of `statusIs`, but in virtually all cases where `statusIs` fails, we need to add a call to `printBody` to do further debugging.

Following in the footsteps of `requireJSONResponse`, this PR automatically prints a portion of the body when `statusIs` fails, assuming the body looks like a text-based response (e.g. not a JPEG).

I've found that a status code alone is often very misleading and leads people on a wild good chase, because e.g. a 403 could be triggered for many different reasons.

I'm opening this PR as a draft to confirm people like the idea of doing this. If so I'll do a closer review of the code (this is my first draft basically), and also write some tests + test the code works in all cases.
2020-06-17 17:31:00 -04:00
Maximilian Tagher
e7cf662af7 Update yesod-test cabal file for distribution
Hackage is now requiring a higher cabal file version, and with that comes a requirement to specify the language of each module
2020-06-01 14:54:39 -04:00
Maximilian Tagher
d03c095b63 Add bumped GHC dependency in yesod-test changelog 2020-06-01 11:53:03 -04:00
Maximilian Tagher
b94da055c0
Merge pull request #1676 from yesodweb/documentYesodTest
Add more documentation to yesod-test
2020-06-01 11:29:12 -04:00
Maximilian Tagher
5c33dcb518 .. 2020-05-31 16:56:09 -04:00
Maximilian Tagher
8e89ec0e40 .. 2020-05-31 16:38:48 -04:00
Maximilian Tagher
ab8a994a34 .. 2020-05-31 16:38:09 -04:00
Maximilian Tagher
c3fa2adddd Add more documentation to yesod-test
This adds high-level documentation to yesod-test, plus some function documentation
2020-05-31 16:35:27 -04:00
Michael Snoyman
074865bca9
Merge pull request #1674 from ericdeansanchez/fix-typo
Fix small typo in documentation
2020-05-23 21:37:16 +03:00
eric
d382d67769
Fix small typo in documentation
This PR seeks to correct a small typo in the word _continuously_.
2020-05-23 11:29:06 -07:00
Michael Snoyman
2b5bf7b9b9
Merge pull request #1672 from amkhlv/master
increase the size of CSRF token
2020-05-01 13:35:16 +03:00
Andrei Mikhailov
c39b165ff3 increase the size of CSRF token 2020-05-01 00:40:11 -03:00
Michael Snoyman
7f37d2b6fa
Merge pull request #1664 from RyanGlScott/master
Use DeriveLift to generate yesod-core's Lift instances
2020-03-31 16:21:23 +03:00
Ryan Scott
29a08425e9 Use DeriveLift to generate yesod-core's Lift instances
GHC 8.0 and later come with the `DeriveLift` extension for deriving
instances of `Language.Haskell.TH.Syntax.Lift`. `yesod-core` supports
GHC 8.2 and up, so it is able to make use of this. Not only does
`DeriveLift` make for much shorter code, but it also fixes warnings
that you get when compiling `yesod-core` with GHC 8.10 or later:

```
[20 of 31] Compiling Yesod.Routes.TH.Types ( src/Yesod/Routes/TH/Types.hs, interpreted )

src/Yesod/Routes/TH/Types.hs:34:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘liftTyped’
    • In the instance declaration for ‘Lift (ResourceTree t)’
   |
34 | instance Lift t => Lift (ResourceTree t) where
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/Yesod/Routes/TH/Types.hs:49:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘liftTyped’
    • In the instance declaration for ‘Lift (Resource t)’
   |
49 | instance Lift t => Lift (Resource t) where
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/Yesod/Routes/TH/Types.hs:59:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘liftTyped’
    • In the instance declaration for ‘Lift (Piece t)’
   |
59 | instance Lift t => Lift (Piece t) where
   |          ^^^^^^^^^^^^^^^^^^^^^^^^

src/Yesod/Routes/TH/Types.hs:78:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘liftTyped’
    • In the instance declaration for ‘Lift (Dispatch t)’
   |
78 | instance Lift t => Lift (Dispatch t) where
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

This is because `DeriveLift` fills in implementations of `liftTyped`,
a method that was introduced to `Lift` in `template-haskell-2.16.0.0`
(bundled with GHC 8.10).
2020-03-31 08:41:00 -04:00
Jezen Thomas
59f601a34c
Add functions for setting description and OG meta (#1663)
* Add functions for setting description and OG meta

It's common that a website author will want to add a specific
description and Open Graph image to a given page in their website. These
functions are simple conveniences to add these meta tags to the document
head.

I decided against simply adding all possible meta tags, because not all
of them are useful, and even in the case of Open Graph tags, many of
them should be set only once and they should use the same value for the
entire website. In those cases, it's probably better for the website
author to add those tags in their layout template.

Closes https://github.com/yesodweb/yesod/issues/1659
2020-03-30 19:00:24 +07:00
Michael Snoyman
4a4dd06df8
Azure matrix/stack.yaml update 2020-03-24 12:20:19 +02:00
Michael Snoyman
179296c568
Merge pull request #1662 from charukiewicz/master
Yesod.Auth.Email: Fixed incorrect confirmation message, enabled customizing 'forgot password' email
2020-03-24 07:15:56 +02:00
charukiewicz
c5e76faf4d adjusted indentation of if/then/else to be more compact 2020-03-21 12:49:18 -05:00
charukiewicz
ff8ad9717f added link to PR in changelog entry 2020-03-21 02:27:52 -05:00
charukiewicz
cef6fc42ef bumped yesod-auth to 1.6.10 and updated ChangeLog 2020-03-21 02:16:40 -05:00
charukiewicz
61f1e5eeae added sendForgotPasswordEmail to YesodAuthEmail typeclass and incorporated into defaultRegisterHelper 2020-03-21 02:15:27 -05:00
charukiewicz
f80ec711ff updated AuthMessage to accommodate user registration flow with password: added EmailVerified and EmailVerifiedChangePass, deprecated AddressVerified 2020-03-20 22:22:13 -05:00
Michael Snoyman
9d0c9180b9
unliftio-core 0.2 2020-03-01 11:00:36 +02:00
Michael Snoyman
b4c613f766
Merge pull request #1660 from charukiewicz/register-helper
Add methods to YesodAuthEmail that allow custom flow after registration/password reset
2020-02-11 08:44:30 +02:00
charukiewicz
4f62e39fb1 added pull request link to ChangeLog entry 2020-02-09 17:03:51 -06:00
charukiewicz
16bf146887 version bump to 1.6.9 and changelog update 2020-02-05 23:49:16 -06:00
charukiewicz
c35c2fddc2 added passwordResetHelper method, exposed defaultRegisterHelper 2020-02-05 23:45:02 -06:00
charukiewicz
abdd844279 added registerHandler method to YesodAuthEmail class to enable override 2020-02-05 01:21:56 -06:00
Sasha Bogicevic
cfaf517f54 Replace decodeFile with decodeFileEither 2020-01-28 09:18:59 +01:00
Michael Snoyman
5943ee527d
Version bump 2020-01-27 17:52:24 +02:00
Michael Snoyman
6c9279c146
Merge pull request #1657 from simonmichael/master
support template-haskell 2.16, build with GHC 8.10
2020-01-27 17:51:35 +02:00
Simon Michael
dd649bf238 support template-haskell 2.16, build with GHC 8.10 2020-01-27 06:11:48 -08:00
Maximilian Tagher
873bee0cfa
Merge pull request #1654 from juanpaucar/remove_typeable
Remove unnecessary Typeable deriving, Solves #1653
2020-01-25 14:03:07 -08:00
Juan Paucar
6e38a3b99d Bump version properly 2020-01-20 11:06:35 -05:00
Juan Paucar
eb159b6fd5 Bump versions 2020-01-17 14:03:59 -05:00
Juan Paucar
c279547962 Remove unnecesary Typeable deriving 2020-01-17 11:48:58 -05:00
Michael Snoyman
804b114d91
Drop LTS 9 2020-01-07 13:36:58 +02:00
Michael Snoyman
dc16761492
Merge pull request #1649 from yesodweb/yesodPersistentCompatibility
Add extensions for yesod-persistent tests to be compatible with lates…
2020-01-03 07:38:07 +02:00
Maximilian Tagher
a8a7b6c7a5 .. 2020-01-02 22:51:54 -05:00
Maximilian Tagher
367d77a8c3 .. 2020-01-02 22:00:13 -05:00
Maximilian Tagher
cf365d18a2 Add extensions for yesod-persistent tests to be compatible with latest persistent-template
The upcoming version of persistent-template will require these yesodweb/persistent#1002
2020-01-02 21:57:00 -05:00
Maximilian Tagher
4f51153b09 Require latest yesod-core for yesod-test
(This was missing from #1646)
2019-12-01 08:06:10 -05:00
Maximilian Tagher
3fac351583
Merge pull request #1646 from yesodweb/requireJSONResponse
[yesod-test] Adds requireJSONResponse function
2019-12-01 08:04:21 -05:00
Maximilian Tagher
8ee771896c .. 2019-11-29 10:46:25 -05:00
Maximilian Tagher
561adc2e17 .. 2019-11-29 10:44:49 -05:00
Maximilian Tagher
42d41f77de .. 2019-11-29 10:43:11 -05:00
Maximilian Tagher
0025226af6 Print preview of JSON body in case of parse failure 2019-11-28 22:33:00 -05:00
Maximilian Tagher
91b75741dd .. 2019-11-24 16:11:50 -05:00
Maximilian Tagher
92afb1150a .. 2019-11-24 15:56:42 -05:00
Maximilian Tagher
596db81d7a .. 2019-11-24 15:55:58 -05:00
Maximilian Tagher
6d0b723eb1 [yesod-test] Adds requireJSONResponse function
This function checks that a response body is JSON, and parses it into a Haskell value. Having something like this function is pretty essential to using Yesod as a JSON API server, so I think it's a good addition. You can use it to parse a Haskell record directly (usually by adding FromJSON classes to your response types), or parse a Value and pull out individual fields, maybe using something like `aeson-lens` (though probably a testing-specific library would be better).

I debated over these things:

1. The name. I was thinking of something like [assert/require/decode/parse]JSON[Response/Body]. I ultimately went with requireJSONResponse:
	- decode/parse sound like the aeson functions that return Either or Maybe, and I wanted this function to throw an error if it failed
	- I'm open to using `assertJSONResponse`—it matches the other functions (`assertEq`) better—but I think it reads less like English.
	- I chose Response over Body because (a) It also checks the content-type header, which is not in the body (b) "Body" felt slightly in-the-weeds of HTTP; I think "response" is more approachable.
2. Should it require the JSON content type? You can definitely have a server that returns JSON without JSON content types, but I think that's a such a bad idea, it's more likely requiring it helps people if they accidentally don't add the header.
3. Should it take a String parameter to add to the error message? This would match `assertEq`, but other functions like `statusIs` don't take a message. Ultimately I went without it, because the messages felt like I was repeating myself: `(comment :: Comment) <- requireJSONResponse "the response has a comment"`
2019-11-24 15:31:05 -05:00
Michael Snoyman
463fd54c5a
Drop rio dep (fixes #1645) 2019-11-22 09:16:52 +02:00
Maximilian Tagher
d5f6fbba8b
Merge pull request #1642 from yesodweb/testModifySite
Add testModifySite
2019-11-20 08:41:08 -08:00
Maximilian Tagher
b0c07ea3cd .. 2019-11-19 23:13:11 -08:00
Maximilian Tagher
cbef19fae9 [yesod-test] Add testModifySite 2019-11-19 23:11:13 -08:00
Maximilian Tagher
a2d200c182 .. 2019-11-17 12:43:24 -08:00
Maximilian Tagher
e39eaeef92 WIP - Add testModifySite 2019-11-17 12:42:20 -08:00
Michael Snoyman
0d4c435e42
Reenable yesod-auth on nightly 2019-11-16 21:18:18 +05:30
Michael Snoyman
bce08f6d86
Merge pull request #1640 from alexeyzab/mention-db-rollback
Add a warning about DB actions getting rolled back
2019-11-14 14:01:22 +05:30
Alexey Zabelin
19f4b26e1f
Add a warning about DB actions getting rolled back 2019-11-13 20:55:47 -08:00
Michael Snoyman
e333735176
Merge pull request #1638 from chrisdone/add-getting-started-code-sample
Add Getting Started to READMEs
2019-11-11 16:38:39 +02:00
Chris Done
4a6aedf88a Add Getting Started to READMEs
* Adds to the README seen on Github.
* Adds to the README that will be seen on Stackage/Hackage.
2019-11-11 13:46:27 +01:00
Michael Snoyman
347ea6775b
Support for Cabal 3.0 2019-11-11 09:47:05 +02:00
Michael Snoyman
6d21107549
Merge pull request #1636 from yesodweb/1635-use-get-stmt-conn
Replace call to connPrepare with getStmtConn (fixes #1635)
2019-10-30 05:49:01 +02:00
Michael Snoyman
7839de4dbd
Replace call to connPrepare with getStmtConn (fixes #1635) 2019-10-29 13:25:53 +02:00
Michael Snoyman
eb9432c01d
Simplify CI 2019-10-27 07:40:39 +02:00
Michael Snoyman
9b93e97a80
Merge pull request #1634 from betlgtu/master
Translated message "CurrentPassword" for russian language.
2019-10-27 07:35:29 +02:00
Виталий Котляров
ddc71e665b Translated message "CurrentPassword" for russian language. 2019-10-26 23:08:38 +03:00
Michael Snoyman
b5fb6caca0
Merge branch 'master' of github.com:yesodweb/yesod 2019-10-10 06:44:12 +03:00
Michael Snoyman
5fb58470fe
Merge pull request #1632 from yesodweb/addBasicAuthToYesodTest
Add Basic Auth support to yesod-test
2019-10-10 06:44:00 +03:00
Michael Snoyman
b788310519
Not compatible with nightly right now 2019-10-10 06:43:51 +03:00
Maximilian Tagher
e4c493d199 .. 2019-10-09 16:11:54 -07:00
Maximilian Tagher
ffd5ba0474 Add Basic Auth support to yesod-test
This allows setting username/password for HTTP basic auth, similar to the --user flag of curl.
2019-10-09 16:08:37 -07:00
Michael Snoyman
c8aeb61ace
Merge pull request #1631 from yogsototh/rss-2.0
Support categories for Atom and RSS newsfeed
2019-10-02 13:37:54 +03:00
Yann Esposito (Yogsototh)
3a7bff1537
Feedback fixes 2019-10-02 09:07:55 +02:00
Yann Esposito (Yogsototh)
a0d35dfe4c
add support for RSS categories 2019-09-30 22:12:27 +02:00
Michael Snoyman
2a71af250f
Merge pull request #1628 from sestrella/areq_and_wreq_custom_error_message
Customize `areq` and `wreq` error message
2019-09-10 11:24:45 +03:00
Sebastián Estrella
32ecbd056d Customize areq and wreq error message 2019-09-10 00:42:21 -05:00
Michael Snoyman
f054bac0e0
Version bump 2019-09-08 09:45:29 +03:00
Michael Snoyman
904644c577
Merge pull request #1627 from DanBurton/yesod-core-th-2-15-fix
Add new CPP branch for template-haskell >= 2.15
2019-09-08 08:58:27 +03:00
Dan Burton
ed69d69347 Add new CPP branch for template-haskell >= 2.15 2019-09-06 13:04:21 -04:00
Michael Snoyman
ba1e48308c
Merge pull request #1623 from jezen/master
Increase CI timeout
2019-08-27 14:54:46 +03:00
Jezen Thomas
d82baf83b6
Increase CI timeout
The build on Azure DevOps is currently failing as the build-time is
exceeding 120 minutes. Hopefully 180 minutes should be sufficient.
2019-08-27 12:19:29 +02:00
Michael Snoyman
50a3242507
Merge pull request #1622 from jezen/master
Add jsAttributesHandler
2019-08-26 16:25:46 +03:00
Jezen Thomas
46af7bfb76
Cleanup 2019-08-26 13:49:57 +02:00
Jezen Thomas
d1e4fd485b
Make jsAttributesHandler use jsAttributes
This addresses this comment:

https://github.com/yesodweb/yesod/pull/1622#discussion_r317467498
2019-08-26 12:05:30 +02:00
Jezen Thomas
59988f46a1
Add jsAttributesHandler
This adds `jsAttributesHandler` to run arbitrary Handler code before
building the attributes map for the script tag generated by `widgetFile`.
This is useful if you need to add a randomised nonce value to that tag.

Closes https://github.com/yesodweb/yesod/issues/1621
2019-08-23 21:57:48 +02:00
Michael Snoyman
2c2531c499
Merge pull request #1619 from 3v0k4/auth-dummy-json
Dummy: Add support for JSON submissions
2019-08-23 09:10:30 +03:00
3v0k4
baa6bfb3a8 Dummy: Check for JSON first 2019-08-23 00:48:17 +02:00
Michael Snoyman
d7a29977bf
Merge pull request #1617 from league/url-encode
URL-encode POST parameters in yesod-test
2019-08-20 09:10:21 +03:00
Michael Snoyman
56e85572d4
Merge pull request #1620 from jezen/master
Move JavaScript form submission to script block
2019-08-20 09:08:55 +03:00
Christopher League
fa90ab19ca Update changelog 2019-08-19 15:57:15 -04:00
Jezen Thomas
d385ada853
Move JavaScript form submission to script block
If someone wants their website to score a good grade on a security
vulnerability scanner like Mozilla Observatory, they will need to enable
the Content Security Policy header. When using CSP, it is possible to
explicitly allow inline JavaScript in `<script>` tags by specifying the
sha256 of the snippet. However the same is _not_ true of any JavaScript
included in a HTML attribute like `onload`.

This changes moves the JavaScript form submission out of the `onload`
HTML attribute and into a `<script>` tag so the user can add the hash of
this script to their explicitly-allowed `script-src` list, and they can
avoid using undesirable CSP rules like `unsafe-inline`.

Without explicitly allowing this script when using CSP, the script would
fail and the user would have to click the button to continue.
2019-08-19 20:56:00 +02:00
3v0k4
657b790a3d Dummy: Add support for JSON submissions 2019-08-19 19:25:37 +02:00
James Burton
d8ebb95c96
Added Multi Input Form Functionality (#1601) 2019-08-16 04:30:20 +01:00
Christopher League
08a9632eba yesod-test: correction to PR for multipart 2019-08-13 11:58:11 -04:00
Christopher League
3f98190645 Bump version to 1.6.6.2 2019-08-13 10:39:44 -04:00
Christopher League
a79f73a040 yesod-test: url-encode addPostParam keys & values
Fixes #1616
2019-08-13 10:34:57 -04:00
Christopher League
ac66323394 yesod-test: failing tests for encoding errors
Related to issue #1616
2019-08-13 10:29:21 -04:00
Michael Snoyman
37c0df8dc1
Merge pull request #1613 from jezen/master
Add mreqMsg
2019-07-18 14:52:35 +03:00
Jezen Thomas
e5e39f353d
Implement mreq in terms of mreqMsg 2019-07-18 13:56:19 +03:00
Jezen Thomas
14ade1ad98
Add mreqMsg
When a form has multiple `mreq` fields and the user submits a form with
more than one of these values missing, they see an ambiguous collection
of "Value is required" messages. The `mreqMsg` function allows you to
provide your own MsgValueRequired message for a better UX.
2019-07-18 11:11:02 +03:00
Michael Snoyman
bc73850173
Merge pull request #1608 from leifmetcalf/ignore
Trivial: add stack.yaml.lock to .gitignore
2019-07-07 20:18:19 +03:00
Michael Snoyman
1af6f243f6
Merge pull request #1607 from leifmetcalf/patch-1
Trivial: use git clone --recurse-submodules instead of --recursive
2019-07-07 20:17:17 +03:00
Leif Metcalf
048148824c
Ignore stack.yaml.lock 2019-07-06 14:22:32 +12:00
Leif Metcalf
6eb597052a
Use git clone --recurse-submodules 2019-07-06 14:00:11 +12:00
Michael Snoyman
636f35b081
Merge pull request #1605 from mmilata/html-typo
Trivial: remove comma from <form> tag
2019-06-18 21:46:36 -07:00
Martin Milata
b4d3e01afb Remove comma from <form> tag 2019-06-18 01:56:32 +02:00
Michael Snoyman
af038b75f8
Merge pull request #1604 from jprider63/optional-labels
Optional labels
2019-06-16 06:28:08 -07:00
James Parker
0f8441eb73 Updated changelog. 2019-06-15 09:45:32 -04:00
James Parker
4424abe449 Hide bootstrap labels when empty 2019-06-11 14:09:54 -04:00
Steven Leiva
c90c50911c
Merge pull request #1598 from StevenXL/update-clearcreds
clearCreds redirect behavior depends on request type.
2019-05-24 10:20:55 -05:00
patrick brisbin
e470f1a3f8 Version bump 2019-05-21 12:10:03 -04:00
patrick brisbin
096049e0e3 sendClose and sendPing act in m ()
The previous type signature was attempting to read the Connection off of
the m in WebSocketsT m, rather than the WebSocketsT itself. This was
likely a typo that happened to type-check. The types for these now align
with the rest of the API, read the Connection off of WebSocketsT, and
make no demands of the m other than MonadIO.

Fixes #1599.
2019-05-21 12:10:03 -04:00
Steven Leiva
ceeeb16ae0 clearCreds redirect behavior depends on request type. 2019-05-14 13:20:34 +00:00
Michael Snoyman
006fa6cb9e
Update ChangeLog 2019-04-30 11:33:32 +03:00
Michael Snoyman
8c4b8e5094
Merge pull request #1595 from yesodweb/move-to-azure
Move to azure
2019-04-30 09:29:34 +03:00
Michael Snoyman
3647bf94b7 Strip CRs for Windows testing 2019-04-30 07:03:22 +03:00
Michael Snoyman
3b58652483
Compat with network 3 2019-04-30 05:13:10 +03:00
Michael Snoyman
3ba15fb8d3
Skip lts-9 on Windows (needs more extra-deps) 2019-04-30 04:53:14 +03:00
Michael Snoyman
5921a10ded
Increase Azure timeout 2019-04-30 04:47:30 +03:00
Michael Snoyman
5fe5e24eb0
Add missing rio dependency
Travis was handling this via the solver, which we don't want to be using
2019-04-29 19:21:27 +03:00
Michael Snoyman
5acc62b5e2
Updated badge for Azure 2019-04-29 18:48:37 +03:00
Michael Snoyman
772324e8cb
Azure config 2019-04-29 18:44:47 +03:00
Michael Snoyman
c7bb2d22af
Remove Travis config 2019-04-29 18:44:26 +03:00
Michael Snoyman
d9d4863fc1
Remove appveyor 2019-04-29 18:37:18 +03:00
Michael Snoyman
abdea8d157
Remove out-of-date ReleaseNotes.md 2019-04-29 18:37:00 +03:00
Michael Snoyman
3015a9a9f1
Remove out-of-date README 2019-04-29 18:36:35 +03:00
Michael Snoyman
b30a2a5b07
Remove sources.txt 2019-04-29 18:36:06 +03:00
Michael Snoyman
de209b3b94
Merge pull request #1594 from yesodweb/persistent-2-10
Test against persistent 2.10
2019-04-29 18:00:40 +03:00
Michael Snoyman
2a51e05448
Test against persistent 2.10 2019-04-29 16:03:39 +03:00
Michael Snoyman
ad6ed03aa4
Add support for persistent 2.10 2019-04-22 08:46:21 +03:00
Michael Snoyman
cc2d19a25d
Relax upper bound on persistent 2019-04-22 08:45:44 +03:00
Michael Snoyman
b5839420aa
Version bump 2019-04-13 22:01:17 +03:00
Michael Snoyman
42fbab9129
Merge pull request #1592 from StevenXL/add-jsonresponse-type
Introduce JSONResponse.
2019-04-13 22:00:23 +03:00
Steven Leiva
ab096c649c Introduce JSONResponse.
This data type allows us to return a domain type in our handlers, even
if we eventually want to send JSON to the client.

See: https://tech.freckle.com/2015/12/21/servant-style-handlers-for-yesod/
2019-04-12 16:13:53 -05:00
Michael Snoyman
6a7370a9e6
Merge pull request #1589 from geekingfrog/maximumContentLengthIO
maximumContentLengthIO
2019-03-17 11:11:45 +02:00
Grégoire Charvet 黑瓜
f1374c9140 add doc for maximumContentLength override 2019-03-17 09:10:07 +00:00
Grégoire Charvet 黑瓜
a691f49258 update changelog 2019-03-15 20:37:46 +00:00
Grégoire Charvet 黑瓜
48bfe0d573 maximumContentLengthIO 2019-03-15 20:35:39 +00:00
Michael Snoyman
4b760a027e
Merge pull request #1586 from yesodweb/documentRunDB
Document runDB
2019-03-07 04:24:28 +02:00
Maximilian Tagher
837b898b35 Document runDB
My coworker who is new to Haskell was pointing out that for such an important function to Yesod, this one is lacking any documentation. It's slightly hard to document because people could provide various implementations for it, but I think this description captures the essence pretty well, and notes the important implicit behavior of opening a transaction.
2019-03-06 11:31:38 -08:00
Michael Snoyman
c67c89007c
src subdir for yesod-core 2019-02-19 04:02:21 +02:00
Michael Snoyman
e5cc9987ae
Move from byteable to memory 2019-02-19 03:58:31 +02:00
Michael Snoyman
b1e718397b
Deprecate GoogleEmail2 (fixes #1579) 2019-02-12 18:52:47 +02:00
Michael Snoyman
90fa4d9eae
Merge pull request #1581 from nytopop/no-multi-session-cookies
Use at most one valid session cookie per request
2019-02-11 19:16:05 +02:00
nytopop
70b730cc4e
Use at most one valid session cookie per request
Makes `loadClientSession` ignore all sessions in a request if more than
a single session cookie decodes successfully. The prior behavior was to
merge all valid session cookies' values.

Bumps version to 1.6.12
2019-02-10 08:42:37 -08:00
Michael Snoyman
9ccdc38b78
Merge pull request #1576 from eborden/eborden/deprecate-insecure-json-body-functions
Deprecate insecure JSON body functions
2019-01-30 09:21:29 +02:00
Evan Rutledge Borden
da9e72b82f Add minor version bump to 1.6.11
JSON parsing function deprecations warrant a minor version bump.
2019-01-29 15:31:35 -06:00
Jonathan Lamothe
6fe7ee6e0d Documentation fix (#1577)
* fixed documentation

* bumped version

* updated ChangeLog
2019-01-26 12:53:04 +05:30
Evan Rutledge Borden
b50ca99566 Deprecate insecure JSON body functions
`parseJsonBody` and `requireJsonBody` do not require a mime type when
parsing `JSON` content. This leaves them open to CSRF. They are now
deprecated and `insecure` versions are added in their place. Consumers
are now given a proper choice between secure and insecure functions.

There is a potential attack vector in that the browser does not trigger
CORS requests for "simple requests", which includes POST requests that
are form or text content-types. An attacker can craft a form whose body
is valid JSON, and when a user visits attacker.com and submits that
form, it can be submitted to bank.com and bypass CORS.

Checking the content-type is application/json prevents this, because if
the content-type was set to application/json, then the browser would
send a CORS request—a preflight OPTIONS request to the server asking if
the current domain (and some other values) are whitelisted to send
requests to that server. If the server doesn't say attacker.com is
whitelisted, the browser will not send the real request to the server.
2019-01-24 09:12:48 -06:00
Michael Snoyman
874a711d47
Merge pull request #1574 from yesodweb/more-ltses
More LTSes are tested
2019-01-23 08:26:41 +02:00
Michael Snoyman
5f597494b5
More LTSes are tested 2019-01-22 20:09:05 +02:00
Michael Snoyman
c7e4dd0a1c
Fix test suite compilation on GHC 8.6.3 commercialhaskell/stackage#4319 2019-01-22 18:40:31 +02:00
Michael Snoyman
9ff1f18a4a
Merge pull request #1573 from yesodweb/getSetCache
Add functions to get and set values in the per-request caches
2019-01-22 09:35:37 +02:00
Maximilian Tagher
c8974d81f9 Add functions to get and set values in the per-request caches
Closes #1572
2019-01-21 10:47:27 -08:00
Sibi
09c4587393
Merge pull request #1571 from jlamothe/FormResult
Make FormResult instances of Eq and Monad
2019-01-20 22:59:18 +05:30
Jonathan Lamothe
92e4e48353 updated ChangeLog 2019-01-19 13:12:29 -05:00
Jonathan Lamothe
fd141d56b7 incremented version number 2019-01-18 18:53:04 -05:00
Jonathan Lamothe
429f78859c make FormResult an instance od Eq 2019-01-18 12:48:36 -05:00
Maximilian Tagher
673db5f6ff
Merge pull request #1570 from yesodweb/cookieHelpers
[yesod-test] Add utility functions to modify cookies
2019-01-15 08:04:30 -08:00
Maximilian Tagher
bedec86c74 [yesod-test] Add utility functions to modify cookies 2019-01-14 16:12:32 -08:00
Michael Snoyman
72c6187a22
Merge pull request #1568 from stevehartdata/master
Eliminate deprecation warnings when building websockets sample.hs
2019-01-09 08:49:51 +02:00
Steve Hart
e1a33248b0 Eliminate deprecation warnings when building websockets sample.hs 2019-01-07 20:12:12 -05:00
Sibi
c5268e3581
Merge pull request #1567 from gabebw/gbw-fix-typo
Fix typo in deprecation message
2018-12-28 10:41:12 +05:30
Gabe Berke-Williams
9720363117 Fix typo in deprecation message
The message recommended using `authTwitterUsingUserID` (note that the
`ID` at the end of the method name is all capitalized).

However, the actual method name is `authTwitterUsingUserId` (note the `Id` at the
end).
2018-12-27 17:31:58 -08:00
Michael Snoyman
f7f356b32e
Relax upper bound (fixes #1566) 2018-12-19 08:28:07 +02:00
Michael Snoyman
7a2c5367e7
Merge pull request #1565 from StevenXL/add-send-response-no-content
Add sendResponseNoContent.
2018-12-03 09:22:06 +02:00
Steven Leiva
2a9bef34c0 Add sendResponseNoContent. 2018-11-30 14:27:21 -06:00
Michael Snoyman
6eb91bdb77
Add missing test file (fixes #1563) 2018-10-15 16:21:17 +03:00
Michael Snoyman
f7e177d5f2
Version bump 2018-10-14 11:10:13 +03:00
Michael Snoyman
ab0ac8b1a2
Fix extra-deps 2018-10-14 10:49:12 +03:00
Michael Snoyman
aed169b43f
Merge branch 'update-persistent' of https://github.com/DanBurton/yesod 2018-10-14 10:47:23 +03:00
Michael Snoyman
b16084ed34
Configuration for persistent 2.9 2018-10-14 10:45:45 +03:00
Michael Snoyman
7f07325dc4
Merge branch 'master' of https://github.com/iand675/yesod 2018-10-14 10:44:18 +03:00
Dan Burton
bff8200ae4
Updated changelogs and versions for #1561 2018-10-11 14:21:17 -04:00
Dan Burton
132abccff2
Compile with ghc 8.6 by pushing MonadFail usage into IO 2018-10-11 13:53:35 -04:00
Dan Burton
90423f5bc7
Downgrade yesod-persistent version bump to patch level 2018-10-11 12:56:49 -04:00
Dan Burton
49dcfe02af
Merge branch 'master' of https://github.com/iand675/yesod into update-persistent 2018-10-11 12:54:52 -04:00
Michael Snoyman
84f77fe34a
Merge pull request #1558 from whittle/routes-file-line-continuations
In the route syntax, allow trailing backslashes to indicate line continuation.
2018-10-09 05:24:15 +03:00
Jason Whittle
ee260e24cb Update changelog with a link to PR #1558. 2018-10-08 18:04:16 -04:00
Jason Whittle
ca602d11bf Bump minor version. 2018-10-08 16:56:20 -04:00
Jason Whittle
4e4efd1627 In the route syntax, allow trailing backslashes to indicate line continuation. 2018-10-08 16:47:06 -04:00
Michael Snoyman
6a9bcc292d
Remove unneeded version bumps 2018-10-08 10:20:49 +03:00
Michael Snoyman
55e0ca4bc3
Add PrimMonad instances
Pointed out at: https://stackoverflow.com/q/52692508/369198
2018-10-08 08:19:32 +03:00
Michael Snoyman
1c2cb0c717
Merge pull request #1554 from oddvars/oddvars-typo-patch
minor typos
2018-09-14 08:29:19 +03:00
oddvars
41101b20dd
minor typos
fixed typo and escaped backslash for haddock
2018-09-13 16:42:52 +02:00
Michael Snoyman
2af5d9c64c
Relax yaml upper bound 2018-08-20 10:11:42 +03:00
Michael Snoyman
67c223d76b
Minor cabal file improvements 2018-08-20 10:11:42 +03:00
Steven Leiva
3ebd8f91a5
Merge pull request #1550 from StevenXL/set-x-xss-protection
Set X-XSS-Protection to 1; mode=block.
2018-08-06 15:46:31 -05:00
Steven Leiva
4015ef2919 Set X-XSS-Protection to 1; mode=block. 2018-08-03 14:17:11 -05:00
Chris Allen
826a607571
Merge pull request #1548 from yesodweb/MaxGabriel-patch-1
Explain how requireCheckJsonBody can prevent CSRF
2018-08-02 10:30:41 -05:00
Maximilian Tagher
1f05d2c72f
Explain how requireCheckJsonBody can prevent CSRF 2018-07-31 21:22:39 -07:00
Michael Snoyman
6f76b5ff91
Merge pull request #1544 from nmk/master
Do not lose selected value in `selectFieldHelper` when validation fails
2018-07-29 10:21:35 +03:00
Nickolay Kolev
073c9fabd4 Do not lose selected value in selectFieldHelper when validation fails 2018-07-26 20:44:26 +02:00
Steven Leiva
db1ff95520
Merge pull request #1540 from StevenXL/error-406
Fix Improper 406 Responses
2018-07-24 07:36:16 -05:00
Steven Leiva
266c436f18 selectRep chooses first rep if no matches found.
The `selectRep` documentation indicates that it choose the first
representation provided if no representation matches.

This was only partially correct, as `selectRep` required that no
representation matched **and** that the `Content-Type` header of the
response was empty.

This led to a problem because `defaultErrorhandler` relies on
`selectRep`, and when `selectRep` was unable to find a suitable
representation, it would "swallow" the original error that resulted in
`defaultErrorhandler` being called, and set a status 406 for all cases.
2018-07-19 21:32:02 -05:00
Michael Snoyman
1c51a93a45
Relax upper bounds 2018-07-11 08:20:20 +03:00
Michael Snoyman
04393855e5
Merge branch 'master' of github.com:yesodweb/yesod 2018-07-10 11:56:46 +03:00
Michael Snoyman
b7a3385a89
Merge pull request #1537 from kikaiteam/fix_forgot_pass_endpoint
AuthEmail: Fix forgot-password endpoint
2018-07-10 11:56:28 +03:00
Michael Snoyman
6fb09cfa5a
Merge pull request #1538 from kikaiteam/improve_auth_email_doc
AuthEmail: no-op, improve doc format
2018-07-10 11:55:46 +03:00
Michael Snoyman
074dc11678
Merge pull request #1539 from h-3-0/patch-1
Fix typo
2018-07-10 02:13:38 +03:00
Henri Jones
c221aa3aaa
Fix typo 2018-07-09 22:47:48 +01:00
hainq
72ad3082ce AuthEmail: no-op, improve doc format 2018-07-09 18:24:11 +07:00
hainq
d38affbe6d yesod-auth: update changelog & bump version 2018-07-09 17:52:07 +07:00
hainq
0c136f14eb AuthEmail: Fix forgot-password endpoint
This is a quick fix for PR#1524.
Currently, defining `emailPreviouslyRegisteredResponse` will prevent
`sendConfirmationEmail` from happening, even when `registerHelper` is
called from `postForgotPasswordR`.
2018-07-09 17:34:15 +07:00
Michael Snoyman
c1344a577f
Fix version bump 2018-07-09 13:12:40 +03:00
Michael Snoyman
71d951c09b
Correct the version bump (previous unreleased changes) 2018-07-09 13:10:55 +03:00
Michael Snoyman
e125795de3
Merge pull request #1536 from kikaiteam/support_more_email_register_flows
AuthEmail: Immediately register with a password
2018-07-09 12:55:08 +03:00
hainq
bd9d0f9922 yesod-auth: Bump version & Update Changelog 2018-07-09 14:06:53 +07:00
hainq
6a64debfa0 Redirect to afterVerificationWithPass when account was registered with a password 2018-07-09 14:02:10 +07:00
hainq
54b1d3d3ff AuthEmail: Immediately register with a password
Register endpoint: Support an optional "password" param that can be used
to set new accounts' password immediately.
2018-07-09 14:02:10 +07:00
Michael Snoyman
ea182bb464
Merge pull request #1534 from yesodweb/reduce-deps
Reduce deps
2018-07-04 09:34:31 +03:00
Michael Snoyman
182abd89bf
Drop some deps 2018-07-03 19:01:58 +03:00
Michael Snoyman
867e7c32dc
Clean up some CPP 2018-07-03 18:57:23 +03:00
Chris Allen
df5ad82a90
Merge pull request #1530 from pythonissam/expose-selectFieldHelper
Expose select field helper
2018-06-24 13:24:20 -05:00
Michael Snoyman
d134c20dab
Merge pull request #1524 from StevenXL/fix-email-registration
Fix email registration
2018-06-24 11:39:59 +03:00
Steven Leiva
8d58a56577 Make behavior of registerHelper configurable.
The behavior of `registerHelper` when an email that is already-verified
tries to register is now configurable via the
`emailPreviouslyRegisteredResponse` method of the `YesodAuthEmail`
typeclass.
2018-06-23 09:56:20 -05:00
pythonissam
01802e984b update the ChangeLog 2018-06-23 05:12:08 +00:00
Michael Snoyman
840f8faaaa
Merge pull request #1528 from ncaq/clean-appveyor
cleaned: appveyor: Easier to detect bugs
2018-06-20 06:22:38 +03:00
ncaq
6187c3cf09 cleaned: appveyor: Easier to detect bugs
* appveyor.yml conf file to simple from offcial reference
* Make the cache easier to use by commenting out
* fixed clone directory, from stack to yesod
* curl check https
* stack URL
* split build and test, So we can easy detect build or test error
* test job to single thread, So we can easy find bug of test
2018-06-20 10:49:28 +09:00
Michael Snoyman
a43e5a1cbb
Merge pull request #1526 from yesodweb/1523-fix-stalled-tests
1523 fix stalled tests
2018-06-19 11:24:12 +03:00
Michael Snoyman
d38d00f114 Skip RawResponse tests on Windows #1523
These tests stall on Windows starting with network-2.6.3.4. I haven't
yet figured out why exactly that's the case, or a minimum repro.
2018-06-19 10:37:20 +03:00
Michael Snoyman
12a2bb58e9 Add timeouts so stalling is more obvious 2018-06-19 09:52:20 +03:00
pythonissam
b5def68be2 Version bump 2018-06-17 08:46:52 +00:00
pythonissam
0b261f5073 Exposed 'selectFieldHelper' 2018-06-17 08:40:55 +00:00
Michael Snoyman
0437ace264
Merge pull request #1525 from StevenXL/bodyequals
bodyEquals prints actual body.
2018-06-14 09:17:51 -07:00
Steven Leiva
0a089c8cb0 bodyEquals prints actual body. 2018-06-14 09:00:48 -05:00
Michael Snoyman
cae2a9159a
Merge pull request #1520 from StevenXL/text-plain-error
defaultErrorHandler handles text/plain request.
2018-06-12 21:29:27 -07:00
Ian Duncan
bf97821b68 Fix merge conflict 2018-06-13 13:21:23 +09:00
Ian Duncan
2d14cdbf1a Update changelogs for yesod-persistent and yesod-auth 2018-06-13 13:12:01 +09:00
Steven Leiva
a63bf16a68 defaultErrorHandler handles text/plain request. 2018-06-12 21:08:55 -05:00
Ian Duncan
3036573f57 Update yesod-auth and yesod-persistent to persistent-2.9 2018-06-13 09:21:45 +09:00
Michael Snoyman
c88c2019ee
Merge pull request #1522 from ncaq/deleted-unneed-build-depends
deleted: unneed cabal build-depends by weeder
2018-06-12 07:53:12 -07:00
ncaq
708648798e deleted: unneed cabal build-depends by weeder
[weeder: Detect dead code](https://hackage.haskell.org/package/weeder)

deleted depends is

* mime-mail
* wai-eventsource

I sort build-depends, because duplicate depend some exist, to sort is detect to easy.
2018-06-09 13:15:21 +09:00
Michael Snoyman
c3a2a6afac
Merge pull request #1518 from psibi/update-issue-template
Update issue template slightly
2018-06-07 07:46:55 -06:00
Sibi Prabakaran
d15080ac4e
stack list-dependencies is deprecated 2018-06-06 20:39:56 +05:30
Michael Snoyman
74e43462cb
Merge pull request #1515 from yesodweb/multiple-stack-yamls
Multiple stack yamls
2018-06-04 06:40:03 -06:00
Michael Snoyman
908a758167 unliftio extra-deps 2018-06-03 14:54:21 -06:00
Michael Snoyman
a77b509bb6 Add warp to extra-deps 2018-06-03 14:35:50 -06:00
Michael Snoyman
3c2de1a763 Add persistent-sqlite extra-dep 2018-06-03 13:14:43 -06:00
Michael Snoyman
cdba6c1678 Fix CABALVER 2018-06-03 13:13:38 -06:00
Michael Snoyman
2cb60c8513 Multiple stack.yaml files 2018-06-03 11:42:44 -06:00
Michael Snoyman
d73f7b579f Travis update 2018-06-03 10:35:09 -06:00
Michael Snoyman
f21140ecf0
Merge pull request #1514 from pythonissam/isstring-textarea-instance
Make Textarea derive IsString
2018-06-03 06:29:09 -06:00
pythonissam
22197d1215 Update ChangeLog 2018-06-03 08:27:57 +00:00
pythonissam
b101276dcb version bump 2018-06-03 08:22:09 +00:00
pythonissam
67d215d2f2 Make Textarea derive IsString 2018-06-03 08:18:31 +00:00
Michael Snoyman
4444c47d39 Relax an upper bound 2018-05-29 06:43:14 +03:00
Michael Snoyman
1cf2f56918
Merge pull request #1510 from bsima/form-fns
Move 'addClass' to Yesod.Form.Functions and add 'removeClass'
2018-05-16 06:51:41 +03:00
Ben Sima
1d95f8315b Remove unnecessary type annotation 2018-05-15 12:51:14 -07:00
Ben Sima
e906768ee9 Replace addClass/removeClass examples with ghci-driven examples 2018-05-15 12:50:39 -07:00
Ben Sima
e8a145ae88 Add examples to addClass and removeClass 2018-05-14 08:45:09 -07:00
Michael Snoyman
e041ff4da9
Cabal 2.2 support in yesod-bin (fixes #1511) 2018-05-13 15:14:04 +03:00
Ben Sima
c163a0841a Add PR link to Changelog 2018-05-11 13:36:10 -07:00
Ben Sima
6334e77515 Update changelog 2018-05-11 13:35:12 -07:00
Ben Sima
c57ba49472 Add @since documentation 2018-05-11 13:31:25 -07:00
Ben Sima
0dffa0e29a Bump version 1.6.2 2018-05-11 13:31:25 -07:00
Ben Sima
56ca6d7914 Move 'addClass' to Yesod.Form.Functions and add 'removeClass'
'addClass' is more general than just Bootstrap forms. In particular, it is
copied into the yesod-form-bootstrap4 project and I found myself using it in my
custom forms. It would be useful to have it exported for use elsewhere.

I added 'removeClass' because I needed it while creating a custom 'readonly'
input in a form and thought it might be generally useful.
2018-05-11 13:23:31 -07:00
Michael Snoyman
5861357923
Merge pull request #1503 from ncaq/add-file-source-bytes
added: fileSourceByteString
2018-05-04 08:45:20 +03:00
ncaq
a8df3c48c2 modified: use sinkLazy and toStrict
Because performance problem.
2018-05-01 17:15:13 +09:00
Michael Snoyman
4cd29ae298
Merge pull request #1485 from NorfairKing/with-generator
yesodSpecWithSiteGenerator with an argument
2018-04-30 17:03:13 +03:00
Michael Snoyman
bc4ecd7dc8
Fix broken http-reverse-proxy < 0.6 support 2018-04-26 12:17:00 +03:00
Tom Sydney Kerckhove
0aa1765b6c Updated according to review 2018-04-26 11:00:48 +02:00
Tom Sydney Kerckhove
a8f5418b22 yesodSpecWithSiteGenerator with an argument 2018-04-26 10:50:51 +02:00
Michael Snoyman
07df43f207
Merge branch 'master' of github.com:yesodweb/yesod 2018-04-26 11:20:26 +03:00
Michael Snoyman
e87c9b5bf0
Support for http-reverse-proxy 0.6.0 2018-04-26 11:20:04 +03:00
ncaq
e664ae2e0e changed: use foldC 2018-04-24 12:55:55 +09:00
ncaq
74ce4c57ff Merge branch 'master' into add-file-source-bytes 2018-04-24 12:36:36 +09:00
Michael Snoyman
b92b2a0871
Merge pull request #1504 from ncaq/add-addContentDispositionFileName
added: addContentDispositionFileName
2018-04-18 09:22:10 +03:00
ncaq
f5855c8397 Merge branch 'master' into add-file-source-bytes 2018-04-17 18:58:09 +09:00
ncaq
33b5171b75 modified: fileSourceByteString: use sinkLazy 2018-04-17 18:54:18 +09:00
ncaq
fc7884f7f2 Merge branch 'master' into add-addContentDispositionFileName 2018-04-17 18:49:53 +09:00
ncaq
a59ee6b62e added: ChangeLog 1.6.4 2018-04-17 18:47:54 +09:00
ncaq
eb220c936a added: addContentDispositionFileName: document comment
I wrote battle of multibyte from code review.
2018-04-17 18:44:19 +09:00
Michael Snoyman
df6ca6e59c
Merge pull request #1506 from ncaq/fix-appveyor
fixed: see th-lift-instances-0.1.11
2018-04-17 12:39:56 +03:00
ncaq
2c1a6c609f Merge branch 'master' into add-addContentDispositionFileName 2018-04-17 18:05:46 +09:00
ncaq
1e89f4d4c3 cleaned: fileSourceByteString: document comment
from code review.
2018-04-17 18:01:36 +09:00
ncaq
11159f3a75 cleaned: use runConduit and .| instead of connect
from code review.
2018-04-17 17:58:49 +09:00
ncaq
1eb553f92c Merge branch 'master' into add-file-source-bytes 2018-04-17 17:52:20 +09:00
ncaq
4d7679775a fixed: see th-lift-instances-0.1.11
th-lift-instances-0.1.11 build is Simple.
2018-04-17 17:49:34 +09:00
Michael Snoyman
cf1073d760
Merge pull request #1502 from pythonissam/general-request-method
add performMethod
2018-04-17 10:31:44 +03:00
ncaq
712e8bb475 added: addContentDispositionFileName 2018-04-17 14:33:38 +09:00
ncaq
955b21d7ea added: ChangeLog 1.6.4 2018-04-17 14:24:57 +09:00
ncaq
7e2ca33ed5 added: fileSourceByteString
This function is to get `FileInfo` raw body.
2018-04-17 13:58:15 +09:00
pythonissam
860ef4127a Change the method in the example of performMethod 2018-04-14 06:47:32 +00:00
pythonissam
a9030aa294 make get and post use performMethod 2018-04-14 06:46:38 +00:00
pythonissam
7ad28d227c Update ChangeLog.md 2018-04-14 06:10:59 +00:00
pythonissam
a7d42846b5 add performMethod 2018-04-14 06:08:15 +00:00
Michael Snoyman
06e0ffb48b
Merge pull request #1501 from rnons/getPerson-signature
generalize type signature of GoogleEmail2.getPerson
2018-04-09 16:38:31 +03:00
Ping Chen
fa1248389d bump version and update ChangeLog 2018-04-09 20:48:57 +09:00
Ping Chen
468fba2226 generalize type signature of GoogleEmail2.getPerson 2018-04-09 14:21:48 +09:00
Michael Snoyman
778cf2cf0b
Add missing SubHandlerFor export 2018-04-04 12:11:27 +03:00
Michael Snoyman
ff5618bd15
Merge pull request #1493 from agreif/patch-1
minor doc change
2018-03-05 17:07:27 +02:00
Alex Greif
760b947ed4
minor doc change 2018-03-05 14:09:38 +00:00
Michael Snoyman
e894239563
Add missing version bump 2018-03-05 11:21:53 +02:00
Michael Snoyman
cb229cf84c
Merge pull request #1492 from yesodweb/showRoutes
Derive Show instances for route data structures
2018-03-05 10:17:39 +02:00
Maximilian Tagher
08ef0e26dc Derive Show instances for route data structures
* It's very helpful to have a Show instance for debugging and development
* Currently third party packages are deriving this instance themselves which is not ideal.
    * http://hackage.haskell.org/package/yesod-routes-flow-2.0/docs/src/Yesod-Routes-Flow-Generator.html
    * http://hackage.haskell.org/package/yesod-routes-typescript-0.3.0.0/docs/src/Yesod-Routes-Typescript-Generator.html
    * This change would break those packages, which isn't great
         * At least the typescript one is broken anyway
2018-03-04 15:59:54 -08:00
Michael Snoyman
bdcb4272cd
Merge pull request #1490 from psibi/issue-1489
Remove MINIMAL pragma for authHttpManager
2018-03-04 14:33:47 +02:00
Sibi Prabakaran
ff043db45b
Update changelog 2018-03-04 13:39:31 +05:30
Sibi Prabakaran
c04d6f9ac7
Remove MINIMAL pragma for authHttpManager
We now have a default implementation for it. See this for more
information:
https://github.com/yesodweb/yesod/issues/1489#issuecomment-370200663

Helps in preventing warnings like this:

```
serverside.hs:40:10: warning: [-Wmissing-methods]
    • No explicit implementation for
        ‘authHttpManager’
    • In the instance declaration for ‘YesodAuth App’
   |
40 | instance YesodAuth App where
   |          ^^^^^^^^^^^^^
```
2018-03-04 13:20:21 +05:30
Michael Snoyman
a3f130233b
Relax a number of type signatures #1488 2018-02-20 13:51:36 +02:00
Michael Snoyman
63006970c6
Remove duplicate description field 2018-02-20 13:51:36 +02:00
Maximilian Tagher
f2b651b695
Merge pull request #1482 from pythonissam/bylabel-contain
Bylabel contain
2018-02-10 18:49:13 -08:00
Maximilian Tagher
a0963e77b2
Merge branch 'master' into bylabel-contain 2018-02-09 22:20:02 -08:00
pythonissam
b8f93e6203 Update the version to 1.6.2 2018-02-10 04:16:07 +00:00
kotaro
acf956443d Updated deprecation warnings accordingly 2018-02-10 08:28:24 +09:00
Michael Snoyman
1e9b7ee664
Comment out lts-7 for OS X 2018-02-05 16:14:19 +02:00
Michael Snoyman
7d60a57e5c
Merge branch 'RyanGlScott-master' 2018-02-05 15:38:12 +02:00
Michael Snoyman
8387ba3e23
More extra-deps for lts-7 2018-02-05 14:21:53 +02:00
Michael Snoyman
3014d8028c
Fix compilation in #1484
Pinging @RyanGlScott, these changes were necessary for older versions of
dependencies. Is there any problem with using this for GHC 8.4?
2018-02-05 12:14:54 +02:00
Ryan Scott
3408e1e630 Adapt to Semigroup changes in base-4.11 2018-02-04 20:09:37 -05:00
pythonissam
54614dd241 Update ChangeLog 2018-02-03 08:40:38 +00:00
pythonissam
88a23129fd Version bump 2018-02-03 08:40:38 +00:00
pythonissam
d03e92ff9b Add new fileByLabel-related functions 2018-02-03 08:40:29 +00:00
pythonissam
ed9306323c Add missing fileByLabelWithMatch and make existing fileByLabel-related functions to use it 2018-02-03 08:39:52 +00:00
pythonissam
064f41d9e9 created new byLabel-related functions 2018-02-03 08:39:40 +00:00
Michael Snoyman
450573ac35
Use newer simple-sendfile for Windows compat 2018-02-02 14:33:46 +02:00
Michael Snoyman
ce0c697659
Merge pull request #1464 from yesodweb/better-monads
Better monads
2018-02-02 11:27:06 +02:00
Michael Snoyman
6b660412e9
memory extra-dep 2018-02-02 10:07:45 +02:00
Michael Snoyman
2317e07851
Add foundation as an extra-dep 2018-02-02 09:45:00 +02:00
Michael Snoyman
3ff2f17b2c
cabal builds: comment out longer test part 2018-02-02 09:41:08 +02:00
Michael Snoyman
42e73e0bfd
Add gauge and basement to extra-deps 2018-02-02 00:36:57 +02:00
Michael Snoyman
c2f9dec1e6
Tighten base lower bound
Technically unnecessary since it's inherited from conduit, but this is
more explicit.
2018-02-02 00:35:30 +02:00
Michael Snoyman
5d49a85f40
Travis update 2018-02-02 00:35:21 +02:00
Michael Snoyman
6ad81f6d15
Merge remote-tracking branch 'origin/master' into better-monads 2018-02-02 00:17:37 +02:00
Michael Snoyman
7f78e81cc1
Lower bounds and missing extra-deps 2018-02-02 00:15:24 +02:00
Michael Snoyman
70d606c820
Updated stack.yaml 2018-02-02 00:04:15 +02:00
Michael Snoyman
fe233dd958
Merge pull request #1478 from jprider63/master
Update `mkYesodWith` and refactor so that `mkYesod` uses the context parser
2018-01-29 14:57:44 +02:00
James Parker
8796310eef More documentation for mkYesod and mkYesodWith 2018-01-24 23:55:57 -05:00
Maximilian Tagher
4901d66a52 Add .DS_Store to .gitignore file 2018-01-24 08:33:46 -08:00
Maximilian Tagher
70ecad3829
Merge pull request #1477 from yesodweb/tooLargeResponseErrorMessage
Give more detail in the error message for too large request bodies.
2018-01-24 08:32:58 -08:00
Michael Snoyman
fa8e1ac00f
Switch to SubHandlerFor
This is much more consistent than suddenly using a ReaderT for subsites.
Thanks to @jprider63 for the inspiration for this, I think it cleans
things up a lot!
2018-01-24 13:01:26 +02:00
James Parker
b71bfae261 Refactor so that mkYesod and mkYesodDispatch use the context parser 2018-01-22 00:45:44 -05:00
James Parker
18910b516b Change mkYesodWith to accept separate lists for contexts and type
arguments
2018-01-22 00:19:04 -05:00
Michael Snoyman
0f09393c34
Merge branch 'simple-content-type' of https://github.com/JaSpa/yesod into better-monads 2018-01-21 11:13:05 +02:00
Maximilian Tagher
6b22a0b9be Give more detail in the error message for too large request bodies.
* Just to be helpful to developers, give the maximum body length and their body length
* Also point developers to the function to change that value

(I don't think this leaks any sensitive info, because you can always binary search with different request body sizes to find the maximum allowable)
2018-01-18 18:30:34 -08:00
Janek Spaderna
492102537f [yesod] Bump version & add changelog entry 2018-01-18 12:11:43 +01:00
Janek Spaderna
7f6f1821e8 [yesod] Fix comment for contentTypeTypes & simpler implementation
In the implementation of contentTypeTypes make use of simpleContentType.
2018-01-18 12:00:46 +01:00
Michael Snoyman
6830a9840c
Merge branch 'better-monads' into no-transformers 2018-01-17 06:43:52 +02:00
Michael Snoyman
2047efd00a
Bump persistent version 2018-01-16 20:21:48 +02:00
Michael Snoyman
ad35ef9431
Deal with another sneaky exception 2018-01-16 16:10:23 +02:00
Michael Snoyman
3956110876
Fix yesod-websockets 2018-01-15 21:07:54 +02:00
Michael Snoyman
f2926e60f0
Remove some deprecated methods from the Yesod class 2018-01-15 20:52:33 +02:00
Michael Snoyman
e3bb03f9af
Missing import 2018-01-15 17:56:20 +02:00
Michael Snoyman
dff7f2372e
Switch to SIO 2018-01-15 17:38:36 +02:00
Michael Snoyman
a210ce59d7
Get it all compiling again 2018-01-15 17:08:55 +02:00
Michael Snoyman
915d9e2fa6
Finish switching header key to a CI
Fixes #1418
2018-01-15 16:47:49 +02:00
Michael Snoyman
25acc5799b
Version bumps and changelog updates 2018-01-15 15:57:36 +02:00
Michael Snoyman
89be12c147
Strictify a bunch of fields 2018-01-15 15:18:09 +02:00
Michael Snoyman
60f65ed267
Cleanup warnings 2018-01-15 15:09:07 +02:00
Michael Snoyman
1f7a2a287b
Switch to gauge 2018-01-15 11:46:38 +02:00
Michael Snoyman
8c96b4e36c
Fix benchmark compile 2018-01-15 10:48:50 +02:00
Michael Snoyman
1a1cb8a45f
Drop mwc-random 2018-01-15 10:18:16 +02:00
Michael Snoyman
aa5b80d9f8
Merge branch 'addTypeTime2' of https://github.com/MaxGabriel/yesod into better-monads 2018-01-12 00:19:54 +02:00
Michael Snoyman
6a715c06c3
Merge remote-tracking branch 'origin/guess-approot-default' into better-monads 2018-01-12 00:18:37 +02:00
Michael Snoyman
3bb654857c
Ditch ResumableSource 2018-01-12 00:09:54 +02:00
Michael Snoyman
3e06942449
Simplify YesodSubDispatch 2018-01-11 23:13:32 +02:00
Michael Snoyman
fbccfe2306
Merge branch 'better-monads' into no-transformers 2018-01-11 22:49:02 +02:00
Michael Snoyman
103c098cf8
Catch up with Data.Conduit.Combinators 2018-01-10 12:16:31 -08:00
Michael Snoyman
a16e75249a
More moving over to unliftio 2017-12-31 09:20:02 +02:00
Michael Snoyman
e2b0a5c454
Merge pull request #1474 from yesodweb/documentCustomizingCSRFByRoute
Document whitelisting certain routes to not need CSRF protection
2017-12-31 06:24:39 +02:00
Michael Snoyman
eac95935e6
Switch over to WIP conduit 1.3 2017-12-30 22:47:56 +02:00
Michael Snoyman
8a30e487b0
Merge remote-tracking branch 'origin/master' into better-monads 2017-12-30 20:41:28 +02:00
Michael Snoyman
f9a87f4022
Merge pull request #1473 from yesodweb/fix1459
Fix Haddock syntax error and test failures introduced by #1459
2017-12-30 18:13:41 +02:00
Michael Snoyman
65093b6b24
Merge branch 'master' into fix1459 2017-12-30 18:13:32 +02:00
Michael Snoyman
ed40b136ea
Fix broken Haddocks 2017-12-30 18:06:45 +02:00
Maximilian Tagher
5cdc0a39ac Document whitelisting certain routes to not need CSRF protection
This question came up on the #yesod Slack channel and I think it's moderately common; I've seen it elsewhere.
2017-12-29 23:44:08 -05:00
Maximilian Tagher
51bdb66252 Fix Haddock syntax error and test failures introduced by #1459 2017-12-29 20:20:18 -05:00
Maximilian Tagher
3bf6a89595
Update PULL_REQUEST_TEMPLATE.md 2017-12-29 20:17:48 -05:00
Maximilian Tagher
5af7fe95ec
Merge pull request #1470 from csi-adziahel/patch-1
Fix typo breaking some parsers
2017-12-28 15:23:40 -08:00
Michael Snoyman
3df82600b8
Merge pull request #1459 from pythonissam/bylabel-exact
Bylabel exact
2017-12-27 13:02:42 +02:00
kotaro
daa953ed1f Add PR link into the deprecated pragmas 2017-12-27 09:47:57 +09:00
Andrey Dyagel
8a0778a58a
Fix typo breaking some parsers 2017-12-22 16:15:13 +03:00
Michael Snoyman
5a5cd81276
Support typed-process-0.2.0.0 (fixes #1467) 2017-12-19 11:53:41 +02:00
Michael Snoyman
1b22e6a908
Further transformer cleanup 2017-12-18 17:06:46 +02:00
Michael Snoyman
8e265f6ebc
It all compiles 2017-12-18 15:04:45 +02:00
kotaro
8e52b490fe Insert deprecated pragmas for byLabel and fileByLabel 2017-12-18 14:10:12 +09:00
kotaro
8cf44ff7c8 Small fix of haddock 2017-12-18 13:34:16 +09:00
pythonissam
31903c34f8 fileByLabelExact: Created the exact version of fileByLabel 2017-12-17 08:50:48 +00:00
Michael Snoyman
aed10fc84a
WIP 2017-12-13 14:39:59 +02:00
Michael Snoyman
61c887f501
Start converting yesod-auth over 2017-12-13 13:44:59 +02:00
Michael Snoyman
47ee7384ea
Be gone with ye HandlerT! 2017-12-13 09:53:14 +02:00
Maximilian Tagher
96758a4d58
Merge pull request #1465 from psibi/unsafe-haddock
Fix haddock doc for the Yesod.Core.Unsafe module
2017-12-12 15:14:21 -08:00
Sibi Prabakaran
323d7f4322
Fix haddock doc for the Yesod.Core.Unsafe module 2017-12-13 02:33:37 +05:30
Michael Snoyman
1c2914eded
MonadUnliftIO instances 2017-12-12 12:46:49 +02:00
Michael Snoyman
5c8b1b542a
WidgetT uses IORef 2017-12-12 12:46:35 +02:00
Michael Snoyman
c5ac821115
Remove some conditionals for old versions 2017-12-12 12:08:06 +02:00
Michael Snoyman
64b5bdb347
Merge pull request #1460 from yesodweb/updated-coc
Switch CoC to Contributor Covenant
2017-12-10 18:22:46 +02:00
Michael Snoyman
09ac889376
Merge pull request #1462 from psibi/master
Fixed exposed version number and also haddock syntax for Yesod.Auth module
2017-12-08 12:07:28 +02:00
Sibi Prabakaran
aff72a7365
Fix since markup 2017-12-08 15:33:15 +05:30
Sibi Prabakaran
663220f334
Fix exposed version number and also haddock syntax 2017-12-08 15:30:32 +05:30
Michael Snoyman
c122af25ad
Merge pull request #1461 from LiveViewTech/redirect_to_current
Redirect to current
2017-12-08 09:02:59 +02:00
Casey Allred
e40178a854 adding link to the PR in changelog.md 2017-12-07 14:24:48 -07:00
Casey Allred
108c0c3984 merged master 2017-12-07 14:22:28 -07:00
Casey Allred
30ccfc8089 didn't mean to change this signature 2017-12-07 14:01:36 -07:00
Casey Allred
f82d08b32a add a way to control redirection to current location when redirectLogin is called 2017-12-07 13:56:53 -07:00
Michael Snoyman
f7f2c1edbd
Switch CoC to Contributor Covenant 2017-12-06 16:02:01 +02:00
kotaro
baf30d0c9d Improve comments 2017-12-04 17:12:51 +09:00
pythonissam
699203f5af Update the Changelog 2017-12-02 10:14:24 +00:00
pythonissam
e5c04a0b8e Version bump 2017-12-02 09:57:40 +00:00
pythonissam
db87b76816 Add version info 2017-12-02 09:56:56 +00:00
pythonissam
8f218307be Improve the function name 2017-12-02 09:46:41 +00:00
pythonissam
57e25eaf39 Improve the document 2017-12-02 09:44:49 +00:00
pythonissam
8693c72c41 Remove the original nameFromLabel 2017-12-02 08:05:59 +00:00
pythonissam
70ec8c6823 new function genericNameFromLabel which abstracts the match methods 2017-12-02 08:00:31 +00:00
Maximilian Tagher
c81ad91cd1
Merge pull request #1455 from yesodweb/csrfBetterErrors
Give better error messages when CSRF validation fails
2017-11-26 10:41:02 -05:00
Maximilian Tagher
1275cce1af Give better error messages when CSRF validation fails
* This is important because historically these errors have tripped people up
* Making security as easy as possible is important so that it doesn't just get turned off
* Giving clear directions about where to get the CSRF token (a cookie) and where to send it (a header/param) is especially helpful to frontend developers not necessarily familiar with the backend codebase
2017-11-26 09:00:30 -05:00
Michael Snoyman
18de949b03
Merge pull request #1457 from bermanjosh/langFix
Fix docs on `languages` set and `getMessageRender` to use it (#1325)
2017-11-26 12:40:56 +02:00
Josh Berman
6d6afcf826 point changelog to PR not issue 2017-11-26 12:09:17 +02:00
Josh Berman
79ab662a80 Fix docs on languages set and getMessageRender to use it (#1325) 2017-11-26 11:52:37 +02:00
pythonissam
80aa45cf18 Simply, create the exact version of byLabel 2017-11-26 07:22:25 +00:00
pythonissam
cab78b65c2 Add a failure test case for byLabel 2017-11-26 04:45:02 +00:00
Maximilian Tagher
c21e77947c
Merge pull request #1456 from eborden/eborden/fix-constraints-on-get404
Fix constraints on get404 and getBy404
2017-11-17 14:13:33 -08:00
Evan Rutledge Borden
cfb8fd9b21 Version bump for get404 and getBy404 constraint changes. 2017-11-17 14:41:57 -05:00
Evan Rutledge Borden
33d3200399 Fix constraints on get404 and getBy404
The constraints on `get404` and `getBy404` were overly powerful. They were
constrained by `PersistStore` and `PersistStoreUnique`, which is an alias for
`PersistStoreWrite...`. These only need `PersistStoreRead...` to accomplish
their job.
2017-11-17 14:28:20 -05:00
Maximilian Tagher
d848a7123f
Merge pull request #1451 from yesodweb/githubTemplates
Add Github issue and PR templates, plus contributing guidelines
2017-11-16 09:28:22 -08:00
Maximilian Tagher
8208e3deac
Fix typo in Haddocks of assertEq 2017-11-15 11:32:59 -08:00
Maximilian Tagher
3247237c44 Respond to @psibi's comments 2017-11-08 22:43:51 -08:00
Maximilian Tagher
7a4b2812c1 Update contributing guidelines based of Snoyberg's "How to Send Me a PR" post 2017-11-08 22:32:15 -08:00
Alex Greif
2c59cb7dcd extend docs of defaultMaybeAuthId (#1453)
* extend docs of defaultMaybeAuthId

make more explicite that on each call a database access is done. This can be of relevance and sometimes redundant with other Handler functionality

* Update Auth.hs
2017-11-08 12:36:39 +00:00
Michael Snoyman
abc50deffe
Drop an upper bound 2017-11-07 06:03:59 +02:00
Maximilian Tagher
42b94f5066 Add Github issue and PR templates, plus contributing guidelines
* Closes #1450
2017-11-02 20:27:24 -07:00
Sibi
299d0569af Merge pull request #1446 from bigs/extensible-password-hashing
Extend `YesodAuthEmail` to support extensible password hashing
2017-10-01 22:31:16 +05:30
Cole Brown
600d307310 Extend YesodAuthEmail to support extensible password hashing.
This change introduces `hashAndSaltPassword` and `verifyPassword` to the
`YesodAuthEmail` type class, allowing users to implement their own hashing
schemes (i.e. to provide compatibility with an existing database). It also
updates the default handlers to use these new functions when appropriate. The
functions have default implementation such that behavior for legacy applications
should not change.
2017-09-28 14:37:21 -04:00
Michael Snoyman
4f6b07c2fb Merge pull request #1444 from iand675/weak-etag
Add support to yesod-core for weak etags
2017-09-10 14:53:52 +03:00
Ian Duncan
05b2193e9f
Code review fixes for #1444 2017-09-08 09:00:12 +09:00
Michael Snoyman
c4ef7e1410
yesod-form version bump 2017-09-06 18:28:00 +03:00
Ian Duncan
fd872cff40
Add support to yesod-core for weak etags 2017-09-06 10:08:45 +09:00
Michael Snoyman
d6e4499c54 Merge pull request #1439 from binarysunrise-io/login-vs-log-in
Correct Yesod-Auth's usage of "log in" vs "login" in English.
2017-08-31 07:58:52 +03:00
Jesse Kempf
9edca8e3b5 Correct Yesod-Auth's usage of "log in" vs "login" in English.
"Log in" (two words) is a verb, indicating the action of, well, logging
in. "Login" (one word) is a noun, indicating the credentials used to log
in.
2017-08-30 20:40:29 -07:00
Sibi
498d373e2d Merge pull request #1437 from paul-rouse/master
Expose Yesod.Auth.Util.PasswordStore
2017-08-30 07:57:58 +05:30
Paul Rouse
3c53acdad8 Add "@since" comments for newly exposed Yesod.Auth.Util.PasswordStore 2017-08-29 18:49:21 +01:00
Paul Rouse
464b055568 Expose Yesod.Auth.Util.PasswordStore 2017-08-29 13:40:32 +01:00
Paul Rouse
59f073a41f Pure move of Yesod.PasswordStore to Yesod.Auth.Util.PasswordStore 2017-08-29 13:34:20 +01:00
Sibi
c7736db69a Merge pull request #1435 from Ephemera/translation
Add Korean translation
2017-08-26 14:04:33 +05:30
GyuYong Jung
1569af55c7 Add Korean translation 2017-08-26 05:27:53 +09:00
Michael Snoyman
a1c7fc5281 Merge pull request #1433 from binarysunrise-io/form-result-instantiate-alternative
Give FormResult an Alternative instance
2017-08-23 08:44:59 +03:00
Jesse Kempf
f08944d888 Give FormResult an Alternative instance 2017-08-22 22:00:42 -07:00
Michael Snoyman
4f14b9b82d
Add a stricter lower bound 2017-08-22 11:25:51 +03:00
Michael Snoyman
1e9427baee
Version bump 2017-08-22 11:24:35 +03:00
Michael Snoyman
469c1c2d01 Merge pull request #1432 from stackbuilders/reduce_verbosity_using_monadic_forms
Reduce verbosity using Monadic Forms
2017-08-22 10:41:00 +03:00
Sebastián Estrella
0f28604cfe Reduce verbosity using Monadic Forms 2017-08-22 02:24:26 -05:00
Michael Snoyman
f65d88d8c5 Merge pull request #1427 from alx741/fix-spanish-trans
Fix auth messages Spanish translation
2017-08-08 19:16:58 +03:00
Daniel Campoverde
e3041aa17b Fix auth messages Spanish translation 2017-08-08 11:03:09 -05:00
Michael Snoyman
896ee9c644
Version bump and close #1413 2017-08-04 16:30:58 +03:00
GyuYong Jung
854f823059 add src/ to file path if Haskell source files in src
Fixes #1413
2017-08-04 15:40:28 +09:00
Michael Snoyman
3b8ca1d3d1
Bad CPP 2017-08-04 08:12:55 +03:00
Michael Snoyman
a4eee30930
Cabal 2.0 support 2017-07-31 09:55:19 +03:00
Sibi
e027652494 Merge pull request #1417 from psibi/header-yesod
Add replaceOrAddHeader function to Yesod.Core.Handler module
2017-07-28 18:35:23 +05:30
Sibi Prabakaran
7cfefdf3fa
Merge remote-tracking branch 'origin/master' into header-yesod
Conflicts resolved in:
	yesod-core/ChangeLog.md
	yesod-core/yesod-core.cabal
2017-07-28 17:01:03 +05:30
Sibi Prabakaran
19ff5c2006
Fix warning in test code 2017-07-28 16:58:11 +05:30
Michael Snoyman
a17779b12d
Fix persistent < 2.5 code 2017-07-23 16:53:12 +03:00
Michael Snoyman
ada76a9636
Revert "Always use solver on Travis"
This reverts commit 5b18bf0c09.
2017-07-23 16:45:47 +03:00
Michael Snoyman
5b18bf0c09
Always use solver on Travis 2017-07-23 16:28:58 +03:00
Michael Snoyman
fff8f8ff5f
Reduce extra-deps, drop LTS 2 and 3 2017-07-23 13:04:40 +03:00
Michael Snoyman
4b34fe9c72
Fix deprecation warning for LTS 8 2017-07-23 12:25:29 +03:00
Michael Snoyman
626719ce28
Fix some version issues 2017-07-23 11:10:47 +03:00
Michael Snoyman
8e367bda3d
Bump to LTS 8 2017-07-23 10:31:51 +03:00
Michael Snoyman
fefe8e0219
Attempt to get Travis building again 2017-07-23 09:34:15 +03:00
Michael Snoyman
42112add3c
Version bump 2017-07-23 07:27:14 +03:00
Michael Snoyman
39c0e0977b
Merge branch 'master' of github.com:yesodweb/yesod 2017-07-23 07:13:24 +03:00
Michael Snoyman
c05c2ddde6 Merge pull request #1424 from QuLogic/yesod-static-cryptonite
Convert yesod-static to cryptonite.
2017-07-23 07:12:22 +03:00
Elliott Sales de Andrade
087f4d2092 Convert yesod-static to cryptonite. 2017-07-22 22:58:23 -04:00
Michael Snoyman
06ca675bb6
Version bump 2017-07-20 13:58:15 +03:00
Michael Snoyman
eb3c570c93 Merge pull request #1421 from sestrella/add_has_call_stack_to_assertions
Add implicit param HasCallStack to assertions
2017-07-20 13:41:37 +03:00
Sebastián Estrella
a58a4d88cd Add implicit param HasCallStack to assertions 2017-07-19 22:47:00 -05:00
Sibi Prabakaran
617591aa4e
Do case insensitive equality on header name 2017-07-14 13:44:21 +05:30
Sibi Prabakaran
89fc6c46e2
Fix ordering logic in replaceHeader function 2017-07-13 16:29:08 +05:30
Sibi Prabakaran
f3ed12ed81
Add additional test to make sure that header value is not lost 2017-07-13 12:43:16 +05:30
Sibi Prabakaran
18951b0de7
Update the replace logic to obey proper ordering 2017-07-13 12:42:30 +05:30
Sibi Prabakaran
8416bb6569
Add Haddock documentation for the added function 2017-07-13 11:27:03 +05:30
Sibi Prabakaran
a31c270893
Update Changelog and do verion bump of the package 2017-07-13 11:24:57 +05:30
Sibi Prabakaran
3cec499c85
ScopedTypeVariables is also needed 2017-07-13 11:17:03 +05:30
Sibi Prabakaran
4e0b084df2
Enable test in YesodCoreTest 2017-07-13 11:16:47 +05:30
Sibi Prabakaran
839b56b032
Implement replaceOrAddHeader function 2017-07-13 11:10:54 +05:30
Sibi Prabakaran
301f4bc630
Expose YesodCoreTest.Header module 2017-07-13 11:07:13 +05:30
Sibi Prabakaran
051339f3dc
Add test code for HTTP headers properties 2017-07-13 11:05:57 +05:30
Michael Snoyman
7038ae6317 Merge pull request #1414 from bermanjosh/th-ghc-8.2.1-rc2
Work with TH from GHC 8.2.1-rc2
2017-07-04 08:58:58 +03:00
Josh Berman
ec85ef735c Work with TH from GHC 8.2.1-rc2 2017-07-03 06:44:25 -04:00
Michael Snoyman
432c31a652 Merge pull request #1412 from ncaq/master
fixed: yesod-form: textareaField: writeHtmlEscapedChar: convert "\r\n" to "<br>" closed #1354
2017-06-25 08:34:50 +03:00
ncaq
e19d220f61 fixed: import Data.Monoid for old ghc 2017-06-23 12:14:28 +09:00
ncaq
33471cbb2f add changelog and version number 2017-06-22 16:32:50 +09:00
ncaq
d5eb1ce026 fixed: yesod-form: textareaField: writeHtmlEscapedChar: convert "\r\n" to "<br>" 2017-06-22 10:05:39 +09:00
Maximilian Tagher
0223c0a586 Merge pull request #1408 from mwotton/master
add clickOn function (closes #1405)
2017-06-21 15:50:12 -07:00
Mark Wotton
c40d39dc5a one more since 2017-06-21 15:12:03 -04:00
Mark Wotton
7cd37db7c6 address review comments 2017-06-15 15:46:25 -04:00
Mark Wotton
1bc30deee7 import Control.Applicative for 7.8.4 2017-06-15 13:30:58 -04:00
Mark Wotton
2a112b5516 -Werror fixes 2017-06-15 12:17:49 -04:00
Mark Wotton
ee9ef1eac5 add clickOn function (closes #1406) 2017-06-14 13:40:44 -04:00
Michael Snoyman
2ade837223 Merge pull request #1404 from iand675/master
Add support to for mapping static unfingerprinted files to their fingerprinted equivalents
2017-06-07 16:01:48 +03:00
Ian Duncan
274b5445a1
Code review fixes for #1404 2017-06-07 20:13:04 +09:00
Ian Duncan
ded136513c
Add support to yesod-static for mapping unfingerprinted files to their fingerprinted equivalents 2017-06-06 15:21:25 +09:00
Michael Snoyman
0b1a4b114c Merge pull request #1366 from jprider63/dev.jp
Contexts can be parsed and included in instances. Standalone deriving…
2017-06-05 11:33:11 +03:00
James Parker
70f643b7e9 Merge branch 'master' of https://github.com/yesodweb/yesod into dev.jp 2017-06-01 11:24:54 -04:00
Sibi
6630c05937 Merge pull request #1399 from psibi/cryptonite
Move yesod-auth to Cryptonite (from cryptohash)
2017-05-26 01:07:45 +05:30
Sibi Prabakaran
ec90f48d88
Fix lts-2 related errors 2017-05-21 19:53:28 +05:30
Sibi Prabakaran
37452896d2
Try fixing travis error 2017-05-18 19:17:55 +05:30
Sibi Prabakaran
635470f750
Change cabal install version 2017-05-18 13:32:35 +05:30
Sibi Prabakaran
36a98bc4ab
Add memory to stack yaml for lts-2 2017-05-18 12:06:36 +05:30
Sibi Prabakaran
4ba2fc8494
Add foundation to stack.yaml 2017-05-18 09:50:47 +05:30
Sibi Prabakaran
d56485c3d3
Cryptonite 2017-05-18 09:46:44 +05:30
Sibi Prabakaran
cc1fa42192
Update travis to use ghc-8.0.2 instead of 8.0.1 2017-05-18 09:46:04 +05:30
Sibi Prabakaran
8f5b0bc238
Do version bump and add Changelog
Partially addresses #1397
2017-05-18 08:43:29 +05:30
Sibi Prabakaran
f1fb571427
Make relevant changes to cabal file for yesod-auth 2017-05-18 08:18:53 +05:30
Sibi Prabakaran
92849d863c
Port to cryptonite 2017-05-18 08:18:39 +05:30
Michael Snoyman
5721f65ebf
Version bumps 2017-05-14 00:24:12 +03:00
Michael Snoyman
602d1ff06a Merge pull request #1394 from mchaver/add-waisubsitewithauth
Add WaiSubsiteWithAuth
2017-05-12 08:25:56 +03:00
James Haver II
5ee51262de Update ChangeLog and Hackage comments 2017-05-12 01:04:13 +08:00
James Haver II
56b09eef93 Add WaiSubsiteWithAuth 2017-05-12 00:13:07 +08:00
Sibi
aeec20592c Merge pull request #1387 from steshaw/static-files-list-haddock-tweak
Fix Haddock formatting for staticFilesList
2017-05-05 05:40:47 +05:30
Steven Shaw
13cea1e3f7 Fix Haddock formatting for staticFilesList 2017-05-04 11:54:17 +10:00
Sibi
b9e57a1a60 Merge pull request #1381 from psibi/yesod-deadlock-2
Fixing race condition in yesod-bin
2017-04-27 20:13:47 +05:30
Sibi Prabakaran
5bb5e8948f
Revert back yesod version 2017-04-27 18:03:39 +05:30
Sibi Prabakaran
3350ca3d9a
Remove flag conditional 2017-04-27 17:49:21 +05:30
Sibi Prabakaran
10b5d4f8e2
Remove debug option 2017-04-27 17:40:32 +05:30
Sibi Prabakaran
67eb728703
Make updateAppPort as a single STM transaction 2017-04-27 17:08:12 +05:30
Sibi Prabakaran
35e0095590
Add releavant flag in yesod-bin 2017-04-26 20:57:13 +05:30
Sibi Prabakaran
37c9d25990
Add Debug flag 2017-04-26 20:56:58 +05:30
Sibi Prabakaran
706de89156
Change logic to use TVar 2017-04-26 19:37:59 +05:30
Sibi Prabakaran
62d7a19149
Fix warnings 2017-04-24 21:51:13 +05:30
Sibi Prabakaran
c37283e300
Update Changelog and do version bump 2017-04-24 20:39:20 +05:30
Sibi Prabakaran
878534a272
Fix race condition in yesod-bin
Stack build process emittles line even after successful build process
which leads to the overwriting of the appPortVar with -1. This leads
it to a compile mode again. Pressing Return Key and rebuilding it
again makes it go, but that's just a workaround I have to do every now
and then to solve the actual issue.

I'm using a `MVar` based locking solution for fixing the race
condition introduced.
2017-04-24 20:33:52 +05:30
Michael Snoyman
bc3054bfa2 Merge pull request #1378 from dawei-dev/patch-1
Fix minor doc typo
2017-04-17 20:17:42 +03:00
Dawei LIU
fcb1b7f6b4 Fix minor doc typo 2017-04-17 15:25:03 +02:00
Michael Snoyman
abf1c1ac5f Merge pull request #1376 from alanz/patch-1
GHC 7.6 not supported
2017-04-13 08:18:49 +03:00
Alan Zimmerman
01d5f02cee GHC 7.6 not supported 2017-04-12 19:31:40 +02:00
Michael Snoyman
3229b7ad93
persistent 2.7 2017-04-12 11:02:27 +03:00
Sibi
0d5b8b884f Merge pull request #1375 from amitaibu/assertNotEq
Add assertNotEq
2017-04-12 09:03:29 +05:30
Amitai Burstein
b8d2647a6a Add assertNotEq 2017-04-11 20:46:49 +03:00
rkaminsk
d8919c2c2d remove value attribute from password field (#1374)
remove value attribute from password field

Fixes https://github.com/yesodweb/yesod/issues/1373
2017-04-09 06:14:39 +05:30
Sibi
5a37a52080 Merge pull request #1371 from dfordivam/master
Japanese message for Current password
2017-04-05 07:34:47 +05:30
Divam
c1fa2645c0 Japanese message for Current password 2017-04-05 10:07:09 +09:00
Sibi
6b4139672e Merge pull request #1369 from mingyuguo/master
Added Yesod.Form.I18n.Chinese, also cleaned up chineseMessage in Yesod.Auth.Message
2017-03-28 17:30:07 +05:30
mingyu guo
db9b51cdf4 Added Yesod.Form.I18n.Chinese 2017-03-28 20:59:48 +10:30
mingyu guo
757514c536 Completed chineseMessage in Yesod.Auth.Message. Previously, most of the
messages are using simplified characters, but the google translated parts are
using traditional characters. I have fixed this as well.
2017-03-28 20:40:05 +10:30
JP
db8bbcd8b5 Merge branch 'master' into dev.jp 2017-03-27 12:10:04 -04:00
James Parker
6b000ecfb4 Version bump and fix for old versions of TH. 2017-03-27 12:06:44 -04:00
James Parker
997714f4c2 Accept multiple argument types inside brackets 2017-03-27 02:42:47 -04:00
James Parker
adf89bcf84 Contexts can be parsed and included in instances. Standalone deriving is used when
a context is provided. Type variables can be included in routes/TH.
2017-03-27 00:10:32 -04:00
Michael Snoyman
de9f5bc4c9
Version bump for #1363 2017-03-26 18:14:40 +03:00
Michael Snoyman
5b5e411cb5 Merge pull request #1363 from jprider63/master
Adds curly brackets to route parser.
2017-03-26 18:13:48 +03:00
Michael Snoyman
52d4a32217
Add curl for AppVeyor 2017-03-23 11:40:27 +02:00
James Parker
6c7a40ea5b Adds curly brackets to route parser. 2017-03-22 18:30:08 -04:00
Michael Snoyman
039046e355
Another Nix workaround (fixes #1359) 2017-03-15 06:42:12 +02:00
Michael Snoyman
c91f92a829 Merge pull request #1360 from dfordivam/master
Fix yesod-websockets/sample.hs example
2017-03-15 06:29:29 +02:00
Divam
85496411f2 Fix yesod-websockets/sample.hs example 2017-03-15 11:54:45 +09:00
Michael Snoyman
182b87e2d4
Version bump 2017-03-07 11:15:11 +02:00
Michael Snoyman
4f30dfca1e
Use --no-nix-pure #1357 2017-03-05 19:12:14 +02:00
Michael Snoyman
55623b76f6 Improve ChangeLog 2017-03-01 08:53:13 +02:00
Michael Snoyman
44675b3664 Merge pull request #1356 from LightAndLight/yesod-form/convertField-docs
Fixed some spelling issues in `Yesod.Form.Functions.convertField` documentation
2017-02-28 10:36:48 +02:00
Isaac Elliott
b76d9c3090 Fixed spelling and wording for Yesod.Form.Functions.convertField's docs 2017-02-28 17:50:20 +10:00
Michael Snoyman
3f9cbf2ff9 Switch to copyright year range #617 2017-02-27 09:47:45 +02:00
Michael Snoyman
40d3b5d2d4 Merge pull request #1355 from madnight/patch-1
Update license to 2017
2017-02-26 10:46:46 +02:00
Fabian Beuke
ab7428b1be Update license to 2017 2017-02-25 21:33:28 +01:00
Sibi
a0088c598b Merge pull request #1346 from alx741/email
Allow for a  custom Email login widget
2017-02-23 19:07:51 +05:30
Daniel Campoverde [alx741]
9014192c66 Update changelog 2017-02-18 18:31:05 -05:00
Daniel Campoverde [alx741]
ea5e1cca26 Update emailLoginHandler 'since' version 2017-02-18 18:28:53 -05:00
Daniel Campoverde [alx741]
c5ddf55937 Update emailLoginHandler 'since' version 2017-02-18 15:14:45 -05:00
Daniel Campoverde [alx741]
c78ae95b3a Fix email auth module 2017-02-18 15:14:45 -05:00
Daniel Campoverde [alx741]
311f7927bb Merge branch 'master' of https://github.com/yesodweb/yesod 2017-02-18 15:14:31 -05:00
Michael Snoyman
1cc30efe41 Merge pull request #1353 from psibi/add-post-function
Add getPostParams function in Yesod.Core.Handler
2017-02-17 08:00:49 +02:00
Sibi Prabakaran
6d7ba59e4b
Update changelog 2017-02-17 00:22:57 +05:30
Sibi Prabakaran
470858f81c
Better Haddock rendering. Since -> @since 2017-02-17 00:21:31 +05:30
Sibi Prabakaran
797278243e
Add and export getPostParams function 2017-02-17 00:18:17 +05:30
Michael Snoyman
4327dac8a7 For nightly, build cabal-install 2017-02-08 13:35:31 +02:00
Michael Snoyman
e032785af9 Another missing --install-ghc 2017-02-08 11:46:08 +02:00
Michael Snoyman
cdc6c8ae04 Version bumps/changelog updates 2017-02-08 11:20:31 +02:00
Michael Snoyman
86411d25f2 Silly typo 2017-02-08 09:08:10 +02:00
Michael Snoyman
305931f322 Not pedantic on OS X (since it applies to deps too) 2017-02-08 08:13:35 +02:00
Michael Snoyman
e83d018002 Add missing --install-ghc 2017-02-08 07:01:17 +02:00
Michael Snoyman
fc9d45aa33 Nightly: use solver --update-config 2017-02-07 21:05:44 +02:00
Michael Snoyman
58119d90cc Another build speed-up attempt
* Don't do the pre-build
* Apply --fast to dependencies on OS X
2017-02-07 21:02:26 +02:00
Michael Snoyman
b55b919800 Revert "Allow latest nightly"
This reverts commit 9ee3c37074.
2017-02-07 20:48:43 +02:00
Michael Snoyman
16924f6603 Merge branch 'allow-latest-nightly' 2017-02-07 20:48:12 +02:00
Michael Snoyman
a53d2cecf5 Try using cabal-head to work around haskell/cabal#4298 2017-02-07 19:00:48 +02:00
Michael Snoyman
9ee3c37074 Allow latest nightly 2017-02-07 17:18:21 +02:00
Michael Snoyman
4920ca11eb Merge pull request #1342 from yesodweb/appveyor
Add appveyor.yml
2017-02-07 17:17:20 +02:00
Michael Snoyman
1bc1ef5a35 Fix a CR test failure 2017-02-07 16:19:08 +02:00
Michael Snoyman
954f813569 Fix route parsing for CRLF line endings 2017-02-07 15:49:23 +02:00
Michael Snoyman
e90b31bb4a Add appveyor.yml 2017-02-07 15:49:23 +02:00
Michael Snoyman
958bc58153 Merge pull request #1345 from psibi/export-pid
Export plugin identifier for GoogleEmail2 module
2017-02-07 14:21:59 +02:00
Daniel Campoverde [alx741]
276a9f1321 Add and export defaultEmailLoginHandler 2017-02-06 16:15:38 -05:00
Sibi Prabakaran
d1ec382fc6
Better haddock rendering: Since -> @since 2017-02-07 01:01:05 +05:30
Sibi Prabakaran
854e0e45e7
Update relevant changelog 2017-02-07 01:00:19 +05:30
Sibi Prabakaran
dddae24786
Export plugin identifier for GoogleEmail2 module 2017-02-07 01:00:00 +05:30
Michael Snoyman
01378311f8 Merge pull request #1343 from psibi/show-creds
Add Show Instance For Creds type
2017-02-06 10:14:57 +02:00
Sibi Prabakaran
6f1356f2a1
Update changelog 2017-02-05 20:27:00 +05:30
Sibi Prabakaran
0c3e1d2299
Derive Show for Creds type
Useful for doing liftIO $ print inside Yesod handlers like
authenticate.
2017-02-05 20:25:23 +05:30
Michael Snoyman
52f67fb04b Merge pull request #1341 from yesodweb/pedantic
Compile with -Wall -Werror
2017-02-05 14:37:19 +02:00
Michael Snoyman
aefd074efa Cleanup GHC 8 redundant constraints 2017-02-05 13:35:12 +02:00
Michael Snoyman
3dc2d10b30 Compile with -Wall -Werror 2017-02-05 12:09:18 +02:00
Michael Snoyman
07147f42c2 Try to speed up OS X builds 2017-02-03 13:05:06 +02:00
Michael Snoyman
1920604d67 Merge pull request #1340 from jprider63/master
Fix for bug in identifyForm with empty forms.
2017-02-02 09:49:04 +02:00
James Parker
d59344b78b housekeeping 2017-02-02 01:55:00 -05:00
Michael Snoyman
64ed0792bc Check mime-type for JSON bodies #1330 2017-02-02 08:10:19 +02:00
Michael Snoyman
db883f19b8 Fix some whitespace 2017-02-02 07:43:55 +02:00
James Parker
ef22b131f1 Fix for bug in identifyForm with empty forms. #1072 2017-01-31 16:06:40 -05:00
Michael Snoyman
5e84a6c063 Merge pull request #1338 from tolysz/websockets-options
yesod-websockets: add `webSocketsOptions` `webSocketsOptionsWith`
2017-01-19 15:54:21 +02:00
Marcin Tolysz
e7e2caeacf yesod-websockets: add webSocketsOptions webSocketsOptionsWith 2017-01-19 11:13:34 +00:00
Michael Snoyman
6d03e6beed Merge pull request #1336 from LightAndLight/master
Fixed misspelling of 'utilities'
2017-01-15 11:06:03 +02:00
Isaac Elliott
2d98587cce Fixed misspelling of 'utilities' 2017-01-15 18:34:16 +10:00
Michael Snoyman
33f58e2bd1 Allow vector 0.12 2017-01-09 00:10:45 +02:00
Michael Snoyman
42e0e9aab9 Merge pull request #1329 from ocharles/master
yesod-core: Build with template-haskell-2.12.0
2017-01-04 16:18:13 +02:00
Ollie Charles
57a9420d99 yesod-core: Build with template-haskell-2.12.0 2017-01-04 13:08:09 +00:00
Michael Snoyman
5469e38d43 Travis: switch language to generic 2017-01-03 20:46:34 +02:00
Michael Snoyman
38fec7f8b8 Reduce dependencies 2017-01-03 12:45:11 +02:00
Michael Snoyman
6d3c530ab8 Merge pull request #1326 from psibi/verkey
Change the type signature from Text to Verkey
2017-01-02 09:40:56 +02:00
Sibi Prabakaran
4330461033
Change the type signature from Text to Verkey
Since the other type signatures of the typeclass has VerKey instead of
Text, it would be better to use VerKey here also to maintain
consistency. Also, IMO this signature is more easy to follow ( I had to
look at source to see how the verification key was generated. )
2016-12-30 18:06:40 +05:30
Michael Snoyman
705b52f7eb Add --host option to yesod devel (fixes #975) 2016-12-21 14:27:52 +02:00
Michael Snoyman
9dbbe030de Fix compatibility with older persistent #1324 2016-12-20 15:21:36 +02:00
Michael Snoyman
8d85ad1ab5 Merge pull request #1321 from whittle/insert400
Add insert400 and insert400_
2016-12-12 19:06:54 +02:00
Jason Whittle
2e284bbe64 Add notes to changelog for yesod-persistent 1.4.1.0. 2016-12-12 08:23:16 -05:00
Jason Whittle
d526658c7b Implement the changes requested by @snoyberg. 2016-12-12 08:18:25 -05:00
Jason Whittle
c573f02d9d Add insert400 and insert400_, which check for violated uniqueness constraints. 2016-12-12 01:54:27 -05:00
Michael Snoyman
b9ece6f242 Merge pull request #1319 from psibi/auth-json-doc
Add documentation for JSON endpoints for Yesod.Auth.Email module
2016-12-08 07:00:08 -05:00
Sibi Prabakaran
08f994103a
Add documentation for JSON endpoints for Yesod.Auth.Email module 2016-12-08 14:25:08 +05:30
Michael Snoyman
9a484f9163 defaultMessageWidget 2016-12-07 20:08:47 -05:00
Michael Snoyman
706a995b67 blaze-markup in extra-deps 2016-12-07 09:42:26 -05:00
Michael Snoyman
98854b4de3 Version bump for #1317 2016-12-07 09:23:53 -05:00
Michael Snoyman
d7be78f82e Merge pull request #1317 from psibi/json-auth
JSON endpoints for Auth.Email, haddock, and i18n fix
2016-12-07 09:23:00 -05:00
Michael Snoyman
03c1ee4807 Compilation fix for GHC 7.8 2016-12-07 08:54:53 -05:00
Michael Snoyman
ae7dfd2408 Changelog for #1310 2016-12-07 08:52:55 -05:00
Michael Snoyman
f54b924137 Merge pull request #1318 from s9gf4ult/master
Exports some internals and fix version bounds
2016-12-07 08:50:52 -05:00
Aleksey Uimanov
80f0b3cd70 Add comments and bump minor version to 1.4.29 2016-12-07 14:04:51 +05:00
Sibi Prabakaran
60f66b4c3a
Add relevant changelog 2016-12-07 14:09:01 +05:30
Sibi Prabakaran
8f8c99db88
Do parseJsonBody only when form data is not found 2016-12-07 14:08:37 +05:30
Sibi Prabakaran
0255f93c22
Export croatianMessage 2016-12-06 18:44:46 +05:30
Sibi Prabakaran
47b2877c79
More Haddock fixes 2016-12-06 18:44:38 +05:30
Sibi Prabakaran
75df4e0468
Use @since for proper haddock rendering 2016-12-06 18:21:36 +05:30
Sibi Prabakaran
83575e92a0
Fix typo: /s/interoprate/interoperate 2016-12-06 18:20:18 +05:30
Sibi Prabakaran
85bd15d109
Add json support for postPasswordR 2016-12-06 18:17:19 +05:30
Sibi Prabakaran
b6cd72f49f
Implement Login via JSON endpoint
Add additional handling of JSON endpoint in addition to the HTML form
method.
2016-12-06 15:20:51 +05:30
Aleksey Uimanov
47ef36012d export getGetMaxExpires 2016-12-05 19:33:04 +05:00
Sibi Prabakaran
19840cdc89
Add json support for postRegisterR 2016-12-05 19:32:23 +05:30
Aleksey Uimanov
2bd3a936c9 fix blaze-markup version bounds 2016-12-02 16:26:19 +05:00
Aleksey Uimanov
d1697a3fde export toWaiAppYre 2016-12-02 15:55:09 +05:00
Michael Snoyman
9c38a4b08e Version bump for #1314 2016-12-01 06:53:41 +02:00
Jason Whittle
7b12f61a91 yesod-test: add getLocation test helper. 2016-11-30 18:05:48 -05:00
Michael Snoyman
fbdaa2f675 Add since lines 2016-11-30 19:36:29 +02:00
Michael Snoyman
312adc40d5 Version bump for #1310 2016-11-30 13:40:46 +02:00
Michael Snoyman
51a5641435 Merge pull request #1310 from yesodweb/text_toWidget
Add ToWidget instances for strict text, lazy text, and text builder
2016-11-30 13:39:59 +02:00
Michael Snoyman
a337bf6d58 websockets 0.10 support 2016-11-30 06:42:15 +02:00
Michael Snoyman
58407c292e Fix a build failure 2016-11-29 13:51:02 +02:00
Michael Snoyman
2c4e19e0b6 Version bump for #1309 2016-11-29 13:48:42 +02:00
Michael Snoyman
00cf852216 Version bump for #1308 2016-11-29 13:47:33 +02:00
Michael Snoyman
a921d6cb31 Add caveat about possible Docker integration issues 2016-11-29 13:44:46 +02:00
Michael Snoyman
784f04ae7a Merge branch '1304-stack-based-devel' 2016-11-29 13:43:01 +02:00
Michael Snoyman
3883063ec2 Devel server indicates when recompilation is occurring
Pinging @amitaibu
2016-11-28 09:58:48 +02:00
Andrew Martin
1781699cab Add ToWidget instances for strict text, lazy text, and text builder 2016-11-27 15:27:54 -05:00
Michael Snoyman
ed87ded970 Merge pull request #1308 from sbditto85/default_attrs_julius
added jsAttributes for the script tag generated by julius files
2016-11-27 05:45:48 +02:00
Michael Snoyman
3159745ee8 Merge pull request #1309 from filipg/master
remove invalid Google OpenID link
2016-11-27 05:44:26 +02:00
Casey Allred
9458e57a58 adjusted to use *{..} syntax 2016-11-26 12:07:49 -07:00
Filip Gralinski
a3929aa9bb remove invalid Google OpenID link 2016-11-26 19:39:24 +01:00
Michael Snoyman
b1f1e4e222 Revert to runghc 2016-11-26 17:57:32 +02:00
Casey Allred
cec6f42a99 added jsAttributes for the script tag generated by julius files 2016-11-25 21:36:51 -07:00
Michael Snoyman
f3fc735a25 README.md, and some minor code cleanups 2016-11-24 07:51:54 +02:00
Michael Snoyman
ab4d6540ca Workaround for compatibility with older fsnotify 2016-11-23 15:59:58 +02:00
Michael Snoyman
6048a2c9bf Got my logic backwards :( 2016-11-23 15:34:14 +02:00
Michael Snoyman
db3beff4f3 Some CPP for Cabal API changes 2016-11-23 15:19:27 +02:00
Michael Snoyman
03307a8cc8 Fix dependency problems for older snapshots 2016-11-23 15:05:41 +02:00
Michael Snoyman
83d3a12a23 Rewrite yesod devel based on Stack #1304
Please see ChangeLog for explanation.
2016-11-23 13:59:56 +02:00
Maximilian Tagher
54cc4205d8 Merge pull request #1302 from psibi/csrf-fix
yesod-auth: Fix CSRF security vulnerability in registerHelper function
2016-11-22 10:49:26 -08:00
Sibi Prabakaran
696faa3fd0
req is not needed. 2016-11-20 13:43:01 +05:30
Sibi Prabakaran
10850f5cee
Use checkCsrfHeaderOrParam instead of manual check 2016-11-20 13:32:15 +05:30
Sibi Prabakaran
7f17d829b3
Fix CSRF security vulnerability in registerHelper function
Return a 403 status code if the csrf tokens are matched. This currently
affects two endpoints: During registration and during password reset
forms.

This curl request demonstrates how this can be exploited to register new
email:

curl -i --header "Accept: application/json" --request POST -F
"email=sibi@psibi.in" http://localhost:3005/auth/page/email/register

With the patch applied, it will respond with this:

{"message":"Permission Denied. A valid CSRF token wasn't present in HTTP
headers or POST parameters. Because the request could have been forged,
it's been rejected altogether. Check the Yesod.Core.Handler docs of the
yesod-core package for details on CSRF protection."}
2016-11-20 03:59:32 +05:30
Michael Snoyman
10a751cdbc Version bump for #1296 2016-11-14 07:04:36 +02:00
Andrew Martin
2d6e5cea02 Added a ToValue instance for Enctype 2016-11-13 14:59:21 -05:00
Michael Snoyman
79aefc694a Make guessApproot the default (for yesod-core1.5) 2015-10-13 10:59:01 +00:00
Maximilian Tagher
ce74e23d87 timeField now uses type="time"
* Also removes deprecation from `timeField`
* Also mildly discourages using `timeFieldTypeText`
2014-11-28 15:57:01 -05:00
208 changed files with 10130 additions and 5071 deletions

29
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,29 @@
<!---
### Bug Reports
Reporting a bug? If relevant, we recommend including:
* Your OS name and version
* The versions of tools you're using (e.g. `stack`, `yesod` `ghc`).
* The versions of dependencies you're using
For your convenience, we recommend pasting this script into bash and uploading the output [as a gist](https://gist.github.com/).
```
command -v sw_vers && sw_vers # OS X only
command -v uname && uname -a # Kernel version
command -v stack && stack --version
command -v stack && stack ghc -- --version
command -v stack && stack ls dependencies
command -v yesod && yesod version
```
* Also, is there anything custom or unusual about your setup? i.e. new or prerelease versions of GHC, stack, etc.
* Finally, if possible, please reproduce the error in a small script, or if necessary create a new Github repo with the smallest possible reproducing case. [Stack's scripting support](https://docs.haskellstack.org/en/stable/GUIDE/#script-interpreter) might be useful for creating your reproduction example.
### Support
Please direct support questions to [Stack Overflow](https://stackoverflow.com/questions/tagged/yesod+haskell) or the [Yesod Google Group](https://groups.google.com/forum/#!forum/yesodweb). If you don't get a response there, or you suspect there may be a bug in Yesod causing your problem, you're welcome to ask here.
-->

14
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,14 @@
Before submitting your PR, check that you've:
- [ ] Bumped the version number
- [ ] Documented new APIs with [Haddock markup](https://www.haskell.org/haddock/doc/html/index.html)
- [ ] Added [`@since` declarations](http://haskell-haddock.readthedocs.io/en/latest/markup.html#since) to the Haddocks for new, public APIs
After submitting your PR:
- [ ] Update the Changelog.md file with a link to your PR
- [ ] Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts)
<!---Thanks so much for contributing! :)
_If these checkboxes don't apply to your PR, you can delete them_-->

56
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Tests
on:
pull_request:
push:
branches:
- master
jobs:
build:
name: CI
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
args:
#- "--resolver nightly"
- "--resolver nightly-2022-02-11"
- "--resolver lts-18"
- "--resolver lts-16"
- "--resolver lts-14"
- "--resolver lts-12"
- "--resolver lts-11"
# Bugs in GHC make it crash too often to be worth running
exclude:
- os: windows-latest
args: "--resolver nightly"
- os: macos-latest
args: "--resolver lts-16"
- os: macos-latest
args: "--resolver lts-14"
- os: macos-latest
args: "--resolver lts-12"
- os: macos-latest
args: "--resolver lts-11"
steps:
- name: Clone project
uses: actions/checkout@v2
# Getting weird OS X errors...
# - name: Cache dependencies
# uses: actions/cache@v1
# with:
# path: ~/.stack
# key: ${{ runner.os }}-${{ matrix.resolver }}-${{ hashFiles('stack.yaml') }}
# restore-keys: |
# ${{ runner.os }}-${{ matrix.resolver }}-
- name: Build and run tests
shell: bash
run: |
set -ex
stack --version
stack test --fast --no-terminal ${{ matrix.args }}

6
.gitignore vendored
View File

@ -4,6 +4,7 @@
*.hi *.hi
dist/ dist/
dist-stack/ dist-stack/
stack.yaml.lock
.stack-work .stack-work
*.swp *.swp
client_session_key.aes client_session_key.aes
@ -21,3 +22,8 @@ tarballs/
.ghc .ghc
.stackage .stackage
.bash_history .bash_history
# OS X
.DS_Store
*.yaml.lock
dist-newstyle/

View File

@ -1,200 +0,0 @@
# This is the complex Travis configuration, which is intended for use
# on open source libraries which need compatibility across multiple GHC
# versions, must work with cabal-install, and should be
# cross-platform. For more information and other options, see:
#
# https://docs.haskellstack.org/en/stable/travis_ci/
#
# Copy these contents into the root directory of your Github project in a file
# named .travis.yml
# Use new container infrastructure to enable caching
sudo: false
# Choose a lightweight base image; we provide our own build tools.
language: c
# Caching so the next build will be fast too.
cache:
directories:
- $HOME/.ghc
- $HOME/.cabal
- $HOME/.stack
# The different configurations we want to test. We have BUILD=cabal which uses
# cabal-install, and BUILD=stack which uses Stack. More documentation on each
# of those below.
#
# We set the compiler values here to tell Travis to use a different
# cache file per set of arguments.
#
# If you need to have different apt packages for each combination in the
# matrix, you can use a line such as:
# addons: {apt: {packages: [libfcgi-dev,libgmp-dev]}}
matrix:
include:
# We grab the appropriate GHC and cabal-install versions from hvr's PPA. See:
# https://github.com/hvr/multi-ghc-travis
#- env: BUILD=cabal GHCVER=7.0.4 CABALVER=1.16 HAPPYVER=1.19.5 ALEXVER=3.1.7
# compiler: ": #GHC 7.0.4"
# addons: {apt: {packages: [cabal-install-1.16,ghc-7.0.4,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
#- env: BUILD=cabal GHCVER=7.2.2 CABALVER=1.16 HAPPYVER=1.19.5 ALEXVER=3.1.7
# compiler: ": #GHC 7.2.2"
# addons: {apt: {packages: [cabal-install-1.16,ghc-7.2.2,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
#- env: BUILD=cabal GHCVER=7.4.2 CABALVER=1.16 HAPPYVER=1.19.5 ALEXVER=3.1.7
# compiler: ": #GHC 7.4.2"
# addons: {apt: {packages: [cabal-install-1.16,ghc-7.4.2,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
#- env: BUILD=cabal GHCVER=7.6.3 CABALVER=1.16 HAPPYVER=1.19.5 ALEXVER=3.1.7
# compiler: ": #GHC 7.6.3"
# addons: {apt: {packages: [cabal-install-1.16,ghc-7.6.3,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
- env: BUILD=cabal GHCVER=7.8.4 CABALVER=1.18 HAPPYVER=1.19.5 ALEXVER=3.1.7
compiler: ": #GHC 7.8.4"
addons: {apt: {packages: [cabal-install-1.18,ghc-7.8.4,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
- env: BUILD=cabal GHCVER=7.10.3 CABALVER=1.22 HAPPYVER=1.19.5 ALEXVER=3.1.7
compiler: ": #GHC 7.10.3"
addons: {apt: {packages: [cabal-install-1.22,ghc-7.10.3,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
- env: BUILD=cabal GHCVER=8.0.1 CABALVER=1.24 HAPPYVER=1.19.5 ALEXVER=3.1.7
compiler: ": #GHC 8.0.1"
addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.1,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
# Build with the newest GHC and cabal-install. This is an accepted failure,
# see below.
- env: BUILD=cabal GHCVER=head CABALVER=head HAPPYVER=1.19.5 ALEXVER=3.1.7
compiler: ": #GHC HEAD"
addons: {apt: {packages: [cabal-install-head,ghc-head,happy-1.19.5,alex-3.1.7], sources: [hvr-ghc]}}
# The Stack builds. We can pass in arbitrary Stack arguments via the ARGS
# variable, such as using --stack-yaml to point to a different file.
- env: BUILD=stack ARGS=""
compiler: ": #stack default"
addons: {apt: {packages: [libgmp-dev]}}
- env: BUILD=stack ARGS="--resolver lts-2"
compiler: ": #stack 7.8.4"
addons: {apt: {packages: [libgmp-dev]}}
- env: BUILD=stack ARGS="--resolver lts-3"
compiler: ": #stack 7.10.2"
addons: {apt: {packages: [libgmp-dev]}}
- env: BUILD=stack ARGS="--resolver lts-6"
compiler: ": #stack 7.10.3"
addons: {apt: {packages: [libgmp-dev]}}
- env: BUILD=stack ARGS="--resolver lts-7"
compiler: ": #stack 8.0.1"
addons: {apt: {packages: [libgmp-dev]}}
# Nightly builds are allowed to fail
- env: BUILD=stack ARGS="--resolver nightly"
compiler: ": #stack nightly"
addons: {apt: {packages: [libgmp-dev]}}
# Build on OS X in addition to Linux
- env: BUILD=stack ARGS=""
compiler: ": #stack default osx"
os: osx
# Travis includes an OS X which is incompatible with GHC 7.8.4
#- env: BUILD=stack ARGS="--resolver lts-2"
# compiler: ": #stack 7.8.4 osx"
# os: osx
- env: BUILD=stack ARGS="--resolver lts-3"
compiler: ": #stack 7.10.2 osx"
os: osx
- env: BUILD=stack ARGS="--resolver lts-6"
compiler: ": #stack 7.10.3 osx"
os: osx
- env: BUILD=stack ARGS="--resolver lts-7"
compiler: ": #stack 8.0.1 osx"
os: osx
- env: BUILD=stack ARGS="--resolver nightly"
compiler: ": #stack nightly osx"
os: osx
allow_failures:
- env: BUILD=cabal GHCVER=head CABALVER=head HAPPYVER=1.19.5 ALEXVER=3.1.7
- env: BUILD=stack ARGS="--resolver nightly"
before_install:
# Using compiler above sets CC to an invalid value, so unset it
- unset CC
# We want to always allow newer versions of packages when building on GHC HEAD
- CABALARGS=""
- if [ "x$GHCVER" = "xhead" ]; then CABALARGS=--allow-newer; fi
# Download and unpack the stack executable
- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$HOME/.local/bin:/opt/alex/$ALEXVER/bin:/opt/happy/$HAPPYVER/bin:$HOME/.cabal/bin:$PATH
- mkdir -p ~/.local/bin
- |
if [ `uname` = "Darwin" ]
then
travis_retry curl --insecure -L https://www.stackage.org/stack/osx-x86_64 | tar xz --strip-components=1 --include '*/stack' -C ~/.local/bin
else
travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
fi
# Use the more reliable S3 mirror of Hackage
mkdir -p $HOME/.cabal
echo 'remote-repo: hackage.haskell.org:http://hackage.fpcomplete.com/' > $HOME/.cabal/config
echo 'remote-repo-cache: $HOME/.cabal/packages' >> $HOME/.cabal/config
if [ "$CABALVER" != "1.16" ]
then
echo 'jobs: $ncpus' >> $HOME/.cabal/config
fi
install:
- echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]"
- if [ -f configure.ac ]; then autoreconf -i; fi
- |
set -ex
case "$BUILD" in
stack)
stack --no-terminal --install-ghc $ARGS test --bench --only-dependencies
;;
cabal)
cabal --version
travis_retry cabal update
# Get the list of packages from the stack.yaml file
PACKAGES=$(stack --install-ghc query locals | grep '^ *path' | sed 's@^ *path:@@')
cabal install --only-dependencies --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES
;;
esac
set +ex
script:
- |
set -ex
case "$BUILD" in
stack)
stack --no-terminal $ARGS test --bench --no-run-benchmarks --haddock --no-haddock-deps
;;
cabal)
cabal install --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES
ORIGDIR=$(pwd)
for dir in $PACKAGES
do
cd $dir
cabal check || [ "$CABALVER" == "1.16" ]
cabal sdist
PKGVER=$(cabal info . | awk '{print $2;exit}')
SRC_TGZ=$PKGVER.tar.gz
cd dist
tar zxfv "$SRC_TGZ"
cd "$PKGVER"
cabal configure --enable-tests
cabal build
cd $ORIGDIR
done
;;
esac
set +ex

View File

@ -1,13 +1,74 @@
# Contributor Code of Conduct # Contributor Covenant Code of Conduct
Always be nice. ## Our Pledge
When communicating online treat people the way you would if In the interest of fostering an open and welcoming environment, we as
they were standing next to you. contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
Don't forget to be nice whenever representing the ## Our Standards
project to others outside the project.
If you are not nice, apologize. Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at `michael at snoyman dot com`. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
If someone is not being nice, tell them in a respectful way or tell a project maintainer: we care about fostering a welcoming community.

95
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,95 @@
# Contributing
Thanks for your interest in contributing to Yesod! This file has some tips for developing Yesod and getting a pull request accepted.
## Development
Yesod is a mega-repo that contains many Haskell packages, each in a different directory. All the subprojects can be developed with Stack, using `stack <command> <subproject>`, e.g.
* `stack build yesod-form`
* `stack test yesod-auth`
* `stack haddock yesod-websockets`
If you'd like to test your changes in a full-fledged Yesod app, you can use Stack to build against it, e.g.:
```
packages:
- '/path/to/this/repo/yesod-auth'
```
## Coding Guidelines
### Safety
Avoid partial functions. Even if you know the partial function is safe in your instance, partial functions require more reasoning from the programmer and are not resilient to refactoring. For the rare cases where a partial function is appropriate, a custom `error` should be used.
### Style
Keep coding style consistent with the rest of the file, but don't worry about style too much otherwise. PRs changing code style are viewed skeptically.
### Dependencies
Avoid adding unnecessary dependencies. If a dependency provides only a minor convenience for your implementation, it's probably better to skip it.
If you do add a new dependency, try to support a wide range of versions of it.
### Backwards Compatibility
Backwards incompatible changes are viewed skeptically—best to ask in an issue to see if a particular backwards incompatible change would be approved. If possible keep backwards compatibility by adding new APIs and deprecating old ones.
Keep backwards compatibility with old versions of dependencies when possible.
## PR Guidelines
### PR Scope
As much as possible, keep separate changes in separate PRs.
### Testing
Tests are recommended, but not required.
### Documentation
All public APIs must be documented. Documenting private functions is optional, but may be nice depending on their complexity. Example documentation:
```
-- | Looks up the hidden input named "_token" and adds its value to the params.
--
-- ==== __Examples__
--
-- > request $ do
-- > addToken_ "#formID"
--
-- @since 1.5.4
addToken_ :: Query -- ^ CSS selector that resolves to the @<form>@ containing the token.
-> RequestBuilder site ()
```
Examples are recommended, but not required, in documentation. Marking new APIs with `@since <version number>` is required.
### Versioning
Yesod packages roughly follow the Haskell Package Versioning Policy style of A.B.C.[D] (MAJOR.MAJOR.MINOR.[PATCH])
* A - Used for massive changes in the library. (Example: 1.2.3.4 becomes 2.0.0)
* B - Used for smaller breaking changes, like removing, renaming, or changing behavior of existing public API. (Example: 1.2.3.4 becomes 1.3.0)
* C - Used for new public APIs (Example: 1.2.3.4 becomes 1.2.4)
* D - Used for bug fixes (Example: 1.2.3.4 becomes 1.2.3.5).
* D is optional in the version number, so 2.0.0 is a valid version.
Documentation changes don't require a new version.
If you feel there is ambiguity to a change (e.g. fixing a bug in a function, when people may be relying on the old broken behavior), you can ask in an issue or pull request.
Unlike in the Package Versioning Policy, deprecations are not counted as MAJOR changes.
In some cases, dropping compatibility with a major version of a dependency (e.g. changing from transformers >= 0.3 to transformers >= 0.4), is considered a breaking change.
### Changelog
After you submit a PR, update the subproject's Changelog.md file with the new version number and a link to your PR. If your PR does not need to bump the version number, include the change in an "Unreleased" section at the top.
### Releases
Releases should be done as soon as possible after a pull request is merged—don't be shy about reminding us to make a release if we forget.

View File

@ -1,4 +1,4 @@
Copyright (c) 2012 Michael Snoyman, http://www.yesodweb.com/ Copyright (c) 2012-2017 Michael Snoyman, http://www.yesodweb.com/
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

15
README
View File

@ -1,15 +0,0 @@
Authentication methods for Haskell web applications.
Note for Rpxnow:
By default on some (all?) installs wget does not come with root certificates
for SSL. If this is the case then Web.Authenticate.Rpxnow.authenticate will
fail as wget cannot establish a secure connection to rpxnow's servers.
A simple *nix solution, if potentially insecure (man in the middle attacks as
you are downloading the certs) is to grab a copy of the certs extracted from
those that come with firefox, hosted by CURL at
http://curl.haxx.se/ca/cacert.pem , put them somewhere (for ex,
~/.wget/cacert.pem) and then edit your ~/.wgetrc to include:
ca_certificate=~/.wget/cacert.pem
This should fix the problem.

View File

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/yesodweb/yesod.svg?branch=master)](https://travis-ci.org/yesodweb/yesod) ![Tests](https://github.com/yesodweb/yesod/workflows/Tests/badge.svg)
# Yesod Web Framework # Yesod Web Framework
@ -12,20 +12,50 @@ An advanced web framework using the Haskell programming language. Featuring:
* asynchronous IO * asynchronous IO
* this is built in to the Haskell programming language (like Erlang) * this is built in to the Haskell programming language (like Erlang)
## Getting Started
Learn more about Yesod on [its main website](http://www.yesodweb.com/). If you Learn more about Yesod on [its main website](http://www.yesodweb.com/). If you
want to get started using Yesod, we strongly recommend the [quick start want to get started using Yesod, we strongly recommend the [quick start
guide](http://www.yesodweb.com/page/quickstart), based on [the Haskell build guide](http://www.yesodweb.com/page/quickstart), based on [the Haskell build
tool stack](https://github.com/commercialhaskell/stack#readme). tool stack](https://github.com/commercialhaskell/stack#readme).
Here's a minimal example!
```haskell
{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell, TypeFamilies #-}
import Yesod
data App = App -- Put your config, database connection pool, etc. in here.
-- Derive routes and instances for App.
mkYesod "App" [parseRoutes|
/ HomeR GET
|]
instance Yesod App -- Methods in here can be overridden as needed.
-- The handler for the GET request at /, corresponds to HomeR.
getHomeR :: Handler Html
getHomeR = defaultLayout [whamlet|Hello World!|]
main :: IO ()
main = warp 3000 App
```
To read about each of the concepts in use above (routing, handlers,
linking, JSON), in detail, visit
[Basics in the Yesod book](https://www.yesodweb.com/book/basics#basics_routing).
## Hacking on Yesod ## Hacking on Yesod
Yesod consists mostly of four repositories: Yesod consists mostly of four repositories:
```bash ```bash
git clone --recursive http://github.com/yesodweb/shakespeare git clone --recurse-submodules http://github.com/yesodweb/shakespeare
git clone --recursive http://github.com/yesodweb/persistent git clone --recurse-submodules http://github.com/yesodweb/persistent
git clone --recursive http://github.com/yesodweb/wai git clone --recurse-submodules http://github.com/yesodweb/wai
git clone --recursive http://github.com/yesodweb/yesod git clone --recurse-submodules http://github.com/yesodweb/yesod
``` ```
Each repository can be built with `stack build`. Each repository can be built with `stack build`.

View File

@ -1,5 +0,0 @@
Release notes are maintained on the wiki.
https://github.com/yesodweb/yesod/wiki/Changelog (high level features)
https://github.com/yesodweb/yesod/wiki/Detailed-change-list (see for breaking changes)

15
cabal.project Normal file
View File

@ -0,0 +1,15 @@
packages:
yesod-core
yesod-static
yesod-persistent
yesod-newsfeed
yesod-form
yesod-form-multi
yesod-auth
yesod-auth-oauth
yesod-sitemap
yesod-test
yesod-bin
yesod
yesod-eventsource
yesod-websockets

View File

@ -1,4 +1,3 @@
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
@ -15,7 +14,6 @@ import Data.Yaml
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy.Encoding as LTE import qualified Data.Text.Lazy.Encoding as LTE
import Data.Typeable (Typeable)
import Database.Persist.Sqlite import Database.Persist.Sqlite
import Database.Persist.TH import Database.Persist.TH
import Network.Mail.Mime import Network.Mail.Mime
@ -37,7 +35,6 @@ User
verkey Text Maybe -- Used for resetting passwords verkey Text Maybe -- Used for resetting passwords
verified Bool verified Bool
UniqueUser email UniqueUser email
deriving Typeable
|] |]
data App = App data App = App

View File

@ -21,7 +21,7 @@ data Wiki = Wiki
} }
-- | A typeclass that all master sites that want a Wiki must implement. A -- | A typeclass that all master sites that want a Wiki must implement. A
-- master must be able to render form messages, as we use yesod-forms for -- master must be able to render form messages, as we use yesod-form for
-- processing user input. -- processing user input.
class (RenderMessage master FormMessage, Yesod master) => YesodWiki master where class (RenderMessage master FormMessage, Yesod master) => YesodWiki master where
-- | Write protection. By default, no protection. -- | Write protection. By default, no protection.

View File

@ -1,13 +0,0 @@
./yesod-core
./yesod-static
./yesod-persistent
./yesod-newsfeed
./yesod-form
./yesod-auth
./yesod-auth-oauth
./yesod-sitemap
./yesod-test
./yesod-bin
./yesod
./yesod-eventsource
./yesod-websockets

View File

@ -1,10 +1,11 @@
resolver: lts-6.23 resolver: lts-18.3
packages: packages:
- ./yesod-core - ./yesod-core
- ./yesod-static - ./yesod-static
- ./yesod-persistent - ./yesod-persistent
- ./yesod-newsfeed - ./yesod-newsfeed
- ./yesod-form - ./yesod-form
- ./yesod-form-multi
- ./yesod-auth - ./yesod-auth
- ./yesod-auth-oauth - ./yesod-auth-oauth
- ./yesod-sitemap - ./yesod-sitemap
@ -14,15 +15,5 @@ packages:
- ./yesod-eventsource - ./yesod-eventsource
- ./yesod-websockets - ./yesod-websockets
# Needed for LTS 2
extra-deps: extra-deps:
- wai-app-static-3.1.4.1 - attoparsec-aeson-2.1.0.0
- http-api-data-0.2
- yaml-0.8.17
- nonce-1.0.2
- persistent-2.5
- persistent-sqlite-2.5
- cookie-0.4.2
- conduit-extra-1.1.14
- streaming-commons-0.1.16

19
stack.yaml.lock Normal file
View File

@ -0,0 +1,19 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files
packages:
- completed:
hackage: attoparsec-aeson-2.1.0.0@sha256:fa83aba43bfa58490de8f274d19b9d58b6403a207b12cac5f93922102b084c52,1154
pantry-tree:
sha256: 294c3a8a19a7ddad58097e18c624c6b34894b3c4a4cc490759cb31d842db242a
size: 114
original:
hackage: attoparsec-aeson-2.1.0.0
snapshots:
- completed:
sha256: 694573e96dca34db5636edb1fe6c96bb233ca0f9fb96c1ead1671cdfa9bd73e9
size: 585603
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/3.yaml
original: lts-18.3

View File

@ -1,3 +1,29 @@
# ChangeLog for yesod-auth-oauth
## 1.6.1
* Allow newer GHC
## 1.6.0.3
* Allow yesod-form 1.7
## 1.6.0.2
* Remove unnecessary deriving of Typeable
## 1.6.0.1
* Compile with GHC 8.6 [#1561](https://github.com/yesodweb/yesod/pull/1561)
## 1.6.0
* Upgrade to yesod-core 1.6.0
## 1.4.2
* Fix warnings
## 1.4.1 ## 1.4.1
* change OAuth Twitter ID, screen_name → user_id [#1168](https://github.com/yesodweb/yesod/pull/1168) * change OAuth Twitter ID, screen_name → user_id [#1168](https://github.com/yesodweb/yesod/pull/1168)

View File

@ -1,5 +1,9 @@
{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, QuasiQuotes #-} {-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Yesod.Auth.OAuth module Yesod.Auth.OAuth
( authOAuth ( authOAuth
, oauthUrl , oauthUrl
@ -10,9 +14,9 @@ module Yesod.Auth.OAuth
, tumblrUrl , tumblrUrl
, module Web.Authenticate.OAuth , module Web.Authenticate.OAuth
) where ) where
import Control.Applicative ((<$>), (<*>)) import Control.Applicative as A ((<$>), (<*>))
import Control.Arrow ((***)) import Control.Arrow ((***))
import Control.Exception.Lifted import UnliftIO.Exception
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Maybe import Data.Maybe
@ -20,7 +24,6 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Typeable
import Web.Authenticate.OAuth import Web.Authenticate.OAuth
import Yesod.Auth import Yesod.Auth
import Yesod.Form import Yesod.Form
@ -28,34 +31,42 @@ import Yesod.Core
data YesodOAuthException = CredentialError String Credential data YesodOAuthException = CredentialError String Credential
| SessionError String | SessionError String
deriving (Show, Typeable) deriving Show
instance Exception YesodOAuthException instance Exception YesodOAuthException
oauthUrl :: Text -> AuthRoute oauthUrl :: Text -> AuthRoute
oauthUrl name = PluginR name ["forward"] oauthUrl name = PluginR name ["forward"]
authOAuth :: YesodAuth m authOAuth :: forall master. YesodAuth master
=> OAuth -- ^ 'OAuth' data-type for signing. => OAuth -- ^ 'OAuth' data-type for signing.
-> (Credential -> IO (Creds m)) -- ^ How to extract ident. -> (Credential -> IO (Creds master)) -- ^ How to extract ident.
-> AuthPlugin m -> AuthPlugin master
authOAuth oauth mkCreds = AuthPlugin name dispatch login authOAuth oauth mkCreds = AuthPlugin name dispatch login
where where
name = T.pack $ oauthServerName oauth name = T.pack $ oauthServerName oauth
url = PluginR name [] url = PluginR name []
lookupTokenSecret = bsToText . fromMaybe "" . lookup "oauth_token_secret" . unCredential lookupTokenSecret = bsToText . fromMaybe "" . lookup "oauth_token_secret" . unCredential
oauthSessionName :: Text
oauthSessionName = "__oauth_token_secret" oauthSessionName = "__oauth_token_secret"
dispatch
:: Text
-> [Text]
-> AuthHandler master TypedContent
dispatch "GET" ["forward"] = do dispatch "GET" ["forward"] = do
render <- lift getUrlRender render <- getUrlRender
tm <- getRouteToParent tm <- getRouteToParent
let oauth' = oauth { oauthCallback = Just $ encodeUtf8 $ render $ tm url } let oauth' = oauth { oauthCallback = Just $ encodeUtf8 $ render $ tm url }
master <- lift getYesod manager <- authHttpManager
tok <- lift $ getTemporaryCredential oauth' (authHttpManager master) tok <- getTemporaryCredential oauth' manager
setSession oauthSessionName $ lookupTokenSecret tok setSession oauthSessionName $ lookupTokenSecret tok
redirect $ authorizeUrl oauth' tok redirect $ authorizeUrl oauth' tok
dispatch "GET" [] = lift $ do dispatch "GET" [] = do
Just tokSec <- lookupSession oauthSessionName tokSec <- lookupSession oauthSessionName >>= \case
Just t -> return t
Nothing -> liftIO $ fail "lookupSession could not find session"
deleteSession oauthSessionName deleteSession oauthSessionName
reqTok <- reqTok <-
if oauthVersion oauth == OAuth10 if oauthVersion oauth == OAuth10
@ -66,14 +77,14 @@ authOAuth oauth mkCreds = AuthPlugin name dispatch login
] ]
else do else do
(verifier, oaTok) <- (verifier, oaTok) <-
runInputGet $ (,) <$> ireq textField "oauth_verifier" runInputGet $ (,) A.<$> ireq textField "oauth_verifier"
<*> ireq textField "oauth_token" A.<*> ireq textField "oauth_token"
return $ Credential [ ("oauth_verifier", encodeUtf8 verifier) return $ Credential [ ("oauth_verifier", encodeUtf8 verifier)
, ("oauth_token", encodeUtf8 oaTok) , ("oauth_token", encodeUtf8 oaTok)
, ("oauth_token_secret", encodeUtf8 tokSec) , ("oauth_token_secret", encodeUtf8 tokSec)
] ]
master <- getYesod manager <- authHttpManager
accTok <- getAccessToken oauth reqTok (authHttpManager master) accTok <- getAccessToken oauth reqTok manager
creds <- liftIO $ mkCreds accTok creds <- liftIO $ mkCreds accTok
setCredsRedirect creds setCredsRedirect creds
dispatch _ _ = notFound dispatch _ _ = notFound
@ -83,7 +94,7 @@ authOAuth oauth mkCreds = AuthPlugin name dispatch login
let oaUrl = render $ tm $ oauthUrl name let oaUrl = render $ tm $ oauthUrl name
[whamlet| <a href=#{oaUrl}>Login via #{name} |] [whamlet| <a href=#{oaUrl}>Login via #{name} |]
mkExtractCreds :: YesodAuth m => Text -> String -> Credential -> IO (Creds m) mkExtractCreds :: Text -> String -> Credential -> IO (Creds m)
mkExtractCreds name idName (Credential dic) = do mkExtractCreds name idName (Credential dic) = do
let mcrId = decodeUtf8With lenientDecode <$> lookup (encodeUtf8 $ T.pack idName) dic let mcrId = decodeUtf8With lenientDecode <$> lookup (encodeUtf8 $ T.pack idName) dic
case mcrId of case mcrId of
@ -113,7 +124,7 @@ authTwitter :: YesodAuth m
-> ByteString -- ^ Consumer Secret -> ByteString -- ^ Consumer Secret
-> AuthPlugin m -> AuthPlugin m
authTwitter key secret = authTwitter' key secret "screen_name" authTwitter key secret = authTwitter' key secret "screen_name"
{-# DEPRECATED authTwitter "Use authTwitterUsingUserID instead" #-} {-# DEPRECATED authTwitter "Use authTwitterUsingUserId instead" #-}
-- | Twitter plugin which uses Twitter's /user_id/ as ID. -- | Twitter plugin which uses Twitter's /user_id/ as ID.
-- --

View File

@ -1,5 +1,6 @@
cabal-version: >= 1.10
name: yesod-auth-oauth name: yesod-auth-oauth
version: 1.4.1.1 version: 1.6.1
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Hiromi Ishii author: Hiromi Ishii
@ -7,28 +8,21 @@ maintainer: Michael Litchard
synopsis: OAuth Authentication for Yesod. synopsis: OAuth Authentication for Yesod.
category: Web, Yesod category: Web, Yesod
stability: Stable stability: Stable
cabal-version: >= 1.6.0
build-type: Simple build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
description: API docs and the README are available at <http://www.stackage.org/package/yesod-auth-oauth> description: API docs and the README are available at <http://www.stackage.org/package/yesod-auth-oauth>
extra-source-files: README.md ChangeLog.md extra-source-files: README.md ChangeLog.md
flag ghc7
library library
if flag(ghc7) default-language: Haskell2010
build-depends: base >= 4.3 && < 5 build-depends: authenticate-oauth >= 1.5 && < 1.8
cpp-options: -DGHC7 , base >= 4.10 && < 5
else
build-depends: base >= 4 && < 4.3
build-depends: authenticate-oauth >= 1.5 && < 1.6
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, yesod-core >= 1.4 && < 1.5
, yesod-auth >= 1.4 && < 1.5
, text >= 0.7 , text >= 0.7
, yesod-form >= 1.4 && < 1.5 , unliftio
, transformers >= 0.2.2 && < 0.6 , yesod-auth >= 1.6 && < 1.7
, lifted-base >= 0.2 && < 0.3 , yesod-core >= 1.6 && < 1.7
, yesod-form >= 1.6 && < 1.8
exposed-modules: Yesod.Auth.OAuth exposed-modules: Yesod.Auth.OAuth
ghc-options: -Wall ghc-options: -Wall

View File

@ -1,3 +1,159 @@
# ChangeLog for yesod-auth
## 1.6.11.2
* Add support for aeson 2.2 [#1820](https://github.com/yesodweb/yesod/pull/1820)
## 1.6.11.1
* No star is type [#1797](https://github.com/yesodweb/yesod/pull/1797)
## 1.6.11
* Add support for aeson 2
## 1.6.10.5
* Fix German translations of AuthMessage [#1741](https://github.com/yesodweb/yesod/pull/1741)
## 1.6.10.4
* Add support for GHC 9 [#1737](https://github.com/yesodweb/yesod/pull/1737)
## 1.6.10.3
* Relax bounds for yesod-form 1.7
## 1.6.10.2
* Relax bounds for persistent 2.12
## 1.6.10.1
* Add support for Persistent 2.11 [#1701](https://github.com/yesodweb/yesod/pull/1701)
## 1.6.10
* Updated `AuthMessage` data type in `Yesod.Auth.Message` to accommodate registration flow where password is supplied initially: deprecated `AddressVerified` and split into `EmailVerifiedChangePass` and `EmailVerified`
* Fixed a bug in `getVerifyR` related to the above, where the incorrect message was displayed when password was set during registration
* Added `sendForgotPasswordEmail` to `YesodAuthEmail` typeclass, allowing for different emails for account registration vs. forgot password
* See pull request [#1662](https://github.com/yesodweb/yesod/pull/1662)
## 1.6.9
* Added `registerHelper` and `passwordResetHelper` methods to the `YesodAuthEmail` class, allowing for customizing behavior for user registration and forgot password requests [#1660](https://github.com/yesodweb/yesod/pull/1660)
* Exposed `defaultRegisterHelper` as default implementation for the above methods
## 1.6.8.1
* Email: Fix typo in `defaultEmailLoginHandler` template [#1605](https://github.com/yesodweb/yesod/pull/1605)
* Remove unnecessary deriving of Typeable
## 1.6.8
* Dummy: Add support for JSON submissions [#1619](https://github.com/yesodweb/yesod/pull/1619)
## 1.6.7
* Redirect behavior of `clearCreds` depends on request type [#1598](https://github.com/yesodweb/yesod/pull/1598)
## 1.6.6
* Deprecated `Yesod.Auth.GoogleEmail2`, see [#1579](https://github.com/yesodweb/yesod/issues/1579) and [migration blog post](https://pbrisbin.com/posts/googleemail2_deprecation/)
## 1.6.5
* Add support for persistent 2.9 [#1516](https://github.com/yesodweb/yesod/pull/1516), [#1561](https://github.com/yesodweb/yesod/pull/1561)
## 1.6.4.1
* Email: Fix forgot-password endpoint [#1537](https://github.com/yesodweb/yesod/pull/1537)
## 1.6.4
* Make `registerHelper` configurable [#1524](https://github.com/yesodweb/yesod/issues/1524)
* Email: Immediately register with a password [#1389](https://github.com/yesodweb/yesod/issues/1389)
To configure this new functionality:
1. Define `addUnverifiedWithPass`, e.g:
```
addUnverified email verkey = liftHandler $ runDB $ do
void $ insert $ UserLogin email Nothing (Just verkey) False
return email
addUnverifiedWithPass email verkey pass = liftHandler $ runDB $ do
void $ insert $ UserLogin email (Just pass) (Just verkey) False
return email
```
2. Add a `password` field to your client forms.
## 1.6.3
* Generalize GoogleEmail2.getPerson [#1501](https://github.com/yesodweb/yesod/pull/1501)
## 1.6.2
* Remove MINIMAL praggma for authHttpManager [#1489](https://github.com/yesodweb/yesod/issues/1489)
## 1.6.1
* Relax a number of type signatures [#1488](https://github.com/yesodweb/yesod/issues/1488)
## 1.6.0
* Upgrade to yesod-core 1.6.0
## 1.4.21
* Add redirectToCurrent to Yesod.Auth module for controlling setUltDestCurrent in redirectLogin [#1461](https://github.com/yesodweb/yesod/pull/1461)
## 1.4.20
* Extend `YesodAuthEmail` to support extensible password hashing via
`hashAndSaltPassword` and `verifyPassword` functions
## 1.4.19
* Adjust English localization to distinguish between "log in" (verb) and "login" (noun)
## 1.4.18
* Expose Yesod.Auth.Util.PasswordStore
## 1.4.17.3
* Some translation fixes
## 1.4.17.2
* Move to cryptonite from cryptohash
## 1.4.17.1
* Some translation fixes
## 1.4.17
* Add Show instance for user credentials `Creds`
* Export pid type for identifying plugin
* Fix warnings
* Allow for a custom Email Login DOM with `emailLoginHandler`
## 1.4.16
* Fix email provider [#1330](https://github.com/yesodweb/yesod/issues/1330)
* Document JSON endpoints of Yesod.Auth.Email
## 1.4.15
* Add JSON endpoints to Yesod.Auth.Email module
* Export croatianMessage from Message module
* Minor Haddock rendering fixes at Auth.Email module
## 1.4.14
* Remove Google OpenID link [#1309](https://github.com/yesodweb/yesod/pull/1309)
* Add CSRF Security check in `registerHelperFunction` [#1302](https://github.com/yesodweb/yesod/pull/1302)
## 1.4.13.5 ## 1.4.13.5
* Translation fix * Translation fix

View File

@ -6,6 +6,7 @@ BrowserID (a.k.a., Mozilla Persona), and email. Other packages are available
from Hackage as well. If you've written such an add-on, please notify me so from Hackage as well. If you've written such an add-on, please notify me so
that it can be added to this description. that it can be added to this description.
* [yesod-auth-oauth2](https://hackage.haskell.org/package/yesod-auth-oauth2): Library to authenticate with OAuth 2.0.
* [yesod-auth-account](http://hackage.haskell.org/package/yesod-auth-account): An account authentication plugin for Yesod * [yesod-auth-account](http://hackage.haskell.org/package/yesod-auth-account): An account authentication plugin for Yesod
* [yesod-auth-hashdb](http://www.stackage.org/package/yesod-auth-hashdb): The HashDB module previously packaged in yesod-auth, now with stronger, but compatible, security. * [yesod-auth-hashdb](http://www.stackage.org/package/yesod-auth-hashdb): The HashDB module previously packaged in yesod-auth, now with stronger, but compatible, security.
* [yesod-auth-bcrypt](https://github.com/ollieh/yesod-auth-bcrypt/): An alternative to the HashDB module. * [yesod-auth-bcrypt](https://hackage.haskell.org/package/yesod-auth-bcrypt): An alternative to the HashDB module.

View File

@ -8,7 +8,6 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Yesod.Auth module Yesod.Auth
@ -39,6 +38,7 @@ module Yesod.Auth
-- * Exception -- * Exception
, AuthException (..) , AuthException (..)
-- * Helper -- * Helper
, MonadAuthHandler
, AuthHandler , AuthHandler
-- * Internal -- * Internal
, credsKey , credsKey
@ -47,12 +47,11 @@ module Yesod.Auth
, asHtml , asHtml
) where ) where
import Control.Applicative ((<$>))
import Control.Monad (when) import Control.Monad (when)
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import UnliftIO (withRunInIO, MonadUnliftIO)
import Yesod.Auth.Routes import Yesod.Auth.Routes
import Data.Aeson hiding (json)
import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Text (Text) import Data.Text (Text)
@ -60,11 +59,11 @@ import qualified Data.Text as T
import qualified Data.HashMap.Lazy as Map import qualified Data.HashMap.Lazy as Map
import Data.Monoid (Endo) import Data.Monoid (Endo)
import Network.HTTP.Client (Manager, Request, withResponse, Response, BodyReader) import Network.HTTP.Client (Manager, Request, withResponse, Response, BodyReader)
import Network.HTTP.Client.TLS (getGlobalManager)
import qualified Network.Wai as W import qualified Network.Wai as W
import Yesod.Core import Yesod.Core
import Yesod.Core.Types (HandlerT(..), unHandlerT)
import Yesod.Persist import Yesod.Persist
import Yesod.Auth.Message (AuthMessage, defaultMessage) import Yesod.Auth.Message (AuthMessage, defaultMessage)
import qualified Yesod.Auth.Message as Msg import qualified Yesod.Auth.Message as Msg
@ -72,20 +71,21 @@ import Yesod.Form (FormMessage)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import Control.Exception (Exception) import Control.Exception (Exception)
import Network.HTTP.Types (Status, internalServerError500, unauthorized401) import Network.HTTP.Types (Status, internalServerError500, unauthorized401)
import Control.Monad.Trans.Resource (MonadResourceBase)
import qualified Control.Monad.Trans.Writer as Writer import qualified Control.Monad.Trans.Writer as Writer
import Control.Monad (void) import Control.Monad (void)
import Data.Kind (Type)
type AuthRoute = Route Auth type AuthRoute = Route Auth
type AuthHandler master a = YesodAuth master => HandlerT Auth (HandlerT master IO) a type MonadAuthHandler master m = (MonadHandler m, YesodAuth master, master ~ HandlerSite m, Auth ~ SubHandlerSite m, MonadUnliftIO m)
type AuthHandler master a = forall m. MonadAuthHandler master m => m a
type Method = Text type Method = Text
type Piece = Text type Piece = Text
-- | The result of an authentication based on credentials -- | The result of an authentication based on credentials
-- --
-- Since 1.4.4 -- @since 1.4.4
data AuthenticationResult master data AuthenticationResult master
= Authenticated (AuthId master) -- ^ Authenticated successfully = Authenticated (AuthId master) -- ^ Authenticated successfully
| UserError AuthMessage -- ^ Invalid credentials provided by user | UserError AuthMessage -- ^ Invalid credentials provided by user
@ -94,7 +94,7 @@ data AuthenticationResult master
data AuthPlugin master = AuthPlugin data AuthPlugin master = AuthPlugin
{ apName :: Text { apName :: Text
, apDispatch :: Method -> [Piece] -> AuthHandler master TypedContent , apDispatch :: Method -> [Piece] -> AuthHandler master TypedContent
, apLogin :: (Route Auth -> Route master) -> WidgetT master IO () , apLogin :: (Route Auth -> Route master) -> WidgetFor master ()
} }
getAuth :: a -> Auth getAuth :: a -> Auth
@ -105,14 +105,14 @@ data Creds master = Creds
{ credsPlugin :: Text -- ^ How the user was authenticated { credsPlugin :: Text -- ^ How the user was authenticated
, credsIdent :: Text -- ^ Identifier. Exact meaning depends on plugin. , credsIdent :: Text -- ^ Identifier. Exact meaning depends on plugin.
, credsExtra :: [(Text, Text)] , credsExtra :: [(Text, Text)]
} } deriving (Show)
class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage) => YesodAuth master where class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage) => YesodAuth master where
type AuthId master type AuthId master
-- | specify the layout. Uses defaultLayout by default -- | specify the layout. Uses defaultLayout by default
authLayout :: WidgetT master IO () -> HandlerT master IO Html authLayout :: (MonadHandler m, HandlerSite m ~ master) => WidgetFor master () -> m Html
authLayout = defaultLayout authLayout = liftHandler . defaultLayout
-- | Default destination on successful login, if no other -- | Default destination on successful login, if no other
-- destination exists. -- destination exists.
@ -126,8 +126,8 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
-- --
-- Default implementation is in terms of @'getAuthId'@ -- Default implementation is in terms of @'getAuthId'@
-- --
-- Since: 1.4.4 -- @since: 1.4.4
authenticate :: Creds master -> HandlerT master IO (AuthenticationResult master) authenticate :: (MonadHandler m, HandlerSite m ~ master) => Creds master -> m (AuthenticationResult master)
authenticate creds = do authenticate creds = do
muid <- getAuthId creds muid <- getAuthId creds
@ -137,7 +137,7 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
-- --
-- Default implementation is in terms of @'authenticate'@ -- Default implementation is in terms of @'authenticate'@
-- --
getAuthId :: Creds master -> HandlerT master IO (Maybe (AuthId master)) getAuthId :: (MonadHandler m, HandlerSite m ~ master) => Creds master -> m (Maybe (AuthId master))
getAuthId creds = do getAuthId creds = do
auth <- authenticate creds auth <- authenticate creds
@ -182,19 +182,27 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
redirectToReferer :: master -> Bool redirectToReferer :: master -> Bool
redirectToReferer _ = False redirectToReferer _ = False
-- | When being redirected to the login page should the current page
-- be set to redirect back to. Default is 'True'.
--
-- @since 1.4.21
redirectToCurrent :: master -> Bool
redirectToCurrent _ = True
-- | Return an HTTP connection manager that is stored in the foundation -- | Return an HTTP connection manager that is stored in the foundation
-- type. This allows backends to reuse persistent connections. If none of -- type. This allows backends to reuse persistent connections. If none of
-- the backends you're using use HTTP connections, you can safely return -- the backends you're using use HTTP connections, you can safely return
-- @error \"authHttpManager\"@ here. -- @error \"authHttpManager\"@ here.
authHttpManager :: master -> Manager authHttpManager :: (MonadHandler m, HandlerSite m ~ master) => m Manager
authHttpManager = liftIO getGlobalManager
-- | Called on a successful login. By default, calls -- | Called on a successful login. By default, calls
-- @addMessageI "success" NowLoggedIn@. -- @addMessageI "success" NowLoggedIn@.
onLogin :: HandlerT master IO () onLogin :: (MonadHandler m, master ~ HandlerSite m) => m ()
onLogin = addMessageI "success" Msg.NowLoggedIn onLogin = addMessageI "success" Msg.NowLoggedIn
-- | Called on logout. By default, does nothing -- | Called on logout. By default, does nothing
onLogout :: HandlerT master IO () onLogout :: (MonadHandler m, master ~ HandlerSite m) => m ()
onLogout = return () onLogout = return ()
-- | Retrieves user credentials, if user is authenticated. -- | Retrieves user credentials, if user is authenticated.
@ -205,17 +213,17 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
-- especially useful for creating an API to be accessed via some means -- especially useful for creating an API to be accessed via some means
-- other than a browser. -- other than a browser.
-- --
-- Since 1.2.0 -- @since 1.2.0
maybeAuthId :: HandlerT master IO (Maybe (AuthId master)) maybeAuthId :: (MonadHandler m, master ~ HandlerSite m) => m (Maybe (AuthId master))
default maybeAuthId default maybeAuthId
:: (YesodAuthPersist master, Typeable (AuthEntity master)) :: (MonadHandler m, master ~ HandlerSite m, YesodAuthPersist master, Typeable (AuthEntity master))
=> HandlerT master IO (Maybe (AuthId master)) => m (Maybe (AuthId master))
maybeAuthId = defaultMaybeAuthId maybeAuthId = defaultMaybeAuthId
-- | Called on login error for HTTP requests. By default, calls -- | Called on login error for HTTP requests. By default, calls
-- @addMessage@ with "error" as status and redirects to @dest@. -- @addMessage@ with "error" as status and redirects to @dest@.
onErrorHtml :: (MonadResourceBase m) => Route master -> Text -> HandlerT master m Html onErrorHtml :: (MonadHandler m, HandlerSite m ~ master) => Route master -> Text -> m Html
onErrorHtml dest msg = do onErrorHtml dest msg = do
addMessage "error" $ toHtml msg addMessage "error" $ toHtml msg
fmap asHtml $ redirect dest fmap asHtml $ redirect dest
@ -225,30 +233,35 @@ class (Yesod master, PathPiece (AuthId master), RenderMessage master FormMessage
-- --
-- The HTTP 'Request' is given in case it is useful to change behavior based on inspecting the request. -- The HTTP 'Request' is given in case it is useful to change behavior based on inspecting the request.
-- This is an experimental API that is not broadly used throughout the yesod-auth code base -- This is an experimental API that is not broadly used throughout the yesod-auth code base
runHttpRequest :: Request -> (Response BodyReader -> HandlerT master IO a) -> HandlerT master IO a runHttpRequest
:: (MonadHandler m, HandlerSite m ~ master, MonadUnliftIO m)
=> Request
-> (Response BodyReader -> m a)
-> m a
runHttpRequest req inner = do runHttpRequest req inner = do
man <- authHttpManager <$> getYesod man <- authHttpManager
HandlerT $ \t -> withResponse req man $ \res -> unHandlerT (inner res) t withRunInIO $ \run -> withResponse req man $ run . inner
{-# MINIMAL loginDest, logoutDest, (authenticate | getAuthId), authPlugins, authHttpManager #-} {-# MINIMAL loginDest, logoutDest, (authenticate | getAuthId), authPlugins #-}
{-# DEPRECATED getAuthId "Define 'authenticate' instead; 'getAuthId' will be removed in the next major version" #-} {-# DEPRECATED getAuthId "Define 'authenticate' instead; 'getAuthId' will be removed in the next major version" #-}
-- | Internal session key used to hold the authentication information. -- | Internal session key used to hold the authentication information.
-- --
-- Since 1.2.3 -- @since 1.2.3
credsKey :: Text credsKey :: Text
credsKey = "_ID" credsKey = "_ID"
-- | Retrieves user credentials from the session, if user is authenticated. -- | Retrieves user credentials from the session, if user is authenticated.
-- --
-- This function does /not/ confirm that the credentials are valid, see -- This function does /not/ confirm that the credentials are valid, see
-- 'maybeAuthIdRaw' for more information. -- 'maybeAuthIdRaw' for more information. The first call in a request
-- does a database request to make sure that the account is still in the database.
-- --
-- Since 1.1.2 -- @since 1.1.2
defaultMaybeAuthId defaultMaybeAuthId
:: (YesodAuthPersist master, Typeable (AuthEntity master)) :: (MonadHandler m, HandlerSite m ~ master, YesodAuthPersist master, Typeable (AuthEntity master))
=> HandlerT master IO (Maybe (AuthId master)) => m (Maybe (AuthId master))
defaultMaybeAuthId = runMaybeT $ do defaultMaybeAuthId = runMaybeT $ do
s <- MaybeT $ lookupSession credsKey s <- MaybeT $ lookupSession credsKey
aid <- MaybeT $ return $ fromPathPiece s aid <- MaybeT $ return $ fromPathPiece s
@ -256,8 +269,13 @@ defaultMaybeAuthId = runMaybeT $ do
return aid return aid
cachedAuth cachedAuth
:: (YesodAuthPersist master, Typeable (AuthEntity master)) :: ( MonadHandler m
=> AuthId master -> HandlerT master IO (Maybe (AuthEntity master)) , YesodAuthPersist master
, Typeable (AuthEntity master)
, HandlerSite m ~ master
)
=> AuthId master
-> m (Maybe (AuthEntity master))
cachedAuth cachedAuth
= fmap unCachedMaybeAuth = fmap unCachedMaybeAuth
. cached . cached
@ -270,52 +288,59 @@ cachedAuth
-- This is the default 'loginHandler'. It concatenates plugin widgets and -- This is the default 'loginHandler'. It concatenates plugin widgets and
-- wraps the result in 'authLayout'. See 'loginHandler' for more details. -- wraps the result in 'authLayout'. See 'loginHandler' for more details.
-- --
-- Since 1.4.9 -- @since 1.4.9
defaultLoginHandler :: AuthHandler master Html defaultLoginHandler :: AuthHandler master Html
defaultLoginHandler = do defaultLoginHandler = do
tp <- getRouteToParent tp <- getRouteToParent
lift $ authLayout $ do authLayout $ do
setTitleI Msg.LoginTitle setTitleI Msg.LoginTitle
master <- getYesod master <- getYesod
mapM_ (flip apLogin tp) (authPlugins master) mapM_ (flip apLogin tp) (authPlugins master)
loginErrorMessageI :: (MonadResourceBase m, YesodAuth master) loginErrorMessageI
=> Route child :: Route Auth
-> AuthMessage -> AuthMessage
-> HandlerT child (HandlerT master m) TypedContent -> AuthHandler master TypedContent
loginErrorMessageI dest msg = do loginErrorMessageI dest msg = do
toParent <- getRouteToParent toParent <- getRouteToParent
lift $ loginErrorMessageMasterI (toParent dest) msg loginErrorMessageMasterI (toParent dest) msg
loginErrorMessageMasterI :: (YesodAuth master, MonadResourceBase m, RenderMessage master AuthMessage) loginErrorMessageMasterI
:: (MonadHandler m, HandlerSite m ~ master, YesodAuth master)
=> Route master => Route master
-> AuthMessage -> AuthMessage
-> HandlerT master m TypedContent -> m TypedContent
loginErrorMessageMasterI dest msg = do loginErrorMessageMasterI dest msg = do
mr <- getMessageRender mr <- getMessageRender
loginErrorMessage dest (mr msg) loginErrorMessage dest (mr msg)
-- | For HTML, set the message and redirect to the route. -- | For HTML, set the message and redirect to the route.
-- For JSON, send the message and a 401 status -- For JSON, send the message and a 401 status
loginErrorMessage :: (YesodAuth master, MonadResourceBase m) loginErrorMessage
=> Route master :: (MonadHandler m, YesodAuth (HandlerSite m))
=> Route (HandlerSite m)
-> Text -> Text
-> HandlerT master m TypedContent -> m TypedContent
loginErrorMessage dest msg = messageJson401 msg (onErrorHtml dest msg) loginErrorMessage dest msg = messageJson401 msg (onErrorHtml dest msg)
messageJson401 :: MonadResourceBase m => Text -> HandlerT master m Html -> HandlerT master m TypedContent messageJson401
:: MonadHandler m
=> Text
-> m Html
-> m TypedContent
messageJson401 = messageJsonStatus unauthorized401 messageJson401 = messageJsonStatus unauthorized401
messageJson500 :: MonadResourceBase m => Text -> HandlerT master m Html -> HandlerT master m TypedContent messageJson500 :: MonadHandler m => Text -> m Html -> m TypedContent
messageJson500 = messageJsonStatus internalServerError500 messageJson500 = messageJsonStatus internalServerError500
messageJsonStatus :: MonadResourceBase m messageJsonStatus
:: MonadHandler m
=> Status => Status
-> Text -> Text
-> HandlerT master m Html -> m Html
-> HandlerT master m TypedContent -> m TypedContent
messageJsonStatus status msg html = selectRep $ do messageJsonStatus status msg html = selectRep $ do
provideRep html provideRep html
provideRep $ do provideRep $ do
@ -327,9 +352,10 @@ provideJsonMessage :: Monad m => Text -> Writer.Writer (Endo [ProvidedRep m]) ()
provideJsonMessage msg = provideRep $ return $ object ["message" .= msg] provideJsonMessage msg = provideRep $ return $ object ["message" .= msg]
setCredsRedirect :: YesodAuth master setCredsRedirect
=> Creds master -- ^ new credentials :: (MonadHandler m, YesodAuth (HandlerSite m))
-> HandlerT master IO TypedContent => Creds (HandlerSite m) -- ^ new credentials
-> m TypedContent
setCredsRedirect creds = do setCredsRedirect creds = do
y <- getYesod y <- getYesod
auth <- authenticate creds auth <- authenticate creds
@ -368,10 +394,10 @@ setCredsRedirect creds = do
return $ renderAuthMessage master langs msg return $ renderAuthMessage master langs msg
-- | Sets user credentials for the session after checking them with authentication backends. -- | Sets user credentials for the session after checking them with authentication backends.
setCreds :: YesodAuth master setCreds :: (MonadHandler m, YesodAuth (HandlerSite m))
=> Bool -- ^ if HTTP redirects should be done => Bool -- ^ if HTTP redirects should be done
-> Creds master -- ^ new credentials -> Creds (HandlerSite m) -- ^ new credentials
-> HandlerT master IO () -> m ()
setCreds doRedirects creds = setCreds doRedirects creds =
if doRedirects if doRedirects
then void $ setCredsRedirect creds then void $ setCredsRedirect creds
@ -381,29 +407,36 @@ setCreds doRedirects creds =
_ -> return () _ -> return ()
-- | same as defaultLayoutJson, but uses authLayout -- | same as defaultLayoutJson, but uses authLayout
authLayoutJson :: (YesodAuth site, ToJSON j) authLayoutJson
=> WidgetT site IO () -- ^ HTML :: (ToJSON j, MonadAuthHandler master m)
-> HandlerT site IO j -- ^ JSON => WidgetFor master () -- ^ HTML
-> HandlerT site IO TypedContent -> m j -- ^ JSON
-> m TypedContent
authLayoutJson w json = selectRep $ do authLayoutJson w json = selectRep $ do
provideRep $ authLayout w provideRep $ authLayout w
provideRep $ fmap toJSON json provideRep $ fmap toJSON json
-- | Clears current user credentials for the session. -- | Clears current user credentials for the session.
-- --
-- Since 1.1.7 -- @since 1.1.7
clearCreds :: YesodAuth master clearCreds :: (MonadHandler m, YesodAuth (HandlerSite m))
=> Bool -- ^ if HTTP redirect to 'logoutDest' should be done => Bool -- ^ if HTTP, redirect to 'logoutDest'
-> HandlerT master IO () -> m ()
clearCreds doRedirects = do clearCreds doRedirects = do
y <- getYesod
onLogout onLogout
deleteSession credsKey deleteSession credsKey
when doRedirects $ do y <- getYesod
redirectUltDest $ logoutDest y aj <- acceptsJson
case (aj, doRedirects) of
(True, _) -> sendResponse successfulLogout
(False, True) -> redirectUltDest (logoutDest y)
_ -> return ()
where successfulLogout = object ["message" .= msg]
msg :: Text
msg = "Logged out successfully!"
getCheckR :: AuthHandler master TypedContent getCheckR :: AuthHandler master TypedContent
getCheckR = lift $ do getCheckR = do
creds <- maybeAuthId creds <- maybeAuthId
authLayoutJson (do authLayoutJson (do
setTitle "Authentication Status" setTitle "Authentication Status"
@ -419,12 +452,12 @@ $nothing
<p>Not logged in. <p>Not logged in.
|] |]
jsonCreds creds = jsonCreds creds =
Object $ Map.fromList toJSON $ Map.fromList
[ (T.pack "logged_in", Bool $ maybe False (const True) creds) [ (T.pack "logged_in", Bool $ maybe False (const True) creds)
] ]
setUltDestReferer' :: AuthHandler master () setUltDestReferer' :: (MonadHandler m, YesodAuth (HandlerSite m)) => m ()
setUltDestReferer' = lift $ do setUltDestReferer' = do
master <- getYesod master <- getYesod
when (redirectToReferer master) setUltDestReferer when (redirectToReferer master) setUltDestReferer
@ -432,14 +465,16 @@ getLoginR :: AuthHandler master Html
getLoginR = setUltDestReferer' >> loginHandler getLoginR = setUltDestReferer' >> loginHandler
getLogoutR :: AuthHandler master () getLogoutR :: AuthHandler master ()
getLogoutR = setUltDestReferer' >> redirectToPost LogoutR getLogoutR = do
tp <- getRouteToParent
setUltDestReferer' >> redirectToPost (tp LogoutR)
postLogoutR :: AuthHandler master () postLogoutR :: AuthHandler master ()
postLogoutR = lift $ clearCreds True postLogoutR = clearCreds True
handlePluginR :: Text -> [Text] -> AuthHandler master TypedContent handlePluginR :: Text -> [Text] -> AuthHandler master TypedContent
handlePluginR plugin pieces = do handlePluginR plugin pieces = do
master <- lift getYesod master <- getYesod
env <- waiRequest env <- waiRequest
let method = decodeUtf8With lenientDecode $ W.requestMethod env let method = decodeUtf8With lenientDecode $ W.requestMethod env
case filter (\x -> apName x == plugin) (authPlugins master) of case filter (\x -> apName x == plugin) (authPlugins master) of
@ -450,23 +485,28 @@ handlePluginR plugin pieces = do
-- with the user\'s database identifier to get the value in the database. This -- with the user\'s database identifier to get the value in the database. This
-- assumes that you are using a Persistent database. -- assumes that you are using a Persistent database.
-- --
-- Since 1.1.0 -- @since 1.1.0
maybeAuth :: ( YesodAuthPersist master maybeAuth :: ( YesodAuthPersist master
, val ~ AuthEntity master , val ~ AuthEntity master
, Key val ~ AuthId master , Key val ~ AuthId master
, PersistEntity val , PersistEntity val
, Typeable val , Typeable val
) => HandlerT master IO (Maybe (Entity val)) , MonadHandler m
maybeAuth = runMaybeT $ do , HandlerSite m ~ master
(aid, ae) <- MaybeT maybeAuthPair ) => m (Maybe (Entity val))
return $ Entity aid ae maybeAuth = fmap (fmap (uncurry Entity)) maybeAuthPair
-- | Similar to 'maybeAuth', but doesnt assume that you are using a -- | Similar to 'maybeAuth', but doesnt assume that you are using a
-- Persistent database. -- Persistent database.
-- --
-- Since 1.4.0 -- @since 1.4.0
maybeAuthPair :: (YesodAuthPersist master, Typeable (AuthEntity master)) maybeAuthPair
=> HandlerT master IO (Maybe (AuthId master, AuthEntity master)) :: ( YesodAuthPersist master
, Typeable (AuthEntity master)
, MonadHandler m
, HandlerSite m ~ master
)
=> m (Maybe (AuthId master, AuthEntity master))
maybeAuthPair = runMaybeT $ do maybeAuthPair = runMaybeT $ do
aid <- MaybeT maybeAuthId aid <- MaybeT maybeAuthId
ae <- MaybeT $ cachedAuth aid ae <- MaybeT $ cachedAuth aid
@ -474,7 +514,6 @@ maybeAuthPair = runMaybeT $ do
newtype CachedMaybeAuth val = CachedMaybeAuth { unCachedMaybeAuth :: Maybe val } newtype CachedMaybeAuth val = CachedMaybeAuth { unCachedMaybeAuth :: Maybe val }
deriving Typeable
-- | Class which states that the given site is an instance of @YesodAuth@ -- | Class which states that the given site is an instance of @YesodAuth@
-- and that its @AuthId@ is a lookup key for the full user information in -- and that its @AuthId@ is a lookup key for the full user information in
@ -485,7 +524,7 @@ newtype CachedMaybeAuth val = CachedMaybeAuth { unCachedMaybeAuth :: Maybe val }
-- given value. This is the common case in Yesod, and means that you can -- given value. This is the common case in Yesod, and means that you can
-- easily look up the full information on a given user. -- easily look up the full information on a given user.
-- --
-- Since 1.4.0 -- @since 1.4.0
class (YesodAuth master, YesodPersist master) => YesodAuthPersist master where class (YesodAuth master, YesodPersist master) => YesodAuthPersist master where
-- | If the @AuthId@ for a given site is a persistent ID, this will give the -- | If the @AuthId@ for a given site is a persistent ID, this will give the
-- value for that entity. E.g.: -- value for that entity. E.g.:
@ -493,31 +532,23 @@ class (YesodAuth master, YesodPersist master) => YesodAuthPersist master where
-- > type AuthId MySite = UserId -- > type AuthId MySite = UserId
-- > AuthEntity MySite ~ User -- > AuthEntity MySite ~ User
-- --
-- Since 1.2.0 -- @since 1.2.0
type AuthEntity master :: * type AuthEntity master :: Type
type AuthEntity master = KeyEntity (AuthId master) type AuthEntity master = KeyEntity (AuthId master)
getAuthEntity :: AuthId master -> HandlerT master IO (Maybe (AuthEntity master)) getAuthEntity :: (MonadHandler m, HandlerSite m ~ master)
=> AuthId master -> m (Maybe (AuthEntity master))
#if MIN_VERSION_persistent(2,5,0)
default getAuthEntity default getAuthEntity
:: ( YesodPersistBackend master ~ backend :: ( YesodPersistBackend master ~ backend
, PersistRecordBackend (AuthEntity master) backend , PersistRecordBackend (AuthEntity master) backend
, Key (AuthEntity master) ~ AuthId master , Key (AuthEntity master) ~ AuthId master
, PersistStore backend , PersistStore backend
, MonadHandler m
, HandlerSite m ~ master
) )
=> AuthId master -> HandlerT master IO (Maybe (AuthEntity master)) => AuthId master -> m (Maybe (AuthEntity master))
#else getAuthEntity = liftHandler . runDB . get
default getAuthEntity
:: ( YesodPersistBackend master
~ PersistEntityBackend (AuthEntity master)
, Key (AuthEntity master) ~ AuthId master
, PersistStore (YesodPersistBackend master)
, PersistEntity (AuthEntity master)
)
=> AuthId master -> HandlerT master IO (Maybe (AuthEntity master))
#endif
getAuthEntity = runDB . get
type family KeyEntity key type family KeyEntity key
@ -526,39 +557,46 @@ type instance KeyEntity (Key x) = x
-- | Similar to 'maybeAuthId', but redirects to a login page if user is not -- | Similar to 'maybeAuthId', but redirects to a login page if user is not
-- authenticated or responds with error 401 if this is an API client (expecting JSON). -- authenticated or responds with error 401 if this is an API client (expecting JSON).
-- --
-- Since 1.1.0 -- @since 1.1.0
requireAuthId :: YesodAuth master => HandlerT master IO (AuthId master) requireAuthId :: (MonadHandler m, YesodAuth (HandlerSite m)) => m (AuthId (HandlerSite m))
requireAuthId = maybeAuthId >>= maybe handleAuthLack return requireAuthId = maybeAuthId >>= maybe handleAuthLack return
-- | Similar to 'maybeAuth', but redirects to a login page if user is not -- | Similar to 'maybeAuth', but redirects to a login page if user is not
-- authenticated or responds with error 401 if this is an API client (expecting JSON). -- authenticated or responds with error 401 if this is an API client (expecting JSON).
-- --
-- Since 1.1.0 -- @since 1.1.0
requireAuth :: ( YesodAuthPersist master requireAuth :: ( YesodAuthPersist master
, val ~ AuthEntity master , val ~ AuthEntity master
, Key val ~ AuthId master , Key val ~ AuthId master
, PersistEntity val , PersistEntity val
, Typeable val , Typeable val
) => HandlerT master IO (Entity val) , MonadHandler m
, HandlerSite m ~ master
) => m (Entity val)
requireAuth = maybeAuth >>= maybe handleAuthLack return requireAuth = maybeAuth >>= maybe handleAuthLack return
-- | Similar to 'requireAuth', but not tied to Persistent's 'Entity' type. -- | Similar to 'requireAuth', but not tied to Persistent's 'Entity' type.
-- Instead, the 'AuthId' and 'AuthEntity' are returned in a tuple. -- Instead, the 'AuthId' and 'AuthEntity' are returned in a tuple.
-- --
-- Since 1.4.0 -- @since 1.4.0
requireAuthPair :: (YesodAuthPersist master, Typeable (AuthEntity master)) requireAuthPair
=> HandlerT master IO (AuthId master, AuthEntity master) :: ( YesodAuthPersist master
, Typeable (AuthEntity master)
, MonadHandler m
, HandlerSite m ~ master
)
=> m (AuthId master, AuthEntity master)
requireAuthPair = maybeAuthPair >>= maybe handleAuthLack return requireAuthPair = maybeAuthPair >>= maybe handleAuthLack return
handleAuthLack :: Yesod master => HandlerT master IO a handleAuthLack :: (YesodAuth (HandlerSite m), MonadHandler m) => m a
handleAuthLack = do handleAuthLack = do
aj <- acceptsJson aj <- acceptsJson
if aj then notAuthenticated else redirectLogin if aj then notAuthenticated else redirectLogin
redirectLogin :: Yesod master => HandlerT master IO a redirectLogin :: (YesodAuth (HandlerSite m), MonadHandler m) => m a
redirectLogin = do redirectLogin = do
y <- getYesod y <- getYesod
setUltDestCurrent when (redirectToCurrent y) setUltDestCurrent
case authRoute y of case authRoute y of
Just z -> redirect z Just z -> redirect z
Nothing -> permissionDenied "Please configure authRoute" Nothing -> permissionDenied "Please configure authRoute"
@ -567,10 +605,10 @@ instance YesodAuth master => RenderMessage master AuthMessage where
renderMessage = renderAuthMessage renderMessage = renderAuthMessage
data AuthException = InvalidFacebookResponse data AuthException = InvalidFacebookResponse
deriving (Show, Typeable) deriving Show
instance Exception AuthException instance Exception AuthException
instance YesodAuth master => YesodSubDispatch Auth (HandlerT master IO) where instance YesodAuth master => YesodSubDispatch Auth master where
yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth) yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth)
asHtml :: Html -> Html asHtml :: Html -> Html

View File

@ -70,20 +70,21 @@ authBrowserId bis@BrowserIdSettings {..} = AuthPlugin
, apDispatch = \m ps -> , apDispatch = \m ps ->
case (m, ps) of case (m, ps) of
("GET", [assertion]) -> do ("GET", [assertion]) -> do
master <- lift getYesod
audience <- audience <-
case bisAudience of case bisAudience of
Just a -> return a Just a -> return a
Nothing -> do Nothing -> do
r <- getUrlRender r <- getUrlRender
return $ T.takeWhile (/= '/') $ stripScheme $ r LoginR tm <- getRouteToParent
memail <- lift $ checkAssertion audience assertion (authHttpManager master) return $ T.takeWhile (/= '/') $ stripScheme $ r $ tm LoginR
manager <- authHttpManager
memail <- checkAssertion audience assertion manager
case memail of case memail of
Nothing -> do Nothing -> do
$logErrorS "yesod-auth" "BrowserID assertion failure" $logErrorS "yesod-auth" "BrowserID assertion failure"
tm <- getRouteToParent tm <- getRouteToParent
lift $ loginErrorMessage (tm LoginR) "BrowserID login error." loginErrorMessage (tm LoginR) "BrowserID login error."
Just email -> lift $ setCredsRedirect Creds Just email -> setCredsRedirect Creds
{ credsPlugin = pid { credsPlugin = pid
, credsIdent = email , credsIdent = email
, credsExtra = [] , credsExtra = []
@ -116,7 +117,7 @@ $newline never
createOnClickOverride :: BrowserIdSettings createOnClickOverride :: BrowserIdSettings
-> (Route Auth -> Route master) -> (Route Auth -> Route master)
-> Maybe (Route master) -> Maybe (Route master)
-> WidgetT master IO Text -> WidgetFor master Text
createOnClickOverride BrowserIdSettings {..} toMaster mOnRegistration = do createOnClickOverride BrowserIdSettings {..} toMaster mOnRegistration = do
unless bisLazyLoad $ addScriptRemote browserIdJs unless bisLazyLoad $ addScriptRemote browserIdJs
onclick <- newIdent onclick <- newIdent
@ -165,5 +166,5 @@ createOnClickOverride BrowserIdSettings {..} toMaster mOnRegistration = do
-- name. -- name.
createOnClick :: BrowserIdSettings createOnClick :: BrowserIdSettings
-> (Route Auth -> Route master) -> (Route Auth -> Route master)
-> WidgetT master IO Text -> WidgetFor master Text
createOnClick bidSettings toMaster = createOnClickOverride bidSettings toMaster Nothing createOnClick bidSettings toMaster = createOnClickOverride bidSettings toMaster Nothing

View File

@ -1,23 +1,67 @@
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- | Provides a dummy authentication module that simply lets a user specify -- | Provides a dummy authentication module that simply lets a user specify
-- his/her identifier. This is not intended for real world use, just for -- their identifier. This is not intended for real world use, just for
-- testing. -- testing. This plugin supports form submissions via JSON (since 1.6.8).
--
-- = Using the JSON Login Endpoint
--
-- We are assuming that you have declared `authRoute` as follows
--
-- @
-- Just $ AuthR LoginR
-- @
--
-- If you are using a different one, then you have to adjust the
-- endpoint accordingly.
--
-- @
-- Endpoint: \/auth\/page\/dummy
-- Method: POST
-- JSON Data: {
-- "ident": "my identifier"
-- }
-- @
--
-- Remember to add the following headers:
--
-- - Accept: application\/json
-- - Content-Type: application\/json
module Yesod.Auth.Dummy module Yesod.Auth.Dummy
( authDummy ( authDummy
) where ) where
import Data.Aeson.Types (Parser, Result (..))
import qualified Data.Aeson.Types as A (parseEither, withObject)
import Data.Text (Text)
import Yesod.Auth import Yesod.Auth
import Yesod.Form (runInputPost, textField, ireq)
import Yesod.Core import Yesod.Core
import Yesod.Form (ireq, runInputPost, textField)
identParser :: Value -> Parser Text
identParser = A.withObject "Ident" (.: "ident")
authDummy :: YesodAuth m => AuthPlugin m authDummy :: YesodAuth m => AuthPlugin m
authDummy = authDummy =
AuthPlugin "dummy" dispatch login AuthPlugin "dummy" dispatch login
where where
dispatch :: Text -> [Text] -> AuthHandler m TypedContent
dispatch "POST" [] = do dispatch "POST" [] = do
ident <- lift $ runInputPost $ ireq textField "ident" (jsonResult :: Result Value) <- parseCheckJsonBody
lift $ setCredsRedirect $ Creds "dummy" ident [] eIdent <- case jsonResult of
Success val -> return $ A.parseEither identParser val
Error err -> return $ Left err
case eIdent of
Right ident ->
setCredsRedirect $ Creds "dummy" ident []
Left _ -> do
ident <- runInputPost $ ireq textField "ident"
setCredsRedirect $ Creds "dummy" ident []
dispatch _ _ = notFound dispatch _ _ = notFound
url = PluginR "dummy" [] url = PluginR "dummy" []
login authToMaster = do login authToMaster = do

View File

@ -4,23 +4,87 @@
{-# LANGUAGE PatternGuards #-} {-# LANGUAGE PatternGuards #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE Rank2Types #-} {-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
-- | A Yesod plugin for Authentication via e-mail -- | A Yesod plugin for Authentication via e-mail
-- --
-- This plugin works out of the box by only setting a few methods on the type class -- This plugin works out of the box by only setting a few methods on
-- that tell the plugin how to interoprate with your user data storage (your database). -- the type class that tell the plugin how to interoperate with your
-- However, almost everything is customizeable by setting more methods on the type class. -- user data storage (your database). However, almost everything is
-- In addition, you can send all the form submissions via JSON and completely control the user's flow. -- customizeable by setting more methods on the type class. In
-- addition, you can send all the form submissions via JSON and
-- completely control the user's flow.
--
-- This is a standard registration e-mail flow -- This is a standard registration e-mail flow
-- --
-- 1) A user registers a new e-mail address, and an e-mail is sent there -- 1. A user registers a new e-mail address, and an e-mail is sent there
-- 2) The user clicks on the registration link in the e-mail -- 2. The user clicks on the registration link in the e-mail. Note that
-- Note that at this point they are actually logged in (without a password) -- at this point they are actually logged in (without a
-- That means that when they log out they will need to reset their password -- password). That means that when they log out they will need to
-- 3) The user sets their password and is redirected to the site. -- reset their password.
-- 4) The user can now -- 3. The user sets their password and is redirected to the site.
-- 4. The user can now
--
-- * logout and sign in -- * logout and sign in
-- * reset their password -- * reset their password
--
-- = Using JSON Endpoints
--
-- We are assuming that you have declared auth route as follows
--
-- @
-- /auth AuthR Auth getAuth
-- @
--
-- If you are using a different route, then you have to adjust the
-- endpoints accordingly.
--
-- * Registration
--
-- @
-- Endpoint: \/auth\/page\/email\/register
-- Method: POST
-- JSON Data: {
-- "email": "myemail@domain.com",
-- "password": "myStrongPassword" (optional)
-- }
-- @
--
-- * Forgot password
--
-- @
-- Endpoint: \/auth\/page\/email\/forgot-password
-- Method: POST
-- JSON Data: { "email": "myemail@domain.com" }
-- @
--
-- * Login
--
-- @
-- Endpoint: \/auth\/page\/email\/login
-- Method: POST
-- JSON Data: {
-- "email": "myemail@domain.com",
-- "password": "myStrongPassword"
-- }
-- @
--
-- * Set new password
--
-- @
-- Endpoint: \/auth\/page\/email\/set-password
-- Method: POST
-- JSON Data: {
-- "new": "newPassword",
-- "confirm": "newPassword",
-- "current": "currentPassword"
-- }
-- @
--
-- Note that in the set password endpoint, the presence of the key
-- "current" is dependent on how the 'needOldPassword' is defined in
-- the instance for 'YesodAuthEmail'.
module Yesod.Auth.Email module Yesod.Auth.Email
( -- * Plugin ( -- * Plugin
authEmail authEmail
@ -45,24 +109,25 @@ module Yesod.Auth.Email
, loginLinkKey , loginLinkKey
, setLoginLinkKey , setLoginLinkKey
-- * Default handlers -- * Default handlers
, defaultEmailLoginHandler
, defaultRegisterHandler , defaultRegisterHandler
, defaultForgotPasswordHandler , defaultForgotPasswordHandler
, defaultSetPasswordHandler , defaultSetPasswordHandler
-- * Default helpers
, defaultRegisterHelper
) where ) where
import Yesod.Auth
import qualified Yesod.Auth.Message as Msg
import Yesod.Core
import Yesod.Form
import qualified Yesod.PasswordStore as PS
import Control.Applicative ((<$>), (<*>)) import Control.Applicative ((<$>), (<*>))
import qualified Crypto.Hash.MD5 as H import qualified Crypto.Hash as H
import qualified Crypto.Nonce as Nonce import qualified Crypto.Nonce as Nonce
import Data.Aeson.Types (Parser, Result (..), parseMaybe,
withObject, (.:?))
import Data.ByteArray (convert)
import Data.ByteString.Base16 as B16 import Data.ByteString.Base16 as B16
import Data.Maybe (isJust)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as TS
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text as TS
import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding as TE
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
@ -70,7 +135,12 @@ import Data.Time (addUTCTime, getCurrentTime)
import Safe (readMay) import Safe (readMay)
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import qualified Text.Email.Validate import qualified Text.Email.Validate
import Yesod.Auth
import qualified Yesod.Auth.Message as Msg
import qualified Yesod.Auth.Util.PasswordStore as PS
import Yesod.Core
import Yesod.Core.Types (TypedContent (TypedContent))
import Yesod.Form
loginR, registerR, forgotPasswordR, setpassR :: AuthRoute loginR, registerR, forgotPasswordR, setpassR :: AuthRoute
loginR = PluginR "email" ["login"] loginR = PluginR "email" ["login"]
@ -78,11 +148,15 @@ registerR = PluginR "email" ["register"]
forgotPasswordR = PluginR "email" ["forgot-password"] forgotPasswordR = PluginR "email" ["forgot-password"]
setpassR = PluginR "email" ["set-password"] setpassR = PluginR "email" ["set-password"]
verifyURLHasSetPassText :: Text
verifyURLHasSetPassText = "has-set-pass"
-- | -- |
-- --
-- Since 1.4.5 -- @since 1.4.5
verifyR :: Text -> Text -> AuthRoute -- FIXME verifyR :: Text -> Text -> Bool -> AuthRoute -- FIXME
verifyR eid verkey = PluginR "email" ["verify", eid, verkey] verifyR eid verkey hasSetPass = PluginR "email" path
where path = "verify":eid:verkey:(if hasSetPass then [verifyURLHasSetPassText] else [])
type Email = Text type Email = Text
type VerKey = Text type VerKey = Text
@ -95,7 +169,7 @@ type VerStatus = Bool
-- --
-- Note that any of these other identifiers must not be valid email addresses. -- Note that any of these other identifiers must not be valid email addresses.
-- --
-- Since 1.2.0 -- @since 1.2.0
type Identifier = Text type Identifier = Text
-- | Data stored in a database for each e-mail address. -- | Data stored in a database for each e-mail address.
@ -107,10 +181,10 @@ data EmailCreds site = EmailCreds
, emailCredsEmail :: Email , emailCredsEmail :: Email
} }
data ForgotPasswordForm = ForgotPasswordForm { forgotEmail :: Text } data ForgotPasswordForm = ForgotPasswordForm { _forgotEmail :: Text }
data PasswordForm = PasswordForm { passwordCurrent :: Text, passwordNew :: Text, passwordConfirm :: Text } data PasswordForm = PasswordForm { _passwordCurrent :: Text, _passwordNew :: Text, _passwordConfirm :: Text }
data UserForm = UserForm { email :: Text } data UserForm = UserForm { _userFormEmail :: Text }
data UserLoginForm = UserLoginForm { loginEmail :: Text, loginPassword :: Text } data UserLoginForm = UserLoginForm { _loginEmail :: Text, _loginPassword :: Text }
class ( YesodAuth site class ( YesodAuth site
, PathPiece (AuthEmailId site) , PathPiece (AuthEmailId site)
@ -122,23 +196,61 @@ class ( YesodAuth site
-- | Add a new email address to the database, but indicate that the address -- | Add a new email address to the database, but indicate that the address
-- has not yet been verified. -- has not yet been verified.
-- --
-- Since 1.1.0 -- @since 1.1.0
addUnverified :: Email -> VerKey -> HandlerT site IO (AuthEmailId site) addUnverified :: Email -> VerKey -> AuthHandler site (AuthEmailId site)
-- | Similar to `addUnverified`, but comes with the registered password.
--
-- The default implementation is just `addUnverified`, which ignores the password.
--
-- You may override this to save the salted password to your database.
--
-- @since 1.6.4
addUnverifiedWithPass :: Email -> VerKey -> SaltedPass -> AuthHandler site (AuthEmailId site)
addUnverifiedWithPass email verkey _ = addUnverified email verkey
-- | Send an email to the given address to verify ownership. -- | Send an email to the given address to verify ownership.
-- --
-- Since 1.1.0 -- @since 1.1.0
sendVerifyEmail :: Email -> VerKey -> VerUrl -> HandlerT site IO () sendVerifyEmail :: Email -> VerKey -> VerUrl -> AuthHandler site ()
-- | Send an email to the given address to re-verify ownership in the case of
-- a password reset. This can be used to send a different email when a user
-- goes through the 'forgot password' flow as opposed to the 'account registration'
-- flow.
--
-- Default: Will call 'sendVerifyEmail', resulting in the same email getting sent
-- for both registrations and password resets.
--
-- @since 1.6.10
sendForgotPasswordEmail :: Email -> VerKey -> VerUrl -> AuthHandler site ()
sendForgotPasswordEmail = sendVerifyEmail
-- | Get the verification key for the given email ID. -- | Get the verification key for the given email ID.
-- --
-- Since 1.1.0 -- @since 1.1.0
getVerifyKey :: AuthEmailId site -> HandlerT site IO (Maybe VerKey) getVerifyKey :: AuthEmailId site -> AuthHandler site (Maybe VerKey)
-- | Set the verification key for the given email ID. -- | Set the verification key for the given email ID.
-- --
-- Since 1.1.0 -- @since 1.1.0
setVerifyKey :: AuthEmailId site -> VerKey -> HandlerT site IO () setVerifyKey :: AuthEmailId site -> VerKey -> AuthHandler site ()
-- | Hash and salt a password
--
-- Default: 'saltPass'.
--
-- @since 1.4.20
hashAndSaltPassword :: Text -> AuthHandler site SaltedPass
hashAndSaltPassword password = liftIO $ saltPass password
-- | Verify a password matches the stored password for the given account.
--
-- Default: Fetch a password with 'getPassword' and match using 'Yesod.Auth.Util.PasswordStore.verifyPassword'.
--
-- @since 1.4.20
verifyPassword :: Text -> SaltedPass -> AuthHandler site Bool
verifyPassword plain salted = return $ isValidPass plain salted
-- | Verify the email address on the given account. -- | Verify the email address on the given account.
-- --
@ -148,48 +260,54 @@ class ( YesodAuth site
-- --
-- See <https://github.com/yesodweb/yesod/issues/1222>. -- See <https://github.com/yesodweb/yesod/issues/1222>.
-- --
-- Since 1.1.0 -- @since 1.1.0
verifyAccount :: AuthEmailId site -> HandlerT site IO (Maybe (AuthId site)) verifyAccount :: AuthEmailId site -> AuthHandler site (Maybe (AuthId site))
-- | Get the salted password for the given account. -- | Get the salted password for the given account.
-- --
-- Since 1.1.0 -- @since 1.1.0
getPassword :: AuthId site -> HandlerT site IO (Maybe SaltedPass) getPassword :: AuthId site -> AuthHandler site (Maybe SaltedPass)
-- | Set the salted password for the given account. -- | Set the salted password for the given account.
-- --
-- Since 1.1.0 -- @since 1.1.0
setPassword :: AuthId site -> SaltedPass -> HandlerT site IO () setPassword :: AuthId site -> SaltedPass -> AuthHandler site ()
-- | Get the credentials for the given @Identifier@, which may be either an -- | Get the credentials for the given @Identifier@, which may be either an
-- email address or some other identification (e.g., username). -- email address or some other identification (e.g., username).
-- --
-- Since 1.2.0 -- @since 1.2.0
getEmailCreds :: Identifier -> HandlerT site IO (Maybe (EmailCreds site)) getEmailCreds :: Identifier -> AuthHandler site (Maybe (EmailCreds site))
-- | Get the email address for the given email ID. -- | Get the email address for the given email ID.
-- --
-- Since 1.1.0 -- @since 1.1.0
getEmail :: AuthEmailId site -> HandlerT site IO (Maybe Email) getEmail :: AuthEmailId site -> AuthHandler site (Maybe Email)
-- | Generate a random alphanumeric string. -- | Generate a random alphanumeric string.
-- --
-- Since 1.1.0 -- @since 1.1.0
randomKey :: site -> IO Text randomKey :: site -> IO VerKey
randomKey _ = Nonce.nonce128urlT defaultNonceGen randomKey _ = Nonce.nonce128urlT defaultNonceGen
-- | Route to send user to after password has been set correctly. -- | Route to send user to after password has been set correctly.
-- --
-- Since 1.2.0 -- @since 1.2.0
afterPasswordRoute :: site -> Route site afterPasswordRoute :: site -> Route site
-- | Route to send user to after verification with a password
--
-- @since 1.6.4
afterVerificationWithPass :: site -> Route site
afterVerificationWithPass = afterPasswordRoute
-- | Does the user need to provide the current password in order to set a -- | Does the user need to provide the current password in order to set a
-- new password? -- new password?
-- --
-- Default: if the user logged in via an email link do not require a password. -- Default: if the user logged in via an email link do not require a password.
-- --
-- Since 1.2.1 -- @since 1.2.1
needOldPassword :: AuthId site -> HandlerT site IO Bool needOldPassword :: AuthId site -> AuthHandler site Bool
needOldPassword aid' = do needOldPassword aid' = do
mkey <- lookupSession loginLinkKey mkey <- lookupSession loginLinkKey
case mkey >>= readMay . TS.unpack of case mkey >>= readMay . TS.unpack of
@ -201,15 +319,15 @@ class ( YesodAuth site
-- | Check that the given plain-text password meets minimum security standards. -- | Check that the given plain-text password meets minimum security standards.
-- --
-- Default: password is at least three characters. -- Default: password is at least three characters.
checkPasswordSecurity :: AuthId site -> Text -> HandlerT site IO (Either Text ()) checkPasswordSecurity :: AuthId site -> Text -> AuthHandler site (Either Text ())
checkPasswordSecurity _ x checkPasswordSecurity _ x
| TS.length x >= 3 = return $ Right () | TS.length x >= 3 = return $ Right ()
| otherwise = return $ Left "Password must be at least three characters" | otherwise = return $ Left "Password must be at least three characters"
-- | Response after sending a confirmation email. -- | Response after sending a confirmation email.
-- --
-- Since 1.2.2 -- @since 1.2.2
confirmationEmailSentResponse :: Text -> HandlerT site IO TypedContent confirmationEmailSentResponse :: Text -> AuthHandler site TypedContent
confirmationEmailSentResponse identifier = do confirmationEmailSentResponse identifier = do
mr <- getMessageRender mr <- getMessageRender
selectRep $ do selectRep $ do
@ -220,21 +338,40 @@ class ( YesodAuth site
where where
msg = Msg.ConfirmationEmailSent identifier msg = Msg.ConfirmationEmailSent identifier
-- | If a response is set, it will be used when an already-verified email
-- tries to re-register. Otherwise, `confirmationEmailSentResponse` will be
-- used.
--
-- @since 1.6.4
emailPreviouslyRegisteredResponse :: MonadAuthHandler site m => Text -> Maybe (m TypedContent)
emailPreviouslyRegisteredResponse _ = Nothing
-- | Additional normalization of email addresses, besides standard canonicalization. -- | Additional normalization of email addresses, besides standard canonicalization.
-- --
-- Default: Lower case the email address. -- Default: Lower case the email address.
-- --
-- Since 1.2.3 -- @since 1.2.3
normalizeEmailAddress :: site -> Text -> Text normalizeEmailAddress :: site -> Text -> Text
normalizeEmailAddress _ = TS.toLower normalizeEmailAddress _ = TS.toLower
-- | Handler called to render the login page.
-- The default works fine, but you may want to override it in
-- order to have a different DOM.
--
-- Default: 'defaultEmailLoginHandler'.
--
-- @since 1.4.17
emailLoginHandler :: (Route Auth -> Route site) -> WidgetFor site ()
emailLoginHandler = defaultEmailLoginHandler
-- | Handler called to render the registration page. The -- | Handler called to render the registration page. The
-- default works fine, but you may want to override it in -- default works fine, but you may want to override it in
-- order to have a different DOM. -- order to have a different DOM.
-- --
-- Default: 'defaultRegisterHandler'. -- Default: 'defaultRegisterHandler'.
-- --
-- Since: 1.2.6. -- @since: 1.2.6
registerHandler :: AuthHandler site Html registerHandler :: AuthHandler site Html
registerHandler = defaultRegisterHandler registerHandler = defaultRegisterHandler
@ -244,7 +381,7 @@ class ( YesodAuth site
-- --
-- Default: 'defaultForgotPasswordHandler'. -- Default: 'defaultForgotPasswordHandler'.
-- --
-- Since: 1.2.6. -- @since: 1.2.6
forgotPasswordHandler :: AuthHandler site Html forgotPasswordHandler :: AuthHandler site Html
forgotPasswordHandler = defaultForgotPasswordHandler forgotPasswordHandler = defaultForgotPasswordHandler
@ -254,7 +391,7 @@ class ( YesodAuth site
-- --
-- Default: 'defaultSetPasswordHandler'. -- Default: 'defaultSetPasswordHandler'.
-- --
-- Since: 1.2.6. -- @since: 1.2.6
setPasswordHandler :: setPasswordHandler ::
Bool Bool
-- ^ Whether the old password is needed. If @True@, a -- ^ Whether the old password is needed. If @True@, a
@ -264,10 +401,40 @@ class ( YesodAuth site
-> AuthHandler site TypedContent -> AuthHandler site TypedContent
setPasswordHandler = defaultSetPasswordHandler setPasswordHandler = defaultSetPasswordHandler
-- | Helper that controls what happens after a user registration
-- request is submitted. This method can be overridden to completely
-- customize what happens during the user registration process,
-- such as for handling additional fields in the registration form.
--
-- The default implementation is in terms of 'defaultRegisterHelper'.
--
-- @since: 1.6.9
registerHelper :: Route Auth
-- ^ Where to sent the user in the event
-- that registration fails
-> AuthHandler site TypedContent
registerHelper = defaultRegisterHelper False False
-- | Helper that controls what happens after a forgot password
-- request is submitted. As with `registerHelper`, this method can
-- be overridden to customize the behavior when a user attempts
-- to recover their password.
--
-- The default implementation is in terms of 'defaultRegisterHelper'.
--
-- @since: 1.6.9
passwordResetHelper :: Route Auth
-- ^ Where to sent the user in the event
-- that the password reset fails
-> AuthHandler site TypedContent
passwordResetHelper = defaultRegisterHelper True True
authEmail :: (YesodAuthEmail m) => AuthPlugin m authEmail :: (YesodAuthEmail m) => AuthPlugin m
authEmail = authEmail =
AuthPlugin "email" dispatch emailLoginHandler AuthPlugin "email" dispatch emailLoginHandler
where where
dispatch :: YesodAuthEmail m => Text -> [Text] -> AuthHandler m TypedContent
dispatch "GET" ["register"] = getRegisterR >>= sendResponse dispatch "GET" ["register"] = getRegisterR >>= sendResponse
dispatch "POST" ["register"] = postRegisterR >>= sendResponse dispatch "POST" ["register"] = postRegisterR >>= sendResponse
dispatch "GET" ["forgot-password"] = getForgotPasswordR >>= sendResponse dispatch "GET" ["forgot-password"] = getForgotPasswordR >>= sendResponse
@ -275,21 +442,31 @@ authEmail =
dispatch "GET" ["verify", eid, verkey] = dispatch "GET" ["verify", eid, verkey] =
case fromPathPiece eid of case fromPathPiece eid of
Nothing -> notFound Nothing -> notFound
Just eid' -> getVerifyR eid' verkey >>= sendResponse Just eid' -> getVerifyR eid' verkey False >>= sendResponse
dispatch "GET" ["verify", eid, verkey, hasSetPass] =
case fromPathPiece eid of
Nothing -> notFound
Just eid' -> getVerifyR eid' verkey (hasSetPass == verifyURLHasSetPassText) >>= sendResponse
dispatch "POST" ["login"] = postLoginR >>= sendResponse dispatch "POST" ["login"] = postLoginR >>= sendResponse
dispatch "GET" ["set-password"] = getPasswordR >>= sendResponse dispatch "GET" ["set-password"] = getPasswordR >>= sendResponse
dispatch "POST" ["set-password"] = postPasswordR >>= sendResponse dispatch "POST" ["set-password"] = postPasswordR >>= sendResponse
dispatch _ _ = notFound dispatch _ _ = notFound
getRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html getRegisterR :: YesodAuthEmail master => AuthHandler master Html
getRegisterR = registerHandler getRegisterR = registerHandler
emailLoginHandler :: YesodAuthEmail master => (Route Auth -> Route master) -> WidgetT master IO () -- | Default implementation of 'emailLoginHandler'.
emailLoginHandler toParent = do --
(widget, enctype) <- liftWidgetT $ generateFormPost loginForm -- @since 1.4.17
defaultEmailLoginHandler
:: YesodAuthEmail master
=> (Route Auth -> Route master)
-> WidgetFor master ()
defaultEmailLoginHandler toParent = do
(widget, enctype) <- generateFormPost loginForm
[whamlet| [whamlet|
<form method="post" action="@{toParent loginR}"> <form method="post" action="@{toParent loginR}" enctype=#{enctype}>
<div id="emailLoginForm"> <div id="emailLoginForm">
^{widget} ^{widget}
<div> <div>
@ -308,7 +485,8 @@ emailLoginHandler toParent = do
passwordMsg <- renderMessage' Msg.Password passwordMsg <- renderMessage' Msg.Password
(passwordRes, passwordView) <- mreq passwordField (passwordSettings passwordMsg) Nothing (passwordRes, passwordView) <- mreq passwordField (passwordSettings passwordMsg) Nothing
let userRes = UserLoginForm <$> emailRes <*> passwordRes let userRes = UserLoginForm Control.Applicative.<$> emailRes
Control.Applicative.<*> passwordRes
let widget = do let widget = do
[whamlet| [whamlet|
#{extra} #{extra}
@ -339,14 +517,15 @@ emailLoginHandler toParent = do
langs <- languages langs <- languages
master <- getYesod master <- getYesod
return $ renderAuthMessage master langs msg return $ renderAuthMessage master langs msg
-- | Default implementation of 'registerHandler'. -- | Default implementation of 'registerHandler'.
-- --
-- Since: 1.2.6 -- @since 1.2.6
defaultRegisterHandler :: YesodAuthEmail master => AuthHandler master Html defaultRegisterHandler :: YesodAuthEmail master => AuthHandler master Html
defaultRegisterHandler = do defaultRegisterHandler = do
(widget, enctype) <- lift $ generateFormPost registrationForm (widget, enctype) <- generateFormPost registrationForm
toParentRoute <- getRouteToParent toParentRoute <- getRouteToParent
lift $ authLayout $ do authLayout $ do
setTitleI Msg.RegisterLong setTitleI Msg.RegisterLong
[whamlet| [whamlet|
<p>_{Msg.EnterEmail} <p>_{Msg.EnterEmail}
@ -377,62 +556,98 @@ defaultRegisterHandler = do
return (userRes, widget) return (userRes, widget)
registerHelper :: YesodAuthEmail master parseRegister :: Value -> Parser (Text, Maybe Text)
=> Bool -- ^ allow usernames? parseRegister = withObject "email" (\obj -> do
email <- obj .: "email"
pass <- obj .:? "password"
return (email, pass))
defaultRegisterHelper :: YesodAuthEmail master
=> Bool -- ^ Allow lookup via username in addition to email
-> Bool -- ^ Set to `True` for forgot password flow, `False` for new account registration
-> Route Auth -> Route Auth
-> HandlerT Auth (HandlerT master IO) TypedContent -> AuthHandler master TypedContent
registerHelper allowUsername dest = do defaultRegisterHelper allowUsername forgotPassword dest = do
y <- lift getYesod y <- getYesod
midentifier <- lookupPostParam "email" checkCsrfHeaderOrParam defaultCsrfHeaderName defaultCsrfParamName
let eidentifier = case midentifier of result <- runInputPostResult $ (,)
<$> ireq textField "email"
<*> iopt textField "password"
creds <- case result of
FormSuccess (iden, pass) -> return $ Just (iden, pass)
_ -> do
(creds :: Result Value) <- parseCheckJsonBody
return $ case creds of
Error _ -> Nothing
Success val -> parseMaybe parseRegister val
let eidentifier = case creds of
Nothing -> Left Msg.NoIdentifierProvided Nothing -> Left Msg.NoIdentifierProvided
Just x Just (x, _)
| Just x' <- Text.Email.Validate.canonicalizeEmail (encodeUtf8 x) -> | Just x' <- Text.Email.Validate.canonicalizeEmail (encodeUtf8 x) ->
Right $ normalizeEmailAddress y $ decodeUtf8With lenientDecode x' Right $ normalizeEmailAddress y $ decodeUtf8With lenientDecode x'
| allowUsername -> Right $ TS.strip x | allowUsername -> Right $ TS.strip x
| otherwise -> Left Msg.InvalidEmailAddress | otherwise -> Left Msg.InvalidEmailAddress
case eidentifier of let mpass = case (forgotPassword, creds) of
Left route -> loginErrorMessageI dest route (False, Just (_, mp)) -> mp
Right identifier -> do _ -> Nothing
mecreds <- lift $ getEmailCreds identifier case eidentifier of
Left failMsg -> loginErrorMessageI dest failMsg
Right identifier -> do
mecreds <- getEmailCreds identifier
registerCreds <- registerCreds <-
case mecreds of case mecreds of
Just (EmailCreds lid _ _ (Just key) email) -> return $ Just (lid, key, email) Just (EmailCreds lid _ verStatus (Just key) email) -> return $ Just (lid, verStatus, key, email)
Just (EmailCreds lid _ _ Nothing email) -> do Just (EmailCreds lid _ verStatus Nothing email) -> do
key <- liftIO $ randomKey y key <- liftIO $ randomKey y
lift $ setVerifyKey lid key setVerifyKey lid key
return $ Just (lid, key, email) return $ Just (lid, verStatus, key, email)
Nothing Nothing
| allowUsername -> return Nothing | allowUsername -> return Nothing
| otherwise -> do | otherwise -> do
key <- liftIO $ randomKey y key <- liftIO $ randomKey y
lid <- lift $ addUnverified identifier key lid <- case mpass of
return $ Just (lid, key, identifier) Just pass -> do
salted <- hashAndSaltPassword pass
addUnverifiedWithPass identifier key salted
_ -> addUnverified identifier key
return $ Just (lid, False, key, identifier)
case registerCreds of case registerCreds of
Nothing -> loginErrorMessageI dest (Msg.IdentifierNotFound identifier) Nothing -> loginErrorMessageI dest (Msg.IdentifierNotFound identifier)
Just (lid, verKey, email) -> do Just creds@(_, False, _, _) -> sendConfirmationEmail creds
Just creds@(_, True, _, _) -> do
if forgotPassword
then sendConfirmationEmail creds
else case emailPreviouslyRegisteredResponse identifier of
Just response -> response
Nothing -> sendConfirmationEmail creds
where sendConfirmationEmail (lid, _, verKey, email) = do
render <- getUrlRender render <- getUrlRender
let verUrl = render $ verifyR (toPathPiece lid) verKey tp <- getRouteToParent
lift $ sendVerifyEmail email verKey verUrl let verUrl = render $ tp $ verifyR (toPathPiece lid) verKey (isJust mpass)
lift $ confirmationEmailSentResponse identifier if forgotPassword
then sendForgotPasswordEmail email verKey verUrl
else sendVerifyEmail email verKey verUrl
confirmationEmailSentResponse identifier
postRegisterR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
postRegisterR = registerHelper False registerR
getForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) Html postRegisterR :: YesodAuthEmail master => AuthHandler master TypedContent
postRegisterR = registerHelper registerR
getForgotPasswordR :: YesodAuthEmail master => AuthHandler master Html
getForgotPasswordR = forgotPasswordHandler getForgotPasswordR = forgotPasswordHandler
-- | Default implementation of 'forgotPasswordHandler'. -- | Default implementation of 'forgotPasswordHandler'.
-- --
-- Since: 1.2.6 -- @since 1.2.6
defaultForgotPasswordHandler :: YesodAuthEmail master => AuthHandler master Html defaultForgotPasswordHandler :: YesodAuthEmail master => AuthHandler master Html
defaultForgotPasswordHandler = do defaultForgotPasswordHandler = do
(widget, enctype) <- lift $ generateFormPost forgotPasswordForm (widget, enctype) <- generateFormPost forgotPasswordForm
toParent <- getRouteToParent toParent <- getRouteToParent
lift $ authLayout $ do authLayout $ do
setTitleI Msg.PasswordResetTitle setTitleI Msg.PasswordResetTitle
[whamlet| [whamlet|
<p>_{Msg.PasswordResetPrompt} <p>_{Msg.PasswordResetPrompt}
@ -463,35 +678,45 @@ defaultForgotPasswordHandler = do
fsAttrs = [("autofocus", "")] fsAttrs = [("autofocus", "")]
} }
postForgotPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent postForgotPasswordR :: YesodAuthEmail master => AuthHandler master TypedContent
postForgotPasswordR = registerHelper True forgotPasswordR postForgotPasswordR = passwordResetHelper forgotPasswordR
getVerifyR :: YesodAuthEmail site getVerifyR :: YesodAuthEmail site
=> AuthEmailId site => AuthEmailId site
-> Text -> Text
-> HandlerT Auth (HandlerT site IO) TypedContent -> Bool
getVerifyR lid key = do -> AuthHandler site TypedContent
realKey <- lift $ getVerifyKey lid getVerifyR lid key hasSetPass = do
memail <- lift $ getEmail lid realKey <- getVerifyKey lid
mr <- lift getMessageRender memail <- getEmail lid
mr <- getMessageRender
case (realKey == Just key, memail) of case (realKey == Just key, memail) of
(True, Just email) -> do (True, Just email) -> do
muid <- lift $ verifyAccount lid muid <- verifyAccount lid
case muid of case muid of
Nothing -> invalidKey mr Nothing -> invalidKey mr
Just uid -> do Just uid -> do
lift $ setCreds False $ Creds "email-verify" email [("verifiedEmail", email)] -- FIXME uid? setCreds False $ Creds "email-verify" email [("verifiedEmail", email)] -- FIXME uid?
lift $ setLoginLinkKey uid setLoginLinkKey uid
let msgAv = Msg.AddressVerified let msgAv = if hasSetPass
then Msg.EmailVerified
else Msg.EmailVerifiedChangePass
selectRep $ do selectRep $ do
provideRep $ do provideRep $ do
lift $ addMessageI "success" msgAv addMessageI "success" msgAv
fmap asHtml $ redirect setpassR redirectRoute <- if hasSetPass
then do
y <- getYesod
return $ afterVerificationWithPass y
else do
tp <- getRouteToParent
return $ tp setpassR
fmap asHtml $ redirect redirectRoute
provideJsonMessage $ mr msgAv provideJsonMessage $ mr msgAv
_ -> invalidKey mr _ -> invalidKey mr
where where
msgIk = Msg.InvalidKey msgIk = Msg.InvalidKey
invalidKey mr = messageJson401 (mr msgIk) $ lift $ authLayout $ do invalidKey mr = messageJson401 (mr msgIk) $ authLayout $ do
setTitleI msgIk setTitleI msgIk
[whamlet| [whamlet|
$newline never $newline never
@ -499,67 +724,87 @@ $newline never
|] |]
postLoginR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent parseCreds :: Value -> Parser (Text, Text)
parseCreds = withObject "creds" (\obj -> do
email' <- obj .: "email"
pass <- obj .: "password"
return (email', pass))
postLoginR :: YesodAuthEmail master => AuthHandler master TypedContent
postLoginR = do postLoginR = do
(identifier, pass) <- lift $ runInputPost $ (,) result <- runInputPostResult $ (,)
<$> ireq textField "email" <$> ireq textField "email"
<*> ireq textField "password" <*> ireq textField "password"
mecreds <- lift $ getEmailCreds identifier
midentifier <- case result of
FormSuccess (iden, pass) -> return $ Just (iden, pass)
_ -> do
(creds :: Result Value) <- parseCheckJsonBody
case creds of
Error _ -> return Nothing
Success val -> return $ parseMaybe parseCreds val
case midentifier of
Nothing -> loginErrorMessageI LoginR Msg.NoIdentifierProvided
Just (identifier, pass) -> do
mecreds <- getEmailCreds identifier
maid <- maid <-
case ( mecreds >>= emailCredsAuthId case ( mecreds >>= emailCredsAuthId
, emailCredsEmail <$> mecreds , emailCredsEmail <$> mecreds
, emailCredsStatus <$> mecreds , emailCredsStatus <$> mecreds
) of ) of
(Just aid, Just email, Just True) -> do (Just aid, Just email', Just True) -> do
mrealpass <- lift $ getPassword aid mrealpass <- getPassword aid
case mrealpass of case mrealpass of
Nothing -> return Nothing Nothing -> return Nothing
Just realpass -> return $ Just realpass -> do
if isValidPass pass realpass passValid <- verifyPassword pass realpass
then Just email return $ if passValid
then Just email'
else Nothing else Nothing
_ -> return Nothing _ -> return Nothing
let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier let isEmail = Text.Email.Validate.isValid $ encodeUtf8 identifier
case maid of case maid of
Just email -> Just email' ->
lift $ setCredsRedirect $ Creds setCredsRedirect $ Creds
(if isEmail then "email" else "username") (if isEmail then "email" else "username")
email email'
[("verifiedEmail", email)] [("verifiedEmail", email')]
Nothing -> Nothing ->
loginErrorMessageI LoginR $ loginErrorMessageI LoginR $
if isEmail if isEmail
then Msg.InvalidEmailPass then Msg.InvalidEmailPass
else Msg.InvalidUsernamePass else Msg.InvalidUsernamePass
getPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent getPasswordR :: YesodAuthEmail master => AuthHandler master TypedContent
getPasswordR = do getPasswordR = do
maid <- lift maybeAuthId maid <- maybeAuthId
case maid of case maid of
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
Just _ -> do Just aid -> do
needOld <- maybe (return True) (lift . needOldPassword) maid needOld <- needOldPassword aid
setPasswordHandler needOld setPasswordHandler needOld
-- | Default implementation of 'setPasswordHandler'. -- | Default implementation of 'setPasswordHandler'.
-- --
-- Since: 1.2.6 -- @since 1.2.6
defaultSetPasswordHandler :: YesodAuthEmail master => Bool -> AuthHandler master TypedContent defaultSetPasswordHandler :: YesodAuthEmail master => Bool -> AuthHandler master TypedContent
defaultSetPasswordHandler needOld = do defaultSetPasswordHandler needOld = do
messageRender <- lift getMessageRender messageRender <- getMessageRender
toParent <- getRouteToParent toParent <- getRouteToParent
selectRep $ do selectRep $ do
provideJsonMessage $ messageRender Msg.SetPass provideJsonMessage $ messageRender Msg.SetPass
provideRep $ lift $ authLayout $ do provideRep $ authLayout $ do
(widget, enctype) <- liftWidgetT $ generateFormPost $ setPasswordForm needOld (widget, enctype) <- generateFormPost setPasswordForm
setTitleI Msg.SetPassTitle setTitleI Msg.SetPassTitle
[whamlet| [whamlet|
<h3>_{Msg.SetPass} <h3>_{Msg.SetPass}
<form method="post" action="@{toParent setpassR}"> <form method="post" action="@{toParent setpassR}" enctype=#{enctype}>
^{widget} ^{widget}
|] |]
where where
setPasswordForm needOld extra = do setPasswordForm extra = do
(currentPasswordRes, currentPasswordView) <- mreq passwordField currentPasswordSettings Nothing (currentPasswordRes, currentPasswordView) <- mreq passwordField currentPasswordSettings Nothing
(newPasswordRes, newPasswordView) <- mreq passwordField newPasswordSettings Nothing (newPasswordRes, newPasswordView) <- mreq passwordField newPasswordSettings Nothing
(confirmPasswordRes, confirmPasswordView) <- mreq passwordField confirmPasswordSettings Nothing (confirmPasswordRes, confirmPasswordView) <- mreq passwordField confirmPasswordSettings Nothing
@ -616,53 +861,82 @@ defaultSetPasswordHandler needOld = do
fsAttrs = [("autofocus", "")] fsAttrs = [("autofocus", "")]
} }
parsePassword :: Value -> Parser (Text, Text, Maybe Text)
parsePassword = withObject "password" (\obj -> do
email' <- obj .: "new"
pass <- obj .: "confirm"
curr <- obj .:? "current"
return (email', pass, curr))
postPasswordR :: YesodAuthEmail master => AuthHandler master TypedContent
postPasswordR :: YesodAuthEmail master => HandlerT Auth (HandlerT master IO) TypedContent
postPasswordR = do postPasswordR = do
maid <- lift maybeAuthId maid <- maybeAuthId
(creds :: Result Value) <- parseCheckJsonBody
let jcreds = case creds of
Error _ -> Nothing
Success val -> parseMaybe parsePassword val
let doJsonParsing = isJust jcreds
case maid of case maid of
Nothing -> loginErrorMessageI LoginR Msg.BadSetPass Nothing -> loginErrorMessageI LoginR Msg.BadSetPass
Just aid -> do Just aid -> do
tm <- getRouteToParent tm <- getRouteToParent
needOld <- needOldPassword aid
needOld <- lift $ needOldPassword aid if not needOld then confirmPassword aid tm jcreds else do
if not needOld then confirmPassword aid tm else do res <- runInputPostResult $ ireq textField "current"
current <- lift $ runInputPost $ ireq textField "current" let fcurrent = case res of
mrealpass <- lift $ getPassword aid FormSuccess currentPass -> Just currentPass
case mrealpass of _ -> Nothing
Nothing -> let current = if doJsonParsing
lift $ loginErrorMessage (tm setpassR) "You do not currently have a password set on your account" then getThird jcreds
Just realpass else fcurrent
| isValidPass current realpass -> confirmPassword aid tm mrealpass <- getPassword aid
| otherwise -> case (mrealpass, current) of
lift $ loginErrorMessage (tm setpassR) "Invalid current password, please try again" (Nothing, _) ->
loginErrorMessage (tm setpassR) "You do not currently have a password set on your account"
(_, Nothing) ->
loginErrorMessageI LoginR Msg.BadSetPass
(Just realpass, Just current') -> do
passValid <- verifyPassword current' realpass
if passValid
then confirmPassword aid tm jcreds
else loginErrorMessage (tm setpassR) "Invalid current password, please try again"
where where
msgOk = Msg.PassUpdated msgOk = Msg.PassUpdated
confirmPassword aid tm = do getThird (Just (_,_,t)) = t
(new, confirm) <- lift $ runInputPost $ (,) getThird Nothing = Nothing
getNewConfirm (Just (a,b,_)) = Just (a,b)
getNewConfirm _ = Nothing
confirmPassword aid tm jcreds = do
res <- runInputPostResult $ (,)
<$> ireq textField "new" <$> ireq textField "new"
<*> ireq textField "confirm" <*> ireq textField "confirm"
let creds = if (isJust jcreds)
then getNewConfirm jcreds
else case res of
FormSuccess res' -> Just res'
_ -> Nothing
case creds of
Nothing -> loginErrorMessageI setpassR Msg.PassMismatch
Just (new, confirm) ->
if new /= confirm if new /= confirm
then loginErrorMessageI setpassR Msg.PassMismatch then loginErrorMessageI setpassR Msg.PassMismatch
else do else do
isSecure <- lift $ checkPasswordSecurity aid new isSecure <- checkPasswordSecurity aid new
case isSecure of case isSecure of
Left e -> lift $ loginErrorMessage (tm setpassR) e Left e -> loginErrorMessage (tm setpassR) e
Right () -> do Right () -> do
salted <- liftIO $ saltPass new salted <- hashAndSaltPassword new
y <- lift $ do y <- do
setPassword aid salted setPassword aid salted
deleteSession loginLinkKey deleteSession loginLinkKey
addMessageI "success" msgOk addMessageI "success" msgOk
getYesod getYesod
mr <- lift getMessageRender mr <- getMessageRender
selectRep $ do selectRep $ do
provideRep $ provideRep $
fmap asHtml $ lift $ redirect $ afterPasswordRoute y fmap asHtml $ redirect $ afterPasswordRoute y
provideJsonMessage (mr msgOk) provideJsonMessage (mr msgOk)
saltLength :: Int saltLength :: Int
@ -676,7 +950,7 @@ saltPass = fmap (decodeUtf8With lenientDecode)
saltPass' :: String -> String -> String saltPass' :: String -> String -> String
saltPass' salt pass = saltPass' salt pass =
salt ++ T.unpack (TE.decodeUtf8 $ B16.encode $ H.hash $ TE.encodeUtf8 $ T.pack $ salt ++ pass) salt ++ T.unpack (TE.decodeUtf8 $ B16.encode $ convert (H.hash (TE.encodeUtf8 $ T.pack $ salt ++ pass) :: H.Digest H.MD5))
isValidPass :: Text -- ^ cleartext password isValidPass :: Text -- ^ cleartext password
-> SaltedPass -- ^ salted password -> SaltedPass -- ^ salted password
@ -697,19 +971,21 @@ isValidPass' clear' salted' =
-- | Session variable set when user logged in via a login link. See -- | Session variable set when user logged in via a login link. See
-- 'needOldPassword'. -- 'needOldPassword'.
-- --
-- Since 1.2.1 -- @since 1.2.1
loginLinkKey :: Text loginLinkKey :: Text
loginLinkKey = "_AUTH_EMAIL_LOGIN_LINK" loginLinkKey = "_AUTH_EMAIL_LOGIN_LINK"
-- | Set 'loginLinkKey' to the current time. -- | Set 'loginLinkKey' to the current time.
-- --
-- Since 1.2.1 -- @since 1.2.1
setLoginLinkKey :: (YesodAuthEmail site, MonadHandler m, HandlerSite m ~ site) => AuthId site -> m () --setLoginLinkKey :: (MonadHandler m) => AuthId site -> m ()
setLoginLinkKey :: (MonadHandler m, YesodAuthEmail (HandlerSite m))
=> AuthId (HandlerSite m)
-> m ()
setLoginLinkKey aid = do setLoginLinkKey aid = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
setSession loginLinkKey $ TS.pack $ show (toPathPiece aid, now) setSession loginLinkKey $ TS.pack $ show (toPathPiece aid, now)
-- See https://github.com/yesodweb/yesod/issues/1245 for discussion on this -- See https://github.com/yesodweb/yesod/issues/1245 for discussion on this
-- use of unsafePerformIO. -- use of unsafePerformIO.
defaultNonceGen :: Nonce.Generator defaultNonceGen :: Nonce.Generator

View File

@ -1,89 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE RankNTypes #-}
-- | Use an email address as an identifier via Google's OpenID login system.
--
-- This backend will not use the OpenID identifier at all. It only uses OpenID
-- as a login system. By using this plugin, you are trusting Google to validate
-- an email address, and requiring users to have a Google account. On the plus
-- side, you get to use email addresses as the identifier, many users have
-- existing Google accounts, the login system has been long tested (as opposed
-- to BrowserID), and it requires no credential managing or setup (as opposed
-- to Email).
module Yesod.Auth.GoogleEmail
{-# DEPRECATED "Google no longer provides OpenID support, please use Yesod.Auth.GoogleEmail2" #-}
( authGoogleEmail
, forwardUrl
) where
import Yesod.Auth
import qualified Web.Authenticate.OpenId as OpenId
import Yesod.Core
import Data.Text (Text)
import qualified Yesod.Auth.Message as Msg
import qualified Data.Text as T
import Control.Exception.Lifted (try, SomeException)
pid :: Text
pid = "googleemail"
forwardUrl :: AuthRoute
forwardUrl = PluginR pid ["forward"]
googleIdent :: Text
googleIdent = "https://www.google.com/accounts/o8/id"
authGoogleEmail :: YesodAuth m => AuthPlugin m
authGoogleEmail =
AuthPlugin pid dispatch login
where
complete = PluginR pid ["complete"]
login tm =
[whamlet|<a href=@{tm forwardUrl}>_{Msg.LoginGoogle}|]
dispatch "GET" ["forward"] = do
render <- getUrlRender
let complete' = render complete
master <- lift getYesod
eres <- lift $ try $ OpenId.getForwardUrl googleIdent complete' Nothing
[ ("openid.ax.type.email", "http://schema.openid.net/contact/email")
, ("openid.ns.ax", "http://openid.net/srv/ax/1.0")
, ("openid.ns.ax.required", "email")
, ("openid.ax.mode", "fetch_request")
, ("openid.ax.required", "email")
, ("openid.ui.icon", "true")
] (authHttpManager master)
either
(\err -> do
tm <- getRouteToParent
lift $ loginErrorMessage (tm LoginR) $ T.pack $ show (err :: SomeException))
redirect
eres
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
dispatch "GET" ["complete"] = do
rr <- getRequest
completeHelper $ reqGetParams rr
dispatch "POST" ["complete", ""] = dispatch "POST" ["complete"] -- compatibility issues
dispatch "POST" ["complete"] = do
(posts, _) <- runRequestBody
completeHelper posts
dispatch _ _ = notFound
completeHelper :: YesodAuth master => [(Text, Text)] -> AuthHandler master TypedContent
completeHelper gets' = do
master <- lift getYesod
eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master)
tm <- getRouteToParent
either (onFailure tm) (onSuccess tm) eres
where
onFailure tm err =
lift $ loginErrorMessage (tm LoginR) $ T.pack $ show (err :: SomeException)
onSuccess tm oir = do
let OpenId.Identifier ident = OpenId.oirOpLocal oir
memail <- lookupGetParam "openid.ext1.value.email"
case (memail, "https://www.google.com/accounts/o8/id" `T.isPrefixOf` ident) of
(Just email, True) -> lift $ setCredsRedirect $ Creds pid email []
(_, False) -> lift $ loginErrorMessage (tm LoginR) "Only Google login is supported"
(Nothing, _) -> lift $ loginErrorMessage (tm LoginR) "No email address provided"

View File

@ -1,6 +1,9 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
-- | Use an email address as an identifier via Google's login system. -- | Use an email address as an identifier via Google's login system.
-- --
-- Note that this is a replacement for "Yesod.Auth.GoogleEmail", which depends -- Note that this is a replacement for "Yesod.Auth.GoogleEmail", which depends
@ -21,8 +24,9 @@
-- --
-- * Enable the Google+ API. -- * Enable the Google+ API.
-- --
-- Since 1.3.1 -- @since 1.3.1
module Yesod.Auth.GoogleEmail2 module Yesod.Auth.GoogleEmail2
{-# DEPRECATED "Google+ is being shut down, please migrate to Google Sign-in https://pbrisbin.com/posts/googleemail2_deprecation/" #-}
( -- * Authentication handlers ( -- * Authentication handlers
authGoogleEmail authGoogleEmail
, authGoogleEmailSaveToken , authGoogleEmailSaveToken
@ -45,22 +49,24 @@ module Yesod.Auth.GoogleEmail2
, Place(..) , Place(..)
, Email(..) , Email(..)
, EmailType(..) , EmailType(..)
-- * Other functions
, pid
) where ) where
import Yesod.Auth (Auth, AuthPlugin (AuthPlugin), import Yesod.Auth (Auth, AuthHandler,
AuthPlugin (AuthPlugin),
AuthRoute, Creds (Creds), AuthRoute, Creds (Creds),
Route (PluginR), YesodAuth, Route (PluginR), YesodAuth,
runHttpRequest, setCredsRedirect, logoutDest, runHttpRequest,
logoutDest) setCredsRedirect)
import qualified Yesod.Auth.Message as Msg import qualified Yesod.Auth.Message as Msg
import Yesod.Core (HandlerSite, HandlerT, MonadHandler, import Yesod.Core (HandlerSite, MonadHandler,
TypedContent, getRouteToParent, TypedContent, addMessage,
getUrlRender, invalidArgs, getRouteToParent, getUrlRender,
lift, liftIO, lookupGetParam, getYesod, invalidArgs, liftIO,
liftSubHandler, lookupGetParam,
lookupSession, notFound, redirect, lookupSession, notFound, redirect,
setSession, whamlet, (.:), setSession, toHtml, whamlet, (.:))
addMessage, getYesod, authRoute,
toHtml)
import Blaze.ByteString.Builder (fromByteString, toByteString) import Blaze.ByteString.Builder (fromByteString, toByteString)
@ -71,13 +77,16 @@ import Control.Monad.IO.Class (MonadIO)
import qualified Crypto.Nonce as Nonce import qualified Crypto.Nonce as Nonce
import Data.Aeson ((.:?)) import Data.Aeson ((.:?))
import qualified Data.Aeson as A import qualified Data.Aeson as A
#if MIN_VERSION_aeson(1,0,0)
import qualified Data.Aeson.Text as A
#else
import qualified Data.Aeson.Encode as A import qualified Data.Aeson.Encode as A
#endif
import Data.Aeson.Parser (json') import Data.Aeson.Parser (json')
import Data.Aeson.Types (FromJSON (parseJSON), parseEither, import Data.Aeson.Types (FromJSON (parseJSON), parseEither,
parseMaybe, withObject, withText) parseMaybe, withObject, withText)
import Data.Conduit (($$+-), ($$)) import Data.Conduit
import Data.Conduit.Attoparsec (sinkParser) import Data.Conduit.Attoparsec (sinkParser)
import qualified Data.HashMap.Strict as M
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Monoid (mappend) import Data.Monoid (mappend)
import Data.Text (Text) import Data.Text (Text)
@ -85,14 +94,26 @@ import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TL import qualified Data.Text.Lazy.Builder as TL
import Network.HTTP.Client (Manager, parseUrl, requestHeaders, import Network.HTTP.Client (Manager, requestHeaders,
responseBody, urlEncodedBody) responseBody, urlEncodedBody)
import qualified Network.HTTP.Client as HTTP
import Network.HTTP.Client.Conduit (Request, bodyReaderSource) import Network.HTTP.Client.Conduit (Request, bodyReaderSource)
import Network.HTTP.Conduit (http) import Network.HTTP.Conduit (http)
import Network.HTTP.Types (renderQueryText) import Network.HTTP.Types (renderQueryText)
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
#if MIN_VERSION_aeson(2, 0, 0)
import qualified Data.Aeson.Key
import qualified Data.Aeson.KeyMap
#else
import qualified Data.HashMap.Strict as M
#endif
-- | Plugin identifier. This is used to identify the plugin used for
-- authentication. The 'credsPlugin' will contain this value when this
-- plugin is used for authentication.
-- @since 1.4.17
pid :: Text pid :: Text
pid = "googleemail2" pid = "googleemail2"
@ -133,7 +154,7 @@ authGoogleEmail = authPlugin False
-- | An alternative version which stores user access token in the session -- | An alternative version which stores user access token in the session
-- variable. Use it if you want to request user's profile from your app. -- variable. Use it if you want to request user's profile from your app.
-- --
-- Since 1.4.3 -- @since 1.4.3
authGoogleEmailSaveToken :: YesodAuth m authGoogleEmailSaveToken :: YesodAuth m
=> Text -- ^ client ID => Text -- ^ client ID
-> Text -- ^ client secret -> Text -- ^ client secret
@ -167,7 +188,7 @@ authPlugin storeToken clientID clientSecret =
return $ decodeUtf8 return $ decodeUtf8
$ toByteString $ toByteString
$ fromByteString "https://accounts.google.com/o/oauth2/auth" $ fromByteString "https://accounts.google.com/o/oauth2/auth"
`mappend` renderQueryText True qs `Data.Monoid.mappend` renderQueryText True qs
login tm = do login tm = do
[whamlet|<a href=@{tm forwardUrl}>_{Msg.LoginGoogle}|] [whamlet|<a href=@{tm forwardUrl}>_{Msg.LoginGoogle}|]
@ -175,10 +196,10 @@ authPlugin storeToken clientID clientSecret =
dispatch :: YesodAuth site dispatch :: YesodAuth site
=> Text => Text
-> [Text] -> [Text]
-> HandlerT Auth (HandlerT site IO) TypedContent -> AuthHandler site TypedContent
dispatch "GET" ["forward"] = do dispatch "GET" ["forward"] = do
tm <- getRouteToParent tm <- getRouteToParent
lift (getDest tm) >>= redirect getDest tm >>= redirect
dispatch "GET" ["complete"] = do dispatch "GET" ["complete"] = do
mstate <- lookupGetParam "state" mstate <- lookupGetParam "state"
@ -195,24 +216,27 @@ authPlugin storeToken clientID clientSecret =
case merr of case merr of
Nothing -> invalidArgs ["Missing code paramter"] Nothing -> invalidArgs ["Missing code paramter"]
Just err -> do Just err -> do
master <- lift getYesod master <- getYesod
let msg = let msg =
case err of case err of
"access_denied" -> "Access denied" "access_denied" -> "Access denied"
_ -> "Unknown error occurred: " `T.append` err _ -> "Unknown error occurred: " `T.append` err
addMessage "error" $ toHtml msg addMessage "error" $ toHtml msg
lift $ redirect $ logoutDest master redirect $ logoutDest master
Just c -> return c Just c -> return c
render <- getUrlRender render <- getUrlRender
tm <- getRouteToParent
req' <- liftIO $ parseUrl "https://accounts.google.com/o/oauth2/token" -- FIXME don't hardcode, use: https://accounts.google.com/.well-known/openid-configuration req' <- liftIO $
HTTP.parseUrlThrow
"https://accounts.google.com/o/oauth2/token" -- FIXME don't hardcode, use: https://accounts.google.com/.well-known/openid-configuration
let req = let req =
urlEncodedBody urlEncodedBody
[ ("code", encodeUtf8 code) [ ("code", encodeUtf8 code)
, ("client_id", encodeUtf8 clientID) , ("client_id", encodeUtf8 clientID)
, ("client_secret", encodeUtf8 clientSecret) , ("client_secret", encodeUtf8 clientSecret)
, ("redirect_uri", encodeUtf8 $ render complete) , ("redirect_uri", encodeUtf8 $ render $ tm complete)
, ("grant_type", "authorization_code") , ("grant_type", "authorization_code")
] ]
req' req'
@ -229,7 +253,9 @@ authPlugin storeToken clientID clientSecret =
-- User's access token is saved for further access to API -- User's access token is saved for further access to API
when storeToken $ setSession accessTokenKey accessToken' when storeToken $ setSession accessTokenKey accessToken'
personValue <- makeHttpRequest =<< personValueRequest token personValReq <- personValueRequest token
personValue <- makeHttpRequest personValReq
person <- case parseEither parseJSON personValue of person <- case parseEither parseJSON personValue of
Left e -> error e Left e -> error e
Right x -> return x Right x -> return x
@ -239,32 +265,31 @@ authPlugin storeToken clientID clientSecret =
[e] -> return e [e] -> return e
[] -> error "No account email" [] -> error "No account email"
x -> error $ "Too many account emails: " ++ show x x -> error $ "Too many account emails: " ++ show x
lift $ setCredsRedirect $ Creds pid email $ allPersonInfo personValue setCredsRedirect $ Creds pid email $ allPersonInfo personValue
dispatch _ _ = notFound dispatch _ _ = notFound
makeHttpRequest makeHttpRequest :: Request -> AuthHandler site A.Value
:: (YesodAuth site) makeHttpRequest req =
=> Request liftSubHandler $ runHttpRequest req $ \res ->
-> HandlerT Auth (HandlerT site IO) A.Value runConduit $ bodyReaderSource (responseBody res) .| sinkParser json'
makeHttpRequest req = lift $
runHttpRequest req $ \res -> bodyReaderSource (responseBody res) $$ sinkParser json'
-- | Allows to fetch information about a user from Google's API. -- | Allows to fetch information about a user from Google's API.
-- In case of parsing error returns 'Nothing'. -- In case of parsing error returns 'Nothing'.
-- Will throw 'HttpException' in case of network problems or error response code. -- Will throw 'HttpException' in case of network problems or error response code.
-- --
-- Since 1.4.3 -- @since 1.4.3
getPerson :: Manager -> Token -> HandlerT site IO (Maybe Person) getPerson :: MonadHandler m => Manager -> Token -> m (Maybe Person)
getPerson manager token = parseMaybe parseJSON <$> (do getPerson manager token = liftSubHandler $ parseMaybe parseJSON <$> (do
req <- personValueRequest token req <- personValueRequest token
res <- http req manager res <- http req manager
responseBody res $$+- sinkParser json' runConduit $ responseBody res .| sinkParser json'
) )
personValueRequest :: MonadIO m => Token -> m Request personValueRequest :: MonadIO m => Token -> m Request
personValueRequest token = do personValueRequest token = do
req2' <- liftIO $ parseUrl "https://www.googleapis.com/plus/v1/people/me" req2' <- liftIO
$ HTTP.parseUrlThrow "https://www.googleapis.com/plus/v1/people/me"
return req2' return req2'
{ requestHeaders = { requestHeaders =
[ ("Authorization", encodeUtf8 $ "Bearer " `mappend` accessToken token) [ ("Authorization", encodeUtf8 $ "Bearer " `mappend` accessToken token)
@ -277,20 +302,20 @@ personValueRequest token = do
-- 'authGoogleEmailSaveToken'. -- 'authGoogleEmailSaveToken'.
-- You can acquire saved token with 'getUserAccessToken'. -- You can acquire saved token with 'getUserAccessToken'.
-- --
-- Since 1.4.3 -- @since 1.4.3
data Token = Token { accessToken :: Text data Token = Token { accessToken :: Text
, tokenType :: Text , tokenType :: Text
} deriving (Show, Eq) } deriving (Show, Eq)
instance FromJSON Token where instance FromJSON Token where
parseJSON = withObject "Tokens" $ \o -> Token parseJSON = withObject "Tokens" $ \o -> Token
<$> o .: "access_token" Control.Applicative.<$> o .: "access_token"
<*> o .: "token_type" Control.Applicative.<*> o .: "token_type"
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Gender of the person -- | Gender of the person
-- --
-- Since 1.4.3 -- @since 1.4.3
data Gender = Male | Female | OtherGender deriving (Show, Eq) data Gender = Male | Female | OtherGender deriving (Show, Eq)
instance FromJSON Gender where instance FromJSON Gender where
@ -302,7 +327,7 @@ instance FromJSON Gender where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | URIs specified in the person's profile -- | URIs specified in the person's profile
-- --
-- Since 1.4.3 -- @since 1.4.3
data PersonURI = data PersonURI =
PersonURI { uriLabel :: Maybe Text PersonURI { uriLabel :: Maybe Text
, uriValue :: Maybe Text , uriValue :: Maybe Text
@ -317,7 +342,7 @@ instance FromJSON PersonURI where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | The type of URI -- | The type of URI
-- --
-- Since 1.4.3 -- @since 1.4.3
data PersonURIType = OtherProfile -- ^ URI for another profile data PersonURIType = OtherProfile -- ^ URI for another profile
| Contributor -- ^ URI to a site for which this person is a contributor | Contributor -- ^ URI to a site for which this person is a contributor
| Website -- ^ URI for this Google+ Page's primary website | Website -- ^ URI for this Google+ Page's primary website
@ -336,7 +361,7 @@ instance FromJSON PersonURIType where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Current or past organizations with which this person is associated -- | Current or past organizations with which this person is associated
-- --
-- Since 1.4.3 -- @since 1.4.3
data Organization = data Organization =
Organization { orgName :: Maybe Text Organization { orgName :: Maybe Text
-- ^ The person's job title or role within the organization -- ^ The person's job title or role within the organization
@ -363,7 +388,7 @@ instance FromJSON Organization where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | The type of an organization -- | The type of an organization
-- --
-- Since 1.4.3 -- @since 1.4.3
data OrganizationType = Work data OrganizationType = Work
| School | School
| OrganizationType Text -- ^ Something else | OrganizationType Text -- ^ Something else
@ -377,7 +402,7 @@ instance FromJSON OrganizationType where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | A place where the person has lived or is living at the moment. -- | A place where the person has lived or is living at the moment.
-- --
-- Since 1.4.3 -- @since 1.4.3
data Place = data Place =
Place { -- | A place where this person has lived. For example: "Seattle, WA", "Near Toronto". Place { -- | A place where this person has lived. For example: "Seattle, WA", "Near Toronto".
placeValue :: Maybe Text placeValue :: Maybe Text
@ -391,7 +416,7 @@ instance FromJSON Place where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Individual components of a name -- | Individual components of a name
-- --
-- Since 1.4.3 -- @since 1.4.3
data Name = data Name =
Name { -- | The full name of this person, including middle names, suffixes, etc Name { -- | The full name of this person, including middle names, suffixes, etc
nameFormatted :: Maybe Text nameFormatted :: Maybe Text
@ -418,7 +443,7 @@ instance FromJSON Name where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | The person's relationship status. -- | The person's relationship status.
-- --
-- Since 1.4.3 -- @since 1.4.3
data RelationshipStatus = Single -- ^ Person is single data RelationshipStatus = Single -- ^ Person is single
| InRelationship -- ^ Person is in a relationship | InRelationship -- ^ Person is in a relationship
| Engaged -- ^ Person is engaged | Engaged -- ^ Person is engaged
@ -447,7 +472,7 @@ instance FromJSON RelationshipStatus where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | The URI of the person's profile photo. -- | The URI of the person's profile photo.
-- --
-- Since 1.4.3 -- @since 1.4.3
newtype PersonImage = PersonImage { imageUri :: Text } deriving (Show, Eq) newtype PersonImage = PersonImage { imageUri :: Text } deriving (Show, Eq)
instance FromJSON PersonImage where instance FromJSON PersonImage where
@ -457,7 +482,7 @@ instance FromJSON PersonImage where
-- the image under the URI. If for some reason you need to modify the query -- the image under the URI. If for some reason you need to modify the query
-- part, you should do it after resizing. -- part, you should do it after resizing.
-- --
-- Since 1.4.3 -- @since 1.4.3
resizePersonImage :: PersonImage -> Int -> PersonImage resizePersonImage :: PersonImage -> Int -> PersonImage
resizePersonImage (PersonImage uri) size = resizePersonImage (PersonImage uri) size =
PersonImage $ uri `mappend` "?sz=" `mappend` T.pack (show size) PersonImage $ uri `mappend` "?sz=" `mappend` T.pack (show size)
@ -466,7 +491,7 @@ resizePersonImage (PersonImage uri) size =
-- | Information about the user -- | Information about the user
-- Full description of the resource https://developers.google.com/+/api/latest/people -- Full description of the resource https://developers.google.com/+/api/latest/people
-- --
-- Since 1.4.3 -- @since 1.4.3
data Person = Person data Person = Person
{ personId :: Text { personId :: Text
-- | The name of this person, which is suitable for display -- | The name of this person, which is suitable for display
@ -536,7 +561,7 @@ instance FromJSON Person where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Person's email -- | Person's email
-- --
-- Since 1.4.3 -- @since 1.4.3
data Email = Email data Email = Email
{ emailValue :: Text { emailValue :: Text
, emailType :: EmailType , emailType :: EmailType
@ -551,7 +576,7 @@ instance FromJSON Email where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | Type of email -- | Type of email
-- --
-- Since 1.4.3 -- @since 1.4.3
data EmailType = EmailAccount -- ^ Google account email address data EmailType = EmailAccount -- ^ Google account email address
| EmailHome -- ^ Home email address | EmailHome -- ^ Home email address
| EmailWork -- ^ Work email adress | EmailWork -- ^ Work email adress
@ -568,9 +593,19 @@ instance FromJSON EmailType where
_ -> EmailType t _ -> EmailType t
allPersonInfo :: A.Value -> [(Text, Text)] allPersonInfo :: A.Value -> [(Text, Text)]
allPersonInfo (A.Object o) = map enc $ M.toList o allPersonInfo (A.Object o) = map enc $ mapToList o
where enc (key, A.String s) = (key, s) where
enc (key, v) = (key, TL.toStrict $ TL.toLazyText $ A.encodeToTextBuilder v) enc (key, A.String s) = (keyToText key, s)
enc (key, v) = (keyToText key, TL.toStrict $ TL.toLazyText $ A.encodeToTextBuilder v)
#if MIN_VERSION_aeson(2, 0, 0)
keyToText = Data.Aeson.Key.toText
mapToList = Data.Aeson.KeyMap.toList
#else
keyToText = id
mapToList = M.toList
#endif
allPersonInfo _ = [] allPersonInfo _ = []

View File

@ -52,7 +52,7 @@ be unique).
'AuthId' must have an instance of 'PathPiece' class, this is needed to store 'AuthId' must have an instance of 'PathPiece' class, this is needed to store
user identifier in session (this happens in 'setCreds' and 'setCredsRedirect' user identifier in session (this happens in 'setCreds' and 'setCredsRedirect'
actions) and to read that identifier from session (this happens in actions) and to read that identifier from session (this happens in
`dafaultMaybeAuthId` action). So we have to define it: `defaultMaybeAuthId` action). So we have to define it:
@ @
import Text.Read (readMaybe) import Text.Read (readMaybe)
@ -85,7 +85,7 @@ Here @lookupUser@ is just a helper function to lookup hardcoded users by name:
@ @
lookupUser :: Text -> Maybe SiteManager lookupUser :: Text -> Maybe SiteManager
lookupUser username = find (\m -> manUserName m == username) siteManagers lookupUser username = find (\\m -> manUserName m == username) siteManagers
@ @
@ -113,7 +113,7 @@ instance YesodAuthHardcoded App where
validPassword :: Text -> Text -> Bool validPassword :: Text -> Text -> Bool
validPassword u p = validPassword u p =
case find (\m -> manUserName m == u && manPassWord m == p) siteManagers of case find (\\m -> manUserName m == u && manPassWord m == p) siteManagers of
Just _ -> True Just _ -> True
_ -> False _ -> False
@ @
@ -131,7 +131,7 @@ module Yesod.Auth.Hardcoded
, loginR ) , loginR )
where where
import Yesod.Auth (Auth, AuthPlugin (..), AuthRoute, import Yesod.Auth (AuthHandler, AuthPlugin (..), AuthRoute,
Creds (..), Route (..), YesodAuth, Creds (..), Route (..), YesodAuth,
loginErrorMessageI, setCredsRedirect) loginErrorMessageI, setCredsRedirect)
import qualified Yesod.Auth.Message as Msg import qualified Yesod.Auth.Message as Msg
@ -148,16 +148,17 @@ loginR = PluginR "hardcoded" ["login"]
class (YesodAuth site) => YesodAuthHardcoded site where class (YesodAuth site) => YesodAuthHardcoded site where
-- | Check whether given user name exists among hardcoded names. -- | Check whether given user name exists among hardcoded names.
doesUserNameExist :: Text -> HandlerT site IO Bool doesUserNameExist :: Text -> AuthHandler site Bool
-- | Validate given user name with given password. -- | Validate given user name with given password.
validatePassword :: Text -> Text -> HandlerT site IO Bool validatePassword :: Text -> Text -> AuthHandler site Bool
authHardcoded :: YesodAuthHardcoded m => AuthPlugin m authHardcoded :: YesodAuthHardcoded m => AuthPlugin m
authHardcoded = authHardcoded =
AuthPlugin "hardcoded" dispatch loginWidget AuthPlugin "hardcoded" dispatch loginWidget
where where
dispatch :: YesodAuthHardcoded m => Text -> [Text] -> AuthHandler m TypedContent
dispatch "POST" ["login"] = postLoginR >>= sendResponse dispatch "POST" ["login"] = postLoginR >>= sendResponse
dispatch _ _ = notFound dispatch _ _ = notFound
loginWidget toMaster = do loginWidget toMaster = do
@ -182,16 +183,16 @@ authHardcoded =
|] |]
postLoginR :: (YesodAuthHardcoded master) postLoginR :: YesodAuthHardcoded site
=> HandlerT Auth (HandlerT master IO) TypedContent => AuthHandler site TypedContent
postLoginR = postLoginR =
do (username, password) <- lift (runInputPost do (username, password) <- runInputPost
((,) <$> ireq textField "username" ((,) Control.Applicative.<$> ireq textField "username"
<*> ireq textField "password")) Control.Applicative.<*> ireq textField "password")
isValid <- lift (validatePassword username password) isValid <- validatePassword username password
if isValid if isValid
then lift (setCredsRedirect (Creds "hardcoded" username [])) then setCredsRedirect (Creds "hardcoded" username [])
else do isExists <- lift (doesUserNameExist username) else do isExists <- doesUserNameExist username
loginErrorMessageI LoginR loginErrorMessageI LoginR
(if isExists (if isExists
then Msg.InvalidUsernamePass then Msg.InvalidUsernamePass

View File

@ -13,11 +13,13 @@ module Yesod.Auth.Message
, japaneseMessage , japaneseMessage
, finnishMessage , finnishMessage
, chineseMessage , chineseMessage
, croatianMessage
, spanishMessage , spanishMessage
, czechMessage , czechMessage
, russianMessage , russianMessage
, dutchMessage , dutchMessage
, danishMessage , danishMessage
, koreanMessage
) where ) where
import Data.Monoid (mappend, (<>)) import Data.Monoid (mappend, (<>))
@ -38,6 +40,8 @@ data AuthMessage =
| ConfirmationEmailSentTitle | ConfirmationEmailSentTitle
| ConfirmationEmailSent Text | ConfirmationEmailSent Text
| AddressVerified | AddressVerified
| EmailVerifiedChangePass
| EmailVerified
| InvalidKeyTitle | InvalidKeyTitle
| InvalidKey | InvalidKey
| InvalidEmailPass | InvalidEmailPass
@ -67,6 +71,7 @@ data AuthMessage =
| LogoutTitle | LogoutTitle
| AuthError | AuthError
{-# DEPRECATED Logout "Please, use LogoutTitle instead." #-} {-# DEPRECATED Logout "Please, use LogoutTitle instead." #-}
{-# DEPRECATED AddressVerified "Please, use EmailVerifiedChangePass instead." #-}
-- | Defaults to 'englishMessage'. -- | Defaults to 'englishMessage'.
defaultMessage :: AuthMessage -> Text defaultMessage :: AuthMessage -> Text
@ -86,10 +91,12 @@ englishMessage RegisterLong = "Register a new account"
englishMessage EnterEmail = "Enter your e-mail address below, and a confirmation e-mail will be sent to you." englishMessage EnterEmail = "Enter your e-mail address below, and a confirmation e-mail will be sent to you."
englishMessage ConfirmationEmailSentTitle = "Confirmation e-mail sent" englishMessage ConfirmationEmailSentTitle = "Confirmation e-mail sent"
englishMessage (ConfirmationEmailSent email) = englishMessage (ConfirmationEmailSent email) =
"A confirmation e-mail has been sent to " `mappend` "A confirmation e-mail has been sent to " `Data.Monoid.mappend`
email `mappend` email `mappend`
"." "."
englishMessage AddressVerified = "Address verified, please set a new password" englishMessage AddressVerified = "Email address verified, please set a new password"
englishMessage EmailVerifiedChangePass = "Email address verified, please set a new password"
englishMessage EmailVerified = "Email address verified"
englishMessage InvalidKeyTitle = "Invalid verification key" englishMessage InvalidKeyTitle = "Invalid verification key"
englishMessage InvalidKey = "I'm sorry, but that was an invalid verification key." englishMessage InvalidKey = "I'm sorry, but that was an invalid verification key."
englishMessage InvalidEmailPass = "Invalid email/password combination" englishMessage InvalidEmailPass = "Invalid email/password combination"
@ -137,6 +144,8 @@ portugueseMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
portugueseMessage AddressVerified = "Endereço verificado, por favor entre com uma nova senha" portugueseMessage AddressVerified = "Endereço verificado, por favor entre com uma nova senha"
portugueseMessage EmailVerifiedChangePass = "Endereço verificado, por favor entre com uma nova senha"
portugueseMessage EmailVerified = "Endereço verificado"
portugueseMessage InvalidKeyTitle = "Chave de verificação inválida" portugueseMessage InvalidKeyTitle = "Chave de verificação inválida"
portugueseMessage InvalidKey = "Por favor nos desculpe, mas essa é uma chave de verificação inválida." portugueseMessage InvalidKey = "Por favor nos desculpe, mas essa é uma chave de verificação inválida."
portugueseMessage InvalidEmailPass = "E-mail e/ou senha inválidos" portugueseMessage InvalidEmailPass = "E-mail e/ou senha inválidos"
@ -173,7 +182,7 @@ spanishMessage LoginOpenID = "Entrar utilizando OpenID"
spanishMessage LoginGoogle = "Entrar utilizando Google" spanishMessage LoginGoogle = "Entrar utilizando Google"
spanishMessage LoginYahoo = "Entrar utilizando Yahoo" spanishMessage LoginYahoo = "Entrar utilizando Yahoo"
spanishMessage Email = "Correo electrónico" spanishMessage Email = "Correo electrónico"
spanishMessage UserName = "Nombre de Usuario" -- FIXME by Google Translate "user name" spanishMessage UserName = "Nombre de Usuario"
spanishMessage Password = "Contraseña" spanishMessage Password = "Contraseña"
spanishMessage CurrentPassword = "Contraseña actual" spanishMessage CurrentPassword = "Contraseña actual"
spanishMessage Register = "Registrarse" spanishMessage Register = "Registrarse"
@ -185,6 +194,8 @@ spanishMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
spanishMessage AddressVerified = "Dirección verificada, por favor introduzca una contraseña" spanishMessage AddressVerified = "Dirección verificada, por favor introduzca una contraseña"
spanishMessage EmailVerifiedChangePass = "Dirección verificada, por favor introduzca una contraseña"
spanishMessage EmailVerified = "Dirección verificada"
spanishMessage InvalidKeyTitle = "Clave de verificación invalida" spanishMessage InvalidKeyTitle = "Clave de verificación invalida"
spanishMessage InvalidKey = "Lo sentimos, pero esa clave de verificación es inválida." spanishMessage InvalidKey = "Lo sentimos, pero esa clave de verificación es inválida."
spanishMessage InvalidEmailPass = "La combinación cuenta de correo/contraseña es inválida" spanishMessage InvalidEmailPass = "La combinación cuenta de correo/contraseña es inválida"
@ -204,9 +215,9 @@ spanishMessage PleaseProvideUsername = "Por favor escriba su nombre de usuario"
spanishMessage PleaseProvidePassword = "Por favor escriba su contraseña" spanishMessage PleaseProvidePassword = "Por favor escriba su contraseña"
spanishMessage NoIdentifierProvided = "No ha indicado una cuenta de correo/nombre de usuario" spanishMessage NoIdentifierProvided = "No ha indicado una cuenta de correo/nombre de usuario"
spanishMessage InvalidEmailAddress = "La cuenta de correo es inválida" spanishMessage InvalidEmailAddress = "La cuenta de correo es inválida"
spanishMessage PasswordResetTitle = "Contraseña actualizada" spanishMessage PasswordResetTitle = "Actualización de contraseña"
spanishMessage ProvideIdentifier = "Cuenta de correo o nombre de usuario" spanishMessage ProvideIdentifier = "Cuenta de correo o nombre de usuario"
spanishMessage SendPasswordResetEmail = "Correo de actualización de contraseña enviado" spanishMessage SendPasswordResetEmail = "Enviar correo de actualización de contraseña"
spanishMessage PasswordResetPrompt = "Escriba su cuenta de correo o nombre de usuario, y una confirmación de actualización de contraseña será enviada a su cuenta de correo." spanishMessage PasswordResetPrompt = "Escriba su cuenta de correo o nombre de usuario, y una confirmación de actualización de contraseña será enviada a su cuenta de correo."
spanishMessage InvalidUsernamePass = "Combinación de nombre de usuario/contraseña invalida" spanishMessage InvalidUsernamePass = "Combinación de nombre de usuario/contraseña invalida"
-- TODO -- TODO
@ -233,6 +244,8 @@ swedishMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
swedishMessage AddressVerified = "Adress verifierad, vänligen välj nytt lösenord" swedishMessage AddressVerified = "Adress verifierad, vänligen välj nytt lösenord"
swedishMessage EmailVerifiedChangePass = "Adress verifierad, vänligen välj nytt lösenord"
swedishMessage EmailVerified = "Adress verifierad"
swedishMessage InvalidKeyTitle = "Ogiltig verifikationsnyckel" swedishMessage InvalidKeyTitle = "Ogiltig verifikationsnyckel"
swedishMessage InvalidKey = "Tyvärr, du angav en ogiltig verifimationsnyckel." swedishMessage InvalidKey = "Tyvärr, du angav en ogiltig verifimationsnyckel."
swedishMessage InvalidEmailPass = "Ogiltig epost/lösenord kombination" swedishMessage InvalidEmailPass = "Ogiltig epost/lösenord kombination"
@ -269,19 +282,21 @@ germanMessage NoOpenID = "Kein OpenID-Identifier gefunden"
germanMessage LoginOpenID = "Login via OpenID" germanMessage LoginOpenID = "Login via OpenID"
germanMessage LoginGoogle = "Login via Google" germanMessage LoginGoogle = "Login via Google"
germanMessage LoginYahoo = "Login via Yahoo" germanMessage LoginYahoo = "Login via Yahoo"
germanMessage Email = "Email" germanMessage Email = "E-Mail"
germanMessage UserName = "Benutzername" -- FIXME by Google Translate "user name" germanMessage UserName = "Benutzername"
germanMessage Password = "Passwort" germanMessage Password = "Passwort"
germanMessage CurrentPassword = "Aktuelles Passwort" germanMessage CurrentPassword = "Aktuelles Passwort"
germanMessage Register = "Registrieren" germanMessage Register = "Registrieren"
germanMessage RegisterLong = "Neuen Account registrieren" germanMessage RegisterLong = "Neuen Account registrieren"
germanMessage EnterEmail = "Bitte die e-Mail Adresse angeben, eine Bestätigungsmail wird verschickt." germanMessage EnterEmail = "Bitte die E-Mail Adresse angeben, eine Bestätigungsmail wird verschickt."
germanMessage ConfirmationEmailSentTitle = "Bestätigung verschickt." germanMessage ConfirmationEmailSentTitle = "Bestätigung verschickt."
germanMessage (ConfirmationEmailSent email) = germanMessage (ConfirmationEmailSent email) =
"Eine Bestätigung wurde an " `mappend` "Eine Bestätigung wurde an " `mappend`
email `mappend` email `mappend`
" versandt." " versandt."
germanMessage AddressVerified = "Adresse bestätigt, bitte neues Passwort angeben" germanMessage AddressVerified = "Adresse bestätigt, bitte neues Passwort angeben"
germanMessage EmailVerifiedChangePass = "Adresse bestätigt, bitte neues Passwort angeben"
germanMessage EmailVerified = "Adresse bestätigt"
germanMessage InvalidKeyTitle = "Ungültiger Bestätigungsschlüssel" germanMessage InvalidKeyTitle = "Ungültiger Bestätigungsschlüssel"
germanMessage InvalidKey = "Das war leider ein ungültiger Bestätigungsschlüssel" germanMessage InvalidKey = "Das war leider ein ungültiger Bestätigungsschlüssel"
germanMessage InvalidEmailPass = "Ungültiger Nutzername oder Passwort" germanMessage InvalidEmailPass = "Ungültiger Nutzername oder Passwort"
@ -293,24 +308,23 @@ germanMessage ConfirmPass = "Bestätigen"
germanMessage PassMismatch = "Die Passwörter stimmen nicht überein" germanMessage PassMismatch = "Die Passwörter stimmen nicht überein"
germanMessage PassUpdated = "Passwort überschrieben" germanMessage PassUpdated = "Passwort überschrieben"
germanMessage Facebook = "Login über Facebook" germanMessage Facebook = "Login über Facebook"
germanMessage LoginViaEmail = "Login via e-Mail" germanMessage LoginViaEmail = "Login via E-Mail"
germanMessage InvalidLogin = "Ungültiger Login" germanMessage InvalidLogin = "Ungültiger Login"
germanMessage NowLoggedIn = "Login erfolgreich" germanMessage NowLoggedIn = "Login erfolgreich"
germanMessage LoginTitle = "Log In" germanMessage LoginTitle = "Anmelden"
germanMessage PleaseProvideUsername = "Bitte Nutzername angeben" germanMessage PleaseProvideUsername = "Bitte Nutzername angeben"
germanMessage PleaseProvidePassword = "Bitte Passwort angeben" germanMessage PleaseProvidePassword = "Bitte Passwort angeben"
germanMessage NoIdentifierProvided = "Keine Email-Adresse oder kein Nutzername angegeben" germanMessage NoIdentifierProvided = "Keine E-Mail-Adresse oder kein Nutzername angegeben"
germanMessage InvalidEmailAddress = "Unzulässiger Email-Anbieter" germanMessage InvalidEmailAddress = "Unzulässiger E-Mail-Anbieter"
germanMessage PasswordResetTitle = "Passwort zurücksetzen" germanMessage PasswordResetTitle = "Passwort zurücksetzen"
germanMessage ProvideIdentifier = "Email-Adresse oder Nutzername" germanMessage ProvideIdentifier = "E-Mail-Adresse oder Nutzername"
germanMessage SendPasswordResetEmail = "Email zusenden um Passwort zurückzusetzen" germanMessage SendPasswordResetEmail = "E-Mail zusenden um Passwort zurückzusetzen"
germanMessage PasswordResetPrompt = "Nach Einhabe der Email-Adresse oder des Nutzernamen wird eine Email zugesendet mit welcher das Passwort zurückgesetzt werden kann." germanMessage PasswordResetPrompt = "Nach Einhabe der E-Mail-Adresse oder des Nutzernamen wird eine E-Mail zugesendet mit welcher das Passwort zurückgesetzt werden kann."
germanMessage InvalidUsernamePass = "Ungültige Kombination aus Nutzername und Passwort" germanMessage InvalidUsernamePass = "Ungültige Kombination aus Nutzername und Passwort"
-- TODO germanMessage i@(IdentifierNotFound _) = englishMessage i -- TODO
germanMessage i@(IdentifierNotFound _) = englishMessage i germanMessage Logout = "Abmelden"
germanMessage Logout = "Ausloggen" -- FIXME by Google Translate germanMessage LogoutTitle = "Abmelden"
germanMessage LogoutTitle = "Ausloggen" -- FIXME by Google Translate germanMessage AuthError = "Fehler beim Anmelden"
germanMessage AuthError = "Autorisierungsfehler" -- FIXME by Google Translate
frenchMessage :: AuthMessage -> Text frenchMessage :: AuthMessage -> Text
frenchMessage NoOpenID = "Aucun fournisseur OpenID n'a été trouvé" frenchMessage NoOpenID = "Aucun fournisseur OpenID n'a été trouvé"
@ -330,6 +344,8 @@ frenchMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
frenchMessage AddressVerified = "Votre adresse électronique a été validée, merci de choisir un nouveau mot de passe." frenchMessage AddressVerified = "Votre adresse électronique a été validée, merci de choisir un nouveau mot de passe."
frenchMessage EmailVerifiedChangePass = "Votre adresse électronique a été validée, merci de choisir un nouveau mot de passe."
frenchMessage EmailVerified = "Votre adresse électronique a été validée"
frenchMessage InvalidKeyTitle = "Clef de validation incorrecte" frenchMessage InvalidKeyTitle = "Clef de validation incorrecte"
frenchMessage InvalidKey = "Désolé, mais cette clef de validation est incorrecte" frenchMessage InvalidKey = "Désolé, mais cette clef de validation est incorrecte"
frenchMessage InvalidEmailPass = "La combinaison de ce mot de passe et de cette adresse électronique n'existe pas." frenchMessage InvalidEmailPass = "La combinaison de ce mot de passe et de cette adresse électronique n'existe pas."
@ -377,6 +393,8 @@ norwegianBokmålMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
norwegianBokmålMessage AddressVerified = "Adresse verifisert, vennligst sett et nytt passord." norwegianBokmålMessage AddressVerified = "Adresse verifisert, vennligst sett et nytt passord."
norwegianBokmålMessage EmailVerifiedChangePass = "Adresse verifisert, vennligst sett et nytt passord."
norwegianBokmålMessage EmailVerified = "Adresse verifisert"
norwegianBokmålMessage InvalidKeyTitle = "Ugyldig verifiseringsnøkkel" norwegianBokmålMessage InvalidKeyTitle = "Ugyldig verifiseringsnøkkel"
norwegianBokmålMessage InvalidKey = "Beklager, men det var en ugyldig verifiseringsnøkkel." norwegianBokmålMessage InvalidKey = "Beklager, men det var en ugyldig verifiseringsnøkkel."
norwegianBokmålMessage InvalidEmailPass = "Ugyldig e-post/passord-kombinasjon" norwegianBokmålMessage InvalidEmailPass = "Ugyldig e-post/passord-kombinasjon"
@ -415,7 +433,7 @@ japaneseMessage LoginYahoo = "Yahooでログイン"
japaneseMessage Email = "Eメール" japaneseMessage Email = "Eメール"
japaneseMessage UserName = "ユーザー名" -- FIXME by Google Translate "user name" japaneseMessage UserName = "ユーザー名" -- FIXME by Google Translate "user name"
japaneseMessage Password = "パスワード" japaneseMessage Password = "パスワード"
japaneseMessage CurrentPassword = "Current password" japaneseMessage CurrentPassword = "現在のパスワード"
japaneseMessage Register = "登録" japaneseMessage Register = "登録"
japaneseMessage RegisterLong = "新規アカウント登録" japaneseMessage RegisterLong = "新規アカウント登録"
japaneseMessage EnterEmail = "メールアドレスを入力してください。確認メールが送られます" japaneseMessage EnterEmail = "メールアドレスを入力してください。確認メールが送られます"
@ -425,6 +443,8 @@ japaneseMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
" に送信しました" " に送信しました"
japaneseMessage AddressVerified = "アドレスは認証されました。新しいパスワードを設定してください" japaneseMessage AddressVerified = "アドレスは認証されました。新しいパスワードを設定してください"
japaneseMessage EmailVerifiedChangePass = "アドレスは認証されました。新しいパスワードを設定してください"
japaneseMessage EmailVerified = "アドレスは認証されました"
japaneseMessage InvalidKeyTitle = "認証キーが無効です" japaneseMessage InvalidKeyTitle = "認証キーが無効です"
japaneseMessage InvalidKey = "申し訳ありません。無効な認証キーです" japaneseMessage InvalidKey = "申し訳ありません。無効な認証キーです"
japaneseMessage InvalidEmailPass = "メールアドレスまたはパスワードが無効です" japaneseMessage InvalidEmailPass = "メールアドレスまたはパスワードが無効です"
@ -463,7 +483,7 @@ finnishMessage LoginYahoo = "Kirjaudu Yahoo-tilillä"
finnishMessage Email = "Sähköposti" finnishMessage Email = "Sähköposti"
finnishMessage UserName = "Käyttäjätunnus" -- FIXME by Google Translate "user name" finnishMessage UserName = "Käyttäjätunnus" -- FIXME by Google Translate "user name"
finnishMessage Password = "Salasana" finnishMessage Password = "Salasana"
finnishMessage Password = "Current password" finnishMessage CurrentPassword = "Current password"
finnishMessage Register = "Luo uusi" finnishMessage Register = "Luo uusi"
finnishMessage RegisterLong = "Luo uusi tili" finnishMessage RegisterLong = "Luo uusi tili"
finnishMessage EnterEmail = "Kirjoita alle sähköpostiosoitteesi, johon vahvistussähköposti lähetetään." finnishMessage EnterEmail = "Kirjoita alle sähköpostiosoitteesi, johon vahvistussähköposti lähetetään."
@ -474,6 +494,8 @@ finnishMessage (ConfirmationEmailSent email) =
"." "."
finnishMessage AddressVerified = "Sähköpostiosoite vahvistettu. Anna uusi salasana" finnishMessage AddressVerified = "Sähköpostiosoite vahvistettu. Anna uusi salasana"
finnishMessage EmailVerifiedChangePass = "Sähköpostiosoite vahvistettu. Anna uusi salasana"
finnishMessage EmailVerified = "Sähköpostiosoite vahvistettu"
finnishMessage InvalidKeyTitle = "Virheellinen varmistusavain" finnishMessage InvalidKeyTitle = "Virheellinen varmistusavain"
finnishMessage InvalidKey = "Valitettavasti varmistusavain on virheellinen." finnishMessage InvalidKey = "Valitettavasti varmistusavain on virheellinen."
finnishMessage InvalidEmailPass = "Virheellinen sähköposti tai salasana." finnishMessage InvalidEmailPass = "Virheellinen sähköposti tai salasana."
@ -510,9 +532,9 @@ chineseMessage LoginOpenID = "用OpenID登录"
chineseMessage LoginGoogle = "用Google帐户登录" chineseMessage LoginGoogle = "用Google帐户登录"
chineseMessage LoginYahoo = "用Yahoo帐户登录" chineseMessage LoginYahoo = "用Yahoo帐户登录"
chineseMessage Email = "邮箱" chineseMessage Email = "邮箱"
chineseMessage UserName = "用户名" -- FIXME by Google Translate "user name" chineseMessage UserName = "用户名"
chineseMessage Password = "密码" chineseMessage Password = "密码"
chineseMessage CurrentPassword = "Current password" chineseMessage CurrentPassword = "当前密码"
chineseMessage Register = "注册" chineseMessage Register = "注册"
chineseMessage RegisterLong = "注册新帐户" chineseMessage RegisterLong = "注册新帐户"
chineseMessage EnterEmail = "输入你的邮箱地址,你将收到一封确认邮件。" chineseMessage EnterEmail = "输入你的邮箱地址,你将收到一封确认邮件。"
@ -522,6 +544,8 @@ chineseMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
chineseMessage AddressVerified = "地址验证成功,请设置新密码" chineseMessage AddressVerified = "地址验证成功,请设置新密码"
chineseMessage EmailVerifiedChangePass = "地址验证成功,请设置新密码"
chineseMessage EmailVerified = "地址验证成功"
chineseMessage InvalidKeyTitle = "无效的验证码" chineseMessage InvalidKeyTitle = "无效的验证码"
chineseMessage InvalidKey = "对不起,验证码无效。" chineseMessage InvalidKey = "对不起,验证码无效。"
chineseMessage InvalidEmailPass = "无效的邮箱/密码组合" chineseMessage InvalidEmailPass = "无效的邮箱/密码组合"
@ -546,11 +570,10 @@ chineseMessage ProvideIdentifier = "邮箱或用户名"
chineseMessage SendPasswordResetEmail = "发送密码重置邮件" chineseMessage SendPasswordResetEmail = "发送密码重置邮件"
chineseMessage PasswordResetPrompt = "输入你的邮箱地址或用户名,你将收到一封密码重置邮件。" chineseMessage PasswordResetPrompt = "输入你的邮箱地址或用户名,你将收到一封密码重置邮件。"
chineseMessage InvalidUsernamePass = "无效的用户名/密码组合" chineseMessage InvalidUsernamePass = "无效的用户名/密码组合"
-- TODO chineseMessage (IdentifierNotFound ident) = "邮箱/用户名不存在: " `mappend` ident
chineseMessage i@(IdentifierNotFound _) = englishMessage i chineseMessage Logout = "注销"
chineseMessage Logout = "註銷" -- FIXME by Google Translate chineseMessage LogoutTitle = "注销"
chineseMessage LogoutTitle = "註銷" -- FIXME by Google Translate chineseMessage AuthError = "验证错误"
chineseMessage AuthError = "验证错误" -- FIXME by Google Translate
czechMessage :: AuthMessage -> Text czechMessage :: AuthMessage -> Text
czechMessage NoOpenID = "Nebyl nalezen identifikátor OpenID" czechMessage NoOpenID = "Nebyl nalezen identifikátor OpenID"
@ -568,6 +591,8 @@ czechMessage ConfirmationEmailSentTitle = "Potvrzovací e-mail odeslán"
czechMessage (ConfirmationEmailSent email) = czechMessage (ConfirmationEmailSent email) =
"Potvrzovací e-mail byl odeslán na " `mappend` email `mappend` "." "Potvrzovací e-mail byl odeslán na " `mappend` email `mappend` "."
czechMessage AddressVerified = "Adresa byla ověřena, prosím nastavte si nové heslo" czechMessage AddressVerified = "Adresa byla ověřena, prosím nastavte si nové heslo"
czechMessage EmailVerifiedChangePass = "Adresa byla ověřena, prosím nastavte si nové heslo"
czechMessage EmailVerified = "Adresa byla ověřena"
czechMessage InvalidKeyTitle = "Neplatný ověřovací klíč" czechMessage InvalidKeyTitle = "Neplatný ověřovací klíč"
czechMessage InvalidKey = "Bohužel, ověřovací klíč je neplatný." czechMessage InvalidKey = "Bohužel, ověřovací klíč je neplatný."
czechMessage InvalidEmailPass = "Neplatná kombinace e-mail/heslo" czechMessage InvalidEmailPass = "Neplatná kombinace e-mail/heslo"
@ -608,7 +633,7 @@ russianMessage LoginYahoo = "Вход с помощью Yahoo"
russianMessage Email = "Эл.почта" russianMessage Email = "Эл.почта"
russianMessage UserName = "Имя пользователя" russianMessage UserName = "Имя пользователя"
russianMessage Password = "Пароль" russianMessage Password = "Пароль"
russianMessage CurrentPassword = "Current password" russianMessage CurrentPassword = "Старый пароль"
russianMessage Register = "Регистрация" russianMessage Register = "Регистрация"
russianMessage RegisterLong = "Создать учётную запись" russianMessage RegisterLong = "Создать учётную запись"
russianMessage EnterEmail = "Введите свой адрес эл.почты ниже, вам будет отправлено письмо для подтверждения." russianMessage EnterEmail = "Введите свой адрес эл.почты ниже, вам будет отправлено письмо для подтверждения."
@ -618,6 +643,8 @@ russianMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
russianMessage AddressVerified = "Адрес подтверждён. Пожалуйста, установите новый пароль." russianMessage AddressVerified = "Адрес подтверждён. Пожалуйста, установите новый пароль."
russianMessage EmailVerifiedChangePass = "Адрес подтверждён. Пожалуйста, установите новый пароль."
russianMessage EmailVerified = "Адрес подтверждён"
russianMessage InvalidKeyTitle = "Неверный ключ подтверждения" russianMessage InvalidKeyTitle = "Неверный ключ подтверждения"
russianMessage InvalidKey = "Извините, но ключ подтверждения оказался недействительным." russianMessage InvalidKey = "Извините, но ключ подтверждения оказался недействительным."
russianMessage InvalidEmailPass = "Неверное сочетание эл.почты и пароля" russianMessage InvalidEmailPass = "Неверное сочетание эл.почты и пароля"
@ -665,6 +692,8 @@ dutchMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
dutchMessage AddressVerified = "Adres geverifieerd, stel alstublieft een nieuwe wachtwoord in" dutchMessage AddressVerified = "Adres geverifieerd, stel alstublieft een nieuwe wachtwoord in"
dutchMessage EmailVerifiedChangePass = "Adres geverifieerd, stel alstublieft een nieuwe wachtwoord in"
dutchMessage EmailVerified = "Adres geverifieerd"
dutchMessage InvalidKeyTitle = "Ongeldig verificatietoken" dutchMessage InvalidKeyTitle = "Ongeldig verificatietoken"
dutchMessage InvalidKey = "Dat was helaas een ongeldig verificatietoken." dutchMessage InvalidKey = "Dat was helaas een ongeldig verificatietoken."
dutchMessage InvalidEmailPass = "Ongeldige e-mailadres/wachtwoord combinatie" dutchMessage InvalidEmailPass = "Ongeldige e-mailadres/wachtwoord combinatie"
@ -712,6 +741,8 @@ croatianMessage PasswordResetPrompt = "Dolje unesite adresu e-pošte ili korisni
croatianMessage ConfirmationEmailSentTitle = "E-poruka za potvrdu" croatianMessage ConfirmationEmailSentTitle = "E-poruka za potvrdu"
croatianMessage (ConfirmationEmailSent email) = "E-poruka za potvrdu poslana je na adresu " <> email <> "." croatianMessage (ConfirmationEmailSent email) = "E-poruka za potvrdu poslana je na adresu " <> email <> "."
croatianMessage AddressVerified = "Adresa ovjerena, postavite novu lozinku" croatianMessage AddressVerified = "Adresa ovjerena, postavite novu lozinku"
croatianMessage EmailVerifiedChangePass = "Adresa ovjerena, postavite novu lozinku"
croatianMessage EmailVerified = "Adresa ovjerena"
croatianMessage InvalidKeyTitle = "Ključ za ovjeru nije valjan" croatianMessage InvalidKeyTitle = "Ključ za ovjeru nije valjan"
croatianMessage InvalidKey = "Nažalost, taj ključ za ovjeru nije valjan." croatianMessage InvalidKey = "Nažalost, taj ključ za ovjeru nije valjan."
croatianMessage InvalidEmailPass = "Kombinacija e-pošte i lozinke nije valjana" croatianMessage InvalidEmailPass = "Kombinacija e-pošte i lozinke nije valjana"
@ -756,6 +787,8 @@ danishMessage (ConfirmationEmailSent email) =
email `mappend` email `mappend`
"." "."
danishMessage AddressVerified = "Adresse bekræftet, sæt venligst et nyt kodeord" danishMessage AddressVerified = "Adresse bekræftet, sæt venligst et nyt kodeord"
danishMessage EmailVerifiedChangePass = "Adresse bekræftet, sæt venligst et nyt kodeord"
danishMessage EmailVerified = "Adresse bekræftet"
danishMessage InvalidKeyTitle = "Ugyldig verifikationsnøgle" danishMessage InvalidKeyTitle = "Ugyldig verifikationsnøgle"
danishMessage InvalidKey = "Beklager, det var en ugyldigt verifikationsnøgle." danishMessage InvalidKey = "Beklager, det var en ugyldigt verifikationsnøgle."
danishMessage InvalidEmailPass = "Ugyldigt e-mail/kodeord" danishMessage InvalidEmailPass = "Ugyldigt e-mail/kodeord"
@ -784,3 +817,52 @@ danishMessage (IdentifierNotFound ident) = "Brugernavn findes ikke: " `mappend`
danishMessage Logout = "Log ud" danishMessage Logout = "Log ud"
danishMessage LogoutTitle = "Log ud" danishMessage LogoutTitle = "Log ud"
danishMessage AuthError = "Fejl ved bekræftelse af identitet" danishMessage AuthError = "Fejl ved bekræftelse af identitet"
koreanMessage :: AuthMessage -> Text
koreanMessage NoOpenID = "OpenID ID가 없습니다"
koreanMessage LoginOpenID = "OpenID로 로그인"
koreanMessage LoginGoogle = "Google로 로그인"
koreanMessage LoginYahoo = "Yahoo로 로그인"
koreanMessage Email = "이메일"
koreanMessage UserName = "사용자 이름"
koreanMessage Password = "비밀번호"
koreanMessage CurrentPassword = "현재 비밀번호"
koreanMessage Register = "등록"
koreanMessage RegisterLong = "새 계정 등록"
koreanMessage EnterEmail = "이메일 주소를 아래에 입력하시면 확인 이메일이 발송됩니다."
koreanMessage ConfirmationEmailSentTitle = "확인 이메일을 보냈습니다"
koreanMessage (ConfirmationEmailSent email) =
"확인 이메일을 " `mappend`
email `mappend`
"에 보냈습니다."
koreanMessage AddressVerified = "주소가 인증되었습니다. 새 비밀번호를 설정하세요."
koreanMessage EmailVerifiedChangePass = "주소가 인증되었습니다. 새 비밀번호를 설정하세요."
koreanMessage EmailVerified = "주소가 인증되었습니다"
koreanMessage InvalidKeyTitle = "인증키가 잘못되었습니다"
koreanMessage InvalidKey = "죄송합니다. 잘못된 인증키입니다."
koreanMessage InvalidEmailPass = "이메일 주소나 비밀번호가 잘못되었습니다"
koreanMessage BadSetPass = "비밀번호를 설정하기 위해서는 로그인해야 합니다"
koreanMessage SetPassTitle = "비밀번호 설정"
koreanMessage SetPass = "새 비밀번호 설정"
koreanMessage NewPass = "새 비밀번호"
koreanMessage ConfirmPass = "확인"
koreanMessage PassMismatch = "비밀번호가 맞지 않습니다. 다시 시도해주세요."
koreanMessage PassUpdated = "비밀번호가 업데이트 되었습니다"
koreanMessage Facebook = "Facebook으로 로그인"
koreanMessage LoginViaEmail = "이메일로"
koreanMessage InvalidLogin = "잘못된 로그인입니다"
koreanMessage NowLoggedIn = "로그인했습니다"
koreanMessage LoginTitle = "로그인"
koreanMessage PleaseProvideUsername = "사용자 이름을 입력하세요"
koreanMessage PleaseProvidePassword = "비밀번호를 입력하세요"
koreanMessage NoIdentifierProvided = "이메일 주소나 사용자 이름이 입력되어 있지 않습니다"
koreanMessage InvalidEmailAddress = "이메일 주소가 잘못되었습니다"
koreanMessage PasswordResetTitle = "비밀번호 변경"
koreanMessage ProvideIdentifier = "이메일 주소나 사용자 이름"
koreanMessage SendPasswordResetEmail = "비밀번호 재설정 이메일 보내기"
koreanMessage PasswordResetPrompt = "이메일 주소나 사용자 이름을 아래에 입력하시면 비밀번호 재설정 이메일이 발송됩니다."
koreanMessage InvalidUsernamePass = "사용자 이름이나 비밀번호가 잘못되었습니다"
koreanMessage (IdentifierNotFound ident) = ident `mappend` "는 등록되어 있지 않습니다"
koreanMessage Logout = "로그아웃"
koreanMessage LogoutTitle = "로그아웃"
koreanMessage AuthError = "인증오류"

View File

@ -3,6 +3,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GADTs #-}
module Yesod.Auth.OpenId module Yesod.Auth.OpenId
( authOpenId ( authOpenId
, forwardUrl , forwardUrl
@ -19,7 +20,7 @@ import Yesod.Form
import Yesod.Core import Yesod.Core
import Data.Text (Text, isPrefixOf) import Data.Text (Text, isPrefixOf)
import qualified Yesod.Auth.Message as Msg import qualified Yesod.Auth.Message as Msg
import Control.Exception.Lifted (SomeException, try) import UnliftIO.Exception (tryAny)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import qualified Data.Text as T import qualified Data.Text as T
@ -36,7 +37,10 @@ authOpenId idType extensionFields =
AuthPlugin "openid" dispatch login AuthPlugin "openid" dispatch login
where where
complete = PluginR "openid" ["complete"] complete = PluginR "openid" ["complete"]
name :: Text
name = "openid_identifier" name = "openid_identifier"
login tm = do login tm = do
ident <- newIdent ident <- newIdent
-- FIXME this is a hack to get GHC 7.6's type checker to allow the -- FIXME this is a hack to get GHC 7.6's type checker to allow the
@ -49,9 +53,6 @@ authOpenId idType extensionFields =
|] $ x `asTypeOf` y) |] $ x `asTypeOf` y)
[whamlet| [whamlet|
$newline never $newline never
<form method="get" action="@{tm forwardUrl}">
<input type="hidden" name="openid_identifier" value="https://www.google.com/accounts/o8/id">
<button .openid-google>_{Msg.LoginGoogle}
<form method="get" action="@{tm forwardUrl}"> <form method="get" action="@{tm forwardUrl}">
<input type="hidden" name="openid_identifier" value="http://me.yahoo.com"> <input type="hidden" name="openid_identifier" value="http://me.yahoo.com">
<button .openid-yahoo>_{Msg.LoginYahoo} <button .openid-yahoo>_{Msg.LoginYahoo}
@ -60,19 +61,19 @@ $newline never
<input id="#{ident}" type="text" name="#{name}" value="http://"> <input id="#{ident}" type="text" name="#{name}" value="http://">
<input type="submit" value="_{Msg.LoginOpenID}"> <input type="submit" value="_{Msg.LoginOpenID}">
|] |]
dispatch :: Text -> [Text] -> AuthHandler master TypedContent
dispatch "GET" ["forward"] = do dispatch "GET" ["forward"] = do
roid <- lift $ runInputGet $ iopt textField name roid <- runInputGet $ iopt textField name
case roid of case roid of
Just oid -> do Just oid -> do
render <- getUrlRender
let complete' = render complete
master <- lift getYesod
eres <- lift $ try $ OpenId.getForwardUrl oid complete' Nothing extensionFields (authHttpManager master)
case eres of
Left err -> do
tm <- getRouteToParent tm <- getRouteToParent
lift $ loginErrorMessage (tm LoginR) $ T.pack $ render <- getUrlRender
show (err :: SomeException) let complete' = render $ tm complete
manager <- authHttpManager
eres <- tryAny $ OpenId.getForwardUrl oid complete' Nothing extensionFields manager
case eres of
Left err -> loginErrorMessage (tm LoginR) $ T.pack $ show err
Right x -> redirect x Right x -> redirect x
Nothing -> loginErrorMessageI LoginR Msg.NoOpenID Nothing -> loginErrorMessageI LoginR Msg.NoOpenID
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
@ -87,14 +88,13 @@ $newline never
completeHelper :: IdentifierType -> [(Text, Text)] -> AuthHandler master TypedContent completeHelper :: IdentifierType -> [(Text, Text)] -> AuthHandler master TypedContent
completeHelper idType gets' = do completeHelper idType gets' = do
master <- lift getYesod manager <- authHttpManager
eres <- try $ OpenId.authenticateClaimed gets' (authHttpManager master) eres <- tryAny $ OpenId.authenticateClaimed gets' manager
either onFailure onSuccess eres either onFailure onSuccess eres
where where
onFailure err = do onFailure err = do
tm <- getRouteToParent tm <- getRouteToParent
lift $ loginErrorMessage (tm LoginR) $ T.pack $ loginErrorMessage (tm LoginR) $ T.pack $ show err
show (err :: SomeException)
onSuccess oir = do onSuccess oir = do
let claimed = let claimed =
case OpenId.oirClaimed oir of case OpenId.oirClaimed oir of
@ -108,7 +108,7 @@ completeHelper idType gets' = do
case idType of case idType of
OPLocal -> OpenId.oirOpLocal oir OPLocal -> OpenId.oirOpLocal oir
Claimed -> fromMaybe (OpenId.oirOpLocal oir) $ OpenId.oirClaimed oir Claimed -> fromMaybe (OpenId.oirOpLocal oir) $ OpenId.oirClaimed oir
lift $ setCredsRedirect $ Creds "openid" i gets'' setCredsRedirect $ Creds "openid" i gets''
-- | The main identifier provided by the OpenID authentication plugin is the -- | The main identifier provided by the OpenID authentication plugin is the
-- \"OP-local identifier\". There is also sometimes a \"claimed\" identifier -- \"OP-local identifier\". There is also sometimes a \"claimed\" identifier

View File

@ -4,7 +4,6 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
module Yesod.Auth.Routes where module Yesod.Auth.Routes where

View File

@ -2,6 +2,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
module Yesod.Auth.Rpxnow module Yesod.Auth.Rpxnow
( authRpxnow ( authRpxnow
) where ) where
@ -17,10 +18,10 @@ import Data.Text.Encoding.Error (lenientDecode)
import Control.Arrow ((***)) import Control.Arrow ((***))
import Network.HTTP.Types (renderQuery) import Network.HTTP.Types (renderQuery)
authRpxnow :: YesodAuth m authRpxnow :: YesodAuth master
=> String -- ^ app name => String -- ^ app name
-> String -- ^ key -> String -- ^ key
-> AuthPlugin m -> AuthPlugin master
authRpxnow app apiKey = authRpxnow app apiKey =
AuthPlugin "rpxnow" dispatch login AuthPlugin "rpxnow" dispatch login
where where
@ -32,14 +33,16 @@ authRpxnow app apiKey =
$newline never $newline never
<iframe src="http://#{app}.rpxnow.com/openid/embed#{queryString}" scrolling="no" frameBorder="no" allowtransparency="true" style="width:400px;height:240px"> <iframe src="http://#{app}.rpxnow.com/openid/embed#{queryString}" scrolling="no" frameBorder="no" allowtransparency="true" style="width:400px;height:240px">
|] |]
dispatch :: a -> [b] -> AuthHandler master TypedContent
dispatch _ [] = do dispatch _ [] = do
token1 <- lookupGetParams "token" token1 <- lookupGetParams "token"
token2 <- lookupPostParams "token" token2 <- lookupPostParams "token"
token <- case token1 ++ token2 of token <- case token1 ++ token2 of
[] -> invalidArgs ["token: Value not supplied"] [] -> invalidArgs ["token: Value not supplied"]
x:_ -> return $ unpack x x:_ -> return $ unpack x
master <- lift getYesod manager <- authHttpManager
Rpxnow.Identifier ident extra <- lift $ Rpxnow.authenticate apiKey token (authHttpManager master) Rpxnow.Identifier ident extra <- Rpxnow.authenticate apiKey token manager
let creds = let creds =
Creds "rpxnow" ident Creds "rpxnow" ident
$ maybe id (\x -> (:) ("verifiedEmail", x)) $ maybe id (\x -> (:) ("verifiedEmail", x))
@ -47,7 +50,7 @@ $newline never
$ maybe id (\x -> (:) ("displayName", x)) $ maybe id (\x -> (:) ("displayName", x))
(fmap pack $ getDisplayName $ map (unpack *** unpack) extra) (fmap pack $ getDisplayName $ map (unpack *** unpack) extra)
[] []
lift $ setCredsRedirect creds setCredsRedirect creds
dispatch _ _ = notFound dispatch _ _ = notFound
-- | Get some form of a display name. -- | Get some form of a display name.

View File

@ -1,13 +1,8 @@
{-# LANGUAGE OverloadedStrings, BangPatterns #-} {-# LANGUAGE OverloadedStrings, BangPatterns #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
-- | -- |
-- Module : Crypto.PasswordStore -- This is a fork of pwstore-fast, originally copyright (c) Peter Scott, 2011,
-- Copyright : (c) Peter Scott, 2011 -- and released under a BSD-style licence.
-- License : BSD-style
--
-- Maintainer : pjscott@iastate.edu
-- Stability : experimental
-- Portability : portable
-- --
-- Securely store hashed, salted passwords. If you need to store and verify -- Securely store hashed, salted passwords. If you need to store and verify
-- passwords, there are many wrong ways to do it, most of them all too -- passwords, there are many wrong ways to do it, most of them all too
@ -70,8 +65,10 @@
-- Note that, as of version 2.4, you can also use PBKDF2, and specify the exact -- Note that, as of version 2.4, you can also use PBKDF2, and specify the exact
-- iteration count. This does not have a significant effect on security, but can -- iteration count. This does not have a significant effect on security, but can
-- be handy for compatibility with other code. -- be handy for compatibility with other code.
--
-- @since 1.4.18
module Yesod.PasswordStore ( module Yesod.Auth.Util.PasswordStore (
-- * Algorithms -- * Algorithms
pbkdf1, -- :: ByteString -> Salt -> Int -> ByteString pbkdf1, -- :: ByteString -> Salt -> Int -> ByteString
@ -102,16 +99,14 @@ module Yesod.PasswordStore (
importSalt -- :: ByteString -> Salt importSalt -- :: ByteString -> Salt
) where ) where
import qualified Crypto.MAC.HMAC as CH
import qualified Crypto.Hash as CH import qualified Crypto.Hash as CH
import qualified Crypto.Hash.SHA256 as H
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import Control.Monad import Control.Monad
import Control.Monad.ST import Control.Monad.ST
import Data.Byteable (toBytes)
import Data.STRef import Data.STRef
import Data.Bits import Data.Bits
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
@ -120,6 +115,7 @@ import System.IO
import System.Random import System.Random
import Data.Maybe import Data.Maybe
import qualified Control.Exception import qualified Control.Exception
import Data.ByteArray (convert)
--------------------- ---------------------
-- Cryptographic base -- Cryptographic base
@ -132,16 +128,23 @@ import qualified Control.Exception
-- key should be stored in the password file. When a user wishes to authenticate -- key should be stored in the password file. When a user wishes to authenticate
-- a password, just pass it and the salt to this function, and see if the output -- a password, just pass it and the salt to this function, and see if the output
-- matches. -- matches.
--
-- @since 1.4.18
--
pbkdf1 :: ByteString -> Salt -> Int -> ByteString pbkdf1 :: ByteString -> Salt -> Int -> ByteString
pbkdf1 password (SaltBS salt) iter = hashRounds first_hash (iter + 1) pbkdf1 password (SaltBS salt) iter = hashRounds first_hash (iter + 1)
where first_hash = H.finalize $ H.init `H.update` password `H.update` salt where
first_hash =
convert $
((CH.hashFinalize $ CH.hashInit `CH.hashUpdate` password `CH.hashUpdate` salt) :: CH.Digest CH.SHA256)
-- | Hash a 'ByteString' for a given number of rounds. The number of rounds is 0 -- | Hash a 'ByteString' for a given number of rounds. The number of rounds is 0
-- or more. If the number of rounds specified is 0, the ByteString will be -- or more. If the number of rounds specified is 0, the ByteString will be
-- returned unmodified. -- returned unmodified.
hashRounds :: ByteString -> Int -> ByteString hashRounds :: ByteString -> Int -> ByteString
hashRounds (!bs) 0 = bs hashRounds (!bs) 0 = bs
hashRounds bs rounds = hashRounds (H.hash bs) (rounds - 1) hashRounds bs rounds = hashRounds (convert (CH.hash bs :: CH.Digest CH.SHA256)) (rounds - 1)
-- | Computes the hmacSHA256 of the given message, with the given 'Salt'. -- | Computes the hmacSHA256 of the given message, with the given 'Salt'.
hmacSHA256 :: ByteString hmacSHA256 :: ByteString
@ -151,19 +154,22 @@ hmacSHA256 :: ByteString
-> ByteString -> ByteString
-- ^ The encoded message -- ^ The encoded message
hmacSHA256 secret msg = hmacSHA256 secret msg =
toBytes (CH.hmacGetDigest (CH.hmac secret msg) :: CH.Digest CH.SHA256) convert (CH.hmacGetDigest (CH.hmac secret msg) :: CH.Digest CH.SHA256)
-- | PBKDF2 key-derivation function. -- | PBKDF2 key-derivation function.
-- For details see @http://tools.ietf.org/html/rfc2898@. -- For details see @http://tools.ietf.org/html/rfc2898@.
-- @32@ is the most common digest size for @SHA256@, and is -- @32@ is the most common digest size for @SHA256@, and is
-- what the algorithm internally uses. -- what the algorithm internally uses.
-- @HMAC+SHA256@ is used as @PRF@, because @HMAC+SHA1@ is considered too weak. -- @HMAC+SHA256@ is used as @PRF@, because @HMAC+SHA1@ is considered too weak.
--
-- @since 1.4.18
--
pbkdf2 :: ByteString -> Salt -> Int -> ByteString pbkdf2 :: ByteString -> Salt -> Int -> ByteString
pbkdf2 password (SaltBS salt) c = pbkdf2 password (SaltBS salt) c =
let hLen = 32 let hLen = 32
dkLen = hLen in go hLen dkLen dkLen = hLen in go hLen dkLen
where where
go hLen dkLen | dkLen > (2^32 - 1) * hLen = error "Derived key too long." go hLen dkLen | dkLen > (2^(32 :: Int) - 1) * hLen = error "Derived key too long."
| otherwise = | otherwise =
let !l = ceiling ((fromIntegral dkLen / fromIntegral hLen) :: Double) let !l = ceiling ((fromIntegral dkLen / fromIntegral hLen) :: Double)
!r = dkLen - (l - 1) * hLen !r = dkLen - (l - 1) * hLen
@ -196,6 +202,9 @@ pbkdf2 password (SaltBS salt) c =
-- | Generate a 'Salt' from 128 bits of data from @\/dev\/urandom@, with the -- | Generate a 'Salt' from 128 bits of data from @\/dev\/urandom@, with the
-- system RNG as a fallback. This is the function used to generate salts by -- system RNG as a fallback. This is the function used to generate salts by
-- 'makePassword'. -- 'makePassword'.
--
-- @since 1.4.18
--
genSaltIO :: IO Salt genSaltIO :: IO Salt
genSaltIO = genSaltIO =
Control.Exception.catch genSaltDevURandom def Control.Exception.catch genSaltDevURandom def
@ -249,6 +258,9 @@ writePwHash (strength, SaltBS salt, hash) =
-- database. Generates a salt using high-quality randomness from -- database. Generates a salt using high-quality randomness from
-- @\/dev\/urandom@ or (if that is not available, for example on Windows) -- @\/dev\/urandom@ or (if that is not available, for example on Windows)
-- 'System.Random', which is included in the hashed output. -- 'System.Random', which is included in the hashed output.
--
-- @since 1.4.18
--
makePassword :: ByteString -> Int -> IO ByteString makePassword :: ByteString -> Int -> IO ByteString
makePassword = makePasswordWith pbkdf1 makePassword = makePasswordWith pbkdf1
@ -257,6 +269,8 @@ makePassword = makePasswordWith pbkdf1
-- --
-- >>> makePasswordWith pbkdf1 "password" 14 -- >>> makePasswordWith pbkdf1 "password" 14
-- --
-- @since 1.4.18
--
makePasswordWith :: (ByteString -> Salt -> Int -> ByteString) makePasswordWith :: (ByteString -> Salt -> Int -> ByteString)
-- ^ The algorithm to use (e.g. pbkdf1) -- ^ The algorithm to use (e.g. pbkdf1)
-> ByteString -> ByteString
@ -273,6 +287,9 @@ makePasswordWith algorithm password strength = do
-- Note that, unlike 'makePasswordWith', this function takes the @raw@ -- Note that, unlike 'makePasswordWith', this function takes the @raw@
-- number of iterations. This means the user will need to specify a -- number of iterations. This means the user will need to specify a
-- sensible value, typically @10000@ or @20000@. -- sensible value, typically @10000@ or @20000@.
--
-- @since 1.4.18
--
makePasswordSaltWith :: (ByteString -> Salt -> Int -> ByteString) makePasswordSaltWith :: (ByteString -> Salt -> Int -> ByteString)
-- ^ A function modeling an algorithm (e.g. 'pbkdf1') -- ^ A function modeling an algorithm (e.g. 'pbkdf1')
-> (Int -> Int) -> (Int -> Int)
@ -293,6 +310,9 @@ makePasswordSaltWith algorithm strengthModifier pwd salt strength = writePwHash
-- --
-- > >>> makePasswordSalt "hunter2" (makeSalt "72cd18b5ebfe6e96") 14 -- > >>> makePasswordSalt "hunter2" (makeSalt "72cd18b5ebfe6e96") 14
-- > "sha256|14|NzJjZDE4YjVlYmZlNmU5Ng==|yuiNrZW3KHX+pd0sWy9NTTsy5Yopmtx4UYscItSsoxc=" -- > "sha256|14|NzJjZDE4YjVlYmZlNmU5Ng==|yuiNrZW3KHX+pd0sWy9NTTsy5Yopmtx4UYscItSsoxc="
--
-- @since 1.4.18
--
makePasswordSalt :: ByteString -> Salt -> Int -> ByteString makePasswordSalt :: ByteString -> Salt -> Int -> ByteString
makePasswordSalt = makePasswordSaltWith pbkdf1 (2^) makePasswordSalt = makePasswordSaltWith pbkdf1 (2^)
@ -309,6 +329,8 @@ makePasswordSalt = makePasswordSaltWith pbkdf1 (2^)
-- > >>> verifyPasswordWith pbkdf2 id "hunter2" "sha256..." -- > >>> verifyPasswordWith pbkdf2 id "hunter2" "sha256..."
-- > True -- > True
-- --
-- @since 1.4.18
--
verifyPasswordWith :: (ByteString -> Salt -> Int -> ByteString) verifyPasswordWith :: (ByteString -> Salt -> Int -> ByteString)
-- ^ A function modeling an algorithm (e.g. pbkdf1) -- ^ A function modeling an algorithm (e.g. pbkdf1)
-> (Int -> Int) -> (Int -> Int)
@ -325,6 +347,9 @@ verifyPasswordWith algorithm strengthModifier userInput pwHash =
encode (algorithm userInput salt (strengthModifier strength)) == goodHash encode (algorithm userInput salt (strengthModifier strength)) == goodHash
-- | Like 'verifyPasswordWith', but uses 'pbkdf1' as algorithm. -- | Like 'verifyPasswordWith', but uses 'pbkdf1' as algorithm.
--
-- @since 1.4.18
--
verifyPassword :: ByteString -> ByteString -> Bool verifyPassword :: ByteString -> ByteString -> Bool
verifyPassword = verifyPasswordWith pbkdf1 (2^) verifyPassword = verifyPasswordWith pbkdf1 (2^)
@ -338,6 +363,9 @@ verifyPassword = verifyPasswordWith pbkdf1 (2^)
-- This function can be used to periodically update your password database when -- This function can be used to periodically update your password database when
-- computers get faster, in order to keep up with Moore's law. This isn't hugely -- computers get faster, in order to keep up with Moore's law. This isn't hugely
-- important, but it's a good idea. -- important, but it's a good idea.
--
-- @since 1.4.18
--
strengthenPassword :: ByteString -> Int -> ByteString strengthenPassword :: ByteString -> Int -> ByteString
strengthenPassword pwHash newstr = strengthenPassword pwHash newstr =
case readPwHash pwHash of case readPwHash pwHash of
@ -352,6 +380,9 @@ strengthenPassword pwHash newstr =
hash = decodeLenient hashB64 hash = decodeLenient hashB64
-- | Return the strength of a password hash. -- | Return the strength of a password hash.
--
-- @since 1.4.18
--
passwordStrength :: ByteString -> Int passwordStrength :: ByteString -> Int
passwordStrength pwHash = case readPwHash pwHash of passwordStrength pwHash = case readPwHash pwHash of
Nothing -> 0 Nothing -> 0
@ -365,12 +396,18 @@ passwordStrength pwHash = case readPwHash pwHash of
-- hash. You can generate a salt with 'genSaltIO' or 'genSaltRandom', or if you -- hash. You can generate a salt with 'genSaltIO' or 'genSaltRandom', or if you
-- really know what you're doing, you can create them from your own ByteString -- really know what you're doing, you can create them from your own ByteString
-- values with 'makeSalt'. -- values with 'makeSalt'.
--
-- @since 1.4.18
--
newtype Salt = SaltBS ByteString newtype Salt = SaltBS ByteString
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)
-- | Create a 'Salt' from a 'ByteString'. The input must be at least 8 -- | Create a 'Salt' from a 'ByteString'. The input must be at least 8
-- characters, and can contain arbitrary bytes. Most users will not need to use -- characters, and can contain arbitrary bytes. Most users will not need to use
-- this function. -- this function.
--
-- @since 1.4.18
--
makeSalt :: ByteString -> Salt makeSalt :: ByteString -> Salt
makeSalt = SaltBS . encode . check_length makeSalt = SaltBS . encode . check_length
where check_length salt | B.length salt < 8 = where check_length salt | B.length salt < 8 =
@ -379,17 +416,26 @@ makeSalt = SaltBS . encode . check_length
-- | Convert a 'Salt' into a 'ByteString'. The resulting 'ByteString' will be -- | Convert a 'Salt' into a 'ByteString'. The resulting 'ByteString' will be
-- base64-encoded. Most users will not need to use this function. -- base64-encoded. Most users will not need to use this function.
--
-- @since 1.4.18
--
exportSalt :: Salt -> ByteString exportSalt :: Salt -> ByteString
exportSalt (SaltBS bs) = bs exportSalt (SaltBS bs) = bs
-- | Convert a raw 'ByteString' into a 'Salt'. -- | Convert a raw 'ByteString' into a 'Salt'.
-- Use this function with caution, since using a weak salt will result in a -- Use this function with caution, since using a weak salt will result in a
-- weak password. -- weak password.
--
-- @since 1.4.18
--
importSalt :: ByteString -> Salt importSalt :: ByteString -> Salt
importSalt = SaltBS importSalt = SaltBS
-- | Is the format of a password hash valid? Attempts to parse a given password -- | Is the format of a password hash valid? Attempts to parse a given password
-- hash. Returns 'True' if it parses correctly, and 'False' otherwise. -- hash. Returns 'True' if it parses correctly, and 'False' otherwise.
--
-- @since 1.4.18
--
isPasswordFormatValid :: ByteString -> Bool isPasswordFormatValid :: ByteString -> Bool
isPasswordFormatValid = isJust . readPwHash isPasswordFormatValid = isJust . readPwHash
@ -397,6 +443,9 @@ isPasswordFormatValid = isJust . readPwHash
-- generator. Returns the salt and the updated random number generator. This is -- generator. Returns the salt and the updated random number generator. This is
-- meant to be used with 'makePasswordSalt' by people who would prefer to either -- meant to be used with 'makePasswordSalt' by people who would prefer to either
-- use their own random number generator or avoid the 'IO' monad. -- use their own random number generator or avoid the 'IO' monad.
--
-- @since 1.4.18
--
genSaltRandom :: (RandomGen b) => b -> (Salt, b) genSaltRandom :: (RandomGen b) => b -> (Salt, b)
genSaltRandom gen = (salt, newgen) genSaltRandom gen = (salt, newgen)
where rands _ 0 = [] where rands _ 0 = []
@ -413,17 +462,3 @@ modifySTRef' ref f = do
let x' = f x let x' = f x
x' `seq` writeSTRef ref x' x' `seq` writeSTRef ref x'
#endif #endif
#if MIN_VERSION_bytestring(0, 10, 0)
toStrict :: BL.ByteString -> BS.ByteString
toStrict = BL.toStrict
fromStrict :: BS.ByteString -> BL.ByteString
fromStrict = BL.fromStrict
#else
toStrict :: BL.ByteString -> BS.ByteString
toStrict = BS.concat . BL.toChunks
fromStrict :: BS.ByteString -> BL.ByteString
fromStrict = BL.fromChunks . return
#endif

View File

@ -1,5 +1,6 @@
cabal-version: >=1.10
name: yesod-auth name: yesod-auth
version: 1.4.13.5 version: 1.6.11.2
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman, Patrick Brisbin author: Michael Snoyman, Patrick Brisbin
@ -7,7 +8,6 @@ maintainer: Michael Snoyman <michael@snoyman.com>
synopsis: Authentication for Yesod. synopsis: Authentication for Yesod.
category: Web, Yesod category: Web, Yesod
stability: Stable stability: Stable
cabal-version: >= 1.6.0
build-type: Simple build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
description: API docs and the README are available at <http://www.stackage.org/package/yesod-auth> description: API docs and the README are available at <http://www.stackage.org/package/yesod-auth>
@ -20,51 +20,49 @@ flag network-uri
default: True default: True
library library
build-depends: base >= 4 && < 5 default-language: Haskell2010
, authenticate >= 1.3 build-depends: base >= 4.10 && < 5
, bytestring >= 0.9.1.4
, yesod-core >= 1.4.20 && < 1.5
, wai >= 1.4
, template-haskell
, base16-bytestring
, cryptohash
, random >= 1.0.0.2
, text >= 0.7
, mime-mail >= 0.3
, yesod-persistent >= 1.4
, shakespeare
, containers
, unordered-containers
, yesod-form >= 1.4 && < 1.5
, transformers >= 0.2.2
, persistent >= 2.1 && < 2.7
, persistent-template >= 2.1 && < 2.7
, http-client
, http-conduit >= 2.1
, aeson >= 0.7 , aeson >= 0.7
, lifted-base >= 0.1 , attoparsec-aeson >= 2.1
, authenticate >= 1.3.4
, base16-bytestring
, base64-bytestring
, binary
, blaze-builder
, blaze-html >= 0.5 , blaze-html >= 0.5
, blaze-markup >= 0.5.1 , blaze-markup >= 0.5.1
, http-types , bytestring >= 0.9.1.4
, file-embed , conduit >= 1.3
, email-validate >= 1.0
, data-default
, resourcet
, safe
, time
, base64-bytestring
, byteable
, binary
, http-client
, blaze-builder
, conduit
, conduit-extra , conduit-extra
, containers
, cryptonite
, data-default
, email-validate >= 1.0
, file-embed
, http-client >= 0.5
, http-client-tls
, http-conduit >= 2.1
, http-types
, memory
, nonce >= 1.0.2 && < 1.1 , nonce >= 1.0.2 && < 1.1
, persistent >= 2.8
, random >= 1.0.0.2
, safe
, shakespeare
, template-haskell
, text >= 0.7
, time
, transformers >= 0.2.2
, unliftio
, unliftio-core
, unordered-containers
, wai >= 1.4
, yesod-core >= 1.6 && < 1.7
, yesod-form >= 1.6 && < 1.8
, yesod-persistent >= 1.6
if flag(network-uri) if flag(network-uri)
build-depends: network-uri >= 2.6 build-depends: network-uri >= 2.6
else
build-depends: network < 2.6
exposed-modules: Yesod.Auth exposed-modules: Yesod.Auth
Yesod.Auth.BrowserId Yesod.Auth.BrowserId
@ -73,11 +71,10 @@ library
Yesod.Auth.OpenId Yesod.Auth.OpenId
Yesod.Auth.Rpxnow Yesod.Auth.Rpxnow
Yesod.Auth.Message Yesod.Auth.Message
Yesod.Auth.GoogleEmail
Yesod.Auth.GoogleEmail2 Yesod.Auth.GoogleEmail2
Yesod.Auth.Hardcoded Yesod.Auth.Hardcoded
Yesod.Auth.Util.PasswordStore
other-modules: Yesod.Auth.Routes other-modules: Yesod.Auth.Routes
Yesod.PasswordStore
ghc-options: -Wall ghc-options: -Wall
source-repository head source-repository head

View File

@ -1,3 +1,4 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE PatternGuards #-} {-# LANGUAGE PatternGuards #-}
module AddHandler (addHandler) where module AddHandler (addHandler) where
@ -5,9 +6,24 @@ import Prelude hiding (readFile)
import System.IO (hFlush, stdout) import System.IO (hFlush, stdout)
import Data.Char (isLower, toLower, isSpace) import Data.Char (isLower, toLower, isSpace)
import Data.List (isPrefixOf, isSuffixOf, stripPrefix) import Data.List (isPrefixOf, isSuffixOf, stripPrefix)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe, listToMaybe)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.IO as TIO import qualified Data.Text.IO as TIO
#if MIN_VERSION_Cabal(3, 7, 0)
import Distribution.Simple.PackageDescription (readGenericPackageDescription)
#elif MIN_VERSION_Cabal(2, 2, 0)
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
#elif MIN_VERSION_Cabal(2, 0, 0)
import Distribution.PackageDescription.Parse (readGenericPackageDescription)
#else
import Distribution.PackageDescription.Parse (readPackageDescription)
#endif
#if MIN_VERSION_Cabal(3, 6, 0)
import Distribution.Utils.Path
#endif
import Distribution.PackageDescription.Configuration (flattenPackageDescription)
import Distribution.PackageDescription (allBuildInfo, hsSourceDirs)
import Distribution.Verbosity (normal)
import System.Directory (getDirectoryContents, doesFileExist) import System.Directory (getDirectoryContents, doesFileExist)
import Control.Monad (unless) import Control.Monad (unless)
@ -31,7 +47,7 @@ cmdLineArgsError = "You have to specify a route name if you want to add handler
addHandler :: Maybe String -> Maybe String -> [String] -> IO () addHandler :: Maybe String -> Maybe String -> [String] -> IO ()
addHandler (Just route) pat met = do addHandler (Just route) pat met = do
cabal <- getCabal cabal <- getCabal
checked <- checkRoute route checked <- checkRoute route cabal
let routePair = case checked of let routePair = case checked of
Left err@EmptyRoute -> (error . show) err Left err@EmptyRoute -> (error . show) err
Left err@RouteCaseError -> (error . show) err Left err@RouteCaseError -> (error . show) err
@ -54,7 +70,7 @@ addHandlerInteractive = do
putStr "Name of route (without trailing R): " putStr "Name of route (without trailing R): "
hFlush stdout hFlush stdout
name <- getLine name <- getLine
checked <- checkRoute name checked <- checkRoute name cabal
case checked of case checked of
Left err@EmptyRoute -> (error . show) err Left err@EmptyRoute -> (error . show) err
Left err@RouteCaseError -> print err >> routeInput Left err@RouteCaseError -> print err >> routeInput
@ -73,11 +89,22 @@ addHandlerInteractive = do
methods <- getLine methods <- getLine
addHandlerFiles cabal routePair pattern methods addHandlerFiles cabal routePair pattern methods
getRoutesFilePath :: IO FilePath
getRoutesFilePath = do
let oldPath = "config/routes"
oldExists <- doesFileExist oldPath
pure $ if oldExists
then oldPath
else "config/routes.yesodroutes"
addHandlerFiles :: FilePath -> (String, FilePath) -> String -> String -> IO () addHandlerFiles :: FilePath -> (String, FilePath) -> String -> String -> IO ()
addHandlerFiles cabal (name, handlerFile) pattern methods = do addHandlerFiles cabal (name, handlerFile) pattern methods = do
modify "Application.hs" $ fixApp name src <- getSrcDir cabal
let applicationFile = concat [src, "/Application.hs"]
modify applicationFile $ fixApp name
modify cabal $ fixCabal name modify cabal $ fixCabal name
modify "config/routes" $ fixRoutes name pattern methods routesPath <- getRoutesFilePath
modify routesPath $ fixRoutes name pattern methods
writeFile handlerFile $ mkHandler name pattern methods writeFile handlerFile $ mkHandler name pattern methods
specExists <- doesFileExist specFile specExists <- doesFileExist specFile
unless specExists $ unless specExists $
@ -94,15 +121,16 @@ getCabal = do
[] -> error "No cabal file found" [] -> error "No cabal file found"
_ -> error "Too many cabal files found" _ -> error "Too many cabal files found"
checkRoute :: String -> IO (Either RouteError (String, FilePath)) checkRoute :: String -> FilePath -> IO (Either RouteError (String, FilePath))
checkRoute name = checkRoute name cabal =
case name of case name of
[] -> return $ Left EmptyRoute [] -> return $ Left EmptyRoute
c:_ c:_
| isLower c -> return $ Left RouteCaseError | isLower c -> return $ Left RouteCaseError
| otherwise -> do | otherwise -> do
-- Check that the handler file doesn't already exist -- Check that the handler file doesn't already exist
let handlerFile = concat ["Handler/", name, ".hs"] src <- getSrcDir cabal
let handlerFile = concat [src, "/Handler/", name, ".hs"]
exists <- doesFileExist handlerFile exists <- doesFileExist handlerFile
if exists if exists
then (return . Left . RouteExists) handlerFile then (return . Left . RouteExists) handlerFile
@ -214,3 +242,18 @@ mkHandler name pattern methods = unlines
uncapitalize :: String -> String uncapitalize :: String -> String
uncapitalize (x:xs) = toLower x : xs uncapitalize (x:xs) = toLower x : xs
uncapitalize "" = "" uncapitalize "" = ""
getSrcDir :: FilePath -> IO FilePath
getSrcDir cabal = do
#if MIN_VERSION_Cabal(2, 0, 0)
pd <- flattenPackageDescription <$> readGenericPackageDescription normal cabal
#else
pd <- flattenPackageDescription <$> readPackageDescription normal cabal
#endif
let buildInfo = allBuildInfo pd
srcDirs = concatMap hsSourceDirs buildInfo
#if MIN_VERSION_Cabal(3, 6, 0)
return $ maybe "." getSymbolicPath $ listToMaybe srcDirs
#else
return $ fromMaybe "." $ listToMaybe srcDirs
#endif

View File

@ -1,270 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts #-}
module Build
( getDeps
, touchDeps
, touch
, recompDeps
, isNewerThan
, safeReadFile
) where
import Control.Applicative ((<|>), many, (<$>))
import qualified Data.Attoparsec.Text as A
import Data.Char (isSpace, isUpper)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import Control.Exception (SomeException, try, IOException)
import Control.Exception.Lifted (handle)
import Control.Monad (when, filterM, forM, forM_, (>=>))
import Control.Monad.Trans.State (StateT, get, put, execStateT)
import Control.Monad.Trans.Writer (WriterT, tell, execWriterT)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Class (lift)
import Data.Monoid (Monoid (mappend, mempty))
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified System.Posix.Types
import System.Directory
import System.FilePath (takeExtension, replaceExtension, (</>), takeDirectory,
splitPath, joinPath)
import System.PosixCompat.Files (getFileStatus, setFileTimes,
accessTime, modificationTime)
import Text.Shakespeare (Deref)
import Text.Julius (juliusUsedIdentifiers)
import Text.Cassius (cassiusUsedIdentifiers)
import Text.Lucius (luciusUsedIdentifiers)
safeReadFile :: MonadIO m => FilePath -> m (Either IOException ByteString)
safeReadFile = liftIO . try . S.readFile
touch :: IO ()
touch = do
m <- handle (\(_ :: SomeException) -> return Map.empty) $ readFile touchCache >>= readIO
x <- fmap snd (getDeps [])
m' <- execStateT (execWriterT $ touchDeps id updateFileTime x) m
createDirectoryIfMissing True $ takeDirectory touchCache
writeFile touchCache $ show m'
where
touchCache = "dist/touchCache.txt"
-- | Returns True if any files were touched, otherwise False
recompDeps :: [FilePath] -> StateT (Map.Map FilePath (Set.Set Deref)) IO Bool
recompDeps =
fmap toBool . execWriterT . (liftIO . getDeps >=> touchDeps hiFile removeHi . snd)
where
toBool NoFilesTouched = False
toBool SomeFilesTouched = True
type Deps = Map.Map FilePath ([FilePath], ComparisonType)
getDeps :: [FilePath] -> IO ([FilePath], Deps)
getDeps hsSourceDirs = do
let defSrcDirs = case hsSourceDirs of
[] -> ["."]
ds -> ds
hss <- fmap concat $ mapM findHaskellFiles defSrcDirs
deps' <- mapM determineDeps hss
return $ (hss, fixDeps $ zip hss deps')
data AnyFilesTouched = NoFilesTouched | SomeFilesTouched
instance Monoid AnyFilesTouched where
mempty = NoFilesTouched
mappend NoFilesTouched NoFilesTouched = mempty
mappend _ _ = SomeFilesTouched
touchDeps :: (FilePath -> FilePath) ->
(FilePath -> FilePath -> IO ()) ->
Deps -> WriterT AnyFilesTouched (StateT (Map.Map FilePath (Set.Set Deref)) IO) ()
touchDeps f action deps = (mapM_ go . Map.toList) deps
where
go (x, (ys, ct)) = do
isChanged <- handle (\(_ :: SomeException) -> return True) $ lift $
case ct of
AlwaysOutdated -> return True
CompareUsedIdentifiers getDerefs -> do
derefMap <- get
ebs <- safeReadFile x
let newDerefs =
case ebs of
Left _ -> Set.empty
Right bs -> Set.fromList $ getDerefs $ T.unpack $ decodeUtf8With lenientDecode bs
put $ Map.insert x newDerefs derefMap
case Map.lookup x derefMap of
Just oldDerefs | oldDerefs == newDerefs -> return False
_ -> return True
when isChanged $ forM_ ys $ \y -> do
n <- liftIO $ x `isNewerThan` f y
when n $ do
liftIO $ putStrLn ("Forcing recompile for " ++ y ++ " because of " ++ x)
liftIO $ action x y
tell SomeFilesTouched
-- | remove the .hi files for a .hs file, thereby forcing a recompile
removeHi :: FilePath -> FilePath -> IO ()
removeHi _ hs = mapM_ removeFile' hiFiles
where
removeFile' file = try' (removeFile file) >> return ()
hiFiles = map (\e -> "dist/build" </> removeSrc (replaceExtension hs e))
["hi", "p_hi"]
-- | change file mtime of .hs file to that of the dependency
updateFileTime :: FilePath -> FilePath -> IO ()
updateFileTime x hs = do
(_ , modx) <- getFileStatus' x
(access, _ ) <- getFileStatus' hs
_ <- try' (setFileTimes hs access modx)
return ()
hiFile :: FilePath -> FilePath
hiFile hs = "dist/build" </> removeSrc (replaceExtension hs "hi")
removeSrc :: FilePath -> FilePath
removeSrc f = case splitPath f of
("src/" : xs) -> joinPath xs
_ -> f
try' :: IO x -> IO (Either SomeException x)
try' = try
isNewerThan :: FilePath -> FilePath -> IO Bool
isNewerThan f1 f2 = do
(_, mod1) <- getFileStatus' f1
(_, mod2) <- getFileStatus' f2
return (mod1 > mod2)
getFileStatus' :: FilePath ->
IO (System.Posix.Types.EpochTime, System.Posix.Types.EpochTime)
getFileStatus' fp = do
efs <- try' $ getFileStatus fp
case efs of
Left _ -> return (0, 0)
Right fs -> return (accessTime fs, modificationTime fs)
fixDeps :: [(FilePath, [(ComparisonType, FilePath)])] -> Deps
fixDeps =
Map.unionsWith combine . map go
where
go :: (FilePath, [(ComparisonType, FilePath)]) -> Deps
go (x, ys) = Map.fromList $ map (\(ct, y) -> (y, ([x], ct))) ys
combine (ys1, ct) (ys2, _) = (ys1 `mappend` ys2, ct)
findHaskellFiles :: FilePath -> IO [FilePath]
findHaskellFiles path = do
contents <- getDirectoryContents path
fmap concat $ mapM go contents
where
go ('.':_) = return []
go filename = do
d <- doesDirectoryExist full
if not d
then if isHaskellFile
then return [full]
else return []
else if isHaskellDir
then findHaskellFiles full
else return []
where
-- this could fail on unicode
isHaskellDir = isUpper (head filename)
isHaskellFile = takeExtension filename `elem` watch_files
full = path </> filename
watch_files = [".hs", ".lhs"]
data TempType = StaticFiles FilePath
| Verbatim | Messages FilePath | Hamlet | Widget | Julius | Cassius | Lucius
deriving Show
-- | How to tell if a file is outdated.
data ComparisonType = AlwaysOutdated
| CompareUsedIdentifiers (String -> [Deref])
determineDeps :: FilePath -> IO [(ComparisonType, FilePath)]
determineDeps x = do
y <- safeReadFile x
case y of
Left _ -> return []
Right bs -> do
let z = A.parseOnly (many $ (parser <|> (A.anyChar >> return Nothing)))
$ decodeUtf8With lenientDecode bs
case z of
Left _ -> return []
Right r -> mapM go r >>= filterM (doesFileExist . snd) . concat
where
go (Just (StaticFiles fp, _)) = map ((,) AlwaysOutdated) <$> getFolderContents fp
go (Just (Hamlet, f)) = return [(AlwaysOutdated, f)]
go (Just (Widget, f)) = return
[ (AlwaysOutdated, "templates/" ++ f ++ ".hamlet")
, (CompareUsedIdentifiers $ map fst . juliusUsedIdentifiers, "templates/" ++ f ++ ".julius")
, (CompareUsedIdentifiers $ map fst . luciusUsedIdentifiers, "templates/" ++ f ++ ".lucius")
, (CompareUsedIdentifiers $ map fst . cassiusUsedIdentifiers, "templates/" ++ f ++ ".cassius")
]
go (Just (Julius, f)) = return [(CompareUsedIdentifiers $ map fst . juliusUsedIdentifiers, f)]
go (Just (Cassius, f)) = return [(CompareUsedIdentifiers $ map fst . cassiusUsedIdentifiers, f)]
go (Just (Lucius, f)) = return [(CompareUsedIdentifiers $ map fst . luciusUsedIdentifiers, f)]
go (Just (Verbatim, f)) = return [(AlwaysOutdated, f)]
go (Just (Messages f, _)) = map ((,) AlwaysOutdated) <$> getFolderContents f
go Nothing = return []
parser = do
ty <- (do _ <- A.string "\nstaticFiles \""
x' <- A.many1 $ A.satisfy (/= '"')
return $ StaticFiles x')
<|> (A.string "$(parseRoutesFile " >> return Verbatim)
<|> (A.string "$(hamletFile " >> return Hamlet)
<|> (A.string "$(ihamletFile " >> return Hamlet)
<|> (A.string "$(whamletFile " >> return Hamlet)
<|> (A.string "$(html " >> return Hamlet)
<|> (A.string "$(widgetFile " >> return Widget)
<|> (A.string "$(Settings.hamletFile " >> return Hamlet)
<|> (A.string "$(Settings.widgetFile " >> return Widget)
<|> (A.string "$(juliusFile " >> return Julius)
<|> (A.string "$(cassiusFile " >> return Cassius)
<|> (A.string "$(luciusFile " >> return Lucius)
<|> (A.string "$(persistFile " >> return Verbatim)
<|> (
A.string "$(persistFileWith " >>
A.many1 (A.satisfy (/= '"')) >>
return Verbatim)
<|> (do
_ <- A.string "\nmkMessage \""
A.skipWhile (/= '"')
_ <- A.string "\" \""
x' <- A.many1 $ A.satisfy (/= '"')
_ <- A.string "\" \""
_y <- A.many1 $ A.satisfy (/= '"')
_ <- A.string "\""
return $ Messages x')
case ty of
Messages{} -> return $ Just (ty, "")
StaticFiles{} -> return $ Just (ty, "")
_ -> do
A.skipWhile isSpace
_ <- A.char '"'
y <- A.many1 $ A.satisfy (/= '"')
_ <- A.char '"'
A.skipWhile isSpace
_ <- A.char ')'
return $ Just (ty, y)
getFolderContents :: FilePath -> IO [FilePath]
getFolderContents fp = do
cs <- getDirectoryContents fp
let notHidden ('.':_) = False
notHidden ('t':"mp") = False
notHidden ('f':"ay") = False
notHidden _ = True
fmap concat $ forM (filter notHidden cs) $ \c -> do
let f = fp ++ '/' : c
isFile <- doesFileExist f
if isFile then return [f] else getFolderContents f

View File

@ -1,3 +1,108 @@
# ChangeLog for yesod-bin
## 1.6.2.2
* Support Cabal 3.8 [#1769](https://github.com/yesodweb/yesod/pull/1769)
## 1.6.2.1
* Support Cabal 3.6 [#1754](https://github.com/yesodweb/yesod/pull/1754)
## 1.6.2
* aeson 2.0
## 1.6.1
Added command line options `cert` and `key` to allow TLS certificate and key files to be passed to `yesod devel` [#1717](https://github.com/yesodweb/yesod/pull/1717)
## 1.6.0.6
Fix the `add-handler` subcommand to support both the old default routes filename (`routes`) and the new one (`routes.yesodroutes`) [#1688](https://github.com/yesodweb/yesod/pull/1688)
## 1.6.0.5
* Use process groups to ensure GHC is killed on Ctrl-C [#1683](https://github.com/yesodweb/yesod/pull/1683)
## 1.6.0.4
* Support Cabal 3.0
## 1.6.0.3
* Support Cabal 2.2 [#1151](https://github.com/yesodweb/yesod/issues/1511)
## 1.6.0.2
* Fix broken support for older http-reverse-proxy
## 1.6.0.1
* Support for http-reverse-proxy 0.6
## 1.6.0
* Upgrade to conduit 1.3.0
* Remove configure, build, touch, and test commands
## 1.5.3
* Support typed-process-0.2.0.0
## 1.5.2.6
* Drop an upper bound
## 1.5.2.5
* Support for `add-handler` when modules are in `src/` directory [#1413](https://github.com/yesodweb/yesod/issues/1413)
## 1.5.2.4
* Cabal 2.0 support
## 1.5.2.3
* Fix race condition which leads dev server to stay in compilation mode. [#1380](https://github.com/yesodweb/yesod/issues/1380)
## 1.5.2.2
* I guess `--no-nix-pure` implies Nix... sigh [#1359](https://github.com/yesodweb/yesod/issues/1359)
## 1.5.2.1
* Use `--no-nix-pure` [#1357](https://github.com/yesodweb/yesod/issues/1357)
## 1.5.2
* Fix warnings
## 1.5.1
* Add `--host` option to `yesod devel`
## 1.5.0.1
* Fix build failure
## 1.5.0
Rewrite of `yesod devel` to take advantage of Stack for a simpler codebase.
Advantages:
* Does not link against the ghc library, so can be used with multiple
GHC versions
* Leverages Stack's ability to check for dependent files, which is
more robust than what yesod devel was doing previously
* Seems to involve less rebuilding of the library on initial run
Disadvantages:
* Lost some functionality (e.g., failure hooks, controlling the exit
command)
* Newer codebase, quite likely has bugs that need to be ironed out.
## 1.4.18.7 ## 1.4.18.7
* Actually release the changes for #1284 * Actually release the changes for #1284

View File

@ -1,163 +1,145 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module Devel module Devel
( devel ( devel
, develSignal
, DevelOpts(..) , DevelOpts(..)
, DevelTermOpt(..)
, defaultDevelOpts
) where ) where
import qualified Distribution.Compiler as D import Control.Applicative ((<|>))
import qualified Distribution.ModuleName as D import UnliftIO (race_)
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM
import qualified UnliftIO.Exception as Ex
import Control.Monad (forever, unless, void,
when)
import Data.ByteString (ByteString, isInfixOf)
import qualified Data.ByteString.Lazy as LB
import Conduit
import Data.FileEmbed (embedFile)
import qualified Data.Map as Map
import Data.Maybe (isJust)
import qualified Data.Set as Set
import Data.Streaming.Network (bindPortTCP,
bindRandomPortTCP)
import Data.String (fromString)
import Data.Time (getCurrentTime)
import qualified Distribution.Package as D
import qualified Distribution.PackageDescription as D import qualified Distribution.PackageDescription as D
#if MIN_VERSION_Cabal(3,8,0)
import qualified Distribution.Simple.PackageDescription as D
#endif
#if MIN_VERSION_Cabal(2, 2, 0)
import qualified Distribution.PackageDescription.Parsec as D
#else
import qualified Distribution.PackageDescription.Parse as D import qualified Distribution.PackageDescription.Parse as D
import qualified Distribution.Simple.Configure as D #endif
import qualified Distribution.Simple.Program as D
import qualified Distribution.Simple.Utils as D import qualified Distribution.Simple.Utils as D
import qualified Distribution.Verbosity as D import qualified Distribution.Verbosity as D
import Network.HTTP.Client (newManager)
import Control.Applicative ((<$>), (<*>)) import Network.HTTP.Client (managerSetProxy,
import Control.Concurrent (forkIO, threadDelay) noProxy)
import Control.Concurrent.MVar (MVar, newEmptyMVar, import Network.HTTP.Client.TLS (tlsManagerSettings)
takeMVar, tryPutMVar)
import Control.Concurrent.Async (race_)
import qualified Control.Exception as Ex
import Control.Monad (forever, unless, void,
when, forM)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (evalStateT, get)
import qualified Data.IORef as I
import qualified Data.ByteString.Lazy as LB
import Data.Char (isNumber, isUpper)
import qualified Data.List as L
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set
import System.Directory
import System.Environment (getEnvironment)
import System.Exit (ExitCode (..),
exitFailure,
exitSuccess)
import System.FilePath (dropExtension,
splitDirectories,
takeExtension, (</>))
import System.FSNotify
import System.IO (Handle)
import System.IO.Error (isDoesNotExistError)
import System.Posix.Types (EpochTime)
import System.PosixCompat.Files (getFileStatus,
modificationTime)
import System.Process (ProcessHandle,
createProcess, env,
getProcessExitCode,
proc, readProcess,
system,
terminateProcess)
import System.Timeout (timeout)
import Build (getDeps, isNewerThan,
recompDeps)
import GhcBuild (buildPackage,
getBuildFlags, getPackageArgs)
import qualified Config as GHC
import Data.Streaming.Network (bindPortTCP)
import Network (withSocketsDo)
import Network.HTTP.Conduit (conduitManagerSettings, newManager)
import Data.Default.Class (def)
#if MIN_VERSION_http_client(0,4,7)
import Network.HTTP.Client (managerSetProxy, noProxy)
#endif
import Network.HTTP.ReverseProxy (ProxyDest (ProxyDest), import Network.HTTP.ReverseProxy (ProxyDest (ProxyDest),
waiProxyToSettings, wpsTimeout, wpsOnExc) waiProxyToSettings,
wpsOnExc, wpsTimeout,
#if MIN_VERSION_http_reverse_proxy(0, 6, 0)
defaultWaiProxySettings
#else
def
#endif
)
import qualified Network.HTTP.ReverseProxy as ReverseProxy import qualified Network.HTTP.ReverseProxy as ReverseProxy
import Network.HTTP.Types (status200, status503) import Network.HTTP.Types (status200, status503)
import Network.Socket (sClose) import qualified Network.Socket
import Network.Wai (responseLBS, requestHeaders, import Network.Wai (requestHeaderHost,
requestHeaderHost) requestHeaders,
responseLBS)
import Network.Wai.Handler.Warp (defaultSettings, runSettings,
setPort, setHost)
import Network.Wai.Handler.WarpTLS (runTLS, tlsSettings,
tlsSettingsMemory)
import Network.Wai.Parse (parseHttpAccept) import Network.Wai.Parse (parseHttpAccept)
import Network.Wai.Handler.Warp (run, defaultSettings, setPort) import Say
import Network.Wai.Handler.WarpTLS (runTLS, tlsSettingsMemory) import System.Directory
import SrcLoc (Located) import System.Environment (getEnvironment,
import Data.FileEmbed (embedFile) getExecutablePath)
import System.FilePath (takeDirectory,
takeFileName, (</>))
import System.FSNotify
import System.IO (stdout, stderr)
import System.IO.Error (isDoesNotExistError)
import Data.Conduit.Process.Typed
lockFile :: FilePath -- We have two special files:
lockFile = "yesod-devel/devel-terminate" --
-- * The terminate file tells the child process to die simply by being
-- present. Ideally we'd handle this via killing the process
-- directly, but that's historically never worked reliably.
--
-- * The signal file, which tells us that "stack build" has succeeded
-- yet again.
data SpecialFile = TermFile | SignalFile
writeLock :: DevelOpts -> IO () specialFilePath :: SpecialFile -> FilePath
writeLock _opts = do
createDirectoryIfMissing True "yesod-devel"
writeFile lockFile ""
createDirectoryIfMissing True "dist" -- for compatibility with old devel.hs
writeFile "dist/devel-terminate" ""
removeLock :: DevelOpts -> IO () -- used by scaffolded app, cannot change
removeLock _opts = do specialFilePath TermFile = "yesod-devel/devel-terminate"
removeFileIfExists lockFile
removeFileIfExists "dist/devel-terminate" -- for compatibility with old devel.hs
data DevelTermOpt = TerminateOnEnter | TerminateOnlyInterrupt -- only used internally, can change
deriving (Show, Eq) specialFilePath SignalFile = "yesod-devel/rebuild"
-- | Write a special file
writeSpecialFile :: SpecialFile -> IO ()
writeSpecialFile sp = do
let fp = specialFilePath sp
createDirectoryIfMissing True $ takeDirectory fp
now <- getCurrentTime
writeFile fp $ show now
-- | Remove a special file
removeSpecialFile :: SpecialFile -> IO ()
removeSpecialFile sp = removeFile (specialFilePath sp) `Ex.catch` \e ->
if isDoesNotExistError e
then return ()
else Ex.throwIO e
-- | Get an absolute path to the special file
canonicalizeSpecialFile :: SpecialFile -> IO FilePath
canonicalizeSpecialFile sp = do
let fp = specialFilePath sp
dir = takeDirectory fp
file = takeFileName fp
createDirectoryIfMissing True dir
dir' <- canonicalizePath dir
return $ dir' </> file
-- | Used as a callback from "stack build --exec" to write the signal file
develSignal :: IO ()
develSignal = writeSpecialFile SignalFile
-- | Options to be provided on the command line
data DevelOpts = DevelOpts data DevelOpts = DevelOpts
{ isCabalDev :: Bool { verbose :: Bool
, forceCabal :: Bool
, verbose :: Bool
, eventTimeout :: Int -- negative value for no timeout
, successHook :: Maybe String , successHook :: Maybe String
, failHook :: Maybe String
, buildDir :: Maybe String
, develPort :: Int , develPort :: Int
, develTlsPort :: Int , develTlsPort :: Int
, proxyTimeout :: Int , proxyTimeout :: Int
, useReverseProxy :: Bool , useReverseProxy :: Bool
, terminateWith :: DevelTermOpt , develHost :: Maybe String
, cert :: Maybe (FilePath, FilePath)
-- Support for GHC_PACKAGE_PATH wrapping
, develConfigOpts :: [String]
, develEnv :: Maybe [(String, String)]
} deriving (Show, Eq) } deriving (Show, Eq)
getBuildDir :: DevelOpts -> String -- | Run a reverse proxy from the develPort and develTlsPort ports to
getBuildDir opts = fromMaybe "dist" (buildDir opts) -- the app running in appPortVar. If there is no response on the
-- application port, give an appropriate message to the user.
defaultDevelOpts :: DevelOpts reverseProxy :: DevelOpts -> TVar Int -> IO ()
defaultDevelOpts = DevelOpts reverseProxy opts appPortVar = do
{ isCabalDev = False manager <- newManager $ managerSetProxy noProxy tlsManagerSettings
, forceCabal = False let refreshHtml = LB.fromChunks [$(embedFile "refreshing.html")]
, verbose = False sayV = when (verbose opts) . sayString
, eventTimeout = -1
, successHook = Nothing
, failHook = Nothing
, buildDir = Nothing
, develPort = 3000
, develTlsPort = 3443
, proxyTimeout = 10
, useReverseProxy = True
, terminateWith = TerminateOnEnter
, develConfigOpts = []
, develEnv = Nothing
}
cabalProgram :: DevelOpts -> FilePath
cabalProgram opts
| isCabalDev opts = "cabal-dev"
| otherwise = "cabal"
-- | Run a reverse proxy from port 3000 to 3001. If there is no response on
-- 3001, give an appropriate message to the user.
reverseProxy :: DevelOpts -> I.IORef Int -> IO ()
reverseProxy opts iappPort = do
#if MIN_VERSION_http_client(0,4,7)
manager <- newManager $ managerSetProxy noProxy conduitManagerSettings
#else
manager <- newManager conduitManagerSettings
#endif
let refreshHtml = LB.fromChunks $ return $(embedFile "refreshing.html")
let onExc _ req let onExc _ req
| maybe False (("application/json" `elem`) . parseHttpAccept) | maybe False (("application/json" `elem`) . parseHttpAccept)
(lookup "accept" $ requestHeaders req) = (lookup "accept" $ requestHeaders req) =
@ -173,11 +155,16 @@ reverseProxy opts iappPort = do
let proxyApp = waiProxyToSettings let proxyApp = waiProxyToSettings
(const $ do (const $ do
appPort <- liftIO $ I.readIORef iappPort appPort <- atomically $ readTVar appPortVar
sayV $ "revProxy: appPort " ++ (show appPort)
return $ return $
ReverseProxy.WPRProxyDest ReverseProxy.WPRProxyDest
$ ProxyDest "127.0.0.1" appPort) $ ProxyDest "127.0.0.1" appPort)
#if MIN_VERSION_http_reverse_proxy(0, 6, 0)
defaultWaiProxySettings
#else
def def
#endif
{ wpsOnExc = \e req f -> onExc e req >>= f { wpsOnExc = \e req f -> onExc e req >>= f
, wpsTimeout = , wpsTimeout =
if proxyTimeout opts == 0 if proxyTimeout opts == 0
@ -185,11 +172,14 @@ reverseProxy opts iappPort = do
else Just (1000000 * proxyTimeout opts) else Just (1000000 * proxyTimeout opts)
} }
manager manager
defaultSettings' = maybe id (setHost . fromString) (develHost opts) defaultSettings
runProxyTls port app = do runProxyTls port app = do
let cert = $(embedFile "certificate.pem") let certDef = $(embedFile "certificate.pem")
key = $(embedFile "key.pem") keyDef = $(embedFile "key.pem")
tlsSettings = tlsSettingsMemory cert key theSettings = case cert opts of
runTLS tlsSettings (setPort port defaultSettings) $ \req send -> do Nothing -> tlsSettingsMemory certDef keyDef
Just (c,k) -> tlsSettings c k
runTLS theSettings (setPort port defaultSettings') $ \req send -> do
let req' = req let req' = req
{ requestHeaders { requestHeaders
= ("X-Forwarded-Proto", "https") = ("X-Forwarded-Proto", "https")
@ -207,363 +197,337 @@ reverseProxy opts iappPort = do
(requestHeaders req) (requestHeaders req)
} }
app req' send app req' send
httpProxy = run (develPort opts) proxyApp httpProxy = runSettings (setPort (develPort opts) defaultSettings') proxyApp
httpsProxy = runProxyTls (develTlsPort opts) proxyApp httpsProxy = runProxyTls (develTlsPort opts) proxyApp
putStrLn "Application can be accessed at:\n" say "Application can be accessed at:\n"
putStrLn $ "http://localhost:" ++ show (develPort opts) sayString $ "http://localhost:" ++ show (develPort opts)
putStrLn $ "https://localhost:" ++ show (develTlsPort opts) sayString $ "https://localhost:" ++ show (develTlsPort opts)
putStrLn $ "If you wish to test https capabilities, you should set the following variable:" say $ "If you wish to test https capabilities, you should set the following variable:"
putStrLn $ " export APPROOT=https://localhost:" ++ show (develTlsPort opts) sayString $ " export APPROOT=https://localhost:" ++ show (develTlsPort opts)
putStrLn "" say ""
loop (race_ httpProxy httpsProxy) `Ex.catch` \e -> do race_ httpProxy httpsProxy
print (e :: Ex.SomeException)
_ <- exitFailure
Ex.throwIO e -- heh, just for good measure
where
loop proxies = forever $ do
void proxies
putStrLn $ "Reverse proxy stopped, but it shouldn't"
threadDelay 1000000
putStrLn $ "Restarting reverse proxies"
-- | Check if the given port is available.
checkPort :: Int -> IO Bool checkPort :: Int -> IO Bool
checkPort p = do checkPort p = do
es <- Ex.try $ bindPortTCP p "*4" es <- Ex.tryIO $ bindPortTCP p "*4"
case es of case es of
Left (_ :: Ex.IOException) -> return False Left _ -> return False
Right s -> do Right s -> do
sClose s Network.Socket.close s
return True return True
getPort :: DevelOpts -> Int -> IO Int -- | Get a random, unused port.
getPort opts _ getNewPort :: DevelOpts -> IO Int
| not (useReverseProxy opts) = return $ develPort opts getNewPort opts = do
getPort _ p0 = (port, socket) <- bindRandomPortTCP "*"
loop p0 when (verbose opts) $ sayString $ "Got new port: " ++ show port
where Network.Socket.close socket
loop p = do return port
avail <- checkPort p
if avail then return p else loop (succ p)
-- | Utility function
unlessM :: Monad m => m Bool -> m () -> m () unlessM :: Monad m => m Bool -> m () -> m ()
unlessM c a = c >>= \res -> unless res a unlessM c a = c >>= \res -> unless res a
devel :: DevelOpts -> [String] -> IO () -- | Find the file containing the devel code to be run.
devel opts passThroughArgs = withSocketsDo $ withManager $ \manager -> do
unlessM (checkPort $ develPort opts) $ error "devel port unavailable"
iappPort <- getPort opts 17834 >>= I.newIORef
when (useReverseProxy opts) $ void $ forkIO $ reverseProxy opts iappPort
develHsPath <- checkDevelFile
writeLock opts
let (terminator, after) = case terminateWith opts of
TerminateOnEnter ->
("Type 'quit'", blockQuit)
TerminateOnlyInterrupt -> -- run for one year
("Interrupt", threadDelay $ 1000 * 1000 * 60 * 60 * 24 * 365)
blockQuit = do
s <- getLine
if s == "quit"
then return ()
else do
putStrLn "Type 'quit' to quit"
blockQuit
putStrLn $ "Yesod devel server. " ++ terminator ++ " to quit"
void $ forkIO $ do
filesModified <- newEmptyMVar
void $ forkIO $
void $ watchTree manager "." (const True) (\_ -> void (tryPutMVar filesModified ()))
evalStateT (mainOuterLoop develHsPath iappPort filesModified) Map.empty
after
writeLock opts
exitSuccess
where
bd = getBuildDir opts
-- outer loop re-reads the cabal file
mainOuterLoop develHsPath iappPort filesModified = do
ghcVer <- liftIO ghcVersion
#if MIN_VERSION_Cabal(1,20,0)
cabal <- liftIO $ D.tryFindPackageDesc "."
#else
cabal <- liftIO $ D.findPackageDesc "."
#endif
gpd <- liftIO $ D.readPackageDescription D.normal cabal
ldar <- liftIO lookupLdAr
(hsSourceDirs, _) <- liftIO $ checkCabalFile gpd
liftIO $ removeFileIfExists (bd </> "setup-config")
c <- liftIO $ configure opts passThroughArgs
if c then do
-- these files contain the wrong data after the configure step,
-- remove them to force a cabal build first
liftIO $ mapM_ removeFileIfExists [ "yesod-devel/ghcargs.txt"
, "yesod-devel/arargs.txt"
, "yesod-devel/ldargs.txt"
]
rebuild <- liftIO $ mkRebuild ghcVer cabal opts ldar
mainInnerLoop develHsPath iappPort hsSourceDirs filesModified cabal rebuild
else do
liftIO (threadDelay 5000000)
mainOuterLoop develHsPath iappPort filesModified
-- inner loop rebuilds after files change
mainInnerLoop develHsPath iappPort hsSourceDirs filesModified cabal rebuild = go
where
go = do
_ <- recompDeps hsSourceDirs
list <- liftIO $ getFileList hsSourceDirs [cabal]
success <- liftIO rebuild
pkgArgs <- liftIO (ghcPackageArgs opts)
let devArgs = pkgArgs ++ [develHsPath]
let loop list0 = do
(haskellFileChanged, list1) <- liftIO $
watchForChanges filesModified hsSourceDirs [cabal] list0 (eventTimeout opts)
anyTouched <- recompDeps hsSourceDirs
unless (anyTouched || haskellFileChanged) $ loop list1
if not success
then liftIO $ do
putStrLn "\x1b[1;31mBuild failure, pausing...\x1b[0m"
runBuildHook $ failHook opts
else do
liftIO $ runBuildHook $ successHook opts
liftIO $ removeLock opts
liftIO $ putStrLn
$ if verbose opts then "Starting development server: runghc " ++ L.unwords devArgs
else "Starting development server..."
env0 <- liftIO getEnvironment
-- get a new port for the new process to listen on
appPort <- liftIO $ I.readIORef iappPort >>= getPort opts . (+ 1)
liftIO $ I.writeIORef iappPort appPort
(_,_,_,ph) <- liftIO $ createProcess (proc "runghc" devArgs)
{ env = Just $ Map.toList
$ Map.insert "PORT" (show appPort)
$ Map.insert "DISPLAY_PORT" (show $ develPort opts)
$ Map.fromList env0
}
derefMap <- get
watchTid <- liftIO . forkIO . try_ $ flip evalStateT derefMap $ do
loop list
liftIO $ do
putStrLn "Stopping development server..."
writeLock opts
threadDelay 1000000
putStrLn "Terminating development server..."
terminateProcess ph
ec <- liftIO $ waitForProcess' ph
liftIO $ putStrLn $ "Exit code: " ++ show ec
liftIO $ Ex.throwTo watchTid (userError "process finished")
loop list
n <- liftIO $ cabal `isNewerThan` (bd </> "setup-config")
if n then mainOuterLoop develHsPath iappPort filesModified else go
runBuildHook :: Maybe String -> IO ()
runBuildHook (Just s) = do
ret <- system s
case ret of
ExitFailure _ -> putStrLn ("Error executing hook: " ++ s)
_ -> return ()
runBuildHook Nothing = return ()
{-
run `cabal configure' with our wrappers
-}
configure :: DevelOpts -> [String] -> IO Bool
configure opts extraArgs =
checkExit =<< createProcess (proc (cabalProgram opts) $
[ "configure"
, "-flibrary-only"
, "--disable-tests"
, "--disable-benchmarks"
, "-fdevel"
, "--disable-library-profiling"
, "--with-ld=yesod-ld-wrapper"
, "--with-ghc=yesod-ghc-wrapper"
, "--with-ar=yesod-ar-wrapper"
, "--with-hc-pkg=ghc-pkg"
] ++ develConfigOpts opts ++ extraArgs
) { env = develEnv opts }
removeFileIfExists :: FilePath -> IO ()
removeFileIfExists file = removeFile file `Ex.catch` handler
where
handler :: IOError -> IO ()
handler e | isDoesNotExistError e = return ()
| otherwise = Ex.throw e
mkRebuild :: String -> FilePath -> DevelOpts -> (FilePath, FilePath) -> IO (IO Bool)
mkRebuild ghcVer cabalFile opts (ldPath, arPath)
| GHC.cProjectVersion /= ghcVer =
failWith "Yesod has been compiled with a different GHC version, please reinstall yesod-bin"
| forceCabal opts = return (rebuildCabal opts)
| otherwise =
return $ do
ns <- mapM (cabalFile `isNewerThan`)
[ "yesod-devel/ghcargs.txt", "yesod-devel/arargs.txt", "yesod-devel/ldargs.txt" ]
if or ns
then rebuildCabal opts
else do
bf <- getBuildFlags
rebuildGhc bf ldPath arPath
rebuildGhc :: [Located String] -> FilePath -> FilePath -> IO Bool
rebuildGhc bf ld ar = do
putStrLn "Rebuilding application... (using GHC API)"
buildPackage bf ld ar
rebuildCabal :: DevelOpts -> IO Bool
rebuildCabal opts = do
putStrLn $ "Rebuilding application... (using " ++ cabalProgram opts ++ ")"
checkExit =<< createProcess (proc (cabalProgram opts) args)
{ env = develEnv opts
}
where
args | verbose opts = [ "build" ]
| otherwise = [ "build", "-v0" ]
try_ :: forall a. IO a -> IO ()
try_ x = void (Ex.try x :: IO (Either Ex.SomeException a))
type FileList = Map.Map FilePath EpochTime
getFileList :: [FilePath] -> [FilePath] -> IO FileList
getFileList hsSourceDirs extraFiles = do
(files, deps) <- getDeps hsSourceDirs
let files' = extraFiles ++ files ++ map fst (Map.toList deps)
fmap Map.fromList $ forM files' $ \f -> do
efs <- Ex.try $ getFileStatus f
return $ case efs of
Left (_ :: Ex.SomeException) -> (f, 0)
Right fs -> (f, modificationTime fs)
-- | Returns @True@ if a .hs file changed.
watchForChanges :: MVar () -> [FilePath] -> [FilePath] -> FileList -> Int -> IO (Bool, FileList)
watchForChanges filesModified hsSourceDirs extraFiles list t = do
newList <- getFileList hsSourceDirs extraFiles
if list /= newList
then do
let haskellFileChanged = not $ Map.null $ Map.filterWithKey isHaskell $
Map.differenceWith compareTimes newList list `Map.union`
Map.differenceWith compareTimes list newList
return (haskellFileChanged, newList)
else timeout (1000000*t) (takeMVar filesModified) >>
watchForChanges filesModified hsSourceDirs extraFiles list t
where
compareTimes x y
| x == y = Nothing
| otherwise = Just x
isHaskell filename _ = takeExtension filename `elem` [".hs", ".lhs", ".hsc", ".cabal"]
checkDevelFile :: IO FilePath checkDevelFile :: IO FilePath
checkDevelFile = checkDevelFile =
loop paths loop paths
where where
paths = ["app/devel.hs", "devel.hs", "src/devel.hs"] paths = ["app/devel.hs", "devel.hs", "src/devel.hs"]
loop [] = failWith $ "file devel.hs not found, checked: " ++ show paths loop [] = error $ "file devel.hs not found, checked: " ++ show paths
loop (x:xs) = do loop (x:xs) = do
e <- doesFileExist x e <- doesFileExist x
if e if e
then return x then return x
else loop xs else loop xs
checkCabalFile :: D.GenericPackageDescription -> IO ([FilePath], D.Library) stackSuccessString :: ByteString
checkCabalFile gpd = case D.condLibrary gpd of stackSuccessString = "ExitSuccess"
Nothing -> failWith "incorrect cabal file, no library"
Just ct ->
case lookupDevelLib gpd ct of
Nothing ->
failWith "no development flag found in your configuration file. Expected a 'library-only' flag or the older 'devel' flag"
Just dLib -> do
let hsSourceDirs = D.hsSourceDirs . D.libBuildInfo $ dLib
fl <- getFileList hsSourceDirs []
let unlisted = checkFileList fl dLib
unless (null unlisted) $ do
putStrLn "WARNING: the following source files are not listed in exposed-modules or other-modules:"
mapM_ putStrLn unlisted
when ("Application" `notElem` (map (last . D.components) $ D.exposedModules dLib)) $
putStrLn "WARNING: no exposed module Application"
return (hsSourceDirs, dLib)
failWith :: String -> IO a stackFailureString :: ByteString
failWith msg = do stackFailureString = "ExitFailure"
putStrLn $ "ERROR: " ++ msg
exitFailure
checkFileList :: FileList -> D.Library -> [FilePath] -- We need updateAppPort logic to prevent a race condition.
checkFileList fl lib = filter (not . isSetup) . filter isUnlisted . filter isSrcFile $ sourceFiles -- See https://github.com/yesodweb/yesod/issues/1380
updateAppPort :: ByteString -> TVar Bool -- ^ Bool to indicate if the
-- output from stack has
-- started. False indicate
-- that it hasn't started
-- yet.
-> TVar Int -> STM ()
updateAppPort bs buildStarted appPortVar = do
hasStarted <- readTVar buildStarted
let buildEnd = isInfixOf stackFailureString bs || isInfixOf stackSuccessString bs
case (hasStarted, buildEnd) of
(False, False) -> do
writeTVar appPortVar (-1 :: Int)
writeTVar buildStarted True
(True, False) -> return ()
(_, True) -> writeTVar buildStarted False
-- | Get the set of all flags available in the given cabal file
getAvailableFlags :: D.GenericPackageDescription -> Set.Set String
getAvailableFlags =
Set.fromList . map (unFlagName . D.flagName) . D.genPackageFlags
where where
al = allModules lib #if MIN_VERSION_Cabal(2, 0, 0)
-- a file is only a possible 'module file' if all path pieces start with a capital letter unFlagName = D.unFlagName
sourceFiles = filter isSrcFile . map fst . Map.toList $ fl
isSrcFile file = let dirs = filter (/=".") $ splitDirectories file
in all (isUpper . head) dirs && (takeExtension file `elem` [".hs", ".lhs"])
isUnlisted file = not (toModuleName file `Set.member` al)
toModuleName = L.intercalate "." . filter (/=".") . splitDirectories . dropExtension
isSetup "Setup.hs" = True
isSetup "./Setup.hs" = True
isSetup "Setup.lhs" = True
isSetup "./Setup.lhs" = True
isSetup _ = False
allModules :: D.Library -> Set.Set String
allModules lib = Set.fromList $ map toString $ D.exposedModules lib ++ (D.otherModules . D.libBuildInfo) lib
where
toString = L.intercalate "." . D.components
ghcVersion :: IO String
ghcVersion = fmap getNumber $ readProcess "runghc" ["--numeric-version", "0"] []
where
getNumber = filter (\x -> isNumber x || x == '.')
ghcPackageArgs :: DevelOpts -> IO [String]
ghcPackageArgs opts = getBuildFlags >>= getPackageArgs (buildDir opts)
lookupDevelLib :: D.GenericPackageDescription -> D.CondTree D.ConfVar c a -> Maybe a
lookupDevelLib gpd ct | found = Just (D.condTreeData ct)
| otherwise = Nothing
where
flags = map (unFlagName . D.flagName) $ D.genPackageFlags gpd
unFlagName (D.FlagName x) = x
found = any (`elem` ["library-only", "devel"]) flags
-- location of `ld' and `ar' programs
lookupLdAr :: IO (FilePath, FilePath)
lookupLdAr = do
mla <- lookupLdAr'
case mla of
Nothing -> failWith "Cannot determine location of `ar' or `ld' program"
Just la -> return la
lookupLdAr' :: IO (Maybe (FilePath, FilePath))
lookupLdAr' = do
#if MIN_VERSION_Cabal(1,18,0)
(_, _, pgmc) <- D.configCompilerEx (Just D.GHC) Nothing Nothing D.defaultProgramConfiguration D.silent
#else #else
(_, pgmc) <- D.configCompiler (Just D.GHC) Nothing Nothing D.defaultProgramConfiguration D.silent unFlagName (D.FlagName fn) = fn
#endif #endif
pgmc' <- D.configureAllKnownPrograms D.silent pgmc
return $ (,) <$> look D.ldProgram pgmc' <*> look D.arProgram pgmc'
where
look pgm pdb = fmap D.programPath (D.lookupProgram pgm pdb)
-- | nonblocking version of @waitForProcess@ -- | This is the main entry point. Run the devel server.
waitForProcess' :: ProcessHandle -> IO ExitCode devel :: DevelOpts -- ^ command line options
waitForProcess' pid = go -> [String] -- ^ extra options to pass to Stack
where -> IO ()
go = do devel opts passThroughArgs = do
mec <- getProcessExitCode pid -- Check that the listening ports are available
case mec of unlessM (checkPort $ develPort opts) $ error "devel port unavailable"
Just ec -> return ec unlessM (checkPort $ develTlsPort opts) $ error "devel TLS port unavailable"
Nothing -> threadDelay 100000 >> go
-- | wait for process started by @createProcess@, return True for ExitSuccess -- Friendly message to the user
checkExit :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO Bool say "Yesod devel server. Enter 'quit' or hit Ctrl-C to quit."
checkExit (_,_,_,h) = (==ExitSuccess) <$> waitForProcess' h
-- Find out the name of our package, needed for the upcoming Stack
-- commands
#if MIN_VERSION_Cabal(3, 0, 0)
cabal <- D.tryFindPackageDesc D.silent "."
#elif MIN_VERSION_Cabal(1, 20, 0)
cabal <- D.tryFindPackageDesc "."
#else
cabal <- D.findPackageDesc "."
#endif
#if MIN_VERSION_Cabal(2, 0, 0)
gpd <- D.readGenericPackageDescription D.normal cabal
#else
gpd <- D.readPackageDescription D.normal cabal
#endif
let pd = D.packageDescription gpd
D.PackageIdentifier packageNameWrapped _version = D.package pd
#if MIN_VERSION_Cabal(2, 0, 0)
packageName = D.unPackageName packageNameWrapped
#else
D.PackageName packageName = packageNameWrapped
#endif
-- Which file contains the code to run
develHsPath <- checkDevelFile
-- The port that we're currently listening on, and that the
-- reverse proxy should point to
appPortVar <- newTVarIO (-1)
-- If we're actually using reverse proxying, spawn off a reverse
-- proxy thread
let withRevProxy =
if useReverseProxy opts
then race_ (reverseProxy opts appPortVar)
else id
-- Run the following concurrently. If any of them exit, take the
-- whole thing down.
--
-- We need to put withChangedVar outside of all this, since we
-- need to ensure we start watching files before the stack build
-- loop starts.
withChangedVar $ \changedVar -> withRevProxy $ race_
-- Start the build loop
(runStackBuild appPortVar packageName (getAvailableFlags gpd))
-- Run the app itself, restarting when a build succeeds
(runApp appPortVar changedVar develHsPath)
where
-- say, but only when verbose is on
sayV = when (verbose opts) . sayString
-- Leverage "stack build --file-watch" to do the build
runStackBuild :: TVar Int -> [Char] -> Set.Set [Char] -> IO ()
runStackBuild appPortVar packageName availableFlags = do
-- We call into this app for the devel-signal command
myPath <- getExecutablePath
let procConfig = setStdout createSource
$ setStderr createSource
$ setCreateGroup True -- because need when yesod-bin killed and kill child ghc
$ proc "stack" $
[ "build"
, "--fast"
, "--file-watch"
-- Indicate the component we want
, packageName ++ ":lib"
-- signal the watcher that a build has succeeded
, "--exec", myPath ++ " devel-signal"
] ++
-- Turn on relevant flags
concatMap
(\flagName -> [ "--flag", packageName ++ ":" ++ flagName])
(Set.toList $ Set.intersection
availableFlags
(Set.fromList ["dev", "library-only"])) ++
-- Add the success hook
(case successHook opts of
Nothing -> []
Just h -> ["--exec", h]) ++
-- Any extra args passed on the command line
passThroughArgs
sayV $ show procConfig
buildStarted <- newTVarIO False
-- Monitor the stdout and stderr content from the build process. Any
-- time some output comes, we invalidate the currently running app by
-- changing the destination port for reverse proxying to -1. We also
-- make sure that all content to stdout or stderr from the build
-- process is piped to the actual stdout and stderr handles.
withProcess_ procConfig $ \p -> do
let helper getter h =
runConduit
$ getter p
.| iterMC (\(str :: ByteString) -> atomically (updateAppPort str buildStarted appPortVar))
.| sinkHandle h
race_ (helper getStdout stdout) (helper getStderr stderr)
-- Run the inner action with a TVar which will be set to True
-- whenever the signal file is modified.
withChangedVar :: (TVar Bool -> IO a) -> IO a
withChangedVar inner = withManager $ \manager -> do
-- Variable indicating that the signal file has been changed. We
-- reset it each time we handle the signal.
changedVar <- newTVarIO False
-- Get the absolute path of the signal file, needed for the
-- file watching
develSignalFile' <- canonicalizeSpecialFile SignalFile
-- Start watching the signal file, and set changedVar to
-- True each time it's changed.
void $ watchDir manager
-- Using fromString to work with older versions of fsnotify
-- that use system-filepath
(fromString (takeDirectory develSignalFile'))
(\e -> eventPath e == fromString develSignalFile')
(const $ atomically $ writeTVar changedVar True)
-- Run the inner action
inner changedVar
-- Each time the library builds successfully, run the application
runApp :: TVar Int -> TVar Bool -> String -> IO b
runApp appPortVar changedVar develHsPath = do
-- Wait for the first change, indicating that the library
-- has been built
atomically $ do
changed <- readTVar changedVar
check changed
writeTVar changedVar False
sayV "First successful build complete, running app"
-- We're going to set the PORT and DISPLAY_PORT variables for
-- the child below. Also need to know if the env program
-- exists.
env <- fmap Map.fromList getEnvironment
hasEnv <- fmap isJust $ findExecutable "env"
-- Keep looping forever, print any synchronous exceptions,
-- and eventually die from an async exception from one of
-- the other threads (via race_ above).
forever $ Ex.handleAny (\e -> sayErrString $ "Exception in runApp: " ++ show e) $ do
-- Get the port the child should listen on, and tell
-- the reverse proxy about it
newPort <-
if useReverseProxy opts
then getNewPort opts
-- no reverse proxy, so use the develPort directly
else return (develPort opts)
atomically $ writeTVar appPortVar newPort
-- Modified environment
let env' = Map.toList
$ Map.insert "PORT" (show newPort)
$ Map.insert "DISPLAY_PORT" (show $ develPort opts)
env
-- Remove the terminate file so we don't immediately exit
removeSpecialFile TermFile
-- Launch the main function in the Main module defined
-- in the file develHsPath. We use ghc instead of
-- runghc to avoid the extra (confusing) resident
-- runghc process. Starting with GHC 8.0.2, that will
-- not be necessary.
{- Hmm, unknown errors trying to get this to work. Just doing the
- runghc thing instead.
let procDef = setStdin closed $ setEnv env' $ proc "stack"
[ "ghc"
, "--"
, develHsPath
, "-e"
, "Main.main"
]
-}
-- Nix support in Stack doesn't pass along env vars by
-- default, so we use the env command. But if the command
-- isn't available, just set the env var. I'm sure this
-- will break _some_ combination of systems, but we'll
-- deal with that later. Previous issues:
--
-- https://github.com/yesodweb/yesod/issues/1357
-- https://github.com/yesodweb/yesod/issues/1359
let procDef
| hasEnv = setStdin closed $ proc "stack"
[ "exec"
, "--"
, "env"
, "PORT=" ++ show newPort
, "DISPLAY_PORT=" ++ show (develPort opts)
, "runghc"
, develHsPath
]
| otherwise = setStdin closed $ setEnv env' $ proc "stack"
[ "runghc"
, "--"
, develHsPath
]
sayV $ "Running child process: " ++ show procDef
-- Start running the child process with GHC
withProcess procDef $ \p -> do
-- Wait for either the process to exit, or for a new build to come through
eres <- atomically (fmap Left (waitExitCodeSTM p) <|> fmap Right
(do changed <- readTVar changedVar
check changed
writeTVar changedVar False))
-- on an async exception, make sure the child dies
`Ex.onException` writeSpecialFile TermFile
case eres of
-- Child exited, which indicates some
-- error. Let the user know, sleep for a bit
-- to avoid busy-looping, and then we'll try
-- again.
Left ec -> do
sayErrString $ "Unexpected: child process exited with " ++ show ec
threadDelay 1000000
sayErrString "Trying again"
-- New build succeeded
Right () -> do
-- Kill the child process, both with the
-- TermFile, and by signaling the process
-- directly.
writeSpecialFile TermFile
stopProcess p
-- Wait until the child properly exits, then we'll try again
ec <- waitExitCode p
sayV $ "Expected: child process exited with " ++ show ec

View File

@ -1,547 +0,0 @@
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
{-# OPTIONS_GHC -fno-warn-unused-binds #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
{-# OPTIONS_GHC -fno-warn-unused-matches #-}
{-
There is a lot of code copied from GHC here, and some conditional
compilation. Instead of fixing all warnings and making it much more
difficult to compare the code to the original, just ignore unused
binds and imports.
-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-
build package with the GHC API
-}
module GhcBuild (getBuildFlags, buildPackage, getPackageArgs) where
import qualified Control.Exception as Ex
import Control.Monad (when)
import Data.IORef
import System.Process (rawSystem)
import System.Environment (getEnvironment)
import CmdLineParser
import Data.Char (toLower)
import Data.List (isPrefixOf, isSuffixOf, partition)
import Data.Maybe (fromMaybe)
import DriverPhases (Phase (..), anyHsc, isHaskellSrcFilename,
isSourceFilename, startPhase)
import DriverPipeline (compileFile, link, linkBinary, oneShot)
import DynFlags (DynFlags, compilerInfo)
import qualified DynFlags
import qualified DynFlags as DF
import qualified GHC
import GHC.Paths (libdir)
import HscTypes (HscEnv (..), emptyHomePackageTable)
import qualified Module
import MonadUtils (liftIO)
import Panic (throwGhcException, panic)
import SrcLoc (Located, mkGeneralLocated)
import qualified StaticFlags
#if __GLASGOW_HASKELL__ >= 707
import DynFlags (ldInputs)
#else
import StaticFlags (v_Ld_inputs)
#endif
import System.FilePath (normalise, (</>))
import Util (consIORef, looksLikeModuleName)
{-
This contains a huge hack:
GHC only accepts setting static flags once per process, however it has no way to
get the remaining options from the command line, without setting the static flags.
This code overwrites the IORef to disable the check. This will likely cause
problems if the flags are modified, but fortunately that's relatively uncommon.
-}
getBuildFlags :: IO [Located String]
getBuildFlags = do
argv0 <- fmap read $ readFile "yesod-devel/ghcargs.txt" -- generated by yesod-ghc-wrapper
argv0' <- prependHsenvArgv argv0
let (minusB_args, argv1) = partition ("-B" `isPrefixOf`) argv0'
mbMinusB | null minusB_args = Nothing
| otherwise = Just (drop 2 (last minusB_args))
let argv1' = map (mkGeneralLocated "on the commandline") argv1
writeIORef StaticFlags.v_opt_C_ready False -- the huge hack
(argv2, staticFlagWarnings) <- GHC.parseStaticFlags argv1'
return argv2
prependHsenvArgv :: [String] -> IO [String]
prependHsenvArgv argv = do
env <- getEnvironment
return $ case (lookup "HSENV" env) of
Nothing -> argv
_ -> hsenvArgv ++ argv
where hsenvArgv = words $ fromMaybe "" (lookup "PACKAGE_DB_FOR_GHC" env)
-- construct a command line for loading the right packages
getPackageArgs :: Maybe String -> [Located String] -> IO [String]
getPackageArgs buildDir argv2 = do
(mode, argv3, modeFlagWarnings) <- parseModeFlags argv2
GHC.runGhc (Just libdir) $ do
dflags0 <- GHC.getSessionDynFlags
(dflags1, _, _) <- GHC.parseDynamicFlags dflags0 argv3
let pkgFlags = map convertPkgFlag (GHC.packageFlags dflags1)
ignorePkgFlags =
#if __GLASGOW_HASKELL__ >= 800
map convertIgnorePkgFlag (GHC.ignorePackageFlags dflags1)
#else
[]
#endif
trustPkgFlags =
#if __GLASGOW_HASKELL__ >= 800
map convertTrustPkgFlag (GHC.trustFlags dflags1)
#else
[]
#endif
hideAll | gopt DF.Opt_HideAllPackages dflags1 = [ "-hide-all-packages"]
| otherwise = []
ownPkg = packageString (DF.thisPackage dflags1)
return (reverse (extra dflags1) ++ hideAll ++ trustPkgFlags ++ ignorePkgFlags ++ pkgFlags ++ ownPkg)
where
#if __GLASGOW_HASKELL__ >= 800
convertIgnorePkgFlag (DF.IgnorePackage p) = "-ignore-package" ++ p
convertTrustPkgFlag (DF.TrustPackage p) = "-trust" ++ p
convertTrustPkgFlag (DF.DistrustPackage p) = "-distrust" ++ p
#else
convertPkgFlag (DF.IgnorePackage p) = "-ignore-package" ++ p
convertPkgFlag (DF.TrustPackage p) = "-trust" ++ p
convertPkgFlag (DF.DistrustPackage p) = "-distrust" ++ p
#endif
#if __GLASGOW_HASKELL__ >= 800
convertPkgFlag (DF.ExposePackage _ (DF.PackageArg p) _) = "-package" ++ p
convertPkgFlag (DF.ExposePackage _ (DF.UnitIdArg p) _) = "-package-id" ++ p
#elif __GLASGOW_HASKELL__ == 710
convertPkgFlag (DF.ExposePackage (DF.PackageArg p) _) = "-package" ++ p
convertPkgFlag (DF.ExposePackage (DF.PackageIdArg p) _) = "-package-id" ++ p
convertPkgFlag (DF.ExposePackage (DF.PackageKeyArg p) _) = "-package-key" ++ p
#else
convertPkgFlag (DF.ExposePackage p) = "-package" ++ p
convertPkgFlag (DF.ExposePackageId p) = "-package-id" ++ p
#endif
convertPkgFlag (DF.HidePackage p) = "-hide-package" ++ p
#if __GLASGOW_HASKELL__ >= 800
-- See: https://github.com/yesodweb/yesod/issues/1284
packageString _flags = []
--packageString flags = "-package-id" ++ Module.unitIdString flags
#elif __GLASGOW_HASKELL__ == 710
packageString flags = ["-package-key" ++ Module.packageKeyString flags]
#else
packageString flags = ["-package-id" ++ Module.packageIdString flags ++ "-inplace"]
#endif
#if __GLASGOW_HASKELL__ >= 705
extra df = inplaceConf ++ extra'
where
extra' = concatMap convertExtra (extraConfs df)
-- old cabal-install sometimes misses the .inplace db, fix it here
inplaceConf
| any (".inplace" `isSuffixOf`) extra' = []
| otherwise = ["-package-db" ++ fromMaybe "dist" buildDir
++ "/package.conf.inplace"]
extraConfs df = GHC.extraPkgConfs df []
convertExtra DF.GlobalPkgConf = [ ]
convertExtra DF.UserPkgConf = [ ]
convertExtra (DF.PkgConfFile file) = [ "-package-db" ++ file ]
#else
extra df = inplaceConf ++ extra'
where
extra' = map ("-package-conf"++) (GHC.extraPkgConfs df)
-- old cabal-install sometimes misses the .inplace db, fix it here
inplaceConf
| any (".inplace" `isSuffixOf`) extra' = []
| otherwise = ["-package-conf" ++ fromMaybe "dist" buildDir
++ "/package.conf.inplace"]
#endif
#if __GLASGOW_HASKELL__ >= 707
gopt = DF.gopt
#else
gopt = DF.dopt
#endif
buildPackage :: [Located String] -> FilePath -> FilePath -> IO Bool
buildPackage a ld ar = buildPackage' a ld ar `Ex.catch` \e -> do
putStrLn ("exception building package: " ++ show (e :: Ex.SomeException))
return False
buildPackage' :: [Located String] -> FilePath -> FilePath -> IO Bool
buildPackage' argv2 ld ar = do
(mode, argv3, modeFlagWarnings) <- parseModeFlags argv2
GHC.runGhc (Just libdir) $ do
dflags0 <- GHC.getSessionDynFlags
(dflags1, _, _) <- GHC.parseDynamicFlags dflags0 argv3
let dflags2 = dflags1 { GHC.ghcMode = GHC.CompManager
, GHC.hscTarget = GHC.hscTarget dflags1
, GHC.ghcLink = GHC.LinkBinary
, GHC.verbosity = 1
}
(dflags3, fileish_args, _) <- GHC.parseDynamicFlags dflags2 argv3
GHC.setSessionDynFlags dflags3
let normal_fileish_paths = map (normalise . GHC.unLoc) fileish_args
(srcs, objs) = partition_args normal_fileish_paths [] []
(hs_srcs, non_hs_srcs) = partition haskellish srcs
haskellish (f,Nothing) =
looksLikeModuleName f || isHaskellSrcFilename f || '.' `notElem` f
haskellish (_,Just phase) =
#if MIN_VERSION_ghc(8,0,0)
phase `notElem` [As True, As False, Cc, Cobjc, Cobjcxx, CmmCpp, Cmm, StopLn]
#elif MIN_VERSION_ghc(7,8,3)
phase `notElem` [As True, As False, Cc, Cobjc, Cobjcpp, CmmCpp, Cmm, StopLn]
#elif MIN_VERSION_ghc(7,4,0)
phase `notElem` [As, Cc, Cobjc, Cobjcpp, CmmCpp, Cmm, StopLn]
#else
phase `notElem` [As, Cc, CmmCpp, Cmm, StopLn]
#endif
hsc_env <- GHC.getSession
-- if (null hs_srcs)
-- then liftIO (oneShot hsc_env StopLn srcs)
-- else do
#if MIN_VERSION_ghc(7,2,0)
o_files <- mapM (\x -> liftIO $ compileFile hsc_env StopLn x)
#else
o_files <- mapM (\x -> compileFile hsc_env StopLn x)
#endif
non_hs_srcs
#if __GLASGOW_HASKELL__ >= 707
let dflags4 = dflags3
{ ldInputs = map (DF.FileOption "") (reverse o_files)
++ ldInputs dflags3
}
GHC.setSessionDynFlags dflags4
#else
liftIO $ mapM_ (consIORef v_Ld_inputs) (reverse o_files)
#endif
targets <- mapM (uncurry GHC.guessTarget) hs_srcs
GHC.setTargets targets
ok_flag <- GHC.load GHC.LoadAllTargets
if GHC.failed ok_flag
then return False
else liftIO (linkPkg ld ar) >> return True
linkPkg :: FilePath -> FilePath -> IO ()
linkPkg ld ar = do
arargs <- fmap read $ readFile "yesod-devel/arargs.txt"
rawSystem ar arargs
ldargs <- fmap read $ readFile "yesod-devel/ldargs.txt"
rawSystem ld ldargs
return ()
--------------------------------------------------------------------------------------------
-- stuff below copied from ghc main.hs
--------------------------------------------------------------------------------------------
partition_args :: [String] -> [(String, Maybe Phase)] -> [String]
-> ([(String, Maybe Phase)], [String])
partition_args [] srcs objs = (reverse srcs, reverse objs)
partition_args ("-x":suff:args) srcs objs
| "none" <- suff = partition_args args srcs objs
| StopLn <- phase = partition_args args srcs (slurp ++ objs)
| otherwise = partition_args rest (these_srcs ++ srcs) objs
where phase = startPhase suff
(slurp,rest) = break (== "-x") args
these_srcs = zip slurp (repeat (Just phase))
partition_args (arg:args) srcs objs
| looks_like_an_input arg = partition_args args ((arg,Nothing):srcs) objs
| otherwise = partition_args args srcs (arg:objs)
{-
We split out the object files (.o, .dll) and add them
to v_Ld_inputs for use by the linker.
The following things should be considered compilation manager inputs:
- haskell source files (strings ending in .hs, .lhs or other
haskellish extension),
- module names (not forgetting hierarchical module names),
- and finally we consider everything not containing a '.' to be
a comp manager input, as shorthand for a .hs or .lhs filename.
Everything else is considered to be a linker object, and passed
straight through to the linker.
-}
looks_like_an_input :: String -> Bool
looks_like_an_input m = isSourceFilename m
|| looksLikeModuleName m
|| '.' `notElem` m
-- Parsing the mode flag
parseModeFlags :: [Located String]
-> IO (Mode,
[Located String],
[Located String])
parseModeFlags args = do
let ((leftover, errs1, warns), (mModeFlag, errs2, flags')) =
runCmdLine (processArgs mode_flags args)
(Nothing, [], [])
mode = case mModeFlag of
Nothing -> doMakeMode
Just (m, _) -> m
errs = errs1 ++ map (mkGeneralLocated "on the commandline") errs2
#if __GLASGOW_HASKELL__ >= 710
errorsToGhcException' = errorsToGhcException . map (\(GHC.L _ e) -> ("on the commandline", e))
#else
errorsToGhcException' = errorsToGhcException
#endif
when (not (null errs)) $ throwGhcException $ errorsToGhcException' errs
return (mode, flags' ++ leftover, warns)
type ModeM = CmdLineP (Maybe (Mode, String), [String], [Located String])
-- mode flags sometimes give rise to new DynFlags (eg. -C, see below)
-- so we collect the new ones and return them.
mode_flags :: [Flag ModeM]
mode_flags =
[ ------- help / version ----------------------------------------------
mkFlag "?" (PassFlag (setMode showGhcUsageMode))
, mkFlag "-help" (PassFlag (setMode showGhcUsageMode))
, mkFlag "V" (PassFlag (setMode showVersionMode))
, mkFlag "-version" (PassFlag (setMode showVersionMode))
, mkFlag "-numeric-version" (PassFlag (setMode showNumVersionMode))
, mkFlag "-info" (PassFlag (setMode showInfoMode))
, mkFlag "-supported-languages" (PassFlag (setMode showSupportedExtensionsMode))
, mkFlag "-supported-extensions" (PassFlag (setMode showSupportedExtensionsMode))
] ++
[ mkFlag k' (PassFlag (setMode (printSetting k)))
| k <- ["Project version",
"Booter version",
"Stage",
"Build platform",
"Host platform",
"Target platform",
"Have interpreter",
"Object splitting supported",
"Have native code generator",
"Support SMP",
"Unregisterised",
"Tables next to code",
"RTS ways",
"Leading underscore",
"Debug on",
"LibDir",
"Global Package DB",
"C compiler flags",
"Gcc Linker flags",
"Ld Linker flags"],
let k' = "-print-" ++ map (replaceSpace . toLower) k
replaceSpace ' ' = '-'
replaceSpace c = c
] ++
------- interfaces ----------------------------------------------------
[ mkFlag "-show-iface" (HasArg (\f -> setMode (showInterfaceMode f)
"--show-iface"))
------- primary modes ------------------------------------------------
, mkFlag "c" (PassFlag (\f -> do setMode (stopBeforeMode StopLn) f
addFlag "-no-link" f))
, mkFlag "M" (PassFlag (setMode doMkDependHSMode))
, mkFlag "E" (PassFlag (setMode (stopBeforeMode anyHsc)))
, mkFlag "C" (PassFlag (\f -> do setMode (stopBeforeMode HCc) f
addFlag "-fvia-C" f))
#if MIN_VERSION_ghc(7,8,3)
, mkFlag "S" (PassFlag (setMode (stopBeforeMode (As True))))
#else
, mkFlag "S" (PassFlag (setMode (stopBeforeMode As)))
#endif
, mkFlag "-make" (PassFlag (setMode doMakeMode))
, mkFlag "-interactive" (PassFlag (setMode doInteractiveMode))
, mkFlag "-abi-hash" (PassFlag (setMode doAbiHashMode))
, mkFlag "e" (SepArg (\s -> setMode (doEvalMode s) "-e"))
]
#if MIN_VERSION_ghc(7,10,1)
where mkFlag fName fOptKind = Flag fName fOptKind AllModes
#else
where mkFlag fName fOptKind = Flag fName fOptKind
#endif
setMode :: Mode -> String -> EwM ModeM ()
setMode newMode newFlag = liftEwM $ do
(mModeFlag, errs, flags') <- getCmdLineState
let (modeFlag', errs') =
case mModeFlag of
Nothing -> ((newMode, newFlag), errs)
Just (oldMode, oldFlag) ->
case (oldMode, newMode) of
-- -c/--make are allowed together, and mean --make -no-link
_ | isStopLnMode oldMode && isDoMakeMode newMode
|| isStopLnMode newMode && isDoMakeMode oldMode ->
((doMakeMode, "--make"), [])
-- If we have both --help and --interactive then we
-- want showGhciUsage
_ | isShowGhcUsageMode oldMode &&
isDoInteractiveMode newMode ->
((showGhciUsageMode, oldFlag), [])
| isShowGhcUsageMode newMode &&
isDoInteractiveMode oldMode ->
((showGhciUsageMode, newFlag), [])
-- Otherwise, --help/--version/--numeric-version always win
| isDominantFlag oldMode -> ((oldMode, oldFlag), [])
| isDominantFlag newMode -> ((newMode, newFlag), [])
-- We need to accumulate eval flags like "-e foo -e bar"
(Right (Right (DoEval esOld)),
Right (Right (DoEval [eNew]))) ->
((Right (Right (DoEval (eNew : esOld))), oldFlag),
errs)
-- Saying e.g. --interactive --interactive is OK
_ | oldFlag == newFlag -> ((oldMode, oldFlag), errs)
-- Otherwise, complain
_ -> let err = flagMismatchErr oldFlag newFlag
in ((oldMode, oldFlag), err : errs)
putCmdLineState (Just modeFlag', errs', flags')
where isDominantFlag f = isShowGhcUsageMode f ||
isShowGhciUsageMode f ||
isShowVersionMode f ||
isShowNumVersionMode f
flagMismatchErr :: String -> String -> String
flagMismatchErr oldFlag newFlag
= "cannot use `" ++ oldFlag ++ "' with `" ++ newFlag ++ "'"
addFlag :: String -> String -> EwM ModeM ()
addFlag s flag = liftEwM $ do
(m, e, flags') <- getCmdLineState
putCmdLineState (m, e, mkGeneralLocated loc s : flags')
where loc = "addFlag by " ++ flag ++ " on the commandline"
type Mode = Either PreStartupMode PostStartupMode
type PostStartupMode = Either PreLoadMode PostLoadMode
data PreStartupMode
= ShowVersion -- ghc -V/--version
| ShowNumVersion -- ghc --numeric-version
| ShowSupportedExtensions -- ghc --supported-extensions
| Print String -- ghc --print-foo
showVersionMode, showNumVersionMode, showSupportedExtensionsMode :: Mode
showVersionMode = mkPreStartupMode ShowVersion
showNumVersionMode = mkPreStartupMode ShowNumVersion
showSupportedExtensionsMode = mkPreStartupMode ShowSupportedExtensions
mkPreStartupMode :: PreStartupMode -> Mode
mkPreStartupMode = Left
isShowVersionMode :: Mode -> Bool
isShowVersionMode (Left ShowVersion) = True
isShowVersionMode _ = False
isShowNumVersionMode :: Mode -> Bool
isShowNumVersionMode (Left ShowNumVersion) = True
isShowNumVersionMode _ = False
data PreLoadMode
= ShowGhcUsage -- ghc -?
| ShowGhciUsage -- ghci -?
| ShowInfo -- ghc --info
| PrintWithDynFlags (DynFlags -> String) -- ghc --print-foo
showGhcUsageMode, showGhciUsageMode, showInfoMode :: Mode
showGhcUsageMode = mkPreLoadMode ShowGhcUsage
showGhciUsageMode = mkPreLoadMode ShowGhciUsage
showInfoMode = mkPreLoadMode ShowInfo
printSetting :: String -> Mode
printSetting k = mkPreLoadMode (PrintWithDynFlags f)
where f dflags = fromMaybe (panic ("Setting not found: " ++ show k))
#if MIN_VERSION_ghc(7,2,0)
$ lookup k (compilerInfo dflags)
#else
$ fmap convertPrintable (lookup k compilerInfo)
where
convertPrintable (DynFlags.String s) = s
convertPrintable (DynFlags.FromDynFlags f) = f dflags
#endif
mkPreLoadMode :: PreLoadMode -> Mode
mkPreLoadMode = Right . Left
isShowGhcUsageMode :: Mode -> Bool
isShowGhcUsageMode (Right (Left ShowGhcUsage)) = True
isShowGhcUsageMode _ = False
isShowGhciUsageMode :: Mode -> Bool
isShowGhciUsageMode (Right (Left ShowGhciUsage)) = True
isShowGhciUsageMode _ = False
data PostLoadMode
= ShowInterface FilePath -- ghc --show-iface
| DoMkDependHS -- ghc -M
| StopBefore Phase -- ghc -E | -C | -S
-- StopBefore StopLn is the default
| DoMake -- ghc --make
| DoInteractive -- ghc --interactive
| DoEval [String] -- ghc -e foo -e bar => DoEval ["bar", "foo"]
| DoAbiHash -- ghc --abi-hash
doMkDependHSMode, doMakeMode, doInteractiveMode, doAbiHashMode :: Mode
doMkDependHSMode = mkPostLoadMode DoMkDependHS
doMakeMode = mkPostLoadMode DoMake
doInteractiveMode = mkPostLoadMode DoInteractive
doAbiHashMode = mkPostLoadMode DoAbiHash
showInterfaceMode :: FilePath -> Mode
showInterfaceMode fp = mkPostLoadMode (ShowInterface fp)
stopBeforeMode :: Phase -> Mode
stopBeforeMode phase = mkPostLoadMode (StopBefore phase)
doEvalMode :: String -> Mode
doEvalMode str = mkPostLoadMode (DoEval [str])
mkPostLoadMode :: PostLoadMode -> Mode
mkPostLoadMode = Right . Right
isDoInteractiveMode :: Mode -> Bool
isDoInteractiveMode (Right (Right DoInteractive)) = True
isDoInteractiveMode _ = False
isStopLnMode :: Mode -> Bool
isStopLnMode (Right (Right (StopBefore StopLn))) = True
isStopLnMode _ = False
isDoMakeMode :: Mode -> Bool
isDoMakeMode (Right (Right DoMake)) = True
isDoMakeMode _ = False
#ifdef GHCI
isInteractiveMode :: PostLoadMode -> Bool
isInteractiveMode DoInteractive = True
isInteractiveMode _ = False
#endif
-- isInterpretiveMode: byte-code compiler involved
isInterpretiveMode :: PostLoadMode -> Bool
isInterpretiveMode DoInteractive = True
isInterpretiveMode (DoEval _) = True
isInterpretiveMode _ = False
needsInputsMode :: PostLoadMode -> Bool
needsInputsMode DoMkDependHS = True
needsInputsMode (StopBefore _) = True
needsInputsMode DoMake = True
needsInputsMode _ = False
-- True if we are going to attempt to link in this mode.
-- (we might not actually link, depending on the GhcLink flag)
isLinkMode :: PostLoadMode -> Bool
isLinkMode (StopBefore StopLn) = True
isLinkMode DoMake = True
isLinkMode DoInteractive = True
isLinkMode (DoEval _) = True
isLinkMode _ = False
isCompManagerMode :: PostLoadMode -> Bool
isCompManagerMode DoMake = True
isCompManagerMode DoInteractive = True
isCompManagerMode (DoEval _) = True
isCompManagerMode _ = False

View File

@ -2,20 +2,17 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module HsFile (mkHsFile) where module HsFile (mkHsFile) where
import Text.ProjectTemplate (createTemplate) import Text.ProjectTemplate (createTemplate)
import Data.Conduit import Conduit
( ($$), (=$), awaitForever)
import Data.Conduit.Filesystem (sourceDirectory)
import Control.Monad.Trans.Resource (runResourceT)
import qualified Data.Conduit.List as CL
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.String (fromString) import Data.String (fromString)
mkHsFile :: IO () mkHsFile :: IO ()
mkHsFile = runResourceT $ sourceDirectory "." mkHsFile = runConduitRes
$$ readIt $ sourceDirectory "."
=$ createTemplate .| readIt
=$ awaitForever (liftIO . BS.putStr) .| createTemplate
.| mapM_C (liftIO . BS.putStr)
where where
-- Reads a filepath from upstream and dumps a pair of (filepath, filecontents) -- Reads a filepath from upstream and dumps a pair of (filepath, filecontents)
readIt = CL.map $ \i -> (fromString i, liftIO $ BS.readFile i) readIt = mapC $ \i -> (fromString i, liftIO $ BS.readFile i)

View File

@ -1,10 +1,16 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
module Keter module Keter
( keter ( keter
) where ) where
import Data.Yaml import Data.Yaml
#if MIN_VERSION_aeson(2, 0, 0)
import qualified Data.Aeson.KeyMap as Map
#else
import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict as Map
#endif
import qualified Data.Text as T import qualified Data.Text as T
import System.Environment (getEnvironment) import System.Environment (getEnvironment)
import System.Exit import System.Exit

111
yesod-bin/README.md Normal file
View File

@ -0,0 +1,111 @@
## yesod-bin: the Yesod executable
This executable is almost exclusively used for its `yesod devel`
capabilities, providing a development server for web apps. It also
provides some legacy functionality, almost all of which has been
superceded by functionality in the
[Haskell Stack build tool](http://haskellstack.org/). This README will
speak exclusively about `yesod devel`.
__CAVEAT__ There may be some issues using `yesod devel` in Docker-enabled
projects. See [comment on
Github](https://github.com/yesodweb/yesod/pull/1305#issuecomment-263204471).
### Development server
The development server will automatically recompile your application
whenever you make source code changes. It will then launch your app,
and reverse-proxy to it. The reverse proxying ensures that you can
connect to your application on a dedicated port, always get the latest
version available, and won't get dropped connections when the app
isn't yet ready. Instead, you'll get some very motivating messages:
![Motivation](https://i.sli.mg/nO6DvN.png)
## Common workflows
The standard Yesod scaffoldings are configured to work with `yesod
devel` out of the box (though see below for non-Yesod
development). For the most part, from within your application
directory, you'll just want to run:
* `stack build yesod-bin`
* `stack exec -- yesod devel`
This will install the corresponding version of the `yesod` executable
into your currently selected snapshot, and then use that
executable. (Starting with version 1.5.0, you can be more lax and use
a `yesod` executable compiled for a different snapshot. Once 1.5.0 is
more widespread we'll probably update these instructions.)
Some other common questions:
* If you want to control which port you can access your application
on, use the `--port` command line option, e.g. `stack exec -- yesod
devel --port 4000`. Changing your port inside your source code _will
not work_, because you need to change the reverse proxying port.
* If you want to run a command after each successful build, you can
use `stack exec -- yesod devel --success-hook "echo Yay!"`
* If for some reason you want to disable the reverse proxy
capabilities, use `stack exec -- yesod devel
--disable-reverse-proxy`
## How it works
The workflow of the devel server is pretty simple:
* Launch a reverse proxy server
* Use Stack file-watch capability to run a build loop on your code,
rebuilding each time a file is modified
* Have Stack call `yesod devel-signal` to write to a specific file
(`yesod-devel/rebuild`) each time a rebuild is successful
* Each time `yesod-devel/rebuild` is modified:
* Kill the current child process
* Get a new random port
* Tell the reverse proxy server about the new port to forward to
* Run the application's devel script with two environment variables
set:
* `PORT` gives the newly generated random port. The application
needs to listen on that port.
* `DISPLAY_PORT` gives the port that the reverse proxy is
listening on, used for display purposes or generating URLs.
Now some weird notes:
* The devel script can be one of the following three files. `yesod
devel` will search for them in the given order. That script must
provide a `main` function.
* `app/devel.hs`
* `devel.hs`
* `src/devel.hs`
* Unfortunately, directly killing the `ghc` interpreter has never
worked reliably, so we have an extra hack: when killing the process,
`yesod devel` also writes to a file
`yesod-devel/devel-terminate`. Your devel script should respect this
file and shutdown whenever it exists.
(It may be fixed in 1.6.0.5.)
* If your .cabal file defines them, `yesod devel` will tell Stack to
build with the flags `dev` and `library-only`. You can use this to
speed up compile times (biggest win: skip building executables, thus
the name `library-only`).
If that all seems a little complicated, remember that the Yesod
scaffolding handles all of this for you. But if you want to implement
it yourself...
## Non-Yesod development
If you'd like to use the `yesod devel` server for your non-Yesod
application, or even for a Yesod application not based on the
scaffolding, this section is for you! We've got a
[sample application in the repository](https://github.com/yesodweb/yesod/tree/master/yesod-bin/devel-example)
that demonstrates how to get this set up. It demonstrates a good way
to jump through the hoops implied above.
One important note: I highly recommend putting _all_ of the logic in
your library, and then providing a `develMain :: IO ()` function which
your `app/devel.hs` script reexports as `main`. I've found this to
greatly simplify things overall, since you can ensure all of your
dependencies are specified correctly in your `.cabal` file. Also, I
recommend using `PackageImports` in that file, as the example app
shows.

1
yesod-bin/devel-example/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
yesod-devel/

View File

@ -0,0 +1,5 @@
An example non-Yesod application that is compatible with `yesod devel`. Steps
to use it:
* `stack build yesod-bin`
* `stack exec -- yesod devel`

View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

View File

@ -0,0 +1,6 @@
module Main where
import DevelExample
main :: IO ()
main = prodMain

View File

@ -0,0 +1,5 @@
{-# LANGUAGE PackageImports #-}
import "devel-example" DevelExample (develMain)
main :: IO ()
main = develMain

View File

@ -0,0 +1,30 @@
name: devel-example
version: 0.1.0.0
build-type: Simple
cabal-version: >=1.10
flag library-only
default: False
description: Do not build the executable
library
hs-source-dirs: src
exposed-modules: DevelExample
build-depends: base
, async
, directory
, http-types
, wai
, wai-extra
, warp
default-language: Haskell2010
executable devel-example
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: base
, devel-example
default-language: Haskell2010
if flag(library-only)
buildable: False

View File

@ -0,0 +1,47 @@
{-# LANGUAGE OverloadedStrings #-}
module DevelExample
( prodMain
, develMain
) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (race_)
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Middleware.RequestLogger
import System.Directory (doesFileExist)
import System.Environment
myApp :: Application
myApp _req send = send $ responseLBS
status200
[(hContentType, "text/html; charset=utf-8")]
"<p>Well, this is really <b>boring</b>.</p>"
prodMain :: IO ()
prodMain = do
putStrLn "Running in production mode on port 8080"
run 8080 $ logStdout myApp
develMain :: IO ()
develMain = race_ watchTermFile $ do
port <- fmap read $ getEnv "PORT"
displayPort <- getEnv "DISPLAY_PORT"
putStrLn $ "Running in development mode on port " ++ show port
putStrLn $ "But you should connect to port " ++ displayPort
run port $ logStdoutDev myApp
-- | Would certainly be more efficient to use fsnotify, but this is
-- simpler.
watchTermFile :: IO ()
watchTermFile =
loop
where
loop = do
exists <- doesFileExist "yesod-devel/devel-terminate"
if exists
then return ()
else do
threadDelay 100000
loop

View File

@ -0,0 +1,8 @@
resolver: lts-7.10
packages:
- .
- ..
extra-deps:
- typed-process-0.1.0.0

View File

@ -1,65 +0,0 @@
{-
wrapper executable that captures arguments to ghc, ar or ld
-}
{-# LANGUAGE CPP #-}
module Main where
import Control.Monad (when)
import Data.Maybe (fromMaybe)
import Distribution.Compiler (CompilerFlavor (..))
import qualified Distribution.Simple.Configure as D
import Distribution.Simple.Program (arProgram,
defaultProgramConfiguration,
ghcProgram, ldProgram,
programPath)
import Distribution.Simple.Program.Db (configureAllKnownPrograms,
lookupProgram)
import Distribution.Simple.Program.Types (Program (..))
import Distribution.Verbosity (silent)
import System.Directory (doesDirectoryExist)
import System.Environment (getArgs)
import System.Exit (ExitCode (..), exitWith)
import System.IO (hPutStrLn, stderr)
import System.Process (rawSystem, readProcess)
#ifdef LDCMD
cmd :: Program
cmd = ldProgram
outFile = "yesod-devel/ldargs.txt"
#else
#ifdef ARCMD
cmd :: Program
cmd = arProgram
outFile ="yesod-devel/arargs.txt"
#else
cmd :: Program
cmd = ghcProgram
outFile = "yesod-devel/ghcargs.txt"
#endif
#endif
runProgram :: Program -> [String] -> IO ExitCode
runProgram pgm args = do
#if MIN_VERSION_Cabal(1,18,0)
(_, comp, pgmc) <- D.configCompilerEx (Just GHC) Nothing Nothing defaultProgramConfiguration silent
#else
(comp, pgmc) <- D.configCompiler (Just GHC) Nothing Nothing defaultProgramConfiguration silent
#endif
pgmc' <- configureAllKnownPrograms silent pgmc
case lookupProgram pgm pgmc' of
Nothing -> do
hPutStrLn stderr ("cannot find program '" ++ programName pgm ++ "'")
return (ExitFailure 1)
Just p -> rawSystem (programPath p) args
main :: IO ()
main = do
args <- getArgs
e <- doesDirectoryExist "yesod-devel"
when e $ writeFile outFile (show args ++ "\n")
ex <- runProgram cmd args
exitWith ex

View File

@ -1,38 +1,19 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module Main (main) where
import Control.Monad (unless)
import Data.Monoid import Data.Monoid
import Data.Version (showVersion) import Data.Version (showVersion)
import Options.Applicative import Options.Applicative
import System.Environment (getEnvironment) import System.Exit (exitFailure)
import System.Exit (ExitCode (ExitSuccess), exitWith, exitFailure)
import System.FilePath (splitSearchPath)
import System.Process (rawSystem)
import AddHandler (addHandler) import AddHandler (addHandler)
import Devel (DevelOpts (..), devel, DevelTermOpt(..)) import Devel (DevelOpts (..), devel, develSignal)
import Keter (keter) import Keter (keter)
import Options (injectDefaults) import Options (injectDefaults)
import qualified Paths_yesod_bin import qualified Paths_yesod_bin
import System.IO (hPutStrLn, stderr)
import HsFile (mkHsFile) import HsFile (mkHsFile)
#ifndef WINDOWS
import Build (touch)
touch' :: IO ()
touch' = touch
windowsWarning :: String
windowsWarning = ""
#else
touch' :: IO ()
touch' = return ()
windowsWarning :: String
windowsWarning = " (does not work on Windows)"
#endif
data CabalPgm = Cabal | CabalDev deriving (Show, Eq) data CabalPgm = Cabal | CabalDev deriving (Show, Eq)
@ -48,19 +29,16 @@ data Command = Init [String]
| Configure | Configure
| Build { buildExtraArgs :: [String] } | Build { buildExtraArgs :: [String] }
| Touch | Touch
| Devel { _develDisableApi :: Bool | Devel { develSuccessHook :: Maybe String
, _develSuccessHook :: Maybe String
, _develFailHook :: Maybe String
, _develRescan :: Int
, _develBuildDir :: Maybe String
, develIgnore :: [String]
, develExtraArgs :: [String] , develExtraArgs :: [String]
, _develPort :: Int , develPort :: Int
, _develTlsPort :: Int , develTlsPort :: Int
, _proxyTimeout :: Int , proxyTimeout :: Int
, _noReverseProxy :: Bool , noReverseProxy :: Bool
, _interruptOnly :: Bool , develHost :: Maybe String
, cert :: Maybe (FilePath, FilePath)
} }
| DevelSignal
| Test | Test
| AddHandler | AddHandler
{ addHandlerRoute :: Maybe String { addHandlerRoute :: Maybe String
@ -89,61 +67,34 @@ main = do
d@Devel{} -> d { develExtraArgs = args } d@Devel{} -> d { develExtraArgs = args }
c -> c c -> c
}) })
, ("yesod.devel.ignore" , \o args -> o { optCommand =
case optCommand o of
d@Devel{} -> d { develIgnore = args }
c -> c
})
, ("yesod.build.extracabalarg" , \o args -> o { optCommand = , ("yesod.build.extracabalarg" , \o args -> o { optCommand =
case optCommand o of case optCommand o of
b@Build{} -> b { buildExtraArgs = args } b@Build{} -> b { buildExtraArgs = args }
c -> c c -> c
}) })
] optParser' ] optParser'
let cabal = rawSystem' (cabalCommand o)
case optCommand o of case optCommand o of
Init _ -> initErrorMsg Init _ -> initErrorMsg
HsFiles -> mkHsFile HsFiles -> mkHsFile
Configure -> cabal ["configure"] Configure -> cabalErrorMsg
Build es -> touch' >> cabal ("build":es) Build _ -> cabalErrorMsg
Touch -> touch' Touch -> cabalErrorMsg
Keter{..} -> keter (cabalCommand o) _keterNoRebuild _keterNoCopyTo _keterBuildArgs Keter{..} -> keter (cabalCommand o) _keterNoRebuild _keterNoCopyTo _keterBuildArgs
Version -> putStrLn ("yesod-bin version: " ++ showVersion Paths_yesod_bin.version) Version -> putStrLn ("yesod-bin version: " ++ showVersion Paths_yesod_bin.version)
AddHandler{..} -> addHandler addHandlerRoute addHandlerPattern addHandlerMethods AddHandler{..} -> addHandler addHandlerRoute addHandlerPattern addHandlerMethods
Test -> cabalTest cabal Test -> cabalErrorMsg
Devel{..} ->do Devel{..} -> devel DevelOpts
(configOpts, menv) <- handleGhcPackagePath { verbose = optVerbose o
let develOpts = DevelOpts , successHook = develSuccessHook
{ isCabalDev = optCabalPgm o == CabalDev , develPort = develPort
, forceCabal = _develDisableApi , develTlsPort = develTlsPort
, verbose = optVerbose o , proxyTimeout = proxyTimeout
, eventTimeout = _develRescan , useReverseProxy = not noReverseProxy
, successHook = _develSuccessHook , develHost = develHost
, failHook = _develFailHook , cert = cert
, buildDir = _develBuildDir } develExtraArgs
, develPort = _develPort DevelSignal -> develSignal
, develTlsPort = _develTlsPort
, proxyTimeout = _proxyTimeout
, useReverseProxy = not _noReverseProxy
, terminateWith = if _interruptOnly then TerminateOnlyInterrupt else TerminateOnEnter
, develConfigOpts = configOpts
, develEnv = menv
}
devel develOpts develExtraArgs
where where
cabalTest cabal = do
env <- getEnvironment
case lookup "STACK_EXE" env of
Nothing -> do
touch'
_ <- cabal ["configure", "--enable-tests", "-flibrary-only"]
_ <- cabal ["build"]
cabal ["test"]
Just _ -> do
hPutStrLn stderr "'yesod test' is no longer needed with Stack"
hPutStrLn stderr "Instead, please just run 'stack test'"
exitFailure
initErrorMsg = do initErrorMsg = do
mapM_ putStrLn mapM_ putStrLn
[ "The init command has been removed." [ "The init command has been removed."
@ -154,18 +105,12 @@ main = do
] ]
exitFailure exitFailure
cabalErrorMsg = do
handleGhcPackagePath :: IO ([String], Maybe [(String, String)]) mapM_ putStrLn
handleGhcPackagePath = do [ "The configure, build, touch, and test commands have been removed."
env <- getEnvironment , "Please use 'stack' for building your project."
case lookup "GHC_PACKAGE_PATH" env of ]
Nothing -> return ([], Nothing) exitFailure
Just gpp -> do
let opts = "--package-db=clear"
: "--package-db=global"
: map ("--package-db=" ++)
(drop 1 $ reverse $ splitSearchPath gpp)
return (opts, Just $ filter (\(x, _) -> x /= "GHC_PACKAGE_PATH") env)
optParser' :: ParserInfo Options optParser' :: ParserInfo Options
optParser' = info (helper <*> optParser) ( fullDesc <> header "Yesod Web Framework command line utility" ) optParser' = info (helper <*> optParser) ( fullDesc <> header "Yesod Web Framework command line utility" )
@ -179,15 +124,17 @@ optParser = Options
<> command "hsfiles" (info (pure HsFiles) <> command "hsfiles" (info (pure HsFiles)
(progDesc "Create a hsfiles file for the current folder")) (progDesc "Create a hsfiles file for the current folder"))
<> command "configure" (info (pure Configure) <> command "configure" (info (pure Configure)
(progDesc "Configure a project for building")) (progDesc "DEPRECATED"))
<> command "build" (info (helper <*> (Build <$> extraCabalArgs)) <> command "build" (info (helper <*> (Build <$> extraCabalArgs))
(progDesc $ "Build project (performs TH dependency analysis)" ++ windowsWarning)) (progDesc "DEPRECATED"))
<> command "touch" (info (pure Touch) <> command "touch" (info (pure Touch)
(progDesc $ "Touch any files with altered TH dependencies but do not build" ++ windowsWarning)) (progDesc "DEPRECATED"))
<> command "devel" (info (helper <*> develOptions) <> command "devel" (info (helper <*> develOptions)
(progDesc "Run project with the devel server")) (progDesc "Run project with the devel server"))
<> command "devel-signal" (info (helper <*> pure DevelSignal)
(progDesc "Used internally by the devel command"))
<> command "test" (info (pure Test) <> command "test" (info (pure Test)
(progDesc "Build and run the integration tests")) (progDesc "DEPRECATED"))
<> command "add-handler" (info (helper <*> addHandlerOptions) <> command "add-handler" (info (helper <*> addHandlerOptions)
(progDesc ("Add a new handler and module to the project." (progDesc ("Add a new handler and module to the project."
++ " Interactively asks for input if you do not specify arguments."))) ++ " Interactively asks for input if you do not specify arguments.")))
@ -208,26 +155,10 @@ keterOptions = Keter
where where
optStrToList m = option (words <$> str) $ value [] <> m optStrToList m = option (words <$> str) $ value [] <> m
defaultRescan :: Int
defaultRescan = 10
develOptions :: Parser Command develOptions :: Parser Command
develOptions = Devel <$> switch ( long "disable-api" <> short 'd' develOptions = Devel <$> optStr ( long "success-hook" <> short 's' <> metavar "COMMAND"
<> help "Disable fast GHC API rebuilding")
<*> optStr ( long "success-hook" <> short 's' <> metavar "COMMAND"
<> help "Run COMMAND after rebuild succeeds") <> help "Run COMMAND after rebuild succeeds")
<*> optStr ( long "failure-hook" <> short 'f' <> metavar "COMMAND" <*> extraStackArgs
<> help "Run COMMAND when rebuild fails")
<*> option auto ( long "event-timeout" <> short 't' <> value defaultRescan <> metavar "N"
<> help ("Force rescan of files every N seconds (default "
++ show defaultRescan
++ ", use -1 to rely on FSNotify alone)") )
<*> optStr ( long "builddir" <> short 'b'
<> help "Set custom cabal build directory, default `dist'")
<*> many ( strOption ( long "ignore" <> short 'i' <> metavar "DIR"
<> help "ignore file changes in DIR" )
)
<*> extraCabalArgs
<*> option auto ( long "port" <> short 'p' <> value 3000 <> metavar "N" <*> option auto ( long "port" <> short 'p' <> value 3000 <> metavar "N"
<> help "Devel server listening port" ) <> help "Devel server listening port" )
<*> option auto ( long "tls-port" <> short 'q' <> value 3443 <> metavar "N" <*> option auto ( long "tls-port" <> short 'q' <> value 3443 <> metavar "N"
@ -236,8 +167,18 @@ develOptions = Devel <$> switch ( long "disable-api" <> short 'd'
<> help "Devel server timeout before returning 'not ready' message (in seconds, 0 for none)" ) <> help "Devel server timeout before returning 'not ready' message (in seconds, 0 for none)" )
<*> switch ( long "disable-reverse-proxy" <> short 'n' <*> switch ( long "disable-reverse-proxy" <> short 'n'
<> help "Disable reverse proxy" ) <> help "Disable reverse proxy" )
<*> switch ( long "interrupt-only" <> short 'c' <*> optStr (long "host" <> metavar "HOST"
<> help "Disable exiting when enter is pressed") <> help "Host interface to bind to; IP address, '*' for all interfaces, '*4' for IP4, '*6' for IP6")
<*> optional ( (,)
<$> strOption (long "cert" <> metavar "CERT"
<> help "Path to TLS certificate file, requires that --key is also defined")
<*> strOption (long "key" <> metavar "KEY"
<> help "Path to TLS key file, requires that --cert is also defined") )
extraStackArgs :: Parser [String]
extraStackArgs = many (strOption ( long "extra-stack-arg" <> short 'e' <> metavar "ARG"
<> help "pass extra argument ARG to stack")
)
extraCabalArgs :: Parser [String] extraCabalArgs :: Parser [String]
extraCabalArgs = many (strOption ( long "extra-cabal-arg" <> short 'e' <> metavar "ARG" extraCabalArgs = many (strOption ( long "extra-cabal-arg" <> short 'e' <> metavar "ARG"
@ -257,10 +198,3 @@ addHandlerOptions = AddHandler
-- | Optional @String@ argument -- | Optional @String@ argument
optStr :: Mod OptionFields (Maybe String) -> Parser (Maybe String) optStr :: Mod OptionFields (Maybe String) -> Parser (Maybe String)
optStr m = option (Just <$> str) $ value Nothing <> m optStr m = option (Just <$> str) $ value Nothing <> m
-- | Like @rawSystem@, but exits if it receives a non-success result.
rawSystem' :: String -> [String] -> IO ()
rawSystem' x y = do
res <- rawSystem x y
unless (res == ExitSuccess) $ exitWith res

View File

@ -1,100 +1,71 @@
name: yesod-bin name: yesod-bin
version: 1.4.18.7 version: 1.6.2.2
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
maintainer: Michael Snoyman <michael@snoyman.com> maintainer: Michael Snoyman <michael@snoyman.com>
synopsis: The yesod helper executable. synopsis: The yesod helper executable.
description: Provides scaffolding, devel server, and some simple code generation helpers. description: See README.md for more information
category: Web, Yesod category: Web, Yesod
stability: Stable stability: Stable
cabal-version: >= 1.6 cabal-version: >= 1.10
build-type: Simple build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
data-files: refreshing.html
extra-source-files: extra-source-files:
README.md
ChangeLog.md ChangeLog.md
refreshing.html
*.pem *.pem
executable yesod-ghc-wrapper
main-is: ghcwrapper.hs
build-depends:
base >= 4 && < 5
, Cabal
executable yesod-ld-wrapper
main-is: ghcwrapper.hs
cpp-options: -DLDCMD
build-depends:
base >= 4 && < 5
, Cabal
executable yesod-ar-wrapper
main-is: ghcwrapper.hs
cpp-options: -DARCMD
build-depends:
base >= 4 && < 5
, Cabal
executable yesod executable yesod
default-language: Haskell2010
if os(windows) if os(windows)
cpp-options: -DWINDOWS cpp-options: -DWINDOWS
if os(openbsd) if os(openbsd)
ld-options: -Wl,-zwxneeded ld-options: -Wl,-zwxneeded
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.10 && < 5
, ghc >= 7.0.3 , Cabal >= 1.18
, ghc-paths >= 0.1
, parsec >= 2.1 && < 4
, text >= 0.11
, shakespeare >= 2.0
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, time >= 1.1.4 , conduit >= 1.3
, template-haskell , conduit-extra >= 1.3
, directory >= 1.2.1
, Cabal
, unix-compat >= 0.2 && < 0.5
, containers >= 0.2 , containers >= 0.2
, attoparsec >= 0.10 , data-default-class
, http-types >= 0.7 , directory >= 1.2.1
, blaze-builder >= 0.2.1.4 && < 0.5
, filepath >= 1.1
, process
, zlib >= 0.5
, tar >= 0.4 && < 0.6
, unordered-containers
, yaml >= 0.8 && < 0.9
, optparse-applicative >= 0.11
, fsnotify >= 0.0 && < 0.3
, split >= 0.2 && < 0.3
, file-embed , file-embed
, conduit >= 1.2 , filepath >= 1.1
, conduit-extra , fsnotify
, resourcet >= 0.3 && < 1.2 , http-client >= 0.4.7
, base64-bytestring , http-client-tls
, lifted-base
, http-reverse-proxy >= 0.4 , http-reverse-proxy >= 0.4
, network , http-types >= 0.7
, http-conduit >= 2.1.4 , network >= 2.5
, http-client , optparse-applicative >= 0.11
, process
, project-template >= 0.1.1 , project-template >= 0.1.1
, say
, split >= 0.2 && < 0.3
, stm
, streaming-commons
, tar >= 0.4 && < 0.6
, text >= 0.11
, time >= 1.1.4
, transformers , transformers
, transformers-compat , transformers-compat
, warp >= 1.3.7.5 , unliftio
, unordered-containers
, wai >= 2.0 , wai >= 2.0
, wai-extra , wai-extra
, data-default-class , warp >= 1.3.7.5
, streaming-commons
, warp-tls >= 3.0.1 , warp-tls >= 3.0.1
, async , yaml >= 0.8 && < 0.12
, deepseq , zlib >= 0.5
, aeson
ghc-options: -Wall -threaded -rtsopts ghc-options: -Wall -threaded -rtsopts
main-is: main.hs main-is: main.hs
other-modules: Devel other-modules: Devel
Build
GhcBuild
Keter Keter
AddHandler AddHandler
Paths_yesod_bin Paths_yesod_bin

View File

@ -1,3 +1,302 @@
# ChangeLog for yesod-core
## 1.6.25.1
* Export the options that were created in 1.6.25.0 [#1825](https://github.com/yesodweb/yesod/pull/1825)
## 1.6.25.0
* Add an options structure that allows the user to set which instances will be derived for a routes structure. [#1819](https://github.com/yesodweb/yesod/pull/1819)
## 1.6.24.5
* Support Aeson 2.2 [#1818](https://github.com/yesodweb/yesod/pull/1818)
## 1.6.24.4
* Fix test-suite compilation error for GHC >= 9.0.1 [#1812](https://github.com/yesodweb/yesod/pull/1812)
## 1.6.24.3
* Fix subsite-to-subsite dispatch [#1805](https://github.com/yesodweb/yesod/pull/1805)
## 1.6.24.2
* No star is type [#1797](https://github.com/yesodweb/yesod/pull/1797)
## 1.6.24.1
* Adapt to removal of `ListT` from transformers-0.6. [#1796](https://github.com/yesodweb/yesod/pull/1796)
## 1.6.24.0
* Make catching exceptions configurable and set the default back to rethrowing async exceptions. [#1772](https://github.com/yesodweb/yesod/pull/1772).
## 1.6.23.1
* Fix typo in creation of the description `<meta>` tag in `defaultLayout`. [#1766](https://github.com/yesodweb/yesod/pull/1766)
## 1.6.23
* Add idempotent versions of `setDescription`, `setDescriptionI`. These functions
have odd behaviour when called multiple times, so they are now warned against.
This can't be a silent change - if you want to switch to the new functions, make
sure your layouts are updated to use `pageDescription` as well as `pageTitle`.
[#1765](https://github.com/yesodweb/yesod/pull/1765)
## 1.6.22.1
+ Remove sometimes failing superfluous test. [#1756](https://github.com/yesodweb/yesod/pull/1756)
## 1.6.22.0
* Add missing list to documentation for ``Yesod.Core.Dispatch.warp``. [#1745](https://github.com/yesodweb/yesod/pull/1745)
* Add instances for `ToContent Void`, `ToTypedContent Void`. [#1752](https://github.com/yesodweb/yesod/pull/1752)
* Handle async exceptions within yesod rather then warp. [#1753](https://github.com/yesodweb/yesod/pull/1753)
* Support template-haskell 2.18 [#1754](https://github.com/yesodweb/yesod/pull/1754)
## 1.6.21.0
* Export `Yesod.Core.Dispatch.defaultGen` so that users may reuse it for their own `YesodRunnerEnv`s [#1734](https://github.com/yesodweb/yesod/pull/1734)
## 1.6.20.2
* Fix compatibility with template-haskell 2.17 [#1729](https://github.com/yesodweb/yesod/pull/1729)
## 1.6.20.1
* Throw an error in `breadcrumbs` if the trail of breadcrumbs is circular. [#1727](https://github.com/yesodweb/yesod/issues/1727)
## 1.6.20
* Generate CSRF tokens using a secure entropy source [#1726](https://github.com/yesodweb/yesod/pull/1726)
* Change semantics of `yreGen` and `defaultGen`
## 1.6.19.0
* Change order of priority in `languages`[#1721](https://github.com/yesodweb/yesod/pull/1721)
## 1.6.18.8
* Fix test suite for wai-extra change around vary header
## 1.6.18.7
* Fix functions generating Open Graph metadata[#1709](https://github.com/yesodweb/yesod/pull/1709)
## 1.6.18.6
* Update documentation from `HandlerT` to `HandlerFor` [#1703](https://github.com/yesodweb/yesod/pull/1703)
## 1.6.18.5
Document `ErrorResponse` [#1698](https://github.com/yesodweb/yesod/pull/1698)
## 1.6.18.4
* Fixed a bug where `mkYesod` and other TH functions didn't work for datatypes with explicitly stated type variables, including the case with typeclass constraints. [https://github.com/yesodweb/yesod/pull/1697](#1697)
## 1.6.18.3
* Remove mention of an oudated Yesod type (`GHandler`) from the docs for `handlerToIO`. [https://github.com/yesodweb/yesod/pull/1695](#1695)
## 1.6.18.2
* Recommends `.yesodroutes` as the file extension for Yesod routes files. [#1686](https://github.com/yesodweb/yesod/pull/1686)
## 1.6.18.1
* Increase the size of CSRF token
## 1.6.18
* Add functions for setting description and OG meta [#1663](https://github.com/yesodweb/yesod/pull/1663)
* Use `DeriveLift` to implement the `Lift` instances for `ResourceTree`,
`Resource`, `Piece`, and `Dispatch`. Among other benefits, this provides
implementations of `liftTyped` on `template-haskell-2.16` (GHC 8.10) or
later. [#1664](https://github.com/yesodweb/yesod/pull/1664)
## 1.6.17.3
* Support for `unliftio-core` 0.2
## 1.6.17.2
* Support template-haskell 2.16, build with GHC 8.10 [#1657](https://github.com/yesodweb/yesod/pull/1657)
## 1.6.17.1
* Remove unnecessary deriving of Typeable
## 1.6.17
* Adds `contentTypeIsJson` [#1646](https://github.com/yesodweb/yesod/pull/1646)
## 1.6.16.1
* Compiles with GHC 8.8.1
## 1.6.16
* Add `jsAttributesHandler` to run arbitrary Handler code before building the
attributes map for the script tag generated by `widgetFile` [#1622](https://github.com/yesodweb/yesod/pull/1622)
## 1.6.15
* Move `redirectToPost` JavaScript form submission from HTML element to
`<script>` tag for CSP reasons [#1620](https://github.com/yesodweb/yesod/pull/1620)
## 1.6.14
* Introduce `JSONResponse`. [issue #1481](https://github.com/yesodweb/yesod/issues/1481) and [PR #1592](https://github.com/yesodweb/yesod/pull/1592)
## 1.6.13
* Introduce `maxContentLengthIO`. [issue #1588](https://github.com/yesodweb/yesod/issues/1588) and [PR #1589](https://github.com/yesodweb/yesod/pull/1589)
## 1.6.12
* Use at most one valid session cookie per request [#1581](https://github.com/yesodweb/yesod/pull/1581)
## 1.6.11
* Deprecate insecure JSON parsing functions [#1576](https://github.com/yesodweb/yesod/pull/1576)
## 1.6.10.1
* Fix test suite compilation for [commercialhaskell/stackage#4319](https://github.com/commercialhaskell/stackage/issues/4319)
## 1.6.10
* Adds functions to get and set values in the per-request caches. [#1573](https://github.com/yesodweb/yesod/pull/1573)
## 1.6.9
* Add `sendResponseNoContent` [#1565](https://github.com/yesodweb/yesod/pull/1565)
## 1.6.8.1
* Add missing test file to tarball [#1563](https://github.com/yesodweb/yesod/issues/1563)
## 1.6.8
* In the route syntax, allow trailing backslashes to indicate line
continuation. [#1558](https://github.com/yesodweb/yesod/pull/1558)
## 1.6.7
* If no matches are found, `selectRep` chooses first representation regardless
of the presence or absence of a `Content-Type` header in the request
[#1540](https://github.com/yesodweb/yesod/pull/1540)
* Sets the `X-XSS-Protection` header to `1; mode=block` [#1550](https://github.com/yesodweb/yesod/pull/1550)
* Add `PrimMonad` instances for `HandlerFor` and `WidgetFor` [from
StackOverflow](https://stackoverflow.com/q/52692508/369198)
## 1.6.6
* `defaultErrorHandler` handles text/plain requests [#1522](https://github.com/yesodweb/yesod/pull/1520)
## 1.6.5
* Add `fileSourceByteString` [#1503](https://github.com/yesodweb/yesod/pull/1503)
## 1.6.4
* Add `addContentDispositionFileName` [#1504](https://github.com/yesodweb/yesod/pull/1504)
## 1.6.3
* Add missing export for `SubHandlerFor`
## 1.6.2
* Derive a `Show` instance for `ResourceTree` and `FlatResource` [#1492](https://github.com/yesodweb/yesod/pull/1492)
* Some third party packages, like `yesod-routes-flow` derive their own `Show` instance, and this will break those packages.
## 1.6.1
* Add a `Semigroup LiteApp` instance, and explicitly define `(<>)` in the
already existing `Semigroup` instances for `WidgetFor`, `Head`, `Body`,
`GWData`, and `UniqueList`.
## 1.6.0
* Upgrade to conduit 1.3.0
* Switch to `MonadUnliftIO`
* Drop `mwc-random` and `blaze-builder` dependencies
* Strictify some internal data structures
* Add `CI` wrapper to first field in `Header` data constructor
[#1418](https://github.com/yesodweb/yesod/issues/1418)
* Internal only change, users of stable API are unaffected: `WidgetT`
holds its data in an `IORef` so that it is isomorphic to `ReaderT`,
avoiding state-loss issues..
* Overhaul of `HandlerT`/`WidgetT` to no longer be transformers.
* Fix Haddock comment & simplify implementation for `contentTypeTypes` [#1476](https://github.com/yesodweb/yesod/issues/1476)
## 1.4.37.3
* Improve error message when request body is too large [#1477](https://github.com/yesodweb/yesod/pull/1477)
## 1.4.37.2
* Improve error messages for the CSRF checking functions [#1455](https://github.com/yesodweb/yesod/issues/1455)
## 1.4.37.1
* Fix documentation on `languages` function, update `getMessageRender` to use said function. [#1457](https://github.com/yesodweb/yesod/pull/1457)
## 1.4.37
* Add `setWeakEtag` function in Yesod.Core.Handler module.
## 1.4.36
* Add `replaceOrAddHeader` function in Yesod.Core.Handler module. [1416](https://github.com/yesodweb/yesod/issues/1416)
## 1.4.35.1
* TH fix for GHC 8.2
## 1.4.35
* Contexts can be included in generated TH instances. [1365](https://github.com/yesodweb/yesod/issues/1365)
* Type variables can be included in routes.
## 1.4.34
* Add `WaiSubsiteWithAuth`. [#1394](https://github.com/yesodweb/yesod/pull/1394)
## 1.4.33
* Adds curly brackets to route parser. [#1363](https://github.com/yesodweb/yesod/pull/1363)
## 1.4.32
* Fix warnings
* Route parsing handles CRLF line endings
* Add 'getPostParams' in Yesod.Core.Handler
* Haddock rendering improved.
## 1.4.31
* Add `parseCheckJsonBody` and `requireCheckJsonBody`
## 1.4.30
* Add `defaultMessageWidget`
## 1.4.29
* Exports some internals and fix version bounds [#1318](https://github.com/yesodweb/yesod/pull/1318)
## 1.4.28
* Add ToWidget instances for strict text, lazy text, and text builder [#1310](https://github.com/yesodweb/yesod/pull/1310)
## 1.4.27
* Added `jsAttributes` [#1308](https://github.com/yesodweb/yesod/pull/1308)
## 1.4.26 ## 1.4.26
* Modify `languages` so that, if you previously called `setLanguage`, the newly * Modify `languages` so that, if you previously called `setLanguage`, the newly

View File

@ -1,42 +0,0 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
module Yesod.Core.Class.Dispatch where
import Yesod.Routes.Class
import qualified Network.Wai as W
import Yesod.Core.Types
import Yesod.Core.Content
import Yesod.Core.Handler (stripHandlerT)
import Yesod.Core.Class.Yesod
import Yesod.Core.Class.Handler
-- | This class is automatically instantiated when you use the template haskell
-- mkYesod function. You should never need to deal with it directly.
class Yesod site => YesodDispatch site where
yesodDispatch :: YesodRunnerEnv site -> W.Application
class YesodSubDispatch sub m where
yesodSubDispatch :: YesodSubRunnerEnv sub (HandlerSite m) m
-> W.Application
instance YesodSubDispatch WaiSubsite master where
yesodSubDispatch YesodSubRunnerEnv {..} = app
where
WaiSubsite app = ysreGetSub $ yreSite ysreParentEnv
-- | A helper function for creating YesodSubDispatch instances, used by the
-- internal generated code. This function has been exported since 1.4.11.
-- It promotes a subsite handler to a wai application.
subHelper :: Monad m -- NOTE: This is incredibly similar in type signature to yesodRunner, should probably be pointed out/explained.
=> HandlerT child (HandlerT parent m) TypedContent
-> YesodSubRunnerEnv child parent (HandlerT parent m)
-> Maybe (Route child)
-> W.Application
subHelper handlert YesodSubRunnerEnv {..} route =
ysreParentRunner base ysreParentEnv (fmap ysreToParentRoute route)
where
base = stripHandlerT (fmap toTypedContent handlert) ysreGetSub ysreToParentRoute route

View File

@ -1,100 +0,0 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
module Yesod.Core.Class.Handler
( MonadHandler (..)
, MonadWidget (..)
) where
import Yesod.Core.Types
import Control.Monad (liftM)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Resource (MonadResource, MonadResourceBase)
import Control.Monad.Trans.Class (lift)
#if __GLASGOW_HASKELL__ < 710
import Data.Monoid (Monoid, mempty)
#endif
import Data.Conduit.Internal (Pipe, ConduitM)
import Control.Monad.Trans.Identity ( IdentityT)
import Control.Monad.Trans.List ( ListT )
import Control.Monad.Trans.Maybe ( MaybeT )
import Control.Monad.Trans.Error ( ErrorT, Error)
#if MIN_VERSION_transformers(0,4,0)
import Control.Monad.Trans.Except ( ExceptT )
#endif
import Control.Monad.Trans.Reader ( ReaderT )
import Control.Monad.Trans.State ( StateT )
import Control.Monad.Trans.Writer ( WriterT )
import Control.Monad.Trans.RWS ( RWST )
import qualified Control.Monad.Trans.RWS.Strict as Strict ( RWST )
import qualified Control.Monad.Trans.State.Strict as Strict ( StateT )
import qualified Control.Monad.Trans.Writer.Strict as Strict ( WriterT )
class MonadResource m => MonadHandler m where
type HandlerSite m
liftHandlerT :: HandlerT (HandlerSite m) IO a -> m a
replaceToParent :: HandlerData site route -> HandlerData site ()
replaceToParent hd = hd { handlerToParent = const () }
instance MonadResourceBase m => MonadHandler (HandlerT site m) where
type HandlerSite (HandlerT site m) = site
liftHandlerT (HandlerT f) = HandlerT $ liftIO . f . replaceToParent
{-# RULES "liftHandlerT (HandlerT site IO)" liftHandlerT = id #-}
instance MonadResourceBase m => MonadHandler (WidgetT site m) where
type HandlerSite (WidgetT site m) = site
liftHandlerT (HandlerT f) = WidgetT $ liftIO . liftM (, mempty) . f . replaceToParent
{-# RULES "liftHandlerT (WidgetT site IO)" forall f. liftHandlerT (HandlerT f) = WidgetT $ liftM (, mempty) . f #-}
#define GO(T) instance MonadHandler m => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; liftHandlerT = lift . liftHandlerT
#define GOX(X, T) instance (X, MonadHandler m) => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; liftHandlerT = lift . liftHandlerT
GO(IdentityT)
GO(ListT)
GO(MaybeT)
GOX(Error e, ErrorT e)
#if MIN_VERSION_transformers(0,4,0)
GO(ExceptT e)
#endif
GO(ReaderT r)
GO(StateT s)
GOX(Monoid w, WriterT w)
GOX(Monoid w, RWST r w s)
GOX(Monoid w, Strict.RWST r w s)
GO(Strict.StateT s)
GOX(Monoid w, Strict.WriterT w)
GO(Pipe l i o u)
GO(ConduitM i o)
#undef GO
#undef GOX
class MonadHandler m => MonadWidget m where
liftWidgetT :: WidgetT (HandlerSite m) IO a -> m a
instance MonadResourceBase m => MonadWidget (WidgetT site m) where
liftWidgetT (WidgetT f) = WidgetT $ liftIO . f . replaceToParent
#define GO(T) instance MonadWidget m => MonadWidget (T m) where liftWidgetT = lift . liftWidgetT
#define GOX(X, T) instance (X, MonadWidget m) => MonadWidget (T m) where liftWidgetT = lift . liftWidgetT
GO(IdentityT)
GO(ListT)
GO(MaybeT)
GOX(Error e, ErrorT e)
#if MIN_VERSION_transformers(0,4,0)
GO(ExceptT e)
#endif
GO(ReaderT r)
GO(StateT s)
GOX(Monoid w, WriterT w)
GOX(Monoid w, RWST r w s)
GOX(Monoid w, Strict.RWST r w s)
GO(Strict.StateT s)
GOX(Monoid w, Strict.WriterT w)
GO(Pipe l i o u)
GO(ConduitM i o)
#undef GO
#undef GOX

View File

@ -1,198 +0,0 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE CPP #-}
module Yesod.Core.Internal.TH where
import Prelude hiding (exp)
import Yesod.Core.Handler
import Language.Haskell.TH hiding (cxt, instanceD)
import Language.Haskell.TH.Syntax
import qualified Network.Wai as W
import Data.ByteString.Lazy.Char8 ()
import Data.List (foldl')
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
import Control.Monad (replicateM, void)
import Data.Either (partitionEithers)
import Yesod.Routes.TH
import Yesod.Routes.Parse
import Yesod.Core.Types
import Yesod.Core.Content
import Yesod.Core.Class.Dispatch
import Yesod.Core.Internal.Run
-- | Generates URL datatype and site function for the given 'Resource's. This
-- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter.
-- Use 'parseRoutes' to create the 'Resource's.
mkYesod :: String -- ^ name of the argument datatype
-> [ResourceTree String]
-> Q [Dec]
mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] False return
mkYesodWith :: String
-> [Either String [String]]
-> [ResourceTree String]
-> Q [Dec]
mkYesodWith name args = fmap (uncurry (++)) . mkYesodGeneral name args False return
-- | Sometimes, you will want to declare your routes in one file and define
-- your handlers elsewhere. For example, this is the only way to break up a
-- monolithic file into smaller parts. Use this function, paired with
-- 'mkYesodDispatch', to do just that.
mkYesodData :: String -> [ResourceTree String] -> Q [Dec]
mkYesodData name = mkYesodDataGeneral name False
mkYesodSubData :: String -> [ResourceTree String] -> Q [Dec]
mkYesodSubData name = mkYesodDataGeneral name True
mkYesodDataGeneral :: String -> Bool -> [ResourceTree String] -> Q [Dec]
mkYesodDataGeneral name isSub res = do
let (name':rest) = words name
fst <$> mkYesodGeneral name' (fmap Left rest) isSub return res
-- | See 'mkYesodData'.
mkYesodDispatch :: String -> [ResourceTree String] -> Q [Dec]
mkYesodDispatch name = fmap snd . mkYesodGeneral name [] False return
-- | Get the Handler and Widget type synonyms for the given site.
masterTypeSyns :: [Name] -> Type -> [Dec]
masterTypeSyns vs site =
[ TySynD (mkName "Handler") (fmap PlainTV vs)
$ ConT ''HandlerT `AppT` site `AppT` ConT ''IO
, TySynD (mkName "Widget") (fmap PlainTV vs)
$ ConT ''WidgetT `AppT` site `AppT` ConT ''IO `AppT` ConT ''()
]
-- | 'Left' arguments indicate a monomorphic type, a 'Right' argument
-- indicates a polymorphic type, and provides the list of classes
-- the type must be instance of.
mkYesodGeneral :: String -- ^ foundation type
-> [Either String [String]] -- ^ arguments for the type
-> Bool -- ^ is this a subsite
-> (Exp -> Q Exp) -- ^ unwrap handler
-> [ResourceTree String]
-> Q([Dec],[Dec])
mkYesodGeneral namestr args isSub f resS = do
mname <- lookupTypeName namestr
arity <- case mname of
Just name -> do
info <- reify name
return $
case info of
TyConI dec ->
case dec of
#if MIN_VERSION_template_haskell(2,11,0)
DataD _ _ vs _ _ _ -> length vs
NewtypeD _ _ vs _ _ _ -> length vs
#else
DataD _ _ vs _ _ -> length vs
NewtypeD _ _ vs _ _ -> length vs
#endif
_ -> 0
_ -> 0
_ -> return 0
let name = mkName namestr
(mtys,_) = partitionEithers args
-- Generate as many variable names as the arity indicates
vns <- replicateM (arity - length mtys) $ newName "t"
-- Base type (site type with variables)
let (argtypes,cxt) = (\(ns,r,cs) -> (ns ++ fmap VarT r, cs)) $
foldr (\arg (xs,n:ns,cs) ->
case arg of
Left t -> ( ConT (mkName t):xs, n:ns, cs )
Right ts -> ( VarT n :xs, ns
, fmap (\t ->
#if MIN_VERSION_template_haskell(2,10,0)
AppT (ConT $ mkName t) (VarT n)
#else
ClassP (mkName t) [VarT n]
#endif
) ts ++ cs )
) ([],vns,[]) args
site = foldl' AppT (ConT name) argtypes
res = map (fmap parseType) resS
renderRouteDec <- mkRenderRouteInstance site res
routeAttrsDec <- mkRouteAttrsInstance site res
dispatchDec <- mkDispatchInstance site cxt f res
parse <- mkParseRouteInstance site res
let rname = mkName $ "resources" ++ namestr
eres <- lift resS
let resourcesDec =
[ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)
, FunD rname [Clause [] (NormalB eres) []]
]
let dataDec = concat
[ [parse]
, renderRouteDec
, [routeAttrsDec]
, resourcesDec
, if isSub then [] else masterTypeSyns vns site
]
return (dataDec, dispatchDec)
mkMDS :: (Exp -> Q Exp) -> Q Exp -> MkDispatchSettings a site b
mkMDS f rh = MkDispatchSettings
{ mdsRunHandler = rh
, mdsSubDispatcher =
[|\parentRunner getSub toParent env -> yesodSubDispatch
YesodSubRunnerEnv
{ ysreParentRunner = parentRunner
, ysreGetSub = getSub
, ysreToParentRoute = toParent
, ysreParentEnv = env
}
|]
, mdsGetPathInfo = [|W.pathInfo|]
, mdsSetPathInfo = [|\p r -> r { W.pathInfo = p }|]
, mdsMethod = [|W.requestMethod|]
, mds404 = [|void notFound|]
, mds405 = [|void badMethod|]
, mdsGetHandler = defaultGetHandler
, mdsUnwrapper = f
}
-- | If the generation of @'YesodDispatch'@ instance require finer
-- control of the types, contexts etc. using this combinator. You will
-- hardly need this generality. However, in certain situations, like
-- when writing library/plugin for yesod, this combinator becomes
-- handy.
mkDispatchInstance :: Type -- ^ The master site type
-> Cxt -- ^ Context of the instance
-> (Exp -> Q Exp) -- ^ Unwrap handler
-> [ResourceTree c] -- ^ The resource
-> DecsQ
mkDispatchInstance master cxt f res = do
clause' <- mkDispatchClause (mkMDS f [|yesodRunner|]) res
let thisDispatch = FunD 'yesodDispatch [clause']
return [instanceD cxt yDispatch [thisDispatch]]
where
yDispatch = ConT ''YesodDispatch `AppT` master
mkYesodSubDispatch :: [ResourceTree a] -> Q Exp
mkYesodSubDispatch res = do
clause' <- mkDispatchClause (mkMDS return [|subHelper . fmap toTypedContent|]) res
inner <- newName "inner"
let innerFun = FunD inner [clause']
helper <- newName "helper"
let fun = FunD helper
[ Clause
[]
(NormalB $ VarE inner)
[innerFun]
]
return $ LetE [fun] (VarE helper)
instanceD :: Cxt -> Type -> [Dec] -> Dec
#if MIN_VERSION_template_haskell(2,11,0)
instanceD = InstanceD Nothing
#else
instanceD = InstanceD
#endif

View File

@ -1,181 +0,0 @@
{-# LANGUAGE TemplateHaskell, CPP #-}
module Yesod.Routes.TH.RenderRoute
( -- ** RenderRoute
mkRenderRouteInstance
, mkRenderRouteInstance'
, mkRouteCons
, mkRenderRouteClauses
) where
import Yesod.Routes.TH.Types
#if MIN_VERSION_template_haskell(2,11,0)
import Language.Haskell.TH (conT)
#endif
import Language.Haskell.TH.Syntax
import Data.Maybe (maybeToList)
import Control.Monad (replicateM)
import Data.Text (pack)
import Web.PathPieces (PathPiece (..), PathMultiPiece (..))
import Yesod.Routes.Class
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
import Data.Monoid (mconcat)
#endif
-- | Generate the constructors of a route data type.
mkRouteCons :: [ResourceTree Type] -> Q ([Con], [Dec])
mkRouteCons rttypes =
mconcat <$> mapM mkRouteCon rttypes
where
mkRouteCon (ResourceLeaf res) =
return ([con], [])
where
con = NormalC (mkName $ resourceName res)
$ map (\x -> (notStrict, x))
$ concat [singles, multi, sub]
singles = concatMap toSingle $ resourcePieces res
toSingle Static{} = []
toSingle (Dynamic typ) = [typ]
multi = maybeToList $ resourceMulti res
sub =
case resourceDispatch res of
Subsite { subsiteType = typ } -> [ConT ''Route `AppT` typ]
_ -> []
mkRouteCon (ResourceParent name _check pieces children) = do
(cons, decs) <- mkRouteCons children
#if MIN_VERSION_template_haskell(2,11,0)
dec <- DataD [] (mkName name) [] Nothing cons <$> mapM conT [''Show, ''Read, ''Eq]
#else
let dec = DataD [] (mkName name) [] cons [''Show, ''Read, ''Eq]
#endif
return ([con], dec : decs)
where
con = NormalC (mkName name)
$ map (\x -> (notStrict, x))
$ singles ++ [ConT $ mkName name]
singles = concatMap toSingle pieces
toSingle Static{} = []
toSingle (Dynamic typ) = [typ]
-- | Clauses for the 'renderRoute' method.
mkRenderRouteClauses :: [ResourceTree Type] -> Q [Clause]
mkRenderRouteClauses =
mapM go
where
isDynamic Dynamic{} = True
isDynamic _ = False
go (ResourceParent name _check pieces children) = do
let cnt = length $ filter isDynamic pieces
dyns <- replicateM cnt $ newName "dyn"
child <- newName "child"
let pat = ConP (mkName name) $ map VarP $ dyns ++ [child]
pack' <- [|pack|]
tsp <- [|toPathPiece|]
let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp pieces dyns
childRender <- newName "childRender"
let rr = VarE childRender
childClauses <- mkRenderRouteClauses children
a <- newName "a"
b <- newName "b"
colon <- [|(:)|]
let cons y ys = InfixE (Just y) colon (Just ys)
let pieces' = foldr cons (VarE a) piecesSingle
let body = LamE [TupP [VarP a, VarP b]] (TupE [pieces', VarE b]) `AppE` (rr `AppE` VarE child)
return $ Clause [pat] (NormalB body) [FunD childRender childClauses]
go (ResourceLeaf res) = do
let cnt = length (filter isDynamic $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res)
dyns <- replicateM cnt $ newName "dyn"
sub <-
case resourceDispatch res of
Subsite{} -> return <$> newName "sub"
_ -> return []
let pat = ConP (mkName $ resourceName res) $ map VarP $ dyns ++ sub
pack' <- [|pack|]
tsp <- [|toPathPiece|]
let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp (resourcePieces res) dyns
piecesMulti <-
case resourceMulti res of
Nothing -> return $ ListE []
Just{} -> do
tmp <- [|toPathMultiPiece|]
return $ tmp `AppE` VarE (last dyns)
body <-
case sub of
[x] -> do
rr <- [|renderRoute|]
a <- newName "a"
b <- newName "b"
colon <- [|(:)|]
let cons y ys = InfixE (Just y) colon (Just ys)
let pieces = foldr cons (VarE a) piecesSingle
return $ LamE [TupP [VarP a, VarP b]] (TupE [pieces, VarE b]) `AppE` (rr `AppE` VarE x)
_ -> do
colon <- [|(:)|]
let cons a b = InfixE (Just a) colon (Just b)
return $ TupE [foldr cons piecesMulti piecesSingle, ListE []]
return $ Clause [pat] (NormalB body) []
mkPieces _ _ [] _ = []
mkPieces toText tsp (Static s:ps) dyns = toText s : mkPieces toText tsp ps dyns
mkPieces toText tsp (Dynamic{}:ps) (d:dyns) = tsp `AppE` VarE d : mkPieces toText tsp ps dyns
mkPieces _ _ (Dynamic _ : _) [] = error "mkPieces 120"
-- | Generate the 'RenderRoute' instance.
--
-- This includes both the 'Route' associated type and the
-- 'renderRoute' method. This function uses both 'mkRouteCons' and
-- 'mkRenderRouteClasses'.
mkRenderRouteInstance :: Type -> [ResourceTree Type] -> Q [Dec]
mkRenderRouteInstance = mkRenderRouteInstance' []
-- | A more general version of 'mkRenderRouteInstance' which takes an
-- additional context.
mkRenderRouteInstance' :: Cxt -> Type -> [ResourceTree Type] -> Q [Dec]
mkRenderRouteInstance' cxt typ ress = do
cls <- mkRenderRouteClauses ress
(cons, decs) <- mkRouteCons ress
#if MIN_VERSION_template_haskell(2,11,0)
did <- DataInstD [] ''Route [typ] Nothing cons <$> mapM conT clazzes
#else
let did = DataInstD [] ''Route [typ] cons clazzes
#endif
return $ instanceD cxt (ConT ''RenderRoute `AppT` typ)
[ did
, FunD (mkName "renderRoute") cls
] : decs
where
clazzes = [''Show, ''Eq, ''Read]
#if MIN_VERSION_template_haskell(2,11,0)
notStrict :: Bang
notStrict = Bang NoSourceUnpackedness NoSourceStrictness
#else
notStrict :: Strict
notStrict = NotStrict
#endif
instanceD :: Cxt -> Type -> [Dec] -> Dec
#if MIN_VERSION_template_haskell(2,11,0)
instanceD = InstanceD Nothing
#else
instanceD = InstanceD
#endif

View File

@ -5,26 +5,20 @@
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
module Main where module Main where
import Criterion.Main import Gauge.Main
import Text.Hamlet import Text.Hamlet
import Numeric (showInt)
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import qualified Text.Blaze.Html.Renderer.Utf8 as Utf8 import qualified Text.Blaze.Html.Renderer.Utf8 as Utf8
import Data.Monoid (mconcat) import Data.Monoid (mconcat)
import Text.Blaze.Html5 (table, tr, td) import Text.Blaze.Html5 (table, tr, td)
import Text.Blaze.Html (toHtml) import Text.Blaze.Html (toHtml)
import Yesod.Core.Widget import Data.Int
import Control.Monad.Trans.Writer
import Control.Monad.Trans.RWS
import Data.Functor.Identity
import Yesod.Core.Types
import Data.Monoid
import Data.IORef
main :: IO ()
main = defaultMain main = defaultMain
[ bench "bigTable html" $ nf bigTableHtml bigTableData [ bench "bigTable html" $ nf bigTableHtml bigTableData
, bench "bigTable hamlet" $ nf bigTableHamlet bigTableData , bench "bigTable hamlet" $ nf bigTableHamlet bigTableData
, bench "bigTable widget" $ nfIO (bigTableWidget bigTableData) --, bench "bigTable widget" $ nfIO (bigTableWidget bigTableData)
, bench "bigTable blaze" $ nf bigTableBlaze bigTableData , bench "bigTable blaze" $ nf bigTableBlaze bigTableData
] ]
where where
@ -35,6 +29,7 @@ main = defaultMain
bigTableData = replicate rows [1..10] bigTableData = replicate rows [1..10]
{-# NOINLINE bigTableData #-} {-# NOINLINE bigTableData #-}
bigTableHtml :: Show a => [[a]] -> Int64
bigTableHtml rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet| bigTableHtml rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet|
<table> <table>
$forall row <- rows $forall row <- rows
@ -43,6 +38,7 @@ bigTableHtml rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet|
<td>#{show cell} <td>#{show cell}
|] |]
bigTableHamlet :: Show a => [[a]] -> Int64
bigTableHamlet rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet| bigTableHamlet rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet|
<table> <table>
$forall row <- rows $forall row <- rows
@ -51,6 +47,8 @@ bigTableHamlet rows = L.length $ Utf8.renderHtml $ ($ id) [hamlet|
<td>#{show cell} <td>#{show cell}
|] |]
{-
bigTableWidget :: Show a => [[a]] -> IO Int64
bigTableWidget rows = fmap (L.length . Utf8.renderHtml . ($ render)) (run [whamlet| bigTableWidget rows = fmap (L.length . Utf8.renderHtml . ($ render)) (run [whamlet|
<table> <table>
$forall row <- rows $forall row <- rows
@ -63,7 +61,9 @@ bigTableWidget rows = fmap (L.length . Utf8.renderHtml . ($ render)) (run [whaml
run (WidgetT w) = do run (WidgetT w) = do
(_, GWData { gwdBody = Body x }) <- w undefined (_, GWData { gwdBody = Body x }) <- w undefined
return x return x
-}
bigTableBlaze t = L.length $ Utf8.renderHtml $ table $ mconcat $ map row t bigTableBlaze :: Show a => [[a]] -> Int64
bigTableBlaze t = L.length $ Utf8.renderHtml $ table $ Data.Monoid.mconcat $ map row t
where where
row r = tr $ mconcat $ map (td . toHtml . show) r row r = tr $ mconcat $ map (td . toHtml . show) r

View File

@ -18,7 +18,7 @@ module Yesod.Core
, Approot (..) , Approot (..)
, FileUpload (..) , FileUpload (..)
, ErrorResponse (..) , ErrorResponse (..)
-- * Utitlities -- * Utilities
, maybeAuthorized , maybeAuthorized
, widgetToPageContent , widgetToPageContent
-- * Defaults -- * Defaults
@ -31,7 +31,6 @@ module Yesod.Core
-- * Logging -- * Logging
, defaultMakeLogger , defaultMakeLogger
, defaultMessageLoggerSource , defaultMessageLoggerSource
, defaultShouldLog
, defaultShouldLogIO , defaultShouldLogIO
, formatLogMessage , formatLogMessage
, LogLevel (..) , LogLevel (..)
@ -67,11 +66,9 @@ module Yesod.Core
-- * JS loaders -- * JS loaders
, ScriptLoadPosition (..) , ScriptLoadPosition (..)
, BottomOfHeadAsync , BottomOfHeadAsync
-- * Subsites -- * Generalizing type classes
, MonadHandler (..) , MonadHandler (..)
, MonadWidget (..) , MonadWidget (..)
, getRouteToParent
, defaultLayoutSub
-- * Approot -- * Approot
, guessApproot , guessApproot
, guessApprootOr , guessApprootOr
@ -95,8 +92,7 @@ module Yesod.Core
, module Text.Blaze.Html , module Text.Blaze.Html
, MonadTrans (..) , MonadTrans (..)
, MonadIO (..) , MonadIO (..)
, MonadBase (..) , MonadUnliftIO (..)
, MonadBaseControl
, MonadResource (..) , MonadResource (..)
, MonadLogger , MonadLogger
-- * Commonly referenced functions/datatypes -- * Commonly referenced functions/datatypes
@ -143,9 +139,7 @@ import qualified Yesod.Core.Internal.Run
import qualified Paths_yesod_core import qualified Paths_yesod_core
import Data.Version (showVersion) import Data.Version (showVersion)
import Yesod.Routes.Class import Yesod.Routes.Class
import Control.Monad.IO.Class (MonadIO (..)) import UnliftIO (MonadIO (..), MonadUnliftIO (..))
import Control.Monad.Base (MonadBase (..))
import Control.Monad.Trans.Control (MonadBaseControl (..))
import Control.Monad.Trans.Resource (MonadResource (..)) import Control.Monad.Trans.Resource (MonadResource (..))
import Yesod.Core.Internal.LiteApp import Yesod.Core.Internal.LiteApp
@ -185,14 +179,6 @@ maybeAuthorized r isWrite = do
x <- isAuthorized r isWrite x <- isAuthorized r isWrite
return $ if x == Authorized then Just r else Nothing return $ if x == Authorized then Just r else Nothing
getRouteToParent :: Monad m => HandlerT child (HandlerT parent m) (Route child -> Route parent)
getRouteToParent = HandlerT $ return . handlerToParent
defaultLayoutSub :: Yesod parent
=> WidgetT child IO ()
-> HandlerT child (HandlerT parent IO) Html
defaultLayoutSub cwidget = widgetToParentWidget cwidget >>= lift . defaultLayout
showIntegral :: Integral a => a -> String showIntegral :: Integral a => a -> String
showIntegral x = show (fromIntegral x :: Integer) showIntegral x = show (fromIntegral x :: Integer)

View File

@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
module Yesod.Core.Class.Breadcrumbs where module Yesod.Core.Class.Breadcrumbs where
import Yesod.Core.Handler import Yesod.Core.Handler
@ -11,11 +12,11 @@ import Data.Text (Text)
class YesodBreadcrumbs site where class YesodBreadcrumbs site where
-- | Returns the title and the parent resource, if available. If you return -- | Returns the title and the parent resource, if available. If you return
-- a 'Nothing', then this is considered a top-level page. -- a 'Nothing', then this is considered a top-level page.
breadcrumb :: Route site -> HandlerT site IO (Text , Maybe (Route site)) breadcrumb :: Route site -> HandlerFor site (Text , Maybe (Route site))
-- | Gets the title of the current page and the hierarchy of parent pages, -- | Gets the title of the current page and the hierarchy of parent pages,
-- along with their respective titles. -- along with their respective titles.
breadcrumbs :: YesodBreadcrumbs site => HandlerT site IO (Text, [(Route site, Text)]) breadcrumbs :: (YesodBreadcrumbs site, Show (Route site), Eq (Route site)) => HandlerFor site (Text, [(Route site, Text)])
breadcrumbs = do breadcrumbs = do
x <- getCurrentRoute x <- getCurrentRoute
case x of case x of
@ -26,6 +27,8 @@ breadcrumbs = do
return (title, z) return (title, z)
where where
go back Nothing = return back go back Nothing = return back
go back (Just this) = do go back (Just this)
| this `elem` map fst back = error $ "yesod-core: infinite recursion in breadcrumbs at " ++ show this
| otherwise = do
(title, next) <- breadcrumb this (title, next) <- breadcrumb this
go ((this, title) : back) next go ((this, title) : back) next

View File

@ -0,0 +1,52 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
module Yesod.Core.Class.Dispatch where
import qualified Network.Wai as W
import Yesod.Core.Types
import Yesod.Core.Content (ToTypedContent (..))
import Yesod.Core.Handler (sendWaiApplication)
import Yesod.Core.Class.Yesod
-- | This class is automatically instantiated when you use the template haskell
-- mkYesod function. You should never need to deal with it directly.
class Yesod site => YesodDispatch site where
yesodDispatch :: YesodRunnerEnv site -> W.Application
class YesodSubDispatch sub master where
yesodSubDispatch :: YesodSubRunnerEnv sub master -> W.Application
instance YesodSubDispatch WaiSubsite master where
yesodSubDispatch YesodSubRunnerEnv {..} = app
where
WaiSubsite app = ysreGetSub $ yreSite ysreParentEnv
instance YesodSubDispatch WaiSubsiteWithAuth master where
yesodSubDispatch YesodSubRunnerEnv {..} req =
ysreParentRunner handlert ysreParentEnv (fmap ysreToParentRoute route) req
where
route = Just $ WaiSubsiteWithAuthRoute (W.pathInfo req) []
WaiSubsiteWithAuth set = ysreGetSub $ yreSite $ ysreParentEnv
handlert = sendWaiApplication set
subHelper
:: ToTypedContent content
=> SubHandlerFor child master content
-> YesodSubRunnerEnv child master
-> Maybe (Route child)
-> W.Application
subHelper (SubHandlerFor f) YesodSubRunnerEnv {..} mroute =
ysreParentRunner handler ysreParentEnv (fmap ysreToParentRoute mroute)
where
handler = fmap toTypedContent $ HandlerFor $ \hd ->
let rhe = handlerEnv hd
rhe' = rhe
{ rheRoute = mroute
, rheChild = ysreGetSub $ yreSite ysreParentEnv
, rheRouteToMaster = ysreToParentRoute
}
in f hd { handlerEnv = rhe' }

View File

@ -0,0 +1,126 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
module Yesod.Core.Class.Handler
( MonadHandler (..)
, MonadWidget (..)
, liftHandlerT
, liftWidgetT
) where
import Yesod.Core.Types
import Control.Monad.Logger (MonadLogger)
import Control.Monad.Trans.Resource (MonadResource)
import Control.Monad.Trans.Class (lift)
import Data.Conduit.Internal (Pipe, ConduitM)
import Control.Monad.Trans.Identity ( IdentityT)
#if !MIN_VERSION_transformers(0,6,0)
import Control.Monad.Trans.List ( ListT )
#endif
import Control.Monad.Trans.Maybe ( MaybeT )
import Control.Monad.Trans.Except ( ExceptT )
import Control.Monad.Trans.Reader ( ReaderT )
import Control.Monad.Trans.State ( StateT )
import Control.Monad.Trans.Writer ( WriterT )
import Control.Monad.Trans.RWS ( RWST )
import qualified Control.Monad.Trans.RWS.Strict as Strict ( RWST )
import qualified Control.Monad.Trans.State.Strict as Strict ( StateT )
import qualified Control.Monad.Trans.Writer.Strict as Strict ( WriterT )
-- FIXME should we just use MonadReader instances instead?
class (MonadResource m, MonadLogger m) => MonadHandler m where
type HandlerSite m
type SubHandlerSite m
liftHandler :: HandlerFor (HandlerSite m) a -> m a
liftSubHandler :: SubHandlerFor (SubHandlerSite m) (HandlerSite m) a -> m a
liftHandlerT :: MonadHandler m => HandlerFor (HandlerSite m) a -> m a
liftHandlerT = liftHandler
{-# DEPRECATED liftHandlerT "Use liftHandler instead" #-}
instance MonadHandler (HandlerFor site) where
type HandlerSite (HandlerFor site) = site
type SubHandlerSite (HandlerFor site) = site
liftHandler = id
{-# INLINE liftHandler #-}
liftSubHandler (SubHandlerFor f) = HandlerFor f
{-# INLINE liftSubHandler #-}
instance MonadHandler (SubHandlerFor sub master) where
type HandlerSite (SubHandlerFor sub master) = master
type SubHandlerSite (SubHandlerFor sub master) = sub
liftHandler (HandlerFor f) = SubHandlerFor $ \hd -> f hd
{ handlerEnv =
let rhe = handlerEnv hd
in rhe
{ rheRoute = fmap (rheRouteToMaster rhe) (rheRoute rhe)
, rheRouteToMaster = id
, rheChild = rheSite rhe
}
}
{-# INLINE liftHandler #-}
liftSubHandler = id
{-# INLINE liftSubHandler #-}
instance MonadHandler (WidgetFor site) where
type HandlerSite (WidgetFor site) = site
type SubHandlerSite (WidgetFor site) = site
liftHandler (HandlerFor f) = WidgetFor $ f . wdHandler
{-# INLINE liftHandler #-}
liftSubHandler (SubHandlerFor f) = WidgetFor $ f . wdHandler
{-# INLINE liftSubHandler #-}
#define GO(T) instance MonadHandler m => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; type SubHandlerSite (T m) = SubHandlerSite m; liftHandler = lift . liftHandler; liftSubHandler = lift . liftSubHandler
#define GOX(X, T) instance (X, MonadHandler m) => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; type SubHandlerSite (T m) = SubHandlerSite m; liftHandler = lift . liftHandler; liftSubHandler = lift . liftSubHandler
GO(IdentityT)
#if !MIN_VERSION_transformers(0,6,0)
GO(ListT)
#endif
GO(MaybeT)
GO(ExceptT e)
GO(ReaderT r)
GO(StateT s)
GOX(Monoid w, WriterT w)
GOX(Monoid w, RWST r w s)
GOX(Monoid w, Strict.RWST r w s)
GO(Strict.StateT s)
GOX(Monoid w, Strict.WriterT w)
GO(Pipe l i o u)
GO(ConduitM i o)
#undef GO
#undef GOX
class MonadHandler m => MonadWidget m where
liftWidget :: WidgetFor (HandlerSite m) a -> m a
instance MonadWidget (WidgetFor site) where
liftWidget = id
{-# INLINE liftWidget #-}
liftWidgetT :: MonadWidget m => WidgetFor (HandlerSite m) a -> m a
liftWidgetT = liftWidget
{-# DEPRECATED liftWidgetT "Use liftWidget instead" #-}
#define GO(T) instance MonadWidget m => MonadWidget (T m) where liftWidget = lift . liftWidget
#define GOX(X, T) instance (X, MonadWidget m) => MonadWidget (T m) where liftWidget = lift . liftWidget
GO(IdentityT)
#if !MIN_VERSION_transformers(0,6,0)
GO(ListT)
#endif
GO(MaybeT)
GO(ExceptT e)
GO(ReaderT r)
GO(StateT s)
GOX(Monoid w, WriterT w)
GOX(Monoid w, RWST r w s)
GOX(Monoid w, Strict.RWST r w s)
GO(Strict.StateT s)
GOX(Monoid w, Strict.WriterT w)
GO(Pipe l i o u)
GO(ConduitM i o)
#undef GO
#undef GOX

View File

@ -2,7 +2,8 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE ScopedTypeVariables #-}
module Yesod.Core.Class.Yesod where module Yesod.Core.Class.Yesod where
import Yesod.Core.Content import Yesod.Core.Content
@ -10,14 +11,10 @@ import Yesod.Core.Handler
import Yesod.Routes.Class import Yesod.Routes.Class
import Blaze.ByteString.Builder (Builder, toByteString) import Data.ByteString.Builder (Builder)
import Blaze.ByteString.Builder.ByteString (copyByteString) import Data.Text.Encoding (encodeUtf8Builder)
import Blaze.ByteString.Builder.Char.Utf8 (fromText, fromChar)
import Control.Arrow ((***), second) import Control.Arrow ((***), second)
import Control.Exception (bracket) import Control.Exception (bracket)
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
import Control.Monad (forM, when, void) import Control.Monad (forM, when, void)
import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Logger (LogLevel (LevelInfo, LevelOther), import Control.Monad.Logger (LogLevel (LevelInfo, LevelOther),
@ -28,6 +25,7 @@ import qualified Data.ByteString.Lazy as L
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import Data.List (foldl', nub) import Data.List (foldl', nub)
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Maybe (catMaybes)
import Data.Monoid import Data.Monoid
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
@ -37,9 +35,8 @@ import Data.Text.Lazy.Builder (toLazyText)
import Data.Text.Lazy.Encoding (encodeUtf8) import Data.Text.Lazy.Encoding (encodeUtf8)
import Data.Word (Word64) import Data.Word (Word64)
import Language.Haskell.TH.Syntax (Loc (..)) import Language.Haskell.TH.Syntax (Loc (..))
import Network.HTTP.Types (encodePath, renderQueryText) import Network.HTTP.Types (encodePath)
import qualified Network.Wai as W import qualified Network.Wai as W
import Data.Default (def)
import Network.Wai.Parse (lbsBackEnd, import Network.Wai.Parse (lbsBackEnd,
tempFileBackEnd) tempFileBackEnd)
import Network.Wai.Logger (ZonedDate, clockDateCacher) import Network.Wai.Logger (ZonedDate, clockDateCacher)
@ -52,13 +49,15 @@ import Text.Hamlet
import Text.Julius import Text.Julius
import qualified Web.ClientSession as CS import qualified Web.ClientSession as CS
import Web.Cookie (SetCookie (..), parseCookies, sameSiteLax, import Web.Cookie (SetCookie (..), parseCookies, sameSiteLax,
sameSiteStrict, SameSiteOption) sameSiteStrict, SameSiteOption, defaultSetCookie)
import Yesod.Core.Types import Yesod.Core.Types
import Yesod.Core.Internal.Session import Yesod.Core.Internal.Session
import Yesod.Core.Widget import Yesod.Core.Widget
import Control.Monad.Trans.Class (lift)
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai.Request import qualified Network.Wai.Request
import Data.IORef
import UnliftIO (SomeException, catch, MonadUnliftIO)
-- | Define settings for a Yesod applications. All methods have intelligent -- | Define settings for a Yesod applications. All methods have intelligent
-- defaults, and therefore no implementation is required. -- defaults, and therefore no implementation is required.
@ -66,27 +65,33 @@ class RenderRoute site => Yesod site where
-- | An absolute URL to the root of the application. Do not include -- | An absolute URL to the root of the application. Do not include
-- trailing slash. -- trailing slash.
-- --
-- Default value: 'ApprootRelative'. This is valid under the following -- Default value: 'guessApproot'. If you know your application root
-- conditions: -- statically, it will be more efficient and more reliable to instead use
-- 'ApprootStatic' or 'ApprootMaster'. If you do not need full absolute
-- URLs, you can use 'ApprootRelative' instead.
-- --
-- * Your application is served from the root of the domain. -- Note: Prior to yesod-core 1.5, the default value was 'ApprootRelative'.
--
-- * You do not use any features that require absolute URLs, such as Atom
-- feeds and XML sitemaps.
--
-- If this is not true, you should override with a different
-- implementation.
approot :: Approot site approot :: Approot site
approot = ApprootRelative approot = guessApproot
-- | @since 1.6.24.0
-- allows the user to specify how exceptions are cought.
-- by default all async exceptions are thrown and synchronous
-- exceptions render a 500 page.
-- To catch all exceptions (even async) to render a 500 page,
-- set this to 'UnliftIO.Exception.catchSyncOrAsync'. Beware
-- this may have negative effects with functions like 'timeout'.
catchHandlerExceptions :: MonadUnliftIO m => site -> m a -> (SomeException -> m a) -> m a
catchHandlerExceptions _ = catch
-- | Output error response pages. -- | Output error response pages.
-- --
-- Default value: 'defaultErrorHandler'. -- Default value: 'defaultErrorHandler'.
errorHandler :: ErrorResponse -> HandlerT site IO TypedContent errorHandler :: ErrorResponse -> HandlerFor site TypedContent
errorHandler = defaultErrorHandler errorHandler = defaultErrorHandler
-- | Applies some form of layout to the contents of a page. -- | Applies some form of layout to the contents of a page.
defaultLayout :: WidgetT site IO () -> HandlerT site IO Html defaultLayout :: WidgetFor site () -> HandlerFor site Html
defaultLayout w = do defaultLayout w = do
p <- widgetToPageContent w p <- widgetToPageContent w
msgs <- getMessages msgs <- getMessages
@ -96,6 +101,8 @@ class RenderRoute site => Yesod site where
<html> <html>
<head> <head>
<title>#{pageTitle p} <title>#{pageTitle p}
$maybe description <- pageDescription p
<meta name="description" content="#{description}">
^{pageHead p} ^{pageHead p}
<body> <body>
$forall (status, msg) <- msgs $forall (status, msg) <- msgs
@ -103,12 +110,6 @@ class RenderRoute site => Yesod site where
^{pageBody p} ^{pageBody p}
|] |]
-- | Override the rendering function for a particular URL. One use case for
-- this is to offload static hosting to a different domain name to avoid
-- sending cookies.
urlRenderOverride :: site -> Route site -> Maybe Builder
urlRenderOverride _ _ = Nothing
-- | Override the rendering function for a particular URL and query string -- | Override the rendering function for a particular URL and query string
-- parameters. One use case for this is to offload static hosting to a -- parameters. One use case for this is to offload static hosting to a
-- different domain name to avoid sending cookies. -- different domain name to avoid sending cookies.
@ -121,15 +122,7 @@ class RenderRoute site => Yesod site where
-> Route site -> Route site
-> [(T.Text, T.Text)] -- ^ query string -> [(T.Text, T.Text)] -- ^ query string
-> Maybe Builder -> Maybe Builder
urlParamRenderOverride y route params = addParams params <$> urlRenderOverride y route urlParamRenderOverride _ _ _ = Nothing
where
addParams [] routeBldr = routeBldr
addParams nonEmptyParams routeBldr =
let routeBS = toByteString routeBldr
qsSeparator = fromChar $ if S8.elem '?' routeBS then '&' else '?'
valueToMaybe t = if t == "" then Nothing else Just t
queryText = map (id *** valueToMaybe) nonEmptyParams
in copyByteString routeBS `mappend` qsSeparator `mappend` renderQueryText False queryText
-- | Determine if a request is authorized or not. -- | Determine if a request is authorized or not.
-- --
@ -138,7 +131,7 @@ class RenderRoute site => Yesod site where
-- If authentication is required, return 'AuthenticationRequired'. -- If authentication is required, return 'AuthenticationRequired'.
isAuthorized :: Route site isAuthorized :: Route site
-> Bool -- ^ is this a write request? -> Bool -- ^ is this a write request?
-> HandlerT site IO AuthResult -> HandlerFor site AuthResult
isAuthorized _ _ = return Authorized isAuthorized _ _ = return Authorized
-- | Determines whether the current request is a write request. By default, -- | Determines whether the current request is a write request. By default,
@ -148,7 +141,7 @@ class RenderRoute site => Yesod site where
-- --
-- This function is used to determine if a request is authorized; see -- This function is used to determine if a request is authorized; see
-- 'isAuthorized'. -- 'isAuthorized'.
isWriteRequest :: Route site -> HandlerT site IO Bool isWriteRequest :: Route site -> HandlerFor site Bool
isWriteRequest _ = do isWriteRequest _ = do
wai <- waiRequest wai <- waiRequest
return $ W.requestMethod wai `notElem` return $ W.requestMethod wai `notElem`
@ -191,7 +184,7 @@ class RenderRoute site => Yesod site where
-> [(T.Text, T.Text)] -- ^ query string -> [(T.Text, T.Text)] -- ^ query string
-> Builder -> Builder
joinPath _ ar pieces' qs' = joinPath _ ar pieces' qs' =
fromText ar `mappend` encodePath pieces qs encodeUtf8Builder ar `mappend` encodePath pieces qs
where where
pieces = if null pieces' then [""] else map addDash pieces' pieces = if null pieces' then [""] else map addDash pieces'
qs = map (TE.encodeUtf8 *** go) qs' qs = map (TE.encodeUtf8 *** go) qs'
@ -214,10 +207,11 @@ class RenderRoute site => Yesod site where
addStaticContent :: Text -- ^ filename extension addStaticContent :: Text -- ^ filename extension
-> Text -- ^ mime-type -> Text -- ^ mime-type
-> L.ByteString -- ^ content -> L.ByteString -- ^ content
-> HandlerT site IO (Maybe (Either Text (Route site, [(Text, Text)]))) -> HandlerFor site (Maybe (Either Text (Route site, [(Text, Text)])))
addStaticContent _ _ _ = return Nothing addStaticContent _ _ _ = return Nothing
-- | Maximum allowed length of the request body, in bytes. -- | Maximum allowed length of the request body, in bytes.
-- This method may be ignored if 'maximumContentLengthIO' is overridden.
-- --
-- If @Nothing@, no maximum is applied. -- If @Nothing@, no maximum is applied.
-- --
@ -225,6 +219,18 @@ class RenderRoute site => Yesod site where
maximumContentLength :: site -> Maybe (Route site) -> Maybe Word64 maximumContentLength :: site -> Maybe (Route site) -> Maybe Word64
maximumContentLength _ _ = Just $ 2 * 1024 * 1024 -- 2 megabytes maximumContentLength _ _ = Just $ 2 * 1024 * 1024 -- 2 megabytes
-- | Maximum allowed length of the request body, in bytes. This is similar
-- to 'maximumContentLength', but the result lives in @IO@. This allows
-- you to dynamically change the maximum file size based on some external
-- source like a database or an @IORef@.
--
-- The default implementation uses 'maximumContentLength'. Future version of yesod will
-- remove 'maximumContentLength' and use this method exclusively.
--
-- @since 1.6.13
maximumContentLengthIO :: site -> Maybe (Route site) -> IO (Maybe Word64)
maximumContentLengthIO a b = pure $ maximumContentLength a b
-- | Creates a @Logger@ to use for log messages. -- | Creates a @Logger@ to use for log messages.
-- --
-- Note that a common technique (endorsed by the scaffolding) is to create -- Note that a common technique (endorsed by the scaffolding) is to create
@ -254,6 +260,21 @@ class RenderRoute site => Yesod site where
jsLoader :: site -> ScriptLoadPosition site jsLoader :: site -> ScriptLoadPosition site
jsLoader _ = BottomOfBody jsLoader _ = BottomOfBody
-- | Default attributes to put on the JavaScript <script> tag
-- generated for julius files
jsAttributes :: site -> [(Text, Text)]
jsAttributes _ = []
-- | Same as @jsAttributes@ but allows you to run arbitrary Handler code
--
-- This is useful if you need to add a randomised nonce value to the script
-- tag generated by @widgetFile@. If this function is overridden then
-- @jsAttributes@ is ignored.
--
-- @since 1.6.16
jsAttributesHandler :: HandlerFor site [(Text, Text)]
jsAttributesHandler = jsAttributes <$> getYesod
-- | Create a session backend. Returning 'Nothing' disables -- | Create a session backend. Returning 'Nothing' disables
-- sessions. If you'd like to change the way that the session -- sessions. If you'd like to change the way that the session
-- cookies are created, take a look at -- cookies are created, take a look at
@ -275,22 +296,11 @@ class RenderRoute site => Yesod site where
-- | Should we log the given log source/level combination. -- | Should we log the given log source/level combination.
-- --
-- Default: the 'defaultShouldLog' function. -- Default: the 'defaultShouldLogIO' function.
shouldLog :: site -> LogSource -> LogLevel -> Bool
shouldLog _ = defaultShouldLog
-- | Should we log the given log source/level combination.
--
-- Note that this is almost identical to @shouldLog@, except the result
-- lives in @IO@. This allows you to dynamically alter the logging level of
-- your application by having this result depend on, e.g., an @IORef@.
--
-- The default implementation simply uses @shouldLog@. Future versions of
-- Yesod will remove @shouldLog@ and use this method exclusively.
-- --
-- Since 1.2.4 -- Since 1.2.4
shouldLogIO :: site -> LogSource -> LogLevel -> IO Bool shouldLogIO :: site -> LogSource -> LogLevel -> IO Bool
shouldLogIO a b c = return (shouldLog a b c) shouldLogIO _ = defaultShouldLogIO
-- | A Yesod middleware, which will wrap every handler function. This -- | A Yesod middleware, which will wrap every handler function. This
-- allows you to run code before and after a normal handler. -- allows you to run code before and after a normal handler.
@ -298,7 +308,7 @@ class RenderRoute site => Yesod site where
-- Default: the 'defaultYesodMiddleware' function. -- Default: the 'defaultYesodMiddleware' function.
-- --
-- Since: 1.1.6 -- Since: 1.1.6
yesodMiddleware :: ToTypedContent res => HandlerT site IO res -> HandlerT site IO res yesodMiddleware :: ToTypedContent res => HandlerFor site res -> HandlerFor site res
yesodMiddleware = defaultYesodMiddleware yesodMiddleware = defaultYesodMiddleware
-- | How to allocate an @InternalState@ for each request. -- | How to allocate an @InternalState@ for each request.
@ -314,7 +324,19 @@ class RenderRoute site => Yesod site where
yesodWithInternalState :: site -> Maybe (Route site) -> (InternalState -> IO a) -> IO a yesodWithInternalState :: site -> Maybe (Route site) -> (InternalState -> IO a) -> IO a
yesodWithInternalState _ _ = bracket createInternalState closeInternalState yesodWithInternalState _ _ = bracket createInternalState closeInternalState
{-# INLINE yesodWithInternalState #-} {-# INLINE yesodWithInternalState #-}
{-# DEPRECATED urlRenderOverride "Use urlParamRenderOverride instead" #-}
-- | Convert a title and HTML snippet into a 'Widget'. Used
-- primarily for wrapping up error messages for better display.
--
-- @since 1.4.30
defaultMessageWidget :: Html -> HtmlUrl (Route site) -> WidgetFor site ()
defaultMessageWidget title body = do
setTitle title
toWidget
[hamlet|
<h1>#{title}
^{body}
|]
-- | Default implementation of 'makeLogger'. Sends to stdout and -- | Default implementation of 'makeLogger'. Sends to stdout and
-- automatically flushes on each write. -- automatically flushes on each write.
@ -351,23 +373,18 @@ defaultMessageLoggerSource ckLoggable logger loc source level msg = do
-- above 'LevelInfo'. -- above 'LevelInfo'.
-- --
-- Since 1.4.10 -- Since 1.4.10
defaultShouldLog :: LogSource -> LogLevel -> Bool
defaultShouldLog _ level = level >= LevelInfo
-- | A default implementation of 'shouldLogIO' that can be used with
-- 'defaultMessageLoggerSource'. Just uses 'defaultShouldLog'.
--
-- Since 1.4.10
defaultShouldLogIO :: LogSource -> LogLevel -> IO Bool defaultShouldLogIO :: LogSource -> LogLevel -> IO Bool
defaultShouldLogIO a b = return $ defaultShouldLog a b defaultShouldLogIO _ level = return $ level >= LevelInfo
-- | Default implementation of 'yesodMiddleware'. Adds the response header -- | Default implementation of 'yesodMiddleware'. Adds the response header
-- \"Vary: Accept, Accept-Language\" and performs authorization checks. -- \"Vary: Accept, Accept-Language\", \"X-XSS-Protection: 1; mode=block\", and
-- performs authorization checks.
-- --
-- Since 1.2.0 -- Since 1.2.0
defaultYesodMiddleware :: Yesod site => HandlerT site IO res -> HandlerT site IO res defaultYesodMiddleware :: Yesod site => HandlerFor site res -> HandlerFor site res
defaultYesodMiddleware handler = do defaultYesodMiddleware handler = do
addHeader "Vary" "Accept, Accept-Language" addHeader "Vary" "Accept, Accept-Language"
addHeader "X-XSS-Protection" "1; mode=block"
authorizationCheck authorizationCheck
handler handler
@ -424,10 +441,9 @@ sameSiteSession s = (fmap . fmap) secureSessionCookies
-- headers are ignored over HTTP. -- headers are ignored over HTTP.
-- --
-- Since 1.4.7 -- Since 1.4.7
sslOnlyMiddleware :: Yesod site sslOnlyMiddleware :: Int -- ^ minutes
=> Int -- ^ minutes -> HandlerFor site res
-> HandlerT site IO res -> HandlerFor site res
-> HandlerT site IO res
sslOnlyMiddleware timeout handler = do sslOnlyMiddleware timeout handler = do
addHeader "Strict-Transport-Security" addHeader "Strict-Transport-Security"
$ T.pack $ concat [ "max-age=" $ T.pack $ concat [ "max-age="
@ -440,7 +456,7 @@ sslOnlyMiddleware timeout handler = do
-- 'isWriteRequest'. -- 'isWriteRequest'.
-- --
-- Since 1.2.0 -- Since 1.2.0
authorizationCheck :: Yesod site => HandlerT site IO () authorizationCheck :: Yesod site => HandlerFor site ()
authorizationCheck = getCurrentRoute >>= maybe (return ()) checkUrl authorizationCheck = getCurrentRoute >>= maybe (return ()) checkUrl
where where
checkUrl url = do checkUrl url = do
@ -464,7 +480,7 @@ authorizationCheck = getCurrentRoute >>= maybe (return ()) checkUrl
-- | Calls 'csrfCheckMiddleware' with 'isWriteRequest', 'defaultCsrfHeaderName', and 'defaultCsrfParamName' as parameters. -- | Calls 'csrfCheckMiddleware' with 'isWriteRequest', 'defaultCsrfHeaderName', and 'defaultCsrfParamName' as parameters.
-- --
-- Since 1.4.14 -- Since 1.4.14
defaultCsrfCheckMiddleware :: Yesod site => HandlerT site IO res -> HandlerT site IO res defaultCsrfCheckMiddleware :: Yesod site => HandlerFor site res -> HandlerFor site res
defaultCsrfCheckMiddleware handler = defaultCsrfCheckMiddleware handler =
csrfCheckMiddleware csrfCheckMiddleware
handler handler
@ -478,12 +494,11 @@ defaultCsrfCheckMiddleware handler =
-- For details, see the "AJAX CSRF protection" section of "Yesod.Core.Handler". -- For details, see the "AJAX CSRF protection" section of "Yesod.Core.Handler".
-- --
-- Since 1.4.14 -- Since 1.4.14
csrfCheckMiddleware :: Yesod site csrfCheckMiddleware :: HandlerFor site res
=> HandlerT site IO res -> HandlerFor site Bool -- ^ Whether or not to perform the CSRF check.
-> HandlerT site IO Bool -- ^ Whether or not to perform the CSRF check.
-> CI S8.ByteString -- ^ The header name to lookup the CSRF token from. -> CI S8.ByteString -- ^ The header name to lookup the CSRF token from.
-> Text -- ^ The POST parameter name to lookup the CSRF token from. -> Text -- ^ The POST parameter name to lookup the CSRF token from.
-> HandlerT site IO res -> HandlerFor site res
csrfCheckMiddleware handler shouldCheckFn headerName paramName = do csrfCheckMiddleware handler shouldCheckFn headerName paramName = do
shouldCheck <- shouldCheckFn shouldCheck <- shouldCheckFn
when shouldCheck (checkCsrfHeaderOrParam headerName paramName) when shouldCheck (checkCsrfHeaderOrParam headerName paramName)
@ -494,7 +509,7 @@ csrfCheckMiddleware handler shouldCheckFn headerName paramName = do
-- The cookie's path is set to @/@, making it valid for your whole website. -- The cookie's path is set to @/@, making it valid for your whole website.
-- --
-- Since 1.4.14 -- Since 1.4.14
defaultCsrfSetCookieMiddleware :: Yesod site => HandlerT site IO res -> HandlerT site IO res defaultCsrfSetCookieMiddleware :: HandlerFor site res -> HandlerFor site res
defaultCsrfSetCookieMiddleware handler = setCsrfCookie >> handler defaultCsrfSetCookieMiddleware handler = setCsrfCookie >> handler
-- | Takes a 'SetCookie' and overrides its value with a CSRF token, then sets the cookie. See 'setCsrfCookieWithCookie'. -- | Takes a 'SetCookie' and overrides its value with a CSRF token, then sets the cookie. See 'setCsrfCookieWithCookie'.
@ -504,14 +519,14 @@ defaultCsrfSetCookieMiddleware handler = setCsrfCookie >> handler
-- Make sure to set the 'setCookiePath' to the root path of your application, otherwise you'll generate a new CSRF token for every path of your app. If your app is run from from e.g. www.example.com\/app1, use @app1@. The vast majority of sites will just use @/@. -- Make sure to set the 'setCookiePath' to the root path of your application, otherwise you'll generate a new CSRF token for every path of your app. If your app is run from from e.g. www.example.com\/app1, use @app1@. The vast majority of sites will just use @/@.
-- --
-- Since 1.4.14 -- Since 1.4.14
csrfSetCookieMiddleware :: Yesod site => HandlerT site IO res -> SetCookie -> HandlerT site IO res csrfSetCookieMiddleware :: HandlerFor site res -> SetCookie -> HandlerFor site res
csrfSetCookieMiddleware handler cookie = setCsrfCookieWithCookie cookie >> handler csrfSetCookieMiddleware handler cookie = setCsrfCookieWithCookie cookie >> handler
-- | Calls 'defaultCsrfSetCookieMiddleware' and 'defaultCsrfCheckMiddleware'. -- | Calls 'defaultCsrfSetCookieMiddleware' and 'defaultCsrfCheckMiddleware'.
-- --
-- For details, see the "AJAX CSRF protection" section of "Yesod.Core.Handler". -- For details, see the "AJAX CSRF protection" section of "Yesod.Core.Handler".
-- --
-- You can add this chain this middleware together with other middleware like so: -- You can chain this middleware together with other middleware like so:
-- --
-- @ -- @
-- 'yesodMiddleware' = 'defaultYesodMiddleware' . 'defaultCsrfMiddleware' -- 'yesodMiddleware' = 'defaultYesodMiddleware' . 'defaultCsrfMiddleware'
@ -524,21 +539,29 @@ csrfSetCookieMiddleware handler cookie = setCsrfCookieWithCookie cookie >> handl
-- @ -- @
-- --
-- Since 1.4.14 -- Since 1.4.14
defaultCsrfMiddleware :: Yesod site => HandlerT site IO res -> HandlerT site IO res defaultCsrfMiddleware :: Yesod site => HandlerFor site res -> HandlerFor site res
defaultCsrfMiddleware = defaultCsrfSetCookieMiddleware . defaultCsrfCheckMiddleware defaultCsrfMiddleware = defaultCsrfSetCookieMiddleware . defaultCsrfCheckMiddleware
-- | Convert a widget to a 'PageContent'. -- | Convert a widget to a 'PageContent'.
widgetToPageContent :: (Eq (Route site), Yesod site) widgetToPageContent :: Yesod site
=> WidgetT site IO () => WidgetFor site ()
-> HandlerT site IO (PageContent (Route site)) -> HandlerFor site (PageContent (Route site))
widgetToPageContent w = do widgetToPageContent w = do
master <- getYesod jsAttrs <- jsAttributesHandler
hd <- HandlerT return HandlerFor $ \hd -> do
((), GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head')) <- lift $ unWidgetT w hd master <- unHandlerFor getYesod hd
ref <- newIORef mempty
unWidgetFor w WidgetData
{ wdRef = ref
, wdHandler = hd
}
GWData (Body body) (Last mTitle) (Last mDescription) scripts' stylesheets' style jscript (Head head') <- readIORef ref
let title = maybe mempty unTitle mTitle let title = maybe mempty unTitle mTitle
description = unDescription <$> mDescription
scripts = runUniqueList scripts' scripts = runUniqueList scripts'
stylesheets = runUniqueList stylesheets' stylesheets = runUniqueList stylesheets'
flip unHandlerFor hd $ do
render <- getUrlRenderParams render <- getUrlRenderParams
let renderLoc x = let renderLoc x =
case x of case x of
@ -570,7 +593,7 @@ widgetToPageContent w = do
^{mkScriptTag s} ^{mkScriptTag s}
$maybe j <- jscript $maybe j <- jscript
$maybe s <- jsLoc $maybe s <- jsLoc
<script src="#{s}"> <script src="#{s}" *{jsAttrs}>
$nothing $nothing
<script>^{jelper j} <script>^{jelper j}
|] |]
@ -604,7 +627,7 @@ widgetToPageContent w = do
^{regularScriptLoad} ^{regularScriptLoad}
|] |]
return $ PageContent title headAll $ return $ PageContent title description headAll $
case jsLoader master of case jsLoader master of
BottomOfBody -> bodyScript BottomOfBody -> bodyScript
_ -> body _ -> body
@ -626,29 +649,23 @@ widgetToPageContent w = do
runUniqueList (UniqueList x) = nub $ x [] runUniqueList (UniqueList x) = nub $ x []
-- | The default error handler for 'errorHandler'. -- | The default error handler for 'errorHandler'.
defaultErrorHandler :: Yesod site => ErrorResponse -> HandlerT site IO TypedContent defaultErrorHandler :: Yesod site => ErrorResponse -> HandlerFor site TypedContent
defaultErrorHandler NotFound = selectRep $ do defaultErrorHandler NotFound = selectRep $ do
provideRep $ defaultLayout $ do provideRep $ defaultLayout $ do
r <- waiRequest r <- waiRequest
let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r
setTitle "Not Found" defaultMessageWidget "Not Found" [hamlet|<p>#{path'}|]
toWidget [hamlet|
<h1>Not Found
<p>#{path'}
|]
provideRep $ return $ object ["message" .= ("Not Found" :: Text)] provideRep $ return $ object ["message" .= ("Not Found" :: Text)]
provideRep $ return ("Not Found" :: Text)
-- For API requests. -- For API requests.
-- For a user with a browser, -- For a user with a browser,
-- if you specify an authRoute the user will be redirected there and -- if you specify an authRoute the user will be redirected there and
-- this page will not be shown. -- this page will not be shown.
defaultErrorHandler NotAuthenticated = selectRep $ do defaultErrorHandler NotAuthenticated = selectRep $ do
provideRep $ defaultLayout $ do provideRep $ defaultLayout $ defaultMessageWidget
setTitle "Not logged in" "Not logged in"
toWidget [hamlet| [hamlet|<p style="display:none;">Set the authRoute and the user will be redirected there.|]
<h1>Not logged in
<p style="display:none;">Set the authRoute and the user will be redirected there.
|]
provideRep $ do provideRep $ do
-- 401 *MUST* include a WWW-Authenticate header -- 401 *MUST* include a WWW-Authenticate header
@ -663,45 +680,42 @@ defaultErrorHandler NotAuthenticated = selectRep $ do
let apair u = ["authentication_url" .= rend u] let apair u = ["authentication_url" .= rend u]
content = maybe [] apair (authRoute site) content = maybe [] apair (authRoute site)
return $ object $ ("message" .= ("Not logged in"::Text)):content return $ object $ ("message" .= ("Not logged in"::Text)):content
provideRep $ return ("Not logged in" :: Text)
defaultErrorHandler (PermissionDenied msg) = selectRep $ do defaultErrorHandler (PermissionDenied msg) = selectRep $ do
provideRep $ defaultLayout $ do provideRep $ defaultLayout $ defaultMessageWidget
setTitle "Permission Denied" "Permission Denied"
toWidget [hamlet| [hamlet|<p>#{msg}|]
<h1>Permission denied
<p>#{msg}
|]
provideRep $ provideRep $
return $ object ["message" .= ("Permission Denied. " <> msg)] return $ object ["message" .= ("Permission Denied. " <> msg)]
provideRep $ return $ "Permission Denied. " <> msg
defaultErrorHandler (InvalidArgs ia) = selectRep $ do defaultErrorHandler (InvalidArgs ia) = selectRep $ do
provideRep $ defaultLayout $ do provideRep $ defaultLayout $ defaultMessageWidget
setTitle "Invalid Arguments" "Invalid Arguments"
toWidget [hamlet| [hamlet|
<h1>Invalid Arguments
<ul> <ul>
$forall msg <- ia $forall msg <- ia
<li>#{msg} <li>#{msg}
|] |]
provideRep $ return $ object ["message" .= ("Invalid Arguments" :: Text), "errors" .= ia] provideRep $ return $ object ["message" .= ("Invalid Arguments" :: Text), "errors" .= ia]
provideRep $ return ("Invalid Arguments: " <> T.intercalate " " ia)
defaultErrorHandler (InternalError e) = do defaultErrorHandler (InternalError e) = do
$logErrorS "yesod-core" e $logErrorS "yesod-core" e
selectRep $ do selectRep $ do
provideRep $ defaultLayout $ do provideRep $ defaultLayout $ defaultMessageWidget
setTitle "Internal Server Error" "Internal Server Error"
toWidget [hamlet| [hamlet|<pre>#{e}|]
<h1>Internal Server Error
<pre>#{e}
|]
provideRep $ return $ object ["message" .= ("Internal Server Error" :: Text), "error" .= e] provideRep $ return $ object ["message" .= ("Internal Server Error" :: Text), "error" .= e]
provideRep $ return $ "Internal Server Error: " <> e
defaultErrorHandler (BadMethod m) = selectRep $ do defaultErrorHandler (BadMethod m) = selectRep $ do
provideRep $ defaultLayout $ do provideRep $ defaultLayout $ defaultMessageWidget
setTitle"Bad Method" "Method Not Supported"
toWidget [hamlet| [hamlet|<p>Method <code>#{S8.unpack m}</code> not supported|]
<h1>Method Not Supported
<p>Method <code>#{S8.unpack m}</code> not supported
|]
provideRep $ return $ object ["message" .= ("Bad method" :: Text), "method" .= TE.decodeUtf8With TEE.lenientDecode m] provideRep $ return $ object ["message" .= ("Bad method" :: Text), "method" .= TE.decodeUtf8With TEE.lenientDecode m]
provideRep $ return $ "Bad Method " <> TE.decodeUtf8With TEE.lenientDecode m
asyncHelper :: (url -> [x] -> Text) asyncHelper :: (url -> [x] -> Text)
-> [Script url] -> [Script url]
@ -849,6 +863,12 @@ clientSessionBackend key getCachedDate =
sbLoadSession = loadClientSession key getCachedDate "_SESSION" sbLoadSession = loadClientSession key getCachedDate "_SESSION"
} }
justSingleton :: a -> [Maybe a] -> a
justSingleton d = just . catMaybes
where
just [s] = s
just _ = d
loadClientSession :: CS.Key loadClientSession :: CS.Key
-> IO ClientSessionDateCache -- ^ See 'clientSessionDateCacher' -> IO ClientSessionDateCache -- ^ See 'clientSessionDateCacher'
-> S8.ByteString -- ^ session name -> S8.ByteString -- ^ session name
@ -859,15 +879,15 @@ loadClientSession key getCachedDate sessionName req = load
load = do load = do
date <- getCachedDate date <- getCachedDate
return (sess date, save date) return (sess date, save date)
sess date = Map.unions $ do sess date = justSingleton Map.empty $ do
raw <- [v | (k, v) <- W.requestHeaders req, k == "Cookie"] raw <- [v | (k, v) <- W.requestHeaders req, k == "Cookie"]
val <- [v | (k, v) <- parseCookies raw, k == sessionName] val <- [v | (k, v) <- parseCookies raw, k == sessionName]
let host = "" -- fixme, properly lock sessions to client address let host = "" -- fixme, properly lock sessions to client address
maybe [] return $ decodeClientSession key date host val return $ decodeClientSession key date host val
save date sess' = do save date sess' = do
-- We should never cache the IV! Be careful! -- We should never cache the IV! Be careful!
iv <- liftIO CS.randomIV iv <- liftIO CS.randomIV
return [AddCookie def return [AddCookie defaultSetCookie
{ setCookieName = sessionName { setCookieName = sessionName
, setCookieValue = encodeClientSession key iv date host sess' , setCookieValue = encodeClientSession key iv date host sess'
, setCookiePath = Just "/" , setCookiePath = Just "/"

View File

@ -4,7 +4,6 @@
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE Rank2Types #-} {-# LANGUAGE Rank2Types #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
module Yesod.Core.Content module Yesod.Core.Content
( -- * Content ( -- * Content
Content (..) Content (..)
@ -53,30 +52,24 @@ import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import Data.Text.Lazy (Text, pack) import Data.Text.Lazy (Text, pack)
import qualified Data.Text as T import qualified Data.Text as T
import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString) import Data.Text.Encoding (encodeUtf8Builder)
#if __GLASGOW_HASKELL__ < 710 import qualified Data.Text.Lazy as TL
import Data.Monoid (mempty) import Data.ByteString.Builder (Builder, byteString, lazyByteString, stringUtf8)
#endif
import Text.Hamlet (Html) import Text.Hamlet (Html)
import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder) import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
import Data.Conduit (Source, Flush (Chunk), ResumableSource, mapOutput) import Data.Conduit (Flush (Chunk), SealedConduitT, mapOutput)
import Control.Monad (liftM) import Control.Monad (liftM)
import Control.Monad.Trans.Resource (ResourceT) import Control.Monad.Trans.Resource (ResourceT)
import Data.Conduit.Internal (ResumableSource (ResumableSource))
import qualified Data.Conduit.Internal as CI import qualified Data.Conduit.Internal as CI
import qualified Data.Aeson as J import qualified Data.Aeson as J
#if MIN_VERSION_aeson(0, 7, 0)
import Data.Aeson.Encode (encodeToTextBuilder)
#else
import Data.Aeson.Encode (fromValue)
#endif
import qualified Blaze.ByteString.Builder.Char.Utf8 as Blaze
import Data.Text.Lazy.Builder (toLazyText) import Data.Text.Lazy.Builder (toLazyText)
import Data.Void (Void, absurd)
import Yesod.Core.Types import Yesod.Core.Types
import Text.Lucius (Css, renderCss) import Text.Lucius (Css, renderCss)
import Text.Julius (Javascript, unJavascript) import Text.Julius (Javascript, unJavascript)
import Data.Word8 (_semicolon, _slash) import Data.Word8 (_semicolon, _slash)
import Control.Arrow (second)
-- | Zero-length enumerator. -- | Zero-length enumerator.
emptyContent :: Content emptyContent :: Content
@ -98,23 +91,27 @@ instance ToContent Content where
instance ToContent Builder where instance ToContent Builder where
toContent = flip ContentBuilder Nothing toContent = flip ContentBuilder Nothing
instance ToContent B.ByteString where instance ToContent B.ByteString where
toContent bs = ContentBuilder (fromByteString bs) $ Just $ B.length bs toContent bs = ContentBuilder (byteString bs) $ Just $ B.length bs
instance ToContent L.ByteString where instance ToContent L.ByteString where
toContent = flip ContentBuilder Nothing . fromLazyByteString toContent = flip ContentBuilder Nothing . lazyByteString
instance ToContent T.Text where instance ToContent T.Text where
toContent = toContent . Blaze.fromText toContent = toContent . encodeUtf8Builder
instance ToContent Text where instance ToContent Text where
toContent = toContent . Blaze.fromLazyText toContent = toContent . foldMap encodeUtf8Builder . TL.toChunks
instance ToContent String where instance ToContent String where
toContent = toContent . Blaze.fromString toContent = toContent . stringUtf8
instance ToContent Html where instance ToContent Html where
toContent bs = ContentBuilder (renderHtmlBuilder bs) Nothing toContent bs = ContentBuilder (renderHtmlBuilder bs) Nothing
instance ToContent () where instance ToContent () where
toContent () = toContent B.empty toContent () = toContent B.empty
instance ToContent Void where
toContent = absurd
instance ToContent (ContentType, Content) where instance ToContent (ContentType, Content) where
toContent = snd toContent = snd
instance ToContent TypedContent where instance ToContent TypedContent where
toContent (TypedContent _ c) = c toContent (TypedContent _ c) = c
instance ToContent (JSONResponse a) where
toContent (JSONResponse a) = toContent $ J.toEncoding a
instance ToContent Css where instance ToContent Css where
toContent = toContent . renderCss toContent = toContent . renderCss
@ -122,12 +119,12 @@ instance ToContent Javascript where
toContent = toContent . toLazyText . unJavascript toContent = toContent . toLazyText . unJavascript
instance ToFlushBuilder builder => ToContent (CI.Pipe () () builder () (ResourceT IO) ()) where instance ToFlushBuilder builder => ToContent (CI.Pipe () () builder () (ResourceT IO) ()) where
toContent src = ContentSource $ CI.ConduitM (CI.mapOutput toFlushBuilder src >>=) toContent src = ContentSource $ CI.ConduitT (CI.mapOutput toFlushBuilder src >>=)
instance ToFlushBuilder builder => ToContent (Source (ResourceT IO) builder) where instance ToFlushBuilder builder => ToContent (CI.ConduitT () builder (ResourceT IO) ()) where
toContent src = ContentSource $ mapOutput toFlushBuilder src toContent src = ContentSource $ mapOutput toFlushBuilder src
instance ToFlushBuilder builder => ToContent (ResumableSource (ResourceT IO) builder) where instance ToFlushBuilder builder => ToContent (SealedConduitT () builder (ResourceT IO) ()) where
toContent (ResumableSource src _) = toContent src toContent (CI.SealedConduitT src) = toContent src
-- | A class for all data which can be sent in a streaming response. Note that -- | A class for all data which can be sent in a streaming response. Note that
-- for textual data, instances must use UTF-8 encoding. -- for textual data, instances must use UTF-8 encoding.
@ -136,16 +133,16 @@ instance ToFlushBuilder builder => ToContent (ResumableSource (ResourceT IO) bui
class ToFlushBuilder a where toFlushBuilder :: a -> Flush Builder class ToFlushBuilder a where toFlushBuilder :: a -> Flush Builder
instance ToFlushBuilder (Flush Builder) where toFlushBuilder = id instance ToFlushBuilder (Flush Builder) where toFlushBuilder = id
instance ToFlushBuilder Builder where toFlushBuilder = Chunk instance ToFlushBuilder Builder where toFlushBuilder = Chunk
instance ToFlushBuilder (Flush B.ByteString) where toFlushBuilder = fmap fromByteString instance ToFlushBuilder (Flush B.ByteString) where toFlushBuilder = fmap byteString
instance ToFlushBuilder B.ByteString where toFlushBuilder = Chunk . fromByteString instance ToFlushBuilder B.ByteString where toFlushBuilder = Chunk . byteString
instance ToFlushBuilder (Flush L.ByteString) where toFlushBuilder = fmap fromLazyByteString instance ToFlushBuilder (Flush L.ByteString) where toFlushBuilder = fmap lazyByteString
instance ToFlushBuilder L.ByteString where toFlushBuilder = Chunk . fromLazyByteString instance ToFlushBuilder L.ByteString where toFlushBuilder = Chunk . lazyByteString
instance ToFlushBuilder (Flush Text) where toFlushBuilder = fmap Blaze.fromLazyText instance ToFlushBuilder (Flush Text) where toFlushBuilder = fmap (foldMap encodeUtf8Builder . TL.toChunks)
instance ToFlushBuilder Text where toFlushBuilder = Chunk . Blaze.fromLazyText instance ToFlushBuilder Text where toFlushBuilder = Chunk . foldMap encodeUtf8Builder . TL.toChunks
instance ToFlushBuilder (Flush T.Text) where toFlushBuilder = fmap Blaze.fromText instance ToFlushBuilder (Flush T.Text) where toFlushBuilder = fmap encodeUtf8Builder
instance ToFlushBuilder T.Text where toFlushBuilder = Chunk . Blaze.fromText instance ToFlushBuilder T.Text where toFlushBuilder = Chunk . encodeUtf8Builder
instance ToFlushBuilder (Flush String) where toFlushBuilder = fmap Blaze.fromString instance ToFlushBuilder (Flush String) where toFlushBuilder = fmap stringUtf8
instance ToFlushBuilder String where toFlushBuilder = Chunk . Blaze.fromString instance ToFlushBuilder String where toFlushBuilder = Chunk . stringUtf8
instance ToFlushBuilder (Flush Html) where toFlushBuilder = fmap renderHtmlBuilder instance ToFlushBuilder (Flush Html) where toFlushBuilder = fmap renderHtmlBuilder
instance ToFlushBuilder Html where toFlushBuilder = Chunk . renderHtmlBuilder instance ToFlushBuilder Html where toFlushBuilder = Chunk . renderHtmlBuilder
@ -168,6 +165,8 @@ deriving instance ToContent RepJson
instance HasContentType RepPlain where instance HasContentType RepPlain where
getContentType _ = typePlain getContentType _ = typePlain
deriving instance ToContent RepPlain deriving instance ToContent RepPlain
instance HasContentType (JSONResponse a) where
getContentType _ = typeJson
instance HasContentType RepXml where instance HasContentType RepXml where
getContentType _ = typeXml getContentType _ = typeXml
@ -227,13 +226,13 @@ typeOctet = "application/octet-stream"
simpleContentType :: ContentType -> ContentType simpleContentType :: ContentType -> ContentType
simpleContentType = fst . B.break (== _semicolon) simpleContentType = fst . B.break (== _semicolon)
-- Give just the media types as a pair. -- | Give just the media types as a pair.
--
-- For example, \"text/html; charset=utf-8\" returns ("text", "html") -- For example, \"text/html; charset=utf-8\" returns ("text", "html")
contentTypeTypes :: ContentType -> (B.ByteString, B.ByteString) contentTypeTypes :: ContentType -> (B.ByteString, B.ByteString)
contentTypeTypes ct = (main, fst $ B.break (== _semicolon) (tailEmpty sub)) contentTypeTypes = second tailEmpty . B.break (== _slash) . simpleContentType
where where
tailEmpty x = if B.null x then "" else B.tail x tailEmpty x = if B.null x then "" else B.tail x
(main, sub) = B.break (== _slash) ct
instance HasContentType a => HasContentType (DontFullyEvaluate a) where instance HasContentType a => HasContentType (DontFullyEvaluate a) where
getContentType = getContentType . liftM unDontFullyEvaluate getContentType = getContentType . liftM unDontFullyEvaluate
@ -243,26 +242,17 @@ instance ToContent a => ToContent (DontFullyEvaluate a) where
instance ToContent J.Value where instance ToContent J.Value where
toContent = flip ContentBuilder Nothing toContent = flip ContentBuilder Nothing
. Blaze.fromLazyText . J.fromEncoding
. toLazyText . J.toEncoding
#if MIN_VERSION_aeson(0, 7, 0)
. encodeToTextBuilder
#else
. fromValue
#endif
#if MIN_VERSION_aeson(0, 11, 0)
instance ToContent J.Encoding where instance ToContent J.Encoding where
toContent = flip ContentBuilder Nothing . J.fromEncoding toContent = flip ContentBuilder Nothing . J.fromEncoding
#endif
instance HasContentType J.Value where instance HasContentType J.Value where
getContentType _ = typeJson getContentType _ = typeJson
#if MIN_VERSION_aeson(0, 11, 0)
instance HasContentType J.Encoding where instance HasContentType J.Encoding where
getContentType _ = typeJson getContentType _ = typeJson
#endif
instance HasContentType Html where instance HasContentType Html where
getContentType _ = typeHtml getContentType _ = typeHtml
@ -289,6 +279,8 @@ instance ToTypedContent TypedContent where
toTypedContent = id toTypedContent = id
instance ToTypedContent () where instance ToTypedContent () where
toTypedContent () = TypedContent typePlain (toContent ()) toTypedContent () = TypedContent typePlain (toContent ())
instance ToTypedContent Void where
toTypedContent = absurd
instance ToTypedContent (ContentType, Content) where instance ToTypedContent (ContentType, Content) where
toTypedContent (ct, content) = TypedContent ct content toTypedContent (ct, content) = TypedContent ct content
instance ToTypedContent RepJson where instance ToTypedContent RepJson where
@ -299,10 +291,8 @@ instance ToTypedContent RepXml where
toTypedContent (RepXml c) = TypedContent typeXml c toTypedContent (RepXml c) = TypedContent typeXml c
instance ToTypedContent J.Value where instance ToTypedContent J.Value where
toTypedContent v = TypedContent typeJson (toContent v) toTypedContent v = TypedContent typeJson (toContent v)
#if MIN_VERSION_aeson(0, 11, 0)
instance ToTypedContent J.Encoding where instance ToTypedContent J.Encoding where
toTypedContent e = TypedContent typeJson (toContent e) toTypedContent e = TypedContent typeJson (toContent e)
#endif
instance ToTypedContent Html where instance ToTypedContent Html where
toTypedContent h = TypedContent typeHtml (toContent h) toTypedContent h = TypedContent typeHtml (toContent h)
instance ToTypedContent T.Text where instance ToTypedContent T.Text where
@ -311,6 +301,8 @@ instance ToTypedContent [Char] where
toTypedContent = toTypedContent . pack toTypedContent = toTypedContent . pack
instance ToTypedContent Text where instance ToTypedContent Text where
toTypedContent t = TypedContent typePlain (toContent t) toTypedContent t = TypedContent typePlain (toContent t)
instance ToTypedContent (JSONResponse a) where
toTypedContent c = TypedContent typeJson (toContent c)
instance ToTypedContent a => ToTypedContent (DontFullyEvaluate a) where instance ToTypedContent a => ToTypedContent (DontFullyEvaluate a) where
toTypedContent (DontFullyEvaluate a) = toTypedContent (DontFullyEvaluate a) =
let TypedContent ct c = toTypedContent a let TypedContent ct c = toTypedContent a

View File

@ -3,7 +3,6 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE CPP #-}
module Yesod.Core.Dispatch module Yesod.Core.Dispatch
( -- * Quasi-quoted routing ( -- * Quasi-quoted routing
parseRoutes parseRoutes
@ -11,12 +10,25 @@ module Yesod.Core.Dispatch
, parseRoutesFile , parseRoutesFile
, parseRoutesFileNoCheck , parseRoutesFileNoCheck
, mkYesod , mkYesod
, mkYesodOpts
, mkYesodWith , mkYesodWith
-- ** More fine-grained -- ** More fine-grained
, mkYesodData , mkYesodData
, mkYesodDataOpts
, mkYesodSubData , mkYesodSubData
, mkYesodSubDataOpts
, mkYesodDispatch , mkYesodDispatch
, mkYesodDispatchOpts
, mkYesodSubDispatch , mkYesodSubDispatch
-- *** Route generation options
, RouteOpts
, defaultOpts
, setEqDerived
, setShowDerived
, setReadDerived
-- *** Helpers
, defaultGen
, getGetMaxExpires
-- ** Path pieces -- ** Path pieces
, PathPiece (..) , PathPiece (..)
, PathMultiPiece (..) , PathMultiPiece (..)
@ -24,6 +36,7 @@ module Yesod.Core.Dispatch
-- * Convert to WAI -- * Convert to WAI
, toWaiApp , toWaiApp
, toWaiAppPlain , toWaiAppPlain
, toWaiAppYre
, warp , warp
, warpDebug , warpDebug
, warpEnv , warpEnv
@ -31,7 +44,7 @@ module Yesod.Core.Dispatch
, defaultMiddlewaresNoLogging , defaultMiddlewaresNoLogging
-- * WAI subsites -- * WAI subsites
, WaiSubsite (..) , WaiSubsite (..)
, subHelper , WaiSubsiteWithAuth (..)
) where ) where
import Prelude hiding (exp) import Prelude hiding (exp)
@ -44,21 +57,21 @@ import qualified Network.Wai as W
import Data.ByteString.Lazy.Char8 () import Data.ByteString.Lazy.Char8 ()
import Data.Bits ((.|.), finiteBitSize, shiftL)
import Data.Text (Text) import Data.Text (Text)
#if __GLASGOW_HASKELL__ < 710
import Data.Monoid (mappend)
#endif
import qualified Data.ByteString as S import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Char8 as S8
import qualified Blaze.ByteString.Builder import Data.ByteString.Builder (byteString, toLazyByteString)
import Network.HTTP.Types (status301, status307) import Network.HTTP.Types (status301, status307)
import Yesod.Routes.Parse import Yesod.Routes.Parse
import Yesod.Core.Types import Yesod.Core.Types
import Yesod.Core.Class.Yesod import Yesod.Core.Class.Yesod
import Yesod.Core.Class.Dispatch import Yesod.Core.Class.Dispatch
import Yesod.Core.Internal.Run import Yesod.Core.Internal.Run
import Safe (readMay) import Text.Read (readMaybe)
import System.Environment (getEnvironment) import System.Environment (getEnvironment)
import System.Entropy (getEntropy)
import Control.AutoUpdate (mkAutoUpdate, defaultUpdateSettings, updateAction, updateFreq) import Control.AutoUpdate (mkAutoUpdate, defaultUpdateSettings, updateAction, updateFreq)
import Yesod.Core.Internal.Util (getCurrentMaxExpiresRFC1123) import Yesod.Core.Internal.Util (getCurrentMaxExpiresRFC1123)
@ -74,7 +87,6 @@ import Control.Monad.Logger
import Control.Monad (when) import Control.Monad (when)
import qualified Paths_yesod_core import qualified Paths_yesod_core
import Data.Version (showVersion) import Data.Version (showVersion)
import qualified System.Random.MWC as MWC
-- | Convert the given argument into a WAI application, executable with any WAI -- | Convert the given argument into a WAI application, executable with any WAI
-- handler. This function will provide no middlewares; if you want commonly -- handler. This function will provide no middlewares; if you want commonly
@ -83,16 +95,36 @@ toWaiAppPlain :: YesodDispatch site => site -> IO W.Application
toWaiAppPlain site = do toWaiAppPlain site = do
logger <- makeLogger site logger <- makeLogger site
sb <- makeSessionBackend site sb <- makeSessionBackend site
gen <- MWC.createSystemRandom
getMaxExpires <- getGetMaxExpires getMaxExpires <- getGetMaxExpires
return $ toWaiAppYre YesodRunnerEnv return $ toWaiAppYre YesodRunnerEnv
{ yreLogger = logger { yreLogger = logger
, yreSite = site , yreSite = site
, yreSessionBackend = sb , yreSessionBackend = sb
, yreGen = gen , yreGen = defaultGen
, yreGetMaxExpires = getMaxExpires , yreGetMaxExpires = getMaxExpires
} }
-- | Generate a random number uniformly distributed in the full range
-- of 'Int'.
--
-- Note: Before 1.6.20, this generates pseudo-random number in an
-- unspecified range. The range size may not be a power of 2. Since
-- 1.6.20, this uses a secure entropy source and generates in the full
-- range of 'Int'.
--
-- @since 1.6.21.0
defaultGen :: IO Int
defaultGen = bsToInt <$> getEntropy bytes
where
bits = finiteBitSize (undefined :: Int)
bytes = div (bits + 7) 8
bsToInt = S.foldl' (\v i -> shiftL v 8 .|. fromIntegral i) 0
-- | Pure low level function to construct WAI application. Usefull
-- when you need not standard way to run your app, or want to embed it
-- inside another app.
--
-- @since 1.4.29
toWaiAppYre :: YesodDispatch site => YesodRunnerEnv site -> W.Application toWaiAppYre :: YesodDispatch site => YesodRunnerEnv site -> W.Application
toWaiAppYre yre req = toWaiAppYre yre req =
case cleanPath site $ W.pathInfo req of case cleanPath site $ W.pathInfo req of
@ -106,7 +138,7 @@ toWaiAppYre yre req =
sendRedirect y segments' env sendResponse = sendRedirect y segments' env sendResponse =
sendResponse $ W.responseLBS status sendResponse $ W.responseLBS status
[ ("Content-Type", "text/plain") [ ("Content-Type", "text/plain")
, ("Location", Blaze.ByteString.Builder.toByteString dest') , ("Location", BL.toStrict $ toLazyByteString dest')
] "Redirecting" ] "Redirecting"
where where
-- Ensure that non-GET requests get redirected correctly. See: -- Ensure that non-GET requests get redirected correctly. See:
@ -120,7 +152,7 @@ toWaiAppYre yre req =
if S.null (W.rawQueryString env) if S.null (W.rawQueryString env)
then dest then dest
else dest `mappend` else dest `mappend`
Blaze.ByteString.Builder.fromByteString (W.rawQueryString env) byteString (W.rawQueryString env)
-- | Same as 'toWaiAppPlain', but provides a default set of middlewares. This -- | Same as 'toWaiAppPlain', but provides a default set of middlewares. This
-- set may change with future releases, but currently covers: -- set may change with future releases, but currently covers:
@ -142,13 +174,12 @@ toWaiApp site = do
toWaiAppLogger :: YesodDispatch site => Logger -> site -> IO W.Application toWaiAppLogger :: YesodDispatch site => Logger -> site -> IO W.Application
toWaiAppLogger logger site = do toWaiAppLogger logger site = do
sb <- makeSessionBackend site sb <- makeSessionBackend site
gen <- MWC.createSystemRandom
getMaxExpires <- getGetMaxExpires getMaxExpires <- getGetMaxExpires
let yre = YesodRunnerEnv let yre = YesodRunnerEnv
{ yreLogger = logger { yreLogger = logger
, yreSite = site , yreSite = site
, yreSessionBackend = sb , yreSessionBackend = sb
, yreGen = gen , yreGen = defaultGen
, yreGetMaxExpires = getMaxExpires , yreGetMaxExpires = getMaxExpires
} }
messageLoggerSource messageLoggerSource
@ -166,6 +197,16 @@ toWaiAppLogger logger site = do
-- middlewares. This set may change at any point without a breaking version -- middlewares. This set may change at any point without a breaking version
-- number. Currently, it includes: -- number. Currently, it includes:
-- --
-- * Logging
--
-- * GZIP compression
--
-- * Automatic HEAD method handling
--
-- * Request method override with the _method query string parameter
--
-- * Accept header override with the _accept query string parameter
--
-- If you need more fine-grained control of middlewares, please use 'toWaiApp' -- If you need more fine-grained control of middlewares, please use 'toWaiApp'
-- directly. -- directly.
-- --
@ -233,10 +274,14 @@ warpEnv site = do
case lookup "PORT" env of case lookup "PORT" env of
Nothing -> error "warpEnv: no PORT environment variable found" Nothing -> error "warpEnv: no PORT environment variable found"
Just portS -> Just portS ->
case readMay portS of case readMaybe portS of
Nothing -> error $ "warpEnv: invalid PORT environment variable: " ++ show portS Nothing -> error $ "warpEnv: invalid PORT environment variable: " ++ show portS
Just port -> warp port site Just port -> warp port site
-- | Default constructor for 'yreGetMaxExpires' field. Low level
-- function for simple manual construction of 'YesodRunnerEnv'.
--
-- @since 1.4.29
getGetMaxExpires :: IO (IO Text) getGetMaxExpires :: IO (IO Text)
getGetMaxExpires = mkAutoUpdate defaultUpdateSettings getGetMaxExpires = mkAutoUpdate defaultUpdateSettings
{ updateAction = getCurrentMaxExpiresRFC1123 { updateAction = getCurrentMaxExpiresRFC1123

View File

@ -1,8 +1,8 @@
{-# LANGUAGE TypeFamilies, PatternGuards, CPP #-} {-# LANGUAGE TypeFamilies, PatternGuards, CPP #-}
module Yesod.Core.Internal.LiteApp where module Yesod.Core.Internal.LiteApp where
#if __GLASGOW_HASKELL__ < 710 #if !(MIN_VERSION_base(4,11,0))
import Data.Monoid import Data.Semigroup (Semigroup(..))
#endif #endif
import Yesod.Routes.Class import Yesod.Routes.Class
import Yesod.Core.Class.Yesod import Yesod.Core.Class.Yesod
@ -42,12 +42,17 @@ instance RenderRoute LiteApp where
instance ParseRoute LiteApp where instance ParseRoute LiteApp where
parseRoute (x, _) = Just $ LiteAppRoute x parseRoute (x, _) = Just $ LiteAppRoute x
instance Semigroup LiteApp where
LiteApp x <> LiteApp y = LiteApp $ \m ps -> x m ps <|> y m ps
instance Monoid LiteApp where instance Monoid LiteApp where
mempty = LiteApp $ \_ _ -> Nothing mempty = LiteApp $ \_ _ -> Nothing
mappend (LiteApp x) (LiteApp y) = LiteApp $ \m ps -> x m ps <|> y m ps #if !(MIN_VERSION_base(4,11,0))
mappend = (<>)
#endif
type LiteHandler = HandlerT LiteApp IO type LiteHandler = HandlerFor LiteApp
type LiteWidget = WidgetT LiteApp IO type LiteWidget = WidgetFor LiteApp
liteApp :: Writer LiteApp () -> LiteApp liteApp :: Writer LiteApp () -> LiteApp
liteApp = execWriter liteApp = execWriter

View File

@ -25,6 +25,7 @@ import qualified Network.Wai as W
import Web.Cookie (parseCookiesText) import Web.Cookie (parseCookiesText)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy.Char8 as LS8
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Network.HTTP.Types (queryToQueryText, Status (Status)) import Network.HTTP.Types (queryToQueryText, Status (Status))
import Data.Maybe (fromMaybe, catMaybes) import Data.Maybe (fromMaybe, catMaybes)
@ -33,18 +34,13 @@ import qualified Data.Set as Set
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With, decodeUtf8) import Data.Text.Encoding (decodeUtf8With, decodeUtf8)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Conduit import Conduit
import Data.Conduit.List (sourceList)
import Data.Conduit.Binary (sourceFile, sinkFile)
import Data.Word (Word8, Word64) import Data.Word (Word8, Word64)
import Control.Monad.Trans.Resource (runResourceT, ResourceT)
import Control.Exception (throwIO) import Control.Exception (throwIO)
import Control.Monad ((<=<), liftM) import Control.Monad ((<=<), liftM)
import Yesod.Core.Types import Yesod.Core.Types
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.IORef import Data.IORef
import qualified System.Random.MWC as MWC
import Control.Monad.Primitive (PrimMonad, PrimState)
import qualified Data.Vector.Storable as V import qualified Data.Vector.Storable as V
import Data.ByteString.Internal (ByteString (PS)) import Data.ByteString.Internal (ByteString (PS))
import qualified Data.Word8 as Word8 import qualified Data.Word8 as Word8
@ -60,23 +56,29 @@ limitRequestBody maxLen req = do
let len = fromIntegral $ S8.length bs let len = fromIntegral $ S8.length bs
remaining' = remaining - len remaining' = remaining - len
if remaining < len if remaining < len
then throwIO $ HCWai tooLargeResponse then throwIO $ HCWai $ tooLargeResponse maxLen len
else do else do
writeIORef ref remaining' writeIORef ref remaining'
return bs return bs
} }
tooLargeResponse :: W.Response tooLargeResponse :: Word64 -> Word64 -> W.Response
tooLargeResponse = W.responseLBS tooLargeResponse maxLen bodyLen = W.responseLBS
(Status 413 "Too Large") (Status 413 "Too Large")
[("Content-Type", "text/plain")] [("Content-Type", "text/plain")]
"Request body too large to be processed." (L.concat
[ "Request body too large to be processed. The maximum size is "
, (LS8.pack (show maxLen))
, " bytes; your request body was "
, (LS8.pack (show bodyLen))
, " bytes. If you're the developer of this site, you can configure the maximum length with the `maximumContentLength` or `maximumContentLengthIO` function on the Yesod typeclass."
])
parseWaiRequest :: W.Request parseWaiRequest :: W.Request
-> SessionMap -> SessionMap
-> Bool -> Bool
-> Maybe Word64 -- ^ max body size -> Maybe Word64 -- ^ max body size
-> Either (IO YesodRequest) (MWC.GenIO -> IO YesodRequest) -> Either (IO YesodRequest) (IO Int -> IO YesodRequest)
parseWaiRequest env session useToken mmaxBodySize = parseWaiRequest env session useToken mmaxBodySize =
-- In most cases, we won't need to generate any random values. Therefore, -- In most cases, we won't need to generate any random values. Therefore,
-- we split our results: if we need a random generator, return a Right -- we split our results: if we need a random generator, return a Right
@ -127,7 +129,7 @@ parseWaiRequest env session useToken mmaxBodySize =
-- Already have a token, use it. -- Already have a token, use it.
Just bs -> Left $ Just $ decodeUtf8With lenientDecode bs Just bs -> Left $ Just $ decodeUtf8With lenientDecode bs
-- Don't have a token, get a random generator and make a new one. -- Don't have a token, get a random generator and make a new one.
Nothing -> Right $ fmap Just . randomString 10 Nothing -> Right $ fmap Just . randomString 40
| otherwise = Left Nothing | otherwise = Left Nothing
textQueryString :: W.Request -> [(Text, Text)] textQueryString :: W.Request -> [(Text, Text)]
@ -156,16 +158,21 @@ addTwoLetters (toAdd, exist) (l:ls) =
-- | Generate a random String of alphanumerical characters -- | Generate a random String of alphanumerical characters
-- (a-z, A-Z, and 0-9) of the given length using the given -- (a-z, A-Z, and 0-9) of the given length using the given
-- random number generator. -- random number generator.
randomString :: PrimMonad m => Int -> MWC.Gen (PrimState m) -> m Text randomString :: Monad m => Int -> m Int -> m Text
randomString len gen = randomString len gen =
liftM (decodeUtf8 . fromByteVector) $ V.replicateM len asciiChar liftM (decodeUtf8 . fromByteVector) $ V.replicateM len asciiChar
where where
asciiChar = liftM toAscii $ MWC.uniformR (0, 61) gen asciiChar =
let loop = do
toAscii i x <- gen
| i < 26 = i + Word8._A let y = fromIntegral $ x `mod` 64
| i < 52 = i + Word8._a - 26 case () of
| otherwise = i + Word8._0 - 52 ()
| y < 26 -> return $ y + Word8._A
| y < 52 -> return $ y + Word8._a - 26
| y < 62 -> return $ y + Word8._0 - 52
| otherwise -> loop
in loop
fromByteVector :: V.Vector Word8 -> ByteString fromByteVector :: V.Vector Word8 -> ByteString
fromByteVector v = fromByteVector v =
@ -176,13 +183,13 @@ fromByteVector v =
mkFileInfoLBS :: Text -> Text -> L.ByteString -> FileInfo mkFileInfoLBS :: Text -> Text -> L.ByteString -> FileInfo
mkFileInfoLBS name ct lbs = mkFileInfoLBS name ct lbs =
FileInfo name ct (sourceList $ L.toChunks lbs) (`L.writeFile` lbs) FileInfo name ct (sourceLazy lbs) (`L.writeFile` lbs)
mkFileInfoFile :: Text -> Text -> FilePath -> FileInfo mkFileInfoFile :: Text -> Text -> FilePath -> FileInfo
mkFileInfoFile name ct fp = FileInfo name ct (sourceFile fp) (\dst -> runResourceT $ sourceFile fp $$ sinkFile dst) mkFileInfoFile name ct fp = FileInfo name ct (sourceFile fp) (\dst -> runConduitRes $ sourceFile fp .| sinkFile dst)
mkFileInfoSource :: Text -> Text -> Source (ResourceT IO) ByteString -> FileInfo mkFileInfoSource :: Text -> Text -> ConduitT () ByteString (ResourceT IO) () -> FileInfo
mkFileInfoSource name ct src = FileInfo name ct src (\dst -> runResourceT $ src $$ sinkFile dst) mkFileInfoSource name ct src = FileInfo name ct src (\dst -> runConduitRes $ src .| sinkFile dst)
tokenKey :: IsString a => a tokenKey :: IsString a => a
tokenKey = "_TOKEN" tokenKey = "_TOKEN"

View File

@ -6,29 +6,24 @@ module Yesod.Core.Internal.Response where
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString as S import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as BL
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Network.Wai import Network.Wai
import Control.Monad (mplus) import Control.Monad (mplus)
import Control.Monad.Trans.Resource (runInternalState, InternalState) import Control.Monad.Trans.Resource (runInternalState, InternalState)
import Network.Wai.Internal import Network.Wai.Internal
#if !MIN_VERSION_base(4, 6, 0)
import Prelude hiding (catch)
#endif
import Web.Cookie (renderSetCookie) import Web.Cookie (renderSetCookie)
import Yesod.Core.Content import Yesod.Core.Content
import Yesod.Core.Types import Yesod.Core.Types
import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types as H
import qualified Data.Text as T import qualified Data.Text as T
import Control.Exception (SomeException, handle) import Control.Exception (SomeException, handle)
import Blaze.ByteString.Builder (fromLazyByteString, import Data.ByteString.Builder (lazyByteString, toLazyByteString)
toLazyByteString, toByteString)
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import qualified Data.Map as Map import qualified Data.Map as Map
import Yesod.Core.Internal.Request (tokenKey) import Yesod.Core.Internal.Request (tokenKey)
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Data.Conduit (Flush (..), ($$), transPipe) import Conduit
import qualified Data.Conduit.List as CL
yarToResponse :: YesodResponse yarToResponse :: YesodResponse
-> (SessionMap -> IO [Header]) -- ^ save session -> (SessionMap -> IO [Header]) -- ^ save session
@ -56,9 +51,9 @@ yarToResponse (YRPlain s' hs ct c newSess) saveSession yreq _req is sendResponse
sendResponse $ ResponseBuilder s hs' b sendResponse $ ResponseBuilder s hs' b
go (ContentFile fp p) = sendResponse $ ResponseFile s finalHeaders fp p go (ContentFile fp p) = sendResponse $ ResponseFile s finalHeaders fp p
go (ContentSource body) = sendResponse $ responseStream s finalHeaders go (ContentSource body) = sendResponse $ responseStream s finalHeaders
$ \sendChunk flush -> $ \sendChunk flush -> runConduit $
transPipe (`runInternalState` is) body transPipe (`runInternalState` is) body
$$ CL.mapM_ (\mchunk -> .| mapM_C (\mchunk ->
case mchunk of case mchunk of
Flush -> flush Flush -> flush
Chunk builder -> sendChunk builder) Chunk builder -> sendChunk builder)
@ -86,7 +81,7 @@ defaultStatus = H.mkStatus (-1) "INVALID DEFAULT STATUS"
headerToPair :: Header headerToPair :: Header
-> (CI ByteString, ByteString) -> (CI ByteString, ByteString)
headerToPair (AddCookie sc) = headerToPair (AddCookie sc) =
("Set-Cookie", toByteString $ renderSetCookie sc) ("Set-Cookie", BL.toStrict $ toLazyByteString $ renderSetCookie sc)
headerToPair (DeleteCookie key path) = headerToPair (DeleteCookie key path) =
( "Set-Cookie" ( "Set-Cookie"
, S.concat , S.concat
@ -96,14 +91,14 @@ headerToPair (DeleteCookie key path) =
, "; expires=Thu, 01-Jan-1970 00:00:00 GMT" , "; expires=Thu, 01-Jan-1970 00:00:00 GMT"
] ]
) )
headerToPair (Header key value) = (CI.mk key, value) headerToPair (Header key value) = (key, value)
evaluateContent :: Content -> IO (Either ErrorResponse Content) evaluateContent :: Content -> IO (Either ErrorResponse Content)
evaluateContent (ContentBuilder b mlen) = handle f $ do evaluateContent (ContentBuilder b mlen) = handle f $ do
let lbs = toLazyByteString b let lbs = toLazyByteString b
len = L.length lbs len = L.length lbs
mlen' = mlen `mplus` Just (fromIntegral len) mlen' = mlen `mplus` Just (fromIntegral len)
len `seq` return (Right $ ContentBuilder (fromLazyByteString lbs) mlen') len `seq` return (Right $ ContentBuilder (lazyByteString lbs) mlen')
where where
f :: SomeException -> IO (Either ErrorResponse Content) f :: SomeException -> IO (Either ErrorResponse Content)
f = return . Left . InternalError . T.pack . show f = return . Left . InternalError . T.pack . show

View File

@ -1,4 +1,3 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternGuards #-} {-# LANGUAGE PatternGuards #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
@ -6,17 +5,27 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module Yesod.Core.Internal.Run where {-# LANGUAGE ScopedTypeVariables #-}
module Yesod.Core.Internal.Run
( toErrorHandler
, errFromShow
, basicRunHandler
, handleError
, handleContents
, evalFallback
, runHandler
, safeEh
, runFakeHandler
, yesodRunner
, yesodRender
, resolveApproot
)
where
import qualified Control.Exception as EUnsafe
#if __GLASGOW_HASKELL__ < 710
import Data.Monoid (Monoid, mempty)
import Control.Applicative ((<$>))
#endif
import Yesod.Core.Internal.Response import Yesod.Core.Internal.Response
import Blaze.ByteString.Builder (toByteString) import Data.ByteString.Builder (toLazyByteString)
import Control.Exception (fromException, evaluate) import qualified Data.ByteString.Lazy as BL
import qualified Control.Exception as E
import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Logger (LogLevel (LevelError), LogSource, import Control.Monad.Logger (LogLevel (LevelError), LogSource,
liftLoc) liftLoc)
@ -44,46 +53,31 @@ import Yesod.Core.Internal.Request (parseWaiRequest,
import Yesod.Core.Internal.Util (getCurrentMaxExpiresRFC1123) import Yesod.Core.Internal.Util (getCurrentMaxExpiresRFC1123)
import Yesod.Routes.Class (Route, renderRoute) import Yesod.Routes.Class (Route, renderRoute)
import Control.DeepSeq (($!!), NFData) import Control.DeepSeq (($!!), NFData)
import UnliftIO.Exception
import UnliftIO(MonadUnliftIO, withRunInIO)
import Data.Proxy(Proxy(..))
-- | Catch all synchronous exceptions, ignoring asynchronous -- | Convert a synchronous exception into an ErrorResponse
-- exceptions. toErrorHandler :: SomeException -> IO ErrorResponse
-- toErrorHandler e0 = handleAny errFromShow $
-- Ideally we'd use this from a different library
catchSync :: IO a -> (E.SomeException -> IO a) -> IO a
catchSync thing after = thing `E.catch` \e ->
if isAsyncException e
then E.throwIO e
else after e
-- | Determine if an exception is asynchronous
--
-- Also worth being upstream
isAsyncException :: E.SomeException -> Bool
isAsyncException e =
case fromException e of
Just E.SomeAsyncException{} -> True
Nothing -> False
-- | Convert an exception into an ErrorResponse
toErrorHandler :: E.SomeException -> IO ErrorResponse
toErrorHandler e0 = flip catchSync errFromShow $
case fromException e0 of case fromException e0 of
Just (HCError x) -> evaluate $!! x Just (HCError x) -> evaluate $!! x
_ _ -> errFromShow e0
| isAsyncException e0 -> E.throwIO e0
| otherwise -> errFromShow e0
-- | Generate an @ErrorResponse@ based on the shown version of the exception -- | Generate an @ErrorResponse@ based on the shown version of the exception
errFromShow :: E.SomeException -> IO ErrorResponse errFromShow :: SomeException -> IO ErrorResponse
errFromShow x = evaluate $!! InternalError $! T.pack $! show x errFromShow x = do
text <- evaluate (T.pack $ show x) `catchAny` \_ ->
return (T.pack "Yesod.Core.Internal.Run.errFromShow: show of an exception threw an exception")
return $ InternalError text
-- | Do a basic run of a handler, getting some contents and the final -- | Do a basic run of a handler, getting some contents and the final
-- @GHState@. The @GHState@ unfortunately may contain some impure -- @GHState@. The @GHState@ unfortunately may contain some impure
-- exceptions, but all other synchronous exceptions will be caught and -- exceptions, but all other synchronous exceptions will be caught and
-- represented by the @HandlerContents@. -- represented by the @HandlerContents@.
basicRunHandler :: ToTypedContent c basicRunHandler :: ToTypedContent c
=> RunHandlerEnv site => RunHandlerEnv site site
-> HandlerT site IO c -> HandlerFor site c
-> YesodRequest -> YesodRequest
-> InternalState -> InternalState
-> IO (GHState, HandlerContents) -> IO (GHState, HandlerContents)
@ -94,9 +88,9 @@ basicRunHandler rhe handler yreq resState = do
-- Run the handler itself, capturing any runtime exceptions and -- Run the handler itself, capturing any runtime exceptions and
-- converting them into a @HandlerContents@ -- converting them into a @HandlerContents@
contents' <- catchSync contents' <- rheCatchHandlerExceptions rhe
(do (do
res <- unHandlerT handler (hd istate) res <- unHandlerFor handler (hd istate)
tc <- evaluate (toTypedContent res) tc <- evaluate (toTypedContent res)
-- Success! Wrap it up in an @HCContent@ -- Success! Wrap it up in an @HCContent@
return (HCContent defaultStatus tc)) return (HCContent defaultStatus tc))
@ -121,12 +115,11 @@ basicRunHandler rhe handler yreq resState = do
{ handlerRequest = yreq { handlerRequest = yreq
, handlerEnv = rhe , handlerEnv = rhe
, handlerState = istate , handlerState = istate
, handlerToParent = const ()
, handlerResource = resState , handlerResource = resState
} }
-- | Convert an @ErrorResponse@ into a @YesodResponse@ -- | Convert an @ErrorResponse@ into a @YesodResponse@
handleError :: RunHandlerEnv site handleError :: RunHandlerEnv sub site
-> YesodRequest -> YesodRequest
-> InternalState -> InternalState
-> Map.Map Text S8.ByteString -> Map.Map Text S8.ByteString
@ -135,7 +128,7 @@ handleError :: RunHandlerEnv site
-> IO YesodResponse -> IO YesodResponse
handleError rhe yreq resState finalSession headers e0 = do handleError rhe yreq resState finalSession headers e0 = do
-- Find any evil hidden impure exceptions -- Find any evil hidden impure exceptions
e <- (evaluate $!! e0) `catchSync` errFromShow e <- (evaluate $!! e0) `catchAny` errFromShow
-- Generate a response, leveraging the updated session and -- Generate a response, leveraging the updated session and
-- response headers -- response headers
@ -196,19 +189,22 @@ handleContents handleError' finalSession headers contents =
-- | Evaluate the given value. If an exception is thrown, use it to -- | Evaluate the given value. If an exception is thrown, use it to
-- replace the provided contents and then return @mempty@ in place of the -- replace the provided contents and then return @mempty@ in place of the
-- evaluated value. -- evaluated value.
--
-- Note that this also catches async exceptions.
evalFallback :: (Monoid w, NFData w) evalFallback :: (Monoid w, NFData w)
=> HandlerContents => (forall a. IO a -> (SomeException -> IO a) -> IO a)
-> HandlerContents
-> w -> w
-> IO (w, HandlerContents) -> IO (w, HandlerContents)
evalFallback contents val = catchSync evalFallback catcher contents val = catcher
(fmap (, contents) (evaluate $!! val)) (fmap (, contents) (evaluate $!! val))
(fmap ((mempty, ) . HCError) . toErrorHandler) (fmap ((mempty, ) . HCError) . toErrorHandler)
-- | Function used internally by Yesod in the process of converting a -- | Function used internally by Yesod in the process of converting a
-- 'HandlerT' into an 'Application'. Should not be needed by users. -- 'HandlerFor' into an 'Application'. Should not be needed by users.
runHandler :: ToTypedContent c runHandler :: ToTypedContent c
=> RunHandlerEnv site => RunHandlerEnv site site
-> HandlerT site IO c -> HandlerFor site c
-> YesodApp -> YesodApp
runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -> do runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -> do
-- Get the raw state and original contents -- Get the raw state and original contents
@ -216,15 +212,16 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
-- Evaluate the unfortunately-lazy session and headers, -- Evaluate the unfortunately-lazy session and headers,
-- propagating exceptions into the contents -- propagating exceptions into the contents
(finalSession, contents1) <- evalFallback contents0 (ghsSession state) (finalSession, contents1) <- evalFallback rheCatchHandlerExceptions contents0 (ghsSession state)
(headers, contents2) <- evalFallback contents1 (appEndo (ghsHeaders state) []) (headers, contents2) <- evalFallback rheCatchHandlerExceptions contents1 (appEndo (ghsHeaders state) [])
contents3 <- (evaluate contents2) `catchAny` (fmap HCError . toErrorHandler)
-- Convert the HandlerContents into the final YesodResponse -- Convert the HandlerContents into the final YesodResponse
handleContents handleContents
(handleError rhe yreq resState finalSession headers) (handleError rhe yreq resState finalSession headers)
finalSession finalSession
headers headers
contents2 contents3
safeEh :: (Loc -> LogSource -> LogLevel -> LogStr -> IO ()) safeEh :: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> ErrorResponse -> ErrorResponse
@ -239,31 +236,31 @@ safeEh log' er req = do
(toContent ("Internal Server Error" :: S.ByteString)) (toContent ("Internal Server Error" :: S.ByteString))
(reqSession req) (reqSession req)
-- | Run a 'HandlerT' completely outside of Yesod. This -- | Run a 'HandlerFor' completely outside of Yesod. This
-- function comes with many caveats and you shouldn't use it -- function comes with many caveats and you shouldn't use it
-- unless you fully understand what it's doing and how it works. -- unless you fully understand what it's doing and how it works.
-- --
-- As of now, there's only one reason to use this function at -- As of now, there's only one reason to use this function at
-- all: in order to run unit tests of functions inside 'HandlerT' -- all: in order to run unit tests of functions inside 'HandlerFor'
-- but that aren't easily testable with a full HTTP request. -- but that aren't easily testable with a full HTTP request.
-- Even so, it's better to use @wai-test@ or @yesod-test@ instead -- Even so, it's better to use @wai-test@ or @yesod-test@ instead
-- of using this function. -- of using this function.
-- --
-- This function will create a fake HTTP request (both @wai@'s -- This function will create a fake HTTP request (both @wai@'s
-- 'Request' and @yesod@'s 'Request') and feed it to the -- 'Request' and @yesod@'s 'Request') and feed it to the
-- @HandlerT@. The only useful information the @HandlerT@ may -- @HandlerFor@. The only useful information the @HandlerFor@ may
-- get from the request is the session map, which you must supply -- get from the request is the session map, which you must supply
-- as argument to @runFakeHandler@. All other fields contain -- as argument to @runFakeHandler@. All other fields contain
-- fake information, which means that they can be accessed but -- fake information, which means that they can be accessed but
-- won't have any useful information. The response of the -- won't have any useful information. The response of the
-- @HandlerT@ is completely ignored, including changes to the -- @HandlerFor@ is completely ignored, including changes to the
-- session, cookies or headers. We only return you the -- session, cookies or headers. We only return you the
-- @HandlerT@'s return value. -- @HandlerFor@'s return value.
runFakeHandler :: (Yesod site, MonadIO m) => runFakeHandler :: forall site m a . (Yesod site, MonadIO m) =>
SessionMap SessionMap
-> (site -> Logger) -> (site -> Logger)
-> site -> site
-> HandlerT site IO a -> HandlerFor site a
-> m (Either ErrorResponse a) -> m (Either ErrorResponse a)
runFakeHandler fakeSessionMap logger site handler = liftIO $ do runFakeHandler fakeSessionMap logger site handler = liftIO $ do
ret <- I.newIORef (Left $ InternalError "runFakeHandler: no result") ret <- I.newIORef (Left $ InternalError "runFakeHandler: no result")
@ -273,11 +270,14 @@ runFakeHandler fakeSessionMap logger site handler = liftIO $ do
RunHandlerEnv RunHandlerEnv
{ rheRender = yesodRender site $ resolveApproot site fakeWaiRequest { rheRender = yesodRender site $ resolveApproot site fakeWaiRequest
, rheRoute = Nothing , rheRoute = Nothing
, rheRouteToMaster = id
, rheChild = site
, rheSite = site , rheSite = site
, rheUpload = fileUpload site , rheUpload = fileUpload site
, rheLog = messageLoggerSource site $ logger site , rheLog = messageLoggerSource site $ logger site
, rheOnError = errHandler , rheOnError = errHandler
, rheMaxExpires = maxExpires , rheMaxExpires = maxExpires
, rheCatchHandlerExceptions = catchHandlerExceptions site
} }
handler' handler'
errHandler err req = do errHandler err req = do
@ -303,10 +303,8 @@ runFakeHandler fakeSessionMap logger site handler = liftIO $ do
, vault = mempty , vault = mempty
, requestBodyLength = KnownLength 0 , requestBodyLength = KnownLength 0
, requestHeaderRange = Nothing , requestHeaderRange = Nothing
#if MIN_VERSION_wai(3,2,0)
, requestHeaderReferer = Nothing , requestHeaderReferer = Nothing
, requestHeaderUserAgent = Nothing , requestHeaderUserAgent = Nothing
#endif
} }
fakeRequest = fakeRequest =
YesodRequest YesodRequest
@ -321,14 +319,16 @@ runFakeHandler fakeSessionMap logger site handler = liftIO $ do
_ <- runResourceT $ yapp fakeRequest _ <- runResourceT $ yapp fakeRequest
I.readIORef ret I.readIORef ret
yesodRunner :: (ToTypedContent res, Yesod site) yesodRunner :: forall res site . (ToTypedContent res, Yesod site)
=> HandlerT site IO res => HandlerFor site res
-> YesodRunnerEnv site -> YesodRunnerEnv site
-> Maybe (Route site) -> Maybe (Route site)
-> Application -> Application
yesodRunner handler' YesodRunnerEnv {..} route req sendResponse yesodRunner handler' YesodRunnerEnv {..} route req sendResponse = do
| Just maxLen <- mmaxLen, KnownLength len <- requestBodyLength req, maxLen < len = sendResponse tooLargeResponse mmaxLen <- maximumContentLengthIO yreSite route
| otherwise = do case (mmaxLen, requestBodyLength req) of
(Just maxLen, KnownLength len) | maxLen < len -> sendResponse (tooLargeResponse maxLen len)
_ -> do
let dontSaveSession _ = return [] let dontSaveSession _ = return []
(session, saveSession) <- liftIO $ (session, saveSession) <- liftIO $
maybe (return (Map.empty, dontSaveSession)) (`sbLoadSession` req) yreSessionBackend maybe (return (Map.empty, dontSaveSession)) (`sbLoadSession` req) yreSessionBackend
@ -347,11 +347,14 @@ yesodRunner handler' YesodRunnerEnv {..} route req sendResponse
rheSafe = RunHandlerEnv rheSafe = RunHandlerEnv
{ rheRender = yesodRender yreSite ra { rheRender = yesodRender yreSite ra
, rheRoute = route , rheRoute = route
, rheRouteToMaster = id
, rheChild = yreSite
, rheSite = yreSite , rheSite = yreSite
, rheUpload = fileUpload yreSite , rheUpload = fileUpload yreSite
, rheLog = log' , rheLog = log'
, rheOnError = safeEh log' , rheOnError = safeEh log'
, rheMaxExpires = maxExpires , rheMaxExpires = maxExpires
, rheCatchHandlerExceptions = catchHandlerExceptions yreSite
} }
rhe = rheSafe rhe = rheSafe
{ rheOnError = runHandler rheSafe . errorHandler { rheOnError = runHandler rheSafe . errorHandler
@ -372,7 +375,7 @@ yesodRender :: Yesod y
-> [(Text, Text)] -- ^ url query string -> [(Text, Text)] -- ^ url query string
-> Text -> Text
yesodRender y ar url params = yesodRender y ar url params =
decodeUtf8With lenientDecode $ toByteString $ decodeUtf8With lenientDecode $ BL.toStrict $ toLazyByteString $
fromMaybe fromMaybe
(joinPath y ar ps (joinPath y ar ps
$ params ++ params') $ params ++ params')

View File

@ -0,0 +1,354 @@
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
module Yesod.Core.Internal.TH
( mkYesod
, mkYesodOpts
, mkYesodWith
, mkYesodData
, mkYesodDataOpts
, mkYesodSubData
, mkYesodSubDataOpts
, mkYesodWithParser
, mkYesodWithParserOpts
, mkYesodDispatch
, mkYesodDispatchOpts
, masterTypeSyns
, mkYesodGeneral
, mkYesodGeneralOpts
, mkMDS
, mkDispatchInstance
, mkYesodSubDispatch
, subTopDispatch
, instanceD
, RouteOpts
, defaultOpts
, setEqDerived
, setShowDerived
, setReadDerived
)
where
import Prelude hiding (exp)
import Yesod.Core.Handler
import Language.Haskell.TH hiding (cxt, instanceD)
import Language.Haskell.TH.Syntax
import qualified Network.Wai as W
import Data.ByteString.Lazy.Char8 ()
import Data.List (foldl')
import Control.Monad (replicateM, void)
import Text.Parsec (parse, many1, many, eof, try, option, sepBy1)
import Text.ParserCombinators.Parsec.Char (alphaNum, spaces, string, char)
import Yesod.Routes.TH
import Yesod.Routes.Parse
import Yesod.Core.Content (ToTypedContent (..))
import Yesod.Core.Types
import Yesod.Core.Class.Dispatch
import Yesod.Core.Internal.Run
-- | Generates URL datatype and site function for the given 'Resource's. This
-- is used for creating sites, /not/ subsites. See 'mkYesodSubData' and 'mkYesodSubDispatch' for the latter.
-- Use 'parseRoutes' to create the 'Resource's.
--
-- Contexts and type variables in the name of the datatype are parsed.
-- For example, a datatype @App a@ with typeclass constraint @MyClass a@ can be written as @\"(MyClass a) => App a\"@.
mkYesod :: String -- ^ name of the argument datatype
-> [ResourceTree String]
-> Q [Dec]
mkYesod = mkYesodOpts defaultOpts
-- | `mkYesod` but with custom options.
--
-- @since 1.6.25.0
mkYesodOpts :: RouteOpts
-> String
-> [ResourceTree String]
-> Q [Dec]
mkYesodOpts opts name = fmap (uncurry (++)) . mkYesodWithParserOpts opts name False return
{-# DEPRECATED mkYesodWith "Contexts and type variables are now parsed from the name in `mkYesod`. <https://github.com/yesodweb/yesod/pull/1366>" #-}
-- | Similar to 'mkYesod', except contexts and type variables are not parsed.
-- Instead, they are explicitly provided.
-- You can write @(MyClass a) => App a@ with @mkYesodWith [[\"MyClass\",\"a\"]] \"App\" [\"a\"] ...@.
mkYesodWith :: [[String]] -- ^ list of contexts
-> String -- ^ name of the argument datatype
-> [String] -- ^ list of type variables
-> [ResourceTree String]
-> Q [Dec]
mkYesodWith cxts name args = fmap (uncurry (++)) . mkYesodGeneral cxts name args False return
-- | Sometimes, you will want to declare your routes in one file and define
-- your handlers elsewhere. For example, this is the only way to break up a
-- monolithic file into smaller parts. Use this function, paired with
-- 'mkYesodDispatch', to do just that.
mkYesodData :: String -> [ResourceTree String] -> Q [Dec]
mkYesodData = mkYesodDataOpts defaultOpts
-- | `mkYesodData` but with custom options.
--
-- @since 1.6.25.0
mkYesodDataOpts :: RouteOpts -> String -> [ResourceTree String] -> Q [Dec]
mkYesodDataOpts opts name resS = fst <$> mkYesodWithParserOpts opts name False return resS
mkYesodSubData :: String -> [ResourceTree String] -> Q [Dec]
mkYesodSubData = mkYesodSubDataOpts defaultOpts
-- |
--
-- @since 1.6.25.0
mkYesodSubDataOpts :: RouteOpts -> String -> [ResourceTree String] -> Q [Dec]
mkYesodSubDataOpts opts name resS = fst <$> mkYesodWithParserOpts opts name True return resS
-- | Parses contexts and type arguments out of name before generating TH.
mkYesodWithParser :: String -- ^ foundation type
-> Bool -- ^ is this a subsite
-> (Exp -> Q Exp) -- ^ unwrap handler
-> [ResourceTree String]
-> Q([Dec],[Dec])
mkYesodWithParser = mkYesodWithParserOpts defaultOpts
-- | Parses contexts and type arguments out of name before generating TH.
--
-- @since 1.6.25.0
mkYesodWithParserOpts :: RouteOpts -- ^ Additional route options
-> String -- ^ foundation type
-> Bool -- ^ is this a subsite
-> (Exp -> Q Exp) -- ^ unwrap handler
-> [ResourceTree String]
-> Q([Dec],[Dec])
mkYesodWithParserOpts opts name isSub f resS = do
let (name', rest, cxt) = case parse parseName "" name of
Left err -> error $ show err
Right a -> a
mkYesodGeneralOpts opts cxt name' rest isSub f resS
where
parseName = do
cxt <- option [] parseContext
name' <- parseWord
args <- many parseWord
spaces
eof
return ( name', args, cxt)
parseWord = do
spaces
many1 alphaNum
parseContext = try $ do
cxts <- parseParen parseContexts
spaces
_ <- string "=>"
return cxts
parseParen p = do
spaces
_ <- char '('
r <- p
spaces
_ <- char ')'
return r
parseContexts =
sepBy1 (many1 parseWord) (spaces >> char ',' >> return ())
-- | See 'mkYesodData'.
mkYesodDispatch :: String -> [ResourceTree String] -> Q [Dec]
mkYesodDispatch = mkYesodDispatchOpts defaultOpts
-- | See 'mkYesodDataOpts'
--
-- @since 1.6.25.0
mkYesodDispatchOpts :: RouteOpts -> String -> [ResourceTree String] -> Q [Dec]
mkYesodDispatchOpts opts name = fmap snd . mkYesodWithParserOpts opts name False return
-- | Get the Handler and Widget type synonyms for the given site.
masterTypeSyns :: [Name] -> Type -> [Dec] -- FIXME remove from here, put into the scaffolding itself?
masterTypeSyns vs site =
[ TySynD (mkName "Handler") (fmap plainTV vs)
$ ConT ''HandlerFor `AppT` site
, TySynD (mkName "Widget") (fmap plainTV vs)
$ ConT ''WidgetFor `AppT` site `AppT` ConT ''()
]
mkYesodGeneral :: [[String]] -- ^ Appliction context. Used in RenderRoute, RouteAttrs, and ParseRoute instances.
-> String -- ^ foundation type
-> [String] -- ^ arguments for the type
-> Bool -- ^ is this a subsite
-> (Exp -> Q Exp) -- ^ unwrap handler
-> [ResourceTree String]
-> Q([Dec],[Dec])
mkYesodGeneral = mkYesodGeneralOpts defaultOpts
-- |
--
-- @since 1.6.25.0
mkYesodGeneralOpts :: RouteOpts -- ^ Options to adjust route creation
-> [[String]] -- ^ Appliction context. Used in RenderRoute, RouteAttrs, and ParseRoute instances.
-> String -- ^ foundation type
-> [String] -- ^ arguments for the type
-> Bool -- ^ is this a subsite
-> (Exp -> Q Exp) -- ^ unwrap handler
-> [ResourceTree String]
-> Q([Dec],[Dec])
mkYesodGeneralOpts opts appCxt' namestr mtys isSub f resS = do
let appCxt = fmap (\(c:rest) ->
foldl' (\acc v -> acc `AppT` nameToType v) (ConT $ mkName c) rest
) appCxt'
mname <- lookupTypeName namestr
arity <- case mname of
Just name -> do
info <- reify name
return $
case info of
TyConI dec ->
case dec of
DataD _ _ vs _ _ _ -> length vs
NewtypeD _ _ vs _ _ _ -> length vs
TySynD _ vs _ -> length vs
_ -> 0
_ -> 0
_ -> return 0
let name = mkName namestr
-- Generate as many variable names as the arity indicates
vns <- replicateM (arity - length mtys) $ newName "t"
-- types that you apply to get a concrete site name
let argtypes = fmap nameToType mtys ++ fmap VarT vns
-- typevars that should appear in synonym head
let argvars = (fmap mkName . filter isTvar) mtys ++ vns
-- Base type (site type with variables)
let site = foldl' AppT (ConT name) argtypes
res = map (fmap (parseType . dropBracket)) resS
renderRouteDec <- mkRenderRouteInstanceOpts opts appCxt site res
routeAttrsDec <- mkRouteAttrsInstance appCxt site res
dispatchDec <- mkDispatchInstance site appCxt f res
parseRoute <- mkParseRouteInstance appCxt site res
let rname = mkName $ "resources" ++ namestr
eres <- lift resS
let resourcesDec =
[ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)
, FunD rname [Clause [] (NormalB eres) []]
]
let dataDec = concat
[ [parseRoute]
, renderRouteDec
, [routeAttrsDec]
, resourcesDec
, if isSub then [] else masterTypeSyns argvars site
]
return (dataDec, dispatchDec)
mkMDS :: (Exp -> Q Exp) -> Q Exp -> Q Exp -> MkDispatchSettings a site b
mkMDS f rh sd = MkDispatchSettings
{ mdsRunHandler = rh
, mdsSubDispatcher = sd
, mdsGetPathInfo = [|W.pathInfo|]
, mdsSetPathInfo = [|\p r -> r { W.pathInfo = p }|]
, mdsMethod = [|W.requestMethod|]
, mds404 = [|void notFound|]
, mds405 = [|void badMethod|]
, mdsGetHandler = defaultGetHandler
, mdsUnwrapper = f
}
-- | If the generation of @'YesodDispatch'@ instance require finer
-- control of the types, contexts etc. using this combinator. You will
-- hardly need this generality. However, in certain situations, like
-- when writing library/plugin for yesod, this combinator becomes
-- handy.
mkDispatchInstance :: Type -- ^ The master site type
-> Cxt -- ^ Context of the instance
-> (Exp -> Q Exp) -- ^ Unwrap handler
-> [ResourceTree c] -- ^ The resource
-> DecsQ
mkDispatchInstance master cxt f res = do
clause' <-
mkDispatchClause
(mkMDS
f
[|yesodRunner|]
[|\parentRunner getSub toParent env -> yesodSubDispatch
YesodSubRunnerEnv
{ ysreParentRunner = parentRunner
, ysreGetSub = getSub
, ysreToParentRoute = toParent
, ysreParentEnv = env
}
|])
res
let thisDispatch = FunD 'yesodDispatch [clause']
return [instanceD cxt yDispatch [thisDispatch]]
where
yDispatch = ConT ''YesodDispatch `AppT` master
mkYesodSubDispatch :: [ResourceTree a] -> Q Exp
mkYesodSubDispatch res = do
clause' <-
mkDispatchClause
(mkMDS
return
[|subHelper|]
[|subTopDispatch|])
res
inner <- newName "inner"
let innerFun = FunD inner [clause']
helper <- newName "helper"
let fun = FunD helper
[ Clause
[]
(NormalB $ VarE inner)
[innerFun]
]
return $ LetE [fun] (VarE helper)
subTopDispatch ::
(YesodSubDispatch sub master) =>
(forall content. ToTypedContent content =>
SubHandlerFor child master content ->
YesodSubRunnerEnv child master ->
Maybe (Route child) ->
W.Application
) ->
(mid -> sub) ->
(Route sub -> Route mid) ->
YesodSubRunnerEnv mid master ->
W.Application
subTopDispatch _ getSub toParent env = yesodSubDispatch
(YesodSubRunnerEnv
{ ysreParentRunner = ysreParentRunner env
, ysreGetSub = getSub . ysreGetSub env
, ysreToParentRoute = ysreToParentRoute env . toParent
, ysreParentEnv = ysreParentEnv env
})
instanceD :: Cxt -> Type -> [Dec] -> Dec
instanceD = InstanceD Nothing

View File

@ -13,12 +13,7 @@ import Data.Serialize (Get, Put, Serialize (..))
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time (Day (ModifiedJulianDay, toModifiedJulianDay), import Data.Time (Day (ModifiedJulianDay, toModifiedJulianDay),
DiffTime, UTCTime (..), formatTime, DiffTime, UTCTime (..), formatTime,
getCurrentTime, addUTCTime) getCurrentTime, addUTCTime, defaultTimeLocale)
#if MIN_VERSION_time(1,5,0)
import Data.Time (defaultTimeLocale)
#else
import System.Locale (defaultTimeLocale)
#endif
putTime :: UTCTime -> Put putTime :: UTCTime -> Put
putTime (UTCTime d t) = putTime (UTCTime d t) =

View File

@ -6,12 +6,15 @@ module Yesod.Core.Json
defaultLayoutJson defaultLayoutJson
, jsonToRepJson , jsonToRepJson
, returnJson , returnJson
#if MIN_VERSION_aeson(0, 11, 0)
, returnJsonEncoding , returnJsonEncoding
#endif
, provideJson , provideJson
-- * Convert to a JSON value -- * Convert to a JSON value
, parseCheckJsonBody
, parseInsecureJsonBody
, requireCheckJsonBody
, requireInsecureJsonBody
-- ** Deprecated JSON conversion
, parseJsonBody , parseJsonBody
, parseJsonBody_ , parseJsonBody_
, requireJsonBody , requireJsonBody
@ -27,20 +30,21 @@ module Yesod.Core.Json
-- * Convenience functions -- * Convenience functions
, jsonOrRedirect , jsonOrRedirect
#if MIN_VERSION_aeson(0, 11, 0)
, jsonEncodingOrRedirect , jsonEncodingOrRedirect
#endif
, acceptsJson , acceptsJson
-- * Checking if data is JSON
, contentTypeHeaderIsJson
) where ) where
import Yesod.Core.Handler (HandlerT, getRequest, invalidArgs, redirect, selectRep, provideRep, rawRequestBody, ProvidedRep) import Yesod.Core.Handler (HandlerFor, getRequest, invalidArgs, redirect, selectRep, provideRep, rawRequestBody, ProvidedRep, lookupHeader)
import Control.Monad.Trans.Writer (Writer) import Control.Monad.Trans.Writer (Writer)
import Data.Monoid (Endo) import Data.Monoid (Endo)
import Yesod.Core.Content (TypedContent) import Yesod.Core.Content (TypedContent)
import Yesod.Core.Types (reqAccept) import Yesod.Core.Types (reqAccept)
import Yesod.Core.Class.Yesod (defaultLayout, Yesod) import Yesod.Core.Class.Yesod (defaultLayout, Yesod)
import Yesod.Core.Class.Handler import Yesod.Core.Class.Handler
import Yesod.Core.Widget (WidgetT) import Yesod.Core.Widget (WidgetFor)
import Yesod.Routes.Class import Yesod.Routes.Class
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.Aeson.Parser as JP import qualified Data.Aeson.Parser as JP
@ -60,16 +64,12 @@ import Control.Monad (liftM)
-- --
-- @since 0.3.0 -- @since 0.3.0
defaultLayoutJson :: (Yesod site, J.ToJSON a) defaultLayoutJson :: (Yesod site, J.ToJSON a)
=> WidgetT site IO () -- ^ HTML => WidgetFor site () -- ^ HTML
-> HandlerT site IO a -- ^ JSON -> HandlerFor site a -- ^ JSON
-> HandlerT site IO TypedContent -> HandlerFor site TypedContent
defaultLayoutJson w json = selectRep $ do defaultLayoutJson w json = selectRep $ do
provideRep $ defaultLayout w provideRep $ defaultLayout w
#if MIN_VERSION_aeson(0, 11, 0)
provideRep $ fmap J.toEncoding json provideRep $ fmap J.toEncoding json
#else
provideRep $ fmap J.toJSON json
#endif
-- | Wraps a data type in a 'RepJson'. The data type must -- | Wraps a data type in a 'RepJson'. The data type must
-- support conversion to JSON via 'J.ToJSON'. -- support conversion to JSON via 'J.ToJSON'.
@ -85,53 +85,90 @@ jsonToRepJson = return . J.toJSON
returnJson :: (Monad m, J.ToJSON a) => a -> m J.Value returnJson :: (Monad m, J.ToJSON a) => a -> m J.Value
returnJson = return . J.toJSON returnJson = return . J.toJSON
#if MIN_VERSION_aeson(0, 11, 0)
-- | Convert a value to a JSON representation via aeson\'s 'J.toEncoding' function. -- | Convert a value to a JSON representation via aeson\'s 'J.toEncoding' function.
-- --
-- @since 1.4.21 -- @since 1.4.21
returnJsonEncoding :: (Monad m, J.ToJSON a) => a -> m J.Encoding returnJsonEncoding :: (Monad m, J.ToJSON a) => a -> m J.Encoding
returnJsonEncoding = return . J.toEncoding returnJsonEncoding = return . J.toEncoding
#endif
-- | Provide a JSON representation for usage with 'selectReps', using aeson\'s -- | Provide a JSON representation for usage with 'selectReps', using aeson\'s
-- 'J.toJSON' (aeson >= 0.11: 'J.toEncoding') function to perform the conversion. -- 'J.toJSON' (aeson >= 0.11: 'J.toEncoding') function to perform the conversion.
-- --
-- @since 1.2.1 -- @since 1.2.1
provideJson :: (Monad m, J.ToJSON a) => a -> Writer (Endo [ProvidedRep m]) () provideJson :: (Monad m, J.ToJSON a) => a -> Writer (Endo [ProvidedRep m]) ()
#if MIN_VERSION_aeson(0, 11, 0)
provideJson = provideRep . return . J.toEncoding provideJson = provideRep . return . J.toEncoding
#else
provideJson = provideRep . return . J.toJSON -- | Same as 'parseInsecureJsonBody'
#endif --
-- @since 0.3.0
parseJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a)
parseJsonBody = parseInsecureJsonBody
{-# DEPRECATED parseJsonBody "Use parseCheckJsonBody or parseInsecureJsonBody instead" #-}
-- | Same as 'parseCheckJsonBody', but does not check that the mime type
-- indicates JSON content.
--
-- Note: This function is vulnerable to CSRF attacks.
--
-- @since 1.6.11
parseInsecureJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a)
parseInsecureJsonBody = do
eValue <- runConduit $ rawRequestBody .| runCatchC (sinkParser JP.value')
return $ case eValue of
Left e -> J.Error $ show e
Right value -> J.fromJSON value
-- | Parse the request body to a data type as a JSON value. The -- | Parse the request body to a data type as a JSON value. The
-- data type must support conversion from JSON via 'J.FromJSON'. -- data type must support conversion from JSON via 'J.FromJSON'.
-- If you want the raw JSON value, just ask for a @'J.Result' -- If you want the raw JSON value, just ask for a @'J.Result'
-- 'J.Value'@. -- 'J.Value'@.
-- --
-- The MIME type must indicate JSON content. Requiring a JSON
-- content-type helps secure your site against CSRF attacks
-- (browsers will perform POST requests for form and text/plain
-- content-types without doing a CORS check, and those content-types
-- can easily contain valid JSON).
--
-- Note that this function will consume the request body. As such, calling it -- Note that this function will consume the request body. As such, calling it
-- twice will result in a parse error on the second call, since the request -- twice will result in a parse error on the second call, since the request
-- body will no longer be available. -- body will no longer be available.
-- --
-- @since 0.3.0 -- @since 0.3.0
parseJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a) parseCheckJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a)
parseJsonBody = do parseCheckJsonBody = do
eValue <- rawRequestBody $$ runCatchC (sinkParser JP.value') mct <- lookupHeader "content-type"
return $ case eValue of case fmap contentTypeHeaderIsJson mct of
Left e -> J.Error $ show e Just True -> parseInsecureJsonBody
Right value -> J.fromJSON value _ -> return $ J.Error $ "Non-JSON content type: " ++ show mct
-- | Same as 'parseJsonBody', but return an invalid args response on a parse -- | Same as 'parseInsecureJsonBody', but return an invalid args response on a parse
-- error. -- error.
parseJsonBody_ :: (MonadHandler m, J.FromJSON a) => m a parseJsonBody_ :: (MonadHandler m, J.FromJSON a) => m a
parseJsonBody_ = requireJsonBody parseJsonBody_ = requireInsecureJsonBody
{-# DEPRECATED parseJsonBody_ "Use requireJsonBody instead" #-} {-# DEPRECATED parseJsonBody_ "Use requireCheckJsonBody or requireInsecureJsonBody instead" #-}
-- | Same as 'parseJsonBody', but return an invalid args response on a parse -- | Same as 'parseInsecureJsonBody', but return an invalid args response on a parse
-- error. -- error.
requireJsonBody :: (MonadHandler m, J.FromJSON a) => m a requireJsonBody :: (MonadHandler m, J.FromJSON a) => m a
requireJsonBody = do requireJsonBody = requireInsecureJsonBody
ra <- parseJsonBody {-# DEPRECATED requireJsonBody "Use requireCheckJsonBody or requireInsecureJsonBody instead" #-}
-- | Same as 'parseInsecureJsonBody', but return an invalid args response on a parse
-- error.
--
-- @since 1.6.11
requireInsecureJsonBody :: (MonadHandler m, J.FromJSON a) => m a
requireInsecureJsonBody = do
ra <- parseInsecureJsonBody
case ra of
J.Error s -> invalidArgs [pack s]
J.Success a -> return a
-- | Same as 'parseCheckJsonBody', but return an invalid args response on a parse
-- error.
requireCheckJsonBody :: (MonadHandler m, J.FromJSON a) => m a
requireCheckJsonBody = do
ra <- parseCheckJsonBody
case ra of case ra of
J.Error s -> invalidArgs [pack s] J.Error s -> invalidArgs [pack s]
J.Success a -> return a J.Success a -> return a
@ -153,7 +190,6 @@ jsonOrRedirect :: (MonadHandler m, J.ToJSON a)
-> m J.Value -> m J.Value
jsonOrRedirect = jsonOrRedirect' J.toJSON jsonOrRedirect = jsonOrRedirect' J.toJSON
#if MIN_VERSION_aeson(0, 11, 0)
-- | jsonEncodingOrRedirect simplifies the scenario where a POST handler sends a different -- | jsonEncodingOrRedirect simplifies the scenario where a POST handler sends a different
-- response based on Accept headers: -- response based on Accept headers:
-- --
@ -167,9 +203,8 @@ jsonEncodingOrRedirect :: (MonadHandler m, J.ToJSON a)
-> a -- ^ Data to send via JSON -> a -- ^ Data to send via JSON
-> m J.Encoding -> m J.Encoding
jsonEncodingOrRedirect = jsonOrRedirect' J.toEncoding jsonEncodingOrRedirect = jsonOrRedirect' J.toEncoding
#endif
jsonOrRedirect' :: (MonadHandler m, J.ToJSON a) jsonOrRedirect' :: MonadHandler m
=> (a -> b) => (a -> b)
-> Route (HandlerSite m) -- ^ Redirect target -> Route (HandlerSite m) -- ^ Redirect target
-> a -- ^ Data to send via JSON -> a -- ^ Data to send via JSON
@ -186,3 +221,12 @@ acceptsJson = (maybe False ((== "application/json") . B8.takeWhile (/= ';'))
. listToMaybe . listToMaybe
. reqAccept) . reqAccept)
`liftM` getRequest `liftM` getRequest
-- | Given the @Content-Type@ header, returns if it is JSON.
--
-- This function is currently a simple check for @application/json@, but in the future may check for
-- alternative representations such as @<https://tools.ietf.org/html/rfc6839#section-3.1 xxx/yyy+json>@.
--
-- @since 1.6.17
contentTypeHeaderIsJson :: B8.ByteString -> Bool
contentTypeHeaderIsJson bs = B8.takeWhile (/= ';') bs == "application/json"

View File

@ -7,7 +7,7 @@
-- Note that a TypeRep is unique to a module in a package, so types from different modules will not conflict if they have the same name. -- Note that a TypeRep is unique to a module in a package, so types from different modules will not conflict if they have the same name.
-- --
-- used in 'Yesod.Core.Handler.cached' and 'Yesod.Core.Handler.cachedBy' -- used in 'Yesod.Core.Handler.cached' and 'Yesod.Core.Handler.cachedBy'
module Yesod.Core.TypeCache (cached, cachedBy, TypeMap, KeyedTypeMap) where module Yesod.Core.TypeCache (cached, cacheGet, cacheSet, cachedBy, cacheByGet, cacheBySet, TypeMap, KeyedTypeMap) where
import Prelude hiding (lookup) import Prelude hiding (lookup)
import Data.Typeable (Typeable, TypeRep, typeOf) import Data.Typeable (Typeable, TypeRep, typeOf)
@ -33,22 +33,30 @@ cached :: (Monad m, Typeable a)
=> TypeMap => TypeMap
-> m a -- ^ cache the result of this action -> m a -- ^ cache the result of this action
-> m (Either (TypeMap, a) a) -- ^ Left is a cache miss, Right is a hit -> m (Either (TypeMap, a) a) -- ^ Left is a cache miss, Right is a hit
cached cache action = case clookup cache of cached cache action = case cacheGet cache of
Just val -> return $ Right val Just val -> return $ Right val
Nothing -> do Nothing -> do
val <- action val <- action
return $ Left (cinsert val cache, val) return $ Left (cacheSet val cache, val)
-- | Retrieves a value from the cache
--
-- @since 1.6.10
cacheGet :: Typeable a => TypeMap -> Maybe a
cacheGet cache = res
where where
clookup :: Typeable a => TypeMap -> Maybe a res = lookup (typeOf $ fromJust res) cache >>= fromDynamic
clookup c =
res
where
res = lookup (typeOf $ fromJust res) c >>= fromDynamic
fromJust :: Maybe a -> a fromJust :: Maybe a -> a
fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated" fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated"
cinsert :: Typeable a => a -> TypeMap -> TypeMap -- | Sets a value in the cache
cinsert v = insert (typeOf v) (toDyn v) --
-- @since 1.6.10
cacheSet :: (Typeable a)
=> a
-> TypeMap
-> TypeMap
cacheSet v cache = insert (typeOf v) (toDyn v) cache
-- | similar to 'cached'. -- | similar to 'cached'.
-- 'cached' can only cache a single value per type. -- 'cached' can only cache a single value per type.
@ -65,19 +73,24 @@ cachedBy :: (Monad m, Typeable a)
-> ByteString -- ^ a cache key -> ByteString -- ^ a cache key
-> m a -- ^ cache the result of this action -> m a -- ^ cache the result of this action
-> m (Either (KeyedTypeMap, a) a) -- ^ Left is a cache miss, Right is a hit -> m (Either (KeyedTypeMap, a) a) -- ^ Left is a cache miss, Right is a hit
cachedBy cache k action = case clookup k cache of cachedBy cache k action = case cacheByGet k cache of
Just val -> return $ Right val Just val -> return $ Right val
Nothing -> do Nothing -> do
val <- action val <- action
return $ Left (cinsert k val cache, val) return $ Left (cacheBySet k val cache, val)
where
clookup :: Typeable a => ByteString -> KeyedTypeMap -> Maybe a -- | Retrieves a value from the keyed cache
clookup key c = --
res -- @since 1.6.10
cacheByGet :: Typeable a => ByteString -> KeyedTypeMap -> Maybe a
cacheByGet key c = res
where where
res = lookup (typeOf $ fromJust res, key) c >>= fromDynamic res = lookup (typeOf $ fromJust res, key) c >>= fromDynamic
fromJust :: Maybe a -> a fromJust :: Maybe a -> a
fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated" fromJust = error "Yesod.Handler.cached.fromJust: Argument to typeOf was evaluated"
cinsert :: Typeable a => ByteString -> a -> KeyedTypeMap -> KeyedTypeMap -- | Sets a value in the keyed cache
cinsert key v = insert (typeOf v, key) (toDyn v) --
-- @since 1.6.10
cacheBySet :: Typeable a => ByteString -> a -> KeyedTypeMap -> KeyedTypeMap
cacheBySet key v cache = insert (typeOf v, key) (toDyn v) cache

View File

@ -1,5 +1,5 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
@ -7,32 +7,29 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
module Yesod.Core.Types where module Yesod.Core.Types where
import qualified Blaze.ByteString.Builder as BBuilder import Data.Aeson (ToJSON)
import qualified Blaze.ByteString.Builder.Char.Utf8 import qualified Data.ByteString.Builder as BB
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative (Applicative (..))
import Control.Applicative ((<$>))
import Data.Monoid (Monoid (..))
#endif
import Control.Arrow (first) import Control.Arrow (first)
import Control.Exception (Exception) import Control.Exception (Exception)
import Control.Monad (liftM, ap) import Control.Monad (ap)
import Control.Monad.Base (MonadBase (liftBase))
import Control.Monad.Catch (MonadMask (..), MonadCatch (..))
import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Logger (LogLevel, LogSource, import Control.Monad.Logger (LogLevel, LogSource,
MonadLogger (..)) MonadLogger (..))
import Control.Monad.Trans.Control (MonadBaseControl (..)) import Control.Monad.Primitive (PrimMonad (..))
import Control.Monad.Trans.Resource (MonadResource (..), InternalState, runInternalState, MonadThrow (..), monadThrow, ResourceT) import Control.Monad.Trans.Resource (MonadResource (..), InternalState, runInternalState, MonadThrow (..), ResourceT)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import Data.Conduit (Flush, Source) import Data.CaseInsensitive (CI)
import Data.IORef (IORef) import Data.Conduit (Flush, ConduitT)
import Data.IORef (IORef, modifyIORef')
import Data.Map (Map, unionWith) import Data.Map (Map, unionWith)
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Monoid (Endo (..), Last (..)) import Data.Monoid (Endo (..), Last (..))
import Data.Semigroup (Semigroup(..))
import Data.Serialize (Serialize (..), import Data.Serialize (Serialize (..),
putByteString) putByteString)
import Data.String (IsString (fromString)) import Data.String (IsString (fromString))
@ -40,7 +37,6 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as TBuilder import qualified Data.Text.Lazy.Builder as TBuilder
import Data.Time (UTCTime) import Data.Time (UTCTime)
import Data.Typeable (Typeable)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Language.Haskell.TH.Syntax (Loc) import Language.Haskell.TH.Syntax (Loc)
import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types as H
@ -49,27 +45,18 @@ import Network.Wai (FilePart,
import qualified Network.Wai as W import qualified Network.Wai as W
import qualified Network.Wai.Parse as NWP import qualified Network.Wai.Parse as NWP
import System.Log.FastLogger (LogStr, LoggerSet, toLogStr, pushLogStr) import System.Log.FastLogger (LogStr, LoggerSet, toLogStr, pushLogStr)
import qualified System.Random.MWC as MWC
import Network.Wai.Logger (DateCacheGetter) import Network.Wai.Logger (DateCacheGetter)
import Text.Blaze.Html (Html, toHtml) import Text.Blaze.Html (Html, toHtml)
import Text.Hamlet (HtmlUrl) import Text.Hamlet (HtmlUrl)
import Text.Julius (JavascriptUrl) import Text.Julius (JavascriptUrl)
import Web.Cookie (SetCookie) import Web.Cookie (SetCookie)
import Yesod.Core.Internal.Util (getTime, putTime) import Yesod.Core.Internal.Util (getTime, putTime)
import Control.Monad.Trans.Class (MonadTrans (..))
import Yesod.Routes.Class (RenderRoute (..), ParseRoute (..)) import Yesod.Routes.Class (RenderRoute (..), ParseRoute (..))
import Control.Monad.Reader (MonadReader (..)) import Control.Monad.Reader (MonadReader (..))
#if !MIN_VERSION_base(4, 6, 0)
import Prelude hiding (catch)
#endif
import Control.DeepSeq (NFData (rnf)) import Control.DeepSeq (NFData (rnf))
import Control.DeepSeq.Generics (genericRnf)
import Data.Conduit.Lazy (MonadActive, monadActive)
import Yesod.Core.TypeCache (TypeMap, KeyedTypeMap) import Yesod.Core.TypeCache (TypeMap, KeyedTypeMap)
#if MIN_VERSION_monad_logger(0, 3, 10)
import Control.Monad.Logger (MonadLoggerIO (..)) import Control.Monad.Logger (MonadLoggerIO (..))
#endif import UnliftIO (MonadUnliftIO (..), SomeException)
import Data.Semigroup (Semigroup)
-- Sessions -- Sessions
type SessionMap = Map Text ByteString type SessionMap = Map Text ByteString
@ -82,7 +69,7 @@ newtype SessionBackend = SessionBackend
-> IO (SessionMap, SaveSession) -- ^ Return the session data and a function to save the session -> IO (SessionMap, SaveSession) -- ^ Return the session data and a function to save the session
} }
data SessionCookie = SessionCookie (Either UTCTime ByteString) ByteString SessionMap data SessionCookie = SessionCookie !(Either UTCTime ByteString) !ByteString !SessionMap
deriving (Show, Read) deriving (Show, Read)
instance Serialize SessionCookie where instance Serialize SessionCookie where
put (SessionCookie a b c) = do put (SessionCookie a b c) = do
@ -140,13 +127,13 @@ type RequestBodyContents =
data FileInfo = FileInfo data FileInfo = FileInfo
{ fileName :: !Text { fileName :: !Text
, fileContentType :: !Text , fileContentType :: !Text
, fileSourceRaw :: !(Source (ResourceT IO) ByteString) , fileSourceRaw :: !(ConduitT () ByteString (ResourceT IO) ())
, fileMove :: !(FilePath -> IO ()) , fileMove :: !(FilePath -> IO ())
} }
data FileUpload = FileUploadMemory !(NWP.BackEnd L.ByteString) data FileUpload = FileUploadMemory !(NWP.BackEnd L.ByteString)
| FileUploadDisk !(InternalState -> NWP.BackEnd FilePath) | FileUploadDisk !(InternalState -> NWP.BackEnd FilePath)
| FileUploadSource !(NWP.BackEnd (Source (ResourceT IO) ByteString)) | FileUploadSource !(NWP.BackEnd (ConduitT () ByteString (ResourceT IO) ()))
-- | How to determine the root of the application for constructing URLs. -- | How to determine the root of the application for constructing URLs.
-- --
@ -160,13 +147,13 @@ data Approot master = ApprootRelative -- ^ No application root.
type ResolvedApproot = Text type ResolvedApproot = Text
data AuthResult = Authorized | AuthenticationRequired | Unauthorized Text data AuthResult = Authorized | AuthenticationRequired | Unauthorized !Text
deriving (Eq, Show, Read) deriving (Eq, Show, Read)
data ScriptLoadPosition master data ScriptLoadPosition master
= BottomOfBody = BottomOfBody
| BottomOfHeadBlocking | BottomOfHeadBlocking
| BottomOfHeadAsync (BottomOfHeadAsync master) | BottomOfHeadAsync !(BottomOfHeadAsync master)
type BottomOfHeadAsync master type BottomOfHeadAsync master
= [Text] -- ^ urls to load asynchronously = [Text] -- ^ urls to load asynchronously
@ -175,13 +162,20 @@ type BottomOfHeadAsync master
type Texts = [Text] type Texts = [Text]
-- | Wrap up a normal WAI application as a Yesod subsite. -- | Wrap up a normal WAI application as a Yesod subsite. Ignore parent site's middleware and isAuthorized.
newtype WaiSubsite = WaiSubsite { runWaiSubsite :: W.Application } newtype WaiSubsite = WaiSubsite { runWaiSubsite :: W.Application }
data RunHandlerEnv site = RunHandlerEnv -- | Like 'WaiSubsite', but applies parent site's middleware and isAuthorized.
--
-- @since 1.4.34
newtype WaiSubsiteWithAuth = WaiSubsiteWithAuth { runWaiSubsiteWithAuth :: W.Application }
data RunHandlerEnv child site = RunHandlerEnv
{ rheRender :: !(Route site -> [(Text, Text)] -> Text) { rheRender :: !(Route site -> [(Text, Text)] -> Text)
, rheRoute :: !(Maybe (Route site)) , rheRoute :: !(Maybe (Route child))
, rheRouteToMaster :: !(Route child -> Route site)
, rheSite :: !site , rheSite :: !site
, rheChild :: !child
, rheUpload :: !(RequestBodyLength -> FileUpload) , rheUpload :: !(RequestBodyLength -> FileUpload)
, rheLog :: !(Loc -> LogSource -> LogLevel -> LogStr -> IO ()) , rheLog :: !(Loc -> LogSource -> LogLevel -> LogStr -> IO ())
, rheOnError :: !(ErrorResponse -> YesodApp) , rheOnError :: !(ErrorResponse -> YesodApp)
@ -189,13 +183,17 @@ data RunHandlerEnv site = RunHandlerEnv
-- --
-- Since 1.2.0 -- Since 1.2.0
, rheMaxExpires :: !Text , rheMaxExpires :: !Text
-- | @since 1.6.24.0
-- catch function for rendering 500 pages on exceptions.
-- by default this is catch from unliftio (rethrows all async exceptions).
, rheCatchHandlerExceptions :: !(forall a m . MonadUnliftIO m => m a -> (SomeException -> m a) -> m a)
} }
data HandlerData site parentRoute = HandlerData data HandlerData child site = HandlerData
{ handlerRequest :: !YesodRequest { handlerRequest :: !YesodRequest
, handlerEnv :: !(RunHandlerEnv site) , handlerEnv :: !(RunHandlerEnv child site)
, handlerState :: !(IORef GHState) , handlerState :: !(IORef GHState)
, handlerToParent :: !(Route site -> parentRoute)
, handlerResource :: !InternalState , handlerResource :: !InternalState
} }
@ -203,68 +201,83 @@ data YesodRunnerEnv site = YesodRunnerEnv
{ yreLogger :: !Logger { yreLogger :: !Logger
, yreSite :: !site , yreSite :: !site
, yreSessionBackend :: !(Maybe SessionBackend) , yreSessionBackend :: !(Maybe SessionBackend)
, yreGen :: !MWC.GenIO , yreGen :: !(IO Int)
, yreGetMaxExpires :: IO Text -- ^ Generate a random number uniformly distributed in the full
-- range of 'Int'.
--
-- Note: Before 1.6.20, the default value generates pseudo-random
-- number in an unspecified range. The range size may not be a power
-- of 2. Since 1.6.20, the default value uses a secure entropy source
-- and generates in the full range of 'Int'.
, yreGetMaxExpires :: !(IO Text)
} }
data YesodSubRunnerEnv sub parent parentMonad = YesodSubRunnerEnv data YesodSubRunnerEnv sub parent = YesodSubRunnerEnv
{ ysreParentRunner :: !(ParentRunner parent parentMonad) { ysreParentRunner :: !(ParentRunner parent)
, ysreGetSub :: !(parent -> sub) , ysreGetSub :: !(parent -> sub)
, ysreToParentRoute :: !(Route sub -> Route parent) , ysreToParentRoute :: !(Route sub -> Route parent)
, ysreParentEnv :: !(YesodRunnerEnv parent) -- FIXME maybe get rid of this and remove YesodRunnerEnv in ParentRunner? , ysreParentEnv :: !(YesodRunnerEnv parent) -- FIXME maybe get rid of this and remove YesodRunnerEnv in ParentRunner?
} }
type ParentRunner parent m type ParentRunner parent
= m TypedContent = HandlerFor parent TypedContent
-> YesodRunnerEnv parent -> YesodRunnerEnv parent
-> Maybe (Route parent) -> Maybe (Route parent)
-> W.Application -> W.Application
-- | A generic handler monad, which can have a different subsite and master -- | A generic handler monad, which can have a different subsite and master
-- site. We define a newtype for better error message. -- site. We define a newtype for better error message.
newtype HandlerT site m a = HandlerT newtype HandlerFor site a = HandlerFor
{ unHandlerT :: HandlerData site (MonadRoute m) -> m a { unHandlerFor :: HandlerData site site -> IO a
} }
deriving Functor
type family MonadRoute (m :: * -> *)
type instance MonadRoute IO = ()
type instance MonadRoute (HandlerT site m) = (Route site)
data GHState = GHState data GHState = GHState
{ ghsSession :: SessionMap { ghsSession :: !SessionMap
, ghsRBC :: Maybe RequestBodyContents , ghsRBC :: !(Maybe RequestBodyContents)
, ghsIdent :: Int , ghsIdent :: !Int
, ghsCache :: TypeMap , ghsCache :: !TypeMap
, ghsCacheBy :: KeyedTypeMap , ghsCacheBy :: !KeyedTypeMap
, ghsHeaders :: Endo [Header] , ghsHeaders :: !(Endo [Header])
} }
-- | An extension of the basic WAI 'W.Application' datatype to provide extra -- | An extension of the basic WAI 'W.Application' datatype to provide extra
-- features needed by Yesod. Users should never need to use this directly, as -- features needed by Yesod. Users should never need to use this directly, as
-- the 'HandlerT' monad and template haskell code should hide it away. -- the 'HandlerFor' monad and template haskell code should hide it away.
type YesodApp = YesodRequest -> ResourceT IO YesodResponse type YesodApp = YesodRequest -> ResourceT IO YesodResponse
-- | A generic widget, allowing specification of both the subsite and master -- | A generic widget, allowing specification of both the subsite and master
-- site datatypes. While this is simply a @WriterT@, we define a newtype for -- site datatypes. While this is simply a @WriterT@, we define a newtype for
-- better error messages. -- better error messages.
newtype WidgetT site m a = WidgetT newtype WidgetFor site a = WidgetFor
{ unWidgetT :: HandlerData site (MonadRoute m) -> m (a, GWData (Route site)) { unWidgetFor :: WidgetData site -> IO a
}
deriving Functor
data WidgetData site = WidgetData
{ wdRef :: {-# UNPACK #-} !(IORef (GWData (Route site)))
, wdHandler :: {-# UNPACK #-} !(HandlerData site site)
} }
instance (a ~ (), Monad m) => Monoid (WidgetT site m a) where instance a ~ () => Monoid (WidgetFor site a) where
mempty = return () mempty = return ()
mappend x y = x >> y #if !(MIN_VERSION_base(4,11,0))
instance (a ~ (), Monad m) => Semigroup (WidgetT site m a) mappend = (<>)
#endif
instance a ~ () => Semigroup (WidgetFor site a) where
x <> y = x >> y
-- | A 'String' can be trivially promoted to a widget. -- | A 'String' can be trivially promoted to a widget.
-- --
-- For example, in a yesod-scaffold site you could use: -- For example, in a yesod-scaffold site you could use:
-- --
-- @getHomeR = do defaultLayout "Widget text"@ -- @getHomeR = do defaultLayout "Widget text"@
instance (Monad m, a ~ ()) => IsString (WidgetT site m a) where instance a ~ () => IsString (WidgetFor site a) where
fromString = toWidget . toHtml . T.pack fromString = toWidget . toHtml . T.pack
where toWidget x = WidgetT $ const $ return ((), GWData (Body (const x)) where toWidget x = tellWidget mempty { gwdBody = Body (const x) }
mempty mempty mempty mempty mempty mempty)
tellWidget :: GWData (Route site) -> WidgetFor site ()
tellWidget d = WidgetFor $ \wd -> modifyIORef' (wdRef wd) (<> d)
type RY master = Route master -> [(Text, Text)] -> Text type RY master = Route master -> [(Text, Text)] -> Text
@ -282,13 +295,14 @@ newtype CssBuilder = CssBuilder { unCssBuilder :: TBuilder.Builder }
-- --
-- > PageContent url -> HtmlUrl url -- > PageContent url -> HtmlUrl url
data PageContent url = PageContent data PageContent url = PageContent
{ pageTitle :: Html { pageTitle :: !Html
, pageHead :: HtmlUrl url , pageDescription :: !(Maybe Text)
, pageBody :: HtmlUrl url , pageHead :: !(HtmlUrl url)
, pageBody :: !(HtmlUrl url)
} }
data Content = ContentBuilder !BBuilder.Builder !(Maybe Int) -- ^ The content and optional content length. data Content = ContentBuilder !BB.Builder !(Maybe Int) -- ^ The content and optional content length.
| ContentSource !(Source (ResourceT IO) (Flush BBuilder.Builder)) | ContentSource !(ConduitT () (Flush BB.Builder) (ResourceT IO) ())
| ContentFile !FilePath !(Maybe FilePart) | ContentFile !FilePath !(Maybe FilePart)
| ContentDontEvaluate !Content | ContentDontEvaluate !Content
@ -302,6 +316,20 @@ newtype RepXml = RepXml Content
type ContentType = ByteString -- FIXME Text? type ContentType = ByteString -- FIXME Text?
-- | Wrapper around types so that Handlers can return a domain type, even when
-- the data will eventually be encoded as JSON.
-- Example usage in a type signature:
--
-- > postSignupR :: Handler (JSONResponse CreateUserResponse)
--
-- And in the implementation:
--
-- > return $ JSONResponse $ CreateUserResponse userId
--
-- @since 1.6.14
data JSONResponse a where
JSONResponse :: ToJSON a => a -> JSONResponse a
-- | Prevents a response body from being fully evaluated before sending the -- | Prevents a response body from being fully evaluated before sending the
-- request. -- request.
-- --
@ -311,21 +339,39 @@ newtype DontFullyEvaluate a = DontFullyEvaluate { unDontFullyEvaluate :: a }
-- | Responses to indicate some form of an error occurred. -- | Responses to indicate some form of an error occurred.
data ErrorResponse = data ErrorResponse =
NotFound NotFound
| InternalError Text -- ^ The requested resource was not found.
| InvalidArgs [Text] -- Examples of when this occurs include when an incorrect URL is used, or @yesod-persistent@'s 'get404' doesn't find a value.
-- HTTP status: 404.
| InternalError !Text
-- ^ Some sort of unexpected exception.
-- If your application uses `throwIO` or `error` to throw an exception, this is the form it would take.
-- HTTP status: 500.
| InvalidArgs ![Text]
-- ^ Indicates some sort of invalid or missing argument, like a missing query parameter or malformed JSON body.
-- Examples Yesod functions that send this include 'requireCheckJsonBody' and @Yesod.Auth.GoogleEmail2@.
-- HTTP status: 400.
| NotAuthenticated | NotAuthenticated
| PermissionDenied Text -- ^ Indicates the user is not logged in.
| BadMethod H.Method -- This is thrown when 'isAuthorized' returns 'AuthenticationRequired'.
deriving (Show, Eq, Typeable, Generic) -- HTTP code: 401.
instance NFData ErrorResponse where | PermissionDenied !Text
rnf = genericRnf -- ^ Indicates the user doesn't have permission to access the requested resource.
-- This is thrown when 'isAuthorized' returns 'Unauthorized'.
-- HTTP code: 403.
| BadMethod !H.Method
-- ^ Indicates the URL would have been valid if used with a different HTTP method (e.g. a GET was used, but only POST is handled.)
-- HTTP code: 405.
deriving (Show, Eq, Generic)
instance NFData ErrorResponse
----- header stuff ----- header stuff
-- | Headers to be added to a 'Result'. -- | Headers to be added to a 'Result'.
data Header = data Header =
AddCookie SetCookie AddCookie !SetCookie
| DeleteCookie ByteString ByteString | DeleteCookie !ByteString !ByteString
| Header ByteString ByteString -- ^ name and path
| Header !(CI ByteString) !ByteString
-- ^ key and value
deriving (Eq, Show) deriving (Eq, Show)
-- FIXME In the next major version bump, let's just add strictness annotations -- FIXME In the next major version bump, let's just add strictness annotations
@ -336,31 +382,35 @@ instance NFData Header where
rnf (DeleteCookie x y) = x `seq` y `seq` () rnf (DeleteCookie x y) = x `seq` y `seq` ()
rnf (Header x y) = x `seq` y `seq` () rnf (Header x y) = x `seq` y `seq` ()
data Location url = Local url | Remote Text data Location url = Local !url | Remote !Text
deriving (Show, Eq) deriving (Show, Eq)
-- | A diff list that does not directly enforce uniqueness. -- | A diff list that does not directly enforce uniqueness.
-- When creating a widget Yesod will use nub to make it unique. -- When creating a widget Yesod will use nub to make it unique.
newtype UniqueList x = UniqueList ([x] -> [x]) newtype UniqueList x = UniqueList ([x] -> [x])
data Script url = Script { scriptLocation :: Location url, scriptAttributes :: [(Text, Text)] } data Script url = Script { scriptLocation :: !(Location url), scriptAttributes :: ![(Text, Text)] }
deriving (Show, Eq) deriving (Show, Eq)
data Stylesheet url = Stylesheet { styleLocation :: Location url, styleAttributes :: [(Text, Text)] } data Stylesheet url = Stylesheet { styleLocation :: !(Location url), styleAttributes :: ![(Text, Text)] }
deriving (Show, Eq) deriving (Show, Eq)
newtype Title = Title { unTitle :: Html } newtype Title = Title { unTitle :: Html }
newtype Description = Description { unDescription :: Text }
newtype Head url = Head (HtmlUrl url) newtype Head url = Head (HtmlUrl url)
deriving Monoid deriving Monoid
instance Semigroup (Head a) instance Semigroup (Head url) where
(<>) = mappend
newtype Body url = Body (HtmlUrl url) newtype Body url = Body (HtmlUrl url)
deriving Monoid deriving Monoid
instance Semigroup (Body a) instance Semigroup (Body url) where
(<>) = mappend
type CssBuilderUrl a = (a -> [(Text, Text)] -> Text) -> TBuilder.Builder type CssBuilderUrl a = (a -> [(Text, Text)] -> Text) -> TBuilder.Builder
data GWData a = GWData data GWData a = GWData
{ gwdBody :: !(Body a) { gwdBody :: !(Body a)
, gwdTitle :: !(Last Title) , gwdTitle :: !(Last Title)
, gwdDescription :: !(Last Description)
, gwdScripts :: !(UniqueList (Script a)) , gwdScripts :: !(UniqueList (Script a))
, gwdStylesheets :: !(UniqueList (Stylesheet a)) , gwdStylesheets :: !(UniqueList (Stylesheet a))
, gwdCss :: !(Map (Maybe Text) (CssBuilderUrl a)) -- media type , gwdCss :: !(Map (Maybe Text) (CssBuilderUrl a)) -- media type
@ -368,27 +418,30 @@ data GWData a = GWData
, gwdHead :: !(Head a) , gwdHead :: !(Head a)
} }
instance Monoid (GWData a) where instance Monoid (GWData a) where
mempty = GWData mempty mempty mempty mempty mempty mempty mempty mempty = GWData mempty mempty mempty mempty mempty mempty mempty mempty
mappend (GWData a1 a2 a3 a4 a5 a6 a7) #if !(MIN_VERSION_base(4,11,0))
(GWData b1 b2 b3 b4 b5 b6 b7) = GWData mappend = (<>)
(a1 `mappend` b1) #endif
(a2 `mappend` b2) instance Semigroup (GWData a) where
(a3 `mappend` b3) GWData a1 a2 a3 a4 a5 a6 a7 a8 <>
(a4 `mappend` b4) GWData b1 b2 b3 b4 b5 b6 b7 b8 = GWData
(unionWith mappend a5 b5) (mappend a1 b1)
(a6 `mappend` b6) (mappend a2 b2)
(a7 `mappend` b7) (mappend a3 b3)
instance Semigroup (GWData a) (mappend a4 b4)
(mappend a5 b5)
(unionWith mappend a6 b6)
(mappend a7 b7)
(mappend a8 b8)
data HandlerContents = data HandlerContents =
HCContent H.Status !TypedContent HCContent !H.Status !TypedContent
| HCError ErrorResponse | HCError !ErrorResponse
| HCSendFile ContentType FilePath (Maybe FilePart) | HCSendFile !ContentType !FilePath !(Maybe FilePart)
| HCRedirect H.Status Text | HCRedirect !H.Status !Text
| HCCreated Text | HCCreated !Text
| HCWai W.Response | HCWai !W.Response
| HCWaiApp W.Application | HCWaiApp !W.Application
deriving Typeable
instance Show HandlerContents where instance Show HandlerContents where
show (HCContent status (TypedContent t _)) = "HCContent " ++ show (status, t) show (HCContent status (TypedContent t _)) = "HCContent " ++ show (status, t)
@ -400,153 +453,87 @@ instance Show HandlerContents where
show (HCWaiApp _) = "HCWaiApp" show (HCWaiApp _) = "HCWaiApp"
instance Exception HandlerContents instance Exception HandlerContents
-- Instances for WidgetT -- Instances for WidgetFor
instance Monad m => Functor (WidgetT site m) where instance Applicative (WidgetFor site) where
fmap = liftM pure = WidgetFor . const . pure
instance Monad m => Applicative (WidgetT site m) where
pure = return
(<*>) = ap (<*>) = ap
instance Monad m => Monad (WidgetT site m) where instance Monad (WidgetFor site) where
return a = WidgetT $ const $ return (a, mempty) return = pure
WidgetT x >>= f = WidgetT $ \r -> do WidgetFor x >>= f = WidgetFor $ \wd -> do
(a, wa) <- x r a <- x wd
(b, wb) <- unWidgetT (f a) r unWidgetFor (f a) wd
return (b, wa `mappend` wb) instance MonadIO (WidgetFor site) where
instance MonadIO m => MonadIO (WidgetT site m) where liftIO = WidgetFor . const
liftIO = lift . liftIO -- | @since 1.6.7
instance MonadBase b m => MonadBase b (WidgetT site m) where instance PrimMonad (WidgetFor site) where
liftBase = WidgetT . const . liftBase . fmap (, mempty) type PrimState (WidgetFor site) = PrimState IO
instance MonadBaseControl b m => MonadBaseControl b (WidgetT site m) where primitive = liftIO . primitive
#if MIN_VERSION_monad_control(1,0,0) -- | @since 1.4.38
type StM (WidgetT site m) a = StM m (a, GWData (Route site)) instance MonadUnliftIO (WidgetFor site) where
liftBaseWith f = WidgetT $ \reader' -> {-# INLINE withRunInIO #-}
liftBaseWith $ \runInBase -> withRunInIO inner = WidgetFor $ \x -> inner $ flip unWidgetFor x
fmap (\x -> (x, mempty)) instance MonadReader (WidgetData site) (WidgetFor site) where
(f $ runInBase . flip unWidgetT reader') ask = WidgetFor return
restoreM = WidgetT . const . restoreM local f (WidgetFor g) = WidgetFor $ g . f
#else
data StM (WidgetT site m) a = StW (StM m (a, GWData (Route site)))
liftBaseWith f = WidgetT $ \reader' ->
liftBaseWith $ \runInBase ->
fmap (\x -> (x, mempty))
(f $ fmap StW . runInBase . flip unWidgetT reader')
restoreM (StW base) = WidgetT $ const $ restoreM base
#endif
instance Monad m => MonadReader site (WidgetT site m) where
ask = WidgetT $ \hd -> return (rheSite $ handlerEnv hd, mempty)
local f (WidgetT g) = WidgetT $ \hd -> g hd
{ handlerEnv = (handlerEnv hd)
{ rheSite = f $ rheSite $ handlerEnv hd
}
}
instance MonadTrans (WidgetT site) where instance MonadThrow (WidgetFor site) where
lift = WidgetT . const . liftM (, mempty) throwM = liftIO . throwM
instance MonadThrow m => MonadThrow (WidgetT site m) where
throwM = lift . throwM
instance MonadCatch m => MonadCatch (HandlerT site m) where instance MonadResource (WidgetFor site) where
catch (HandlerT m) c = HandlerT $ \r -> m r `catch` \e -> unHandlerT (c e) r liftResourceT f = WidgetFor $ runInternalState f . handlerResource . wdHandler
instance MonadMask m => MonadMask (HandlerT site m) where
mask a = HandlerT $ \e -> mask $ \u -> unHandlerT (a $ q u) e
where q u (HandlerT b) = HandlerT (u . b)
uninterruptibleMask a =
HandlerT $ \e -> uninterruptibleMask $ \u -> unHandlerT (a $ q u) e
where q u (HandlerT b) = HandlerT (u . b)
instance MonadCatch m => MonadCatch (WidgetT site m) where
catch (WidgetT m) c = WidgetT $ \r -> m r `catch` \e -> unWidgetT (c e) r
instance MonadMask m => MonadMask (WidgetT site m) where
mask a = WidgetT $ \e -> mask $ \u -> unWidgetT (a $ q u) e
where q u (WidgetT b) = WidgetT (u . b)
uninterruptibleMask a =
WidgetT $ \e -> uninterruptibleMask $ \u -> unWidgetT (a $ q u) e
where q u (WidgetT b) = WidgetT (u . b)
instance (Applicative m, MonadIO m, MonadBase IO m, MonadThrow m) => MonadResource (WidgetT site m) where instance MonadLogger (WidgetFor site) where
liftResourceT f = WidgetT $ \hd -> liftIO $ (, mempty) <$> runInternalState f (handlerResource hd) monadLoggerLog a b c d = WidgetFor $ \wd ->
rheLog (handlerEnv $ wdHandler wd) a b c (toLogStr d)
instance MonadIO m => MonadLogger (WidgetT site m) where instance MonadLoggerIO (WidgetFor site) where
monadLoggerLog a b c d = WidgetT $ \hd -> askLoggerIO = WidgetFor $ return . rheLog . handlerEnv . wdHandler
liftIO $ (, mempty) <$> rheLog (handlerEnv hd) a b c (toLogStr d)
#if MIN_VERSION_monad_logger(0, 3, 10) -- Instances for HandlerFor
instance MonadIO m => MonadLoggerIO (WidgetT site m) where instance Applicative (HandlerFor site) where
askLoggerIO = WidgetT $ \hd -> return (rheLog (handlerEnv hd), mempty) pure = HandlerFor . const . return
#endif
instance MonadActive m => MonadActive (WidgetT site m) where
monadActive = lift monadActive
instance MonadActive m => MonadActive (HandlerT site m) where
monadActive = lift monadActive
instance MonadTrans (HandlerT site) where
lift = HandlerT . const
-- Instances for HandlerT
instance Monad m => Functor (HandlerT site m) where
fmap = liftM
instance Monad m => Applicative (HandlerT site m) where
pure = return
(<*>) = ap (<*>) = ap
instance Monad m => Monad (HandlerT site m) where instance Monad (HandlerFor site) where
return = HandlerT . const . return return = pure
HandlerT x >>= f = HandlerT $ \r -> x r >>= \x' -> unHandlerT (f x') r HandlerFor x >>= f = HandlerFor $ \r -> x r >>= \x' -> unHandlerFor (f x') r
instance MonadIO m => MonadIO (HandlerT site m) where instance MonadIO (HandlerFor site) where
liftIO = lift . liftIO liftIO = HandlerFor . const
instance MonadBase b m => MonadBase b (HandlerT site m) where -- | @since 1.6.7
liftBase = lift . liftBase instance PrimMonad (HandlerFor site) where
instance Monad m => MonadReader site (HandlerT site m) where type PrimState (HandlerFor site) = PrimState IO
ask = HandlerT $ return . rheSite . handlerEnv primitive = liftIO . primitive
local f (HandlerT g) = HandlerT $ \hd -> g hd instance MonadReader (HandlerData site site) (HandlerFor site) where
{ handlerEnv = (handlerEnv hd) ask = HandlerFor return
{ rheSite = f $ rheSite $ handlerEnv hd local f (HandlerFor g) = HandlerFor $ g . f
}
}
-- | Note: although we provide a @MonadBaseControl@ instance, @lifted-base@'s
-- @fork@ function is incompatible with the underlying @ResourceT@ system.
-- Instead, if you must fork a separate thread, you should use
-- @resourceForkIO@.
--
-- Using fork usually leads to an exception that says
-- \"Control.Monad.Trans.Resource.register\': The mutable state is being accessed
-- after cleanup. Please contact the maintainers.\"
instance MonadBaseControl b m => MonadBaseControl b (HandlerT site m) where
#if MIN_VERSION_monad_control(1,0,0)
type StM (HandlerT site m) a = StM m a
liftBaseWith f = HandlerT $ \reader' ->
liftBaseWith $ \runInBase ->
f $ runInBase . (\(HandlerT r) -> r reader')
restoreM = HandlerT . const . restoreM
#else
data StM (HandlerT site m) a = StH (StM m a)
liftBaseWith f = HandlerT $ \reader' ->
liftBaseWith $ \runInBase ->
f $ fmap StH . runInBase . (\(HandlerT r) -> r reader')
restoreM (StH base) = HandlerT $ const $ restoreM base
#endif
instance MonadThrow m => MonadThrow (HandlerT site m) where -- | @since 1.4.38
throwM = lift . monadThrow instance MonadUnliftIO (HandlerFor site) where
{-# INLINE withRunInIO #-}
withRunInIO inner = HandlerFor $ \x -> inner $ flip unHandlerFor x
instance (MonadIO m, MonadBase IO m, MonadThrow m) => MonadResource (HandlerT site m) where instance MonadThrow (HandlerFor site) where
liftResourceT f = HandlerT $ \hd -> liftIO $ runInternalState f (handlerResource hd) throwM = liftIO . throwM
instance MonadIO m => MonadLogger (HandlerT site m) where instance MonadResource (HandlerFor site) where
monadLoggerLog a b c d = HandlerT $ \hd -> liftResourceT f = HandlerFor $ runInternalState f . handlerResource
liftIO $ rheLog (handlerEnv hd) a b c (toLogStr d)
#if MIN_VERSION_monad_logger(0, 3, 10) instance MonadLogger (HandlerFor site) where
instance MonadIO m => MonadLoggerIO (HandlerT site m) where monadLoggerLog a b c d = HandlerFor $ \hd ->
askLoggerIO = HandlerT $ \hd -> return (rheLog (handlerEnv hd)) rheLog (handlerEnv hd) a b c (toLogStr d)
#endif
instance MonadLoggerIO (HandlerFor site) where
askLoggerIO = HandlerFor $ \hd -> return (rheLog (handlerEnv hd))
instance Monoid (UniqueList x) where instance Monoid (UniqueList x) where
mempty = UniqueList id mempty = UniqueList id
UniqueList x `mappend` UniqueList y = UniqueList $ x . y #if !(MIN_VERSION_base(4,11,0))
instance Semigroup (UniqueList x) mappend = (<>)
#endif
instance Semigroup (UniqueList x) where
UniqueList x <> UniqueList y = UniqueList $ x . y
instance IsString Content where instance IsString Content where
fromString = flip ContentBuilder Nothing . Blaze.ByteString.Builder.Char.Utf8.fromString fromString = flip ContentBuilder Nothing . BB.stringUtf8
instance RenderRoute WaiSubsite where instance RenderRoute WaiSubsite where
data Route WaiSubsite = WaiSubsiteRoute [Text] [(Text, Text)] data Route WaiSubsite = WaiSubsiteRoute [Text] [(Text, Text)]
@ -555,6 +542,14 @@ instance RenderRoute WaiSubsite where
instance ParseRoute WaiSubsite where instance ParseRoute WaiSubsite where
parseRoute (x, y) = Just $ WaiSubsiteRoute x y parseRoute (x, y) = Just $ WaiSubsiteRoute x y
instance RenderRoute WaiSubsiteWithAuth where
data Route WaiSubsiteWithAuth = WaiSubsiteWithAuthRoute [Text] [(Text,Text)]
deriving (Show, Eq, Read, Ord)
renderRoute (WaiSubsiteWithAuthRoute ps qs) = (ps,qs)
instance ParseRoute WaiSubsiteWithAuth where
parseRoute (x, y) = Just $ WaiSubsiteWithAuthRoute x y
data Logger = Logger data Logger = Logger
{ loggerSet :: !LoggerSet { loggerSet :: !LoggerSet
, loggerDate :: !DateCacheGetter , loggerDate :: !DateCacheGetter
@ -562,3 +557,41 @@ data Logger = Logger
loggerPutStr :: Logger -> LogStr -> IO () loggerPutStr :: Logger -> LogStr -> IO ()
loggerPutStr (Logger ls _) = pushLogStr ls loggerPutStr (Logger ls _) = pushLogStr ls
-- | A handler monad for subsite
--
-- @since 1.6.0
newtype SubHandlerFor sub master a = SubHandlerFor
{ unSubHandlerFor :: HandlerData sub master -> IO a
}
deriving Functor
instance Applicative (SubHandlerFor child master) where
pure = SubHandlerFor . const . return
(<*>) = ap
instance Monad (SubHandlerFor child master) where
return = pure
SubHandlerFor x >>= f = SubHandlerFor $ \r -> x r >>= \x' -> unSubHandlerFor (f x') r
instance MonadIO (SubHandlerFor child master) where
liftIO = SubHandlerFor . const
instance MonadReader (HandlerData child master) (SubHandlerFor child master) where
ask = SubHandlerFor return
local f (SubHandlerFor g) = SubHandlerFor $ g . f
-- | @since 1.4.38
instance MonadUnliftIO (SubHandlerFor child master) where
{-# INLINE withRunInIO #-}
withRunInIO inner = SubHandlerFor $ \x -> inner $ flip unSubHandlerFor x
instance MonadThrow (SubHandlerFor child master) where
throwM = liftIO . throwM
instance MonadResource (SubHandlerFor child master) where
liftResourceT f = SubHandlerFor $ runInternalState f . handlerResource
instance MonadLogger (SubHandlerFor child master) where
monadLoggerLog a b c d = SubHandlerFor $ \sd ->
rheLog (handlerEnv sd) a b c (toLogStr d)
instance MonadLoggerIO (SubHandlerFor child master) where
askLoggerIO = SubHandlerFor $ return . rheLog . handlerEnv

View File

@ -1,7 +1,6 @@
{-# LANGUAGE CPP #-}
-- | This is designed to be used as -- | This is designed to be used as
-- --
-- > qualified import Yesod.Core.Unsafe as Unsafe -- > import qualified Yesod.Core.Unsafe as Unsafe
-- --
-- This serves as a reminder that the functions are unsafe to use in many situations. -- This serves as a reminder that the functions are unsafe to use in many situations.
module Yesod.Core.Unsafe (runFakeHandler, fakeHandlerGetLogger) where module Yesod.Core.Unsafe (runFakeHandler, fakeHandlerGetLogger) where
@ -10,16 +9,16 @@ import Yesod.Core.Internal.Run (runFakeHandler)
import Yesod.Core.Types import Yesod.Core.Types
import Yesod.Core.Class.Yesod import Yesod.Core.Class.Yesod
#if __GLASGOW_HASKELL__ < 710
import Data.Monoid (mempty, mappend)
#endif
import Control.Monad.IO.Class (MonadIO) import Control.Monad.IO.Class (MonadIO)
-- | designed to be used as -- | designed to be used as
-- --
-- > unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger -- > unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
fakeHandlerGetLogger :: (Yesod site, MonadIO m) fakeHandlerGetLogger :: (Yesod site, MonadIO m)
=> (site -> Logger) -> site -> HandlerT site IO a -> m a => (site -> Logger)
-> site
-> HandlerFor site a
-> m a
fakeHandlerGetLogger getLogger app f = fakeHandlerGetLogger getLogger app f =
runFakeHandler mempty getLogger app f runFakeHandler mempty getLogger app f
>>= either (error . ("runFakeHandler issue: " `mappend`) . show) >>= either (error . ("runFakeHandler issue: " `mappend`) . show)

View File

@ -8,12 +8,14 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE QuasiQuotes #-}
-- | Widgets combine HTML with JS and CSS dependencies with a unique identifier -- | Widgets combine HTML with JS and CSS dependencies with a unique identifier
-- generator, allowing you to create truly modular HTML components. -- generator, allowing you to create truly modular HTML components.
module Yesod.Core.Widget module Yesod.Core.Widget
( -- * Datatype ( -- * Datatype
WidgetT WidgetT
, WidgetFor
, PageContent (..) , PageContent (..)
-- * Special Hamlet quasiquoter/TH for Widgets -- * Special Hamlet quasiquoter/TH for Widgets
, whamlet , whamlet
@ -29,6 +31,12 @@ module Yesod.Core.Widget
-- ** Head of page -- ** Head of page
, setTitle , setTitle
, setTitleI , setTitleI
, setDescription
, setDescriptionI
, setDescriptionIdemp
, setDescriptionIdempI
, setOGType
, setOGImage
-- ** CSS -- ** CSS
, addStylesheet , addStylesheet
, addStylesheetAttrs , addStylesheetAttrs
@ -43,7 +51,6 @@ module Yesod.Core.Widget
, addScriptRemoteAttrs , addScriptRemoteAttrs
, addScriptEither , addScriptEither
-- * Subsites -- * Subsites
, widgetToParentWidget
, handlerToWidget , handlerToWidget
-- * Internal -- * Internal
, whamletFileWithSettings , whamletFileWithSettings
@ -57,13 +64,9 @@ import Text.Cassius
import Text.Julius import Text.Julius
import Yesod.Routes.Class import Yesod.Routes.Class
import Yesod.Core.Handler (getMessageRender, getUrlRenderParams) import Yesod.Core.Handler (getMessageRender, getUrlRenderParams)
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
import Control.Monad (liftM)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Text.Shakespeare.I18N (RenderMessage) import Text.Shakespeare.I18N (RenderMessage)
import Data.Text (Text) import Data.Text (Text)
import Data.Kind (Type)
import qualified Data.Map as Map import qualified Data.Map as Map
import Language.Haskell.TH.Quote (QuasiQuoter) import Language.Haskell.TH.Quote (QuasiQuoter)
import Language.Haskell.TH.Syntax (Q, Exp (InfixE, VarE, LamE, AppE), Pat (VarP), newName) import Language.Haskell.TH.Syntax (Q, Exp (InfixE, VarE, LamE, AppE), Pat (VarP), newName)
@ -72,10 +75,14 @@ import qualified Text.Hamlet as NP
import Data.Text.Lazy.Builder (fromLazyText) import Data.Text.Lazy.Builder (fromLazyText)
import Text.Blaze.Html (toHtml, preEscapedToMarkup) import Text.Blaze.Html (toHtml, preEscapedToMarkup)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import Yesod.Core.Types import Yesod.Core.Types
import Yesod.Core.Class.Handler import Yesod.Core.Class.Handler
type WidgetT site (m :: Type -> Type) = WidgetFor site
{-# DEPRECATED WidgetT "Use WidgetFor directly" #-}
preEscapedLazyText :: TL.Text -> Html preEscapedLazyText :: TL.Text -> Html
preEscapedLazyText = preEscapedToMarkup preEscapedLazyText = preEscapedToMarkup
@ -83,23 +90,32 @@ class ToWidget site a where
toWidget :: (MonadWidget m, HandlerSite m ~ site) => a -> m () toWidget :: (MonadWidget m, HandlerSite m ~ site) => a -> m ()
instance render ~ RY site => ToWidget site (render -> Html) where instance render ~ RY site => ToWidget site (render -> Html) where
toWidget x = tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty toWidget x = tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty mempty
instance render ~ RY site => ToWidget site (render -> Css) where instance render ~ RY site => ToWidget site (render -> Css) where
toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . x toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . x
instance ToWidget site Css where instance ToWidget site Css where
toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . const x toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . const x
instance render ~ RY site => ToWidget site (render -> CssBuilder) where instance render ~ RY site => ToWidget site (render -> CssBuilder) where
toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . x) mempty mempty toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . x) mempty mempty
instance ToWidget site CssBuilder where instance ToWidget site CssBuilder where
toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . const x) mempty mempty toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . const x) mempty mempty
instance render ~ RY site => ToWidget site (render -> Javascript) where instance render ~ RY site => ToWidget site (render -> Javascript) where
toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just x) mempty toWidget x = tell $ GWData mempty mempty mempty mempty mempty mempty (Just x) mempty
instance ToWidget site Javascript where instance ToWidget site Javascript where
toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just $ const x) mempty toWidget x = tell $ GWData mempty mempty mempty mempty mempty mempty (Just $ const x) mempty
instance (site' ~ site, IO ~ m, a ~ ()) => ToWidget site' (WidgetT site m a) where instance (site' ~ site, a ~ ()) => ToWidget site' (WidgetFor site a) where
toWidget = liftWidgetT toWidget = liftWidget
instance ToWidget site Html where instance ToWidget site Html where
toWidget = toWidget . const toWidget = toWidget . const
-- | @since 1.4.28
instance ToWidget site Text where
toWidget = toWidget . toHtml
-- | @since 1.4.28
instance ToWidget site TL.Text where
toWidget = toWidget . toHtml
-- | @since 1.4.28
instance ToWidget site TB.Builder where
toWidget = toWidget . toHtml
-- | Allows adding some CSS to the page with a specific media type. -- | Allows adding some CSS to the page with a specific media type.
-- --
@ -117,9 +133,9 @@ instance render ~ RY site => ToWidgetMedia site (render -> Css) where
instance ToWidgetMedia site Css where instance ToWidgetMedia site Css where
toWidgetMedia media x = toWidgetMedia media $ CssBuilder . fromLazyText . renderCss . const x toWidgetMedia media x = toWidgetMedia media $ CssBuilder . fromLazyText . renderCss . const x
instance render ~ RY site => ToWidgetMedia site (render -> CssBuilder) where instance render ~ RY site => ToWidgetMedia site (render -> CssBuilder) where
toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . x) mempty mempty toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . x) mempty mempty
instance ToWidgetMedia site CssBuilder where instance ToWidgetMedia site CssBuilder where
toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . const x) mempty mempty toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . const x) mempty mempty
class ToWidgetBody site a where class ToWidgetBody site a where
toWidgetBody :: (MonadWidget m, HandlerSite m ~ site) => a -> m () toWidgetBody :: (MonadWidget m, HandlerSite m ~ site) => a -> m ()
@ -137,7 +153,7 @@ class ToWidgetHead site a where
toWidgetHead :: (MonadWidget m, HandlerSite m ~ site) => a -> m () toWidgetHead :: (MonadWidget m, HandlerSite m ~ site) => a -> m ()
instance render ~ RY site => ToWidgetHead site (render -> Html) where instance render ~ RY site => ToWidgetHead site (render -> Html) where
toWidgetHead = tell . GWData mempty mempty mempty mempty mempty mempty . Head toWidgetHead = tell . GWData mempty mempty mempty mempty mempty mempty mempty . Head
instance render ~ RY site => ToWidgetHead site (render -> Css) where instance render ~ RY site => ToWidgetHead site (render -> Css) where
toWidgetHead = toWidget toWidgetHead = toWidget
instance ToWidgetHead site Css where instance ToWidgetHead site Css where
@ -153,18 +169,133 @@ instance ToWidgetHead site Javascript where
instance ToWidgetHead site Html where instance ToWidgetHead site Html where
toWidgetHead = toWidgetHead . const toWidgetHead = toWidgetHead . const
-- | Set the page title. Calling 'setTitle' multiple times overrides previously -- | Set the page title.
-- set values. --
-- Calling @setTitle@ or @setTitleI@ multiple times overrides previously set
-- values.
--
-- SEO Notes:
--
-- * Title tags are the second most important on-page factor for SEO, after
-- content
-- * Every page should have a unique title tag
-- * Start your title tag with your main targeted keyword
-- * Don't stuff your keywords
-- * Google typically shows 55-64 characters, so aim to keep your title
-- length under 60 characters
setTitle :: MonadWidget m => Html -> m () setTitle :: MonadWidget m => Html -> m ()
setTitle x = tell $ GWData mempty (Last $ Just $ Title x) mempty mempty mempty mempty mempty setTitle x = tell $ GWData mempty (Last $ Just $ Title x) mempty mempty mempty mempty mempty mempty
-- | Set the page title. Calling 'setTitle' multiple times overrides previously -- | Set the localised page title.
-- set values. --
-- n.b. See comments for @setTitle@
setTitleI :: (MonadWidget m, RenderMessage (HandlerSite m) msg) => msg -> m () setTitleI :: (MonadWidget m, RenderMessage (HandlerSite m) msg) => msg -> m ()
setTitleI msg = do setTitleI msg = do
mr <- getMessageRender mr <- getMessageRender
setTitle $ toHtml $ mr msg setTitle $ toHtml $ mr msg
-- | Add description meta tag to the head of the page
--
-- Google does not use the description tag as a ranking signal, but the
-- contents of this tag will likely affect your click-through rate since it
-- shows up in search results.
--
-- The average length of the description shown in Google's search results is
-- about 160 characters on desktop, and about 130 characters on mobile, at time
-- of writing.
--
-- Source: https://www.advancedwebranking.com/blog/meta-tags-important-in-seo/
--
-- @since 1.6.18
setDescription :: MonadWidget m => Text -> m ()
setDescription description =
toWidgetHead $ [hamlet|<meta name=description content=#{description}>|]
{-# WARNING setDescription
[ "setDescription is not idempotent; we recommend setDescriptionIdemp instead"
, "Multiple calls to setDescription will insert multiple meta tags in the page head."
, "If you want an idempotent function, use setDescriptionIdemp - but if you do, you \
\may need to change your layout to include pageDescription."
]
#-}
-- | Add translated description meta tag to the head of the page
--
-- n.b. See comments for @setDescription@.
--
-- @since 1.6.18
setDescriptionI
:: (MonadWidget m, RenderMessage (HandlerSite m) msg)
=> msg -> m ()
setDescriptionI msg = do
mr <- getMessageRender
toWidgetHead $ [hamlet|<meta name=description content=#{mr msg}>|]
{-# WARNING setDescriptionI
[ "setDescriptionI is not idempotent; we recommend setDescriptionIdempI instead"
, "Multiple calls to setDescriptionI will insert multiple meta tags in the page head."
, "If you want an idempotent function, use setDescriptionIdempI - but if you do, you \
\may need to change your layout to include pageDescription."
]
#-}
-- | Add description meta tag to the head of the page
--
-- Google does not use the description tag as a ranking signal, but the
-- contents of this tag will likely affect your click-through rate since it
-- shows up in search results.
--
-- The average length of the description shown in Google's search results is
-- about 160 characters on desktop, and about 130 characters on mobile, at time
-- of writing.
--
-- Unlike 'setDescription', this version is *idempotent* - calling it multiple
-- times will result in only a single description meta tag in the head.
--
-- Source: https://www.advancedwebranking.com/blog/meta-tags-important-in-seo/
--
-- @since 1.6.23
setDescriptionIdemp :: MonadWidget m => Text -> m ()
setDescriptionIdemp description = tell $ GWData mempty mempty (Last $ Just $ Description description) mempty mempty mempty mempty mempty
-- | Add translated description meta tag to the head of the page
--
-- n.b. See comments for @setDescriptionIdemp@.
--
-- Unlike 'setDescriptionI', this version is *idempotent* - calling it multiple
-- times will result in only a single description meta tag in the head.
--
-- @since 1.6.23
setDescriptionIdempI
:: (MonadWidget m, RenderMessage (HandlerSite m) msg)
=> msg -> m ()
setDescriptionIdempI msg = do
mr <- getMessageRender
setDescriptionIdemp $ mr msg
-- | Add OpenGraph type meta tag to the head of the page
--
-- See all available OG types here: https://ogp.me/#types
--
-- @since 1.6.18
setOGType :: MonadWidget m => Text -> m ()
setOGType a = toWidgetHead $ [hamlet|<meta property="og:type" content=#{a}>|]
-- | Add OpenGraph image meta tag to the head of the page
--
-- Best practices:
--
-- * Use custom images for shareable pages, e.g., homepage, articles, etc.
-- * Use your logo or any other branded image for the rest of your pages.
-- * Use images with a 1.91:1 ratio and minimum recommended dimensions of
-- 1200x630 for optimal clarity across all devices.
--
-- Source: https://ahrefs.com/blog/open-graph-meta-tags/
--
-- @since 1.6.18
setOGImage :: MonadWidget m => Text -> m ()
setOGImage a = toWidgetHead $ [hamlet|<meta property="og:image" content=#{a}>|]
-- | Link to the specified local stylesheet. -- | Link to the specified local stylesheet.
addStylesheet :: MonadWidget m => Route (HandlerSite m) -> m () addStylesheet :: MonadWidget m => Route (HandlerSite m) -> m ()
addStylesheet = flip addStylesheetAttrs [] addStylesheet = flip addStylesheetAttrs []
@ -174,7 +305,7 @@ addStylesheetAttrs :: MonadWidget m
=> Route (HandlerSite m) => Route (HandlerSite m)
-> [(Text, Text)] -> [(Text, Text)]
-> m () -> m ()
addStylesheetAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Stylesheet (Local x) y) mempty mempty mempty addStylesheetAttrs x y = tell $ GWData mempty mempty mempty mempty (toUnique $ Stylesheet (Local x) y) mempty mempty mempty
-- | Link to the specified remote stylesheet. -- | Link to the specified remote stylesheet.
addStylesheetRemote :: MonadWidget m => Text -> m () addStylesheetRemote :: MonadWidget m => Text -> m ()
@ -182,7 +313,7 @@ addStylesheetRemote = flip addStylesheetRemoteAttrs []
-- | Link to the specified remote stylesheet. -- | Link to the specified remote stylesheet.
addStylesheetRemoteAttrs :: MonadWidget m => Text -> [(Text, Text)] -> m () addStylesheetRemoteAttrs :: MonadWidget m => Text -> [(Text, Text)] -> m ()
addStylesheetRemoteAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Stylesheet (Remote x) y) mempty mempty mempty addStylesheetRemoteAttrs x y = tell $ GWData mempty mempty mempty mempty (toUnique $ Stylesheet (Remote x) y) mempty mempty mempty
addStylesheetEither :: MonadWidget m addStylesheetEither :: MonadWidget m
=> Either (Route (HandlerSite m)) Text => Either (Route (HandlerSite m)) Text
@ -200,7 +331,7 @@ addScript = flip addScriptAttrs []
-- | Link to the specified local script. -- | Link to the specified local script.
addScriptAttrs :: MonadWidget m => Route (HandlerSite m) -> [(Text, Text)] -> m () addScriptAttrs :: MonadWidget m => Route (HandlerSite m) -> [(Text, Text)] -> m ()
addScriptAttrs x y = tell $ GWData mempty mempty (toUnique $ Script (Local x) y) mempty mempty mempty mempty addScriptAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Script (Local x) y) mempty mempty mempty mempty
-- | Link to the specified remote script. -- | Link to the specified remote script.
addScriptRemote :: MonadWidget m => Text -> m () addScriptRemote :: MonadWidget m => Text -> m ()
@ -208,7 +339,7 @@ addScriptRemote = flip addScriptRemoteAttrs []
-- | Link to the specified remote script. -- | Link to the specified remote script.
addScriptRemoteAttrs :: MonadWidget m => Text -> [(Text, Text)] -> m () addScriptRemoteAttrs :: MonadWidget m => Text -> [(Text, Text)] -> m ()
addScriptRemoteAttrs x y = tell $ GWData mempty mempty (toUnique $ Script (Remote x) y) mempty mempty mempty mempty addScriptRemoteAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Script (Remote x) y) mempty mempty mempty mempty
whamlet :: QuasiQuoter whamlet :: QuasiQuoter
whamlet = NP.hamletWithSettings rules NP.defaultHamletSettings whamlet = NP.hamletWithSettings rules NP.defaultHamletSettings
@ -258,45 +389,10 @@ ihamletToHtml ih = do
return $ ih (toHtml . mrender) urender return $ ih (toHtml . mrender) urender
tell :: MonadWidget m => GWData (Route (HandlerSite m)) -> m () tell :: MonadWidget m => GWData (Route (HandlerSite m)) -> m ()
tell w = liftWidgetT $ WidgetT $ const $ return ((), w) tell = liftWidget . tellWidget
toUnique :: x -> UniqueList x toUnique :: x -> UniqueList x
toUnique = UniqueList . (:) toUnique = UniqueList . (:)
handlerToWidget :: Monad m => HandlerT site m a -> WidgetT site m a handlerToWidget :: HandlerFor site a -> WidgetFor site a
handlerToWidget (HandlerT f) = WidgetT $ liftM (, mempty) . f handlerToWidget (HandlerFor f) = WidgetFor $ f . wdHandler
widgetToParentWidget :: MonadIO m
=> WidgetT child IO a
-> HandlerT child (HandlerT parent m) (WidgetT parent m a)
widgetToParentWidget (WidgetT f) = HandlerT $ \hd -> do
(a, gwd) <- liftIO $ f hd { handlerToParent = const () }
return $ WidgetT $ const $ return (a, liftGWD (handlerToParent hd) gwd)
liftGWD :: (child -> parent) -> GWData child -> GWData parent
liftGWD tp gwd = GWData
{ gwdBody = fixBody $ gwdBody gwd
, gwdTitle = gwdTitle gwd
, gwdScripts = fixUnique fixScript $ gwdScripts gwd
, gwdStylesheets = fixUnique fixStyle $ gwdStylesheets gwd
, gwdCss = fixCss <$> gwdCss gwd
, gwdJavascript = fixJS <$> gwdJavascript gwd
, gwdHead = fixHead $ gwdHead gwd
}
where
fixRender f route = f (tp route)
fixBody (Body h) = Body $ h . fixRender
fixHead (Head h) = Head $ h . fixRender
fixUnique go (UniqueList f) = UniqueList (map go (f []) ++)
fixScript (Script loc attrs) = Script (fixLoc loc) attrs
fixStyle (Stylesheet loc attrs) = Stylesheet (fixLoc loc) attrs
fixLoc (Local url) = Local $ tp url
fixLoc (Remote t) = Remote t
fixCss f = f . fixRender
fixJS f = f . fixRender

View File

@ -1,5 +1,4 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE PatternGuards #-} {-# LANGUAGE PatternGuards #-}
{-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter {-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter
module Yesod.Routes.Parse module Yesod.Routes.Parse
@ -10,10 +9,13 @@ module Yesod.Routes.Parse
, parseType , parseType
, parseTypeTree , parseTypeTree
, TypeTree (..) , TypeTree (..)
, dropBracket
, nameToType
, isTvar
) where ) where
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Data.Char (isUpper) import Data.Char (isUpper, isLower, isSpace)
import Language.Haskell.TH.Quote import Language.Haskell.TH.Quote
import qualified System.IO as SIO import qualified System.IO as SIO
import Yesod.Routes.TH import Yesod.Routes.TH
@ -34,9 +36,15 @@ parseRoutes = QuasiQuoter { quoteExp = x }
[] -> lift res [] -> lift res
z -> error $ unlines $ "Overlapping routes: " : map show z z -> error $ unlines $ "Overlapping routes: " : map show z
-- | Same as 'parseRoutes', but uses an external file instead of quasiquotation.
--
-- The recommended file extension is @.yesodroutes@.
parseRoutesFile :: FilePath -> Q Exp parseRoutesFile :: FilePath -> Q Exp
parseRoutesFile = parseRoutesFileWith parseRoutes parseRoutesFile = parseRoutesFileWith parseRoutes
-- | Same as 'parseRoutesNoCheck', but uses an external file instead of quasiquotation.
--
-- The recommended file extension is @.yesodroutes@.
parseRoutesFileNoCheck :: FilePath -> Q Exp parseRoutesFileNoCheck :: FilePath -> Q Exp
parseRoutesFileNoCheck = parseRoutesFileWith parseRoutesNoCheck parseRoutesFileNoCheck = parseRoutesFileWith parseRoutesNoCheck
@ -63,7 +71,7 @@ parseRoutesNoCheck = QuasiQuoter
-- invalid input. -- invalid input.
resourcesFromString :: String -> [ResourceTree String] resourcesFromString :: String -> [ResourceTree String]
resourcesFromString = resourcesFromString =
fst . parse 0 . filter (not . all (== ' ')) . lines fst . parse 0 . filter (not . all (== ' ')) . foldr lineContinuations [] . lines . filter (/= '\r')
where where
parse _ [] = ([], []) parse _ [] = ([], [])
parse indent (thisLine:otherLines) parse indent (thisLine:otherLines)
@ -86,7 +94,7 @@ resourcesFromString =
spaces = takeWhile (== ' ') thisLine spaces = takeWhile (== ' ') thisLine
(others, remainder) = parse indent otherLines' (others, remainder) = parse indent otherLines'
(this, otherLines') = (this, otherLines') =
case takeWhile (not . isPrefixOf "--") $ words thisLine of case takeWhile (not . isPrefixOf "--") $ splitSpaces thisLine of
(pattern:rest0) (pattern:rest0)
| Just (constr:rest) <- stripColonLast rest0 | Just (constr:rest) <- stripColonLast rest0
, Just attrs <- mapM parseAttr rest -> , Just attrs <- mapM parseAttr rest ->
@ -102,6 +110,26 @@ resourcesFromString =
[] -> (id, otherLines) [] -> (id, otherLines)
_ -> error $ "Invalid resource line: " ++ thisLine _ -> error $ "Invalid resource line: " ++ thisLine
-- | Splits a string by spaces, as long as the spaces are not enclosed by curly brackets (not recursive).
splitSpaces :: String -> [String]
splitSpaces "" = []
splitSpaces str =
let (rest, piece) = parse $ dropWhile isSpace str in
piece:(splitSpaces rest)
where
parse :: String -> ( String, String)
parse ('{':s) = fmap ('{':) $ parseBracket s
parse (c:s) | isSpace c = (s, [])
parse (c:s) = fmap (c:) $ parse s
parse "" = ("", "")
parseBracket :: String -> ( String, String)
parseBracket ('{':_) = error $ "Invalid resource line (nested curly bracket): " ++ str
parseBracket ('}':s) = fmap ('}':) $ parse s
parseBracket (c:s) = fmap (c:) $ parseBracket s
parseBracket "" = error $ "Invalid resource line (unclosed curly bracket): " ++ str
piecesFromStringCheck :: String -> ([Piece String], Maybe String, Bool) piecesFromStringCheck :: String -> ([Piece String], Maybe String, Bool)
piecesFromStringCheck s0 = piecesFromStringCheck s0 =
(pieces, mmulti, check) (pieces, mmulti, check)
@ -181,7 +209,7 @@ parseTypeTree :: String -> Maybe TypeTree
parseTypeTree orig = parseTypeTree orig =
toTypeTree pieces toTypeTree pieces
where where
pieces = filter (not . null) $ splitOn '-' $ addDashes orig pieces = filter (not . null) $ splitOn (\c -> c == '-' || c == ' ') $ addDashes orig
addDashes [] = [] addDashes [] = []
addDashes (x:xs) = addDashes (x:xs) =
front $ addDashes xs front $ addDashes xs
@ -194,7 +222,7 @@ parseTypeTree orig =
_:y -> x : splitOn c y _:y -> x : splitOn c y
[] -> [x] [] -> [x]
where where
(x, y') = break (== c) s (x, y') = break c s
data TypeTree = TTTerm String data TypeTree = TTTerm String
| TTApp TypeTree TypeTree | TTApp TypeTree TypeTree
@ -232,14 +260,23 @@ toTypeTree orig = do
gos' (front . (t:)) xs' gos' (front . (t:)) xs'
ttToType :: TypeTree -> Type ttToType :: TypeTree -> Type
ttToType (TTTerm s) = ConT $ mkName s ttToType (TTTerm s) = nameToType s
ttToType (TTApp x y) = ttToType x `AppT` ttToType y ttToType (TTApp x y) = ttToType x `AppT` ttToType y
ttToType (TTList t) = ListT `AppT` ttToType t ttToType (TTList t) = ListT `AppT` ttToType t
nameToType :: String -> Type
nameToType t = if isTvar t
then VarT $ mkName t
else ConT $ mkName t
isTvar :: String -> Bool
isTvar (h:_) = isLower h
isTvar _ = False
pieceFromString :: String -> Either (CheckOverlap, String) (CheckOverlap, Piece String) pieceFromString :: String -> Either (CheckOverlap, String) (CheckOverlap, Piece String)
pieceFromString ('#':'!':x) = Right $ (False, Dynamic x) pieceFromString ('#':'!':x) = Right $ (False, Dynamic $ dropBracket x)
pieceFromString ('!':'#':x) = Right $ (False, Dynamic x) -- https://github.com/yesodweb/yesod/issues/652 pieceFromString ('!':'#':x) = Right $ (False, Dynamic $ dropBracket x) -- https://github.com/yesodweb/yesod/issues/652
pieceFromString ('#':x) = Right $ (True, Dynamic x) pieceFromString ('#':x) = Right $ (True, Dynamic $ dropBracket x)
pieceFromString ('*':'!':x) = Left (False, x) pieceFromString ('*':'!':x) = Left (False, x)
pieceFromString ('+':'!':x) = Left (False, x) pieceFromString ('+':'!':x) = Left (False, x)
@ -252,3 +289,19 @@ pieceFromString ('+':x) = Left (True, x)
pieceFromString ('!':x) = Right $ (False, Static x) pieceFromString ('!':x) = Right $ (False, Static x)
pieceFromString x = Right $ (True, Static x) pieceFromString x = Right $ (True, Static x)
dropBracket :: String -> String
dropBracket str@('{':x) = case break (== '}') x of
(s, "}") -> s
_ -> error $ "Unclosed bracket ('{'): " ++ str
dropBracket x = x
-- | If this line ends with a backslash, concatenate it together with the next line.
--
-- @since 1.6.8
lineContinuations :: String -> [String] -> [String]
lineContinuations this [] = [this]
lineContinuations this below@(next:rest) = case unsnoc this of
Just (this', '\\') -> (this'++next):rest
_ -> this:below
where unsnoc s = if null s then Nothing else Just (init s, last s)

View File

@ -1,3 +1,4 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards, TemplateHaskell, ViewPatterns #-} {-# LANGUAGE RecordWildCards, TemplateHaskell, ViewPatterns #-}
module Yesod.Routes.TH.Dispatch module Yesod.Routes.TH.Dispatch
( MkDispatchSettings (..) ( MkDispatchSettings (..)
@ -73,7 +74,7 @@ mkDispatchClause MkDispatchSettings {..} resources = do
handlePiece (Static str) = return (LitP $ StringL str, Nothing) handlePiece (Static str) = return (LitP $ StringL str, Nothing)
handlePiece (Dynamic _) = do handlePiece (Dynamic _) = do
x <- newName "dyn" x <- newName "dyn"
let pat = ViewP (VarE 'fromPathPiece) (ConP 'Just [VarP x]) let pat = ViewP (VarE 'fromPathPiece) (conPCompat 'Just [VarP x])
return (pat, Just $ VarE x) return (pat, Just $ VarE x)
handlePieces :: [Piece a] -> Q ([Pat], [Exp]) handlePieces :: [Piece a] -> Q ([Pat], [Exp])
@ -86,7 +87,7 @@ mkDispatchClause MkDispatchSettings {..} resources = do
mkPathPat final = mkPathPat final =
foldr addPat final foldr addPat final
where where
addPat x y = ConP '(:) [x, y] addPat x y = conPCompat '(:) [x, y]
go :: SDC -> ResourceTree a -> Q Clause go :: SDC -> ResourceTree a -> Q Clause
go sdc (ResourceParent name _check pieces children) = do go sdc (ResourceParent name _check pieces children) = do
@ -124,11 +125,11 @@ mkDispatchClause MkDispatchSettings {..} resources = do
Methods multi methods -> do Methods multi methods -> do
(finalPat, mfinalE) <- (finalPat, mfinalE) <-
case multi of case multi of
Nothing -> return (ConP '[] [], Nothing) Nothing -> return (conPCompat '[] [], Nothing)
Just _ -> do Just _ -> do
multiName <- newName "multi" multiName <- newName "multi"
let pat = ViewP (VarE 'fromPathMultiPiece) let pat = ViewP (VarE 'fromPathMultiPiece)
(ConP 'Just [VarP multiName]) (conPCompat 'Just [VarP multiName])
return (pat, Just $ VarE multiName) return (pat, Just $ VarE multiName)
let dynsMulti = let dynsMulti =
@ -200,3 +201,10 @@ mkDispatchClause MkDispatchSettings {..} resources = do
defaultGetHandler :: Maybe String -> String -> Q Exp defaultGetHandler :: Maybe String -> String -> Q Exp
defaultGetHandler Nothing s = return $ VarE $ mkName $ "handle" ++ s defaultGetHandler Nothing s = return $ VarE $ mkName $ "handle" ++ s
defaultGetHandler (Just method) s = return $ VarE $ mkName $ map toLower method ++ s defaultGetHandler (Just method) s = return $ VarE $ mkName $ map toLower method ++ s
conPCompat :: Name -> [Pat] -> Pat
conPCompat n pats = ConP n
#if MIN_VERSION_template_haskell(2,18,0)
[]
#endif
pats

View File

@ -1,4 +1,3 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module Yesod.Routes.TH.ParseRoute module Yesod.Routes.TH.ParseRoute
( -- ** ParseRoute ( -- ** ParseRoute
@ -11,8 +10,8 @@ import Data.Text (Text)
import Yesod.Routes.Class import Yesod.Routes.Class
import Yesod.Routes.TH.Dispatch import Yesod.Routes.TH.Dispatch
mkParseRouteInstance :: Type -> [ResourceTree a] -> Q Dec mkParseRouteInstance :: Cxt -> Type -> [ResourceTree a] -> Q Dec
mkParseRouteInstance typ ress = do mkParseRouteInstance cxt typ ress = do
cls <- mkDispatchClause cls <- mkDispatchClause
MkDispatchSettings MkDispatchSettings
{ mdsRunHandler = [|\_ _ x _ -> x|] { mdsRunHandler = [|\_ _ x _ -> x|]
@ -28,7 +27,7 @@ mkParseRouteInstance typ ress = do
(map removeMethods ress) (map removeMethods ress)
helper <- newName "helper" helper <- newName "helper"
fixer <- [|(\f x -> f () x) :: (() -> ([Text], [(Text, Text)]) -> Maybe (Route a)) -> ([Text], [(Text, Text)]) -> Maybe (Route a)|] fixer <- [|(\f x -> f () x) :: (() -> ([Text], [(Text, Text)]) -> Maybe (Route a)) -> ([Text], [(Text, Text)]) -> Maybe (Route a)|]
return $ instanceD [] (ConT ''ParseRoute `AppT` typ) return $ instanceD cxt (ConT ''ParseRoute `AppT` typ)
[ FunD 'parseRoute $ return $ Clause [ FunD 'parseRoute $ return $ Clause
[] []
(NormalB $ fixer `AppE` VarE helper) (NormalB $ fixer `AppE` VarE helper)
@ -45,8 +44,4 @@ mkParseRouteInstance typ ress = do
fixDispatch x = x fixDispatch x = x
instanceD :: Cxt -> Type -> [Dec] -> Dec instanceD :: Cxt -> Type -> [Dec] -> Dec
#if MIN_VERSION_template_haskell(2,11,0)
instanceD = InstanceD Nothing instanceD = InstanceD Nothing
#else
instanceD = InstanceD
#endif

View File

@ -0,0 +1,264 @@
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
module Yesod.Routes.TH.RenderRoute
( -- ** RenderRoute
mkRenderRouteInstance
, mkRenderRouteInstanceOpts
, mkRouteCons
, mkRouteConsOpts
, mkRenderRouteClauses
, RouteOpts
, defaultOpts
, setEqDerived
, setShowDerived
, setReadDerived
) where
import Yesod.Routes.TH.Types
import Language.Haskell.TH (conT)
import Language.Haskell.TH.Syntax
import Data.Bits (xor)
import Data.Maybe (maybeToList)
import Control.Monad (replicateM)
import Data.Text (pack)
import Web.PathPieces (PathPiece (..), PathMultiPiece (..))
import Yesod.Routes.Class
-- | General opts data type for generating yesod.
--
-- Contains options for what instances are derived for the route. Use the setting
-- functions on `defaultOpts` to set specific fields.
--
-- @since 1.6.25.0
data RouteOpts = MkRouteOpts
{ roDerivedEq :: Bool
, roDerivedShow :: Bool
, roDerivedRead :: Bool
}
-- | Default options for generating routes.
--
-- Defaults to all instances derived.
--
-- @since 1.6.25.0
defaultOpts :: RouteOpts
defaultOpts = MkRouteOpts True True True
-- |
--
-- @since 1.6.25.0
setEqDerived :: Bool -> RouteOpts -> RouteOpts
setEqDerived b rdo = rdo { roDerivedEq = b }
-- |
--
-- @since 1.6.25.0
setShowDerived :: Bool -> RouteOpts -> RouteOpts
setShowDerived b rdo = rdo { roDerivedShow = b }
-- |
--
-- @since 1.6.25.0
setReadDerived :: Bool -> RouteOpts -> RouteOpts
setReadDerived b rdo = rdo { roDerivedRead = b }
-- |
--
-- @since 1.6.25.0
instanceNamesFromOpts :: RouteOpts -> [Name]
instanceNamesFromOpts (MkRouteOpts eq shw rd) = prependIf eq ''Eq $ prependIf shw ''Show $ prependIf rd ''Read []
where prependIf b = if b then (:) else const id
-- | Generate the constructors of a route data type.
mkRouteCons :: [ResourceTree Type] -> Q ([Con], [Dec])
mkRouteCons = mkRouteConsOpts defaultOpts
-- | Generate the constructors of a route data type, with custom opts.
--
-- @since 1.6.25.0
mkRouteConsOpts :: RouteOpts -> [ResourceTree Type] -> Q ([Con], [Dec])
mkRouteConsOpts opts rttypes =
mconcat <$> mapM mkRouteCon rttypes
where
mkRouteCon (ResourceLeaf res) =
return ([con], [])
where
con = NormalC (mkName $ resourceName res)
$ map (notStrict,)
$ concat [singles, multi, sub]
singles = concatMap toSingle $ resourcePieces res
toSingle Static{} = []
toSingle (Dynamic typ) = [typ]
multi = maybeToList $ resourceMulti res
sub =
case resourceDispatch res of
Subsite { subsiteType = typ } -> [ConT ''Route `AppT` typ]
_ -> []
mkRouteCon (ResourceParent name _check pieces children) = do
(cons, decs) <- mkRouteConsOpts opts children
let conts = mapM conT $ instanceNamesFromOpts opts
#if MIN_VERSION_template_haskell(2,12,0)
dec <- DataD [] (mkName name) [] Nothing cons <$> fmap (pure . DerivClause Nothing) conts
#else
dec <- DataD [] (mkName name) [] Nothing cons <$> conts
#endif
return ([con], dec : decs)
where
con = NormalC (mkName name)
$ map (notStrict,)
$ singles ++ [ConT $ mkName name]
singles = concatMap toSingle pieces
toSingle Static{} = []
toSingle (Dynamic typ) = [typ]
-- | Clauses for the 'renderRoute' method.
mkRenderRouteClauses :: [ResourceTree Type] -> Q [Clause]
mkRenderRouteClauses =
mapM go
where
isDynamic Dynamic{} = True
isDynamic _ = False
go (ResourceParent name _check pieces children) = do
let cnt = length $ filter isDynamic pieces
dyns <- replicateM cnt $ newName "dyn"
child <- newName "child"
let pat = conPCompat (mkName name) $ map VarP $ dyns ++ [child]
pack' <- [|pack|]
tsp <- [|toPathPiece|]
let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp pieces dyns
childRender <- newName "childRender"
let rr = VarE childRender
childClauses <- mkRenderRouteClauses children
a <- newName "a"
b <- newName "b"
colon <- [|(:)|]
let cons y ys = InfixE (Just y) colon (Just ys)
let pieces' = foldr cons (VarE a) piecesSingle
let body = LamE [TupP [VarP a, VarP b]] (TupE
#if MIN_VERSION_template_haskell(2,16,0)
$ map Just
#endif
[pieces', VarE b]
) `AppE` (rr `AppE` VarE child)
return $ Clause [pat] (NormalB body) [FunD childRender childClauses]
go (ResourceLeaf res) = do
let cnt = length (filter isDynamic $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res)
dyns <- replicateM cnt $ newName "dyn"
sub <-
case resourceDispatch res of
Subsite{} -> return <$> newName "sub"
_ -> return []
let pat = conPCompat (mkName $ resourceName res) $ map VarP $ dyns ++ sub
pack' <- [|pack|]
tsp <- [|toPathPiece|]
let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp (resourcePieces res) dyns
piecesMulti <-
case resourceMulti res of
Nothing -> return $ ListE []
Just{} -> do
tmp <- [|toPathMultiPiece|]
return $ tmp `AppE` VarE (last dyns)
body <-
case sub of
[x] -> do
rr <- [|renderRoute|]
a <- newName "a"
b <- newName "b"
colon <- [|(:)|]
let cons y ys = InfixE (Just y) colon (Just ys)
let pieces = foldr cons (VarE a) piecesSingle
return $ LamE [TupP [VarP a, VarP b]] (TupE
#if MIN_VERSION_template_haskell(2,16,0)
$ map Just
#endif
[pieces, VarE b]
) `AppE` (rr `AppE` VarE x)
_ -> do
colon <- [|(:)|]
let cons a b = InfixE (Just a) colon (Just b)
return $ TupE
#if MIN_VERSION_template_haskell(2,16,0)
$ map Just
#endif
[foldr cons piecesMulti piecesSingle, ListE []]
return $ Clause [pat] (NormalB body) []
mkPieces _ _ [] _ = []
mkPieces toText tsp (Static s:ps) dyns = toText s : mkPieces toText tsp ps dyns
mkPieces toText tsp (Dynamic{}:ps) (d:dyns) = tsp `AppE` VarE d : mkPieces toText tsp ps dyns
mkPieces _ _ (Dynamic _ : _) [] = error "mkPieces 120"
-- | Generate the 'RenderRoute' instance.
--
-- This includes both the 'Route' associated type and the
-- 'renderRoute' method. This function uses both 'mkRouteCons' and
-- 'mkRenderRouteClasses'.
mkRenderRouteInstance :: Cxt -> Type -> [ResourceTree Type] -> Q [Dec]
mkRenderRouteInstance = mkRenderRouteInstanceOpts defaultOpts
-- | Generate the 'RenderRoute' instance.
--
-- This includes both the 'Route' associated type and the
-- 'renderRoute' method. This function uses both 'mkRouteCons' and
-- 'mkRenderRouteClasses'.
--
-- @since 1.6.25.0
mkRenderRouteInstanceOpts :: RouteOpts -> Cxt -> Type -> [ResourceTree Type] -> Q [Dec]
mkRenderRouteInstanceOpts opts cxt typ ress = do
cls <- mkRenderRouteClauses ress
(cons, decs) <- mkRouteConsOpts opts ress
#if MIN_VERSION_template_haskell(2,15,0)
did <- DataInstD [] Nothing (AppT (ConT ''Route) typ) Nothing cons <$> fmap (pure . DerivClause Nothing) (mapM conT (clazzes False))
let sds = fmap (\t -> StandaloneDerivD Nothing cxt $ ConT t `AppT` ( ConT ''Route `AppT` typ)) (clazzes True)
#elif MIN_VERSION_template_haskell(2,12,0)
did <- DataInstD [] ''Route [typ] Nothing cons <$> fmap (pure . DerivClause Nothing) (mapM conT (clazzes False))
let sds = fmap (\t -> StandaloneDerivD Nothing cxt $ ConT t `AppT` ( ConT ''Route `AppT` typ)) (clazzes True)
#else
did <- DataInstD [] ''Route [typ] Nothing cons <$> mapM conT (clazzes False)
let sds = fmap (\t -> StandaloneDerivD cxt $ ConT t `AppT` ( ConT ''Route `AppT` typ)) (clazzes True)
#endif
return $ instanceD cxt (ConT ''RenderRoute `AppT` typ)
[ did
, FunD (mkName "renderRoute") cls
]
: sds ++ decs
where
clazzes standalone = if standalone `xor` null cxt then
clazzes'
else
[]
clazzes' = instanceNamesFromOpts opts
notStrict :: Bang
notStrict = Bang NoSourceUnpackedness NoSourceStrictness
instanceD :: Cxt -> Type -> [Dec] -> Dec
instanceD = InstanceD Nothing
conPCompat :: Name -> [Pat] -> Pat
conPCompat n pats = ConP n
#if MIN_VERSION_template_haskell(2,18,0)
[]
#endif
pats

View File

@ -10,14 +10,11 @@ import Yesod.Routes.Class
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Data.Set (fromList) import Data.Set (fromList)
import Data.Text (pack) import Data.Text (pack)
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
mkRouteAttrsInstance :: Type -> [ResourceTree a] -> Q Dec mkRouteAttrsInstance :: Cxt -> Type -> [ResourceTree a] -> Q Dec
mkRouteAttrsInstance typ ress = do mkRouteAttrsInstance cxt typ ress = do
clauses <- mapM (goTree id) ress clauses <- mapM (goTree id) ress
return $ instanceD [] (ConT ''RouteAttrs `AppT` typ) return $ instanceD cxt (ConT ''RouteAttrs `AppT` typ)
[ FunD 'routeAttrs $ concat clauses [ FunD 'routeAttrs $ concat clauses
] ]
@ -30,7 +27,11 @@ goTree front (ResourceParent name _check pieces trees) =
toIgnore = length $ filter isDynamic pieces toIgnore = length $ filter isDynamic pieces
isDynamic Dynamic{} = True isDynamic Dynamic{} = True
isDynamic Static{} = False isDynamic Static{} = False
front' = front . ConP (mkName name) . ignored front' = front . ConP (mkName name)
#if MIN_VERSION_template_haskell(2,18,0)
[]
#endif
. ignored
goRes :: (Pat -> Pat) -> Resource a -> Q Clause goRes :: (Pat -> Pat) -> Resource a -> Q Clause
goRes front Resource {..} = goRes front Resource {..} =
@ -42,8 +43,4 @@ goRes front Resource {..} =
toText s = VarE 'pack `AppE` LitE (StringL s) toText s = VarE 'pack `AppE` LitE (StringL s)
instanceD :: Cxt -> Type -> [Dec] -> Dec instanceD :: Cxt -> Type -> [Dec] -> Dec
#if MIN_VERSION_template_haskell(2,11,0)
instanceD = InstanceD Nothing instanceD = InstanceD Nothing
#else
instanceD = InstanceD
#endif

View File

@ -1,5 +1,5 @@
{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveLift #-}
-- | Warning! This module is considered internal and may have breaking changes -- | Warning! This module is considered internal and may have breaking changes
module Yesod.Routes.TH.Types module Yesod.Routes.TH.Types
( -- * Data types ( -- * Data types
@ -21,7 +21,7 @@ import Language.Haskell.TH.Syntax
data ResourceTree typ data ResourceTree typ
= ResourceLeaf (Resource typ) = ResourceLeaf (Resource typ)
| ResourceParent String CheckOverlap [Piece typ] [ResourceTree typ] | ResourceParent String CheckOverlap [Piece typ] [ResourceTree typ]
deriving Functor deriving (Lift, Show, Functor)
resourceTreePieces :: ResourceTree typ -> [Piece typ] resourceTreePieces :: ResourceTree typ -> [Piece typ]
resourceTreePieces (ResourceLeaf r) = resourcePieces r resourceTreePieces (ResourceLeaf r) = resourcePieces r
@ -31,10 +31,6 @@ resourceTreeName :: ResourceTree typ -> String
resourceTreeName (ResourceLeaf r) = resourceName r resourceTreeName (ResourceLeaf r) = resourceName r
resourceTreeName (ResourceParent x _ _ _) = x resourceTreeName (ResourceParent x _ _ _) = x
instance Lift t => Lift (ResourceTree t) where
lift (ResourceLeaf r) = [|ResourceLeaf $(lift r)|]
lift (ResourceParent a b c d) = [|ResourceParent $(lift a) $(lift b) $(lift c) $(lift d)|]
data Resource typ = Resource data Resource typ = Resource
{ resourceName :: String { resourceName :: String
, resourcePieces :: [Piece typ] , resourcePieces :: [Piece typ]
@ -42,24 +38,17 @@ data Resource typ = Resource
, resourceAttrs :: [String] , resourceAttrs :: [String]
, resourceCheck :: CheckOverlap , resourceCheck :: CheckOverlap
} }
deriving (Show, Functor) deriving (Lift, Show, Functor)
type CheckOverlap = Bool type CheckOverlap = Bool
instance Lift t => Lift (Resource t) where
lift (Resource a b c d e) = [|Resource a b c d e|]
data Piece typ = Static String | Dynamic typ data Piece typ = Static String | Dynamic typ
deriving Show deriving (Lift, Show)
instance Functor Piece where instance Functor Piece where
fmap _ (Static s) = Static s fmap _ (Static s) = Static s
fmap f (Dynamic t) = Dynamic (f t) fmap f (Dynamic t) = Dynamic (f t)
instance Lift t => Lift (Piece t) where
lift (Static s) = [|Static $(lift s)|]
lift (Dynamic t) = [|Dynamic $(lift t)|]
data Dispatch typ = data Dispatch typ =
Methods Methods
{ methodsMulti :: Maybe typ -- ^ type of the multi piece at the end { methodsMulti :: Maybe typ -- ^ type of the multi piece at the end
@ -69,17 +58,12 @@ data Dispatch typ =
{ subsiteType :: typ { subsiteType :: typ
, subsiteFunc :: String , subsiteFunc :: String
} }
deriving Show deriving (Lift, Show)
instance Functor Dispatch where instance Functor Dispatch where
fmap f (Methods a b) = Methods (fmap f a) b fmap f (Methods a b) = Methods (fmap f a) b
fmap f (Subsite a b) = Subsite (f a) b fmap f (Subsite a b) = Subsite (f a) b
instance Lift t => Lift (Dispatch t) where
lift (Methods Nothing b) = [|Methods Nothing $(lift b)|]
lift (Methods (Just t) b) = [|Methods (Just $(lift t)) $(lift b)|]
lift (Subsite t b) = [|Subsite $(lift t) $(lift b)|]
resourceMulti :: Resource typ -> Maybe typ resourceMulti :: Resource typ -> Maybe typ
resourceMulti Resource { resourceDispatch = Methods (Just t) _ } = Just t resourceMulti Resource { resourceDispatch = Methods (Just t) _ } = Just t
resourceMulti _ = Nothing resourceMulti _ = Nothing
@ -90,7 +74,7 @@ data FlatResource a = FlatResource
, frPieces :: [Piece a] , frPieces :: [Piece a]
, frDispatch :: Dispatch a , frDispatch :: Dispatch a
, frCheck :: Bool , frCheck :: Bool
} } deriving (Show)
flatten :: [ResourceTree a] -> [FlatResource a] flatten :: [ResourceTree a] -> [FlatResource a]
flatten = flatten =

View File

@ -1,5 +0,0 @@
import Test.Hspec
import qualified YesodCoreTest
main :: IO ()
main = hspec YesodCoreTest.specs

View File

@ -17,6 +17,9 @@ module Hierarchy
, toText , toText
, Env (..) , Env (..)
, subDispatch , subDispatch
-- to avoid warnings
, deleteDelete2
, deleteDelete3
) where ) where
import Test.Hspec import Test.Hspec
@ -110,9 +113,9 @@ do
-- /#Int TrailingIntR GET -- /#Int TrailingIntR GET
|] |]
rrinst <- mkRenderRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources rrinst <- mkRenderRouteInstance [] (ConT ''Hierarchy) $ map (fmap parseType) resources
rainst <- mkRouteAttrsInstance (ConT ''Hierarchy) $ map (fmap parseType) resources rainst <- mkRouteAttrsInstance [] (ConT ''Hierarchy) $ map (fmap parseType) resources
prinst <- mkParseRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources prinst <- mkParseRouteInstance [] (ConT ''Hierarchy) $ map (fmap parseType) resources
dispatch <- mkDispatchClause MkDispatchSettings dispatch <- mkDispatchClause MkDispatchSettings
{ mdsRunHandler = [|runHandler|] { mdsRunHandler = [|runHandler|]
, mdsSubDispatcher = [|subDispatch|] , mdsSubDispatcher = [|subDispatch|]

View File

@ -17,7 +17,7 @@ import Test.HUnit ((@?=))
import Data.Text (Text, pack, unpack, singleton) import Data.Text (Text, pack, unpack, singleton)
import Yesod.Routes.Class hiding (Route) import Yesod.Routes.Class hiding (Route)
import qualified Yesod.Routes.Class as YRC import qualified Yesod.Routes.Class as YRC
import Yesod.Routes.Parse (parseRoutesNoCheck, parseTypeTree, TypeTree (..)) import Yesod.Routes.Parse (parseRoutesFile, parseRoutesNoCheck, parseTypeTree, TypeTree (..))
import Yesod.Routes.Overlap (findOverlapNames) import Yesod.Routes.Overlap (findOverlapNames)
import Yesod.Routes.TH hiding (Dispatch) import Yesod.Routes.TH hiding (Dispatch)
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
@ -30,11 +30,7 @@ data MyApp = MyApp
data MySub = MySub data MySub = MySub
instance RenderRoute MySub where instance RenderRoute MySub where
data data
#if MIN_VERSION_base(4,5,0)
Route Route
#else
YRC.Route
#endif
MySub = MySubRoute ([Text], [(Text, Text)]) MySub = MySubRoute ([Text], [(Text, Text)])
deriving (Show, Eq, Read) deriving (Show, Eq, Read)
renderRoute (MySubRoute x) = x renderRoute (MySubRoute x) = x
@ -47,11 +43,7 @@ getMySub MyApp = MySub
data MySubParam = MySubParam Int data MySubParam = MySubParam Int
instance RenderRoute MySubParam where instance RenderRoute MySubParam where
data data
#if MIN_VERSION_base(4,5,0)
Route Route
#else
YRC.Route
#endif
MySubParam = ParamRoute Char MySubParam = ParamRoute Char
deriving (Show, Eq, Read) deriving (Show, Eq, Read)
renderRoute (ParamRoute x) = ([singleton x], []) renderRoute (ParamRoute x) = ([singleton x], [])
@ -80,9 +72,9 @@ do
[ ResourceLeaf $ Resource "ChildR" [] (Methods Nothing ["GET"]) ["child"] True [ ResourceLeaf $ Resource "ChildR" [] (Methods Nothing ["GET"]) ["child"] True
] ]
ress = resParent : resLeaves ress = resParent : resLeaves
rrinst <- mkRenderRouteInstance (ConT ''MyApp) ress rrinst <- mkRenderRouteInstance [] (ConT ''MyApp) ress
rainst <- mkRouteAttrsInstance (ConT ''MyApp) ress rainst <- mkRouteAttrsInstance [] (ConT ''MyApp) ress
prinst <- mkParseRouteInstance (ConT ''MyApp) ress prinst <- mkParseRouteInstance [] (ConT ''MyApp) ress
dispatch <- mkDispatchClause MkDispatchSettings dispatch <- mkDispatchClause MkDispatchSettings
{ mdsRunHandler = [|runHandler|] { mdsRunHandler = [|runHandler|]
, mdsSubDispatcher = [|subDispatch dispatcher|] , mdsSubDispatcher = [|subDispatch dispatcher|]
@ -118,7 +110,7 @@ instance Dispatcher MySub master where
route = MySubRoute (pieces, []) route = MySubRoute (pieces, [])
instance Dispatcher MySubParam master where instance Dispatcher MySubParam master where
dispatcher env (pieces, method) = dispatcher env (pieces, _method) =
case map unpack pieces of case map unpack pieces of
[[c]] -> [[c]] ->
let route = ParamRoute c let route = ParamRoute c
@ -227,63 +219,78 @@ main = hspec $ do
it "routes to subparam" $ disp "PUT" ["subparam", "6", "q"] it "routes to subparam" $ disp "PUT" ["subparam", "6", "q"]
@?= (pack "subparam 6 q", Just $ SubparamR 6 $ ParamRoute 'q') @?= (pack "subparam 6 q", Just $ SubparamR 6 $ ParamRoute 'q')
describe "parsing" $ do describe "route parsing" $ do
it "subsites work" $ do it "subsites work" $ do
parseRoute ([pack "subsite", pack "foo"], [(pack "bar", pack "baz")]) @?= parseRoute ([pack "subsite", pack "foo"], [(pack "bar", pack "baz")]) @?=
Just (SubsiteR $ MySubRoute ([pack "foo"], [(pack "bar", pack "baz")])) Just (SubsiteR $ MySubRoute ([pack "foo"], [(pack "bar", pack "baz")]))
describe "routing table parsing" $ do
it "recognizes trailing backslashes as line continuation directives" $ do
let routes :: [ResourceTree String]
routes = $(parseRoutesFile "test/fixtures/routes_with_line_continuations.yesodroutes")
length routes @?= 3
describe "overlap checking" $ do describe "overlap checking" $ do
it "catches overlapping statics" $ do it "catches overlapping statics" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/foo Foo2 /foo Foo2
|] |]
findOverlapNames routes @?= [("Foo1", "Foo2")] findOverlapNames routes @?= [("Foo1", "Foo2")]
it "catches overlapping dynamics" $ do it "catches overlapping dynamics" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/#Int Foo1 /#Int Foo1
/#String Foo2 /#String Foo2
|] |]
findOverlapNames routes @?= [("Foo1", "Foo2")] findOverlapNames routes @?= [("Foo1", "Foo2")]
it "catches overlapping statics and dynamics" $ do it "catches overlapping statics and dynamics" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/#String Foo2 /#String Foo2
|] |]
findOverlapNames routes @?= [("Foo1", "Foo2")] findOverlapNames routes @?= [("Foo1", "Foo2")]
it "catches overlapping multi" $ do it "catches overlapping multi" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/##*Strings Foo2 /##*Strings Foo2
|] |]
findOverlapNames routes @?= [("Foo1", "Foo2")] findOverlapNames routes @?= [("Foo1", "Foo2")]
it "catches overlapping subsite" $ do it "catches overlapping subsite" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/foo Foo2 Subsite getSubsite /foo Foo2 Subsite getSubsite
|] |]
findOverlapNames routes @?= [("Foo1", "Foo2")] findOverlapNames routes @?= [("Foo1", "Foo2")]
it "no false positives" $ do it "no false positives" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/bar/#String Foo2 /bar/#String Foo2
|] |]
findOverlapNames routes @?= [] findOverlapNames routes @?= []
it "obeys ignore rules" $ do it "obeys ignore rules" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/#!String Foo2 /#!String Foo2
/!foo Foo3 /!foo Foo3
|] |]
findOverlapNames routes @?= [] findOverlapNames routes @?= []
it "obeys multipiece ignore rules #779" $ do it "obeys multipiece ignore rules #779" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
/+![String] Foo2 /+![String] Foo2
|] |]
findOverlapNames routes @?= [] findOverlapNames routes @?= []
it "ignore rules for entire route #779" $ do it "ignore rules for entire route #779" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/foo Foo1 /foo Foo1
!/+[String] Foo2 !/+[String] Foo2
!/#String Foo3 !/#String Foo3
@ -291,7 +298,8 @@ main = hspec $ do
|] |]
findOverlapNames routes @?= [] findOverlapNames routes @?= []
it "ignore rules for hierarchy" $ do it "ignore rules for hierarchy" $ do
let routes = [parseRoutesNoCheck| let routes :: [ResourceTree String]
routes = [parseRoutesNoCheck|
/+[String] Foo1 /+[String] Foo1
!/foo Foo2: !/foo Foo2:
/foo Foo3 /foo Foo3
@ -312,7 +320,7 @@ main = hspec $ do
it "hierarchy" $ do it "hierarchy" $ do
routeAttrs (ParentR (pack "ignored") ChildR) @?= Set.singleton (pack "child") routeAttrs (ParentR (pack "ignored") ChildR) @?= Set.singleton (pack "child")
hierarchy hierarchy
describe "parseRouteTyoe" $ do describe "parseRouteType" $ do
let success s t = it s $ parseTypeTree s @?= Just t let success s t = it s $ parseTypeTree s @?= Just t
failure s = it s $ parseTypeTree s @?= Nothing failure s = it s $ parseTypeTree s @?= Nothing
success "Int" $ TTTerm "Int" success "Int" $ TTTerm "Int"
@ -324,6 +332,8 @@ main = hspec $ do
success "[Int]" $ TTList $ TTTerm "Int" success "[Int]" $ TTList $ TTTerm "Int"
success "Foo-Bar" $ TTApp (TTTerm "Foo") (TTTerm "Bar") success "Foo-Bar" $ TTApp (TTTerm "Foo") (TTTerm "Bar")
success "Foo-Bar-Baz" $ TTApp (TTTerm "Foo") (TTTerm "Bar") `TTApp` TTTerm "Baz" success "Foo-Bar-Baz" $ TTApp (TTTerm "Foo") (TTTerm "Bar") `TTApp` TTTerm "Baz"
success "Foo Bar" $ TTApp (TTTerm "Foo") (TTTerm "Bar")
success "Foo Bar Baz" $ TTApp (TTTerm "Foo") (TTTerm "Bar") `TTApp` TTTerm "Baz"
getRootR :: Text getRootR :: Text
getRootR = pack "this is the root" getRootR = pack "this is the root"

View File

@ -5,17 +5,27 @@ import YesodCoreTest.CleanPath
import YesodCoreTest.Exceptions import YesodCoreTest.Exceptions
import YesodCoreTest.Widget import YesodCoreTest.Widget
import YesodCoreTest.Media import YesodCoreTest.Media
import YesodCoreTest.Meta
import YesodCoreTest.Links import YesodCoreTest.Links
import YesodCoreTest.Header
import YesodCoreTest.NoOverloadedStrings import YesodCoreTest.NoOverloadedStrings
import YesodCoreTest.SubSub
import YesodCoreTest.InternalRequest import YesodCoreTest.InternalRequest
import YesodCoreTest.ErrorHandling import YesodCoreTest.ErrorHandling
import YesodCoreTest.Cache import YesodCoreTest.Cache
import YesodCoreTest.ParameterizedSite
import YesodCoreTest.Breadcrumb
import qualified YesodCoreTest.WaiSubsite as WaiSubsite import qualified YesodCoreTest.WaiSubsite as WaiSubsite
import qualified YesodCoreTest.Redirect as Redirect import qualified YesodCoreTest.Redirect as Redirect
import qualified YesodCoreTest.JsLoader as JsLoader import qualified YesodCoreTest.JsLoader as JsLoader
import qualified YesodCoreTest.RequestBodySize as RequestBodySize import qualified YesodCoreTest.RequestBodySize as RequestBodySize
import qualified YesodCoreTest.Json as Json import qualified YesodCoreTest.Json as Json
-- Skip on Windows, see https://github.com/yesodweb/yesod/issues/1523#issuecomment-398278450
#if !WINDOWS
import qualified YesodCoreTest.RawResponse as RawResponse import qualified YesodCoreTest.RawResponse as RawResponse
#endif
import qualified YesodCoreTest.Streaming as Streaming import qualified YesodCoreTest.Streaming as Streaming
import qualified YesodCoreTest.Reps as Reps import qualified YesodCoreTest.Reps as Reps
import qualified YesodCoreTest.Auth as Auth import qualified YesodCoreTest.Auth as Auth
@ -27,21 +37,26 @@ import Test.Hspec
specs :: Spec specs :: Spec
specs = do specs = do
headerTest
cleanPathTest cleanPathTest
exceptionsTest exceptionsTest
widgetTest widgetTest
mediaTest mediaTest
linksTest linksTest
noOverloadedTest noOverloadedTest
subSubTest
internalRequestTest internalRequestTest
errorHandlingTest errorHandlingTest
cacheTest cacheTest
parameterizedSiteTest
WaiSubsite.specs WaiSubsite.specs
Redirect.specs Redirect.specs
JsLoader.specs JsLoader.specs
RequestBodySize.specs RequestBodySize.specs
Json.specs Json.specs
#if !WINDOWS
RawResponse.specs RawResponse.specs
#endif
Streaming.specs Streaming.specs
Reps.specs Reps.specs
Auth.specs Auth.specs
@ -50,3 +65,5 @@ specs = do
Ssl.sslOnlySpec Ssl.sslOnlySpec
Ssl.sameSiteSpec Ssl.sameSiteSpec
Csrf.csrfSpec Csrf.csrfSpec
breadcrumbTest
metaTest

View File

@ -1,5 +1,9 @@
{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-}
module YesodCoreTest.Auth (specs, Widget) where module YesodCoreTest.Auth
( specs
, Widget
, resourcesApp
) where
import Yesod.Core import Yesod.Core
import Test.Hspec import Test.Hspec

View File

@ -0,0 +1,58 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module YesodCoreTest.Breadcrumb
( breadcrumbTest,
)
where
import qualified Data.ByteString.Lazy.Char8 as L8
import Data.Text (Text)
import Data.Typeable (Typeable)
import Network.Wai
import Network.Wai.Test
import Test.Hspec
import UnliftIO.IORef
import Yesod.Core
data A = A
mkYesod
"A"
[parseRoutes|
/ RootR GET
/loop LoopR GET
|]
instance Yesod A
instance YesodBreadcrumbs A where
breadcrumb r = case r of
RootR -> pure ("Root", Nothing)
LoopR -> pure ("Loop", Just LoopR) -- Purposefully a loop
getRootR :: Handler Text
getRootR = fst <$> breadcrumbs
getLoopR :: Handler Text
getLoopR = fst <$> breadcrumbs
breadcrumbTest :: Spec
breadcrumbTest =
describe "Test.Breadcrumb" $ do
it "can fetch the root which contains breadcrumbs" $
runner $ do
res <- request defaultRequest
assertStatus 200 res
it "gets a 500 for a route with a looping breadcrumb" $
runner $ do
res <- request defaultRequest {pathInfo = ["loop"]}
assertStatus 500 res
runner :: Session () -> IO ()
runner f = toWaiApp A >>= runSession f

View File

@ -1,9 +1,12 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE Rank2Types #-} {-# LANGUAGE Rank2Types #-}
module YesodCoreTest.Cache (cacheTest, Widget) where module YesodCoreTest.Cache
( cacheTest
, Widget
, resourcesC
) where
import Test.Hspec import Test.Hspec
@ -11,17 +14,15 @@ import Network.Wai
import Network.Wai.Test import Network.Wai.Test
import Yesod.Core import Yesod.Core
import Data.IORef.Lifted import UnliftIO.IORef
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import qualified Data.ByteString.Lazy.Char8 as L8 import qualified Data.ByteString.Lazy.Char8 as L8
data C = C data C = C
newtype V1 = V1 Int newtype V1 = V1 Int
deriving Typeable
newtype V2 = V2 Int newtype V2 = V2 Int
deriving Typeable
mkYesod "C" [parseRoutes| mkYesod "C" [parseRoutes|
/ RootR GET / RootR GET
@ -42,7 +43,14 @@ getRootR = do
V2 v2a <- cached $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1) V2 v2a <- cached $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
V2 v2b <- cached $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1) V2 v2b <- cached $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b] cacheBySet "3" (V2 3)
V2 v3a <- cacheByGet "3" >>= \x ->
case x of
Just y -> return y
Nothing -> error "must be Just"
V2 v3b <- cachedBy "3" $ (pure $ V2 4)
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b, v3a, v3b]
getKeyR :: Handler RepPlain getKeyR :: Handler RepPlain
getKeyR = do getKeyR = do
@ -56,7 +64,15 @@ getKeyR = do
V2 v3a <- cachedBy "2" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1) V2 v3a <- cachedBy "2" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
V2 v3b <- cachedBy "2" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1) V2 v3b <- cachedBy "2" $ atomicModifyIORef ref $ \i -> (i + 1, V2 $ i + 1)
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b, v3a, v3b]
cacheBySet "4" (V2 4)
V2 v4a <- cacheByGet "4" >>= \x ->
case x of
Just y -> return y
Nothing -> error "must be Just"
V2 v4b <- cachedBy "4" $ (pure $ V2 5)
return $ RepPlain $ toContent $ show [v1a, v1b, v2a, v2b, v3a, v3b, v4a, v4b]
getNestedR :: Handler RepPlain getNestedR :: Handler RepPlain
getNestedR = getNested cached getNestedR = getNested cached
@ -82,12 +98,12 @@ cacheTest =
it "cached" $ runner $ do it "cached" $ runner $ do
res <- request defaultRequest res <- request defaultRequest
assertStatus 200 res assertStatus 200 res
assertBody (L8.pack $ show [1, 1, 2, 2 :: Int]) res assertBody (L8.pack $ show [1, 1, 2, 2, 3, 3 :: Int]) res
it "cachedBy" $ runner $ do it "cachedBy" $ runner $ do
res <- request defaultRequest { pathInfo = ["key"] } res <- request defaultRequest { pathInfo = ["key"] }
assertStatus 200 res assertStatus 200 res
assertBody (L8.pack $ show [1, 1, 2, 2, 3, 3 :: Int]) res assertBody (L8.pack $ show [1, 1, 2, 2, 3, 3, 4, 4 :: Int]) res
it "nested cached" $ runner $ do it "nested cached" $ runner $ do
res <- request defaultRequest { pathInfo = ["nested"] } res <- request defaultRequest { pathInfo = ["nested"] }

View File

@ -2,7 +2,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances, ViewPatterns #-} {-# LANGUAGE FlexibleInstances, ViewPatterns #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module YesodCoreTest.CleanPath (cleanPathTest, Widget) where module YesodCoreTest.CleanPath
( cleanPathTest
, Widget
, resourcesY
) where
import Test.Hspec import Test.Hspec
@ -18,7 +22,7 @@ import qualified Data.Text.Encoding as TE
import Control.Arrow ((***)) import Control.Arrow ((***))
import Network.HTTP.Types (encodePath) import Network.HTTP.Types (encodePath)
import Data.Monoid (mappend) import Data.Monoid (mappend)
import Blaze.ByteString.Builder.Char.Utf8 (fromText) import Data.Text.Encoding (encodeUtf8Builder)
data Subsite = Subsite data Subsite = Subsite
@ -60,7 +64,7 @@ instance Yesod Y where
corrected = filter (not . TS.null) s corrected = filter (not . TS.null) s
joinPath Y ar pieces' qs' = joinPath Y ar pieces' qs' =
fromText ar `mappend` encodePath pieces qs encodeUtf8Builder ar `Data.Monoid.mappend` encodePath pieces qs
where where
pieces = if null pieces' then [""] else pieces' pieces = if null pieces' then [""] else pieces'
qs = map (TE.encodeUtf8 *** go) qs' qs = map (TE.encodeUtf8 *** go) qs'

View File

@ -1,23 +1,37 @@
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE LambdaCase #-}
module YesodCoreTest.ErrorHandling module YesodCoreTest.ErrorHandling
( errorHandlingTest ( errorHandlingTest
, Widget , Widget
, resourcesApp
) where ) where
import Data.Typeable(cast)
import qualified System.Mem as Mem
import qualified Control.Concurrent.Async as Async
import Control.Concurrent as Conc
import Yesod.Core import Yesod.Core
import Test.Hspec import Test.Hspec
import Network.Wai import Network.Wai
import Network.Wai.Test import Network.Wai.Test
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Char8 as S8
import Control.Exception (SomeException, try) import Control.Exception (SomeException, try, AsyncException(..))
import UnliftIO.Exception(finally)
import Network.HTTP.Types (Status, mkStatus) import Network.HTTP.Types (Status, mkStatus)
import Blaze.ByteString.Builder (Builder, fromByteString, toLazyByteString) import Data.ByteString.Builder (Builder, toLazyByteString)
import Data.Monoid (mconcat) import Data.Monoid (mconcat)
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Control.Monad (forM_) import Control.Monad (forM_)
import qualified Control.Exception.Lifted as E import qualified Network.Wai.Handler.Warp as Warp
import qualified YesodCoreTest.ErrorHandling.CustomApp as Custom
import Control.Monad.Trans.State (StateT (..))
import Control.Monad.Trans.Reader (ReaderT (..))
import qualified UnliftIO.Exception as E
import System.Timeout(timeout)
data App = App data App = App
@ -37,6 +51,15 @@ mkYesod "App" [parseRoutes|
/file-bad-name FileBadNameR GET /file-bad-name FileBadNameR GET
/good-builder GoodBuilderR GET /good-builder GoodBuilderR GET
/auth-not-accepted AuthNotAcceptedR GET
/auth-not-adequate AuthNotAdequateR GET
/args-not-valid ArgsNotValidR POST
/only-plain-text OnlyPlainTextR GET
/thread-killed ThreadKilledR GET
/connection-closed-by-peer ConnectionClosedPeerR GET
/sleep-sec SleepASecR GET
|] |]
overrideStatus :: Status overrideStatus :: Status
@ -98,11 +121,28 @@ getFileBadNameR :: Handler TypedContent
getFileBadNameR = return $ TypedContent "ignored" $ ContentFile (error "filebadname") Nothing getFileBadNameR = return $ TypedContent "ignored" $ ContentFile (error "filebadname") Nothing
goodBuilderContent :: Builder goodBuilderContent :: Builder
goodBuilderContent = mconcat $ replicate 100 $ fromByteString "This is a test\n" goodBuilderContent = Data.Monoid.mconcat $ replicate 100 $ "This is a test\n"
getGoodBuilderR :: Handler TypedContent getGoodBuilderR :: Handler TypedContent
getGoodBuilderR = return $ TypedContent "text/plain" $ toContent goodBuilderContent getGoodBuilderR = return $ TypedContent "text/plain" $ toContent goodBuilderContent
-- this handler kills it's own thread
getThreadKilledR :: Handler Html
getThreadKilledR = do
x <- liftIO Conc.myThreadId
liftIO $ Async.withAsync (Conc.killThread x) Async.wait
pure "unreachablle"
getSleepASecR :: Handler Html
getSleepASecR = do
liftIO $ Conc.threadDelay 1000000
pure "slept a second"
getConnectionClosedPeerR :: Handler Html
getConnectionClosedPeerR = do
x <- liftIO Conc.myThreadId
liftIO $ Async.withAsync (E.throwTo x Warp.ConnectionClosedByPeer) Async.wait
pure "unreachablle"
getErrorR :: Int -> Handler () getErrorR :: Int -> Handler ()
getErrorR 1 = setSession undefined "foo" getErrorR 1 = setSession undefined "foo"
getErrorR 2 = setSession "foo" undefined getErrorR 2 = setSession "foo" undefined
@ -114,6 +154,19 @@ getErrorR 7 = setLanguage undefined
getErrorR 8 = cacheSeconds undefined getErrorR 8 = cacheSeconds undefined
getErrorR 9 = setUltDest (undefined :: Text) getErrorR 9 = setUltDest (undefined :: Text)
getErrorR 10 = setMessage undefined getErrorR 10 = setMessage undefined
getErrorR x = error $ "getErrorR: " ++ show x
getAuthNotAcceptedR :: Handler TypedContent
getAuthNotAcceptedR = notAuthenticated
getAuthNotAdequateR :: Handler TypedContent
getAuthNotAdequateR = permissionDenied "That doesn't belong to you. "
postArgsNotValidR :: Handler TypedContent
postArgsNotValidR = invalidArgs ["Doesn't matter.", "Don't want it."]
getOnlyPlainTextR :: Handler TypedContent
getOnlyPlainTextR = selectRep $ provideRepType "text/plain" $ return ("Only plain text." :: Text)
errorHandlingTest :: Spec errorHandlingTest :: Spec
errorHandlingTest = describe "Test.ErrorHandling" $ do errorHandlingTest = describe "Test.ErrorHandling" $ do
@ -128,6 +181,15 @@ errorHandlingTest = describe "Test.ErrorHandling" $ do
it "file with bad name" caseFileBadName it "file with bad name" caseFileBadName
it "builder includes content-length" caseGoodBuilder it "builder includes content-length" caseGoodBuilder
forM_ [1..10] $ \i -> it ("error case " ++ show i) (caseError i) forM_ [1..10] $ \i -> it ("error case " ++ show i) (caseError i)
it "accept DVI file, invalid args -> 400" caseDviInvalidArgs
it "accept audio, not authenticated -> 401" caseAudioNotAuthenticated
it "accept CSS, permission denied -> 403" caseCssPermissionDenied
it "accept image, non-existent path -> 404" caseImageNotFound
it "accept video, bad method -> 405" caseVideoBadMethod
it "default config exception rethrows connection closed" caseDefaultConnectionCloseRethrows
it "custom config rethrows an exception" caseCustomExceptionRethrows
it "thread killed rethrow" caseThreadKilledRethrow
it "can timeout a runner" canTimeoutARunner
runner :: Session a -> IO a runner :: Session a -> IO a
runner f = toWaiApp App >>= runSession f runner f = toWaiApp App >>= runSession f
@ -215,6 +277,100 @@ caseGoodBuilder = runner $ do
caseError :: Int -> IO () caseError :: Int -> IO ()
caseError i = runner $ do caseError i = runner $ do
res <- request defaultRequest { pathInfo = ["error", pack $ show i] } res <- request defaultRequest { pathInfo = ["error", pack $ show i] }
assertStatus 500 res `E.catch` \e -> do ReaderT $ \r -> StateT $ \s -> runStateT (runReaderT (assertStatus 500 res) r) s `E.catch` \e -> do
liftIO $ print res liftIO $ print res
E.throwIO (e :: E.SomeException) E.throwIO (e :: E.SomeException)
caseDviInvalidArgs :: IO ()
caseDviInvalidArgs = runner $ do
res <- request defaultRequest
{ pathInfo = ["args-not-valid"]
, requestMethod = "POST"
, requestHeaders =
("accept", "application/x-dvi") : requestHeaders defaultRequest
}
assertStatus 400 res
caseAudioNotAuthenticated :: IO ()
caseAudioNotAuthenticated = runner $ do
res <- request defaultRequest
{ pathInfo = ["auth-not-accepted"]
, requestHeaders =
("accept", "audio/mpeg") : requestHeaders defaultRequest
}
assertStatus 401 res
caseCssPermissionDenied :: IO ()
caseCssPermissionDenied = runner $ do
res <- request defaultRequest
{ pathInfo = ["auth-not-adequate"]
, requestHeaders =
("accept", "text/css") : requestHeaders defaultRequest
}
assertStatus 403 res
caseImageNotFound :: IO ()
caseImageNotFound = runner $ do
res <- request defaultRequest
{ pathInfo = ["not_a_path"]
, requestHeaders =
("accept", "image/jpeg") : requestHeaders defaultRequest
}
assertStatus 404 res
caseVideoBadMethod :: IO ()
caseVideoBadMethod = runner $ do
res <- request defaultRequest
{ pathInfo = ["good-builder"]
, requestMethod = "DELETE"
, requestHeaders =
("accept", "video/webm") : requestHeaders defaultRequest
}
assertStatus 405 res
fromExceptionUnwrap :: E.Exception e => SomeException -> Maybe e
fromExceptionUnwrap se
| Just (E.AsyncExceptionWrapper e) <- E.fromException se = cast e
| Just (E.SyncExceptionWrapper e) <- E.fromException se = cast e
| otherwise = E.fromException se
caseThreadKilledRethrow :: IO ()
caseThreadKilledRethrow =
shouldThrow testcode $ \e -> case fromExceptionUnwrap e of
(Just ThreadKilled) -> True
_ -> False
where
testcode = runner $ do
res <- request defaultRequest { pathInfo = ["thread-killed"] }
assertStatus 500 res
assertBodyContains "Internal Server Error" res
caseDefaultConnectionCloseRethrows :: IO ()
caseDefaultConnectionCloseRethrows =
shouldThrow testcode $ \e -> case fromExceptionUnwrap e of
Just Warp.ConnectionClosedByPeer -> True
_ -> False
where
testcode = runner $ do
_res <- request defaultRequest { pathInfo = ["connection-closed-by-peer"] }
pure ()
caseCustomExceptionRethrows :: IO ()
caseCustomExceptionRethrows =
shouldThrow testcode $ \case Custom.MkMyException -> True
where
testcode = customAppRunner $ do
_res <- request defaultRequest { pathInfo = ["throw-custom-exception"] }
pure ()
customAppRunner f = toWaiApp Custom.CustomApp >>= runSession f
canTimeoutARunner :: IO ()
canTimeoutARunner = do
res <- timeout 1000 $ runner $ do
res <- request defaultRequest { pathInfo = ["sleep-sec"] }
assertStatus 200 res -- if 500, it's catching the timeout exception
pure () -- it should've timeout by now, either being 500 or Nothing
res `shouldBe` Nothing -- make sure that pure statement didn't happen.

View File

@ -0,0 +1,41 @@
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveAnyClass #-}
-- | a custom app that throws an exception
module YesodCoreTest.ErrorHandling.CustomApp
(CustomApp(..)
, MyException(..)
-- * unused
, Widget
, resourcesCustomApp
) where
import Yesod.Core.Types
import Yesod.Core
import qualified UnliftIO.Exception as E
data CustomApp = CustomApp
mkYesod "CustomApp" [parseRoutes|
/throw-custom-exception CustomHomeR GET
|]
getCustomHomeR :: Handler Html
getCustomHomeR =
E.throwIO MkMyException
data MyException = MkMyException
deriving (Show, E.Exception)
instance Yesod CustomApp where
-- something we couldn't do before, rethrow custom exceptions
catchHandlerExceptions _ action handler =
action `E.catch` \exception -> do
case E.fromException exception of
Just MkMyException -> E.throwIO MkMyException
Nothing -> handler exception

Some files were not shown because too many files have changed in this diff Show More