Skip to content

Commit 6bcc9fe

Browse files
authored
SAK-32211 Added ability to change picture in portal (sakaiproject#3950)
Original work from NYU with some considerable work on standardisation by myself.
1 parent 7823e5d commit 6bcc9fe

File tree

13 files changed

+560
-5
lines changed

13 files changed

+560
-5
lines changed

portal/portal-impl/impl/src/bundle/sitenav.properties

+9
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,12 @@ pc_video_mute_local_audio_title = Mute your audio
161161
pc_video_enable_local_video_title = Enable your video
162162
pc_video_disable_local_video_title = Disable your video
163163
pc_video_askforincoming = Do you want to answer?
164+
165+
pic_changer_title = Change Profile Picture
166+
pic_changer_upload = Upload
167+
pic_changer_choose = Choose a file
168+
pic_changer_save = Save
169+
pic_changer_cancel = Cancel
170+
pic_changer_remove = Remove
171+
pic_changer_remove_error = Error removing image
172+
pic_changer_upload_error = Error uploading image

portal/portal-render-engine-impl/pack/src/webapp/vm/morpheus/includeLoginNav.vm

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
#if (${tabsSites.mrphs_profileToolUrl})
6565

6666
<div id="loginUser" role="menuitem" class="has-avatar Mrphs-userNav__submenuitem--userlink #if (${site.isCurrentSite}) current-site #end">
67-
<a href="javascript:void(0);" class="Mrphs-userNav__drop-btn Mrphs-userNav__submenuitem--profilepicture" style="background-image:url(/direct/profile/${loginUserId}/image/thumb)" tabindex="-1"></a>
67+
<a href="javascript:void(0);" class="Mrphs-userNav__submenuitem--profilepicture" style="background-image:url(/direct/profile/${loginUserId}/image/thumb)" tabindex="-1"></a>
6868
<a href="javascript:void(0);" class="Mrphs-userNav__drop-btn Mrphs-userNav__submenuitem--username">${loginUserFirstName}</a>
6969
</div>
7070

portal/portal-render-engine-impl/pack/src/webapp/vm/morpheus/includeStandardHead.vm

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<link href="${pageScriptPath}jquery/cluetip/1.2.10/css/jquery.cluetip.css$!{portalCDNQuery}" rel="stylesheet">
1919
<link href="${pageScriptPath}jquery/qtip/jquery.qtip-latest.min.css$!{portalCDNQuery}" rel="stylesheet">
2020
<link href="${pageWebjarsPath}pnotify/2.1.0/pnotify.core.min.css$!{portalCDNQuery}" rel="stylesheet">
21+
<link href="${pageWebjarsPath}cropper/2.3.2/dist/cropper.min.css$!{portalCDNQuery}" rel="stylesheet">
2122
<script src="${pageSkinRepo}/${pageSkin}/js/lib/modernizr.js$!{portalCDNQuery}"></script>
2223
#if ($useBullhornAlerts)
2324
<script src="/profile2-tool/javascript/profile2-eb.js$!{portalCDNQuery}"></script>

portal/portal-render-engine-impl/pack/src/webapp/vm/morpheus/site.vm

+42
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
<script src="${pageWebjarsPath}/pnotify/2.1.0/pnotify.core.min.js$!{portalCDNQuery}"></script>
124124
<script src="${pageScriptPath}jquery/qtip/jquery.qtip-latest.min.js$!{portalCDNQuery}"></script>
125125
<script src="${pageScriptPath}jquery/qtip/tutorial.js$!{portalCDNQuery}"></script>
126+
<script src="${pageWebjarsPath}cropper/2.3.2/dist/cropper.min.js$!{portalCDNQuery}"></script>
126127

127128
#if ( $tutorial && $loggedIn )
128129
<script>$(document).ready(function(){startTutorial({'showTutorialLocationOnHide': 'true'});});</script>
@@ -193,5 +194,46 @@
193194
#parse("/vm/morpheus/includeGoogleTagManager.vm")
194195

195196
${includeExtraHead}
197+
198+
<!-- Modal popup for profile picture changer -->
199+
<div class="modal fade" id="profileImageUpload" tabindex="-1" role="dialog">
200+
<div class="modal-dialog" role="document">
201+
<div class="modal-content">
202+
<div class="modal-header"><h3>${rloader.getString("pic_changer_title")}</h3></div>
203+
<div class="modal-body">
204+
<div id="remove-error" class="alert alert-danger">${rloader.getString("pic_changer_remove_error")}</div>
205+
<div id="upload-error" class="alert alert-danger">${rloader.getString("pic_changer_upload_error")}</div>
206+
<a id="upload" class="button">
207+
${rloader.getString("pic_changer_upload")}
208+
<input type="file" id="file" value="${rloader.getString("pic_changer_choose")}" accept="image/*">
209+
</a>
210+
<div id="cropme">
211+
<img style="maxWidth: 100%" />
212+
</div>
213+
<div id="cropToolbar" class="btn-toolbar" style="display: none;">
214+
<div class="btn-group">
215+
<a class="profile-image-zoom-in btn btn-sm btn-default" href="javascript:void(0)"></a>
216+
<a class="profile-image-zoom-out btn btn-sm btn-default" href="javascript:void(0)"></a>
217+
</div>
218+
<div class="btn-group">
219+
<a class="profile-image-pan-up btn btn-sm btn-default" href="javascript:void(0)"></a>
220+
<a class="profile-image-pan-down btn btn-sm btn-default" href="javascript:void(0)"></a>
221+
<a class="profile-image-pan-left btn btn-sm btn-default" href="javascript:void(0)"></a>
222+
<a class="profile-image-pan-right btn btn-sm btn-default" href="javascript:void(0)"></a>
223+
</div>
224+
<div class="btn-group">
225+
<a class="profile-image-rotate btn btn-sm btn-default" href="javascript:void(0)"></a>
226+
</div>
227+
</div>
228+
</div>
229+
<div class="modal-footer">
230+
<button type="button" class="button_color pull-left" id="save" disabled="disabled">${rloader.getString("pic_changer_save")}</button>
231+
<button type="button" class="button pull-left" data-dismiss="modal">${rloader.getString("pic_changer_cancel")}</button>
232+
<button class="btn btn-link remove-profile-image pull-right">${rloader.getString("pic_changer_remove")}</button>
233+
</div>
234+
</div>
235+
</div>
236+
</div>
237+
196238
</body>
197239
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.sakaiproject.profile2.service;
2+
3+
public interface ProfileImageService {
4+
5+
public abstract Long getCurrentProfileImageId(final String userUuid);
6+
public abstract String getProfileImageURL(final String userUuid, final String eid, final boolean thumbnail);
7+
public abstract String resetCachedProfileImageId(final String userId);
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.sakaiproject.profile2.service;
2+
3+
import lombok.Setter;
4+
import org.sakaiproject.profile2.dao.ProfileDao;
5+
import org.sakaiproject.profile2.hbm.model.ProfileImageUploaded;
6+
import org.sakaiproject.profile2.util.ProfileConstants;
7+
import org.sakaiproject.tool.api.SessionManager;
8+
9+
public class ProfileImageServiceImpl implements ProfileImageService {
10+
11+
@Setter
12+
private ProfileDao dao;
13+
14+
@Setter
15+
private SessionManager sessionManager;
16+
17+
public Long getCurrentProfileImageId(final String userId) {
18+
19+
ProfileImageUploaded profileImage = dao.getCurrentProfileImageRecord(userId);
20+
if (profileImage == null) {
21+
return null;
22+
}
23+
return profileImage.getId();
24+
}
25+
26+
public String getProfileImageURL(final String userId, final String eid, final boolean thumbnail) {
27+
28+
if (userId == null) {
29+
if (thumbnail) {
30+
return ProfileConstants.UNAVAILABLE_IMAGE_THUMBNAIL;
31+
} else {
32+
return ProfileConstants.UNAVAILABLE_IMAGE_FULL;
33+
}
34+
}
35+
36+
String url = "/direct/profile/"+eid+"/image";
37+
38+
if (thumbnail) {
39+
url += "/thumb";
40+
}
41+
42+
if (sessionManager.getCurrentSession().getAttribute("profileImageId") == null) {
43+
resetCachedProfileImageId(userId);
44+
}
45+
46+
url += "?_=" + ((Long) sessionManager.getCurrentSession().getAttribute("profileImageId")).toString();
47+
48+
return url;
49+
}
50+
51+
public String resetCachedProfileImageId(final String userId) {
52+
53+
Long profileImageId = getCurrentProfileImageId(userId);
54+
if (profileImageId == null) {
55+
profileImageId = Long.valueOf(0);
56+
}
57+
sessionManager.getCurrentSession().setAttribute("profileImageId", profileImageId);
58+
59+
return profileImageId.toString();
60+
}
61+
62+
}

profile2/pack/src/webapp/WEB-INF/components.xml

+6
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,10 @@
286286
<property name="securityService" ref="org.sakaiproject.authz.api.SecurityService" />
287287
</bean>
288288

289+
<bean id="org.sakaiproject.profile2.service.ProfileImageService"
290+
class="org.sakaiproject.profile2.service.ProfileImageServiceImpl">
291+
<property name="dao" ref="org.sakaiproject.profile2.dao.ProfileDao" />
292+
<property name="sessionManager" ref="org.sakaiproject.tool.api.SessionManager" />
293+
</bean>
294+
289295
</beans>

profile2/tool/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@
100100
<groupId>org.sakaiproject.kernel</groupId>
101101
<artifactId>sakai-kernel-util</artifactId>
102102
</dependency>
103+
<dependency>
104+
<groupId>com.googlecode.json-simple</groupId>
105+
<artifactId>json-simple</artifactId>
106+
<version>1.1.1</version>
107+
</dependency>
103108
</dependencies>
104109

105110
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package org.sakaiproject.profile2.tool.entityprovider;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import lombok.Setter;
5+
import org.apache.commons.codec.binary.Base64;
6+
import org.json.simple.JSONObject;
7+
import org.sakaiproject.entitybroker.EntityView;
8+
import org.sakaiproject.entitybroker.entityprovider.EntityProvider;
9+
import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction;
10+
import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsExecutable;
11+
import org.sakaiproject.entitybroker.entityprovider.capabilities.AutoRegisterEntityProvider;
12+
import org.sakaiproject.entitybroker.entityprovider.capabilities.Describeable;
13+
import org.sakaiproject.entitybroker.entityprovider.capabilities.Outputable;
14+
import org.sakaiproject.entitybroker.entityprovider.extension.Formats;
15+
import org.sakaiproject.entitybroker.util.AbstractEntityProvider;
16+
import org.sakaiproject.profile2.logic.ProfileImageLogic;
17+
import org.sakaiproject.profile2.service.ProfileImageService;
18+
import org.sakaiproject.profile2.util.ProfileConstants;
19+
import org.sakaiproject.tool.api.SessionManager;
20+
import org.sakaiproject.user.api.User;
21+
import org.sakaiproject.user.cover.UserDirectoryService;
22+
23+
import java.io.OutputStream;
24+
import java.util.Map;
25+
import java.util.UUID;
26+
27+
@Setter @Slf4j
28+
public class ProfileImageEntityProvider extends AbstractEntityProvider implements EntityProvider, AutoRegisterEntityProvider, Outputable, Describeable, ActionsExecutable {
29+
30+
private ProfileImageLogic imageLogic;
31+
private ProfileImageService profileImageService;
32+
private SessionManager sessionManager;
33+
34+
@Override
35+
public String[] getHandledOutputFormats() {
36+
return new String[] { Formats.JSON };
37+
}
38+
39+
@Override
40+
public String getEntityPrefix() {
41+
return "profile-image";
42+
}
43+
44+
@EntityCustomAction(action = "upload", viewKey = EntityView.VIEW_NEW)
45+
public String upload(EntityView view, Map<String, Object> params) {
46+
47+
JSONObject result = new JSONObject();
48+
49+
result.put("status", "ERROR");
50+
51+
if (!checkCSRFToken(params)) {
52+
return result.toJSONString();
53+
}
54+
55+
User currentUser = UserDirectoryService.getCurrentUser();
56+
String currentUserId = currentUser.getId();
57+
58+
if (currentUserId == null) {
59+
log.warn("Access denied");
60+
return result.toJSONString();
61+
}
62+
63+
String mimeType = "image/png";
64+
String fileName = UUID.randomUUID().toString();
65+
String base64 = (String) params.get("base64");
66+
byte[] imageBytes = Base64.decodeBase64(base64.getBytes());
67+
68+
if (imageLogic.setUploadedProfileImage(currentUserId, imageBytes, mimeType, fileName)) {
69+
profileImageService.resetCachedProfileImageId(currentUserId);
70+
result.put("status", "SUCCESS");
71+
}
72+
73+
return result.toJSONString();
74+
}
75+
76+
@EntityCustomAction(action = "details", viewKey = EntityView.VIEW_LIST)
77+
public Object getProfileImage(OutputStream out, EntityView view, Map<String,Object> params) {
78+
79+
JSONObject result = new JSONObject();
80+
81+
result.put("status", "ERROR");
82+
83+
User currentUser = UserDirectoryService.getCurrentUser();
84+
String currentUserId = currentUser.getId();
85+
86+
if (currentUserId == null) {
87+
log.warn("Access denied");
88+
return result.toJSONString();
89+
}
90+
91+
String imageUrl = imageLogic.getProfileImageEntityUrl(currentUserId, ProfileConstants.PROFILE_IMAGE_MAIN);
92+
93+
result.put("url", imageUrl);
94+
result.put("isDefault", imageLogic.profileImageIsDefault(currentUserId));
95+
result.put("csrf_token", sessionManager.getCurrentSession().getAttribute("sakai.csrf.token"));
96+
result.put("status", "SUCCESS");
97+
98+
return result.toJSONString();
99+
}
100+
101+
@EntityCustomAction(action = "remove", viewKey = EntityView.VIEW_NEW)
102+
public String remove(EntityView view, Map<String, Object> params) {
103+
104+
JSONObject result = new JSONObject();
105+
106+
result.put("status", "ERROR");
107+
108+
if (!checkCSRFToken(params)) {
109+
return result.toJSONString();
110+
}
111+
112+
User currentUser = UserDirectoryService.getCurrentUser();
113+
String currentUserId = currentUser.getId();
114+
115+
if (currentUserId == null) {
116+
log.warn("Access denied");
117+
return result.toJSONString();
118+
}
119+
120+
if (imageLogic.resetProfileImage(currentUserId)) {
121+
profileImageService.resetCachedProfileImageId(currentUserId);
122+
result.put("status", "SUCCESS");
123+
}
124+
125+
return result.toJSONString();
126+
}
127+
128+
private boolean checkCSRFToken(Map<String, Object> params) {
129+
130+
Object sessionToken = sessionManager.getCurrentSession().getAttribute("sakai.csrf.token");
131+
132+
if (sessionToken == null || !sessionToken.equals(params.get("sakai_csrf_token"))) {
133+
log.warn("CSRF token validation failed");
134+
return false;
135+
}
136+
137+
return true;
138+
}
139+
}

profile2/tool/src/webapp/WEB-INF/applicationContext.xml

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
<property name="authzGroupService" ref="org.sakaiproject.authz.api.AuthzGroupService" />
4343
<property name="profileLogic" ref="org.sakaiproject.profile2.logic.ProfileLogic" />
4444
</bean>
45+
46+
<bean parent="org.sakaiproject.entitybroker.entityprovider.AbstractEntityProvider"
47+
class="org.sakaiproject.profile2.tool.entityprovider.ProfileImageEntityProvider">
48+
<property name="imageLogic" ref="org.sakaiproject.profile2.logic.ProfileImageLogic" />
49+
<property name="profileImageService" ref="org.sakaiproject.profile2.service.ProfileImageService" />
50+
<property name="sessionManager" ref="org.sakaiproject.tool.api.SessionManager" />
51+
</bean>
4552

4653
<!-- ENTITY PROVIDERS END -->
4754

reference/library/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@
155155
<version>${font-awesome-version}</version>
156156
<scope>runtime</scope>
157157
</dependency>
158+
<dependency>
159+
<groupId>org.webjars.bower</groupId>
160+
<artifactId>cropper</artifactId>
161+
<version>2.3.2</version>
162+
<scope>runtime</scope>
163+
</dependency>
158164
</dependencies>
159165
<profiles>
160166
<profile>

0 commit comments

Comments
 (0)