| | @@ -18,32 +18,71 @@ |
| 18 | 18 | ** This file contains code for dealing with attachments. |
| 19 | 19 | */ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include "attach.h" |
| 22 | 22 | #include <assert.h> |
| 23 | + |
| 24 | +/* |
| 25 | +** Given a presumedly legal attachment target name, this guesses the |
| 26 | +** target type and returns one of CFTYPE_FORUM, CFTYPE_WIKI, |
| 27 | +** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot |
| 28 | +** distinguish the target type. |
| 29 | +** |
| 30 | +** In the case of CFTYPE_FORUM, it is up to the caller to ensure that, |
| 31 | +** if needed, they resolve zTarget using forumpost_head_rid2() so that |
| 32 | +** they get the RID of the earliest version of the post, as that is |
| 33 | +** the only one which attachments should target. |
| 34 | +*/ |
| 35 | +int attachment_target_type(const char *zTarget){ |
| 36 | + static Stmt q = empty_Stmt_m; |
| 37 | + int rc = 0; |
| 38 | + if( forumpost_head_rid2(zTarget)>0 ){ |
| 39 | + return CFTYPE_FORUM; |
| 40 | + } |
| 41 | + if( !q.pStmt ){ |
| 42 | + db_static_prepare( |
| 43 | + &q, |
| 44 | + "SELECT CASE " |
| 45 | + "WHEN 'tkt-'||:tgt IN (SELECT tagname FROM tag) THEN %d " |
| 46 | + "WHEN 'event-'||:tgt IN (SELECT tagname FROM tag) THEN %d " |
| 47 | + "WHEN 'wiki-'||:tgt IN (SELECT tagname FROM tag) THEN %d " |
| 48 | + "ELSE 0 END", |
| 49 | + CFTYPE_TICKET, CFTYPE_EVENT, CFTYPE_WIKI |
| 50 | + ); |
| 51 | + } |
| 52 | + db_bind_text(&q, ":tgt", zTarget); |
| 53 | + if( SQLITE_ROW==db_step(&q) ){ |
| 54 | + rc = db_column_int(&q, 0); |
| 55 | + } |
| 56 | + db_reset(&q); |
| 57 | + return rc; |
| 58 | +} |
| 23 | 59 | |
| 24 | 60 | /* |
| 25 | 61 | ** WEBPAGE: attachlist |
| 26 | 62 | ** List attachments. |
| 27 | 63 | ** |
| 28 | 64 | ** tkt=HASH |
| 29 | 65 | ** page=WIKIPAGE |
| 30 | 66 | ** technote=HASH |
| 67 | +** forumpost=HASH |
| 31 | 68 | ** |
| 32 | | -** At most one of technote=, tkt= or page= may be supplied. |
| 69 | +** At most one of technote=, tkt=, forumpost=, or page= may be supplied. |
| 33 | 70 | ** |
| 34 | 71 | ** If none are given, all attachments are listed. If one is given, only |
| 35 | 72 | ** attachments for the designated technote, ticket or wiki page are shown. |
| 36 | 73 | ** |
| 37 | 74 | ** HASH may be just a prefix of the relevant technical note or ticket |
| 38 | 75 | ** artifact hash, in which case all attachments of all technical notes or |
| 39 | | -** tickets with the prefix will be listed. |
| 76 | +** tickets with the prefix will be listed. Forum posts, on the other hand, |
| 77 | +** require a unique hash prefix. |
| 40 | 78 | */ |
| 41 | 79 | void attachlist_page(void){ |
| 42 | 80 | const char *zPage = P("page"); |
| 43 | 81 | const char *zTkt = P("tkt"); |
| 44 | 82 | const char *zTechNote = P("technote"); |
| 83 | + const char *zForumPost = P("forumpost"); |
| 45 | 84 | Blob sql; |
| 46 | 85 | Stmt q; |
| 47 | 86 | |
| 48 | 87 | if( zPage && zTkt ) zTkt = 0; |
| 49 | 88 | login_check_credentials(); |
| | @@ -50,29 +89,34 @@ |
| 50 | 89 | style_set_current_feature("attach"); |
| 51 | 90 | blob_zero(&sql); |
| 52 | 91 | blob_append_sql(&sql, |
| 53 | 92 | "SELECT datetime(mtime,toLocal()), src, target, filename," |
| 54 | 93 | " comment, user," |
| 55 | | - " (SELECT uuid FROM blob WHERE rid=attachid), attachid," |
| 56 | | - " (CASE WHEN 'tkt-'||target IN (SELECT tagname FROM tag)" |
| 57 | | - " THEN 1" |
| 58 | | - " WHEN 'event-'||target IN (SELECT tagname FROM tag)" |
| 59 | | - " THEN 2" |
| 60 | | - " ELSE 0 END)" |
| 94 | + " (SELECT uuid FROM blob WHERE rid=attachid), attachid" |
| 61 | 95 | " FROM attachment" |
| 62 | 96 | ); |
| 63 | | - if( zPage ){ |
| 97 | + if( zForumPost ){ |
| 98 | + int fnid; |
| 99 | + if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; } |
| 100 | + style_header("Attachments To Forum post %S", zForumPost); |
| 101 | + fnid = forumpost_head_rid2(zForumPost); |
| 102 | + if( fnid<=0 ){ |
| 103 | + webpage_error("Invalid forum post ID: %h", zForumPost); |
| 104 | + } |
| 105 | + blob_append_sql(&sql, " WHERE target=" |
| 106 | + "(SELECT uuid FROM blob WHERE rid=%d)", fnid); |
| 107 | + }else if( zPage ){ |
| 64 | 108 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 65 | | - style_header("Attachments To %h", zPage); |
| 109 | + style_header("Attachments To Wiki page %h", zPage); |
| 66 | 110 | blob_append_sql(&sql, " WHERE target=%Q", zPage); |
| 67 | 111 | }else if( zTkt ){ |
| 68 | 112 | if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; } |
| 69 | 113 | style_header("Attachments To Ticket %S", zTkt); |
| 70 | 114 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt); |
| 71 | 115 | }else if( zTechNote ){ |
| 72 | 116 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 73 | | - style_header("Attachments to Tech Note %S", zTechNote); |
| 117 | + style_header("Attachments To Tech Note %S", zTechNote); |
| 74 | 118 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", |
| 75 | 119 | zTechNote); |
| 76 | 120 | }else{ |
| 77 | 121 | if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){ |
| 78 | 122 | login_needed(g.anon.RdTkt || g.anon.RdWiki); |
| | @@ -82,35 +126,58 @@ |
| 82 | 126 | } |
| 83 | 127 | blob_append_sql(&sql, " ORDER BY mtime DESC"); |
| 84 | 128 | db_prepare(&q, "%s", blob_sql_text(&sql)); |
| 85 | 129 | @ <ol> |
| 86 | 130 | while( db_step(&q)==SQLITE_ROW ){ |
| 87 | | - const char *zDate = db_column_text(&q, 0); |
| 88 | | - const char *zSrc = db_column_text(&q, 1); |
| 89 | | - const char *zTarget = db_column_text(&q, 2); |
| 90 | | - const char *zFilename = db_column_text(&q, 3); |
| 91 | | - const char *zComment = db_column_text(&q, 4); |
| 92 | | - const char *zUser = db_column_text(&q, 5); |
| 93 | | - const char *zUuid = db_column_text(&q, 6); |
| 94 | | - int attachid = db_column_int(&q, 7); |
| 95 | | - /* type 0 is a wiki page, 1 is a ticket, 2 is a tech note */ |
| 96 | | - int type = db_column_int(&q, 8); |
| 97 | | - const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous"; |
| 131 | + const char *zDate; |
| 132 | + const char *zSrc; |
| 133 | + const char *zTarget; |
| 134 | + const char *zFilename; |
| 135 | + const char *zComment; |
| 136 | + const char *zUser; |
| 137 | + const char *zUuid; |
| 138 | + const char *zDispUser; |
| 139 | + const int attachid = db_column_int(&q, 7); |
| 140 | + int type; |
| 98 | 141 | int i; |
| 99 | | - char *zUrlTail; |
| 142 | + char *zUrlTail = 0; |
| 143 | + |
| 144 | + if( moderation_pending(attachid) |
| 145 | + && !moderation_user_could(attachid, 1, 0) ){ |
| 146 | + /* Elide entries which are currently pending moderation unless |
| 147 | + ** the user would be able to moderate the entry themselves. */ |
| 148 | + continue; |
| 149 | + } |
| 150 | + |
| 151 | + zDate = db_column_text(&q, 0); |
| 152 | + zSrc = db_column_text(&q, 1); |
| 153 | + zTarget = db_column_text(&q, 2); |
| 154 | + zFilename = db_column_text(&q, 3); |
| 155 | + zComment = db_column_text(&q, 4); |
| 156 | + zUser = db_column_text(&q, 5); |
| 157 | + zUuid = db_column_text(&q, 6); |
| 158 | + zDispUser = zUser && zUser[0] ? zUser : "anonymous"; |
| 100 | 159 | for(i=0; zFilename[i]; i++){ |
| 101 | 160 | if( zFilename[i]=='/' && zFilename[i+1]!=0 ){ |
| 102 | 161 | zFilename = &zFilename[i+1]; |
| 103 | 162 | i = -1; |
| 104 | 163 | } |
| 105 | 164 | } |
| 106 | | - if( type==1 ){ |
| 107 | | - zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename); |
| 108 | | - }else if( type==2 ){ |
| 109 | | - zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename); |
| 110 | | - }else{ |
| 111 | | - zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 165 | + type = attachment_target_type(zTarget); |
| 166 | + switch( type ){ |
| 167 | + case CFTYPE_TICKET: |
| 168 | + zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename); |
| 169 | + break; |
| 170 | + case CFTYPE_EVENT: |
| 171 | + zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename); |
| 172 | + break; |
| 173 | + case CFTYPE_FORUM: |
| 174 | + zUrlTail = mprintf("forumpost=%t&file=%t", zTarget, zFilename); |
| 175 | + break; |
| 176 | + case CFTYPE_WIKI: |
| 177 | + zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 178 | + break; |
| 112 | 179 | } |
| 113 | 180 | @ <li><p> |
| 114 | 181 | @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a> |
| 115 | 182 | moderation_pending_www(attachid); |
| 116 | 183 | @ <br><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> |
| | @@ -117,25 +184,37 @@ |
| 117 | 184 | @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br> |
| 118 | 185 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 119 | 186 | if( zComment && zComment[0] ){ |
| 120 | 187 | @ %!W(zComment)<br> |
| 121 | 188 | } |
| 122 | | - if( zPage==0 && zTkt==0 && zTechNote==0 ){ |
| 189 | + if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){ |
| 123 | 190 | if( zSrc==0 || zSrc[0]==0 ){ |
| 124 | 191 | zSrc = "Deleted from"; |
| 125 | 192 | }else { |
| 126 | 193 | zSrc = "Added to"; |
| 127 | 194 | } |
| 128 | | - if( type==1 ){ |
| 129 | | - @ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)"> |
| 130 | | - @ %S(zTarget)</a> |
| 131 | | - }else if( type==2 ){ |
| 132 | | - @ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)"> |
| 133 | | - @ %S(zTarget)</a> |
| 134 | | - }else{ |
| 135 | | - @ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)"> |
| 136 | | - @ %h(zTarget)</a> |
| 195 | + switch( type ){ |
| 196 | + case CFTYPE_TICKET: |
| 197 | + @ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)"> |
| 198 | + @ %S(zTarget)</a> |
| 199 | + break; |
| 200 | + case CFTYPE_EVENT: |
| 201 | + @ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)"> |
| 202 | + @ %S(zTarget)</a> |
| 203 | + break; |
| 204 | + case CFTYPE_WIKI: |
| 205 | + @ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)"> |
| 206 | + @ %h(zTarget)</a> |
| 207 | + break; |
| 208 | + case CFTYPE_FORUM: |
| 209 | + @ %s(zSrc) forum post <a href="%R/forumpost/%s(zTarget)"> |
| 210 | + @ %h(zTarget)</a> |
| 211 | + break; |
| 212 | + default: |
| 213 | + @ <span class='error'>%s(zSrc) cannot determine target type |
| 214 | + @ of %h(zTarget)</span> |
| 215 | + break; |
| 137 | 216 | } |
| 138 | 217 | }else{ |
| 139 | 218 | if( zSrc==0 || zSrc[0]==0 ){ |
| 140 | 219 | @ Deleted |
| 141 | 220 | }else { |
| | @@ -162,27 +241,35 @@ |
| 162 | 241 | ** Query parameters: |
| 163 | 242 | ** |
| 164 | 243 | ** tkt=HASH |
| 165 | 244 | ** page=WIKIPAGE |
| 166 | 245 | ** technote=HASH |
| 246 | +** forumpost=HASH |
| 167 | 247 | ** file=FILENAME |
| 168 | 248 | ** attachid=ID |
| 169 | 249 | ** |
| 170 | 250 | */ |
| 171 | 251 | void attachview_page(void){ |
| 172 | 252 | const char *zPage = P("page"); |
| 173 | 253 | const char *zTkt = P("tkt"); |
| 174 | 254 | const char *zTechNote = P("technote"); |
| 255 | + const char *zForumPost = P("forumpost"); |
| 175 | 256 | const char *zFile = P("file"); |
| 176 | 257 | const char *zTarget = 0; |
| 177 | 258 | int attachid = atoi(PD("attachid","0")); |
| 178 | | - char *zUUID; |
| 259 | + char *zUUID = 0; |
| 179 | 260 | |
| 180 | 261 | if( zFile==0 ) fossil_redirect_home(); |
| 181 | 262 | login_check_credentials(); |
| 182 | 263 | style_set_current_feature("attach"); |
| 183 | | - if( zPage ){ |
| 264 | + if( zForumPost ){ |
| 265 | + int fnid; |
| 266 | + if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; } |
| 267 | + /* Forum attachments are always tied to the post's initial version */ |
| 268 | + fnid = forumpost_head_rid2(zForumPost); |
| 269 | + if( fnid>0 ) zTarget = rid_to_uuid(fnid); |
| 270 | + }else if( zPage ){ |
| 184 | 271 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 185 | 272 | zTarget = zPage; |
| 186 | 273 | }else if( zTkt ){ |
| 187 | 274 | if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; } |
| 188 | 275 | zTarget = zTkt; |
| | @@ -314,35 +401,60 @@ |
| 314 | 401 | ** Add a new attachment. |
| 315 | 402 | ** |
| 316 | 403 | ** tkt=HASH |
| 317 | 404 | ** page=WIKIPAGE |
| 318 | 405 | ** technote=HASH |
| 406 | +** forumpost=HASH |
| 319 | 407 | ** from=URL |
| 320 | 408 | ** |
| 321 | 409 | */ |
| 322 | 410 | void attachadd_page(void){ |
| 323 | 411 | const char *zPage = P("page"); |
| 412 | + const char *zForumPost = P("forumpost"); |
| 324 | 413 | const char *zTkt = P("tkt"); |
| 325 | 414 | const char *zTechNote = P("technote"); |
| 326 | 415 | const char *zFrom = P("from"); |
| 327 | 416 | const char *aContent = P("f"); |
| 328 | 417 | const char *zName = PD("f:filename","unknown"); |
| 418 | + const char *zComment = PD("comment", ""); |
| 329 | 419 | const char *zTarget; |
| 330 | | - char *zTargetType; |
| 420 | + char * zTo = 0; |
| 421 | + char *zTargetType = 0; |
| 422 | + char *zExtraFree = 0; |
| 331 | 423 | int szContent = atoi(PD("f:bytes","0")); |
| 332 | 424 | int goodCaptcha = 1; |
| 425 | + int szLimit = 0; |
| 333 | 426 | |
| 427 | + if( zFrom==0 ) zFrom = mprintf("%R/home"); |
| 334 | 428 | if( P("cancel") ) cgi_redirect(zFrom); |
| 335 | | - if( (zPage && zTkt) |
| 336 | | - || (zPage && zTechNote) |
| 337 | | - || (zTkt && zTechNote) |
| 338 | | - ){ |
| 339 | | - fossil_redirect_home(); |
| 429 | + if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){ |
| 430 | + webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X," |
| 431 | + " or technote=X"); |
| 340 | 432 | } |
| 341 | | - if( zPage==0 && zTkt==0 && zTechNote==0) fossil_redirect_home(); |
| 342 | 433 | login_check_credentials(); |
| 343 | | - if( zPage ){ |
| 434 | + if( zForumPost ){ |
| 435 | + int fpid; |
| 436 | + if( g.perm.AttachForum==0 ){ |
| 437 | + login_needed(g.anon.AttachForum); |
| 438 | + return; |
| 439 | + } |
| 440 | + fpid = forumpost_head_rid2(zForumPost); |
| 441 | + if( fpid<=0 ){ |
| 442 | + webpage_error("Invalid forum post ID: %h", zForumPost); |
| 443 | + }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){ |
| 444 | + webpage_error("Only admins can attach files to other users' " |
| 445 | + "forum posts."); |
| 446 | + } |
| 447 | + zTarget = zExtraFree = rid_to_uuid(fpid); |
| 448 | + zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>", |
| 449 | + zTarget, zForumPost); |
| 450 | + zTo = 1 |
| 451 | + ? mprintf("%R/forumpost/%S", zTarget) |
| 452 | + : mprintf("%R/attachview?forumpost=%T&file=%T", |
| 453 | + zTarget, zName) |
| 454 | + /* Or we could return directly to the forum post. */; |
| 455 | + }else if( zPage ){ |
| 344 | 456 | if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 345 | 457 | login_needed(g.anon.ApndWiki && g.anon.Attach); |
| 346 | 458 | return; |
| 347 | 459 | } |
| 348 | 460 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){ |
| | @@ -364,10 +476,11 @@ |
| 364 | 476 | zTarget = zTechNote; |
| 365 | 477 | zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>", |
| 366 | 478 | zTechNote, zTechNote); |
| 367 | 479 | |
| 368 | 480 | }else{ |
| 481 | + assert( zTkt ); |
| 369 | 482 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 370 | 483 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| 371 | 484 | return; |
| 372 | 485 | } |
| 373 | 486 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTkt) ){ |
| | @@ -377,21 +490,25 @@ |
| 377 | 490 | } |
| 378 | 491 | zTarget = zTkt; |
| 379 | 492 | zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>", |
| 380 | 493 | zTkt, zTkt); |
| 381 | 494 | } |
| 382 | | - if( zFrom==0 ) zFrom = mprintf("%R/home"); |
| 383 | | - if( P("cancel") ){ |
| 384 | | - cgi_redirect(zFrom); |
| 385 | | - } |
| 386 | | - if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){ |
| 387 | | - int needModerator = (zTkt!=0 && ticket_need_moderation(0)) || |
| 495 | + szLimit = db_get_int("attachment-size-limit", 0); |
| 496 | + if( szContent<0 || (szLimit && szContent>szLimit) ){ |
| 497 | + /* This check must be done late so that zTargetType is set up. */ |
| 498 | + @ <p class="generalError">Attachment %h(zName) is too large. |
| 499 | + @ <a href="%R/help/attachment-size-limit">Limit</a> is |
| 500 | + @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p> |
| 501 | + /* Fall through and render form. */ |
| 502 | + }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){ |
| 503 | + int needModerator = (zForumPost!=0 && forum_need_moderation()) || |
| 504 | + (zTkt!=0 && ticket_need_moderation(0)) || |
| 388 | 505 | (zPage!=0 && wiki_need_moderation(0)); |
| 389 | | - const char *zComment = PD("comment", ""); |
| 390 | 506 | attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); |
| 391 | | - cgi_redirect(zFrom); |
| 507 | + cgi_redirect(zTo ? zTo : zFrom); |
| 392 | 508 | } |
| 509 | + |
| 393 | 510 | style_set_current_feature("attach"); |
| 394 | 511 | style_header("Add Attachment"); |
| 395 | 512 | if( !goodCaptcha ){ |
| 396 | 513 | @ <p class="generalError">Error: Incorrect security code.</p> |
| 397 | 514 | } |
| | @@ -399,12 +516,15 @@ |
| 399 | 516 | form_begin("enctype='multipart/form-data'", "%R/attachadd"); |
| 400 | 517 | @ <div> |
| 401 | 518 | @ File to Attach: |
| 402 | 519 | @ <input type="file" name="f" size="60"><br> |
| 403 | 520 | @ Description:<br> |
| 404 | | - @ <textarea name="comment" cols="80" rows="5" wrap="virtual"></textarea><br> |
| 405 | | - if( zTkt ){ |
| 521 | + @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\ |
| 522 | + @ >%h(zComment)</textarea><br> |
| 523 | + if( zForumPost ){ |
| 524 | + @ <input type="hidden" name="forumpost" value="%h(zTarget)"> |
| 525 | + }else if( zTkt ){ |
| 406 | 526 | @ <input type="hidden" name="tkt" value="%h(zTkt)"> |
| 407 | 527 | }else if( zTechNote ){ |
| 408 | 528 | @ <input type="hidden" name="technote" value="%h(zTechNote)"> |
| 409 | 529 | }else{ |
| 410 | 530 | @ <input type="hidden" name="page" value="%h(zPage)"> |
| | @@ -415,15 +535,18 @@ |
| 415 | 535 | @ </div> |
| 416 | 536 | captcha_generate(0); |
| 417 | 537 | @ </form> |
| 418 | 538 | style_finish_page(); |
| 419 | 539 | fossil_free(zTargetType); |
| 540 | + fossil_free(zExtraFree); |
| 420 | 541 | } |
| 421 | 542 | |
| 422 | 543 | /* |
| 423 | 544 | ** WEBPAGE: ainfo |
| 424 | 545 | ** URL: /ainfo?name=ARTIFACTID |
| 546 | +** |
| 547 | +** name=ATTACHMENT_ARTIFACT_UUID |
| 425 | 548 | ** |
| 426 | 549 | ** Show the details of an attachment artifact. |
| 427 | 550 | */ |
| 428 | 551 | void ainfo_page(void){ |
| 429 | 552 | int rid; /* RID for the control artifact */ |
| | @@ -436,113 +559,141 @@ |
| 436 | 559 | const char *zName; /* Name of the attached file */ |
| 437 | 560 | const char *zDesc; /* Description of the attached file */ |
| 438 | 561 | const char *zWikiName = 0; /* Wiki page name when attached to Wiki */ |
| 439 | 562 | const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */ |
| 440 | 563 | const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */ |
| 564 | + const char *zForumPost = 0; /* Forum UID when attached to forum post */ |
| 441 | 565 | int modPending; /* True if awaiting moderation */ |
| 442 | 566 | const char *zModAction; /* Moderation action or NULL */ |
| 443 | 567 | int isModerator; /* TRUE if user is the moderator */ |
| 444 | 568 | const char *zMime; /* MIME Type */ |
| 445 | 569 | Blob attach; /* Content of the attachment */ |
| 446 | | - int fShowContent = 0; |
| 570 | + int fShowContent = 0; /* True to emit the content */ |
| 571 | + int bUserIsOwner = 0; /* True if pAttach->zUser is login_name() */ |
| 572 | + int showDelMenu = 0; /* True to enable delete option */ |
| 447 | 573 | const char *zLn = P("ln"); |
| 448 | 574 | |
| 449 | 575 | login_check_credentials(); |
| 450 | 576 | if( !g.perm.RdTkt && !g.perm.RdWiki ){ |
| 451 | 577 | login_needed(g.anon.RdTkt || g.anon.RdWiki); |
| 452 | 578 | return; |
| 453 | 579 | } |
| 454 | 580 | rid = name_to_rid_www("name"); |
| 455 | 581 | if( rid==0 ){ fossil_redirect_home(); } |
| 456 | | - zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 582 | + zUuid = rid_to_uuid(rid); |
| 457 | 583 | pAttach = manifest_get(rid, CFTYPE_ATTACHMENT, 0); |
| 458 | 584 | if( pAttach==0 ) fossil_redirect_home(); |
| 585 | + bUserIsOwner = |
| 586 | + 0==fossil_strcmp(pAttach->zUser, login_name()) |
| 587 | + && login_is_individual(); |
| 459 | 588 | zTarget = pAttach->zAttachTarget; |
| 460 | 589 | zSrc = pAttach->zAttachSrc; |
| 461 | 590 | ridSrc = db_int(0,"SELECT rid FROM blob WHERE uuid='%q'", zSrc); |
| 462 | 591 | zName = pAttach->zAttachName; |
| 463 | 592 | zDesc = pAttach->zComment; |
| 464 | 593 | zMime = mimetype_from_name(zName); |
| 465 | 594 | fShowContent = zMime ? strncmp(zMime,"text/", 5)==0 : 0; |
| 466 | | - if( validate16(zTarget, strlen(zTarget)) |
| 595 | + if( db_int(0,"SELECT 1 FROM event WHERE objid=%d and type='f'", rid) ){ |
| 596 | + if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } |
| 597 | + showDelMenu = g.perm.Admin || bUserIsOwner; |
| 598 | + zForumPost = zTarget; |
| 599 | + }else if( validate16(zTarget, strlen(zTarget)) |
| 467 | 600 | && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget) |
| 468 | 601 | ){ |
| 469 | | - zTktUuid = zTarget; |
| 470 | | - if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } |
| 471 | | - if( g.perm.WrTkt ){ |
| 472 | | - style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid); |
| 473 | | - } |
| 474 | | - }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){ |
| 475 | | - zWikiName = zTarget; |
| 476 | | - if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 477 | | - if( g.perm.WrWiki ){ |
| 478 | | - style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid); |
| 479 | | - } |
| 480 | | - }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){ |
| 481 | | - zTNUuid = zTarget; |
| 482 | | - if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 483 | | - if( g.perm.Write && g.perm.WrWiki ){ |
| 484 | | - style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid); |
| 485 | | - } |
| 602 | + if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } |
| 603 | + zTktUuid = zTarget; |
| 604 | + showDelMenu = g.perm.WrTkt; |
| 605 | + }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){ |
| 606 | + if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 607 | + zWikiName = zTarget; |
| 608 | + showDelMenu = g.perm.WrWiki; |
| 609 | + }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){ |
| 610 | + if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 611 | + zTNUuid = zTarget; |
| 612 | + showDelMenu = g.perm.Write && g.perm.WrWiki; |
| 613 | + } |
| 614 | + if( showDelMenu ){ |
| 615 | + style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid); |
| 486 | 616 | } |
| 487 | 617 | zDate = db_text(0, "SELECT datetime(%.12f)", pAttach->rDate); |
| 488 | 618 | |
| 489 | 619 | if( P("confirm") |
| 490 | | - && ((zTktUuid && g.perm.WrTkt) || |
| 620 | + && cgi_csrf_safe(2) |
| 621 | + && ((zForumPost |
| 622 | + && ((bUserIsOwner && g.perm.AttachForum) || |
| 623 | + forumpost_may_close())) || |
| 624 | + (zTktUuid && g.perm.WrTkt) || |
| 491 | 625 | (zWikiName && g.perm.WrWiki) || |
| 492 | 626 | (zTNUuid && g.perm.Write && g.perm.WrWiki)) |
| 493 | 627 | ){ |
| 628 | + /* Delete attachment. */ |
| 494 | 629 | int i, n, rid; |
| 495 | | - char *zDate; |
| 630 | + char *zNewDate; |
| 496 | 631 | Blob manifest; |
| 497 | 632 | Blob cksum; |
| 498 | 633 | const char *zFile = zName; |
| 499 | 634 | |
| 635 | + if( !bUserIsOwner ){ |
| 636 | + if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){ |
| 637 | + webpage_error("Only admins can delete other users' attachments."); |
| 638 | + } |
| 639 | + } |
| 500 | 640 | db_begin_transaction(); |
| 501 | 641 | blob_zero(&manifest); |
| 502 | 642 | for(i=n=0; zFile[i]; i++){ |
| 503 | 643 | if( zFile[i]=='/' || zFile[i]=='\\' ) n = i; |
| 504 | 644 | } |
| 505 | 645 | zFile += n; |
| 506 | 646 | if( zFile[0]==0 ) zFile = "unknown"; |
| 507 | 647 | blob_appendf(&manifest, "A %F %F\n", zFile, zTarget); |
| 508 | | - zDate = date_in_standard_format("now"); |
| 509 | | - blob_appendf(&manifest, "D %s\n", zDate); |
| 648 | + zNewDate = date_in_standard_format("now"); |
| 649 | + blob_appendf(&manifest, "D %s\n", zNewDate); |
| 510 | 650 | blob_appendf(&manifest, "U %F\n", login_name()); |
| 511 | 651 | md5sum_blob(&manifest, &cksum); |
| 512 | 652 | blob_appendf(&manifest, "Z %b\n", &cksum); |
| 513 | 653 | rid = content_put(&manifest); |
| 514 | 654 | manifest_crosslink(rid, &manifest, MC_NONE); |
| 515 | 655 | db_end_transaction(0); |
| 516 | 656 | @ <p>The attachment below has been deleted.</p> |
| 657 | + fossil_free(zNewDate); |
| 517 | 658 | } |
| 518 | 659 | |
| 519 | 660 | if( P("del") |
| 520 | | - && ((zTktUuid && g.perm.WrTkt) || |
| 661 | + && ((zForumPost && (bUserIsOwner || forumpost_may_close())) || |
| 662 | + (zTktUuid && g.perm.WrTkt) || |
| 521 | 663 | (zWikiName && g.perm.WrWiki) || |
| 522 | 664 | (zTNUuid && g.perm.Write && g.perm.WrWiki)) |
| 523 | 665 | ){ |
| 524 | 666 | form_begin(0, "%R/ainfo/%!S", zUuid); |
| 525 | 667 | @ <p>Confirm you want to delete the attachment shown below. |
| 526 | 668 | @ <input type="submit" name="confirm" value="Confirm"> |
| 669 | + login_insert_csrf_secret(); |
| 527 | 670 | @ </form> |
| 528 | 671 | } |
| 529 | 672 | |
| 530 | 673 | isModerator = g.perm.Admin || |
| 531 | | - (zTktUuid && g.perm.ModTkt) || |
| 532 | | - (zWikiName && g.perm.ModWiki); |
| 533 | | - if( isModerator && (zModAction = P("modaction"))!=0 ){ |
| 674 | + (zForumPost && g.perm.ModForum) || |
| 675 | + (zTktUuid && g.perm.ModTkt) || |
| 676 | + (zWikiName && g.perm.ModWiki); |
| 677 | + zModAction = P("modaction"); |
| 678 | + if( zModAction!=0 && cgi_csrf_safe(2) ){ |
| 534 | 679 | if( strcmp(zModAction,"delete")==0 ){ |
| 535 | | - moderation_disapprove(rid); |
| 536 | | - if( zTktUuid ){ |
| 680 | + if( isModerator || bUserIsOwner ){ |
| 681 | + moderation_disapprove(rid); |
| 682 | + } |
| 683 | + if( zForumPost ){ |
| 684 | + cgi_redirectf("%R/forumpost/%!S", zForumPost); |
| 685 | + }else if( zTktUuid ){ |
| 537 | 686 | cgi_redirectf("%R/tktview/%!S", zTktUuid); |
| 538 | | - }else{ |
| 687 | + }else if( zWikiName ) { |
| 539 | 688 | cgi_redirectf("%R/wiki?name=%t", zWikiName); |
| 540 | 689 | } |
| 690 | + /* zTNUuid is intentionally unhandled. Tech note attachments |
| 691 | + ** don't go through moderation. */ |
| 541 | 692 | return; |
| 542 | 693 | } |
| 543 | | - if( strcmp(zModAction,"approve")==0 ){ |
| 694 | + if( isModerator && strcmp(zModAction,"approve")==0 ){ |
| 544 | 695 | moderation_approve('a', rid); |
| 545 | 696 | } |
| 546 | 697 | } |
| 547 | 698 | style_set_current_feature("attach"); |
| 548 | 699 | style_header("Attachment Details"); |
| | @@ -558,19 +709,20 @@ |
| 558 | 709 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 559 | 710 | if( g.perm.Setup ){ |
| 560 | 711 | @ (%d(rid)) |
| 561 | 712 | } |
| 562 | 713 | modPending = moderation_pending_www(rid); |
| 563 | | - if( zTktUuid ){ |
| 714 | + if( zForumPost ){ |
| 715 | + @ <tr><th>Forum Post:</th> |
| 716 | + @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a></td></tr> |
| 717 | + }else if( zTktUuid ){ |
| 564 | 718 | @ <tr><th>Ticket:</th> |
| 565 | 719 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 566 | | - } |
| 567 | | - if( zTNUuid ){ |
| 720 | + }else if( zTNUuid ){ |
| 568 | 721 | @ <tr><th>Tech Note:</th> |
| 569 | 722 | @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr> |
| 570 | | - } |
| 571 | | - if( zWikiName ){ |
| 723 | + }else if( zWikiName ){ |
| 572 | 724 | @ <tr><th>Wiki Page:</th> |
| 573 | 725 | @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a></td></tr> |
| 574 | 726 | } |
| 575 | 727 | @ <tr><th>Date:</th><td> |
| 576 | 728 | hyperlink_to_date(zDate, "</td></tr>"); |
| | @@ -586,66 +738,89 @@ |
| 586 | 738 | @ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr> |
| 587 | 739 | } |
| 588 | 740 | @ <tr><th valign="top">Description:</th><td valign="top">%h(zDesc)</td></tr> |
| 589 | 741 | @ </table> |
| 590 | 742 | |
| 591 | | - if( isModerator && modPending ){ |
| 743 | + if( modPending && (isModerator || bUserIsOwner) ){ |
| 592 | 744 | @ <div class="section">Moderation</div> |
| 593 | 745 | @ <blockquote> |
| 594 | 746 | form_begin(0, "%R/ainfo/%s", zUuid); |
| 595 | 747 | @ <label><input type="radio" name="modaction" value="delete"> |
| 596 | | - @ Delete this change</label><br> |
| 597 | | - @ <label><input type="radio" name="modaction" value="approve"> |
| 598 | | - @ Approve this change</label><br> |
| 748 | + @ Delete this attachment</label><br> |
| 749 | + if( isModerator ){ |
| 750 | + @ <label><input type="radio" name="modaction" value="approve"> |
| 751 | + @ Approve this attachment</label><br> |
| 752 | + } |
| 599 | 753 | @ <input type="submit" value="Submit"> |
| 754 | + login_insert_csrf_secret(); |
| 600 | 755 | @ </form> |
| 601 | 756 | @ </blockquote> |
| 602 | 757 | } |
| 603 | 758 | |
| 604 | | - @ <div class="section">Content Appended</div> |
| 605 | | - @ <blockquote> |
| 759 | + @ <div class="section">Content:</div> |
| 606 | 760 | blob_zero(&attach); |
| 607 | | - if( fShowContent ){ |
| 608 | | - const char *z; |
| 609 | | - content_get(ridSrc, &attach); |
| 610 | | - blob_to_utf8_no_bom(&attach, 0); |
| 611 | | - z = blob_str(&attach); |
| 612 | | - if( zLn ){ |
| 613 | | - output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1); |
| 614 | | - }else{ |
| 615 | | - @ <pre> |
| 616 | | - @ %h(z) |
| 617 | | - @ </pre> |
| 618 | | - } |
| 619 | | - }else if( strncmp(zMime, "image/", 6)==0 ){ |
| 620 | | - int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc); |
| 621 | | - @ <i>(file is %d(sz) bytes of image data)</i><br> |
| 622 | | - @ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img> |
| 623 | | - style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime); |
| 624 | | - }else{ |
| 625 | | - int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc); |
| 626 | | - @ <i>(file is %d(sz) bytes of binary data)</i> |
| 627 | | - } |
| 628 | | - @ </blockquote> |
| 761 | + if( modPending && !moderation_user_could(rid, 1, 0) ){ |
| 762 | + @ <p><span class="modpending">Content is awaiting moderator \ |
| 763 | + @ approval.</span></p> |
| 764 | + }else{ |
| 765 | + @ <blockquote> |
| 766 | + if( fShowContent ){ |
| 767 | + const char *z; |
| 768 | + content_get(ridSrc, &attach); |
| 769 | + blob_to_utf8_no_bom(&attach, 0); |
| 770 | + z = blob_str(&attach); |
| 771 | + if( zLn ){ |
| 772 | + output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1); |
| 773 | + }else{ |
| 774 | + @ <pre> |
| 775 | + @ %h(z) |
| 776 | + @ </pre> |
| 777 | + } |
| 778 | + }else if( strncmp(zMime, "image/", 6)==0 ){ |
| 779 | + int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc); |
| 780 | + @ <i>(file is %d(sz) bytes of image data)</i><br> |
| 781 | + @ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img> |
| 782 | + style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime); |
| 783 | + }else{ |
| 784 | + int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc); |
| 785 | + @ <i>(file is %d(sz) bytes of binary data)</i> |
| 786 | + } |
| 787 | + @ </blockquote> |
| 788 | + } |
| 629 | 789 | manifest_destroy(pAttach); |
| 630 | 790 | blob_reset(&attach); |
| 631 | 791 | style_finish_page(); |
| 632 | 792 | } |
| 793 | + |
| 794 | +#if INTERFACE |
| 795 | +/* |
| 796 | +** Flags for use with attachment_list(). ATTACHLIST_HRULE_ABOVE |
| 797 | +** must have a value of 1 for historical call compatibility. |
| 798 | +*/ |
| 799 | +#define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */ |
| 800 | +#define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */ |
| 801 | +#define ATTACHLIST_SIZE 0x04 /* add size */ |
| 802 | +#define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */ |
| 803 | +#endif |
| 633 | 804 | |
| 634 | 805 | /* |
| 635 | 806 | ** Output HTML to show a list of attachments. |
| 636 | 807 | */ |
| 637 | 808 | void attachment_list( |
| 638 | 809 | const char *zTarget, /* Object that things are attached to */ |
| 639 | 810 | const char *zHeader, /* Header to display with attachments */ |
| 640 | | - int fHorizontalRule /* Insert <hr> separator above header */ |
| 811 | + const int flags /* ATTACHLIST_... flags */ |
| 641 | 812 | ){ |
| 642 | 813 | int cnt = 0; |
| 814 | + char szBuf[36] = {0}; /* scratchpad for attachment size value */ |
| 815 | + const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags) |
| 816 | + ? " target=\"_blank\"" : ""; |
| 643 | 817 | Stmt q; |
| 644 | 818 | db_prepare(&q, |
| 645 | 819 | "SELECT datetime(mtime,toLocal()), filename, user," |
| 646 | | - " (SELECT uuid FROM blob WHERE rid=attachid), src" |
| 820 | + " (SELECT uuid FROM blob WHERE rid=attachid), src, target, " |
| 821 | + " attachid " |
| 647 | 822 | " FROM attachment" |
| 648 | 823 | " WHERE isLatest AND src!='' AND target=%Q" |
| 649 | 824 | " ORDER BY mtime DESC", |
| 650 | 825 | zTarget |
| 651 | 826 | ); |
| | @@ -653,34 +828,55 @@ |
| 653 | 828 | const char *zDate = db_column_text(&q, 0); |
| 654 | 829 | const char *zFile = db_column_text(&q, 1); |
| 655 | 830 | const char *zUser = db_column_text(&q, 2); |
| 656 | 831 | const char *zUuid = db_column_text(&q, 3); |
| 657 | 832 | const char *zSrc = db_column_text(&q, 4); |
| 833 | + const char *zTarget = db_column_text(&q, 5); |
| 658 | 834 | const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous"; |
| 835 | + const char *zTypeArg = 0; /* URL arg name for /attachdownload */ |
| 836 | + const int aid = db_column_int(&q, 6); |
| 837 | + const int iAType = attachment_target_type(zTarget); |
| 838 | + if( (flags & ATTACHLIST_HIDE_UNAPPROVED) |
| 839 | + && moderation_pending(aid) |
| 840 | + && !moderation_user_could(aid, 1, 0) ){ |
| 841 | + continue; |
| 842 | + } |
| 659 | 843 | if( cnt==0 ){ |
| 660 | 844 | @ <section class='attachlist'> |
| 661 | | - if( fHorizontalRule ){ |
| 845 | + if( flags & ATTACHLIST_HRULE_ABOVE ){ |
| 662 | 846 | @ <hr> |
| 663 | 847 | } |
| 664 | 848 | @ %s(zHeader) |
| 665 | 849 | @ <ul> |
| 666 | 850 | } |
| 667 | 851 | cnt++; |
| 852 | + switch( iAType ){ |
| 853 | + case CFTYPE_TICKET: zTypeArg = "tkt"; break; |
| 854 | + case CFTYPE_FORUM: zTypeArg = "forumpost"; break; |
| 855 | + case CFTYPE_EVENT: zTypeArg = "technote"; break; |
| 856 | + case CFTYPE_WIKI: |
| 857 | + default: zTypeArg = "page"; break; |
| 858 | + } |
| 668 | 859 | @ <li> |
| 669 | | - @ %z(href("%R/artifact/%!S",zSrc))%h(zFile)</a> |
| 670 | | - @ [<a href="%R/attachdownload/%t(zFile)?page=%t(zTarget)&file=%t(zFile)">download</a>] |
| 860 | + @ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a> |
| 861 | + if( flags & ATTACHLIST_SIZE ){ |
| 862 | + const int sz = db_int(0,"SELECT size FROM blob WHERE uuid=%Q", zSrc); |
| 863 | + sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz); |
| 864 | + } |
| 865 | + @ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\ |
| 866 | + @&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)] |
| 671 | 867 | @ added by %h(zDispUser) on |
| 672 | 868 | hyperlink_to_date(zDate, "."); |
| 673 | | - @ [%z(href("%R/ainfo/%!S",zUuid))details</a>] |
| 869 | + @ [<a href="%R/ainfo/%!S(zUuid)"%s(zLinkTgt)>details</a>] |
| 870 | + moderation_pending_www(aid); |
| 674 | 871 | @ </li> |
| 675 | 872 | } |
| 676 | 873 | if( cnt ){ |
| 677 | 874 | @ </ul> |
| 678 | 875 | @ </section> |
| 679 | 876 | } |
| 680 | 877 | db_finalize(&q); |
| 681 | | - |
| 682 | 878 | } |
| 683 | 879 | |
| 684 | 880 | /* |
| 685 | 881 | ** COMMAND: attachment* |
| 686 | 882 | ** |
| 687 | 883 | |