├── .gitignore ├── .gitmodules ├── .htaccess ├── README.md ├── assets └── main.css ├── data └── init.sql ├── edit-post.php ├── index.php ├── install.php ├── lib ├── common.php ├── edit-post.php ├── install.php ├── list-posts.php └── view-post.php ├── list-posts.php ├── login.php ├── logout.php ├── templates ├── comment-form.php ├── head.php ├── list-comments.php ├── title.php └── top-menu.php └── view-post.php /.gitignore: -------------------------------------------------------------------------------- 1 | data/data.sqlite 2 | nbproject/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/password_compat"] 2 | path = vendor/password_compat 3 | url = https://github.com/ircmaxell/password_compat.git 4 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | 3 | RewriteCond %{REQUEST_URI} ^/(data|lib|templates|vendor)/ 4 | RewriteRule ^ - [L,R=404] 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Example project for "I ♥ PHP" 2 | === 3 | 4 | This repository contains the blog application code featured in the "I ♥ PHP" tutorial. 5 | The tutorial has been designed for beginner and improving-beginner programmers wishing to learn 6 | some good practices in PHP web development. The code example uses PDO/SQLite, and demonstrates 7 | parameterisation, HTML escaping, logic/content separation, authentication, form handling, sessions 8 | and proper password hashing. The repo is public so that experienced developers may propose 9 | improvements if they wish. 10 | 11 | Code changes in the tutorial are shown as diffs in the text, and each modified file can be 12 | downloaded in its entirety at that point of development. To facilitate this, files and diffs are 13 | extracted from this repo by a script, rather than being copied in manually. This means that 14 | code improvements are much easier to transpose to the tutorial, than the traditional method of 15 | making adjustments by hand. 16 | 17 | Here is [a blog post about the tutorial](http://blog.jondh.me.uk/2014/08/online-php-beginners-tutorial/), 18 | which is presently in alpha status. 19 | 20 | See also the [text repo here](https://github.com/halfer/php-tutorial-text). 21 | 22 | Development process 23 | --- 24 | 25 | Since a fix may be necessary at an early stage of the project commit history, the approach I take 26 | is to use Git's interactive rebase, and edit the file(s) in question. Once this is done, I continue 27 | the rebase and fix up any resulting conflicts. Also, sometimes a new commit will be added, and 28 | that too will be rebased into the correct order. As one would expect, either of these results in a 29 | brand new commit history from the rebase point. 30 | 31 | This has a number of ramifications. Firstly, the tutorial text cannot refer to commits by hash, 32 | since they aren't reliable -- instead, commits are referred to by their comment text. Secondly, 33 | modified branches need to be pushed with force in order to update their remote repositories. This 34 | means that two people cannot work on the same branch, but in practice that's fine - changes of this 35 | magnitude cannot really be merged anyway. 36 | 37 | To make the process easier, sets of changes are added to a new branch in this repo, and the 38 | tutorial is then set to read from that branch instead. In the future this will permit several 39 | versions of the tutorial to co-exist, with each branch of the text repo referring to the 40 | appropriate branch in this repo. This will be helpful for users who are midway through the course - 41 | they can stay on the old version without incompatible changes being introduced that would force 42 | them to start afresh. 43 | 44 | How to help 45 | --- 46 | 47 | It is a good idea to raise an issue ticket about your proposed changes first. I (and the readership) 48 | will be most grateful for any improvements offered, but as maintainer I may have to turn down some 49 | changes. We first need to ensure: 50 | 51 | * the changes are actually an improvement 52 | * no security issues are being introduced 53 | * it is not overly complicated for a beginner audience 54 | * it does not reiterate concepts that have already been illustrated 55 | 56 | If we agree that the change is a good one, then read on. 57 | 58 | Check out the code, and switch to the latest branch. For convenience, they start with "master" (the 59 | oldest) and then go to "rebase1", and increase numerically from there. Create a new branch using 60 | a name of your own choosing. Make your changes and rebase them into the order you believe is 61 | suitable. Then push the new branch. If you have pushed before you will have to push with force; 62 | that's okay though, as no-one else should be working on it! 63 | 64 | You can then create a pull request (or an issue ticket) and we'll look at a diff against the current 65 | branch to ensure the changes are good. If they are, I'll copy the branch into a new branch in the 66 | main repo. Note this usually won't be merged into an existing branch (unless the change is very 67 | trivial) so as to preserve earlier snapshots. I'll then push the changes to the tutorial server, 68 | and regenerate against the new branch. 69 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | /* Success/error message boxes */ 2 | .box { 3 | border: 1px dotted silver; 4 | border-radius: 5px; 5 | padding: 4px; 6 | } 7 | .error { 8 | background-color: #ff6666; 9 | } 10 | .success { 11 | background-color: #88ff88; 12 | } 13 | .box ul { 14 | margin: 4px; 15 | padding-left: 14px; 16 | } 17 | 18 | .box ul li { 19 | margin-bottom: 2px; 20 | } 21 | 22 | .install-password { 23 | font-size: 1.2em; 24 | } 25 | 26 | .top-menu { 27 | border: 1px dotted silver; 28 | min-height: 18px; 29 | padding: 4px; 30 | margin-bottom: 4px; 31 | } 32 | 33 | .menu-options { 34 | float: right; 35 | } 36 | 37 | h1, h2, h3 { 38 | margin-top: 0; 39 | margin-bottom: 8px; 40 | } 41 | 42 | body { 43 | font-family: sans-serif; 44 | } 45 | 46 | .post-synopsis { 47 | padding-bottom: 8px; 48 | border-bottom: 1px dotted silver; 49 | margin-bottom: 20px; 50 | } 51 | 52 | .post-synopsis h2, .post h2 { 53 | color: darkblue; 54 | } 55 | 56 | .post .date, .post-synopsis .meta { 57 | color: white; 58 | background-color: grey; 59 | border-radius: 7px; 60 | padding: 2px; 61 | display: inline; 62 | font-size: 0.95em; 63 | } 64 | 65 | .comment .comment-meta { 66 | font-size: 0.85em; 67 | color: grey; 68 | border-top: 1px dotted silver; 69 | padding-top: 8px; 70 | } 71 | 72 | .comment .comment-meta input { 73 | float: right; 74 | } 75 | 76 | .comment-body p { 77 | margin: 8px 4px; 78 | } 79 | 80 | .comment-list { 81 | border-bottom: 1px dotted silver; 82 | margin-bottom: 12px; 83 | max-width: 900px; 84 | } 85 | 86 | .comment-margin { 87 | margin-bottom: 8px; 88 | } 89 | 90 | .user-form input, 91 | .user-form textarea { 92 | margin: 4px; 93 | } 94 | 95 | .user-form label { 96 | font-size: 0.95em; 97 | margin: 7px; 98 | width: 100px; 99 | width: 7em; 100 | color: grey; 101 | float: left; 102 | text-align: right; 103 | } 104 | 105 | #post-list { 106 | border-collapse: collapse; 107 | border: 1px solid silver; 108 | } 109 | 110 | #post-list td, #post-list th { 111 | padding: 8px; 112 | text-align: left; 113 | } 114 | 115 | #post-list tbody tr:nth-child(odd) { 116 | background-color: #f4f4f4; 117 | } -------------------------------------------------------------------------------- /data/init.sql: -------------------------------------------------------------------------------- 1 | /** 2 | * Database creation script 3 | */ 4 | 5 | /* Foreign key constraints need to be explicitly enabled in SQLite */ 6 | PRAGMA foreign_keys = ON; 7 | 8 | DROP TABLE IF EXISTS user; 9 | 10 | CREATE TABLE user ( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 12 | username VARCHAR NOT NULL, 13 | password VARCHAR NOT NULL, 14 | created_at VARCHAR NOT NULL, 15 | is_enabled BOOLEAN NOT NULL DEFAULT true 16 | ); 17 | 18 | /* This will become user = 1. I'm creating this just to satisfy constraints here. 19 | The password will be properly hashed in the installer */ 20 | INSERT INTO 21 | user 22 | ( 23 | username, password, created_at, is_enabled 24 | ) 25 | VALUES 26 | ( 27 | "admin", "unhashed-password", datetime('now', '-3 months'), 0 28 | ) 29 | ; 30 | 31 | DROP TABLE IF EXISTS post; 32 | 33 | CREATE TABLE post ( 34 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 35 | title VARCHAR NOT NULL, 36 | body VARCHAR NOT NULL, 37 | user_id INTEGER NOT NULL, 38 | created_at VARCHAR NOT NULL, 39 | updated_at VARCHAR, 40 | FOREIGN KEY (user_id) REFERENCES user(id) 41 | ); 42 | 43 | INSERT INTO 44 | post 45 | ( 46 | title, body, user_id, created_at 47 | ) 48 | VALUES( 49 | "Here's our first post", 50 | "This is the body of the first post. 51 | 52 | It is split into paragraphs.", 53 | 1, 54 | datetime('now', '-2 months', '-45 minutes', '+10 seconds') 55 | ) 56 | ; 57 | 58 | INSERT INTO 59 | post 60 | ( 61 | title, body, user_id, created_at 62 | ) 63 | VALUES( 64 | "Now for a second article", 65 | "This is the body of the second post. 66 | This is another paragraph.", 67 | 1, 68 | datetime('now', '-40 days', '+815 minutes', '+37 seconds') 69 | ) 70 | ; 71 | 72 | INSERT INTO 73 | post 74 | ( 75 | title, body, user_id, created_at 76 | ) 77 | VALUES( 78 | "Here's a third post", 79 | "This is the body of the third post. 80 | This is split into paragraphs.", 81 | 1, 82 | datetime('now', '-13 days', '+198 minutes', '+51 seconds') 83 | ) 84 | ; 85 | 86 | DROP TABLE IF EXISTS comment; 87 | 88 | CREATE TABLE comment ( 89 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 90 | post_id INTEGER NOT NULL, 91 | created_at VARCHAR NOT NULL, 92 | name VARCHAR NOT NULL, 93 | website VARCHAR, 94 | text VARCHAR NOT NULL, 95 | FOREIGN KEY (post_id) REFERENCES post(id) 96 | ); 97 | 98 | INSERT INTO 99 | comment 100 | ( 101 | post_id, created_at, name, website, text 102 | ) 103 | VALUES( 104 | 1, 105 | datetime('now', '-10 days', '+231 minutes', '+7 seconds'), 106 | 'Jimmy', 107 | 'http://example.com/', 108 | "This is Jimmy's contribution" 109 | ) 110 | ; 111 | 112 | INSERT INTO 113 | comment 114 | ( 115 | post_id, created_at, name, website, text 116 | ) 117 | VALUES( 118 | 1, 119 | datetime('now', '-8 days', '+549 minutes', '+32 seconds'), 120 | 'Jonny', 121 | 'http://anotherexample.com/', 122 | "This is a comment from Jonny" 123 | ) 124 | ; 125 | -------------------------------------------------------------------------------- /edit-post.php: -------------------------------------------------------------------------------- 1 | 75 | 76 |
77 |86 | View the blog, 87 | or install again. 88 |
89 | 90 | 91 | 92 | 93 |Click the install button to reset the database.
94 | 95 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /lib/common.php: -------------------------------------------------------------------------------- 1 | query('PRAGMA foreign_keys = ON'); 44 | if ($result === false) 45 | { 46 | throw new Exception('Could not turn on foreign key constraints'); 47 | } 48 | 49 | return $pdo; 50 | } 51 | 52 | function convertSqlDate($sqlDate) 53 | { 54 | /* @var $date DateTime */ 55 | $date = DateTime::createFromFormat('Y-m-d H:i:s', $sqlDate); 56 | 57 | return $date->format('d M Y, H:i'); 58 | } 59 | 60 | function getSqlDateForNow() 61 | { 62 | return date('Y-m-d H:i:s'); 63 | } 64 | 65 | /** 66 | * Gets a list of posts in reverse order 67 | * 68 | * @param PDO $pdo 69 | * @return array 70 | */ 71 | function getAllPosts(PDO $pdo) 72 | { 73 | $stmt = $pdo->query( 74 | 'SELECT 75 | id, title, created_at, body, 76 | (SELECT COUNT(*) FROM comment WHERE comment.post_id = post.id) comment_count 77 | FROM 78 | post 79 | ORDER BY 80 | created_at DESC' 81 | ); 82 | if ($stmt === false) 83 | { 84 | throw new Exception('There was a problem running this query'); 85 | } 86 | 87 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 88 | } 89 | 90 | /** 91 | * Converts unsafe text to safe, paragraphed, HTML 92 | * 93 | * @param string $text 94 | * @return string 95 | */ 96 | function convertNewlinesToParagraphs($text) 97 | { 98 | $escaped = htmlspecialchars($text); 99 | 100 | return '' . str_replace("\n", "
", $escaped) . '
'; 101 | } 102 | 103 | function redirectAndExit($script) 104 | { 105 | $host = $_SERVER['HTTP_HOST']; 106 | header('Location: http://' . $host . '/' . $script); 107 | exit(); 108 | } 109 | 110 | /** 111 | * Returns all the comments for the specified post 112 | * 113 | * @param PDO $pdo 114 | * @param integer $postId 115 | * return array 116 | */ 117 | function getCommentsForPost(PDO $pdo, $postId) 118 | { 119 | $sql = " 120 | SELECT 121 | id, name, text, created_at, website 122 | FROM 123 | comment 124 | WHERE 125 | post_id = :post_id 126 | "; 127 | $stmt = $pdo->prepare($sql); 128 | $stmt->execute( 129 | array('post_id' => $postId, ) 130 | ); 131 | 132 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 133 | } 134 | 135 | function tryLogin(PDO $pdo, $username, $password) 136 | { 137 | $sql = " 138 | SELECT 139 | password 140 | FROM 141 | user 142 | WHERE 143 | username = :username 144 | "; 145 | $stmt = $pdo->prepare($sql); 146 | $stmt->execute( 147 | array('username' => $username, ) 148 | ); 149 | 150 | // Get the hash from this row, and use the third-party hashing library to check it 151 | $hash = $stmt->fetchColumn(); 152 | $success = password_verify($password, $hash); 153 | 154 | return $success; 155 | } 156 | 157 | function login($username) 158 | { 159 | $_SESSION['logged_in_username'] = $username; 160 | } 161 | 162 | function logout() 163 | { 164 | unset($_SESSION['logged_in_username']); 165 | } 166 | 167 | function getAuthUser() 168 | { 169 | return isLoggedIn() ? $_SESSION['logged_in_username'] : null; 170 | } 171 | 172 | function isLoggedIn() 173 | { 174 | return isset($_SESSION['logged_in_username']); 175 | } 176 | 177 | /** 178 | * Looks up the user_id for the current auth user 179 | */ 180 | function getAuthUserId(PDO $pdo) 181 | { 182 | // Reply with null if there is no logged-in user 183 | if (!isLoggedIn()) 184 | { 185 | return null; 186 | } 187 | 188 | $sql = " 189 | SELECT id FROM user WHERE username = :username 190 | "; 191 | $stmt = $pdo->prepare($sql); 192 | $stmt->execute( 193 | array( 194 | 'username' => getAuthUser() 195 | ) 196 | ); 197 | 198 | return $stmt->fetchColumn(); 199 | } 200 | -------------------------------------------------------------------------------- /lib/edit-post.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 14 | if ($stmt === false) 15 | { 16 | throw new Exception('Could not prepare post insert query'); 17 | } 18 | 19 | // Now run the query, with these parameters 20 | $result = $stmt->execute( 21 | array( 22 | 'title' => $title, 23 | 'body' => $body, 24 | 'user_id' => $userId, 25 | 'created_at' => getSqlDateForNow(), 26 | ) 27 | ); 28 | if ($result === false) 29 | { 30 | throw new Exception('Could not run post insert query'); 31 | } 32 | 33 | // Finally let's look up the automatically generated primary key 34 | $sqlSeq = "SELECT seq FROM SQLITE_SEQUENCE WHERE name = 'post'"; 35 | $stmtSeq = $pdo->query($sqlSeq); 36 | 37 | return $stmtSeq->fetchColumn(); 38 | } 39 | 40 | function editPost(PDO $pdo, $title, $body, $postId) 41 | { 42 | // Prepare the insert query 43 | $sql = " 44 | UPDATE 45 | post 46 | SET 47 | title = :title, 48 | body = :body 49 | WHERE 50 | id = :post_id 51 | "; 52 | $stmt = $pdo->prepare($sql); 53 | if ($stmt === false) 54 | { 55 | throw new Exception('Could not prepare post update query'); 56 | } 57 | 58 | // Now run the query, with these parameters 59 | $result = $stmt->execute( 60 | array( 61 | 'title' => $title, 62 | 'body' => $body, 63 | 'post_id' => $postId, 64 | ) 65 | ); 66 | if ($result === false) 67 | { 68 | throw new Exception('Could not run post update query'); 69 | } 70 | 71 | return true; 72 | } -------------------------------------------------------------------------------- /lib/install.php: -------------------------------------------------------------------------------- 1 | 0) 20 | { 21 | $error = 'Please delete the existing database manually before installing it afresh'; 22 | } 23 | 24 | // Create an empty file for the database 25 | if (!$error) 26 | { 27 | $createdOk = @touch($database); 28 | if (!$createdOk) 29 | { 30 | $error = sprintf( 31 | 'Could not create the database, please allow the server to create new files in \'%s\'', 32 | dirname($database) 33 | ); 34 | } 35 | } 36 | 37 | // Grab the SQL commands we want to run on the database 38 | if (!$error) 39 | { 40 | $sql = file_get_contents($root . '/data/init.sql'); 41 | 42 | if ($sql === false) 43 | { 44 | $error = 'Cannot find SQL file'; 45 | } 46 | } 47 | 48 | // Connect to the new database and try to run the SQL commands 49 | if (!$error) 50 | { 51 | $result = $pdo->exec($sql); 52 | if ($result === false) 53 | { 54 | $error = 'Could not run SQL: ' . print_r($pdo->errorInfo(), true); 55 | } 56 | } 57 | 58 | // See how many rows we created, if any 59 | $count = array(); 60 | 61 | foreach(array('post', 'comment') as $tableName) 62 | { 63 | if (!$error) 64 | { 65 | $sql = "SELECT COUNT(*) AS c FROM " . $tableName; 66 | $stmt = $pdo->query($sql); 67 | if ($stmt) 68 | { 69 | // We store each count in an associative array 70 | $count[$tableName] = $stmt->fetchColumn(); 71 | } 72 | } 73 | } 74 | 75 | return array($count, $error); 76 | } 77 | 78 | /** 79 | * Updates the admin user in the database 80 | * 81 | * @param PDO $pdo 82 | * @param string $username 83 | * @param integer $length 84 | * @return array Duple of (password, error) 85 | */ 86 | function createUser(PDO $pdo, $username, $length = 10) 87 | { 88 | // This algorithm creates a random password 89 | $alphabet = range(ord('A'), ord('z')); 90 | $alphabetLength = count($alphabet); 91 | 92 | $password = ''; 93 | for($i = 0; $i < $length; $i++) 94 | { 95 | $letterCode = $alphabet[rand(0, $alphabetLength - 1)]; 96 | $password .= chr($letterCode); 97 | } 98 | 99 | $error = ''; 100 | 101 | // Insert the credentials into the database 102 | $sql = " 103 | UPDATE 104 | user 105 | SET 106 | password = :password, created_at = :created_at, is_enabled = 1 107 | WHERE 108 | username = :username 109 | "; 110 | $stmt = $pdo->prepare($sql); 111 | if ($stmt === false) 112 | { 113 | $error = 'Could not prepare the user update'; 114 | } 115 | 116 | if (!$error) 117 | { 118 | // Create a hash of the password, to make a stolen user database (nearly) worthless 119 | $hash = password_hash($password, PASSWORD_BCRYPT); 120 | if ($hash === false) 121 | { 122 | $error = 'Password hashing failed'; 123 | } 124 | } 125 | 126 | // Insert user details, including hashed password 127 | if (!$error) 128 | { 129 | $result = $stmt->execute( 130 | array( 131 | 'username' => $username, 132 | 'password' => $hash, 133 | 'created_at' => getSqlDateForNow(), 134 | ) 135 | ); 136 | if ($result === false) 137 | { 138 | $error = 'Could not run the user password update'; 139 | } 140 | } 141 | 142 | if ($error) 143 | { 144 | $password = ''; 145 | } 146 | 147 | return array($password, $error); 148 | } 149 | -------------------------------------------------------------------------------- /lib/list-posts.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 31 | if ($stmt === false) 32 | { 33 | throw new Exception('There was a problem preparing this query'); 34 | } 35 | 36 | $result = $stmt->execute( 37 | array('id' => $postId, ) 38 | ); 39 | 40 | // Don't continue if something went wrong 41 | if ($result === false) 42 | { 43 | break; 44 | } 45 | } 46 | 47 | return $result !== false; 48 | } 49 | -------------------------------------------------------------------------------- /lib/view-post.php: -------------------------------------------------------------------------------- 1 | Delete ) 33 | * 34 | * which comes directly from input elements of this form: 35 | * 36 | * name="delete-comment[6]" 37 | * 38 | * @param PDO $pdo 39 | * @param integer $postId 40 | * @param array $deleteResponse 41 | */ 42 | function handleDeleteComment(PDO $pdo, $postId, array $deleteResponse) 43 | { 44 | $keys = array_keys($deleteResponse); 45 | $deleteCommentId = $keys[0]; 46 | if ($deleteCommentId) 47 | { 48 | deleteComment($pdo, $postId, $deleteCommentId); 49 | } 50 | 51 | redirectAndExit('view-post.php?post_id=' . $postId); 52 | } 53 | 54 | /** 55 | * Delete the specified comment on the specified post 56 | * 57 | * @param PDO $pdo 58 | * @param integer $postId 59 | * @param integer $commentId 60 | * @return boolean True if the command executed without errors 61 | * @throws Exception 62 | */ 63 | function deleteComment(PDO $pdo, $postId, $commentId) 64 | { 65 | // The comment id on its own would suffice, but post_id is a nice extra safety check 66 | $sql = " 67 | DELETE FROM 68 | comment 69 | WHERE 70 | post_id = :post_id 71 | AND id = :comment_id 72 | "; 73 | $stmt = $pdo->prepare($sql); 74 | if ($stmt === false) 75 | { 76 | throw new Exception('There was a problem preparing this query'); 77 | } 78 | 79 | $result = $stmt->execute( 80 | array( 81 | 'post_id' => $postId, 82 | 'comment_id' => $commentId, 83 | ) 84 | ); 85 | 86 | return $result !== false; 87 | } 88 | 89 | /** 90 | * Retrieves a single post 91 | * 92 | * @param PDO $pdo 93 | * @param integer $postId 94 | * @throws Exception 95 | */ 96 | function getPostRow(PDO $pdo, $postId) 97 | { 98 | $stmt = $pdo->prepare( 99 | 'SELECT 100 | title, created_at, body, 101 | (SELECT COUNT(*) FROM comment WHERE comment.post_id = post.id) comment_count 102 | FROM 103 | post 104 | WHERE 105 | id = :id' 106 | ); 107 | if ($stmt === false) 108 | { 109 | throw new Exception('There was a problem preparing this query'); 110 | } 111 | $result = $stmt->execute( 112 | array('id' => $postId, ) 113 | ); 114 | if ($result === false) 115 | { 116 | throw new Exception('There was a problem running this query'); 117 | } 118 | 119 | // Let's get a row 120 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 121 | 122 | return $row; 123 | } 124 | 125 | /** 126 | * Writes a comment to a particular post 127 | * 128 | * @param PDO $pdo 129 | * @param integer $postId 130 | * @param array $commentData 131 | * @return array 132 | */ 133 | function addCommentToPost(PDO $pdo, $postId, array $commentData) 134 | { 135 | $errors = array(); 136 | 137 | // Do some validation 138 | if (empty($commentData['name'])) 139 | { 140 | $errors['name'] = 'A name is required'; 141 | } 142 | if (empty($commentData['text'])) 143 | { 144 | $errors['text'] = 'A comment is required'; 145 | } 146 | 147 | // If we are error free, try writing the comment 148 | if (!$errors) 149 | { 150 | $sql = " 151 | INSERT INTO 152 | comment 153 | (name, website, text, created_at, post_id) 154 | VALUES(:name, :website, :text, :created_at, :post_id) 155 | "; 156 | $stmt = $pdo->prepare($sql); 157 | if ($stmt === false) 158 | { 159 | throw new Exception('Cannot prepare statement to insert comment'); 160 | } 161 | 162 | $result = $stmt->execute( 163 | array_merge( 164 | $commentData, 165 | array('post_id' => $postId, 'created_at' => getSqlDateForNow(), ) 166 | ) 167 | ); 168 | 169 | if ($result === false) 170 | { 171 | // @todo This renders a database-level message to the user, fix this 172 | $errorInfo = $pdo->errorInfo(); 173 | if ($errorInfo) 174 | { 175 | $errors[] = $errorInfo[2]; 176 | } 177 | } 178 | } 179 | 180 | return $errors; 181 | } -------------------------------------------------------------------------------- /list-posts.php: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | 36 |You have posts. 45 | 46 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /login.php: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | 41 |Login here:
57 | 58 | 85 | 86 | -------------------------------------------------------------------------------- /logout.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 |This paragraph summarises what the blog is about.
7 | -------------------------------------------------------------------------------- /templates/top-menu.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /view-post.php: -------------------------------------------------------------------------------- 1 | $_POST['comment-name'], 37 | 'website' => $_POST['comment-website'], 38 | 'text' => $_POST['comment-text'], 39 | ); 40 | $errors = handleAddComment($pdo, $postId, $commentData); 41 | break; 42 | case 'delete-comment': 43 | $deleteResponse = $_POST['delete-comment']; 44 | handleDeleteComment($pdo, $postId, $deleteResponse); 45 | break; 46 | } 47 | } 48 | else 49 | { 50 | $commentData = array( 51 | 'name' => '', 52 | 'website' => '', 53 | 'text' => '', 54 | ); 55 | } 56 | 57 | ?> 58 | 59 | 60 | 61 |
12 | 13 | 14 | 15 |
16 |