Modified: websites/production/tapestry/content/persistent-page-data.html
==============================================================================
--- websites/production/tapestry/content/persistent-page-data.html (original)
+++ websites/production/tapestry/content/persistent-page-data.html Mon Jul 11
07:12:07 2022
@@ -160,7 +160,7 @@
<span class="icon aui-icon content-type-page"
title="Page">Page:</span> </div>
<div class="details">
- <a href="session-storage.html">Session Storage</a>
+ <a href="performance-and-clustering.html">Performance
and Clustering</a>
</div>
@@ -169,7 +169,7 @@
<span class="icon aui-icon content-type-page"
title="Page">Page:</span> </div>
<div class="details">
- <a href="performance-and-clustering.html">Performance
and Clustering</a>
+ <a href="session-storage.html">Session Storage</a>
</div>
@@ -190,10 +190,10 @@
<pre><code class="language-java"> @Persist
private int value;
</code></pre>
-</div></div><p>Such annotated fields will retain their state between requests.
Generally, speaking, this means that the value is stored into the session (but
other approaches are possible).</p><p>Whenever you make a change to a
persistent field, its value is saved. On later requests to the same page, the
value for the field is restored.</p><h2
id="PersistentPageData-PersistenceStrategies">Persistence Strategies</h2><p>The
value for each field is the <em>strategy</em> used to store the field between
requests.</p><h3 id="PersistentPageData-SessionStrategy">Session
Strategy</h3><div class="navmenu" style="float:right; background:#eee;
margin:3px; padding:0 1em">
+</div></div><p>Such annotated fields will retain their state between requests.
Generally, speaking, this means that the value is stored into the session (but
other approaches are possible).</p><p>Whenever you make a change to a
persistent field, its value is saved. On later requests to the same page, the
value for the field is restored.</p><h2
id="PersistentPageData-PersistenceStrategies">Persistence Strategies</h2><p>The
value for each field is the <em>strategy</em> used to store the field between
requests.</p><h3 id="PersistentPageData-SessionStrategy">Session
Strategy</h3><p></p><div class="navmenu" style="float:right; background:#eee;
margin:3px; padding:0 1em">
<p> <strong>JumpStart Demo:</strong>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/state/storingdatainapage"
rel="nofollow">Storing Data in a Page<sup><img align="middle"
class="rendericon" src="/images/confluence/icons/linkext7.gif" height="7"
width="7" alt="" border="0"></sup></a></span>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/state/passingdatabetweenpages"
rel="nofollow">Passing Data Between Pages<sup><img align="middle"
class="rendericon" src="/images/confluence/icons/linkext7.gif" height="7"
width="7" alt="" border="0"></sup></a></span></p></div>The session strategy
stores field changes into the session; the session is created as necessary.
Session strategy is the default strategy used unless otherwise overridden.<p>A
suitably long session attribute name is used; it incorporates the name of the
page, the nested component id, and the name of the field.</p><div class="code
panel pdl" style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>Example: Session Strategy</b></div><div
class="codeContent panelContent pdl">
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/state/storingdatainapage"
rel="nofollow">Storing Data in a Page<sup><img align="middle"
class="rendericon" src="/images/confluence/icons/linkext7.gif" height="7"
width="7" alt="" border="0"></sup></a></span>
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/state/passingdatabetweenpages"
rel="nofollow">Passing Data Between Pages<sup><img align="middle"
class="rendericon" src="/images/confluence/icons/linkext7.gif" height="7"
width="7" alt="" border="0"></sup></a></span></p></div>The session strategy
stores field changes into the session; the session is created as necessary.
Session strategy is the default strategy used unless otherwise
overridden.<p></p><p>A suitably long session attribute name is used; it
incorporates the name of the page, the nested component id, and the name of the
field.</p><div class="code panel pdl" style="border-width: 1px;"><div
class="codeHeader panelHeader pdl" style="border-bottom-width:
1px;"><b>Example: Session Strategy</b></div><div class="codeContent
panelContent pdl">
<pre><code class="language-java"> @Persist
private int value;
</code></pre>
@@ -208,7 +208,7 @@
</div></div><h3 id="PersistentPageData-HibernateEntityStrategy">Hibernate
Entity Strategy</h3><p><span>Entity persistence is provided by the
tapestry-hibernate module (which extends Tapestry with new
features).</span></p><p>In Entity persistence, the field should store a
Hibernate entity instance.</p><div class="code panel pdl" style="border-width:
1px;"><div class="codeHeader panelHeader pdl" style="border-bottom-width:
1px;"><b>"Hibernate Entity Strategy"</b></div><div class="codeContent
panelContent pdl">
<pre><code class="language-java">
@Persist(HibernatePersistenceConstants.ENTITY)
private User user;</code></pre>
-</div></div><p>The value stored in the HttpSession is a <em>token</em>
for the entity: its Java class name and primary key. When the field is restored
in a later request, the entity is re-instantiated using that data.</p><p>What
is <em>not</em> stored is any changes to the persistent entity that are
not committed to the external datastore (the database).</p><p>Starting in
Tapestry 5.4, it is possible to store a non-persistent entity (a transient
entity). A transient entity is stored directly into the HttpSession, and should
be Serializable if the application is clustered.</p><h3
id="PersistentPageData-JPAEntityStrategy">JPA Entity Strategy</h3><p>The
tapestry-jpa module uses a similar strategy. However, at the current time it
can only store a persisted entity (one that has been saved to the database and
has a primary key).</p><div class="code panel pdl" style="border-width:
1px;"><div class="codeHeader panelHeader pdl" style="border-bottom-width:
1px;"><b>"Example: JPA Ent
ity Strategy"</b></div><div class="codeContent panelContent pdl">
+</div></div><p></p><p>The value stored in the HttpSession is
a <em>token</em> for the entity: its Java class name and primary key. When
the field is restored in a later request, the entity is re-instantiated using
that data.</p><p>What is <em>not</em> stored is any changes to the
persistent entity that are not committed to the external datastore (the
database).</p><p>Starting in Tapestry 5.4, it is possible to store a
non-persistent entity (a transient entity). A transient entity is stored
directly into the HttpSession, and should be Serializable if the application is
clustered.</p><h3 id="PersistentPageData-JPAEntityStrategy">JPA Entity
Strategy</h3><p>The tapestry-jpa module uses a similar strategy. However, at
the current time it can only store a persisted entity (one that has been saved
to the database and has a primary key).</p><div class="code panel pdl"
style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>"Example:
JPA Entity Strategy"</b></div><div class="codeContent panelContent pdl">
<pre><code class="language-java"> @Persist(JpaPersistenceConstants.ENTITY)
private Account account;</code></pre>
</div></div><h3 id="PersistentPageData-PersistenceStrategyInheritance"><span
style="color: rgb(83,145,38);">Persistence Strategy
Inheritance</span></h3><p>By default the value for the Persist annotation is
the empty string. When this is true, then the actual strategy to be used is
determined by a search up the component hierarchy.</p><p>For each component,
the meta-data property <code>tapestry.persistence-strategy</code> is checked.
This can be specified using the @<a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/annotations/Meta.html">Meta</a>
annotation.</p><p>If the value is non-blank, then that strategy is used. This
allows a component to control the persistence strategy used inside any
sub-components (that don't explicitly use a different strategy).</p><p>In any
case, if no component provides the meta data, then the ultimate default,
"session", is used.</p><h2 id="PersistentPageData-DefaultValues">Default
Values</h2><p>Fields marked
with @Persist may not have default values (whether set inline, or inside a
constructor).</p><h2 id="PersistentPageData-ClearingPersistentFields">Clearing
Persistent Fields</h2><p>If you reach a point where you know that all data for
a page can be discarded, you can do exactly that.</p><p>The method
<code>discardPersistentFieldChanges()</code> of ComponentResources will discard
all persistent fields for the page, regardless of which strategy is used to
store the property. This will not affect the page in memory, but takes effect
for subsequent requests.</p><h2
id="PersistentPageData-ClusteringIssues">Clustering Issues</h2><p>The Servlet
API was designed with the intention that there would be only a modest amount of
server-side state, and that the stored values would be individual numbers and
strings, and thus, immutable.</p><p>However, many web applications do not use
the HttpSession this way, instead storing large, mutable objects in the
session. This is not a problem for single se
rvers, but in a cluster, anything stored in the session must be serialized to
a bytestream and distributed to other servers within the cluster, and restored
there.</p><p>Most application servers perform that serialization and
distribution whenever HttpSession.setAttribute() is called. This creates a data
consistency problem for mutable objects, because if you read a mutable session
object, change its state, but <em>don't</em> invoke setAttribute(), the changes
will be isolated to just a single server in the cluster.</p><p>Tapestry
attempts to solve this: any session-persisted object that is read during a
request will be re-stored back into the HttpSession at the end of the request.
This ensures that changed internal state of those mutable objects is properly
replicated around the cluster.</p><p>But while this solution solves the data
consistency problem, it does so at the expense of performance, since all of
those calls to setAttribute() result in extra session data being replicated
needlessly if the internal state of the mutable object hasn't
changed.</p><p>Tapestry has solutions to this, too:</p><h3
id="PersistentPageData-@ImmutableSessionPersistedObjectAnnotation">@ImmutableSessionPersistedObject
Annotation</h3><p>Tapestry knows that Java's String, Number and Boolean
classes are immutable. Immutable objects do not require a re-store into the
session.</p><p>You can mark your own session objects as immutable (and thus not
requiring session replication) using the <a class="external-link"
href="https://tapestry.apache.org/current/apidocs/org/apache/tapestry5/http/annotations/ImmutableSessionPersistedObject.html">ImmutableSessionPersistedObject</a>
annotation.</p><h3
id="PersistentPageData-OptimizedSessionPersistedObjectInterface">OptimizedSessionPersistedObject
Interface</h3><p>The <a class="external-link"
href="https://tapestry.apache.org/current/apidocs/org/apache/tapestry5/http/OptimizedSessionPersistedObject.html">OptimizedSessionPersistedObject</a>
interfa
ce allows an object to control this behavior. An object with this interface
can track when its mutable state changes. Typically, you should extend from the
<a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/BaseOptimizedSessionPersistedObject.html">BaseOptimizedSessionPersistedObject</a>
base class.</p><h3
id="PersistentPageData-SessionPersistedObjectAnalyzerService">SessionPersistedObjectAnalyzer
Service</h3><p>The <a class="external-link"
href="https://tapestry.apache.org/current/apidocs/org/apache/tapestry5/http/services/SessionPersistedObjectAnalyzer.html">SessionPersistedObjectAnalyzer</a>
service is ultimately responsible for determining whether a session persisted
object is dirty or not (dirty meaning in need of a restore into the session).
This is an extensible service where new strategies, for new classes, can be
introduced.</p><div class="code panel pdl" style="border-width: 1px;"><div
class="codeHeader panelHeader pdl" style="bo
rder-bottom-width: 1px;"><b>Example: Entity Session Strategy</b></div><div
class="codeContent panelContent pdl">
Modified: websites/production/tapestry/content/security.html
==============================================================================
--- websites/production/tapestry/content/security.html (original)
+++ websites/production/tapestry/content/security.html Mon Jul 11 07:12:07 2022
@@ -197,7 +197,7 @@
<p></p><h2 id="Security-HTTPS-onlyPages">HTTPS-only Pages</h2><p>Main Article:
<a href="https.html">HTTPS</a></p><p>Tapestry provides several annotations and
configuration settings that you can use to <span>ensure that all access to
certain pages (or all pages) occurs only via the encrypted HTTPS
protocol</span><span>. See <a href="https.html">HTTPS</a> for
details.</span></p><h2 id="Security-ControllingPageAccess"><span>Controlling
Page Access</span></h2><p></p><div class="navmenu" style="float:right;
background:#eee; margin:3px; padding:0 1em">
<p> <strong>JumpStart Demo:</strong>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/infrastructure/protectingpages"
rel="nofollow">Protecting Pages<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div><p><span>For simple access control needs,
you can contribute a <span><a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/services/ComponentRequestFilter.html">ComponentRequestFilter</a>
with your custom logic that decides which pages should be accessed by which
users. The <a class="external-link"
href="https://tapestry-app.apache.org/hotels/">Tapestry Hotel Booking </a>app
demonstrates this approach with an <code>@AnonymousAccess</code> annotation
along with a ComponentRequestFilter
named <code>AuthenticationFilter.java</code>. The filter enforces security
by intercepting all requests to pages that don't hav
e that annotation, and it redirects those requests to the login page. <a
class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/infrastructure/protectingpages"
rel="nofollow">JumpStart</a> has a similar
demo.</span></span></p><p></p><p><span>For more advanced needs see the Security
Framework Integration section below.</span></p><h2
id="Security-White-listedPages">White-listed Pages</h2><p>Pages whose component
classes are annotated with @<a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/annotations/WhitelistAccessOnly.html">WhitelistAccessOnly</a> will
only be displayed to users (clients) that are on the <em>whitelist</em>.
By default the whitelist consists only of clients whose fully-qualified domain
name is "localhost" (or the IP address equivalent, 127.0.0.1 or
0:0:0:0:0:0:0:1), but you can customize this by contributing to the
ClientWhitelist service in your application's module
class (usually AppModule.java):</p><div class="code panel pdl"
style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>AppModule.java (partial) – simple
inline example</b></div><div class="codeContent panelContent pdl">
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/infrastructure/protectingpages"
rel="nofollow">Protecting Pages<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div><span>For simple access control needs,
you can contribute a <span><a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/services/ComponentRequestFilter.html">ComponentRequestFilter</a>
with your custom logic that decides which pages should be accessed by which
users. The <a class="external-link"
href="https://tapestry-app.apache.org/hotels/">Tapestry Hotel Booking </a>app
demonstrates this approach with an <code>@AnonymousAccess</code> annotation
along with a ComponentRequestFilter
named <code>AuthenticationFilter.java</code>. The filter enforces security
by intercepting all requests to pages that don't have that anno
tation, and it redirects those requests to the login page. <a
class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/infrastructure/protectingpages"
rel="nofollow">JumpStart</a> has a similar
demo.</span></span><p></p><p><span>For more advanced needs see the Security
Framework Integration section below.</span></p><h2
id="Security-White-listedPages">White-listed Pages</h2><p>Pages whose component
classes are annotated with @<a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/annotations/WhitelistAccessOnly.html">WhitelistAccessOnly</a> will
only be displayed to users (clients) that are on the <em>whitelist</em>.
By default the whitelist consists only of clients whose fully-qualified domain
name is "localhost" (or the IP address equivalent, 127.0.0.1 or
0:0:0:0:0:0:0:1), but you can customize this by contributing to the
ClientWhitelist service in your application's module class (usually
AppModul
e.java):</p><div class="code panel pdl" style="border-width: 1px;"><div
class="codeHeader panelHeader pdl" style="border-bottom-width:
1px;"><b>AppModule.java (partial) – simple inline example</b></div><div
class="codeContent panelContent pdl">
<pre><code class="language-java"> @Contribute(ClientWhitelist.class)
public static void
provideWhitelistAnalyzer(OrderedConfiguration<WhitelistAnalyzer>
configuration)
{
@@ -210,7 +210,7 @@
}
}, "before:*");
}</code></pre>
-</div></div><p></p><p>Sometimes, in production, a firewall or proxy may make
it look like the client web browser originates from localhost, with the
consequence that whitelisted pages may be visible to all users. See the <a
href="security-faq.html">Security FAQ</a> for how to deal with this.</p><h2
id="Security-AssetSecurity">Asset Security</h2><p>Main Article: <a
href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=85462227">Security</a></p><p
class="confluence-link">Tapestry serves assets (static content such as CSS
files, images, and JavaScript, many of which are on the classpath alongside
your compiled class files) to the client. Because of this, great care has
gone into ensuring that certain file types cannot be served to the client. By
default, file ending with ".class', ".tml" and ".properties" can be served to
the client only if the request includes the file's MD5 checksum. As you would
expect, that blacklist can be extended. See <a href="ass
ets.html">Assets</a> for more information.</p><h2
id="Security-ProtectingSerializedObjectDataontheClient">Protecting Serialized
Object Data on the Client</h2><p><span style="color: rgb(0,0,0);">As of version
5.3.6, Tapestry integrates a </span><a class="external-link"
style="text-decoration: underline;text-align: justify;"
href="http://en.wikipedia.org/wiki/HMAC" rel="nofollow">hash-based message
authentication code</a><span style="color: rgb(0,0,0);"> (HMAC) into
serialized Java object data that it sends to the client (generally, this means
the </span><code style="text-align: justify;">t:formdata</code><span
style="color: rgb(0,0,0);"> hidden field used by the Form component). This
ensures that the hidden binary object data is guaranteed to be unaltered when
it returns to the server upon form (or AJAX) submission. The HMAC pass phrase
is set using the <a href="configuration.html">tapestry.hmac-passphrase</a>
configuration symbol. If you don't set that value
, you'll see a warning message in the browser, like this: </span></p><div
class="preformatted panel" style="border-width: 1px;"><div
class="preformattedContent panelContent">
+</div></div><p></p><p>Sometimes, in production, a firewall or proxy may make
it look like the client web browser originates from localhost, with the
consequence that whitelisted pages may be visible to all users. See the <a
href="security-faq.html">Security FAQ</a> for how to deal with this.</p><h2
id="Security-AssetSecurity">Asset Security</h2><p>Main Article: <a
href="security.html">Security</a></p><p class="confluence-link">Tapestry serves
assets (static content such as CSS files, images, and JavaScript, many of which
are on the classpath alongside your compiled class files) to the
client. Because of this, great care has gone into ensuring that certain
file types cannot be served to the client. By default, file ending with
".class', ".tml" and ".properties" can be served to the client only if the
request includes the file's MD5 checksum. As you would expect, that blacklist
can be extended. See <a href="assets.html">Assets</a> for more
information.</p><h2 id="Securi
ty-ProtectingSerializedObjectDataontheClient">Protecting Serialized Object
Data on the Client</h2><p><span style="color: rgb(0,0,0);">As of version 5.3.6,
Tapestry integrates a </span><a class="external-link"
style="text-decoration: underline;text-align: justify;"
href="http://en.wikipedia.org/wiki/HMAC" rel="nofollow">hash-based message
authentication code</a><span style="color: rgb(0,0,0);"> (HMAC) into
serialized Java object data that it sends to the client (generally, this means
the </span><code style="text-align: justify;">t:formdata</code><span
style="color: rgb(0,0,0);"> hidden field used by the Form component). This
ensures that the hidden binary object data is guaranteed to be unaltered when
it returns to the server upon form (or AJAX) submission. The HMAC pass phrase
is set using the <a href="configuration.html">tapestry.hmac-passphrase</a>
configuration symbol. If you don't set that value, you'll see a warning message
in the browser, like this:
60;</span></p><div class="preformatted panel" style="border-width: 1px;"><div
class="preformattedContent panelContent">
<pre>The symbol 'tapestry.hmac-passphrase' has not been configured. This is
used to configure hash-based message authentication of Tapestry data stored in
forms, or in the URL. You application is less secure, and more vulnerable to
denial-of-service attacks, when this symbol is not configured.</pre>
</div></div><p><span style="color: rgb(0,0,0);">The solution is to set the
tapestry.hmac-passphrase to some value (any fixed, private string, such as 30
to 40 random-looking characters, will do) in your application's module class
(usually AppModule.java).</span></p><h2
id="Security-CrossSiteRequestForgery(CSRF)"><span style="color:
rgb(83,145,38);">Cross Site Request Forgery (CSRF)</span></h2><p>Cross Site
Request Forgery is a type of security vulnerability in which legitimate,
authorized users may be made to unwittingly submit malicious requests to your
web application.</p><p><a class="external-link"
href="https://github.com/porscheinformatik/tapestry-csrf-protection"
rel="nofollow">Tapestry-csrf-protection</a> is a 3rd party module that has
several features for preventing CSRF attacks. It protects
all <span>component event handlers (event links, forms, etc.) by adding
a </span><span>CSRF token to event links and adds a CSRF token as a hidden
field to all forms. 
;</span><span>Tokens are generated on a per-session basis.</span></p><h2
id="Security-SecurityFrameworkIntegration"><span>Security Framework
Integration</span></h2><p>Tapestry does not lock you into a specific
authentication/authorization implementation. There are integration modules
available for the more popular open source Java security frameworks. A popular
choice among Tapestry users is <a class="external-link"
href="http://www.tynamo.org/tapestry-security+guide/"
rel="nofollow">tapestry-security (based on Apache Shiro) from Tynamo.org</a>.
It is always kept up-to-date with the latest Tapestry versions and offers
several supporting security modules (e.g. <a class="external-link"
href="http://www.tynamo.org/tapestry-security-jpa+guide/"
rel="nofollow">tapestry-security-jpa</a>, <a class="external-link"
href="http://www.tynamo.org/tynamo-federatedaccounts+guide/"
rel="nofollow">tynamo-federatedaccounts</a>). There's also an <a
class="external-link" href="http://www.localhost.nu/j
ava/tapestry-spring-security" rel="nofollow">integration module available for
Spring Security</a> but lately, it hasn't kept up with the latest versions of
Tapestry 5.</p><p>Additional information:</p><ul><li><a class="external-link"
href="http://www.tynamo.org/tynamo-federatedaccounts+guide/"
rel="nofollow">Tynamo-federatedaccounts</a> <span style="color:
rgb(0,0,0);">is an add-on to the </span><a class="external-link"
href="http://www.tynamo.org/tapestry-security+guide/"
rel="nofollow">tapestry-security</a><span style="color:
rgb(0,0,0);"> module, providing federated (third-party) authentication
with Facebook, Twitter or Google.</span></li></ul><ul><li><span>To include
OpenID with Spring Security in your application, see the following Wiki
entry: </span><a class="external-link"
href="http://wiki.apache.org/tapestry/Tapestry5HowToSpringSecurityAndOpenId">http://wiki.apache.org/tapestry/Tapestry5HowToSpringSecurityAndOpenId</a></li></ul><h2
id="Security-Vulnerabi
lityDisclosures">Vulnerability Disclosures</h2><h3
id="Security-CVE-2019-0195:FilereadingLeadstoJavaDeserializationVulnerability.">CVE-2019-0195:
File reading Leads to Java Deserialization Vulnerability.</h3><p>Disclosure
date: <a class="external-link"
href="https://lists.apache.org/thread.html/5173c4eed06e2fca6fd5576ed723ff6bb1711738ec515cb51a04ab24@%3Cusers.tapestry.apache.org%3E">September
13th, 2019</a></p><p>Versions affected: all Apache Tapestry versions from
5.4.0 (including its betas) through 5.4.3</p><p>Description: Manipulating
classpath asset file URLs, an attacker could guess the path to a known
file in the classpath and have it downloaded. If the attacker found the
file with the value of the tapestry.hmac-passphrase configuration symbol,
most probably the webapp's AppModule class, the value of this symbol could
be used to craft a Java deserialization attack, thus running malicious
injected Java code. The vector would be the t:formdata
parameter from the Form component.</p><p>Mitigation: Upgrade to Tapestry
5.4.5, which is a drop-in replacement for any 5.4.x version.</p><p>Credit:
Ricter Zheng</p><h3
id="Security-CVE-2019-0207:ApacheTapestry5.4.2PathTraversalvulnerability">CVE-2019-0207:
Apache Tapestry 5.4.2 Path Traversal vulnerability</h3><p>Disclosure
date: <a class="external-link"
href="https://lists.apache.org/thread.html/765be3606d865de513f6df9288842c3cf58b09a987c617a535f2b99d@%3Cusers.tapestry.apache.org%3E">September
13th, 2019</a></p><p>Versions affected: all Apache Tapestry versions from
5.4.0 (including its betas) through 5.4.4.</p><p>Description: Tapestry
processes assets `/assets/ctx` using classes chain `StaticFilesFilter ->
AssetDispatcher -> ContextResource`, which doesn't filter the character `\`,
so attacker can perform a path traversal attack to read any files on Windows
platform.</p><p>Mitigation: Upgrade to Tapestry 5.4.5, which is a drop-in
replacement for any 5.4.x version.<
/p><p>Credit: Ricter Zheng</p><h3
id="Security-CVE-2019-10071:NewIssueinFixforCVE-2014-1972">CVE-2019-10071: New
Issue in Fix for CVE-2014-1972</h3><p>Disclosure date: <a
rel="nofollow">September 13th, 2019</a></p><p>Versions affected: all Apache
Tapestry versions from 5.4.0 (including its betas) through
5.4.3</p><p>Description: The code which checks HMAC in form submissions
usedString.equals() for comparisons, which results in a timing side channel
vulnerability inthe comparison of the HMAC signatures. This could lead to
remote codeexecution if an attacker is able to determine the correct signature
fortheir payload. The comparison should have been done with a constant time
algorithm instead.</p><p>Mitigation: Upgrade to Tapestry 5.4.5, which is a
drop-in replacement for any 5.4.xversion.</p><p>Credit: </p><pre>David
Tomaschik of the Google Security Team</pre><h3
id="Security-CVE-2019-10071:BypassofthefixforCVE-2019-0195">CVE-2019-10071: Bypass
of the fix for CVE-2019
-0195</h3><p>Disclosure date: <a rel="nofollow">March 14th,
2021</a></p><p>Versions affected: all Apache Tapestry versions from 5.4.0
(including its betas) through 5.6.1, plus 5.7.0.</p><p>Description: A
critical unauthenticated remote code execution vulnerability was found all
recent versions of Apache Tapestry. The affected versions include 5.4.5,
5.5.0, 5.6.2 and 5.7.0.</p><p>The vulnerability I have found is a bypass of the
fix for CVE-2019-0195. </p><p>Recap:</p><p>Before the fix of CVE-2019-0195
it was possible to download arbitrary class files from the classpath by
providing a crafted asset file URL. An attacker was able to download the
file `AppModule.class` by requesting the URL `<span
class="nolink">http://localhost:8080/assets/something/services/AppModule.class</span>`
which contains a HMAC secret key. The fix for that bug was a blacklist
filter that checks if the URL ends with `.class`, `.properties` or
`.xml`.</p><p>Bypass:</
p><p>Unfortunately, the blacklist solution can simply be bypassed by appending
a `/` at the end of the URL:
`http://localhost:8080/assets/something/services/AppModule.class/`</p><p>The
slash is stripped after the blacklist check and the file `AppModule.class` is
loaded into the response. This class usually contains the HMAC secret key
which is used to sign</p><p>serialized Java objects. With the knowledge of
that key an attacker can sign a Java gadget chain that leads to RCE (e.g.
CommonsBeanUtils1 from ysoserial). </p><p>Solution for this
vulnerability:</p><p>* For Apache Tapestry 5.4.0 to 5.6.1, upgrade to 5.6.2 or
later.</p><p>* For Apache Tapestry 5.7.0, upgrade to 5.7.1 or later.</p><p>This
issue is being tracked as TAP5-2663</p><p>Credit:</p><p>Apache Tapestry would
like to thank Johannes Moritz for finding and notifying this
vulnerability</p><p></p><pre></pre></div>
</div>
Modified: websites/production/tapestry/content/uploading-files.html
==============================================================================
--- websites/production/tapestry/content/uploading-files.html (original)
+++ websites/production/tapestry/content/uploading-files.html Mon Jul 11
07:12:07 2022
@@ -151,7 +151,7 @@
</code></pre>
</div></div><p>If you aren't using Maven, you'll have to download the jar and
its dependencies yourself.</p><h1 id="UploadingFiles-Usage">Usage</h1><p>The
upload component supports default value binding (based on id) and
validation.</p><div class="navmenu" style="float:right; background:#eee;
margin:3px; padding:0 1em">
<p> <strong>JumpStart Demo:</strong>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/javascript/fileupload"
rel="nofollow">File Upload<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div><h2
id="UploadingFiles-ComponentTemplate">Component Template</h2><div class="code
panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/javascript/fileupload"
rel="nofollow">File Upload<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div><h2
id="UploadingFiles-ComponentTemplate">Component Template</h2><div class="code
panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<pre><code class="language-java"> <t:form>
<t:errors/>
<input t:type="upload" t:id="file" t:value="file"
validate="required"/>
Modified: websites/production/tapestry/content/using-select-with-a-list.html
==============================================================================
--- websites/production/tapestry/content/using-select-with-a-list.html
(original)
+++ websites/production/tapestry/content/using-select-with-a-list.html Mon Jul
11 07:12:07 2022
@@ -168,11 +168,11 @@
</div><h1 id="UsingSelectWithaList-UsingSelectWithaList">Using Select With a
List</h1><p>The documentation for the <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/corelib/components/Select.html">Select
Component</a> and the <a href="tapestry-tutorial.html">Tapestry Tutorial</a>
provide simplistic examples of populating a drop-down menu (as the (X)HTML
<em>Select</em> element) using comma-delimited strings and enums. However, most
real-world Tapestry applications need to populate such menus using values from
a database, commonly in the form of java.util.List objects. Doing so generally
requires a <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/SelectModel.html">SelectModel</a>
and a <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/ValueEncoder.html">ValueEncoder</a>
bound to the Select component with its "model" and "encoder"
parameters:</p><div
class="code panel pdl" style="border-width: 1px;"><div class="codeContent
panelContent pdl">
<pre><code class="language-java"><t:select t:id="colorMenu"
value="selectedColor" model="ColorSelectModel" encoder="colorEncoder" />
</code></pre>
-</div></div><p>In the above example, ColorSelectModel must be of type
SelectModel, or anything that Tapestry knows how to <a
href="parameter-type-coercion.html">coerce</a> into a SelectModel, such as a
List or a Map or a "value=label,value=label,..." delimited string, or anything
Tapestry knows how to coerce into a List or Map, such as an Array or a
comma-delimited String.</p><h2
id="UsingSelectWithaList-SelectModel">SelectModel</h2><div class="navmenu"
style="float:right; background:#eee; margin:3px; padding:0 1em">
+</div></div><p>In the above example, ColorSelectModel must be of type
SelectModel, or anything that Tapestry knows how to <a
href="parameter-type-coercion.html">coerce</a> into a SelectModel, such as a
List or a Map or a "value=label,value=label,..." delimited string, or anything
Tapestry knows how to coerce into a List or Map, such as an Array or a
comma-delimited String.</p><h2
id="UsingSelectWithaList-SelectModel">SelectModel</h2><p></p><div
class="navmenu" style="float:right; background:#eee; margin:3px; padding:0 1em">
<p> <strong>JumpStart Demos:</strong>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/select/totalcontrolobject"
rel="nofollow">Total Control Object Select<sup><img align="middle"
class="rendericon" src="/images/confluence/icons/linkext7.gif" height="7"
width="7" alt="" border="0"></sup></a></span>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/select/id"
rel="nofollow">ID Select<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/select/easyid"
rel="nofollow">Easy ID Select<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div>A SelectModel is a collection of options
(specifically <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/OptionModel.html">OptionModel</a>
objects) for a drop-down menu. Basically, each option is a value (an object)
and a label (presented to the user).<p>If you provide a property of type List
for the "model" parameter, Tapestry automatically builds a SelectModel that
uses each object's toString() for both the select option value and the select
option label. For database-derrived lists this is rarely useful, however, since
after form submission you would then have to look up the selected object using
that label.</p><p>If you provide a
Map, Tapestry builds a SelectModel that uses each item's key as the encoded
value and its value as the user-visible label. This is more useful, but if you
are going to build a copy of the list as a map just for this purpose, you may
as well let Tapestry do it for you, using SelectModelFactory.</p><h2
id="UsingSelectWithaList-SelectModelFactory">SelectModelFactory</h2><p>To have
Tapestry create a SelectModel for you, use the <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/services/SelectModelFactory.html">SelectModelFactory</a>
service. SelectModelFactory creates a SelectModel from a List of objects (of
whatever type) and a label property name that you choose:</p><div class="code
panel pdl" style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>SelectWithListDemo.java (a page
class)</b></div><div class="codeContent panelContent pdl">
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/select/totalcontrolobject"
rel="nofollow">Total Control Object Select<sup><img align="middle"
class="rendericon" src="/images/confluence/icons/linkext7.gif" height="7"
width="7" alt="" border="0"></sup></a></span>
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/select/id"
rel="nofollow">ID Select<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span>
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/select/easyid"
rel="nofollow">Easy ID Select<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div>A SelectModel is a collection of options
(specifically <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/OptionModel.html">OptionModel</a>
objects) for a drop-down menu. Basically, each option is a value (an object)
and a label (presented to the user).<p></p><p>If you provide a property of type
List for the "model" parameter, Tapestry automatically builds a SelectModel
that uses each object's toString() for both the select option value and the
select option label. For database-derrived lists this is rarely useful,
however, since after form submission you would then have to look up the
selected object using that label.</p><p>If you provide a M
ap, Tapestry builds a SelectModel that uses each item's key as the encoded
value and its value as the user-visible label. This is more useful, but if you
are going to build a copy of the list as a map just for this purpose, you may
as well let Tapestry do it for you, using SelectModelFactory.</p><h2
id="UsingSelectWithaList-SelectModelFactory">SelectModelFactory</h2><p>To have
Tapestry create a SelectModel for you, use the <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/services/SelectModelFactory.html">SelectModelFactory</a>
service. SelectModelFactory creates a SelectModel from a List of objects (of
whatever type) and a label property name that you choose:</p><div class="code
panel pdl" style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>SelectWithListDemo.java (a page
class)</b></div><div class="codeContent panelContent pdl">
<pre><code class="language-java">@Property
private SelectModel colorSelectModel;
@Inject
@@ -195,7 +195,7 @@ public String toString() {
</code></pre>
</div></div><p>But that is contorting the purpose of the toString() method,
and if you go to that much trouble you're already half way to the recommended
practice: creating a ValueEncoder.</p><h2
id="UsingSelectWithaList-ValueEncoder">ValueEncoder</h2><p>In addition to a
SelectModel, your Select menu is likely to need a ValueEncoder. While a
SelectModel is concerned only with how to construct a Select menu, a
ValueEncoder is used when constructing the Select menu <em>and</em> when
interpreting the encoded value that is submitted back to the server. A
ValueEncoder is a converter between the type of objects you want to represent
as options in the menu and the client-side encoded values that uniquely
identify them, and vice-versa.</p><div class="navmenu" style="float:right;
background:#eee; margin:3px; padding:0 1em">
<p> <strong>JumpStart Demo:</strong>
- <span class="nobr"><a class="external-link"
href="http://jumpstart.doublenegative.com.au/jumpstart/examples/select/easyobject"
rel="nofollow">Easy Object Select<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div>Most commonly, your ValueEncoder's
toClient() method will return a unique ID (e.g. a database primary key, or
perhaps a UUID) of the given object, and its toValue() method will return the
<em>object</em> matching the given ID by doing a database lookup (ideally using
a service or DAO method).<p>If you're using one of the ORM integration modules
(<a href="hibernate.html">Tapestry-Hibernate</a>, <a
href="integrating-with-jpa.html">Tapestry-JPA</a>, or <a class="external-link"
href="http://code.google.com/p/tapestry5-cayenne/wiki/ValueEncoder"
rel="nofollow">Tapestry-Cayenne</a>), the ValueEncoder is automatically
provided for each of your mapped entity classes. The Hiberna
te module's implementation is typical: the primary key field of the object
(converted to a String) is used as the client-side value, and that same primary
key is used to look up the selected object.</p><p>That's exactly what you
should do in your own ValueEncoders too:</p><div class="code panel pdl"
style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>ColorEncoder.java (perhaps in your
com.example.myappname.encoders package)</b></div><div class="codeContent
panelContent pdl">
+ <span class="nobr"><a class="external-link"
href="https://tapestry-jumpstart.org/jumpstart/examples/select/easyobject"
rel="nofollow">Easy Object Select<sup><img align="middle" class="rendericon"
src="/images/confluence/icons/linkext7.gif" height="7" width="7" alt=""
border="0"></sup></a></span></p></div>Most commonly, your ValueEncoder's
toClient() method will return a unique ID (e.g. a database primary key, or
perhaps a UUID) of the given object, and its toValue() method will return the
<em>object</em> matching the given ID by doing a database lookup (ideally using
a service or DAO method).<p></p><p>If you're using one of the ORM integration
modules (<a href="hibernate.html">Tapestry-Hibernate</a>, <a
href="integrating-with-jpa.html">Tapestry-JPA</a>, or <a class="external-link"
href="http://code.google.com/p/tapestry5-cayenne/wiki/ValueEncoder"
rel="nofollow">Tapestry-Cayenne</a>), the ValueEncoder is automatically
provided for each of your mapped entity classes. The Hibernat
e module's implementation is typical: the primary key field of the object
(converted to a String) is used as the client-side value, and that same primary
key is used to look up the selected object.</p><p>That's exactly what you
should do in your own ValueEncoders too:</p><div class="code panel pdl"
style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>ColorEncoder.java (perhaps in your
com.example.myappname.encoders package)</b></div><div class="codeContent
panelContent pdl">
<pre><code class="language-java">public class ColorEncoder implements
ValueEncoder<Color>, ValueEncoderFactory<Color> {
@Inject
Modified:
websites/production/tapestry/content/using-tapestry-with-hibernate.html
==============================================================================
--- websites/production/tapestry/content/using-tapestry-with-hibernate.html
(original)
+++ websites/production/tapestry/content/using-tapestry-with-hibernate.html Mon
Jul 11 07:12:07 2022
@@ -254,7 +254,7 @@ public class CreateAddress
}
}
</code></pre>
-</div></div><p>The <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/ioc/annotations/Inject.html">Inject</a>
annotation tells Tapestry to inject a service into the annotated field;
Tapestry includes a sophisticated Inversion of Control container (similar in
many ways to Spring) that is very good at locating available services by type,
rather than by a string id. In any case, the Hibernate Session object is
exposed as a Tapestry IoC service, ready to be injected (this is one of the
things provided by the tapestry-hibernate module).</p><p>Tapestry automatically
starts a transaction as necessary; however that transaction will be
<em>aborted</em> at the end of the request by default. If we make changes to
persistent objects, such as adding a new Address object, then it is necessary
to commit the transaction.</p><p>The <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/hibernate/annotations/CommitAft
er.html">CommitAfter</a> annotation can be applied to any component method; if
the method completes normally, the transaction will be committed (and a new
transaction started to replace the committed transaction).</p><p>After
persisting the new address, we return to the main Index page of the
application.</p><p><em>Note: In real applications, it is rare to have pages and
components directly use the Hibernate Session. It is generally a better
approach to define your own Data Access Object layer to perform common update
operations and queries.</em></p><h2
id="UsingTapestryWithHibernate-ShowingAddresses">Showing Addresses</h2><p>As a
little preview of what's next, let's display all the Addresses entered by the
user on the Index page of the application. After you enter a few names, it will
look something like:</p><p><span class="confluence-embedded-file-wrapper
confluence-embedded-manual-size"><img class="confluence-embedded-image"
width="722" src="using-tapestry-with-hibernate.data/ind
ex-grid-v1.png"></span></p><h2
id="UsingTapestryWithHibernate-AddingtheGridtotheIndexpage">Adding the Grid to
the Index page</h2><p>So, how is this implemented? Primarily, its accomplished
by the <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/corelib/components/Grid.html">Grid</a>
component.</p><p>The Grid component is based on the same concepts as the
BeanEditForm component; it can pull apart a bean into columns. The columns are
sortable, and when there are more entries than will fit on a single page, page
navigation is automatically added.</p><p>A minimal Grid is very easy to add to
the template. Just add this near the bottom of Index.tml:</p><div class="code
panel pdl" style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>src/main/webapp/Index.tml
(partial)</b></div><div class="codeContent panelContent pdl">
+</div></div><p>The <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/ioc/annotations/Inject.html">Inject</a>
annotation tells Tapestry to inject a service into the annotated field;
Tapestry includes a sophisticated Inversion of Control container (similar in
many ways to Spring) that is very good at locating available services by type,
rather than by a string id. In any case, the Hibernate Session object is
exposed as a Tapestry IoC service, ready to be injected (this is one of the
things provided by the tapestry-hibernate module).</p><p>Tapestry automatically
starts a transaction as necessary; however that transaction will be
<em>aborted</em> at the end of the request by default. If we make changes to
persistent objects, such as adding a new Address object, then it is necessary
to commit the transaction.</p><p>The <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/hibernate/annotations/CommitAft
er.html">CommitAfter</a> annotation can be applied to any component method; if
the method completes normally, the transaction will be committed (and a new
transaction started to replace the committed transaction).</p><p>After
persisting the new address, we return to the main Index page of the
application.</p><p><em>Note: In real applications, it is rare to have pages and
components directly use the Hibernate Session. It is generally a better
approach to define your own Data Access Object layer to perform common update
operations and queries.</em></p><h2
id="UsingTapestryWithHibernate-ShowingAddresses">Showing Addresses</h2><p>As a
little preview of what's next, let's display all the Addresses entered by the
user on the Index page of the application. After you enter a few names, it will
look something like:</p><p><span class="confluence-embedded-file-wrapper
confluence-embedded-manual-size"><img class="confluence-embedded-image"
draggable="false" width="722" src="using-tapestry-with-
hibernate.data/index-grid-v1.png"></span></p><h2
id="UsingTapestryWithHibernate-AddingtheGridtotheIndexpage">Adding the Grid to
the Index page</h2><p>So, how is this implemented? Primarily, its accomplished
by the <a class="external-link"
href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/corelib/components/Grid.html">Grid</a>
component.</p><p>The Grid component is based on the same concepts as the
BeanEditForm component; it can pull apart a bean into columns. The columns are
sortable, and when there are more entries than will fit on a single page, page
navigation is automatically added.</p><p>A minimal Grid is very easy to add to
the template. Just add this near the bottom of Index.tml:</p><div class="code
panel pdl" style="border-width: 1px;"><div class="codeHeader panelHeader pdl"
style="border-bottom-width: 1px;"><b>src/main/webapp/Index.tml
(partial)</b></div><div class="codeContent panelContent pdl">
<pre><code class="language-xml"> <t:grid source="addresses"
include="honorific,firstName,lastName,street1,city,state,zip,phone"/>
</code></pre>
@@ -274,7 +274,7 @@ public class Index
}
}
</code></pre>
-</div></div><p>Here, we're using the Hibernate Session object to find all
Address objects in the database. Any sorting that takes place will be done in
memory. This is fine for now (with only a handful of Address objects in the
database). Later we'll see how to optimize this for very large result
sets.</p><h2 id="UsingTapestryWithHibernate-What'sNext?">What's Next?</h2><p>We
have lots more to talk about: more components, more customizations, built-in
Ajax support, more common design and implementation patterns, and even writing
your own components (which is easy!).</p><p class="confluence-link">Check out
the many Tapestry resources available on the <a
href="documentation.html">Documentation</a> page, including the <a
href="getting-started.html">Getting Started</a> and <a
href="frequently-asked-questions.html">FAQ</a> pages and the <a
href="cookbook.html">Cookbook</a>. Be sure to peruse the <a
href="user-guide.html">User Guide</a>, which provides comprehensive details on
ne
arly every Tapestry topic. Finally, be sure to visit (and bookmark) <a
class="external-link" href="http://jumpstart.doublenegative.com.au/jumpstart"
rel="nofollow">Tapestry JumpStart</a>, which provides a nearly exhaustive set
of tutorials.</p></div>
+</div></div><p>Here, we're using the Hibernate Session object to find all
Address objects in the database. Any sorting that takes place will be done in
memory. This is fine for now (with only a handful of Address objects in the
database). Later we'll see how to optimize this for very large result
sets.</p><h2 id="UsingTapestryWithHibernate-What'sNext?">What's Next?</h2><p>We
have lots more to talk about: more components, more customizations, built-in
Ajax support, more common design and implementation patterns, and even writing
your own components (which is easy!).</p><p class="confluence-link">Check out
the many Tapestry resources available on the <a
href="documentation.html">Documentation</a> page, including the <a
href="getting-started.html">Getting Started</a> and <a
href="frequently-asked-questions.html">FAQ</a> pages and the <a
href="cookbook.html">Cookbook</a>. Be sure to peruse the <a
href="user-guide.html">User Guide</a>, which provides comprehensive details on
ne
arly every Tapestry topic. Finally, be sure to visit (and bookmark) <a
class="external-link" href="https://tapestry-jumpstart.org/jumpstart"
rel="nofollow">Tapestry JumpStart</a>, which provides a nearly exhaustive set
of tutorials.</p><p></p></div>
</div>
<!-- /// Content End -->
</div>