Creating a Mobile Photo Blog, Part 2

Tweet
This entry is part 1 of 2 in the series Creating a Mobile Photo Blog

Creating a Mobile Photo Blog

This is the second article in a two part series in which I show you how to create a photo blog that is updated from your mobile device. Part 1 laid out the plan for building the application, set up the database requirements, and introduced you to some user contributed functions from the PHP manual which make it easy to retrieve messages from a POP3 mail server. In this installment I show you how to bring everything together so you can start photo-blogging on the go.

Asking For Approval

In Part 1 you saw how to retrieve a list of emails from the server. Of course, it would be unwise to publish them immediately in case your secret email address was discovered, which is why updates should be explicitly approved. Before proceeding, I want to set some more constants and connect to the database.

<?php
// miscellaneous
define("PUBLIC_EMAIL", "mobile@example.com");
define("MAX_WIDTH", 240);
define("ROOT_PATH", "/var/www/");
define("IMG_PATH", "photoblog/");

// database connection
define("DB_HOST", "localhost");
define("DB_USER", "root");
define("DB_PASSWORD", "********");
define("DB_NAME", "test");

// connect to database
$db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASSWORD);

The PUBLIC_EMAIL constant is your mobile device’s email address to which approval messages will be sent. MAX_WIDTH is the maximum width allowed when the images are resized as thumbnails. The ROOT_PATH is an absolute path to a web-accessible location, and IMG_PATH is a writable folder in which your images will be stored. Note how IMG_PATH has no forward slash at the beginning; this is not a mistake as it is a relative path inside ROOT_PATH.

While iterating the list of emails, the script checks that each message has a token associated with it. If a message doesn’t, the script creates a new token and sends off an email to ask for approval. Messages that have a token are checked to see whether or not they have been approved. See the following example:

<?php
require_once "POP3.php";

// prepared SQL statements
$sqlSelectPending = "SELECT is_valid FROM pending WHERE message_id = :msgno";
$stmSelectPending = $db->prepare($sqlSelectPending);

$sqlInsertPending = "INSERT INTO pending (message_id, token) VALUES (:msgno, :token)";
$stmInsertPending = $db->prepare($sqlInsertPending);

// retrieve messages
$pop3 = new POP3(EMAIL_HOST, EMAIL_USER, EMAIL_PASSWORD);
$msgList = $pop3->listMessages();

if (!empty($msgList)) {
    foreach ($msgList as $value) {
        // see if a token exists
        $stmSelectPending->execute(array(":msgno" => $value["msgno"]));
        $isValid = $stmSelectPending->fetchColumn();

        // message has been approved
        if ($isValid == "Y") {
            // ...
        }
        // message has no token
        elseif ($isValid === false)  {
            // create a unique token
            $token = md5(uniqid(mt_rand(), true));
            $stmInsertPending->execute(array(":msgno" => $value["msgno"], ":token" => $token));

            // send email for approval
            $title = htmlentities($value["subject"], ENT_QUOTES);
            $subject = "Pending Post Notification: " . $title;
            $message = '<a href="http://www.example.com/approve.php?token=' . $token . '">Click Here To Approve</a>';
            mail(PUBLIC_EMAIL, $subject, $message);
        }
    }
}

If $isValid has the value Y then the message has been approved. If $isValid has a value other than Y then the message has not been approved so you don’t need to go any further. If $isValid has no value then you need to create a token.

The approve.php page referenced in the notification email simply changes the status of the token:

<?php
// connect to database
// ...

// receive incoming token
$token = isset($_GET["token"]) && ctype_alnum($_GET["token"]) ? $_GET["token"] : null;

if (!empty($token)) {
    // verify token
    $sql = "SELECT message_id FROM pending WHERE token = :token AND is_valid = 'N'";
    $stm = $db->prepare($sql);
    $stm->execute(array(":token" => $token));
    $pendID = $stm->fetchColumn();
    $stm->closeCursor();

    if (!empty($pendID)) {
        // set the entry to be published
        $sql = "UPDATE pending SET is_valid = 'Y' WHERE message_id = :pend_id";
        $stm = $db->prepare($sql);
        $stm->execute(array(":pend_id" => $pendID));
        echo "<p>Publishing...</p>";
    }
    else {
        echo "<p>Invalid token.</p>";
    }
}

Because the first script will be setup to run as a cron job every few minutes, the next time it executes it will see a valid flag because of the approval. If you ever receive a notification email which you did not submit, then you know the private receiving email address has somehow been compromised and you should probably change it. In any case, nothing will be published without approval.

Publishing Blogs

Now that you have a token marked for approval, you need to extract the email contents, copy the attached images to the server and update the database. This code is a continuation of the previous script that runs under cron and picks up where $isValid indicates the message has been approved.

<?php
// prepared SQL statements
// ...

$sqlUpdatePending = "UPDATE pending SET message_id = message_id - 1 WHERE message_id > :msgno";
$stmUpdatePending = $db->prepare($sqlUpdatePending);

$sqlDeletePending = "DELETE FROM pending WHERE message_id = :msgno";
$stmDeletePending = $db->prepare($sqlDeletePending);

$sqlInsertPost = "INSERT INTO blog_posts (title, body, create_ts) VALUES (:title, :body, FROM_UNIXTIME(:timestamp))";
$stmInsertPost = $db->prepare($sqlInsertPost);

$sqlInsertImage = "INSERT INTO images (post_id, image_path) VALUES (:post_id, :img_path)";
$stmInsertImage = $db->prepare($sqlInsertImage);

// ...

// message has been approved
if ($isValid == "Y") {
    // get message contents
    $msg = $pop3->mimeToArray($value["msgno"], true);

    // convert date to timestamp
    $timestamp = strtotime($value["date"]);
    if ($timestamp === false) {
        $timestamp = null;
    }
    $title = $value["subject"];

    if(sizeof($msg) > 1) {
        $body = (isset($msg["1.1"])) ? $msg["1.1"]["data"] : $msg[1]["data"];
    } else {
        $body = $pop3->fetchBody($value["msgno"]);
    }

    // copy images to server
    $files = array();
    foreach ($msg as $parts) {
        if (isset($parts["filename"])) {
            $dir = ROOT_PATH . IMG_PATH;
            $ext = strtolower(pathinfo($parts["filename"], PATHINFO_EXTENSION));
            // only accept jpg or png
            if (in_array($ext, array("jpg","png"))) {
                // give the file a unique name
                $hash = sha1($parts["data"]);
                $file = $hash . "." . $ext;
                $thumb = $hash . "_t." . $ext;

                if (!file_exists($dir . $file)) {
                    // copy image and make thumbnails
                    $img = new Imagick();
                    $img->readimageblob($parts["data"]);
                    $img->writeImage($dir . $file);
                    $img->thumbnailImage(MAX_WIDTH, 0);
                    $img->writeImage($dir . $thumb);
                    $img->clear();
                    $img->destroy();
                }
                $files[] = IMG_PATH . $file;
            }
        }
    }

    // update database
    if (isset($timestamp, $title, $body)) {
        // insert post
        $stmInsertPost->execute(array(":title" => $title, ":body" => $body, ":timestamp" => $timestamp));
        $postID = $db->lastInsertId();

        // insert images
        $stmInsertImage->bindParam(":post_id", $postID);
        $stmInsertImage->bindParam(":img_path", $path);
        foreach($files as $path) {
            $stmInsertImage->execute();
        }

        // delete token
        $stmDeletePending->execute(array(":msgno" => $value["msgno"]));

        // update existing tokens
        $stmUpdatePending->execute(array(":msgno" => $value["msgno"]));
    }
    // mark email for deletion
    $pop3->deleteMessage($value["msgno"]);
    break;
}
// message has no approval token
elseif ($isValid === false)  {
    // ...

In the example above, the date provided by the email client is no good for the database so it is converted to a unix timestamp. The body is a little tricky in that depending on which email client you use it may send multiple versions, so we attempt to weed out the plain text version and ignore the others.

To copy images, the full path of where they will be stored is defined first. Next, you grab the extension and make sure it is an allowed image format and then calculate the sha1() hash of the image to get a unique filename. Even though phones automatically give pictures unique filenames, there is always the chance you could change phones so you still want to give the images completely unique filenames before copying them to the server to make sure nothing gets overwritten. Thumbnails are given the same name with “_t” added to the end.

While Imagick is relatively fast and efficient with its memory usage, you may still run into problems if you attempt to upload more than a few very large images. If you do run into problems then you’ll want to do some testing to make sure your script isn’t running out of memory, timing out, or that the cron job isn’t trying to run the script again while a previous run is still processing.

After the blog content has been extracted from the message body and the images are copied, you update the database and delete the token since it is no longer needed. You also need to re-index your tokens at this point because once an email is deleted from the inbox, any remaining messages will be automatically re-numbered. For example, if you have the following messages in your inbox:

message_id		subject
===================================
1			hello world
2			foo
3			bar

If you process and delete message 1, the remaining messages will be re-numbered as follows:

message_id		subject
===================================
1			foo
2			bar

Since tokens are stored with a reference to a specific message id, if you don’t re-index the tokens to accommodate for this behavior then you’ll end up with a complete mess.

At the end of this example you mark the email for deletion and call the break command to stop looping through $msgList. Not only does this ensure that only one blog will be updated per execution which can help save memory, but since the token ids have been updated the remaining entries in this cycle wouldn’t be valid anyway.

When the script terminates, the POP3 class’ destructor will be called and the processed email will be deleted.

Displaying The Blog

When it comes to displaying the content, the options are endless. You could further improve the blog to add BBCode, comments, pagination, and so on and so forth. Whatever your needs may be, the following example should help get you started:

<?php
// connect to database
// ...

// prepared SQL statements
$sqlSelectImages = "SELECT * FROM images WHERE post_id = :post_id";
$stmSelectImages = $db->prepare($sqlSelectImages);

// output each blog post
$sqlSelectPosts = "SELECT * FROM blog_posts ORDER BY create_ts DESC";
$result = $db->query($sqlSelectPosts);
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
    $stmSelectImages->execute(array(":post_id" => $row["post_id"]));
    $images = $stmSelectImages->fetchAll();

    echo "<div>";
    echo "<h2>" . htmlspecialchars($row["title"]) . "</h2>";
    echo "<p>" . htmlspecialchars($row["body"]) . "</p>";
    if (!empty($images)) {
        // output thumbnail and link for each image
        foreach ($images as $img) {
            $ext = "." . pathinfo($img["image_path"], PATHINFO_EXTENSION);
            $thumb = str_replace($ext, "_t" . $ext, $img["image_path"]);
            echo '<a href="' . $img["image_path"] . '">';
            echo '<img src="' . $thumb . '" alt="' . basename($img["image_path"]) . '">';
            echo "</a>";
        }
    }
    echo "</div>";
}

Summary

You now know how to write your own photo blog application from the ground up to post images and text right from your mobile device. Source code for this project can be downloaded from GitHub.

I hope this article was as helpful to you as it was fun for me writing it. It’s side projects like these that can really help motivate us to keep blogging even when it feels like a chore. As an added bonus, it’s entertaining content for your visitors; I mean, who doesn’t like looking at pictures?

Image via Angela Waye / Shutterstock

Creating a Mobile Photo Blog

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Vladislav

    Ctype is not widely available, especially in shared hosting environments. You can use is_numeric instead of ctype_album.
    I really like the fact that you are using PDO in your tutorial.