Fossil SCM

fossil-scm / src / setupuser.c
Blame History Raw 1189 lines
1
/*
2
** Copyright (c) 2007 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** Setup pages associated with user management. The code in this
19
** file was formerly part of the "setup.c" module, but has been broken
20
** out into its own module to improve maintainability.
21
**
22
** Note: Do not confuse "Users" with "Subscribers". Code to deal with
23
** subscribers is over in the "alerts.c" source file.
24
*/
25
#include "config.h"
26
#include <assert.h>
27
#include "setupuser.h"
28
29
/*
30
** WEBPAGE: setup_ulist
31
**
32
** Show a list of users. Clicking on any user jumps to the edit
33
** screen for that user. Requires Admin privileges.
34
**
35
** Query parameters:
36
**
37
** with=CAP Only show users that have one or more capabilities in CAP.
38
** ubg Color backgrounds by username hash
39
*/
40
void setup_ulist(void){
41
Stmt s;
42
double rNow;
43
const char *zWith = P("with");
44
int bUnusedOnly = P("unused")!=0;
45
int bUbg = P("ubg")!=0;
46
int bHaveAlerts;
47
48
login_check_credentials();
49
if( !g.perm.Admin ){
50
login_needed(0);
51
return;
52
}
53
bHaveAlerts = alert_tables_exist();
54
style_submenu_element("Add", "setup_uedit");
55
style_submenu_element("Log", "access_log");
56
style_submenu_element("Help", "setup_ulist_notes");
57
if( bHaveAlerts ){
58
style_submenu_element("Subscribers", "subscribers");
59
}
60
style_set_current_feature("setup");
61
style_header("User List");
62
if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){
63
@ <table border=1 cellpadding=2 cellspacing=0 class='userTable'>
64
@ <thead><tr>
65
@ <th>Category
66
@ <th>Capabilities (<a href='%R/setup_ucap_list'>key</a>)
67
@ <th>Info <th>Last Change</tr></thead>
68
@ <tbody>
69
db_prepare(&s,
70
"SELECT uid, login, cap, date(mtime,'unixepoch')"
71
" FROM user"
72
" WHERE login IN ('anonymous','nobody','developer','reader')"
73
" ORDER BY login"
74
);
75
while( db_step(&s)==SQLITE_ROW ){
76
int uid = db_column_int(&s, 0);
77
const char *zLogin = db_column_text(&s, 1);
78
const char *zCap = db_column_text(&s, 2);
79
const char *zDate = db_column_text(&s, 4);
80
@ <tr>
81
@ <td><a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
82
@ <td>%h(zCap)
83
84
if( fossil_strcmp(zLogin,"anonymous")==0 ){
85
@ <td>All logged-in users
86
}else if( fossil_strcmp(zLogin,"developer")==0 ){
87
@ <td>Users with '<b>v</b>' capability
88
}else if( fossil_strcmp(zLogin,"nobody")==0 ){
89
@ <td>All users without login
90
}else if( fossil_strcmp(zLogin,"reader")==0 ){
91
@ <td>Users with '<b>u</b>' capability
92
}else{
93
@ <td>
94
}
95
if( zDate && zDate[0] ){
96
@ <td>%h(zDate)
97
}else{
98
@ <td>
99
}
100
@ </tr>
101
}
102
db_finalize(&s);
103
@ </tbody></table>
104
@ <div class='section'>Users</div>
105
}else{
106
style_submenu_element("All Users", "setup_ulist");
107
if( bUnusedOnly ){
108
@ <div class='section'>Unused logins</div>
109
}else if( zWith ){
110
if( zWith[1]==0 ){
111
@ <div class='section'>Users with capability "%h(zWith)"</div>
112
}else{
113
@ <div class='section'>Users with any capability in "%h(zWith)"</div>
114
}
115
}
116
}
117
if( !bUnusedOnly ){
118
style_submenu_element("Unused", "setup_ulist?unused");
119
}
120
@ <table border=1 cellpadding=2 cellspacing=0 class='userTable sortable' \
121
@ data-column-types='ktxKTKt' data-init-sort='4'>
122
@ <thead><tr>
123
@ <th>Login Name<th>Caps<th>Info<th>Date<th>Expire<th>Last Login\
124
@ <th>Alerts</tr></thead>
125
@ <tbody>
126
db_multi_exec(
127
"CREATE TEMP TABLE lastAccess(uname TEXT PRIMARY KEY, atime REAL)"
128
"WITHOUT ROWID;"
129
);
130
if( db_table_exists("repository","accesslog") ){
131
db_multi_exec(
132
"INSERT INTO lastAccess(uname, atime)"
133
" SELECT uname, max(mtime) FROM ("
134
" SELECT uname, mtime FROM accesslog WHERE success"
135
" UNION ALL"
136
" SELECT login AS uname, rcvfrom.mtime AS mtime"
137
" FROM rcvfrom JOIN user USING(uid))"
138
" GROUP BY 1;"
139
);
140
}
141
if( !db_table_exists("repository","subscriber") ){
142
db_multi_exec(
143
"CREATE TEMP TABLE subscriber(suname PRIMARY KEY, ssub, subscriberId)"
144
"WITHOUT ROWID;"
145
);
146
}
147
if( bUnusedOnly ){
148
zWith = mprintf(
149
" AND login NOT IN ("
150
"SELECT user FROM event WHERE user NOT NULL "
151
"UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)"
152
" AND uid NOT IN (SELECT uid FROM rcvfrom)",
153
bHaveAlerts ?
154
" UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":"");
155
}else if( zWith && zWith[0] ){
156
zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
157
}else{
158
zWith = "";
159
}
160
db_prepare(&s,
161
/*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
162
/* 5 */"lower(login) AS sortkey, "
163
/* 6 */"CASE WHEN info LIKE '%%expires 20%%'"
164
" THEN substr(info,instr(lower(info),'expires')+8,10)"
165
" END AS exp,"
166
/* 7 */"atime,"
167
/* 8 */"user.mtime AS sorttime,"
168
/*9-11*/"%s"
169
" FROM user LEFT JOIN lastAccess ON login=uname"
170
" LEFT JOIN subscriber ON login=suname"
171
" WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
172
" ORDER BY sorttime DESC",
173
bHaveAlerts
174
? "subscriber.ssub, subscriber.subscriberId, subscriber.semail"
175
: "null, null, null",
176
zWith/*safe-for-%s*/
177
);
178
rNow = db_double(0.0, "SELECT julianday('now');");
179
while( db_step(&s)==SQLITE_ROW ){
180
int uid = db_column_int(&s, 0);
181
const char *zLogin = db_column_text(&s, 1);
182
const char *zCap = db_column_text(&s, 2);
183
const char *zInfo = db_column_text(&s, 3);
184
const char *zDate = db_column_text(&s, 4);
185
const char *zSortKey = db_column_text(&s,5);
186
const char *zExp = db_column_text(&s,6);
187
double rATime = db_column_double(&s,7);
188
char *zAge = 0;
189
const char *zSub;
190
int sid = db_column_int(&s,10);
191
sqlite3_int64 sorttime = db_column_int64(&s, 8);
192
if( rATime>0.0 ){
193
zAge = human_readable_age(rNow - rATime);
194
}
195
if( bUbg ){
196
@ <tr style='background-color: %h(user_color(zLogin));'>
197
}else{
198
@ <tr>
199
}
200
@ <td data-sortkey='%h(zSortKey)'>\
201
@ <a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
202
@ <td>%h(zCap)
203
@ <td>%h(zInfo)
204
@ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"")
205
@ <td>%h(zExp?zExp:"")
206
@ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
207
if( db_column_type(&s,9)==SQLITE_NULL ){
208
@ <td>
209
}else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){
210
@ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
211
}else{
212
const char *zEmail = db_column_text(&s, 11);
213
char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
214
@ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
215
}
216
217
@ </tr>
218
fossil_free(zAge);
219
}
220
@ </tbody></table>
221
db_finalize(&s);
222
style_table_sorter();
223
style_finish_page();
224
}
225
226
/*
227
** WEBPAGE: setup_ulist_notes
228
**
229
** A documentation page showing notes about user configuration. This
230
** information used to be a side-bar on the user list page, but has been
231
** factored out for improved presentation.
232
*/
233
void setup_ulist_notes(void){
234
style_set_current_feature("setup");
235
style_header("User Configuration Notes");
236
@ <h1>User Configuration Notes:</h1>
237
@ <ol>
238
@ <li><p>
239
@ Every user, logged in or not, inherits the privileges of
240
@ <span class="usertype">nobody</span>.
241
@ </p></li>
242
@
243
@ <li><p>
244
@ Any human can login as <span class="usertype">anonymous</span> since the
245
@ password is clearly displayed on the login page for them to type. The
246
@ purpose of requiring anonymous to log in is to prevent access by spiders.
247
@ Every logged-in user inherits the combined privileges of
248
@ <span class="usertype">anonymous</span> and
249
@ <span class="usertype">nobody</span>.
250
@ </p></li>
251
@
252
@ <li><p>
253
@ Users with privilege <span class="capability">u</span> inherit the combined
254
@ privileges of <span class="usertype">reader</span>,
255
@ <span class="usertype">anonymous</span>, and
256
@ <span class="usertype">nobody</span>.
257
@ </p></li>
258
@
259
@ <li><p>
260
@ Users with privilege <span class="capability">v</span> inherit the combined
261
@ privileges of <span class="usertype">developer</span>,
262
@ <span class="usertype">anonymous</span>, and
263
@ <span class="usertype">nobody</span>.
264
@ </p></li>
265
@
266
@ <li><p>The permission flags are as follows:</p>
267
capabilities_table(CAPCLASS_ALL);
268
@ </li>
269
@ </ol>
270
style_finish_page();
271
}
272
273
/*
274
** WEBPAGE: setup_ucap_list
275
**
276
** A documentation page showing the meaning of the various user capabilities
277
** code letters.
278
*/
279
void setup_ucap_list(void){
280
style_set_current_feature("setup");
281
style_header("User Capability Codes");
282
@ <h1>All capabilities</h1>
283
capabilities_table(CAPCLASS_ALL);
284
@ <h1>Capabilities associated with checked-in content</h1>
285
capabilities_table(CAPCLASS_CODE);
286
@ <h1>Capabilities associated with data transfer and sync</h1>
287
capabilities_table(CAPCLASS_DATA);
288
@ <h1>Capabilities associated with the forum</h1>
289
capabilities_table(CAPCLASS_FORUM);
290
@ <h1>Capabilities associated with tickets</h1>
291
capabilities_table(CAPCLASS_TKT);
292
@ <h1>Capabilities associated with wiki</h1>
293
capabilities_table(CAPCLASS_WIKI);
294
@ <h1>Administrative capabilities</h1>
295
capabilities_table(CAPCLASS_SUPER);
296
@ <h1>Miscellaneous capabilities</h1>
297
capabilities_table(CAPCLASS_OTHER);
298
style_finish_page();
299
}
300
301
/*
302
** Return true if zPw is a valid password string. A valid
303
** password string is:
304
**
305
** (1) A zero-length string, or
306
** (2) a string that contains a character other than '*'.
307
*/
308
static int isValidPwString(const char *zPw){
309
if( zPw==0 ) return 0;
310
if( zPw[0]==0 ) return 1;
311
while( zPw[0]=='*' ){ zPw++; }
312
return zPw[0]!=0;
313
}
314
315
/*
316
** Return true if user capability strings zOrig and zNew materially
317
** differ, taking into account that they may be sorted in an arbitary
318
** order. This does not take inherited permissions into
319
** account. Either argument may be NULL. A NULL and an empty string
320
** are considered equivalent here. e.g. "abc" and "cab" are equivalent
321
** for this purpose, but "aCb" and "acb" are not.
322
*/
323
static int userCapsChanged(const char *zOrig, const char *zNew){
324
if( !zOrig ){
325
return zNew ? (0!=*zNew) : 0;
326
}else if( !zNew ){
327
return 0!=*zOrig;
328
}else if( 0==fossil_strcmp(zOrig, zNew) ){
329
return 0;
330
}else{
331
/* We don't know that zOrig and zNew are sorted equivalently. The
332
** following steps will compare strings which contain all the same
333
** capabilities letters as equivalent, regardless of the letters'
334
** order in their strings. */
335
char aOrig[128]; /* table of zOrig bytes */
336
int nOrig = 0, nNew = 0;
337
338
memset( &aOrig[0], 0, sizeof(aOrig) );
339
for( ; *zOrig; ++zOrig, ++nOrig ){
340
if( 0==(*zOrig & 0x80) ){
341
aOrig[(int)*zOrig] = 1;
342
}
343
}
344
for( ; *zNew; ++zNew, ++nNew ){
345
if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
346
return 1;
347
}
348
}
349
return nOrig!=nNew;
350
}
351
}
352
353
/*
354
** COMMAND: test-user-caps-changed
355
**
356
** Usage: %fossil test-user-caps-changed caps1 caps2
357
**
358
*/
359
void test_user_caps_changed(void){
360
361
char const * zOld = g.argc>2 ? g.argv[2] : NULL;
362
char const * zNew = g.argc>3 ? g.argv[3] : NULL;
363
fossil_print("Has changes? = %d\n",
364
userCapsChanged( zOld, zNew ));
365
}
366
367
/*
368
** Sends notification of user permission elevation changes to all
369
** subscribers with a "u" subscription. This is a no-op if alerts are
370
** not enabled.
371
**
372
** These subscriptions differ from most, in that:
373
**
374
** - They currently lack an "unsubscribe" link.
375
**
376
** - Only an admin can assign this subscription, but if a non-admin
377
** edits their subscriptions after an admin assigns them this one,
378
** this particular one will be lost. "Feature or bug?" is unclear,
379
** but it would be odd for a non-admin to be assigned this
380
** capability.
381
*/
382
static void alert_user_cap_change(const char *zLogin, /*Affected user*/
383
int uid, /*[user].uid*/
384
int bIsNew, /*true if new user*/
385
const char *zOrigCaps,/*Old caps*/
386
const char *zNewCaps /*New caps*/){
387
Blob hdr, body;
388
Stmt q;
389
int nBody;
390
AlertSender *pSender;
391
char *zSubname;
392
char *zURL;
393
char * zSubject;
394
395
if( !alert_enabled() ) return;
396
zSubject = bIsNew
397
? mprintf("New user created: [%q]", zLogin)
398
: mprintf("User [%q] capabilities changed", zLogin);
399
zURL = db_get("email-url",0);
400
zSubname = db_get("email-subname", "[Fossil Repo]");
401
blob_init(&body, 0, 0);
402
blob_init(&hdr, 0, 0);
403
if( bIsNew ){
404
blob_appendf(&body, "User [%q] was created with "
405
"permissions [%q] by user [%q].\n",
406
zLogin, zNewCaps, g.zLogin);
407
} else {
408
blob_appendf(&body, "Permissions for user [%q] were changed "
409
"from [%q] to [%q] by user [%q].\n",
410
zLogin, zOrigCaps, zNewCaps, g.zLogin);
411
}
412
if( zURL ){
413
blob_appendf(&body, "\nUser editor: %s/setup_uedit?id=%d\n", zURL, uid);
414
}
415
nBody = blob_size(&body);
416
pSender = alert_sender_new(0, 0);
417
db_prepare(&q,
418
"SELECT semail, hex(subscriberCode)"
419
" FROM subscriber, user "
420
" WHERE sverified AND NOT sdonotcall"
421
" AND suname=login"
422
" AND ssub GLOB '*u*'");
423
while( !pSender->zErr && db_step(&q)==SQLITE_ROW ){
424
const char *zTo = db_column_text(&q, 0);
425
blob_truncate(&hdr, 0);
426
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n",
427
zTo, zSubname, zSubject);
428
if( zURL ){
429
const char *zCode = db_column_text(&q, 1);
430
blob_truncate(&body, nBody);
431
blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
432
zURL, zCode);
433
}
434
alert_send(pSender, &hdr, &body, 0);
435
}
436
db_finalize(&q);
437
alert_sender_free(pSender);
438
fossil_free(zURL);
439
fossil_free(zSubname);
440
fossil_free(zSubject);
441
}
442
443
/*
444
** WEBPAGE: setup_uedit
445
**
446
** Edit information about a user or create a new user.
447
** Requires Admin privileges.
448
*/
449
void user_edit(void){
450
const char *zId, *zLogin, *zInfo, *zCap, *zPw;
451
const char *zGroup;
452
const char *zOldLogin;
453
int uid, i;
454
char *zOldCaps = 0; /* Capabilities before edit */
455
char *zDeleteVerify = 0; /* Delete user verification text */
456
int higherUser = 0; /* True if user being edited is SETUP and the */
457
/* user doing the editing is ADMIN. Disallow editing */
458
const char *inherit[128];
459
int a[128];
460
const char *oa[128];
461
462
/* Must have ADMIN privileges to access this page
463
*/
464
login_check_credentials();
465
if( !g.perm.Admin ){ login_needed(0); return; }
466
467
/* Check to see if an ADMIN user is trying to edit a SETUP account.
468
** Don't allow that.
469
*/
470
zId = PD("id", "0");
471
uid = atoi(zId);
472
if( uid>0 ){
473
zOldCaps = db_text("", "SELECT cap FROM user WHERE uid=%d",uid);
474
if( zId && !g.perm.Setup ){
475
higherUser = zOldCaps && strchr(zOldCaps,'s');
476
}
477
}
478
479
if( P("can") ){
480
/* User pressed the cancel button */
481
cgi_redirect(cgi_referer("setup_ulist"));
482
return;
483
}
484
485
/* Check for requests to delete the user */
486
if( P("delete") && cgi_csrf_safe(2) ){
487
int n;
488
if( P("verifydelete") ){
489
/* Verified delete user request */
490
db_unprotect(PROTECT_USER);
491
if( alert_tables_exist() ){
492
/* Also delete any subscriptions associated with this user */
493
db_multi_exec("DELETE FROM subscriber WHERE suname="
494
"(SELECT login FROM user WHERE uid=%d)", uid);
495
}
496
db_multi_exec("DELETE FROM user WHERE uid=%d", uid);
497
db_protect_pop();
498
moderation_disapprove_for_missing_users();
499
admin_log("Deleted user [%s] (uid %d).",
500
PD("login","???")/*safe-for-%s*/, uid);
501
cgi_redirect(cgi_referer("setup_ulist"));
502
return;
503
}
504
n = db_int(0, "SELECT count(*) FROM event"
505
" WHERE user=%Q AND objid NOT IN private",
506
P("login"));
507
if( n==0 ){
508
zDeleteVerify = mprintf("Check this box and press \"Delete User\" again");
509
}else{
510
zDeleteVerify = mprintf(
511
"User \"%s\" has %d or more artifacts in the block-chain. "
512
"Delete anyhow?",
513
P("login")/*safe-for-%s*/, n);
514
}
515
}
516
517
style_set_current_feature("setup");
518
519
/* If we have all the necessary information, write the new or
520
** modified user record. After writing the user record, redirect
521
** to the page that displays a list of users.
522
*/
523
if( !cgi_all("login","info","pw","apply") ){
524
/* need all of the above properties to make a change. Since one or
525
** more are missing, no-op */
526
}else if( higherUser ){
527
/* An Admin (a) user cannot edit a Superuser (s) */
528
}else if( zDeleteVerify!=0 ){
529
/* Need to verify a delete request */
530
}else if( !cgi_csrf_safe(2) ){
531
/* This might be a cross-site request forgery, so ignore it */
532
}else{
533
/* We have all the information we need to make the change to the user */
534
char c;
535
int bCapsChanged = 0 /* 1 if user's permissions changed */;
536
const int bIsNew = uid<=0;
537
char aCap[70], zNm[4];
538
zNm[0] = 'a';
539
zNm[2] = 0;
540
for(i=0, c='a'; c<='z'; c++){
541
zNm[1] = c;
542
a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0;
543
if( a[c&0x7f] ) aCap[i++] = c;
544
}
545
for(c='0'; c<='9'; c++){
546
zNm[1] = c;
547
a[c&0x7f] = P(zNm)!=0;
548
if( a[c&0x7f] ) aCap[i++] = c;
549
}
550
for(c='A'; c<='Z'; c++){
551
zNm[1] = c;
552
a[c&0x7f] = P(zNm)!=0;
553
if( a[c&0x7f] ) aCap[i++] = c;
554
}
555
556
aCap[i] = 0;
557
bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
558
zPw = P("pw");
559
zLogin = P("login");
560
if( strlen(zLogin)==0 ){
561
const char *zRef = cgi_referer("setup_ulist");
562
style_header("User Creation Error");
563
@ <span class="loginError">Empty login not allowed.</span>
564
@
565
@ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
566
@ [Bummer]</a></p>
567
style_finish_page();
568
return;
569
}
570
if( isValidPwString(zPw) ){
571
zPw = sha1_shared_secret(zPw, zLogin, 0);
572
}else{
573
zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
574
}
575
zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
576
if( db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d",zLogin,uid) ){
577
const char *zRef = cgi_referer("setup_ulist");
578
style_header("User Creation Error");
579
@ <span class="loginError">Login "%h(zLogin)" is already used by
580
@ a different user.</span>
581
@
582
@ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
583
@ [Bummer]</a></p>
584
style_finish_page();
585
return;
586
}
587
cgi_csrf_verify();
588
db_unprotect(PROTECT_USER);
589
uid = db_int(0,
590
"REPLACE INTO user(uid,login,info,pw,cap,mtime) "
591
"VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now()) "
592
"RETURNING uid",
593
uid, zLogin, P("info"), zPw, &aCap[0]);
594
assert( uid>0 );
595
if( zOldLogin && fossil_strcmp(zLogin, zOldLogin)!=0 ){
596
if( alert_tables_exist() ){
597
/* Rename matching subscriber entry, else the user cannot
598
re-subscribe with their same email address. */
599
db_multi_exec("UPDATE subscriber SET suname=%Q WHERE suname=%Q",
600
zLogin, zOldLogin);
601
}
602
admin_log( "Renamed user [%q] to [%q].", zOldLogin, zLogin );
603
}
604
db_protect_pop();
605
setup_incr_cfgcnt();
606
admin_log( "%s user [%q] with capabilities [%q].",
607
bIsNew ? "Added" : "Updated",
608
zLogin, &aCap[0] );
609
if( atoi(PD("all","0"))>0 ){
610
Blob sql;
611
char *zErr = 0;
612
blob_zero(&sql);
613
if( zOldLogin==0 ){
614
blob_appendf(&sql,
615
"INSERT INTO user(login)"
616
" SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);",
617
zLogin, zLogin
618
);
619
zOldLogin = zLogin;
620
}
621
#if 0
622
/* Problem: when renaming a user we need to update the subscriber
623
** names to match but we cannot know from here if each member of
624
** the login group has the subscriber tables, so we cannot blindly
625
** include this SQL. */
626
else if( fossil_strcmp(zLogin, zOldLogin)!=0
627
&& alert_tables_exist() ){
628
/* Rename matching subscriber entry, else the user cannot
629
re-subscribe with their same email address. */
630
blob_appendf(&sql,
631
"UPDATE subscriber SET suname=%Q WHERE suname=%Q;",
632
zLogin, zOldLogin);
633
}
634
#endif
635
blob_appendf(&sql,
636
"UPDATE user SET login=%Q,"
637
" pw=coalesce(shared_secret(%Q,%Q,"
638
"(SELECT value FROM config WHERE name='project-code')),pw),"
639
" info=%Q,"
640
" cap=%Q,"
641
" mtime=now()"
642
" WHERE login=%Q;",
643
zLogin, P("pw"), zLogin, P("info"), &aCap[0],
644
zOldLogin
645
);
646
db_unprotect(PROTECT_USER);
647
login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
648
db_protect_pop();
649
blob_reset(&sql);
650
admin_log( "Updated user [%q] in all login groups "
651
"with capabilities [%q].",
652
zLogin, &aCap[0] );
653
if( zErr ){
654
const char *zRef = cgi_referer("setup_ulist");
655
style_header("User Change Error");
656
admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
657
@ <span class="loginError">%h(zErr)</span>
658
@
659
@ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
660
@ [Bummer]</a></p>
661
style_finish_page();
662
if( bCapsChanged ){
663
/* It's possible that caps were updated locally even if
664
** login group updates failed. */
665
alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
666
}
667
return;
668
}
669
}
670
if( bCapsChanged ){
671
alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
672
}
673
cgi_redirect(cgi_referer("setup_ulist"));
674
return;
675
}
676
677
/* Load the existing information about the user, if any
678
*/
679
zLogin = "";
680
zInfo = "";
681
zCap = zOldCaps;
682
zPw = "";
683
for(i='a'; i<='z'; i++) oa[i] = "";
684
for(i='0'; i<='9'; i++) oa[i] = "";
685
for(i='A'; i<='Z'; i++) oa[i] = "";
686
if( uid ){
687
assert( zCap );
688
zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid);
689
zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid);
690
zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid);
691
for(i=0; zCap[i]; i++){
692
char c = zCap[i];
693
if( (c>='a' && c<='z') || (c>='0' && c<='9') || (c>='A' && c<='Z') ){
694
oa[c&0x7f] = " checked=\"checked\"";
695
}
696
}
697
}
698
699
/* figure out inherited permissions */
700
memset((char *)inherit, 0, sizeof(inherit));
701
if( fossil_strcmp(zLogin, "developer") ){
702
char *z1, *z2;
703
z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='developer'");
704
while( z1 && *z1 ){
705
inherit[0x7f & *(z1++)] =
706
"<span class=\"ueditInheritDeveloper\"><sub>[D]</sub></span>";
707
}
708
free(z2);
709
}
710
if( fossil_strcmp(zLogin, "reader") ){
711
char *z1, *z2;
712
z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='reader'");
713
while( z1 && *z1 ){
714
inherit[0x7f & *(z1++)] =
715
"<span class=\"ueditInheritReader\"><sub>[R]</sub></span>";
716
}
717
free(z2);
718
}
719
if( fossil_strcmp(zLogin, "anonymous") ){
720
char *z1, *z2;
721
z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='anonymous'");
722
while( z1 && *z1 ){
723
inherit[0x7f & *(z1++)] =
724
"<span class=\"ueditInheritAnonymous\"><sub>[A]</sub></span>";
725
}
726
free(z2);
727
}
728
if( fossil_strcmp(zLogin, "nobody") ){
729
char *z1, *z2;
730
z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='nobody'");
731
while( z1 && *z1 ){
732
inherit[0x7f & *(z1++)] =
733
"<span class=\"ueditInheritNobody\"><sub>[N]</sub></span>";
734
}
735
free(z2);
736
}
737
738
/* Begin generating the page
739
*/
740
style_submenu_element("Cancel", "%s", cgi_referer("setup_ulist"));
741
if( uid ){
742
style_header("Edit User %h", zLogin);
743
if( !login_is_special(zLogin) ){
744
style_submenu_element("Access Log", "%R/access_log?u=%t", zLogin);
745
style_submenu_element("Timeline","%R/timeline?u=%t", zLogin);
746
}
747
}else{
748
style_header("Add A New User");
749
}
750
@ <div class="ueditCapBox">
751
@ <form action="%s(g.zPath)" method="post"><div>
752
login_insert_csrf_secret();
753
if( login_is_special(zLogin) ){
754
@ <input type="hidden" name="login" value="%s(zLogin)">
755
@ <input type="hidden" name="info" value="">
756
@ <input type="hidden" name="pw" value="*">
757
}
758
@ <input type="hidden" name="referer" value="%h(cgi_referer("setup_ulist"))">
759
@ <table width="100%%">
760
@ <tr>
761
@ <td class="usetupEditLabel" id="suuid">User ID:</td>
762
if( uid ){
763
@ <td>%d(uid) <input aria-labelledby="suuid" type="hidden" \
764
@ name="id" value="%d(uid)"/>\
765
@ </td>
766
}else{
767
@ <td>(new user)<input aria-labelledby="suuid" type="hidden" name="id" \
768
@ value="0"></td>
769
}
770
@ </tr>
771
@ <tr>
772
@ <td class="usetupEditLabel" id="sulgn">Login:</td>
773
if( login_is_special(zLogin) ){
774
@ <td><b>%h(zLogin)</b></td>
775
}else{
776
@ <td><input aria-labelledby="sulgn" type="text" name="login" \
777
@ value="%h(zLogin)">
778
if( alert_tables_exist() ){
779
int sid;
780
sid = db_int(0, "SELECT subscriberId FROM subscriber"
781
" WHERE suname=%Q", zLogin);
782
if( sid>0 ){
783
@ &nbsp;&nbsp;<a href="%R/alerts?sid=%d(sid)">\
784
@ (subscription info for %h(zLogin))</a>\
785
}
786
}
787
@ </td></tr>
788
@ <tr>
789
@ <td class="usetupEditLabel" id="sucnfo">Contact&nbsp;Info:</td>
790
@ <td><textarea aria-labelledby="sucnfo" name="info" cols="40" \
791
@ rows="2">%h(zInfo)</textarea></td>
792
}
793
@ </tr>
794
@ <tr>
795
@ <td class="usetupEditLabel">Capabilities:</td>
796
@ <td width="100%%">
797
#define B(x) inherit[x]
798
@ <div class="columns" style="column-width:13em;">
799
@ <ul style="list-style-type: none;">
800
if( g.perm.Setup ){
801
@ <li><label><input type="checkbox" name="as"%s(oa['s'])>
802
@ Setup%s(B('s'))</label>
803
}
804
@ <li><label><input type="checkbox" name="aa"%s(oa['a'])>
805
@ Admin%s(B('a'))</label>
806
@ <li><label><input type="checkbox" name="au"%s(oa['u'])>
807
@ Reader%s(B('u'))</label>
808
@ <li><label><input type="checkbox" name="av"%s(oa['v'])>
809
@ Developer%s(B('v'))</label>
810
#if 0 /* Not Used */
811
@ <li><label><input type="checkbox" name="ad"%s(oa['d'])>
812
@ Delete%s(B('d'))</label>
813
#endif
814
@ <li><label><input type="checkbox" name="ae"%s(oa['e'])>
815
@ View-PII%s(B('e'))</label>
816
@ <li><label><input type="checkbox" name="ap"%s(oa['p'])>
817
@ Password%s(B('p'))</label>
818
@ <li><label><input type="checkbox" name="ai"%s(oa['i'])>
819
@ Check-In%s(B('i'))</label>
820
@ <li><label><input type="checkbox" name="ao"%s(oa['o'])>
821
@ Check-Out%s(B('o'))</label>
822
@ <li><label><input type="checkbox" name="ah"%s(oa['h'])>
823
@ Hyperlinks%s(B('h'))</label>
824
@ <li><label><input type="checkbox" name="ab"%s(oa['b'])>
825
@ Attachments%s(B('b'))</label>
826
@ <li><label><input type="checkbox" name="ag"%s(oa['g'])>
827
@ Clone%s(B('g'))</label>
828
@ <li><label><input type="checkbox" name="aj"%s(oa['j'])>
829
@ Read Wiki%s(B('j'))</label>
830
@ <li><label><input type="checkbox" name="af"%s(oa['f'])>
831
@ New Wiki%s(B('f'))</label>
832
@ <li><label><input type="checkbox" name="am"%s(oa['m'])>
833
@ Append Wiki%s(B('m'))</label>
834
@ <li><label><input type="checkbox" name="ak"%s(oa['k'])>
835
@ Write Wiki%s(B('k'))</label>
836
@ <li><label><input type="checkbox" name="al"%s(oa['l'])>
837
@ Moderate Wiki%s(B('l'))</label>
838
@ <li><label><input type="checkbox" name="ar"%s(oa['r'])>
839
@ Read Ticket%s(B('r'))</label>
840
@ <li><label><input type="checkbox" name="an"%s(oa['n'])>
841
@ New Tickets%s(B('n'))</label>
842
@ <li><label><input type="checkbox" name="ac"%s(oa['c'])>
843
@ Append To Ticket%s(B('c'))</label>
844
@ <li><label><input type="checkbox" name="aw"%s(oa['w'])>
845
@ Write Tickets%s(B('w'))</label>
846
@ <li><label><input type="checkbox" name="aq"%s(oa['q'])>
847
@ Moderate Tickets%s(B('q'))</label>
848
@ <li><label><input type="checkbox" name="at"%s(oa['t'])>
849
@ Ticket Report%s(B('t'))</label>
850
@ <li><label><input type="checkbox" name="ax"%s(oa['x'])>
851
@ Private%s(B('x'))</label>
852
@ <li><label><input type="checkbox" name="ay"%s(oa['y'])>
853
@ Write Unversioned%s(B('y'))</label>
854
@ <li><label><input type="checkbox" name="az"%s(oa['z'])>
855
@ Download Zip%s(B('z'))</label>
856
@ <li><label><input type="checkbox" name="a2"%s(oa['2'])>
857
@ Read Forum%s(B('2'))</label>
858
@ <li><label><input type="checkbox" name="a3"%s(oa['3'])>
859
@ Write Forum%s(B('3'))</label>
860
@ <li><label><input type="checkbox" name="a4"%s(oa['4'])>
861
@ WriteTrusted Forum%s(B('4'))</label>
862
@ <li><label><input type="checkbox" name="a5"%s(oa['5'])>
863
@ Moderate Forum%s(B('5'))</label>
864
@ <li><label><input type="checkbox" name="a6"%s(oa['6'])>
865
@ Supervise Forum%s(B('6'))</label>
866
@ <li><label><input type="checkbox" name="a7"%s(oa['7'])>
867
@ Email Alerts%s(B('7'))</label>
868
@ <li><label><input type="checkbox" name="aA"%s(oa['A'])>
869
@ Send Announcements%s(B('A'))</label>
870
@ <li><label><input type="checkbox" name="aC"%s(oa['C'])>
871
@ Chatroom%s(B('C'))</label>
872
@ <li><label><input type="checkbox" name="aD"%s(oa['D'])>
873
@ Enable Debug%s(B('D'))</label>
874
@ </ul></div>
875
@ </td>
876
@ </tr>
877
@ <tr>
878
@ <td class="usetupEditLabel">Selected Cap:</td>
879
@ <td>
880
@ <span id="usetupEditCapability">(missing JS?)</span>
881
@ <a href="%R/setup_ucap_list">(key)</a>
882
@ </td>
883
@ </tr>
884
if( !login_is_special(zLogin) ){
885
@ <tr>
886
@ <td align="right" id="supw">Password:</td>
887
if( zPw[0] ){
888
/* Obscure the password for all users */
889
@ <td><input aria-labelledby="supw" type="password" autocomplete="off" \
890
@ name="pw" value="**********">
891
@ (Leave unchanged to retain password)</td>
892
}else{
893
/* Show an empty password as an empty input field */
894
char *zRPW = fossil_random_password(12);
895
@ <td><input aria-labelledby="supw" type="password" name="pw" \
896
@ autocomplete="off" value=""> Password suggestion: %z(zRPW)</td>
897
}
898
@ </tr>
899
}
900
zGroup = login_group_name();
901
if( zGroup ){
902
@ <tr>
903
@ <td valign="top" align="right">Scope:</td>
904
@ <td valign="top">
905
@ <input type="radio" name="all" checked value="0">
906
@ Apply changes to this repository only.<br>
907
@ <input type="radio" name="all" value="1">
908
@ Apply changes to all repositories in the "<b>%h(zGroup)</b>"
909
@ login group.</td></tr>
910
}
911
if( !higherUser ){
912
if( zDeleteVerify ){
913
@ <tr>
914
@ <td valign="top" align="right">Verify:</td>
915
@ <td><label><input type="checkbox" name="verifydelete">\
916
@ Confirm Delete \
917
@ <span class="loginError">&larr; %h(zDeleteVerify)</span>
918
@ </label></td>
919
@ <tr>
920
}
921
@ <tr>
922
@ <td>&nbsp;</td>
923
@ <td><input type="submit" name="apply" value="Apply Changes">
924
if( !login_is_special(zLogin) ){
925
@ <input type="submit" name="delete" value="Delete User">
926
}
927
@ <input type="submit" name="can" value="Cancel"></td>
928
@ </tr>
929
}
930
@ </table>
931
@ </div></form>
932
@ </div>
933
builtin_request_js("useredit.js");
934
@ <hr>
935
@ <h1>Notes On Privileges And Capabilities:</h1>
936
@ <ul>
937
if( higherUser ){
938
@ <li><p class="missingPriv">
939
@ User %h(zLogin) has Setup privileges and you only have Admin privileges
940
@ so you are not permitted to make changes to %h(zLogin).
941
@ </p></li>
942
@
943
}
944
@ <li><p>
945
@ The <span class="capability">Setup</span> user can make arbitrary
946
@ configuration changes. An <span class="usertype">Admin</span> user
947
@ can add other users and change user privileges
948
@ and reset user passwords. Both automatically get all other privileges
949
@ listed below. Use these two settings with discretion.
950
@ </p></li>
951
@
952
@ <li><p>
953
@ The "<span class="ueditInheritNobody"><sub>N</sub></span>" subscript suffix
954
@ indicates the privileges of <span class="usertype">nobody</span> that
955
@ are available to all users regardless of whether or not they are logged in.
956
@ </p></li>
957
@
958
@ <li><p>
959
@ The "<span class="ueditInheritAnonymous"><sub>A</sub></span>"
960
@ subscript suffix
961
@ indicates the privileges of <span class="usertype">anonymous</span> that
962
@ are inherited by all logged-in users.
963
@ </p></li>
964
@
965
@ <li><p>
966
@ The "<span class="ueditInheritDeveloper"><sub>D</sub></span>"
967
@ subscript suffix indicates the privileges of
968
@ <span class="usertype">developer</span> that
969
@ are inherited by all users with the
970
@ <span class="capability">Developer</span> privilege.
971
@ </p></li>
972
@
973
@ <li><p>
974
@ The "<span class="ueditInheritReader"><sub>R</sub></span>" subscript suffix
975
@ indicates the privileges of <span class="usertype">reader</span> that
976
@ are inherited by all users with the <span class="capability">Reader</span>
977
@ privilege.
978
@ </p></li>
979
@
980
@ <li><p>
981
@ The <span class="capability">Delete</span> privilege give the user the
982
@ ability to erase wiki, tickets, and attachments that have been added
983
@ by anonymous users. This capability is intended for deletion of spam.
984
@ The delete capability is only in effect for 24 hours after the item
985
@ is first posted. The <span class="usertype">Setup</span> user can
986
@ delete anything at any time.
987
@ </p></li>
988
@
989
@ <li><p>
990
@ The <span class="capability">Hyperlinks</span> privilege allows a user
991
@ to see most hyperlinks. This is recommended ON for most logged-in users
992
@ but OFF for user "nobody" to avoid problems with spiders trying to walk
993
@ every diff and annotation of every historical check-in and file.
994
@ </p></li>
995
@
996
@ <li><p>
997
@ The <span class="capability">Zip</span> privilege allows a user to
998
@ see the "download as ZIP"
999
@ hyperlink and permits access to the <tt>/zip</tt> page. This allows
1000
@ users to download ZIP archives without granting other rights like
1001
@ <span class="capability">Read</span> or
1002
@ <span class="capability">Hyperlink</span>. The "z" privilege is recommended
1003
@ for user <span class="usertype">nobody</span> so that automatic package
1004
@ downloaders can obtain the sources without going through the login
1005
@ procedure.
1006
@ </p></li>
1007
@
1008
@ <li><p>
1009
@ The <span class="capability">Check-in</span> privilege allows remote
1010
@ users to "push". The <span class="capability">Check-out</span> privilege
1011
@ allows remote users to "pull". The <span class="capability">Clone</span>
1012
@ privilege allows remote users to "clone".
1013
@ </p></li>
1014
@
1015
@ <li><p>
1016
@ The <span class="capability">Read Wiki</span>,
1017
@ <span class="capability">New Wiki</span>,
1018
@ <span class="capability">Append Wiki</span>, and
1019
@ <b>Write Wiki</b> privileges control access to wiki pages. The
1020
@ <span class="capability">Read Ticket</span>,
1021
@ <span class="capability">New Ticket</span>,
1022
@ <span class="capability">Append Ticket</span>, and
1023
@ <span class="capability">Write Ticket</span> privileges control access
1024
@ to trouble tickets.
1025
@ The <span class="capability">Ticket Report</span> privilege allows
1026
@ the user to create or edit ticket report formats.
1027
@ </p></li>
1028
@
1029
@ <li><p>
1030
@ Users with the <span class="capability">Password</span> privilege
1031
@ are allowed to change their own password. Recommended ON for most
1032
@ users but OFF for special users <span class="usertype">developer</span>,
1033
@ <span class="usertype">anonymous</span>,
1034
@ and <span class="usertype">nobody</span>.
1035
@ </p></li>
1036
@
1037
@ <li><p>
1038
@ The <span class="capability">View-PII</span> privilege allows the display
1039
@ of personally-identifiable information information such as the
1040
@ email address of users and contact
1041
@ information on tickets. Recommended OFF for
1042
@ <span class="usertype">anonymous</span> and for
1043
@ <span class="usertype">nobody</span> but ON for
1044
@ <span class="usertype">developer</span>.
1045
@ </p></li>
1046
@
1047
@ <li><p>
1048
@ The <span class="capability">Attachment</span> privilege is needed in
1049
@ order to add attachments to tickets or wiki. Write privilege on the
1050
@ ticket or wiki is also required.
1051
@ </p></li>
1052
@
1053
@ <li><p>
1054
@ Login is prohibited if the password is an empty string.
1055
@ </p></li>
1056
@ </ul>
1057
@
1058
@ <h2>Special Logins</h2>
1059
@
1060
@ <ul>
1061
@ <li><p>
1062
@ No login is required for user <span class="usertype">nobody</span>. The
1063
@ capabilities of the <span class="usertype">nobody</span> user are
1064
@ inherited by all users, regardless of whether or not they are logged in.
1065
@ To disable universal access to the repository, make sure that the
1066
@ <span class="usertype">nobody</span> user has no capabilities
1067
@ enabled. The password for <span class="usertype">nobody</span> is ignored.
1068
@ </p></li>
1069
@
1070
@ <li><p>
1071
@ Login is required for user <span class="usertype">anonymous</span> but the
1072
@ password is displayed on the login screen beside the password entry box
1073
@ so anybody who can read should be able to login as anonymous.
1074
@ On the other hand, spiders and web-crawlers will typically not
1075
@ be able to login. Set the capabilities of the
1076
@ <span class="usertype">anonymous</span>
1077
@ user to things that you want any human to be able to do, but not any
1078
@ spider. Every other logged-in user inherits the privileges of
1079
@ <span class="usertype">anonymous</span>.
1080
@ </p></li>
1081
@
1082
@ <li><p>
1083
@ The <span class="usertype">developer</span> user is intended as a template
1084
@ for trusted users with check-in privileges. When adding new trusted users,
1085
@ simply select the <span class="capability">developer</span> privilege to
1086
@ cause the new user to inherit all privileges of the
1087
@ <span class="usertype">developer</span>
1088
@ user. Similarly, the <span class="usertype">reader</span> user is a
1089
@ template for users who are allowed more access than
1090
@ <span class="usertype">anonymous</span>,
1091
@ but less than a <span class="usertype">developer</span>.
1092
@ </p></li>
1093
@ </ul>
1094
style_finish_page();
1095
}
1096
1097
/*
1098
** WEBPAGE: setup_uinfo
1099
**
1100
** Detailed information about a user account, available to administrators
1101
** only.
1102
**
1103
** u=UID
1104
** l=LOGIN
1105
*/
1106
void setup_uinfo_page(void){
1107
Stmt q;
1108
Blob sql;
1109
const char *zLogin;
1110
int uid;
1111
1112
/* Must have ADMIN privileges to access this page
1113
*/
1114
login_check_credentials();
1115
if( !g.perm.Admin ){ login_needed(0); return; }
1116
style_set_current_feature("setup");
1117
zLogin = P("l");
1118
uid = atoi(PD("u","0"));
1119
if( zLogin==0 && uid==0 ){
1120
uid = db_int(1,"SELECT uid FROM user");
1121
}
1122
blob_init(&sql, 0, 0);
1123
blob_append_sql(&sql,
1124
"SELECT "
1125
/* 0 */ "uid,"
1126
/* 1 */ "login,"
1127
/* 2 */ "cap,"
1128
/* 3 */ "cookie,"
1129
/* 4 */ "datetime(cexpire),"
1130
/* 5 */ "info,"
1131
/* 6 */ "datetime(user.mtime,'unixepoch'),"
1132
);
1133
if( db_table_exists("repository","subscriber") ){
1134
blob_append_sql(&sql,
1135
/* 7 */ "subscriberId,"
1136
/* 8 */ "semail,"
1137
/* 9 */ "sverified,"
1138
/* 10 */ "date(lastContact+2440587.5)"
1139
" FROM user LEFT JOIN subscriber ON suname=login"
1140
);
1141
}else{
1142
blob_append_sql(&sql,
1143
/* 7 */ "NULL,"
1144
/* 8 */ "NULL,"
1145
/* 9 */ "NULL,"
1146
/* 10 */ "NULL"
1147
" FROM user"
1148
);
1149
}
1150
if( zLogin!=0 ){
1151
blob_append_sql(&sql, " WHERE login=%Q", zLogin);
1152
}else{
1153
blob_append_sql(&sql, " WHERE uid=%d", uid);
1154
}
1155
db_prepare(&q, "%s", blob_sql_text(&sql));
1156
blob_zero(&sql);
1157
if( db_step(&q)!=SQLITE_ROW ){
1158
style_header("No Such User");
1159
if( zLogin ){
1160
@ <p>Cannot find any information on user %h(zLogin).
1161
}else{
1162
@ <p>Cannot find any information on userid %d(uid).
1163
}
1164
style_finish_page();
1165
db_finalize(&q);
1166
return;
1167
}
1168
style_header("User %h", db_column_text(&q,1));
1169
@ <table class="label-value">
1170
@ <tr><th>uid:</th><td>%d(db_column_int(&q,0))
1171
@ (<a href="%R/setup_uedit?id=%d(db_column_int(&q,0))">edit</a>)</td></tr>
1172
@ <tr><th>login:</th><td>%h(db_column_text(&q,1))</td></tr>
1173
@ <tr><th>capabilities:</th><td>%h(db_column_text(&q,2))</td></tr>
1174
@ <tr><th valign="top">info:</th>
1175
@ <td valign="top"><span style='white-space:pre-line;'>\
1176
@ %h(db_column_text(&q,5))</span></td></tr>
1177
@ <tr><th>user.mtime:</th><td>%h(db_column_text(&q,6))</td></tr>
1178
if( db_column_type(&q,7)!=SQLITE_NULL ){
1179
@ <tr><th>subscriberId:</th><td>%d(db_column_int(&q,7))
1180
@ (<a href="%R/alerts?sid=%d(db_column_int(&q,7))">edit</a>)</td></tr>
1181
@ <tr><th>semail:</th><td>%h(db_column_text(&q,8))</td></tr>
1182
@ <tr><th>verified:</th><td>%s(db_column_int(&q,9)?"yes":"no")</td></th>
1183
@ <tr><th>lastContact:</th><td>%h(db_column_text(&q,10))</td></tr>
1184
}
1185
@ </table>
1186
db_finalize(&q);
1187
style_finish_page();
1188
}
1189

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button