Questions? Head to cs50.net/discuss or join classmates at office hours!
Objectives
-
Introduce you to HTML, CSS, PHP, and SQL.
-
Teach you how to teach yourself new languages.
Recommended Reading
Academic Honesty
This course’s philosophy on academic honesty is best stated as "be reasonable." The course recognizes that interactions with classmates and others can facilitate mastery of the course’s material. However, there remains a line between enlisting the help of another and submitting the work of another. This policy characterizes both sides of that line.
The essence of all work that you submit to this course must be your own. Collaboration on problem sets is not permitted except to the extent that you may ask classmates and others for help so long as that help does not reduce to another doing your work for you. Generally speaking, when asking for help, you may show your code to others, but you may not view theirs, so long as you and they respect this policy’s other constraints. Collaboration on quizzes is not permitted at all. Collaboration on the course’s final project is permitted to the extent prescribed by its specification.
Below are rules of thumb that (inexhaustively) characterize acts that the course considers reasonable and not reasonable. If in doubt as to whether some act is reasonable, do not commit it until you solicit and receive approval in writing from the course’s heads. Acts considered not reasonable by the course are handled harshly. If the course refers some matter to the Administrative Board and the outcome is Admonish, Probation, Requirement to Withdraw, or Recommendation to Dismiss, the course reserves the right to impose local sanctions on top of that outcome that may include an unsatisfactory or failing grade for work submitted or for the course itself.
Reasonable
-
Communicating with classmates about problem sets' problems in English (or some other spoken language).
-
Discussing the course’s material with others in order to understand it better.
-
Helping a classmate identify a bug in his or her code at Office Hours, elsewhere, or even online, as by viewing, compiling, or running his or her code, even on your own computer.
-
Incorporating snippets of code that you find online or elsewhere into your own code, provided that those snippets are not themselves solutions to assigned problems and that you cite the snippets' origins.
-
Reviewing past semesters' quizzes and solutions thereto.
-
Sending or showing code that you’ve written to someone, possibly a classmate, so that he or she might help you identify and fix a bug.
-
Sharing snippets of your own code on CS50 Discuss or elsewhere so that others might help you identify and fix a bug.
-
Turning to the web or elsewhere for instruction beyond the course’s own, for references, and for solutions to technical difficulties, but not for outright solutions to problem set’s problems or your own final project.
-
Whiteboarding solutions to problem sets with others using diagrams or pseudocode but not actual code.
-
Working with (and even paying) a tutor to help you with the course, provided the tutor does not do your work for you.
Not Reasonable
-
Accessing a solution in CS50 Vault to some problem prior to (re-)submitting your own.
-
Asking a classmate to see his or her solution to a problem set’s problem before (re-)submitting your own.
-
Failing to cite (as with comments) the origins of code or techniques that you discover outside of the course’s own lessons and integrate into your own work, even while respecting this policy’s other constraints.
-
Giving or showing to a classmate your solution to a problem set’s problem when it is he or she, and not you, who is struggling to solve it.
-
Looking at another individual’s work during a quiz.
-
Paying or offering to pay an individual for work that you may submit as (part of) your own.
-
Providing or making available solutions to problem sets to individuals who might take this course in the future.
-
Searching for, soliciting, or viewing a quiz’s questions or answers prior to taking the quiz.
-
Searching for or soliciting outright solutions to problem sets online or elsewhere.
-
Splitting a problem set’s workload with another individual and combining your work.
-
Submitting (after possibly modifying) the work of another individual beyond allowed snippets.
-
Submitting the same or similar work to this course that you have submitted or will submit to another.
-
Submitting work to this course that you intend to use outside of the course (e.g., for a job) without prior approval from the course’s heads.
-
Using resources during a quiz beyond those explicitly allowed in the quiz’s instructions.
-
Viewing another’s solution to a problem set’s problem and basing your own solution on it.
Scores
Your work on this problem set will be evaluated along four axes primarily.
- Scope
-
To what extent does your code implement the features required by our specification?
- Correctness
-
To what extent is your code consistent with our specifications and free of bugs?
- Design
-
To what extent is your code written well (i.e., clearly, efficiently, elegantly, and/or logically)?
- Style
-
To what extent is your code readable (i.e., commented and indented with variables aptly named)?
All students, whether taking the course SAT/UNS or for a letter grade, must ordinarily submit this and all other problem sets to be eligible for a satisfactory grade unless granted an exception in writing by the course’s heads.
Learning New Languages
Beyond introducing you to web programming, the overarching goal of this problem set is to teach you—nay, empower you—to teach yourself new languages so that you can stand on your own after term’s end. We’ll guide you through each, but if you nonetheless find yourself Googling and asking lots of questions of classmates and staff, rest assured you’re doing it right!
Incidentally, if you’d like to speed up or slow down any of the videos below using Chrome, join YouTube’s HTML5 trial at http://www.youtube.com/html5 and then re-load this specification. In the bottom-right corner of each video (when hovered upon), you should find a gear icon, via which you should be able to choose 2x and more. And once feeling comfortable, you’re welcome to skip over any of those videos altogether.
-
Let’s first take a look at HTTP (Hypertext Transfer Protocol), the protocol via which you can access the web.
-
Let’s next take a look at HTML (Hypertext Markup Language) in which web pages are written. Here’s Daven!
-
Let’s next take a look at CSS (Cascading Stylesheets) with Joseph.
-
So far so good? Let’s now leverage HTTP, HTML, and CSS to make our own search engine. Well, at least its front end. Take a peek at search-0 through search-4 via the playlist below. (Click PLAYLIST in the player’s top-left corner for a menu as needed.)
-
You are now a web programmer! Okay, not quite. Neither HTML nor CSS are programming languages, but PHP is. Here’s Tommy with a look at PHP’s syntax. You’ll find it’s fairly similar to C’s!
-
Alright, let’s now use PHP to make a website with an actual "back end", server-side code that responds to submissions of HTML forms. Take a look at froshims-0 through froshims-3 via the playlist below.
-
Now let’s look at a common "design pattern" for websites called MVC (Model-View-Controller) that we’ll ultimately use for this problem set. Take a look at mvc-0 through mvc-5 via the playlist below.
-
Finally, let’s hear about SQL (Structured Query Language). Here’s Christopher and cupcakes.
-
Phew, bit of a fire hydrant, no? Not to worry, some fun and more comfort await! Let’s get you started.
Getting Started
-
Start up your appliance and, upon reaching John Harvard’s desktop, open a terminal window (remember how?) and execute
update50
to ensure that your appliance is up-to-date!
-
Like Problem Set 6, this problem set comes with some distribution code that you’ll need to download before getting started. Go ahead and execute
cd ~/vhosts
in order to navigate to your
~/vhosts
directory. Then executewget http://cdn.cs50.net/2013/fall/psets/7/pset7/pset7.zip
in order to download a ZIP (i.e., compressed version) of this problem set’s distro. If you then execute
ls
you should see that you now have a file called
pset7.zip
in your~/vhosts
directory. Unzip it by executing the below.unzip pset7.zip
If you again execute
ls
you should see that you now also have a directory called
pset7
. You’re now welcome to delete the ZIP file with the below.rm -f pset7.zip
If you next execute
cd pset7
followed by
ls
you should see that
pset7
contains three subdirectories:includes
,public
, andtemplates
. But more on those soon. -
Next, ensure a few directories are world-executable by executing
chmod a+x ~ chmod a+x ~/vhosts chmod a+x ~/vhosts/pset7 chmod a+x ~/vhosts/pset7/public
so that the appliance’s web server (and you, from a browser) will be able to access your work. Then, navigate your way to
~/vhosts/pset7/public
by executing the below.cd ~/vhosts/pset7/public
If you execute
ls
you should see that
public
contains four subdirectories and three files. Ensure that the former are word-executable by executing the below.chmod a+x css fonts img js
Finally, ensure that the files within those directories are world-readable by executing the below.
chmod a+r css/* fonts/* img/* js/*
If unfamiliar,
*
is a "wildcard character," socss/*
, for instance, simply means "all files within thecss
directory."For security’s sake, don’t make
~/vhosts/pset7/includes
or~/vhosts/pset7/templates
world-executable (or their contents world-readable), as they shouldn’t be accessible to the whole world (only to your PHP code, as you’ll soon see). -
Even though your code for this problem set will live in
~/vhosts/pset7
, let’s ensure that it’s nonetheless backed up via Dropbox, assuming you set up Dropbox inside of the appliance. In a terminal window, executeln -s ~/vhosts/pset7 ~/Dropbox
in order to create a "symbolic link" (i.e., alias or shortcut) to your
~/vhosts/pset7
directory within your~/Dropbox
directory so that Dropbox knows to start backing it up. -
So why did we put
pset7
inside of a directory calledvhosts
? Well, the appliance is configured to serve "virtual hosts" (i.e., websites) out of the latter. Specifically, if you visit, say, http://pset7/ using Chrome inside of the appliance, the appliance is configured to look in~/vhosts/pset7/public
for that website’s web-accessible files. But for that to work, we also need to associate the appliance’s own IP address withpset7
so that it "resolves" via DNS to it. Rather than set up a whole DNS server to do that, we can actually edit a file calledhosts
in a directory calledetc
. Let’s do that.In a terminal window, execute
sudo gedit /etc/hosts
in order to run
gedit
as the appliance’s "superuser" (aka "root") so that you can edit what’s otherwise a read-only file. Carefully add this line at the bottom of that file, which will associatepset7
with the appliance’s "loopback" address (which won’t ever change):127.0.0.1 pset7
Then save the file and quit
gedit
. Then enjoy http://xkcd.com/149/.If you encounter a Gtk-WARNING with
gedit
, try restarting the appliance, as via Menu > Log Out > Restart (or via VMware’s own Restart option), then try again; the warning appears to be a bug in Fedora (the appliance’s operating system). -
Alright, time for a test! Open up Chrome inside of the appliance and visit http://pset7/.
You should find yourself redirected to C$50 Finance! (If you instead see Forbidden, odds are you missed a step earlier; best to try all those chmod steps again.) If you try logging into C$50 Finance with a username of, oh, skroob and a password of 12345, you should encounter an error about an Unknown database. That’s simply because you haven’t created it yet! Let’s create it.
Head to http://pset7/phpMyAdmin using Chrome inside of the appliance to access phpMyAdmin, a Web-based tool (that happens to be written in PHP) with which you can manage MySQL databases. (MySQL is a free, open-source database that CS50, Facebook, and lots of other sites use.) Log in as John Harvard if prompted (with a username of jharvard and a password of crimson). You should then find yourself at phpMyAdmin’s main page. In phpMyAdmin’s top-left corner, you should see No databases. Normally, you can create a database by clicking phpMyAdmin’s Databases tab, but you can also execute some SQL commands manually.
Go ahead and visit http://cdn.cs50.net/2013/fall/psets/7/pset7/pset7.sql using Chrome inside of the appliance, and then open the file in
gedit
, as by clicking its name in Chrome’s bottom-left corner or by selecting File > Open… ingedit
and then navigating your way to Downloads. You should ultimately see a whole bunch of SQL (i.e., database queries) withinpset7.sql
. Highlight it all, then select Edit > Copy (or hit ctrl-c), then return to phpMyAdmin. Click phpMyAdmin’s SQL tab, and paste everything you copied into that page’s big text box. Skim what you just pasted to get a sense of the commands you’re about to execute, then click Go. You should then see a green banner, proclaiming Your SQL query has been executed successfully. In phpMyAdmin’s top-left corner, you should now see link to a database called pset7, beneath which is a link to a table called users. But more on those later.Return to http://pset7/ using Chrome inside of the appliance and reload that page. Then try to log in with a username of skroob and a password of 12345. This time, you should see some construction.
-
Okay, time for a heads-up. Anytime you create a new file or directory in
~/vhosts/pset7
or some subdirectory therein for this problem set, you’ll want to set its permissions withchmod
. Thus far, we’ve relied ona+r
anda+x
, but let’s empower you with more precise control over permissions.Henceforth, for any PHP file, file, that you create, execute
chmod 600 file
so that it’s accessible only by you (and the appliance’s webserver). After all, we don’t want visitors to see the contents of PHP files; rather, we want them to see the output of PHP files once executed (or, rather, interpreted) by the appliance’s web server.
For any non-PHP file, file, that you create (or upload), execute
chmod 644 file
so that it’s accessible via a browser (if that’s indeed your intention).
And for any directory, directory, that you create, execute
chmod 711 directory
so that its contents are accessible via a browser (if that’s indeed your intention).
What’s with all these numbers we’re having you type? Well,
600
happens to meanrw-------
, and so all PHP files are made readable and writable only by you;644
happens to meanrw-r--r--
, and so all non-PHP files are to be readable and writable by you and just readable by everyone else; and711
happens to meanrwx--x--x
, and so all directories are to be readable, writable, and executable by you and just executable by everyone else. Wait a minute, don’t we want everyone to be able to read (i.e., interpret) your PHP files? Nope! For security reasons, PHP-based web pages are interpreted "as you" (i.e., under John Harvard’s username) in the appliance. For the curious, we’re using suPHP with Apache.Okay, still, what’s with all those numbers? Well, think of
rw-r--r--
as representing three triples of bits, the first triple of which, to be clear, isrw-
. Imagine that-
represents0
, whereasr
,w
, andx
represent1
. And, so, this same triple (rw-
) is just110
in binary, or6
in decimal! The other two triples,r--
andr--
, then, are just100
and100
in binary, or4
and4
in decimal! How, then, to express a pattern likerw-r--r--
with numbers? Why, with644
.Actually, this is a bit of a white lie. Because you can represent only eight possible values with three bits, these numbers (
6
,4
, and4
) are not actually decimal digits but "octal." So you can now tell your friends that you speak not only binary, decimal, and hexadecimal, but octal as well.
Yahoo!
-
If you’re not quite sure what it means to buy and sell stocks (i.e., shares of a company), surf on over to http://www.investopedia.com/university/stocks/ for a tutorial.
You’re about to implement C$50 Finance, a Web-based tool with which you can manage portfolios of stocks. Not only will this tool allow you to check real stocks' actual prices and portfolios' values, it will also let you buy (okay, "buy") and sell (fine, "sell") stocks! (Per Yahoo’s fine print, "Quotes delayed [by a few minutes], except where indicated otherwise.")
-
Just the other day, I received the stock tip below in my inbox!
"Discovery Ventures Signs Letter Of Intent To Acquire The Willa Gold Deposit"
Let’s get in on this opportunity now. Head on over to Yahoo! Finance at http://finance.yahoo.com/. Type the symbol for Discovery Ventures, DVN.V, into the text field in that page’s top-left corner and click Get Quotes. Odds are you’ll see a table like the below, which no one has apparently yet Liked!
Wow, only 22.5 cents per share! That must be a good thing. Anyhow, notice how Yahoo reports a stock’s most recent (i.e., "Last Trade") price ($0.27) and more. Moreover, scroll down to the page’s bottom, and you should see a toolbox like the below.
Looks like Yahoo lets you download all that data. Go ahead and click Download Data to download a file in CSV format (i.e., as comma-separated values). Open the file in Excel or any text editor (e.g.,
gedit
), and you should see a "row" of values, all excerpted from that table. It turns out that the link you just clicked led to the URL below.Notice how Discovery Ventures' symbol is embedded in this URL (as the value of the HTTP parameter called
s
); that’s how Yahoo knows whose data to return. Notice also the value of the HTTP parameter calledf
; it’s a bit cryptic (and officially undocumented), but the value of that parameter tells Yahoo which fields of data to return to you. If curious as to what they mean, head to http://www.gummy-stuff.org/Yahoo-data.htm.It’s worth noting that a lot of websites that integrate data from other websites do so via "screen scraping," a process that requires writing programs that parse (or, really, search) HTML for data of interest (e.g., air fares, stock prices, etc.). Writing a screen scraper for a site tends to be a nightmare, though, because a site’s markup is often a mess, and if the site changes the format of its pages overnight, you need to re-write your scraper. (See https://manual.cs50.net/scraping/ if curious as to how it can be done nonetheless.)
Thankfully, because Yahoo provides data in CSV, C$50 Finance will avoid screen scraping altogether by downloading (effectively pretending to be a browser) and parsing CSV files instead. Even more thankfully, we’ve written that code for you!
In fact, let’s turn our attention to the code you’ve been given.
-
Navigate your way to
~/vhosts/pset7/public
and open upindex.php
withgedit
. (Remember how?) Know thatindex.php
is the file that’s loaded by default when you visit a URL like http://pset7/. Well, it turns out there’s not much PHP code in this file. And there isn’t any HTML at all. Rather,index.php
"requires"config.php
(which is in a directory calledincludes
inindex.php
's parent directory). Andindex.php
then callsrender
(a function implemented in a file calledfunctions.php
that can also be found inside ofincludes
) in order to render (i.e., output) a template calledportfolio.php
(which is in a directory calledtemplates
inindex.php
's parent directory). Phew, that was a mouthful.It turns out that
index.php
is considered a "controller," whereby its purpose in life is to control the behavior of your website when a user visits http://pset7/ (or, equivalently, http://pset7/index.php). Eventually, you’ll need to add some more PHP code to this file in order to pass more than just title to render. But for now, let’s take a look atportfolio.php
, the template that this controller ultimately renders.Navigate your way to
~/vhosts/pset7/templates
and open upportfolio.php
withgedit
. Ah, there’s some HTML. Of course, it’s not very interesting HTML, but it does explain why your website is "under construction," thanks to the GIF referenced therein.Now navigate your way to
~/vhosts/pset7/includes
and open upconfig.php
withgedit
. Recall thatconfig.php
was required byindex.php
. Notice howconfig.php
first enables display of all errors (and warnings and notices, which are less severe errors) so that you’re aware of any syntactical mistakes (and more) in your code. Notice, too, thatconfig.php
itself requires two other files:constants.php
andfunctions.php
. Next,config.php
callssession_start
in order to enable$_SESSION
, a "superglobal" variable via which we’ll remember that a user is logged in. (Even though HTTP is a "stateless" protocol, whereby browsers are supposed to disconnect from servers as soon as they’re done downloading pages, "cookies" allow browsers to remind servers who they or, really, you are on subsequent requests for content. PHP uses "session cookies" to provide you with$_SESSION
, an associative array in which you can store any data to which you’d like to have access for the duration of some user’s visit. The moment a user ends his or her "session" (i.e., visit) by quitting his or her browser, the contents of$_SESSION
are lost for that user specifically because the next time that user visits, he or she will be assigned a new cookie!) Meanwhile,config.php
uses a "regular expression" (via a call topreg_match
) to redirect the users tologin.php
anytime they visit some page other thanlogin.php
,logout.php
, andregister.php
, assuming$_SESSION["id"]
isn’t yet set. In other words, that block of code requires users to log in if they aren’t logged in already (and if they aren’t already at one of those three pages).Okay, now open up
functions.php
withgedit
. Interesting, it looks likefunctions.php
requiresconstants.php
. More on that file, though, in a moment. It looks likefunctions.php
also defines a bunch of functions, the first of which isapologize
, which you can call anytime you need to apologize to the user (because they made some mistake). Defined next isdump
, which you’re welcome to call anytime you want to see the contents (perhaps recursively) of some variable while developing your site. That function is only for diagnostic purposes, though. Be sure to remove all calls thereto before submitting your work. Next in the file islogout
, a function that logs users out by destroying their sessions. Thereafter islookup
, a function that queries Yahoo Finance for stocks' prices and more. More on that, though, in a bit. Up next isquery
, a function that executes a SQL query and then returns the result set’s rows, if any. Below it isredirect
, a function that allows you to redirect users from one URL to another. Last in the file isrender
, the function thatindex.php
called in order to renderportfolio.php
. The function then "extracts" those values into the local scope (whereby a key of"foo"
with a value of"bar"
in$values
becomes a local variable called$foo
with a value of"bar"
). And it then requiresheader.php
followed by$template
followed byfooter.php
, effectively outputting all three.In fact, navigate your way back to
~/vhosts/pset7/templates
and open upheader.php
andfooter.php
ingedit
. Ah, even more HTML! Thanks to render, those files' contents will be included at the top and bottom, respectively, of each of your pages. As a result, each of your pages will have access to Twitter’s Bootstrap library, per the link and script tags therein. And each page will have at least fourdiv
elements, three of which have unique IDs (top
,middle
, andbottom
), if only to make styling them with CSS easier. Even more interestingly, though, notice howheader.php
conditionally outputs$title
, if it is set. Remember howindex.php
contained the below line of code?render("portfolio.php", ["title" => "Portfolio"]);
Well, because
render
callsextract
on that second argument, an array, before requiringheader.php
,header.php
ends up having access to a variable called$title
. Neat, eh? You can pass even more values into a template simply by separating such key/value pairs with a comma, as in the below.render("portfolio.php", ["cash" => 10000.00, "title" => "Portfolio"]);
Okay, now open up
constants.php
in~/vhosts/pset7/includes
(which, recall,config.php
required). Suffice it to say, this file defines a bunch of constants, but you shouldn’t need to change any of them.Navigate your way back to
~/vhosts/pset7/public
and open uplogin.php
, another controller, withgedit
. This controller’s a bit more involved thanindex.php
as it handles the authentication of users. Read through its lines carefully, taking note of how it how it queries the appliance’s MySQL database using thatquery
function fromfunctions.php
. That function (which we wrote) essentially simplifies use of PDO (PHP Data Objects), a library with which you can query MySQL (and other) databases. Per its definition infunctions.php
, the function accepts one or more arguments: a string of SQL followed by a comma-separated list of zero or more parameters that can be plugged into that string, not unlikeprintf
. Whereasprintf
uses%d
,%s
, and the like for placeholders, though,query
simply relies on question marks, no matter the type of value. And so the effect ofquery("SELECT * FROM users WHERE username = ?", $_POST["username"]);
in
login.php
is to replace?
with whatever username has been submitted (via POST) via an HTML form. (The function also ensures that any such placeholders' values are properly escaped so that your code is not vulnerable to "SQL injection attacks.") For instance, suppose that President Skroob tries to log into C$50 Finance by inputting his username and password. That line of code will ultimately execute the SQL statement below.SELECT * FROM users WHERE username='skroob'
Beware, though. PHP is weakly (i.e., loosely) typed, and so functions like query can actually return different types. Indeed, even though query usually returns an array of rows (thanks to its invocation of PDO’s
fetchAll
), it can also returnfalse
in case of errors. But, unlikeSELECT
s, some SQL queries (e.g.,DELETE
s,UPDATE
s, andINSERT
s) don’t actually return rows, and so the array thatquery
returns might sometimes be empty. When checking the return value ofquery
forfalse
, then, take care not to use==
, because it turns out than an empty array is==
tofalse
because of implicit casting. But an empty array does not necessarily signify an error, onlyfalse
does! Use, then, PHP’s===
(or!==
) operator when checking return values forfalse
, which compares its operands' values and types (not just their values), as in the below.$result = query("INSERT INTO users (username, hash, cash) VALUES(?, ?, 10000.00)", $_POST["username"], crypt($_POST["password"])); if ($result === false) { // the INSERT failed, presumably because username already existed }
See http://php.net/manual/en/language.operators.comparison.php for more details.
Anyhow, notice too that
login.php
"remembers" that a user is logged in by storing his or her unique ID inside of$_SESSION
. As before, this controller does not contain any HTML. Rather, it callsapologize
or renderslogin_form.php
as needed. In fact, open uplogin_form.php
in~/vhosts/pset7/templates
withgedit
. Most of that file is HTML that’s stylized via some of Bootstrap’s CSS classes, but notice how the HTML form therein POSTs tologin.php
. Just for good measure, take a peek atapology.php
while you’re in that directory as well. And also take a peek atlogout.php
back in~/vhosts/pset7/public
to see how it logs out a user.Alright, now navigate your way to
~/vhosts/pset7/public/css
and open upstyles.css
withgedit
. Notice how this file already has a few "selectors" so that you don’t have to include style attributes the elements matched by those selectors. No need to master CSS for this problem set, but do know that you should not have more than onediv
element per page whoseid
attribute has a value oftop
, more than onediv
element per page whoseid
attribute has a value ofmiddle
, or more than onediv
element per page whoseid
attribute has a value ofbottom
; anid
must be unique. In any case, you are welcome to modifystyles.css
as you see fit.You’re also welcome to poke around
~/vhosts/pset7/public/js
, which contains some JavaScript files. But no need to use or write any JavaScript for this problem set. Those files are just there in case you’d like to experiment.Phew, that was a lot. Help yourself to a snack.
-
Alright, let’s talk about that database we keep mentioning. So that you have someplace to store users' portfolios, the appliance comes with a MySQL database (called
pset7
). We’ve even pre-populated it with a table calledusers
(which is why you were able to log in as President Skroob). Let’s take a look.Head back to http://pset7/phpMyAdmin/ using Chrome inside of the appliance to access phpMyAdmin. Log in as John Harvard if prompted (with a username of jharvard and a password of crimson). You should then find yourself at phpMyAdmin’s main page, in the top-left corner of which is that table called users. Click the name of that table to see its contents. Ah, some familiar folks. In fact, there’s President Skroob’s username and a hash of his password (which is the same as the combination to his luggage)!
Now click the tab labeled Structure. Ah, some familiar fields. Recall that
login.php
generates queries like the below.SELECT id FROM users WHERE username='skroob'
As phpMyAdmin makes clear, this table called users contains three fields:
id
(the type of which is anINT
that’sUNSIGNED
) along withusername
andhash
(each of whose types isVARCHAR
). It appears that none of these fields is allowed to beNULL
, and the maximum length for each of each ofusername
andhash
is255
. A neat feature ofid
, meanwhile, is that it willAUTO_INCREMENT
: when inserting a new user into the table, you needn’t specify a value forid
; the user will be assigned the next availableINT
. Finally, if you click Indexes (above Information), you’ll see that this table’sPRIMARY
key isid
, the implication of which is that (as expected) no two users can share the same user ID. Recall that a primary key is a field with no duplicates (i.e., that is guaranteed to identify rows uniquely). Of course,username
should also be unique across users, and so we have also defined it to be so (per the additional Yes under Unique). To be sure, we could have defined username as this table’s primary key. But, for efficiency’s sake, the more conventional approach is to use anINT
likeid
. Incidentally, these fields are called "indexes" because, for primary keys and otherwise unique fields, databases tend to build "indexes," data structures that enable them to find rows quickly by way of those fields.Make sense?
-
Okay, let’s give each of your users some cash. Assuming you’re still on phpMyAdmin’s Structure tab, you should see a form with which you can add new columns. Click the radio button immediately to the left of After, select hash from the drop-down menu, as in the below, then click Go.
Via the form that appears, define a field called cash of type
DECIMAL
with a length of65,4
, with a default value of0.0000
, and with an attribute ofUNSIGNED
, as in the below, then click Save.If you pull up the documentation for MySQL at http://dev.mysql.com/doc/refman/5.6/en/numeric-types.html, you’ll see that the
DECIMAL
data type is used to "store exact numeric data values." A length of65,4
for aDECIMAL
means that values forcash
can have no more than 65 digits in total, 4 of which can be to the right of the decimal point. (Ooo, fractions of pennies. Sounds like Office Space.)Okay, return to the tab labeled Browse and give everyone $10,000.00 manually. (In theory, we could have defined
cash
as having a default value of10000.000
, but, in general, best to put such settings in code, not your database, so that they’re easier to change.) The easiest way is to click Check All, then click Change to the right of the pencil icon. On the page that appears, change0.0000
to10000.0000
for each of your users, then click Go. Won’t they be happy! -
It’s now time to code! Let’s empower new users to register.
Return to a terminal window, navigate your way to
~/vhosts/pset7/templates
and execute the below. (You are welcome, particularly if among those more comfortable, to stray from these filename conventions and structure your site as you see fit, so long as your implementation adheres to all other requirements.)cp login_form.php register_form.php
Then open up
register_form.php
withgedit
and change the value of form’saction
attribute fromlogin.php
toregister.php
. Next add an additional field of typepassword
to the HTML form calledconfirmation
so that users are prompted to input their choice of passwords twice (to discourage mistakes). Finally, change the button’s text fromLog In
toRegister
and changeor <a href="register.php">register</a> for an account
to
or <a href="login.php">log in</a>
so that users can navigate away from this page if they already have accounts.
Then, using
gedit
, create a new file calledregister.php
with the contents below, taking care to save it in~/vhosts/pset7/public
.<?php // configuration require("../includes/config.php"); // if form was submitted if ($_SERVER["REQUEST_METHOD"] == "POST") { // TODO } else { // else render form render("register_form.php", ["title" => "Register"]); } ?>
Alright, let’s take a look at your work! Bring up http://pset7/login.php in Chrome inside of the appliance and click that page’s link to
register.php
. You should then find yourself at http://pset7/register.php. If anything appears awry, feel free to make tweaks toregister_form.php
orregister.php
. Just be sure to save your changes and then reload the page in the browser.Of course,
register.php
doesn’t actually register users yet, so it’s time to tackle thatTODO
! Allow us to offer some hints.-
If
$_POST["username"]
or$_POST["password"]
is empty or if$_POST["password"]
does not equal$_POST["confirmation"]
, you’ll want to inform registrants of their error. -
To insert a new user into your database, you might want to call
query("INSERT INTO users (username, hash, cash) VALUES(?, ?, 10000.00)", $_POST["username"], crypt($_POST["password"]));
though we leave it to you to decide how much cash your code should give to new users.
-
Know that
query
will returnfalse
if yourINSERT
fails (as can happen if, say,username
already exists). Be sure to check for false with===
and not==
. -
If, though, your
INSERT
succeeds, know that you can find out whichid
was assigned to that user with code like the below.$rows = query("SELECT LAST_INSERT_ID() AS id"); $id = $rows[0]["id"];
-
If registration succeeds, you might as well log the new user in (as by "remembering" that
id
in$_SESSION
), thereafter redirecting toindex.php
.
Here’s Zamyla with some additional hints:
-
-
All done with
register.php
? Ready to test? Head back to http://pset7/register.php using Chrome inside of the appliance and try to register a new username. If you reachindex.php
, odds are you done good! Confirm as much by returning to phpMyAdmin, clicking once more that tab labeled Browse for the table calledusers
. May that you see your new user. If not, it’s time to debug!Be sure, incidentally, that any HTML generated by
register.php
is valid, as by ctrl- or right-clicking on the page in Chrome, selecting View Page Source, highlighting and copying the source code, and then pasting it into the W3C’s validator at http://validator.w3.org/#validate_by_input and then clicking Check. Ultimately, the Result of checking your page for validity via the W3C’s validator should be Passed or Tentatively passed, in which case you should see a friendly green banner. Warnings are okay. Errors (and big red banners) are not. Note that you won’t be able to "validate by URI" at http://validator.w3.org/#validate_by_uri, since your appliance isn’t accessible on the public Internet! -
Do bear in mind as you proceed further that you are welcome to play with and learn from the staff’s implementation of C$50 Finance at https://www.cs50.net/finance.
In particular, you are welcome to register with as many (fake) usernames as you would like in order to play. And you are welcome to view our pages' HTML and CSS (by viewing our source using your browser) so that you might learn from or improve upon our own design. If you wish, feel free to adopt our HTML and CSS as your own.
But do not feel that you need copy our design. In fact, for this problem set, you may modify every one of the files we have given you to suit your own tastes as well as incorporate your own images and more. In fact, may that your version of C$50 Finance be nicer than ours!
-
Okay, now it’s time to empower users to look up quotes for individual stocks. Odds are you’ll want to create a new controller called, say,
quote.php
plus two new templates, the first of which displays an HTML form via which a user can submit a stock’s symbol, the second of which displays, minimally, a stock’s latest price (if passed, via render, an appropriate value).How to look up a stock’s latest price? Well, recall that function called
lookup
infunctions.php
. Odds are you’ll want to call it with code like the below.$stock = lookup($_POST["symbol"]);
Assuming the value of
$_POST["symbol"]
is a valid symbol for an actual stock, lookup will return an associative array with three keys for that stock, namely itssymbol
, itsname
, and itsprice
. Know that you can use PHP’snumber_format
function (somehow!) to format price to at least two decimal places but no more than four decimal places. See http://php.net/manual/en/function.number-format.php for details.Of course, if the user submits an invalid symbol (for which lookup returns false), be sure to inform the user somehow. Be sure, too, that any HTML generated by your templates is valid, per the W3C’s validator.
Here’s Zamyla again:
-
And now it’s time to do a bit of design. At present, your database has no way of keeping track of users' portfolios, only users themselves. (By "portfolio," we mean a collection of stocks (i.e., shares of companies) that some user owns.) It doesn’t really make sense to add additional fields to users itself in order to keep track of the stocks owned by users (using, say, one field per company owned). After all, how many different stocks might a user own? Better to maintain that data in a new table altogether so that we do not impose limits on users' portfolios or waste space with potentially unused fields.
Exactly what sort of information need we keep in this new table in order to "remember" users' portfolios? Well, we probably want a field for users' IDs (
id
) so that we can cross-reference holdings with entries inusers
. We probably want to keep track of stocks owned by way of their symbols since those symbols are likely shorter (and thus more efficiently stored) than stocks' actual names. Of course, you could also assign unique numeric IDs to stocks and remember those instead of their symbols. But then you’d have to maintain your own database of companies, built up over time based on data from, say, Yahoo. It’s probably better (and it’s certainly simpler), then, to keep track of stocks simply by way of their symbols. And we probably want to keep track of how many shares a user owns of a particular stock. In other words, a table with three fields (id
,symbol
, andshares
) sounds pretty good, but you’re welcome to proceed with a design of your own. Whatever your decision, head back to phpMyAdmin and create this new table, naming it however you see fit. To create a new table, click pset7 in phpMyAdmin’s top-left corner, and on the screen that appears, input a name for your table and some number of columns below Create table, then click Go. On the screen that appears next, define (in any order) each of your fields.If you decide to go with three fields (namely
id
,symbol
, andshares
), realize thatid
should not be defined as a primary key in this table, else each user could own no more than one company’s stock (since his or herid
could not appear in more than one row). Realize, too, that you shouldn’t let someid
and somesymbol
to appear together in more than one row. Better to consolidate users' holdings by updating shares whenever some user sells or buys more shares of some stock he or she already owns. A neat way to impose this restriction while creating your table is to define a "joint primary key" by selecting an Index ofPRIMARY
for bothid
andsymbol
. That way,INSERT
will fail if you try to insert more than one row for some pair of id and symbol. We leave it to you, though, to decide your fields' types. (If you includeid
in this table, know that its type should match that inusers
. But don’t specifyAUTO_INCREMENT
for that field in this new table, as you only want auto-incrementation when user IDs are created for new users. And don’t call your tabletbl
.) When done defining your table, click Save! -
Before we let users buy and sell stocks themselves, let’s give some shares to President Skroob and friends at no charge. Click, in phpMyAdmin’s left-hand frame, the link to
users
and remind yourself of your current users' IDs. Then click, in phpMyAdmin’s left-hand frame, the link to your new table (for users' portfolios), followed by the tab labeled Insert. Via this interface, go ahead and "buy" some shares of some stocks on behalf of your users by manually inserting rows into this table. (You may want to return to Yahoo! Finance to look up some actual symbols.) No need to debit theircash
inusers
; consider these shares freebies.Once you’ve bought your users some shares, let’s see what you did. Click the tab labeled SQL and run a query like the below, where
tbl
represents your new table’s name.SELECT * FROM tbl WHERE id = 7
Assuming
7
is President Skroob’s user ID, that query should return all rows fromtbl
that represent the president’s holdings. If the only fields in table are, say,id
,symbol
, andshares
, then know that the above is actually equivalent to the below.SELECT id, symbol, shares FROM tbl WHERE id = 7
If, meanwhile, you’d like to retrieve only President Skroob’s shares of Discovery Ventures, you might like to try a query like the below.
SELECT shares FROM tbl WHERE id = 7 AND symbol = 'DVN.V'
If you happened to buy President Skroob some shares of that company, the above should return one row with one column, the number of shares. If you did not get buy any such shares, the above will return an empty result set.
Incidentally, via this SQL tab, you could have inserted those "purchases" with
INSERT
statements. But phpMyAdmin’s GUI saved you the trouble.Alright, let’s put this knowledge to use. It’s time to let users peruse their portfolios! Overhaul
index.php
(a controller) andportfolio.php
(a template) in such a way that they report each of the stocks in a user’s portfolio, including number of shares and current price thereof, along with a user’s current cash balance. Needless to say,index.php
will need to invokelookup
much likequote.php
did, though perhaps multiple times. And know that a PHP script can certainly invokequery
multiple times, even though, thus far, we’ve seen it used in a file no more than once. And you can certainly iterate over the array it returns in a template (assuming you pass it in viarender
). For instance, if your goal is simply to display, say, President Skroob’s holdings, one per row in some HTML table, you can generate rows with code like the below, where$positions
is an array of associative arrays, each of which represents a position (i.e., a stock owned).<table> <?php foreach ($positions as $position) { print("<tr>"); print("<td>" . $position["symbol"] . "</td>"); print("<td>" . $position["shares"] . "</td>"); print("<td>" . $position["price"] . "</td>"); print("</tr>"); } ?> </table>
Alternatively, you can avoid using the concatenation operator (
.
) via syntax like the below:<table> <?php foreach ($positions as $position) { print("<tr>"); print("<td>{$position["symbol"]}</td>"); print("<td>{$position["shares"]}</td>"); print("<td>{$position["price"]}</td>"); print("</tr>"); } ?> </table>
Note that, in the above version, we’ve surrounded the lines of HTML with double quotes instead of single quotes so that the variables within (
$position["symbol"]
,$position["shares"]
, and$position["price"]
) are interpolated (i.e., substituted with their values) by PHP’s interpreter; variables between single quotes are not interpolated. And we’ve also surrounded those same variables with curly braces so that PHP realizes they’re variables; variables with simpler syntax (e.g.,$foo
) do not require the curly braces for interpolation. (It’s fine to use double quotes inside those curly braces, even though we’ve also used double quotes to surround the entire argument toprint
.) Anyhow, though commonly done, generating HTML via calls toprint
isn’t terribly elegant. An alternative approach, though still a bit inelegant, is code more like the below.<?php foreach ($positions as $position): ?> <tr> <td><?= $position["symbol"] ?></td> <td><?= $position["shares"] ?></td> <td><?= $position["price"] ?></td> </tr> <?php endforeach ?>
Of course, before you can even pass
$positions
toportfolio.php
, you’ll need to define it inindex.php
. Allow us to suggest code like the below, which combines names and prices fromlookup
with shares and symbols, as might be returned as$rows
fromquery
.$positions = []; foreach ($rows as $row) { $stock = lookup($row["symbol"]); if ($stock !== false) { $positions[] = [ "name" => $stock["name"], "price" => $stock["price"], "shares" => $row["shares"], "symbol" => $row["symbol"] ]; } }
Note that, with this code, we’re deliberately create a new array of associative arrays (
$positions
) rather than add names and prices to an existing array of associative arrays ($rows
). In the interests of good design, it’s generally best not to alter functions' return values (like$rows
fromquery
).Now, much like you can pass a page’s title to render, so can you pass these positions, as with the below.
render("portfolio.php", ["positions" => $positions, "title" => "Portfolio"]);
Of course, you’ll also need to pass a user’s current cash balance from
index.php
toportfolio.php
viarender
as well, but we leave it to you to figure out how.To be clear, in the spirit of MVC, though, do take care not to call
lookup
inside of that (or any other) template; you should only calllookup
in controllers. Even though templates (aka views) can contain PHP code, that code should only be used to print and/or iterate over data that’s been passed in (as via render) from a controller.As for what HTML to generate, look, as before, to https://www.cs50.net/finance/ for inspiration or hints. But do not feel obliged to mimic our design. Make this website your own! Although any HTML and PHP code that you yourself write should be pretty-printed (i.e., nicely indented), it’s okay if lines exceed 80 characters in length. HTML that you generate dynamically (as via calls to
print
), though, does not need to be pretty-printed.As before, be sure to display stocks' prices and users' cash balances to at least two decimal places but no more than four.
Incidentally, though we keep using President Skroob in examples, your code should work for whichever user is logged in.
As always, be sure that the HTML generated by
index.php
is valid.Here’s Zamyla with some additional tips:
-
And now it is time to implement the ability to sell with a controller called, say,
sell.php
and some number of templates. We leave the design of this feature to you. But know that you can delete rows from your table (on behalf of, say, President Skroob) with SQL like the below.DELETE FROM tbl WHERE id = 7 AND symbol = 'DVN.V'
We leave it to you to infer exactly what that statement should do. Of course, you could try the above out via phpMyAdmin’s SQL tab. Now what about the user’s cash balance? Odds are, your user is going to want the proceeds of all sales. So selling a stock involves updating not only your table for users' portfolios but
users
as well. We leave it to you to determine how to compute how much cash a user is owed upon sale of some stock. But once you know that amount (say, $500), SQL like the below should take care of the deposit (for, say, President Skroob).UPDATE users SET cash = cash + 500 WHERE id = 7
Of course, if the database or web server happens to die between this
DELETE
andUPDATE
, President Skroob might lose out on all of that cash. You need not worry about such cases! It’s also possible, because of multithreading and, thus, race conditions, that a clever president could trick your site into paying out more than once. You need not worry about such cases either! Though, if you’re so very inclined, you can employ SQL transactions (with InnoDB tables). See http://dev.mysql.com/doc/refman/5.6/en/innodb.html for reference.It’s fine, for simplicity, to require that users sell all shares of some stock or none, rather than only a few. Needless to say, try out your code by logging in as some user and selling some stuff. You can always "buy" it back manually with phpMyAdmin.
As always, be sure that your HTML is valid!
And as always, here is Zamyla!
-
Now it’s time to support actual buys. Implement the ability to buy, with a controller called, say,
buy.php
and some number of templates. (As before, you need not worry about interruptions of service or race conditions.) The interface with which you provide a user is entirely up to you, though, as before, feel free to look to https://www.cs50.net/finance for inspiration or hints. Of course, you’ll need to ensure that a user cannot spend more cash than he or she has on hand. And you’ll want to make sure that users can only buy whole shares of stocks, not fractions thereof. For this latter requirement, know that a call likepreg_match("/^\d+$/", $_POST["shares"])
will return
true
if and only if$_POST["shares"]
contains a non-negative integer, thanks to its use of a regular expression. See http://www.php.net/preg_match for details. Take care to apologize to the user if you must reject their input for any reason. In other words, be sure to perform rigorous error-checking. (We leave to you to determine what needs to be checked!)When it comes time to store stocks' symbols in your database table, take care to store them in uppercase (as is convention), no matter how they were inputted by users, so that you don’t accidentally treat, say,
dvn.v
andDVN.V
as different stocks. Don’t force users, though, to input symbols in uppercase.Incidentally, if you implemented your table for users' portfolios as we did ours (with that joint primary key), know that SQL like the below (which, unfortunately, wraps onto two lines) will insert a new row into table unless the specified pair of
id
andsymbol
already exists in some row, in which case that row’s number of shares will simply be increased (say, by10
).INSERT INTO table (id, symbol, shares) VALUES(7, 'DVN.V', 10) ON DUPLICATE KEY UPDATE shares = shares + VALUES(shares)
As always, be sure to bang on your code. And be sure that your HTML is valid!
Here’s Zamyla with some additional help:
-
Alright, so your users can now buy and sell stocks and even check their portfolio’s value. But they have no way of viewing their history of transactions.
Enhance your implementations for buying and selling in such a way that you start logging transactions, recording for each:
-
Whether a stock was bought or sold.
-
The symbol bought or sold.
-
The number of shares bought or sold.
-
The price of a share at the time of transaction.
-
The date and time of the transaction.
Then, by way of a controller called, say,
history.php
and some number of templates, enable users to peruse their own history of transactions, formatted as you see fit. Be sure that your HTML is valid!Here’s Zamyla again:
-
-
Phew. Glance back at
index.php
now and, if not there already, make that it somehow links to, at least,buy.php
,history.php
,logout.php
,quote.php
, andsell.php
(or their equivalents) so that each is only one click away from a user’s portfolio! -
And now the icing on the cake. Only one feature to go, but you get to choose. Implement at least one (1) of the features below. You may interpret each of the below as you see fit; we leave all design decisions to you. Be sure that your HTML is valid.
-
Empower users (who’re already logged in) to change their passwords.
-
Empower users who’ve forgotten their password to reset it (as by having them register with an email address so that you can email them a link via which to do so).
-
Email users "receipts" anytime they buy or sell stocks.
-
Empower users to deposit additional funds.
For tips on how to send email programmatically, see https://manual.cs50.net/Sending_Mail.
Here’s Zamyla with a few final thoughts:
-
Sanity Checks
Before you consider this problem set done, best to ask yourself these questions and then go back and improve your code as needed! Do not consider the below an exhaustive list of expectations, though, just some helpful reminders. The checkboxes that have come before these represent the exhaustive list! To be clear, consider the questions below rhetorical. No need to answer them in writing for us, since all of your answers should be "yes!"
-
Is the HTML generated by all of your PHP files valid according to http://validator.w3.org/?
-
Do your pages detect and handle invalid inputs properly?
-
Are you recording users' histories of transactions properly?
-
Did you add one (1) additional feature of your own?
-
Did you choose appropriate data types for your database tables' fields?
-
Are you displaying any dollar amounts to at least two decimal places but no more than four?
-
Are you storing stocks' symbols in your table(s) in uppercase?
As always, if you can’t answer "yes" to one or more of the above because you’re having some trouble, do turn to cs50.net/discuss!
How to Submit
Step 1 of 2
-
When ready to submit, open up a Terminal window and "export" your MySQL database (i.e., save it into a text file) by executing the commands below, inputting crimson when prompted for a password. For security, you won’t see the password as you type it.
cd ~/vhosts/pset7 mysqldump -u jharvard -p pset7 > pset7.sql
If you type
ls
thereafter, you should see that you have a new file calledpset7.sql
in~/vhosts/pset7
. (If you realize later that you need to make a change to your database and re-export it, you can deletepset7.sql
withrm pset7.sql
, then re-export as before.) Next create a ZIP (i.e., compressed) file containing your entirepset7
directory by executing the below. Incidentally,-r
means "recursive," which in this case means to ZIP up everything inside ofpset7
, including any subdirectories (or even subsubdirectories!).cd ~/vhosts zip -r pset7.zip pset7/
If you type
ls
thereafter, you should see that you have a new file calledpset7.zip
in~/vhosts
. (If you realize later that you need to make a change to some file and re-ZIP everything, you can delete the ZIP file you already made withrm pset7.zip
, then create a new ZIP file as before.) -
Once done creating your ZIP file, open up Chrome inside of the appliance (not on your own computer) and visit cs50.net/submit, logging in if prompted.
-
Click Submit toward the window’s top-left corner.
-
Under Problem Set 7 on the screen that appears, click Upload New Submission.
-
On the screen that appears, click Add files…. A window entitled Open Files should appear.
-
Navigate your way to
pset7.zip
, as by clicking jharvard, then double-clicking Dropbox. Once you findpset7.zip
, click it once to select it, then click Open. -
Click Start upload to upload your ZIP file to CS50’s servers.
-
On the screen that appears, you should see a window with No File Selected. If you move your mouse toward the window’s lefthand side, you should see a list of the files you uploaded. Click each to confirm the contents of each. (No need to click any other buttons or icons.) If confident that you submitted the files you intended, consider your source code submitted! If you’d like to re-submit different (or modified) files, simply return to cs50.net/submit and repeat these steps. You may re-submit as many times as you’d like; we’ll grade your most recent submission, so long as it’s before the deadline.
Step 2 of 2
-
Head to https://www.cs50.net/forms/psets/7/ where a short form awaits. Once you have submitted that form (as well as your source code), you are done!
This was Problem Set 7.