[opw-web] Evolve user profile handling



commit 84c47deebac05fb3747a436f08ec84cc00f1a2d3
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Sun Mar 9 17:58:20 2014 -0400

    Evolve user profile handling
    
    * Show more information on the user profile screen
    * Show different information to different users for the user profile
    * Add user profile editing
    * Add concept of website / profile urls to the database
    * Improve user creation routines
    * Fix various bugs

 classes/class_user.php                             |  100 ++++++++----
 lang/en-gb.php                                     |    8 +
 modules/mod_login.php                              |   10 +-
 modules/mod_user_profile.php                       |  159 ++++++++++++++++----
 schema.sql                                         |    5 +
 skins/easterngreen/css/main.css                    |    5 +
 skins/easterngreen/html/tpl_user_profile.html      |   35 ++++-
 .../easterngreen/html/tpl_user_profile_editor.html |   43 ++++++
 .../html/tpl_user_profile_identity.html            |   23 +++
 9 files changed, 317 insertions(+), 71 deletions(-)
---
diff --git a/classes/class_user.php b/classes/class_user.php
index a39b318..10c9ae2 100644
--- a/classes/class_user.php
+++ b/classes/class_user.php
@@ -216,6 +216,7 @@ class user
     {
         $hybridauth = $this->hybridauth();
         if ($hybridauth->authenticate("Google", array (
+            'scope' => 'https://www.googleapis.com/auth/userinfo.profile 
https://www.googleapis.com/auth/userinfo.email',
             'hauth_return_to' => Hybrid_Auth::getCurrentUrl() . '&p=Google'
         ))) {
             return $this->finish_login('Google');
@@ -229,6 +230,7 @@ class user
     {
         $hybridauth = $this->hybridauth();
         if ($hybridauth->authenticate("Facebook", array (
+            'scope' => 'email, basic_info',
             'hauth_return_to' => Hybrid_Auth::getCurrentUrl() . '&p=Facebook'
         ))) {
             return $this->finish_login('Facebook');
@@ -265,7 +267,7 @@ class user
 
         if ($values == false) {
             // Get current session data
-            $sql = "SELECT is_admin, fullname, email FROM {$db->prefix}profiles " .
+            $sql = "SELECT * FROM {$db->prefix}profiles " .
                    "WHERE username = ?";
             $row = $db->query($sql, $username, true);
             if ($row === false) {
@@ -275,7 +277,9 @@ class user
 
                 $values['is_admin'] = $row['is_admin'] != 0;
                 $values['fullname'] = $row['fullname'];
+                $values['websiteUrl'] = $row['websiteUrl'];
                 $values['email'] = $row['email'];
+                $values['emailVerified'] = $row['emailVerified'] != 0;
 
                 $cache->put($key, $values, 'users');
             }
@@ -286,8 +290,21 @@ class user
 
     function create_user($provider, $identifier, $profile)
     {
-        global $db;
+        global $db, $cache;
+
+        // The OpenID provider sets displayName to a reversed lastname - firstname,
+        // if unset. Detect this to avoid thinking we got a real DisplayName from
+        // the provider.
+        $badDisplayName = trim( $profile->lastName . " " . $profile->firstName );
 
+        if ($profile->displayName != '' && $profile->displayName != $badDisplayName)
+            $fullname = trim($profile->displayName);
+        else if ($profile->firstName != '' && $profile->lastName != '')
+            $fullname = trim($profile->firstName) . " " . trim($profile->lastName);
+        else
+            $fullname = '';
+
+        // Derive the username first from the email; if not present from the display name
         $suffix = "";
         $email = $profile->email;
         if ($email != "") {
@@ -296,16 +313,28 @@ class user
                 $username = $email;
             else
                 $username = substr($email, 0, $pos);
-        } else if ($profile->firstName != '' && $profile->lastName != '') {
-            $username = trim($profile->firstName) . "." . trim($profile->lastName);
-            $suffix = 1;
+        } else if ($fullname != '') {
+            $username = $fullname;
         } else {
             $username = 'user';
+            $suffix = 1;
+        }
+
+        // Sanitize resulting username
+        $username = preg_replace('/[^A-Za-z0-9_.-]+/', '.', $username);
+        $username = preg_replace('/^\./', '', $username);
+        $username = preg_replace('/\.$/', '', $username);
+
+        // If we sanitized everything away, go back to the extreme fallback
+        if ($username == '') {
+            $username = 'user';
+            $suffix = 1;
         }
 
+        // Now we need to make sure that everything is unique
         while ($this->lookup_user($username . $suffix)) {
             if ($suffix === "")
-                $suffix = 1;
+                $suffix = 2;
             else
                 $suffix++;
         }
@@ -320,45 +349,40 @@ class user
             $emailVerified = 0;
         }
 
-        // The OpenID provider sets displayName to a reversed lastname - firstname,
-        // if unset. Detect this to avoid thinking we got a real DisplayName from
-        // the provider.
-        $badDisplayName = trim( $this->user->profile->lastName . " " . $this->user->profile->firstName );
-
-        if ($profile->displayName != '' && $profile->displayName != $badDisplayName)
-            $fullname = $profile->displayName;
-        else if ($profile->firstName != '' && $profile->lastName != '')
-            $fullname = trim($profile->firstName) . " " . trim($profile->lastName);
+        # The profileUrl for the provider should be something verified
+        if ($provider == 'OpenID')
+            $profileUrl = $identifier;
         else
-            $fullname = '';
+            $profileUrl = $profile->profileURL;
 
         $params = array('username' => $username,
                         'fullname' => $fullname,
                         'email' => $email,
                         'emailVerified' => $emailVerified,
+                        'profileUrl' => $profileUrl,
                         'is_admin' => 0,
                         'provider' => $provider,
                         'identifier' => $identifier);
 
         $sql = "INSERT INTO {$db->prefix}profiles " .
-                   "(username, fullname, email, emailVerified, is_admin) " .
-                   "VALUES (:username, :fullname, :email, :emailVerified, :is_admin)";
+                   "(username, fullname, email, emailVerified, websiteUrl, is_admin) " .
+                   "VALUES (:username, :fullname, :email, :emailVerified, :profileUrl, :is_admin)";
         $db->query($sql, $params);
 
         $sql = "INSERT INTO {$db->prefix}identities " .
-                   "(provider, identifier, username) " .
-                   "VALUES (:provider, :identifier, :username)";
+                   "(provider, identifier, username, fullname, email, emailVerified, profileUrl) " .
+                   "VALUES (:provider, :identifier, :username, :fullname, :email, :emailVerified, 
:profileUrl)";
         $db->query($sql, $params);
 
         $cache->purge('users');
+
+        return $username;
     }
 
-    function finish_login($provider)
+    function finish_login($provider, &$user_created)
     {
         $hybridauth = $this->hybridauth();
 
-        Hybrid_Logger::info("finish_login - $provider");
-
         if ($provider == "OpenID" && $hybridauth->isConnectedWith("OpenID")) {
             $adapter = $hybridauth->getAdapter("OpenID");
             $profile = $adapter->getUserProfile();
@@ -372,16 +396,14 @@ class user
            return false;
         }
 
-        Hybrid_Logger::info("finish_login - connected");
+        $identifier = $profile->identifier;
 
-        $identifier = $adapter->getUserProfile()->identifier;
-
-        Hybrid_Logger::info("finish_login - looking up identity");
         $username = $this->lookup_identity($provider, $identifier);
 
+        $user_created = false;
         if ($username == null) {
-            Hybrid_Logger::info("finish_login - create user");
             $username = $this->create_user($provider, $identifier, $profile);
+            $user_created = true;
         }
 
         $info = $this->lookup_user($username);
@@ -440,6 +462,12 @@ class user
     function get_role($program_id, &$role, &$organization) {
         global $cache, $db;
 
+        $organization = null;
+        $role = 'g';
+
+        if ($this->username === null)
+            return;
+
         $key = "role_{$this->username}_{$program_id}";
         $role_data = $cache->get($key, 'roles');
 
@@ -462,11 +490,17 @@ class user
             $role = $role_data['role'];
             $organization = $role_data['organization_id'];
         }
-        else
-        {
-            $role = 'g';
-            $organization = null;
-        }
+    }
+
+    // See if the current user is a mentor in any program (allows seeing user profile details)
+    function isMentorAnyProgram() {
+        global $db;
+
+        $sql = "SELECT COUNT(*) as count FROM {$db->prefix}roles " .
+                   "WHERE username = ? and role = 'm'";
+        $row = $db->query($sql, $this->username, true);
+
+        return $row['count'] > 0;
     }
 
     // Restricts a screen to a specific condition only
diff --git a/lang/en-gb.php b/lang/en-gb.php
index 4200c49..49978ed 100644
--- a/lang/en-gb.php
+++ b/lang/en-gb.php
@@ -194,12 +194,20 @@ $lang_data = array(
     'user_profile'          => 'User profile',
     'full_name'             => 'Full name',
     'email'                 => 'Email address',
+    'website'               => 'Website',
+    'verified'              => '(verified)',
+    'unverified'            => '(unverified)',
     'previous_page'         => 'Previous page',
     'user_avatar'           => 'User avatar',
     'user_404'              => 'The requested user could not be found',
     'contact_user'          => 'Contact user',
     'full_profile'          => 'View full profile',
     'site_admin'            => 'Site admin',
+    'log_in_identity'       => 'Log-in Identity',
+    'profile_url'           => 'Profile page',
+    'visible_to_mentors'    => '(visible to mentors)',
+    'visible_to_public'     => '(visible to public)',
+    'edit_user_profile'     => 'Edit user profile',
 
     /* Module: user_bans */
     'ban_user'              => 'Ban a user',
diff --git a/modules/mod_login.php b/modules/mod_login.php
index 2cc300a..0ec4d01 100644
--- a/modules/mod_login.php
+++ b/modules/mod_login.php
@@ -20,8 +20,14 @@ if ($user->is_logged_in)
 }
 
 // If we're on the end of successful authentication request
-if ($provider != '' && $user->finish_login($provider)) {
-    $core->redirect($dest_url);
+if ($provider != '' && $user->finish_login($provider, $user_created)) {
+    if ($user_created) {
+        // The first time a user logs in, take them to their profile
+        $username_url = urlencode($user->username);
+        $core->redirect("?q=user_profile&u={$username_url}");
+    } else {
+        $core->redirect($dest_url);
+    }
 }
 
 // Login data was submitted
diff --git a/modules/mod_user_profile.php b/modules/mod_user_profile.php
index 4b5b8fd..2f0f9e2 100644
--- a/modules/mod_user_profile.php
+++ b/modules/mod_user_profile.php
@@ -8,6 +8,7 @@
 if (!defined('IN_PANDORA')) exit;
 
 // Get the username and return URL
+$action = $core->variable('a', 'view');
 $return_encoded = $core->variable('r', '');
 $username_encoded = $core->variable('u', '');
 
@@ -18,44 +19,148 @@ $username = urldecode($username_encoded);
 if (empty($username)) {
    $user->restrict(!is_null($user->username));
    $username = $user->username;
+   $username_encoded = urlencode($username);
 }
 
+$is_self = ($username == $user->username);
+$can_edit = $is_self || $user->is_admin;
+$can_view_details = $is_self || $user->is_admin || $user->isMentorAnyProgram();
+
 // Get the user data
 $username_data = $user->lookup_user($username);
 
-// Set the template data
-if (!is_null($username_data))
-{
-    $is_admin   = false;
-    $avatar_url = "?q=user_avatar&amp;u={$username_encoded}";
-    $full_name  = $username_data['fullname'];
-    $user_email = $username_data['email'];
-    $is_admin = $username_data['is_admin'];
+$skin->assign(array(
+   'visible_to_public' => $is_self ? $lang->get('visible_to_public') : '',
+   'visible_to_mentors' => $is_self ? $lang->get('visible_to_mentors') : ''
+));
+
+if ($action == 'view') {
+    // Set the template data
+    if (!is_null($username_data))
+    {
+        // And the identity data
+        if ($can_view_details) {
+            $sql = "SELECT * from {$db->prefix}identities " .
+                "WHERE username = ?";
+            $identity_data = $db->query($sql, $username);
+        } else {
+            $identity_data = array();
+        }
+
+        $identities_list = '';
+        foreach ($identity_data as $row)
+        {
+            // Assign data for each project
+            $skin->assign(array(
+                    'identity_provider'      => $row['provider'],
+                    'identity_full_name'     => htmlspecialchars($row['fullname']),
+                    'identity_email'         => htmlspecialchars($row['email']),
+                    'identity_email_verified' => $row['emailVerified'] != 0 ?
+                                                  $lang->get('{{verified}}') : $lang->get('{{unverified}}'),
+                    'identity_profile_url'   => htmlspecialchars($row['profileUrl'])
+                ));
+
+                $identities_list .= $skin->output('tpl_user_profile_identity');
+            }
+
+        $is_admin   = false;
+        $avatar_url = "?q=user_avatar&amp;u={$username_encoded}";
+        $edit_url = "?q=user_profile&amp;a=editor&amp;u={$username_encoded}";
+        $full_name  = $username_data['fullname'];
+        $user_email = $can_view_details ? $username_data['email'] : '';
+        $user_email_verified = $can_view_details ? $username_data['emailVerified'] : false;
+        $website_url = $username_data['websiteUrl'];
+        $is_admin = $username_data['is_admin'];
+
+        // Assign profile variables
+        $skin->assign(array(
+            'user_username'         => htmlspecialchars($username),
+            'user_fullname'         => htmlspecialchars($full_name),
+            'user_email'            => htmlspecialchars($user_email),
+            'user_email_verified'   => $user_email_verified ? $lang->get('{{verified}}') : 
$lang->get('{{unverified}}'),
+            'website_url'           => htmlspecialchars($website_url),
+            'avatar_url'            => $avatar_url,
+            'return_url'            => $return_url,
+            'edit_url'              => $edit_url,
+            'identities_list'       => $identities_list,
+            'profile_visibility'    => $skin->visibility(true),
+            'notice_visibility'     => $skin->visibility(false),
+            'edit_visibility'       => $skin->visibility($can_edit),
+            'details_visibility'    => $skin->visibility($can_view_details),
+            'contact_visibility'    => $skin->visibility(!$is_self && $can_view_details),
+            'badge_visibility'      => $skin->visibility($is_admin),
+            'return_visibility'     => $skin->visibility(empty($return_url), true),
+        ));    
+    }
+    else
+    {
+        // No profile found, show notice
+        $skin->assign(array(
+            'profile_visibility'    => $skin->visibility(false),
+            'notice_visibility'     => $skin->visibility(true),
+        ));
+    }
+    // Output the page
+    $module_title = $lang->get('user_profile');
+    $module_data = $skin->output('tpl_user_profile');
+
+} else if ($action == 'editor') {
+    $user->restrict($can_edit);
+
+    if (isset($_POST['user_save'])) {
+        $fullname = $core->variable('fullname', '', false, true);
+        $email = $core->variable('email', '', false, true);
+        $website = $core->variable('website', '', false, true);
+
+        if ($email == $username_data['email']) {
+            $email_verified = $username_data['emailVerified'];
+        } else {
+            // Check if the user is changing the email back to a verified login email
+            $sql = "SELECT COUNT(*) as count from {$db->prefix}identities " .
+                   "WHERE username = :username " .
+                       "AND email = :email " .
+                       "AND emailVerified = 1";
+            $row = $db->query($sql,
+                              array('username' => $username,
+                                    'email' => $email),
+                              true);
+            $email_verified = $row['count'] > 0;
+        }
+
+        $sql = "UPDATE {$db->prefix}profiles " .
+               "SET fullname = :fullname, " .
+                   "email = :email, " .
+                   "emailVerified = :email_verified, " .
+                   "websiteUrl = :website_url " .
+               "WHERE username = :username";
+        $db->query($sql,
+                   array('username' => $username,
+                         'fullname' => $fullname,
+                         'email' => $email,
+                         'email_verified' => $email_verified ? 1 : 0,
+                         'website_url' => $website));
+
+        $core->redirect("?q=user_profile&u={$username_encoded}");
+    }
 
     // Assign profile variables
     $skin->assign(array(
-        'user_username'         => htmlspecialchars($username),
-        'user_fullname'         => htmlspecialchars($full_name),
-        'user_email'            => htmlspecialchars($user_email),
-        'avatar_url'            => $avatar_url,
-        'return_url'            => $return_url,
-        'profile_visibility'    => $skin->visibility(empty($user_email), true),
-        'notice_visibility'     => $skin->visibility(empty($user_email)),
-        'badge_visibility'      => $skin->visibility($is_admin),
-        'return_visibility'     => $skin->visibility(empty($return_url), true),
-    ));    
+        'user_fullname'         => htmlspecialchars($username_data['fullname']),
+        'user_email'            => htmlspecialchars($username_data['email']),
+        'website_url'           => htmlspecialchars($username_data['websiteUrl']),
+        'cancel_url'            => "?q=user_profile&amp;u={$username_encoded}",
+        'error_visibility'      => $skin->visibility(false),
+        'error_message'         => ''
+    ));
+
+    // Output the page
+    $module_title = $lang->get('edit_user_profile');
+    $module_data = $skin->output('tpl_user_profile_editor');
 }
 else
 {
-    // No profile found, show notice
-    $skin->assign(array(
-        'profile_visibility'    => $skin->visibility(false),
-        'notice_visibility'     => $skin->visibility(true),
-    ));
+    // Unknown action
+    $core->redirect($core->path());
 }
 
-// Output the page
-$module_title = $lang->get('user_profile');
-$module_data = $skin->output('tpl_user_profile');
-    
 ?>
\ No newline at end of file
diff --git a/schema.sql b/schema.sql
index 3688929..77c8a05 100644
--- a/schema.sql
+++ b/schema.sql
@@ -89,6 +89,10 @@ CREATE TABLE `opw_identities` (
   `provider` varchar(20) NOT NULL,
   `identifier` varchar(255) NOT NULL,
   `username` varchar(255) NOT NULL,
+  `fullname` varchar(255) NOT NULL,
+  `email` varchar(255) NOT NULL,
+  `emailVerified` tinyint(1) DEFAULT 0,
+  `profileUrl` varchar(255) NOT NULL,
   PRIMARY KEY (`provider`, `identifier`)
 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
@@ -97,6 +101,7 @@ CREATE TABLE `opw_profiles` (
   `fullname` varchar(255) NOT NULL,
   `email` varchar(255) NOT NULL,
   `emailVerified` tinyint(1) DEFAULT 0,
+  `websiteUrl` varchar(255) NOT NULL DEFAULT '',
   `is_admin` tinyint(1) DEFAULT 0,
   PRIMARY KEY (`username`)
 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/skins/easterngreen/css/main.css b/skins/easterngreen/css/main.css
index 06065d4..34ba1b2 100644
--- a/skins/easterngreen/css/main.css
+++ b/skins/easterngreen/css/main.css
@@ -168,6 +168,11 @@ h3 {
     padding: 30px 30px 20px 30px;
 }
 
+.visibility-note {
+    font-size: 90%;
+    color: #888;
+}
+
 /*-----------------Links and images-----------------*/
 
 .learn-more {
diff --git a/skins/easterngreen/html/tpl_user_profile.html b/skins/easterngreen/html/tpl_user_profile.html
index 61f882c..20a657f 100644
--- a/skins/easterngreen/html/tpl_user_profile.html
+++ b/skins/easterngreen/html/tpl_user_profile.html
@@ -14,7 +14,9 @@
     <table>
         <tr>
             <td style="width:165px">
+          <!--
                 <img class="thumbnail" src="[[avatar_url]]" alt="{{user_avatar}}" />
+            -->
 
                 <div class="align-center top-spacer [[badge_visibility]]">
                     <span class="label label-admin">
@@ -30,16 +32,25 @@
                         <tr>
                             <td style="width:140px">{{username}}</td>
                             <td>[[user_username]]</td>
+                            <td class="visibility-note"></td>
                         </tr>
 
                         <tr>
                             <td>{{full_name}}</td>
                             <td>[[user_fullname]]</td>
+                            <td class="visibility-note">[[visible_to_public]]</td>
                         </tr>
 
                         <tr>
+                            <td>{{website}}</td>
+                            <td><a href="[[website_url]]">[[website_url]]</a></td>
+                            <td class="visibility-note">[[visible_to_public]]</td>
+                        </tr>
+
+                        <tr class="[[details_visibility]]"> <!-- avoid blank row -->
                             <td>{{email}}</td>
-                            <td>[[user_email]]</td>
+                            <td>[[user_email]] [[user_email_verified]]</td>
+                            <td class="visibility-note">[[visible_to_mentors]]</td>
                         </tr>
                     </tbody>
                 </table>
@@ -47,16 +58,22 @@
         </tr>
     </table>
 
+    <h4 class="[[details_visibility]]">{{log_in_identity}} <span 
class="visibility-note">[[visible_to_mentors]]</span></h4>
+    <table>
+      <tbody>
+        [[identities_list]]
+      </tbody>
+    </table>
+
     <div class="form-actions">
-        <a href="mailto:[[user_email]]"; class="btn btn-primary">
-            <i class="icon-envelope icon-white"></i>
-            {{contact_user}}
+        <a href="[[edit_url]]" class="btn btn-primary [[edit_visibility]]">
+            <i class="icon-pencil icon-white"></i>
+            {{edit}}
         </a>
 
-        <a href="https://identity.kde.org/index.php?r=people/view&amp;uid=[[user_username]]"; class="btn"
-           target="_blank">
-            <i class="icon-user icon-black"></i>
-            {{full_profile}}
+        <a href="mailto:[[user_email]]"; class="btn btn-primary [[contact_visibility]]">
+            <i class="icon-envelope icon-white"></i>
+            {{contact_user}}
         </a>
     </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/skins/easterngreen/html/tpl_user_profile_editor.html 
b/skins/easterngreen/html/tpl_user_profile_editor.html
new file mode 100644
index 0000000..dd886ff
--- /dev/null
+++ b/skins/easterngreen/html/tpl_user_profile_editor.html
@@ -0,0 +1,43 @@
+<h1>{{edit_user_profile}}</h1>
+<hr class="hr-head" />
+
+<div class="alert alert-error [[error_visibility]]">
+    <a class="close" data-dismiss="alert">×</a>
+    [[error_message]]
+</div>
+
+<div class="control-group">
+    <label class="control-label">{{full_name}}</label>
+    <div class="controls">
+        <input type="text" name="fullname" maxlength="100" class="input-xxlarge" value="[[user_fullname]]" />
+        <span class="visibility-note">[[visible_to_public]]</span>
+    </div>
+</div>
+
+<div class="control-group">
+    <label class="control-label">{{website}}</label>
+    <div class="controls">
+        <input type="text" name="website" maxlength="100" class="input-xxlarge" value="[[website_url]]" />
+        <span class="visibility-note">[[visible_to_public]]</span>
+    </div>
+</div>
+
+<div class="control-group">
+    <label class="control-label">{{email}}</label>
+    <div class="controls">
+        <input type="text" name="email" maxlength="100" class="input-xxlarge" value="[[user_email]]" />
+        <span class="visibility-note">[[visible_to_mentors]]</span>
+    </div>
+</div>
+
+<div class="form-actions">
+    <button type="submit" name="user_save" class="btn btn-primary">
+        <i class="icon-ok-sign icon-white"></i>
+        {{save}}
+    </button>
+
+    <a href="[[cancel_url]]" class="btn">
+        <i class="icon-remove icon-black"></i>
+        {{cancel}}
+    </a>
+</div>
diff --git a/skins/easterngreen/html/tpl_user_profile_identity.html 
b/skins/easterngreen/html/tpl_user_profile_identity.html
new file mode 100644
index 0000000..7e10dea
--- /dev/null
+++ b/skins/easterngreen/html/tpl_user_profile_identity.html
@@ -0,0 +1,23 @@
+<tr>
+  <td style="width:165px">
+    [[identity_provider]]
+  </td>
+  <td style="width:140px">{{full_name}}</td>
+  <td>
+    [[identity_full_name]]
+  </td>
+</tr>
+<tr>
+  <td></td>
+  <td>{{email}}</td>
+  <td>
+    [[identity_email]] [[identity_email_verified]]
+  </td>
+</tr>
+<tr>
+  <td></td>
+  <td>{{profile_url}}</td>
+  <td>
+    <a href="[[identity_profile_url]]">[[identity_profile_url]]</a> {{verified}}
+  </td>
+</tr>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]