Download as pdf or txt
Download as pdf or txt
You are on page 1of 23

UNIT 9: DATA AND FILE MANIPULATION

IN COLDFUSION
Estimated time to complete this unit: 1-1.5 hours

Summary
ColdFusion provides many options to manipulate files and data to suit a particular programming
need. In this unit you will create CAPTCHA (Completely Automated Public Turing test to tell
Computers and Humans Apart) images, read images to memory and display them, process
images with watermarks and output data as an RSS feed.
After completing this unit, you should be able to:
• Programmatically manipulate images
• Syndicate and Consume RSS Feeds

The Assignment
In this activity, you will add a CAPTCHA (Completely Automated Public Turing test to tell
Computers and Humans Apart) image to a Café Townsend public website page in order to
validate that the submitter of the form is human (Figure 9-1).

Figure 9-1. Contact Us page with CAPTCHA image

1
LESSON 1: MANIPULATING IMAGE DATA WITH <cfimage>
Estimated time to complete this lesson: 15 minutes

Introduction
In order to manipulate images in prior versions of ColdFusion, you would have had to leverage a
third party image library such as:
• ImageMagick
• Alagad Image Component
• JSP tag library
While these solutions worked well for ColdFusion 6/7, they required the developer to learn
syntax that did not conform with the tag-based metaphor that has made CFML so popular.
ColdFusion 8 now natively abstracts the most popular image types as a series of CFML tags and
functions.

Objectives:
Upon completion of this lesson, you will be able to:
• Know how to transform images using CFML.
• Create CAPTCHA images using ColdFusion.

Supported Image Transforms


The following image-specific features have been natively incorporated into ColdFusion 8:
• Add Border • Rotate • Convert to
• Blur • Translate Negative
• Read into Memory • Get Information • Convert to base64
• Sharpen • Convert to string
• Create Captcha Grayscale • Rotate
• Crop • Write to file • Enforce Size
• Resize • Draw lines Restrictions
• Shear • Create Thumbnails • Create an Image
• Convert File • Overlay from a BLOB
Formats • Insert into database • Add Text
• Flip BLOB Field • Draw Shapes

File Formats
ColdFusion 8 supports the most popular image file types on the web, including the following:
• JPG • BMP • PNG
• GIF • TIFF

It does not, however, support Animated GIF, Multi-Page TIFF, PSD, or AI.

2
Database Support
Using ColdFusion 8, you can easily read and write image data to a database. Encode images as
text using the toBase64() function for storage in Microsoft Access memo fields. You can
write image data directly into ORACLE Blob columns. SQL Server Image columns are now
directly supported.

Using <CFIMAGE>
The most commonly requested types of image transforms are supported by the new <cfimage>
tag. Less frequently used procedures are implemented through a rich new set of functions. Using
<cfimage> you can perform the following tasks:
• Read an image into memory • Rotate an Image
• Create Thumbnails • Convert File Formats
• Add a Border • Write to a File
• Resize an Image • Get Image Information
• Create Captcha • Write to a browser
Each of image transforms are achieved by using the new <cfimage> tag with its action
attribute set to one of the following values:
• border • resize
• Captcha • rotate
• convert • write
• info • writetobrowser
• read

<cfimage type="captcha">
ColdFusion 8 allows you to quickly and easily create CAPTCHA images. CAPTCHA:
• Is a type of challenge used in online forms to determine whether or not the user is human.
• Is a trademarked acronym for "Completely Automated Public Turing test to tell
Computers and Humans Apart" coined in 2000 by Luis von Ahn, Manuel Blum, Nicholas
J. Hopper (all of Carnegie Mellon University), and John Langford (of IBM).
• Systems typically display an image containing a series of random distorted letters and
numbers. A user must then type the displayed string into a text field.
• The string must be distorted enough so that only a human's perception could accurately
parse it.
Note: Unfortunately, the underlying principal behind image-based CAPTCHA (the ability to
perceive and parse visual images) also makes it impossible for persons with certain types of
visual impairments to successfully pass. In order to remain Accessible, a web site may need to
offer audio-based CAPTCHA as an alternative.
ColdFusion 8's implementation of CAPTCHA:
• Capable of producing PNG files only.

• Make the generated image easier to read by keeping the height and width of the generated
image to a minimum.

3
• The minimum width of a CAPTCHA image is defined by the following equation:
Minimum width = fontsize * #chars * 1.08
Figure 9-2 is an example a CAPTCHA image of the text ColdFusion8 is produced:
<cfimage action="captcha"
height="40"
width="320"
text="ColdFusion8"
difficulty="low" >

Figure 9-2. CAPTCHA image of ColdFusion 8

Step-by-Step: Using CAPTCHA


In this Step-by-Step, you will perform the following tasks:
• Create a CAPTCHA image
• Validate that the submitter of the form is human

How to use CAPTCHA:

1. Open the file sbs9-1.cfm in the directory /CF8advanced/stepByStep/


2. Underneath the comment <!--- Step by Step 9-1 step 2: --> insert a
<CFIMAGE> tag. Use the following attributes:
• action: "CAPTCHA"
• height: "40"
• text: "#session.captcha#"
• width: "150"
• difficulty: "low"
3. Save the file and test (Figure 9-3).

4
Figure 9-3. CAPTCHA image in the Intranet Site Feedback page
4. Return to Dreamweaver.
5. Change the Difficulty attribute in the <CFIMAGE> tag to High. Save the file and
browse (Figure 9-4).

Figure 9-4. CAPTCHA difficulty set to high


6. Return to Dreamweaver.
7. Change the Width attribute in the <CFIMAGE> tag to 300.
8. Save the file and browse (Figure 9-5).

Figure 9-5 .CAPTCHA width set to 300

5
LESSON 2: READING AND STORING IMAGE DATA
Estimated time to complete this lesson: 15 minutes

Introduction
With ColdFusion 8 you can now read image data into memory and even store its information into
database fields. In this lesson we will explore how to read and store image data.

Objectives:
Upon completion of this lesson, you will be able to:
• Read, write and display images with ColdFusion.

Read an Image into Memory


ColdFusion 8 allows you to read an image file into memory. You can perform a myriad of image
transformations through the use of the new image functions. Once an image has been read, you
have direct access to its properties including image width, height, and color model. The
following example demonstrates how to read an image into memory and access its properties:
<cfimage action="read"
name="imgBurger"
source="#expandpath('.')#\chickenburgers.jpg">

<cfdump var="#imgBurger#">

Saving an Image to a Blob Field


ColdFusion 8 makes it a fairly straightforward process to save image data into an ORACLE
BLOB database field. Pass the image data through a <cfqueryparam
cfsqltype="cf_sql_blob"> instruction, using the new imageGetBlob() function.
In the following example, the Chicken_Burgers.JPG file read into memory from the
previous example is inserted into a database column:
<cfset answer = createObject("component","math").TimesTen(10
)>

<cfquery datasource="#application.datasource#">

insert into dish


(dishtypeid,dishname,dishdescription,photo)
values ( 2,
'Chicken Burgers',
'Great for u',
<cfqueryparam cfsqltype="cf_sql_blob"
value="#imageGetBlob(imgBurger)#">)

</cfquery>

6
Saving Image Data to a Memo Field
ColdFusion 8 also provides facilities for saving image data into a Microsoft Access Memo field.
Before the data can be written to disk, the blob data must be encoded as a Base64 character
string as the following example demonstrates:
<cfquery datasource="#application.datasource#">

insert into dish


(dishtypeid,dishname,dishdescription,photo)
values (2,
'Chicken Burgers',
'Great for u',
'#ToBase64(imageGetBlob(imgBurger))#')

</cfquery>

Displaying Images Stored in a Database


Displaying image data stored in a database requires using the <cfimage
action="writetobrowser"> tag. Image data stored in the database can only be output
using the JPG or PNG file formats. Using this technique causes ColdFusion to write out a
dynamic <img> tag that points to a Java Servlet as the following example demonstrates:
<cfquery
datasource="#application.datasource#"

name="qburger">
select photo
from dish
where dishname = 'Chicken Burgers'
</cfquery>
<cfimage source="#qburger.photo#" isbase64="yes"
action="writetobrowser">
Yielding the following output:
<img src="/CFFileServlet/_cf_image/_cfimg-
4693052593638653847.PNG" alt="" />

Step-by-Step: Working with Images


In this Step-by-Step, you will perform the following tasks:
• Read an image file into memory and write it to a database field
• Ultimately you will retrieve the image data and display it to the user

7
How to work with images:

1. In Dreamweaver open the file sbs9-2.cfm located in the


/CF8advanced/stepByStep/ directory.

Read the image data from a disk


2. Where indicated by the comment, insert a <cfimage> tag to read an image off the
server's disk:
• action: "READ"
• name: "stImageData"
• source: "#qimages.directory#\#qimages.name#"

Write the image data to a database


3. Underneath the comment for step 3, insert a <cfquery> to insert the image data into
the imageGallery table in the database. The columns that you will populate are the
following:
• imagename: the filename
• imagewidth: the width of the image contained in stImageData
• imageheight: the height of the image contained in stImageData
• imagesize: the size, in bytes, of the image (from qImages)
• imagedata: the Base64 encoded stImageData Blob
4. Your code should appear similar to the following:
<cfquery
datasource="#application.datasource#">
insert into ImageGallery (
imagename,
imagewidth,
imageheight,
imagesize,
imagedata)
values ( '#qimages.name#',
#stImageData.width#,
#stImageData.height#,
#qimages.size#,
'#tobase64(imageGetBlob(stImageData))#')

</cfquery>
5. Save the file and test. Note that you will not see any output yet.
6. Return to Dreamweaver and open the file sbs9-2a.cfm in the
/CF8advanced/stepByStep directory. Review the code.
7. Under the comment for step 7 insert a <CFIMAGE> tag using the following attributes:

8
• action: "WriteToBrowser"
• isBase64: "Yes"
• source: "#qFoodPhotos.imagedata[thisrow]#"
8. Save the file and test. You will see a selection of randomly generated photographs
(Figure 9-6).

Figure 9-6. Browsing file sbs9-2a.cfm

9
LESSON 3: IMAGE PROCESSING
Estimated time to complete this lesson: 15 minutes

Introduction
ColdFusion 8 now supports over fifty different image processing functions. These effects can be
combined to yield a virtually unlimited number of exciting transforms. In broad strokes, the new
functionality falls into the following six categories:
• Create New Image Objects - Read / Write / Convert
• Verify images and supported image formats - Image Manipulation
• Retrieve Image Information - Drawing

Objectives:
Upon completion of this lesson, you will be able to:
• Process an image in ColdFusion to include a watermark.

Using ImageNew() to create Images


The CFML tag ImageNew() is similar to <cfimage> in that it can read an image file from
the server into memory. Unlike <cfimage>, however, it can also create a new empty image
object in memory. The syntax for ImageNew is the following:
<cfset img = ImageNew(source, width, height, imageType,
canvasColor)>
Leave the source parameter empty to create a new empty image in memory. The imageType
argument specifies a color scheme of RGB, ARGB, or Grayscale. CanvasColor refers to the
background color of the image. It can be specified using Hex colors, a small list of named colors,
or 3 numbers corresponding to RGB values.

Dynamically Changing an Image


In the following example, an image of a burger is read off the server's disk and then an empty
image to be used as a matte is created. Additional functions are used to add copyright text to the
matte:
<!--- fetch original image --->

<cfset imgBurger =
imageNew("#expandpath('.')#\chicken_burgers.jpg")>

<!--- new image --->


<cfset imgNewImage = imageNew ("",
imgburger.width + 20,
imgburger.height,
"rgb",
"white")>

10
<!--- overlay --->
<cfset imagePaste(imgnewimage,imgburger,0,0)>

<!--- write text --->


<cfset ImageRotateDrawingAxis(imgnewimage,90,0,0)>

<cfset imageSetDrawingColor(imgnewimage,"black")>

<cfset imageDrawText(imgnewimage,"Copyright #chr(169)# 2008,


Cafe Townsend",0,-1 * imgburger.width - 5)>

<cfimage action="writeToBrowser" source="#imgnewimage#">

Step-by-Step: Modifying Images


In this Step-by-Step, you will perform the following tasks:
• Embed a watermark into an image

How to modify images:

1. In Dreamweaver open the file sbs9-3.cfm located in the


/CF8advanced/stepByStep/ directory.
2. Review the code.
3. Where indicated under the comment for step 3, initialize a local structure variable named
stFont with a single key/value pair of size="20". Your code should appear similar to
the following:
<cfset var stFont = {size="20"}>
4. After the comment for step 4, set the drawing color of the image equal to the argument
value passed to the function as indicated below:
<cfset ImageSetDrawingColor(imgsource,arguments.color )>
5. After the comment for step 5, turn on anti-aliasing as indicated below:
<cfset ImageSetAntiAliasing(imgsource,"on")>
6. Where indicated by the comment for step 6, make your text semitransparent by issuing
the following command:
<cfset ImageSetDrawingTransparency(imgsource,"70")>
7. In order to draw the text at a 45 degree down-angle, you will need to shear the drawing
axis as indicated below:
<cfset ImageShearDrawingAxis(imgSource,0,0.5)>

11
8. Finally, insert the command to draw the text onto the image. It should appear similar to
the following:
<cfset ImageDrawText(imgSource,arguments.message,10,1
0,stFont)>
9. Save the file and test (Figure 9-7).

Figure 9-7. Image with embedded watermark

12
LESSON 4: CREATING RSS FEEDS
Estimated time to complete this lesson: 20 minutes

Introduction
ColdFusion 8 makes it a relatively straightforward process to syndicate your site's content
through the Really Simple Syndication (RSS) or Atomz protocols. Conversely, you can also
import RSS and Atomz content into your web site, co-mingling it with your original content.
RSS and Atomz are XML schemas used to syndicate content across disparate systems. While
their use originated on news-oriented sites, they have since been adopted as a standard
transmission mechanism for blogs and can be fed into 3rd party aggregators like Yahoo Pipes
and iGoogle.
Using the <CFFEED> tag, you can perform the following functions:
• Create a feed from a structure
• Create a feed from a query
• Consume a 3rd Party Feed

Objectives:
Upon completion of this lesson, you will be able to:
• Export query data as an RSS feed.

The RSS Format


The RSS format is based around the concept of channels and items. Content is broken down into
a series of channels. Each channel, in turn, contains a series of items. In prior versions of
ColdFusion you could implement RSS by placing <cfoutput query> tags around the
<item> block of an RSS example and using <cfcontent> to inform the client that it was
about to receive XML:
<cfcontent type="text/xml"><?xml version="1.0" encoding="UTF-
8"?>

<rdf:RDF xmlns="http://purl.org/rss/1.0/"
xmlns:admin="http://webns.net/mvcb/"
xmlns:ag="http://purl.org/rss/1.0/modules/
aggregation/" xmlns:dc="http://purl.org/dc/
elements/1.1/" xmlns:rdf="http://www.w3.org/
1999/02/22-rdf-syntax-ns#">

<channel rdf:about="About Cafe Townsend">

<title>About Cafe Townsend</title>

<link>http://localhost:8500/CF8advanced/menu/specials.cfm</link>

<description> Cafe Townsend - the best place for bits and

13
bytes of the gastronomic persuasion
</description>

<cfoutput query="qarticles">

<item rdf:about="http://localhost/
displaydish.cfm?dishid=#dishid#">

<title>#qarticles.dishname#</title>

<link> http://localhost/
displaydish.cfm?dishid=#dishid#
</link>

<description>
#qarticles.description#
</description>

</item>
</channel>
</rdf:RDF>

Introducing <CFFEED>
The new <CFFEED> tag in ColdFusion 8 makes the creation of RSS feeds even simpler. It
supports the following functions:
• Reads or Creates an RSS or Atom syndication feed
• Can create RSS 2.0 or Atom 1.0 feeds
• Supports the Dublin Core Metadata Element Set specification
• Supports Apple iTunes™ extensions

Creating a feed from a ColdFusion Structure


You can convert information from a ColdFusion structure into an RSS feed by simply passing it
through the NAME attribute of the <CFFEED> tag.
<cffeed action="create"
name="#structure#"
outputfile="Path" | xmlVar="variable name"
overwrite="no|yes">
This syntax is deceptively simple; however, as the structure that you create must have keys that
correspond to the tags used in the RSS XML schema as indicated by the following example.
<cfquery name="qBooks" datasource="cfbookclub">

SELECT APP.BOOKS.TITLE,

APP.BOOKS.BOOKDESCRIPTION,
APP.BOOKS.BOOKID

14
FROM APP.AUTHORS, APP.BOOKS
WHERE APP.BOOKS.AUTHORID = APP.AUTHORS.AUTHORID
</cfquery>
<!--- structure corresponds to rss tags --->
<cfset stBooks = structnew()>
<cfset stBooks.title = "New Books">
<cfset stBooks.link="http://www.abcbooks.com">
<cfset stBooks.description = "New Books at ABC">
<cfset stBooks.version = "rss_2.0">
<cfset stBooks.item = arraynew(1)>
<cfloop query="qbooks">
<cfset stTemp = structnew()>
<cfset stTemp.title = qbooks.title>
<cfset stTemp.description = structnew()>
<cfset stTemp.description.value = qbooks.bookdescription>
<cfset stTemp.link = "http://www.amazon.com
/findabook?bookid=#qbooks.bookid#">

<cfset arrayAppend(stBooks.item,stTemp)>
</cfloop>

<cffeed action="create" name="#stBooks#" xmlvar="xBooks">

<cfcontent type="text/xml">
<cfoutput>#xbooks#</cfoutput>

Note: The use of the VERSION key that instructs <CFFEED> to generate content using the RSS
2.0 schema. Also, note the ITEM key which contains an array holding the syndicated data.

Creating a feed from a ColdFusion Query


You can translate query data into an RSS or Atom format as well. The tag syntax for passing a
query into <cffeed> is the following:
<cffeed
action="create"
properties="#metadata structure#"
query="queryname"
[outputfile = "path"]
[xmlVar = "variablename"]
columnmap = "mapping structure">
In order for ColdFusion to be able to place query column values into their appropriate location in
the XML data feed, you must pass in a structure that correlates your query column names with
tags in your RSS format as indicated in the example below:
<cfquery name="qBooks" datasource="cfbookclub">

SELECT APP.BOOKS.TITLE,
APP.BOOKS.BOOKDESCRIPTION,

15
APP.BOOKS.BOOKID,
'http://www.amazon.com?bookid=' as LINKHREF

FROM APP.AUTHORS, APP.BOOKS


WHERE APP.BOOKS.AUTHORID = APP.AUTHORS.AUTHORID
</cfquery>
<!--- structure corresponds to rss tags --->
<cfset stBooks = structnew()>
<cfset stBooks.title = "New Books">
<cfset stBooks.link="http://www.abcbooks.com">
<cfset stBooks.description = "New Books at ABC">
<cfset stBooks.version = "rss_2.0">
<cfset stColumnMap = structnew()>
<cfset stColumnMap.title="TITLE">
<cfset stColumnMap.rsslink = "BOOKID">
<cfset stColumnMap.content = "BOOKDESCRIPTION">

<cffeed action="create"
properties="#stBooks#"
query="#qbooks#"
columnmap = "#stColumnMap#"
xmlvar="xBooks">
<cfcontent type="text/xml">
<cfoutput>#xbooks#</cfoutput>

Step-by-Step: Exporting Data as RSS


In this Step-by-Step, you will perform the following task:
• Output query data as an RSS 2.0 Feed

How to explore data as RSS:

1. In Dreamweaver open the file sbs9-4.cfm located in the


/CF8advanced/stepByStep/ directory.
2. Review the code.
3. Under the comment for step 3, create a structure named stArticles. Populate it using
the following codeg:
<cfset stArticles = structnew()>

<cfset stArticles.title = "Cafe Townsend Newswire">

<cfset stArticles.link =
"http://localhost:8500/CF8advanced/stepByStep/">

16
<cfset stArticles.description = "The latest happenings at
Cafe Townsend">

<cfset stArticles.version = "rss_2.0">


4. Under the comment for step 4, create a new structure named stColumnMap using the
following key/value combinations:
• title: TITLE
• rsslink: URL
• content: TEASER
5. Under the comment for step 5, insert a <cffeed> tag using the following attribute:
• action: Create
• properties: #stArticles#
• query: #qArticles#
• columnmap: #stColumnMap#
• xmlVar: xArticles
6. Uncomment the <cfcontent> tag at the bottom of the screen
7. Save the file and test. To see the RSS Feed you may need to browse with FireFox 2.0
(Figure 9-8) or Internet Explorer 7.

Figure 9-8 Browsing file sbs9-4.cfm

17
LESSON 5: CONSUMING RSS FEEDS
Estimated time to complete this lesson: 10 minutes

Introduction
ColdFusion can also parse an RSS feed from either an URL or local XML file. The data that it
reads is transposed into either a native ColdFusion structure or query data type.

Objectives:
Upon completion of this lesson, you will be able to:
• Parse a remote RSS feed.

Consuming RSS with <CFFEED>


The specific versions of RSS and ATOM that are supported include the following.
• RSS 0.9
• RSS 0.91
• RSS 0.92
• RSS 0.93
• RSS 0.94
• RSS 1.0
• RSS 2.0
• Atom 0.3
• Atom 1.0

18
The syntax for reading an RSS document is as follows:
<cffeed action="read"
source = "feed source"
properties = "metadata structure"
query = "items/entries query"
outputFile = "path"
xmlVar = "variable name"
enclosureDir = "path"
ignoreEnclosureError = "no|yes"
overwrite = "no|yes" o
verwriteEnclosure = "no|yes"
proxyServer = "IP address or server name for proxy host"
proxyPassword = "password for the proxy host"
proxyPort = "port of the proxy host"
proxyUser = "user name for the proxy host"
timeout = "request timeout in seconds"
userAgent = "HTTP user agent identifier">
When using this tag, consider the following:
• All entry and metadata can be saved into a variable designated as the NAME attribute
• Entries or items can also be saved in a query format through the tag's QUERY attribute
• The feed's metadata can be saved into a native ColdFusion structure designated by the
tag's PROPERTIES attribute
• The raw feed XML can be saved into a ColdFusion XML variable defined through the
tag's XML attribute.
• <cffeed> can parse and save the metadata, item data, or both simultaneously
In the following example, an RSS feed is retrieved and parsed from a public web feed located on
the Voice of America web site:
<cffeed action="read"
source="http://www.voanews.com/english/customCF
/RecentStoriesRSS.cfm?keyword=TopStories"
name="stData"
properties="stProperties"
query="qdata"
xmlvar="xnews">
<cfdump var="#xmlparse(xnews)#">
<cfdump var="#qdata#">
<cfdump var="#stdata#">
<cfdump var="#stProperties#">

Step-by-Step: Parsing a Remote Feed


In this Step-by-Step, you will perform the following tasks:
• Read, parse, and output a remote RSS Feed.

19
How to parse a remote feed:

1. In Dreamweaver open the file sbs9-5.cfm located in the


/CF8advanced/stepByStep/ directory.
2. Review the code.
3. Under the comment for step 3, insert a <cffeed> tag with the following attributes:
• action: "READ"
• source: "http://rss.allrecipes.com/daily.aspx?hubID=85"
• name: "stData"
• properties: "stProperties"
4. Insert <cfdump> tags to output the contents of STDATA and STPROPERTIES
5. Save the file and test.
6. Comment out the <cfdump> tags.
7. After the <cffeed> tag, insert the contents of /snippets/sbs9-5.cfm. Note the
structure referencing for the DESCRIPTION data
8. Save and browse the file. You should see a list of RSS feeds (Figure 9-9).

Figure 9-9. Browsing sbs9-5.cfm displaying remote RSS feeds

20
21
SUMMARY
Keep these recommendations in mind when manipulating data and files in ColdFusion:
• ColdFusion 8 allows you to create CAPTCHA images, store and retrieve image data
to/from a database, as well as programmatically manipulate image data.
• ColdFusion 8 now has a very robust library of functions that allow you to process and
manipulate images.
• <cffeed> allows you to both create and consume RSS feeds.

22
ON YOUR OWN: ADD CAPTCHA TO FEEDBACK PAGE
Estimated time to complete this activity: 10 minutes

Introduction
In the Lesson 1 activity, you added a CAPTCHA image to a Café Townsend Intranet Feedback
page. In this activity, you will also add a CAPTCHA image to the Café Townsend public website
Contact Us page.

Objectives
In this activity, you will perform the following tasks:
• Create a CAPTCHA image
• Validate that the submitter of the form is human

Steps
1. Open the file /CF8advanced/stepByStep/contactus.cfm
2. Underneath the comment <!--- OYO 10: output CAPTCHA image --->
insert a <CFIMAGE> tag. Use the following attributes:
• action: "CAPTCHA"
• height: "40"
• text: "#session.captcha#"
• width: "130"
• difficulty: "low"
3. Save the file and browse.
4. Click the Send Feedback button.
5. A “Thanks for your invaluable feedback” message appears.

23

You might also like