Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/profile.cgi
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/profile.cgi

added on local at 2026-07-01 12:34:35

Added
+298
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 347c6f036ec9
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2
3##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26$CGI::POST_MAX = 4 * 1024 * 1024;
27use CGI; my $query = new CGI; my $form = $query->Vars;
28use MODS::Login; my $login = new MODS::Login;
29use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls();
30use MODS::Template; my $tfile = new MODS::Template;
31use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper;
32use MODS::DBConnect; my $db = new MODS::DBConnect;
33use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name');
34use MODS::PTMatrix::PM;
35use Digest::SHA qw(sha256_hex);
36use File::Path qw(make_path);
37
38
39
40#############################################################
41## Get form data
42#############################################################
43my %rawform = %$form;
44
45foreach my $line(keys %$form){
46 $form->{$line} =~ s/[^!-~\s]//g;
47 $form->{$line} = quotemeta("$form->{$line}");
48}
49
50
51
52############################################################
53# Program Controls
54############################################################
55$|=1;
56my $userinfo = $login->login_verify();
57print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
58if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;}
59elsif(($ENV{REQUEST_METHOD}||'') eq 'POST'){&profile_save;}
60else{&profile_view;}
61
62
63############################################################
64# Subroutines
65############################################################
66#Load the target user row (self by default, ?id=N for a teammate)
67sub load_target{
68 my($dbh) = @_;
69 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
70 my $cid = MODS::PTMatrix::PM::get_company_id($userinfo);
71 my $target_id = $uid;
72 if($form->{id}){
73 my $req = $form->{id}; $req =~ s/[^0-9]//g;
74 $target_id = $req if length $req;
75 }
76 my $row = $db->db_readwrite($dbh, qq~SELECT * FROM `${database_name}`.users WHERE id='$target_id' AND company_id='$cid' LIMIT 1~, $ENV{SCRIPT_NAME}, __LINE__);
77 return ($row, $target_id);
78}
79
80#Compute the actor's permission flags for this target
81sub perm_flags{
82 my($dbh, $target_id, $t_row) = @_;
83
84 my $is_super = $userinfo->{is_super_admin} ? 1 : 0;
85 my $is_company_admin = ($userinfo->{role} && ($userinfo->{role} eq 'company_admin' || $userinfo->{role} eq 'company_owner')) ? 1 : 0;
86
87 #Detect optional HR-admin columns - pre-migration installs treat them as 0
88 my $col_check = $db->db_readwrite($dbh, qq~SELECT COUNT(IF(COLUMN_NAME='is_hr_admin',1,NULL)) AS has_hr, COUNT(IF(COLUMN_NAME='profile_locked',1,NULL)) AS has_lock FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${database_name}' AND TABLE_NAME='users'~, $ENV{SCRIPT_NAME}, __LINE__);
89 my $has_hr_cols = ($col_check && $col_check->{has_hr}) ? 1 : 0;
90 my $has_lock = ($col_check && $col_check->{has_lock}) ? 1 : 0;
91
92 my $is_hr_admin = ($has_hr_cols && $userinfo->{is_hr_admin}) ? 1 : 0;
93 my $target_locked = ($has_lock && $t_row->{profile_locked}) ? 1 : 0;
94 my $target_hr = ($has_hr_cols && $t_row->{is_hr_admin}) ? 1 : 0;
95
96 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
97 my $is_self = ($target_id == $uid) ? 1 : 0;
98
99 my $can_edit_target = $is_super || $is_company_admin || $is_hr_admin || ($is_self && !$target_locked);
100 my $can_override_lock = $is_super || $is_hr_admin;
101 my $can_toggle_hr = $is_super;
102
103 return {
104 is_super => $is_super,
105 is_company_admin => $is_company_admin,
106 is_hr_admin => $is_hr_admin,
107 has_hr_cols => $has_hr_cols,
108 has_lock => $has_lock,
109 target_locked => $target_locked,
110 target_hr => $target_hr,
111 is_self => $is_self,
112 can_edit_target => $can_edit_target,
113 can_override_lock=> $can_override_lock,
114 can_toggle_hr => $can_toggle_hr,
115 };
116}
117
118sub profile_save{
119 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
120 my $cid = MODS::PTMatrix::PM::get_company_id($userinfo);
121
122 my $dbh = $db->db_connect();
123 my ($t_row, $target_id) = &load_target($dbh);
124 if(!$t_row || !$t_row->{id}){
125 $db->db_disconnect($dbh);
126 print qq~<script>window.location="$url->{profile}";</script>~;
127 return;
128 }
129 my $p = &perm_flags($dbh, $target_id, $t_row);
130
131 my $action = $form->{act} || 'update';
132 my $flash = '';
133
134 if($action eq 'lock_profile' && $p->{can_override_lock} && $p->{has_lock}){
135 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.users SET profile_locked=1, profile_locked_at=NOW(), profile_locked_by_user_id='$uid' WHERE id='$target_id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__);
136 $flash = 'Profile locked. The user can no longer edit their own profile.';
137 }elsif($action eq 'unlock_profile' && $p->{can_override_lock} && $p->{has_lock}){
138 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.users SET profile_locked=0, profile_locked_at=NULL, profile_locked_by_user_id=NULL WHERE id='$target_id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__);
139 $flash = 'Profile unlocked. The user can edit their own profile again.';
140 }elsif($action eq 'toggle_hr_admin' && $p->{can_toggle_hr} && $p->{has_hr_cols}){
141 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.users SET is_hr_admin = 1 - IFNULL(is_hr_admin,0) WHERE id='$target_id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__);
142 $flash = 'HR-admin role toggled.';
143 }elsif($action eq 'change_password' && $p->{is_self}){
144 my $cur = $rawform{current_password} || '';
145 my $new = $rawform{new_password} || '';
146 if(length($new) < 8){
147 $flash = 'New password must be at least 8 characters.';
148 }else{
149 my ($info) = $login->login_exec($userinfo->{email}, $cur);
150 if($info){
151 $login->update_password($uid, $new);
152 $flash = 'Password updated. You will be signed out of other devices.';
153 }else{
154 $flash = 'Current password is incorrect.';
155 }
156 }
157 }elsif($action eq 'update' && $p->{can_edit_target}){
158 my @sets;
159 push @sets, "display_name='" . MODS::PTMatrix::PM::sql_quote($rawform{display_name}) . "'" if defined $rawform{display_name};
160 push @sets, "job_title='" . MODS::PTMatrix::PM::sql_quote($rawform{job_title}) . "'" if defined $rawform{job_title};
161 push @sets, "avatar_color='" . MODS::PTMatrix::PM::safe_color($form->{avatar_color}) . "'" if defined $form->{avatar_color};
162 if(defined $form->{timezone}){
163 my $tz = $form->{timezone}; $tz =~ s/[^A-Za-z0-9_+\-:\/]//g;
164 push @sets, "timezone='$tz'";
165 }
166
167 #Org-chart columns - only touched if migration ran
168 my $has_orgcols = $db->db_readwrite($dbh, qq~SELECT COUNT(*) AS n FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${database_name}' AND TABLE_NAME='users' AND COLUMN_NAME IN ('phone','bio_short','skills_csv','manager_user_id','photo_url')~, $ENV{SCRIPT_NAME}, __LINE__);
169 if($has_orgcols && $has_orgcols->{n}){
170 push @sets, "phone='" . MODS::PTMatrix::PM::sql_quote(substr($rawform{phone} || '', 0, 40)) . "'" if defined $rawform{phone};
171 push @sets, "bio_short='" . MODS::PTMatrix::PM::sql_quote(substr($rawform{bio_short} || '', 0, 255)) . "'" if defined $rawform{bio_short};
172 if(defined $rawform{skills_csv}){
173 my $sk = $rawform{skills_csv} || '';
174 $sk =~ s/^\s+|\s+$//g;
175 $sk = substr($sk, 0, 500);
176 push @sets, "skills_csv='" . MODS::PTMatrix::PM::sql_quote($sk) . "'";
177 }
178 if(defined $form->{manager_user_id} && ($p->{is_super} || $p->{is_company_admin} || $p->{is_hr_admin} || $p->{is_self})){
179 my $mid = $form->{manager_user_id};
180 $mid =~ s/[^0-9]//g;
181 $mid = '' if $mid eq "$target_id"; #Can't manage yourself
182 push @sets, "manager_user_id=" . (length($mid) ? "'$mid'" : 'NULL');
183 }
184
185 #Optional avatar upload
186 my $fh = $query->upload('photo_file');
187 if($fh){
188 my $info = $query->uploadInfo($query->param('photo_file')) || {};
189 my $mime = $info->{'Content-Type'} || '';
190 my %allowed = ('image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif');
191 if($allowed{$mime}){
192 my $root = '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/uploads/avatars';
193 make_path($root) unless -d $root;
194 binmode $fh;
195 my $blob = do { local $/; <$fh> };
196 if(length($blob) > 0 && length($blob) <= 4 * 1024 * 1024){
197 my $hash = substr(sha256_hex($blob), 0, 12);
198 my $ext = $allowed{$mime};
199 my $fn = "${target_id}-${hash}.${ext}";
200 my $abs = "$root/$fn";
201 if(open my $out, '>', $abs){
202 binmode $out;
203 print $out $blob;
204 close $out;
205 push @sets, "photo_url='" . MODS::PTMatrix::PM::sql_quote("/uploads/avatars/$fn") . "'";
206 }else{
207 $flash = "Could not save photo: $!";
208 }
209 }
210 }else{
211 $flash = 'Photo must be JPG, PNG, WebP, or GIF.';
212 }
213 }
214 push @sets, "photo_url=NULL" if $form->{remove_photo} && $form->{remove_photo} eq '1';
215 }
216
217 if(@sets){
218 my $set = join(', ', @sets);
219 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.users SET $set WHERE id='$target_id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__);
220 }
221 $flash ||= 'Profile saved.';
222 }
223
224 $db->db_disconnect($dbh);
225 &profile_view($flash);
226}
227
228sub profile_view{
229 my($flash) = @_;
230 $flash ||= '';
231
232 my $cid = MODS::PTMatrix::PM::get_company_id($userinfo);
233
234 my $dbh = $db->db_connect();
235 my ($t_row, $target_id) = &load_target($dbh);
236 if(!$t_row || !$t_row->{id}){
237 $db->db_disconnect($dbh);
238 print qq~Status: 404 Not Found\nContent-Type: text/plain\n\nUser not found in this company.\n~;
239 exit;
240 }
241
242 my $p = &perm_flags($dbh, $target_id, $t_row);
243
244 #Manager dropdown - all OTHER active teammates
245 my @mgr_choices = $db->db_readwrite_multiple($dbh, qq~SELECT id, display_name, email FROM `${database_name}`.users WHERE company_id='$cid' AND account_status='active' AND id<>'$target_id' ORDER BY display_name~, $ENV{SCRIPT_NAME}, __LINE__);
246 my @manager_opts;
247 push @manager_opts, { id => '', name => '&mdash; no manager &mdash;', selected => (!$t_row->{manager_user_id}) ? 1 : 0 };
248 foreach my $m(@mgr_choices){
249 push @manager_opts, {
250 id => $m->{id},
251 name => MODS::PTMatrix::PM::h($m->{display_name} || $m->{email}),
252 selected => (($t_row->{manager_user_id} || '') eq $m->{id}) ? 1 : 0,
253 };
254 }
255
256 $db->db_disconnect($dbh);
257
258 #Create all template variables
259 my $tvars;
260 $tvars->{flash} = $flash;
261 $tvars->{has_flash} = length($flash) ? 1 : 0;
262 $tvars->{target_id} = $target_id;
263 $tvars->{is_self} = $p->{is_self};
264 $tvars->{is_super} = $p->{is_super};
265 $tvars->{is_company_admin} = $p->{is_company_admin};
266 $tvars->{is_hr_admin} = $p->{is_hr_admin};
267 $tvars->{target_is_hr} = $p->{target_hr};
268 $tvars->{target_locked} = $p->{target_locked};
269 $tvars->{can_edit} = $p->{can_edit_target} ? 1 : 0;
270 $tvars->{can_lock} = $p->{can_override_lock} ? 1 : 0;
271 $tvars->{can_toggle_hr} = $p->{can_toggle_hr} ? 1 : 0;
272 $tvars->{has_hr_cols} = $p->{has_hr_cols};
273 $tvars->{has_lock} = $p->{has_lock};
274
275 $tvars->{display_name} = MODS::PTMatrix::PM::h($t_row->{display_name});
276 $tvars->{job_title} = MODS::PTMatrix::PM::h($t_row->{job_title} || '');
277 $tvars->{email} = MODS::PTMatrix::PM::h($t_row->{email});
278 $tvars->{avatar_color} = $t_row->{avatar_color} || '#5aa9ff';
279 $tvars->{timezone} = $t_row->{timezone} || 'UTC';
280 $tvars->{initials} = MODS::PTMatrix::PM::initials($t_row->{display_name});
281 $tvars->{company_name} = MODS::PTMatrix::PM::h($userinfo->{company_name} || '');
282 $tvars->{role_label} = ucfirst($t_row->{role} || 'member');
283 $tvars->{photo_url} = MODS::PTMatrix::PM::h($t_row->{photo_url} || '');
284 $tvars->{has_photo} = $t_row->{photo_url} ? 1 : 0;
285 $tvars->{phone} = MODS::PTMatrix::PM::h($t_row->{phone} || '');
286 $tvars->{bio_short} = MODS::PTMatrix::PM::h($t_row->{bio_short} || '');
287 $tvars->{skills_csv} = MODS::PTMatrix::PM::h($t_row->{skills_csv} || '');
288 $tvars->{manager_opts} = \@manager_opts;
289
290 my $body = join('', $tfile->template('tf_profile.html', $tvars, $userinfo));
291
292 $load->render({
293 userinfo => $userinfo,
294 page_key => 'profile',
295 title => $p->{is_self} ? 'My Profile' : "Profile: $tvars->{display_name}",
296 body => $body,
297 });
298}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help