OmarElGabry/miniPHP

View on GitHub
app/models/User.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

 /**
  * User Class
  *
  * @license    http://opensource.org/licenses/MIT The MIT License (MIT)
  * @author     Omar El Gabry <omar.elgabry.93@gmail.com>
  */

class User extends Model{

    /**
      * Table name for this & extending classes.
      *
      * @var string
      */
    public $table = "users";

    /**
     * returns an associative array holds the user info(image, name, id, ...etc.)
     *
     * @access public
     * @param  integer $userId
     * @return array Associative array of current user info/data.
     * @throws Exception if $userId is invalid.
     */
    public function getProfileInfo($userId){

        $database = Database::openConnection();
        $database->getById("users", $userId);

        if($database->countRows() !== 1){
            throw new Exception("User ID " .  $userId . " doesn't exists");
        }

        $user = $database->fetchAssociative();

        $user["id"]    = (int)$user["id"];
        $user["image"] = PUBLIC_ROOT . "img/profile_pictures/" . $user['profile_picture'];
        // $user["email"] = empty($user['is_email_activated'])? null: $user['email'];

        return $user;
      }

    /**
     * Update the current profile
     *
     * @access public
     * @param  integer $userId
     * @param  string  $name
     * @param  string  $password
     * @param  string  $email
     * @param  string  $confirmEmail
     * @return bool|array
     * @throws Exception If profile couldn't be updated
     *
     */
    public function updateProfileInfo($userId, $name, $password, $email, $confirmEmail){

        $database = Database::openConnection();
        $curUser = $this->getProfileInfo($userId);

        $name   = (!empty($name) && $name !== $curUser["name"])? $name: null;
        $email  = (!empty($confirmEmail) || (!empty($email) && $email !== $curUser["email"]))? $email: null;

        // if new email === old email, this shouldn't return any errors for email,
        // because they are not 'required', same for name.
        $validation = new Validation();
        if(!$validation->validate([
            "Name" => [$name, "alphaNumWithSpaces|minLen(4)|maxLen(30)"],
            "Password" => [$password, "minLen(6)|password"],
            "Email" => [$email, "email|emailUnique|maxLen(50)|equals(".$confirmEmail.")"]])){
            $this->errors = $validation->errors();
            return false;
        }

        $profileUpdated = ($password || $name || $email)? true: false;
        if($profileUpdated) {

            $options = [
                $name     => "name = :name ",
                $password => "hashed_password = :hashed_password ",
                $email    => "pending_email = :pending_email, pending_email_token = :pending_email_token, email_token = :email_token "
            ];

            $database->beginTransaction();
            $query   = "UPDATE users SET ";
            $query  .= $this->applyOptions($options, ", ");
            $query  .= "WHERE id = :id LIMIT 1 ";
            $database->prepare($query);

            if($name) {
                $database->bindValue(':name', $name);
            }
            if($password) {
                $database->bindValue(':hashed_password', password_hash($password, PASSWORD_DEFAULT, array('cost' => Config::get('HASH_COST_FACTOR'))));
            }
            if($email) {
                $emailToken = sha1(uniqid(mt_rand(), true));
                $pendingEmailToken = sha1(uniqid(mt_rand(), true));
                $database->bindValue(':pending_email', $email);
                $database->bindValue(':pending_email_token', $pendingEmailToken);
                $database->bindValue(':email_token', $emailToken);
            }

            $database->bindValue(':id', $userId);
            $result = $database->execute();

            if(!$result){
                $database->rollBack();
                throw new Exception("Couldn't update profile");
            }

            // If email was updated, then send two emails,
            // one for the current one asking user optionally to revoke,
            // and another one for the new email asking user to confirm changes.
            if($email){
                $name = ($name)? $name: $curUser["name"];
                Email::sendEmail(Config::get('EMAIL_REVOKE_EMAIL'), $curUser["email"], ["name" => $name, "id" => $curUser["id"]], ["email_token" => $emailToken]);
                Email::sendEmail(Config::get('EMAIL_UPDATE_EMAIL'), $email, ["name" => $name, "id" => $curUser["id"]], ["pending_email_token" => $pendingEmailToken]);
            }

            $database->commit();
        }

        return ["emailUpdated" => (($email)? true: false)];
    }

    /**
     * Update Profile Picture.
     *
     * @access public
     * @param  integer $userId
     * @param  array   $fileData
     * @return mixed
     * @throws Exception If failed to update profile picture.
     */
    public function updateProfilePicture($userId, $fileData){

        $image = Uploader::uploadPicture($fileData, $userId);

        if(!$image) {
            $this->errors = Uploader::errors();
            return false;
        }

        $database = Database::openConnection();
        $query  =  "UPDATE users SET profile_picture = :profile_picture WHERE id = :id LIMIT 1";

        $database->prepare($query);
        $database->bindValue(':profile_picture', $image["basename"]);
        $database->bindValue(':id', $userId);
        $result = $database->execute();

        // if update failed, then delete the user picture
        if(!$result){
            Uploader::deleteFile(IMAGES . "profile_pictures/" . $image["basename"]);
            throw new Exception("Profile Picture ". $image["basename"] . " couldn't be updated");
        }

        return $image;
      }

    /**
     * revoke Email updates
     *
     * @access public
     * @param  integer  $userId
     * @param  string   $emailToken
     * @return mixed
     * @throws Exception If failed to revoke email updates.
     */
    public function revokeEmail($userId, $emailToken){

        if (empty($userId) || empty($emailToken)) {
            return false;
        }

        $database = Database::openConnection();
        $database->prepare("SELECT * FROM users WHERE id = :id AND email_token = :email_token AND is_email_activated = 1 LIMIT 1");
        $database->bindValue(':id', $userId);
        $database->bindValue(':email_token', $emailToken);
        $database->execute();
        $users = $database->countRows();

        $query = "UPDATE users SET email_token = NULL, pending_email = NULL, pending_email_token = NULL WHERE id = :id LIMIT 1";
        $database->prepare($query);
        $database->bindValue(':id', $userId);
        $result = $database->execute();

        if(!$result){
            throw new Exception("Couldn't revoke email updates");
        }

        if ($users === 1){
            return true;
        }else{
            Logger::log("REVOKE EMAIL", "User ID ". $userId . " is trying to revoke email using wrong token " . $emailToken, __FILE__, __LINE__);
            return false;
        }
    }

    /**
     * update Email
     *
     * @access public
     * @param  integer  $userId
     * @param  string   $emailToken
     * @return mixed
     * @throws Exception If failed to update current email.
     */
    public function updateEmail($userId, $emailToken){

        if (empty($userId) || empty($emailToken)) {
            return false;
        }

        $database = Database::openConnection();
        $database->prepare("SELECT * FROM users WHERE id = :id AND pending_email_token = :pending_email_token AND is_email_activated = 1 LIMIT 1");
        $database->bindValue(':id', $userId);
        $database->bindValue(':pending_email_token', $emailToken);
        $database->execute();

        if($database->countRows() === 1){

            $user = $database->fetchAssociative();
            $validation = new Validation();
            $validation->addRuleMessage("emailUnique", "We can't change your email because it has been already taken!");

            if(!$validation->validate(["Email" => [$user["pending_email"], "emailUnique"]])){

                $query = "UPDATE users SET email_token = NULL, pending_email = NULL, pending_email_token = NULL WHERE id = :id LIMIT 1";
                $database->prepare($query);
                $database->bindValue(':id', $userId);
                $database->execute();

                $this->errors = $validation->errors();

                return false;

            }else{

                $query = "UPDATE users SET email = :email, email_token = NULL, pending_email = NULL, pending_email_token = NULL WHERE id = :id LIMIT 1";
                $database->prepare($query);
                $database->bindValue(':id', $userId);
                $database->bindValue(':email', $user["pending_email"]);
                $result = $database->execute();

                if(!$result){
                    throw new Exception("Couldn't update current email");
                }

                return true;
            }
        }else {

            $query = "UPDATE users SET email_token = NULL, pending_email = NULL, pending_email_token = NULL WHERE id = :id LIMIT 1";
            $database->prepare($query);
            $database->bindValue(':id', $userId);
            $database->execute();

            Logger::log("UPDATE EMAIL", "User ID ". $userId . " is trying to update email using wrong token " . $emailToken, __FILE__, __LINE__);
            return false;
        }

    }

    /**
     * Get Notifications for newsfeed, posts & files.
     *
     * @access public
     * @param  integer $userId
     * @return array
     */
    public function getNotifications($userId){

        $database = Database::openConnection();
        $query = "SELECT target, count FROM notifications WHERE user_id = :user_id";

        $database->prepare($query);
        $database->bindValue(":user_id", $userId);
        $database->execute();

        $notifications = $database->fetchAllAssociative();
        return $notifications;
      }

    /**
     * Clear Notifications for a specific target
     *
     * @access public
     * @param  integer $userId
     * @param  string $table
     */
    public function clearNotifications($userId, $table){

          $database = Database::openConnection();
          $query = "UPDATE notifications SET count = 0 WHERE user_id = :user_id AND target = :target";

          $database->prepare($query);
          $database->bindValue(":user_id", $userId);
          $database->bindValue(":target", $table);
          $result = $database->execute();

          if(!$result) {
              Logger::log("NOTIFICATIONS", "Couldn't clear notifications", __FILE__, __LINE__);
          }
      }

    /**
     * Returns an overview about the current system:
     * 1. counts of newsfeed, posts, files, users
     * 2. latest updates by using "UNION"
     *
     * @access public
     * @return array
     *
     */
    public function dashboard(){

        $database = Database::openConnection();

        // 1. count
        $tables = ["newsfeed", "posts", "files", "users"];
        $stats  = [];

        foreach($tables as $table){
            $stats[$table] = $database->countAll($table);
        }

        // 2. latest updates
        // Using UNION to union the data fetched from different tables.
        // @see http://www.w3schools.com/sql/sql_union.asp
        // @see (mikeY) http://stackoverflow.com/questions/6849063/selecting-data-from-two-tables-and-ordering-by-date

        // Sub Query: In SELECT, The outer SELECT must have alias, like "updates" here.
        // NOTE: The outer SELECT is not needed; You don't need to wrap the union-ed select statements.
        // @see http://stackoverflow.com/questions/1888779/every-derived-table-must-have-its-own-alias

        $query  = "SELECT * FROM (";
        $query .= "SELECT 'newsfeed' AS target, content AS title, date, users.name FROM newsfeed, users WHERE user_id = users.id UNION ";
        $query .= "SELECT 'posts' AS target, title, date, users.name FROM posts, users WHERE user_id = users.id UNION ";
        $query .= "SELECT 'files' AS target, filename AS title, date, users.name FROM files, users WHERE user_id = users.id ";
        $query .= ") AS updates ORDER BY date DESC LIMIT 10";
        $database->prepare($query);
        $database->execute();
        $updates = $database->fetchAllAssociative();

        $data = array("stats" => $stats, "updates" => $updates);
        return $data;
    }

    /**
     * Reporting Bug, Feature, or Enhancement.
     *
     * @access public
     * @param  integer $userId
     * @param  string  $subject
     * @param  string  $label
     * @param  string  $message
     * @return bool
     *
     */
    public function reportBug($userId, $subject, $label, $message){

        $validation = new Validation();
        if(!$validation->validate([
            "Subject" => [$subject, "required|minLen(4)|maxLen(80)"],
            "Label" => [$label, "required|inArray(".Utility::commas(["bug", "feature", "enhancement"]).")"],
            "Message" => [$message, "required|minLen(4)|maxLen(1800)"]])){

            $this->errors = $validation->errors();
            return false;
          }

        $curUser = $this->getProfileInfo($userId);
        $data = ["subject" => $subject, "label" => $label, "message" => $message];

        // email will be sent to the admin
        Email::sendEmail(Config::get('EMAIL_REPORT_BUG'), Config::get('ADMIN_EMAIL'), ["id" => $userId, "name" => $curUser["name"]], $data);

        return true;
      }
  }