Continuing my work on my Personal Home Page (PHP) site, very long post
from ICastFist@programming.dev to programming@programming.dev on 21 Mar 18:54
https://programming.dev/post/47568121

This is a follow up to my previous post here - programming.dev/post/46041021 - For those that want a tldr: I’m making a php site for myself writing nearly everything by hand. The only external library I’m using is Parsedown.

After a good time working on my site, I’m happy to announce that I’ve officially shared it with my friends^[I won’t share it here as the site is tied to a different online persona of mine]! The site isn’t really “ready” yet, but it’s very usable and readable, so that’s good!

As for code quality? Well… It’s kinda awful. Instead of this:

class User {
  $login = new String();
  $email = new String();
  ...
}

I’m using named arrays (hashes)^[Kinda funny how associative arrays have soe many different names in other languages: hash, dictionary, map] everywhere:

class User {
  $columns = array( 'login' => '',
  'email' => '',
  ...
}

“But WHY???”, you might be asking. Well, to facilitate the creation of the database from zero! Here’s an example of my trick:

abstract class Common {
 /**
  a bunch of different, generic select and update functions
*/
}
class Users extends Common{
$cols = array('uid'=> 'primary key auto_increment',
    'vc1_login'=> 'unique not null',
    'vc1_display_name'=> '',
    'vc2_password'=> 'not null',
    'dat_created_at'=> 'not null',
    'bol_enabled'=> 'default 1',
    ...
}

With this, the $key part of the hash doubles as the column name and their default/new values are always the details needed for the creation of their respective columns. I also treat the ::class as part of the table name. With a few functions, I can easily recreate the database from zero, something which I’ve tested a few times now and can confirm that it works great! Also, with key pairs, making generic SQL functions becomes very easy with foreach() loops of the $cols hash. Example:

abstract class Common {
public function selectColumns($columns, $table = '', $where='1', $orderby = '') {
        $conn = connectDb(); //static function outside class
        if ($table == '') {$table = $this::class;}
        $coll = '';
        foreach ($columns as $cols) {
            $coll .= $cols.', ';
        }
        $coll = substr($coll,0,-2);
        $stmt = $conn->prepare("SELECT ".$coll." FROM `T_".$table."` WHERE ".$where." ".$orderby.";");
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC); 
//Fetch_Assoc is used so I'm forced to always use the $key in the returned array
    }

// This function will attempt to update all non-empty pairs of a given object
public function updateColsUid(){
        $conn = conectaBanco();
        $sql = "UPDATE `T_".$this::class."` SET ";
        $keys = array('uid' => $this->cols['uid']);
        foreach ($this->cols as $key => $value) {
            if (($value != '') and ($key != 'uid')) {
                $sql .= " `". $key. "` = :" . $key . " ,";
                $keys[$key] = $value;
            }
        }
        $sql = substr($sql,0,-1);
        $sql .= " WHERE `uid` = :uid;";
        $stmt = $conn->prepare($sql);
        $stmt->execute($keys);
        return $stmt->rowCount();
    }

The biggest problem with this is that if I ever remove, add or rename any of these $keys, it’ll be a fucking chore to update code that references it. I’ll look into using proper variables for each column in the future, especially as a database creation is something you usually only do once. On the plus side, this is the most portable php site I’ve ever did (1 out of 1, but whatever)

Anyway, current functionality includes creating an account, modifying some aspects^[I want to note that there was a bunch of validation that I initially didn’t think of doing, but luckily had a couple of “Wait, what if…” moments. One of those was to properly escape a user’s username and display name, otherwise, when echo’ing it, <b>Bob</b> would show as Bob. While the fields probably wouldn’t be enough to fit anything malicious (fitting something malicious inside a varchar100 would be a real feat, ngl), it’s better to close this potential hole.] of it (profile description, display name (which is html escaped, so no funny business here), signature), logging in, letting the admin make new posts, letting anyone logged in comment on existing posts, comment moderation.

I also keep track of every page visitors are going to, saving these to the database (user agent, IP, page visited) - this will be the table that will fill up faster than any other, but might also allow me to catch eventual bots that ignore robots.txt - supposing I can figure them out.

Initially, I was planning on having each post select from a list of existing categories (category N -> N posts), but after some thought, decided against that and came up with a working alternative. Posts now have a single column where categories are manually written in, separated by commas. I later retrieve them with select distinct, explode() the string into an array and finally remove duplicates with array_unique(), making it easy for visitors, and for me, to get all the unique and valid categories.

One thing I’m doing that I’m not sure whether it’s good, neutral or bad design/architecture, is using the same site that has the form to also validate/insert data, as in: instead of having newpost.php and validate_and_insert_post.php files doing separate jobs, my newpost.php is the page has the form and also receives the form in order to validate and insert into the database.

The whole thing’s currently sitting at 220kb, unzipped, counting the leftover files that I’m no longer using. The fact that I can deploy this literally anywhere with a working php 8+ server without typing any terminal commands makes me very happy.

#programming

threaded - newest

lowspeedchase@lemmy.dbzer0.com on 21 Mar 19:19 next collapse

Always fun to learn and create! Just a quick note:

(fitting something malicious inside a varchar100 would be a real feat, ngl)

Fitting something malicious into 100 characters is not a feat, it’s trivial - especially with your custom query builder function, for example if I enter the following into the name field:

“name’); DELETE FROM users; --”

I could delete the whole table; same scenario if I created an API call to the endpoint that resolves to your selectColumns func, if I submit $orderby with a similar SQL statement I could run any arbitrary command I wanted.

This technique is called ‘SQL injection’ and is a very common attack vector, please google it and check your code!

moonpiedumplings@programming.dev on 21 Mar 19:46 next collapse

It looks like they are using prepared statements, which prevent sql injection:

www.php.net/manual/…/pdo.prepared-statements.php

lowspeedchase@lemmy.dbzer0.com on 21 Mar 20:38 collapse

When using bind parameters you are correct, but they are using string concatenation which removes the safeguards prepared statements offer.

ICastFist@programming.dev on 22 Mar 15:43 collapse

For user input, it’s always ? in my code :)

TehPers@beehaw.org on 22 Mar 00:57 next collapse

Is that little Bobby Tables? I guess he’d be grown up now, but he’ll always be little to me.

lowspeedchase@lemmy.dbzer0.com on 22 Mar 15:03 collapse
gramie@lemmy.ca on 22 Mar 12:01 next collapse

I don’t know if it’s still true, but I believe that MySQL used to ignore anything after a semicolon in a single command. It’s obviously better to prevent SQL injections and used parameterized queries, though.

lowspeedchase@lemmy.dbzer0.com on 22 Mar 15:03 collapse

TIL thanks

ICastFist@programming.dev on 22 Mar 12:53 collapse

From my understanding, using the PDO->prepare function, which is what I do with every SQL, is enough to sanitize all inputs. Can it still allow sql injection?

lowspeedchase@lemmy.dbzer0.com on 22 Mar 14:57 collapse

The ‘prepare’ in prepared statements is the process of sanitizing user input - when you feed prepare a prebuilt sql command (your concatenated string) you’re telling PDO “I’ve already sanitized this command, go run it” - PDO has no way to determine what part of that string is user input and what part of that string is your prebuilt command.

Sanitizing user input is done via parameterization: (example from php docs)

$stmt = $dbh->prepare(“SELECT * FROM REGISTRY where name = ?”); $stmt->execute([$_GET[‘name’]]);

Now when a user submits a variable ‘name’ to an endpoint that resolves to this function, we tell PDO hey, this is the trusted bit: SELECT * FROM REGISTRY where name = and here is this bit you should sanitize, i.e. protect against injection: ?

It’s a small code refactor to move from you manually creating the string to feeding PDO the parameters - happy coding!

ICastFist@programming.dev on 22 Mar 15:35 collapse

Ok, that’s already what I’m doing, so I should be ok against sql injection. Usernames don’t accept special characters, only [a-Z][0-9], and email is dealt with filter function. I had to sanitize user display names with htmlspecialchars in order to avoid html/javascript injection

org@lemmy.org on 21 Mar 20:18 next collapse

I have a soft spot for PHP :)

rezifon@lemmy.world on 21 Mar 22:48 collapse

That’s called a “fontanelle”

rimu@piefed.social on 21 Mar 22:20 next collapse

https://laravel.com/docs/12.x/migrations

luciole@beehaw.org on 22 Mar 02:22 collapse

Congrats on your progress! You’re learning 100x more then if you adopted a framework right away imo.

Database creation is something you usually only do once.

In an ideal world. In this world you absolutely have to plan how you’ll make your site (and its database schema) evolve while preserving your precious production data. Check out migrations.

Here’s an example of my trick

Be careful with tricks. Your future self prefers boring code to clever code 100% of the time. Way back when, I had made up a whole system for doing aspect oriented programming in JavaScript after reading some book. I was so proud, then after a while I hated myself so much for it. Finally I was so relieved when that monstrosity went offline.

instead of having newpost.php and validate_and_insert_post.php files doing separate jobs, my newpost.php is the page has the form and also receives the form in order to validate and insert into the database

This is a fine pattern don’t worry about it.

single column where categories are manually written in, separated by commas

If I’m understanding well and you have a database field containing a list of comma separated IDs, this is a code smell. Your site won’t catch fire if you don’t fix it, but you’ll limit yourself with the kinds of queries you can make against your relational database. You have a lot to gain by representing many-to-many relationships with a junction table.

Sorry for the lengthy comment, I enjoy talking about “the craft”. <img alt="bee happy emoji" src="https://beehaw.org/emoji/blobbee_happy.png"> Wishing you lots of fun with your project!

ICastFist@programming.dev on 22 Mar 13:12 collapse

Check out migrations.

Only way to implement those is with a library, right? I did find this tutorial here (Building a PHP Database Migration System from Scratch: A Complete Developer’s Guide), so I might check and try it out sometime in the future

Be careful with tricks.

I am. Every time I have to deal with hashes here (all the time), I die a little bit inside

you have a database field containing a list of comma separated IDs

No, the column is just saving plain text, like Category: “video, blog, news” - from where I do the rest of the string juggling, there’s no second table involved in this.

Wishing you lots of fun with your project!

Thank you! 😄