1# -- 2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 3# -- 4# This software comes with ABSOLUTELY NO WARRANTY. For details, see 5# the enclosed file COPYING for license information (GPL). If you 6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. 7# -- 8 9package Kernel::System::FormDraft; 10 11use strict; 12use warnings; 13 14use Kernel::System::VariableCheck qw(:all); 15use MIME::Base64; 16use Storable; 17 18our @ObjectDependencies = ( 19 'Kernel::System::Cache', 20 'Kernel::System::DB', 21 'Kernel::System::Log', 22 'Kernel::System::Storable', 23); 24 25=head1 NAME 26 27Kernel::System::FormDraft - draft lib 28 29=head1 SYNOPSIS 30 31All draft functions. 32 33=head1 PUBLIC INTERFACE 34 35=over 4 36 37=cut 38 39=item new() 40 41create an object 42 43 use Kernel::System::ObjectManager; 44 local $Kernel::OM = Kernel::System::ObjectManager->new(); 45 my $FormDraftObject = $Kernel::OM->Get('Kernel::System::FormDraft'); 46 47=cut 48 49sub new { 50 my ( $Type, %Param ) = @_; 51 52 # allocate new hash for object 53 my $Self = {}; 54 bless( $Self, $Type ); 55 56 $Self->{CacheType} = 'FormDraft'; 57 $Self->{CacheTTL} = 60 * 60 * 24 * 30; 58 59 return $Self; 60} 61 62=item FormDraftGet() 63 64get draft attributes 65 66 my $FormDraft = $FormDraftObject->FormDraftGet( 67 FormDraftID => 123, 68 GetContent => 1, # optional, default 1 69 UserID => 123, 70 ); 71 72Returns (with GetContent = 0): 73 74 $FormDraft = { 75 FormDraftID => 123, 76 ObjectType => 'Ticket', 77 ObjectID => 12, 78 Action => 'AgentTicketCompose', 79 CreateTime => '2016-04-07 15:41:15', 80 CreateBy => 1, 81 ChangeTime => '2016-04-07 15:59:45', 82 ChangeBy => 2, 83 }; 84 85Returns (without GetContent or GetContent = 1): 86 87 $FormDraft = { 88 FormData => { 89 InformUserID => [ 123, 124, ], 90 Subject => 'Request for information', 91 ... 92 }, 93 FileData => [ 94 { 95 'Content' => 'Dear customer\n\nthank you!', 96 'ContentType' => 'text/plain', 97 'ContentID' => undef, # optional 98 'Filename' => 'thankyou.txt', 99 'Filesize' => 25, 100 'FileID' => 1, 101 'Disposition' => 'attachment', 102 }, 103 ... 104 ], 105 FormDraftID => 123, 106 ObjectType => 'Ticket', 107 ObjectID => 12, 108 Action => 'AgentTicketCompose', 109 CreateTime => '2016-04-07 15:41:15', 110 CreateBy => 1, 111 ChangeTime => '2016-04-07 15:59:45', 112 ChangeBy => 2, 113 Title => 'my draft', 114 }; 115 116=cut 117 118sub FormDraftGet { 119 my ( $Self, %Param ) = @_; 120 121 # check needed stuff 122 for my $Needed (qw(FormDraftID UserID)) { 123 if ( !$Param{$Needed} ) { 124 $Kernel::OM->Get('Kernel::System::Log')->Log( 125 Priority => 'error', 126 Message => "Need $Needed!", 127 ); 128 return; 129 } 130 } 131 132 # determine if we should get content 133 $Param{GetContent} //= 1; 134 if ( $Param{GetContent} !~ m{ \A [01] \z }xms ) { 135 $Kernel::OM->Get('Kernel::System::Log')->Log( 136 Priority => 'error', 137 Message => "Invalid value '$Param{GetContent}' for GetContent!", 138 ); 139 return; 140 } 141 142 # check cache 143 my $CacheKey = 'FormDraftGet::GetContent' . $Param{GetContent} . '::ID' . $Param{FormDraftID}; 144 my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get( 145 Type => $Self->{CacheType}, 146 Key => $CacheKey, 147 ); 148 return $Cache if $Cache; 149 150 # get database object 151 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 152 153 # prepare query 154 my $SQL = 155 'SELECT id, object_type, object_id, action, title,' 156 . ' create_time, create_by, change_time, change_by'; 157 158 my @EncodeColumns = ( 1, 1, 1, 1, 1, 1, 1, 1, 1 ); 159 if ( $Param{GetContent} ) { 160 $SQL .= ', content'; 161 push @EncodeColumns, 0; 162 } 163 $SQL .= ' FROM form_draft WHERE id = ?'; 164 165 # ask the database 166 return if !$DBObject->Prepare( 167 SQL => $SQL, 168 Bind => [ \$Param{FormDraftID} ], 169 Limit => 1, 170 Encode => \@EncodeColumns, 171 ); 172 173 # fetch the result 174 my %FormDraft; 175 while ( my @Row = $DBObject->FetchrowArray() ) { 176 %FormDraft = ( 177 FormDraftID => $Row[0], 178 ObjectType => $Row[1], 179 ObjectID => $Row[2], 180 Action => $Row[3], 181 Title => $Row[4] || '', 182 CreateTime => $Row[5], 183 CreateBy => $Row[6], 184 ChangeTime => $Row[7], 185 ChangeBy => $Row[8], 186 ); 187 188 if ( $Param{GetContent} ) { 189 190 my $RawContent = $Row[9] // {}; 191 my $StorableContent = $RawContent; 192 193 if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) { 194 $StorableContent = MIME::Base64::decode_base64($RawContent); 195 } 196 197 # convert form and file data from yaml 198 my $Content = $Kernel::OM->Get('Kernel::System::Storable')->Deserialize( Data => $StorableContent ) // {}; 199 200 $FormDraft{FormData} = $Content->{FormData}; 201 $FormDraft{FileData} = $Content->{FileData}; 202 } 203 } 204 205 # no data found 206 if ( !%FormDraft ) { 207 $Kernel::OM->Get('Kernel::System::Log')->Log( 208 Priority => 'error', 209 Message => "FormDraft with ID '$Param{FormDraftID}' not found!", 210 ); 211 return; 212 } 213 214 # always cache version without content 215 my $CacheKeyNoContent; 216 my %FormDraftNoContent; 217 if ( $Param{GetContent} ) { 218 $CacheKeyNoContent = 'FormDraftGet::GetContent0::ID' . $Param{FormDraftID}; 219 %FormDraftNoContent = %{ Storable::dclone( \%FormDraft ) }; 220 delete $FormDraftNoContent{FileData}; 221 delete $FormDraftNoContent{FormData}; 222 } 223 else { 224 $CacheKeyNoContent = $CacheKey; 225 %FormDraftNoContent = %FormDraft; 226 } 227 $Kernel::OM->Get('Kernel::System::Cache')->Set( 228 Type => $Self->{CacheType}, 229 Key => $CacheKeyNoContent, 230 Value => \%FormDraftNoContent, 231 ); 232 233 return \%FormDraft if !$Param{GetContent}; 234 235 # set cache with content (shorter cache time due to potentially large content) 236 $Kernel::OM->Get('Kernel::System::Cache')->Set( 237 Type => $Self->{CacheType}, 238 TTL => 60 * 60, 239 Key => $CacheKey, 240 Value => \%FormDraft, 241 ); 242 243 return \%FormDraft; 244} 245 246=item FormDraftAdd() 247 248add a new draft 249 250 my $Success = $FormDraftObject->FormDraftAdd( 251 FormData => { 252 InformUserID => [ 123, 124, ], 253 Subject => 'Request for information', 254 ... 255 }, 256 FileData => [ # optional 257 { 258 'Content' => 'Dear customer\n\nthank you!', 259 'ContentType' => 'text/plain', 260 'ContentID' => undef, # optional 261 'Filename' => 'thankyou.txt', 262 'Filesize' => 25, 263 'FileID' => 1, 264 'Disposition' => 'attachment', 265 }, 266 ... 267 ], 268 ObjectType => 'Ticket', 269 ObjectID => 12, 270 Action => 'AgentTicketCompose', 271 Title => 'my draft', # optional 272 UserID => 123, 273 ); 274 275=cut 276 277sub FormDraftAdd { 278 my ( $Self, %Param ) = @_; 279 280 # check needed stuff 281 for my $Needed (qw(FormData ObjectType Action)) { 282 if ( !$Param{$Needed} ) { 283 $Kernel::OM->Get('Kernel::System::Log')->Log( 284 Priority => 'error', 285 Message => "Need $Needed!", 286 ); 287 return; 288 } 289 } 290 for my $Needed (qw(ObjectID UserID)) { 291 if ( !IsInteger( $Param{$Needed} ) ) { 292 $Kernel::OM->Get('Kernel::System::Log')->Log( 293 Priority => 'error', 294 Message => "Need $Needed!", 295 ); 296 return; 297 } 298 } 299 300 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 301 302 # serialize form and file data 303 my $StorableContent = $Kernel::OM->Get('Kernel::System::Storable')->Serialize( 304 Data => { 305 FormData => $Param{FormData}, 306 FileData => $Param{FileData} || [], 307 }, 308 ); 309 310 my $Content = $StorableContent; 311 if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) { 312 $Content = MIME::Base64::encode_base64($StorableContent); 313 } 314 315 # add to database 316 return if !$DBObject->Do( 317 SQL => 318 'INSERT INTO form_draft' 319 . ' (object_type, object_id, action, title, content, create_time, create_by, change_time, change_by)' 320 . ' VALUES (?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)', 321 Bind => [ 322 \$Param{ObjectType}, \$Param{ObjectID}, \$Param{Action}, \$Param{Title}, \$Content, 323 \$Param{UserID}, \$Param{UserID}, 324 ], 325 ); 326 327 # delete affected caches 328 $Self->_DeleteAffectedCaches(%Param); 329 330 return 1; 331} 332 333=item FormDraftUpdate() 334 335update an existing draft 336 337 my $Success = $FormDraftObject->FormDraftUpdate( 338 FormData => { 339 InformUserID => [ 123, 124, ], 340 Subject => 'Request for information', 341 ... 342 }, 343 FileData => [ # optional 344 { 345 'Content' => 'Dear customer\n\nthank you!', 346 'ContentType' => 'text/plain', 347 'ContentID' => undef, # optional 348 'Filename' => 'thankyou.txt', 349 'Filesize' => 25, 350 'FileID' => 1, 351 'Disposition' => 'attachment', 352 }, 353 ... 354 ], 355 ObjectType => 'Ticket', 356 ObjectID => 12, 357 Action => 'AgentTicketCompose', 358 Title => 'my draft', 359 FormDraftID => 1, 360 UserID => 123, 361 ); 362 363=cut 364 365sub FormDraftUpdate { 366 my ( $Self, %Param ) = @_; 367 368 # check needed stuff 369 for my $Needed (qw(FormData ObjectType Action)) { 370 if ( !$Param{$Needed} ) { 371 $Kernel::OM->Get('Kernel::System::Log')->Log( 372 Priority => 'error', 373 Message => "Need $Needed!", 374 ); 375 return; 376 } 377 } 378 for my $Needed (qw(ObjectID FormDraftID UserID)) { 379 if ( !IsInteger( $Param{$Needed} ) ) { 380 $Kernel::OM->Get('Kernel::System::Log')->Log( 381 Priority => 'error', 382 Message => "Need $Needed!", 383 ); 384 return; 385 } 386 } 387 388 # check if specified draft already exists and do sanity checks 389 my $FormDraft = $Self->FormDraftGet( 390 FormDraftID => $Param{FormDraftID}, 391 GetContent => 0, 392 UserID => $Param{UserID}, 393 ); 394 if ( !$FormDraft ) { 395 $Kernel::OM->Get('Kernel::System::Log')->Log( 396 Priority => 'error', 397 Message => "FormDraft with ID '$Param{FormDraftID}' not found!", 398 ); 399 return; 400 } 401 VALIDATEPARAM: 402 for my $ValidateParam (qw(ObjectType ObjectID Action)) { 403 next VALIDATEPARAM if $Param{$ValidateParam} eq $FormDraft->{$ValidateParam}; 404 405 $Kernel::OM->Get('Kernel::System::Log')->Log( 406 Priority => 'error', 407 Message => 408 "Param '$ValidateParam' for draft with ID '$Param{FormDraftID}'" 409 . " must not be changed on update!", 410 ); 411 return; 412 } 413 414 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 415 416 # serialize form and file data 417 my $StorableContent = $Kernel::OM->Get('Kernel::System::Storable')->Serialize( 418 Data => { 419 FormData => $Param{FormData}, 420 FileData => $Param{FileData} || [], 421 }, 422 ); 423 424 my $Content = $StorableContent; 425 if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) { 426 $Content = MIME::Base64::encode_base64($StorableContent); 427 } 428 429 # add to database 430 return if !$DBObject->Do( 431 SQL => 432 'UPDATE form_draft' 433 . ' SET title = ?, content = ?, change_time = current_timestamp, change_by = ?' 434 . ' WHERE id = ?', 435 Bind => [ \$Param{Title}, \$Content, \$Param{UserID}, \$Param{FormDraftID}, ], 436 ); 437 438 # delete affected caches 439 $Self->_DeleteAffectedCaches(%Param); 440 441 return 1; 442} 443 444=item FormDraftDelete() 445 446remove draft 447 448 my $Success = $FormDraftObject->FormDraftDelete( 449 FormDraftID => 123, 450 UserID => 123, 451 ); 452 453=cut 454 455sub FormDraftDelete { 456 my ( $Self, %Param ) = @_; 457 458 # check needed stuff 459 for my $Needed (qw(FormDraftID UserID)) { 460 if ( !$Param{$Needed} ) { 461 $Kernel::OM->Get('Kernel::System::Log')->Log( 462 Priority => 'error', 463 Message => "Need $Needed!", 464 ); 465 return; 466 } 467 } 468 469 # get draft data as sanity check and to determine which caches should be removed 470 # use database query directly (we don't need raw content) 471 my $FormDraft = $Self->FormDraftGet( 472 FormDraftID => $Param{FormDraftID}, 473 GetContent => 0, 474 UserID => $Param{UserID}, 475 ); 476 if ( !$FormDraft ) { 477 $Kernel::OM->Get('Kernel::System::Log')->Log( 478 Priority => 'error', 479 Message => "FormDraft with ID '$Param{FormDraftID}' not found!", 480 ); 481 return; 482 } 483 484 # remove from database 485 return if !$Kernel::OM->Get('Kernel::System::DB')->Do( 486 SQL => 'DELETE FROM form_draft WHERE id = ?', 487 Bind => [ \$Param{FormDraftID} ], 488 ); 489 490 # delete affected caches 491 $Self->_DeleteAffectedCaches( %{$FormDraft} ); 492 493 return 1; 494} 495 496=item FormDraftListGet() 497 498get list of drafts, optionally filtered by object type, object id and action 499 500 my $FormDraftList = $FormDraftObject->FormDraftListGet( 501 ObjectType => 'Ticket', # optional 502 ObjectID => 123, # optional 503 Action => 'AgentTicketCompose', # optional 504 UserID => 123, 505 ); 506 507Returns: 508 509 $FormDraftList = [ 510 { 511 FormDraftID => 123, 512 ObjectType => 'Ticket', 513 ObjectID => 12, 514 Action => 'AgentTicketCompose', 515 Title => 'my draft', 516 CreateTime => '2016-04-07 15:41:15', 517 CreateBy => 1, 518 ChangeTime => '2016-04-07 15:59:45', 519 ChangeBy => 2, 520 }, 521 ... 522 ]; 523 524=cut 525 526sub FormDraftListGet { 527 my ( $Self, %Param ) = @_; 528 529 # check needed stuff 530 if ( !$Param{UserID} ) { 531 $Kernel::OM->Get('Kernel::System::Log')->Log( 532 Priority => 'error', 533 Message => 'Need UserID!', 534 ); 535 return; 536 } 537 538 # check cache 539 my $CacheKey = 'FormDraftListGet'; 540 RESTRICTION: 541 for my $Restriction (qw(ObjectType Action ObjectID)) { 542 next RESTRICTION if !IsStringWithData( $Param{$Restriction} ); 543 $CacheKey .= '::' . $Restriction . $Param{$Restriction}; 544 } 545 my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get( 546 Type => $Self->{CacheType}, 547 Key => $CacheKey, 548 ); 549 return $Cache if $Cache; 550 551 # prepare database restrictions by given parameters 552 my %ParamToField = ( 553 ObjectType => 'object_type', 554 Action => 'action', 555 ObjectID => 'object_id', 556 ); 557 my $SQLExt = ''; 558 my @Bind; 559 RESTRICTION: 560 for my $Restriction (qw(ObjectType Action ObjectID)) { 561 next RESTRICTION if !IsStringWithData( $Param{$Restriction} ); 562 $SQLExt .= $SQLExt ? ' AND ' : ' WHERE '; 563 $SQLExt .= $ParamToField{$Restriction} . ' = ?'; 564 push @Bind, \$Param{$Restriction}; 565 } 566 567 # get database object 568 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 569 570 # ask the database 571 return if !$DBObject->Prepare( 572 SQL => 573 'SELECT id, object_type, object_id, action, title,' 574 . ' create_time, create_by, change_time, change_by' 575 . ' FROM form_draft' . $SQLExt 576 . ' ORDER BY id ASC', 577 Bind => \@Bind, 578 ); 579 580 # fetch the results 581 my @FormDrafts; 582 while ( my @Row = $DBObject->FetchrowArray() ) { 583 push @FormDrafts, { 584 FormDraftID => $Row[0], 585 ObjectType => $Row[1], 586 ObjectID => $Row[2], 587 Action => $Row[3], 588 Title => $Row[4] || '', 589 CreateTime => $Row[5], 590 CreateBy => $Row[6], 591 ChangeTime => $Row[7], 592 ChangeBy => $Row[8], 593 }; 594 } 595 596 # set cache 597 $Kernel::OM->Get('Kernel::System::Cache')->Set( 598 Type => $Self->{CacheType}, 599 Key => $CacheKey, 600 Value => \@FormDrafts, 601 ); 602 603 return \@FormDrafts; 604} 605 606=item _DeleteAffectedCaches() 607 608remove all potentially affected caches 609 610 my $Success = $FormDraftObject->_DeleteAffectedCaches( 611 FormDraftID => 1, # optional 612 ObjectType => 'Ticket', 613 ObjectID => 12, 614 Action => 'AgentTicketCompose', 615 ); 616 617=cut 618 619sub _DeleteAffectedCaches { 620 my ( $Self, %Param ) = @_; 621 622 # prepare affected cache keys 623 my @CacheKeys = ( 624 'FormDraftListGet', 625 'FormDraftListGet::ObjectType' . $Param{ObjectType}, 626 'FormDraftListGet::Action' . $Param{Action}, 627 'FormDraftListGet::ObjectID' . $Param{ObjectID}, 628 'FormDraftListGet::ObjectType' . $Param{ObjectType} 629 . '::Action' . $Param{Action}, 630 'FormDraftListGet::ObjectType' . $Param{ObjectType} 631 . '::ObjectID' . $Param{ObjectID}, 632 'FormDraftListGet::Action' . $Param{Action} 633 . '::ObjectID' . $Param{ObjectID}, 634 'FormDraftListGet::ObjectType' . $Param{ObjectType} 635 . '::Action' . $Param{Action} 636 . '::ObjectID' . $Param{ObjectID}, 637 ); 638 if ( $Param{FormDraftID} ) { 639 push @CacheKeys, 640 'FormDraftGet::GetContent0::ID' . $Param{FormDraftID}, 641 'FormDraftGet::GetContent1::ID' . $Param{FormDraftID}; 642 } 643 644 # delete affected caches 645 my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache'); 646 for my $CacheKey (@CacheKeys) { 647 $CacheObject->Delete( 648 Type => $Self->{CacheType}, 649 Key => $CacheKey, 650 ); 651 } 652 653 return 1; 654} 655 6561; 657 658=back 659 660=head1 TERMS AND CONDITIONS 661 662This software is part of the OTRS project (L<https://otrs.org/>). 663 664This software comes with ABSOLUTELY NO WARRANTY. For details, see 665the enclosed file COPYING for license information (GPL). If you 666did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>. 667 668=cut 669