diff --git a/CHANGELOG b/CHANGELOG index 9ffacd301..592890c9b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,306 @@ +-------------------------------------------------------------------------------- + Changes in version 6.0.24 +-------------------------------------------------------------------------------- +- add task to send list of recent changes by email +- merge changes up to 5.1.31 + +-------------------------------------------------------------------------------- + Changes in version 6.0.23 +-------------------------------------------------------------------------------- +- fix setting recipients and revisors +- check in of a document is allowed for the user having done the check out + or those users with unlimited access rights on the document +- merge changes up to 5.1.30 + +-------------------------------------------------------------------------------- + Changes in version 6.0.22 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.29 + +-------------------------------------------------------------------------------- + Changes in version 6.0.21 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.28 +- add new check for documents with identical sequence numbers in a folder + +-------------------------------------------------------------------------------- + Changes in version 6.0.20 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.27 +- fix triggering workflow (Closes: #542) +- create original file name from new document name when uploading document + from the library folder. Used to be the original file name + +-------------------------------------------------------------------------------- + Changes in version 6.0.19 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.26 +- fix deletion of tasks when using bootstrap4 theme +- fix deletion of documents when clicking on icon in document list (my documents) + +-------------------------------------------------------------------------------- + Changes in version 6.0.18 +-------------------------------------------------------------------------------- +- finish op/op.Cron.php, returns json +- merge changes up to 5.1.25 +- fix sending trigger workflow notification (Closes: #522) +- fix updating und deleting items in document lists +- call hook 'filenameDownloadItem' in search export and transmittal download +- fix possible xss attack in UsrMgr (CVE-2022-28479) + +-------------------------------------------------------------------------------- + Changes in version 6.0.17 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.24 +- send notification when a receiption of a document was submitted + +-------------------------------------------------------------------------------- + Changes in version 6.0.16 +-------------------------------------------------------------------------------- +- cancel checkout needs confirmation +- add input field to filter list of recipients if more then 10 +- add task for creating missing preview images +- no longer use old PHPExcel classes, use PhpOffice\PhpSpreadsheet\Spreadsheet + instead + +-------------------------------------------------------------------------------- + Changes in version 6.0.15 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.22 +- add a new task for checking the checksum of all document versions +- add searching for revision date +- list of open tasks will no longer contain expired documents but MyDocuments + page still list them +- fixed downloading approval file (Closes: #503) +- regular users can no longer set owner of document while uploading + +-------------------------------------------------------------------------------- + Changes in version 6.0.14 +-------------------------------------------------------------------------------- +- show debug menu only if debug mode is on +- merge changes up to 5.1.21 +- document links can be added by regular users again +- add list of checked out documents to tasks +- issue a warning when removing a document which is checked out +- checked out can be discarded if it was changed + +-------------------------------------------------------------------------------- + Changes in version 6.0.13 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.20 +- create download file for transmittal in system tmp (Closes: #478) +- sync source code of checkin with update document + +-------------------------------------------------------------------------------- + Changes in version 6.0.12 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.19 +- fix various errors concerning workflows +- show menu tasks even if not admin (Closes: #485) + +-------------------------------------------------------------------------------- + Changes in version 6.0.11 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.18 +- fix access restriction for roles (content of documents was visible even if the + role and status didn't allow it) +- fix missing Content-Type in UserList (Closes: #480) + +-------------------------------------------------------------------------------- + Changes in version 6.0.10 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.17 +- fix list of previous document versions (Closes: #471) +- fix uploading files with fine uploader (Closes: #472) +- clear revision date when all revisors have been deleted +- improve scheduler task management, tasks can be deleted, fix setting parameters +- add op.Cron.php for running all scheduled tasks + +-------------------------------------------------------------------------------- + Changes in version 6.0.9 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.16 +- fix removal of roles (Closes: #465) +- fix password forgotten process +- fix setting role of new user and retrieving role of existing user +- processes of users can be deleted again, instead of only transfered to + another user +- fix export of search results, headers of excel file can be translated +- fix arcordeon for folder filters on search page +- fix upload from dropfolder +- fix adding new calendar event + +-------------------------------------------------------------------------------- + Changes in version 6.0.8 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.15 +- fix syntax error in op/op.EditComment.php +- fix use of private variable in op/op.SetRecipients.php and op/op.SetRevisors.php +- fix triggering a transition in advanced workflow mode + +-------------------------------------------------------------------------------- + Changes in version 6.0.7 +-------------------------------------------------------------------------------- +- fix editing of document attachments +- make receipt summary look like approval/review summary +- merge changes up to 5.1.14 +- do not show the updating user in a revision workflow if the status is 0 + this is misleading because the user starting the revision workflow is the one + first accessing the document +- rejection of document receipts are turned off by default, but can be turned + on in the settings +- documents in DocumentChooser are sorted by name +- instead of just removing a user from all processes it can be replaced by a new user + +-------------------------------------------------------------------------------- + Changes in version 6.0.6 +-------------------------------------------------------------------------------- +- fix setting attributes when checking in a new document version +- setting a document revision to 'needs correction' will no longer set the + documents status to 'needѕ correction' if this was turned off in the settings +- a document will not leave draft status when setting the approver/reviewer + without setting a reviewer/approver +- tasks to be counted in menu can be configured +- add number of documents which need correction to menu +- minor 2 factor auth. fixes when initially setting the secret +- remove ѕome unneeded code from AddDocument which just caused php warnings +- do not set the uploader of new documents to owner if the owner is different from + the uploader +- add scheduler +- add hook showVersionComment in out.ViewDocument.php +- Various minor corrections of database tables tblWorkflowLog and + tblWorkflowDocumentContent +- merge changes up to 5.1.7 + +-------------------------------------------------------------------------------- + Changes in version 6.0.5 +-------------------------------------------------------------------------------- +- sync form for updating document by upload and checkin +- add list of documents which need correction on MyDocuments page + +-------------------------------------------------------------------------------- + Changes in version 6.0.4 +-------------------------------------------------------------------------------- +- merge changes up to 5.1.5 + +-------------------------------------------------------------------------------- + Changes in version 6.0.3 +-------------------------------------------------------------------------------- +- add list of documents without a receiver on MyDocuments page +- propperly calculate number of documents for each value of value set in attribute mgr +- output of progress bar for reception of a document can be controlled by access list +- recipientof a document version can be set when uploading the file +- fix export of search and display of 2nd, 3rd, ... search page +- speed up creation of document lists if reception progress bar is shown +- status of rejected documents can be overriden +- do not add users from group as recipients if they are the uploader or reviewer + of a document +- add list of documents without a receiver, list of drafts, and list of absolete + documents on MyDocuments page +- add callback onCheckAccessDocument to SeedDMS_Core_Document +- add new document status 'needs correction', revised documents which do not pass + will no longer be in status 'rejected' but 'needs correction' +- better error handling when indexing documents fails +- apache xsendfile module is used for downloading documents when installed +- add view access check for ApprovalSummary, ReviewSummary, ReceiptSummary, + WorkflowSummary, DocumentAccess, GroupView, UsrView, WorkflowSummary +- filter out reviewers and uploader of a document version when setting recipients + by user group + +-------------------------------------------------------------------------------- + Changes in version 6.0.2 +-------------------------------------------------------------------------------- +- check if user has access on document and is not disabled if set as + receiver, revisor +- check if group has members if set as reviewer, approver, receiver, revisor +- fix bug in notification of approver after successful review +- add document check for docs in revision and missing access rights of revisor +- add document check for docs requiring receptions but user lacks access right +- fix Acl manager when using pgsql +- list all open tasks of user in user info of user manager +- owner of document may see review/approval/receipt/revision log +- fix sending mails to reviewer/approvers after check in +- downloading of review/approval files works again +- optimizing retrieval of open tasks +- do not show user which has been removed from a process except for admins +- show scheduled revisions in calendar +- merge changes up to 5.1.5 + +-------------------------------------------------------------------------------- + Changes in version 6.0.1 +-------------------------------------------------------------------------------- +- call hook 'rawcontent' when downloading transmittal list or search content +- speed up list of locked documents on MyDocuments page +- sql queries and execution times can be written to file in database layer + +-------------------------------------------------------------------------------- + Changes in version 6.0.0 +-------------------------------------------------------------------------------- +- merge changes up to 5.0.10 +- filter documents by status 'draft' on search page +- list of documents to look at now contains documents in revision +- add list of documents waiting for reception on MyDocuments page +- group document lists on MyDocuments page into three sections +- show progressbar and comments for reception of document in documentlist +- restructure page for document/folder check, add check for missing access + on documents by recipient or revisor +- overhaul revision workflow, add hook after revision workflow was finished +- add two factor authentication based on google authenticator +- set timeout for ajax call 'mytasks' from 200ms to 1000ms +- use a similar layout for document list on the ViewDocument page +- add RSS feed of timeline +- put more operations under access control +- add receipent list for documents +- add revision of documents +- add substitute user command for regular users +- add access controll list for many functions +- add document list which can be exported as an archive +- search results can be exported + +-------------------------------------------------------------------------------- + Changes in version 5.1.31 +-------------------------------------------------------------------------------- +- rest api returns error msg and not just http status +- comment of document, documentcontent and folder can be rendered as + markdown +- fix preview for file in drop folder +- fix export of search result +- use openssl instead of mcrypt +- search form shows 'from' and 'to' field for integers and floats +- show preview of document in EditAttributes page +- major overhaul of fulltext search, add new search tab for a more + facetted based search +- add initial version of Dashboard +- default public status of attachments can be configured + +-------------------------------------------------------------------------------- + Changes in version 5.1.30 +-------------------------------------------------------------------------------- +- conversion from pdf to png replaces alpha channel with white +- add list of conversion services in debug menu of admin tool +- use chosen select for custom attributes +- color category (use first 6 chars of md5(category name) as hex color) +- create missing preview images in category or attribute manager +- support README of extension in different languages +- do not force password change if in substitute user mode + +-------------------------------------------------------------------------------- + Changes in version 5.1.29 +-------------------------------------------------------------------------------- +- fix php errors in restapi +- fix 'maximum size' error when uploading a file with drag&drop +- update jquery to 3.6.1 (only bootstrap4 theme) +- introduce authentication service +- new hook in restapi to add middleware +- previews for png, txt, pdf in different directories. This will + enforce a recreation of all cached preview images, pdf and text files! +- various improvements of fulltext service +- show number of documents per category in category manager +- show number of keywords per category in keyword manager +- improve drag&drop with clipboard +- old pear packages SeedDMS/Core, SeedDMS/Lucene, SeedDMS/Preview + are now based on composer and has moved into vendor dir + -------------------------------------------------------------------------------- Changes in version 5.1.28 -------------------------------------------------------------------------------- @@ -17,7 +320,7 @@ - rest api returns version attributes as 'version_attributes' (was 'version-attributes'), each attribute also contains the name - new hook in rest api to add more routes in extensions -- uploaded serveral documents at once by fast upload will assign random +- uploaded several documents at once by fast upload will assign random sequence number to allow manually sorting the documents afterwards - fix counting of login failures if both ldap and db authentication is done diff --git a/Makefile b/Makefile index 64114fe21..ba8176ffa 100644 --- a/Makefile +++ b/Makefile @@ -67,4 +67,32 @@ doc: apidoc: tools/apigen/bin/apigen SeedDMS_Core/Core --exclude "tests/*" --output html -.PHONY: doc webdav webapp repository +# Turn the package.xml file into CHANGELOG.md +# +# The idea is to form blocks of lines separated by an empty line. +# Echo block consists of the version number, release date und notes. +# This blocks are turned into single lines which are than sorted. +# Afterwards the single lines are turned back into blocks. +# +# It first uses sgrep to extract the version, date und notes. This is +# feed to sed to isolated the date and version and put them on separate +# lines. Each version +# forms a block of n lines with the first two being the version and date. +# All remaining lines are notes. Blocks are separated by an empty line. +# It's important to form blocks without ane empty lines because the following +# awk will create a single line from each block which can then be sorted +# (or simply reversed in order). +# Because the blocks are listed in the wrong order (last version first and +# previous version last, e.g. 5.1.29, 3.3.0, 3.3.1, ...., 5.1.27, 5.1.28) they +# need to be reversed in order. This is done by turning each block into line +# with the former new lines replaced by a '|'. So it's basically a '|' separated +# csv file which is then reversed in order by 'sort -r'. In order to separate +# blocks by a newline, each line of that output is appended by another +# line break. Result is put back +# into the original format by replacing all '|' by newline. +# +PKGFILE=SeedDMS_Core/package.xml +changelog: + @sgrep 'stag("DATE") .. etag("DATE") or ((stag("RELEASE") .. etag("RELEASE")) in (stag("VERSION") .. etag("VERSION"))) or inner(stag("NOTES") __ etag("NOTES"))' ${PKGFILE} | sed -e 's#^ *\([-0-9]*\)\([0-9.preRC]*\)#\n\n\2 (\1)\n---------------------#' | awk -F'\n' -vRS='' -vOFS='|' '{$$1=$$1}1' | sort -V -r | sed 's/$$/\n/' | tr '|' '\n' + +.PHONY: doc webdav webapp repository changelog diff --git a/SeedDMS_Core/CHANGELOG.md b/SeedDMS_Core/CHANGELOG.md new file mode 100644 index 000000000..4ac0e57c0 --- /dev/null +++ b/SeedDMS_Core/CHANGELOG.md @@ -0,0 +1,780 @@ +6.0.22 (2022-12-10) +--------------------- +- all changes from 5.1.29 merged + +6.0.21 (2022-11-18) +--------------------- +- all changes from 5.1.28 merged + + +6.0.20 (2022-09-18) +--------------------- +- all changes from 5.1.27 merged +- SeedDMЅ_Core_DMS::getDocumentsInRevision() returns status from revision log + +6.0.19 (2022-05-20) +--------------------- +- all changes from 5.1.26 +- removeFromProcesses() will not touch documents for which the new user does not have at least read access + +6.0.18 (2022-04-22) +--------------------- +- all changes from 5.1.25 +- fix searching for document content with a custom attribute having a value set +- SeedDMS_Core_DocumentContent::getWorkflow() has optional parameter to return data from table tblWorkflowDocumentContent + +6.0.17 (2021-12-11) +--------------------- +- all changes from 5.1.24 + +6.0.16 (2021-05-07) +--------------------- + +6.0.15 (2021-04-13) +--------------------- +- add searching for revision date +- expired documents can be skipped from counting in countTasks() +- SeedDMS_Core_DMS::getDocumentList() uses ambiguous column name when sorting by status +- add list type SleepingReviseByMe to SeedDMS_Core_DMS::getDocumentList() +- parameter 2 of SeedDMS_Core_DMS::getDocumentList() is treated as number of + days for list DueRevisions + +6.0.14 (2021-01-04) +--------------------- +better error checking in SeedDMS_Core_Document::cancelCheckOut() + +6.0.13 (2020-09-29) +--------------------- +- SeedDMS_Folder_DMS::getAccessList() and getDefaultAccess() do not return fals anymore if the parent does not exists. They just stop inheritance. + +6.0.12 (2020-06-05) +--------------------- + +6.0.11 (2020-06-05) +--------------------- +SeedDMS_Core_DMS::filterAccess() properly checks for documents + +6.0.10 (2020-05-22) +--------------------- +SeedDMS_Core_DocumentContent::delRevisor() returns -4 if user has already made a revision + +6.0.9 (2020-05-14) +--------------------- +- no changes, just keep same version as seeddms application + +6.0.8 (2020-03-02) +--------------------- +- no changes, just keep same version as seeddms application + +6.0.7 (2020-02-17) +--------------------- +SeedDMS_Core_Document::getTimeline() returns revision only for latest content +add callback onSetStatus in SeedDMS_Core_DocumentContent::setStatus() +add new list type 'DueRevision' in SeedDMS_Core_DMS::getDocumentList() +a revision can also be started if some revisors have already reviewed the document +remove a user from all its process can also be used to set a new user + +6.0.6 (2018-11-13) +--------------------- +SeedDMS_Core_Folder::addContent() uses currently logged in user as uploader instead of owner +SeedDMS_Core_DocumentContent::verifyStatus() will not set status to S_RELEASED +if currently in S_DRAFT status und no workflow, review, approval, or revision +is pending. + +6.0.5 (2018-02-27) +--------------------- +add list 'NeedsCorrectionOwner' to SeedDMS_Core_DMS::getDocumentList() + +6.0.4 (2018-02-14) +--------------------- +add lists of drafts and obsolete docs in SeedDMS_Core_DMS::getDocumentList() +add fast sql statement to SeedDMS_Core_Document::getReceiptStatus() if limit=1 +add callback onCheckAccessDocument to SeedDMS_Core_Document::getAccessMode() +add new document status 'needs correction' (S_NEEDS_CORRECTION) +do not use views as a replacement for temp. tables anymore, because they are much +slower. +add SeedDMS_Core_DocumentContent::getInstance() + +6.0.3 (2018-01-23) +--------------------- +pass 0 as default to getObjects() +SeedDMS_Core_AttributeDefinition::getStatistics() returns propper values for each item in a value set +SeedDMS_Core_DMS::getDocumentList() returns list of documents without a receiver + +6.0.2 (2017-12-19) +--------------------- +- speed up getting list of locked documents +- setting _logfile in inc.DBAccessPDO.php will log execution times in file +- fix sql statement to create temp table ttrevisionid and ttreceiptid +- SeedDMS_Core_DMS::noReadForStatus no longer needed +- SeedDMS_Core_Document::checkForDueRevisionWorkflow() also checks if there +are any waiting or pending revisions at all +- SeedDMS_Core_User::getReverseSubstitutes() works with new roles +- fix field name in getDocumentList() to make it work for pgsql +- views instead of temp. tables can be used +- ReceiveOwner list does not contain old versions anymore +- all changes up to 5.1.5 merged +- getTimeline() also returns data for documents with a scheduled revision + +6.0.1 (2017-05-28) +--------------------- +- speed up getting list of locked documents +- setting _logfile in inc.DBAccessPDO.php will log execution times in file + +6.0.0 (2017-02-28) +--------------------- +- all changes from 5.0.14 merged +- SeedDMS_Core_User::getReceiptStatus() and SeedDMS_Core_User::getReviewStatus() +only return entries of the latest document version if not specific document and +version is passed +- temp. table for revisions can be created +- new methods SeedDMS_Core_DMS::getDocumentsInReception() and +SeedDMS_Core_DMS::getDocumentsInRevision() +- limit hits of sql statement in SeedDMЅ_Core_DMS::getDuplicateDocumentContent() to 1000 +- finishRevsion() puts all revisors into state waiting, so a new revision can be started +- fix SeedDMS_Core_Group::getRevisionStatus(), which did not always return the last +log entry first +- add roles +- use classname from SeedDMS_Core_DMS::_classnames for SeedDMS_Core_DocumentContent +- add virtual access mode for document links and attachments plus callbacks to + check access mode in a hook +- add new method SeedDMS_Core_DMS::getDocumentsExpired() + +5.1.29 (2022-11-21) +--------------------- +- SeedDMS_Core_Folder::addDocument() does rollback transaction propperly when setting document categories fail +- add $skiproot and $sep parameter to SeedDMS_Core_Folder::getFolderPathPlain() +- add class name for 'documentfile' +- add method SeedDMS_Core_KeywordCategory::countKeywordLists() + +5.1.28 (2022-11-07) +--------------------- +- fix SeedDMS_Core_User::getDocumentContents() +- fix SeedDMS_Core_File::fileExtension() +- SeedDMS_Core_DMS::createPasswordRequest() creates a cryptographically secure hash +- fix sql error when deleting a folder attribute +- add SeedDMS_Core_Attribute::getParsedValue() and use it in SeedDMS_Core_Object::getAttributeValue() +- add SeedDMS_Core_DMS::getDuplicateSequenceNo() and SeedDMS_Core_Folder::reorderDocuments() +- add SeedDMS_Core_File::mimetype(), fix SeedDMS_Core_File::moveDir() +- all file operations use methods of SeedDMS_Core_File +- change namespace of iterators from SeedDMS to SeedDMS\Core + +5.1.27 (2022-08-31) +--------------------- +- fix SeedDMS_Core_DMS::addAttributeDefinition() when objtype is 0 +- sort search result even if sortorder is 'i' or 'n' +- pass an array as an attribute to search() will OR each element + +5.1.26 (2022-05-20) +--------------------- +- fix validating multi value attributes +- SeedDMS_Core_User::removeFromProcesses() can be limited to a list of documents. In that case only the last version will be modified. +- add more types to getStatisticalData() +- add optional parameter $op to SeedDMS_Core_AttributeDefinition::getObjects() +- SeedDMS_Core_AttributeDefinition::getObjects() will not filter by value if null is passed +- SeedDMS_Core_DMS::getAllAttributeDefinitions() has second parameter to filter attributes by type + +5.1.25 (2022-04-22) +--------------------- +- rename getLastWorkflowTransition() to getLastWorkflowLog() +- getLastWorkflowLog() returns a workflow entry even if the workflow has ended +- backport setFileType() from 6.0.x +- add SeedDMS_Core_File::fileExtension() +- add callbacks on onPostUpdateAttribute, onPostRemoveAttribute, onPostAddAttribute +- fix searching for document content with a custom attribute having a value set + +5.1.24 (2021-12-11) +--------------------- +- in SeedDMS_Core_DocumentContent::removeWorkflow() remove records from tblWorklflowLog before tblDWorkflowDocumentContent +- make all class variables of SeedDMS_Core_User protected +- fix various errors in SeedDMS_Core_AttributeDefinition::validate() +- add lots of unit tests +- replace incorrect use of array_search() by in_array() +- move method SeedDMS_Core_DMS::createDump() into SeedDMS_Core_DatabaseAccess +- lots of parameter checking when calling methods() +- make sure callbacks are callable +- SeedDMS_Core_Folder::getParent() returns null if there is no parent (used to be false) +- SeedDMS_Core_DMS::search() will not find document without an expiration date anymore, if the search is limited by an expiration end date but no start date +- add method SeedDMS_Core_Folder::getFoldersMinMax() +- init internal cache variables of SeedDMS_Core_Folder/SeedDMS_Core_Document and add method clearCache() +- SeedDMS_Core_Folder::hasDocuments() does not use the interal document cache anymore +- SeedDMS_Core_Document::addDocumentLink() returns an object of type SeedDMS_Core_DocumentLink in case of success +- trim email, comment, language, theme when setting data of user +- more checks whether an id > 0 when getting a database record + +5.1.23 (2021-08-19) +--------------------- +- SeedDMS_Core_DMS::getTimeline() uses status log instead of document content +- add methods SeedDMS_Core_DocumentContent::getReviewers() and SeedDMS_Core_DocumentContent::getApprovers() +- add methods SeedDMS_Core_DocumentContent::getApproveLog() and SeedDMS_Core_DocumentContent::getReviewLog() +- better handling of document with an empty workflow state +- fix checking of email addresses by using filter_var instead of regex +- add new method SeedDMS_Core_Document::hasCategory() +- add new method SeedDMS_Core_DocumentContent::removeReview() +- add new method SeedDMS_Core_DocumentContent::removeApproval() +- add new method SeedDMS_Core_User::getFolders() +- add new method SeedDMS_Core_User::getDocumentContents() +- add new method SeedDMS_Core_User::getDocumentFiles() +- add new method SeedDMS_Core_User::getDocumentLinks() +- add new type 'foldersperuser' to method SeedDMS_Core_DMS::getStatisticalData() + +5.1.22 (2021-03-15) +--------------------- +- add SeedDMS_Core_DatabaseAccess::hasTable() +- add SeedDMS_Core_User->isType() and SeedDMS_Core_Group->isType() +- add SeedDMS_Core_User->getDMS() and SeedDMS_Core_Group->getDMS() +- add new parameter to SeedDMS_Core_DMS->getDocumentList() for skipping expired documents +- add parameter $incdisabled to SeedDMS_Core_Folder::getNotifyList() +- do not validate value in SeedDMS_Core_Attribute::setValue(), it should have been done before +- SeedDMS_Core_DMS::search() can search for last date of document status change +- smarter caching in SeedDMS_Core_Document::getDocumentFiles() which fixes a potential + problem when removing a document + +5.1.21 (2020-09-29) +--------------------- +- SeedDMS_Folder_DMS::getAccessList() and getDefaultAccess() do not return fals anymore if the parent does not exists. They just stop inheritance. +- pass attribute value to callback 'onAttributeValidate' +- new paramter 'new' of methode SeedDMЅ_Core_AttributeDefinition::validate() +- check if folder/document is below rootDir can be turned on (default off) +- SeedDMS_Core_User::setHomeFolder() can be used to unset the home folder +- check if attribute definition exists when setting attributes of folders and documents + +5.1.20 (2020-09-29) +--------------------- +- SeedDMS_Core_DMS::getDocumentList() returns false, if an unknown list is passed +- SeedDMS_Core_Document::getDocumentFiles() has new parameter to select only those files attached to a specific version of the document +- removing a document version will not remove attachments of the document anymore +- set dms of new user instances in SeedDMS_Core_Group + +5.1.19 (2020-07-30) +--------------------- +- add method SeedDMS_Core_Document::setParent() as an alias for setFolder() +- clear the save content list and latest content in SeedDMS_Core_Document after + a version has been deleted. +- new method SeedDMS_Core_Document::isLatestVersion() +- add new attribute types 'document', 'folder', 'user', 'group' + +5.1.18 (2020-05-28) +--------------------- +- fixed remaining todos +- fixed parsing of file size in SeedDMS_Core_File::parse_filesize() +- fix SeedDMS_Core_DMS::getDocumentByOriginalFilename() + +5.1.17 (2020-05-22) +--------------------- +- add new callback onSetStatus +- fix SeedDMS_Core_DMS::getExpiredDocuments(), sql statement failed because temp. tables were not created +- add parameters $orderdir, $orderby, $update to SeedDMS_Core::getExpiredDocuments() + +5.1.16 (2020-04-14) +--------------------- +- fix call of hooks in SeedDMS_Core +- add variable lasterror in SeedDMS_Core_DMS which can be set by hooks to pass an + error msg to the calling application +- better error checking in SeedDMS_Core_Document::addDocumentFile() + +5.1.15 (2020-03-02) +--------------------- +- no changes, just keep same version as seeddms application + +5.1.14 (2020-02-17) +--------------------- +- speed up SeedDMS_Core_Folder::getSubFolders() SeedDMS_Core_Folder::getDocuments() by minimizing the number of sql queries. + +5.1.13 (2019-09-06) +--------------------- +- add decorators +- add new methods SeedDMS_Core_Document::isType(), SeedDMS_Core_Folder::isType(), SeedDMS_Core_DocumentContent::isType(). Use them instead of checking the class name. +- skip a fileType with just a '.' + +5.1.12 (2019-07-01) +--------------------- +- parameter $orderby passed to SeedDMS_Core_Folder::getDocuments() and SeedDMS_Core_Folder::getSubFolders() can be a string, but only the first char is evaluated +- SeedDMS_Core_DMS::search() excepts parameters as array, added orderby +- add SeedDMS_Core_Folder::hasSubFolderByName() +- fix SeedDMS_Core_Folder::hasDocumentByName() which returned an int > 0 if documents + has been loaded before and even if the document searching for was not among them. +- add new method SeedDMS_Core_Folder::empty() + +5.1.11 (2019-05-03) +--------------------- +- ??? + +5.1.10 (2019-04-04) +--------------------- +- fix php warning if workflow state doesn' have next transition +- add method SeedDMS_Core_DatabaseAccess::setLogFp() + +5.1.9 (2018-11-13) +--------------------- +- context can be passed to getAccessMode() +- call hook in SeedDMS_Core_Folder::getAccessMode() +- new optional parameter $listguest for SeedDMS_Core_Document::getReadAccessList() +- remove deprecated methods SeedDMS_Core_Document::convert(), SeedDMS_Core_Document::wasConverted(), SeedDMS_Core_Document::viewOnline(), SeedDMS_Core_Document::getUrl() + +5.1.8 (2018-07-02) +--------------------- +- SeedDMS_Core_DMS::search() returns false in case of an error +- do not use views in DBAccessPDO by default anymore, use temp. tables +- SeedDMS_Core_Document::getNotifyList() has new parameter to include disabled user in list +- fix possible sql injection in SeedDMS_Core_User + +5.1.7 (2018-04-05) +--------------------- +- just bump version + +5.1.6 (2018-02-14) +--------------------- +- add SeedDMS_Core_Folder::getDocumentsMinMax() +- add lots of DocBlocks from merge request #8 +- add SeedDMS_Core_AttributeDefinition::removeValue() + +5.1.5 (2017-11-07) +--------------------- +- use views instead of temp. tables +- add list of expired documents in SeedDMS_Core_DMS::getDocumentList() +- add methods to set comment, name, public, version of document files +- add method SeedDMS_Core_Document::transferToUser() +- SeedDMS_Core_Document::addDocumentFile() returns object of file +- add SeedDMS_Core_DocumentFile::setDate() +- remove SeedDMS_Core_DocumentCategory::addCategory() and getCategories() +- add optional parameters $limit and $offset to SeedDMS_Core_Folder::getDocuments() + and SeedDMS_Core_Folder::getSubFolders() +- getInstance() returns now null instead of false if the object was not found in the db +- add new methods SeedDMS_Core_Document::addCategories() and + SeedDMS_Core_Document::removeCategories() + +5.1.4 (2017-09-05) +--------------------- +- add virtual access mode for document links and attachments plus callbacks to + check access mode in a hook +- add new method SeedDMS_Core_DMS::getDocumentsExpired() +- all changes from 5.0.14 merged + +5.1.3 (2017-08-23) +--------------------- +- SeedDMS_Core_Document::getNotifyList() and SeedDMS_Core_Folder::getNotifyList() +returns just users which are not disabled +- add new methods removeFromProcesses(), getWorkflowsInvolved(), getKeywordCategories() to SeedDMS_Core_User +- add methods isMandatoryReviewerOf() and isMandatoryApproverOf() +- add methods transferDocumentsFolders() and transferEvents() +- add method SeedDMS_Core_DMS::getDocumentByOriginalFilename() + +5.1.2 (2017-03-23) +--------------------- +- SeedDMS_Core_DMS::filterDocumentFiles() returns also documents which are not public + if the owner tries to access them +- Check return value of onPreRemove[Document +Folder], return from calling method if bool +- Add SeedDMS_Core_DMS::getDocumentList() +- Limit number of duplicate files to 1000 +- Add hook on(Pre +Post)RemoveContent +- Add hook onAttributeValidate + +5.1.1 (2017-02-20) +--------------------- +- all changes from 5.0.11 merged + +5.1.0 (2017-02-20) +--------------------- +- added postgres support + +5.0.13 (2017-07-13) +--------------------- +- all changes from 4.3.36 merged + +5.0.12 (2017-03-23) +--------------------- +all sql statements can be logged to a file +do not sort some temporary tables anymore, because it causes an error in mysql if sql_mode=only_full_group_by is set + +5.0.11 (2017-02-28) +--------------------- +- all changes from 4.3.34 merged + +5.0.10 (2017-02-20) +--------------------- +- all changes from 4.3.33 merged + +5.0.9 (2016-11-02) +--------------------- +- all changes from 4.3.32 merged + +5.0.8 (2016-11-02) +--------------------- +- all changes from 4.3.31 merged + +5.0.7 (2016-11-02) +--------------------- +- all changes from 4.3.30 merged +- better attribute value checking + +5.0.6 (2016-09-06) +--------------------- +- all changes from 4.3.29 merged + +5.0.5 (2016-08-09) +--------------------- +- all changes from 4.3.28 merged + +5.0.4 (2016-05-03) +--------------------- +- all changes from 4.3.27 merged + +5.0.3 (2016-04-04) +--------------------- +- use classname from SeedDMS_Core_DMS::_classnames for SeedDMS_Core_DocumentContent +- all changes from 4.3.26 merged + +5.0.2 (2016-04-26) +--------------------- +- all changes from 4.3.25 merged + +5.0.1 (2016-01-22) +--------------------- +- all changes from 4.3.24 merged + +5.0.0 (2016-01-22) +--------------------- +- classes can be overloaded +- clean workflow log when a document version was deleted + +4.3.37 (2018-02-14) +--------------------- +- SeedDMS_Core_DMS::search() finds documents without a status log + +4.3.36 (2017-03-22) +--------------------- +- fix sql statement for creating temp. tables (sqlite) + +4.3.35 (2017-07-11) +--------------------- +do not sort some temporary tables anymore, because it causes an error in mysql if sql_mode=only_full_group_by is set + +4.3.34 (2017-02-28) +--------------------- +SeedDMS_Core_DMS::getDuplicateDocumentContent() returns complete document + +4.3.33 (2017-02-22) +--------------------- +- SeedDMЅ_Core_DMS::getTimeline() no longer returns duplicate documents +- SeedDMЅ_Core_Document::addContent() sets workflow after status was set +- SeedDMЅ_Core_Keyword::setOwner() fix sql statement +- SeedDMЅ_Core_User::setFullname() minor fix in sql statement + +4.3.32 (2017-01-12) +--------------------- +- order groups by name returned by getReadAccessList() +- add optional parameter to SeedDMS_Core_DMS::filterDocumentLinks() +- SeedDMS_Core_DMS::search() can search for document/folder id + +4.3.31 (2016-11-02) +--------------------- +- new method SeedDMЅ_Core_WorkflowAction::getTransitions() +- new method SeedDMЅ_Core_WorkflowState::getTransitions() +- new method SeedDMЅ_Core_AttributeDefinition::parseValue() +- add check for cycles in workflow SeedDMS_Core_Workflow::checkForCycles() + +4.3.30 (2016-10-07) +--------------------- +- new method SeedDMЅ_Core_AttributeDefinition::getValueSetSeparator() +- trim each value of a value set before saving the complete value set as a string + +4.3.29 (2016-09-06) +--------------------- +- SeedDMЅ_Core_Object::getAttributes() orders attributes by name of attribute definition +- SeedDMЅ_Core_Workflow::addTransition() force reload of transition list after adding a +- SeedDMЅ_Core_Document::rewrite[Review +Approval]Log() will also copy file if it exists +- add method SeedDMЅ_Core_Document::rewriteWorkflowLog() + +4.3.28 (2016-08-24) +--------------------- +- SeedDMЅ_Core_DMS::search() searches also comment of document version + +4.3.27 (2016-04-26) +--------------------- +- callbacks can have more then one user function +- fix some sql statements, because they didn't work with mysql 5.7.5 anymore + +4.3.26 (2016-04-04) +--------------------- +- add more callbacks + +4.3.25 (2016-03-08) +--------------------- +- rename SeedDMS_Core_Group::getNotificationsByGroup() to getNotifications() +- use __construct() for all constructors +- fix setting multi value attributes for versions + +4.3.24 (2016-01-22) +--------------------- +- make sure boolean attribute is saved as 0/1 +- add SeedDMS_Core_User::[g +s]etMandatoryWorkflows() +- add SeedDMS_Core_User::getNotifications() +- add SeedDMS_Core_Group::getNotifications() +- SeedDMS_Core_DMS::getNotificationsByGroup() and +SeedDMS_Core_DMS::getNotificationsByUser() are deprecated +- SeedDMS_Core_DocumentCategory::getDocumentsByCategory() now returns the documents +- add SeedDMS_Core_Group::getWorkflowStatus() +- SeedDMS_Core_User::getDocumentsLocked() sets locking user propperly + +4.3.24 (2016-01-21) +--------------------- +- make sure boolean attribute is saved as 0/1 +- add SeedDMS_Core_User::[g +s]etMandatoryWorkflows() +- add SeedDMS_Core_User::getNotifications() +- add SeedDMS_Core_Group::getNotifications() +- SeedDMS_Core_DMS::getNotificationsByGroup() and +SeedDMS_Core_DMS::getNotificationsByUser() are deprecated +- SeedDMS_Core_DocumentCategory::getDocumentsByCategory() now returns the documents +- add SeedDMS_Core_Group::getWorkflowStatus() +- SeedDMS_Core_User::getDocumentsLocked() sets locking user propperly + +4.3.23 (2016-01-21) +--------------------- +- new method SeedDMS_Core_DMS::createDump() +- minor improvements int SeedDMS_Core_Document::getReadAccessList() + +4.3.22 (2015-11-09) +--------------------- +- fix sql statement to reset password +- pass some more information for timeline + +4.3.21 (2015-09-28) +--------------------- +- add method SeedDMS_Core_Database::getCurrentTimestamp() +- add method SeedDMS_Core_Database::getCurrentDatetime() +- user getCurrentTimestamp() and getCurrentDatetime() whenever possible + +4.3.20 (2015-06-26) +--------------------- +- add method SeedDMS_Core_DMS::checkDate() +- add method SeedDMS_Core_Document::setDate() +- add method SeedDMS_Core_Folder::setDate() +- date can be passed to SeedDMS_Core_DocumentContent::setStatus() +- add method SeedDMS_Core_DocumentContent::rewriteStatusLog() +- add method SeedDMS_Core_DocumentContent::rewriteReviewLog() +- add method SeedDMS_Core_DocumentContent::rewriteApprovalLog() +- access rights for guest are also taken into account if set in an acl. Previously guest could gain read rights even if the access was probibited +by a group or user right + +4.3.19 (2015-06-26) +--------------------- +- add optional paramter $noclean to clearAccessList(), setDefaultAccess(), setInheritAccess() +- clearAccessList() will clean up the notifier list +- new method cleanNotifyList() + +4.3.18 (2015-06-09) +--------------------- +- add optional paramter $msg to SeedDMS_Core_DocumentContent::verifyStatus() +- add method SeedDMS_Core_DMS::getDuplicateDocumentContent() + +4.3.17 (2015-03-27) +--------------------- +clean workflow log when a document version was deleted + +4.3.16 (2015-03-20) +--------------------- +no changes + +4.3.15 (2015-02-12) +--------------------- +users returned by SeedDMS_Core_DMS::getAllUsers() have language and theme set again + +4.3.13 (2014-11-27) +--------------------- +- fix searching for attributes +- add some more documentation +- SeedDMS_Core_DMS::getDocumentCategories() returns categories sorted by name (Bug #181) +- new methode SeedDMS_Core_Document::replaceContent() which replaces the content of a version. + 4.3.14 +- add missing start transaction in SeedDMD_Core_Folder::remove() +- SeedDMD_Core_Folder::isSubFolder() doesn't compare object instances anymore (Bug #194) + +4.3.12 (2014-11-17) +--------------------- +- fix searching folders with multivalue attributes + +4.3.11 (2014-11-13) +--------------------- +- fixed saving multivalue attributes +- add method SeedDMS_Core_Attribute::getValueAsArray() + +4.3.10 (2014-10-22) +--------------------- +new release + +4.3.9 (2014-07-30) +--------------------- +- SeedDMS_Core_KeywordCategory::getKeywordLists() sorts keywords aphabetically +- SeedDMS_Core_DMS::addUser() doesn't throw an error if sql_mode is set to STRICT_TRANS_TABLES and pwdexpiration is not set to a valid date. + +4.3.8 (2014-04-09) +--------------------- +- new method SeedDMS_Core_DMS::getStatisticalData() + +4.3.7 (2014-03-21) +--------------------- +no changes + +4.3.6 (2014-03-18) +--------------------- +- add optional parameters $publiconly=false and $user=null to SeedDMS_Core_Document::getDocumentLinks() +- add new method SeedDMS_Core_Document::getReverseDocumentLinks() + +4.3.5 (2014-03-04) +--------------------- +no changes + +4.3.4 (2014-02-01) +--------------------- +- fix handling of multivalue attributes + +4.3.3 (2014-02-01) +--------------------- +- SeedDMS_Folder::getDocuments() and SeedDMS_Folder::getSubFolders() do not + do any sorting if $orderby is not set. +- database hostname can have port seperated by ':' +- make all functions in SeedDMS_Core_File static (fixes problem with php 5.5.x) + +4.3.2 (2013-11-27) +--------------------- +- new method SeedDMS_Core_Folder::isSubFolder() +- check for subFolder in SeedDMS_Core_Folder::setParent() +- new methods SeedDMS_Core_DMS::checkFolders() and SeedDMS_Core_DMS::checkDocuments() + +4.3.0 (2013-09-05) +--------------------- +- various small corrections +- comment of version is no longer taken from document if version comment is empty +- passing an array of users to SeedDMЅ_Core_DMS::search() instead of a single user ist now allowed +- turn on foreign key constraints for sqlite3 +- SeedDMЅ_Core_Folder::getPath() can handle a subfolder treated as a root folder + +4.2.2 (2013-05-17) +--------------------- +- admins can be added as reviewer/approver again + +4.2.1 (2013-04-30) +--------------------- +- fixed bug in SeedDMS_Core_DocumentContent::addIndApp() + +4.2.0 (2013-04-22) +--------------------- +- fixed bug in SeedDMS_Core_DocumentContent::addIndApp() + +4.1.3 (2013-04-08) +--------------------- +- stay in sync with seeddms application + +4.1.2 (2013-04-05) +--------------------- +- set propper folderList of sub folders after moving a folder + +4.1.1 (2013-04-05) +--------------------- +- stay in sync with seeddms application + +4.1.0 (2013-03-28) +--------------------- +- minor bugfixes + +4.0.0 (2013-02-26) +--------------------- +- minor bugfixes + +4.0.0pre5 (2013-02-14) +--------------------- +- changed name from letodms to seeddms +- fixed SeedDMS_Database::TableList() + +4.0.0pre4 (2013-02-11) +--------------------- +- calculate checksum for document versions +- some bug fixes +- some more documentation +- added new methods SeedDMS_Core_Document::getReadUserList() and + SeedDMS_Core_Folder::getReadUserList() which replaces getApproversList() +- fixed sql statement in getReadUserList() for sqlite3 + +4.0.0pre3 (2013-02-08) +--------------------- +- minor bug fixes + +4.0.0pre2 (2013-02-06) +--------------------- +- lots of bug fixes +- replaced more of old var declaration +- more code documentation + +4.0.0pre1 (2013-01-24) +--------------------- +- added database transactions +- new workflow +- replaced old var declaration + +4.0.0RC1 (2013-02-20) +--------------------- +- minor bugfixes + +3.4.0 (2012-12-13) +--------------------- +- added PDO database driver, several sql changes for better compatiblity +- fixed bug when adding a new document category +- make sure the database remains consistent even in case of errors + +3.3.9 (2012-09-19) +--------------------- +- version update to be in sync with letodms application + +3.3.8 (2012-09-16) +--------------------- +- more sql injection protection in LetoDMS_Core_User + +3.3.7 (2012-08-25) +--------------------- +- no changes, just keep same version as letodms application + +3.3.6 (2012-07-16) +--------------------- +- no changes, just keep same version as letodms application + +3.3.5 (2012-04-30) +--------------------- +- minor corrections + +3.3.4 (2012-04-11) +--------------------- +- fixed bug in LetoDMS_Core_DocumentFile::getPath() + +3.3.3 (2012-03-28) +--------------------- +- fixed bug in LetoDMS_Core_Document::getPath() + +3.3.2 (2012-03-22) +--------------------- +- fixed bug in LetoDMS_Core_Document::getDir() + +3.3.1 (2012-03-21) +--------------------- +- new release + +3.3.0 (2012-02-08) +--------------------- +- added methods to find and repair errors in document and folder records +- removed sendmail parameter from some methods in LetoDMS_Core_Document +- do not use some of the temporay tables anymore +- SetFetchMode(ADODB_FETCH_ASSOC) in LetoDMS_Core_DatabaseAccess::connect() + +3.2.0 (2011-07-23) +--------------------- +New release + +3.0.0 (2010-04-27) +--------------------- +Initial release + diff --git a/SeedDMS_Core/Core.php b/SeedDMS_Core/Core.php index 13cf1d142..8a068e295 100644 --- a/SeedDMS_Core/Core.php +++ b/SeedDMS_Core/Core.php @@ -96,7 +96,13 @@ require_once('Core/inc.AccessUtils.php'); */ require_once('Core/inc.FileUtils.php'); +/** + * @uses SeedDMS_Transmittal + */ +require_once('Core/inc.ClassTransmittal.php'); + /** * @uses SeedDMS_File */ require_once('Core/inc.ClassIterator.php'); +?> diff --git a/SeedDMS_Core/Core/inc.ClassDMS.php b/SeedDMS_Core/Core/inc.ClassDMS.php index 7adec5e8b..b2ada7676 100644 --- a/SeedDMS_Core/Core/inc.ClassDMS.php +++ b/SeedDMS_Core/Core/inc.ClassDMS.php @@ -135,7 +135,9 @@ class SeedDMS_Core_DMS { /** * @var array $noReadForStatus list of status without read right - * online. + * online. DO NOT USE ANYMORE. SeedDMS_Core_DocumentContent::getAccessMode() + * was the only method using it, but it now takes the noReadForStatus info + * from the user's role * @access public */ public $noReadForStatus; @@ -257,6 +259,12 @@ class SeedDMS_Core_DMS { * least the access right on the object as passed in $minMode. * Hence, passing a group instead of user is possible. * + * This function can be used for documents and folders and calls + * {@link SeedDMS_Core_Folder::getAccessMode()} or + * {@link SeedDMS_Core_Document::getAccessMode()}. A document is also + * filtered out if it has no latest content, which can happen if access + * on documents in a certain state has been restricted. + * * @param array $objArr list of objects (either documents or folders) * @param object $user user for which access is checked * @param integer $minMode minimum access mode required (M_ANY, M_NONE, @@ -269,8 +277,15 @@ class SeedDMS_Core_DMS { } $newArr = array(); foreach ($objArr as $obj) { - if ($obj->getAccessMode($user) >= $minMode) - array_push($newArr, $obj); + if ($obj->getAccessMode($user) >= $minMode) { + $dms = $obj->getDMS(); + if($obj->isType('document')) { + if($obj->getLatestContent()) + array_push($newArr, $obj); + } else { + array_push($newArr, $obj); + } + } } return $newArr; } /* }}} */ @@ -335,6 +350,61 @@ class SeedDMS_Core_DMS { } /* }}} */ /** + * Merge access lists + * + * Merges two access lists. Objects of the second list will override objects + * in the first list. + * + * @param array $first list of access rights as returned by + * SeedDMS_Core_Document:: getAccessList() or SeedDMS_Core_Folder::getAccessList() + * @param array $secont list of access rights + * @return array merged list + */ + static function mergeAccessLists($first, $second) { /* {{{ */ + if($first && !$second) + return $first; + if(!$first && $second) + return $second; + + $tmp = array('users'=>array(), 'groups'=>array()); + if(!isset($first['users']) || !isset($first['groups']) || + !isset($second['users']) || !isset($second['groups'])) + return false; + + foreach ($first['users'] as $f) { + $new = $f; + foreach ($second['users'] as $i=>$s) { + if($f->getUserID() == $s->getUserID()) { + $new = $s; + unset($second['users'][$i]); + break; + } + } + array_push($tmp['users'], $new); + } + foreach ($seconf['users'] as $f) { + array_push($tmp['users'], $f); + } + + foreach ($first['groups'] as $f) { + $new = $f; + foreach ($second['groups'] as $i=>$s) { + if($f->getGroupID() == $s->getGroupID()) { + $new = $s; + unset($second['groups'][$i]); + break; + } + } + array_push($tmp['groups'], $new); + } + foreach ($second['groups'] as $f) { + array_push($tmp['groups'], $f); + } + + return $tmp; + } /* }}} */ + + /* * Filter out document attachments which can not be accessed by a given user * * Returns a filtered list of files which are accessible by the @@ -381,13 +451,17 @@ class SeedDMS_Core_DMS { $this->classnames['folder'] = 'SeedDMS_Core_Folder'; $this->classnames['document'] = 'SeedDMS_Core_Document'; $this->classnames['documentcontent'] = 'SeedDMS_Core_DocumentContent'; + $this->classnames['documentfile'] = 'SeedDMS_Core_DocumentFile'; $this->classnames['user'] = 'SeedDMS_Core_User'; + $this->classnames['role'] = 'SeedDMS_Core_Role'; $this->classnames['group'] = 'SeedDMS_Core_Group'; + $this->classnames['transmittal'] = 'SeedDMS_Core_Transmittal'; + $this->classnames['transmittalitem'] = 'SeedDMS_Core_TransmittalItem'; $this->callbacks = array(); $this->lasterror = ''; $this->version = '@package_version@'; if($this->version[0] == '@') - $this->version = '5.1.27'; + $this->version = '6.0.20'; } /* }}} */ /** @@ -840,20 +914,162 @@ class SeedDMS_Core_DMS { */ function getDocumentContent($id) { /* {{{ */ - if (!is_numeric($id)) return false; - if ($id < 1) return false; + $classname = $this->classnames['documentcontent']; + return $classname::getInstance($id, $this); + } /* }}} */ - $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `id` = ".(int) $id; - $resArr = $this->db->getResultArray($queryStr); - if (is_bool($resArr) && $resArr == false) + /** + * Returns all documents with a predefined search criteria + * + * @param string $listtype type of document list, can be 'AppRevByMe', + * 'AppRevOwner', 'ReceiptByMe', 'ReviseByMe', 'LockedByMe', 'MyDocs' + * @param object $user user + * @return array list of documents records + */ + function countTasks($listtype, $user=null, $param5=true) { /* {{{ */ + if (!$this->db->createTemporaryTable("ttstatid") || !$this->db->createTemporaryTable("ttcontentid")) { return false; - if (count($resArr) != 1) - return null; - $row = $resArr[0]; + } + $groups = array(); + if($user) { + $tmp = $user->getGroups(); + foreach($tmp as $group) + $groups[] = $group->getID(); + } + $selectStr = "count(distinct ttcontentid.document) c "; + $queryStr = + "FROM `ttcontentid` ". + "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID`=`ttcontentid`.`document` AND `tblDocumentStatus`.`version`=`ttcontentid`.`maxVersion` ". + "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ". + "LEFT JOIN `tblDocumentStatusLog` ON `ttstatid`.`statusID` = `tblDocumentStatusLog`.`statusID` AND `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` "; + switch($listtype) { + case 'ReviewByMe': // Documents I have to review {{{ + if (!$this->db->createTemporaryTable("ttreviewid")) { + return false; + } + $queryStr .= + "LEFT JOIN `tblDocumentReviewers` on `ttcontentid`.`document`=`tblDocumentReviewers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentReviewers`.`version` ". + "LEFT JOIN `ttreviewid` ON `ttreviewid`.`reviewID` = `tblDocumentReviewers`.`reviewID` ". + "LEFT JOIN `tblDocumentReviewLog` ON `tblDocumentReviewLog`.`reviewLogID`=`ttreviewid`.`maxLogID` "; - $document = $this->getDocument($row['document']); - $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']); - return $version; + $queryStr .= "WHERE (`tblDocumentReviewers`.`type` = 0 AND `tblDocumentReviewers`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentReviewers`.`type` = 1 AND `tblDocumentReviewers`.`required` IN (".implode(',', $groups).") "; + $queryStr .= ") "; + $queryStr .= "AND `tblDocumentReviewLog`.`status` = 0 "; + $docstatarr = array(S_DRAFT_REV); + if($param5) + $docstatarr[] = S_EXPIRED; + $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") "; + break; /* }}} */ + case 'ApproveByMe': // Documents I have to approve {{{ + if (!$this->db->createTemporaryTable("ttapproveid")) { + return false; + } + $queryStr .= + "LEFT JOIN `tblDocumentApprovers` on `ttcontentid`.`document`=`tblDocumentApprovers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentApprovers`.`version` ". + "LEFT JOIN `ttapproveid` ON `ttapproveid`.`approveID` = `tblDocumentApprovers`.`approveID` ". + "LEFT JOIN `tblDocumentApproveLog` ON `tblDocumentApproveLog`.`approveLogID`=`ttapproveid`.`maxLogID` "; + + if($user) { + $queryStr .= "WHERE (`tblDocumentApprovers`.`type` = 0 AND `tblDocumentApprovers`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentApprovers`.`type` = 1 AND `tblDocumentApprovers`.`required` IN (".implode(',', $groups).") "; + $queryStr .= ") "; + } + $queryStr .= "AND `tblDocumentApproveLog`.`status` = 0 "; + $docstatarr = array(S_DRAFT_APP); + if($param5) + $docstatarr[] = S_EXPIRED; + $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") "; + break; /* }}} */ + case 'ReceiptByMe': // Documents I have to receipt {{{ + if (!$this->db->createTemporaryTable("ttreceiptid")) { + return false; + } + $queryStr .= + "LEFT JOIN `tblDocumentRecipients` on `ttcontentid`.`document`=`tblDocumentRecipients`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRecipients`.`version` ". + "LEFT JOIN `ttreceiptid` ON `ttreceiptid`.`receiptID` = `tblDocumentRecipients`.`receiptID` ". + "LEFT JOIN `tblDocumentReceiptLog` ON `tblDocumentReceiptLog`.`receiptLogID`=`ttreceiptid`.`maxLogID` "; + + if($user) { + $queryStr .= "WHERE (`tblDocumentRecipients`.`type` = 0 AND `tblDocumentRecipients`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentRecipients`.`type` = 1 AND `tblDocumentRecipients`.`required` IN (".implode(',', $groups).") "; + $queryStr .= ") "; + } + $queryStr .= "AND `tblDocumentReceiptLog`.`status` = 0 "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") "; + break; /* }}} */ + case 'ReviseByMe': // Documents I have to receipt {{{ + if (!$this->db->createTemporaryTable("ttrevisionid")) { + return false; + } + $queryStr .= + "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ". + "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ". + "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` "; + + if($user) { + $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).") "; + $queryStr .= ") "; + } + $queryStr .= "AND `tblDocumentRevisionLog`.`status` = 0 "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_IN_REVISION.") "; + break; /* }}} */ + case 'SleepingReviseByMe': // Documents I have to receipt {{{ + if (!$this->db->createTemporaryTable("ttrevisionid")) { + return false; + } + $queryStr .= + "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ". + "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ". + "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` "; + + if($user) { + $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).") "; + $queryStr .= ") "; + } + $queryStr .= "AND `tblDocumentContent`.`revisiondate` IS NOT NULL AND `tblDocumentContent`.`revisiondate` <= ".$this->db->getCurrentDatetime(14)." "; + $queryStr .= "AND `tblDocumentRevisionLog`.`status` = -3 "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") "; + break; /* }}} */ + case 'NeedsCorrectionOwner': // Documents that need to be corrected {{{ + $queryStr .= + "LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `ttcontentid`.`document` "; + $queryStr .= "WHERE `tblDocuments`.`owner` = '".$user->getID()."' ". + "AND `tblDocumentStatusLog`.`status` IN (".S_NEEDS_CORRECTION.") "; + break; /* }}} */ + case 'WorkflowByMe': // Documents which need my workflow action {{{ + + $queryStr .= + "LEFT JOIN `tblWorkflowDocumentContent` on `ttcontentid`.`document`=`tblWorkflowDocumentContent`.`document` AND `ttcontentid`.`maxVersion`=`tblWorkflowDocumentContent`.`version` ". + "LEFT JOIN `tblWorkflowTransitions` on `tblWorkflowDocumentContent`.`workflow`=`tblWorkflowTransitions`.`workflow` AND `tblWorkflowDocumentContent`.`state`=`tblWorkflowTransitions`.`state` ". + "LEFT JOIN `tblWorkflowTransitionUsers` on `tblWorkflowTransitionUsers`.`transition` = `tblWorkflowTransitions`.`id` ". + "LEFT JOIN `tblWorkflowTransitionGroups` on `tblWorkflowTransitionGroups`.`transition` = `tblWorkflowTransitions`.`id` "; + + if($user) { + $queryStr .= "WHERE (`tblWorkflowTransitionUsers`.`userid` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblWorkflowTransitionGroups`.`groupid` IN (".implode(',', $groups).")"; + $queryStr .= ") "; + } + $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_WORKFLOW." "; + break; // }}} + } + if($queryStr) { + $resArr = $this->db->getResultArray('SELECT '.$selectStr.$queryStr); + if (is_bool($resArr) && !$resArr) { + return false; + } + } else { + return false; + } + return $resArr[0]['c']; } /* }}} */ /** @@ -899,7 +1115,8 @@ class SeedDMS_Core_DMS { * 'ReviewByMe', 'ApproveByMe', 'AppRevByMe', 'ReviseByMe', 'ReceiptByMe' * will also return documents which the reviewer, approver, etc. * has already taken care of. If set to false only - * untouched documents will be returned. In case of 'ExpiredOwner' this + * untouched documents will be returned. In case of 'ExpiredOwner', + * 'SleepingReviseByMe' this * parameter contains the number of days (a negative number is allowed) * relativ to the current date or a date in format 'yyyy-mm-dd' * (even in the past). @@ -1174,6 +1391,221 @@ class SeedDMS_Core_DMS { } } break; // }}} + case 'ReceiptByMe': // Documents I have to receipt {{{ + if (!$this->db->createTemporaryTable("ttreceiptid")) { + return false; + } + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + $groups = array(); + $tmp = $user->getGroups(); + foreach($tmp as $group) + $groups[] = $group->getID(); + + $selectStr .= ", `tblDocumentReceiptLog`.`date` as `duedate` "; + $queryStr .= + "LEFT JOIN `tblDocumentRecipients` on `ttcontentid`.`document`=`tblDocumentRecipients`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRecipients`.`version` ". + "LEFT JOIN `ttreceiptid` ON `ttreceiptid`.`receiptID` = `tblDocumentRecipients`.`receiptID` ". + "LEFT JOIN `tblDocumentReceiptLog` ON `tblDocumentReceiptLog`.`receiptLogID`=`ttreceiptid`.`maxLogID` "; + + if(1) { + $queryStr .= "WHERE (`tblDocumentRecipients`.`type` = 0 AND `tblDocumentRecipients`.`required` = ".$user->getID()." "; + /* Checking for groups slows down the statement extremly on sqlite */ + if($groups) + $queryStr .= "OR `tblDocumentRecipients`.`type` = 1 AND `tblDocumentRecipients`.`required` IN (".implode(',', $groups).")"; + $queryStr .= ") "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_RELEASED." "; + if(!$param2) + $queryStr .= " AND `tblDocumentReceiptLog`.`status` = 0 "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr .= "WHERE 1=1 "; + + // Get document list for the current user. + $receiptStatus = $user->getReceiptStatus(); + + // Create a comma separated list of all the documentIDs whose information is + // required. + // Take only those documents into account which hasn't be touched by the user + // ($st["status"]==0) + $dList = array(); + foreach ($receiptStatus["indstatus"] as $st) { + if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) { + $dList[] = $st["documentID"]; + } + } + foreach ($receiptStatus["grpstatus"] as $st) { + if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) { + $dList[] = $st["documentID"]; + } + } + $docCSV = ""; + foreach ($dList as $d) { + $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'"; + } + + if (strlen($docCSV)>0) { + $queryStr .= "AND `tblDocuments`.`id` IN (" . $docCSV . ") "; +// $queryStr .= "ORDER BY `statusDate` DESC"; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr = ''; + } + } + break; // }}} + case 'ReviseByMe': // Documents I have to revise {{{ + if (!$this->db->createTemporaryTable("ttrevisionid")) { + return false; + } + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + $groups = array(); + $tmp = $user->getGroups(); + foreach($tmp as $group) + $groups[] = $group->getID(); + + $selectStr .= ", `tblDocumentRevisionLog`.`date` as `duedate` "; + $queryStr .= + "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ". + "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ". + "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` "; + + if(1) { + $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).")"; + $queryStr .= ") "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_REVISION." "; + if(!$param2) + $queryStr .= " AND `tblDocumentRevisionLog`.`status` = 0 "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr .= "WHERE 1=1 "; + + // Get document list for the current user. + $revisionStatus = $user->getRevisionStatus(); + + // Create a comma separated list of all the documentIDs whose information is + // required. + $dList = array(); + foreach ($revisionStatus["indstatus"] as $st) { + if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) { + $dList[] = $st["documentID"]; + } + } + foreach ($revisionStatus["grpstatus"] as $st) { + if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) { + $dList[] = $st["documentID"]; + } + } + $docCSV = ""; + foreach ($dList as $d) { + $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'"; + } + + if (strlen($docCSV)>0) { + $queryStr .= "AND `tblDocuments`.`id` IN (" . $docCSV . ") "; + //$queryStr .= "ORDER BY `statusDate` DESC"; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr = ''; + } + } + break; // }}} + case 'SleepingReviseByMe': // Documents I have to revise but are still sleeping {{{ + if (!$this->db->createTemporaryTable("ttrevisionid")) { + return false; + } + + $dayoffset = 0; + if(is_int($param2)) { + $dayoffset = (int) $param2; + } + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + $groups = array(); + $tmp = $user->getGroups(); + foreach($tmp as $group) + $groups[] = $group->getID(); + + $selectStr .= ", `tblDocumentRevisionLog`.`date` as `duedate` "; + $queryStr .= + "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ". + "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ". + "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` "; + + $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." "; + if($groups) + $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).")"; + $queryStr .= ") "; + $queryStr .= "AND `tblDocumentContent`.`revisiondate` IS NOT NULL AND `tblDocumentContent`.`revisiondate` <= ".$this->db->getCurrentDatetime($dayoffset)." "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_RELEASED." "; + $queryStr .= " AND `tblDocumentRevisionLog`.`status` = -3 "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + break; // }}} + case 'DueRevision': // Documents with a due revision, which is not started {{{ + if (!$this->db->createTemporaryTable("ttrevisionid")) { + return false; + } + + $dayoffset = 0; + if(is_int($param2)) { + $dayoffset = (int) $param2; + } + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + $selectStr .= ", `tblDocumentContent`.`revisiondate` "; + $queryStr .= "WHERE `tblDocumentContent`.`revisiondate` IS NOT NULL AND `tblDocumentContent`.`revisiondate` <= ".$this->db->getCurrentDatetime($dayoffset)." "; + $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_RELEASED." "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + $queryStr .= ", `tblDocumentContent`.`revisiondate` ASC"; + break; // }}} case 'WorkflowByMe': // Documents I to trigger in Worklflow {{{ $user = $param1; $orderby = $param3; @@ -1251,7 +1683,7 @@ class SeedDMS_Core_DMS { $orderdir = 'ASC'; /** @noinspection PhpUndefinedConstantInspection */ $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ". - "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.") "; + "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.", ".S_IN_REVISION.") "; if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; @@ -1261,6 +1693,76 @@ class SeedDMS_Core_DMS { // "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.") ". // "ORDER BY `statusDate` DESC"; break; // }}} + case 'ReceiveOwner': // Documents having a reception I'm owning {{{ + $queryStr .= "WHERE 1=1 "; + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + // $qs = 'SELECT DISTINCT `documentID` FROM `tblDocumentRecipients` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` AND `ttcontentid`.`document` = `tblDocumentRecipients`.`documentID`'; + // sql statement without older versions of a document + $qs = 'SELECT DISTINCT `document` as `documentID` FROM `ttcontentid` a LEFT JOIN `tblDocumentRecipients` b on a.`document`=b.`documentID` AND a.`maxVersion`=b.`version` WHERE b.`receiptID` IS NOT NULL'; + $ra = $this->db->getResultArray($qs); + if (is_bool($ra) && !$ra) { + return false; + } + $docs = array(); + foreach($ra as $d) { + $docs[] = $d['documentID']; + } + + if ($docs) { + $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") "; + $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."'"; + $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr = ''; + } + break; // }}} + case 'NoReceiveOwner': // Documents *not* having a reception I'm owning {{{ + $queryStr .= "WHERE 1=1 "; + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + // $qs = 'SELECT DISTINCT `documentID` FROM `tblDocumentRecipients` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` AND `ttcontentid`.`document` = `tblDocumentRecipients`.`documentID`'; + // sql statement without older versions of a document + $qs = 'SELECT DISTINCT `document` as `documentID` FROM `ttcontentid` a LEFT JOIN `tblDocumentRecipients` b on a.`document`=b.`documentID` AND a.`maxVersion`=b.`version` WHERE b.`receiptID` IS NULL'; + $ra = $this->db->getResultArray($qs); + if (is_bool($ra) && !$ra) { + return false; + } + $docs = array(); + foreach($ra as $d) { + $docs[] = $d['documentID']; + } + + if ($docs) { + $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") "; + $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ". + "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr = ''; + } + break; // }}} case 'RejectOwner': // Documents that has been rejected and I'm owning {{{ $queryStr .= "WHERE 1=1 "; @@ -1348,6 +1850,60 @@ class SeedDMS_Core_DMS { else $queryStr .= "ORDER BY `name`"; $queryStr .= " ".$orderdir; break; // }}} + case 'ObsoleteOwner': // Documents that are obsolete and I'm owning {{{ + $queryStr .= "WHERE 1=1 "; + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ". + "AND `tblDocumentStatusLog`.`status` IN (".S_OBSOLETE.") "; + //$queryStr .= "ORDER BY `statusDate` DESC"; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + break; // }}} + case 'NeedsCorrectionOwner': // Documents that needs correction and I'm owning {{{ + $queryStr .= "WHERE 1=1 "; + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ". + "AND `tblDocumentStatusLog`.`status` IN (".S_NEEDS_CORRECTION.") "; + //$queryStr .= "ORDER BY `statusDate` DESC"; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + break; // }}} + case 'DraftOwner': // Documents in draft status and I'm owning {{{ + $queryStr .= "WHERE 1=1 "; + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ". + "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT.") "; + //$queryStr .= "ORDER BY `statusDate` DESC"; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + break; // }}} case 'WorkflowOwner': // Documents waiting for workflow trigger I'm owning {{{ $queryStr .= "WHERE 1=1 "; @@ -1372,6 +1928,37 @@ class SeedDMS_Core_DMS { else $queryStr .= "ORDER BY `name`"; $queryStr .= " ".$orderdir; break; // }}} + case 'CheckedOutByMe': // Documents I have checked out {{{ + $queryStr .= "WHERE 1=1 "; + + $user = $param1; + $orderby = $param3; + if($param4 == 'desc') + $orderdir = 'DESC'; + else + $orderdir = 'ASC'; + + $qs = 'SELECT `document` FROM `tblDocumentCheckOuts` WHERE `userID`='.$user->getID(); + $ra = $this->db->getResultArray($qs); + if (is_bool($ra) && !$ra) { + return false; + } + $docs = array(); + foreach($ra as $d) { + $docs[] = $d['document']; + } + + if ($docs) { + $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") "; + if ($orderby=='e') $queryStr .= "ORDER BY `expires`"; + else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`"; + else if ($orderby=='s') $queryStr .= "ORDER BY `status`"; + else $queryStr .= "ORDER BY `name`"; + $queryStr .= " ".$orderdir; + } else { + $queryStr = ''; + } + break; // }}} default: // {{{ return false; break; // }}} @@ -1461,12 +2048,13 @@ class SeedDMS_Core_DMS { * @param array $expirationenddate search for documents expiring before and on this date * @return array|bool */ - function search($query, $limit=0, $offset=0, $logicalmode='AND', $searchin=array(), $startFolder=null, $owner=null, $status = array(), $creationstartdate=array(), $creationenddate=array(), $modificationstartdate=array(), $modificationenddate=array(), $categories=array(), $attributes=array(), $mode=0x3, $expirationstartdate=array(), $expirationenddate=array()) { /* {{{ */ + function search($query, $limit=0, $offset=0, $logicalmode='AND', $searchin=array(), $startFolder=null, $owner=null, $status = array(), $creationstartdate=array(), $creationenddate=array(), $modificationstartdate=array(), $modificationenddate=array(), $categories=array(), $attributes=array(), $mode=0x3, $expirationstartdate=array(), $expirationenddate=array(), $reception=array()) { /* {{{ */ $orderby = ''; + $revisionstartdate = $revisionenddate = ''; $statusstartdate = array(); $statusenddate = array(); if(is_array($query)) { - foreach(array('limit', 'offset', 'logicalmode', 'searchin', 'startFolder', 'owner', 'status', 'creationstartdate', 'creationenddate', 'modificationstartdate', 'modificationenddate', 'categories', 'attributes', 'mode', 'expirationstartdate', 'expirationenddate') as $paramname) + foreach(array('limit', 'offset', 'logicalmode', 'searchin', 'startFolder', 'owner', 'status', 'creationstartdate', 'creationenddate', 'modificationstartdate', 'modificationenddate', 'categories', 'attributes', 'mode', 'revisionstartdate', 'revisionenddate', 'expirationstartdate', 'expirationenddate', 'reception') as $paramname) ${$paramname} = isset($query[$paramname]) ? $query[$paramname] : ${$paramname}; foreach(array('orderby', 'statusstartdate', 'statusenddate') as $paramname) ${$paramname} = isset($query[$paramname]) ? $query[$paramname] : ''; @@ -1818,6 +2406,23 @@ class SeedDMS_Core_DMS { $searchCreateDate .= "`tblDocumentContent`.`date` <= ".$this->db->qstr($stopdate); } } + $searchRevisionDate = ""; + if ($revisionstartdate) { + $startdate = $revisionstartdate['year'].'-'.$revisionstartdate["month"].'-'.$revisionstartdate["day"]; + if ($startdate) { + if($searchRevisionDate) + $searchRevisionDate .= " AND "; + $searchRevisionDate .= "`tblDocumentContent`.`revisiondate` >= '".$startdate."'"; + } + } + if ($revisionenddate) { + $stopdate = $revisionenddate["year"].'-'.$revisionenddate["month"].'-'.$revisionenddate["day"]; + if ($stopdate) { + if($searchRevisionDate) + $searchRevisionDate .= " AND "; + $searchRevisionDate .= "`tblDocumentContent`.`revisiondate` <= '".$stopdate."'"; + } + } $searchExpirationDate = ''; if ($expirationstartdate) { $startdate = SeedDMS_Core_DMS::makeTimeStamp($expirationstartdate['hour'], $expirationstartdate['minute'], $expirationstartdate['second'], $expirationstartdate['year'], $expirationstartdate["month"], $expirationstartdate["day"]); @@ -1862,6 +2467,11 @@ class SeedDMS_Core_DMS { if (!$this->db->createTemporaryTable("ttcontentid") || !$this->db->createTemporaryTable("ttstatid")) { return false; } + if($reception) { + if (!$this->db->createTemporaryTable("ttreceiptid")) { + return false; + } + } $searchQuery = "FROM `tblDocuments` ". "LEFT JOIN `tblDocumentContent` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` ". @@ -1873,8 +2483,12 @@ class SeedDMS_Core_DMS { "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentStatus`.`version` AND `ttcontentid`.`document` = `tblDocumentStatus`.`documentID` ". "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ". "LEFT JOIN `tblDocumentCategory` ON `tblDocuments`.`id`=`tblDocumentCategory`.`documentID` ". +// "LEFT JOIN `tblDocumentRecipients` ON `tblDocuments`.`id`=`tblDocumentRecipients`.`documentID` ". +// "LEFT JOIN `tblDocumentReceiptLog` ON `tblDocumentRecipients`.`receiptID`=`tblDocumentReceiptLog`.`receiptID` ". +// "LEFT JOIN `ttreceiptid` ON `ttreceiptid`.`maxLogID` = `tblDocumentReceiptLog`.`receiptLogID` ". "WHERE ". - // "`ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` AND ". +// "`ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` AND ". +// "`ttreceiptid`.`maxLogID`=`tblDocumentReceiptLog`.`receiptLogID` AND ". "`ttcontentid`.`maxVersion` = `tblDocumentContent`.`version`"; if (strlen($searchKey)>0) { @@ -1892,6 +2506,9 @@ class SeedDMS_Core_DMS { if (strlen($searchCreateDate)>0) { $searchQuery .= " AND (".$searchCreateDate.")"; } + if (strlen($searchRevisionDate)>0) { + $searchQuery .= " AND (".$searchRevisionDate.")"; + } if (strlen($searchExpirationDate)>0) { $searchQuery .= " AND (".$searchExpirationDate.")"; } @@ -1907,7 +2524,28 @@ class SeedDMS_Core_DMS { $searchQuery .= " AND `tblDocumentStatusLog`.`status` IN (".implode(',', $status).")"; } - if($searchKey || $searchOwner || $searchCategories || $searchCreateDate || $searchExpirationDate || $searchStatusDate || $searchAttributes || $status) { + if($reception) { + $searchReception = array(); + /* still waiting for users/groups to acknownledge reception */ + if(in_array("missingaction", $reception)) + $searchReception[] = "b.`status` IN (0)"; + /* document has not been acknowledeged by at least one user/group */ + if(in_array("hasrejection", $reception)) + $searchReception[] = "b.`status` IN (-1)"; + /* document has been acknowledeged by at least one user/group */ + if(in_array("hasacknowledge", $reception)) + $searchReception[] = "b.`status` IN (1)"; + /* document has been acknowledeged by all users/groups !!! not working !!! */ + if(in_array("completeacknowledge", $reception)) + $searchReception[] = "b.`status` NOT IN (-1, 0)"; + if($searchReception) { + $searchQuery .= " AND EXISTS (SELECT NULL FROM `tblDocumentRecipients` a LEFT JOIN `tblDocumentReceiptLog` b ON a.`receiptID`=b.`receiptID` LEFT JOIN `ttreceiptid` c ON c.`maxLogID` = b.`receiptLogID` WHERE "; + $searchQuery .= "c.`maxLogID`=b.`receiptLogID` AND `tblDocuments`.`id` = a.`documentID` "; + $searchQuery .= "AND (".implode(' OR ', $searchReception)."))"; + } + } + + if($searchKey || $searchOwner || $searchCategories || $searchCreateDate || $searchRevisionDate || $searchExpirationDate || $searchStatusDate || $searchAttributes || $status) { // Count the number of rows that the search will produce. $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblDocuments`.`id` ".$searchQuery.") a"); $totalDocs = 0; @@ -2230,13 +2868,17 @@ class SeedDMS_Core_DMS { * @param null $homefolder * @return bool|SeedDMS_Core_User */ - function addUser($login, $pwd, $fullName, $email, $language, $theme, $comment, $role='0', $isHidden=0, $isDisabled=0, $pwdexpiration='', $quota=0, $homefolder=null) { /* {{{ */ + function addUser($login, $pwd, $fullName, $email, $language, $theme, $comment, $role='3', $isHidden=0, $isDisabled=0, $pwdexpiration='', $quota=0, $homefolder=null) { /* {{{ */ $db = $this->db; if (is_object($this->getUserByLogin($login))) { return false; } - if($role == '') - $role = '0'; + if(!is_object($role)) { + if($role == '') + $role = SeedDMS_Core_Role::getInstance(3, $this); + else + $role = SeedDMS_Core_Role::getInstance($role, $this); + } if(trim($pwdexpiration) == '' || trim($pwdexpiration) == 'never') { $pwdexpiration = 'NULL'; } elseif(trim($pwdexpiration) == 'now') { @@ -2244,7 +2886,7 @@ class SeedDMS_Core_DMS { } else { $pwdexpiration = $db->qstr($pwdexpiration); } - $queryStr = "INSERT INTO `tblUsers` (`login`, `pwd`, `fullName`, `email`, `language`, `theme`, `comment`, `role`, `hidden`, `disabled`, `pwdExpiration`, `quota`, `homefolder`) VALUES (".$db->qstr($login).", ".$db->qstr($pwd).", ".$db->qstr($fullName).", ".$db->qstr($email).", '".$language."', '".$theme."', ".$db->qstr($comment).", '".intval($role)."', '".intval($isHidden)."', '".intval($isDisabled)."', ".$pwdexpiration.", '".intval($quota)."', ".($homefolder ? intval($homefolder) : "NULL").")"; + $queryStr = "INSERT INTO `tblUsers` (`login`, `pwd`, `fullName`, `email`, `language`, `theme`, `comment`, `role`, `hidden`, `disabled`, `pwdExpiration`, `quota`, `homefolder`) VALUES (".$db->qstr($login).", ".$db->qstr($pwd).", ".$db->qstr($fullName).", ".$db->qstr($email).", '".$language."', '".$theme."', ".$db->qstr($comment).", '".intval($role->getId())."', '".intval($isHidden)."', '".intval($isDisabled)."', ".$pwdexpiration.", '".intval($quota)."', ".($homefolder ? intval($homefolder) : "NULL").")"; $res = $this->db->getResult($queryStr); if (!$res) return false; @@ -2328,6 +2970,110 @@ class SeedDMS_Core_DMS { return $group; } /* }}} */ + /** + * Get a role by its id + * + * @param integer $id id of role + * @return object/boolean role or false if no role was found + */ + function getRole($id) { /* {{{ */ + $classname = $this->classnames['role']; + return $classname::getInstance($id, $this); + } /* }}} */ + + /** + * Get a role by its name + * + * @param integer $name name of role + * @return object/boolean role or false if no role was found + */ + function getRoleByName($name) { /* {{{ */ + $classname = $this->classnames['role']; + return $classname::getInstance($name, $this, 'name'); + } /* }}} */ + + /** + * Return list of all roles + * + * @return array of instances of {@link SeedDMS_Core_Role} or false + */ + function getAllRoles($orderby = '') { /* {{{ */ + $classname = $this->classnames['role']; + return $classname::getAllInstances($orderby, $this); + } /* }}} */ + + /** + * Create a new role + * + * @param string $name name of role + * @return object/boolean instance of {@link SeedDMS_Core_Role} or false in + * case of an error. + */ + function addRole($name, $role) { /* {{{ */ + if (is_object($this->getRoleByName($name))) { + return false; + } + + $queryStr = "INSERT INTO `tblRoles` (`name`, `role`) VALUES (".$this->db->qstr($name).", ".$role.")"; + if (!$this->db->getResult($queryStr)) + return false; + + return $this->getRole($this->db->getInsertID('tblRoles')); + } /* }}} */ + + /** + * Get a transmittal by its id + * + * @param integer $id id of transmittal + * @return object/boolean transmittal or false if no group was found + */ + function getTransmittal($id) { /* {{{ */ + $classname = $this->classnames['transmittal']; + return $classname::getInstance($id, $this, ''); + } /* }}} */ + + /** + * Get a transmittal by its name + * + * @param string $name name of transmittal + * @return object/boolean transmittal or false if no group was found + */ + function getTransmittalByName($name) { /* {{{ */ + $classname = $this->classnames['transmittal']; + return $classname::getInstance($name, $this, 'name'); + } /* }}} */ + + /** + * Return list of all transmittals + * + * @return array of instances of {@link SeedDMS_Core_Transmittal} or false + */ + function getAllTransmittals($user=null, $orderby = '') { /* {{{ */ + $classname = $this->classnames['transmittal']; + return $classname::getAllInstances($user, $orderby, $this); + } /* }}} */ + + /** + * Create a new transmittal + * + * @param string $name name of group + * @param string $comment comment of group + * @param object $user user this transmittal belongs to + * @return object/boolean instance of {@link SeedDMS_Core_Transmittal} or + * false in case of an error. + */ + function addTransmittal($name, $comment, $user) { /* {{{ */ + if (is_object($this->getTransmittalByName($name))) { + return false; + } + + $queryStr = "INSERT INTO `tblTransmittals` (`name`, `comment`, `userID`) VALUES (".$this->db->qstr($name).", ".$this->db->qstr($comment).", ".$user->getID().")"; + if (!$this->db->getResult($queryStr)) + return false; + + return $this->getTransmittal($this->db->getInsertID('tblTransmittals')); + } /* }}} */ + function getKeywordCategory($id) { /* {{{ */ if (!is_numeric($id) || $id < 1) return false; @@ -2759,7 +3505,7 @@ class SeedDMS_Core_DMS { $workflows = array(); for ($i = 0; $i < count($resArr); $i++) { /** @noinspection PhpUndefinedVariableInspection */ - $workflow = new SeedDMS_Core_Workflow($resArr[$i]["id"], $resArr[$i]["name"], $wkfstates[$resArr[$i]["initstate"]]); + $workflow = new SeedDMS_Core_Workflow($resArr[$i]["id"], $resArr[$i]["name"], $wkfstates[$resArr[$i]["initstate"]], $resArr[$i]["layoutdata"]); $workflow->setDMS($this); $workflows[$i] = $workflow; } @@ -2788,7 +3534,7 @@ class SeedDMS_Core_DMS { $initstate = $this->getWorkflowState($resArr[0]['initstate']); - $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate); + $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate, $resArr[0]["layoutdata"]); $workflow->setDMS($this); return $workflow; @@ -2815,7 +3561,7 @@ class SeedDMS_Core_DMS { $initstate = $this->getWorkflowState($resArr[0]['initstate']); - $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate); + $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate, $resArr[0]["layoutdata"]); $workflow->setDMS($this); return $workflow; @@ -3070,6 +3816,48 @@ class SeedDMS_Core_DMS { return $transition; } /* }}} */ + /** + * Return all documents waiting for or in reception + * + * This function retrieves all documents and its version which are waiting for + * reception + * + * @return object instance of {@link SeedDMS_Core_DocumentContent} or false + */ + function getDocumentsInReception() { /* {{{ */ + if (!$this->db->createTemporaryTable("ttreceiptid") || !$this->db->createTemporaryTable("ttcontentid")) { + return false; + } + $queryStr = + "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`status` FROM `tblDocumentRecipients` LEFT JOIN `ttreceiptid` ON `tblDocumentRecipients`.`receiptID` = `ttreceiptid`.`receiptID` LEFT JOIN `tblDocumentReceiptLog` ON `ttreceiptid`.`maxLogID` = `tblDocumentReceiptLog`.`receiptLogID` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion`=`tblDocumentRecipients`.`version` AND `ttcontentid`.`document`=`tblDocumentRecipients`.`documentID` WHERE `tblDocumentReceiptLog`.`status`=0 AND `ttcontentid`.`maxVersion` IS NOT NULL"; + $resArr = $this->db->getResultArray($queryStr); + + return $resArr; + } /* }}} */ + + /** + * Return all documents revisors waiting for a revision to start (sleeping) + * or are required to revise the document (waiting) + * + * This function retrieves all revisors which are waiting for + * revision or already in revision + * Note: the name of the method is somewhat misleading, because it + * does not return documents but just database records from table + * tblDocumentRevisors and tblDocumentRevisionLog + * + * @return array list of revisors or false in case of an error + */ + function getDocumentsInRevision() { /* {{{ */ + if (!$this->db->createTemporaryTable("ttrevisionid") || !$this->db->createTemporaryTable("ttcontentid")) { + return false; + } + $queryStr = + "SELECT `tblDocumentRevisors`.*, `tblDocumentRevisionLog`.`status` FROM `tblDocumentRevisors` LEFT JOIN `ttrevisionid` ON `tblDocumentRevisors`.`revisionID` = `ttrevisionid`.`revisionID` LEFT JOIN `tblDocumentRevisionLog` ON `ttrevisionid`.`maxLogID` = `tblDocumentRevisionLog`.`revisionLogID` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` AND `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` WHERE `tblDocumentRevisionLog`.`status` in (".S_LOG_WAITING.", ".S_LOG_SLEEPING.") AND `ttcontentid`.`maxVersion` IS NOT NULL"; + $resArr = $this->db->getResultArray($queryStr); + + return $resArr; + } /* }}} */ + /** * Returns document content which is not linked to a document * @@ -3160,6 +3948,49 @@ class SeedDMS_Core_DMS { } /* }}} */ + /** + * Returns document content which has the incorrect file type + * + * This method is for finding document content with an incorrect + * or missing file type. It just checks documents contents + * with a certain mime type. + * @return bool|SeedDMS_Core_Document[] + */ + function getWrongFiletypeDocumentContent() { /* {{{ */ + $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `mimeType` in ('application/zip', 'application/pdf', 'image/png', 'image/gif', 'image/jpg', 'audio/mp3', 'text/rtf')"; + $resArr = $this->db->getResultArray($queryStr); + if ($resArr === false) + return false; + + /** @var SeedDMS_Core_Document[] $versions */ + $versions = array(); + foreach($resArr as $row) { + $expect = ''; + switch($row['mimeType']) { + case "application/zip": + case "application/pdf": + case "image/png": + case "image/gif": + case "image/jpg": + case "audio/mp3": + case "text/rtf": + $expect = substr($row['mimeType'], -3, 3); + break; + } + if($expect) { + if($row['fileType'] != '.'.$expect) { + /** @var SeedDMS_Core_Document $document */ + $document = new $this->classnames['document']($row['document'], '', '', '', '', '', '', '', '', '', '', ''); + $document->setDMS($this); + $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']); + $versions[] = $version; + } + } + } + return $versions; + + } /* }}} */ + /** * Returns document content which is duplicated * @@ -3230,8 +4061,8 @@ class SeedDMS_Core_DMS { } /* }}} */ /** - * Returns a list of reviews, approvals which are not linked - * to a user, group anymore + * Returns a list of reviews, approvals, receipts, revisions which are not + * linked to a user, group anymore * * This method is for finding reviews or approvals whose user * or group was deleted and not just removed from the process. @@ -3248,6 +4079,12 @@ class SeedDMS_Core_DMS { case 'approval': $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentApprovers`"; break; + case 'receipt': + $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentRecipients`"; + break; + case 'revision': + $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentRevisors`"; + break; } /** @noinspection PhpUndefinedVariableInspection */ $queryStr .= " a LEFT JOIN `tblDocuments` b ON a.`documentID`=b.`id` WHERE"; @@ -3263,7 +4100,7 @@ class SeedDMS_Core_DMS { } /* }}} */ /** - * Removes all reviews, approvals which are not linked + * Removes all reviews, approvals, receipts, revisions which are not linked * to a user, group anymore * * This method is for removing all reviews or approvals whose user @@ -3285,6 +4122,12 @@ class SeedDMS_Core_DMS { case 'approval': $queryStr = "DELETE FROM tblDocumentApprovers"; break; + case 'receipt': + $queryStr = "DELETE FROM tblDocumentRecipients"; + break; + case 'revision': + $queryStr = "DELETE FROM tblDocumentRevisors"; + break; } /** @noinspection PhpUndefinedVariableInspection */ $queryStr .= " WHERE"; @@ -3432,7 +4275,7 @@ class SeedDMS_Core_DMS { $timeline = array(); if(0) { - $queryStr = "SELECT DISTINCT `document` FROM `tblDocumentContent` WHERE `date` > ".$startts." AND `date` < ".$endts." UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts; + $queryStr = "SELECT DISTINCT `document` FROM `tblDocumentContent` WHERE `date` > ".$startts." AND `date` < ".$endts." OR `revisiondate` > '".date('Y-m-d H:i:s', $startts)."' AND `revisiondate` < '".date('Y-m-d H:i:s', $endts)."' UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts; } else { $startdate = date('Y-m-d H:i:s', $startts); $enddate = date('Y-m-d H:i:s', $endts); diff --git a/SeedDMS_Core/Core/inc.ClassDocument.php b/SeedDMS_Core/Core/inc.ClassDocument.php index 966710335..97787a028 100644 --- a/SeedDMS_Core/Core/inc.ClassDocument.php +++ b/SeedDMS_Core/Core/inc.ClassDocument.php @@ -41,6 +41,27 @@ define("S_RELEASED", 2); */ define("S_IN_WORKFLOW", 3); +/* + * Document is in a revision workflow. A revision workflow is started + * some time after the document has been released. + */ +define("S_IN_REVISION", 4); + +/* + * Document is in draft status. Being in draft means that the document + * is still worked on. This status is mainly for uploading documents + * which aren't fully complete but needs to accessible for the public, + * e.g. in order to colaborate on them. + */ +define("S_DRAFT", 5); + +/* + * Document needs correction after revision. This needs to be different from + * the regular S_REJECTED because documents which has been rejected + * in revision are not necessarily invalid but just needs correction. + */ +define("S_NEEDS_CORRECTION", 6); + /* * Document was rejected. A document is in rejected state when * the review failed or approval was not given. @@ -63,7 +84,7 @@ define("S_EXPIRED", -3); * Lowest and highest status that may be set */ define("S_LOWEST_STATUS", -3); -define("S_HIGHEST_STATUS", 3); +define("S_HIGHEST_STATUS", 6); /** * The different states a workflow log can be in. This is used in @@ -713,9 +734,31 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ if(!$newOwner->isType('user')) return false; + $oldOwner = self::getOwner(); + + $db->startTransaction(); $queryStr = "UPDATE `tblDocuments` set `owner` = " . $newOwner->getID() . " WHERE `id` = " . $this->_id; - if (!$db->getResult($queryStr)) + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); return false; + } + + /* FIXME: Update also all locks and checkouts done by the previous owner */ + /* + $queryStr = "UPDATE `tblDocumentLocks` set `userID` = " . $newOwner->getID() . " WHERE `document` = " . $this->_id . " AND `userID` = " . $oldOwner->getID(); + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + + $queryStr = "UPDATE `tblDocumentCheckOuts` set `userID` = " . $newOwner->getID() . " WHERE `document` = " . $this->_id . " AND `userID` = " . $oldOwner->getID(); + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + */ + + $db->commitTransaction(); $this->_ownerID = $newOwner->getID(); /** @noinspection PhpUndefinedFieldInspection */ @@ -893,7 +936,7 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ if($lc) { $st=$lc->getStatus(); - if (($st["status"]==S_DRAFT_REV || $st["status"]==S_DRAFT_APP || $st["status"]==S_IN_WORKFLOW || $st["status"]==S_RELEASED) && $this->hasExpired()){ + if (($st["status"]==S_DRAFT_REV || $st["status"]==S_DRAFT_APP || $st["status"]==S_IN_WORKFLOW || $st["status"]==S_RELEASED || $st["status"]==S_IN_REVISION) && $this->hasExpired()){ return $lc->setStatus(S_EXPIRED,"", $this->getOwner()); } elseif ($st["status"]==S_EXPIRED && !$this->hasExpired() ){ @@ -904,6 +947,26 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ return false; } /* }}} */ + /** + * Check if latest content of the document has a scheduled + * revision workflow. + * + * This method was moved into SeedDMS_Core_DocumentContent and + * the original method in SeedDMS_Core_Document now uses it for + * the latest version. + * + * @param object $user user requesting the possible automatic change + * @param string $next next date for review + * @return boolean true if status has changed + */ + function checkForDueRevisionWorkflow($user, $next=''){ /* {{{ */ + $lc=$this->getLatestContent(); + if($lc) { + return $lc->checkForDueRevisionWorkflow($user, $next); + } + return false; + } /* }}} */ + /** * Check if document is locked * @@ -953,6 +1016,241 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ return $this->_lockingUser; } /* }}} */ + /** + * Check if document is checked out + * + * @return boolean true if checked out otherwise false + */ + function isCheckedOut() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "SELECT * FROM `tblDocumentCheckOuts` WHERE `document` = " . (int) $this->_id; + $resArr = $db->getResultArray($queryStr); + if ((is_bool($resArr) && $resArr==false) || (count($resArr)==0)) { + // Could not find a check out for the selected document. + return false; + } else { + // A check out has been identified for this document. + return true; + } + } /* }}} */ + + /** + * Get checkout info for document + * + * This returns the checkouts for a document. There could be several checkouts + * for one document, but usually there is just one. + * + * @return array/boolean records from table tblDocumentCheckOuts or false + * in case of an error. + */ + function getCheckOutInfo() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "SELECT * FROM `tblDocumentCheckOuts` WHERE `document` = " . (int) $this->_id; + $resArr = $db->getResultArray($queryStr); + if ((is_bool($resArr) && $resArr==false) || (count($resArr)==0)) { + // Could not find a check out for the selected document. + return false; + } else { + // A check out has been identified for this document. + return $resArr; + } + } /* }}} */ + + + /** + * Check out document + * + * Creates a check out record for the document and copies the latest + * version of the document into the given checkout dir. + * + * @param object $user object of user doing the checkout + * @param string $checkoutdir directory where the file will be placed + * @return object object of class SeedDMS_Core_DocumentCheckOut + */ + function checkOut($user, $checkoutdir) { /* {{{ */ + $db = $this->_dms->getDB(); + + if(self::isCheckedOut()) + return false; + + /* Check if checkout dir is writable */ + if(!file_exists($checkoutdir)) { + return false; + } + + $db->startTransaction(); + + $lc = self::getLatestContent(); + + $ext = pathinfo($this->getName(), PATHINFO_EXTENSION); + $oext = pathinfo($lc->getOriginalFileName(), PATHINFO_EXTENSION); + if($ext == $oext) + $filename = preg_replace('/[^A-Za-z0-9_.-]/', '_', $this->getName()); + else { + $filename = preg_replace('/[^A-Za-z0-9_-]/', '_', $this->getName()).'.'.$oext; + } + $filename = $checkoutdir.$this->getID().'-'.$lc->getVersion().'-'.$filename; //$lc->getOriginalFileName(); + $queryStr = "INSERT INTO `tblDocumentCheckOuts` (`document`, `version`, `userID`, `date`, `filename`) VALUES (".$this->_id.", ".$lc->getVersion().", ".$user->getID().", ".$db->getCurrentDatetime().", ".$db->qstr($filename).")"; + if (!$db->getResult($queryStr)) + return false; + + /* Try to copy the file */ + $err = SeedDMS_Core_File::copyFile($this->_dms->contentDir . $this->getDir() . $lc->getFileName(), $filename); + if (!$err) { + $db->rollbackTransaction(); + return false; + } + + $db->commitTransaction(); + return true; + } /* }}} */ + + /** + * Check in document + * + * Τhis function is similar to SeedDMS_Core_Document::addContent() + * but reads the content from the file was previously checked out. + * Internal this method calls + * SeedDMS_Core_Document::addContent() but takes over the original + * filename, filetype and mimetype from the checked out version. + * No matter in which state the current checked out file is, the + * document will be checked back in afterwards. + * + * @param string $comment + * @param object $user + * @param array $reviewers + * @param array $approvers + * @param integer $version + * @param array $attributes + * @param object $workflow + * @param integer $initstate intial document status + * @return boolean|object false in case of error, true if no error occurs but + * the document remains unchanged (because the checked out file has not + * changed or it has disappeared and couldnt't be checked in), or + * an instance of class SeedDMS_Core_AddContentResultSet if the document + * was updated. + */ + function checkIn($comment, $user, $reviewers=array(), $approvers=array(), $version=0, $attributes=array(), $workflow=null, $initstate=S_RELEASED) { /* {{{ */ + $db = $this->_dms->getDB(); + + $infos = self::getCheckOutInfo(); + if(!$infos) + return false; + $info = $infos[0]; + $lc = self::getLatestContent(); + + /* If file doesn't exist anymore, then just remove the record from the db */ + if(!file_exists($info['filename'])) { + $queryStr = "DELETE FROM `tblDocumentCheckOuts` WHERE `document` = ".$this->_id; + $db->getResult($queryStr); + return true; + } + + /* Check if version of checked out file is equal to current version */ + if($lc->getVersion() != $info['version']) { + return true; + } + + if($user->getID() != $info['userID']) { + return true; + } + + $content = true; + /* Do not create a new version if the file was unchanged */ + $checksum = SeedDMS_Core_File::checksum($info['filename']); + if($checksum != $lc->getChecksum()) { + $content = $this->addContent($comment, $user, $info['filename'], $lc->getOriginalFileName(), $lc->getFileType(), $lc->getMimeType(), $reviewers, $approvers, $version, $attributes, $workflow, $initstate); + if($content) { + if(!$this->_dms->forceRename) { + SeedDMS_Core_File::removeFile($info['filename']); + } + $queryStr = "DELETE FROM `tblDocumentCheckOuts` WHERE `document` = ".$this->_id; + $db->getResult($queryStr); + return $content; + } else { + return false; + } + } else { + SeedDMS_Core_File::removeFile($info['filename']); + $queryStr = "DELETE FROM `tblDocumentCheckOuts` WHERE `document` = ".$this->_id; + $db->getResult($queryStr); + return true; + } + } /* }}} */ + + /** + * Cancel check out of document + * + * This function will cancel a check out in progress by removing + * the check out record from the database and removing the file + * from the check out folder. + * + * @return boolean true if cancelation was successful + */ + function cancelCheckOut() { /* {{{ */ + $db = $this->_dms->getDB(); + + $infos = self::getCheckOutInfo(); + if($infos) { + $info = $infos[0]; + + $db->startTransaction(); + $queryStr = "DELETE FROM `tblDocumentCheckOuts` WHERE `document` = ".$this->_id; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + if(file_exists($info['filename']) && !SeedDMS_Core_File::removeFile($info['filename'])) { + $db->rollbackTransaction(); + return false; + } + $db->commitTransaction(); + } + + return true; + + } /* }}} */ + + /** + * Return the check out status of the document + * + * This method returns the checkout status of a previosly checked out + * document. + * + * @return int 1=The checked out file doesn't exists anymore, + * 2=The checked out version doesn't exists anymore + * 3=The checked out file has not been modified yet + * 4=new check out record in database found + * 0=The checked out file is modified and check in will create a new version + */ + function checkOutStatus() { /* {{{ */ + $infos = self::getCheckOutInfo(); + if(!$infos) + return 4; + + $info = $infos[0]; + $lc = self::getLatestContent(); + + /* If file doesn't exist anymore, then just remove the record from the db */ + if(!file_exists($info['filename'])) { + return 1; + } + + /* Check if version of checked out file is equal to current version */ + if($lc->getVersion() != $info['version']) { + return 2; + } + + $checksum = SeedDMS_Core_File::checksum($info['filename']); + if($checksum == $lc->getChecksum()) { + return 3; + } + + return 0; + } /* }}} */ + /** * @return float */ @@ -1018,7 +1316,10 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ if ($this->inheritsAccess()) { $res = $this->getFolder(); if (!$res) return false; - return $this->_folder->getAccessList($mode, $op); + $pacl = $res->getAccessList($mode, $op); + return $pacl; + } else { + $pacl = array("groups" => array(), "users" => array()); } if (!isset($this->_accessList[$mode])) { @@ -1045,6 +1346,7 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ } return $this->_accessList[$mode]; + return SeedDMS_Core_DMS::mergeAccessLists($pacl, $this->_accessList[$mode]); } /* }}} */ /** @@ -1562,9 +1864,10 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ * @param array $attributes list of version attributes. The element key * must be the id of the attribute definition. * @param object $workflow + * @param integer $initstate intial document status * @return bool|SeedDMS_Core_AddContentResultSet */ - function addContent($comment, $user, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers=array(), $approvers=array(), $version=0, $attributes=array(), $workflow=null) { /* {{{ */ + function addContent($comment, $user, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers=array(), $approvers=array(), $version=0, $attributes=array(), $workflow=null, $initstate=S_RELEASED) { /* {{{ */ $db = $this->_dms->getDB(); // the doc path is id/version.filetype @@ -1701,6 +2004,9 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ elseif($workflow) { $status = S_IN_WORKFLOW; $comment = ", workflow: ".$workflow->getName(); + } elseif($initstate == S_DRAFT) { + $status = $initstate; + $comment = ""; } else { $status = S_RELEASED; $comment = ""; @@ -1825,7 +2131,7 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $user = $this->_dms->getLoggedInUser(); foreach ($resArr as $row) { /** @var SeedDMS_Core_DocumentContent $content */ - $content = new $classname($row["id"], $this, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum']); + $content = new $classname($row["id"], $this, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum'], $row['revisiondate']); /* TODO: Better use content id as key in $this->_content. This * would allow to remove a single content object in removeContent(). * Currently removeContent() must clear $this->_content completely @@ -1876,7 +2182,7 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $resArr = $resArr[0]; $classname = $this->_dms->getClassname('documentcontent'); /** @var SeedDMS_Core_DocumentContent $content */ - if($content = new $classname($resArr["id"], $this, $resArr["version"], $resArr["comment"], $resArr["date"], $resArr["createdBy"], $resArr["dir"], $resArr["orgFileName"], $resArr["fileType"], $resArr["mimeType"], $resArr['fileSize'], $resArr['checksum'])) { + if($content = new $classname($resArr["id"], $this, $resArr["version"], $resArr["comment"], $resArr["date"], $resArr["createdBy"], $resArr["dir"], $resArr["orgFileName"], $resArr["fileType"], $resArr["mimeType"], $resArr['fileSize'], $resArr['checksum'], $resArr['revisiondate'])) { $user = $this->_dms->getLoggedInUser(); /* A user with write access on the document may always see the version */ if($user && $content->getAccessMode($user) == M_NONE) @@ -1914,7 +2220,7 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $resArr = $resArr[0]; $classname = $this->_dms->getClassname('documentcontent'); - $this->_latestContent = new $classname($resArr["id"], $this, $resArr["version"], $resArr["comment"], $resArr["date"], $resArr["createdBy"], $resArr["dir"], $resArr["orgFileName"], $resArr["fileType"], $resArr["mimeType"], $resArr['fileSize'], $resArr['checksum']); + $this->_latestContent = new $classname($resArr["id"], $this, $resArr["version"], $resArr["comment"], $resArr["date"], $resArr["createdBy"], $resArr["dir"], $resArr["orgFileName"], $resArr["fileType"], $resArr["mimeType"], $resArr['fileSize'], $resArr['checksum'], $resArr['revisiondate']); } return $this->_latestContent; } /* }}} */ @@ -1923,8 +2229,8 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ * Get the latest version of document * * This function returns the latest accessible version of a document. - * If content access has been restricted by setting - * {@link SeedDMS_Core_DMS::noReadForStatus} the function will go + * If content access has been restricted by the role of the user + * the function will go * backwards in history until an accessible version is found. If none * is found null will be returned. * Access rights based on the document status are calculated for the @@ -1943,9 +2249,9 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $classname = $this->_dms->getClassname('documentcontent'); $user = $this->_dms->getLoggedInUser(); foreach ($resArr as $row) { + /** @var SeedDMS_Core_DocumentContent $content */ if (!$this->_latestContent) { - /** @var SeedDMS_Core_DocumentContent $content */ - $content = new $classname($row["id"], $this, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum']); + $content = new $classname($row["id"], $this, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum'], $row['revisiondate']); if($user) { /* If the user may even write the document, then also allow to see all content. * This is needed because the user could upload a new version @@ -1989,6 +2295,12 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ return false; } + $queryStr = "DELETE FROM `tblTransmittalItems` WHERE `document` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $queryStr = "DELETE FROM `tblDocumentStatusLog` WHERE `statusID` = '".$stID."'"; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); @@ -2060,17 +2372,78 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ return false; } + /* Remove all receipts of document version. + * This implmentation is different from the above for removing approvals + * and reviews. It doesn't use getReceiptStatus() but reads the database + */ + $queryStr = "SELECT * FROM `tblDocumentRecipients` WHERE `documentID` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'"; + $resArr = $db->getResultArray($queryStr); + if ((is_bool($resArr) && !$resArr)) { + $db->rollbackTransaction(); + return false; + } + + $stList = array(); + foreach($resArr as $res) { + $stList[] = $res['receiptID']; + } + + if ($stList) { + $queryStr = "DELETE FROM `tblDocumentReceiptLog` WHERE `receiptID` IN (".implode(',', $stList).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $queryStr = "DELETE FROM `tblDocumentRecipients` WHERE `receiptID` IN (".implode(',', $stList).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + } + + /* Remove all revisions of document version. + * This implementation is different from the above for removing approvals + * and reviews. It doesn't use getRevisionStatus() but reads the database + */ + $queryStr = "SELECT * FROM `tblDocumentRevisors` WHERE `documentID` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'"; + $resArr = $db->getResultArray($queryStr); + if ((is_bool($resArr) && !$resArr)) { + $db->rollbackTransaction(); + return false; + } + + $stList = array(); + foreach($resArr as $res) { + $stList[] = $res['revisionID']; + } + + if ($stList) { + $queryStr = "DELETE FROM `tblDocumentRevisionLog` WHERE `revisionID` IN (".implode(',', $stList).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $queryStr = "DELETE FROM `tblDocumentRevisors` WHERE `revisionID` IN (".implode(',', $stList).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + } + $queryStr = "DELETE FROM `tblWorkflowDocumentContent` WHERE `document` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'"; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; } - $queryStr = "DELETE FROM `tblWorkflowLog` WHERE `document` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'"; + /* Will be deleted automatically when record will be deleted + * from tblWorkflowDocumentContent + $queryStr = "DELETE FROM `tblWorkflowLog` WHERE `document` = '". $this->getID() ."' AND `version` = '" . $version->getVersion."'"; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; } + */ // remove only those document files attached to version $res = $this->getDocumentFiles($version->getVersion(), false); @@ -2341,7 +2714,8 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ if ((is_bool($resArr) && !$resArr) || count($resArr)==0) return false; $resArr = $resArr[0]; - $file = new SeedDMS_Core_DocumentFile($resArr["id"], $this, $resArr["userID"], $resArr["comment"], $resArr["date"], $resArr["dir"], $resArr["fileType"], $resArr["mimeType"], $resArr["orgFileName"], $resArr["name"],$resArr["version"],$resArr["public"]); + $classname = $this->_dms->getClassname('documentfile'); + $file = new $classname($resArr["id"], $this, $resArr["userID"], $resArr["comment"], $resArr["date"], $resArr["dir"], $resArr["fileType"], $resArr["mimeType"], $resArr["orgFileName"], $resArr["name"],$resArr["version"],$resArr["public"]); $user = $this->_dms->getLoggedInUser(); if($file->getAccessMode($user) >= M_READ) return $file; @@ -2382,8 +2756,9 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $this->_documentFiles = array($hash=>array()); $user = $this->_dms->getLoggedInUser(); + $classname = $this->_dms->getClassname('documentfile'); foreach ($resArr as $row) { - $file = new SeedDMS_Core_DocumentFile($row["id"], $this, $row["userID"], $row["comment"], $row["date"], $row["dir"], $row["fileType"], $row["mimeType"], $row["orgFileName"], $row["name"], $row["version"], $row["public"]); + $file = new $classname($row["id"], $this, $row["userID"], $row["comment"], $row["date"], $row["dir"], $row["fileType"], $row["mimeType"], $row["orgFileName"], $row["name"], $row["version"], $row["public"]); if($file->getAccessMode($user) >= M_READ) array_push($this->_documentFiles[$hash], $file); } @@ -2545,6 +2920,11 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $db->rollbackTransaction(); return false; } + $queryStr = "DELETE FROM `tblDocumentCheckOuts` WHERE `document` = " . $this->_id; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } $queryStr = "DELETE FROM `tblDocumentFiles` WHERE `document` = " . $this->_id; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); @@ -2804,18 +3184,16 @@ class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */ $timeline = array(); - /* No need to add entries for new version because the status log - * will generate an entry as well. - $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = " . $this->_id; + $lc=$this->getLatestContent(); + $queryStr = "SELECT `revisiondate`, `version` FROM `tblDocumentContent` WHERE `document` = " . $this->_id . " AND `version` = " . $lc->getVersion(); $resArr = $db->getResultArray($queryStr); if (is_bool($resArr) && $resArr == false) return false; foreach ($resArr as $row) { - $date = date('Y-m-d H:i:s', $row['date']); - $timeline[] = array('date'=>$date, 'msg'=>'Added version '.$row['version'], 'type'=>'add_version', 'version'=>$row['version'], 'document'=>$this, 'params'=>array($row['version'])); + if($row['revisiondate'] && substr($row['revisiondate'], 0, 4) != '0000') + $timeline[] = array('date'=>substr($row['revisiondate'], 0, 10)." 00:00:00", 'allday'=>true, 'msg'=>'Scheduled revision of version '.$row['version'], 'type'=>'scheduled_revision', 'version'=>$row['version'], 'document'=>$this, 'params'=>array($row['version'])); } - */ $queryStr = "SELECT * FROM `tblDocumentFiles` WHERE `document` = " . $this->_id; $resArr = $db->getResultArray($queryStr); @@ -3000,7 +3378,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ * If status is draft and there are no approver and no reviewers => set * status to S_RELEASED * The status of a document with the current status S_OBSOLETE, S_REJECTED, - * or S_EXPIRED will not be changed unless the parameter + * S_NEEDS_CORRECTION or S_EXPIRED will not be changed unless the parameter * $ignorecurrentstatus is set to true. * * This method may not be called after a negative approval or review to @@ -3015,21 +3393,30 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ * This method will call {@see SeedDMS_Core_DocumentContent::setStatus()} * which checks if the status has actually changed. This is, why this * function can be called at any time without harm to the status log. + * The $initialstatus can be set, to define the status set when no other + * status is set. This happens if the document has no * * @param boolean $ignorecurrentstatus ignore the current status and * recalculate a new status in any case * @param object $user the user initiating this method * @param string $msg message stored in status log when status is set + * @param integer $initialstatus status to be set if no other status is set */ - function verifyStatus($ignorecurrentstatus=false, $user=null, $msg='') { /* {{{ */ + function verifyStatus($ignorecurrentstatus=false, $user=null, $msg='', $initialstatus=S_RELEASED) { /* {{{ */ unset($this->_status); $st=$this->getStatus(); - if (!$ignorecurrentstatus && ($st["status"]==S_OBSOLETE || $st["status"]==S_REJECTED || $st["status"]==S_EXPIRED )) return $st['status']; + /* Documents already obsoleted, rejected or expired will not change + * its status anymore, unless explicitly requested. Be aware, that + * this method has an unsufficient check for negative reviews and + * approvals. A document in status S_REJECTED may become S_RELEASED + * if there is at least one positive review or approval. + */ + if (!$ignorecurrentstatus && ($st["status"]==S_OBSOLETE || $st["status"]==S_REJECTED || $st["status"]==S_EXPIRED || $st["status"]==S_NEEDS_CORRECTION)) return $st['status']; $this->_workflow = null; // force to be reloaded from DB - $hasworkflow = $this->getWorkflow() ? true : false; + $hasworkflow = $this->getWorkflow() ? true : false; /* $pendingReview will be set when there are still open reviews */ $pendingReview=false; @@ -3064,16 +3451,70 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ } } } + $pendingRevision=false; + $hasRevision=false; + $needsCorrection=false; + unset($this->_revisionStatus); // force to be reloaded from DB + $revsisionStatus=$this->getRevisionStatus(); + if (is_array($revsisionStatus) && count($revsisionStatus)>0) { + foreach ($revsisionStatus as $a){ + if ($a["status"]==0){ + $pendingRevision=true; + break; + } elseif($a["status"]==1){ + $hasRevision=true; + } elseif($a["status"]==-1){ + $needsCorrection=true; + } + } + } - /* First check for a running workflow or open reviews or approvals. */ + $ret = false; + /* First check for a running workflow or open reviews, approvals, revisions. */ if ($hasworkflow) { $newstatus = S_IN_WORKFLOW; $ret = $this->setStatus(S_IN_WORKFLOW,$msg,$user); } elseif ($pendingReview) { $newstatus = S_DRAFT_REV; $ret = $this->setStatus(S_DRAFT_REV,$msg,$user); } elseif ($pendingApproval) { $newstatus = S_DRAFT_APP; $ret = $this->setStatus(S_DRAFT_APP,$msg,$user); } - else { $newstatus = S_RELEASED; $ret = $this->setStatus(S_RELEASED,$msg,$user); } + elseif ($pendingRevision) { $newstatus = S_IN_REVISION; $ret = $this->setStatus(S_IN_REVISION,$msg,$user); } + /* This point will only be reached if there is no pending workflow, review, + * approval or revision but the current status is one of S_DRAFT_REV, + * S_DRAFT_APP or S_IN_REVISION. This can happen if formely set reviewers, + * approvers, revisors are completly removed. In case of S_DRAFT_REV and + * S_DRAFT_APP the document will go back into its initial status. If a + * positive review or approval was found the document will be released. + * Be aware that negative reviews or approvals are not taken into account, + * because in that case the document must have been rejected before calling + * this function. FIXME: this is a problem if the parameter $ignorecurrentstatus + * was set, because an already rejected document may be released with just + * one positive review or approval disregarding any negative reviews or + * approvals. + * A document in status S_IN_REVISION will be treated differently. + * It takes negative revisions into account! + * + * A document in status S_DRAFT will never go into S_RELEASED and document + * already released will never go back at this point into the given + * initial status, which can only by S_DRAFT or S_RELEASED + */ + elseif ($st["status"]!=S_DRAFT && $st["status"]!=S_RELEASED ) { + if($st["status"]==S_DRAFT_REV || $st["status"]==S_DRAFT_APP) { + if($hasReview || $hasApproval) { $newstatus = S_RELEASED; $ret = $this->setStatus(S_RELEASED,$msg,$user); } + else { $newstatus = $initialstatus; $ret = $this->setStatus($initialstatus,$msg,$user); } + } elseif($st["status"]==S_IN_REVISION) { + if($needsCorrection) { $newstatus = S_NEEDS_CORRECTION; $ret = $this->setStatus(S_NEEDS_CORRECTION,$msg,$user); } + else { + $newstatus = S_RELEASED; + $ret = $this->finishRevision($user, S_RELEASED, 'Finished revision workflow', $msg); + } + } elseif($st["status"]==S_EXPIRED) { + $newstatus = S_RELEASED; $ret = $this->setStatus(S_RELEASED,$msg,$user); + } elseif($st["status"]==S_IN_WORKFLOW) { + $newstatus = $initialstatus; $ret = $this->setStatus($initialstatus,$msg,$user); + } + } + return $ret ? $newstatus : $ret; } /* }}} */ - function __construct($id, $document, $version, $comment, $date, $userID, $dir, $orgFileName, $fileType, $mimeType, $fileSize=0, $checksum='') { /* {{{ */ + function __construct($id, $document, $version, $comment, $date, $userID, $dir, $orgFileName, $fileType, $mimeType, $fileSize=0, $checksum='', $revisionDate=null) { /* {{{ */ parent::__construct($id); $this->_document = $document; $this->_version = (int) $version; @@ -3093,6 +3534,41 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $this->_checksum = $checksum; $this->_workflow = null; $this->_workflowState = null; + $this->_revisionDate = $revisionDate; + } /* }}} */ + + /** + * Return an document content by its id + * + * @param integer $id id of document + * @param SeedDMS_Core_DMS $dms + * @return bool|SeedDMS_Core_DocumentContent instance of SeedDMS_Core_DocumentContent + * if document content exists, null if document does not exist, false in case of error + */ + public static function getInstance($id, $dms) { /* {{{ */ + $db = $dms->getDB(); + + $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `id` = " . (int) $id; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + if (count($resArr) != 1) + return null; + $row = $resArr[0]; + + $classname = $dms->getClassname('documentcontent'); + $user = $dms->getLoggedInUser(); + $document = $dms->getDocument($row['document']); + $document->setDMS($dms); + /** @var SeedDMS_Core_DocumentContent $documentcontent */ + $content = new $classname($row["id"], $document, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum'], $row['revisiondate']); + if($user) { + if($content->getAccessMode($user) >= M_READ) + return $content; + } else { + return $content; + } + return null; } /* }}} */ /** @@ -3115,6 +3591,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ */ function __getDir() { return $this->_dir; } function getMimeType() { return $this->_mimeType; } + function getRevisionDate() { return $this->_revisionDate; } function getDocument() { return $this->_document; } function getUser() { /* {{{ */ @@ -3134,6 +3611,23 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ */ function getPath() { return $this->_document->getDir() . $this->_version . $this->_fileType; } + function setRevisionDate($date = false) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + if(!$date) + $queryStr = "UPDATE `tblDocumentContent` SET `revisiondate` = null WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version; + elseif($date == 'now') + $queryStr = "UPDATE `tblDocumentContent` SET `revisiondate` = ".$db->getCurrentDatetime()." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version; + else + $queryStr = "UPDATE `tblDocumentContent` SET `revisiondate` = ".$db->qstr($date)." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version; + if (!$db->getResult($queryStr)) + return false; + + $this->_revisionDate = $date; + + return true; + } /* }}} */ + /** * Set upload date of document content * @@ -3278,6 +3772,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ * S_DRAFT_APP, 1 * S_RELEASED, 2 * S_IN_WORKFLOW, 3 + * S_IN_REVISION, 4 * S_REJECTED, -1 * S_OBSOLETE, -2 * S_EXPIRED, -3 @@ -3498,15 +3993,20 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ } } - return M_READ; +// return M_READ; if(!$u) return M_NONE; /* If read access isn't further restricted by status, than grant read access */ + /* Old code if(!$dms->noReadForStatus) return M_READ; $noReadForStatus = $dms->noReadForStatus; + */ + $noReadForStatus = $u->getRole()->getNoAccess(); + if(!$noReadForStatus) + return M_READ; /* If the current status is not in list of status without read access, then grant read access */ if(!in_array($this->getStatus()['status'], $noReadForStatus)) @@ -3571,13 +4071,30 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if($this->_workflow) { if (!$this->_workflowState) $this->getWorkflowState(); - $transitions = $this->_workflow->getNextTransitions($this->_workflowState); + $transitions = $this->_workflow['workflow']->getNextTransitions($this->_workflowState); foreach($transitions as $transition) { if($this->triggerWorkflowTransitionIsAllowed($u, $transition)) return M_READ; } } break; + case S_IN_REVISION: + $status = $this->getRevisionStatus(); + foreach ($status as $r) { + if($r['status'] != -2) // Check if reviewer was removed + switch ($r["type"]) { + case 0: // Revisor is an individual. + if($u->getId() == $r["required"]) + return M_READ; + break; + case 1: // Revisor is a group. + $required = $dms->getGroup($r["required"]); + if (is_object($required) && $required->isMember($u)) + return M_READ; + break; + } + } + break; case S_REJECTED: break; case S_OBSOLETE: @@ -3949,6 +4466,328 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ return true; } /* }}} */ + /** + * Get the current receipt status of the document content + * The receipt status is a list of receipts + * + * @param integer $limit maximum number of status changes per receiver + * @return array list of receipts + */ + function getReceiptStatus($limit=1) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + if (!is_numeric($limit)) return false; + + // Retrieve the current status of each assigned reviewer for the content + // represented by this object. + // When just the last log entry for each recipient is needed then a single + // sql statement is much faster than the code below which first retrieves + // all receivers and than the logs + // FIXME: caching was turned off to make list of review log in ViewDocument + // possible + if($limit == 1) { + /* The following sql statement is somewhat optimized. The first join is + * crucial because it should first take the table with the least number + * of records and join the other tables. ttreceiptid join tblDocumentRecipients + * is faster than tblDocumentRecipients join ttreceiptid + */ + if (!$db->createTemporaryTable("ttreceiptid")) { + return false; + } + $queryStr= + "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`receiptLogID`, `tblDocumentReceiptLog`.`status`, `tblDocumentReceiptLog`.`comment`, `tblDocumentReceiptLog`.`date`, `tblDocumentReceiptLog`.`userID`, `tblUsers`.`fullName`, `tblGroups`.`name` FROM `ttreceiptid` LEFT JOIN `tblDocumentRecipients` ON `tblDocumentRecipients`.`receiptID`=`ttreceiptid`.`receiptID` LEFT JOIN `tblDocumentReceiptLog` ON `ttreceiptid`.`maxLogID`=`tblDocumentReceiptLog`.`receiptLogID` LEFT JOIN `tblUsers` ON `tblDocumentRecipients`.`required`=`tblUsers`.`id` LEFT JOIN `tblGroups` ON `tblDocumentRecipients`.`required`=`tblGroups`.`id` WHERE `version`='".$this->_version + ."' AND `documentID` = '". $this->_document->getID() ."' "; + $recs = $db->getResultArray($queryStr); + if (is_bool($recs) && !$recs) { + unset($this->_receiptStatus); + return false; + } + $this->_receiptStatus = $recs; + } elseif (1 || !isset($this->_receiptStatus)) { + /* First get a list of all receipts for this document content */ + $queryStr= + "SELECT `receiptID` FROM `tblDocumentRecipients` WHERE `version`='".$this->_version + ."' AND `documentID` = '". $this->_document->getID() ."' "; + $recs = $db->getResultArray($queryStr); + if (is_bool($recs) && !$recs) + return false; + $this->_receiptStatus = array(); + if($recs) { + foreach($recs as $rec) { + $queryStr= + "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`receiptLogID`, ". + "`tblDocumentReceiptLog`.`status`, ". + "`tblDocumentReceiptLog`.`comment`, ". + "`tblDocumentReceiptLog`.`date`, ". + "`tblDocumentReceiptLog`.`userID`, `tblUsers`.`fullName`, `tblGroups`.`name` AS `groupName` ". + "FROM `tblDocumentRecipients` ". + "LEFT JOIN `tblDocumentReceiptLog` USING (`receiptID`) ". + "LEFT JOIN `tblUsers` on `tblUsers`.`id` = `tblDocumentRecipients`.`required` ". + "LEFT JOIN `tblGroups` on `tblGroups`.`id` = `tblDocumentRecipients`.`required` ". + "WHERE `tblDocumentRecipients`.`receiptID` = '". $rec['receiptID'] ."' ". + "ORDER BY `tblDocumentReceiptLog`.`receiptLogID` DESC LIMIT ".(int) $limit; + + $res = $db->getResultArray($queryStr); + if (is_bool($res) && !$res) { + unset($this->_receiptStatus); + return false; + } + $this->_receiptStatus = array_merge($this->_receiptStatus, $res); + } + } + } + return $this->_receiptStatus; + } /* }}} */ + + /** + * Rewrites the complete receipt log + * + * Attention: this function is highly dangerous. + * It removes an existing receipt log and rewrites it. + * This method was added for importing an xml dump. + * + * @param array $receiptlog new status log with the newest log entry first. + * @return boolean true on success, otherwise false + */ + function rewriteReceiptLog($recipients) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + $queryStr= "SELECT `tblDocumentRecipients`.* FROM `tblDocumentRecipients` WHERE `tblDocumentRecipients`.`documentID` = '". $this->_document->getID() ."' AND `tblDocumentRecipients`.`version` = '". $this->_version ."' "; + $res = $db->getResultArray($queryStr); + if (is_bool($res) && !$res) + return false; + + $db->startTransaction(); + + if($res) { + foreach($res as $receipt) { + $receiptID = $receipt['receiptID']; + + /* First, remove the old entries */ + $queryStr = "DELETE from `tblDocumentReceiptLog` where `receiptID`=".$receiptID; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + + $queryStr = "DELETE from `tblDocumentRecipients` where `receiptID`=".$receiptID; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + } + } + + /* Second, insert the new entries */ + foreach($recipients as $receipt) { + $queryStr = "INSERT INTO `tblDocumentRecipients` (`documentID`, `version`, `type`, `required`) ". + "VALUES ('".$this->_document->getID()."', '".$this->_version."', ".$receipt['type'] .", ".(is_object($receipt['required']) ? $receipt['required']->getID() : (int) $receipt['required']).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $receiptID = $db->getInsertID('tblDocumentRecipients', 'receiptID'); + $receiptlog = array_reverse($receipt['logs']); + foreach($receiptlog as $log) { + if(!SeedDMS_Core_DMS::checkDate($log['date'], 'Y-m-d H:i:s')) { + $db->rollbackTransaction(); + return false; + } + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('".$receiptID ."', '".(int) $log['status']."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".(is_object($log['user']) ? $log['user']->getID() : (int) $log['user']).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $receiptLogID = $db->getInsertID('tblDocumentReceiptLog', 'receiptLogID'); + if(!empty($log['file'])) { + SeedDMS_Core_File::copyFile($log['file'], $this->_dms->contentDir . $this->_document->getDir() . 'r' . $receiptLogID); + } + } + } + + $db->commitTransaction(); + return true; + } /* }}} */ + + /** + * Get the current revision status of the document content + * The revision status is a list of revisions + * If $limit is 1 it will return just the last log entry for each + * revisor. + * Keep in mind that a revision log may contain repeating revisions. + * + * @param integer $limit maximum number of records per revisor + * @return array list of revisions + */ + function getRevisionStatus($limit=1) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + if (!is_numeric($limit)) return false; + + // Retrieve the current status of each assigned reviewer for the content + // represented by this object. + // FIXME: caching was turned off to make list of review log in ViewDocument + // possible + if (1 || !isset($this->_revisionStatus)) { + /* First get a list of all revisions for this document content */ + $queryStr= + "SELECT `revisionID` FROM `tblDocumentRevisors` WHERE `version`='".$this->_version + ."' AND `documentID` = '". $this->_document->getID() ."' "; + $recs = $db->getResultArray($queryStr); + if (is_bool($recs) && !$recs) + return false; + $this->_revisionStatus = array(); + if($recs) { + foreach($recs as $rec) { + $queryStr= + "SELECT `tblDocumentRevisors`.*, `tblDocumentRevisionLog`.`revisionLogID`, ". + "`tblDocumentRevisionLog`.`status`, ". + "`tblDocumentRevisionLog`.`comment`, ". + "`tblDocumentRevisionLog`.`date`, ". + "`tblDocumentRevisionLog`.`userID`, `tblUsers`.`fullName`, `tblGroups`.`name` AS `groupName` ". + "FROM `tblDocumentRevisors` ". + "LEFT JOIN `tblDocumentRevisionLog` USING (`revisionID`) ". + "LEFT JOIN `tblUsers` on `tblUsers`.`id` = `tblDocumentRevisors`.`required` ". + "LEFT JOIN `tblGroups` on `tblGroups`.`id` = `tblDocumentRevisors`.`required` ". + "WHERE `tblDocumentRevisors`.`revisionID` = '". $rec['revisionID'] ."' ". + "ORDER BY `tblDocumentRevisionLog`.`revisionLogID` DESC LIMIT ".(int) $limit; + + $res = $db->getResultArray($queryStr); + if (is_bool($res) && !$res) { + unset($this->_revisionStatus); + return false; + } + $this->_revisionStatus = array_merge($this->_revisionStatus, $res); + } + } + } + return $this->_revisionStatus; + } /* }}} */ + + /** + * Rewrites the complete revision log + * + * Attention: this function is highly dangerous. + * It removes an existing revision log and rewrites it. + * This method was added for importing an xml dump. + * + * @param array $revisionlog new status log with the newest log entry first. + * @return boolean 0 on success, otherwise a negativ error number + */ + function rewriteRevisionLog($revisions) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + $queryStr= "SELECT `tblDocumentRevisors`.* FROM `tblDocumentRevisors` WHERE `tblDocumentRevisors`.`documentID` = '". $this->_document->getID() ."' AND `tblDocumentRevisors`.`version` = '". $this->_version ."' "; + $res = $db->getResultArray($queryStr); + if (is_bool($res) && !$res) + return false; + + $db->startTransaction(); + + if($res) { + foreach($res as $revision) { + $revisionID = $revision['revisionID']; + + /* First, remove the old entries */ + $queryStr = "DELETE from `tblDocumentRevisionLog` where `revisionID`=".$revisionID; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + + $queryStr = "DELETE from `tblDocumentRevisors` where `revisionID`=".$revisionID; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + } + } + + /* Second, insert the new entries */ + foreach($revisions as $revision) { + $queryStr = "INSERT INTO `tblDocumentRevisors` (`documentID`, `version`, `type`, `required`) ". + "VALUES ('".$this->_document->getID()."', '".$this->_version."', ".$revision['type'] .", ".(is_object($revision['required']) ? $revision['required']->getID() : (int) $revision['required']).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $revisionID = $db->getInsertID('tblDocumentRevisors', 'revisionID'); + $revisionlog = array_reverse($revision['logs']); + foreach($revisionlog as $log) { + if(!SeedDMS_Core_DMS::checkDate($log['date'], 'Y-m-d H:i:s')) { + $db->rollbackTransaction(); + return false; + } + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('".$revisionID ."', '".(int) $log['status']."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".(is_object($log['user']) ? $log['user']->getID() : (int) $log['user']).")"; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + $revisionLogID = $db->getInsertID('tblDocumentRevisionLog', 'revisionLogID'); + if(!empty($log['file'])) { + SeedDMS_Core_File::copyFile($log['file'], $this->_dms->contentDir . $this->_document->getDir() . 'r' . $revisionLogID); + } + } + } + + $db->commitTransaction(); + return true; + } /* }}} */ + + /** + * Check if document version has a scheduled revision workflow. + * The method will update the document status log database table + * if needed and set the revisiondate of the content to $next. + * + * FIXME: This method does not check if there are any revisors left. Even + * if all revisors have been removed, it will still start the revision workflow! + * NOTE: This seems not the case anymore. The status of each revision is + * checked. Only if at least one status is S_LOG_SLEEPING the revision will be + * started. This wouldn't be the case if all revisors had been removed. + * + * @param object $user user requesting the possible automatic change + * @param string $next next date for review + * @return boolean true if status has changed + */ + function checkForDueRevisionWorkflow($user, $next=''){ /* {{{ */ + $st=$this->getStatus(); + + /* A revision workflow will only be started if the document version is released */ + if($st["status"] == S_RELEASED) { + /* First check if there are any scheduled revisions currently sleeping */ + $pendingRevision=false; + unset($this->_revisionStatus); // force to be reloaded from DB + $revisionStatus=$this->getRevisionStatus(); + if (is_array($revisionStatus) && count($revisionStatus)>0) { + foreach ($revisionStatus as $a){ + if ($a["status"]==S_LOG_SLEEPING || $a["status"]==S_LOG_SLEEPING){ + $pendingRevision=true; + break; + } + } + } + if(!$pendingRevision) + return false; + + /* We have sleeping revision, next check if the revision is already due */ + if($this->getRevisionDate() && $this->getRevisionDate() <= date('Y-m-d 00:00:00')) { + if($this->startRevision($user, 'Automatic start of revision workflow scheduled for '.$this->getRevisionDate())) { + if($next) { + $tmp = explode('-', substr($next, 0, 10)); + if(checkdate($tmp[1], $tmp[2], $tmp[0])) + $this->setRevisionDate($next); + } else { + $this->setRevisionDate(false); + } + return true; + } + } + } + return false; + } /* }}} */ + /** * Add user as new reviewer * @@ -4228,7 +5067,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $db = $this->_document->getDMS()->getDB(); if(!$group->isType('group')) - return -1; + return -1; // Check if the group is on the review list at all. $reviewStatus = $group->getReviewStatus($this->_document->getID(), $this->_version); @@ -4522,7 +5361,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ return false; return true; - } /* }}} */ + } /* }}} */ /** * Sets approval status of a document content for a group @@ -4573,6 +5412,427 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ SeedDMS_Core_File::copyFile($file, $this->_dms->contentDir . $this->_document->getDir() . 'a' . $approveLogID); } return $approveLogID; + } /* }}} */ + + function addIndRecipient($user, $requestUser) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + $userID = $user->getID(); + + // Get the list of users and groups with read access to this document. + if($this->_document->getAccessMode($user) < M_READ) { + return -2; + } + + // Check to see if the user has already been added to the receipt list. + $receiptStatus = $user->getReceiptStatus($this->_document->getID(), $this->_version); + if (is_bool($receiptStatus) && !$receiptStatus) { + return -1; + } + $indstatus = false; + if (count($receiptStatus["indstatus"]) > 0) { + $indstatus = array_pop($receiptStatus["indstatus"]); + if($indstatus["status"]!=-2) { + // User is already on the list of recipients; return an error. + return -3; + } + } + + // Add the user into the recipients database. + if (!$indstatus || ($indstatus && $indstatus["status"]!=-2)) { + $queryStr = "INSERT INTO `tblDocumentRecipients` (`documentID`, `version`, `type`, `required`) ". + "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '0', '". $userID ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + $receiptID = $db->getInsertID('tblDocumentRecipients', 'receiptID'); + } + else { + $receiptID = isset($indstatus["receiptID"]) ? $indstatus["receiptID"] : NULL; + } + + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $receiptID ."', '0', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + + // Add recipient to event notification table. + //$this->_document->addNotify($userID, true); + + $receiptLogID = $db->getInsertID('tblDocumentReceiptLog', 'receiptLogID'); + $db->dropTemporaryTable('ttreceiptid'); + return $receiptLogID; + } /* }}} */ + + function addGrpRecipient($group, $requestUser) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + $groupID = $group->getID(); + + // Get the list of users and groups with read access to this document. + if (!isset($this->_readAccessList)) { + // TODO: error checking. + $this->_readAccessList = $this->_document->getReadAccessList(); + } + $approved = false; + foreach ($this->_readAccessList["groups"] as $appGroup) { + if ($groupID == $appGroup->getID()) { + $approved = true; + break; + } + } + if (!$approved) { + return -2; + } + + // Check to see if the group has already been added to the review list. + $receiptStatus = $group->getReceiptStatus($this->_document->getID(), $this->_version); + if (is_bool($receiptStatus) && !$receiptStatus) { + return -1; + } + $status = false; + if (count($receiptStatus["status"]) > 0) { + $status = array_pop($receiptStatus["status"]); + if($status["status"]!=-2) { + // User is already on the list of recipients; return an error. + return -3; + } + } + + // Add the group into the recipients database. + if (!$status || ($status && $status["status"]!=-2)) { + $queryStr = "INSERT INTO `tblDocumentRecipients` (`documentID`, `version`, `type`, `required`) ". + "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '1', '". $groupID ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + $receiptID = $db->getInsertID('tblDocumentRecipients', 'receiptID'); + } + else { + $receiptID = isset($status["receiptID"]) ? $status["receiptID"] : NULL; + } + + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $receiptID ."', '0', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + + $receiptLogID = $db->getInsertID('tblDocumentReceiptLog', 'receiptLogID'); + $db->dropTemporaryTable('ttreceiptid'); + return $receiptLogID; + } /* }}} */ + + /** + * Add an individual revisor to the document content + * + * This function adds a user as a revisor but doesn't start the + * revision workflow by default. This behaviour is different from all + * other workflows (approval, review, receipt), because it adds + * an initial entry in the revision log, which marks the revision as + * 'sleeping'. The workflow is started at a later point in time by adding + * the second entry in the revision log which puts it into 'waiting'. + * + * @param object $user user to be added as a revisor + * @param object $requestUser user requesting the addition + * @return integer 0 if successful otherwise a value < 0 + */ + function addRevisor($object, $requestUser) { /* {{{ */ + $dms = $this->_document->getDMS(); + $db = $dms->getDB(); + + /* getRevisionStatus() returns an array with either an element + * 'indstatus' (user) or 'status' (group) containing the revision log + */ + if(get_class($object) == $dms->getClassname('user')) { + $field = 'indstatus'; + $type = 0; + + // Get the list of users and groups with read access to this document. + if($this->_document->getAccessMode($object) < M_READ) { + return -2; + } + } elseif(get_class($object) == $dms->getClassname('group')) { + $field = 'status'; + $type = 1; + + // Get the list of users and groups with read access to this document. + if($this->_document->getGroupAccessMode($object) < M_READ) { + return -2; + } + } else { + return -1; + } + + // Check to see if the user has already been added to the revisor list. + $revisionStatus = $object->getRevisionStatus($this->_document->getID(), $this->_version); + if (is_bool($revisionStatus) && !$revisionStatus) { + return -1; + } + + /* There are two cases: 1. the user has not been added at all or 2. + * the user was added before but has been removed later. In both + * cases the user may be added. In case 2. 'indstatus' will be set + * and the last status is -2. If it is not -2, then the user is still + * in the process and cannot be added again. + */ + $indstatus = false; + if(isset($revisionStatus[$field])) { + if (count($revisionStatus[$field]) > 0) { + $indstatus = array_pop($revisionStatus[$field]); + if($indstatus["status"] != S_LOG_USER_REMOVED) { + // User is already on the list of recipients; return an error. + return -3; + } + } + } + + // Add the user into the revisors database. + if (!$indstatus) { + $queryStr = "INSERT INTO `tblDocumentRevisors` (`documentID`, `version`, `type`, `required`) ". + "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '". $type ."', '". $object->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + $revisionID = $db->getInsertID('tblDocumentRevisors', 'revisionID'); + } else { + $revisionID = isset($indstatus["revisionID"]) ? $indstatus["revisionID"] : NULL; + } + + /* If a user is added when the revision has already been startet, then + * put it into S_LOG_WAITING otherwise into S_LOG_SLEEPING. Attention, if a + * document content is in any other status but S_IN_REVISION, then it will + * end up in S_LOG_SLEEPING. As this method is also called by removeFromProcesses() + * when another user takes over the processes, it may happen that revisions + * of document contents in status e.g. S_OBSOLETE, S_EXPIRED will change its + * status from S_LOG_WAITING to S_LOG_SLEEPING. + * This could only be fixed if this method could set an initial revision status + * by possibly passing it as another parameter to the method. + */ + $st=$this->getStatus(); + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $revisionID ."', '".($st["status"] == S_IN_REVISION ? S_LOG_WAITING : S_LOG_SLEEPING)."', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + + $revisionLogID = $db->getInsertID('tblDocumentRevisionLog', 'revisionLogID'); + $db->dropTemporaryTable('ttrevisionid'); + return $revisionLogID; + } /* }}} */ + + function addIndRevisor($user, $requestUser, $donotstart=true) { /* {{{ */ + return self::addRevisor($user, $requestUser, $donotstart); + } /* }}} */ + + function addGrpRevisor($group, $requestUser, $donotstart=true) { /* {{{ */ + return self::addRevisor($group, $requestUser, $donotstart); + } /* }}} */ + + /** + * Add a receipt to the document content + * + * This method will add an entry to the table tblDocumentReceiptLog. + * It will first check if the user is ment to receipt the document version. + * If not the return value is -3. + * Next it will check if the user has been removed from the list of + * recipients. In that case -4 will be returned. + * If the given receipt has been set by the user before, it cannot + * be set again and 0 will be returned. Іf the receipt could be succesfully + * added, the receiptview log id will be returned. + * + * @see SeedDMS_Core_DocumentContent::setApprovalByInd() + * @param object $user user doing the receipt + * @param object $requestUser user asking for the receipt, this is mostly + * @param integer $status the status of the receipt, possible values are + * 0=unprocessed (may be used to reset a status) + * 1=received, + * -1=rejected, + * -2=user is deleted (use {link + * SeedDMS_Core_DocumentContent::delIndRecipient} instead) + * the user currently logged in. + * @return integer new receipt log id + */ + function setReceiptByInd($user, $requestUser, $status, $comment) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + // Check to see if the user can be removed from the review list. + $receiptStatus = $user->getReceiptStatus($this->_document->getID(), $this->_version); + if (is_bool($receiptStatus) && !$receiptStatus) { + return -1; + } + if (count($receiptStatus["indstatus"])==0) { + // User is not assigned to receipt this document. No action required. + // Return an error. + return -3; + } + $indstatus = array_pop($receiptStatus["indstatus"]); + if ($indstatus["status"] == S_LOG_USER_REMOVED) { + // User has been deleted from recipients + return -4; + } + // Check if the status is really different from the current status + if ($indstatus["status"] == $status) + return 0; + + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, + `comment`, `date`, `userID`) ". + "VALUES ('". $indstatus["receiptID"] ."', '". + (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '". + $requestUser->getID() ."')"; + $res=$db->getResult($queryStr); + if (is_bool($res) && !$res) + return -1; + else { + $receiptLogID = $db->getInsertID('tblDocumentReceiptLog', 'receiptLogID'); + return $receiptLogID; + } + } /* }}} */ + + /** + * Add a receipt to the document content + * + * This method is similar to + * {@see SeedDMS_Core_DocumentContent::setReceiptByInd()} but adds a receipt + * for a group instead of a user. + * + * @param object $group group doing the receipt + * @param object $requestUser user asking for the receipt, this is mostly + * the user currently logged in. + * @return integer new receipt log id + */ + function setReceiptByGrp($group, $requestUser, $status, $comment) { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + // Check to see if the user can be removed from the recipient list. + $receiptStatus = $group->getReceiptStatus($this->_document->getID(), $this->_version); + if (is_bool($receiptStatus) && !$receiptStatus) { + return -1; + } + if (count($receiptStatus)==0) { + // User is not assigned to receipt this document. No action required. + // Return an error. + return -3; + } + $grpstatus = array_pop($receiptStatus["status"]); + if ($grpstatus["status"] == S_LOG_USER_REMOVED) { + // Group has been deleted from recipients + return -4; + } + + // Check if the status is really different from the current status + if ($grpstatus["status"] == $status) + return 0; + + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, + `comment`, `date`, `userID`) ". + "VALUES ('". $grpstatus["receiptID"] ."', '". + (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '". + $requestUser->getID() ."')"; + $res=$db->getResult($queryStr); + if (is_bool($res) && !$res) + return -1; + else { + $receiptLogID = $db->getInsertID('tblDocumentReceiptLog', 'receiptLogID'); + return $receiptLogID; + } + } /* }}} */ + + /** + * Add a revision to the document content + * + * This method will add an entry to the table tblDocumentRevisionLog. + * It will first check if the user is ment to revision the document version. + * If not the return value is -3. + * Next it will check if the user has been removed from the list of + * recipients. In that case -4 will be returned. + * If the given revision has been set by the user before, it cannot + * be set again and 0 will be returned. Іf the revision could be succesfully + * added, the revision log id will be returned. + * + * @see SeedDMS_Core_DocumentContent::setApprovalByInd() + * @param object $user user doing the revision + * @param object $requestUser user asking for the revision, this is mostly + * the user currently logged in. + * @param integer $status the status of the revision, possible values are + * 0=unprocessed (may be used to reset a status) + * 1=received, + * -2=user is deleted (use {link + * SeedDMS_Core_DocumentContent::delIndRecipient} instead) + * -3=workflow revision is sleeping + * @return integer new revision log id, 0, or a value < 0. 0 means the + * status has not changed because the new status is equal the current + * status. A value < 0 indicate + * an error. -1: internal error, -3: user may not revise this document + * -4: the user has been removed from the list of revisors, + * -5: the revision has not been started at all. + */ + function setRevision($object, $requestUser, $status, $comment) { /* {{{ */ + $dms = $this->_document->getDMS(); + $db = $dms->getDB(); + + /* getRevisionStatus() returns an array with either an element + * 'indstatus' (user) or 'status' (group) containing the revision log + */ + if(get_class($object) == $dms->getClassname('user')) { + $field = 'indstatus'; + } elseif(get_class($object) == $dms->getClassname('group')) { + $field = 'status'; + } else { + return -1; + } + + // Check to see if the user/group can be removed from the review list. + $revisionStatus = $object->getRevisionStatus($this->_document->getID(), $this->_version); + if (is_bool($revisionStatus) && !$revisionStatus) { + return -1; + } + if (!isset($revisionStatus[$field])) { + // User is not assigned to revision this document. No action required. + // Return an error. + return -3; + } + $indstatus = array_pop($revisionStatus[$field]); + + /* check if revision workflow has been started already */ + if($indstatus['status'] == S_LOG_SLEEPING && ($status == S_LOG_REJECTED || $status == S_LOG_ACCEPTED)) + return -5; + + if ($indstatus["status"] == -2) { + // User has been deleted from recipients + return -4; + } + // Check if the status is really different from the current status + if ($indstatus["status"] == $status) + return 0; + + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, + `comment`, `date`, `userID`) ". + "VALUES ('". $indstatus["revisionID"] ."', '". + (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '". + $requestUser->getID() ."')"; + $res=$db->getResult($queryStr); + if (is_bool($res) && !$res) + return -1; + else { + $revisionLogID = $db->getInsertID('tblDocumentRevisionLog', 'revisionLogID'); + return $revisionLogID; + } + } /* }}} */ + + function setRevisionByInd($user, $requestUser, $status, $comment) { /* {{{ */ + return self::setRevision($user, $requestUser, $status, $comment); + } /* }}} */ + + function setRevisionByGrp($group, $requestUser, $status, $comment) { /* {{{ */ + return self::setRevision($group, $requestUser, $status, $comment); } /* }}} */ function delIndReviewer($user, $requestUser, $msg='') { /* {{{ */ @@ -4711,6 +5971,280 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ return 0; } /* }}} */ + function delIndRecipient($user, $requestUser, $msg='') { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + $userID = $user->getID(); + + // Check to see if the user can be removed from the recipient list. + $receiptStatus = $user->getReceiptStatus($this->_document->getID(), $this->_version); + if (is_bool($receiptStatus) && !$receiptStatus) { + return -1; + } + if (count($receiptStatus["indstatus"])==0) { + // User is not assigned to receipt this document. No action required. + // Return an error. + return -2; + } + $indstatus = array_pop($receiptStatus["indstatus"]); + if ($indstatus["status"]!=0) { + // User has already submitted a receipt or has already been deleted; + // return an error. + return -3; + } + + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $indstatus["receiptID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + + return 0; + } /* }}} */ + + function delGrpRecipient($group, $requestUser, $msg='') { /* {{{ */ + $db = $this->_document->getDMS()->getDB(); + + $groupID = $group->getID(); + + // Check to see if the user can be removed from the recipient list. + $receiptStatus = $group->getReceiptStatus($this->_document->getID(), $this->_version); + if (is_bool($receiptStatus) && !$receiptStatus) { + return -1; + } + if (count($receiptStatus["status"])==0) { + // User is not assigned to receipt this document. No action required. + // Return an error. + return -2; + } + $status = array_pop($receiptStatus["status"]); + if ($status["status"]!=0) { + // User has already submitted a receipt or has already been deleted; + // return an error. + return -3; + } + + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $status["receiptID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + + return 0; + } /* }}} */ + + /** + * Removes a user from the revision workflow + * + * This methods behaves differently from one in the other workflows, e.g. + * {@see SeedDMS_Core_DocumentContent::delIndReviewer}, because it + * also takes into account if the workflow has been started already. + * A workflow has been started, when there are entries in the revision log. + * If the revision workflow has not been started, then the user will + * be silently removed from the list of revisors. If the workflow has + * started already, then log entry will indicated the removal of the + * user (just as it is done with the other workflows) + * + * @param object $object user/group which is to be removed + * @param object $requestUser user requesting the removal + * @return integer 0 if removal was successfull, -1 if an internal error + * occured, -3 if the user is not in the list of revisors + * + */ + function delRevisor($object, $requestUser, $msg='') { /* {{{ */ + $dms = $this->_document->getDMS(); + $db = $dms->getDB(); + + /* getRevisionStatus() returns an array with either an element + * 'indstatus' (user) or 'status' (group) containing the revision log + */ + if(get_class($object) == $dms->getClassname('user')) { + $field = 'indstatus'; + $type = 0; + } elseif(get_class($object) == $dms->getClassname('group')) { + $field = 'status'; + $type = 1; + } else { + return -1; + } + + // Check to see if the user/group can be removed from the revisor list. + $revisionStatus = $object->getRevisionStatus($this->_document->getID(), $this->_version); + if (is_bool($revisionStatus) && !$revisionStatus) { + return -1; + } + + if (!isset($revisionStatus[$field])) { + // User is not assigned to revision this document. No action required. + // Return an error. + return -2; + } + + /* If the revision log doesn't contain an entry yet, then remove the + * user/group from the list of revisors. The first case should not happen. + */ + if(count($revisionStatus[$field]) == 0) { + $queryStr = "DELETE from `tblDocumentRevisors` WHERE `documentID` = ". $this->_document->getID() ." AND `version` = ".$this->_version." AND `type` = ". $type ." AND `required` = ".$object->getID(); + if (!$db->getResult($queryStr)) { + return -1; + } + } else { + $indstatus = array_pop($revisionStatus[$field]); + if ($indstatus["status"] != S_LOG_WAITING && $indstatus["status"] != S_LOG_SLEEPING) { + // User has already submitted a revision or has already been deleted; + // return an error. + if($indstatus["status"] == S_LOG_USER_REMOVED) + return -3; + else + return -4; + } + + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $indstatus["revisionID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')"; + $res = $db->getResult($queryStr); + if (is_bool($res) && !$res) { + return -1; + } + } + + return 0; + } /* }}} */ + + function delIndRevisor($user, $requestUser, $msg='') { /* {{{ */ + return self::delRevisor($user, $requestUser, $msg); + } /* }}} */ + + function delGrpRevisor($group, $requestUser, $msg='') { /* {{{ */ + return self::delRevisor($group, $requestUser, $msg); + } /* }}} */ + + /** + * Start a new revision workflow + * + * This function starts a new revision unless there are users/groups + * having finished the previous revision. This means the log status + * must be S_LOG_SLEEPING or the user/group was removed (S_LOG_USER_REMOVED) + * + * @param object $requestUser user requesting the revision start + * @param string $msg message saved for the initial log message + */ + function startRevision($requestUser, $msg='') { /* {{{ */ + $dms = $this->_document->getDMS(); + $db = $dms->getDB(); + + $revisionStatus = self::getRevisionStatus(); + if(!$revisionStatus) + return false; + + /* A new revision may only be started if we are not in the middle of + * revision or the user/group has been removed from the workflow + */ + /* Taken out, because it happened that a revision wasn't started for each revisor + * but just for some. + * Checking for each revisor not being sleeping prevented a second start of the + * revision for the remaining revisors still sleeping. + foreach($revisionStatus as $status) { + if($status['status'] != S_LOG_SLEEPING && $status['status'] != S_LOG_USER_REMOVED) + return false; + } + */ + + /* Make sure all Logs will be set to the right status, in order to + * prevent inconsistent states. Actually it could be a feature to + * force only some users/groups to revise the document, but for now + * this may not be possible. + */ + $db->startTransaction(); + $startedrev = false; + foreach($revisionStatus as $status) { + if($status['status'] == S_LOG_SLEEPING) { + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, + `comment`, `date`, `userID`) ". + "VALUES ('". $status["revisionID"] ."', ". + S_LOG_WAITING.", ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". + $requestUser->getID() ."')"; + $res=$db->getResult($queryStr); + if (is_bool($res) && !$res) { + $db->rollbackTransaction(); + return false; + } + $startedrev = true; + } + } + /* Set status only if at least one revision was started */ + if($startedrev) + if(!$this->setStatus(S_IN_REVISION, "Started revision scheduled for ".$this->getRevisionDate(), $requestUser)) { + $db->rollbackTransaction(); + return false; + } + $db->commitTransaction(); + return true; + + } /* }}} */ + + /** + * Finish a revision workflow + * + * This function ends a revision This means the log status + * is set back S_LOG_SLEEPING and the document status is set as + * passed to the method. The function doesn't not check if all + * users/groups has made it vote already. + * + * @param object $requestUser user requesting the revision start + * @param integer $docstatus document status + * @param string $msg message saved in revision log + * @param string $msg message saved in document status log + */ + function finishRevision($requestUser, $docstatus, $msg='', $docmsg='') { /* {{{ */ + $dms = $this->_document->getDMS(); + $db = $dms->getDB(); + + $revisionStatus = self::getRevisionStatus(); + if(!$revisionStatus) + return false; + + /* A revision may only be finished if it wasn't finished already + */ + foreach($revisionStatus as $status) { + if($status['status'] == S_LOG_SLEEPING) + return false; + } + + /* Make sure all Logs will be set to the right status, in order to + * prevent inconsistent states. Actually it could be a feature to + * end only some users/groups to revise the document, but for now + * this may not be possible. + */ + $db->startTransaction(); + /* Does it make sense to put all revisions into sleeping mode? I guess + * not. If a document was released or rejected the revision are useless + * anyway + */ + foreach($revisionStatus as $status) { + if($status['status'] != S_LOG_SLEEPING && $status['status'] != S_LOG_USER_REMOVED) { + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, + `comment`, `date`, `userID`) ". + "VALUES ('". $status["revisionID"] ."', ". + S_LOG_SLEEPING.", ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". + $requestUser->getID() ."')"; + $res=$db->getResult($queryStr); + if (is_bool($res) && !$res) { + $db->rollbackTransaction(); + return false; + } + } + } + if(!$this->setStatus($docstatus, $docmsg, $requestUser)) { + $db->rollbackTransaction(); + return false; + } + $db->commitTransaction(); + return true; + + } /* }}} */ + /** * Set state of workflow assigned to the document content * @@ -4720,7 +6254,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $db = $this->_document->getDMS()->getDB(); if($this->_workflow) { - $queryStr = "UPDATE `tblWorkflowDocumentContent` set `state`=". $state->getID() ." WHERE `workflow`=". intval($this->_workflow->getID()). " AND `document`=". intval($this->_document->getID()) ." AND version=". intval($this->_version) .""; + $queryStr = "UPDATE `tblWorkflowDocumentContent` set `state`=". $state->getID() ." WHERE `id`=". $this->_workflow['id']; if (!$db->getResult($queryStr)) { return false; } @@ -4747,9 +6281,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if (!$this->_workflowState) { $queryStr= - "SELECT b.* FROM `tblWorkflowDocumentContent` a LEFT JOIN `tblWorkflowStates` b ON a.`state` = b.`id` WHERE a.`state` IS NOT NULL AND `workflow`=". intval($this->_workflow->getID()) - ." AND a.`version`='".$this->_version - ."' AND a.`document` = '". $this->_document->getID() ."' "; + "SELECT b.* FROM `tblWorkflowDocumentContent` a LEFT JOIN `tblWorkflowStates` b ON a.`state` = b.id WHERE a.`state` IS NOT NULL AND `a`.`id`=". $this->_workflow['id']; $recs = $db->getResultArray($queryStr); if (!$recs) return false; @@ -4779,7 +6311,11 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $db->rollbackTransaction(); return false; } - $this->_workflow = $workflow; + $this->getWorkflow(); + if($workflow->getID() != $this->_workflow['workflow']->getID()) { + $db->rollbackTransaction(); + return false; + } if(!$this->setStatus(S_IN_WORKFLOW, "Added workflow '".$workflow->getName()."'", $user)) { $db->rollbackTransaction(); return false; @@ -4796,27 +6332,41 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ * The method returns the last workflow if one was assigned. * If the document version is in a sub workflow, it will have * a never date and therefore will be found first. + * The methods also sets $this->_workflow['id'] and + * $this->_workflow['parent']. $this->_workflow['id'] is the + * id from table tblWorkflowDocumentContent which is used to + * get log entries for this workflow. + * This method will only get a currently running workflow in + * a state. Once a + * workflow has ended, the current state of the workflow was + * set to null. * + * @param bool $full return not just workflow but the data from + * tblWorkflowDocumentContent too * @return object/boolean an object of class SeedDMS_Core_Workflow * or false in case of error, e.g. the version has not a workflow */ - function getWorkflow() { /* {{{ */ + function getWorkflow($full = false) { /* {{{ */ $db = $this->_document->getDMS()->getDB(); if (!$this->_workflow) { $queryStr= - "SELECT b.* FROM `tblWorkflowDocumentContent` a LEFT JOIN `tblWorkflows` b ON a.`workflow` = b.`id` WHERE a.`version`='".$this->_version + "SELECT a.`id` as `wdcid`, a.`parent`, a.`date`, b.* FROM `tblWorkflowDocumentContent` a LEFT JOIN `tblWorkflows` b ON a.`workflow` = b.`id` WHERE a.`version`='".$this->_version ."' AND a.`document` = '". $this->_document->getID() ."' " + ." AND a.`state` IS NOT NULL" ." ORDER BY `date` DESC LIMIT 1"; $recs = $db->getResultArray($queryStr); if (is_bool($recs) && !$recs) return false; if(!$recs) return false; - $this->_workflow = new SeedDMS_Core_Workflow($recs[0]['id'], $recs[0]['name'], $this->_document->getDMS()->getWorkflowState($recs[0]['initstate'])); - $this->_workflow->setDMS($this->_document->getDMS()); + $this->_workflow = array('id'=>(int)$recs[0]['wdcid'], 'parent'=>(int)$recs[0]['parent'], 'date'=>$recs[0]['date'], 'workflow'=>new SeedDMS_Core_Workflow($recs[0]['id'], $recs[0]['name'], $this->_document->getDMS()->getWorkflowState($recs[0]['initstate']), $recs[0]["layoutdata"])); + $this->_workflow['workflow']->setDMS($this->_document->getDMS()); } - return $this->_workflow; + if($full) + return $this->_workflow; + else + return $this->_workflow['workflow']; } /* }}} */ /** @@ -4832,10 +6382,18 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ function rewriteWorkflowLog($workflowlog) { /* {{{ */ $db = $this->_document->getDMS()->getDB(); + /* Get the workflowdocumentcontent */ + $queryStr = "SELECT `id` FROM `tblWorkflowDocumentContent` WHERE `tblWorkflowDocumentContent`.`document` = '". $this->_document->getID() ."' AND `tblWorkflowDocumentContent`.`version` = '". $this->_version ."'"; + $recs = $db->getResultArray($queryStr); + if (is_bool($recs) && !$recs) + return false; + if (!$recs) + return false; + $db->startTransaction(); /* First, remove the old entries */ - $queryStr = "DELETE FROM `tblWorkflowLog` WHERE `tblWorkflowLog`.`document` = '". $this->_document->getID() ."' AND `tblWorkflowLog`.`version` = '". $this->_version ."'"; + $queryStr = "DELETE FROM `tblWorkflowLog` WHERE `tblWorkflowLog`.`workflowdocumentcontent` IN (SELECT `id` FROM `tblWorkflowDocumentContent` WHERE `tblWorkflowDocumentContent`.`document` = '". $this->_document->getID() ."' AND `tblWorkflowDocumentContent`.`version` = '". $this->_version ."')"; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; @@ -4848,8 +6406,8 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $db->rollbackTransaction(); return false; } - $queryStr = "INSERT INTO `tblWorkflowLog` (`document`, `version`, `workflow`, `transition`, `comment`, `date`, `userid`) ". - "VALUES ('".$this->_document->getID() ."', '".(int) $this->_version."', '".(int) $log['workflow']->getID()."', '".(int) $log['transition']->getID()."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".$log['user']->getID().")"; + $queryStr = "INSERT INTO `tblWorkflowLog` (`workflowdocumentcontent`, `transition`, `comment`, `date`, `userid`) ". + "VALUES ('".$recs[0]['id'] ."', '".(int) $log['transition']->getID()."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".$log['user']->getID().")"; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; @@ -4874,15 +6432,16 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if (!$this->_workflow) { return true; } + $workflow = $this->_workflow['workflow']; $db->startTransaction(); - $queryStr = "DELETE from `tblWorkflowLog` WHERE `document` = ". $this->_document->getID() ." AND `version` = ".$this->_version." AND `workflow` = ".$this->_workflow->getID(); + $queryStr = "DELETE from `tblWorkflowLog` WHERE `workflowdocumentcontent` = ".$this->_workflow['id']; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; } - $this->setWorkflowState($this->_workflow->getInitState()); + $this->setWorkflowState($workflow->getInitState()); $db->commitTransaction(); return true; @@ -4894,7 +6453,8 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ * Fully removing a workflow including entries in the workflow log is * only allowed if the workflow is still its initial state. * At a later point of time only unlinking the document from the - * workflow is allowed. It will keep any log entries. + * workflow is allowed. It will keep any log entries and set the state + * to NULL. * A workflow is unlinked from a document when enterNextState() * succeeds. * @@ -4916,31 +6476,39 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ return true; } + $workflow = $this->_workflow['workflow']; + /* A workflow should always be in a state, but in case it isn't, the * at least allow to remove the workflow. */ $currentstate = $this->getWorkflowState(); - if(!$currentstate || SeedDMS_Core_DMS::checkIfEqual($this->_workflow->getInitState(), $currentstate) || $unlink == true) { + if(!$currentstate || SeedDMS_Core_DMS::checkIfEqual($workflow->getInitState(), $currentstate) || $unlink == true) { $db->startTransaction(); - if(!$unlink) { + if($unlink) { $queryStr= - "DELETE FROM `tblWorkflowLog` WHERE " - ."`version`='".$this->_version."' " - ." AND `document` = '". $this->_document->getID() ."' " - ." AND `workflow` = '". $this->_workflow->getID() ."' "; + "UPDATE `tblWorkflowDocumentContent` SET `state` = NULL WHERE `id`=".$this->_workflow['id']; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; } - } - $queryStr= - "DELETE FROM `tblWorkflowDocumentContent` WHERE " - ."`version`='".$this->_version."' " - ." AND `document` = '". $this->_document->getID() ."' " - ." AND `workflow` = '". $this->_workflow->getID() ."' "; - if (!$db->getResult($queryStr)) { - $db->rollbackTransaction(); - return false; + } else { + $queryStr= + "DELETE FROM `tblWorkflowDocumentContent` WHERE `id`=".$this->_workflow['id']; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + /* will be deleted automatically when tblWorkflowDocumentContent is deleted + $queryStr= + "DELETE FROM `tblWorkflowLog` WHERE " + ."`version`='".$this->_version."' " + ." AND `document` = '". $this->_document->getID() ."' " + ." AND `workflow` = '". $workflow->getID() ."' "; + if (!$db->getResult($queryStr)) { + $db->rollbackTransaction(); + return false; + } + */ } $this->_workflow = null; $this->_workflowState = null; @@ -4964,19 +6532,19 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if(!$this->_workflow) return false; + if(!$this->_workflow['parent']) + return false; + $queryStr= - "SELECT * FROM `tblWorkflowDocumentContent` WHERE " - ."`version`='".$this->_version."' " - ." AND `document` = '". $this->_document->getID() ."' " - ." AND `workflow` = '". $this->_workflow->getID() ."' "; + "SELECT * FROM `tblWorkflowDocumentContent` WHERE `parent`=".$this->_workflow['parent']; $recs = $db->getResultArray($queryStr); if (is_bool($recs) && !$recs) return false; if(!$recs) return false; - if($recs[0]['parentworkflow']) - return $this->_document->getDMS()->getWorkflow($recs[0]['parentworkflow']); + if($recs[0]['workflow']) + return $this->_document->getDMS()->getWorkflow((int)$recs[0]['workflow']); return false; } /* }}} */ @@ -5000,11 +6568,11 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if($subworkflow) { $initstate = $subworkflow->getInitState(); - $queryStr = "INSERT INTO `tblWorkflowDocumentContent` (`parentworkflow`, `workflow`, `document`, `version`, `state`, `date`) VALUES (". $this->_workflow->getID(). ", ". $subworkflow->getID(). ", ". $this->_document->getID() .", ". $this->_version .", ".$initstate->getID().", ".$db->getCurrentDatetime().")"; + $queryStr = "INSERT INTO `tblWorkflowDocumentContent` (`parent`, `workflow`, `document`, `version`, `state`, `date`) VALUES (". $this->_workflow['id']. ", ". $subworkflow->getID(). ", ". $this->_document->getID() .", ". $this->_version .", ".$initstate->getID().", ".$db->getCurrentDatetime().")"; if (!$db->getResult($queryStr)) { return false; } - $this->_workflow = $subworkflow; + $this->_workflow = array('id'=>$db->getInsertID('tblWorkflowDocumentContent'), 'parent'=>$this->_workflow['id'], 'workflow'=>$subworkflow); return true; } return true; @@ -5031,28 +6599,20 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if ($this->_workflow) { $db->startTransaction(); - $queryStr= - "SELECT * FROM `tblWorkflowDocumentContent` WHERE `workflow`=". intval($this->_workflow->getID()) - . " AND `version`='".$this->_version - ."' AND `document` = '". $this->_document->getID() ."' "; - $recs = $db->getResultArray($queryStr); - if (is_bool($recs) && !$recs) { - $db->rollbackTransaction(); - return false; - } - if(!$recs) { - $db->rollbackTransaction(); - return false; - } - - $queryStr = "DELETE FROM `tblWorkflowDocumentContent` WHERE `workflow` =". intval($this->_workflow->getID())." AND `document` = '". $this->_document->getID() ."' AND `version` = '" . $this->_version."'"; + $queryStr = "UPDATE `tblWorkflowDocumentContent` SET `state` = NULL WHERE `id` = '" . $this->_workflow['id']."'"; if (!$db->getResult($queryStr)) { $db->rollbackTransaction(); return false; } - $this->_workflow = $this->_document->getDMS()->getWorkflow($recs[0]['parentworkflow']); - $this->_workflow->setDMS($this->_document->getDMS()); + /* Calling getWorkflow() should find the parent workflow, better check */ + $parent = $this->_workflow['parent']; + $this->_workflow = null; + $this->getWorkflow(); + if($this->_workflow['id'] != $parent) { + $db->rollbackTransaction(); + return false; + } if($transition) { if(false === $this->triggerWorkflowTransition($user, $transition, $comment)) { @@ -5063,7 +6623,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $db->commitTransaction(); } - return $this->_workflow; + return $this->_workflow['workflow']; } /* }}} */ /** @@ -5090,7 +6650,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ /* Check if the user has already triggered the transition */ $queryStr= - "SELECT * FROM `tblWorkflowLog` WHERE `version`='".$this->_version ."' AND `document` = '". $this->_document->getID() ."' AND `workflow` = ". $this->_workflow->getID(). " AND userid = ".$user->getID(); + "SELECT * FROM `tblWorkflowLog` WHERE `workflowdocumentcontent` = ".$this->_workflow['id']." AND userid = ".$user->getID(); $queryStr .= " AND `transition` = ".$transition->getID(); $resArr = $db->getResultArray($queryStr); if (is_bool($resArr) && !$resArr) @@ -5238,8 +6798,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ if(!$this->triggerWorkflowTransitionIsAllowed($user, $transition)) return false; - $state = $this->_workflowState; - $queryStr = "INSERT INTO `tblWorkflowLog` (`document`, `version`, `workflow`, `userid`, `transition`, `date`, `comment`) VALUES (".$this->_document->getID().", ".$this->_version.", " . (int) $this->_workflow->getID() . ", " .(int) $user->getID(). ", ".(int) $transition->getID().", ".$db->getCurrentDatetime().", ".$db->qstr($comment).")"; + $queryStr = "INSERT INTO `tblWorkflowLog` (`workflowdocumentcontent`, `userid`, `transition`, `date`, `comment`) VALUES (".$this->_workflow['id'].", ".(int) $user->getID(). ", ".(int) $transition->getID().", ".$db->getCurrentDatetime().", ".$db->qstr($comment).")"; if (!$db->getResult($queryStr)) return false; @@ -5291,7 +6850,8 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ * leading to the given state can be processed. */ if($nextstate->getPreCondFunc() == '') { - $transitions = $this->_workflow->getPreviousTransitions($nextstate); + $workflow = $this->_workflow['workflow']; + $transitions = $workflow->getPreviousTransitions($nextstate); foreach($transitions as $transition) { // echo "transition ".$transition->getID()." led to state ".$nextstate->getName()."
"; if($this->executeWorkflowTransitionIsAllowed($transition)) { @@ -5316,7 +6876,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ /* make sure the users and groups allowed to trigger the next * transitions are also allowed to read the document */ - $transitions = $this->_workflow->getNextTransitions($nextstate); + $transitions = $workflow->getNextTransitions($nextstate); foreach($transitions as $tran) { // echo "checking access for users/groups allowed to trigger transition ".$tran->getID()."
"; $transusers = $tran->getUsers(); @@ -5355,27 +6915,51 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ /** * Get the so far logged operations on the document content within the - * workflow. Even after finishing the workflow (when the document content - * does not have workflow set anymore) this function returns the list of all - * log entries. + * workflow. If the document content is currently in a workflow and + * a transition is passed, then the + * log entries will be restricted on the workflow and returned as a one + * dimensional list. Without a running workflow the log entries of + * all workflows in the past are returned grouped by workflow. + * This result is a two dimensional array. The keys of the first + * dimension are the ids used in table tblWorkflowDocumentContent. + * If only the logs of last workflow run are of interesst, then just + * take the last element of the returned array. + * + * Example: A workflow was started for a document content. + * This will add an entry in tblWorkflowDocumentContent whose state is set + * to the initial state of the workflow and a new autoinc id, e.g. with id 45 + * Once any step in the workflow was triggered, the table tblWorkflowLog will + * have an entry for workflowdocumentcontent=45. + * Retrieving the workflow log as long the document is still in the workflow + * will return the log entries for the current workflow. In this particular + * case it will be an array with one log entry. + * Once the workflow has ended this method will still return the log entries + * but in a 2-dimensional array with the first dimension set to 45. + * + * The same document version can be run through the same or a different + * workflow again which will lead to a new entry in + * tblWorkflowDocumentContent, e.g. with id 46. Getting the log entries + * while the content is still in the workflow will return only those entries + * for the current workflow. Once the workflow has ended, this methods + * returns a 2-dimensional array with two elements in the first dimension. + * One for key 45 and another one for key 46. * * @return array list of objects */ function getWorkflowLog($transition = null) { /* {{{ */ $db = $this->_document->getDMS()->getDB(); -/* if(!$this->_workflow) $this->getWorkflow(); - if(!$this->_workflow) - return false; -*/ $queryStr= - "SELECT * FROM `tblWorkflowLog` WHERE `version`='".$this->_version ."' AND `document` = '". $this->_document->getID() ."'"; // AND `workflow` = ". $this->_workflow->getID(); - if($transition) - $queryStr .= " AND `transition` = ".$transition->getID(); - $queryStr .= " ORDER BY `date`"; + "SELECT `a`.`id`, `a`.`userid`, `a`.`transition`, `a`.`date`, `a`.`comment`, `a`.`workflowdocumentcontent`, `b`.`version`, `b`.`document`, `b`.`workflow` FROM `tblWorkflowLog` `a` LEFT JOIN `tblWorkflowDocumentContent` `b` ON `a`.`workflowdocumentcontent` = `b`.`id` WHERE `b`.`version`='".$this->_version ."' AND `b`.`document` = '". $this->_document->getID() ."'"; // AND `workflow` = ". $this->_workflow->getID(); + if($transition) { + $queryStr .= " AND `a`.`transition` = ".$transition->getID(); + } + if($this->_workflow) + $queryStr .= " AND `a`.`workflowdocumentcontent` = ".$this->_workflow['id']; + $queryStr .= " ORDER BY `a`.`date`"; $resArr = $db->getResultArray($queryStr); if (is_bool($resArr) && !$resArr) return false; @@ -5385,7 +6969,10 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ $workflow = $this->_document->getDMS()->getWorkflow($resArr[$i]["workflow"]); $workflowlog = new SeedDMS_Core_Workflow_Log($resArr[$i]["id"], $this->_document->getDMS()->getDocument($resArr[$i]["document"]), $resArr[$i]["version"], $workflow, $this->_document->getDMS()->getUser($resArr[$i]["userid"]), $workflow->getTransition($resArr[$i]["transition"]), $resArr[$i]["date"], $resArr[$i]["comment"]); $workflowlog->setDMS($this); - $workflowlogs[$i] = $workflowlog; + if($this->_workflow) + $workflowlogs[] = $workflowlog; + else + $workflowlogs[$resArr[$i]["workflowdocumentcontent"]][] = $workflowlog; } return $workflowlogs; @@ -5394,7 +6981,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ /** * Get the latest workflow log entry for the document content within the * workflow. Even after finishing the workflow (when the document content - * does not have workflow set anymore) this function returns the last + * does not have a workflow set anymore) this function returns the last * log entry. * * @return object @@ -5410,7 +6997,7 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ return false; */ $queryStr= - "SELECT * FROM `tblWorkflowLog` WHERE `version`='".$this->_version ."' AND `document` = '". $this->_document->getID() ."'"; // AND `workflow` = ". $this->_workflow->getID(); + "SELECT `a`.*, `b`.`workflow`, `b`.`document`, `b`.`version` FROM `tblWorkflowLog` `a` LEFT JOIN `tblWorkflowDocumentContent` `b` ON `a`.`workflowdocumentcontent` = `b`.`id` WHERE `b`.`version`='".$this->_version ."' AND `b`.`document` = '". $this->_document->getID() ."'"; $queryStr .= " ORDER BY `id` DESC LIMIT 1"; $resArr = $db->getResultArray($queryStr); if (is_bool($resArr) && !$resArr) @@ -5436,10 +7023,11 @@ class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */ function needsWorkflowAction($user) { /* {{{ */ $needwkflaction = false; if($this->_workflow) { + $workflow = $this->_workflow['workflow']; if (!$this->_workflowState) $this->getWorkflowState(); $workflowstate = $this->_workflowState; - if($transitions = $this->_workflow->getNextTransitions($workflowstate)) { + if($transitions = $workflow->getNextTransitions($workflowstate)) { foreach($transitions as $transition) { if($this->triggerWorkflowTransitionIsAllowed($user, $transition)) { $needwkflaction = true; diff --git a/SeedDMS_Core/Core/inc.ClassFolder.php b/SeedDMS_Core/Core/inc.ClassFolder.php index 23324676f..af47030f3 100644 --- a/SeedDMS_Core/Core/inc.ClassFolder.php +++ b/SeedDMS_Core/Core/inc.ClassFolder.php @@ -778,21 +778,25 @@ class SeedDMS_Core_Folder extends SeedDMS_Core_Object { /** * Returns a file system path * - * This path contains spaces around the slashes for better readability. - * Run str_replace(' / ', '/', $path) on it to get a valid unix + * This path contains by default spaces around the slashes for better readability. + * Run str_replace(' / ', '/', $path) on it or pass '/' as $sep to get a valid unix * file system path. * + * @param bool $skiproot skip the name of the root folder and start with $sep + * @param string $sep separator between path elements * @return string path separated with ' / ' */ - function getFolderPathPlain() { /* {{{ */ - $path=""; + function getFolderPathPlain($skiproot = false, $sep = ' / ') { /* {{{ */ + $path="".$sep; $folderPath = $this->getPath(); - for ($i = 0; $i < count($folderPath); $i++) { - $path .= $folderPath[$i]->getName(); - if ($i +1 < count($folderPath)) - $path .= " / "; + for ($i = 0; $i < count($folderPath); $i++) { + if($i > 0 || !$skiproot) { + $path .= $folderPath[$i]->getName(); + if ($i+1 < count($folderPath)) + $path .= $sep; + } } - return $path; + return trim($path); } /* }}} */ /** @@ -1021,11 +1025,13 @@ class SeedDMS_Core_Folder extends SeedDMS_Core_Object { * @param array $version_attributes list of document version attributes. * The element key must be the id of the attribute definition. * @param SeedDMS_Core_Workflow $workflow + * @param integer $initstate initial document state (only S_RELEASED and + * S_DRAFT are allowed) * @return array|bool false in case of error, otherwise an array * containing two elements. The first one is the new document, the * second one is the result set returned when inserting the content. */ - function addDocument($name, $comment, $expires, $owner, $keywords, $categories, $tmpFile, $orgFileName, $fileType, $mimeType, $sequence, $reviewers=array(), $approvers=array(),$reqversion=0,$version_comment="", $attributes=array(), $version_attributes=array(), $workflow=null) { /* {{{ */ + function addDocument($name, $comment, $expires, $owner, $keywords, $categories, $tmpFile, $orgFileName, $fileType, $mimeType, $sequence, $reviewers=array(), $approvers=array(),$reqversion=0,$version_comment="", $attributes=array(), $version_attributes=array(), $workflow=null, $initstate=S_RELEASED) { /* {{{ */ $db = $this->_dms->getDB(); $expires = (!$expires) ? 0 : $expires; @@ -1051,7 +1057,8 @@ class SeedDMS_Core_Folder extends SeedDMS_Core_Object { $document = $this->_dms->getDocument($db->getInsertID('tblDocuments')); - $res = $document->addContent($version_comment, $owner, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers, $approvers, $reqversion, $version_attributes, $workflow); + $curuser = $this->_dms->getLoggedInUser(); + $res = $document->addContent($version_comment, $curuser ? $curuser : $owner, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers, $approvers, $reqversion, $version_attributes, $workflow, $initstate); if (is_bool($res) && !$res) { $db->rollbackTransaction(); @@ -1059,7 +1066,11 @@ class SeedDMS_Core_Folder extends SeedDMS_Core_Object { } if($categories) { - $document->setCategories($categories); + if(!$document->setCategories($categories)) { + $document->remove(); + $db->rollbackTransaction(); + return false; + } } if($attributes) { @@ -1308,8 +1319,12 @@ class SeedDMS_Core_Folder extends SeedDMS_Core_Object { * below the actual root folder. */ $res = $this->getParent(); - if ($res) - return $this->_parent->getAccessList($mode, $op); + if ($res) { + $pacl = $res->getAccessList($mode, $op); + return $pacl; + } + } else { + $pacl = array("groups" => array(), "users" => array()); } if (!isset($this->_accessList[$mode])) { @@ -1336,6 +1351,7 @@ class SeedDMS_Core_Folder extends SeedDMS_Core_Object { } return $this->_accessList[$mode]; + return SeedDMS_Core_DMS::mergeAccessLists($pacl, $this->_accessList[$mode]); } /* }}} */ /** diff --git a/SeedDMS_Core/Core/inc.ClassGroup.php b/SeedDMS_Core/Core/inc.ClassGroup.php index 66f5f8ee0..a5ac7a350 100644 --- a/SeedDMS_Core/Core/inc.ClassGroup.php +++ b/SeedDMS_Core/Core/inc.ClassGroup.php @@ -212,10 +212,12 @@ class SeedDMS_Core_Group { /* {{{ */ $this->_users = array(); + $classnamerole = $this->_dms->getClassname('role'); $classname = $this->_dms->getClassname('user'); foreach ($resArr as $row) { /** @var SeedDMS_Core_User $user */ - $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $row["role"], $row['hidden']); + $role = $classnamerole::getInstance($row['role'], $this->_dms); + $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $role, $row['hidden']); $user->setDMS($this->_dms); array_push($this->_users, $user); } @@ -238,10 +240,12 @@ class SeedDMS_Core_Group { /* {{{ */ $managers = array(); + $classnamerole = $this->_dms->getClassname('role'); $classname = $this->_dms->getClassname('user'); foreach ($resArr as $row) { /** @var SeedDMS_Core_User $user */ - $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $row["role"], $row['hidden']); + $role = $classnamerole::getInstance($row['role'], $this->_dms); + $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $role, $row['hidden']); $user->setDMS($this->_dms); array_push($managers, $user); } @@ -402,6 +406,28 @@ class SeedDMS_Core_Group { /* {{{ */ } } + $receiptStatus = $this->getReceiptStatus(); + foreach ($receiptStatus as $r) { + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $r["receiptID"] ."', '-2', 'Recipients group removed from process', ".$db->getCurrentDatetime().", '". $user->getID() ."')"; + $res=$db->getResult($queryStr); + if(!$res) { + $db->rollbackTransaction(); + return false; + } + } + + $revisionStatus = $this->getRevisionStatus(); + foreach ($revisionStatus as $r) { + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $r["revisionID"] ."', '-2', 'Revisors group removed from process', ".$db->getCurrentDatetime().", '". $user->getID() ."')"; + $res=$db->getResult($queryStr); + if(!$res) { + $db->rollbackTransaction(); + return false; + } + } + $db->commitTransaction(); return true; @@ -471,6 +497,79 @@ class SeedDMS_Core_Group { /* {{{ */ return $status; } /* }}} */ + function getReceiptStatus($documentID=null, $version=null) { /* {{{ */ + $db = $this->_dms->getDB(); + + $status = array(); + + // See if the group is assigned as a recipient. + $queryStr = "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`status`, ". + "`tblDocumentReceiptLog`.`comment`, `tblDocumentReceiptLog`.`date`, ". + "`tblDocumentReceiptLog`.`userID` ". + "FROM `tblDocumentRecipients` ". + "LEFT JOIN `tblDocumentReceiptLog` USING (`receiptID`) ". + "WHERE `tblDocumentRecipients`.`type`='1' ". + ($documentID==null ? "" : "AND `tblDocumentRecipients`.`documentID` = '". (int) $documentID ."' "). + ($version==null ? "" : "AND `tblDocumentRecipients`.`version` = '". (int) $version ."' "). + "AND `tblDocumentRecipients`.`required`='". $this->_id ."' "; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + if (count($resArr)>0) { + foreach ($resArr as $res) { + if(isset($status["status"][$res['documentID']])) { + if($status["status"][$res['documentID']]['date'] < $res['date']) { + $status["status"][$res['documentID']] = $res; + } + } else { + $status["status"][$res['documentID']] = $res; + } + } + } + return $status; + } /* }}} */ + + function getRevisionStatus($documentID=null, $version=null) { /* {{{ */ + $db = $this->_dms->getDB(); + + $status = array(); + + if (!$db->createTemporaryTable("ttcontentid")) { + return false; + } + // See if the group is assigned as a revisor. + $queryStr = "SELECT `tblDocumentRevisors`.*, `tblDocumentRevisionLog`.`status`, ". + "`tblDocumentRevisionLog`.`comment`, `tblDocumentRevisionLog`.`date`, ". + "`tblDocumentRevisionLog`.`userID` ". + "FROM `tblDocumentRevisors` ". + "LEFT JOIN `tblDocumentRevisionLog` USING (`revisionID`) ". + "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRevisors`.`version` AND `ttcontentid`.`document` = `tblDocumentRevisors`.`documentID` ". + "WHERE `tblDocumentRevisors`.`type`='1' ". + ($documentID==null ? "" : "AND `tblDocumentRevisors`.`documentID` = '". (int) $documentID ."' "). + ($version==null ? "" : "AND `tblDocumentRevisors`.`version` = '". (int) $version ."' "). + ($documentID==null && $version==null ? "AND `ttcontentid`.`maxVersion` = `tblDocumentRevisors`.`version` " : ""). + "AND `tblDocumentRevisors`.`required`='". $this->_id ."' ". + "ORDER BY `tblDocumentRevisionLog`.`revisionLogID` DESC"; + $resArr = $db->getResultArray($queryStr); + if ($resArr === false) + return false; + if (count($resArr)>0) { + $status['status'] = array(); + foreach ($resArr as $res) { + if($res['date']) { + if(isset($status["status"][$res['documentID']])) { + if($status["status"][$res['documentID']]['date'] < $res['date']) { + $status["status"][$res['documentID']] = $res; + } + } else { + $status["status"][$res['documentID']] = $res; + } + } + } + } + return $status; + } /* }}} */ + /** * Get a list of documents with a workflow * diff --git a/SeedDMS_Core/Core/inc.ClassKeywords.php b/SeedDMS_Core/Core/inc.ClassKeywords.php index aa11c9dc3..16afc994f 100644 --- a/SeedDMS_Core/Core/inc.ClassKeywords.php +++ b/SeedDMS_Core/Core/inc.ClassKeywords.php @@ -8,7 +8,7 @@ * @version @version@ * @author Uwe Steinmann * @copyright Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe, - * 2010 Uwe Steinmann + * 2010-2023 Uwe Steinmann * @version Release: @package_version@ */ @@ -19,7 +19,7 @@ * @package SeedDMS_Core * @author Markus Westphal, Malcolm Cowe, Uwe Steinmann * @copyright Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe, - * 2010 Uwe Steinmann + * 2010-2023 Uwe Steinmann * @version Release: @package_version@ */ class SeedDMS_Core_KeywordCategory { @@ -47,53 +47,53 @@ class SeedDMS_Core_KeywordCategory { */ protected $_dms; - /** - * SeedDMS_Core_KeywordCategory constructor. - * @param $id - * @param $ownerID - * @param $name - */ - function __construct($id, $ownerID, $name) { + /** + * SeedDMS_Core_KeywordCategory constructor. + * @param $id + * @param $ownerID + * @param $name + */ + function __construct($id, $ownerID, $name) { /* {{{ */ $this->_id = $id; $this->_name = $name; $this->_ownerID = $ownerID; $this->_dms = null; - } + } /* }}} */ - /** - * @param SeedDMS_Core_DMS $dms - */ - function setDMS($dms) { + /** + * @param SeedDMS_Core_DMS $dms + */ + function setDMS($dms) { /* {{{ */ $this->_dms = $dms; - } + } /* }}} */ - /** - * @return int - */ + /** + * @return int + */ function getID() { return $this->_id; } - /** - * @return string - */ + /** + * @return string + */ function getName() { return $this->_name; } - /** - * @return bool|SeedDMS_Core_User - */ - function getOwner() { + /** + * @return bool|SeedDMS_Core_User + */ + function getOwner() { /* {{{ */ if (!isset($this->_owner)) $this->_owner = $this->_dms->getUser($this->_ownerID); return $this->_owner; - } + } /* }}} */ - /** - * @param $newName - * @return bool - */ - function setName($newName) { - $newName = trim($newName); - if(!$newName) - return false; + /** + * @param $newName + * @return bool + */ + function setName($newName) { /* {{{ */ + $newName = trim($newName); + if(!$newName) + return false; $db = $this->_dms->getDB(); @@ -103,75 +103,89 @@ class SeedDMS_Core_KeywordCategory { $this->_name = $newName; return true; - } + } /* }}} */ - /** - * @param SeedDMS_Core_User $user - * @return bool - */ - function setOwner($user) { - if(!$user || !$user->isType('user')) - return false; + /** + * @param SeedDMS_Core_User $user + * @return bool + */ + function setOwner($user) { /* {{{ */ + if(!$user || !$user->isType('user')) + return false; $db = $this->_dms->getDB(); - $queryStr = "UPDATE `tblKeywordCategories` SET `owner` = " . $user->getID() . " WHERE `id` = " . $this->_id; + $queryStr = "UPDATE `tblKeywordCategories` SET `owner` = " . $user->getID() . " WHERE `id` = " . $this->_id; if (!$db->getResult($queryStr)) return false; $this->_ownerID = $user->getID(); $this->_owner = $user; return true; - } + } /* }}} */ - /** - * @return array - */ - function getKeywordLists() { + /** + * @return array keywords in this list + */ + function getKeywordLists() { /* {{{ */ $db = $this->_dms->getDB(); $queryStr = "SELECT * FROM `tblKeywords` WHERE `category` = " . $this->_id . " order by `keywords`"; return $db->getResultArray($queryStr); } - /** - * @param $listID - * @param $keywords - * @return bool - */ - function editKeywordList($listID, $keywords) { + /** + * @return integer number of keywords in this list + */ + function countKeywordLists() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "SELECT COUNT(*) as `c` FROM `tblKeywords` where `category`=".$this->_id; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && !$resArr) + return false; + + return $resArr[0]['c']; + } /* }}} */ + + /** + * @param $listID + * @param $keywords + * @return bool + */ + function editKeywordList($listID, $keywords) { /* {{{ */ $db = $this->_dms->getDB(); $queryStr = "UPDATE `tblKeywords` SET `keywords` = ".$db->qstr($keywords)." WHERE `id` = $listID"; return $db->getResult($queryStr); - } + } /* }}} */ - /** - * @param $keywords - * @return bool - */ - function addKeywordList($keywords) { + /** + * @param $keywords + * @return bool + */ + function addKeywordList($keywords) { /* {{{ */ $db = $this->_dms->getDB(); $queryStr = "INSERT INTO `tblKeywords` (`category`, `keywords`) VALUES (" . $this->_id . ", ".$db->qstr($keywords).")"; return $db->getResult($queryStr); - } + } /* }}} */ - /** - * @param $listID - * @return bool - */ - function removeKeywordList($listID) { + /** + * @param $listID + * @return bool + */ + function removeKeywordList($listID) { /* {{{ */ $db = $this->_dms->getDB(); $queryStr = "DELETE FROM `tblKeywords` WHERE `id` = $listID"; return $db->getResult($queryStr); - } + } /* }}} */ - /** - * @return bool - */ - function remove() { + /** + * @return bool + */ + function remove() { /* {{{ */ $db = $this->_dms->getDB(); $db->startTransaction(); @@ -189,5 +203,5 @@ class SeedDMS_Core_KeywordCategory { $db->commitTransaction(); return true; - } + } /* }}} */ } diff --git a/SeedDMS_Core/Core/inc.ClassTransmittal.php b/SeedDMS_Core/Core/inc.ClassTransmittal.php new file mode 100644 index 000000000..719f25131 --- /dev/null +++ b/SeedDMS_Core/Core/inc.ClassTransmittal.php @@ -0,0 +1,398 @@ + + * @copyright Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe, + * 2010 Uwe Steinmann + * @version Release: @package_version@ + */ + +/** + * Class to represent a transmittal in the document management system + * + * @category DMS + * @package SeedDMS_Core + * @author Markus Westphal, Malcolm Cowe, Uwe Steinmann + * @copyright Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe, + * 2010 Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_Core_Transmittal { + /** + * @var integer id of transmittal + * + * @access protected + */ + var $_id; + + /** + * @var string name of transmittal + * + * @access protected + */ + var $_name; + + /** + * @var string comment of transmittal + * + * @access protected + */ + var $_comment; + + /** + * @var boolean true if transmittal is public + * + * @access protected + */ + var $_isPublic; + + /** + * @var object user this transmittal belongs to + * + * @access protected + */ + var $_user; + + /** + * @var object date of creation + * + * @access protected + */ + var $_date; + + /** + * @var object items + * + * @access protected + */ + var $_items; + + /** + * @var object reference to the dms instance this user belongs to + * + * @access protected + */ + var $_dms; + + function __construct($id, $user, $name, $comment, $isPublic=0, $date='') { + $this->_id = $id; + $this->_name = $name; + $this->_comment = $comment; + $this->_user = $user; + $this->_isPublic = $isPublic; + $this->_date = $date; + $this->_items = array(); + $this->_dms = null; + } + + /** + * Get an instance of a transmittal object + * + * @param string|integer $id id or name of transmittal, depending + * on the 3rd parameter. + * @param object $dms instance of dms + * @param string $by search by [id|name]. If this + * parameter is left empty, the user will be search by its Id. + * @return object instance of class SeedDMS_Core_Transmittal + */ + public static function getInstance($id, $dms, $by='') { /* {{{ */ + if(!$dms || get_class($dms) != 'SeedDMS_Core_DMS') + return false; + + $db = $dms->getDB(); + + switch($by) { + case 'name': + $queryStr = "SELECT * FROM `tblTransmittals` WHERE `name` = ".$db->qstr($id); + break; + default: + $queryStr = "SELECT * FROM `tblTransmittals` WHERE id = " . (int) $id; + } + + $resArr = $db->getResultArray($queryStr); + + if (is_bool($resArr) && $resArr == false) return false; + if (count($resArr) != 1) return false; + + $resArr = $resArr[0]; + + $uclassname = $dms->getClassname('user'); + $user = $uclassname::getInstance($resArr['userID'], $dms); + $transmittal = new self($resArr["id"], $user, $resArr["name"], $resArr["comment"], $resArr["public"], $resArr["date"]); + $transmittal->setDMS($dms); + return $transmittal; + } /* }}} */ + + /** + * Get all instances of a transmittal object + * + * @param string|integer $id id or name of transmittal, depending + * on the 3rd parameter. + * @param object $dms instance of dms + * @param string $by search by [id|name]. If this + * parameter is left empty, the user will be search by its Id. + * @return object instance of class SeedDMS_Core_Transmittal + */ + public static function getAllInstances($user, $orderby, $dms) { /* {{{ */ + $db = $dms->getDB(); + + $queryStr = "SELECT * FROM `tblTransmittals`"; + if($user) + $queryStr .= " WHERE `userID` = " . $user->getID(); + + $resArr = $db->getResultArray($queryStr); + + if (is_bool($resArr) && $resArr == false) return false; + + $uclassname = $dms->getClassname('user'); + $transmittals = array(); + foreach ($resArr as $res) { + $user = $uclassname::getInstance($res['userID'], $dms); + $transmittal = new self($res["id"], $user, $res["name"], $res["comment"], $res["public"], $res["date"]); + $transmittal->setDMS($dms); + $transmittals[] = $transmittal; + } + return $transmittals; + } /* }}} */ + + function setDMS($dms) { + $this->_dms = $dms; + } + + function getID() { return $this->_id; } + + function getName() { return $this->_name; } + + function setName($newName) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblTransmittals` SET `name` =".$db->qstr($newName)." WHERE `id` = " . $this->_id; + $res = $db->getResult($queryStr); + if (!$res) + return false; + + $this->_name = $newName; + return true; + } /* }}} */ + + function getComment() { return $this->_comment; } + + function setComment($newComment) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblTransmittals` SET `comment` =".$db->qstr($newComment)." WHERE `id` = " . $this->_id; + $res = $db->getResult($queryStr); + if (!$res) + return false; + + $this->_comment = $newComment; + return true; + } /* }}} */ + + function getUser() { return $this->_user; } + + function getItems() { /* {{{ */ + $db = $this->_dms->getDB(); + + if (!$this->_items) { + $queryStr = "SELECT `tblTransmittalItems`.* FROM `tblTransmittalItems` ". + "LEFT JOIN `tblDocuments` ON `tblTransmittalItems`.`document`=`tblDocuments`.`id` ". + "WHERE `tblTransmittalItems`.`transmittal` = '". $this->_id ."'"; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + + $this->_users = array(); + + $classname = $this->_dms->getClassname('transmittalitem'); + foreach ($resArr as $row) { + $document = $this->_dms->getDocument($row['document']); + $content = $document->getContentByVersion($row['version']); + $item = new $classname($row["id"], $this, $content, $row["date"]); + array_push($this->_items, $item); + } + } + return $this->_items; + } /* }}} */ + + function getSize() { /* {{{ */ + $db = $this->_dms->getDB(); + + if (!$this->_items) { + self::getItems(); + } + + $size = 0; + foreach ($this->_items as $item) { + if($content = $item->getContent()) { + $size += $content->getFileSize(); + } + } + return $size; + } /* }}} */ + + /** + * Add an item to the transmittal + * + * @param object $item instance of SeedDMS_Core_DocumentContent + * @return boolean true if item could be added, otherwise false + */ + function addContent($item) { /* {{{ */ + $db = $this->_dms->getDB(); + + if(get_class($item) != $this->_dms->getClassname('documentcontent')) + return false; + + $document = $item->getDocument(); + $queryStr = "INSERT INTO `tblTransmittalItems` (`transmittal`, `document`, `version`, `date`) ". + "VALUES ('". $this->_id ."', ".$document->getID().", ".$item->getVersion().", ".$db->getCurrentDatetime().")"; + $res=$db->getResult($queryStr); + if(!$res) { + return false; + } + $itemID = $db->getInsertID('tblTransmittalItems'); + + return SeedDMS_Core_TransmittalItem::getInstance($itemID, $this->_dms); + } /* }}} */ + + function remove() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "DELETE FROM `tblTransmittals` WHERE `id` = " . $this->_id; + if (!$db->getResult($queryStr)) { + return false; + } + + return true; + } /* }}} */ + +} + +/** + * Class to represent a transmittal in the document management system + * + * @category DMS + * @package SeedDMS_Core + * @author Markus Westphal, Malcolm Cowe, Uwe Steinmann + * @copyright Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe, + * 2010 Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_Core_TransmittalItem { + /** + * @var integer id of transmittal item + * + * @access protected + */ + var $_id; + + /** + * @var object document content + * + * @access protected + */ + var $_content; + + /** + * @var object transmittal + * + * @access protected + */ + var $_transmittal; + + /** + * @var object date of creation + * + * @access protected + */ + var $_date; + + function __construct($id, $transmittal, $content, $date='') { + $this->_id = $id; + $this->_transmittal = $transmittal; + $this->_content = $content; + $this->_date = $date; + $this->_dms = null; + } + + public static function getInstance($id, $dms) { /* {{{ */ + if(!$dms || get_class($dms) != 'SeedDMS_Core_DMS') + return false; + + $db = $dms->getDB(); + + $queryStr = "SELECT * FROM `tblTransmittalItems` WHERE `id` = " . (int) $id; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + if (count($resArr) != 1) + return false; + $resArr = $resArr[0]; + + $transmittal = SeedDMS_Core_Transmittal::getInstance($resArr['transmittal'], $dms); + $dclassname = $dms->getClassname('document'); + $document = $dclassname::getInstance($resArr['document'], $dms); + $content = $document->getContentByVersion((int) $resArr['version']); + + $item = new self($resArr["id"], $transmittal, $content, $resArr["date"]); + $item->setDMS($dms); + return $item; + } /* }}} */ + + function setDMS($dms) { + $this->_dms = $dms; + } + + function getID() { return $this->_id; } + + function getTransmittal() { return $this->_transmittal; } + + function getContent() { return $this->_content; } + + function getDate() { return $this->_date; } + + function remove() { /* {{{ */ + $db = $this->_dms->getDB(); + $transmittal = $this->_transmittal; + + $queryStr = "DELETE FROM `tblTransmittalItems` WHERE `id` = " . $this->_id; + if (!$db->getResult($queryStr)) { + return false; + } + + return true; + } /* }}} */ + + /** + * Check if the content referenzed by the transmittal item is unequal + * to the latest content of the document. + * + * This function updateѕ always to the latest version of the document, + * even if the version in the item is higher. This can happen if a + * version has been removed. + * + * @return boolean/integer false in case of an error, otherwise the new + * version. + */ + function updateContent() { /* {{{ */ + $db = $this->_dms->getDB(); + $transmittal = $this->_transmittal; + + $document = $this->_content->getDocument(); + $latestcontent = $document->getLatestContent(); + if($latestcontent->getVersion() != $this->_content->getVersion()) { + $queryStr = "UPDATE `tblTransmittalItems` set `version` = ".$latestcontent->getVersion()." WHERE `id` = " . $this->_id; + if (!$db->getResult($queryStr)) { + return false; + } + } + + return $latestcontent->getVersion(); + } /* }}} */ +} +?> diff --git a/SeedDMS_Core/Core/inc.ClassUser.php b/SeedDMS_Core/Core/inc.ClassUser.php index bbc0aac87..bd482ece5 100644 --- a/SeedDMS_Core/Core/inc.ClassUser.php +++ b/SeedDMS_Core/Core/inc.ClassUser.php @@ -12,6 +12,223 @@ * @version Release: @package_version@ */ +/** + * Class to represent a role in the document management system + * + * @category DMS + * @package SeedDMS_Core + * @author Uwe Steinmann + * @copyright Copyright (C) 2016 Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_Core_Role { /* {{{ */ + /** + * @var integer id of role + * + * @access protected + */ + var $_id; + + /** + * @var string name of role + * + * @access protected + */ + var $_login; + + /** + * @var string role of user. Can be one of SeedDMS_Core_User::role_user, + * SeedDMS_Core_User::role_admin, SeedDMS_Core_User::role_guest + * + * @access protected + */ + var $_role; + + /** + * @var array list of status without access + * + * @access protected + */ + var $_noaccess; + + /** + * @var object reference to the dms instance this user belongs to + * + * @access protected + */ + var $_dms; + + const role_user = '0'; + const role_admin = '1'; + const role_guest = '2'; + + function __construct($id, $name, $role, $noaccess=array()) { /* {{{ */ + $this->_id = $id; + $this->_name = $name; + $this->_role = $role; + $this->_noaccess = $noaccess; + $this->_dms = null; + } /* }}} */ + + /** + * Create an instance of a role object + * + * @param string|integer $id Id, login name, or email of user, depending + * on the 3rd parameter. + * @param object $dms instance of dms + * @param string $by search by name. If 'name' is passed, the method + * will search by name instead of id. If this + * parameter is left empty, the role will be searched by its Id. + * + * @return object instance of class SeedDMS_Core_User + */ + public static function getInstance($id, $dms, $by='') { /* {{{ */ + $db = $dms->getDB(); + + switch($by) { + case 'name': + $queryStr = "SELECT * FROM `tblRoles` WHERE `name` = ".$db->qstr($id); + break; + default: + $queryStr = "SELECT * FROM `tblRoles` WHERE `id` = " . (int) $id; + } + + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) return false; + if (count($resArr) != 1) return false; + + $resArr = $resArr[0]; + + $role = new self($resArr["id"], $resArr["name"], $resArr["role"], $resArr['noaccess'] ? explode(',', $resArr['noaccess']) : array()); + $role->setDMS($dms); + return $role; + } /* }}} */ + + public static function getAllInstances($orderby, $dms) { /* {{{ */ + $db = $dms->getDB(); + + if($orderby == 'name') + $queryStr = "SELECT * FROM `tblRoles` ORDER BY `name`"; + else + $queryStr = "SELECT * FROM `tblRoles` ORDER BY `id`"; + $resArr = $db->getResultArray($queryStr); + + if (is_bool($resArr) && $resArr == false) + return false; + + $roles = array(); + + for ($i = 0; $i < count($resArr); $i++) { + $role = new self($resArr[$i]["id"], $resArr[$i]["name"], $resArr[$i]["role"], explode(',', $resArr[$i]['noaccess'])); + $role->setDMS($dms); + $roles[$i] = $role; + } + + return $roles; +} /* }}} */ + + function setDMS($dms) { + $this->_dms = $dms; + } + + function getID() { return $this->_id; } + + function getName() { return $this->_name; } + + function setName($newName) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblRoles` SET `name` =".$db->qstr($newName)." WHERE `id` = " . $this->_id; + $res = $db->getResult($queryStr); + if (!$res) + return false; + + $this->_name = $newName; + return true; + } /* }}} */ + + function isAdmin() { return ($this->_role == SeedDMS_Core_Role::role_admin); } + + function isGuest() { return ($this->_role == SeedDMS_Core_Role::role_guest); } + + function getRole() { return $this->_role; } + + function setRole($newrole) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblRoles` SET `role` = " . $newrole . " WHERE `id` = " . $this->_id; + if (!$db->getResult($queryStr)) + return false; + + $this->_role = $newrole; + return true; + } /* }}} */ + + function getNoAccess() { return $this->_noaccess; } + + function setNoAccess($noaccess) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblRoles` SET `noaccess` = " . $db->qstr($noaccess ? implode(',',$noaccess) : '') . " WHERE `id` = " . $this->_id; + if (!$db->getResult($queryStr)) + return false; + + $this->_noaccess = $noaccess; + return true; + } /* }}} */ + + /** + * Delete role + * + * @return boolean true on success or false in case of an error + */ + function remove() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "DELETE FROM `tblRoles` WHERE `id` = " . $this->_id; + if (!$db->getResult($queryStr)) { + return false; + } + + return true; + } /* }}} */ + + function isUsed() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "SELECT * FROM `tblUsers` WHERE `role`=".$this->_id; + $resArr = $db->getResultArray($queryStr); + if (is_array($resArr) && count($resArr) == 0) + return false; + return true; + } /* }}} */ + + function getUsers() { /* {{{ */ + $db = $this->_dms->getDB(); + + if (!isset($this->_users)) { + $queryStr = "SELECT * FROM `tblUsers` WHERE `role`=".$this->_id; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + + $this->_users = array(); + + $classnamerole = $this->_dms->getClassname('role'); + + $classname = $this->_dms->getClassname('user'); + foreach ($resArr as $row) { + $role = $classnamerole::getInstance($row['role'], $this->_dms); + $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $role, $row['hidden']); + $user->setDMS($this->_dms); + array_push($this->_users, $user); + } + } + return $this->_users; + } /* }}} */ + +} /* }}} */ + /** * Class to represent a user in the document management system * @@ -44,6 +261,13 @@ class SeedDMS_Core_User { /* {{{ */ */ protected $_pwd; + /** + * @var string secret of user for 2-factor authentication + * + * @access protected + */ + var $_secret; + /** * @var string date when password expires * @@ -123,6 +347,20 @@ class SeedDMS_Core_User { /* {{{ */ */ protected $_homeFolder; + /** + * @var array list of users this user can substitute + * + * @access protected + */ + var $_substitutes; + + /** + * @var array reverse list of users this user can substitute + * + * @access protected + */ + var $_rev_substitutes; + /** * @var SeedDMS_Core_DMS reference to the dms instance this user belongs to * @@ -165,8 +403,9 @@ class SeedDMS_Core_User { /* {{{ */ * @param int $loginFailures * @param int $quota * @param null $homeFolder + * @param string $secret */ - function __construct($id, $login, $pwd, $fullName, $email, $language, $theme, $comment, $role, $isHidden=0, $isDisabled=0, $pwdExpiration='', $loginFailures=0, $quota=0, $homeFolder=null) { + function __construct($id, $login, $pwd, $fullName, $email, $language, $theme, $comment, $role, $isHidden=0, $isDisabled=0, $pwdExpiration='', $loginFailures=0, $quota=0, $homeFolder=null, $secret='') { $this->_id = $id; $this->_login = $login; $this->_pwd = $pwd; @@ -182,6 +421,9 @@ class SeedDMS_Core_User { /* {{{ */ $this->_loginFailures = $loginFailures; $this->_quota = $quota; $this->_homeFolder = $homeFolder; + $this->_secret = $secret; + $this->_substitutes = null; + $this->_rev_substitutes = null; $this->_dms = null; } @@ -220,7 +462,10 @@ class SeedDMS_Core_User { /* {{{ */ $resArr = $resArr[0]; - $user = new self((int) $resArr["id"], $resArr["login"], $resArr["pwd"], $resArr["fullName"], $resArr["email"], $resArr["language"], $resArr["theme"], $resArr["comment"], $resArr["role"], $resArr["hidden"], $resArr["disabled"], $resArr["pwdExpiration"], $resArr["loginfailures"], $resArr["quota"], $resArr["homefolder"]); + $classname = $dms->getClassname('role'); + $role = $classname::getInstance($resArr['role'], $dms); + + $user = new self((int) $resArr["id"], $resArr["login"], $resArr["pwd"], $resArr["fullName"], $resArr["email"], $resArr["language"], $resArr["theme"], $resArr["comment"], $role, $resArr["hidden"], $resArr["disabled"], $resArr["pwdExpiration"], $resArr["loginfailures"], $resArr["quota"], $resArr["homefolder"], $resArr["secret"]); $user->setDMS($dms); return $user; } /* }}} */ @@ -244,9 +489,11 @@ class SeedDMS_Core_User { /* {{{ */ $users = array(); + $classname = $dms->getClassname('role'); for ($i = 0; $i < count($resArr); $i++) { /** @var SeedDMS_Core_User $user */ - $user = new self($resArr[$i]["id"], $resArr[$i]["login"], $resArr[$i]["pwd"], $resArr[$i]["fullName"], $resArr[$i]["email"], (isset($resArr[$i]["language"])?$resArr[$i]["language"]:NULL), (isset($resArr[$i]["theme"])?$resArr[$i]["theme"]:NULL), $resArr[$i]["comment"], $resArr[$i]["role"], $resArr[$i]["hidden"], $resArr[$i]["disabled"], $resArr[$i]["pwdExpiration"], $resArr[$i]["loginfailures"], $resArr[$i]["quota"], $resArr[$i]["homefolder"]); + $role = $classname::getInstance($resArr[$i]['role'], $dms); + $user = new self($resArr[$i]["id"], $resArr[$i]["login"], $resArr[$i]["pwd"], $resArr[$i]["fullName"], $resArr[$i]["email"], (isset($resArr[$i]["language"])?$resArr[$i]["language"]:NULL), (isset($resArr[$i]["theme"])?$resArr[$i]["theme"]:NULL), $resArr[$i]["comment"], $role, $resArr[$i]["hidden"], $resArr[$i]["disabled"], $resArr[$i]["pwdExpiration"], $resArr[$i]["loginfailures"], $resArr[$i]["quota"], $resArr[$i]["homefolder"]); $user->setDMS($dms); $users[$i] = $user; } @@ -349,6 +596,26 @@ class SeedDMS_Core_User { /* {{{ */ return true; } /* }}} */ + /** + * @return string + */ + function getSecret() { return $this->_secret; } + + /** + * @param string $newSecret + */ + function setSecret($newSecret) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblUsers` SET `secret` =".$db->qstr($newSecret)." WHERE `id` = " . $this->_id; + $res = $db->getResult($queryStr); + if (!$res) + return false; + + $this->_secret = $newSecret; + return true; + } /* }}} */ + /** * @return string */ @@ -364,11 +631,11 @@ class SeedDMS_Core_User { /* {{{ */ if(trim($newPwdExpiration) == '' || trim($newPwdExpiration) == 'never') { $newPwdExpiration = null; $queryStr = "UPDATE `tblUsers` SET `pwdExpiration` = NULL WHERE `id` = " . $this->_id; - } else { - if(trim($newPwdExpiration) == 'now') - $newPwdExpiration = date('Y-m-d H:i:s'); + } else { + if(trim($newPwdExpiration) == 'now') + $newPwdExpiration = date('Y-m-d H:i:s'); $queryStr = "UPDATE `tblUsers` SET `pwdExpiration` =".$db->qstr($newPwdExpiration)." WHERE `id` = " . $this->_id; - } + } $res = $db->getResult($queryStr); if (!$res) return false; @@ -472,10 +739,14 @@ class SeedDMS_Core_User { /* {{{ */ */ function setRole($newrole) { /* {{{ */ $db = $this->_dms->getDB(); - if(!in_array($newrole, array(SeedDMS_Core_User::role_admin, SeedDMS_Core_User::role_guest, SeedDMS_Core_User::role_user), true)) - return false; - $queryStr = "UPDATE `tblUsers` SET `role` = " . $newrole . " WHERE `id` = " . $this->_id; + if(!is_object($newrole) || (get_class($newrole) != $this->_dms->getClassname('role'))) + return false; + + if(is_object($newrole)) + $queryStr = "UPDATE `tblUsers` SET `role` = " . $newrole->getID() . " WHERE `id` = " . $this->_id; + else + $queryStr = "UPDATE `tblUsers` SET `role` = " . $newrole . " WHERE `id` = " . $this->_id; if (!$db->getResult($queryStr)) return false; @@ -486,12 +757,12 @@ class SeedDMS_Core_User { /* {{{ */ /** * @return bool */ - function isAdmin() { return ($this->_role == SeedDMS_Core_User::role_admin); } + function isAdmin() { return (is_object($this->_role) ? $this->_role->isAdmin() : $this->_role == SeedDMS_Core_User::role_admin); } /** - * @return bool + * Was never used and is now deprecated */ - function setAdmin() { /* {{{ */ + function _setAdmin($isAdmin) { /* {{{ */ $db = $this->_dms->getDB(); $queryStr = "UPDATE `tblUsers` SET `role` = " . SeedDMS_Core_User::role_admin . " WHERE `id` = " . $this->_id; @@ -505,12 +776,12 @@ class SeedDMS_Core_User { /* {{{ */ /** * @return bool */ - function isGuest() { return ($this->_role == SeedDMS_Core_User::role_guest); } + function isGuest() { return (is_object($this->_role) ? $this->_role->isGuest() : $this->_role == SeedDMS_Core_User::role_guest); } /** - * @return bool + * Was never used and is now deprecated */ - function setGuest() { /* {{{ */ + function _setGuest($isGuest) { /* {{{ */ $db = $this->_dms->getDB(); $queryStr = "UPDATE `tblUsers` SET `role` = " . SeedDMS_Core_User::role_guest . " WHERE `id` = " . $this->_id; @@ -660,13 +931,24 @@ class SeedDMS_Core_User { /* {{{ */ /** * Remove user from all processes * - * This method adds another log entry to the reviews and approvals - * which indicates the user has been deleted from the process. By default it will - * do so for each review/approval regardless of its current state unles + * This method adds another log entry to the reviews, approvals, receptions, revisions, + * which indicates that the user has been deleted from the process. By default it will + * do so for each review/approval/reception/revision regardless of its current state unless * the user has been removed already (status=-2). So even - * reviews/approvals already processed by the user will be added the log + * reviews/approvals/receptions/revisions already processed by the user will be added the log * entry. Only, if the last log entry was a removal already, it will not be * added a second time. + * This behaviour can be changed by passing a list of states in the optional + * argument $states. Only reviews, etc. in the given state will be affected. + * This allows to remove the user only if the review, etc. has not been done + * (state = 0). + * + * The last optional parameter $newuser is for replacing the user currently in + * charge by another user. If this parameter is passed, the old user will + * be deleted first (like described above) and afterwards the new user will + * be addded. The deletion of the old user and adding the new user will only + * happen if the new user has at least read access on the document. If not, + * the document will be skipped and remained unchanged. * * This removal from processes will also take place for older versions of a document. * @@ -773,6 +1055,84 @@ class SeedDMS_Core_User { /* {{{ */ } $db->commitTransaction(); + /* Get a list of all receptions, even those of older document versions */ + $receiptStatus = $this->getReceiptStatus(); + $db->startTransaction(); + foreach ($receiptStatus["indstatus"] as $ri) { + if(!($doc = $this->_dms->getDocument($ri['documentID']))) + continue; + if($docs) { + if(!in_array($doc->getID(), $docs)) + continue; + if(!$doc->isLatestContent($ri['version'])) + continue; + } + if($newuser && $doc->getAccessMode($newuser) < M_READ) + continue; + if($ri['status'] != -2 && (!isset($states['receipt']) || in_array($ri['status'], $states['receipt']))) { + $queryStr = "INSERT INTO `tblDocumentReceiptLog` (`receiptID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $ri["receiptID"] ."', '-2', '".(($newuser && $ri['status'] == 0) ? 'Recipient replaced by '.$newuser->getLogin() : 'Recipient removed from process')."', ".$db->getCurrentDatetime().", '". $user->getID() ."')"; + $res=$db->getResult($queryStr); + if(!$res) { + $db->rollbackTransaction(); + return false; + } + /* Only receptions not done already can be transferred to a new user */ + if($newuser && $ri['status'] == 0) { + if($doc = $this->_dms->getDocument($ri['documentID'])) { + if($version = $doc->getContentByVersion($ri['version'])) { + $ret = $version->addIndRecipient($newuser, $user); + /* returns -3 if the user is already a recipient */ + if($ret === false || ($ret < 0 && $ret != -3)) { + $db->rollbackTransaction(); + return false; + } + } + } + } + } + } + $db->commitTransaction(); + + /* Get a list of all revisions, even those of older document versions */ + $revisionStatus = $this->getRevisionStatus(); + $db->startTransaction(); + foreach ($revisionStatus["indstatus"] as $ri) { + if(!($doc = $this->_dms->getDocument($ri['documentID']))) + continue; + if($docs) { + if(!in_array($doc->getID(), $docs)) + continue; + if(!$doc->isLatestContent($ri['version'])) + continue; + } + if($newuser && $doc->getAccessMode($newuser) < M_READ) + continue; + if($ri['status'] != -2 && (!isset($states['revision']) || in_array($ri['status'], $states['revision']))) { + $queryStr = "INSERT INTO `tblDocumentRevisionLog` (`revisionID`, `status`, `comment`, `date`, `userID`) ". + "VALUES ('". $ri["revisionID"] ."', '-2', '".(($newuser && in_array($ri['status'], array(S_LOG_WAITING, S_LOG_SLEEPING))) ? 'Revisor replaced by '.$newuser->getLogin() : 'Revisor removed from process')."', ".$db->getCurrentDatetime().", '". $user->getID() ."')"; + $res=$db->getResult($queryStr); + if(!$res) { + $db->rollbackTransaction(); + return false; + } + /* Only revisions not already done or sleeping can be transferred to a new user */ + if($newuser && in_array($ri['status'], array(S_LOG_WAITING, S_LOG_SLEEPING))) { + if($doc = $this->_dms->getDocument($ri['documentID'])) { + if($version = $doc->getContentByVersion($ri['version'])) { + $ret = $version->addIndRevisor($newuser, $user); + /* returns -3 if the user is already a revisor */ + if($ret === false || ($ret < 0 && $ret != -3)) { + $db->rollbackTransaction(); + return false; + } + } + } + } + } + } + $db->commitTransaction(); + return true; } /* }}} */ @@ -1279,6 +1639,34 @@ class SeedDMS_Core_User { /* {{{ */ return $documents; } /* }}} */ + /** + * Returns all documents check out by a given user + * + * @param object $user + * @return array list of documents + */ + function getDocumentsCheckOut() { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "SELECT `tblDocuments`.* ". + "FROM `tblDocumentCheckOuts` LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `tblDocumentCheckOuts`.`document` ". + "WHERE `tblDocumentCheckOuts`.`userID` = '".$this->_id."' ". + "ORDER BY `id` ASC"; + + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && !$resArr) + return false; + + $documents = array(); + $classname = $this->_dms->getClassname('document'); + foreach ($resArr as $row) { + $document = new $classname($row["id"], $row["name"], $row["comment"], $row["date"], $row["expires"], $row["owner"], $row["folder"], $row["inheritAccess"], $row["defaultAccess"], $row["lockUser"], $row["keywords"], $row["sequence"]); + $document->setDMS($this->_dms); + $documents[] = $document; + } + return $documents; + } /* }}} */ + /** * Returns all document links of a given user * @return SeedDMS_Core_DocumentLink[]|bool list of document links @@ -1582,6 +1970,185 @@ class SeedDMS_Core_User { /* {{{ */ return $status; } /* }}} */ + /** + * Get a list of receipts + * This function returns a list of all receipts seperated by individual + * and group receipts. If the document id + * is passed, then only this document will be checked for receipts. The + * same is true for the version of a document which limits the list + * further. + * + * For a detaile description of the result array see + * {link SeedDMS_Core_User::getApprovalStatus} which does the same for + * approvals. + * + * @param int $documentID optional document id for which to retrieve the + * receipt + * @param int $version optional version of the document + * @return array list of all receipts + */ + function getReceiptStatus($documentID=null, $version=null) { /* {{{ */ + $db = $this->_dms->getDB(); + + $status = array("indstatus"=>array(), "grpstatus"=>array()); + + if (!$db->createTemporaryTable("ttcontentid")) { + return false; + } + // See if the user is assigned as an individual recipient. + // left join with ttcontentid to restrict result on latest version of document + // unless a document and version is given + $queryStr = "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`status`, ". + "`tblDocumentReceiptLog`.`comment`, `tblDocumentReceiptLog`.`date`, ". + "`tblDocumentReceiptLog`.`userID` ". + "FROM `tblDocumentRecipients` ". + "LEFT JOIN `tblDocumentReceiptLog` USING (`receiptID`) ". + "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` AND `ttcontentid`.`document` = `tblDocumentRecipients`.`documentID` ". + "WHERE `tblDocumentRecipients`.`type`='0' ". + ($documentID==null ? "" : "AND `tblDocumentRecipients`.`documentID` = '". (int) $documentID ."' "). + ($version==null ? "" : "AND `tblDocumentRecipients`.`version` = '". (int) $version ."' "). + ($documentID==null && $version==null ? "AND `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` " : ""). + "AND `tblDocumentRecipients`.`required`='". $this->_id ."' ". + "ORDER BY `tblDocumentReceiptLog`.`receiptLogID` DESC"; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr === false) + return false; + if (count($resArr)>0) { + foreach ($resArr as $res) { + if(isset($status["indstatus"][$res['documentID']])) { + if($status["indstatus"][$res['documentID']]['date'] < $res['date']) { + $status["indstatus"][$res['documentID']] = $res; + } + } else { + $status["indstatus"][$res['documentID']] = $res; + } + } + } + + // See if the user is the member of a group that has been assigned to + // receipt the document version. + $queryStr = "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`status`, ". + "`tblDocumentReceiptLog`.`comment`, `tblDocumentReceiptLog`.`date`, ". + "`tblDocumentReceiptLog`.`userID` ". + "FROM `tblDocumentRecipients` ". + "LEFT JOIN `tblDocumentReceiptLog` USING (`receiptID`) ". + "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` AND `ttcontentid`.`document` = `tblDocumentRecipients`.`documentID` ". + "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`groupID` = `tblDocumentRecipients`.`required` ". + "WHERE `tblDocumentRecipients`.`type`='1' ". + ($documentID==null ? "" : "AND `tblDocumentRecipients`.`documentID` = '". (int) $documentID ."' "). + ($version==null ? "" : "AND `tblDocumentRecipients`.`version` = '". (int) $version ."' "). + ($documentID==null && $version==null ? "AND `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` " : ""). + "AND `tblGroupMembers`.`userID`='". $this->_id ."' ". + "ORDER BY `tblDocumentReceiptLog`.`receiptLogID` DESC"; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr === false) + return false; + if (count($resArr)>0) { + foreach ($resArr as $res) { + if(isset($status["grpstatus"][$res['documentID']])) { + if($status["grpstatus"][$res['documentID']]['date'] < $res['date']) { + $status["grpstatus"][$res['documentID']] = $res; + } + } else { + $status["grpstatus"][$res['documentID']] = $res; + } + } + } + return $status; + } /* }}} */ + + /** + * Get a list of revisions + * This function returns a list of all revisions seperated by individual + * and group revisions. If the document id + * is passed, then only this document will be checked for revisions. The + * same is true for the version of a document which limits the list + * further. + * + * For a detaile description of the result array see + * {link SeedDMS_Core_User::getApprovalStatus} which does the same for + * approvals. + * + * @param int $documentID optional document id for which to retrieve the + * revisions + * @param int $version optional version of the document + * @return array list of all revisions. If the result array has no elements, + * then the user was not a revisor. If there elements for 'indstatus' or + * 'grpstatus' then the revision hasn't been started. + */ + function getRevisionStatus($documentID=null, $version=null) { /* {{{ */ + $db = $this->_dms->getDB(); + + $status = array("indstatus"=>array(), "grpstatus"=>array()); + + if (!$db->createTemporaryTable("ttcontentid")) { + return false; + } + // See if the user is assigned as an individual revisor. + // left join with ttcontentid to restrict result on latest version of document + // unless a document and version is given + $queryStr = "SELECT `tblDocumentRevisors`.*, `tblDocumentRevisionLog`.`status`, ". + "`tblDocumentRevisionLog`.`comment`, `tblDocumentRevisionLog`.`date`, ". + "`tblDocumentRevisionLog`.`userID` ". + "FROM `tblDocumentRevisors` ". + "LEFT JOIN `tblDocumentRevisionLog` USING (`revisionID`) ". + "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRevisors`.`version` AND `ttcontentid`.`document` = `tblDocumentRevisors`.`documentID` ". + "WHERE `tblDocumentRevisors`.`type`='0' ". + ($documentID==null ? "" : "AND `tblDocumentRevisors`.`documentID` = '". (int) $documentID ."' "). + ($version==null ? "" : "AND `tblDocumentRevisors`.`version` = '". (int) $version ."' "). + ($documentID==null && $version==null ? "AND `ttcontentid`.`maxVersion` = `tblDocumentRevisors`.`version` " : ""). + "AND `tblDocumentRevisors`.`required`='". $this->_id ."' ". + "ORDER BY `tblDocumentRevisionLog`.`revisionLogID` DESC"; + $resArr = $db->getResultArray($queryStr); + if ($resArr === false) + return false; + if (count($resArr)>0) { + $status['indstatus'] = array(); + foreach ($resArr as $res) { + if($res['date']) { + if(isset($status["indstatus"][$res['documentID']])) { + if($status["indstatus"][$res['documentID']]['date'] < $res['date']) { + $status["indstatus"][$res['documentID']] = $res; + } + } else { + $status["indstatus"][$res['documentID']] = $res; + } + } + } + } + + // See if the user is the member of a group that has been assigned to + // revision the document version. + $queryStr = "SELECT `tblDocumentRevisors`.*, `tblDocumentRevisionLog`.`status`, ". + "`tblDocumentRevisionLog`.`comment`, `tblDocumentRevisionLog`.`date`, ". + "`tblDocumentRevisionLog`.`userID` ". + "FROM `tblDocumentRevisors` ". + "LEFT JOIN `tblDocumentRevisionLog` USING (`revisionID`) ". + "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRevisors`.`version` AND `ttcontentid`.`document` = `tblDocumentRevisors`.`documentID` ". + "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`groupID` = `tblDocumentRevisors`.`required` ". + "WHERE `tblDocumentRevisors`.`type`='1' ". + ($documentID==null ? "" : "AND `tblDocumentRevisors`.`documentID` = '". (int) $documentID ."' "). + ($version==null ? "" : "AND `tblDocumentRevisors`.`version` = '". (int) $version ."' "). + ($documentID==null && $version==null ? "AND `ttcontentid`.`maxVersion` = `tblDocumentRevisors`.`version` " : ""). + "AND `tblGroupMembers`.`userID`='". $this->_id ."' ". + "ORDER BY `tblDocumentRevisionLog`.`revisionLogID` DESC"; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr === false) + return false; + if (count($resArr)>0) { + foreach ($resArr as $res) { + if(isset($status["grpstatus"][$res['documentID']])) { + if($status["grpstatus"][$res['documentID']]['date'] < $res['date']) { + $status["grpstatus"][$res['documentID']] = $res; + } + } else { + $status["grpstatus"][$res['documentID']] = $res; + } + } + } + return $status; + } /* }}} */ + /** * Get a list of documents with a workflow * @@ -1939,6 +2506,165 @@ class SeedDMS_Core_User { /* {{{ */ return true; } /* }}} */ + /** + * Get all substitutes of the user + * + * These users are substitutes of the current user + * + * @return array list of users + */ + function getSubstitutes() { /* {{{ */ + $db = $this->_dms->getDB(); + + if (!isset($this->_substitutes)) { + $queryStr = "SELECT `tblUsers`.* FROM `tblUserSubstitutes` ". + "LEFT JOIN `tblUsers` ON `tblUserSubstitutes`.`substitute` = `tblUsers`.`id` ". + "WHERE `tblUserSubstitutes`.`user`='". $this->_id ."'"; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + + $this->_substitutes = array(); + $classname = $this->_dms->getClassname('user'); + foreach ($resArr as $row) { + $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $row["role"], $row["hidden"], $row["disabled"], $row["pwdExpiration"], $row["loginfailures"], $row["quota"], $row["homefolder"]); + $user->setDMS($this->_dms); + array_push($this->_substitutes, $user); + } + } + return $this->_substitutes; + } /* }}} */ + + /** + * Get all users this user is a substitute of + * + * @return array list of users + */ + function getReverseSubstitutes() { /* {{{ */ + $db = $this->_dms->getDB(); + + if (!isset($this->_rev_substitutes)) { + $queryStr = "SELECT `tblUsers`.* FROM `tblUserSubstitutes` ". + "LEFT JOIN `tblUsers` ON `tblUserSubstitutes`.`user` = `tblUsers`.`id` ". + "LEFT JOIN `tblRoles` ON `tblRoles`.`id`=`tblUsers`.`role` ". + "WHERE `tblUserSubstitutes`.`substitute`='". $this->_id ."'"; + /* None admins can only be substitutes for regular users, otherwise + * regular users can become admin + */ + if(!$this->isAdmin()) + $queryStr .= " AND `tblRoles`.`role` = ".SeedDMS_Core_User::role_user; + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) + return false; + + $this->_rev_substitutes = array(); + $classnamerole = $this->_dms->getClassname('role'); + $classname = $this->_dms->getClassname('user'); + foreach ($resArr as $row) { + $role = $classnamerole::getInstance($row['role'], $this->_dms); + $user = new $classname($row["id"], $row["login"], $row["pwd"], $row["fullName"], $row["email"], $row["language"], $row["theme"], $row["comment"], $role, $row["hidden"], $row["disabled"], $row["pwdExpiration"], $row["loginfailures"], $row["quota"], $row["homefolder"]); + $user->setDMS($this->_dms); + array_push($this->_rev_substitutes, $user); + } + } + return $this->_rev_substitutes; + } /* }}} */ + + /** + * Add a substitute to the user + * + * @return boolean true if successful otherwise false + */ + function addSubstitute($substitute) { /* {{{ */ + $db = $this->_dms->getDB(); + + if(get_class($substitute) != $this->_dms->getClassname('user')) + return false; + + $queryStr = "SELECT * FROM `tblUserSubstitutes` WHERE `user`=" . $this->_id . " AND `substitute`=".$substitute->getID(); + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) return false; + if (count($resArr) == 1) return true; + + $queryStr = "INSERT INTO `tblUserSubstitutes` (`user`, `substitute`) VALUES (" . $this->_id . ", ".$substitute->getID().")"; + if (!$db->getResult($queryStr)) + return false; + + $this->_substitutes = null; + return true; + } /* }}} */ + + /** + * Remove a substitute from the user + * + * @return boolean true if successful otherwise false + */ + function removeSubstitute($substitute) { /* {{{ */ + $db = $this->_dms->getDB(); + + if(get_class($substitute) != $this->_dms->getClassname('user')) + return false; + + $queryStr = "SELECT * FROM `tblUserSubstitutes` WHERE `user`=" . $this->_id . " AND `substitute`=".$substitute->getID(); + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) return false; + if (count($resArr) == 0) return true; + + $queryStr = "DELETE FROM `tblUserSubstitutes` WHERE `user`=" . $this->_id . " AND `substitute`=".$substitute->getID(); + if (!$db->getResult($queryStr)) + return false; + + $this->_substitutes = null; + return true; + } /* }}} */ + + /** + * Check if user is a substitute of the current user + * + * @return boolean true if successful otherwise false + */ + function isSubstitute($substitute) { /* {{{ */ + $db = $this->_dms->getDB(); + + if(get_class($substitute) != $this->_dms->getClassname('user')) + return false; + + $queryStr = "SELECT * FROM `tblUserSubstitutes` WHERE `user`=" . $this->_id . " AND `substitute`=".$substitute->getID(); + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) return false; + if (count($resArr) == 1) return true; + + return false; + } /* }}} */ + + /** + * Check if user may switch to the given user + * + * Switching to the given user is only allowed if the given user + * is a substitute for the current user. + * + * @return boolean true if successful otherwise false + */ + function maySwitchToUser($touser) { /* {{{ */ + $db = $this->_dms->getDB(); + + if(get_class($touser) != $this->_dms->getClassname('user')) + return false; + + /* switching to an admin account is always forbitten, unless the + * current user is admin itself + */ + if(!$this->isAdmin() && $touser->isAdmin()) + return false; + + $queryStr = "SELECT * FROM `tblUserSubstitutes` WHERE `substitute`=" . $this->_id . " AND `user`=".$touser->getID(); + $resArr = $db->getResultArray($queryStr); + if (is_bool($resArr) && $resArr == false) return false; + if (count($resArr) == 1) return true; + + return false; + } /* }}} */ + /** * Get all notifications of user * diff --git a/SeedDMS_Core/Core/inc.ClassWorkflow.php b/SeedDMS_Core/Core/inc.ClassWorkflow.php index ea104dee0..f1fc70a2b 100644 --- a/SeedDMS_Core/Core/inc.ClassWorkflow.php +++ b/SeedDMS_Core/Core/inc.ClassWorkflow.php @@ -43,7 +43,14 @@ class SeedDMS_Core_Workflow { /* {{{ */ var $_initstate; /** - * @var SeedDMS_Core_Workflow_Transition[] name of the workflow state + * @var data for rendering graph + * + * @access protected + */ + var $_layoutdata; + + /** + * @var SeedDMS_Core_Workflow_Transition[] list of transitions * * @access protected */ @@ -61,11 +68,13 @@ class SeedDMS_Core_Workflow { /* {{{ */ * @param int $id * @param string $name * @param SeedDMS_Core_Workflow_State $initstate + * @param string $layoutdata */ - function __construct($id, $name, $initstate) { /* {{{ */ + function __construct($id, $name, $initstate, $layoutdata) { /* {{{ */ $this->_id = $id; $this->_name = $name; $this->_initstate = $initstate; + $this->_layoutdata = $layoutdata; $this->_transitions = null; $this->_dms = null; } /* }}} */ @@ -124,6 +133,26 @@ class SeedDMS_Core_Workflow { /* {{{ */ return true; } /* }}} */ + /** + * @return string + */ + function getLayoutData() { return $this->_layoutdata; } + + /** + * @param string $layoutdata + */ + function setLayoutData($newdata) { /* {{{ */ + $db = $this->_dms->getDB(); + + $queryStr = "UPDATE `tblWorkflows` SET `layoutdata` = ".$db->qstr($newdata)." WHERE `id` = " . $this->_id; + $res = $db->getResult($queryStr); + if (!$res) + return false; + + $this->_layoutdata = $newdata; + return true; + } /* }}} */ + /** * @return SeedDMS_Core_Workflow_Transition[]|bool */ diff --git a/SeedDMS_Core/Core/inc.DBAccessPDO.php b/SeedDMS_Core/Core/inc.DBAccessPDO.php index 1c3a48a9f..105c859fc 100644 --- a/SeedDMS_Core/Core/inc.DBAccessPDO.php +++ b/SeedDMS_Core/Core/inc.DBAccessPDO.php @@ -88,6 +88,16 @@ class SeedDMS_Core_DatabaseAccess { */ private $_ttcontentid; + /** + * @var boolean set to true if temp. table for doc reception has been created + */ + private $_ttreceiptid; + + /** + * @var boolean set to true if temp. table for doc revision has been created + */ + private $_ttrevisionid; + /** * @var boolean set to true if in a database transaction */ @@ -232,6 +242,8 @@ class SeedDMS_Core_DatabaseAccess { $this->_ttapproveid = false; $this->_ttstatid = false; $this->_ttcontentid = false; + $this->_ttreceiptid = false; + $this->_ttrevisionid = false; $this->_useviews = false; // turn off views, because they are much slower then temp. tables $this->_debug = false; } /* }}} */ @@ -301,6 +313,7 @@ class SeedDMS_Core_DatabaseAccess { switch($this->_driver) { case 'mysql': $this->_conn->exec('SET NAMES utf8'); +// $this->_conn->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE); /* Turn this on if you want strict checking of default values, etc. */ /* $this->_conn->exec("SET SESSION sql_mode = 'STRICT_TRANS_TABLES'"); */ /* The following is the default on Ubuntu 16.04 */ @@ -315,7 +328,7 @@ class SeedDMS_Core_DatabaseAccess { } if($this->_useviews) { $tmp = $this->ViewList(); - foreach(array('ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid') as $viewname) { + foreach(array('ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid', 'ttreceiptid', 'ttrevisionid') as $viewname) { if(in_array($viewname, $tmp)) { $this->{"_".$viewname} = true; } @@ -447,9 +460,15 @@ class SeedDMS_Core_DatabaseAccess { $this->_conn->beginTransaction(); } $this->_intransaction++; + if($this->_logfp) { + fwrite($this->_logfp, microtime()." START ".$htis->_intransaction."\n"); + } } /* }}} */ function rollbackTransaction() { /* {{{ */ + if($this->_logfp) { + fwrite($this->_logfp, microtime()." ROLLBACK ".$htis->_intransaction."\n"); + } if($this->_intransaction == 1) { $this->_conn->rollBack(); } @@ -457,6 +476,9 @@ class SeedDMS_Core_DatabaseAccess { } /* }}} */ function commitTransaction() { /* {{{ */ + if($this->_logfp) { + fwrite($this->_logfp, microtime()." COMMIT ".$htis->_intransaction."\n"); + } if($this->_intransaction == 1) { $this->_conn->commit(); } @@ -658,6 +680,88 @@ class SeedDMS_Core_DatabaseAccess { } return $this->_ttcontentid; } + elseif (!strcasecmp($tableName, "ttreceiptid")) { + switch($this->_driver) { + case 'sqlite': + $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttreceiptid` AS ". + "SELECT `tblDocumentReceiptLog`.`receiptID` AS `receiptID`, ". + "MAX(`tblDocumentReceiptLog`.`receiptLogID`) AS `maxLogID` ". + "FROM `tblDocumentReceiptLog` ". + "GROUP BY `tblDocumentReceiptLog`.`receiptID` "; +// "ORDER BY `maxLogID`"; + break; + case 'pgsql': + $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttreceiptid` (`receiptID` INTEGER, `maxLogID` INTEGER, PRIMARY KEY (`receiptID`));". + "INSERT INTO `ttreceiptid` SELECT `tblDocumentReceiptLog`.`receiptID`, ". + "MAX(`tblDocumentReceiptLog`.`receiptLogID`) AS `maxLogID` ". + "FROM `tblDocumentReceiptLog` ". + "GROUP BY `tblDocumentReceiptLog`.`receiptID` "; +// "ORDER BY `maxLogID`"; + break; + default: + $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttreceiptid` (PRIMARY KEY (`receiptID`), INDEX (`maxLogID`)) ". + "SELECT `tblDocumentReceiptLog`.`receiptID`, ". + "MAX(`tblDocumentReceiptLog`.`receiptLogID`) AS `maxLogID` ". + "FROM `tblDocumentReceiptLog` ". + "GROUP BY `tblDocumentReceiptLog`.`receiptID` "; +// "ORDER BY `maxLogID`"; + } + if (!$this->_ttreceiptid) { + if (!$this->getResult($queryStr)) + return false; + $this->_ttreceiptid=true; + } + else { + if (is_bool($override) && $override) { + if (!$this->getResult("DELETE FROM `ttreceiptid`")) + return false; + if (!$this->getResult($queryStr)) + return false; + } + } + return $this->_ttreceiptid; + } + elseif (!strcasecmp($tableName, "ttrevisionid")) { + switch($this->_driver) { + case 'sqlite': + $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttrevisionid` AS ". + "SELECT `tblDocumentRevisionLog`.`revisionID` AS `revisionID`, ". + "MAX(`tblDocumentRevisionLog`.`revisionLogID`) AS `maxLogID` ". + "FROM `tblDocumentRevisionLog` ". + "GROUP BY `tblDocumentRevisionLog`.`revisionID` "; +// "ORDER BY `maxLogID`"; + break; + case 'pgsql': + $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttrevisionid` (`revisionID` INTEGER, `maxLogID` INTEGER, PRIMARY KEY (`revisionID`));". + "INSERT INTO `ttrevisionid` SELECT `tblDocumentRevisionLog`.`revisionID`, ". + "MAX(`tblDocumentRevisionLog`.`revisionLogID`) AS `maxLogID` ". + "FROM `tblDocumentRevisionLog` ". + "GROUP BY `tblDocumentRevisionLog`.`revisionID` "; +// "ORDER BY `maxLogID`"; + break; + default: + $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttrevisionid` (PRIMARY KEY (`revisionID`), INDEX (`maxLogID`)) ". + "SELECT `tblDocumentRevisionLog`.`revisionID`, ". + "MAX(`tblDocumentRevisionLog`.`revisionLogID`) AS `maxLogID` ". + "FROM `tblDocumentRevisionLog` ". + "GROUP BY `tblDocumentRevisionLog`.`revisionID` "; +// "ORDER BY `maxLogID`"; + } + if (!$this->_ttrevisionid) { + if (!$this->getResult($queryStr)) + return false; + $this->_ttrevisionid=true; + } + else { + if (is_bool($override) && $override) { + if (!$this->getResult("DELETE FROM `ttrevisionid`")) + return false; + if (!$this->getResult($queryStr)) + return false; + } + } + return $this->_ttrevisionid; + } return false; } /* }}} */ @@ -857,6 +961,82 @@ class SeedDMS_Core_DatabaseAccess { } return $this->_ttcontentid; } + elseif (!strcasecmp($tableName, "ttreceiptid")) { + switch($this->_driver) { + case 'sqlite': + $queryStr = "CREATE VIEW `ttreceiptid` AS ". + "SELECT `tblDocumentReceiptLog`.`receiptID` AS `receiptID`, ". + "MAX(`tblDocumentReceiptLog`.`receiptLogID`) AS `maxLogID` ". + "FROM `tblDocumentReceiptLog` ". + "GROUP BY `tblDocumentReceiptLog`.`receiptID` "; + break; + case 'pgsql': + $queryStr = "CREATE VIEW `ttreceiptid` AS ". + "SELECT `tblDocumentReceiptLog`.`receiptID` AS `receiptID`, ". + "MAX(`tblDocumentReceiptLog`.`receiptLogID`) AS `maxLogID` ". + "FROM `tblDocumentReceiptLog` ". + "GROUP BY `tblDocumentReceiptLog`.`receiptID` "; + break; + default: + $queryStr = "CREATE".($override ? " OR REPLACE" : "")." VIEW `ttreceiptid` AS ". + "SELECT `tblDocumentReceiptLog`.`receiptID`, ". + "MAX(`tblDocumentReceiptLog`.`receiptLogID`) AS `maxLogID` ". + "FROM `tblDocumentReceiptLog` ". + "GROUP BY `tblDocumentReceiptLog`.`receiptID` "; + } + if (!$this->_ttreceiptid) { + if (!$this->getResult($queryStr)) + return false; + $this->_ttreceiptid=true; + } + else { + if (is_bool($override) && $override) { + if (!$this->getResult("DROP VIEW `ttreceiptid`")) + return false; + if (!$this->getResult($queryStr)) + return false; + } + } + return $this->_ttreceiptid; + } + elseif (!strcasecmp($tableName, "ttrevisionid")) { + switch($this->_driver) { + case 'sqlite': + $queryStr = "CREATE VIEW `ttrevisionid` AS ". + "SELECT `tblDocumentRevisionLog`.`revisionID` AS `revisionID`, ". + "MAX(`tblDocumentRevisionLog`.`revisionLogID`) AS `maxLogID` ". + "FROM `tblDocumentRevisionLog` ". + "GROUP BY `tblDocumentRevisionLog`.`revisionID` "; + break; + case 'pgsql': + $queryStr = "CREATE VIEW `ttrevisionid` AS ". + "SELECT `tblDocumentRevisionLog`.`revisionID` AS `revisionID`, ". + "MAX(`tblDocumentRevisionLog`.`revisionLogID`) AS `maxLogID` ". + "FROM `tblDocumentRevisionLog` ". + "GROUP BY `tblDocumentRevisionLog`.`revisionID` "; + break; + default: + $queryStr = "CREATE".($override ? " OR REPLACE" : "")." VIEW `ttrevisionid` AS ". + "SELECT `tblDocumentRevisionLog`.`revisionID`, ". + "MAX(`tblDocumentRevisionLog`.`revisionLogID`) AS `maxLogID` ". + "FROM `tblDocumentRevisionLog` ". + "GROUP BY `tblDocumentRevisionLog`.`revisionID` "; + } + if (!$this->_ttrevisionid) { + if (!$this->getResult($queryStr)) + return false; + $this->_ttrevisionid=true; + } + else { + if (is_bool($override) && $override) { + if (!$this->getResult("DROP VIEW `ttrevisionid`")) + return false; + if (!$this->getResult($queryStr)) + return false; + } + } + return $this->_ttrevisionid; + } return false; } /* }}} */ @@ -925,16 +1105,25 @@ class SeedDMS_Core_DatabaseAccess { * * @return string sql code */ - function getCurrentDatetime() { /* {{{ */ + function getCurrentDatetime($dayoffset=0) { /* {{{ */ switch($this->_driver) { case 'mysql': - return "CURRENT_TIMESTAMP"; + if($dayoffset) + return "DATE_ADD(CURRENT_TIMESTAMP, INTERVAL ".$dayoffset." DAY)"; + else + return "CURRENT_TIMESTAMP"; break; case 'sqlite': - return "datetime('now', 'localtime')"; + if($dayoffset) + return "datetime('now', '".$dayoffset." days', 'localtime')"; + else + return "datetime('now', 'localtime')"; break; case 'pgsql': - return "now()"; + if($dayoffset) + return "now() + interval '".$dayoffset." day'"; + else + return "now()"; break; } return ''; diff --git a/SeedDMS_Core/bootstrap-5.php b/SeedDMS_Core/bootstrap-5.php new file mode 100644 index 000000000..5d0d0e4a5 --- /dev/null +++ b/SeedDMS_Core/bootstrap-5.php @@ -0,0 +1,3 @@ +uwe@steinmann.cx yes - 2022-11-07 + 2022-12-10 - 5.1.28 - 5.1.28 + 6.0.22 + 6.0.22 stable @@ -24,15 +24,7 @@ GPL License -- fix SeedDMS_Core_User::getDocumentContents() -- fix SeedDMS_Core_File::fileExtension() -- SeedDMS_Core_DMS::createPasswordRequest() creates a cryptographically secure hash -- fix sql error when deleting a folder attribute -- add SeedDMS_Core_Attribute::getParsedValue() and use it in SeedDMS_Core_Object::getAttributeValue() -- add SeedDMS_Core_DMS::getDuplicateSequenceNo() and SeedDMS_Core_Folder::reorderDocuments() -- add SeedDMS_Core_File::mimetype(), fix SeedDMS_Core_File::moveDir() -- all file operations use methods of SeedDMS_Core_File -- change namespace of iterators from SeedDMS to SeedDMS\Core +- all changes from 5.1.29 merged @@ -82,6 +74,9 @@ + + + @@ -120,6 +115,7 @@ + 2010-04-27 3.0.0 3.0.0 @@ -128,13 +124,14 @@ stable stable - 2010-04-27 GPL License Initial release + 2011-07-23 + 3.2.0 3.2.0 @@ -143,8 +140,6 @@ Initial release stable stable - 2011-07-23 - GPL License New release @@ -704,21 +699,21 @@ no changes - 2014-07-30 - - - 4.3.9 - 4.3.9 - - - stable - stable - - GPL License - + 2014-07-30 + + + 4.3.9 + 4.3.9 + + + stable + stable + + GPL License + - SeedDMS_Core_KeywordCategory::getKeywordLists() sorts keywords aphabetically - SeedDMS_Core_DMS::addUser() doesn't throw an error if sql_mode is set to STRICT_TRANS_TABLES and pwdexpiration is not set to a valid date. - + 2014-10-22 @@ -960,7 +955,7 @@ by a group or user right - new method SeedDMS_Core_DMS::createDump() - minor improvements int SeedDMS_Core_Document::getReadAccessList() - + 2016-01-22 @@ -984,7 +979,7 @@ SeedDMS_Core_DMS::getNotificationsByUser() are deprecated - SeedDMS_Core_DocumentCategory::getDocumentsByCategory() now returns the documents - add SeedDMS_Core_Group::getWorkflowStatus() - SeedDMS_Core_User::getDocumentsLocked() sets locking user propperly - + 2016-01-22 @@ -1059,7 +1054,7 @@ SeedDMS_Core_DMS::getNotificationsByUser() are deprecated GPL License - add more callbacks - + 2016-04-26 @@ -1280,7 +1275,7 @@ do not sort some temporary tables anymore, because it causes an error in mysql i GPL License - all changes from 4.3.25 merged - + 2016-04-04 @@ -1440,7 +1435,7 @@ do not sort some temporary tables anymore, because it causes an error in mysql i stable GPL License - + all sql statements can be logged to a file do not sort some temporary tables anymore, because it causes an error in mysql if sql_mode=only_full_group_by is set @@ -1506,13 +1501,13 @@ do not sort some temporary tables anymore, because it causes an error in mysql i GPL License -SeedDMS_Core_DMS::filterDocumentFiles() returns also documents which are not public -if the owner tries to access them -Check return value of onPreRemove[Document|Folder], return from calling method if bool -Add SeedDMS_Core_DMS::getDocumentList() -Limit number of duplicate files to 1000 -Add hook on(Pre|Post)RemoveContent -Add hook onAttributeValidate +- SeedDMS_Core_DMS::filterDocumentFiles() returns also documents which are not public + if the owner tries to access them +- Check return value of onPreRemove[Document|Folder], return from calling method if bool +- Add SeedDMS_Core_DMS::getDocumentList() +- Limit number of duplicate files to 1000 +- Add hook on(Pre|Post)RemoveContent +- Add hook onAttributeValidate @@ -1595,9 +1590,9 @@ returns just users which are not disabled GPL License -add SeedDMS_Core_Folder::getDocumentsMinMax() -add lots of DocBlocks from merge request #8 -add SeedDMS_Core_AttributeDefinition::removeValue() +- add SeedDMS_Core_Folder::getDocumentsMinMax() +- add lots of DocBlocks from merge request #8 +- add SeedDMS_Core_AttributeDefinition::removeValue() @@ -1613,7 +1608,7 @@ add SeedDMS_Core_AttributeDefinition::removeValue() GPL License -just bump version +- just bump version @@ -1629,10 +1624,10 @@ just bump version GPL License -SeedDMS_Core_DMS::search() returns false in case of an error -do not use views in DBAccessPDO by default anymore, use temp. tables -SeedDMS_Core_Document::getNotifyList() has new parameter to include disabled user in list -fix possible sql injection in SeedDMS_Core_User +- SeedDMS_Core_DMS::search() returns false in case of an error +- do not use views in DBAccessPDO by default anymore, use temp. tables +- SeedDMS_Core_Document::getNotifyList() has new parameter to include disabled user in list +- fix possible sql injection in SeedDMS_Core_User @@ -1648,10 +1643,10 @@ fix possible sql injection in SeedDMS_Core_User GPL License -context can be passed to getAccessMode() -call hook in SeedDMS_Core_Folder::getAccessMode() -new optional parameter $listguest for SeedDMS_Core_Document::getReadAccessList() -remove deprecated methods SeedDMS_Core_Document::convert(), SeedDMS_Core_Document::wasConverted(), SeedDMS_Core_Document::viewOnline(), SeedDMS_Core_Document::getUrl() +- context can be passed to getAccessMode() +- call hook in SeedDMS_Core_Folder::getAccessMode() +- new optional parameter $listguest for SeedDMS_Core_Document::getReadAccessList() +- remove deprecated methods SeedDMS_Core_Document::convert(), SeedDMS_Core_Document::wasConverted(), SeedDMS_Core_Document::viewOnline(), SeedDMS_Core_Document::getUrl() @@ -1667,8 +1662,8 @@ remove deprecated methods SeedDMS_Core_Document::convert(), SeedDMS_Core_Documen GPL License -fix php warning if workflow state doesn' have next transition -add method SeedDMS_Core_DatabaseAccess::setLogFp() +- fix php warning if workflow state doesn' have next transition +- add method SeedDMS_Core_DatabaseAccess::setLogFp() @@ -1684,7 +1679,7 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() GPL License -??? +- ??? @@ -1709,7 +1704,7 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() - 2019-08-07 + 2019-09-06 5.1.13 @@ -1744,7 +1739,7 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() 2020-03-02 - + 5.1.15 5.1.15 @@ -1814,7 +1809,7 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() - 2020-07-28 + 2020-07-30 5.1.19 @@ -1911,7 +1906,7 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() GPL License - SeedDMS_Core_DMS::getTimeline() uses status log instead of document content -- add methods SeedDMS_Core_DocumentContent::getReviewers() and SeedDMS_Core_DocumentContent::getApprovers() +- add methods SeedDMS_Core_DocumentContent::getReviewers() and SeedDMS_Core_DocumentContent::getApprovers() - add methods SeedDMS_Core_DocumentContent::getApproveLog() and SeedDMS_Core_DocumentContent::getReviewLog() - better handling of document with an empty workflow state - fix checking of email addresses by using filter_var instead of regex @@ -1978,7 +1973,7 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() - 2022-04-25 + 2022-05-20 5.1.26 @@ -2016,5 +2011,449 @@ add method SeedDMS_Core_DatabaseAccess::setLogFp() - pass an array as an attribute to search() will OR each element + + 2022-11-07 + + + 5.1.28 + 5.1.28 + + + stable + stable + + GPL License + +- fix SeedDMS_Core_User::getDocumentContents() +- fix SeedDMS_Core_File::fileExtension() +- SeedDMS_Core_DMS::createPasswordRequest() creates a cryptographically secure hash +- fix sql error when deleting a folder attribute +- add SeedDMS_Core_Attribute::getParsedValue() and use it in SeedDMS_Core_Object::getAttributeValue() +- add SeedDMS_Core_DMS::getDuplicateSequenceNo() and SeedDMS_Core_Folder::reorderDocuments() +- add SeedDMS_Core_File::mimetype(), fix SeedDMS_Core_File::moveDir() +- all file operations use methods of SeedDMS_Core_File +- change namespace of iterators from SeedDMS to SeedDMS\Core + + + + 2022-11-21 + + + 5.1.29 + 5.1.29 + + + stable + stable + + GPL License + +- SeedDMS_Core_Folder::addDocument() does rollback transaction propperly when setting document categories fail +- add $skiproot and $sep parameter to SeedDMS_Core_Folder::getFolderPathPlain() +- add class name for 'documentfile' +- add method SeedDMS_Core_KeywordCategory::countKeywordLists() + + + + 2017-02-28 + + + 6.0.0 + 6.0.0 + + + stable + stable + + GPL License + +- all changes from 5.0.14 merged +- SeedDMS_Core_User::getReceiptStatus() and SeedDMS_Core_User::getReviewStatus() +only return entries of the latest document version if not specific document and +version is passed +- temp. table for revisions can be created +- new methods SeedDMS_Core_DMS::getDocumentsInReception() and +SeedDMS_Core_DMS::getDocumentsInRevision() +- limit hits of sql statement in SeedDMЅ_Core_DMS::getDuplicateDocumentContent() to 1000 +- finishRevsion() puts all revisors into state waiting, so a new revision can be started +- fix SeedDMS_Core_Group::getRevisionStatus(), which did not always return the last +log entry first +- add roles +- use classname from SeedDMS_Core_DMS::_classnames for SeedDMS_Core_DocumentContent +- add virtual access mode for document links and attachments plus callbacks to + check access mode in a hook +- add new method SeedDMS_Core_DMS::getDocumentsExpired() + + + + 2017-05-28 + + + 6.0.1 + 6.0.1 + + + stable + stable + + GPL License + +- speed up getting list of locked documents +- setting _logfile in inc.DBAccessPDO.php will log execution times in file + + + + 2017-12-19 + + + 6.0.2 + 6.0.2 + + + stable + stable + + GPL License + +- speed up getting list of locked documents +- setting _logfile in inc.DBAccessPDO.php will log execution times in file +- fix sql statement to create temp table ttrevisionid and ttreceiptid +- SeedDMS_Core_DMS::noReadForStatus no longer needed +- SeedDMS_Core_Document::checkForDueRevisionWorkflow() also checks if there +are any waiting or pending revisions at all +- SeedDMS_Core_User::getReverseSubstitutes() works with new roles +- fix field name in getDocumentList() to make it work for pgsql +- views instead of temp. tables can be used +- ReceiveOwner list does not contain old versions anymore +- all changes up to 5.1.5 merged +- getTimeline() also returns data for documents with a scheduled revision + + + + 2018-01-23 + + + 6.0.3 + 6.0.3 + + + stable + stable + + GPL License + +pass 0 as default to getObjects() +SeedDMS_Core_AttributeDefinition::getStatistics() returns propper values for each item in a value set +SeedDMS_Core_DMS::getDocumentList() returns list of documents without a receiver + + + + 2018-02-14 + + + 6.0.4 + 6.0.4 + + + stable + stable + + GPL License + +add lists of drafts and obsolete docs in SeedDMS_Core_DMS::getDocumentList() +add fast sql statement to SeedDMS_Core_Document::getReceiptStatus() if limit=1 +add callback onCheckAccessDocument to SeedDMS_Core_Document::getAccessMode() +add new document status 'needs correction' (S_NEEDS_CORRECTION) +do not use views as a replacement for temp. tables anymore, because they are much +slower. +add SeedDMS_Core_DocumentContent::getInstance() + + + + 2018-02-27 + + + 6.0.5 + 6.0.5 + + + stable + stable + + GPL License + +add list 'NeedsCorrectionOwner' to SeedDMS_Core_DMS::getDocumentList() + + + + 2018-11-13 + + + 6.0.6 + 6.0.6 + + + stable + stable + + GPL License + +SeedDMS_Core_Folder::addContent() uses currently logged in user as uploader instead of owner +SeedDMS_Core_DocumentContent::verifyStatus() will not set status to S_RELEASED +if currently in S_DRAFT status und no workflow, review, approval, or revision +is pending. + + + + 2020-02-17 + + + 6.0.7 + 6.0.7 + + + stable + stable + + GPL License + +SeedDMS_Core_Document::getTimeline() returns revision only for latest content +add callback onSetStatus in SeedDMS_Core_DocumentContent::setStatus() +add new list type 'DueRevision' in SeedDMS_Core_DMS::getDocumentList() +a revision can also be started if some revisors have already reviewed the document +remove a user from all its process can also be used to set a new user + + + + 2020-03-02 + + + 6.0.8 + 6.0.8 + + + stable + stable + + GPL License + +- no changes, just keep same version as seeddms application + + + + 2020-05-14 + + + 6.0.9 + 6.0.9 + + + stable + stable + + GPL License + +- no changes, just keep same version as seeddms application + + + + 2020-05-22 + + + 6.0.10 + 6.0.10 + + + stable + stable + + GPL License + +SeedDMS_Core_DocumentContent::delRevisor() returns -4 if user has already made a revision + + + + 2020-06-05 + + + 6.0.11 + 6.0.11 + + + stable + stable + + GPL License + +SeedDMS_Core_DMS::filterAccess() properly checks for documents + + + + 2020-06-05 + + + 6.0.12 + 6.0.12 + + + stable + stable + + GPL License + + + + + 2020-09-29 + + + 6.0.13 + 6.0.13 + + + stable + stable + + GPL License + +- SeedDMS_Folder_DMS::getAccessList() and getDefaultAccess() do not return fals anymore if the parent does not exists. They just stop inheritance. + + + + 2021-01-04 + + + 6.0.14 + 6.0.14 + + + stable + stable + + GPL License + +better error checking in SeedDMS_Core_Document::cancelCheckOut() + + + + 2021-04-13 + + + 6.0.15 + 6.0.15 + + + stable + stable + + GPL License + +- add searching for revision date +- expired documents can be skipped from counting in countTasks() +- SeedDMS_Core_DMS::getDocumentList() uses ambiguous column name when sorting by status +- add list type SleepingReviseByMe to SeedDMS_Core_DMS::getDocumentList() +- parameter 2 of SeedDMS_Core_DMS::getDocumentList() is treated as number of + days for list DueRevisions + + + + 2021-05-07 + + + 6.0.16 + 6.0.16 + + + stable + stable + + GPL License + + + + + 2021-12-11 + + + 6.0.17 + 6.0.17 + + + stable + stable + + GPL License + +- all changes from 5.1.24 + + + + 2022-04-22 + + + 6.0.18 + 6.0.18 + + + stable + stable + + GPL License + +- all changes from 5.1.25 +- fix searching for document content with a custom attribute having a value set +- SeedDMS_Core_DocumentContent::getWorkflow() has optional parameter to return data from table tblWorkflowDocumentContent + + + + 2022-05-20 + + + 6.0.19 + 6.0.19 + + + stable + stable + + GPL License + +- all changes from 5.1.26 +- removeFromProcesses() will not touch documents for which the new user does not have at least read access + + + + 2022-09-18 + + + 6.0.20 + 6.0.20 + + + stable + stable + + GPL License + +- all changes from 5.1.27 merged +- SeedDMЅ_Core_DMS::getDocumentsInRevision() returns status from revision log + + + + 2022-11-18 + + + 6.0.21 + 6.0.21 + + + stable + stable + + GPL License + +- all changes from 5.1.28 merged + + diff --git a/SeedDMS_Core/phpunit.xml b/SeedDMS_Core/phpunit.xml new file mode 100644 index 000000000..103f11089 --- /dev/null +++ b/SeedDMS_Core/phpunit.xml @@ -0,0 +1,26 @@ + + + + + tests + + + + + + Core + + + diff --git a/SeedDMS_Core/tests/.phpunit.result.cache b/SeedDMS_Core/tests/.phpunit.result.cache new file mode 100644 index 000000000..007ae03d8 --- /dev/null +++ b/SeedDMS_Core/tests/.phpunit.result.cache @@ -0,0 +1 @@ +C:37:"PHPUnit\Runner\DefaultTestResultCache":106:{a:2:{s:7:"defects";a:1:{s:17:"DmsTest::testInit";i:3;}s:5:"times";a:1:{s:17:"DmsTest::testInit";d:0.002;}}} \ No newline at end of file diff --git a/SeedDMS_Core/tests/AttributeDefinitionTest.php b/SeedDMS_Core/tests/AttributeDefinitionTest.php new file mode 100644 index 000000000..f6276e536 --- /dev/null +++ b/SeedDMS_Core/tests/AttributeDefinitionTest.php @@ -0,0 +1,574 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\TestCase; + +/** + * Attribute definition test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class AttributeDefinitionTest extends TestCase +{ + + /** + * Create a real dms object with a mocked db + * + * This mock is only used if \SeedDMS_Core_DatabaseAccess::getResult() is + * called once. This is the case for all \SeedDMS_Core_AttributeDefinition::setXXX() + * methods like setName(). + * + * @return \SeedDMS_Core_DMS + */ + protected function getDmsWithMockedDb() : \SeedDMS_Core_DMS + { + $db = $this->createMock(\SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE ")) + ->willReturn(true); + $dms = new \SeedDMS_Core_DMS($db, ''); + return $dms; + } + + /** + * Create a mocked dms + * + * @return \SeedDMS_Core_DMS + */ + protected function getDmsMock() : \SeedDMS_Core_DMS + { + $dms = $this->createMock(\SeedDMS_Core_DMS::class); + $dms->expects($this->any()) + ->method('getDocument') + ->with(1) + ->willReturn(true); + $dms->expects($this->any()) + ->method('getFolder') + ->with(1) + ->willReturn(true); + $dms->expects($this->any()) + ->method('getUser') + ->will( + $this->returnValueMap( + array( + array(1, new \SeedDMS_Core_User(1, 'admin', 'pass', 'Joe Foo', 'baz@foo.de', 'en_GB', 'bootstrap', 'My comment', \SeedDMS_Core_User::role_admin)), + array(2, new \SeedDMS_Core_User(2, 'admin2', 'pass', 'Joe Bar', 'bar@foo.de', 'en_GB', 'bootstrap', 'My comment', \SeedDMS_Core_User::role_admin)), + array(3, null) + ) + ) + ); + $dms->expects($this->any()) + ->method('getGroup') + ->will( + $this->returnValueMap( + array( + array(1, new \SeedDMS_Core_Group(1, 'admin group 1', 'My comment')), + array(2, new \SeedDMS_Core_Group(2, 'admin group 2', 'My comment')), + array(3, null) + ) + ) + ); + return $dms; + } + + /** + * Create a mock attribute definition object + * + * @param int $type type of attribute + * @param boolean $multiple set to true for multi value attributes + * @param int $minvalues minimum number of attribute values + * @param int $maxvalues maximum number of attribute values + * @param string $valueset list of allowed values separated by the first char + * @param string $regex regular expression that must match the attribute value + * + * @return \SeedDMS_Core_AttributeDefinition + */ + protected function getAttributeDefinition($type, $multiple=false, $minvalues=0, $maxvalues=0, $valueset='', $regex='') + { + $attrdef = new \SeedDMS_Core_AttributeDefinition(1, 'foo attr', \SeedDMS_Core_AttributeDefinition::objtype_folder, $type, $multiple, $minvalues, $maxvalues, $valueset, $regex); + return $attrdef; + } + + /** + * Test getId() + * + * @return void + */ + public function testGetId() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals(1, $attrdef->getId()); + } + + /** + * Test getName() + * + * @return void + */ + public function testGetName() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals('foo attr', $attrdef->getName()); + } + + /** + * Test setName() + * + * @return void + */ + public function testSetName() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + $attrdef->setName('bar attr'); + $this->assertEquals('bar attr', $attrdef->getName()); + } + + /** + * Test getObjType() + * + * @return void + */ + public function testGetObjType() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::objtype_folder, $attrdef->getObjType()); + } + + /** + * Test setObjType() + * + * @return void + */ + public function testSetObjType() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + $attrdef->setObjType(\SeedDMS_Core_AttributeDefinition::objtype_document); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::objtype_document, $attrdef->getObjType()); + } + + /** + * Test getType() + * + * @return void + */ + public function testGetType() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::type_int, $attrdef->getType()); + } + + /** + * Test setType() + * + * @return void + */ + public function testSetType() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + $attrdef->setType(\SeedDMS_Core_AttributeDefinition::type_string); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::type_string, $attrdef->getType()); + } + + /** + * Test getMultipleValues() + * + * @return void + */ + public function testGetMultipleValues() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals(false, $attrdef->getMultipleValues()); + } + + /** + * Test setMultipleValues() + * + * @return void + */ + public function testSetMultipleValues() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + /* Toogle the current value of multiple values */ + $oldvalue = $attrdef->getMultipleValues(); + $attrdef->setMultipleValues(!$oldvalue); + $this->assertEquals(!$oldvalue, $attrdef->getMultipleValues()); + } + + /** + * Test getMinValues() + * + * @return void + */ + public function testGetMinValues() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals(0, $attrdef->getMinValues()); + } + + /** + * Test setMinValues() + * + * @return void + */ + public function testSetMinValues() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + /* add 5 to value of min values */ + $oldvalue = $attrdef->getMinValues(); + $attrdef->setMinValues($oldvalue+5); + $this->assertEquals($oldvalue+5, $attrdef->getMinValues()); + } + + /** + * Test getMaxValues() + * + * @return void + */ + public function testGetMaxValues() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEquals(0, $attrdef->getMaxValues()); + } + + /** + * Test setMaxValues() + * + * @return void + */ + public function testSetMaxValues() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + /* add 5 to value of max values */ + $oldvalue = $attrdef->getMaxValues(); + $attrdef->setMaxValues($oldvalue+5); + $this->assertEquals($oldvalue+5, $attrdef->getMaxValues()); + } + + /** + * Test getValueSet() + * + * @return void + */ + public function testGetValueSet() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|foo|bar|baz'); + $this->assertEquals('|foo|bar|baz', $attrdef->getValueSet()); + } + + /** + * Test getValueSetSeparator() + * + * @return void + */ + public function testGetValueSetSeparator() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|foo|bar|baz'); + $this->assertEquals('|', $attrdef->getValueSetSeparator()); + /* No value set will return no separator */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertEmpty($attrdef->getValueSetSeparator()); + /* Even a 1 char value set will return no separator */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|'); + $this->assertEmpty($attrdef->getValueSetSeparator()); + /* Multiple users or groups always use a ',' as a separator */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_user, true); + $this->assertEquals(',', $attrdef->getValueSetSeparator()); + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_group, true); + $this->assertEquals(',', $attrdef->getValueSetSeparator()); + } + + /** + * Test getValueSetAsArray() + * + * @return void + */ + public function testGetValueSetAsArray() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|foo|bar|baz '); + $valueset = $attrdef->getValueSetAsArray(); + $this->assertIsArray($valueset); + $this->assertCount(3, $valueset); + /* value set must contain 'baz' though 'baz ' was originally set */ + $this->assertContains('baz', $valueset); + /* No value set will return an empty array */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string); + $valueset = $attrdef->getValueSetAsArray(); + $this->assertIsArray($valueset); + $this->assertEmpty($valueset); + } + + /** + * Test getValueSetValue() + * + * @return void + */ + public function testGetValueSetValue() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|foo|bar|baz '); + $this->assertEquals('foo', $attrdef->getValueSetValue(0)); + /* Check if trimming of 'baz ' worked */ + $this->assertEquals('baz', $attrdef->getValueSetValue(2)); + /* Getting the value of a none existing index returns false */ + $this->assertFalse($attrdef->getValueSetValue(3)); + + /* Getting a value from a none existing value set returns false as well */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string); + $this->assertFalse($attrdef->getValueSetValue(0)); + } + + /** + * Test setValueSet() + * + * @return void + */ + public function testSetValueSet() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + /* add 5 to value of min values */ + $attrdef->setValueSet(' |foo|bar | baz '); + $this->assertEquals('|foo|bar|baz', $attrdef->getValueSet()); + } + + /** + * Test getRegex() + * + * @return void + */ + public function testGetRegex() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '', '[0-9].*'); + $this->assertEquals('[0-9].*', $attrdef->getRegex()); + } + + /** + * Test setRegex() + * + * @return void + */ + public function testSetRegex() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + /* set a new valid regex */ + $this->assertTrue($attrdef->setRegex(' /[0-9].*/i ')); + $this->assertEquals('/[0-9].*/i', $attrdef->getRegex()); + /* set a new invalid regex will return false and keep the old regex */ + $this->assertFalse($attrdef->setRegex(' /([0-9].*/i ')); + $this->assertEquals('/[0-9].*/i', $attrdef->getRegex()); + } + + /** + * Test setEmptyRegex() + * + * @return void + */ + public function testSetEmptyRegex() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string); + /* A mocked dms is needed for updating the database */ + $attrdef->setDMS(self::getDmsWithMockedDb()); + /* set an empty regex */ + $this->assertTrue($attrdef->setRegex('')); + } + + /** + * Test parseValue() + * + * @return void + */ + public function testParseValue() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string); + $value = $attrdef->parseValue('foo'); + $this->assertIsArray($value); + $this->assertCount(1, $value); + $this->assertContains('foo', $value); + /* An attribute definition with multiple values will split the value by the first char */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, true, 0, 0, '|baz|bar|foo'); + $value = $attrdef->parseValue('|bar|baz'); + $this->assertIsArray($value); + $this->assertCount(2, $value); + /* An attribute definition without multiple values, will treat the value as a string */ + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|baz|bar|foo'); + $value = $attrdef->parseValue('|bar|baz'); + $this->assertIsArray($value); + $this->assertCount(1, $value); + $this->assertContains('|bar|baz', $value); + } + + /** + * Test validate() + * + * @TODO Instead of having a lengthy list of assert calls, this could be + * implemented with data providers for each attribute type + * + * @return void + */ + public function testValidate() + { + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string); + $this->assertTrue($attrdef->validate('')); // even an empty string is valid + $this->assertTrue($attrdef->validate('foo')); // there is no invalid string + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '', '/[0-9]*S/'); + $this->assertFalse($attrdef->validate('foo')); // doesn't match the regex + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_regex, $attrdef->getValidationError()); + $this->assertTrue($attrdef->validate('S')); // no leading numbers needed + $this->assertTrue($attrdef->validate('8980S')); // leading numbers are ok + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, false, 0, 0, '|foo|bar|baz', ''); + $this->assertTrue($attrdef->validate('foo')); // is part of value map + $this->assertFalse($attrdef->validate('foz')); // is not part of value map + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_valueset, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, true, 0, 0, '|foo|bar|baz', ''); + $this->assertTrue($attrdef->validate('foo')); // is part of value map + $this->assertFalse($attrdef->validate('')); // an empty value cannot be in the valueset + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_valueset, $attrdef->getValidationError()); + $this->assertTrue($attrdef->validate('|foo|baz')); // both are part of value map + $this->assertFalse($attrdef->validate('|foz|baz')); // 'foz' is not part of value map + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_valueset, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_string, true, 1, 1, '|foo|bar|baz', ''); + $this->assertTrue($attrdef->validate('foo')); // is part of value map + $this->assertFalse($attrdef->validate('')); // empty string is invalid because of min values = 1 + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_min_values, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('|foo|baz')); // both are part of value map, but only value is allowed + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_max_values, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_boolean); + $this->assertTrue($attrdef->validate(0)); + $this->assertTrue($attrdef->validate(1)); + $this->assertFalse($attrdef->validate(2)); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_boolean, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_int); + $this->assertTrue($attrdef->validate(0)); + $this->assertTrue($attrdef->validate('0')); + $this->assertFalse($attrdef->validate('a')); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_int, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_date); + $this->assertTrue($attrdef->validate('2021-09-30')); + $this->assertTrue($attrdef->validate('1968-02-29')); // 1968 was a leap year + $this->assertTrue($attrdef->validate('2000-02-29')); // 2000 was a leap year + $this->assertFalse($attrdef->validate('1900-02-29')); // 1900 didn't was a leap year + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_date, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('1970-02-29')); // 1970 didn't was a leap year + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_date, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('2010/02/28')); // This has the wrong format + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_date, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('1970-00-29')); // 0 month is not allowed + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_date, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('1970-01-00')); // 0 day is not allowed + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_date, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_float); + $this->assertTrue($attrdef->validate('0.567')); + $this->assertTrue($attrdef->validate('1000')); + $this->assertTrue($attrdef->validate('1000e3')); + $this->assertTrue($attrdef->validate('1000e-3')); + $this->assertTrue($attrdef->validate('-1000')); + $this->assertTrue($attrdef->validate('+1000')); + $this->assertFalse($attrdef->validate('0,567')); // wrong decimal point + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_float, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('0.56.7')); // two decimal point + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_float, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_email); + $this->assertTrue($attrdef->validate('info@seeddms.org')); + $this->assertTrue($attrdef->validate('info@seeddms.verylongtopleveldomain')); + $this->assertFalse($attrdef->validate('@seeddms.org')); // no user + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('info@localhost')); // no tld + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('info@@seeddms.org')); // double @ + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + $this->assertTrue($attrdef->validate('info@subsubdomain.subdomain.seeddms.org')); // multiple subdomains are ok + $this->assertFalse($attrdef->validate('info@seeddms..org')); // double . is not allowed + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('info@s.org')); // 2nd level domain name is too short + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + $this->assertFalse($attrdef->validate('info@seeddms.o')); // top level domain name is too short + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + $this->assertTrue($attrdef->validate('info@0123456789-0123456789-0123456789-0123456789-0123456789-01234567.org')); // domain name is 63 chars long, which is the max length + $this->assertFalse($attrdef->validate('info@0123456789-0123456789-0123456789-0123456789-0123456789-012345678.org')); // domain name is 1 char longer than 63 chars + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_email, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_url); + $this->assertTrue($attrdef->validate('http://seeddms.org')); + $this->assertTrue($attrdef->validate('https://seeddms.org')); + $this->assertFalse($attrdef->validate('ftp://seeddms.org')); // ftp is not allowed + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_url, $attrdef->getValidationError()); + $this->assertTrue($attrdef->validate('http://localhost')); // no tld is just fine + $this->assertFalse($attrdef->validate('http://localhost.o')); // tld is to short + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_url, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_user); + $attrdef->setDMS(self::getDmsMock()); + $this->assertTrue($attrdef->validate(1)); + $this->assertFalse($attrdef->validate(3)); + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_user, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_group); + $attrdef->setDMS(self::getDmsMock()); + $this->assertTrue($attrdef->validate('1')); + $this->assertTrue($attrdef->validate('2')); + $this->assertFalse($attrdef->validate('3')); // there is no group with id=3 + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_group, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_group, true); + $attrdef->setDMS(self::getDmsMock()); + $this->assertTrue($attrdef->validate(',1,2')); + $this->assertFalse($attrdef->validate(',1,2,3')); // there is no group with id=3 + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_group, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_user); + $attrdef->setDMS(self::getDmsMock()); + $this->assertTrue($attrdef->validate('1')); + $this->assertTrue($attrdef->validate('2')); + $this->assertFalse($attrdef->validate('3')); // there is no user with id=3 + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_user, $attrdef->getValidationError()); + + $attrdef = self::getAttributeDefinition(\SeedDMS_Core_AttributeDefinition::type_user, true); + $attrdef->setDMS(self::getDmsMock()); + $this->assertTrue($attrdef->validate(',1,2')); + $this->assertFalse($attrdef->validate(',1,2,3')); // there is no user with id=3 + $this->assertEquals(\SeedDMS_Core_AttributeDefinition::val_error_user, $attrdef->getValidationError()); + } + +} diff --git a/SeedDMS_Core/tests/AttributeTest.php b/SeedDMS_Core/tests/AttributeTest.php new file mode 100644 index 000000000..66d7dba84 --- /dev/null +++ b/SeedDMS_Core/tests/AttributeTest.php @@ -0,0 +1,155 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\TestCase; + +/** + * Attribute and attribute definition test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class AttributeTest extends TestCase +{ + + /** + * Create a mock dms object + * + * @return SeedDMS_Core_DMS + */ + protected function getMockDMS() : SeedDMS_Core_DMS + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->any()) + ->method('getResult') + ->with($this->stringContains("UPDATE ")) + ->willReturn(true); + $dms = new SeedDMS_Core_DMS($db, ''); + return $dms; + } + + /** + * Create a mock attribute definition object + * + * @param int $type type of attribute + * @param boolean $multiple true if multiple values are allowed + * @param int $minvalues minimum number of required values + * @param int $maxvalues maximum number of required value + * @param string $valueset list of allowed values separated by the first char + * @param string $regex regular expression the attribute value must match + * + * @return SeedDMS_Core_AttributeDefinition + */ + protected function getAttributeDefinition($type, $multiple=false, $minvalues=0, $maxvalues=0, $valueset='', $regex='') + { + $attrdef = new SeedDMS_Core_AttributeDefinition(1, 'foo attrdef', SeedDMS_Core_AttributeDefinition::objtype_folder, $type, $multiple, $minvalues, $maxvalues, $valueset, $regex); + return $attrdef; + } + + /** + * Create a mock attribute object + * + * @param SeedDMS_Core_AttributeDefinition $attrdef attribute defintion of attribute + * @param mixed $value value of attribute + * + * @return SeedDMS_Core_Attribute + */ + static protected function getAttribute($attrdef, $value) + { + $folder = new SeedDMS_Core_Folder(1, 'Folder', null, '', '', '', 0, 0, 0); + $attribute = new SeedDMS_Core_Attribute(1, $folder, $attrdef, $value); + $attribute->setDMS($attrdef->getDMS()); + return $attribute; + } + + /** + * Test getId() + * + * @return void + */ + public function testGetId() + { + $attrdef = self::getAttributeDefinition(SeedDMS_Core_AttributeDefinition::type_int); + $attribute = self::getAttribute($attrdef, ''); + $this->assertEquals(1, $attribute->getId()); + } + + /** + * Test getValue() + * + * @return void + */ + public function testGetValue() + { + $attrdef = self::getAttributeDefinition(SeedDMS_Core_AttributeDefinition::type_int); + $attribute = self::getAttribute($attrdef, 7); + $this->assertEquals(7, $attribute->getValue()); + } + + /** + * Test getValueAsArray() + * + * @return void + */ + public function testGetValueAsArray() + { + $attrdef = self::getAttributeDefinition(SeedDMS_Core_AttributeDefinition::type_int); + $attribute = self::getAttribute($attrdef, 7); + $this->assertIsArray($attribute->getValueAsArray()); + $this->assertCount(1, $attribute->getValueAsArray()); + $this->assertContains(7, $attribute->getValueAsArray()); + + /* Test a multi value integer */ + $attrdef = self::getAttributeDefinition(SeedDMS_Core_AttributeDefinition::type_int, true); + $attribute = self::getAttribute($attrdef, ',3,4,6'); + $value = $attribute->getValueAsArray(); + $this->assertIsArray($attribute->getValueAsArray()); + $this->assertCount(3, $attribute->getValueAsArray()); + $this->assertContains('6', $attribute->getValueAsArray()); + } + + /** + * Test setValue() + * + * @return void + */ + public function testSetValue() + { + $attrdef = self::getAttributeDefinition(SeedDMS_Core_AttributeDefinition::type_int); + $attrdef->setDMS(self::getMockDMS()); + $attribute = self::getAttribute($attrdef, 0); + $this->assertTrue($attribute->setValue(9)); + $this->assertEquals(9, $attribute->getValue()); + /* Setting an array of values for a none multi value attribute will just take the + * element of the array. + */ + $this->assertTrue($attribute->setValue([8,9])); + $this->assertEquals(8, $attribute->getValue()); + + $attrdef = self::getAttributeDefinition(SeedDMS_Core_AttributeDefinition::type_int, true); + $attrdef->setDMS(self::getMockDMS()); + $attribute = self::getAttribute($attrdef, ',3,4,6'); + $attribute->setValue([8,9,10]); + $this->assertEquals(',8,9,10', $attribute->getValue()); + $this->assertIsArray($attribute->getValueAsArray()); + $this->assertCount(3, $attribute->getValueAsArray()); + $this->assertContains('9', $attribute->getValueAsArray()); + } +} diff --git a/SeedDMS_Core/tests/DatabaseTest.php b/SeedDMS_Core/tests/DatabaseTest.php new file mode 100644 index 000000000..bfbc5ce42 --- /dev/null +++ b/SeedDMS_Core/tests/DatabaseTest.php @@ -0,0 +1,324 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +namespace PHPUnit\Framework; + +use PHPUnit\Framework\SeedDmsTest; + +require_once('SeedDmsBase.php'); + +/** + * Low level Database test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DatabaseTest extends SeedDmsTest +{ + + /** + * Create a sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + } + + /** + * Check if connection to database exists + * + * @return void + */ + public function testIsConnected() + { + $this->assertTrue(self::$dbh->ensureConnected()); + } + + /** + * Test for number of tables in database + * + * @return void + */ + public function testTableList() + { + $tablelist = self::$dbh->TableList(); + $this->assertIsArray($tablelist); + // There are just 42 tables in SeedDMS5 and 55 tables in SeedDMS6, + // but one additional + // table 'sqlite_sequence' + $dms = new \SeedDMS_Core_DMS(null, ''); + if($dms->version[0] == '5') + $this->assertCount(43, $tablelist); + else + $this->assertCount(56, $tablelist); + } + + /** + * Test createTemporaryTable() + * + * @return void + */ + public function testCreateTemporaryTable() + { + foreach (['ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid'] as $temp) { + $ret = self::$dbh->createTemporaryTable($temp); + $rec = self::$dbh->getResultArray("SELECT * FROM `".$temp."`"); + $this->assertIsArray($rec); + } + /* Running it again will not harm */ + foreach (['ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid'] as $temp) { + $ret = self::$dbh->createTemporaryTable($temp); + $rec = self::$dbh->getResultArray("SELECT * FROM `".$temp."`"); + $this->assertIsArray($rec); + } + /* Running it again and overwrite the old table contents */ + foreach (['ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid'] as $temp) { + $ret = self::$dbh->createTemporaryTable($temp, true); + $rec = self::$dbh->getResultArray("SELECT * FROM `".$temp."`"); + $this->assertIsArray($rec); + } + } + + /** + * Test createTemporaryTable() based on views + * + * @return void + */ + public function testCreateTemporaryTableBasedOnViews() + { + self::$dbh->useViews(true); + foreach (['ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid'] as $temp) { + $ret = self::$dbh->createTemporaryTable($temp); + $rec = self::$dbh->getResultArray("SELECT * FROM `".$temp."`"); + $this->assertIsArray($rec); + } + $viewlist = self::$dbh->ViewList(); + $this->assertIsArray($viewlist); + $this->assertCount(4, $viewlist); + + /* Running it again will not harm */ + foreach (['ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid'] as $temp) { + $ret = self::$dbh->createTemporaryTable($temp); + $rec = self::$dbh->getResultArray("SELECT * FROM `".$temp."`"); + $this->assertIsArray($rec); + } + /* Running it again and replace the old view */ + foreach (['ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid'] as $temp) { + $ret = self::$dbh->createTemporaryTable($temp, true); + $rec = self::$dbh->getResultArray("SELECT * FROM `".$temp."`"); + $this->assertIsArray($rec); + } + } + + /** + * Test for number of views in database + * + * @return void + */ + public function testViewList() + { + $viewlist = self::$dbh->ViewList(); + $this->assertIsArray($viewlist); + // There are 0 views + $this->assertCount(0, $viewlist); + } + + /** + * Test getDriver() + * + * @return void + */ + public function testGetDriver() + { + $driver = self::$dbh->getDriver(); + $this->assertEquals('sqlite', $driver); + } + + /** + * Test rbt() + * + * @return void + */ + public function testRbt() + { + $str = self::$dbh->rbt("SELECT * FROM `tblUsers`"); + $this->assertEquals('SELECT * FROM "tblUsers"', $str); + } + + /** + * Test if table tblFolders has root folder + * + * @return void + */ + public function testInitialRootFolder() + { + $this->assertTrue(self::$dbh->hasTable('tblFolders')); + $query = 'SELECT * FROM `tblFolders`'; + $recs = self::$dbh->getResultArray($query); + $this->assertIsArray($recs); + $this->assertCount(1, $recs); + } + + /** + * Test if table tblUsers has two initial users + * + * @return void + */ + public function testInitialUsers() + { + $this->assertTrue(self::$dbh->hasTable('tblUsers')); + $query = 'SELECT * FROM `tblUsers`'; + $recs = self::$dbh->getResultArray($query); + $this->assertIsArray($recs); + $this->assertCount(2, $recs); + } + + /** + * Test getCurrentDatetime() + * + * @return void + */ + public function testGetCurrentDatetime() + { + $query = 'SELECT '.self::$dbh->getCurrentDatetime().' as a'; + $recs = self::$dbh->getResultArray($query); + $now = date('Y-m-d H:i:s'); + $this->assertIsArray($recs); + $this->assertEquals($now, $recs[0]['a'], 'Make sure php.ini has the proper timezone configured'); + } + + /** + * Test getCurrentTimestamp() + * + * @return void + */ + public function testGetCurrentTimestamp() + { + $query = 'SELECT '.self::$dbh->getCurrentTimestamp().' as a'; + $recs = self::$dbh->getResultArray($query); + $now = time(); + $this->assertIsArray($recs); + $this->assertEquals($now, $recs[0]['a'], 'Make sure php.ini has the proper timezone configured'); + } + + /** + * Test concat() + * + * @return void + */ + public function testConcat() + { + $query = 'SELECT '.self::$dbh->concat(["'foo'", "'baz'", "'bar'"]).' as a'; + $recs = self::$dbh->getResultArray($query); + $this->assertIsArray($recs); + $this->assertEquals('foobazbar', $recs[0]['a']); + } + + /** + * Test qstr() + * + * @return void + */ + public function testQstr() + { + $str = self::$dbh->qstr("bar"); + $this->assertEquals("'bar'", $str); + } + + /** + * Test getResult() if the sql fails + * + * @return void + */ + public function testGetResultSqlFail() + { + $ret = self::$dbh->getResult("UPDATE FOO SET `name`='foo'"); + $this->assertFalse($ret); + $errmsg = self::$dbh->getErrorMsg(); + $this->assertStringContainsString('no such table: FOO', $errmsg); + } + + /** + * Test getResultArray() if the sql fails + * + * @return void + */ + public function testGetResultArraySqlFail() + { + $ret = self::$dbh->getResultArray("SELECT * FROM FOO"); + $this->assertFalse($ret); + $errmsg = self::$dbh->getErrorMsg(); + $this->assertStringContainsString('no such table: FOO', $errmsg); + } + + /** + * Test logging into file + * + * @return void + */ + public function testLogging() + { + $fp = fopen('php://memory', 'r+'); + self::$dbh->setLogFp($fp); + $sql = "SELECT * FROM `tblUsers`"; + $ret = self::$dbh->getResultArray($sql); + $this->assertIsArray($ret); + fseek($fp, 0); + $contents = fread($fp, 200); + /* Check if sql statement was logged into file */ + $this->assertStringContainsString($sql, $contents); + fclose($fp); + } + + /** + * Test createDump() + * + * @return void + */ + public function testCreateDump() + { + $fp = fopen('php://memory', 'r+'); + $ret = self::$dbh->createDump($fp); + $this->assertTrue($ret); + $stat = fstat($fp); + $this->assertIsArray($stat); + $dms = new \SeedDMS_Core_DMS(null, ''); + if($dms->version[0] == '5') + $this->assertEquals(1724, $stat['size']); + else + $this->assertEquals(2272, $stat['size']); +// fseek($fp, 0); +// echo fread($fp, 200); + fclose($fp); + } +} + diff --git a/SeedDMS_Core/tests/DmsTest.php b/SeedDMS_Core/tests/DmsTest.php new file mode 100644 index 000000000..a6811c5d6 --- /dev/null +++ b/SeedDMS_Core/tests/DmsTest.php @@ -0,0 +1,2956 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * DMS test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DmsTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Create a mock admin role object (only used for SeedDMS 6) + * + * @return SeedDMS_Core_User + */ + protected function getAdminRole() + { + $role = new SeedDMS_Core_Role(1, 'admin', SeedDMS_Core_Role::role_admin); + return $role; + } + + /** + * Test checkIfEqual() + * + * @return void + */ + public function testCheckIfEqual() + { + $user1 = new SeedDMS_Core_User(1, 'user 1', '', '', '', '', '', '', 1); + $group1 = new SeedDMS_Core_Group(1, 'group 1', ''); + $group1n = new SeedDMS_Core_Group(1, 'group 1n', ''); + $group1c = clone $group1; + $group2 = new SeedDMS_Core_Group(2, 'group 1', ''); + $dms = new SeedDMS_Core_DMS(null, ''); + $this->assertFalse($dms->checkIfEqual($group1, $user1)); // different classes + $this->assertFalse($dms->checkIfEqual($group1, $group2)); // different id + $this->assertTrue($dms->checkIfEqual($group1, $group1c)); // a clone is always equal + $this->assertTrue($dms->checkIfEqual($group1, $group1n)); // different instances but same id is sufficient to be equal + } /* }}} */ + + /** + * Test checkDate() + * + * @return void + */ + public function testCheckDate() + { + $dms = new SeedDMS_Core_DMS(null, ''); + $this->assertTrue($dms->checkDate('2020-02-28 10:12:34')); + $this->assertTrue($dms->checkDate('2020-02-29 10:12:34')); // a leap year + $this->assertFalse($dms->checkDate('2020-02-30 10:12:34')); // feb has never 30 days + $this->assertFalse($dms->checkDate('2021-02-29 10:12:34')); // not a leap year + $this->assertFalse($dms->checkDate('2020-02-28 24:12:34')); // hour is out of range + $this->assertFalse($dms->checkDate('2020-02-28 23:60:34')); // minute is out of range + $this->assertFalse($dms->checkDate('2020-02-28 23:59:60')); // second is out of range + $this->assertFalse($dms->checkDate('2020-02-28 23:59:')); // second is missing + $this->assertTrue($dms->checkDate('2020-02-28', 'Y-m-d')); // just checking the date + $this->assertFalse($dms->checkDate('28.2.2020', 'd.m.Y')); // month must be 01-12 + $this->assertTrue($dms->checkDate('28.2.2020', 'd.n.Y')); // month must be 1-12 + $this->assertFalse($dms->checkDate('28.02.2020', 'd.n.Y')); // month must be 1-12 + } /* }}} */ + + /** + * Test getClassname() + * + * @return void + */ + public function testGetClassName() + { + /* Do not mess up the global instance self::$dms, but create my own */ + $dms = new SeedDMS_Core_DMS(null, ''); + $this->assertEquals('SeedDMS_Core_Folder', $dms->getClassname('folder')); + $this->assertEquals('SeedDMS_Core_Document', $dms->getClassname('document')); + $this->assertEquals('SeedDMS_Core_DocumentContent', $dms->getClassname('documentcontent')); + $this->assertEquals('SeedDMS_Core_User', $dms->getClassname('user')); + $this->assertEquals('SeedDMS_Core_Group', $dms->getClassname('group')); + $this->assertFalse($dms->getClassname('foo')); + } + + /** + * Test setClassname() + * + * @return void + */ + public function testSetClassName() + { + /* Do not mess up the global instance self::$dms, but create my own */ + $dms = new SeedDMS_Core_DMS(null, ''); + $this->assertEquals('SeedDMS_Core_Folder', $dms->setClassname('folder', 'MyNewFolderClass')); + $this->assertEquals('MyNewFolderClass', $dms->getClassname('folder')); + $this->assertEquals('MyNewFolderClass', $dms->setClassname('folder', 'MySuperNewFolderClass')); + $this->assertFalse($dms->setClassname('foo', 'MyNewFolderClass')); + } + + /** + * Test addCallback() + * + * @return void + */ + public function testAddCallback() + { + /* Do not mess up the global instance self::$dms, but create my own */ + $dms = new SeedDMS_Core_DMS(null, ''); + /* Add a closure as a callback is just fine */ + $this->assertTrue( + $dms->addCallback( + 'onPostSomething', function () { + } + ) + ); + /* An empty callback will make addCallback() fail */ + $this->assertFalse( + $dms->addCallback( + '', function () { + } + ) + ); + /* Passing a class method is ok */ + $this->assertTrue($dms->addCallback('onPostSomething', 'DmsTest::testAddCallback')); + /* Passing a none existing class mehtod makes addCallback() fail */ + $this->assertFalse($dms->addCallback('onPostSomething', 'DmsTest::thisMethodDoesNotExist')); + } + + /** + * Test for hasCallback + * + * @return void + */ + public function testHasCallback() + { + /* Do not mess up the global instance self::$dms, but create my own */ + $dms = new SeedDMS_Core_DMS(null, ''); + /* Add a closure as a callback is just fine */ + $this->assertTrue( + $dms->addCallback( + 'onPostSomething', function () { + } + ) + ); + $this->assertTrue($dms->hasCallback('onPostSomething')); + $this->assertFalse($dms->hasCallback('thisOneDoesNotExist')); + } + + /** + * Test for getDecorators + * + * @return void + */ + public function testGetDecorators() + { + /* Do not mess up the global instance self::$dms, but create my own */ + $dms = new SeedDMS_Core_DMS(null, ''); + $this->assertFalse($dms->getDecorators('folder')); + } + + /** + * Test for addDecorator + * + * @return void + */ + public function testaddDecorator() + { + /* Do not mess up the global instance self::$dms, but create my own */ + $dms = new SeedDMS_Core_DMS(null, ''); + $this->assertTrue($dms->addDecorator('folder', 'MyNewDecorator')); + $decorators = $dms->getDecorators('folder'); + $this->assertIsArray($decorators); + $this->assertCount(1, $decorators); + } + + /** + * Test getDb() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDb() + { + $this->assertEquals(self::$dbh, self::$dms->getDb()); + } + + /** + * Test getDBVersion() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDbVersion() + { + $version = self::$dms->getDBVersion(); + $this->assertCount(4, $version); + $this->assertGreaterThanOrEqual(5, $version['major']); + $this->assertGreaterThanOrEqual(0, $version['minor']); + } + + /** + * Test getDBVersionFailMissingTable() + * + * This method checks if getDBVersion() returns false if the table + * list of the database does not contain the table 'tblVersion' + * + * @return void + */ + public function testGetDbVersionFailMissingTable() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->getDBVersion(); + $this->assertFalse($version); + } + + /** + * Test getDBVersionSqlFail() + * + * This method checks if getDBVersion() returns false if the sql + * for selecting the records in table 'tblVersion' fail + * + * @return void + */ + public function testGetDbVersionSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1") + ->willReturn(false); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblVersion', 'tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->getDBVersion(); + $this->assertFalse($version); + } + + /** + * Test getDBVersionNoRecord() + * + * This method checks if getDBVersion() returns false a table 'tblVersion' + * exists but has no record + * + * @return void + */ + public function testGetDbVersionNoRecord() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1") + ->willReturn(array()); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblVersion', 'tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->getDBVersion(); + $this->assertFalse($version); + } + + /** + * Test checkVersion() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCheckVersion() + { + $this->assertTrue(self::$dms->checkVersion()); + } + + /** + * Test checkVersionFail() + * + * This method checks if checkVersion() returns false if the version + * in table 'tblVersion' does not match the version in the class variable + * $version. To make this method independant of version changes, the + * current version is taken from SeedDMS_Core_DMS::version and modified + * in order to differ from the version stored in the database. + * + * @return void + */ + public function testcheckVersionFail() + { + $verstr = (new SeedDMS_Core_DMS(null, ''))->version; + $verarr = explode('.', $verstr); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1") + ->willReturn([['major'=>$verarr[0], 'minor'=>$verarr[1]+1]]); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblVersion', 'tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->checkVersion(); + $this->assertFalse($version); + } + + /** + * Test checkVersionSqlFail() + * + * This method checks if checkVersion() returns false if the sql + * for selecting the records in table 'tblVersion' fail + * + * @return void + */ + public function testcheckVersionSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1") + ->willReturn(false); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblVersion', 'tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->checkVersion(); + $this->assertFalse($version); + } + + /** + * Test checkVersionFailMissingTable() + * + * This method checks if checkVersion() returns false if the table + * list of the database does not contain the table 'tblVersion' + * + * @return void + */ + public function testCheckVersionFailMissingTable() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->checkVersion(); + $this->assertTrue($version); // A missing table tblVersion returns true! + } + + /** + * Test checkVersionNoRecord() + * + * This method checks if checkVersion() returns false a table 'tblVersion' + * exists but has no record + * + * @return void + */ + public function testCheckVersionNoRecord() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1") + ->willReturn(array()); + $db->expects($this->once()) + ->method('TableList') + ->willReturn(['tblVersion', 'tblFolders', 'tblDocuments']); + $dms = new SeedDMS_Core_DMS($db, ''); + $version = $dms->checkVersion(); + $this->assertFalse($version); + } + + /** + * Test setRootFolderID() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetRootFolderID() + { + /* Setting the same root folder is ok */ + $oldid = self::$dms->setRootFolderID(1); + $this->assertEquals(1, $oldid); + /* Setting a none existing root folder id will not change the root folder */ + $oldid = self::$dms->setRootFolderID(2); + $this->assertFalse($oldid); + /* Make sure the old root folder is still set */ + $rootfolder = self::$dms->getRootFolder(); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $this->assertEquals(1, $rootfolder->getId()); + } + + /** + * Test getRootFolder() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetRootFolder() + { + $rootfolder = self::$dms->getRootFolder(); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $this->assertEquals(1, $rootfolder->getId()); + } + + /** + * Test setUser() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetUser() + { + $user = self::$dms->getUser(1); + $olduser = self::$dms->setUser($user); // returns null because there is no old user + $this->assertNull($olduser); + $olduser = self::$dms->setUser($user); // second call will return the user set before + $this->assertIsObject($olduser); + $olduser = self::$dms->setUser(null); // old user is still an object + $this->assertIsObject($olduser); + $olduser = self::$dms->setUser(8); // invalid user + $this->assertFalse($olduser); + } + + /** + * Test getLoggedInUser() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetLoggedInUser() + { + $olduser = self::$dms->getLoggedInUser(); // initially this is set to null + $this->assertNull($olduser); + $user = self::$dms->getUser(1); + self::$dms->setUser($user); + $olduser = self::$dms->getLoggedInUser(); + $this->assertEquals($olduser->getId(), $user->getId()); + } + + /** + * Test getDocument() + * + * As there is currently no document, getDocument() must return null. + * If false was returned it would indicated an sql error. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocument() + { + $document = self::$dms->getDocument(1); + $this->assertNull($document); + } + + /** + * Test getDocumentsByUser() + * + * As there is currently no document, getDocumentsByUser() must return + * an empty array. + * If false was returned it would indicated an sql error. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsByUser() + { + $documents = self::$dms->getDocumentsByUser(self::$dms->getUser(1)); + $this->assertIsArray($documents); + $this->assertCount(0, $documents); + } + + /** + * Test getDocumentsLockedByUser() + * + * As there is currently no document, getDocumentsLockedByUser() must return + * an empty array. + * If false was returned it would indicated an sql error. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsLockedByUser() + { + $documents = self::$dms->getDocumentsLockedByUser(self::$dms->getUser(1)); + $this->assertIsArray($documents); + $this->assertCount(0, $documents); + } + + /** + * Test makeTimeStamp() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testMakeTimeStamp() + { + /* Assert correct date */ + $this->assertEquals(0, self::$dms->makeTimeStamp(1, 0, 0, 1970, 1, 1)); + $this->assertEquals(68166000, self::$dms->makeTimeStamp(0, 0, 0, 1972, 2, 29)); + /* Assert incorrect dates */ + $this->assertFalse(self::$dms->makeTimeStamp(0, 0, 0, 1970, 13, 1), 'Incorrect month not recognized'); + $this->assertFalse(self::$dms->makeTimeStamp(0, 0, 0, 1970, 1, 32), 'Incorrect day in january not recognized'); + $this->assertFalse(self::$dms->makeTimeStamp(0, 0, 0, 1970, 4, 31), 'Incorrect day in april not recognized'); + $this->assertFalse(self::$dms->makeTimeStamp(0, 0, 0, 1970, 2, 29), 'Incorrect day in february not recognized'); + $this->assertFalse(self::$dms->makeTimeStamp(24, 0, 0, 1970, 1, 1), 'Incorrect hour not recognized'); + $this->assertFalse(self::$dms->makeTimeStamp(0, 60, 0, 1970, 1, 1), 'Incorrect minute not recognized'); + $this->assertFalse(self::$dms->makeTimeStamp(0, 0, 60, 1970, 1, 1), 'Incorrect second not recognized'); + } + + /** + * Test search() + * + * Just search the root folder in different ways. Because the initial database + * does not have any documents, this method will test various ways to + * find the root folder 'DMS' with id=1 + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSearchRootFolder() + { + /* searching for folders/documents in any field */ + $result = self::$dms->search( + array( + 'query'=>'DMS' + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + $this->assertEquals(0, $result['totalDocs']); + $this->assertCount(0, $result['docs']); + + /* searching for folders in any field */ + $result = self::$dms->search( + array( + 'query'=>'DMS', + 'mode'=>0x2 + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + $this->assertEquals(0, $result['totalDocs']); + $this->assertCount(0, $result['docs']); + + /* searching for documents in any field will not return any folders*/ + $result = self::$dms->search( + array( + 'query'=>'DMS', + 'mode'=>0x1 + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + $this->assertEquals(0, $result['totalDocs']); + $this->assertCount(0, $result['docs']); + + /* searching for folders with a bogus name may not return any folders */ + $result = self::$dms->search( + array( + 'query'=>'foo', + 'mode'=>0x2 + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + + /* searching for folders by its id */ + $result = self::$dms->search( + array( + 'query'=>'1', + 'mode'=>0x2 + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + + /* searching for folders by an unknown id */ + $result = self::$dms->search( + array( + 'query'=>'2', + 'mode'=>0x2 + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + + /* searching for folders with two terms ANDed, but only one matches */ + $result = self::$dms->search( + array( + 'query'=>'DMS foo', + 'mode'=>0x2, + 'logicalmode'=>'AND', + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + + /* searching for folders with two terms ORed, but only one matches */ + $result = self::$dms->search( + array( + 'query'=>'DMS foo', + 'mode'=>0x2, + 'logicalmode'=>'OR', + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + + /* searching for folders with two terms ANDed, both match, but in different fields (name and id) */ + $result = self::$dms->search( + array( + 'query'=>'DMS 1', + 'mode'=>0x2, + 'logicalmode'=>'AND', + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + + /* searching for folders with two terms ANDed, both match, but in different fields (name and id). But only one field is searched. */ + $result = self::$dms->search( + array( + 'query'=>'DMS 1', + 'mode'=>0x2, + 'logicalmode'=>'AND', + 'searchin'=>array(2,3), // name, comment + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + + /* searching for folders below a start folder will not find the folder 'DMS' + * anymore, because the start folder itself will not be found. + */ + $result = self::$dms->search( + array( + 'query'=>'DMS', + 'mode'=>0x2, + 'startFolder'=>self::$dms->getRootFolder() + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + + /* Restrict search to the owner of the folder 'DMS' + */ + $result = self::$dms->search( + array( + 'query'=>'DMS', + 'mode'=>0x2, + 'owner'=>self::$dms->getUser(1) + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + + /* Restrict search to user who does not own a document + */ + $result = self::$dms->search( + array( + 'query'=>'DMS', + 'mode'=>0x2, + 'owner'=>self::$dms->getUser(2) + ) + ); + $this->assertEquals(0, $result['totalFolders']); + $this->assertCount(0, $result['folders']); + + /* Restrict search to a list of owners (in this case all users) + */ + $result = self::$dms->search( + array( + 'query'=>'DMS', + 'mode'=>0x2, + 'owner'=>self::$dms->getAllUsers() + ) + ); + $this->assertEquals(1, $result['totalFolders']); + $this->assertCount(1, $result['folders']); + + } + + /** + * Test getFolder() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetFolder() + { + $folder = self::$dms->getFolder(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $folder); + $this->assertEquals(1, $folder->getId()); + } + + /** + * Test getFolderByName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetFolderByName() + { + $folder = self::$dms->getFolderByName('DMS'); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $folder); + $this->assertEquals(1, $folder->getId()); + $folder = self::$dms->getFolderByName('FOO'); + $this->assertNull($folder); + } + + /** + * Test checkFolders() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCheckFolders() + { + $errors = self::$dms->checkFolders(); + $this->assertIsArray($errors); + $this->assertCount(0, $errors); + } + + /** + * Test checkFoldersSqlFail() + * + * This test catches the case when the sql statement for getting all + * folders fails. + * + * @return void + */ + public function testCheckFoldersSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblFolders`") + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->checkFolders()); + } + + /** + * Test checkFoldersFailNoParent() + * + * This test catches the case when a folder's parent is not present + * + * @return void + */ + public function testCheckFoldersFailNoParent() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblFolders`") + ->willReturn( + array( + array('id'=>1, 'name'=>'DMS', 'parent'=>0, 'folderList'=>''), + array('id'=>5, 'name'=>'Subfolder', 'parent'=>3, 'folderList'=>':1:'), + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $errors = $dms->checkFolders(); + $this->assertIsArray($errors); + $this->assertCount(1, $errors); // there should be 1 error + $this->assertArrayHasKey(5, $errors); // folder with id=5 has the wrong parent + $this->assertEquals('Missing parent', $errors[5]['msg']); + } + + /** + * Test checkFoldersFailWrongFolderList() + * + * This test catches the case when a folder's parent is not present + * + * @return void + */ + public function testCheckFoldersFailWrongFolderList() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblFolders`") + ->willReturn( + array( + array('id'=>1, 'name'=>'DMS', 'parent'=>0, 'folderList'=>''), + array('id'=>5, 'name'=>'Subfolder', 'parent'=>1, 'folderList'=>':1:2:'), + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $errors = $dms->checkFolders(); + $this->assertIsArray($errors); + $this->assertCount(1, $errors); // there should be 1 error + $this->assertArrayHasKey(5, $errors); // folder with id=5 has the wrong parent + $this->assertStringContainsString('Wrong folder list', $errors[5]['msg']); + } + + /** + /** + * Test checkDocuments() + * + * The intitial database does not have any documents which makes this + * test less usefull. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCheckDocuments() + { + $errors = self::$dms->checkDocuments(); + $this->assertIsArray($errors); + $this->assertCount(0, $errors); + } + + /** + * Test checkDocumentsSqlFoldersFail() + * + * This test catches the case when the sql statement for getting all + * folders fails. + * + * @return void + */ + public function testCheckDocumentsSqlFoldersFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblFolders`") + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->checkDocuments()); + } + + /** + * Test checkDocumentsSqlDocumentsFail() + * + * This test catches the case when the sql statement for getting all + * documents fails, after getting all folders succeeded. + * + * @return void + */ + public function testCheckDocumentsSqlDocumentsFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResultArray') + ->will( + $this->returnValueMap( + array( + array("SELECT * FROM `tblFolders`", true, array( + array('id'=>1, 'name'=>'DMS', 'parent'=>0, 'folderList'=>'') + )), + array("SELECT * FROM `tblDocuments`", true, false) + ) + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->checkDocuments()); + } + + /** + * Test checkDocumentsFailNoParent() + * + * This test catches the case when a documents's parent is not present + * + * @return void + */ + public function testCheckDocumentsFailNoParent() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResultArray') + ->will( + $this->returnValueMap( + array( + array("SELECT * FROM `tblFolders`", true, array( + array('id'=>1, 'name'=>'DMS', 'parent'=>0, 'folderList'=>''), + array('id'=>5, 'name'=>'Subfolder', 'parent'=>1, 'folderList'=>':1:'), + )), + array("SELECT * FROM `tblDocuments`", true, array( + array('id'=>1, 'name'=>'Document 1', 'folder'=>1, 'folderList'=>':1:'), + array('id'=>2, 'name'=>'Document 2', 'folder'=>2, 'folderList'=>':1:5:'), + )) + ) + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $errors = $dms->checkDocuments(); + $this->assertIsArray($errors); + $this->assertCount(1, $errors); // there should be 1 error + $this->assertArrayHasKey(2, $errors); // document with id=2 has the wrong parent + $this->assertEquals('Missing parent', $errors[2]['msg']); + } + + /** + * Test checkDocumentsFailWrongFolderList() + * + * This test catches the case when a documents's parent is not present + * + * @return void + */ + public function testCheckDocumentsFailWrongFolderList() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResultArray') + ->will( + $this->returnValueMap( + array( + array("SELECT * FROM `tblFolders`", true, array( + array('id'=>1, 'name'=>'DMS', 'parent'=>0, 'folderList'=>''), + array('id'=>5, 'name'=>'Subfolder', 'parent'=>1, 'folderList'=>':1:'), + )), + array("SELECT * FROM `tblDocuments`", true, array( + array('id'=>1, 'name'=>'Document 1', 'folder'=>1, 'folderList'=>':1:'), + array('id'=>2, 'name'=>'Document 2', 'folder'=>5, 'folderList'=>':1:2:'), + )) + ) + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $errors = $dms->checkDocuments(); + $this->assertIsArray($errors); + $this->assertCount(1, $errors); // there should be 1 error + $this->assertArrayHasKey(2, $errors); // document with id=2 has the wrong parent + $this->assertStringContainsString('Wrong folder list', $errors[2]['msg']); + } + + /** + * Test getUser() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUser() + { + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_User::class, $user); + $this->assertEquals(1, $user->getId()); + } + + /** + * Test getUserByLogin() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUserByLogin() + { + $user = self::$dms->getUserByLogin('admin'); + $this->assertInstanceOf(SeedDMS_Core_User::class, $user); + $this->assertEquals('admin', $user->getLogin()); + } + + /** + * Test getUserByEmail() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUserByEmail() + { + $user = self::$dms->getUserByEmail('info@seeddms.org'); + $this->assertInstanceOf(SeedDMS_Core_User::class, $user); + $this->assertEquals('admin', $user->getLogin()); + } + + /** + * Test getAllUsers() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllUsers() + { + $users = self::$dms->getAllUsers(); + $this->assertIsArray($users); + $this->assertCount(2, $users); + } + + /** + * Test addUser() + * + * Add a new user and retrieve it afterwards. Also check if the number + * of users has increased by one. Add a user with the same name a + * second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddUser() + { + /* Adding a new user */ + $user = self::$dms->addUser('new user', 'pwd', 'Full Name', 'newuser@seeddms.org', 'en_GB', 'bootstrap', 'with comment'); + $this->assertIsObject($user); + $this->assertEquals('new user', $user->getLogin()); + $this->assertEquals('with comment', $user->getComment()); + + /* Adding a user with the same login must fail */ + $user = self::$dms->addUser('new user', 'pwd', 'Full Name', 'newuser@seeddms.org', 'en_GB', 'bootstrap', 'with comment'); + $this->assertFalse($user); + + /* There should be 3 users now */ + $users = self::$dms->getAllUsers(); + $this->assertIsArray($users); + $this->assertCount(3, $users); + + /* Check if setting the password expiration to 'now' works */ + $now = date('Y-m-d H:i:s'); + $user = self::$dms->addUser('new user pwdexpiration 1', 'pwd', 'Full Name', 'newuser@seeddms.org', 'en_GB', 'bootstrap', 'with comment', '', false, false, 'now'); + $this->assertEquals($now, $user->getPwdExpiration()); + $now = date('Y-m-d H:i:s'); + $user = self::$dms->addUser('new user pwdexpiration 2', 'pwd', 'Full Name', 'newuser@seeddms.org', 'en_GB', 'bootstrap', 'with comment', '', false, false, $now); + $this->assertEquals($now, $user->getPwdExpiration()); + } + + /** + * Test addUserWithPostAddHook() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddUserWithPostAddHook() + { + /* Add the 'onPostAddUser' callback */ + $ret = 0; + $callback = function ($param, $user) use (&$ret) { + $ret = 1; + }; + self::$dms->addCallback('onPostAddUser', $callback, 1); + /* Adding a new user */ + $user = self::$dms->addUser('new user', 'pwd', 'Full Name', 'newuser@seeddms.org', 'en_GB', 'bootstrap', 'with comment'); + $this->assertIsObject($user); + $this->assertEquals('new user', $user->getLogin()); + $this->assertEquals(1, $ret); + } + + /** + * Test addUser() with sql failure + * + * @return void + */ + public function testAddUserSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("INSERT INTO `tblUsers`")) + ->willReturn(false); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblUsers` WHERE `login` = ") + ->willReturn([]); + $dms = new SeedDMS_Core_DMS($db, ''); + if(self::$dbversion['major'] < 6) + $role = 1; + else + $role = $this->getAdminRole(); + $user = $dms->addUser('new user', 'pwd', 'Full Name', 'newuser@seeddms.org', 'en_GB', 'bootstrap', 'with comment', $role); + $this->assertFalse($user); + } + + /** + * Test getGroup() + * + * Get a group by its id + * + * @return void + */ + public function testGetGroup() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblGroups` WHERE `id` = 1") + ->willReturn([['id'=>1, 'name'=>'foo', 'comment'=>'']]); + $dms = new SeedDMS_Core_DMS($db, ''); + $group = $dms->getGroup(1); + $this->assertIsObject($group); + $this->assertEquals(1, $group->getId()); + } + + /** + * Test getGroupByName() + * + * Get a group by its name + * + * qstr must be mocked because it is used in the sql statement to quote + * the name. + * + * @return void + */ + public function testGetGroupByName() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblGroups` WHERE `name` = 'foo'") + ->willReturn([['id'=>1, 'name'=>'foo', 'comment'=>'']]); + $db->expects($this->once()) + ->method('qstr') + ->will( + $this->returnCallback( + function ($a) { + return "'".$a."'"; + } + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $group = $dms->getGroupByName('foo'); + $this->assertIsObject($group); + $this->assertEquals('foo', $group->getName()); + } + + /** + * Test getAllGroups() + * + * The intitial database does not have any groups + * + * @return void + */ + public function testGetAllGroups() + { + $groups = self::$dms->getAllGroups(); + $this->assertIsArray($groups); + $this->assertCount(0, $groups); + } + + /** + * Test addGroup() + * + * Add a new group and retrieve it afterwards. Also check if the number + * of groups has increased by one. Add a group with the same name a + * second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddGroup() + { + /* Adding a new group */ + $group = self::$dms->addGroup('new group', 'with comment'); + $this->assertIsObject($group); + $this->assertEquals('new group', $group->getName()); + /* Adding a group with the same name must fail */ + $group = self::$dms->addGroup('new group', 'with comment'); + $this->assertFalse($group); + /* There should be one group now */ + $groups = self::$dms->getAllGroups(); + $this->assertIsArray($groups); + $this->assertCount(1, $groups); + } + + /** + * Test addGroupWithPostAddHook() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddGroupWithPostAddHook() + { + /* Add the 'onPostAddGroup' callback */ + $ret = 0; + $callback = function ($param, $group) use (&$ret) { + $ret = 1; + }; + self::$dms->addCallback('onPostAddGroup', $callback, 1); + /* Adding a new group */ + $group = self::$dms->addGroup('new group', 'with comment'); + $this->assertIsObject($group); + $this->assertEquals('new group', $group->getName()); + $this->assertEquals(1, $ret); + } + + /** + * Test addGroup() with sql failure + * + * @return void + */ + public function testAddGroupSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("INSERT INTO `tblGroups`")) + ->willReturn(false); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblGroups` WHERE `name` = ") + ->willReturn([]); + $dms = new SeedDMS_Core_DMS($db, ''); + $group = $dms->addGroup('new group', 'with comment'); + $this->assertFalse($group); + } + + /** + * Test getAllKeywordCategories() + * + * The intitial database does not have any keyword categories + * + * @return void + */ + public function testGetAllKeywordCategories() + { + $cats = self::$dms->getAllKeywordCategories(); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + /* Even passing bogus ids is handled propperly */ + $cats = self::$dms->getAllKeywordCategories(['kk', '0', 3, true]); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + } + + /** + * Test getAllUserKeywordCategories() + * + * Method getAllUserKeywordCategories() actually uses + * getAllKeywordCategories() + * + * The intitial database does not have any keyword categories + * + * @return void + */ + public function testGetAllUserKeywordCategories() + { + $cats = self::$dms->getAllUserKeywordCategories(1); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + /* Passing a none existing user id will return an empty array */ + $cats = self::$dms->getAllUserKeywordCategories(3); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + /* Passing an invalid user id will return false */ + $cats = self::$dms->getAllUserKeywordCategories(0); + $this->assertFalse($cats); + } + + /** + * Test getAllKeywordCategories() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllKeywordCategoriesSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblKeywordCategories`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $cats = $dms->getAllKeywordCategories(); + $this->assertFalse($cats); + } + + /** + * Test addKeywordCategory() + * + * Add a new keyword category and retrieve it afterwards. Also check if the + * number of keyword categories has increased by one. Add a keyword category + * with the same name a second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddKeywordCategory() + { + /* Adding a new keyword category */ + $cat = self::$dms->addKeywordCategory(1, 'new category'); + $this->assertIsObject($cat); + $this->assertEquals('new category', $cat->getName()); + + /* Adding a keyword category for the same user and with the same name must fail */ + $cat = self::$dms->addKeywordCategory(1, 'new category'); + $this->assertFalse($cat); + + /* Adding a keyword category with a non existing user id must fail */ + $cat = self::$dms->addKeywordCategory(0, 'new category'); + $this->assertFalse($cat); + + /* Adding a keyword category with an empty name must fail */ + $cat = self::$dms->addKeywordCategory(1, ' '); + $this->assertFalse($cat); + + /* Adding a keyword category with a non existing user id must fail */ + // $cat = self::$dms->addKeywordCategory(3, 'new category'); + // $this->assertFalse($cat); + + /* There should be 1 keyword category now */ + $cats = self::$dms->getAllKeywordCategories(); + $this->assertIsArray($cats); + $this->assertCount(1, $cats); + } + + /** + * Test addKeywordCategoryWithPostAddHook() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddKeywordCategoryWithPostAddHook() + { + /* Add the 'onPostAddKeywordCategory' callback */ + $ret = 0; + $callback = function ($param, $cat) use (&$ret) { + $ret = 1; + }; + self::$dms->addCallback('onPostAddKeywordCategory', $callback, 1); + /* Adding a new keyword category */ + $cat = self::$dms->addKeywordCategory(1, 'new category'); + $this->assertIsObject($cat); + $this->assertEquals('new category', $cat->getName()); + $this->assertEquals(1, $ret); + } + + /** + * Test addKeywordCategory() with sql failure + * + * @return void + */ + public function testAddKeywordCategorySqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("INSERT INTO `tblKeywordCategories`")) + ->willReturn(false); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblKeywordCategories` WHERE `name` =")) + ->willReturn([]); + $dms = new SeedDMS_Core_DMS($db, ''); + $cat = $dms->addKeywordCategory(1, 'new category'); + $this->assertFalse($cat); + } + + /** + * Test getKeywordCategory() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetKeywordCategory() + { + $cat = self::$dms->addKeywordCategory(1, 'new category'); + $cat = self::$dms->getKeywordCategory(1); + $this->assertInstanceOf(SeedDMS_Core_Keywordcategory::class, $cat); + $this->assertEquals(1, $cat->getId()); + /* Return false if the id is invalid */ + $cat = self::$dms->getKeywordCategory(0); + $this->assertFalse($cat); + /* Return null if the keyword category with the id does not exist */ + $cat = self::$dms->getKeywordCategory(2); + $this->assertNull($cat); + } + + /** + * Test getKeywordCategory() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetKeywordCategorySqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblKeywordCategories` WHERE `id` = 1") + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $cat = $dms->getKeywordCategory(1); + $this->assertFalse($cat); + } + + /** + * Test getKeywordCategoryByName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetKeywordCategoryByName() + { + $cat = self::$dms->addKeywordCategory(1, 'new category'); + $cat = self::$dms->getKeywordCategory(1); + $this->assertInstanceOf(SeedDMS_Core_Keywordcategory::class, $cat); + $this->assertEquals(1, $cat->getId()); + /* Return false if the user id is invalid */ + $cat = self::$dms->getKeywordCategoryByName('new category', 0); + $this->assertFalse($cat); + /* Return null if the keyword category with the passed name does not exist */ + $cat = self::$dms->getKeywordCategoryByName('foo', 1); + $this->assertNull($cat); + /* Return category if the keyword category with the passed name exists */ + $cat = self::$dms->getKeywordCategoryByName('new category', 1); + $this->assertIsObject($cat); + $this->assertInstanceOf(SeedDMS_Core_Keywordcategory::class, $cat); + } + + /** + * Test getKeywordCategoryByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetKeywordCategoryByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblKeywordCategories` WHERE `name` =")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $cat = $dms->getKeywordCategoryByName('foo', 1); + $this->assertFalse($cat); + } + + /** + * Test getDocumentCategories() + * + * The intitial database does not have any document categories + * + * @return void + */ + public function testGetDocumentCategories() + { + $cats = self::$dms->getDocumentCategories(); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + } + + /** + * Test getDocumentCategories() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentCategoriesNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblCategory`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $cats = $dms->getDocumentCategories(); + $this->assertFalse($cats); + } + + /** + * Test getDocumentCategory() + * + * The intitial database does not have any document categories + * + * @return void + */ + public function testGetDocumentCategory() + { + /* Adding a new keyword category */ + $cat = self::$dms->addDocumentCategory('new category'); + $this->assertIsObject($cat); + $this->assertEquals('new category', $cat->getName()); + + $cat = self::$dms->getDocumentCategory($cat->getId()); + $this->assertIsObject($cat); + + /* Return false if the id is out of range */ + $cat = self::$dms->getDocumentCategory(0); + $this->assertFalse($cat); + + /* Return null if the keyword category with the id does not exist */ + $cat = self::$dms->getDocumentCategory(2); + $this->assertNull($cat); + } + + /** + * Test getDocumentCategory() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentCategorySqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with("SELECT * FROM `tblCategory` WHERE `id` = 1") + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $cat = $dms->getDocumentCategory(1); + $this->assertFalse($cat); + } + + /** + * Test getDocumentCategoryByName() + * + * The intitial database does not have any document categories + * + * @return void + */ + public function testGetDocumentCategoryByName() + { + /* Adding a new keyword category with leading and trailing spaces*/ + $cat = self::$dms->addDocumentCategory(' new category '); + $this->assertIsObject($cat); + $this->assertEquals('new category', $cat->getName()); + + $cat = self::$dms->getDocumentCategoryByName($cat->getName()); + $this->assertIsObject($cat); + + $cat = self::$dms->getDocumentCategoryByName(' '); + $this->assertFalse($cat); + } + + /** + * Test getDocumentCategoryByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentCategoryByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblCategory` WHERE `name`=")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $cat = $dms->getDocumentCategoryByName('foo'); + $this->assertFalse($cat); + } + + /** + * Test addDocumentCategory() + * + * Add a new keyword category and retrieve it afterwards. Also check if the + * number of keyword categories has increased by one. Add a keyword category + * with the same name a second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddDocumentCategory() + { + /* Adding a new keyword category */ + $cat = self::$dms->addDocumentCategory('new category'); + $this->assertIsObject($cat); + $this->assertEquals('new category', $cat->getName()); + + /* Adding a document category with the same name must fail */ + $cat = self::$dms->addDocumentCategory('new category'); + $this->assertFalse($cat); + + /* Adding a document category with an empty name must fail */ + $cat = self::$dms->addDocumentCategory(' '); + $this->assertFalse($cat); + + /* There should be 1 document category now */ + $cats = self::$dms->getDocumentCategories(); + $this->assertIsArray($cats); + $this->assertCount(1, $cats); + } + + /** + * Test addDocumentCategoryWithPostAddHook() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddDocumentCategoryWithPostAddHook() + { + /* Add the 'onPostAddDocumentCategory' callback */ + $ret = 0; + $callback = function ($param, $group) use (&$ret) { + $ret = 1; + }; + self::$dms->addCallback('onPostAddDocumentCategory', $callback, 1); + /* Adding a new group */ + $cat = self::$dms->addDocumentCategory('new category'); + $this->assertIsObject($cat); + $this->assertEquals('new category', $cat->getName()); + $this->assertEquals(1, $ret); + } + + /** + * Test addDocumentCategory() with sql failure + * + * @return void + */ + public function testAddDocumentCategorySqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("INSERT INTO `tblCategory`")) + ->willReturn(false); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblCategory` WHERE `name`=")) + ->willReturn([]); + $dms = new SeedDMS_Core_DMS($db, ''); + $cat = $dms->addDocumentCategory('new category'); + $this->assertFalse($cat); + } + + /** + * Test getAttributeDefinition() with a none existing workflow + * + * The intitial database does not have any workflows + * + * @return void + */ + public function testGetAttributeDefinitionNoExists() + { + $workflow = self::$dms->getAttributeDefinition(1); + $this->assertNull($workflow); + /* Passing an id not a numeric value returns false */ + $workflow = self::$dms->getAttributeDefinition('foo'); + $this->assertFalse($workflow); + /* Passing an id out of range returns false */ + $workflow = self::$dms->getAttributeDefinition(0); + $this->assertFalse($workflow); + } + + /** + * Test getAttributeDefinition() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAttributeDefinitionSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblAttributeDefinitions` WHERE `id` =")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $attrdef = $dms->getAttributeDefinition(1); + $this->assertFalse($attrdef); + } + + /** + * Test getAttributeDefinitionByName() with a none existing workflow + * + * The intitial database does not have any workflows + * + * @return void + */ + public function testGetAttributeDefinitionByNameNoExists() + { + $workflow = self::$dms->getAttributeDefinitionByName('foo'); + $this->assertNull($workflow); + } + + /** + * Test getAttributeDefinitionByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAttributeDefinitionByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblAttributeDefinitions` WHERE `name` =")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $attrdef = $dms->getAttributeDefinitionByName('foo'); + $this->assertFalse($attrdef); + } + + /** + * Test getAllAttributeDefinitions() + * + * The intitial database does not have any attribute definitions + * + * @return void + */ + public function testGetAllAttributeDefinitions() + { + $attrdefs = self::$dms->getAllAttributeDefinitions(); + $this->assertIsArray($attrdefs); + $this->assertCount(0, $attrdefs); + } + + /** + * Test getAllAttributeDefinitions() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllAttributeDefinitionsSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblAttributeDefinitions`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $attrdef = $dms->getAllAttributeDefinitions(); + $this->assertFalse($attrdef); + } + + /** + * Test addAttributeDefinition() + * + * Add a new group and retrieve it afterwards. Also check if the number + * of groups has increased by one. Add a group with the same name a + * second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAttributeDefinition() + { + /* Adding a new attribute definition */ + $attrdef = self::$dms->addAttributeDefinition('new attribute definition', SeedDMS_Core_AttributeDefinition::objtype_folder, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertIsObject($attrdef); + $this->assertEquals('new attribute definition', $attrdef->getName()); + /* Get the new attribute definition by its id */ + $newattrdef = self::$dms->getAttributeDefinition($attrdef->getId()); + $this->assertIsObject($newattrdef); + $this->assertEquals($attrdef->getId(), $newattrdef->getId()); + /* Get the new attribute definition by its name */ + $newattrdef = self::$dms->getAttributeDefinitionByName('new attribute definition'); + $this->assertIsObject($newattrdef); + $this->assertEquals($attrdef->getId(), $newattrdef->getId()); + /* Adding an attribute definition with the same name must fail */ + $attrdef = self::$dms->addAttributeDefinition('new attribute definition', SeedDMS_Core_AttributeDefinition::objtype_folder, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertFalse($attrdef); + /* Adding an attribute definition with an empty name must fail */ + $attrdef = self::$dms->addAttributeDefinition(' ', SeedDMS_Core_AttributeDefinition::objtype_folder, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertFalse($attrdef); + /* Adding an attribute definition with an invalid object type must fail */ + $attrdef = self::$dms->addAttributeDefinition('no object type', -1, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertFalse($attrdef); + /* Adding an attribute definition without a type must fail */ + $attrdef = self::$dms->addAttributeDefinition('no type', SeedDMS_Core_AttributeDefinition::objtype_folder, 0, 0, '', ''); + $this->assertFalse($attrdef); + /* There should be one attribute definition now */ + $attrdefs = self::$dms->getAllAttributeDefinitions(); + $this->assertIsArray($attrdefs); + $this->assertCount(1, $attrdefs); + /* There should be one attribute definition of object type folder now */ + $attrdefs = self::$dms->getAllAttributeDefinitions(SeedDMS_Core_AttributeDefinition::objtype_folder); + $this->assertIsArray($attrdefs); + $this->assertCount(1, $attrdefs); + /* The object type can also be passed as an array */ + $attrdefs = self::$dms->getAllAttributeDefinitions([SeedDMS_Core_AttributeDefinition::objtype_folder]); + $this->assertIsArray($attrdefs); + $this->assertCount(1, $attrdefs); + /* Adding more attribute definitions of different object type */ + $attrdef = self::$dms->addAttributeDefinition('new attribute definition all', SeedDMS_Core_AttributeDefinition::objtype_all, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertIsObject($attrdef); + $this->assertEquals('new attribute definition all', $attrdef->getName()); + $attrdef = self::$dms->addAttributeDefinition('new attribute definition document', SeedDMS_Core_AttributeDefinition::objtype_all, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertIsObject($attrdef); + $this->assertEquals('new attribute definition document', $attrdef->getName()); + $attrdef = self::$dms->addAttributeDefinition('new attribute definition documentcontent', SeedDMS_Core_AttributeDefinition::objtype_all, SeedDMS_Core_AttributeDefinition::type_int, false, 0, 0, '', ''); + $this->assertIsObject($attrdef); + $this->assertEquals('new attribute definition documentcontent', $attrdef->getName()); + /* There should be four attribute definitions now */ + $attrdefs = self::$dms->getAllAttributeDefinitions(); + $this->assertIsArray($attrdefs); + $this->assertCount(4, $attrdefs); + } + + /** + * Test getAllWorkflows() + * + * The intitial database does not have any workflows + * + * @return void + */ + public function testGetAllWorkflows() + { + $workflows = self::$dms->getAllWorkflows(); + $this->assertIsArray($workflows); + $this->assertCount(0, $workflows); + } + + /** + * Test getAllWorkflows() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllWorkflowsSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflows`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $workflows = $dms->getAllWorkflows(); + $this->assertFalse($workflows); + } + + /** + * Test getWorkflow() with a none existing workflow + * + * The intitial database does not have any workflows + * + * @return void + */ + public function testGetWorkflowNoExists() + { + $workflow = self::$dms->getWorkflow(1); + $this->assertNull($workflow); + /* Passing an id not a numeric value returns false */ + $workflow = self::$dms->getWorkflow('foo'); + $this->assertFalse($workflow); + /* Passing an id out of range returns false */ + $workflow = self::$dms->getWorkflow(0); + $this->assertFalse($workflow); + } + + /** + * Test getWorkflow() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflows` WHERE `id`=")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $workflow = $dms->getWorkflow(1); + $this->assertFalse($workflow); + } + + /** + * Test getWorkflowByName() with a none existing workflow + * + * The intitial database does not have any workflows + * + * @return void + */ + public function testGetWorkflowByNameNoExists() + { + $workflow = self::$dms->getWorkflowByName('foo'); + $this->assertNull($workflow); + } + + /** + * Test getWorkflowByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflows` WHERE `name`=")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $workflow = $dms->getWorkflowByName('foo'); + $this->assertFalse($workflow); + } + + /** + * Test addWorkflow() + * + * Add a new workflow and retrieve it afterwards. Also check if the number + * of workflows has increased by one. Add a workflow with the same name a + * second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddWorkflow() + { + /* Adding a new workflow */ + $workflowstate = self::$dms->addWorkflowState('new workflow state', S_RELEASED); + $workflow = self::$dms->addWorkflow('new workflow', $workflowstate); + $this->assertIsObject($workflow); + $this->assertEquals('new workflow', $workflow->getName()); + /* Adding a workflow with the same name must fail */ + $workflow = self::$dms->addWorkflow('new workflow', $workflowstate); + $this->assertFalse($workflow); + /* Adding a workflow with an empty name must fail */ + $workflow = self::$dms->addWorkflow(' ', $workflowstate); + $this->assertFalse($workflow); + /* There should be one workflow now */ + $workflows = self::$dms->getAllWorkflows(); + $this->assertIsArray($workflows); + $this->assertCount(1, $workflows); + } + + /** + * Test getAllWorkflowStates() + * + * The intitial database does not have any workflow states + * + * @return void + */ + public function testGetAllWorkflowStates() + { + $states = self::$dms->getAllWorkflowStates(); + $this->assertIsArray($states); + $this->assertCount(0, $states); + } + + /** + * Test getAllWorkflowStates() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllWorkflowStatesSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflowStates`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $states = $dms->getAllWorkflowStates(); + $this->assertFalse($states); + } + + /** + * Test getWorkflowState() with a none existing workflow state + * + * The intitial database does not have any workflow states + * + * @return void + */ + public function testGetWorkflowStateNoExists() + { + $workflowstate = self::$dms->getWorkflowState(1); + $this->assertNull($workflowstate); + /* Passing an id not a numeric value returns false */ + $workflowstate = self::$dms->getWorkflowState('foo'); + $this->assertFalse($workflowstate); + /* Passing an id out of range returns false */ + $workflowstate = self::$dms->getWorkflowState(0); + $this->assertFalse($workflowstate); + } + + /** + * Test getWorkflowState() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowStateSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflowStates` WHERE `id` =")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $state = $dms->getWorkflowState(1); + $this->assertFalse($state); + } + + /** + * Test getWorkflowStateByName() with a none existing workflow state + * + * The intitial database does not have any workflow states + * + * @return void + */ + public function testGetWorkflowStateByNameNoExists() + { + $workflowstate = self::$dms->getWorkflowStateByName('foo'); + $this->assertNull($workflowstate); + } + + /** + * Test getWorkflowStateByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowStateByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflowStates` WHERE `name`=")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $state = $dms->getWorkflowStateByName('foo'); + $this->assertFalse($state); + } + + /** + * Test addWorkflowState() + * + * Add a new workflow state and retrieve it afterwards. Also check if the number + * of workflow states has increased by one. Add a workflow state with the same name a + * second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddWorkflowState() + { + /* Adding a new workflow state */ + $workflowstate = self::$dms->addWorkflowState('new workflow state', S_RELEASED); + $this->assertIsObject($workflowstate); + $this->assertEquals('new workflow state', $workflowstate->getName()); + /* Adding a workflow state with the same name must fail */ + $workflowstate = self::$dms->addWorkflowState('new workflow state', S_RELEASED); + $this->assertFalse($workflowstate); + /* Adding a workflow state with an empty name must fail */ + $workflowstate = self::$dms->addWorkflowState(' ', S_RELEASED); + $this->assertFalse($workflowstate); + /* There should be one workflow state now */ + $workflowstates = self::$dms->getAllWorkflowStates(); + $this->assertIsArray($workflowstates); + $this->assertCount(1, $workflowstates); + } + + /** + * Test getAllWorkflowActions() + * + * The intitial database does not have any workflow actions + * + * @return void + */ + public function testGetAllWorkflowActions() + { + $actions = self::$dms->getAllWorkflowActions(); + $this->assertIsArray($actions); + $this->assertCount(0, $actions); + } + + /** + * Test getAllWorkflowActions() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllWorkflowActionsSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflowActions`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $actions = $dms->getAllWorkflowActions(); + $this->assertFalse($actions); + } + + /** + * Test getWorkflowAction() with a none existing workflow + * + * The intitial database does not have any workflow actions + * + * @return void + */ + public function testGetWorkflowActionNoExists() + { + $workflowaction = self::$dms->getWorkflowAction(1); + $this->assertNull($workflowaction); + /* Passing an id not a numeric value returns false */ + $workflowaction = self::$dms->getWorkflowAction('foo'); + $this->assertFalse($workflowaction); + /* Passing an id out of range returns false */ + $workflowaction = self::$dms->getWorkflowAction(0); + $this->assertFalse($workflowaction); + } + + /** + * Test getWorkflowAction() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowActionSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflowActions` WHERE `id` =")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $action = $dms->getWorkflowAction(1); + $this->assertFalse($action); + } + + /** + * Test getWorkflowActionByName() with a none existing workflow action + * + * The intitial database does not have any workflow actions + * + * @return void + */ + public function testGetWorkflowActionByNameNoExists() + { + $workflowaction = self::$dms->getWorkflowActionByName('foo'); + $this->assertNull($workflowaction); + } + + /** + * Test getWorkflowActionByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowActionByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblWorkflowActions` WHERE `name` =")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $action = $dms->getWorkflowActionByName('foo'); + $this->assertFalse($action); + } + + /** + * Test addWorkflowAction() + * + * Add a new workflow state and retrieve it afterwards. Also check if the number + * of workflow states has increased by one. Add a workflow state with the same name a + * second time and check if it returns false. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddWorkflowAction() + { + /* Adding a new workflow action */ + $workflowaction = self::$dms->addWorkflowAction('new workflow action', S_RELEASED); + $this->assertIsObject($workflowaction); + $this->assertEquals('new workflow action', $workflowaction->getName()); + /* Adding a workflow action with the same name must fail */ + $workflowaction = self::$dms->addWorkflowAction('new workflow action', S_RELEASED); + $this->assertFalse($workflowaction); + /* Adding a workflow action with an empty name must fail */ + $workflowaction = self::$dms->addWorkflowAction(' ', S_RELEASED); + $this->assertFalse($workflowaction); + /* There should be one workflow action now */ + $workflowactions = self::$dms->getAllWorkflowActions(); + $this->assertIsArray($workflowactions); + $this->assertCount(1, $workflowactions); + } + + /** + * Test getStatisticalData() + * + * @return void + */ + public function testGetStatisticalData() + { + /* There should one folder (root folder) */ + $data = self::$dms->getStatisticalData('foldersperuser'); + $this->assertIsArray($data); + $this->assertEquals(1, $data[0]['total']); + /* There should be no documents */ + foreach (array('docsperuser', 'docspermimetype', 'docspercategory', 'docspermonth', 'docsperstatus', 'docsaccumulated', 'sizeperuser') as $type) { + $data = self::$dms->getStatisticalData($type); + $this->assertIsArray($data); + $this->assertCount(0, $data); + } + /* Passing an unknown name returns an empty array */ + $data = self::$dms->getStatisticalData('foo'); + $this->assertIsArray($data); + $this->assertCount(0, $data); + } + + /** + * Test getStatisticalDataFail() + * + * Check if getStatisticalData() fails if the sql statements fail + * + * @return void + */ + public function testGetStatisticalDataFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->any()) + ->method('getResultArray') + ->with($this->stringContains("SELECT ")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + foreach (array('foldersperuser', 'docsperuser', 'docspermimetype', 'docspercategory', 'docspermonth', 'docsperstatus', 'docsaccumulated', 'sizeperuser') as $type) { + $data = $dms->getStatisticalData($type); + $this->assertFalse($data); + } + } + + /** + * Test createPasswordRequest() and checkPasswordRequest() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCreateAndCheckAndDeletePasswordRequest() + { + $user = self::$dms->getUser(1); + $hash = self::$dms->createPasswordRequest($user); + $this->assertIsString($hash); + $user = self::$dms->checkPasswordRequest($hash); + $this->assertIsObject($user); + $this->assertEquals(1, $user->getId()); + /* Check a non existing hash */ + $user = self::$dms->checkPasswordRequest('foo'); + $this->assertFalse($user); + /* Delete the hash */ + $ret = self::$dms->deletePasswordRequest($hash); + $this->assertTrue($ret); + /* Checking the hash again must return false, because it was deleted */ + $user = self::$dms->checkPasswordRequest($hash); + $this->assertFalse($user); + } + + /** + * Test method checkPasswordRequest() with sql failure + * + * @return void + */ + public function testCheckPasswordRequestSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblUserPasswordRequest`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->checkPasswordRequest('foo')); + } + + /** + * Test method deletePasswordRequest() with sql failure + * + * @return void + */ + public function testDeletePasswordRequestSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("DELETE FROM `tblUserPasswordRequest`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->deletePasswordRequest('foo')); + } + + /** + * Test getTimeline() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetTimeline() + { + $timeline = self::$dms->getTimeline(); + $this->assertIsArray($timeline); + } + + /** + * Test getUnlinkedDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUnlinkedDocumentContent() + { + $contents = self::$dms->getUnlinkedDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(0, $contents); + } + + /** + * Test getNoFileSizeDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNoFileSizeDocumentContent() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $contents = self::$dms->getNoFileSizeDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(0, $contents); + /* Manipulate the file size right in the database */ + $dbh = self::$dms->getDB(); + $ret = $dbh->getResult("UPDATE `tblDocumentContent` SET `fileSize` = 0"); + $this->assertTrue($ret); + $contents = self::$dms->getNoFileSizeDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(1, $contents); + } + + /** + * Test getNoFileSizeDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNoFileSizeDocumentContentSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblDocumentContent` WHERE `fileSize`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->getNoFileSizeDocumentContent()); + } + + /** + * Test getNoChecksumDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNoChecksumDocumentContent() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $contents = self::$dms->getNoChecksumDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(0, $contents); + /* Manipulate the checksum right in the database */ + $dbh = self::$dms->getDB(); + $ret = $dbh->getResult("UPDATE `tblDocumentContent` SET `checksum` = null"); + $this->assertTrue($ret); + $contents = self::$dms->getNoChecksumDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(1, $contents); + } + + /** + * Test getNoChecksumDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNoChecksumDocumentContentSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblDocumentContent` WHERE `checksum`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->getNoChecksumDocumentContent()); + } + + /** + * Test getDuplicateDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDuplicateDocumentContent() + { + $contents = self::$dms->getDuplicateDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(0, $contents); + } + + /** + * Test getDuplicateDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDuplicateDocumentContentWithDuplicates() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $document1 = self::createDocument($rootfolder, $user, 'Document 1'); + + $filename = self::createTempFile(200); + list($document2, $res) = $rootfolder->addDocument( + 'Documet 2', // name + '', // comment + null, // no expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + list($document3, $res) = $rootfolder->addDocument( + 'Documet 3', // name + '', // comment + null, // no expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + + $contents = self::$dms->getDuplicateDocumentContent(); + $this->assertIsArray($contents); + $this->assertCount(1, $contents); + } + + /** + * Test method getDuplicateDocumentContent() with sql failure + * + * @return void + */ + public function testGetDuplicateDocumentContentSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT a.*, b.`id` as dupid FROM `tblDocumentContent`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->getDuplicateDocumentContent()); + } + + /** + * Test getNotificationsByUser() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNotificationsByUser() + { + $user = self::$dms->getUser(1); + $notifications = self::$dms->getNotificationsByUser($user, 0); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + $notifications = self::$dms->getNotificationsByUser($user, 1); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + } + + /** + * Test getNotificationsByGroup() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNotificationsByGroup() + { + $group = self::$dms->addGroup('new group', 'with comment'); + $this->assertIsObject($group); + $notifications = self::$dms->getNotificationsByGroup($group, 0); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + $notifications = self::$dms->getNotificationsByGroup($group, 1); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + } + + /** + * Test getDocumentsExpired() + * + * Check if getDocumentsExpired() fails if the parameters are wrong + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsExpiredFail() + { + $documents = self::$dms->getDocumentsExpired(false); + $this->assertFalse($documents); + $documents = self::$dms->getDocumentsExpired('2021-04'); + $this->assertFalse($documents); + $documents = self::$dms->getDocumentsExpired('2021-01-32'); + $this->assertFalse($documents); + $documents = self::$dms->getDocumentsExpired('2021-01-31'); // valid date + $this->assertIsArray($documents); + } + + /** + * Test method getDocumentsExpired() with sql failure + * + * @return void + */ + public function testGetDocumentsExpiredSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('createTemporaryTable') + ->with('ttstatid') + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->getDocumentsExpired(1)); + } + + /** + * Test getDocumentByName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentByName() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $subfolder); + + /* Add a new document */ + $filename = self::createTempFile(200); + list($document, $res) = $subfolder->addDocument( + 'Document 1', // name + '', // comment + null, // no expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertIsObject($document); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + /* Search without a parent folder restriction */ + $document = self::$dms->getDocumentByName('Document 1'); + $this->assertInstanceOf(SeedDMS_Core_Document::class, $document); + /* Searching in the root folder will return no document */ + $document = self::$dms->getDocumentByName('Document 1', $rootfolder); + $this->assertNull($document); + /* Searching in the sub folder will return the document */ + $document = self::$dms->getDocumentByName('Document 1', $subfolder); + $this->assertInstanceOf(SeedDMS_Core_Document::class, $document); + /* Searching for an empty name returns false */ + $document = self::$dms->getDocumentByName(' '); + $this->assertFalse($document); + } + + /** + * Test getDocumentByName() with sql failure + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document = $dms->getDocumentByName('foo'); + $this->assertFalse($document); + } + + /** + * Test getDocumentByOriginalFilename() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentByOriginalFilename() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $subfolder); + + /* Add a new document */ + $filename = self::createTempFile(200); + list($document, $res) = $subfolder->addDocument( + 'Document 1', // name + '', // comment + null, // no expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertIsObject($document); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + /* Search without a parent folder restriction */ + $document = self::$dms->getDocumentByOriginalFilename('file1.txt'); + $this->assertInstanceOf(SeedDMS_Core_Document::class, $document); + /* Searching in the root folder will return no document */ + $document = self::$dms->getDocumentByOriginalFilename('file1.txt', $rootfolder); + $this->assertNull($document); + /* Searching in the sub folder will return the document */ + $document = self::$dms->getDocumentByOriginalFilename('file1.txt', $subfolder); + $this->assertInstanceOf(SeedDMS_Core_Document::class, $document); + /* Searching for an empty name returns false */ + $document = self::$dms->getDocumentByOriginalFilename(' '); + $this->assertFalse($document); + } + + /** + * Test method getDocumentByOriginalFilename() with sql failure + * + * @return void + */ + public function testGetDocumentByOriginalFilenameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('createTemporaryTable') + ->with('ttcontentid') + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->getDocumentByOriginalFilename(1)); + } + + /** + * Test getDocumentList() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentList() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + + /* Add a new document */ + $filename = self::createTempFile(200); + list($document, $res) = $rootfolder->addDocument( + 'Document 1', // name + '', // comment + null, // no expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($document); + + /* Add a second new document */ + $filename = self::createTempFile(200); + list($document, $res) = $rootfolder->addDocument( + 'Document 2', // name + '', // comment + mktime(0, 0, 0), // expires today + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file2.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($document); + + $documents = self::$dms->getDocumentList('MyDocs', $user); + $this->assertIsArray($documents); + $this->assertCount(2, $documents); + /* All documents expiring from 1 year ago till today */ + $documents = self::$dms->getDocumentList('ExpiredOwner', $user); + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + /* All documents expiring today */ + $documents = self::$dms->getDocumentList('ExpiredOwner', $user, 0); + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + /* All documents expiring tomorrow */ + $documents = self::$dms->getDocumentList('ExpiredOwner', $user, date("Y-m-d", time()+86400)); + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + /* Get unknown list */ + $documents = self::$dms->getDocumentList('foo', $user); + $this->assertFalse($documents); + } + + /** + * Test search() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSearch() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $subfolder); + + /* Add a new document to the subfolder*/ + $filename = self::createTempFile(200); + list($document, $res) = $subfolder->addDocument( + 'Document 1', // name + '', // comment + null, // no expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + /* Add a new document to the root folder. All documents expire. The first + * expires today, the second expires tomorrow, etc. + */ + for ($i=2; $i<=30; $i++) { + $filename = self::createTempFile(200); + list($document, $res) = $rootfolder->addDocument( + 'Document '.$i, // name + '', // comment + mktime(0, 0, 0)+($i-2)*86400, // expires in $i-2 days + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file'.$i.'.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0+$i // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + } + $hits = self::$dms->search(['query'=>'Document']); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(30, $hits['totalDocs']); + $this->assertCount(30, $hits['docs']); + /* Limit number of documents to 10 */ + $hits = self::$dms->search(['query'=>'Document', 'limit'=>10]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(30, $hits['totalDocs']); + $this->assertCount(10, $hits['docs']); + /* Same number of documents if startFolder is the root folder */ + $hits = self::$dms->search(['query'=>'Document', 'startFolder'=>$rootfolder]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(30, $hits['totalDocs']); + $this->assertCount(30, $hits['docs']); + /* There is just one document below the sub folder */ + $hits = self::$dms->search(['query'=>'Document', 'startFolder'=>$subfolder]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(1, $hits['totalDocs']); + /* Get documents with a given expiration date in the future + * All documents in subfolder, but not the one in the root folder + */ + $expts = mktime(0, 0, 0); + $expstart = [ + 'year'=>date('Y', $expts), + 'month'=>date('m', $expts), + 'day'=>date('d', $expts), + 'hour'=>date('H', $expts), + 'minute'=>date('i', $expts), + 'second'=>date('s', $expts) + ]; + $hits = self::$dms->search(['query'=>'Document', 'expirationstartdate'=>$expstart]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(29, $hits['totalDocs']); + /* Get documents with a given expiration date in the future, starting tomorrow + * All documents in subfolder - 1 + */ + $expts = mktime(0, 0, 0)+86400; + $expstart = [ + 'year'=>date('Y', $expts), + 'month'=>date('m', $expts), + 'day'=>date('d', $expts), + 'hour'=>date('H', $expts), + 'minute'=>date('i', $expts), + 'second'=>date('s', $expts) + ]; + $hits = self::$dms->search(['query'=>'Document', 'expirationstartdate'=>$expstart]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(28, $hits['totalDocs']); + /* Get documents expire today or tomorrow + * 2 documents in subfolder + */ + $expts = mktime(0, 0, 0); + $expstart = [ + 'year'=>date('Y', $expts), + 'month'=>date('m', $expts), + 'day'=>date('d', $expts), + 'hour'=>date('H', $expts), + 'minute'=>date('i', $expts), + 'second'=>date('s', $expts) + ]; + $expts += 1*86400; + $expstop = [ + 'year'=>date('Y', $expts), + 'month'=>date('m', $expts), + 'day'=>date('d', $expts), + 'hour'=>date('H', $expts), + 'minute'=>date('i', $expts), + 'second'=>date('s', $expts) + ]; + $hits = self::$dms->search(['query'=>'Document', 'expirationstartdate'=>$expstart, 'expirationenddate'=>$expstop]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(2, $hits['totalDocs']); + /* Get documents expire before and tomorrow + * 2 documents in subfolder + */ + $expts = mktime(0, 0, 0); // Start of today + $expts += 1*86400; // Start of tomorrow + $expstop = [ + 'year'=>date('Y', $expts), + 'month'=>date('m', $expts), + 'day'=>date('d', $expts), + 'hour'=>date('H', $expts), + 'minute'=>date('i', $expts), + 'second'=>date('s', $expts) + ]; + $hits = self::$dms->search(['query'=>'Document', 'expirationenddate'=>$expstop]); + $this->assertIsArray($hits); + $this->assertCount(5, $hits); + $this->assertEquals(2, $hits['totalDocs']); + } + +} + diff --git a/SeedDMS_Core/tests/DmsWithDataTest.php b/SeedDMS_Core/tests/DmsWithDataTest.php new file mode 100644 index 000000000..17fde09e7 --- /dev/null +++ b/SeedDMS_Core/tests/DmsWithDataTest.php @@ -0,0 +1,290 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * DMS test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DmsWithDataTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test getFoldersMinMax() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetFoldersMinMax() + { + self::createSimpleFolderStructure(); + $rootfolder = self::$dms->getRootFolder(); + $minmax = $rootfolder->getFoldersMinMax(); + $this->assertIsArray($minmax); + $this->assertCount(2, $minmax); + $this->assertEquals(0.5, $minmax['min']); + $this->assertEquals(2.0, $minmax['max']); + } + + /** + * Test method getFoldersMinMax() + * + * @return void + */ + public function testGetFoldersMinMaxSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT min(`sequence`) AS `min`, max(`sequence`) AS `max` FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->getFoldersMinMax()); + } + + /** + * Test getDocumentsMinMax() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsMinMax() + { + self::createSimpleFolderStructureWithDocuments(); + $subfolder = self::$dms->getFolderByName('Subfolder 1'); + $this->assertIsObject($subfolder); + $minmax = $subfolder->getDocumentsMinMax(); + $this->assertIsArray($minmax); + $this->assertCount(2, $minmax); + $this->assertEquals(2.0, $minmax['min']); + $this->assertEquals(16.0, $minmax['max']); + } + + /** + * Test method getDocumentsMinMax() + * + * @return void + */ + public function testGetDocumentsMinMaxSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT min(`sequence`) AS `min`, max(`sequence`) AS `max` FROM `tblDocuments`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->getDocumentsMinMax()); + } + + /** + * Test addDocument() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddDocument() + { + self::createSimpleFolderStructure(); + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $this->assertEquals(1, $rootfolder->getId()); + /* Add a new document */ + $filename = self::createTempFile(200); + list($document, $res) = $rootfolder->addDocument( + 'Document 1', // name + '', // comment + null, // expiration + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($document); + $this->assertInstanceOf(SeedDMS_Core_Document::class, $document); + $this->assertEquals('Document 1', $document->getName()); + } + + /** + * Test getDocumentsExpired() + * + * Create two documents which will expired today and tomorrow + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsExpiredFuture() + { + self::createSimpleFolderStructure(); + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $this->assertEquals(1, $rootfolder->getId()); + /* Add a new document */ + $filename = self::createTempFile(200); + list($document, $res) = $rootfolder->addDocument( + 'Document 1', // name + '', // comment + mktime(23,59,59), // expiration is still today at 23:59:59 + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertIsObject($document); + list($document, $res) = $rootfolder->addDocument( + 'Document 2', // name + '', // comment + mktime(23,59,59)+1, // expiration is tomorrow today at 0:00:00 + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertIsObject($document); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $documents = self::$dms->getDocumentsExpired(0); /* Docs expire today */ + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + $documents = self::$dms->getDocumentsExpired(date('Y-m-d')); /* Docs expire today */ + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + $documents = self::$dms->getDocumentsExpired(1); /* Docs expire till tomorrow 23:59:59 */ + $this->assertIsArray($documents); + $this->assertCount(2, $documents); + $documents = self::$dms->getDocumentsExpired(date('Y-m-d', time()+86400)); /* Docs expire till tomorrow 23:59:59 */ + $this->assertIsArray($documents); + $this->assertCount(2, $documents); + $documents = self::$dms->getDocumentsExpired(date('Y-m-d', time()+86400), $user); /* Docs expire till tomorrow 23:59:59 owned by $user */ + $this->assertIsArray($documents); + $this->assertCount(2, $documents); + } + + /** + * Test getDocumentsExpired() + * + * Create two documents which have expired yesterday and the day before + * yesterday + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsExpiredPast() + { + self::createSimpleFolderStructure(); + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $rootfolder); + $this->assertEquals(1, $rootfolder->getId()); + /* Add a new document */ + $filename = self::createTempFile(200); + list($document, $res) = $rootfolder->addDocument( + 'Document 1', // name + '', // comment + mktime(0,0,0)-1, // expiration was yesterday + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertIsObject($document); + list($document, $res) = $rootfolder->addDocument( + 'Document 2', // name + '', // comment + mktime(0,0,0)-1-86400, // expiration the day before yesterday + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($document); + $documents = self::$dms->getDocumentsExpired(0); /* No Docs expire today */ + $this->assertIsArray($documents); + $this->assertCount(0, $documents); + $documents = self::$dms->getDocumentsExpired(-1); /* Docs expired yesterday */ + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + $documents = self::$dms->getDocumentsExpired(-2); /* Docs expired since the day before yesterday */ + $this->assertIsArray($documents); + $this->assertCount(2, $documents); + } + + +} diff --git a/SeedDMS_Core/tests/DocumentCategoryTest.php b/SeedDMS_Core/tests/DocumentCategoryTest.php new file mode 100644 index 000000000..37309d9a1 --- /dev/null +++ b/SeedDMS_Core/tests/DocumentCategoryTest.php @@ -0,0 +1,295 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * User test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DocumentCategoryTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetName() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $cat = self::$dms->addDocumentCategory('Category 1'); + $name = $cat->getName(); + $ret = $cat->setName('foo'); + $this->assertTrue($ret); + $name = $cat->getName(); + $this->assertEquals('foo', $name); + $ret = $cat->setName(' '); + $this->assertFalse($ret); + } + + /** + * Test method addCategories(), hasCategory(), setCategory() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddCategoryToDocument() + { + $rootfolder = self::$dms->getRootFolder(); + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + + /* Add a new document and two categories */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $cat1 = self::$dms->addDocumentCategory('Category 1'); + $cat2 = self::$dms->addDocumentCategory('Category 2'); + + /* There are no categories yet */ + $ret = $document->hasCategory($cat1); + $this->assertFalse($ret); + + /* Not passing a category yields on error */ + $ret = $document->hasCategory(null); + $this->assertFalse($ret); + + /* Adding a category ... */ + $ret = $document->addCategories([$cat1]); + $this->assertTrue($ret); + + /* ... and check if it is there */ + $ret = $document->hasCategory($cat1); + $this->assertTrue($ret); + + /* There should be one category now */ + $cats = $document->getCategories(); + $this->assertIsArray($cats); + $this->assertCount(1, $cats); + $this->assertEquals($cat1->getName(), $cats[0]->getName()); + + /* Adding the same category shouldn't change anything */ + $ret = $document->addCategories([$cat1]); + $this->assertTrue($ret); + + /* Check if category is used */ + $ret = $cat1->isUsed(); + $this->assertTrue($ret); + $ret = $cat2->isUsed(); + $this->assertFalse($ret); + + /* There is one document with cat 1 but none with cat 2 */ + $docs = $cat1->getDocumentsByCategory(); + $this->assertIsArray($docs); + $this->assertCount(1, $docs); + $num = $cat1->countDocumentsByCategory(); + $this->assertEquals(1, $num); + $docs = $cat2->getDocumentsByCategory(); + $this->assertIsArray($docs); + $this->assertCount(0, $docs); + $num = $cat2->countDocumentsByCategory(); + $this->assertEquals(0, $num); + + /* Still only one category */ + $cats = $document->getCategories(); + $this->assertIsArray($cats); + $this->assertCount(1, $cats); + + /* Setting new categories will replace the old ones */ + $ret = $document->setCategories([$cat1, $cat2]); + $this->assertTrue($ret); + + /* Now we have two categories */ + $cats = $document->getCategories(); + $this->assertIsArray($cats); + $this->assertCount(2, $cats); + + /* Remove a category */ + $ret = $document->removeCategories([$cat1]); + $this->assertTrue($ret); + + /* Removing the same category again does not harm*/ + $ret = $document->removeCategories([$cat1]); + $this->assertTrue($ret); + + /* We are back to one category */ + $cats = $document->getCategories(); + $this->assertIsArray($cats); + $this->assertCount(1, $cats); + + /* Remove the remaining category from the document */ + $ret = $document->removeCategories($cats); + $this->assertTrue($ret); + + /* No category left */ + $cats = $document->getCategories(); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + + /* Remove the category itself */ + $cats = self::$dms->getDocumentCategories(); + $this->assertIsArray($cats); + $this->assertCount(2, $cats); + $ret = $cat1->remove(); + $cats = self::$dms->getDocumentCategories(); + $this->assertIsArray($cats); + $this->assertCount(1, $cats); + } + + /** + * Test method getCategories() with sql fail + * + * @return void + */ + public function testGetCategoriesSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblCategory` WHERE")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->getCategories()); + } + + /** + * Test method addCategories() with sql fail + * + * @return void + */ + public function testAddCategoriesSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + /* mock sql statement in getCategories() which is called in addCategories() */ + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblCategory` WHERE")) + ->willReturn([]); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("INSERT INTO `tblDocumentCategory`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document = $this->getMockedDocument(); + $document->setDMS($dms); + $cat = new SeedDMS_Core_DocumentCategory(1, 'Category'); + $cat->setDMS($dms); + $this->assertFalse($document->addCategories([$cat])); + } + + /** + * Test method removeCategories() with sql fail + * + * @return void + */ + public function testRemoveCategoriesSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("DELETE FROM `tblDocumentCategory` WHERE")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document = $this->getMockedDocument(); + $document->setDMS($dms); + $cat = new SeedDMS_Core_DocumentCategory(1, 'Category'); + $cat->setDMS($dms); + $this->assertFalse($document->removeCategories([$cat])); + } + + /** + * Test method setCategories() with sql fail when deleting categories + * + * @return void + */ + public function testSetCategoriesSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("DELETE FROM `tblDocumentCategory` WHERE")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document = $this->getMockedDocument(); + $document->setDMS($dms); + $cat = new SeedDMS_Core_DocumentCategory(1, 'Category'); + $cat->setDMS($dms); + $this->assertFalse($document->setCategories([$cat])); + } + + /** + * Test method setCategories() with sql fail when inserting new categories + * + * @return void + */ + public function testSetCategoriesSqlFail2() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResult') + ->will( + $this->returnValueMap( + array( + array("DELETE FROM `tblDocumentCategory` WHERE `documentID` = 1", true, true), + array("INSERT INTO `tblDocumentCategory`", true, false) + ) + ) + ); + $dms = new SeedDMS_Core_DMS($db, ''); + $document = $this->getMockedDocument(); + $document->setDMS($dms); + $cat = new SeedDMS_Core_DocumentCategory(1, 'Category'); + $cat->setDMS($dms); + $this->assertFalse($document->setCategories([$cat])); + } + +} + diff --git a/SeedDMS_Core/tests/DocumentContentTest.php b/SeedDMS_Core/tests/DocumentContentTest.php new file mode 100644 index 000000000..9a8e9a14b --- /dev/null +++ b/SeedDMS_Core/tests/DocumentContentTest.php @@ -0,0 +1,593 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DocumentContentTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getContent(), getContentByVersion(), getLatestContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetContent() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $version = $document->getContentByVersion(1); + $this->assertIsObject($version); + $this->assertEquals($version->getId(), $lcontent->getId()); + $content = $document->getContent(); + $this->assertIsArray($content); + $this->assertCount(1, $content); + $this->assertEquals($version->getId(), $content[0]->getId()); + } + + /** + * Test method getContent() mit sql fail + * + * @return void + */ + public function testGetContentSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblDocumentContent` WHERE `document` ")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->getContent()); + } + + /** + * Test method getContentByVersion() mit sql fail + * + * @return void + */ + public function testGetContentByVersionSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblDocumentContent` WHERE `document` ")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->getContentByVersion(1)); + } + + /** + * Test method getLatestContent() mit sql fail + * + * @return void + */ + public function testGetLatestContentSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblDocumentContent` WHERE `document` ")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->getLatestContent()); + } + + /** + * Test method removeContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testRemoveContent() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + + /* Removing the only version will fail */ + $ret = $document->removeContent($lcontent); + $this->assertFalse($ret); + + /* Add a new version */ + $filename = self::createTempFile(300); + $result = $document->addContent('', $user, $filename, 'file2.txt', '.txt', 'text/plain'); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($result); + $this->assertIsObject($result->getContent()); + + /* Second trial to remove a version. Now it succeeds because it is not + * the last version anymore. + */ + $ret = $document->removeContent($lcontent); + $this->assertTrue($ret); + + /* The latest version is now version 2 */ + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $this->assertEquals(2, $lcontent->getVersion()); + + /* There is only 1 version left */ + $contents = $document->getContent(); + $this->assertIsArray($contents); + $this->assertCount(1, $contents); + } + + /** + * Test method isType() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsType() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $ret = $lcontent->isType('documentcontent'); + $this->assertTrue($ret); + } + + /** + * Test method getUser(), getDocument() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testVarious() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $ret = $lcontent->isType('documentcontent'); + $this->assertTrue($ret); + $doc = $lcontent->getDocument(); + $this->assertEquals($document->getId(), $doc->getId()); + $u = $lcontent->getUser(); + $this->assertEquals($user->getId(), $u->getId()); + $filetype = $lcontent->getFileType(); + $this->assertEquals('.txt', $filetype); + $origfilename = $lcontent->getOriginalFileName(); + $this->assertEquals('file1.txt', $origfilename); + } + + /** + * Test method getComment(), setComment() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetComment() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $comment = $lcontent->getComment(); + $this->assertEquals('', $comment); + $ret = $lcontent->setComment('Document content comment'); + $this->assertTrue($ret); + /* Retrieve the document content from the database again */ + $content = self::$dms->getDocumentContent($lcontent->getId()); + $comment = $content->getComment(); + $this->assertEquals('Document content comment', $comment); + } + + /** + * Test method getDate(), setDate() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetDate() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $date = $lcontent->getDate(); + $this->assertIsInt($date); + $this->assertGreaterThanOrEqual(time(), $date); + + /* Set date as timestamp */ + $ret = $lcontent->setDate($date-1000); + $this->assertTrue($ret); + /* Retrieve the document content from the database again */ + $content = self::$dms->getDocumentContent($lcontent->getId()); + $newdate = $content->getDate(); + $this->assertEquals($date-1000, $newdate); + + /* Set date in Y-m-d H:i:s format */ + $date = time()-500; + $ret = $lcontent->setDate(date('Y-m-d H:i:s', $date)); + $this->assertTrue($ret); + /* Retrieve the document content from the database again */ + $content = self::$dms->getDocumentContent($lcontent->getId()); + $newdate = $content->getDate(); + $this->assertEquals($date, $newdate); + + /* Not passing a date will set the current date/time */ + $date = time(); + $ret = $lcontent->setDate(); + $this->assertTrue($ret); + /* Retrieve the document content from the database again */ + $content = self::$dms->getDocumentContent($lcontent->getId()); + $newdate = $content->getDate(); + $this->assertEquals($date, $newdate); + } + + /** + * Test method getFileSize(), setFileSize() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetFileSize() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $filesize = $lcontent->getFileSize(); + $this->assertEquals(200, $filesize); + + /* Intentially corrupt the file size */ + $db = self::$dms->getDb(); + $ret = $db->getResult("UPDATE `tblDocumentContent` SET `fileSize` = 300 WHERE `document` = " . $document->getID() . " AND `version` = " . $lcontent->getVersion()); + $this->assertTrue($ret); + + $corcontent = self::$dms->getDocumentContent($lcontent->getId()); + $filesize = $corcontent->getFileSize(); + $this->assertEquals(300, $filesize); + + /* Repair filesize by calling setFileSize() */ + $ret = $corcontent->setFileSize(); + $this->assertTrue($ret); + $filesize = $corcontent->getFileSize(); + $this->assertEquals(200, $filesize); + } + + /** + * Test method getChecksum(), setChecksum() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetChecksum() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + $orgchecksum = $lcontent->getChecksum(); + $this->assertIsString($orgchecksum); + $this->assertEquals(32, strlen($orgchecksum)); + + /* Intentially corrupt the checksum */ + $db = self::$dms->getDb(); + $ret = $db->getResult("UPDATE `tblDocumentContent` SET `checksum` = 'foobar' WHERE `document` = " . $document->getID() . " AND `version` = " . $lcontent->getVersion()); + $this->assertTrue($ret); + + $corcontent = self::$dms->getDocumentContent($lcontent->getId()); + $checksum = $corcontent->getChecksum(); + $this->assertEquals('foobar', $checksum); + + /* Repair filesize by calling setChecksum() */ + $ret = $corcontent->setChecksum(); + $this->assertTrue($ret); + $checksum = $corcontent->getChecksum(); + $this->assertEquals($orgchecksum, $checksum); + } + + /** + * Test method getStatus(), setStatus(), getStatusLog() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetStatus() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + + $status = $lcontent->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + $statuslog = $lcontent->getStatusLog(); + $this->assertIsArray($statuslog); + $this->assertCount(1, $statuslog); + + /* Missing update user returns false */ + $ret = $lcontent->setStatus(S_OBSOLETE, '', null); + $this->assertFalse($ret); + + /* A status out of range returns false */ + $ret = $lcontent->setStatus(9, '', $user); + $this->assertFalse($ret); + + /* A wrong date returns false */ + $ret = $lcontent->setStatus(S_OBSOLETE, '', $user, '2021-02-29 10:10:10'); + $this->assertFalse($ret); + + $ret = $lcontent->setStatus(S_OBSOLETE, 'No longer valid', $user, date('Y-m-d H:i:s')); + $status = $lcontent->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_OBSOLETE, $status['status']); + + /* Status log has now 2 entries */ + $statuslog = $lcontent->getStatusLog(); + $this->assertIsArray($statuslog); + $this->assertCount(2, $statuslog); + + /* Add the 'onSetStatus' callback */ + $callret = ''; + $callback = function ($param, $content, $updateuser, $oldstatus, $newstatus) use (&$callret) { + $callret = $oldstatus.' to '.$newstatus; + return $param; + }; + /* Because the callback will return false, the status will not be set */ + self::$dms->setCallback('onSetStatus', $callback, false); + /* Trying to go back to status released with a callback returning false */ + $ret = $lcontent->setStatus(S_RELEASED, 'Valid again', $user); + $status = $lcontent->getStatus(); + $this->assertIsArray($status); + /* Status is still S_OBSOLETE because the callback returned false */ + $this->assertEquals(S_OBSOLETE, $status['status']); + $this->assertEquals(S_OBSOLETE.' to '.S_RELEASED, $callret); + + /* Do it again, but this time the callback returns true */ + self::$dms->setCallback('onSetStatus', $callback, true); + /* Trying to go back to status released with a callback returning true */ + $ret = $lcontent->setStatus(S_RELEASED, 'Valid again', $user); + $status = $lcontent->getStatus(); + $this->assertIsArray($status); + /* Status updated to S_RELEASED because the callback returned true */ + $this->assertEquals(S_RELEASED, $status['status']); + $this->assertEquals(S_OBSOLETE.' to '.S_RELEASED, $callret); + + /* Status log has now 3 entries */ + $statuslog = $lcontent->getStatusLog(); + $this->assertIsArray($statuslog); + $this->assertCount(3, $statuslog); + + /* Get just the last entry */ + $statuslog = $lcontent->getStatusLog(1); + $this->assertIsArray($statuslog); + $this->assertCount(1, $statuslog); + $this->assertEquals('Valid again', $statuslog[0]['comment']); + } + + /** + * Test method getMimeType(), setMimeType() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetMimeType() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + + $ret = $lcontent->setMimeType('text/csv'); + $this->assertTrue($ret); + + /* Retrieve the document content from the database again */ + $content = self::$dms->getDocumentContent($lcontent->getId()); + $this->assertIsObject($content); + $this->assertEquals('text/csv', $content->getMimeType()); + } + + /** + * Test method getFileType(), setFileType() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetFileType() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + + $ret = $lcontent->setMimeType('text/css'); + $this->assertTrue($ret); + + $ret = $lcontent->setFileType(); + $this->assertTrue($ret); + + /* Retrieve the document content from the database again */ + $content = self::$dms->getDocumentContent($lcontent->getId()); + $this->assertIsObject($content); + $this->assertEquals('.css', $content->getFileType()); + + /* Also get the file content to ensure the renaming of the file + * on disc has succeeded. + */ + $c = file_get_contents(self::$dms->contentDir.$lcontent->getPath()); + $this->assertEquals(200, strlen($c)); + } + + /** + * Test method replaceContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testReplaceContent() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $guest = self::$dms->getUser(2); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + + $filename = self::createTempFile(300); + /* Not using the same user yields an error */ + $ret = $document->replaceContent(1, $guest, $filename, 'file1.txt', '.txt', 'text/plain'); + $this->assertFalse($ret); + /* Not using the same orig. file name yields an error */ + $ret = $document->replaceContent(1, $user, $filename, 'file2.txt', '.txt', 'text/plain'); + $this->assertFalse($ret); + /* Not using the same file type yields an error */ + $ret = $document->replaceContent(1, $user, $filename, 'file1.txt', '.csv', 'text/plain'); + $this->assertFalse($ret); + /* Not using the same mime type yields an error */ + $ret = $document->replaceContent(1, $user, $filename, 'file1.txt', '.txt', 'text/csv'); + $this->assertFalse($ret); + + /* Setting version to 0 will replace the latest version */ + $ret = $document->replaceContent(0, $user, $filename, 'file1.txt', '.txt', 'text/plain'); + $this->assertTrue($ret); + + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + + /* Retrieve the document content from the database again */ + $newcontent = $document->getLatestContent(); + $this->assertIsObject($newcontent); + $this->assertEquals('text/plain', $newcontent->getMimeType()); + /* File size has grown from 200 to 300 bytes */ + $filesize = $newcontent->getFileSize(); + $this->assertEquals(300, $filesize); + /* Still version 1 */ + $version = $newcontent->getVersion(); + $this->assertEquals(1, $version); + } + + /** + * Test method replaceContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAccessMode() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $guest = self::$dms->getUser(2); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1', 200); + $this->assertIsObject($document); + $lcontent = $document->getLatestContent(); + $this->assertIsObject($lcontent); + + /* Access rights on a document content are always M_READ unless the callback + * onCheckAccessDocumentContent is implemented */ + $mode = $lcontent->getAccessMode($user); + $this->assertEquals(M_READ, $mode); + } +} diff --git a/SeedDMS_Core/tests/DocumentFileTest.php b/SeedDMS_Core/tests/DocumentFileTest.php new file mode 100644 index 000000000..95d758c56 --- /dev/null +++ b/SeedDMS_Core/tests/DocumentFileTest.php @@ -0,0 +1,290 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DocumentFileTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getInstance() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetMockedDocumentFile() + { + $user = self::getMockedUser(); + $document1 = self::getMockedDocument(1, 'Document 1'); + $file = new SeedDMS_Core_DocumentFile(1, $document1, $user->getId(), 'comment', time(), '', '.txt', 'text/plain', 'test.txt', 'name', 1, true); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $document = $file->getDocument(); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 1', $document->getName()); + + $ispublic = $file->isPublic(); + $this->assertTrue($ispublic); + + $comment = $file->getComment(); + $this->assertEquals('comment', $comment); + + $filetype = $file->getFileType(); + $this->assertEquals('.txt', $filetype); + + $mimetype = $file->getMimeType(); + $this->assertEquals('text/plain', $mimetype); + + $name = $file->getName(); + $this->assertEquals('name', $name); + + $origfilename = $file->getOriginalFileName(); + $this->assertEquals('test.txt', $origfilename); + + $version = $file->getVersion(); + $this->assertEquals(1, $version); + + $accessmode = $file->getAccessMode($user); + $this->assertEquals(M_READ, $accessmode); + } + + /** + * Test method setComment() mit sql fail + * + * @return void + */ + public function testSetCommentSqlFail() + { + $user = self::getMockedUser(); + $document = $this->getMockedDocument(); + $file = new SeedDMS_Core_DocumentFile(1, $document, $user->getId(), 'comment', time(), '', '.txt', 'text/plain', 'test.txt', 'name', 1, true); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocumentFiles` SET `comment`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($file->setComment('my comment')); + } + + /** + * Test method setName() mit sql fail + * + * @return void + */ + public function testSetNameSqlFail() + { + $user = self::getMockedUser(); + $document = $this->getMockedDocument(); + $file = new SeedDMS_Core_DocumentFile(1, $document, $user->getId(), 'comment', time(), '', '.txt', 'text/plain', 'test.txt', 'name', 1, true); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocumentFiles` SET `name`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($file->setName('my name')); + } + + /** + * Test method setDate() mit sql fail + * + * @return void + */ + public function testSetDateSqlFail() + { + $user = self::getMockedUser(); + $document = $this->getMockedDocument(); + $file = new SeedDMS_Core_DocumentFile(1, $document, $user->getId(), 'comment', time(), '', '.txt', 'text/plain', 'test.txt', 'name', 1, true); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocumentFiles` SET `date`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($file->setDate()); + } + + /** + * Test method setVersion() mit sql fail + * + * @return void + */ + public function testSetVersionSqlFail() + { + $user = self::getMockedUser(); + $document = $this->getMockedDocument(); + $file = new SeedDMS_Core_DocumentFile(1, $document, $user->getId(), 'comment', time(), '', '.txt', 'text/plain', 'test.txt', 'name', 1, true); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocumentFiles` SET `version`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($file->setVersion(1)); + } + + /** + * Test method setPublic() mit sql fail + * + * @return void + */ + public function testSetPublicnSqlFail() + { + $user = self::getMockedUser(); + $document = $this->getMockedDocument(); + $file = new SeedDMS_Core_DocumentFile(1, $document, $user->getId(), 'comment', time(), '', '.txt', 'text/plain', 'test.txt', 'name', 1, true); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocumentFiles` SET `public`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($file->setPublic(true)); + } + + /** + * Test method addDocumentFile(), getDocumentFile() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddDocumentFile() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $tmpfile = self::createTempFile(); + $file = $document->addDocumentFile('attachment.txt', 'comment', $user, $tmpfile, 'attachment.txt', '.txt', 'text/plain', 0, true); + $this->assertTrue(SeedDMS_Core_File::removeFile($tmpfile)); + $this->assertIsObject($file); + $this->assertTrue($file->isType('documentfile')); + + $files = $document->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(1, $files); + + $file = $files[0]; + + $document = $file->getDocument(); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 1', $document->getName()); + + $ispublic = $file->isPublic(); + $this->assertTrue($ispublic); + + $luser = $file->getUser(); + $this->assertIsObject($luser); + $this->assertTrue($luser->isType('user')); + + $ret = $file->setComment('new comment'); + $this->assertTrue($ret); + $comment = $file->getComment(); + $this->assertEquals('new comment', $comment); + + $ret = $file->setName('new name'); + $this->assertTrue($ret); + $name = $file->getName(); + $this->assertEquals('new name', $name); + + $now = time(); + $ret = $file->setDate($now); + $this->assertTrue($ret); + $date = $file->getDate(); + $this->assertEquals($now, $date); + + $ret = $file->setDate('fail'); + $this->assertFalse($ret); + + $ret = $file->setVersion(2); + $this->assertTrue($ret); + $version = $file->getVersion(); + $this->assertEquals(2, $version); + + $ret = $file->setVersion('fail'); + $this->assertFalse($ret); + + $ret = $file->setPublic(true); + $this->assertTrue($ret); + $ispublic = $file->isPublic(); + $this->assertEquals(1, $ispublic); + + + } +} diff --git a/SeedDMS_Core/tests/DocumentLinkTest.php b/SeedDMS_Core/tests/DocumentLinkTest.php new file mode 100644 index 000000000..d646607e9 --- /dev/null +++ b/SeedDMS_Core/tests/DocumentLinkTest.php @@ -0,0 +1,125 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DocumentLinkTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getInstance() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetMockedDocumentLink() + { + $user = self::getMockedUser(); + $document1 = self::getMockedDocument(1, 'Document 1'); + $document2 = self::getMockedDocument(2, 'Document 2'); + $link = new SeedDMS_Core_DocumentLink(1, $document1, $document2, $user, true); + $this->assertIsObject($link); + $this->assertTrue($link->isType('documentlink')); + + $document = $link->getDocument(); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 1', $document->getName()); + + $document = $link->getTarget(); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 2', $document->getName()); + + $ispublic = $link->isPublic(); + $this->assertTrue($ispublic); + } + + /** + * Test method addDocumentLink(), getDocumentLink() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddDocumentLink() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $document1 = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document1); + $document2 = self::createDocument($rootfolder, $user, 'Document 2'); + $this->assertIsObject($document2); + $link = $document1->addDocumentLink($document2->getId(), $user->getId(), true); + $this->assertIsObject($link); + $this->assertTrue($link->isType('documentlink')); + + $document = $link->getDocument(); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 1', $document->getName()); + + $document = $link->getTarget(); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 2', $document->getName()); + + $ispublic = $link->isPublic(); + $this->assertTrue($ispublic); + + $luser = $link->getUser(); + $this->assertIsObject($luser); + $this->assertTrue($luser->isType('user')); + } +} diff --git a/SeedDMS_Core/tests/DocumentTest.php b/SeedDMS_Core/tests/DocumentTest.php new file mode 100644 index 000000000..5331f367d --- /dev/null +++ b/SeedDMS_Core/tests/DocumentTest.php @@ -0,0 +1,1323 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class DocumentTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getInstance() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetInstance() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + /* Get the document with id 1, which must be 'Document 1' */ + $document = SeedDMS_Core_Document::getInstance(1, self::$dms); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $this->assertEquals('Document 1', $document->getName()); + /* Get a none existing document */ + $document = SeedDMS_Core_Document::getInstance(2, self::$dms); + $this->assertNull($document); + } + + /** + * Test method getInstance() within a root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetInstanceWithinRoot() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* The simple folder structure will create to subfolders, each + * with 15 documents. + */ + self::createSimpleFolderStructureWithDocuments(); + $subfolder = self::$dms->getFolderByName('Subfolder 1'); + /* Get a document in Subfolder 1 */ + $document1 = self::$dms->getDocumentByName('Document 1-1'); + /* Get a document in Subfolder 2 */ + $document2 = self::$dms->getDocumentByName('Document 2-1'); + + /* Getting a document in subfolder 1 without any restrictions must succeed */ + $document = SeedDMS_Core_Document::getInstance($document1->getId(), self::$dms); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + + /* Make Subfolder 1 the root folder */ + self::$dms->checkWithinRootDir = true; + self::$dms->setRootFolderID($subfolder->getId()); + + /* Getting a document by id in subfolder 1 still must succeed */ + $document = SeedDMS_Core_Document::getInstance($document1->getId(), self::$dms); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + + /* Getting a document by id in subfolder 2 must fail */ + $document = SeedDMS_Core_Document::getInstance($document2->getId(), self::$dms); + $this->assertNull($document); + + /* Get a document in Subfolder 1 */ + $document = self::$dms->getDocumentByName('Document 1-1'); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + + /* Get a document in Subfolder 2 */ + $document = self::$dms->getDocumentByName('Document 2-1'); + $this->assertNull($document); + } + + /** + * Test method getInstance() + * + * @return void + */ + public function testGetInstanceSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lock` FROM `tblDocuments`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse(SeedDMS_Core_Document::getInstance(1, $dms)); + } + + /** + * Test method getDir() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDir() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $this->assertIsObject($document); + $this->assertEquals('1/', $document->getDir()); + } + + /** + * Test method getComment() and setComment() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetComment() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $document = self::createDocument($folder, $user, 'Document 1'); + $this->assertIsObject($document); + $comment = $document->getComment(); + $this->assertEquals('', $comment); + $ret = $document->setComment('foo'); + $this->assertTrue($ret); + $comment = $document->getComment(); + $this->assertEquals('foo', $comment); + } + + /** + * Test method setComment() mit sql fail + * + * @return void + */ + public function testSetCommentSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `comment`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setComment('my comment')); + } + + /** + * Test method getKeywords() and setKeywords() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetKeywords() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $document = self::createDocument($folder, $user, 'Document 1'); + $this->assertIsObject($document); + $keywords = $document->getKeywords(); + $this->assertEquals('', $keywords); + $ret = $document->setKeywords('foo bar'); + $this->assertTrue($ret); + $keywords = $document->getKeywords(); + $this->assertEquals('foo bar', $keywords); + } + + /** + * Test method setKeywords() mit sql fail + * + * @return void + */ + public function testSetKeywordsSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `keywords`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setKeywords('keywords')); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetName() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $document = self::createDocument($folder, $user, 'Document 1'); + $this->assertIsObject($document); + $name = $document->getName(); + $this->assertEquals('Document 1', $name); + $ret = $document->setName('foo'); + $this->assertTrue($ret); + $name = $document->getName(); + $this->assertEquals('foo', $name); + } + + /** + * Test method setName() mit sql fail + * + * @return void + */ + public function testSetNameSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `name`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setName('my name')); + } + + /** + * Test method getDate() and setDate() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetDate() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $document = self::createDocument($folder, $user, 'Document 1'); + $now = time(); + /* Passing false as a time stamp will take current time stamp */ + $ret = $document->setDate(false); + $this->assertTrue($ret); + $date = $document->getDate(); + $this->assertEquals($now, $date); + /* Setting a time stamp */ + $now -= 1000; + $ret = $document->setDate($now); + $this->assertTrue($ret); + $date = $document->getDate(); + $this->assertEquals($now, $date); + /* Setting a none numeric value will fail */ + $ret = $document->setDate('foo'); + $this->assertFalse($ret); + } + + /** + * Test method setDate() with sql fail + * + * @return void + */ + public function testSetDateSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `date` = ")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setDate(null)); + } + + /** + * Test method getDefaultAccess() and setDefaultAccess() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetDefaultAccess() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $document = self::createDocument($folder, $user, 'Document 1'); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $defaultaccess = $document->getDefaultAccess(); + $this->assertEquals(M_READ, $defaultaccess); + + /* Setting a default access out of range yields an error */ + $ret = $document->setDefaultAccess(0, true); + $this->assertFalse($ret); + + /* Setting a default access out of range yields an error */ + $ret = $document->setDefaultAccess(M_ALL+1, true); + $this->assertFalse($ret); + + /* Setting the default access will have no effect as long as access + * rights are inherited. */ + $ret = $document->setDefaultAccess(M_READWRITE, true); + $this->assertTrue($ret); + $defaultaccess = $document->getDefaultAccess(); + $this->assertEquals(M_READ, $defaultaccess); + + /* Once inheritance of access rights is turned off, the previously + * set default access right will take effect. */ + $ret = $document->setInheritAccess(false, true); + $this->assertTrue($ret); + $defaultaccess = $document->getDefaultAccess(); + $this->assertEquals(M_READWRITE, $defaultaccess); + + /* Also check if inherited access was turned off */ + $ret = $document->getInheritAccess(); + $this->assertFalse($ret); + } + + /** + * Test method setDefaultAccess() mit sql fail + * + * @return void + */ + public function testSetDefaultAccessSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `defaultAccess`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setDefaultAccess(M_READ)); + } + + /** + * Test method setInheritAccess() mit sql fail + * + * @return void + */ + public function testSetInheritAccessSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `inheritAccess`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setInheritAccess(0)); + } + + /** + * Test method addAccess(), removeAccess(), changeAccess(), getAccessList() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetAccess() + { + self::createGroupsAndUsers(); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $adminuser = self::$dms->getUser(1); + $guestuser = self::$dms->getUser(2); + $user = self::$dms->getUserByLogin('user-1-1'); + $this->assertIsObject($user); + $this->assertTrue($user->isType('user')); + $group = self::$dms->getGroupByName('Group 1'); + $this->assertIsObject($group); + $this->assertTrue($group->isType('group')); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + $defaultaccess = $document->getDefaultAccess(); + $this->assertEquals(M_READ, $defaultaccess); + + /* Turn off inheritance, otherwise the access rights have no effect */ + $ret = $document->setInheritAccess(false, true); + $this->assertTrue($ret); + + /* Retrieving an access mode without a valid user will always return M_NONE */ + $mode = $document->getAccessMode(null); + $this->assertEquals(M_NONE, $mode); + + /* The admin user has always unlimited access */ + $mode = $document->getAccessMode($adminuser); + $this->assertEquals(M_ALL, $mode); + + /* Without setting any specific access, the document has a default mode M_READ */ + $mode = $document->getAccessMode($user); + $this->assertEquals(M_READ, $mode); + + /* Access mode for group is also the default access */ + $mode = $document->getGroupAccessMode($group); + $this->assertEquals(M_READ, $mode); + + /* Set unlimited access rights for everybody */ + $ret = $document->setDefaultAccess(M_ALL); + $this->assertTrue($ret); + $mode = $document->getAccessMode($user); + $this->assertEquals(M_ALL, $mode); + $mode = $document->getGroupAccessMode($group); + $this->assertEquals(M_ALL, $mode); + + /* Guest still have just read access */ + $mode = $document->getAccessMode($guestuser); + $this->assertEquals(M_READ, $mode); + + /* Add wrong access type returns false */ + $ret = $document->addAccess(M_ALL+1, $user->getId(), true); + $this->assertFalse($ret); + + /* Add read/write access on the document for user */ + $ret = $document->addAccess(M_READWRITE, $user->getId(), true); + $this->assertTrue($ret); + /* Adding another access right (not matter which one) for the + * same user yields an error + */ + $ret = $document->addAccess(M_READ, $user->getId(), true); + $this->assertFalse($ret); + + /* Passing an invalid second parameter will return false */ + $accesslist = $document->getAccessList(M_ANY, 5); + $this->assertFalse($accesslist); + + /* Searching for mode == M_READ will return neither a group nor + * the user, because the user has read&write access + */ + $accesslist = $document->getAccessList(M_READ); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['groups']); + $this->assertCount(0, $accesslist['users']); + + $accesslist = $document->getAccessList(M_READWRITE); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['groups']); + $this->assertCount(1, $accesslist['users']); + + $accesslist = $document->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['groups']); + $this->assertCount(1, $accesslist['users']); + + /* Access mode is just read/write for the user thought the default is unlimited */ + $mode = $document->getAccessMode($user); + $this->assertEquals(M_READWRITE, $mode); + /* Access mode for the group is still unlimited */ + $mode = $document->getGroupAccessMode($group); + $this->assertEquals(M_ALL, $mode); + + /* Setting default access to M_READ + * is just a precaution to ensure the unlimeted access rights is not + * derived from the default access which was set to M_ALL above. + */ + $ret = $document->setDefaultAccess(M_READ); + $this->assertTrue($ret); + $mode = $document->getGroupAccessMode($group); + $this->assertEquals(M_READ, $mode); + + /* Add unlimeted access on the document for group */ + $ret = $document->addAccess(M_ALL, $group->getId(), false); + $this->assertTrue($ret); + /* Adding another access right (not matter which one) for the + * same group yields an error + */ + $ret = $document->addAccess(M_READ, $group->getId(), false); + $this->assertFalse($ret); + + $accesslist = $document->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(1, $accesslist['groups']); + $this->assertCount(1, $accesslist['users']); + + /* The group has now unlimited access rights */ + $mode = $document->getGroupAccessMode($group); + $this->assertEquals(M_ALL, $mode); + + /* The user still has just read/write access, though the group he belongs + * to has unlimeted rights. The specific user rights has higher priority. + */ + $mode = $document->getAccessMode($user); + $this->assertEquals(M_READWRITE, $mode); + + /* Remove all specific access rights for the user */ + $ret = $document->removeAccess($user->getId(), true); + $this->assertTrue($ret); + + /* Now the group rights apply for the user, because there are no + * specific user rights anymore. + */ + $mode = $document->getAccessMode($user); + $this->assertEquals(M_ALL, $mode); + + /* change unlimeted access on the document for group to none */ + $ret = $document->changeAccess(M_NONE, $group->getId(), false); + $this->assertTrue($ret); + $mode = $document->getAccessMode($user); + $this->assertEquals(M_NONE, $mode); + + /* clear all access rights */ + $ret = $document->clearAccessList(); + $this->assertTrue($ret); + $accesslist = $document->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['groups']); + $this->assertCount(0, $accesslist['users']); + + /* We are back to the default access rights */ + $mode = $document->getAccessMode($user); + $this->assertEquals(M_READ, $mode); + + } + + /** + * Test method addNotify(), removeNotify(), getNotifyList(), cleanNotifyList() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetNotify() + { + self::createGroupsAndUsers(); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $adminuser = self::$dms->getUser(1); + $guestuser = self::$dms->getUser(2); + $user = self::$dms->getUserByLogin('user-1-1'); + $this->assertIsObject($user); + $this->assertTrue($user->isType('user')); + $group = self::$dms->getGroupByName('Group 1'); + $this->assertIsObject($group); + $this->assertTrue($group->isType('group')); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + $this->assertIsObject($document); + $this->assertTrue($document->isType('document')); + + $notifylist = $document->getNotifyList(); + $this->assertIsArray($notifylist); + $this->assertCount(0, $notifylist['groups']); + $this->assertCount(0, $notifylist['users']); + + /* Add notify on the document for user */ + $ret = $document->addNotify($user->getId(), true); + $this->assertEquals(0, $ret); + + /* Add notify on the document for group */ + $ret = $document->addNotify($group->getId(), false); + $this->assertEquals(0, $ret); + + /* Add notify on the document for a user that does not exists */ + $ret = $document->addNotify(15, true); + $this->assertEquals(-1, $ret); + + $notifylist = $document->getNotifyList(); + $this->assertIsArray($notifylist); + $this->assertCount(1, $notifylist['groups']); + $this->assertCount(1, $notifylist['users']); + + /* Setting the default access to M_NONE and turning off inheritance + * will clean the notification list, because the notifiers have no + * longer read access on the document and therefore will be removed + * from the notification list. + */ + $ret = $document->setInheritAccess(false); + $this->assertTrue($ret); + $ret = $document->setDefaultAccess(M_NONE); + $this->assertTrue($ret); + + $notifylist = $document->getNotifyList(); + $this->assertIsArray($notifylist); + $this->assertCount(0, $notifylist['groups']); + $this->assertCount(0, $notifylist['users']); + } + + /** + * Test method isDescendant() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsDescendant() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder1 = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subfolder2 = $rootfolder->addSubFolder('Subfolder 2', '', $user, 1.0); + $document = self::createDocument($subfolder1, $user, 'Document 1'); + /* document is a descendant of root folder and subfolder 1 */ + $this->assertTrue($document->isDescendant($rootfolder)); + $this->assertTrue($document->isDescendant($subfolder1)); + /* subfolder is not a descendant of subfolder 2 */ + $this->assertFalse($document->isDescendant($subfolder2)); + } + + /** + * Test method getParent() + * + * Create a new document below root folder and check if parent + * of the document is the root folder. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetParent() + { + $user = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $parent = $document->getParent(); + $this->assertIsObject($parent); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $parent); + $this->assertEquals(1, $parent->getId()); + } + + /** + * Test method setParent() + * + * Create a new document below root folder, move it to a subfolder + * and check if parent of the document is the sub folder. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetParent() + { + $user = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 0); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + /* Setting a null folder is not allowed */ + $ret = $document->setParent(null); + $this->assertFalse($ret); + + /* Passed object must be a folder */ + $ret = $document->setParent($user); + $this->assertFalse($ret); + + $ret = $document->setParent($subfolder); + $this->assertTrue($ret); + $parent = $document->getParent(); + $this->assertIsObject($parent); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $parent); + $this->assertEquals(2, $parent->getId()); + } + + /** + * Test method setParent() mit sql fail + * + * @return void + */ + public function testSetParentSqlFail() + { + $document = $this->getMockedDocument(); + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `folder`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setParent($rootfolder)); + } + + /** + * Test method setOwner() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetOwner() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document = self::createDocument($rootfolder, $adminuser, 'Document 1'); + /* Setting a null user is not allowed */ + $ret = $document->setOwner(null); + $this->assertFalse($ret); + + /* Passed object must be a folder */ + $ret = $document->setOwner($rootfolder); + $this->assertFalse($ret); + + $res = $document->setOwner($user); + $this->assertTrue($res); + $owner = $document->getOwner(); + $this->assertIsObject($owner); + $this->assertInstanceOf(SeedDMS_Core_User::class, $owner); + $this->assertEquals($user->getId(), $owner->getId()); + } + + /** + * Test method setOwner() mit sql fail + * + * @return void + */ + public function testSetOwnerSqlFail() + { + $document = $this->getMockedDocument(); + $user = $this->getMockedUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `owner`")) + ->willReturn(false); + // SeedDMS 6 will fetch the old owner in setOwner() before setting the + // new owner + if(self::$dbversion['major'] == 6) { + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblUsers` WHERE `id` = ")) + ->willReturn([]); + } + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setOwner($user)); + } + + /** + * Test method expires(), setExpires(), getExpires() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetExpires() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $document = self::createDocument($rootfolder, $adminuser, 'Document 1'); + $expires = $document->expires(); + $this->assertFalse($expires); + $expires = $document->getExpires(); + $this->assertFalse($expires); + $now = time(); + $res = $document->setExpires($now); + $this->assertTrue($res); + /* Setting it again will return true */ + $res = $document->setExpires($now); + $this->assertTrue($res); + $expires = $document->expires(); + $this->assertTrue($res); + $expirets = $document->getExpires(); + $this->assertEquals($now, $expirets); + } + + /** + * Test method setExpires() mit sql fail + * + * @return void + */ + public function testSetExpiresSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `expires`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setExpires(time())); + } + + /** + * Test method setLocked(), isLocked() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetAndIsLocked() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document = self::createDocument($rootfolder, $adminuser, 'Document 1'); + $res = $document->isLocked(); + $this->assertFalse($res); + $res = $document->setLocked($user); + $this->assertTrue($res); + $res = $document->isLocked(); + $this->assertTrue($res); + $lockuser = $document->getLockingUser(); + $this->assertIsObject($lockuser); + $this->assertInstanceOf(SeedDMS_Core_User::class, $lockuser); + $this->assertEquals($user->getId(), $lockuser->getId()); + /* parameter passed to setLocked must be false or a user */ + $res = $document->setLocked(null); + /* document is still locked and locking user is unchanged */ + $res = $document->isLocked(); + $this->assertTrue($res); + $lockuser = $document->getLockingUser(); + $this->assertIsObject($lockuser); + $this->assertInstanceOf(SeedDMS_Core_User::class, $lockuser); + $this->assertEquals($user->getId(), $lockuser->getId()); + /* Unlock the document */ + $res = $document->setLocked(false); + $this->assertTrue($res); + $res = $document->isLocked(); + $this->assertFalse($res); + $lockuser = $document->getLockingUser(); + $this->assertFalse($lockuser); + } + + /** + * Test method setLocked() with sql fail + * + * @return void + */ + public function testSetLockedSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("DELETE FROM `tblDocumentLocks`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setLocked(false)); + } + + /** + * Test method getSequence() and setSequence() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetSequence() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + /* The document still has sequence = 1.0 */ + $sequence = $document->getSequence(); + $this->assertEquals(1.0, $sequence); + $ret = $document->setSequence(1.5); + $this->assertTrue($ret); + $sequence = $document->getSequence(); + $this->assertEquals(1.5, $sequence); + } + + /** + * Test method setSequence() mit sql fail + * + * @return void + */ + public function testSetSequenceSqlFail() + { + $document = $this->getMockedDocument(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblDocuments` SET `sequence`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $document->setDMS($dms); + $this->assertFalse($document->setSequence(1.1)); + } + + /** + * Test method getContentByVersion(), isLatestContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetContentByVersion() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + /* Get version 1 */ + $content = $document->getContentByVersion(1); + $this->assertIsObject($content); + $this->assertInstanceOf(SeedDMS_Core_DocumentContent::class, $content); + /* There is no version 2 */ + $content = $document->getContentByVersion(2); + $this->assertNull($content); + /* version must be numeric */ + $content = $document->getContentByVersion('foo'); + $this->assertFalse($content); + /* Check if 1 is the latest version number */ + $ret = $document->isLatestContent(1); + $this->assertTrue($ret); + $ret = $document->isLatestContent(2); + $this->assertFalse($ret); + } + + /** + * Test method getDocumentContent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentContent() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + /* Get version 1 */ + $content = $document->getContentByVersion(1); + $this->assertIsObject($content); + $this->assertInstanceOf(SeedDMS_Core_DocumentContent::class, $content); + $again = self::$dms->getDocumentContent($content->getId()); + $this->assertIsObject($again); + $this->assertInstanceOf(SeedDMS_Core_DocumentContent::class, $again); + $this->assertEquals($content->getId(), $again->getId()); + $none = self::$dms->getDocumentContent(2); + $this->assertNull($none); + } + + /** + * Test method getDocumentContent() with sql failure + * + * @return void + */ + public function testGetDocumentContentSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblDocumentContent` WHERE `id`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse($dms->getDocumentContent(1)); + } + + /** + * Test method addDocumentLink(), getDocumentLinks(), getReverseDocumentLinks() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAndGetDocumentLinks() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document1 = self::createDocument($folder, $adminuser, 'Document 1'); + $document2 = self::createDocument($folder, $adminuser, 'Document 2'); + + /* document 1 has no links */ + $links = $document1->getDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(0, $links); + $links = $document1->getReverseDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(0, $links); + + /* Adding a link to none existing target or by a none existing user fails */ + $ret = $document1->addDocumentLink(3, $user->getId(), false); + $this->assertFalse($ret); + $ret = $document1->addDocumentLink($document2->getId(), 4, false); + $this->assertFalse($ret); + + /* Adding a link with a bogus target or user must fail */ + $ret = $document1->addDocumentLink('foo', 1, false); + $this->assertFalse($ret); + $ret = $document1->addDocumentLink(3, 'foo', false); + $this->assertFalse($ret); + + /* Adding a link to myself must fail */ + $ret = $document1->addDocumentLink($document1->getId(), $user->getId(), false); + $this->assertFalse($ret); + + /* Add a non public link to document 2 by user */ + $link = $document1->addDocumentLink($document2->getId(), $user->getId(), false); + $this->assertIsObject($link); + $this->assertInstanceOf(SeedDMS_Core_DocumentLink::class, $link); + $links = $document1->getDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(1, $links); + $links = $document2->getReverseDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(1, $links); + /* There is one reverse link of a user */ + $links = $document2->getReverseDocumentLinks(false, $user); + $this->assertIsArray($links); + $this->assertCount(1, $links); + /* There are no public reverse links */ + $links = $document2->getReverseDocumentLinks(true); + $this->assertIsArray($links); + $this->assertCount(0, $links); + + /* There are no public links of document 1 */ + $document1->clearCache(); + $links = $document1->getDocumentLinks(true); + $this->assertIsArray($links); + $this->assertCount(0, $links); + + /* There are no links by adminuser of document 1 */ + $document1->clearCache(); + $links = $document1->getDocumentLinks(false, $adminuser); + $this->assertIsArray($links); + $this->assertCount(0, $links); + + /* There are links by user of document 1 */ + $document1->clearCache(); + $links = $document1->getDocumentLinks(false, $user); + $this->assertIsArray($links); + $this->assertCount(1, $links); + + $link = $document1->getDocumentLink($links[0]->getId()); + $this->assertIsObject($link); + $this->assertTrue($link->isType('documentlink')); + } + + /** + * Test method addDocumentLink(), removeDocumentLinks() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAndRemoveDocumentLink() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document1 = self::createDocument($folder, $adminuser, 'Document 1'); + $document2 = self::createDocument($folder, $adminuser, 'Document 2'); + + /* Add a non public link to document 2 by user */ + $link = $document1->addDocumentLink($document2->getId(), $user->getId(), false); + $this->assertIsObject($link); + $this->assertInstanceOf(SeedDMS_Core_DocumentLink::class, $link); + $links = $document1->getDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(1, $links); + + /* Remove the link again */ + $link = $links[0]; + $ret = $document1->removeDocumentLink($link->getId()); + $this->assertTrue($ret); + $links = $document1->getDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(0, $links); + } + + /** + * Test method addDocumentFile(), getDocumentFiles() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAndGetDocumentFiles() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + + /* document has no files */ + $files = $document->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(0, $files); + + $filename = self::createTempFile(100); + $file1 = $document->addDocumentFile('Attachment 1', '', $user, $filename, 'attachment1.txt', '.txt', 'plain/text'); + unlink($filename); + $this->assertIsObject($file1); + $this->assertInstanceOf(SeedDMS_Core_DocumentFile::class, $file1); + + $filename = self::createTempFile(100); + $file2 = $document->addDocumentFile('Attachment 2', '', $user, $filename, 'attachment2.txt', '.txt', 'plain/text', 1); + unlink($filename); + $this->assertIsObject($file2); + $this->assertInstanceOf(SeedDMS_Core_DocumentFile::class, $file2); + + /* Get all attachments */ + $files = $document->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(2, $files); + + /* Get attachments for version 1 only */ + $files = $document->getDocumentFiles(1, false); + $this->assertIsArray($files); + $this->assertCount(1, $files); + + /* Get attachments for version 1 and version independed */ + $files = $document->getDocumentFiles(1, true); + $this->assertIsArray($files); + $this->assertCount(2, $files); + } + + /** + * Test method addDocumentFile(), removeDocumentFile() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAndRemoveDocumentFiles() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + + /* document has no files */ + $files = $document->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(0, $files); + + $filename = self::createTempFile(100); + $file1 = $document->addDocumentFile('Attachment 1', '', $user, $filename, 'attachment1.txt', '.txt', 'plain/text'); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($file1); + $this->assertInstanceOf(SeedDMS_Core_DocumentFile::class, $file1); + + /* document has now 1 file */ + $files = $document->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(1, $files); + + /* Removing a file with a none exiting or bogus id must fail */ + $ret = $document->removeDocumentFile(2); + $this->assertFalse($ret); + $ret = $document->removeDocumentFile('foo'); + $this->assertFalse($ret); + + $ret = $document->removeDocumentFile($files[0]->getId()); + $this->assertTrue($ret); + + $files = $document->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(0, $files); + } + + /** + * Test method addDocument(), removeDocument() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAndRemoveDocument() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $document = self::createDocument($folder, $adminuser, 'Document 1'); + $docid = $document->getId(); + + $filename = self::createTempFile(100); + $file1 = $document->addDocumentFile('Attachment 1', '', $user, $filename, 'attachment1.txt', '.txt', 'plain/text'); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($file1); + $this->assertInstanceOf(SeedDMS_Core_DocumentFile::class, $file1); + + $ret = $document->remove(); + $this->assertTrue($ret); + $document = self::$dms->getDocument($docid); + $this->assertNull($document); + } + + /** + * Test method getUsedDiskSpace() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUsedDiskSpace() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + /* Create a document with 1234 Bytes */ + $document = self::createDocument($folder, $adminuser, 'Document 1', 1234); + $size = $document->getUsedDiskSpace(); + $this->assertEquals(1234, $size); + } + + /** + * Test method getTimeline() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetTimeline() + { + $adminuser = self::$dms->getUser(1); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + /* Create a document */ + $document = self::createDocument($folder, $adminuser, 'Document 1'); + /* Attach a file */ + $filename = self::createTempFile(100); + $file1 = $document->addDocumentFile('Attachment 1', '', $adminuser, $filename, 'attachment1.txt', '.txt', 'plain/text'); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($file1); + $this->assertInstanceOf(SeedDMS_Core_DocumentFile::class, $file1); + + /* Get the timeline. It must contain two entries + * - the initial release of the document + * - adding the attachment + */ + $timeline = $document->getTimeLine(); + $this->assertIsArray($timeline); + $this->assertCount(2, $timeline); + } + + /** + * Test method transferToUser() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testTransferToUser() + { + $adminuser = self::$dms->getUser(1); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($user); + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + /* Create two documents */ + $document1 = self::createDocument($folder, $adminuser, 'Document 1'); + $document2 = self::createDocument($folder, $adminuser, 'Document 2'); + + /* Attach a file */ + $filename = self::createTempFile(100); + $file1 = $document1->addDocumentFile('Attachment 1', '', $adminuser, $filename, 'attachment1.txt', '.txt', 'plain/text'); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + $this->assertIsObject($file1); + $this->assertInstanceOf(SeedDMS_Core_DocumentFile::class, $file1); + + /* Add a non public link to document 2 */ + $link = $document1->addDocumentLink($document2->getId(), $adminuser->getId(), false); + $this->assertIsObject($link); + $this->assertInstanceOf(SeedDMS_Core_DocumentLink::class, $link); + + /* Transfer document to $user */ + $this->assertEquals('admin', $document1->getOwner()->getLogin()); + $links = $document1->getDocumentLinks(false, $adminuser); + $this->assertIsArray($links); + $this->assertCount(1, $links); + + $ret = $document1->transferToUser($user); + $this->assertTrue($ret); + $this->assertEquals('user1', $document1->getOwner()->getLogin()); + $links = $document1->getDocumentLinks(false, $user); + $this->assertIsArray($links); + $this->assertCount(1, $links); + $files = $document1->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(1, $files); + $this->assertEquals($files[0]->getUserID(), $user->getId()); + } +} diff --git a/SeedDMS_Core/tests/FileUtilsTest.php b/SeedDMS_Core/tests/FileUtilsTest.php new file mode 100644 index 000000000..7e1b87cfb --- /dev/null +++ b/SeedDMS_Core/tests/FileUtilsTest.php @@ -0,0 +1,219 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class FileUtilsTest extends SeedDmsTest +{ + /** + * Create temporary directory + * + * @return void + */ + protected function setUp(): void + { + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method format_filesize() + * + * @return void + */ + public function testFormatFileSize() + { + $this->assertEquals('1 Byte', SeedDMS_Core_File::format_filesize(1)); + $this->assertEquals('0 Bytes', SeedDMS_Core_File::format_filesize(0)); + $this->assertEquals('1000 Bytes', SeedDMS_Core_File::format_filesize(1000)); + $this->assertEquals('1 KiB', SeedDMS_Core_File::format_filesize(1024)); + $this->assertEquals('1 KiB', SeedDMS_Core_File::format_filesize(1025)); + $this->assertEquals('2 KiB', SeedDMS_Core_File::format_filesize(2047)); + $this->assertEquals('1 MiB', SeedDMS_Core_File::format_filesize(1024*1024)); + $this->assertEquals('1 GiB', SeedDMS_Core_File::format_filesize(1024*1024*1024)); + } + + /** + * Test method format_filesize() + * + * @return void + */ + public function testParseFileSize() + { + $this->assertEquals(200, SeedDMS_Core_File::parse_filesize('200B')); + $this->assertEquals(200, SeedDMS_Core_File::parse_filesize('200 B')); + $this->assertEquals(200, SeedDMS_Core_File::parse_filesize('200')); + $this->assertEquals(1024, SeedDMS_Core_File::parse_filesize('1K')); + $this->assertEquals(2*1024*1024, SeedDMS_Core_File::parse_filesize('2M')); + $this->assertEquals(3*1024*1024*1024, SeedDMS_Core_File::parse_filesize('3 G')); + $this->assertEquals(4*1024*1024*1024*1024, SeedDMS_Core_File::parse_filesize('4 T')); + $this->assertFalse(SeedDMS_Core_File::parse_filesize('4 t')); + $this->assertFalse(SeedDMS_Core_File::parse_filesize('-4T')); + } + + /** + * Test method fileSize() + * + * @return void + */ + public function testFileSize() + { + $filename = self::createTempFile(200, self::$contentdir); + $this->assertEquals(200, SeedDMS_Core_File::fileSize($filename)); + /* Getting the size of a none existing file returns false */ + $this->assertFalse(SeedDMS_Core_File::fileSize('foobar')); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + } + + /** + * Test method file_exists() + * + * @return void + */ + public function testFileExists() + { + $filename = self::createTempFile(200, self::$contentdir); + $this->assertTrue(SeedDMS_Core_File::file_exists($filename)); + $this->assertFalse(SeedDMS_Core_File::file_exists($filename.'bla')); + $this->assertTrue(SeedDMS_Core_File::removeFile($filename)); + } + + /** + * Test method fileExtension() + * + * @return void + */ + public function testFileExtension() + { + $this->assertEquals('png', SeedDMS_Core_File::fileExtension('image/png')); + $this->assertEquals('', SeedDMS_Core_File::fileExtension('image/kpng')); + $this->assertEquals('txt', SeedDMS_Core_File::fileExtension('text/plain')); + $this->assertEquals('md', SeedDMS_Core_File::fileExtension('text/markdown')); + } + + /** + * Test method moveFile() + * + * @return void + */ + public function testMoveFile() + { + $filename = self::createTempFile(200, self::$contentdir); + $this->assertEquals(200, SeedDMS_Core_File::fileSize($filename)); + $ret = SeedDMS_Core_File::moveFile($filename, self::$contentdir.DIRECTORY_SEPARATOR."foobar"); + $this->assertTrue($ret); + /* Getting the file size of the old doc must fail now */ + $this->assertFalse(SeedDMS_Core_File::fileSize($filename)); + /* Getting the file size of the new doc succeds */ + $this->assertEquals(200, SeedDMS_Core_File::fileSize(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + $this->assertTrue(SeedDMS_Core_File::removeFile(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + } + + /** + * Test method makeDir(), renameDir(), removeDir() + * + * @return void + */ + public function testMakeRenameAndRemoveDir() + { + /* Create a directory and put a file into it */ + $ret = SeedDMS_Core_File::makeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar"); + system('touch '.self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."tt"); + /* Rename the directory */ + $ret = SeedDMS_Core_File::renameDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar", self::$contentdir.DIRECTORY_SEPARATOR."bazfoo"); + $this->assertTrue($ret); + /* The new must exist and the old one is gone */ + $this->assertTrue(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertFalse(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + $this->assertTrue(SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertFalse(SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertFalse(SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + + /* Create a directory, a sub directory and a file */ + $ret = SeedDMS_Core_File::makeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar"); + $this->assertTrue($ret); + $ret = SeedDMS_Core_File::makeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."bazfoo"); + $this->assertTrue($ret); + system('touch '.self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."bazfoo".DIRECTORY_SEPARATOR."tt"); + $this->assertTrue(SeedDMS_Core_File::file_exists(self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."bazfoo".DIRECTORY_SEPARATOR."tt")); + + $ret = SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar"); + $this->assertTrue($ret); + $this->assertFalse(SeedDMS_Core_File::file_exists(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + $this->assertFalse(SeedDMS_Core_File::file_exists(self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."bazfoo")); + $this->assertFalse(SeedDMS_Core_File::file_exists(self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."bazfoo".DIRECTORY_SEPARATOR."tt")); + } + + /** + * Test method makeDir(), copyDir(), removeDir() + * + * @return void + */ + public function testMakeCopyAndRemoveDir() + { + /* Create a directory and put a file into it */ + $ret = SeedDMS_Core_File::makeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar"); + system('touch '.self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."tt"); + /* Rename the directory */ + $ret = SeedDMS_Core_File::copyDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar", self::$contentdir.DIRECTORY_SEPARATOR."bazfoo"); + $this->assertTrue($ret); + /* The new and the old dir must exist */ + $this->assertTrue(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertTrue(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + $this->assertTrue(SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertTrue(SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + } + + /** + * Test method moveDir() + * + * @return void + */ + public function testMakeAndMoveDir() + { + /* Create a directory and put a file into it */ + $ret = SeedDMS_Core_File::makeDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar"); + system('touch '.self::$contentdir.DIRECTORY_SEPARATOR."foobar".DIRECTORY_SEPARATOR."tt"); + /* Rename the directory */ + $ret = SeedDMS_Core_File::moveDir(self::$contentdir.DIRECTORY_SEPARATOR."foobar", self::$contentdir.DIRECTORY_SEPARATOR."bazfoo"); + $this->assertTrue($ret); + /* The new must exist and the old dir must be disappeared */ + $this->assertTrue(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertFalse(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."foobar")); + $this->assertTrue(SeedDMS_Core_File::removeDir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + $this->assertFalse(is_dir(self::$contentdir.DIRECTORY_SEPARATOR."bazfoo")); + } +} diff --git a/SeedDMS_Core/tests/FolderTest.php b/SeedDMS_Core/tests/FolderTest.php new file mode 100644 index 000000000..ee05f0b8c --- /dev/null +++ b/SeedDMS_Core/tests/FolderTest.php @@ -0,0 +1,1221 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class FolderTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getInstance() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetInstanceRootFolder() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $this->assertIsObject($folder); + $this->assertEquals('DMS', $folder->getName()); + /* get instance of none existing folder */ + $folder = SeedDMS_Core_Folder::getInstance(2, self::$dms); + $this->assertNull($folder); + } + + /** + * Test method isType() + * + * @return void + */ + public function testIsType() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $this->assertTrue($folder->isType('folder')); + } + + /** + * Test method getInstance() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetInstance() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + /* Get the folder with id 2, which must be 'Subfolder 1' */ + $folder = SeedDMS_Core_Folder::getInstance(2, self::$dms); + $this->assertIsObject($folder); + $this->assertEquals('Subfolder 1', $folder->getName()); + /* Get a none existing folder */ + $folder = SeedDMS_Core_Folder::getInstance(4, self::$dms); + $this->assertNull($folder); + } + + /** + * Test method getInstance() + * + * @return void + */ + public function testGetInstanceSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse(SeedDMS_Core_Folder::getInstance(1, $dms)); + } + + /** + * Test method getInstanceByName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetInstanceByName() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + /* Search for it anywhere in the folder hierarchy */ + $folder = SeedDMS_Core_Folder::getInstanceByName('Subsubfolder 1', null, self::$dms); + $this->assertIsObject($folder); + $this->assertEquals('Subsubfolder 1', $folder->getName()); + /* Search for it within 'Subfolder 1' will find it */ + $folder = SeedDMS_Core_Folder::getInstanceByName('Subsubfolder 1', $subfolder, self::$dms); + $this->assertIsObject($folder); + $this->assertEquals('Subsubfolder 1', $folder->getName()); + /* Search for it within root folder will not find it */ + $folder = SeedDMS_Core_Folder::getInstanceByName('Subsubfolder 1', $rootfolder, self::$dms); + $this->assertNull($folder); + } + + /** + * Test method getInstanceByName() + * + * @return void + */ + public function testGetInstanceByNameSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $this->assertFalse(SeedDMS_Core_Folder::getInstanceByName('foo', null, $dms)); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetName() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $name = $folder->getName(); + $this->assertEquals('DMS', $name); + $ret = $folder->setName('foo'); + $this->assertTrue($ret); + $name = $folder->getName(); + $this->assertEquals('foo', $name); + } + + /** + * Test method setName() + * + * @return void + */ + public function testSetNameSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `name`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setName('foo')); + } + + /** + * Test method getComment() and setComment() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetComment() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $comment = $folder->getComment(); + $this->assertEquals('DMS root', $comment); + $ret = $folder->setComment('foo'); + $this->assertTrue($ret); + $comment = $folder->getComment(); + $this->assertEquals('foo', $comment); + } + + /** + * Test method setComment() + * + * @return void + */ + public function testSetCommentSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `comment`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setComment('foo')); + } + + /** + * Test method getSequence() and setSequence() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetSequence() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + /* The root folder's sequence in the initial database is 0.0 */ + $sequence = $folder->getSequence(); + $this->assertEquals(0.0, $sequence); + $ret = $folder->setSequence(1.5); + $this->assertTrue($ret); + $sequence = $folder->getSequence(); + $this->assertEquals(1.5, $sequence); + } + + /** + * Test method setSequence() + * + * @return void + */ + public function testSetSequenceSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `sequence`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setSequence(0.0)); + } + + /** + * Test method getDate() and setDate() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetDate() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $now = time(); + /* Passing false as a time stamp will take current time stamp */ + $ret = $folder->setDate(false); + $this->assertTrue($ret); + $date = $folder->getDate(); + $this->assertEquals($now, $date); + /* Setting a time stamp */ + $now -= 1000; + $ret = $folder->setDate($now); + $this->assertTrue($ret); + $date = $folder->getDate(); + $this->assertEquals($now, $date); + /* Setting a none numeric value will fail */ + $ret = $folder->setDate('foo'); + $this->assertFalse($ret); + } + + /** + * Test method setDate() + * + * @return void + */ + public function testSetDateSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `date`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setDate(time())); + } + + /** + * Test method getParent() + * + * Get parent of root folder which is always null. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetParentRootFolder() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $parent = $folder->getParent(); + $this->assertNull($parent); + } + + /** + * Test method getParent() + * + * Create a new subfolder below root folder and check if parent + * of the folder is the root folder. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetParent() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $adminuser, 0); + $parent = $subfolder->getParent(); + $this->assertIsObject($parent); + $this->assertInstanceOf(SeedDMS_Core_Folder::class, $parent); + $this->assertEquals(1, $parent->getId()); + } + + /** + * Test method setParent() on root folder + * + * Moving the root folder will always fail + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetParentRootFolder() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $ret = $folder->setParent(1); + $this->assertFalse($ret); + } + + /** + * Test method getOwner() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetOwner() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $owner = $folder->getOwner(); + $this->assertIsObject($owner); + $this->assertInstanceOf(SeedDMS_Core_User::class, $owner); + $this->assertEquals(1, $owner->getId()); + } + + /** + * Test method setOwner() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetOwner() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->addUser('user1', 'user1', 'User One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $adminuser, 0); + $res = $subfolder->setOwner($user); + $this->assertTrue($res); + $owner = $subfolder->getOwner(); + $this->assertIsObject($owner); + $this->assertInstanceOf(SeedDMS_Core_User::class, $owner); + $this->assertEquals($user->getId(), $owner->getId()); + } + + /** + * Test method setOwner() + * + * @return void + */ + public function testSetOwnerSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $user = new SeedDMS_Core_User(1, 'admin', 'pass', 'Joe Foo', 'baz@foo.de', 'en_GB', 'bootstrap', 'My comment', SeedDMS_Core_User::role_admin); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `owner`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setOwner($user)); + } + + /** + * Test method getDefaultAccess() + * + * The default access is always M_READ unless it was set differently + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDefaultAccess() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $accessmode = $rootfolder->getDefaultAccess(); + $this->assertEquals(M_READ, $accessmode); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $adminuser, 0); + $accessmode = $subfolder->getDefaultAccess(); + $this->assertEquals(M_READ, $accessmode); + } + + /** + * Test method setDefaultAccess() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetDefaultAccess() + { + $adminuser = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $adminuser, 0); + /* Setting the default access to something != M_READ will not have + * any effect as long as inheritage of access rights is turned on. + */ + $subfolder->setDefaultAccess(M_READWRITE, true); + $accessmode = $subfolder->getDefaultAccess(); + $this->assertEquals(M_READ, $accessmode); + /* Turning inheritage off will use the default access */ + $subfolder->setInheritAccess(false, true); + $accessmode = $subfolder->getDefaultAccess(); + $this->assertEquals(M_READWRITE, $accessmode); + } + + /** + * Test method setDefaultAccess() + * + * @return void + */ + public function testSetDefaultAccessSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `defaultAccess`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setDefaultAccess(M_NONE)); + } + + /** + * Test method setInheritAccess() + * + * @return void + */ + public function testSetInheritAccessSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblFolders` SET `inheritAccess`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->setInheritAccess(true)); + } + + /** + * Test method hasSubFolders() on root folder and after adding + * new subfolders. + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testHasSubFolders() + { + $user = self::$dms->getUser(1); + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $ret = $rootfolder->hasSubFolders(); + $this->assertIsInt($ret); + $this->assertEquals(0, $ret); + $subfolder1 = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subfolder2 = $rootfolder->addSubFolder('Subfolder 2', '', $user, 1.0); + $ret = $rootfolder->hasSubFolders(); + $this->assertIsInt($ret); + $this->assertEquals(2, $ret); + /* hasSubFolderByName() just returns true or false */ + $ret = $rootfolder->hasSubFolderByName('Subfolder 1'); + $this->assertTrue($ret); + $ret = $rootfolder->hasSubFolderByName('Subfolder 3'); + $this->assertFalse($ret); + } + + /** + * Test method hasSubFolders with sql fail() + * + * @return void + */ + public function testHasSubFoldersSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT count(*) as c FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->hasSubFolders()); + } + + /** + * Test method hasSubFolderByName with sql fail() + * + * @return void + */ + public function testHasSubFolderByNameSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT count(*) as c FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->hasSubFolderByName('foo')); + } + + /** + * Test method getSubFolders() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetSubFoldersRootOnly() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $folders = $folder->getSubFolders(); + $this->assertIsArray($folders); + $this->assertCount(0, $folders); + } + + /** + * Test method getSubFolders() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetSubFolders() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder1 = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subfolder2 = $rootfolder->addSubFolder('Subfolder 2', '', $user, 1.0); + $folders = $rootfolder->getSubFolders(); + $this->assertIsArray($folders); + $this->assertCount(2, $folders); + + /* Get sub folders order by name descending */ + $rootfolder->clearCache(); // Force retrieving sub folders from database + $folders = $rootfolder->getSubFolders('n', 'desc', 1, 0); + $this->assertIsArray($folders); + $this->assertCount(1, $folders); + $this->assertEquals('Subfolder 2', $folders[0]->getName()); + + /* Get sub folders order by name descending with an offset of 1 */ + $rootfolder->clearCache(); // Force retrieving sub folders from database + $folders = $rootfolder->getSubFolders('n', 'desc', 1, 1); + $this->assertIsArray($folders); + $this->assertCount(1, $folders); + $this->assertEquals('Subfolder 1', $folders[0]->getName()); + + /* Get sub folders order by sequence ascending */ + $rootfolder->clearCache(); // Force retrieving sub folders from database + $folders = $rootfolder->getSubFolders('s', 'asc', 1, 0); + $this->assertIsArray($folders); + $this->assertCount(1, $folders); + $this->assertEquals('Subfolder 2', $folders[0]->getName()); + + /* Get sub folders order by sequence ascending with a bogus offset */ + $rootfolder->clearCache(); // Force retrieving sub folders from database + $folders = $rootfolder->getSubFolders('s', 'asc', 0, 4); + $this->assertIsArray($folders); + $this->assertCount(2, $folders); + $this->assertEquals('Subfolder 2', $folders[0]->getName()); + } + + /** + * Test method getSubFolders() + * + * @return void + */ + public function testGetSubFoldersSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->getSubFolders()); + } + + /** + * Test method isDescendant() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsDescendant() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + /* subsubfolder is a descendant of root folder */ + $this->assertTrue($subsubfolder->isDescendant($rootfolder)); + /* subfolder is not a descendant of subsubfolder */ + $this->assertFalse($subfolder->isDescendant($subsubfolder)); + } + + /** + * Test method isSubFolder() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsSubFolder() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + $this->assertTrue($rootfolder->isSubFolder($subsubfolder)); + $this->assertFalse($subsubfolder->isSubFolder($subfolder)); + } + + /** + * Test method setParent() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetParent() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + /* Add a new document for folderList checking afterwards */ + $document = self::createDocument($subsubfolder, $user, 'Document 1'); + $folderlist = $subsubfolder->getFolderList(); + $this->assertEquals(':1:2:', $folderlist); + $folderlist = $document->getFolderList(); + $this->assertEquals(':1:2:3:', $folderlist); + /* Making $subsubfolder parent of $subfolder will fail, because + * $subfolder is a parent of $subsubfolder + */ + $this->assertFalse($subfolder->setParent($subsubfolder)); + /* Moving $subsubfolder into rool folder is possible */ + $this->assertTrue($subsubfolder->setParent($rootfolder)); + /* Root folder has now two children */ + $children = $rootfolder->getSubFolders(); + $this->assertIsArray($children); + $this->assertCount(2, $children); + /* Move the folder will have changed the folder list. Check it */ + $errors = self::$dms->checkFolders(); + $this->assertIsArray($errors); + $this->assertCount(0, $errors); + $errors = self::$dms->checkDocuments(); + $this->assertIsArray($errors); + $this->assertCount(0, $errors); + } + + /** + * Test method getPath() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetPathRootOnly() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $path = $folder->getPath(); + $this->assertIsArray($path); + $this->assertCount(1, $path); + /* The only folder in the path is the root folder itself */ + $this->assertEquals(1, $path[0]->getId()); + } + + /** + * Test method getPath() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetPath() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + $path = $subsubfolder->getPath(); + $this->assertIsArray($path); + $this->assertCount(3, $path); + } + + /** + * Test method getFolderPathPlain() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetFolderPathPlain() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $path = $folder->getFolderPathPlain(); + $this->assertIsString($path); + $this->assertEquals('/ DMS', $path); + } + + /** + * Test method hasDocuments() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testHasDocuments() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $documents = $folder->hasDocuments(); + $this->assertIsInt($documents); + $this->assertEquals(0, $documents); + /* Add a new document for calling hasDocuments() afterwards */ + $document = self::createDocument($folder, $user, 'Document 1'); + $documents = $folder->hasDocuments(); + $this->assertIsInt($documents); + $this->assertEquals(1, $documents); + } + + /** + * Test method hasDocuments() + * + * @return void + */ + public function testHasDokumentsSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT count(*) as c FROM `tblDocuments`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->hasDocuments()); + } + + /** + * Test method hasDocumentByName() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testHasDocumentByName() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $res = $folder->hasDocumentByName('foo'); + $this->assertFalse($res); + /* Add a new document for calling hasDocumentByName() afterwards */ + $document = self::createDocument($folder, $user, 'Document 1'); + $res = $folder->hasDocumentByName('Document 1'); + $this->assertTrue($res); + } + + /** + * Test method hasDocumentByName() + * + * @return void + */ + public function testHasDokumentByNameSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT count(*) as c FROM `tblDocuments`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->hasDocumentByName('foo')); + } + + /** + * Test method getDocuments() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocuments() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $documents = $folder->getDocuments(); + $this->assertIsArray($documents); + $this->assertCount(0, $documents); + /* Add a new document for calling getDocuments() afterwards */ + $folder->clearCache(); + $document = self::createDocument($folder, $user, 'Document 1'); + $document = self::createDocument($folder, $user, 'Document 2'); + $documents = $folder->getDocuments(); + $this->assertIsArray($documents); + $this->assertCount(2, $documents); + $folder->clearCache(); + /* sort by name asc, limit 1, offset 0 */ + $documents = $folder->getDocuments('n', 'asc', 1); + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + $this->assertEquals('Document 1', $documents[0]->getName()); + $folder->clearCache(); + /* sort by name desc, limit 1, offset 0 */ + $documents = $folder->getDocuments('n', 'desc', 1); + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + $this->assertEquals('Document 2', $documents[0]->getName()); + $folder->clearCache(); + /* sort by name asc, limit 1, offset 1 */ + $documents = $folder->getDocuments('n', 'asc', 1, 1); + $this->assertIsArray($documents); + $this->assertCount(1, $documents); + $this->assertEquals('Document 2', $documents[0]->getName()); + } + + /** + * Test method getDocuments() + * + * @return void + */ + public function testGetDokumentsSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lock` FROM `tblDocuments`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->getDocuments()); + } + + /** + * Test method countChildren() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCountChildren() + { + $folder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $count = $folder->countChildren($user, 0); + $this->assertIsArray($count); + $this->assertCount(4, $count); + $this->assertEquals(0, $count['folder_count']); + $this->assertEquals(0, $count['document_count']); + /* Add some folders and documents */ + $this->createSimpleFolderStructure(); + $document = self::createDocument($folder, $user, 'Document 1'); + $count = $folder->countChildren($user, 6); + $this->assertIsArray($count); + $this->assertCount(4, $count); + $this->assertEquals(5, $count['folder_count']); + $this->assertEquals(1, $count['document_count']); + } + + /** + * Test method emptyFolder() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testEmptyFolder() + { + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + /* Add some folders and documents */ + $this->createSimpleFolderStructure(); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $res = $rootfolder->emptyFolder(); + $this->assertTrue($res); + } + + /** + * Test method emptyFolder() on root folder + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testEmptyFolderWithCallback() + { + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + /* Add some folders and documents */ + $this->createSimpleFolderStructure(); + $document = self::createDocument($rootfolder, $user, 'Document 1'); + + /* Add the 'onPostAddUser' callback */ + $msgs = []; + $callback = function ($param, $object) use (&$msgs) { + $msgs[] = $param." ".$object->getName(). " (".$object->getId().")"; + }; + self::$dms->addCallback('onPreRemoveFolder', $callback, 'onPreRemoveFolder'); + self::$dms->addCallback('onPostRemoveFolder', $callback, 'onPostRemoveFolder'); + self::$dms->addCallback('onPreRemoveDocument', $callback, 'onPreRemoveDocument'); + self::$dms->addCallback('onPostRemoveDocument', $callback, 'onPostRemoveDocument'); + self::$dms->addCallback('onPreEmptyFolder', $callback, 'onPreEmptyFolder'); + self::$dms->addCallback('onPostEmptyFolder', $callback, 'onPostEmptyFolder'); + + $res = $rootfolder->emptyFolder(); + $this->assertTrue($res); + $this->assertIsArray($msgs); + $this->assertCount(14, $msgs); // 5 folders x 2 callbacks + 1 document x 2 callbacks + 2 emptyFolder callbacks + } + + /** + * Test method getAccessList() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAccessList() + { + /* Add some folders and documents */ + $this->createSimpleFolderStructure(); + $this->createGroupsAndUsers(); + + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + $group = self::$dms->getGroup(1); + $this->assertIsObject($group); + $subfolder = self::$dms->getFolderByName('Subfolder 1'); + $this->assertIsObject($subfolder); + $subsubfolder = self::$dms->getFolderByName('Subsubfolder 1'); + $this->assertIsObject($subsubfolder); + + /* Adding an access rule will have no effect until the inheritance + * is turned off. + */ + $subfolder->addAccess(M_NONE, $user->getId(), true); + $subfolder->addAccess(M_READWRITE, $group->getId(), false); + $accesslist = $subfolder->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['users']); + $this->assertCount(0, $accesslist['groups']); + /* Turn inheritance off */ + $res = $subfolder->setInheritAccess(false); + $this->assertTrue($res); + /* Now the access rules on $subfolder take effect */ + $accesslist = $subfolder->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(1, $accesslist['users']); + $this->assertCount(1, $accesslist['groups']); + /* get list of users/groups which no access */ + $accesslist = $subfolder->getAccessList(M_NONE, O_EQ); + $this->assertIsArray($accesslist); + $this->assertCount(1, $accesslist['users']); + $this->assertCount(0, $accesslist['groups']); + /* get list of users/groups which read+write access */ + $accesslist = $subfolder->getAccessList(M_READWRITE, O_EQ); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['users']); + $this->assertCount(1, $accesslist['groups']); + /* get list of users/groups which have at least read access */ + $accesslist = $subfolder->getAccessList(M_READ, O_GTEQ); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['users']); + $this->assertCount(1, $accesslist['groups']); + /* get list of users/groups which have at least unlimited access */ + $accesslist = $subfolder->getAccessList(M_ALL, O_GTEQ); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['users']); + $this->assertCount(0, $accesslist['groups']); + /* Subsubfolder 1 inherits from Subfolder 1 */ + $accesslist = $subsubfolder->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(1, $accesslist['users']); + $this->assertCount(1, $accesslist['groups']); + /* clear the access list */ + $res = $subfolder->clearAccessList(); + $this->assertTrue($res); + $accesslist = $subfolder->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['users']); + $this->assertCount(0, $accesslist['groups']); + /* calling getAccessList() on the $subsubfolder will still return + * the user and group, because the getParent() call in getAccessList() + * will not return the same instance like $subfolder. Hence calling + * $subfolder->clearAccessList() won't clear the accesslist of $subsubfolder's + * parent. You would have to explicitly + * clear acceslist of $subsubfolder's parent. + $res = $subsubfolder->getParent()->clearAccessList(); + $this->assertTrue($res); + $accesslist = $subsubfolder->getAccessList(); + $this->assertIsArray($accesslist); + $this->assertCount(0, $accesslist['users']); + $this->assertCount(0, $accesslist['groups']); + */ + } + + /** + * Test method addAccess() + * + * @return void + */ + public function testAddAccessWrongMode() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->addAccess(M_ANY, 1, true)); + } + + /** + * Test method addAccess() + * + * @return void + */ + public function testAddAccessSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("INSERT INTO `tblACLs`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->addAccess(M_NONE, 1, true)); + } + + /** + * Test method getAccessMode() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAccessMode() + { + /* Add some folders and documents */ + $this->createSimpleFolderStructureWithDocuments(); + $this->createGroupsAndUsers(); + + $rootfolder = SeedDMS_Core_Folder::getInstance(1, self::$dms); + $admin = self::$dms->getUser(1); + $this->assertIsObject($admin); + $this->assertTrue($admin->isAdmin()); + $guest = self::$dms->getUser(2); + $this->assertTrue($guest->isGuest()); + $user = self::$dms->getUser(3); + $this->assertIsObject($user); + if(self::$dms->version[0] == '5') + $this->assertTrue($user->getRole() == SeedDMS_Core_User::role_user); + else + $this->assertTrue($user->getRole()->getRole() == SeedDMS_Core_Role::role_user); + $joe = self::$dms->getUser(4); + $this->assertIsObject($joe); + if(self::$dms->version[0] == '5') + $this->assertTrue($joe->getRole() == SeedDMS_Core_User::role_user); + else + $this->assertTrue($joe->getRole()->getRole() == SeedDMS_Core_Role::role_user); + $sally = self::$dms->getUser(6); + $this->assertIsObject($sally); + if(self::$dms->version[0] == '5') + $this->assertTrue($sally->getRole() == SeedDMS_Core_User::role_user); + else + $this->assertTrue($sally->getRole()->getRole() == SeedDMS_Core_Role::role_user); + $group = self::$dms->getGroup(1); + $this->assertIsObject($group); + /* add guest and joe to group */ + if(!$group->isMember($guest)) { + $res = $guest->joinGroup($group); + $this->assertTrue($res); + } + if(!$group->isMember($joe)) { + $res = $joe->joinGroup($group); + $this->assertTrue($res); + } + + $subfolder1 = self::$dms->getFolderByName('Subfolder 1'); + $this->assertIsObject($subfolder1); + $subsubfolder = self::$dms->getFolderByName('Subsubfolder 1'); + $this->assertIsObject($subsubfolder); + $subfolder2 = self::$dms->getFolderByName('Subfolder 2'); + $this->assertIsObject($subfolder2); + $subfolder3 = self::$dms->getFolderByName('Subfolder 3'); + $this->assertIsObject($subfolder3); + $res = $subfolder3->setOwner($sally); + $this->assertTrue($res); + + /* Setup Subfolder 1: + * no inheritance, user has read-write access, group has unlimited access, + * default is no access + */ + $res = $subfolder1->setInheritAccess(false); + $this->assertTrue($res); + $res = $subfolder1->setDefaultAccess(M_NONE); + $this->assertTrue($res); + $res = $subfolder1->addAccess(M_READWRITE, $user->getId(), true); + $this->assertTrue($res); + $res = $subfolder1->addAccess(M_ALL, $group->getId(), false); + $this->assertTrue($res); + + /* Admin has always access mode M_ALL */ + $mode = $subfolder1->getAccessMode($admin); + $this->assertEquals(M_ALL, $mode); + /* Guest has max read access, though it's group has any access */ + $mode = $subfolder1->getAccessMode($guest); + $this->assertEquals(M_READ, $mode); + /* Joe has any access, because it's group has any access */ + $mode = $subfolder1->getAccessMode($joe); + $this->assertEquals(M_ALL, $mode); + /* Sally has no access, because it has no explicit access right and the + * default access is M_NONE. + */ + $mode = $subfolder1->getAccessMode($sally); + $this->assertEquals(M_NONE, $mode); + + /* Subfolder 3 inherits from the root folder, but sally is the owner */ + $mode = $subfolder3->getAccessMode($sally); + $this->assertEquals(M_ALL, $mode); + /* joe has just read access which is the default inherited from root */ + $mode = $subfolder3->getAccessMode($joe); + $this->assertEquals(M_READ, $mode); + + } + + /** + * Test method getFolderList() + * + * @return void + */ + public function testGetFolderListSqlFail() + { + $rootfolder = $this->getMockedRootFolder(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT `folderList` FROM `tblFolders`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $rootfolder->setDMS($dms); + $this->assertFalse($rootfolder->getFolderList()); + } + +} diff --git a/SeedDMS_Core/tests/GroupTest.php b/SeedDMS_Core/tests/GroupTest.php new file mode 100644 index 000000000..df14a13f5 --- /dev/null +++ b/SeedDMS_Core/tests/GroupTest.php @@ -0,0 +1,410 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class GroupTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Create a mock group object + * + * @return SeedDMS_Core_Group + */ + protected function getMockGroup() + { + $user = $this->getMockBuilder(SeedDMS_Core_Group::class) + ->onlyMethods([]) + ->disableOriginalConstructor()->getMock(); + return $user; + } + + /** + * Create a mock group object + * + * @return SeedDMS_Core_Group + */ + protected function getGroup() + { + $group = new SeedDMS_Core_Group(1, 'foogroup', 'My comment'); + return $group; + } + + /** + * Create a mock regular user object + * + * @return SeedDMS_Core_User + */ + protected function getUser() + { + $user = new SeedDMS_Core_User(2, 'user', 'pass', 'Joe Baz', 'joe@foo.de', 'en_GB', 'bootstrap', 'My comment', SeedDMS_Core_User::role_user); + return $user; + } + + /** + * Test method isType() + * + * @return void + */ + public function testIsType() + { + $group = $this->getGroup(); + $this->assertTrue($group->isType('group')); + } + + /** + * Test method getName() + * + * @return void + */ + public function testGetName() + { + $group = $this->getGroup(); + $this->assertEquals('foogroup', $group->getName()); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetName() + { + $group = self::$dms->addGroup('Group', ''); + $ret = $group->setName('foo'); + $this->assertTrue($ret); + $name = $group->getName(); + $this->assertEquals('foo', $name); + /* Setting an empty name must fail */ + $ret = $group->setName(' '); + $this->assertFalse($ret); + } + + /** + * Test method setName() + * + * @return void + */ + public function testSetNameSqlFail() + { + $group = $this->getGroup(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblGroups` SET `name`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $group->setDMS($dms); + $this->assertFalse($group->setName('my name')); + } + + /** + * Test method getComment() + * + * @return void + */ + public function testGetComment() + { + $group = $this->getGroup(); + $this->assertEquals('My comment', $group->getComment()); + } + + /** + * Test method getComment() and setComment() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetComment() + { + $group = self::$dms->addGroup('Group', ''); + $ret = $group->setComment('foo'); + $this->assertTrue($ret); + $comment = $group->getComment(); + $this->assertEquals('foo', $comment); + } + + /** + * Test method setComment() + * + * @return void + */ + public function testSetCommentSqlFail() + { + $group = $this->getGroup(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblGroups` SET `comment`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $group->setDMS($dms); + $this->assertFalse($group->setComment('my comment')); + } + + /** + * Test method getUsers() + * + * @return void + */ + public function testGetUsersSqlFail() + { + $group = $this->getGroup(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT `tblUsers`.* FROM `tblUsers`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $group->setDMS($dms); + $this->assertFalse($group->getUsers()); + } + + /** + * Test method addUser(), isMember(), and removeUser() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAddAndRemoveUser() + { + $group = self::$dms->addGroup('Group', ''); + if(self::$dms->version[0] == '5') + $role = SeedDMS_Core_User::role_user; + else { + $role = SeedDMS_Core_Role::getInstance(3, self::$dms); + $this->assertIsObject($role); + $this->assertEquals($role->getRole(), SeedDMS_Core_Role::role_user); + } + $user1 = self::$dms->addUser('joe', 'pass', 'Joe Foo', 'joe@foo.de', 'en_GB', 'bootstrap', 'My comment', $role); + $user2 = self::$dms->addUser('sally', 'pass', 'Sally Foo', 'sally@foo.de', 'en_GB', 'bootstrap', 'My comment', $role); + + /* Add user1 and user2. user2 is also a manager */ + $ret = $group->addUser($user1); + $this->assertTrue($ret); + $ret = $group->addUser($user2, true); + $this->assertTrue($ret); + + $users = $group->getUsers(); + $this->assertIsArray($users); + $this->assertCount(2, $users); + + $ret = $group->removeUser($user1); + $this->assertTrue($ret); + $users = $group->getUsers(); + $this->assertIsArray($users); + $this->assertCount(1, $users); + } + + /** + * Test method isMember(), toggleManager() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsMember() + { + $group = self::$dms->addGroup('Group', ''); + $user1 = self::$dms->addUser('joe', 'pass', 'Joe Foo', 'joe@foo.de', 'en_GB', 'bootstrap', 'My comment'); + $user2 = self::$dms->addUser('sally', 'pass', 'Sally Foo', 'sally@foo.de', 'en_GB', 'bootstrap', 'My comment'); + + /* Add user1 and user2. user2 is also a manager */ + $ret = $group->addUser($user1); + $this->assertTrue($ret); + $ret = $group->addUser($user2, true); + $this->assertTrue($ret); + + /* user1 is a member but not a manager */ + $ret = $group->isMember($user1); + $this->assertTrue($ret); + $ret = $group->isMember($user1, true); + $this->assertFalse($ret); + + /* user2 is a member and a manager */ + $ret = $group->isMember($user2, true); + $this->assertTrue($ret); + } + + /** + * Test method toggleManager() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testToggleManager() + { + $group = self::$dms->addGroup('Group', ''); + $user1 = self::$dms->addUser('joe', 'pass', 'Joe Foo', 'joe@foo.de', 'en_GB', 'bootstrap', 'My comment'); + + /* Add user1 */ + $ret = $group->addUser($user1); + $this->assertTrue($ret); + + /* user1 is a member but not a manager */ + $ret = $group->isMember($user1); + $this->assertTrue($ret); + $ret = $group->isMember($user1, true); + $this->assertFalse($ret); + + /* Toggle manager mode of user 1 and check again */ + $ret = $group->toggleManager($user1); + $ret = $group->isMember($user1, true); + $this->assertTrue($ret); + } + + /** + * Test method getUsers() + * + * @return void + */ + public function testGetUsers() + { + $group = $this->getGroup(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + if(self::$dbversion['major'] == 6) { + $db->expects($this->exactly(2)) + ->method('getResultArray') + ->withConsecutive([$this->stringContains("`tblGroupMembers`.`groupID` = '".$group->getId()."'")], [$this->stringContains("SELECT * FROM `tblRoles` WHERE `id` =")]) + ->willReturnOnConsecutiveCalls(array(array('id'=>2, 'login'=>'user', 'pwd'=>'pass', 'fullName'=>'Joe Baz', 'email'=>'joe@foo.de', 'language'=>'en_GB', 'theme'=>'bootstrap', 'comment'=>'', 'role'=>SeedDMS_Core_User::role_user, 'hidden'=>0, 'role'=>1)), array('id'=>1, 'name'=>'role', 'role'=>1, 'noaccess'=>'')); + } else { + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("`tblGroupMembers`.`groupID` = '".$group->getId()."'")) + ->willReturn(array(array('id'=>2, 'login'=>'user', 'pwd'=>'pass', 'fullName'=>'Joe Baz', 'email'=>'joe@foo.de', 'language'=>'en_GB', 'theme'=>'bootstrap', 'comment'=>'', 'role'=>SeedDMS_Core_User::role_user, 'hidden'=>0, 'role'=>1))); + } + $dms = new SeedDMS_Core_DMS($db, ''); + + $group->setDMS($dms); + $users = $group->getUsers(); + $this->assertIsArray($users); + $this->assertCount(1, $users); + } + + /** + * Test method getManagers() + * + * @return void + */ + public function testGetManagers() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + if(self::$dbversion['major'] == 6) { + $db->expects($this->exactly(2)) + ->method('getResultArray') + ->withConsecutive([$this->stringContains("`manager` = 1")], [$this->stringContains("SELECT * FROM `tblRoles` WHERE `id` =")]) + ->willReturnOnConsecutiveCalls(array(array('id'=>2, 'login'=>'user', 'pwd'=>'pass', 'fullName'=>'Joe Baz', 'email'=>'joe@foo.de', 'language'=>'en_GB', 'theme'=>'bootstrap', 'comment'=>'', 'role'=>SeedDMS_Core_User::role_user, 'hidden'=>0, 'role'=>1)), array('id'=>1, 'name'=>'role', 'role'=>1, 'noaccess'=>'')); + } else { + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains('`manager` = 1')) + ->willReturn(array(array('id'=>2, 'login'=>'user', 'pwd'=>'pass', 'fullName'=>'Joe Baz', 'email'=>'joe@foo.de', 'language'=>'en_GB', 'theme'=>'bootstrap', 'comment'=>'', 'role'=>SeedDMS_Core_User::role_user, 'hidden'=>0))); + } + $dms = new SeedDMS_Core_DMS($db, ''); + + $group = $this->getGroup(); + $group->setDMS($dms); + $managers = $group->getManagers(); + $this->assertIsArray($managers); + $this->assertCount(1, $managers); + } + + /** + * Test method getNotifications() + * + * @return void + */ + public function testGetNotifications() + { + $group = $this->getGroup(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("WHERE `tblNotify`.`groupID` = ".$group->getId())) + ->willReturn(array(array('target'=>2, 'targetType'=>'0', 'userID'=>0, 'groupID'=>$group->getId()))); + $dms = new SeedDMS_Core_DMS($db, ''); + $group->setDMS($dms); + $notifications = $group->getNotifications(); + $this->assertIsArray($notifications); + $this->assertCount(1, $notifications); + $this->assertInstanceOf(SeedDMS_Core_Notification::class, $notifications[0]); + } + + /** + * Test method getNotifications() with target type + * + * @return void + */ + public function testGetNotificationsWithTargetType() + { + $group = $this->getGroup(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("WHERE `tblNotify`.`groupID` = ".$group->getId()." AND `tblNotify`.`targetType` = 1")) + ->willReturn(array(array('target'=>2, 'targetType'=>'1', 'userID'=>0, 'groupID'=>$group->getId()))); + $dms = new SeedDMS_Core_DMS($db, ''); + $group->setDMS($dms); + $notifications = $group->getNotifications(1); + $this->assertIsArray($notifications); + $this->assertCount(1, $notifications); + $this->assertInstanceOf(SeedDMS_Core_Notification::class, $notifications[0]); + } + + +} diff --git a/SeedDMS_Core/tests/KeywordCategoryTest.php b/SeedDMS_Core/tests/KeywordCategoryTest.php new file mode 100644 index 000000000..e22d138ed --- /dev/null +++ b/SeedDMS_Core/tests/KeywordCategoryTest.php @@ -0,0 +1,147 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * User test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class KeywordCategoryTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetName() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $cat = self::$dms->addKeywordCategory($user->getId(), 'Category 1'); + $name = $cat->getName(); + $ret = $cat->setName('foo'); + $this->assertTrue($ret); + $name = $cat->getName(); + $this->assertEquals('foo', $name); + $ret = $cat->setName(' '); + $this->assertFalse($ret); + } + + /** + * Test method getOwner() and setOwner() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetOwner() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $guest = SeedDMS_Core_User::getInstance(2, self::$dms); + $cat = self::$dms->addKeywordCategory($user->getId(), 'Category 1'); + $this->assertIsObject($cat); + $ret = $cat->setOwner($guest); + $this->assertTrue($ret); + $owner = $cat->getOwner(); + $this->assertEquals(2, $owner->getId()); + $ret = $cat->setOwner(null); + $this->assertFalse($ret); + } + + /** + * Test method addKeywordList() and editKeywordList(), getKeywordLists(), removeKeywordList() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetSetEditAndRemoveKeywordList() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $cat = self::$dms->addKeywordCategory($user->getId(), 'Category 1'); + $this->assertIsObject($cat); + $ret = $cat->addKeywordList('foo'); + $this->assertTrue($ret); + $ret = $cat->addKeywordList('bar'); + $this->assertTrue($ret); + $list = $cat->getKeywordLists(); + $this->assertIsArray($list); + $this->assertCount(2, $list); + $ret = $cat->editKeywordList(1, 'baz'); + $this->assertTrue($ret); + + $ret = $cat->removeKeywordList(1); + $this->assertTrue($ret); + $list = $cat->getKeywordLists(); + $this->assertIsArray($list); + $this->assertCount(1, $list); + } + + /** + * Test method addKeywordCategory() and remove() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAndAndRemoveKeywordCategory() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $cat = self::$dms->addKeywordCategory($user->getId(), 'Category 1'); + $this->assertIsObject($cat); + $ret = $cat->addKeywordList('foo'); + $this->assertTrue($ret); + $ret = $cat->addKeywordList('bar'); + $this->assertTrue($ret); + $ret = $cat->remove(); + $this->assertTrue($ret); + } +} diff --git a/SeedDMS_Core/tests/ReviewApprovalTest.php b/SeedDMS_Core/tests/ReviewApprovalTest.php new file mode 100644 index 000000000..aea040e39 --- /dev/null +++ b/SeedDMS_Core/tests/ReviewApprovalTest.php @@ -0,0 +1,477 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class ReviewApprovalTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method addIndReviewer(), addGrpReviewer(), verifyStatus(), + * getReviewStatus(), removeReview(), delIndReviewer() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testReviewDocumentByUserAndGroup() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the reviewer */ + $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($reviewer); + + /* Add a new group which will be the reviewer */ + $reviewergrp = self::$dms->addGroup('reviewer', ''); + $this->assertIsObject($reviewergrp); + + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $content = $document->getLatestContent(); + $this->assertIsObject($content); + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* A missing reviewer or user causes an error */ + $ret = $content->addIndReviewer($reviewer, null); + $this->assertEquals(-1, $ret); + + /* A missing reviewer or user causes an error */ + $ret = $content->addIndReviewer(null, $user); + $this->assertEquals(-1, $ret); + + /* Adding a group instead of a user causes an error */ + $ret = $content->addIndReviewer($reviewergrp, $user); + $this->assertEquals(-1, $ret); + + /* Finally add the reviewer */ + $ret = $content->addIndReviewer($reviewer, $user); + $this->assertGreaterThan(0, $ret); + + /* Adding the user again will yield in an error */ + $ret = $content->addIndReviewer($reviewer, $user); + $this->assertEquals(-3, $ret); + + /* Needs to call verifyStatus() in order to recalc the status */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_DRAFT_REV, $newstatus); + + /* Get all reviews */ + $reviewstatus = $content->getReviewStatus(); + $this->assertIsArray($reviewstatus); + $this->assertCount(1, $reviewstatus); + + /* Get list of individual und group reviewers */ + $reviewers = $content->getReviewers(); + $this->assertIsArray($reviewers); + $this->assertCount(2, $reviewers); + $this->assertCount(1, $reviewers['i']); + $this->assertCount(0, $reviewers['g']); +/* + $db = self::$dms->getDB(); + $db->createTemporaryTable("ttreviewid", true); + $queryStr = "SELECT * FROM ttreviewid"; + $recs = $db->getResultArray($queryStr); + echo $db->getErrorMsg(); + var_dump($recs); +*/ + + /* A missing reviewer or user causes an error */ + $ret = $content->addGrpReviewer($reviewergrp, null); + $this->assertEquals(-1, $ret); + + /* A missing reviewer or user causes an error */ + $ret = $content->addGrpReviewer(null, $user); + $this->assertEquals(-1, $ret); + + /* Adding a user instead of a group causes an error */ + $ret = $content->addGrpReviewer($reviewer, $user); + $this->assertEquals(-1, $ret); + + /* Finally add the reviewer */ + $ret = $content->addGrpReviewer($reviewergrp, $user); + $this->assertGreaterThan(0, $ret); + $groupstatus = $reviewergrp->getReviewStatus(); + + /* Adding the group again will yield in an error */ + $ret = $content->addGrpReviewer($reviewergrp, $user); + $this->assertEquals(-3, $ret); + + /* Get all reviews */ + $reviewstatus = $content->getReviewStatus(); + $this->assertIsArray($reviewstatus); + $this->assertCount(2, $reviewstatus); + + /* Get list of individual und group reviewers */ + $reviewers = $content->getReviewers(); + $this->assertIsArray($reviewers); + $this->assertCount(2, $reviewers); + $this->assertCount(1, $reviewers['i']); + $this->assertCount(1, $reviewers['g']); + + $userstatus = $reviewer->getReviewStatus(); + $groupstatus = $reviewergrp->getReviewStatus(); + + /* There should be two log entries, one for each reviewer */ + $reviewlog = $content->getReviewLog(5); + $this->assertIsArray($reviewlog); + $this->assertCount(2, $reviewlog); + + /* Adding a review without a user of reviewer causes an error */ + $ret = $content->setReviewByInd($reviewer, null, S_LOG_ACCEPTED, 'Comment of individual reviewer'); + $this->assertEquals(-1, $ret); + $ret = $content->setReviewByInd(null, $user, S_LOG_ACCEPTED, 'Comment of individual reviewer'); + $this->assertEquals(-1, $ret); + + /* Adding a review as an individual but passing a group causes an error */ + $ret = $content->setReviewByInd($reviewergrp, $user, S_LOG_ACCEPTED, 'Comment of individual reviewer'); + $this->assertEquals(-1, $ret); + + /* Individual reviewer reviews document */ + $ret = $content->setReviewByInd($reviewer, $user, S_LOG_ACCEPTED, 'Comment of individual reviewer'); + $this->assertIsInt(0, $ret); + $this->assertGreaterThan(0, $ret); + + /* Get the last 5 review log entries (actually there are just 3 now) */ + $reviewlog = $content->getReviewLog(5); + $this->assertIsArray($reviewlog); + $this->assertCount(3, $reviewlog); + $this->assertEquals('Comment of individual reviewer', $reviewlog[0]['comment']); + $this->assertEquals(1, $reviewlog[0]['status']); + + /* Needs to call verifyStatus() in order to recalc the status. + * It must not be changed because the group reviewer has not done the + * review. + */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_DRAFT_REV, $newstatus); + + /* Adding a review without a user of reviewer causes an error */ + $ret = $content->setReviewByGrp($reviewergrp, null, S_LOG_ACCEPTED, 'Comment of group reviewer'); + $this->assertEquals(-1, $ret); + $ret = $content->setReviewByGrp(null, $user, S_LOG_ACCEPTED, 'Comment of group reviewer'); + $this->assertEquals(-1, $ret); + + /* Adding a review as an group but passing a user causes an error */ + $ret = $content->setReviewByGrp($reviewer, $user, S_LOG_ACCEPTED, 'Comment of group reviewer'); + $this->assertEquals(-1, $ret); + + /* Group reviewer reviews document */ + $ret = $content->setReviewByGrp($reviewergrp, $user, S_LOG_ACCEPTED, 'Comment of group reviewer'); + $this->assertIsInt(0, $ret); + $this->assertGreaterThan(0, $ret); + + /* Get the last 5 review log entries (actually there are just 4 now) */ + $reviewlog = $content->getReviewLog(5); + $this->assertIsArray($reviewlog); + $this->assertCount(4, $reviewlog); + $this->assertEquals('Comment of group reviewer', $reviewlog[0]['comment']); + $this->assertEquals(1, $reviewlog[0]['status']); + + /* Now the document has received all reviews */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_RELEASED, $newstatus); + + /* Remove the last review of the user */ + $userstatus = $reviewer->getReviewStatus($document->getId(), $content->getVersion()); + $this->assertIsArray($userstatus); + $this->assertCount(2, $userstatus); + $this->assertCount(1, $userstatus['indstatus']); + $ret = $content->removeReview($userstatus['indstatus'][$document->getId()]['reviewID'], $user, 'Undo review'); + $this->assertTrue($ret); + + /* Get the last 8 review log entries (actually there are just 5 now) */ + $reviewlog = $content->getReviewLog(8); + $this->assertIsArray($reviewlog); + $this->assertCount(5, $reviewlog); + $this->assertEquals('Undo review', $reviewlog[0]['comment']); + $this->assertEquals(0, $reviewlog[0]['status']); + + /* Now the document must be back in draft mode */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_DRAFT_REV, $newstatus); + + /* Removing the user as a reviewer completly will release the + * document again, because the group reviewer became the only + * reviewer and has done the review already. + */ + $ret = $content->delIndReviewer($reviewer, $user, 'Reviewer removed'); + $this->assertIsInt($ret); + $this->assertEquals(0, $ret); + + /* Get the last 8 review log entries (actually there are just 6 now) */ + $reviewlog = $content->getReviewLog(8); + $this->assertIsArray($reviewlog); + $this->assertCount(6, $reviewlog); + $this->assertEquals('Reviewer removed', $reviewlog[0]['comment']); + $this->assertEquals(-2, $reviewlog[0]['status']); + + /* Now the document will be released again */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_RELEASED, $newstatus); + } + + /** + * Test method addIndApprover(), addGrpApprover(), verifyStatus(), + * getApprovalStatus(), removeApproval(), delIndApprover() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testApproveDocumentByUserAndGroup() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the approver */ + $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($approver); + + /* Add a new group which will be the approver */ + $approvergrp = self::$dms->addGroup('approver', ''); + $this->assertIsObject($approvergrp); + + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $content = $document->getLatestContent(); + $this->assertIsObject($content); + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* A missing approver or user causes an error */ + $ret = $content->addIndApprover($approver, null); + $this->assertEquals(-1, $ret); + + /* A missing approver or user causes an error */ + $ret = $content->addIndApprover(null, $user); + $this->assertEquals(-1, $ret); + + /* Adding a group instead of a user causes an error */ + $ret = $content->addIndApprover($approvergrp, $user); + $this->assertEquals(-1, $ret); + + /* Finally add the reviewer */ + $ret = $content->addIndApprover($approver, $user); + $this->assertGreaterThan(0, $ret); + + /* Adding the user again will yield in an error */ + $ret = $content->addIndApprover($approver, $user); + $this->assertEquals(-3, $ret); + + /* Needs to call verifyStatus() in order to recalc the status */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_DRAFT_APP, $newstatus); + + /* Get all approvals */ + $approvalstatus = $content->getApprovalStatus(); + $this->assertIsArray($approvalstatus); + $this->assertCount(1, $approvalstatus); + + /* Get list of individual und group approvers */ + $approvers = $content->getApprovers(); + $this->assertIsArray($approvers); + $this->assertCount(2, $approvers); + $this->assertCount(1, $approvers['i']); + $this->assertCount(0, $approvers['g']); + + /* A missing approver or user causes an error */ + $ret = $content->addGrpApprover($approvergrp, null); + $this->assertEquals(-1, $ret); + + /* A missing approver or user causes an error */ + $ret = $content->addGrpApprover(null, $user); + $this->assertEquals(-1, $ret); + + /* Adding a user instead of a group causes an error */ + $ret = $content->addGrpApprover($approver, $user); + $this->assertEquals(-1, $ret); + + /* Finally add the reviewer */ + $ret = $content->addGrpApprover($approvergrp, $user); + $this->assertGreaterThan(0, $ret); + $groupstatus = $approvergrp->getApprovalStatus(); + + /* Adding the group again will yield in an error */ + $ret = $content->addGrpApprover($approvergrp, $user); + $this->assertEquals(-3, $ret); + + /* Get all approvals */ + $approvalstatus = $content->getApprovalStatus(); + $this->assertIsArray($approvalstatus); + $this->assertCount(2, $approvalstatus); + + /* Get list of individual und group approvers */ + $approvers = $content->getApprovers(); + $this->assertIsArray($approvers); + $this->assertCount(2, $approvers); + $this->assertCount(1, $approvers['i']); + $this->assertCount(1, $approvers['g']); + + $userstatus = $approver->getApprovalStatus(); + $groupstatus = $approvergrp->getApprovalStatus(); + + /* There should be two log entries, one for each approver */ + $approvallog = $content->getApproveLog(5); + $this->assertIsArray($approvallog); + $this->assertCount(2, $approvallog); + + /* Adding a approval without a user of approver causes an error */ + $ret = $content->setApprovalByInd($approver, null, S_LOG_ACCEPTED, 'Comment of individual approver'); + $this->assertEquals(-1, $ret); + $ret = $content->setApprovalByInd(null, $user, S_LOG_ACCEPTED, 'Comment of individual approver'); + $this->assertEquals(-1, $ret); + + /* Adding a approval as an individual but passing a group causes an error */ + $ret = $content->setApprovalByInd($approvergrp, $user, S_LOG_ACCEPTED, 'Comment of individual approver'); + $this->assertEquals(-1, $ret); + + /* Individual approver approvals document */ + $ret = $content->setApprovalByInd($approver, $user, S_LOG_ACCEPTED, 'Comment of individual approver'); + $this->assertIsInt(0, $ret); + $this->assertGreaterThan(0, $ret); + + /* Get the last 5 approval log entries (actually there are just 3 now) */ + $approvallog = $content->getApproveLog(5); + $this->assertIsArray($approvallog); + $this->assertCount(3, $approvallog); + $this->assertEquals('Comment of individual approver', $approvallog[0]['comment']); + $this->assertEquals(1, $approvallog[0]['status']); + + /* Needs to call verifyStatus() in order to recalc the status. + * It must not be changed because the group approver has not done the + * approval. + */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_DRAFT_APP, $newstatus); + + /* Adding a approval without a user of approver causes an error */ + $ret = $content->setApprovalByGrp($approvergrp, null, S_LOG_ACCEPTED, 'Comment of group approver'); + $this->assertEquals(-1, $ret); + $ret = $content->setApprovalByGrp(null, $user, S_LOG_ACCEPTED, 'Comment of group approver'); + $this->assertEquals(-1, $ret); + + /* Adding a approval as an group but passing a user causes an error */ + $ret = $content->setApprovalByGrp($approver, $user, S_LOG_ACCEPTED, 'Comment of group approver'); + $this->assertEquals(-1, $ret); + + /* Group approver approvals document */ + $ret = $content->setApprovalByGrp($approvergrp, $user, S_LOG_ACCEPTED, 'Comment of group approver'); + $this->assertIsInt(0, $ret); + $this->assertGreaterThan(0, $ret); + + /* Get the last 5 approval log entries (actually there are just 4 now) */ + $approvallog = $content->getApproveLog(5); + $this->assertIsArray($approvallog); + $this->assertCount(4, $approvallog); + $this->assertEquals('Comment of group approver', $approvallog[0]['comment']); + $this->assertEquals(1, $approvallog[0]['status']); + + /* Now the document has received all approvals */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_RELEASED, $newstatus); + + /* Remove the last approval of the user */ + $userstatus = $approver->getApprovalStatus($document->getId(), $content->getVersion()); + $this->assertIsArray($userstatus); + $this->assertCount(2, $userstatus); + $this->assertCount(1, $userstatus['indstatus']); + $ret = $content->removeApproval($userstatus['indstatus'][$document->getId()]['approveID'], $user, 'Undo approval'); + $this->assertTrue($ret); + + /* Get the last 8 approval log entries (actually there are just 5 now) */ + $approvallog = $content->getApproveLog(8); + $this->assertIsArray($approvallog); + $this->assertCount(5, $approvallog); + $this->assertEquals('Undo approval', $approvallog[0]['comment']); + $this->assertEquals(0, $approvallog[0]['status']); + + /* Now the document must be back in draft mode */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_DRAFT_APP, $newstatus); + + /* Removing the user as a approver completly will release the + * document again, because the group approver became the only + * approver and has done the approval already. + */ + $ret = $content->delIndApprover($approver, $user, 'Approver removed'); + $this->assertIsInt($ret); + $this->assertEquals(0, $ret); + + /* Get the last 8 approval log entries (actually there are just 6 now) */ + $approvallog = $content->getApproveLog(8); + $this->assertIsArray($approvallog); + $this->assertCount(6, $approvallog); + $this->assertEquals('Approver removed', $approvallog[0]['comment']); + $this->assertEquals(-2, $approvallog[0]['status']); + + /* Now the document will be released again */ + $newstatus = $content->verifyStatus(false, $user); + $this->assertIsInt($newstatus); + $this->assertEquals(S_RELEASED, $newstatus); + } +} diff --git a/SeedDMS_Core/tests/SeedDmsBase.php b/SeedDMS_Core/tests/SeedDmsBase.php new file mode 100644 index 000000000..dc8f7e920 --- /dev/null +++ b/SeedDMS_Core/tests/SeedDmsBase.php @@ -0,0 +1,365 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +namespace PHPUnit\Framework; + +use PHPUnit\Framework\TestCase; + +/** + * Database test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class SeedDmsTest extends TestCase +{ + + public static $dbh; + + public static $dms; + + public static $contentdir; + + public static $dbversion; + + /** + * Create a sqlite database in memory + * + * @return void + */ + public static function createInMemoryDatabase(): object + { + $dbh = new \SeedDMS_Core_DatabaseAccess('sqlite', '', '', '', ':memory:'); + $dbh->connect(); + $queries = file_get_contents(getenv("SEEDDMS_CORE_SQL")); + // generate SQL query + $queries = explode(";", $queries); + + // execute queries + $errorMsg = ''; + foreach ($queries as $query) { + //echo $query; + $query = trim($query); + if (!empty($query)) { + $dbh->getResult($query); + + if ($dbh->getErrorNo() != 0) { + //echo $dbh->getErrorMsg()."\n"; + $errorMsg .= $dbh->getErrorMsg()."\n"; + } + } + } + return $dbh; + } + + /** + * Create a mocked root folder object + * + * @return \SeedDMS_Core_Folder + */ + protected function getMockedRootFolder($id=1, $name='DMS') + { + $folder = new \SeedDMS_Core_Folder($id, $name, 0, 'DMS root', time(), 1, 0, 0, 0.0); + return $folder; + } + + /** + * Create a mocked document object + * + * @return \SeedDMS_Core_Document + */ + protected function getMockedDocument($id=1, $name='Document') + { + $document = new \SeedDMS_Core_Document($id, $name, '', time(), null, 1, 1, 1, M_READ, 0, '', 1.0); + return $document; + } + + /** + * Create a mocked user object + * + * @return \SeedDMS_Core_User + */ + protected function getMockedUser() + { + $user = new \SeedDMS_Core_User(1, 'login', '', 'New User', 'email@seeddms.org', 'de_DE', 'bootstrap', '', null); + return $user; + } + + /** + * Create a temporary file with random content and the given length. + * + * @param integer $length length of file + * + * @return string name of temporary file + */ + protected static function createTempFile($length=200, $dir='') + { + if($tmpfname = @tempnam($dir ? $dir : sys_get_temp_dir(), 'foo')) { + file_put_contents($tmpfname, substr(str_shuffle(str_repeat($x='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) ceil($length/strlen($x)) )),1,$length)); + return $tmpfname; + } else + return false; + } + + /** + * Create a temporary directory with random name in systems temp dir. + * + * @param integer $mode access mode of new directory + * + * @return string name of temporary directory + */ + protected static function createTempDir(string $dir = null, int $mode = 0700): string { + /* Use the system temp dir by default. */ + if (is_null($dir)) { + $dir = sys_get_temp_dir(); + } + + do { $tmp = $dir . '/' . mt_rand(); } + while (!@mkdir($tmp, $mode)); + return $tmp; + } + + /** + * Create a simple document. + * + * @param \SeedDMS_Core_Folder $parent parent folder + * @param \SeedDMS_Core_User $owner owner of document + * @param string $name name of document + * @param integer $length length of file + * + * @return string name of temporary file + */ + protected static function createDocument($parent, $owner, $name, $length=200) + { + $filename = self::createTempFile($length); + list($document, $res) = $parent->addDocument( + $name, // name + '', // comment + null, // no expiration + $owner, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file1.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0 // sequence + ); + unlink($filename); + return $document; + } + + /** + * Create a simple folder structure without documents + * + * DMS root -+- Subfolder 1 -+- Subsubfolder 1 -+- Subsubsubfolder 1 + * | + * +- Subfolder 2 + * | + * +- Subfolder 3 + * + * The sequence field of Subfolder x is: + * Subfolder 1: 2.0 + * Subfolder 2: 1.0 + * Subfolder 1: 0.5 + * + * @return void + */ + protected static function createSimpleFolderStructure() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + /* Set up a folder structure */ + $subfolder = $rootfolder->addSubFolder('Subfolder 1', '', $user, 2.0); + $subsubfolder = $subfolder->addSubFolder('Subsubfolder 1', '', $user, 1.0); + $subsubsubfolder = $subsubfolder->addSubFolder('Subsubsubfolder 1', '', $user, 1.0); + $rootfolder->addSubFolder('Subfolder 2', '', $user, 1.0); + $rootfolder->addSubFolder('Subfolder 3', '', $user, 0.5); + } + + /** + * Create a simple folder structure with documents + * + * Creates the same folder structure like createSimpleFolderStructure() + * but adds 30 documents to 'Subfolder 1'. They are named 'Document 1' + * to 'Document 30'. + * + * @return void + */ + protected static function createSimpleFolderStructureWithDocuments() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + self::createSimpleFolderStructure(); + /* Add documents to 'Subfolder 1' */ + $subfolder = self::$dms->getFolderByName('Subfolder 1'); + for ($i=1; $i<=15; $i++) { + $filename = self::createTempFile(200); + list($document, $res) = $subfolder->addDocument( + 'Document 1-'.$i, // name + '', // comment + null, + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file-1-'.$i.'.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0+$i // sequence + ); + unlink($filename); + } + /* Add documents to 'Subfolder 2' */ + $subfolder = self::$dms->getFolderByName('Subfolder 2'); + for ($i=1; $i<=15; $i++) { + $filename = self::createTempFile(200); + list($document, $res) = $subfolder->addDocument( + 'Document 2-'.$i, // name + '', // comment + null, + $user, // owner + '', // keywords + [], // categories + $filename, // name of file + 'file-2-'.$i.'.txt', // original file name + '.txt', // file type + 'text/plain', // mime type + 1.0+$i // sequence + ); + unlink($filename); + } + } + + /** + * Create two groups with 3 users each + * The groups are named 'Group 1' and 'Group 2'. The users in Group 1 + * are named 'User-1-1', 'User-1-2', 'User-1-3'. The users in Group 2 + * are named 'User-2-1', 'User-2-2', 'User-2-3'. + * The login name is the lower case of the name. + * + * @return void + */ + protected static function createGroupsAndUsers() + { + for($i=1; $i<=2; $i++) { + $group = self::$dms->addGroup('Group '.$i, ''); + for($j=1; $j<=3; $j++) { + $user = self::$dms->addUser('user-'.$i.'-'.$j, '', 'User '.$j.' in group '.$i, 'user@seeddms.org', 'en_GB', 'bootstrap', ''); + $user->joinGroup($group); + } + } + } + + /** + * Creates a workflow with two transitions identical to the traditional + * workflow + * + * NR --- review --> NA -+- approve --> RL + * +- reject --> RJ | + * +- reject ---> RJ + * + * States: + * NR = needs review + * NA = needs approval + * RL = released + * RJ = rejected + * + * Actions: + * review + * approve + * reject + * + * Transitions: + * NR -- review -> NA maybe done by reviewer + * NR -- reject -> RJ maybe done by reviewer + * NA -- approve -> RL maybe done by approver + * NA -- reject -> RJ maybe done by approver + */ + protected function createWorkflow(\SeedDMS_Core_User $reviewer, \SeedDMS_Core_User $approver): \SeedDMS_Core_Workflow + { + /* Create workflow states */ + $ws_nr = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); + $ws_na = self::$dms->addWorkflowState('needs approval', S_IN_WORKFLOW); + $ws_rl = self::$dms->addWorkflowState('released', S_RELEASED); + $ws_rj = self::$dms->addWorkflowState('rejected', S_REJECTED); + + /* Create workflow actions */ + $wa_rv = self::$dms->addWorkflowAction('review', S_IN_WORKFLOW); + $wa_rj = self::$dms->addWorkflowAction('reject', S_REJECTED); + $wa_ap = self::$dms->addWorkflowAction('approve', S_RELEASED); + + /* Create a workflow which starts in state 'needs review' */ + $workflow = self::$dms->addWorkflow('traditional workflow', $ws_nr); + /* Add transition NR -- review -> NA */ + $wt_nr_na = $workflow->addTransition($ws_nr, $wa_rv, $ws_na, [$reviewer], []); + /* Add transition NR -- review -> RJ */ + $wt_nr_rj = $workflow->addTransition($ws_nr, $wa_rj, $ws_rj, [$reviewer], []); + /* Add transition NA -- approve -> RL */ + $wt_na_rl = $workflow->addTransition($ws_na, $wa_ap, $ws_rl, [$approver], []); + /* Add transition NA -- reject -> RJ */ + $wt_na_rj = $workflow->addTransition($ws_na, $wa_rj, $ws_rj, [$approver], []); + + return $workflow; + } + + /** + * Creates a workflow with one transitions for approving a document + * + * NA -+- approve --> RL + * | + * +- reject ---> RJ + * + * States: + * NA = needs approval + * RL = released + * RJ = rejected + * + * Actions: + * approve + * reject + * + * Transitions: + * NA -- approve -> RL maybe done by approver + * NA -- reject -> RJ maybe done by approver + */ + protected function createSimpleWorkflow(\SeedDMS_Core_User $approver): \SeedDMS_Core_Workflow + { + /* Create workflow states */ + $ws_na = self::$dms->addWorkflowState('simple needs approval', S_IN_WORKFLOW); + $ws_rl = self::$dms->addWorkflowState('simple released', S_RELEASED); + $ws_rj = self::$dms->addWorkflowState('simple rejected', S_REJECTED); + + /* Create workflow actions */ + $wa_rj = self::$dms->addWorkflowAction('simple reject', S_REJECTED); + $wa_ap = self::$dms->addWorkflowAction('simple approve', S_RELEASED); + + /* Create a workflow which starts in state 'needs approval' */ + $workflow = self::$dms->addWorkflow('simple workflow', $ws_na); + /* Add transition NA -- approve -> RL */ + $wt_na_rl = $workflow->addTransition($ws_na, $wa_ap, $ws_rl, [$approver], []); + /* Add transition NA -- reject -> RJ */ + $wt_na_rj = $workflow->addTransition($ws_na, $wa_rj, $ws_rj, [$approver], []); + + return $workflow; + } + +} + diff --git a/SeedDMS_Core/tests/UserTest.php b/SeedDMS_Core/tests/UserTest.php new file mode 100644 index 000000000..6d2e6f208 --- /dev/null +++ b/SeedDMS_Core/tests/UserTest.php @@ -0,0 +1,1679 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * User test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class UserTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Create a mock admin user object + * + * @return SeedDMS_Core_User + */ + protected function getAdminUser() + { + $user = new SeedDMS_Core_User(1, 'admin', 'pass', 'Joe Foo', 'baz@foo.de', 'en_GB', 'bootstrap', 'My comment', SeedDMS_Core_User::role_admin); + return $user; + } + + /** + * Create a mock admin role object (only used for SeedDMS 6) + * + * @return SeedDMS_Core_User + */ + protected function getAdminRole() + { + $role = new SeedDMS_Core_Role(1, 'admin', SeedDMS_Core_Role::role_admin); + return $role; + } + + /** + * Create a mock regular user object + * + * @return SeedDMS_Core_User + */ + protected function getUser() + { + $user = new SeedDMS_Core_User(2, 'user', 'pass', 'Joe Baz', 'joe@foo.de', 'en_GB', 'bootstrap', 'My comment', SeedDMS_Core_User::role_user); + return $user; + } + + /** + * Test method setDMS() and getDMS() + * + * @return void + */ + public function testSetAndGetDMS() + { + $user = $this->getAdminUser(); + $user->setDMS(self::$dms); + $this->assertInstanceOf(SeedDMS_Core_DMS::class, $user->getDMS()); + } + + /** + * Test method isType() + * + * @return void + */ + public function testIsType() + { + $user = $this->getAdminUser(); + $this->assertTrue($user->isType('user')); + } + + /** + * Test method getPwd() + * + * @return void + */ + public function testGetPwd() + { + $user = $this->getAdminUser(); + $this->assertEquals('pass', $user->getPwd()); + } + + /** + * Test method getEmail() + * + * @return void + */ + public function testGetEmail() + { + $user = $this->getAdminUser(); + $this->assertEquals('baz@foo.de', $user->getEmail()); + } + + /** + * Test method getLanguage() + * + * @return void + */ + public function testGetLanguage() + { + $user = $this->getAdminUser(); + $this->assertEquals('en_GB', $user->getLanguage()); + } + + /** + * Test method getTheme() + * + * @return void + */ + public function testGetTheme() + { + $user = $this->getAdminUser(); + $this->assertEquals('bootstrap', $user->getTheme()); + } + + /** + * Test method getComment() + * + * @return void + */ + public function testGetComment() + { + $user = $this->getAdminUser(); + $this->assertEquals('My comment', $user->getComment()); + } + + /** + * Test method getRole() + * + * @return void + */ + public function testGetRole() + { + $user = $this->getAdminUser(); + $this->assertEquals(1, $user->getRole()); + } + + /** + * Test method isAdmin() + * + * @return void + */ + public function testIsAdmin() + { + $user = $this->getAdminUser(); + $this->assertTrue($user->isAdmin()); + $this->assertFalse($user->isGuest()); + } + + /** + * Test method isGuest() + * + * @return void + */ + public function testIsGuest() + { + $user = $this->getAdminUser(); + $this->assertFalse($user->isGuest()); + } + + /** + * Test method isHidden() + * + * @return void + */ + public function testIsHidden() + { + $user = $this->getAdminUser(); + $this->assertFalse($user->isHidden()); + } + + /** + * Test method getQuota() + * + * @return void + */ + public function testGetQuota() + { + $user = $this->getAdminUser(); + $this->assertEquals(0, $user->getQuota()); + } + + /** + * Test method getSecret() + * + * @return void + */ + public function testGetSecret() + { + if(self::$dbversion['major'] < 6) { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 5.' + ); + } else { + $user = $this->getAdminUser(); + $this->assertEquals('', $user->getSecret()); + } + } + + /** + * Test method getInstance() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetInstance() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $this->assertIsObject($user); + $this->assertEquals('admin', $user->getLogin()); + $user = SeedDMS_Core_User::getInstance('admin', self::$dms, 'name'); + $this->assertIsObject($user); + $this->assertEquals('admin', $user->getLogin()); + $user = SeedDMS_Core_User::getInstance('admin', self::$dms, 'name', 'info@seeddms.org'); + $this->assertIsObject($user); + $this->assertEquals('admin', $user->getLogin()); + /* get instance of none existing user */ + $user = SeedDMS_Core_User::getInstance('foo', self::$dms, 'name'); + $this->assertNull($user); + } + + /** + * Test method getAllInstances() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAllInstancesSqlFail() + { + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblUsers` ORDER BY")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + /* Order by login */ + $users = SeedDMS_Core_User::getAllInstances('', $dms); + $this->assertFalse($users); + /* Order by fullname */ + $users = SeedDMS_Core_User::getAllInstances('fullname', $dms); + $this->assertFalse($users); + } + + /** + * Test method getLogin() + * + * @return void + */ + public function testGetLogin() + { + $user = $this->getAdminUser(); + $this->assertEquals('admin', $user->getLogin()); + } + + /** + * Test method setLogin() + * + * @return void + */ + public function testSetLoginSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `login`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setLogin('foo')); + } + + /** + * Test method getLogin() and setLogin() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetLogin() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $login = $user->getLogin(); + $ret = $user->setLogin('foo'); + $this->assertTrue($ret); + $login = $user->getLogin(); + $this->assertEquals('foo', $login); + $ret = $user->setLogin(' '); + $this->assertFalse($ret); + } + + /** + * Test method getFullName() + * + * @return void + */ + public function testGetFullName() + { + $user = $this->getAdminUser(); + $this->assertEquals('Joe Foo', $user->getFullName()); + } + + /** + * Test method setFullName() + * + * @return void + */ + public function testSetFullNameSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `fullName`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setFullName('foo')); + } + + /** + * Test method getFullName() and setFullName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetFullName() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $fullname = $user->getFullName(); + $ret = $user->setFullName('foo'); + $this->assertTrue($ret); + $fullname = $user->getFullName(); + $this->assertEquals('foo', $fullname); + } + + /** + * Test method getPwd() and setPwd() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetPwd() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $pwd = $user->getPwd(); + $ret = $user->setPwd('foo'); + $this->assertTrue($ret); + $pwd = $user->getPwd(); + $this->assertEquals('foo', $pwd); + } + + /** + * Test method setPwd() + * + * @return void + */ + public function testSetPwdSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `pwd`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setPwd('foo')); + } + + /** + * Test method getPwdExpiration() and setPwdExpiration() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetPwdExpiration() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $pwdexp = $user->getPwdExpiration(); + /* Set password expiration to 'never' */ + $ret = $user->setPwdExpiration('never'); + $this->assertTrue($ret); + $pwdexp = $user->getPwdExpiration(); + $this->assertNull($pwdexp); + + /* Set password expiration to 'now' */ + $now = date('Y-m-d H:i:s'); + $ret = $user->setPwdExpiration('now'); + $this->assertTrue($ret); + $pwdexp = $user->getPwdExpiration(); + $this->assertEquals($now, $pwdexp); + } + + /** + * Test method setPwdExpiration() + * + * @return void + */ + public function testSetPwdExpirationSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `pwdExpiration`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setPwdExpiration('foo')); + } + + /** + * Test method getEmail() and setEmail() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetEmail() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $email = $user->getEmail(); + $ret = $user->setEmail('new@seeddms.org'); + $this->assertTrue($ret); + $email = $user->getEmail(); + $this->assertEquals('new@seeddms.org', $email); + } + + /** + * Test method setEmail() + * + * @return void + */ + public function testSetEmailSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `email`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setEmail('foo')); + } + + /** + * Test method getLanguage() and setLanguage() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetLanguage() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $language = $user->getLanguage(); + $ret = $user->setLanguage('de_DE'); + $this->assertTrue($ret); + $language = $user->getLanguage(); + $this->assertEquals('de_DE', $language); + } + + /** + * Test method setLanguage() + * + * @return void + */ + public function testSetLanguageSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `language`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setLanguage('de_DE')); + } + + /** + * Test method getTheme() and setTheme() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetTheme() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $theme = $user->getTheme(); + $ret = $user->setTheme('bootstrap4'); + $this->assertTrue($ret); + $theme = $user->getTheme(); + $this->assertEquals('bootstrap4', $theme); + } + + /** + * Test method setTheme() + * + * @return void + */ + public function testSetThemeSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `theme`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setTheme('bootstrap')); + } + + /** + * Test method getComment() and setComment() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetComment() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $comment = $user->getComment(); + $ret = $user->setComment('my comment'); + $this->assertTrue($ret); + $comment = $user->getComment(); + $this->assertEquals('my comment', $comment); + } + + /** + * Test method setComment() + * + * @return void + */ + public function testSetCommentSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `comment`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setComment('my comment')); + } + + /** + * Test method getRole() and setRole() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetRole() + { + if(self::$dbversion['major'] < 6) { + // SeedDMS 5 use integers for roles: 0=user, 1=admin, 2=guest + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + // User with id=1 is the admin user in the initial database + $role = $user->getRole(); + $this->assertEquals(SeedDMS_Core_User::role_admin, $role); + $ret = $user->setRole(SeedDMS_Core_User::role_guest); + $this->assertTrue($ret); + $role = $user->getRole(); + $this->assertEquals(SeedDMS_Core_User::role_guest, $role); + $ret = $user->setRole(''); + $this->assertFalse($ret); + } else { + // Starting with SeedDMS 6 a role is an object + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + // User with id=1 is the admin user in the initial database + $role = $user->getRole(); + $this->assertTrue($role->isAdmin()); + // SeedDMS_Core_User has an isAdmin() method too, which internally + // uses SeedDMS_Core_Role::isAdmin() + $this->assertTrue($user->isAdmin()); + // Get the guest role, which is supposed to have id=2 in the + // initial database + $guestrole = SeedDMS_Core_Role::getInstance(2, self::$dms); + $this->assertTrue($guestrole->isGuest()); + // Assign guest role and check if the user is a guest + $ret = $user->setRole($guestrole); + $this->assertTrue($ret); + $this->assertTrue($user->isGuest()); + } + } + + /** + * Test method setRole() + * + * @return void + */ + public function testSetRoleSqlFail() + { + if(self::$dbversion['major'] > 5) { + $role = $this->getAdminRole(); + } + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `role`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + if(self::$dbversion['major'] > 5) { + $this->assertFalse($user->setRole($role)); + } else { + $this->assertFalse($user->setRole(SeedDMS_Core_User::role_admin)); + } + } + + /** + * Test method setGuest() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetGuest() + { + if(self::$dbversion['major'] == '5') { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $role = $user->getRole(); + $ret = $user->setGuest(); + $this->assertTrue($ret); + $role = $user->getRole(); + $this->assertEquals(SeedDMS_Core_User::role_guest, $role); + } else { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 6.' + ); + } + } + + /** + * Test method setGuest() + * + * @return void + */ + public function testSetGuestSqlFail() + { + $dms = new SeedDMS_Core_DMS(null, ''); + if(self::$dbversion['major'] == '5') { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `role`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setGuest()); + } else { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 6.' + ); + } + } + + /** + * Test method setAdmin() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetAdmin() + { + if(self::$dbversion['major'] == '5') { + $user = SeedDMS_Core_User::getInstance(2, self::$dms); + $role = $user->getRole(); + $ret = $user->setAdmin(); + $this->assertTrue($ret); + $role = $user->getRole(); + $this->assertEquals(SeedDMS_Core_User::role_admin, $role); + } else { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 6.' + ); + } + } + + /** + * Test method setAdmin() + * + * @return void + */ + public function testSetAdminSqlFail() + { + $dms = new SeedDMS_Core_DMS(null, ''); + if(self::$dbversion['major'] == '5') { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `role`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setAdmin()); + } else { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 6.' + ); + } + } + + /** + * Test method getQuota() and setQuota() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetQuota() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $quota = $user->getQuota(); + $ret = $user->setQuota(100000); + $this->assertTrue($ret); + $quota = $user->getQuota(); + $this->assertEquals(100000, $quota); + /* Setting a non numeric or negative value will fail */ + $ret = $user->setQuota('foo'); + $this->assertFalse($ret); + $ret = $user->setQuota(-100); + $this->assertFalse($ret); + } + + /** + * Test method setQuota() + * + * @return void + */ + public function testSetQuotaSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `quota`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setQuota(10000)); + } + + /** + * Test method getSecret() and setSecret() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetSecret() + { + if(self::$dbversion['major'] < 6) { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 5.' + ); + } else { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $secret = $user->getSecret(); + $ret = $user->setSecret('secret'); + $this->assertTrue($ret); + $secret = $user->getSecret(); + $this->assertEquals('secret', $secret); + } + } + + /** + * Test method setSecret() + * + * @return void + */ + public function testSetSecretSqlFail() + { + if(self::$dbversion['major'] < 6) { + $this->markTestSkipped( + 'This test is not applicable for SeedDMS 5.' + ); + } else { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `secret`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setSecret('secret')); + } + } + + /** + * Test method isHidden() and setHidden() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsAndSetHidden() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $ishidden = $user->isHidden(); + /* set hidden to true */ + $ret = $user->setHidden(true); + $this->assertTrue($ret); + $ishidden = $user->isHidden(); + $this->assertTrue($ishidden); + /* set hidden to false */ + $ret = $user->setHidden(false); + $this->assertTrue($ret); + $ishidden = $user->isHidden(); + $this->assertFalse($ishidden); + } + + /** + * Test method setHidden() + * + * @return void + */ + public function testSetHiddentSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `hidden`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setHidden(true)); + } + + /** + * Test method isDisabled() and setDisabled() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testIsAndSetDisabled() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $isdisabled = $user->isDisabled(); + /* set disabled to true */ + $ret = $user->setDisabled(true); + $this->assertTrue($ret); + $isdisabled = $user->isDisabled(); + $this->assertTrue($isdisabled); + /* set disabled to false */ + $ret = $user->setDisabled(false); + $this->assertTrue($ret); + $isdisabled = $user->isDisabled(); + $this->assertFalse($isdisabled); + } + + /** + * Test method setDisabled() + * + * @return void + */ + public function testSetDisabledtSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `disabled`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setDisabled(true)); + } + + /** + * Test method addLoginFailure() + * + * @return void + */ + public function testAddLoginFailure() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `loginfailures`")) + ->willReturn(true); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertEquals(1, $user->addLoginFailure()); + $this->assertEquals(2, $user->addLoginFailure()); + } + + /** + * Test method addLoginFailure() + * + * @return void + */ + public function testAddLoginFailureSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `loginfailures`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->addLoginFailure()); + } + + /** + * Test method clearLoginFailure() + * + * @return void + */ + public function testClearLoginFailure() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->exactly(2)) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `loginfailures`")) + ->willReturn(true); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertEquals(1, $user->addLoginFailure()); + $this->assertEquals(true, $user->clearLoginFailures()); + } + + /** + * Test method clearLoginFailure() + * + * @return void + */ + public function testClearLoginFailureSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `loginfailures`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->clearLoginFailures()); + } + + /** + * Test method setHomeFolder() and getHomeFolder() + * + * @return void + */ + public function testSetAndGetHomeFolder() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `homefolder`")) + ->willReturn(true); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertTrue($user->setHomeFolder(1)); + $this->assertEquals(1, $user->getHomeFolder()); + } + + /** + * Test method setHomeFolder() + * + * @return void + */ + public function testSetHomeFolderSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResult') + ->with($this->stringContains("UPDATE `tblUsers` SET `homefolder`")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->setHomeFolder(1)); + } + + /** + * Test method getUsedDiskSpace() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUsedDiskSpace() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $size = $user->getUsedDiskSpace(); + $this->assertEquals(0, $size); + } + + /** + * Test method getUsedDiskSpace() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetUsedDiskSpaceSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT SUM(`fileSize`) sum")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->getUsedDiskSpace()); + } + + /** + * Test method removeFromProcesses() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testRemoveFromProcesses() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $ret = $user->removeFromProcesses($user); + $this->assertTrue($ret); + } + + /** + * Test method transferDocumentsFolders() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testTransferDocumentsFolders() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + self::createSimpleFolderStructureWithDocuments(); + $newuser = self::$dms->addUser('newuser', '', 'New User', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + /* Transfering documents and folders to the same user returns true */ + $ret = $user->transferDocumentsFolders($user); + $this->assertTrue($ret); + /* A subfolder still belongs to $user */ + $subfolder = self::$dms->getFolder(2); + $this->assertEquals($user->getId(), $subfolder->getOwner()->getId()); + /* A document still belongs to $user */ + $document = self::$dms->getDocument(1); + $this->assertEquals($user->getId(), $document->getOwner()->getId()); + /* Transfer the documents and folders to $newuser */ + $ret = $user->transferDocumentsFolders($newuser); + $this->assertTrue($ret); + /* Get the folder again, because the owner has changed */ + $subfolder = self::$dms->getFolder(2); + $this->assertEquals($newuser->getId(), $subfolder->getOwner()->getId()); + /* Get the document again, because the owner has changed */ + $document = self::$dms->getDocument(1); + $this->assertEquals($newuser->getId(), $document->getOwner()->getId()); + } + + /** + * Test method remove() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testRemove() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + self::createSimpleFolderStructureWithDocuments(); + $newuser = self::$dms->addUser('newuser', '', 'New User', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + /* removing a user without passed a new user for docs and folders will fail */ + $ret = $user->remove($newuser, null); + $this->assertFalse($ret); + + $ret = $user->remove($newuser, $newuser); + $this->assertTrue($ret); + + /* all documents and folders now belong to $newuser */ + $document = self::$dms->getDocument(1); + $this->assertEquals($newuser->getId(), $document->getOwner()->getId()); + $subfolder = self::$dms->getFolder(1); + $this->assertEquals($newuser->getId(), $subfolder->getOwner()->getId()); + } + + /** + * Test method getDocuments() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocuments() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $documents = $user->getDocuments(); + $this->assertIsArray($documents); + $this->assertCount(0, $documents); + } + + /** + * Test method getDocumentsLocked() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentsLocked() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $documents = $user->getDocumentsLocked(); + $this->assertIsArray($documents); + $this->assertCount(0, $documents); + } + + /** + * Test method getDocumentLinks() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentLinks() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $links = $user->getDocumentLinks(); + $this->assertIsArray($links); + $this->assertCount(0, $links); + } + + /** + * Test method getDocumentFiles() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentFiles() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $files = $user->getDocumentFiles(); + $this->assertIsArray($files); + $this->assertCount(0, $files); + } + + /** + * Test method getDocumentContents() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetDocumentContents() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $contents = $user->getDocumentContents(); + $this->assertIsArray($contents); + $this->assertCount(0, $contents); + } + + /** + * Test method getFolders() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetFolders() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $folders = $user->getFolders(); + $this->assertIsArray($folders); + $this->assertCount(1, $folders); + } + + /** + * Test method getReviewStatus() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetReviewStatus() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $status = $user->getReviewStatus(); + $this->assertIsArray($status); + $this->assertCount(2, $status); + $this->assertCount(0, $status['indstatus']); + $this->assertCount(0, $status['grpstatus']); + } + + /** + * Test method getApprovalStatus() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetApprovalStatus() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $status = $user->getApprovalStatus(); + $this->assertIsArray($status); + $this->assertCount(2, $status); + $this->assertCount(0, $status['indstatus']); + $this->assertCount(0, $status['grpstatus']); + } + + /** + * Test method getWorkflowStatus() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowStatus() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $status = $user->getWorkflowStatus(); + $this->assertIsArray($status); + $this->assertCount(2, $status); + $this->assertCount(0, $status['u']); + $this->assertCount(0, $status['g']); + } + + /** + * Test method getWorkflowsInvolved() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetWorkflowsInvolved() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $workflows = $user->getWorkflowsInvolved(); + $this->assertIsArray($workflows); + $this->assertCount(0, $workflows); + } + + /** + * Test method getMandatoryReviewers() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetMandatoryReviewers() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $reviewers = $user->getMandatoryReviewers(); + $this->assertIsArray($reviewers); + $this->assertCount(0, $reviewers); + } + + /** + * Test method setMandatoryReviewer() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetMandatoryReviewer() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $newuser = self::$dms->addUser('newuser', '', 'New User', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + $ret = $user->setMandatoryReviewer($newuser->getId(), false); + $this->assertTrue($ret); + $reviewers = $user->getMandatoryReviewers(); + $this->assertIsArray($reviewers); + $this->assertCount(1, $reviewers); + /* $newuser is now a mandatory user of $user */ + $mandatoryreviewers = $newuser->isMandatoryReviewerOf(); + $this->assertIsArray($mandatoryreviewers); + $this->assertCount(1, $mandatoryreviewers); + $this->assertEquals($user->getId(), $mandatoryreviewers[0]->getId()); + + $group = self::$dms->addGroup('Group', ''); + $ret = $user->setMandatoryReviewer($group->getId(), true); + $this->assertTrue($ret); + $reviewers = $user->getMandatoryReviewers(); + $this->assertIsArray($reviewers); + $this->assertCount(2, $reviewers); + /* FIXME: there is not isMandatoryReviewerOf() for groups */ + } + + /** + * Test method getMandatoryApprovers() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetMandatoryApprovers() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $approvers = $user->getMandatoryApprovers(); + $this->assertIsArray($approvers); + $this->assertCount(0, $approvers); + } + + /** + * Test method setMandatoryApprover() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetMandatoryApprover() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $newuser = self::$dms->addUser('newuser', '', 'New User', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + $ret = $user->setMandatoryApprover($newuser->getId(), false); + $this->assertTrue($ret); + $approvers = $user->getMandatoryApprovers(); + $this->assertIsArray($approvers); + $this->assertCount(1, $approvers); + /* $newuser is now a mandatory user of $user */ + $mandatoryapprovers = $newuser->isMandatoryApproverOf(); + $this->assertIsArray($mandatoryapprovers); + $this->assertCount(1, $mandatoryapprovers); + $this->assertEquals($user->getId(), $mandatoryapprovers[0]->getId()); + + $group = self::$dms->addGroup('Group', ''); + $ret = $user->setMandatoryApprover($group->getId(), true); + $this->assertTrue($ret); + $approvers = $user->getMandatoryApprovers(); + $this->assertIsArray($approvers); + $this->assertCount(2, $approvers); + /* FIXME: there is not isMandatoryApproverOf() for groups */ + } + + /** + * Test method setMandatoryWorkflow() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetMandatoryWorkflow() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $approver = self::$dms->addUser('approver', '', 'Approver', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + $reviewer = self::$dms->addUser('reviewer', '', 'Reviewer', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + $simpleworkflow = self::createSimpleWorkflow($approver); + $traditionalworkflow = self::createWorkflow($reviewer, $approver); + $newuser = self::$dms->addUser('newuser', '', 'New User', 'newuser@seeddms.org', 'en_GB', 'bootstrap', ''); + /* Set a single mandatory workflow */ + $ret = $newuser->setMandatoryWorkflow($simpleworkflow); + $this->assertTrue($ret); + $workflows = $newuser->getMandatoryWorkflows(); + $this->assertIsArray($workflows); + $this->assertCount(1, $workflows); + + /* Set a single mandatory workflow will add it to the list of workflows */ + $ret = $newuser->setMandatoryWorkflow($traditionalworkflow); + $this->assertTrue($ret); + $workflows = $newuser->getMandatoryWorkflows(); + $this->assertIsArray($workflows); + $this->assertCount(2, $workflows); + + /* Set a single mandatory workflow with setMandatoryWorkflows() will delete + * all existing workflows and set a new list of workflows + */ + $ret = $newuser->setMandatoryWorkflows([$simpleworkflow]); + $this->assertTrue($ret); + $workflows = $newuser->getMandatoryWorkflows(); + $this->assertIsArray($workflows); + $this->assertCount(1, $workflows); + + /* Set several mandatory workflows will delete all existing workflows + * and set new workflows. + */ + $ret = $newuser->setMandatoryWorkflows([$simpleworkflow, $traditionalworkflow]); + $this->assertTrue($ret); + $workflows = $newuser->getMandatoryWorkflows(); + $this->assertIsArray($workflows); + $this->assertCount(2, $workflows); + + /* Setting an empty list will delete all mandatory workflows */ + $ret = $newuser->setMandatoryWorkflows([]); + $this->assertTrue($ret); + $workflows = $newuser->getMandatoryWorkflows(); + $this->assertNull($workflows); + } + + /** + * Test method getMandatoryWorkflow() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetMandatoryWorkflow() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $workflow = $user->getMandatoryWorkflow(); + $this->assertNull($workflow); + } + + /** + * Test method getMandatoryWorkflows() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetMandatoryWorkflows() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $workflow = $user->getMandatoryWorkflows(); + $this->assertNull($workflow); + } + + /** + * Test method getGroups() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetGroups() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $groups = $user->getGroups(); + $this->assertIsArray($groups); + $this->assertCount(0, $groups); + $group = self::$dms->addGroup('Group', ''); + $ret = $user->joinGroup($group); + $this->assertTrue($ret); + /* Adding the user a twice to a group will fail */ + $ret = $user->joinGroup($group); + $this->assertFalse($ret); + /* user now belongs to two groups */ + $groups = $user->getGroups(); + $this->assertIsArray($groups); + $this->assertCount(1, $groups); + /* Leave the group */ + $ret = $user->leaveGroup($group); + $this->assertTrue($ret); + /* Leave the group again will fail */ + $ret = $user->leaveGroup($group); + $this->assertFalse($ret); + /* the user is no longer in any group */ + $groups = $user->getGroups(); + $this->assertIsArray($groups); + $this->assertCount(0, $groups); + } + + /** + * Test method hasImage() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testHasImage() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $image = $user->hasImage(); + $this->assertFalse($image); + } + + /** + * Test method hasImage() + * + * @return void + */ + public function testHasImageSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT COUNT(*) AS num FROM `tblUserImages` WHERE")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->hasImage()); + } + + /** + * Test method getImage() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetImage() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $image = $user->getImage(); + $this->assertNull($image); + } + + /** + * Test method getImage() + * + * @return void + */ + public function testGetImageSqlFail() + { + $user = $this->getAdminUser(); + $db = $this->createMock(SeedDMS_Core_DatabaseAccess::class); + $db->expects($this->once()) + ->method('getResultArray') + ->with($this->stringContains("SELECT * FROM `tblUserImages` WHERE")) + ->willReturn(false); + $dms = new SeedDMS_Core_DMS($db, ''); + $user->setDMS($dms); + $this->assertFalse($user->getImage()); + } + + /** + * Test method setImage() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testSetImage() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $file = self::createTempFile(200); + $ret = $user->setImage($file, 'text/plain'); + $this->assertTrue(SeedDMS_Core_File::removeFile($file)); + $this->assertTrue($ret); + $ret = $user->hasImage(); + $this->assertTrue($ret); + $image = $user->getImage(); + $this->assertIsArray($image); + $this->assertEquals('text/plain', $image['mimeType']); + } + + /** + * Test method delMandatoryReviewers() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testDelMandatoryReviewers() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $ret = $user->delMandatoryReviewers(); + $this->assertTrue($ret); + } + + /** + * Test method delMandatoryApprovers() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testDelMandatoryApprovers() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $ret = $user->delMandatoryApprovers(); + $this->assertTrue($ret); + } + + /** + * Test method delMandatoryWorkflow() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testDelMandatoryWorkflow() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $ret = $user->delMandatoryWorkflow(); + $this->assertTrue($ret); + } + + /** + * Test method getNotifications() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetNotifications() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $notifications = $user->getNotifications(); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + $notifications = $user->getNotifications(0); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + $notifications = $user->getNotifications(1); + $this->assertIsArray($notifications); + $this->assertCount(0, $notifications); + } + + /** + * Test method getKeywordCategories() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetKeywordCategories() + { + $user = SeedDMS_Core_User::getInstance(1, self::$dms); + $cats = $user->getKeywordCategories(); + $this->assertIsArray($cats); + $this->assertCount(0, $cats); + } +} + diff --git a/SeedDMS_Core/tests/WorkflowTest.php b/SeedDMS_Core/tests/WorkflowTest.php new file mode 100644 index 000000000..3abe1c26d --- /dev/null +++ b/SeedDMS_Core/tests/WorkflowTest.php @@ -0,0 +1,638 @@ + + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version @package_version@ + * @link https://www.seeddms.org + */ + +use PHPUnit\Framework\SeedDmsTest; + +/** + * Group test class + * + * @category SeedDMS + * @package Tests + * @author Uwe Steinmann + * @copyright 2021 Uwe Steinmann + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @version Release: @package_version@ + * @link https://www.seeddms.org + */ +class WorkflowTest extends SeedDmsTest +{ + + /** + * Create a real sqlite database in memory + * + * @return void + */ + protected function setUp(): void + { + self::$dbh = self::createInMemoryDatabase(); + self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); + mkdir(self::$contentdir); + // echo "Creating temp content dir: ".self::$contentdir."\n"; + self::$dms = new \SeedDMS_Core_DMS(self::$dbh, self::$contentdir); + self::$dbversion = self::$dms->getDBVersion(); + } + + /** + * Clean up at tear down + * + * @return void + */ + protected function tearDown(): void + { + self::$dbh = null; + // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; + exec('rm -rf '.self::$contentdir); + } + + /** + * Test method getInitState() and setInitState() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetInitState() + { + $ws_nr = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); + $ws_na = self::$dms->addWorkflowState('needs approval', S_IN_WORKFLOW); + $workflow = self::$dms->addWorkflow('traditional workflow', $ws_nr); + $initstate = $workflow->getInitState(); + $this->assertEquals($ws_nr->getName(), $initstate->getName()); + $ret = $workflow->setInitState($ws_na); + $this->assertTrue($ret); + $initstate = $workflow->getInitState(); + $this->assertEquals($ws_na->getName(), $initstate->getName()); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetStateName() + { + $state = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); + $name = $state->getName(); + $this->assertEquals('needs review', $name); + $ret = $state->setName('foobar'); + $this->assertTrue($ret); + $name = $state->getName(); + $this->assertEquals('foobar', $name); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetActionName() + { + $action = self::$dms->addWorkflowAction('action'); + $name = $action->getName(); + $this->assertEquals('action', $name); + $ret = $action->setName('foobar'); + $this->assertTrue($ret); + $name = $action->getName(); + $this->assertEquals('foobar', $name); + } + + /** + * Test method getName() and setName() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetWorkflowName() + { + $ws_nr = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); + $workflow = self::$dms->addWorkflow('traditional workflow', $ws_nr); + $name = $workflow->getName(); + $this->assertEquals('traditional workflow', $name); + $ret = $workflow->setName('foo'); + $this->assertTrue($ret); + $name = $workflow->getName(); + $this->assertEquals('foo', $name); + } + + /** + * Test method getDocumentStatus() and setDocumentStatus() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testGetAndSetDocumentStatus() + { + $state = self::$dms->addWorkflowState('some name', S_RELEASED); + $docstatus = $state->getDocumentStatus(); + $this->assertEquals(S_RELEASED, $docstatus); + $ret = $state->setDocumentStatus(S_REJECTED); + $this->assertTrue($ret); + $docstatus = $state->getDocumentStatus(); + $this->assertEquals(S_REJECTED, $docstatus); + } + + /** + * Test method workflow->remove() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCreateAndRemoveWorkflow() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the reviewer */ + $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($reviewer); + + /* Add a new user who will be the approver */ + $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($approver); + + $workflow = self::createWorkflow($reviewer, $approver); + $this->assertIsObject($workflow); + + $ret = $workflow->remove(); + $this->assertTrue($ret); + + $states = self::$dms->getAllWorkflowStates(); + $this->assertIsArray($states); + $this->assertCount(4, $states); + foreach($states as $state) + $this->assertFalse($state->isUsed()); + + $actions = self::$dms->getAllWorkflowActions(); + $this->assertIsArray($actions); + $this->assertCount(3, $actions); + foreach($actions as $action) + $this->assertFalse($action->isUsed()); + + } + + /** + * Test method remove() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCreateAndRemoveAction() + { + $action = self::$dms->addWorkflowAction('action'); + $this->assertIsObject($action); + $actions = self::$dms->getAllWorkflowActions(); + $this->assertIsArray($actions); + $this->assertCount(1, $actions); + $ret = $action->remove(); + $this->assertTrue($ret); + $actions = self::$dms->getAllWorkflowActions(); + $this->assertIsArray($actions); + $this->assertCount(0, $actions); + } + + /** + * Test method remove() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testCreateAndRemoveState() + { + $state = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); + $this->assertIsObject($state); + $states = self::$dms->getAllWorkflowStates(); + $this->assertIsArray($states); + $this->assertCount(1, $states); + $ret = $state->remove(); + $this->assertTrue($ret); + $states = self::$dms->getAllWorkflowStates(); + $this->assertIsArray($states); + $this->assertCount(0, $states); + } + + /** + * Test method setWorkflow(), getWorkflow(), getWorkflowState() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testAssignWorkflow() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the reviewer */ + $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($reviewer); + + /* Add a new user who will be the approver */ + $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($approver); + + $workflow = self::createWorkflow($reviewer, $approver); + $this->assertIsObject($workflow); + + /* Check for cycles */ + $cycles = $workflow->checkForCycles(); + $this->assertFalse($cycles); + + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $content = $document->getLatestContent(); + $this->assertIsObject($content); + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* Assign the workflow */ + $ret = $content->setWorkflow($workflow, $user); + $this->assertTrue($ret); + + /* Assign a workflow again causes an error */ + $ret = $content->setWorkflow($workflow, $user); + $this->assertFalse($ret); + + /* Get a fresh copy of the content from the database and get the workflow */ + $again = self::$dms->getDocumentContent($content->getId()); + $this->assertIsObject($again); + $w = $again->getWorkflow(); + $this->assertEquals($workflow->getId(), $w->getId()); + + /* Status of content should be S_IN_WORKFLOW now */ + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_IN_WORKFLOW, $status['status']); + + /* Get current workflow state */ + $state = $content->getWorkflowState(); + $this->assertEquals('needs review', $state->getName()); + + $workflowlog = $content->getWorkflowLog(); + $this->assertIsArray($workflowlog); + $this->assertCount(0, $workflowlog); + + /* The workflow has altogether 4 states */ + $states = $workflow->getStates(); + $this->assertIsArray($states); + $this->assertCount(4, $states); + + /* Check the initial state */ + $initstate = $workflow->getInitState(); + $this->assertEquals('needs review', $initstate->getName()); + + /* init state is definitely used */ + $ret = $initstate->isUsed(); + $this->assertTrue($ret); + + /* init state has two transistions linked to it */ + $transitions = $initstate->getTransitions(); + $this->assertIsArray($transitions); + $this->assertCount(2, $transitions); + + /* Check if workflow is used by any document */ + $isused = $workflow->isUsed(); + $this->assertTrue($isused); + + } + + /** + * Test method setWorkflow(), getWorkflow(), getWorkflowState() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testStepThroughWorkflow() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the reviewer */ + $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($reviewer); + + /* Add a new user who will be the approver */ + $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($approver); + + $workflow = self::createWorkflow($reviewer, $approver); + + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $content = $document->getLatestContent(); + $this->assertIsObject($content); + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* Assign the workflow */ + $ret = $content->setWorkflow($workflow, $user); + $this->assertTrue($ret); + + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_IN_WORKFLOW, $status['status']); + + /* Remove the workflow */ + $ret = $content->removeWorkflow($user); + $this->assertTrue($ret); + + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* Remove the workflow again is just fine */ + $ret = $content->removeWorkflow($user); + $this->assertTrue($ret); + + /* Assign the workflow again */ + $ret = $content->setWorkflow($workflow, $user); + $this->assertTrue($ret); + + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_IN_WORKFLOW, $status['status']); + + + /* Check if workflow needs action by the reviewer/approver */ + $ret = $content->needsWorkflowAction($reviewer); + $this->assertTrue($ret); + $ret = $content->needsWorkflowAction($approver); + $this->assertFalse($ret); + + /* Get current workflow state*/ + $state = $content->getWorkflowState(); + $this->assertEquals('needs review', $state->getName()); + + /* There should be two possible transitions now + * NR -- review -> NA + * NR -- reject -> RJ + */ + $nexttransitions = $workflow->getNextTransitions($state); + $this->assertIsArray($nexttransitions); + $this->assertCount(2, $nexttransitions); + + /* But of course, there were no previous transitions */ + $prevtransitions = $workflow->getPreviousTransitions($state); + $this->assertIsArray($prevtransitions); + $this->assertCount(0, $prevtransitions); + + /* Check if reviewer is allowed to trigger the transition. + * As we are still in the intitial state, the possible transitions + * may both be triggered by the reviewer but not by the approver. + */ + foreach($nexttransitions as $nexttransition) { + if($nexttransition->getNextState()->getDocumentStatus() == S_REJECTED) + $rejecttransition = $nexttransition; + elseif($nexttransition->getNextState()->getDocumentStatus() == S_IN_WORKFLOW) + $reviewtransition = $nexttransition; + $ret = $content->triggerWorkflowTransitionIsAllowed($reviewer, $nexttransition); + $this->assertTrue($ret); + $ret = $content->triggerWorkflowTransitionIsAllowed($approver, $nexttransition); + $this->assertFalse($ret); + } + + /* Trigger the successful review transition. + * As there is only one reviewer the transition will fire and the workflow + * moves forward into the next state. triggerWorkflowTransition() returns the + * next state. + */ + $nextstate = $content->triggerWorkflowTransition($reviewer, $reviewtransition, 'Review succeeded'); + $this->assertIsObject($nextstate); + $this->assertEquals('needs approval', $nextstate->getName()); + + $state = $content->getWorkflowState(); + $this->assertEquals($nextstate->getId(), $state->getId()); + $this->assertEquals('needs approval', $state->getName()); + + /* The workflow log has one entry now */ + $workflowlog = $content->getLastWorkflowLog(); + $this->assertIsObject($workflowlog); + $this->assertEquals('Review succeeded', $workflowlog->getComment()); + + /* There should be two possible transitions now + * NA -- approve -> RL + * NA -- reject -> RJ + */ + $nexttransitions = $workflow->getNextTransitions($state); + $this->assertIsArray($nexttransitions); + $this->assertCount(2, $nexttransitions); + + /* But of course, there is one previous transitions, the one that led to + * the current state of the workflow. + */ + $prevtransitions = $workflow->getPreviousTransitions($state); + $this->assertIsArray($prevtransitions); + $this->assertCount(1, $prevtransitions); + $this->assertEquals($reviewtransition->getId(), $prevtransitions[0]->getId()); + + /* Check if approver is allowed to trigger the transition. + * As we are now in 'needs approval' state, the possible transitions + * may both be triggered by the approver but not by the reviewer. + */ + foreach($nexttransitions as $nexttransition) { + if($nexttransition->getNextState()->getDocumentStatus() == S_REJECTED) + $rejecttransition = $nexttransition; + elseif($nexttransition->getNextState()->getDocumentStatus() == S_RELEASED) + $releasetransition = $nexttransition; + $ret = $content->triggerWorkflowTransitionIsAllowed($approver, $nexttransition); + $this->assertTrue($ret); + $ret = $content->triggerWorkflowTransitionIsAllowed($reviewer, $nexttransition); + $this->assertFalse($ret); + } + + /* Trigger the successful approve transition. + * As there is only one approver the transition will fire and the workflow + * moves forward into the next state. triggerWorkflowTransition() returns the + * next state. + */ + $nextstate = $content->triggerWorkflowTransition($approver, $releasetransition, 'Approval succeeded'); + $this->assertIsObject($nextstate); + $this->assertEquals('released', $nextstate->getName()); + + /* The workflow log has two entries now */ + $workflowlog = $content->getLastWorkflowLog(); + $this->assertIsObject($workflowlog); + $this->assertEquals('Approval succeeded', $workflowlog->getComment()); + + /* Because the workflow has reached a final state, the workflow will no + * longer be attached to the document. + */ + $workflow = $content->getWorkflow(); + $this->assertFalse($workflow); + + /* There is also no way to get the state anymore */ + $state = $content->getWorkflowState(); + $this->assertFalse($state); + + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* Even after the workflow has been finished the log can still be retrieved */ + $workflowlog = $content->getLastWorkflowLog(); + $this->assertIsObject($workflowlog); + $this->assertEquals('Approval succeeded', $workflowlog->getComment()); + } + + /** + * Test method rewindWorkflow() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testRewindWorkflow() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the reviewer */ + $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($reviewer); + + /* Add a new user who will be the approver */ + $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($approver); + + $workflow = self::createWorkflow($reviewer, $approver); + + /* Add a new document */ + $document = self::createDocument($rootfolder, $user, 'Document 1'); + $content = $document->getLatestContent(); + $this->assertIsObject($content); + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_RELEASED, $status['status']); + + /* Assign the workflow */ + $ret = $content->setWorkflow($workflow, $user); + $this->assertTrue($ret); + + $status = $content->getStatus(); + $this->assertIsArray($status); + $this->assertEquals(S_IN_WORKFLOW, $status['status']); + + /* Check if workflow needs action by the reviewer */ + $ret = $content->needsWorkflowAction($reviewer); + $this->assertTrue($ret); + + /* Get current workflow state*/ + $state = $content->getWorkflowState(); + $this->assertEquals('needs review', $state->getName()); + + /* There should be two possible transitions now + * NR -- review -> NA + * NR -- reject -> RJ + */ + $nexttransitions = $workflow->getNextTransitions($state); + $this->assertIsArray($nexttransitions); + $this->assertCount(2, $nexttransitions); + + /* Check if reviewer is allowed to trigger the transition. + * As we are still in the intitial state, the possible transitions + * may both be triggered by the reviewer but not by the approver. + */ + foreach($nexttransitions as $nexttransition) { + if($nexttransition->getNextState()->getDocumentStatus() == S_IN_WORKFLOW) + $reviewtransition = $nexttransition; + } + + /* Trigger the successful review transition. + * As there is only one reviewer the transition will fire and the workflow + * moves forward into the next state. triggerWorkflowTransition() returns the + * next state. + */ + $nextstate = $content->triggerWorkflowTransition($reviewer, $reviewtransition, 'Review succeeded'); + $this->assertIsObject($nextstate); + $this->assertEquals('needs approval', $nextstate->getName()); + + /* Get current workflow state*/ + $state = $content->getWorkflowState(); + $this->assertEquals('needs approval', $state->getName()); + + /* The workflow log has one entry now */ + $workflowlogs = $content->getWorkflowLog(); + $this->assertIsArray($workflowlogs); + $this->assertCount(1, $workflowlogs); + if(self::$dbversion['major'] > 5) + $this->assertEquals('Review succeeded', $workflowlogs[1][0]->getComment()); + else + $this->assertEquals('Review succeeded', $workflowlogs[0]->getComment()); + + $ret = $content->rewindWorkflow(); + $this->assertTrue($ret); + + /* After rewinding the workflow the initial state is set ... */ + $state = $content->getWorkflowState(); + $this->assertEquals('needs review', $state->getName()); + + /* and the workflow log has been cleared */ + $workflowlogs = $content->getWorkflowLog(); + $this->assertIsArray($workflowlogs); + $this->assertCount(0, $workflowlogs); + } + + /** + * Test method getTransitionsByStates() + * + * This method uses a real in memory sqlite3 database. + * + * @return void + */ + public function testTransitionsByStateWorkflow() + { + $rootfolder = self::$dms->getRootFolder(); + $user = self::$dms->getUser(1); + $this->assertIsObject($user); + + /* Add a new user who will be the reviewer */ + $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($reviewer); + + /* Add a new user who will be the approver */ + $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); + $this->assertIsObject($approver); + + $workflow = self::createWorkflow($reviewer, $approver); + + /* Check the initial state */ + $initstate = $workflow->getInitState(); + $this->assertEquals('needs review', $initstate->getName()); + + /* init state has two transistions linked to it */ + $transitions = $initstate->getTransitions(); + $this->assertIsArray($transitions); + $this->assertCount(2, $transitions); + + $t = $workflow->getTransitionsByStates($initstate, $transitions[1]->getNextState()); + $this->assertEquals($transitions[1]->getId(), $t[0]->getId()); + } + +} diff --git a/SeedDMS_Lucene/CHANGELOG.md b/SeedDMS_Lucene/CHANGELOG.md new file mode 100644 index 000000000..ce4685461 --- /dev/null +++ b/SeedDMS_Lucene/CHANGELOG.md @@ -0,0 +1,94 @@ +1.1.18 (2023-01-09) +--------------------- +- IndexedDocument() accepts a callable for conversion to text +- SeedDMS_Lucene_Search::open and create return itself but Zend_Search_Lucene + +1.1.17 (2021-05-10) +--------------------- +- close pipes in execWithTimeout(), also return exit code of command + + +1.1.16 (2020-12-12) +--------------------- +- add indexing of folders + +1.1.15 (2020-09-10) +--------------------- +- add searching for document status +- better error handling if opening index fails +- parameters for SeedDMS_Lucene_Search::search() has changed +- SeedDMS_Lucene_Search::search() returns array of hits, count and facets +- pass config array instead of index directory to SeedDMS_Lucene_Indexer::create() + and SeedDMS_Lucene_Indexer::open() + +1.1.14 (2020-09-02) +--------------------- +- Index users with at least read access on the document + +1.1.13 (2018-04-11) +--------------------- +- IndexedDocument() remembers cmd and mimetype + +1.1.12 (2018-01-30) +--------------------- +- execWithTimeout() reads data from stderr and saves it into error msg + +1.1.11 (2017-12-04) +--------------------- +- allow conversion commands for mimetypes with wildcards + +1.1.10 (2017-03-01) +--------------------- +- catch exception in execWithTimeout() + +1.1.9 (2016-04-28) +--------------------- +- pass variables to stream_select() to fullfill strict standards. +- make all functions in Indexer.php static + +1.1.8 (2016-03-29) +--------------------- +- set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 + +1.1.7 (2016-02-01) +--------------------- +- add command for indexing postѕcript files + +1.1.6 (2015-08-05) +--------------------- +- run external commands with a timeout + +1.1.5 (2014-07-30) +--------------------- +- field for original filename is treated as utf-8 +- declare SeeDMS_Lucene_Indexer::open() static + +1.1.4 (2013-08-13) +--------------------- +- class SeedDMS_Lucene_Search::search returns false if query is invalid instead of an empty result record + +1.1.3 (2013-06-27) +--------------------- +- explicitly set encoding to utf-8 when adding fields +- do not check if deleting document from index fails, update it in any case + +1.1.2 (2013-06-17) +--------------------- +- parse query term and catch errors before using it + +1.1.1 (2012-12-03) +--------------------- +- catch exception if index is opened but not available + +1.1.0 (2012-11-06) +--------------------- +- use a configurable list of mime type converters, fixed indexing and searching + of special chars like german umlaute. + +1.0.1 (2011-11-06) +--------------------- +- New Release + +0.0.1 (2009-04-27) +--------------------- + diff --git a/SeedDMS_Lucene/Lucene/IndexedDocument.php b/SeedDMS_Lucene/Lucene/IndexedDocument.php index 0ac27a583..c8e76b26c 100644 --- a/SeedDMS_Lucene/Lucene/IndexedDocument.php +++ b/SeedDMS_Lucene/Lucene/IndexedDocument.php @@ -145,12 +145,14 @@ class SeedDMS_Lucene_IndexedDocument extends Zend_Search_Lucene_Document { if($document->isType('document')) { $this->addField(Zend_Search_Lucene_Field::Keyword('document_id', 'D'.$document->getID())); + $this->addField(Zend_Search_Lucene_Field::Keyword('record_type', 'document')); $version = $document->getLatestContent(); if($version) { $this->addField(Zend_Search_Lucene_Field::Keyword('mimetype', $version->getMimeType())); $this->addField(Zend_Search_Lucene_Field::Keyword('origfilename', $version->getOriginalFileName(), 'utf-8')); + $this->addField(Zend_Search_Lucene_Field::UnIndexed('created', $version->getDate())); if(!$nocontent) - $this->addField(Zend_Search_Lucene_Field::UnIndexed('created', $version->getDate())); + $this->addField(Zend_Search_Lucene_Field::UnIndexed('indexed', time())); if($attributes = $version->getAttributes()) { foreach($attributes as $attribute) { $attrdef = $attribute->getAttributeDefinition(); @@ -181,7 +183,15 @@ class SeedDMS_Lucene_IndexedDocument extends Zend_Search_Lucene_Document { if(file_exists($path)) { $mimetype = $version->getMimeType(); $this->mimetype = $mimetype; - if(is_object($convcmd) && (get_class($convcmd) == 'SeedDMS_ConversionMgr')) { + if(is_callable($convcmd)) { + $result = $convcmd($document); + if($result['content']) { + self::setContent($result['content']); + } elseif($result['content'] === false) { + $this->errormsg = $result['errormsg']; + } + $this->cmd = $result['cmd']; + } elseif(is_object($convcmd) && (get_class($convcmd) == 'SeedDMS_ConversionMgr')) { if($service = $convcmd->getService($mimetype, 'text/plain')) { $content = $convcmd->convert($path, $mimetype, 'text/plain'); if($content) { @@ -223,7 +233,9 @@ class SeedDMS_Lucene_IndexedDocument extends Zend_Search_Lucene_Document { } } elseif($document->isType('folder')) { $this->addField(Zend_Search_Lucene_Field::Keyword('document_id', 'F'.$document->getID())); + $this->addField(Zend_Search_Lucene_Field::Keyword('record_type', 'folder')); $this->addField(Zend_Search_Lucene_Field::UnIndexed('created', $document->getDate())); + $this->addField(Zend_Search_Lucene_Field::UnIndexed('indexed', time())); } } /* }}} */ diff --git a/SeedDMS_Lucene/Lucene/Indexer.php b/SeedDMS_Lucene/Lucene/Indexer.php index 7c3b3f68a..f0dee91ce 100644 --- a/SeedDMS_Lucene/Lucene/Indexer.php +++ b/SeedDMS_Lucene/Lucene/Indexer.php @@ -29,10 +29,23 @@ class SeedDMS_Lucene_Indexer { */ protected $indexname; + /** + * @var string $index lucene index + * @access protected + */ + protected $index; + + public function __construct($index) { + $this->index = $index; + } + static function open($conf) { /* {{{ */ try { $index = Zend_Search_Lucene::open($conf['indexdir']); - return($index); + if($index) + return new self($index); + else + return null; } catch (Exception $e) { return null; } @@ -41,7 +54,10 @@ class SeedDMS_Lucene_Indexer { static function create($conf) { /* {{{ */ try { $index = Zend_Search_Lucene::create($conf['indexdir']); - return($index); + if($index) + return new self($index); + else + return null; } catch (Exception $e) { return null; } @@ -51,7 +67,7 @@ class SeedDMS_Lucene_Indexer { * Do some initialization * */ - static function init($stopWordsFile='') { /* {{{ */ + public function init($stopWordsFile='') { /* {{{ */ $analyzer = new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8_CaseInsensitive(); if($stopWordsFile && file_exists($stopWordsFile)) { $stopWordsFilter = new Zend_Search_Lucene_Analysis_TokenFilter_StopWords(); @@ -62,6 +78,131 @@ class SeedDMS_Lucene_Indexer { Zend_Search_Lucene_Analysis_Analyzer::setDefault($analyzer); } /* }}} */ + /** + * Add document to index + * + * @param object $doc indexed document of class + * SeedDMS_Lucene_IndexedDocument + * @return boolean false in case of an error, otherwise true + */ + function addDocument($doc) { /* {{{ */ + if(!$this->index) + return false; + return $this->index->addDocument($doc); + } /* }}} */ + + /** + * Remove document from index + * + * @param object $id internal id of document + * @return boolean false in case of an error, otherwise true + */ + public function delete($id) { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->delete($id); + } /* }}} */ + + /** + * Check if document was deleted + * + * @param object $id internal id of document + * @return boolean true if document was deleted + */ + public function isDeleted($id) { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->isDeleted($id); + } /* }}} */ + + /** + * Search in index + * + * @param string $query + * @return array result + */ + public function find($query) { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->find($query); + } /* }}} */ + + /** + * Get a single document from index + * + * @param string $id id of document + * @return boolean false in case of an error, otherwise true + */ + public function findById($id) { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->findById($id); + } /* }}} */ + + /** + * Get a single document from index + * + * @param integer $id id of index record + * @return boolean false in case of an error, otherwise true + */ + public function getDocument($id, $content=true) { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->getDocument($id); + } /* }}} */ + + /** + * Return list of terms in index + * + * @return array list of Zend_Lucene_Term + */ + public function terms($prefix='', $col='') { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->terms(); + } /* }}} */ + + /** + * Return number of documents in index + * + * @return interger number of documents + */ + public function count() { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->count(); + } /* }}} */ + + /** + * Commit changes + * + * This function does nothing! + */ + function commit() { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->commit(); + } /* }}} */ + + /** + * Optimize index + * + * This function does nothing! + */ + function optimize() { /* {{{ */ + if(!$this->index) + return false; + + return $this->index->optimize(); + } /* }}} */ } ?> diff --git a/SeedDMS_Lucene/Lucene/Search.php b/SeedDMS_Lucene/Lucene/Search.php index 5ca533d93..38827a02b 100644 --- a/SeedDMS_Lucene/Lucene/Search.php +++ b/SeedDMS_Lucene/Lucene/Search.php @@ -77,10 +77,10 @@ class SeedDMS_Lucene_Search { $querystr = substr($term, -1) != '*' ? $term.'*' : $term; } if(!empty($fields['owner'])) { - if(is_string($owner)) { + if(is_string($fields['owner'])) { if($querystr) $querystr .= ' && '; - $querystr .= 'owner:'.$owner; + $querystr .= 'owner:'.$fields['owner']; } elseif(is_array($fields['owner'])) { if($querystr) $querystr .= ' && '; @@ -89,6 +89,13 @@ class SeedDMS_Lucene_Search { $querystr .= '")'; } } + if(!empty($fields['record_type'])) { + if($querystr) + $querystr .= ' && '; + $querystr .= '(record_type:'; + $querystr .= implode(' || record_type:', $fields['record_type']); + $querystr .= ')'; + } if(!empty($fields['category'])) { if($querystr) $querystr .= ' && '; @@ -137,7 +144,7 @@ class SeedDMS_Lucene_Search { $recs = array(); $c = 0; foreach($hits as $hit) { - if($c >= $limit['offset'] && ($c-$limit['offset'] < $limit)) + if($c >= $limit['offset'] && ($c-$limit['offset'] < $limit['limit'])) $recs[] = array('id'=>$hit->id, 'document_id'=>$hit->document_id); $c++; } diff --git a/SeedDMS_Lucene/composer.json b/SeedDMS_Lucene/composer.json new file mode 100644 index 000000000..68a37caaf --- /dev/null +++ b/SeedDMS_Lucene/composer.json @@ -0,0 +1,23 @@ +{ + "name": "seeddms/lucene", + "description": "Lucene based fulltext search for SeedDMS ", + "type": "library", + "license": "GPL-2.0-or-later", + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Seeddms\\Lucene\\": "Lucene/" + }, + "classmap": ["Lucene/"] + }, + "authors": [ + { + "name": "Uwe Steinmann", + "email": "info@seeddms.org" + } + ], + "require-dev": { + "phpunit/phpunit": "^9" + } + +} diff --git a/SeedDMS_Lucene/package.xml b/SeedDMS_Lucene/package.xml index 01192ead0..eeb33269a 100644 --- a/SeedDMS_Lucene/package.xml +++ b/SeedDMS_Lucene/package.xml @@ -11,11 +11,11 @@ uwe@steinmann.cx yes - 2021-05-10 + 2023-01-09 - 1.1.17 - 1.1.17 + 1.1.18 + 1.1.18 stable @@ -23,7 +23,8 @@ GPL License -- close pipes in execWithTimeout(), also return exit code of command +- IndexedDocument() accepts a callable for conversion to text +- SeedDMS_Lucene_Search::open and create return itself but Zend_Search_Lucene @@ -58,6 +59,7 @@ + 2009-04-27 0.0.1 0.0.1 @@ -66,7 +68,6 @@ alpha alpha - 2009-04-27 BSD License @@ -100,8 +101,8 @@ GPL License -use a configurable list of mime type converters, fixed indexing and searching -of special chars like german umlaute. +- use a configurable list of mime type converters, fixed indexing and searching + of special chars like german umlaute. @@ -117,7 +118,7 @@ of special chars like german umlaute. GPL License -catch exception if index is opened but not available +- catch exception if index is opened but not available @@ -133,7 +134,7 @@ catch exception if index is opened but not available GPL License -parse query term and catch errors before using it +- parse query term and catch errors before using it @@ -149,8 +150,8 @@ parse query term and catch errors before using it GPL License -explicitly set encoding to utf-8 when adding fields -do not check if deleting document from index fails, update it in any case +- explicitly set encoding to utf-8 when adding fields +- do not check if deleting document from index fails, update it in any case @@ -166,7 +167,7 @@ do not check if deleting document from index fails, update it in any case GPL License -class SeedDMS_Lucene_Search::search returns false if query is invalid instead of an empty result record +- class SeedDMS_Lucene_Search::search returns false if query is invalid instead of an empty result record @@ -182,8 +183,8 @@ class SeedDMS_Lucene_Search::search returns false if query is invalid instead of GPL License -field for original filename is treated as utf-8 -declare SeeDMS_Lucene_Indexer::open() static +- field for original filename is treated as utf-8 +- declare SeeDMS_Lucene_Indexer::open() static @@ -199,7 +200,7 @@ declare SeeDMS_Lucene_Indexer::open() static GPL License -run external commands with a timeout +- run external commands with a timeout @@ -215,7 +216,7 @@ run external commands with a timeout GPL License -add command for indexing postѕcript files +- add command for indexing postѕcript files @@ -231,7 +232,7 @@ add command for indexing postѕcript files GPL License -set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 +- set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 @@ -247,8 +248,8 @@ set last parameter of stream_select() to 200000 micro sec. in case the timeout i GPL License -pass variables to stream_select() to fullfill strict standards. -make all functions in Indexer.php static +- pass variables to stream_select() to fullfill strict standards. +- make all functions in Indexer.php static @@ -264,7 +265,7 @@ make all functions in Indexer.php static GPL License -catch exception in execWithTimeout() +- catch exception in execWithTimeout() @@ -280,7 +281,7 @@ catch exception in execWithTimeout() GPL License -allow conversion commands for mimetypes with wildcards +- allow conversion commands for mimetypes with wildcards @@ -296,7 +297,7 @@ allow conversion commands for mimetypes with wildcards GPL License -execWithTimeout() reads data from stderr and saves it into error msg +- execWithTimeout() reads data from stderr and saves it into error msg @@ -312,7 +313,7 @@ execWithTimeout() reads data from stderr and saves it into error msg GPL License -IndexedDocument() remembers cmd and mimetype +- IndexedDocument() remembers cmd and mimetype @@ -328,7 +329,7 @@ IndexedDocument() remembers cmd and mimetype GPL License -Index users with at least read access on the document +- Index users with at least read access on the document @@ -368,5 +369,21 @@ Index users with at least read access on the document - add indexing of folders + + 2021-05-10 + + + 1.1.17 + 1.1.17 + + + stable + stable + + GPL License + +- close pipes in execWithTimeout(), also return exit code of command + + diff --git a/SeedDMS_Preview/CHANGELOG.md b/SeedDMS_Preview/CHANGELOG.md new file mode 100644 index 000000000..76b1546c5 --- /dev/null +++ b/SeedDMS_Preview/CHANGELOG.md @@ -0,0 +1,128 @@ +1.5.0 (2023-01-09) +--------------------- +- add previewer which creates txt + +1.4.0 (2021-10-16) +--------------------- +- use new conversion service if available +- createRawPreview() checks early if a converter exists + + +1.3.3 (2020-12-23) +--------------------- +- close pipes in execWithTimeout(), also return exit code of command +- createPreview() has optional parameter by referenz to return true if a + preview image was actually created + +1.3.2 (2020-12-23) +--------------------- +- set header Content-Length +- update package description + +1.3.1 (2020-03-21) +--------------------- +- add parameter $target to SeedDMS_Preview_pdfPreviewer::hasRawPreview() and SeedDMS_Preview_pdfPreviewer::getRawPreview() + +1.3.0 (2020-02-17) +--------------------- +- add new methode getPreviewFile() + +1.2.10 (2019-02-11) +--------------------- +- new parameter for enabling/disabling xsendfile +- fix creation of pdf preview if document content class is not SeedDMS_Core_DocumentContent + +1.2.9 (2018-07-13) +--------------------- +- make sure list of converters is always an array +- usage of mod_sendfile can be configured + +1.2.8 (2018-03-08) +--------------------- +- preview is also created if SeedDMS_Core_DocumentContent has a child class + +1.2.7 (2018-01-18) +--------------------- +- add SeedDMS_Preview_Base::sendFile() as a replacement for readfile() which uses +- mod_xsendfile if available +- execWithTimeout() reads data from stderr and returns it together with stdout in array + +1.2.6 (2017-12-04) +--------------------- +- SeedDMS_Preview_Base::setConverters() overrides existing converters. +- New method SeedDMS_Preview_Base::addConverters() merges new converters with old ones. + +1.2.5 (2017-10-11) +--------------------- +- SeedDMS_Preview_Base::hasConverter() returns only try if command is set + +1.2.4 (2017-10-11) +--------------------- +- fix typo in converter for tar.gz files + +1.2.3 (2017-09-18) +--------------------- +- createPreview() returns false if running the converter command fails + +1.2.2 (2017-03-02) +--------------------- +- commands can be set for mimetypes 'xxxx/*' and '*' +- pass mimetype as parameter '%m' to converter + +1.2.1 (2016-11-15) +--------------------- +- setConverters() overrides exiting converters + +1.2.0 (2016-11-07) +--------------------- +- add new previewer which converts document to pdf instead of png + +1.1.9 (2016-04-26) +--------------------- +- add more documentation +- finish deletePreview() +- add new method deleteDocumentPreviews() +- fix calculation of timeout (Bug #269) +- check if cache dir exists before deleting it in deleteDocumentPreviews() + +1.1.8 (2016-04-05) +--------------------- +- pass variables to stream_select (required by php7) + +1.1.7 (2016-03-29) +--------------------- +- set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 + +1.1.6 (2016-03-08) +--------------------- +- check if object passed to createPreview(), hasPreview() is not null + +1.1.5 (2016-02-11) +--------------------- +- add method getFilesize() +- timeout for external commands can be passed to contructor of SeedDMS_Preview_Previewer + +1.1.4 (2015-08-08) +--------------------- +- command for creating the preview will be called with a given timeout + +1.1.3 (2015-02-13) +--------------------- +- preview images will also be recreated if the object this image belongs is of newer date than the image itself. This happens if versions are being deleted and than a new version is uploaded. Because the new version will get the version number of the old version, it will also take over the old preview image.Comparing the creation date of the image with the object detects this case. + +1.1.2 (2014-04-10) +--------------------- +- create fixed width image with proportional height + +1.1.1 (2014-03-18) +--------------------- +- add converters for .tar.gz, .ps, .txt + +1.1.0 (2013-04-29) +--------------------- +- preview image can also be created from a document file (SeedDMS_Core_DocumentFile) + +1.0.0 (2012-11-20) +--------------------- +- initial version + diff --git a/SeedDMS_Preview/Preview.php b/SeedDMS_Preview/Preview.php index d19d82943..ce5853001 100644 --- a/SeedDMS_Preview/Preview.php +++ b/SeedDMS_Preview/Preview.php @@ -31,4 +31,9 @@ require_once('Preview/Previewer.php'); */ require_once('Preview/PdfPreviewer.php'); +/** + * @uses Preview/PdfPreviewer.php + */ +require_once('Preview/TxtPreviewer.php'); + ?> diff --git a/SeedDMS_Preview/Preview/Base.php b/SeedDMS_Preview/Preview/Base.php index 1b67bcfae..9fb401ada 100644 --- a/SeedDMS_Preview/Preview/Base.php +++ b/SeedDMS_Preview/Preview/Base.php @@ -73,6 +73,7 @@ class SeedDMS_Preview_Base { $this->previewDir = $previewDir; } $this->timeout = intval($timeout); + $this->converters = array(); $this->xsendfile = $xsendfile; $this->conversionmgr = null; } /* }}} */ @@ -135,6 +136,15 @@ class SeedDMS_Preview_Base { } } /* }}} */ + /** + * Get preview dir + * + * @return string name of preview directory on disc + */ + public function getPreviewDir() { /* {{{ */ + return $this->previewDir; + } /* }}} */ + /** * Set a list of converters * @@ -145,7 +155,10 @@ class SeedDMS_Preview_Base { * and the value is the command to be called for creating the preview */ function setConverters($arr) { /* {{{ */ - $this->converters = $arr; + if(is_array($arr)) + $this->converters = $arr; + else + $this->converters = array(); } /* }}} */ /** diff --git a/SeedDMS_Preview/Preview/PdfPreviewer.php b/SeedDMS_Preview/Preview/PdfPreviewer.php index 53086867c..2a219b476 100644 --- a/SeedDMS_Preview/Preview/PdfPreviewer.php +++ b/SeedDMS_Preview/Preview/PdfPreviewer.php @@ -25,41 +25,30 @@ class SeedDMS_Preview_PdfPreviewer extends SeedDMS_Preview_Base { function __construct($previewDir, $timeout=5, $xsendfile=true) { /* {{{ */ - parent::__construct($previewDir, $timeout, $xsendfile); + parent::__construct($previewDir.DIRECTORY_SEPARATOR.'pdf', $timeout, $xsendfile); $this->converters = array( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'application/vnd.oasis.opendocument.text' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'text/rtf' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'application/msword' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'application/vnd.ms-excel' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'text/plain' => "unoconv -d document -f pdf --stdout -v '%f' > '%o'", - 'application/postscript' => "ps2pdf '%f' - > '%o'", - 'image/jpeg' => "convert '%f' pdf:- > '%o'", - 'image/png' => "convert '%f' pdf:- > '%o'", - 'image/gif' => "convert '%f' pdf:- > '%o'", - 'video/mp4' => "convert '%f[1-20]' pdf:- > '%o'", ); } /* }}} */ /** - * Return the physical filename of the preview image on disk + * Return the physical filename of the preview image on disc + * including the path * * @param object $object document content or document file * @return string file name of preview image */ - protected function getFileName($object) { /* {{{ */ + public function getFileName($object) { /* {{{ */ if(!$object) return false; $document = $object->getDocument(); $dms = $document->_dms; - $dir = $this->previewDir.'/'.$document->getDir(); + $dir = $this->previewDir.DIRECTORY_SEPARATOR.$document->getDir(); switch(get_class($object)) { case $dms->getClassname('documentcontent'): $target = $dir.'p'.$object->getVersion(); break; - case "SeedDMS_Core_DocumentFile": + case $dms->getClassname('documentfile'): $target = $dir.'f'.$object->getID(); break; default: @@ -101,8 +90,8 @@ class SeedDMS_Preview_PdfPreviewer extends SeedDMS_Preview_Base { if(!$this->previewDir) return false; - if(!is_dir($this->previewDir.'/'.$dir)) { - if (!SeedDMS_Core_File::makeDir($this->previewDir.'/'.$dir)) { + if(!is_dir($this->previewDir.DIRECTORY_SEPARATOR.$dir)) { + if (!SeedDMS_Core_File::makeDir($this->previewDir.DIRECTORY_SEPARATOR.$dir)) { return false; } } @@ -128,9 +117,11 @@ class SeedDMS_Preview_PdfPreviewer extends SeedDMS_Preview_Base { } elseif(isset($this->converters['*'])) { $cmd = str_replace(array('%f', '%o', '%m'), array($infile, $target.'.pdf', $mimetype), $this->converters['*']); } + if($cmd) { try { self::execWithTimeout($cmd, $this->timeout); + $new = true; } catch(Exception $e) { $this->lastpreviewfile = ''; return false; @@ -139,6 +130,7 @@ class SeedDMS_Preview_PdfPreviewer extends SeedDMS_Preview_Base { } return true; } + $new = false; return true; } /* }}} */ @@ -305,7 +297,7 @@ class SeedDMS_Preview_PdfPreviewer extends SeedDMS_Preview_Base { if(!$this->previewDir) return false; - $dir = $this->previewDir.'/'.$document->getDir(); + $dir = $this->previewDir.DIRECTORY_SEPARATOR.$document->getDir(); if(file_exists($dir) && is_dir($dir)) { return SeedDMS_Preview_Previewer::recurseRmdir($dir); } else { diff --git a/SeedDMS_Preview/Preview/Previewer.php b/SeedDMS_Preview/Preview/Previewer.php index 327f5e099..f53780e7a 100644 --- a/SeedDMS_Preview/Preview/Previewer.php +++ b/SeedDMS_Preview/Preview/Previewer.php @@ -29,41 +29,48 @@ class SeedDMS_Preview_Previewer extends SeedDMS_Preview_Base { */ protected $width; + /** + * Create instance of image previewer + * + * @param string $previewDir path of base directory where all images are + * stored. This directory will have a subdirectory derived from the object id. + * @param integer $width default width of an image + * @param integer $timeout timeout for shell commands to create a preview image + * @param boolean $xsendfile if set to true the apache module xsendfile will + * be used. + */ function __construct($previewDir, $width=40, $timeout=5, $xsendfile=true) { /* {{{ */ - parent::__construct($previewDir, $timeout, $xsendfile); + parent::__construct($previewDir.DIRECTORY_SEPARATOR.'png', $timeout, $xsendfile); $this->converters = array( - 'image/png' => "convert -resize %wx '%f' '%o'", - 'image/gif' => "convert -resize %wx '%f' '%o'", - 'image/jpg' => "convert -resize %wx '%f' '%o'", - 'image/jpeg' => "convert -resize %wx '%f' '%o'", - 'image/svg+xml' => "convert -resize %wx '%f' '%o'", - 'text/plain' => "convert -resize %wx '%f' '%o'", - 'application/pdf' => "convert -density 100 -resize %wx '%f[0]' '%o'", - 'application/postscript' => "convert -density 100 -resize %wx '%f[0]' '%o'", - 'application/x-compressed-tar' => "tar tzvf '%f' | convert -density 100 -resize %wx text:-[0] '%o'", ); $this->width = intval($width); } /* }}} */ /** - * Return the physical filename of the preview image on disk + * Return the physical filename of the preview image on disc + * including the path * * @param object $object document content or document file * @param integer $width width of preview image * @return string file name of preview image */ - public function getFileName($object, $width) { /* {{{ */ + public function getFileName($object, $width=0) { /* {{{ */ if(!$object) return false; + if($width == 0) + $width = $this->width; + else + $width = intval($width); + $document = $object->getDocument(); $dms = $document->_dms; - $dir = $this->previewDir.'/'.$document->getDir(); + $dir = $this->previewDir.DIRECTORY_SEPARATOR.$document->getDir(); switch(get_class($object)) { case $dms->getClassname('documentcontent'): $target = $dir.'p'.$object->getVersion().'-'.$width; break; - case "SeedDMS_Core_DocumentFile": + case $dms->getClassname('documentfile'): $target = $dir.'f'.$object->getID().'-'.$width; break; default: @@ -98,6 +105,7 @@ class SeedDMS_Preview_Previewer extends SeedDMS_Preview_Base { * @param string $mimetype MimeType of input file * @param integer $width width of generated preview image * @param string $target optional name of preview image (without extension) + * @param boolean $new will be set to true if the preview images was created * @return boolean true on success, false on failure */ public function createRawPreview($infile, $dir, $mimetype, $width=0, $target='', &$new=false) { /* {{{ */ @@ -110,8 +118,8 @@ class SeedDMS_Preview_Previewer extends SeedDMS_Preview_Base { $width = intval($width); if(!$this->previewDir) return false; - if(!is_dir($this->previewDir.'/'.$dir)) { - if (!SeedDMS_Core_File::makeDir($this->previewDir.'/'.$dir)) { + if(!is_dir($this->previewDir.DIRECTORY_SEPARATOR.$dir)) { + if (!SeedDMS_Core_File::makeDir($this->previewDir.DIRECTORY_SEPARATOR.$dir)) { return false; } } @@ -166,6 +174,7 @@ class SeedDMS_Preview_Previewer extends SeedDMS_Preview_Base { * @param object $object instance of SeedDMS_Core_DocumentContent * or SeedDMS_Core_DocumentFile * @param integer $width desired width of preview image + * @param boolean $new will be set to true if the preview images was created * @return boolean true on success, false on failure */ public function createPreview($object, $width=0, &$new=false) { /* {{{ */ @@ -350,7 +359,7 @@ class SeedDMS_Preview_Previewer extends SeedDMS_Preview_Base { if(!$this->previewDir) return false; - $dir = $this->previewDir.'/'.$document->getDir(); + $dir = $this->previewDir.DIRECTORY_SEPARATOR.$document->getDir(); if(file_exists($dir) && is_dir($dir)) { return SeedDMS_Preview_Previewer::recurseRmdir($dir); } else { diff --git a/SeedDMS_Preview/Preview/TxtPreviewer.php b/SeedDMS_Preview/Preview/TxtPreviewer.php new file mode 100644 index 000000000..7826ff793 --- /dev/null +++ b/SeedDMS_Preview/Preview/TxtPreviewer.php @@ -0,0 +1,306 @@ + + * @copyright Copyright (C) 2010, Uwe Steinmann + * @version Release: @package_version@ + */ + + +/** + * Class for managing creation of text preview for documents. + * + * @category DMS + * @package SeedDMS_Preview + * @version @version@ + * @author Uwe Steinmann + * @copyright Copyright (C) 2011, Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_Preview_TxtPreviewer extends SeedDMS_Preview_Base { + + function __construct($previewDir, $timeout=5, $xsendfile=true) { /* {{{ */ + parent::__construct($previewDir.DIRECTORY_SEPARATOR.'txt', $timeout, $xsendfile); + $this->converters = array( + ); + } /* }}} */ + + /** + * Return the physical filename of the preview image on disc + * including the path + * + * @param object $object document content or document file + * @return string file name of preview image + */ + public function getFileName($object) { /* {{{ */ + if(!$object) + return false; + + $document = $object->getDocument(); + $dms = $document->_dms; + $dir = $this->previewDir.DIRECTORY_SEPARATOR.$document->getDir(); + switch(get_class($object)) { + case $dms->getClassname('documentcontent'): + $target = $dir.'t'.$object->getVersion(); + break; + default: + return false; + } + return $target; + } /* }}} */ + + /** + * Check if converter for a given mimetype is set + * + * @param string $mimetype from mimetype + * + * @return boolean true if converter exists, otherwise false + */ + function hasConverter($from, $to='') { /* {{{ */ + return parent::hasConverter($from, 'text/plain'); + } /* }}} */ + + /** + * Create a text preview for a given file + * + * This method creates a preview in text format for a regular file + * in the file system and stores the result in the directory $dir relative + * to the configured preview directory. The filename of the resulting preview + * image is either $target.text (if set) or md5($infile).text. + * The $mimetype is used to select the propper conversion programm. + * An already existing text preview is replaced. + * + * @param string $infile name of input file including full path + * @param string $dir directory relative to $this->previewDir + * @param string $mimetype MimeType of input file + * @param string $target optional name of preview image (without extension) + * @return boolean true on success, false on failure + */ + public function createRawPreview($infile, $dir, $mimetype, $target='') { /* {{{ */ + if(!self::hasConverter($mimetype)) + return true; + + if(!$this->previewDir) + return false; + if(!is_dir($this->previewDir.DIRECTORY_SEPARATOR.$dir)) { + if (!SeedDMS_Core_File::makeDir($this->previewDir.DIRECTORY_SEPARATOR.$dir)) { + return false; + } + } + if(!file_exists($infile)) + return false; + if(!$target) + $target = $this->previewDir.$dir.md5($infile); + $this->lastpreviewfile = $target.'.txt'; + if($target != '' && (!file_exists($target.'.txt') || filectime($target.'.txt') < filectime($infile))) { + if($this->conversionmgr) { + if(!$this->conversionmgr->convert($infile, $mimetype, 'text/plain', $target.'.txt')) { + $this->lastpreviewfile = ''; + return false; + } + $new = true; + } else { + $cmd = ''; + $mimeparts = explode('/', $mimetype, 2); + if(isset($this->converters[$mimetype])) { + $cmd = str_replace(array('%f', '%o', '%m'), array($infile, $target.'.txt', $mimetype), $this->converters[$mimetype]); + } elseif(isset($this->converters[$mimeparts[0].'/*'])) { + $cmd = str_replace(array('%f', '%o', '%m'), array($infile, $target.'.txt', $mimetype), $this->converters[$mimeparts[0].'/*']); + } elseif(isset($this->converters['*'])) { + $cmd = str_replace(array('%f', '%o', '%m'), array($infile, $target.'.txt', $mimetype), $this->converters['*']); + } + + if($cmd) { + try { + self::execWithTimeout($cmd, $this->timeout); + $new = true; + } catch(Exception $e) { + $this->lastpreviewfile = ''; + return false; + } + } + } + return true; + } + $new = false; + return true; + + } /* }}} */ + + /** + * Create preview image + * + * This function creates a preview image for the given document + * content or document file. It internally uses + * {@link SeedDMS_Preview::createRawPreview()}. The filename of the + * preview image is created by {@link SeedDMS_Preview_Previewer::getFileName()} + * + * @param object $object instance of SeedDMS_Core_DocumentContent + * or SeedDMS_Core_DocumentFile + * @return boolean true on success, false on failure + */ + public function createPreview($object) { /* {{{ */ + if(!$object) + return false; + + $document = $object->getDocument(); + $file = $document->_dms->contentDir.$object->getPath(); + $target = $this->getFileName($object); + return $this->createRawPreview($file, $document->getDir(), $object->getMimeType(), $target); + } /* }}} */ + + /** + * Check if a preview image already exists. + * + * This function is a companion to {@link SeedDMS_Preview_Previewer::createRawPreview()}. + * + * @param string $infile name of input file including full path + * @param string $dir directory relative to $this->previewDir + * @return boolean true if preview exists, otherwise false + */ + public function hasRawPreview($infile, $dir, $target='') { /* {{{ */ + if(!$this->previewDir) + return false; + if(!$target) + $target = $this->previewDir.$dir.md5($infile); + if($target !== false && file_exists($target.'.txt') && filectime($target.'.txt') >= filectime($infile)) { + return true; + } + return false; + } /* }}} */ + + /** + * Check if a preview txt already exists. + * + * This function is a companion to {@link SeedDMS_Preview_Previewer::createPreview()}. + * + * @param object $object instance of SeedDMS_Core_DocumentContent + * or SeedDMS_Core_DocumentFile + * @return boolean true if preview exists, otherwise false + */ + public function hasPreview($object) { /* {{{ */ + if(!$object) + return false; + + if(!$this->previewDir) + return false; + $target = $this->getFileName($object); + if($target !== false && file_exists($target.'.txt') && filectime($target.'.txt') >= $object->getDate()) { + return true; + } + return false; + } /* }}} */ + + /** + * Return a preview image. + * + * This function returns the content of a preview image if it exists.. + * + * @param string $infile name of input file including full path + * @param string $dir directory relative to $this->previewDir + * @return boolean/string image content if preview exists, otherwise false + */ + public function getRawPreview($infile, $dir, $target='') { /* {{{ */ + if(!$this->previewDir) + return false; + + if(!$target) + $target = $this->previewDir.$dir.md5($infile); + if($target && file_exists($target.'.txt')) { + $this->sendFile($target.'.txt'); + } + } /* }}} */ + + /** + * Return a preview image. + * + * This function returns the content of a preview image if it exists.. + * + * @param object $object instance of SeedDMS_Core_DocumentContent + * or SeedDMS_Core_DocumentFile + * @return boolean/string image content if preview exists, otherwise false + */ + public function getPreview($object) { /* {{{ */ + if(!$this->previewDir) + return false; + + $target = $this->getFileName($object); + if($target && file_exists($target.'.txt')) { + $this->sendFile($target.'.txt'); + } + } /* }}} */ + + /** + * Return file size preview image. + * + * @param object $object instance of SeedDMS_Core_DocumentContent + * or SeedDMS_Core_DocumentFile + * @return boolean/integer size of preview image or false if image + * does not exist + */ + public function getFilesize($object) { /* {{{ */ + $target = $this->getFileName($object); + if($target && file_exists($target.'.txt')) { + return(filesize($target.'.txt')); + } else { + return false; + } + + } /* }}} */ + + /** + * Delete preview image. + * + * @param object $object instance of SeedDMS_Core_DocumentContent + * or SeedDMS_Core_DocumentFile + * @return boolean true if deletion succeded or false if file does not exist + */ + public function deletePreview($object) { /* {{{ */ + if(!$this->previewDir) + return false; + + $target = $this->getFileName($object); + if($target && file_exists($target.'.txt')) { + return(unlink($target.'.txt')); + } else { + return false; + } + } /* }}} */ + + static function recurseRmdir($dir) { + $files = array_diff(scandir($dir), array('.','..')); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? SeedDMS_Preview_Previewer::recurseRmdir("$dir/$file") : unlink("$dir/$file"); + } + return rmdir($dir); + } + + /** + * Delete all preview text belonging to a document + * + * This function removes the preview text of all versions and + * files of a document including the directory. It actually just + * removes the directory for the document in the cache. + * + * @param object $document instance of SeedDMS_Core_Document + * @return boolean true if deletion succeded or false if file does not exist + */ + public function deleteDocumentPreviews($document) { /* {{{ */ + if(!$this->previewDir) + return false; + + $dir = $this->previewDir.DIRECTORY_SEPARATOR.$document->getDir(); + if(file_exists($dir) && is_dir($dir)) { + return SeedDMS_Preview_Previewer::recurseRmdir($dir); + } else { + return false; + } + + } /* }}} */ +} +?> diff --git a/SeedDMS_Preview/composer.json b/SeedDMS_Preview/composer.json new file mode 100644 index 000000000..9a4b5a11a --- /dev/null +++ b/SeedDMS_Preview/composer.json @@ -0,0 +1,23 @@ +{ + "name": "seeddms/preview", + "description": "Create Preview images, pdf and txt for for SeedDMS ", + "type": "library", + "license": "GPL-2.0-or-later", + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Seeddms\\Preview\\": "Preview/" + }, + "classmap": ["Preview/"] + }, + "authors": [ + { + "name": "Uwe Steinmann", + "email": "info@seeddms.org" + } + ], + "require-dev": { + "phpunit/phpunit": "^9" + } + +} diff --git a/SeedDMS_Preview/package.xml b/SeedDMS_Preview/package.xml index 5dd1e4b41..d6dd7bc59 100644 --- a/SeedDMS_Preview/package.xml +++ b/SeedDMS_Preview/package.xml @@ -11,11 +11,11 @@ uwe@steinmann.cx yes - 2021-10-16 + 2023-01-09 - 1.4.0 - 1.4.0 + 1.5.0 + 1.5.0 stable @@ -23,8 +23,7 @@ GPL License -- use new conversion service if available -- createRawPreview() checks early if a converter exists +- add previewer which creates txt @@ -38,6 +37,9 @@ + + + @@ -49,7 +51,7 @@ - 4.3.0 + 7.4.0 1.5.4 @@ -71,7 +73,7 @@ GPL License - initial version +- initial version @@ -87,7 +89,7 @@ GPL License -preview image can also be created from a document file (SeedDMS_Core_DocumentFile) +- preview image can also be created from a document file (SeedDMS_Core_DocumentFile) @@ -103,7 +105,7 @@ preview image can also be created from a document file (SeedDMS_Core_DocumentFil GPL License -add converters for .tar.gz, .ps, .txt +- add converters for .tar.gz, .ps, .txt @@ -119,7 +121,7 @@ add converters for .tar.gz, .ps, .txt GPL License -create fixed width image with proportional height +- create fixed width image with proportional height @@ -135,7 +137,7 @@ create fixed width image with proportional height GPL License -preview images will also be recreated if the object this image belongs is of newer date than the image itself. This happens if versions are being deleted and than a new version is uploaded. Because the new version will get the version number of the old version, it will also take over the old preview image.Comparing the creation date of the image with the object detects this case. +- preview images will also be recreated if the object this image belongs is of newer date than the image itself. This happens if versions are being deleted and than a new version is uploaded. Because the new version will get the version number of the old version, it will also take over the old preview image.Comparing the creation date of the image with the object detects this case. @@ -151,7 +153,7 @@ preview images will also be recreated if the object this image belongs is of new GPL License -command for creating the preview will be called with a given timeout +- command for creating the preview will be called with a given timeout @@ -167,8 +169,8 @@ command for creating the preview will be called with a given timeout GPL License -add method getFilesize() -timeout for external commands can be passed to contructor of SeedDMS_Preview_Previewer +- add method getFilesize() +- timeout for external commands can be passed to contructor of SeedDMS_Preview_Previewer @@ -184,7 +186,7 @@ timeout for external commands can be passed to contructor of SeedDMS_Preview_Pre GPL License -check if object passed to createPreview(), hasPreview() is not null +- check if object passed to createPreview(), hasPreview() is not null @@ -200,7 +202,7 @@ check if object passed to createPreview(), hasPreview() is not null GPL License -set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 +- set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 @@ -216,7 +218,7 @@ set last parameter of stream_select() to 200000 micro sec. in case the timeout i GPL License -pass variables to stream_select (required by php7) +- pass variables to stream_select (required by php7) @@ -232,11 +234,11 @@ pass variables to stream_select (required by php7) GPL License -add more documentation -finish deletePreview() -add new method deleteDocumentPreviews() -fix calculation of timeout (Bug #269) -check if cache dir exists before deleting it in deleteDocumentPreviews() +- add more documentation +- finish deletePreview() +- add new method deleteDocumentPreviews() +- fix calculation of timeout (Bug #269) +- check if cache dir exists before deleting it in deleteDocumentPreviews() @@ -252,7 +254,7 @@ check if cache dir exists before deleting it in deleteDocumentPreviews() GPL License -add new previewer which converts document to pdf instead of png +- add new previewer which converts document to pdf instead of png @@ -268,7 +270,7 @@ add new previewer which converts document to pdf instead of png GPL License -setConverters() overrides exiting converters +- setConverters() overrides exiting converters @@ -284,8 +286,8 @@ setConverters() overrides exiting converters GPL License -commands can be set for mimetypes 'xxxx/*' and '*' -pass mimetype as parameter '%m' to converter +- commands can be set for mimetypes 'xxxx/*' and '*' +- pass mimetype as parameter '%m' to converter @@ -301,7 +303,7 @@ pass mimetype as parameter '%m' to converter GPL License -createPreview() returns false if running the converter command fails +- createPreview() returns false if running the converter command fails @@ -317,7 +319,7 @@ createPreview() returns false if running the converter command fails GPL License -fix typo in converter for tar.gz files +- fix typo in converter for tar.gz files @@ -333,7 +335,7 @@ fix typo in converter for tar.gz files GPL License -SeedDMS_Preview_Base::hasConverter() returns only try if command is set +- SeedDMS_Preview_Base::hasConverter() returns only try if command is set @@ -349,8 +351,8 @@ SeedDMS_Preview_Base::hasConverter() returns only try if command is set GPL License -SeedDMS_Preview_Base::setConverters() overrides existing converters. -New method SeedDMS_Preview_Base::addConverters() merges new converters with old ones. +- SeedDMS_Preview_Base::setConverters() overrides existing converters. +- New method SeedDMS_Preview_Base::addConverters() merges new converters with old ones. @@ -366,9 +368,9 @@ New method SeedDMS_Preview_Base::addConverters() merges new converters with old GPL License -add SeedDMS_Preview_Base::sendFile() as a replacement for readfile() which uses -mod_xsendfile if available -execWithTimeout() reads data from stderr and returns it together with stdout in array +- add SeedDMS_Preview_Base::sendFile() as a replacement for readfile() which uses +- mod_xsendfile if available +- execWithTimeout() reads data from stderr and returns it together with stdout in array @@ -384,7 +386,7 @@ execWithTimeout() reads data from stderr and returns it together with stdout in GPL License -preview is also created if SeedDMS_Core_DocumentContent has a child class +- preview is also created if SeedDMS_Core_DocumentContent has a child class @@ -400,8 +402,8 @@ preview is also created if SeedDMS_Core_DocumentContent has a child class GPL License -make sure list of converters is always an array -usage of mod_sendfile can be configured +- make sure list of converters is always an array +- usage of mod_sendfile can be configured @@ -417,8 +419,8 @@ usage of mod_sendfile can be configured GPL License -new parameter for enabling/disabling xsendfile -fix creation of pdf preview if document content class is not SeedDMS_Core_DocumentContent +- new parameter for enabling/disabling xsendfile +- fix creation of pdf preview if document content class is not SeedDMS_Core_DocumentContent @@ -434,7 +436,7 @@ fix creation of pdf preview if document content class is not SeedDMS_Core_Docume GPL License -add new methode getPreviewFile() +- add new methode getPreviewFile() @@ -450,7 +452,7 @@ add new methode getPreviewFile() GPL License -add parameter $target to SeedDMS_Preview_pdfPreviewer::hasRawPreview() and SeedDMS_Preview_pdfPreviewer::getRawPreview() +- add parameter $target to SeedDMS_Preview_pdfPreviewer::hasRawPreview() and SeedDMS_Preview_pdfPreviewer::getRawPreview() @@ -466,8 +468,8 @@ add parameter $target to SeedDMS_Preview_pdfPreviewer::hasRawPreview() and SeedD GPL License -set header Content-Length -update package description +- set header Content-Length +- update package description @@ -488,5 +490,22 @@ update package description preview image was actually created + + 2021-10-16 + + + 1.4.0 + 1.4.0 + + + stable + stable + + GPL License + +- use new conversion service if available +- createRawPreview() checks early if a converter exists + + diff --git a/SeedDMS_SQLiteFTS/CHANGELOG.md b/SeedDMS_SQLiteFTS/CHANGELOG.md new file mode 100644 index 000000000..d75aace4c --- /dev/null +++ b/SeedDMS_SQLiteFTS/CHANGELOG.md @@ -0,0 +1,89 @@ +1.0.18 (2023-01-09) +--------------------- +- add optional parameter $order to SeedDMS_SQLiteFTS_Indexer::find() +- add optional parameters $query and $col to SeedDMS_SQLiteFTS_Indexer::terms() +- IndexedDocument() accepts a callable for conversion to text +- remove stop words from content + +1.0.17 (2022-03-04) +--------------------- +- throw exeption in find() instead of returning false +- fix query if rootFolder or startFolder is set + + +1.0.16 (2021-05-10) +--------------------- +- close pipes in execWithTimeout(), also return exit code of command +- add support for fts5 (make it the default) +- add class SeedDMS_SQLiteFTS_Field + +1.0.15 (2020-12-12) +--------------------- +- add indexing folders + +1.0.14 (2020-09-11) +--------------------- +- add searching for document status +- search even if query is empty (will find all documents) +- parameters for SeedDMS_SQLiteFTS_Search::search() has changed +- SeedDMS_Lucene_Search::search() returns array of hits, count and facets +- pass config array instead of index directory to SeedDMS_Lucene_Indexer::create() + and SeedDMS_Lucene_Indexer::open() + +1.0.13 (2020-09-02) +--------------------- +- add user to list of terms + +1.0.12 (2020-09-02) +--------------------- +- Index users with at least read access on a document + +1.0.11 (2019-11-28) +--------------------- +- Set 'created' in index to creation date of indexed content (was set to current +timestamp) + +1.0.10 (2018-04-11) +--------------------- +- IndexedDocument() remembers cmd and mimetype + +1.0.9 (2018-01-30) +--------------------- +- execWithTimeout() reads data from stderr and saves it into error msg + +1.0.8 (2017-12-04) +--------------------- +- allow conversion commands for mimetypes with wildcards + +1.0.7 (2017-03-01) +--------------------- +- catch exception in execWithTimeout() + +1.0.6 (2016-03-29) +--------------------- +- fix calculation of timeout (see bug #269) + +1.0.5 (2016-03-29) +--------------------- +- set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 + +1.0.4 (2016-03-15) +--------------------- +- make it work with sqlite3 < 3.8.0 + +1.0.3 (2016-02-01) +--------------------- +- add command for indexing postѕcript files + +1.0.2 (2016-01-10) +--------------------- +- check if index exists before removing it when creating a new one + +1.0.1 (2015-11-16) +--------------------- +- add __get() to SQLiteFTS_Document because class.IndexInfo.php access class variable title which doesn't exists + +1.0.0 (2015-08-10) +--------------------- +- initial release + diff --git a/SeedDMS_SQLiteFTS/SQLiteFTS/Document.php b/SeedDMS_SQLiteFTS/SQLiteFTS/Document.php index ea63d733e..7b299a9a0 100644 --- a/SeedDMS_SQLiteFTS/SQLiteFTS/Document.php +++ b/SeedDMS_SQLiteFTS/SQLiteFTS/Document.php @@ -111,7 +111,7 @@ class SeedDMS_SQLiteFTS_Document { * @return string */ public function getFieldValue($fieldName) { - return $this->getField($fieldName)->value; + return $this->getField($fieldName)->value; } } ?> diff --git a/SeedDMS_SQLiteFTS/SQLiteFTS/IndexedDocument.php b/SeedDMS_SQLiteFTS/SQLiteFTS/IndexedDocument.php index ab553bd96..1bb3a32f0 100644 --- a/SeedDMS_SQLiteFTS/SQLiteFTS/IndexedDocument.php +++ b/SeedDMS_SQLiteFTS/SQLiteFTS/IndexedDocument.php @@ -151,8 +151,9 @@ class SeedDMS_SQLiteFTS_IndexedDocument extends SeedDMS_SQLiteFTS_Document { if($version) { $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('mimetype', $version->getMimeType())); $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('origfilename', $version->getOriginalFileName())); + $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('created', $version->getDate(), 'unindexed')); if(!$nocontent) - $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('created', $version->getDate(), 'unindexed')); + $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('indexed', time(), 'unindexed')); if($attributes = $version->getAttributes()) { foreach($attributes as $attribute) { $attrdef = $attribute->getAttributeDefinition(); @@ -168,7 +169,7 @@ class SeedDMS_SQLiteFTS_IndexedDocument extends SeedDMS_SQLiteFTS_Document { foreach($categories as $cat) { $names[] = $cat->getName(); } - $this->addField(SeedDMS_SQLiteFTS_Field::Text('category', implode(' ', $names))); + $this->addField(SeedDMS_SQLiteFTS_Field::Text('category', implode('#', $names))); } if($keywords = $document->getKeywords()) { $this->addField(SeedDMS_SQLiteFTS_Field::Text('keywords', $keywords)); @@ -182,7 +183,15 @@ class SeedDMS_SQLiteFTS_IndexedDocument extends SeedDMS_SQLiteFTS_Document { if(file_exists($path)) { $mimetype = $version->getMimeType(); $this->mimetype = $mimetype; - if(is_object($convcmd) && (get_class($convcmd) == 'SeedDMS_ConversionMgr')) { + if(is_callable($convcmd)) { + $result = $convcmd($document); + if($result['content']) { + self::setContent($result['content']); + } elseif($result['content'] === false) { + $this->errormsg = $result['errormsg']; + } + $this->cmd = $result['cmd']; + } elseif(is_object($convcmd) && (get_class($convcmd) == 'SeedDMS_ConversionMgr')) { if($service = $convcmd->getService($mimetype, 'text/plain')) { $content = $convcmd->convert($path, $mimetype, 'text/plain'); if($content) { @@ -226,6 +235,7 @@ class SeedDMS_SQLiteFTS_IndexedDocument extends SeedDMS_SQLiteFTS_Document { $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('document_id', 'F'.$document->getID())); $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('record_type', 'folder')); $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('created', $document->getDate(), 'unindexed')); + $this->addField(SeedDMS_SQLiteFTS_Field::Keyword('indexed', time(), 'unindexed')); } } /* }}} */ diff --git a/SeedDMS_SQLiteFTS/SQLiteFTS/Indexer.php b/SeedDMS_SQLiteFTS/SQLiteFTS/Indexer.php index fd2ec197c..559b93bc4 100644 --- a/SeedDMS_SQLiteFTS/SQLiteFTS/Indexer.php +++ b/SeedDMS_SQLiteFTS/SQLiteFTS/Indexer.php @@ -25,18 +25,48 @@ class SeedDMS_SQLiteFTS_Indexer { /** - * @var string $ftstype + * @var string $_ftstype * @access protected */ protected $_ftstype; /** - * @var object $index sqlite index + * @var object $_conn sqlite index * @access protected */ protected $_conn; + /** + * @var array $_stop_words array of stop words + * @access protected + */ + protected $_stop_words; + const ftstype = 'fts5'; + + /** + * Remove stopwords from string + */ + protected function strip_stopwords($str = "") { /* {{{ */ + // 1.) break string into words + // [^-\w\'] matches characters, that are not [0-9a-zA-Z_-'] + // if input is unicode/utf-8, the u flag is needed: /pattern/u + $words = preg_split('/[^-\w\']+/u', $str, -1, PREG_SPLIT_NO_EMPTY); + + // 2.) if we have at least 2 words, remove stopwords + if(!empty($words)) { + $stopwords = $this->_stop_words; + $words = array_filter($words, function ($w) use (&$stopwords) { + return ((mb_strlen($w, 'utf-8') > 2) && !isset($stopwords[mb_strtolower($w, "utf-8")])); + }); + } + + // check if not too much was removed such as "the the" would return empty + if(!empty($words)) + return implode(" ", $words); + return $str; + } /* }}} */ + /** * Constructor * @@ -48,6 +78,7 @@ class SeedDMS_SQLiteFTS_Indexer { $this->_rawid = 'rowid'; else $this->_rawid = 'docid'; + $this->_stop_words = []; } /* }}} */ /** @@ -59,7 +90,7 @@ class SeedDMS_SQLiteFTS_Indexer { if(file_exists($conf['indexdir'].'/index.db')) { return new SeedDMS_SQLiteFTS_Indexer($conf['indexdir']); } else - return self::create($conf); + return static::create($conf); } /* }}} */ /** @@ -77,9 +108,9 @@ class SeedDMS_SQLiteFTS_Indexer { $version = SQLite3::version(); if(self::ftstype == 'fts4') { if($version['versionNumber'] >= 3008000) - $sql = 'CREATE VIRTUAL TABLE docs USING fts4(documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created, users, status, path, notindexed=created, matchinfo=fts3)'; + $sql = 'CREATE VIRTUAL TABLE docs USING fts4(documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created, indexed, users, status, path, notindexed=created, notindexed=indexed, matchinfo=fts3)'; else - $sql = 'CREATE VIRTUAL TABLE docs USING fts4(documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created, users, status, path, matchinfo=fts3)'; + $sql = 'CREATE VIRTUAL TABLE docs USING fts4(documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created, indexed, users, status, path, matchinfo=fts3)'; $res = $index->_conn->exec($sql); if($res === false) { return null; @@ -90,7 +121,7 @@ class SeedDMS_SQLiteFTS_Indexer { return null; } } elseif(self::ftstype == 'fts5') { - $sql = 'CREATE VIRTUAL TABLE docs USING fts5(documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created unindexed, users, status, path)'; + $sql = 'CREATE VIRTUAL TABLE docs USING fts5(documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created unindexed, indexed unindexed, users, status, path)'; $res = $index->_conn->exec($sql); if($res === false) { return null; @@ -109,7 +140,9 @@ class SeedDMS_SQLiteFTS_Indexer { * Do some initialization * */ - static function init($stopWordsFile='') { /* {{{ */ + public function init($stopWordsFile='') { /* {{{ */ + if($stopWordsFile) + $this->_stop_words = array_flip(preg_split("/[\s,]+/", file_get_contents($stopWordsFile))); } /* }}} */ /** @@ -123,7 +156,7 @@ class SeedDMS_SQLiteFTS_Indexer { if(!$this->_conn) return false; - foreach(array('comment', 'keywords', 'category', 'content', 'mimetype', 'origfilename', 'status', 'created') as $kk) { + foreach(array('comment', 'keywords', 'category', 'content', 'mimetype', 'origfilename', 'status', 'created', 'indexed') as $kk) { try { ${$kk} = $doc->getFieldValue($kk); } catch (Exception $e) { @@ -135,7 +168,10 @@ class SeedDMS_SQLiteFTS_Indexer { if($res === false) { return false; } - $sql = "INSERT INTO docs (documentid, record_type, title, comment, keywords, category, owner, content, mimetype, origfilename, created, users, status, path) VALUES (".$this->_conn->quote($doc->getFieldValue('document_id')).", ".$this->_conn->quote($doc->getFieldValue('record_type')).", ".$this->_conn->quote($doc->getFieldValue('title')).", ".$this->_conn->quote($comment).", ".$this->_conn->quote($keywords).", ".$this->_conn->quote($category).", ".$this->_conn->quote($doc->getFieldValue('owner')).", ".$this->_conn->quote($content).", ".$this->_conn->quote($mimetype).", ".$this->_conn->quote($origfilename).", ".(int)$created.", ".$this->_conn->quote($doc->getFieldValue('users')).", ".$this->_conn->quote($status).", ".$this->_conn->quote($doc->getFieldValue('path'))/*time()*/.")"; + if($this->_stop_words) + $content = $this->strip_stopwords($content); + + $sql = "INSERT INTO docs (documentid, record_type, title, comment, keywords, category, owner, content, mimetype, origfilename, created, indexed, users, status, path) VALUES (".$this->_conn->quote($doc->getFieldValue('document_id')).", ".$this->_conn->quote($doc->getFieldValue('record_type')).", ".$this->_conn->quote($doc->getFieldValue('title')).", ".$this->_conn->quote($comment).", ".$this->_conn->quote($keywords).", ".$this->_conn->quote($category).", ".$this->_conn->quote($doc->getFieldValue('owner')).", ".$this->_conn->quote($content).", ".$this->_conn->quote($mimetype).", ".$this->_conn->quote($origfilename).", ".(int)$created.", ".(int)$indexed.", ".$this->_conn->quote($doc->getFieldValue('users')).", ".$this->_conn->quote($status).", ".$this->_conn->quote($doc->getFieldValue('path'))/*time()*/.")"; $res = $this->_conn->exec($sql); if($res === false) { return false; @@ -147,8 +183,7 @@ class SeedDMS_SQLiteFTS_Indexer { /** * Remove document from index * - * @param object $doc indexed document of class - * SeedDMS_SQLiteFTS_IndexedDocument + * @param object $id internal id of document * @return boolean false in case of an error, otherwise true */ public function delete($id) { /* {{{ */ @@ -179,15 +214,22 @@ class SeedDMS_SQLiteFTS_Indexer { * @return boolean false in case of an error, otherwise array with elements * 'count', 'hits', 'facets'. 'hits' is an array of SeedDMS_SQLiteFTS_QueryHit */ - public function find($query, $limit=array()) { /* {{{ */ + public function find($query, $filter='', $limit=array(), $order=array()) { /* {{{ */ if(!$this->_conn) return false; /* First count some records for facets */ - foreach(array('owner', 'mimetype', 'category') as $facetname) { + foreach(array('owner', 'mimetype', 'category', 'status') as $facetname) { $sql = "SELECT `".$facetname."`, count(*) AS `c` FROM `docs`"; - if($query) + if($query) { $sql .= " WHERE docs MATCH ".$this->_conn->quote($query); + } + if($filter) { + if($query) + $sql .= " AND ".$filter; + else + $sql .= " WHERE ".$filter; + } $res = $this->_conn->query($sql." GROUP BY `".$facetname."`"); if(!$res) throw new SeedDMS_SQLiteFTS_Exception("Counting records in facet \"$facetname\" failed."); @@ -196,7 +238,7 @@ class SeedDMS_SQLiteFTS_Indexer { foreach($res as $row) { if($row[$facetname] && $row['c']) { if($facetname == 'category') { - $tmp = explode(' ', $row[$facetname]); + $tmp = explode('#', $row[$facetname]); if(count($tmp) > 1) { foreach($tmp as $t) { if(!isset($facets[$facetname][$t])) @@ -210,6 +252,8 @@ class SeedDMS_SQLiteFTS_Indexer { else $facets[$facetname][$row[$facetname]] += $row['c']; } + } elseif($facetname == 'status') { + $facets[$facetname][($row[$facetname]-10).''] = $row['c']; } else $facets[$facetname][$row[$facetname]] = $row['c']; } @@ -219,6 +263,12 @@ class SeedDMS_SQLiteFTS_Indexer { $sql = "SELECT `record_type`, count(*) AS `c` FROM `docs`"; if($query) $sql .= " WHERE docs MATCH ".$this->_conn->quote($query); + if($filter) { + if($query) + $sql .= " AND ".$filter; + else + $sql .= " WHERE ".$filter; + } $res = $this->_conn->query($sql." GROUP BY `record_type`"); if(!$res) throw new SeedDMS_SQLiteFTS_Exception("Counting records in facet \"record_type\" failed."); @@ -232,10 +282,32 @@ class SeedDMS_SQLiteFTS_Indexer { $sql = "SELECT ".$this->_rawid.", documentid FROM docs"; if($query) $sql .= " WHERE docs MATCH ".$this->_conn->quote($query); - if($this->_ftstype == 'fts5') + if($filter) { + if($query) + $sql .= " AND ".$filter; + else + $sql .= " WHERE ".$filter; + } + if($this->_ftstype == 'fts5') { //$sql .= " ORDER BY rank"; - // boost documentid, title and comment - $sql .= " ORDER BY bm25(docs, 10.0, 10.0, 10.0)"; + // boost documentid, record_type, title, comment, keywords, category, mimetype, origfilename, owner, content, created unindexed, users, status, path + if(!empty($order['by'])) { + switch($order['by']) { + case "title": + $sql .= " ORDER BY title"; + break; + case "created": + $sql .= " ORDER BY created"; + break; + default: + $sql .= " ORDER BY bm25(docs, 10.0, 0.0, 10.0, 5.0, 5.0, 10.0)"; + } + if(!empty($order['dir'])) { + if($order['dir'] == 'desc') + $sql .= " DESC"; + } + } + } if(!empty($limit['limit'])) $sql .= " LIMIT ".(int) $limit['limit']; if(!empty($limit['offset'])) @@ -289,7 +361,7 @@ class SeedDMS_SQLiteFTS_Indexer { if(!$this->_conn) return false; - $sql = "SELECT ".$this->_rawid.", documentid, title, comment, owner, keywords, category, mimetype, origfilename, created, users, status, path".($content ? ", content" : "")." FROM docs WHERE ".$this->_rawid."='".$id."'"; + $sql = "SELECT ".$this->_rawid.", documentid, title, comment, owner, keywords, category, mimetype, origfilename, created, indexed, users, status, path".($content ? ", content" : "")." FROM docs WHERE ".$this->_rawid."='".$id."'"; $res = $this->_conn->query($sql); $doc = false; if($res) { @@ -306,9 +378,10 @@ class SeedDMS_SQLiteFTS_Indexer { $doc->addField(SeedDMS_SQLiteFTS_Field::Keyword('origfilename', $rec['origfilename'])); $doc->addField(SeedDMS_SQLiteFTS_Field::Text('owner', $rec['owner'])); $doc->addField(SeedDMS_SQLiteFTS_Field::Keyword('created', $rec['created'])); + $doc->addField(SeedDMS_SQLiteFTS_Field::Keyword('indexed', $rec['indexed'])); $doc->addField(SeedDMS_SQLiteFTS_Field::Text('users', $rec['users'])); $doc->addField(SeedDMS_SQLiteFTS_Field::Keyword('status', $rec['status'])); - $doc->addField(SeedDMS_SQLiteFTS_Field::Keyword('path', $rec['path'])); + $doc->addField(SeedDMS_SQLiteFTS_Field::Keyword('path', explode('x', substr($rec['path'], 1, -1)))); if($content) $doc->addField(SeedDMS_SQLiteFTS_Field::UnStored('content', $rec['content'])); } @@ -318,16 +391,33 @@ class SeedDMS_SQLiteFTS_Indexer { /** * Return list of terms in index * - * This function does nothing! + * @return array list of SeedDMS_SQLiteFTS_Term */ - public function terms() { /* {{{ */ + public function terms($prefix='', $col='') { /* {{{ */ if(!$this->_conn) return false; - if($this->_ftstype == 'fts5') - $sql = "SELECT term, col, doc as occurrences FROM docs_terms WHERE col!='*' ORDER BY col"; - else - $sql = "SELECT term, col, occurrences FROM docs_terms WHERE col!='*' ORDER BY col"; + if($this->_ftstype == 'fts5') { + $sql = "SELECT term, col, doc as occurrences FROM docs_terms"; + if($prefix || $col) { + $sql .= " WHERE"; + if($prefix) { + $sql .= " term like '".$prefix."%'"; + if($col) + $sql .= " AND"; + } + if($col) + $sql .= " col = '".$col."'"; + } + $sql .= " ORDER BY col, occurrences desc"; + } else { + $sql = "SELECT term, col, occurrences FROM docs_terms WHERE col!='*'"; + if($prefix) + $sql .= " AND term like '".$prefix."%'"; + if($col) + $sql .= " AND col = '".$col."'"; + $sql .= " ORDER BY col, occurrences desc"; + } $res = $this->_conn->query($sql); $terms = array(); if($res) { @@ -340,8 +430,9 @@ class SeedDMS_SQLiteFTS_Indexer { } /* }}} */ /** - * Return list of documents in index + * Return number of documents in index * + * @return interger number of documents */ public function count() { /* {{{ */ $sql = "SELECT count(*) c FROM docs"; diff --git a/SeedDMS_SQLiteFTS/SQLiteFTS/QueryHit.php b/SeedDMS_SQLiteFTS/SQLiteFTS/QueryHit.php index facbd9519..843be973d 100644 --- a/SeedDMS_SQLiteFTS/SQLiteFTS/QueryHit.php +++ b/SeedDMS_SQLiteFTS/SQLiteFTS/QueryHit.php @@ -53,6 +53,7 @@ class SeedDMS_SQLiteFTS_QueryHit { */ public function __construct(SeedDMS_SQLiteFTS_Indexer $index) { /* {{{ */ $this->_index = $index; + $this->_document = null; } /* }}} */ /** diff --git a/SeedDMS_SQLiteFTS/SQLiteFTS/Search.php b/SeedDMS_SQLiteFTS/SQLiteFTS/Search.php index d6ba6b6a7..22c43a1e5 100644 --- a/SeedDMS_SQLiteFTS/SQLiteFTS/Search.php +++ b/SeedDMS_SQLiteFTS/SQLiteFTS/Search.php @@ -70,7 +70,7 @@ class SeedDMS_SQliteFTS_Search { * @param object $index SQlite FTS index * @return object instance of SeedDMS_Lucene_Search */ - function search($term, $fields=array(), $limit=array()) { /* {{{ */ + function search($term, $fields=array(), $limit=array(), $order=array()) { /* {{{ */ $querystr = ''; $term = trim($term); if($term) { @@ -139,8 +139,20 @@ class SeedDMS_SQliteFTS_Search { $querystr .= str_replace(':', 'x', $fields['startFolder']->getFolderList().$fields['startFolder']->getID().':'); $querystr .= '*)'; } + + $filterstr = ''; + if(!empty($fields['created_start'])) { + if($filterstr) + $filterstr .= ' AND '; + $filterstr .= '(created>='.$fields['created_start'].')'; + } + if(!empty($fields['created_end'])) { + if($filterstr) + $filterstr .= ' AND '; + $filterstr .= '(created<'.$fields['created_end'].')'; + } try { - $result = $this->index->find($querystr, $limit); + $result = $this->index->find($querystr, $filterstr, $limit, $order); $recs = array(); foreach($result["hits"] as $hit) { $recs[] = array('id'=>$hit->id, 'document_id'=>$hit->documentid); diff --git a/SeedDMS_SQLiteFTS/SQLiteFTS/Term.php b/SeedDMS_SQLiteFTS/SQLiteFTS/Term.php index 462d6123e..e31ff56bc 100644 --- a/SeedDMS_SQLiteFTS/SQLiteFTS/Term.php +++ b/SeedDMS_SQLiteFTS/SQLiteFTS/Term.php @@ -60,7 +60,8 @@ class SeedDMS_SQLiteFTS_Term { 9 => 'created', 10 => 'user', 11 => 'status', - 12 => 'path' + 12 => 'path', + 13 => 'indexed', ); /* fts5 pass the column name in $col, fts4 uses an integer */ if(is_int($col)) diff --git a/SeedDMS_SQLiteFTS/composer.json b/SeedDMS_SQLiteFTS/composer.json new file mode 100644 index 000000000..90a19cd7d --- /dev/null +++ b/SeedDMS_SQLiteFTS/composer.json @@ -0,0 +1,23 @@ +{ + "name": "seeddms/lucene", + "description": "SQLiteFTS based fulltext search for SeedDMS ", + "type": "library", + "license": "GPL-2.0-or-later", + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Seeddms\\SQLiteFTS\\": "SQLiteFTS/" + }, + "classmap": ["SQLiteFTS/"] + }, + "authors": [ + { + "name": "Uwe Steinmann", + "email": "info@seeddms.org" + } + ], + "require-dev": { + "phpunit/phpunit": "^9" + } + +} diff --git a/SeedDMS_SQLiteFTS/package.xml b/SeedDMS_SQLiteFTS/package.xml index a862245cf..2b7dfe0b5 100644 --- a/SeedDMS_SQLiteFTS/package.xml +++ b/SeedDMS_SQLiteFTS/package.xml @@ -11,11 +11,11 @@ uwe@steinmann.cx yes - 2022-03-04 + 2023-01-09 - 1.0.17 - 1.0.17 + 1.0.18 + 1.0.18 stable @@ -23,8 +23,10 @@ GPL License -- throw exeption in find() instead of returning false -- fix query if rootFolder or startFolder is set +- add optional parameter $order to SeedDMS_SQLiteFTS_Indexer::find() +- add optional parameters $query and $col to SeedDMS_SQLiteFTS_Indexer::terms() +- IndexedDocument() accepts a callable for conversion to text +- remove stop words from content @@ -86,7 +88,7 @@ GPL License -initial release +- initial release @@ -102,7 +104,7 @@ initial release GPL License -add __get() to SQLiteFTS_Document because class.IndexInfo.php access class variable title which doesn't exists +- add __get() to SQLiteFTS_Document because class.IndexInfo.php access class variable title which doesn't exists @@ -118,7 +120,7 @@ add __get() to SQLiteFTS_Document because class.IndexInfo.php access class varia GPL License -check if index exists before removing it when creating a new one +- check if index exists before removing it when creating a new one @@ -134,7 +136,7 @@ check if index exists before removing it when creating a new one GPL License -add command for indexing postѕcript files +- add command for indexing postѕcript files @@ -150,7 +152,7 @@ add command for indexing postѕcript files GPL License -make it work with sqlite3 < 3.8.0 +- make it work with sqlite3 < 3.8.0 @@ -166,7 +168,7 @@ make it work with sqlite3 < 3.8.0 GPL License -set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 +- set last parameter of stream_select() to 200000 micro sec. in case the timeout in sec. is set to 0 @@ -182,7 +184,7 @@ set last parameter of stream_select() to 200000 micro sec. in case the timeout i GPL License -fix calculation of timeout (see bug #269) +- fix calculation of timeout (see bug #269) @@ -198,7 +200,7 @@ fix calculation of timeout (see bug #269) GPL License -catch exception in execWithTimeout() +- catch exception in execWithTimeout() @@ -214,7 +216,7 @@ catch exception in execWithTimeout() GPL License -allow conversion commands for mimetypes with wildcards +- allow conversion commands for mimetypes with wildcards @@ -230,7 +232,7 @@ allow conversion commands for mimetypes with wildcards GPL License -execWithTimeout() reads data from stderr and saves it into error msg +- execWithTimeout() reads data from stderr and saves it into error msg @@ -246,7 +248,7 @@ execWithTimeout() reads data from stderr and saves it into error msg GPL License -IndexedDocument() remembers cmd and mimetype +- IndexedDocument() remembers cmd and mimetype @@ -262,7 +264,7 @@ IndexedDocument() remembers cmd and mimetype GPL License -Set 'created' in index to creation date of indexed content (was set to current +- Set 'created' in index to creation date of indexed content (was set to current timestamp) @@ -279,7 +281,7 @@ timestamp) GPL License -Index users with at least read access on a document +- Index users with at least read access on a document @@ -295,7 +297,7 @@ Index users with at least read access on a document GPL License -add user to list of terms +- add user to list of terms @@ -353,5 +355,22 @@ add user to list of terms - add class SeedDMS_SQLiteFTS_Field + + 2022-03-04 + + + 1.0.17 + 1.0.17 + + + stable + stable + + GPL License + +- throw exeption in find() instead of returning false +- fix query if rootFolder or startFolder is set + + diff --git a/build.xml b/build.xml index b02d9167e..4d4be7f17 100644 --- a/build.xml +++ b/build.xml @@ -16,7 +16,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -151,7 +151,7 @@ - + + + diff --git a/composer-dist.json b/composer-dist.json index 7c0171634..1e4d01ddb 100644 --- a/composer-dist.json +++ b/composer-dist.json @@ -1,9 +1,19 @@ { + "config": { + "platform": { + "php": "7.4" + } + }, + "require": { + "robthree/twofactorauth": "^1.5", "sabre/dav": "^3.", + "sabre/xml": "^1.4.", "slim/slim": "^3.0", "erusev/parsedown": "*", "erusev/parsedown-extra": "*", + "mibe/feedwriter": "^1.1", + "phpoffice/phpspreadsheet": "*", "pear/log": "*", "pear/mail": "*", "pear/mail_mime": "*", @@ -11,7 +21,51 @@ "pear/auth_sasl": "*", "pear/db": "*", "alecrabbit/php-console-colour": "*", + "dragonmantank/cron-expression": "^3", "zf1/zend-search-lucene": "*", - "symfony/http-foundation": "^5.4" - } + "symfony/http-foundation": "^5.4", + "seeddms/core": "dev-master", + "seeddms/lucene": "dev-master", + "seeddms/preview": "dev-master", + "seeddms/sqlitefts": "dev-master", + "seeddms/http_webdav_server": "dev-master" + }, + "repositories": [ + { + "type": "path", + "url": "/home/cvs/seeddms-ext/core", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "/home/cvs/seeddms-ext/lucene", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "/home/cvs/seeddms-ext/preview", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "/home/cvs/seeddms-ext/sqlitefts", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "/home/cvs/seeddms-ext/http_webdav_server", + "options": { + "symlink": false + } + } + ] + } diff --git a/conf/settings.xml.template b/conf/settings.xml.template index 01704a1ce..180362d15 100644 --- a/conf/settings.xml.template +++ b/conf/settings.xml.template @@ -15,7 +15,7 @@ footNote = "SeedDMS free document management system - www.seeddms.org" printDisclaimer = "true" language = "en_GB" - theme = "bootstrap" + theme = "bootstrap4" previewWidthList = "40" previewWidthDetail = "100" onePageMode="true" @@ -60,8 +60,8 @@ enableDropUpload = "false" enableRecursiveCount = "false" maxRecursiveCount = "0" - enableThemeSelector = "false" - fullSearchEngine = "lucene" + enableThemeSelector = "true" + fullSearchEngine = "sqlitefts" sortFoldersDefault = "u" defaultDocPosition = "end" defaultFolderPosition = "end" @@ -290,7 +290,7 @@ updateNotifyTime = "86400" extraPath = "" maxExecutionTime = "30" - cmdTimeout = "1" + cmdTimeout = "10" /> showConfigHeadline('settings_Server'); ?> showConfigText('settings_rootDir', 'rootDir'); ?> +showConfigText('settings_baseUrl', 'baseUrl'); ?> showConfigText('settings_httpRoot', 'httpRoot'); ?> showConfigText('settings_contentDir', 'contentDir'); ?> showConfigText('settings_backupDir', 'backupDir'); ?> @@ -398,6 +434,8 @@ if(($kkk = $this->callHook('getFullSearchEngine')) && is_array($kkk)) showConfigText('settings_stagingDir', 'stagingDir'); ?> showConfigText('settings_luceneDir', 'luceneDir'); ?> showConfigText('settings_dropFolderDir', 'dropFolderDir'); ?> +showConfigText('settings_checkOutDir', 'checkOutDir'); ?> +showConfigCheckbox('settings_createCheckOutDir', 'createCheckOutDir'); ?> showConfigText('settings_repositoryUrl', 'repositoryUrl'); ?> showConfigText('settings_proxyUrl', 'proxyUrl'); ?> showConfigText('settings_proxyUser', 'proxyUser'); ?> @@ -414,6 +452,7 @@ if(($kkk = $this->callHook('getFullSearchEngine')) && is_array($kkk)) showConfigHeadline('settings_Authentication'); ?> showConfigCheckbox('settings_enableGuestLogin', 'enableGuestLogin'); ?> showConfigCheckbox('settings_enableGuestAutoLogin', 'enableGuestAutoLogin'); ?> +showConfigCheckbox('settings_enable2FactorAuthentication', 'enable2FactorAuthentication'); ?> showConfigCheckbox('settings_restricted', 'restricted'); ?> showConfigCheckbox('settings_enableUserImage', 'enableUserImage'); ?> showConfigCheckbox('settings_disableSelfEdit', 'disableSelfEdit'); ?> @@ -482,20 +521,34 @@ if(($kkk = $this->callHook('getFullSearchEngine')) && is_array($kkk)) --> showConfigHeadline('settings_Edition'); ?> showConfigOption('settings_workflowMode', 'workflowMode', array('traditional'=>'settings_workflowMode_valtraditional', 'traditional_only_approval'=>'settings_workflowMode_valtraditional_only_approval', 'advanced'=>'settings_workflowMode_valadvanced', 'none'=>'settings_workflowMode_valnone'), false, true); ?> +showConfigCheckbox('settings_enableReceiptWorkflow', 'enableReceiptWorkflow'); ?> +showConfigCheckbox('settings_enableReceiptReject', 'enableReceiptReject'); ?> +showConfigCheckbox('settings_enableRevisionWorkflow', 'enableRevisionWorkflow'); ?> +showConfigCheckbox('settings_enableRevisionOneVoteReject', 'enableRevisionOneVoteReject'); ?> showConfigText('settings_versioningFileName', 'versioningFileName'); ?> showConfigText('settings_presetExpirationDate', 'presetExpirationDate'); ?> +showConfigOption('settings_initialDocumentStatus', 'initialDocumentStatus', array(' '.S_RELEASED=>'settings_initialDocumentStatus_released', ' '.S_DRAFT=>'settings_initialDocumentStatus_draft'), false, true); ?> showConfigCheckbox('settings_allowReviewerOnly', 'allowReviewerOnly'); ?> +showConfigCheckbox('settings_allowChangeRevAppInProcess', 'allowChangeRevAppInProcess'); ?> showConfigCheckbox('settings_enableAdminRevApp', 'enableAdminRevApp'); ?> showConfigCheckbox('settings_enableOwnerRevApp', 'enableOwnerRevApp'); ?> showConfigCheckbox('settings_enableSelfRevApp', 'enableSelfRevApp'); ?> showConfigCheckbox('settings_enableUpdateRevApp', 'enableUpdateRevApp'); ?> showConfigCheckbox('settings_enableRemoveRevApp', 'enableRemoveRevApp'); ?> +showConfigCheckbox('settings_enableSelfReceipt', 'enableSelfReceipt'); ?> +showConfigCheckbox('settings_enableAdminReceipt', 'enableAdminReceipt'); ?> +showConfigCheckbox('settings_enableOwnerReceipt', 'enableOwnerReceipt'); ?> +showConfigCheckbox('settings_enableUpdateReceipt', 'enableUpdateReceipt'); ?> +showConfigCheckbox('settings_enableFilterReceipt', 'enableFilterReceipt'); ?> showConfigCheckbox('settings_enableVersionDeletion', 'enableVersionDeletion'); ?> showConfigCheckbox('settings_enableVersionModification', 'enableVersionModification'); ?> showConfigCheckbox('settings_enableDuplicateDocNames', 'enableDuplicateDocNames'); ?> showConfigCheckbox('settings_enableDuplicateSubFolderNames', 'enableDuplicateSubFolderNames'); ?> +showConfigCheckbox('settings_enableCancelCheckout', 'enableCancelCheckout'); ?> showConfigCheckbox('settings_overrideMimeType', 'overrideMimeType'); ?> +showConfigCheckbox('settings_advancedAcl', 'advancedAcl'); ?> showConfigCheckbox('settings_removeFromDropFolder', 'removeFromDropFolder'); ?> +showConfigCheckbox('settings_uploadedAttachmentIsPublic', 'uploadedAttachmentIsPublic'); ?> getExtensionConfiguration() as $extname=>$extconf) { + echo ''.$extconf['title']." ● "; + } foreach($extmgr->getExtensionConfiguration() as $extname=>$extconf) { if($this->hasHook('processConfig')) $extconf = $this->callHook('processConfig', $extname, $extconf); if($this->isVisible($extname.'|')) { if($extconf['config']) { - $this->showRawConfigHeadline("".'_extensions[$extname]["__disable__"] ? '1' : '').'" />_extensions[$extname]["__disable__"] ? ' disabled' : ' enabled').'"> '.$extconf['title'].''); + $this->showRawConfigHeadline("".'_extensions[$extname]["__disable__"] ? '1' : '').'" />_extensions[$extname]["__disable__"] ? ' disabled' : ' enabled').'"> '.$extconf['title'].''); foreach($extconf['config'] as $confkey=>$conf) { ob_start(); if($this->isVisible($extname.'|'.$confkey)) { @@ -624,7 +680,7 @@ if(($kkk = $this->callHook('getFullSearchEngine')) && is_array($kkk)) echo ""; + echo " data-subtitle=\"".htmlspecialchars(getAttributeObjectTypeText($rec).", ".getAttributeTypeText($rec))."\">".htmlspecialchars($rec->getName()).""; } echo ""; } else { @@ -651,6 +707,9 @@ if(($kkk = $this->callHook('getFullSearchEngine')) && is_array($kkk)) case "folders": $this->formField(null, $this->getFolderChooserHtml("form".$extname.$confkey, M_READ, -1, $selections ? $dms->getFolder($selections[0]) : 0, 'extensions['.$extname."][".$confkey."]")); break; + case "documents": + $this->formField(null, $this->getDocumentChooserHtml("form".$extname.$confkey, M_READ, -1, $selections ? $dms->getDocument($selections[0]) : 0, 'extensions['.$extname."][".$confkey."]")); + break; } } break; @@ -680,9 +739,7 @@ if(($kkk = $this->callHook('getFullSearchEngine')) && is_array($kkk)) _configFilePath)) { -?> - -formSubmit(" ".getMLText('save')); } ?> @@ -707,6 +764,11 @@ if(is_writeable($settings->_configFilePath)) { if($settings->_enableLargeFileUpload && $mus2 < $mus1) { $this->warningMsg(getMLText("settings_partionsize_below_max_filesize")); } + + foreach($extmgr->getExtensionConfiguration() as $extname=>$extconf) { + if($this->hasHook('checkConfig')) + $this->callHook('checkConfig', $extname, $extconf); + } $this->columnEnd(4); $this->rowEnd(4); $this->contentEnd(); diff --git a/views/bootstrap/class.Setup2Factor.php b/views/bootstrap/class.Setup2Factor.php index 6c6cfd2b4..a42bb4b9b 100644 --- a/views/bootstrap/class.Setup2Factor.php +++ b/views/bootstrap/class.Setup2Factor.php @@ -78,34 +78,44 @@ $(document).ready( function() { $this->contentHeading(getMLText('2_factor_auth')); $this->infoMsg(getMLText('2_factor_auth_info')); $this->rowStart(); - $this->contentContainerStart('span6'); + $this->columnStart(6); + $this->contentHeading(getMLText('2_fact_auth_new_secret')); $tfa = new \RobThree\Auth\TwoFactorAuth('SeedDMS'); $oldsecret = $user->getSecret(); $secret = $tfa->createSecret(); ?>
-
-
-
+formField( + getMLText('2_fact_auth_secret'), + array( + 'element'=>'input', + 'type'=>'text', + 'name'=>'secret', + 'class'=>'secret', + 'value'=>htmlspecialchars($secret), + 'required'=>true + ) + ); + $this->formSubmit(getMLText('submit_2_fact_auth')); +?>
-formSubmit(getMLText('submit_2_fact_auth')); -?>
contentContainerEnd(); - $this->contentContainerStart('span6'); + $this->columnEnd(); + $this->columnStart(6); + $this->contentHeading(getMLText('2_fact_auth_current_secret')); echo '
'.$oldsecret.'
'; echo '
'; ?> contentContainerEnd(); + $this->columnEnd(); $this->rowEnd(); $this->contentEnd(); $this->htmlEndPage(); diff --git a/views/bootstrap/class.SubstituteUser.php b/views/bootstrap/class.SubstituteUser.php index 7ad0d639c..335b27556 100644 --- a/views/bootstrap/class.SubstituteUser.php +++ b/views/bootstrap/class.SubstituteUser.php @@ -73,24 +73,16 @@ class SeedDMS_View_SubstituteUser extends SeedDMS_Theme_Style { echo htmlspecialchars($currUser->getFullName())." (".htmlspecialchars($currUser->getLogin()).")"; if($hasemail) echo ""; - echo "
"; + if($currUser->getComment()) + echo "
".htmlspecialchars($currUser->getComment()).""; if($hasemail) - echo "".htmlspecialchars($currUser->getEmail())."
"; - echo "".htmlspecialchars($currUser->getComment()).""; + echo "
getEmail())."\">".htmlspecialchars($currUser->getEmail()).""; + if($homefolder = $currUser->getHomeFolder()) + echo "
".htmlspecialchars($dms->getFolder($homefolder)->getName()).""; echo ""; echo ""; echo getMLText('role').": "; - switch($currUser->getRole()) { - case SeedDMS_Core_User::role_user: - printMLText("role_user"); - break; - case SeedDMS_Core_User::role_admin: - printMLText("role_admin"); - break; - case SeedDMS_Core_User::role_guest: - printMLText("role_guest"); - break; - } + echo htmlspecialchars($currUser->getRole()->getName()); echo "
"; $groups = $currUser->getGroups(); if (count($groups) != 0) { @@ -102,7 +94,6 @@ class SeedDMS_View_SubstituteUser extends SeedDMS_Theme_Style { } echo ""; echo ""; - echo ""; echo ""; if($currUser->getID() != $user->getID()) { echo "getID())."&formtoken=".createFormKey('substituteuser')."\"> ".getMLText('substitute_user')." "; diff --git a/views/bootstrap/class.Tasks.php b/views/bootstrap/class.Tasks.php index b30b5ad79..846466bd0 100644 --- a/views/bootstrap/class.Tasks.php +++ b/views/bootstrap/class.Tasks.php @@ -18,11 +18,6 @@ */ //require_once("class.Bootstrap.php"); -/** - * Include class to preview documents - */ -require_once("SeedDMS/Preview.php"); - /** * Class which outputs the html page for clipboard view * @@ -39,8 +34,12 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { private function __myTasks() { /* {{{ */ $dms = $this->params['dms']; $user = $this->params['user']; + $enablereceiptworkflow = $this->params['enablereceiptworkflow']; + $enablerevisionworkflow = $this->params['enablerevisionworkflow']; $workflowmode = $this->params['workflowmode']; + $tasksinmenu = $this->params['tasksinmenu']; $tasks = array(); +/* if($workflowmode == 'traditional' || $workflowmode == 'traditional_only_approval') { $tasks['approval'] = array(); if($workflowmode == 'traditional') @@ -48,25 +47,31 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { } elseif($workflowmode == 'advanced') $tasks['workflow'] = array(); $tasks['rejected'] = array(); + $tasks['checkedout'] = array(); +*/ - if($workflowmode == 'traditional' || $workflowmode == 'traditional_only_approval') { - $resArr = $dms->getDocumentList('ApproveByMe', $user); - if($resArr) { - $docs = array(); - foreach ($resArr as $res) { - $document = $dms->getDocument($res["id"]); - if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { - $docs[] = $document; -// $tasks['approval'][] = array('id'=>$res['id'], 'name'=>$res['name']); + if($workflowmode == 'traditional' || $workflowmode == 'traditional_only_approval') + if(!$tasksinmenu || in_array('approval', $tasksinmenu)) { + $tasks['approval'] = array(); + $resArr = $dms->getDocumentList('ApproveByMe', $user, false, '', '', false); + if($resArr) { + $docs = array(); + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $docs[] = $document; + } } + if($this->hasHook('filterApprovalTaskList')) + $docs = $this->callHook('filterApprovalTaskList', $docs); + foreach($docs as $doc) + $tasks['approval'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); } - if($this->hasHook('filterApprovalTaskList')) - $docs = $this->callHook('filterApprovalTaskList', $docs); - foreach($docs as $doc) - $tasks['approval'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); } - if($workflowmode == 'traditional') { - $resArr = $dms->getDocumentList('ReviewByMe', $user); + if($workflowmode == 'traditional') + if(!$tasksinmenu || in_array('review', $tasksinmenu)) { + $tasks['review'] = array(); + $resArr = $dms->getDocumentList('ReviewByMe', $user, false, '', '', false); if($resArr) { $docs = array(); foreach ($resArr as $res) { @@ -82,38 +87,111 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { $tasks['review'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); } } - } elseif($workflowmode == 'advanced') { - $resArr = $dms->getDocumentList('WorkflowByMe', $user); + if($enablereceiptworkflow) + if(!$tasksinmenu || in_array('receipt', $tasksinmenu)) { + $tasks['receipt'] = array(); + $resArr = $dms->getDocumentList('ReceiptByMe', $user); + if($resArr) { + $docs = array(); + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $docs[] = $document; + } + } + if($this->hasHook('filterReceiptTaskList')) + $docs = $this->callHook('filterReceiptTaskList', $docs); + foreach($docs as $doc) + $tasks['receipt'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); + } + } + if($enablerevisionworkflow) + if(!$tasksinmenu || in_array('revision', $tasksinmenu)) { + $tasks['revision'] = array(); + $resArr = $dms->getDocumentList('ReviseByMe', $user); + if($resArr) { + $docs = array(); + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $docs[] = $document; + } + } + if($this->hasHook('filterRevisionTaskList')) + $docs = $this->callHook('filterRevisionTaskList', $docs); + foreach($docs as $doc) + $tasks['revision'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); + } + } + if(!$tasksinmenu || in_array('needscorrection', $tasksinmenu)) { + $tasks['needscorrection'] = array(); + $resArr = $dms->getDocumentList('NeedsCorrectionOwner', $user); if($resArr) { $docs = array(); foreach ($resArr as $res) { $document = $dms->getDocument($res["id"]); if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { $docs[] = $document; -// $tasks['workflow'][] = array('id'=>$res['id'], 'name'=>$res['name']); } } - if($this->hasHook('filterWorkflowTaskList')) - $docs = $this->callHook('filterWorkflowTaskList', $docs); + if($this->hasHook('filterNeedsCorrectionTaskList')) + $docs = $this->callHook('filterNeedsCorrectionTaskList', $docs); foreach($docs as $doc) - $tasks['workflow'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); + $tasks['needscorrection'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); } } - $resArr = $dms->getDocumentList('RejectOwner', $user); - if($resArr) { - $docs = array(); - foreach ($resArr as $res) { - $document = $dms->getDocument($res["id"]); - if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { - $docs[] = $document; + if($workflowmode == 'advanced') + if(!$tasksinmenu || in_array('workflow', $tasksinmenu)) { + $tasks['workflow'] = array(); + $resArr = $dms->getDocumentList('WorkflowByMe', $user); + if($resArr) { + $docs = array(); + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $docs[] = $document; + } + } + if($this->hasHook('filterWorkflowTaskList')) + $docs = $this->callHook('filterWorkflowTaskList', $docs); + foreach($docs as $doc) + $tasks['workflow'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); } } - if($this->hasHook('filterRejectedTaskList')) - $docs = $this->callHook('filterRejectedTaskList', $docs); - foreach($docs as $doc) - $tasks['rejected'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); + if(!$tasksinmenu || in_array('rejected', $tasksinmenu)) { + $tasks['rejected'] = array(); + $resArr = $dms->getDocumentList('RejectOwner', $user); + if($resArr) { + $docs = array(); + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $docs[] = $document; + } + } + if($this->hasHook('filterRejectedTaskList')) + $docs = $this->callHook('filterRejectedTaskList', $docs); + foreach($docs as $doc) + $tasks['rejected'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); + } + } + if(!$tasksinmenu || in_array('checkedout', $tasksinmenu)) { + $tasks['checkedout'] = array(); + $resArr = $dms->getDocumentList('CheckedOutByMe', $user); + if($resArr) { + $docs = array(); + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document && $document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $docs[] = $document; + } + } + if($this->hasHook('filterCheckedOutTaskList')) + $docs = $this->callHook('filterCheckedOutTaskList', $docs); + foreach($docs as $doc) + $tasks['checkedout'][] = array('id'=>$doc->getId(), 'name'=>$doc->getName()); + } } - return $tasks; } /* }}} */ @@ -127,6 +205,46 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { * documents and folders. * @return string html code */ + function countTasks() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + $enablereceiptworkflow = $this->params['enablereceiptworkflow']; + $enablerevisionworkflow = $this->params['enablerevisionworkflow']; + $workflowmode = $this->params['workflowmode']; + $tasksinmenu = $this->params['tasksinmenu']; + + $startts = microtime(true); + $tasks = array(); + if($workflowmode == 'traditional' || $workflowmode == 'traditional_only_approval') + if(!$tasksinmenu || in_array('approval', $tasksinmenu)) { + $tasks['approval'] = $dms->countTasks('ApproveByMe', $user); + } + if($workflowmode == 'traditional') + if(!$tasksinmenu || in_array('review', $tasksinmenu)) { + $tasks['review'] = $dms->countTasks('ReviewByMe', $user); + } + if($workflowmode == 'advanced') + if(!$tasksinmenu || in_array('workflow', $tasksinmenu)) { + $tasks['workflow'] = $dms->countTasks('WorkflowByMe', $user); + } + if($enablereceiptworkflow) + if(!$tasksinmenu || in_array('receipt', $tasksinmenu)) + $tasks['receipt'] = $dms->countTasks('ReceiptByMe', $user); + if($enablerevisionworkflow) + if(!$tasksinmenu || in_array('revision', $tasksinmenu)) + $tasks['revision'] = $dms->countTasks('ReviseByMe', $user); + if(!$tasksinmenu || in_array('needscorrection', $tasksinmenu)) + $tasks['needscorrection'] = $dms->countTasks('NeedsCorrectionOwner', $user); + + header('Content-Type: application/json'); + echo json_encode(array('error'=>0, 'data'=>$tasks, 'processing_time'=>microtime(true)-$startts)); + } /* }}} */ + + /** + * Returns a json array of tasks to be done + * + * @return string json string + */ function myTasks() { /* {{{ */ $dms = $this->params['dms']; $user = $this->params['user']; @@ -144,13 +262,12 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { * This function renders the tasks in a way suitable to be * used as a menu * - * @param array $clipboard clipboard containing two arrays for both - * documents and folders. * @return string html code */ function menuTasks() { /* {{{ */ $dms = $this->params['dms']; $user = $this->params['user']; + $accessobject = $this->params['accessobject']; if(!$tasks = $this->__myTasks()) { echo ''; @@ -166,13 +283,21 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { $ct[] = count($tasks['approval']); if(isset($tasks['workflow'])) $ct[] = count($tasks['workflow']); + if(isset($tasks['receipt'])) + $ct[] = count($tasks['receipt']); + if(isset($tasks['revision'])) + $ct[] = count($tasks['revision']); + if(isset($tasks['needscorrection'])) + $ct[] = count($tasks['needscorrection']); if(isset($tasks['rejected'])) $ct[] = count($tasks['rejected']); + if(isset($tasks['checkedout'])) + $ct[] = count($tasks['checkedout']); $menuitems['tasks'] = array('label'=>getMLText('tasks')." (".implode('/', $ct).")"); $menuitems['tasks']['children'] = []; - if(!empty($tasks['review']) || !empty($tasks['approval']) || !empty($tasks['workflow'])) { + if(!empty($tasks['review']) || !empty($tasks['approval']) || !empty($tasks['receipt']) || !empty($tasks['revision']) || !empty($tasks['needscorrection']) || !empty($tasks['workflow']) || !empty($tasks['rejected']) || !empty($tasks['rejected']) || !empty($tasks['checkedout'])) { if(!empty($tasks['review'])) { $subitems = []; foreach($tasks['review'] as $t) { @@ -198,6 +323,30 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { } $menuitems['tasks']['children']['workflow'] = array('label'=>getMLText('documents_to_trigger_workflow'), 'children'=>$subitems); } + if(!empty($tasks['receipt'])) { + $subitems = []; + foreach($tasks['receipt'] as $t) { + $doc = $dms->getDocument($t['id']); + $subitems[] = array('label'=>$doc->getName(), 'link'=>"../out/out.ViewDocument.php?documentid=".$doc->getID()."¤ttab=recipients", 'class'=>"table-row-document", 'rel'=>"document_".$doc->getID()); + } + $menuitems['tasks']['children']['receipt'] = array('label'=>getMLText('documents_to_receipt'), 'children'=>$subitems); + } + if(!empty($tasks['revision'])) { + $subitems = []; + foreach($tasks['revision'] as $t) { + $doc = $dms->getDocument($t['id']); + $subitems[] = array('label'=>$doc->getName(), 'link'=>"../out/out.ViewDocument.php?documentid=".$doc->getID()."¤ttab=revision", 'class'=>"table-row-document", 'rel'=>"document_".$doc->getID()); + } + $menuitems['tasks']['children']['revision'] = array('label'=>getMLText('documents_to_revise'), 'children'=>$subitems); + } + if(!empty($tasks['needscorrection'])) { + $subitems = []; + foreach($tasks['needscorrection'] as $t) { + $doc = $dms->getDocument($t['id']); + $subitems[] = array('label'=>$doc->getName(), 'link'=>"../out/out.ViewDocument.php?documentid=".$doc->getID()."¤ttab=docinfo", 'class'=>"table-row-document", 'rel'=>"document_".$doc->getID()); + } + $menuitems['tasks']['children']['needscorrection'] = array('label'=>getMLText('documents_to_correct'), 'children'=>$subitems); + } if(!empty($tasks['rejected'])) { $subitems = []; foreach($tasks['rejected'] as $t) { @@ -206,10 +355,20 @@ class SeedDMS_View_Tasks extends SeedDMS_Theme_Style { } $menuitems['tasks']['children']['rejected'] = array('label'=>getMLText('documents_rejected'), 'children'=>$subitems); } + if(!empty($tasks['checkedout'])) { + $subitems = []; + foreach($tasks['checkedout'] as $t) { + $doc = $dms->getDocument($t['id']); + $subitems[] = array('label'=>$doc->getName(), 'link'=>"../out/out.ViewDocument.php?documentid=".$doc->getID()."¤ttab=docinfo", 'class'=>"table-row-document", 'rel'=>"document_".$doc->getID()); + } + $menuitems['tasks']['children']['checkedout'] = array('label'=>getMLText('documents_checked_out_by_you'), 'children'=>$subitems); + } + if ($accessobject->check_view_access('MyDocuments')) { $menuitems['tasks']['children']['divider'] = array('divider'=>true); $menuitems['tasks']['children']['mydocuments'] = array('label'=>getMLText('my_documents'), 'link'=>$this->params['settings']->_httpRoot.'out/out.MyDocuments.php'); } self::showNavigationBar($menuitems, array('right'=>true)); + } } /* }}} */ /** diff --git a/views/bootstrap/class.Timeline.php b/views/bootstrap/class.Timeline.php index 93732e9dc..d4529c511 100644 --- a/views/bootstrap/class.Timeline.php +++ b/views/bootstrap/class.Timeline.php @@ -18,11 +18,6 @@ */ //require_once("class.Bootstrap.php"); -/** - * Include class to preview documents - */ -require_once("SeedDMS/Preview.php"); - /** * Class which outputs the html page for Timeline view * @@ -94,8 +89,11 @@ class SeedDMS_View_Timeline extends SeedDMS_Theme_Style { case 'status_change': $msg = getMLText('timeline_full_'.$item['type'], array('document'=>htmlspecialchars($item['document']->getName()), 'version'=> $item['version'], 'status'=> getOverallStatusText($item['status']))); break; + case 'scheduled_revision': + $msg = getMLText('timeline_full_'.$item['type'], array('document'=>htmlspecialchars($item['document']->getName()), 'version'=> $item['version'])); + break; default: - $msg = '???'; + $msg = getMLText('timeline_full_'.$item['type'], array('document'=>htmlspecialchars($item['document']->getName()))); } $data[$i]['msg'] = $msg; } @@ -276,6 +274,8 @@ div.timeline-event-selected { '.getMLText('timeline_skip_status_change_1').'
'.getMLText('timeline_skip_status_change_2').'
'.getMLText('timeline_skip_status_change_3').'
+ '.getMLText('timeline_skip_status_change_4').'
+ '.getMLText('timeline_skip_status_change_5').'
'.getMLText('timeline_skip_status_change_-1').'
'.getMLText('timeline_skip_status_change_-2').'
'.getMLText('timeline_skip_status_change_-3').'
'; @@ -286,6 +286,7 @@ div.timeline-event-selected { $this->formSubmit(' '.getMLText('update'), 'update'); ?> + contentContainerEnd(); echo "
"; @@ -296,7 +297,6 @@ div.timeline-event-selected { $this->columnEnd(); $this->rowEnd(); - $this->contentContainerEnd(); $this->contentEnd(); $this->htmlEndPage(); } /* }}} */ diff --git a/views/bootstrap/class.TimelineFeed.php b/views/bootstrap/class.TimelineFeed.php new file mode 100644 index 000000000..854744f91 --- /dev/null +++ b/views/bootstrap/class.TimelineFeed.php @@ -0,0 +1,150 @@ + + * @copyright Copyright (C)2016 Uwe Steinmann + * @version Release: @package_version@ + */ + +/** + * Include parent class + */ +//require_once("class.Bootstrap.php"); + +require_once("vendor/autoload.php"); + +use \FeedWriter\RSS2; + +/** + * Class which outputs the html page for UserList view + * + * @category DMS + * @package SeedDMS + * @author Uwe Steinmann + * @copyright Copyright (C)2016 Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_View_TimelineFeed extends SeedDMS_Theme_Style { + + function show() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + $settings = $this->params['settings']; + $httproot = $this->params['httproot']; + $skip = $this->params['skip']; + $fromdate = $this->params['fromdate']; + $todate = $this->params['todate']; + $cachedir = $this->params['cachedir']; + $sitename = $this->params['sitename']; + $previewwidthlist = $this->params['previewWidthList']; + $previewwidthdetail = $this->params['previewWidthDetail']; + $timeout = $this->params['timeout']; + + if($fromdate) { + $from = makeTsFromLongDate($fromdate.' 00:00:00'); + } else { + $from = time()-7*86400; + } + + if($todate) { + $to = makeTsFromLongDate($todate.' 23:59:59'); + } else { + $to = time(); + } + + $baseurl = "http".((isset($_SERVER['HTTPS']) && (strcmp($_SERVER['HTTPS'],'off')!=0)) ? "s" : "")."://".$_SERVER['HTTP_HOST'].$httproot; + $feed = new RSS2; + $feed->setTitle($sitename.': Recent Changes'); + $feed->setLink($baseurl); + $feed->setDescription('Show recent changes in SeedDMS.'); + // Image title and link must match with the 'title' and 'link' channel elements for RSS 2.0, + // which were set above. +// $feed->setImage('Testing & Checking the Feed Writer project', 'https://github.com/mibe/FeedWriter', 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Rss-feed.svg/256px-Rss-feed.svg.png'); + // Use core setChannelElement() function for other optional channel elements. + // See http://www.rssboard.org/rss-specification#optionalChannelElements + // for other optional channel elements. Here the language code for American English and + $feed->setChannelElement('language', str_replace('_', '-', $user->getLanguage())); + // The date when this feed was lastly updated. The publication date is also set. + $feed->setDate(date(DATE_RSS, time())); + $feed->setChannelElement('pubDate', date(\DATE_RSS, time() /*strtotime('2013-04-06')*/)); + // You can add additional link elements, e.g. to a PubSubHubbub server with custom relations. + // It's recommended to provide a backlink to the feed URL. + $feed->setSelfLink($baseurl.'out/out.TimelineFeed.php'); +// $feed->setAtomLink('http://pubsubhubbub.appspot.com', 'hub'); + // You can add more XML namespaces for more custom channel elements which are not defined + // in the RSS 2 specification. Here the 'creativeCommons' element is used. There are much more + // available. Have a look at this list: http://feedvalidator.org/docs/howto/declare_namespaces.html +// $feed->addNamespace('creativeCommons', 'http://backend.userland.com/creativeCommonsRssModule'); +// $feed->setChannelElement('creativeCommons:license', 'http://www.creativecommons.org/licenses/by/1.0'); + // If you want you can also add a line to publicly announce that you used + // this fine piece of software to generate the feed. ;-) +// $feed->addGenerator(); + + if($data = $dms->getTimeline($from, $to)) { + foreach($data as $i=>$item) { + switch($item['type']) { + case 'add_version': + $msg = getMLText('timeline_'.$item['type'], array(), null, $user->getLanguage()); + break; + case 'add_file': + $msg = getMLText('timeline_'.$item['type'], array(), null, $user->getLanguage()); + break; + case 'status_change': + $msg = getMLText('timeline_'.$item['type'], array('version'=> $item['version'], 'status'=> getOverallStatusText($item['status'])), null, $user->getLanguage()); + break; + default: + $msg = '???'; + } + $data[$i]['msg'] = $msg; + } + + $previewer = new SeedDMS_Preview_Previewer($cachedir, $previewwidthdetail, $timeout); + foreach($data as $item) { + if($item['type'] == 'status_change') + $classname = $item['type']."_".$item['status']; + else + $classname = $item['type']; + if(!$skip || !in_array($classname, $skip)) { + $doc = $item['document']; + $owner = $doc->getOwner(); + $d = makeTsFromLongDate($item['date']); + $newItem = $feed->createNewItem(); + $newItem->setTitle($doc->getName()." (".$item['msg'].")"); + $newItem->setLink($baseurl.'out/out.ViewDocument.php?documentid='.$doc->getID()); + $newItem->setDescription("

".$item['msg']."

". + "

".getMLText('comment', array(), null, $user->getLanguage()).": ".$doc->getComment()."

". + "

".getMLText('owner', array(), null, $user->getLanguage()).": getEmail())."\">".htmlspecialchars($owner->getFullName())."

". + "

".getMLText("creation_date", array(), null, $user->getLanguage()).": ".getLongReadableDate($doc->getDate())."

" + ); + $newItem->setDate(date('c', $d)); + $newItem->setAuthor($owner->getFullName(), preg_match('/.+@.+/', $owner->getEmail()) == 1 ? $owner->getEmail() : null); + $newItem->setId($baseurl.'out/out.ViewDocument.php?documentid='.$doc->getID()."&kkk=".$classname, true); + if(!empty($item['version'])) { + $version = $doc->getContentByVersion($item['version']); + $previewer->createPreview($version); + if($previewer->hasPreview($version)) { + $token = new SeedDMS_JwtToken($settings->_encryptionKey); + $data = array('d'=>$doc->getId(), 'v'=>$item['version'], 'u'=>$user->getId(), 'w'=>$previewwidthdetail,); + $hash = $token->jwtEncode($data); + $newItem->addElement('enclosure', null, array('url' => $baseurl.'op/op.TimelineFeedPreview.php?hash='.$hash, 'length'=>$previewer->getFileSize($version), 'type'=>'image/png')); + } + } + $feed->addItem($newItem); + } + } + } + + // OK. Everything is done. Now generate the feed. + // If you want to send the feed directly to the browser, use the printFeed() method. + $myFeed = $feed->generateFeed(); + // Do anything you want with the feed in $myFeed. Why not send it to the browser? ;-) + // You could also save it to a file if you don't want to invoke your script every time. + header('Content-Type: application/rss+xml'); + echo $myFeed; + } /* }}} */ +} diff --git a/views/bootstrap/class.TransmittalMgr.php b/views/bootstrap/class.TransmittalMgr.php new file mode 100644 index 000000000..b89bd7a31 --- /dev/null +++ b/views/bootstrap/class.TransmittalMgr.php @@ -0,0 +1,246 @@ + + * @copyright Copyright (C) 2002-2005 Markus Westphal, + * 2006-2008 Malcolm Cowe, 2010 Matteo Lucarelli, + * 2010-2012 Uwe Steinmann + * @version Release: @package_version@ + */ + +/** + * Include parent class + */ +//require_once("class.Bootstrap.php"); + +/** + * Class which outputs the html page for TransmittalMgr view + * + * @category DMS + * @package SeedDMS + * @author Markus Westphal, Malcolm Cowe, Uwe Steinmann + * @copyright Copyright (C) 2002-2005 Markus Westphal, + * 2006-2008 Malcolm Cowe, 2010 Matteo Lucarelli, + * 2010-2012 Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_View_TransmittalMgr extends SeedDMS_Theme_Style { + + use TransmittalDeleteButton; + use TransmittalUpdateButton; + + function js() { /* {{{ */ + $showtree = $this->params['showtree']; + $onepage = $this->params['onepage']; + + header('Content-Type: application/javascript; charset=UTF-8'); + parent::jsTranslations(array('js_form_error', 'js_form_errors', 'cancel', 'splash_move_document', 'confirm_move_document', 'move_document', 'confirm_transfer_link_document', 'transfer_content', 'link_document', 'splash_move_folder', 'confirm_move_folder', 'move_folder')); + $this->printDeleteDocumentButtonJs(); + $this->printDeleteItemButtonJs(); + $this->printUpdateItemButtonJs(); + if($onepage) + $this->printClickDocumentJs(); +?> +function runValidation() { + $("#form1").validate({ + rules: { + name: { + required: true + }, + }, + messages: { + name: "", + } + }); +} +$(document).ready( function() { + $('body').on('click', '.selecttransmittal', function(ev){ + ev.preventDefault(); + $('div.ajax').trigger('update', {transmittalid: $(ev.currentTarget).data('transmittalid')}); + window.history.pushState({"html":"","pageTitle":""},"", '../out/out.TransmittalMgr.php?transmittalid=' + $(ev.currentTarget).data('transmittalid')); + }); +}); +params['dms']; + $user = $this->params['user']; + $accessop = $this->params['accessobject']; +?> +
+ + + + + +contentContainerStart(); + $this->formField( + getMLText("name"), + array( + 'element'=>'input', + 'type'=>'text', + 'id'=>'name', + 'name'=>'name', + 'value'=>($transmittal ? htmlspecialchars($transmittal->getName()) : '') + ) + ); + $this->formField( + getMLText("comment"), + array( + 'element'=>'textarea', + 'id'=>'comment', + 'name'=>'comment', + 'rows'=>4, + 'value'=>($transmittal ? htmlspecialchars($transmittal->getComment()) : '') + ) + ); + $this->contentContainerEnd(); + if($transmittal && $accessop->check_controller_access('TransmittalMgr', array('action'=>'edittransmittal')) || !$transmittal && $accessop->check_controller_access('TransmittalMgr', array('action'=>'addtransmittal'))) { + $this->formSubmit(" ".($transmittal ? getMLText('save') : getMLText('add_transmittal'))); + } +?> +
+params['seltransmittal']; + + $this->showTransmittalForm($seltransmittal); + } /* }}} */ + + protected function showTransmittalItems($seltransmittal) { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + $accessop = $this->params['accessobject']; + $cachedir = $this->params['cachedir']; + $timeout = $this->params['timeout']; + $previewwidth = $this->params['previewWidthList']; + $previewconverters = $this->params['previewConverters']; + + $previewer = new SeedDMS_Preview_Previewer($cachedir, $previewwidth, $timeout); + $previewer->setConverters($previewconverters); + + if($seltransmittal) { + $items = $seltransmittal->getItems(); + if($items) { + print ""; + print "\n\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n\n\n"; + foreach($items as $item) { + if($content = $item->getContent()) { + $document = $content->getDocument(); + $latestcontent = $document->getLatestContent(); + if ($document->getAccessMode($user) >= M_READ) { +// echo "getID()."\">"; + echo $this->documentListRowStart($document); + echo $this->documentListRow($document, $previewer, true, $content->getVersion()); + echo ""; + echo $this->documentListRowEnd($document); + } + } else { + echo "getID()."\">"; + echo ""; + echo ""; + } + } + print "\n
".getMLText("name")."".getMLText("status")."".getMLText("document")."".getMLText("action")."
"; + $this->printDeleteItemButton($item, getMLText('transmittalitem_removed')); + if($latestcontent->getVersion() != $content->getVersion()) + $this->printUpdateItemButton($item, getMLText('transmittalitem_updated', array('prevversion'=>$content->getVersion(), 'newversion'=>$latestcontent->getVersion()))); + echo "
content ist weg
\n"; + print "getID()."\">".getMLText('download').""; + } + } + } /* }}} */ + + function items() { /* {{{ */ + $seltransmittal = $this->params['seltransmittal']; + + $this->showTransmittalItems($seltransmittal); + } /* }}} */ + + function show() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + $accessop = $this->params['accessobject']; + $seltransmittal = $this->params['seltransmittal']; + + $this->htmlAddHeader(''."\n", 'js'); + $this->htmlAddHeader(''."\n", 'js'); + + $this->htmlStartPage(getMLText("my_transmittals")); + $this->globalNavigation(); + $this->contentStart(); + $this->pageNavigation(getMLText("my_transmittals"), "my_documents"); + $this->contentHeading(getMLText("my_transmittals")); + $this->rowStart(); + $this->columnStart(4); + + $transmittals = $dms->getAllTransmittals($user); + + if ($transmittals){ + print ""; + print "\n\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n"; + print "\n\n\n"; + foreach($transmittals as $transmittal) { + print "\n"; + print ""; + print ""; + $items = $transmittal->getItems(); + print ""; + print ""; + print "\n"; + } + print "\n
".getMLText("name")."".getMLText("comment")."".getMLText("transmittal_size")."
".$transmittal->getName()."".$transmittal->getComment()."".count($items)." (".SeedDMS_Core_File::format_filesize($transmittal->getSize()).")"; + print ""; + print "
\n"; + } + + $this->columnEnd(); + $this->columnStart(8); + if($accessop->check_view_access($this, array('action'=>'form'))) { +?> +
getID()."\"" : "") ?>>
+check_view_access($this, array('action'=>'items'))) { +?> +
getID()."\"" : "") ?>>
+columnEnd(); + $this->rowEnd(); + $this->contentEnd(); + $this->htmlEndPage(); + } /* }}} */ +} +?> diff --git a/views/bootstrap/class.TriggerWorkflow.php b/views/bootstrap/class.TriggerWorkflow.php index 0cc18bd70..f04e1c2e2 100644 --- a/views/bootstrap/class.TriggerWorkflow.php +++ b/views/bootstrap/class.TriggerWorkflow.php @@ -65,8 +65,17 @@ $(document).ready(function() { $action = $transition->getAction(); $currentstate = $latestContent->getWorkflowState(); - $wkflog = $latestContent->getWorkflowLog(); - $workflow = $latestContent->getWorkflow(); + $wkflogs = $latestContent->getWorkflowLog(); + /* Check if latest content is still in workflow, which should be + * always the case, otherwise this code would be executed. + * In that case the returned log is just a list of entries for the + * current workflow. If the document was not in a workflow, then the + * log entries for all workflows of this content will be returned + */ + if($workflow = $latestContent->getWorkflow()) + $wkflog = $wkflogs; + else + $wkflog = array_shift($wkflogs); $msg = "The document is currently in state: ".$currentstate->getName()."
"; if($wkflog) { @@ -104,7 +113,7 @@ $(document).ready(function() { 'required'=>false ) ); - $this->formSubmit(getMLText("action_".strtolower($action->getName()), array(), $action->getName())); + $this->formSubmit(getMLText("action_".strtolower($action->getName()), array(), htmlspecialchars($action->getName()))); ?> params['enableadminrevapp']; $enableownerrevapp = $this->params['enableownerrevapp']; $enableselfrevapp = $this->params['enableselfrevapp']; + $enablereceiptworkflow = $this->params['enablereceiptworkflow']; + $enableselfreceipt = $this->params['enableselfreceipt']; $dropfolderdir = $this->params['dropfolderdir']; $workflowmode = $this->params['workflowmode']; $presetexpiration = $this->params['presetexpiration']; @@ -185,6 +187,7 @@ console.log(element); $latestContent = $document->getLatestContent(); $reviewStatus = $latestContent->getReviewStatus(); + $receiptStatus = $latestContent->getReceiptStatus(); $approvalStatus = $latestContent->getApprovalStatus(); if($workflowmode == 'advanced') { if($status = $latestContent->getStatus()) { @@ -196,6 +199,12 @@ console.log(element); $msg = getMLText("max_upload_size").": ".SeedDMS_Core_File::format_filesize($maxuploadsize); $this->warningMsg($msg); + + if ($document->isCheckedOut()) { + $msg = getMLText('document_is_checked_out_update'); + $this->warningMsg($msg); + } + ?>
@@ -329,10 +338,14 @@ console.log(element); ) ); } - $this->warningMsg(getMLText("add_doc_workflow_warning")); + $this->contentContainerEnd(); + if($settings->_initialDocumentStatus == S_RELEASED) + $this->warningMsg(getMLText("add_doc_workflow_warning")); } elseif($workflowmode == 'traditional' || $workflowmode == 'traditional_only_approval') { if($workflowmode == 'traditional') { + $this->contentContainerEnd(); $this->contentSubHeading(getMLText("assign_reviewers")); + $this->contentContainerStart(); $res=$user->getMandatoryReviewers(); $options = array(); foreach ($docAccess["users"] as $usr) { @@ -409,6 +422,24 @@ console.log(element); } } } + + $options = array(); + foreach ($docAccess["groups"] as $grp) { + $options[] = array($grp->getID(), htmlspecialchars($grp->getName())); + } + $this->formField( + getMLText("individuals_in_groups"), + array( + 'element'=>'select', + 'name'=>'grpIndReviewers[]', + 'id'=>'GrpIndReviewers', + 'class'=>'chzn-select', + 'attributes'=>array(array('data-placeholder', getMLText('select_grp_ind_reviewers'))), + 'multiple'=>true, + 'options'=>$options + ) + ); + $options = array(); foreach ($docAccess["groups"] as $grp) { @@ -480,9 +511,11 @@ console.log(element); } } } + $this->contentContainerEnd(); } $this->contentSubHeading(getMLText("assign_approvers")); + $this->contentContainerStart(); $options = array(); $res=$user->getMandatoryApprovers(); foreach ($docAccess["users"] as $usr) { @@ -558,6 +591,23 @@ console.log(element); } } + $options = array(); + foreach ($docAccess["groups"] as $grp) { + $options[] = array($grp->getID(), htmlspecialchars($grp->getName())); + } + $this->formField( + getMLText("individuals_in_groups"), + array( + 'element'=>'select', + 'name'=>'grpIndApprovers[]', + 'id'=>'GrpIndApprovers', + 'class'=>'chzn-select', + 'attributes'=>array(array('data-placeholder', getMLText('select_grp_ind_approvers'))), + 'multiple'=>true, + 'options'=>$options + ) + ); + $options = array(); foreach ($docAccess["groups"] as $grp) { @@ -631,9 +681,90 @@ console.log(element); } } } + $this->contentContainerEnd(); $this->warningMsg(getMLText("add_doc_reviewer_approver_warning")); + } else { + $this->contentContainerEnd(); + } + if($enablereceiptworkflow) { + $this->contentSubHeading(getMLText("assign_recipients")); + $this->contentContainerStart(); + $options = array(); + foreach ($docAccess["users"] as $usr) { + if (!$enableselfreceipt && $usr->getID()==$user->getID()) continue; + $options[] = array($usr->getID(), htmlspecialchars($usr->getLogin()." - ".$usr->getFullName())); + } + $tmp = array(); + foreach($receiptStatus as $r) { + if($r['type'] == 0) { + $tmp[] = $r['required']; + } + } + $fieldwrap = array(); + if($tmp) { + $fieldwrap = array('', $this->getSelectPresetButtonHtml("IndRecipient", $tmp)); + } + $this->formField( + getMLText("individuals"), + array( + 'element'=>'select', + 'name'=>'indRecipients[]', + 'id'=>'IndRecipient', + 'class'=>'chzn-select', + 'attributes'=>array(array('data-placeholder', getMLText('select_ind_recipients')), array('data-no_results_text', getMLText('unknown_owner'))), + 'multiple'=>true, + 'options'=>$options + ), + array('field_wrap'=>$fieldwrap) + ); + + $options = array(); + foreach ($docAccess["groups"] as $grp) { + $options[] = array($grp->getID(), htmlspecialchars($grp->getName())); + } + $this->formField( + getMLText("individuals_in_groups"), + array( + 'element'=>'select', + 'name'=>'grpIndRecipients[]', + 'id'=>'GrpIndRecipient', + 'class'=>'chzn-select', + 'attributes'=>array(array('data-placeholder', getMLText('select_grp_ind_recipients'))), + 'multiple'=>true, + 'options'=>$options + ) + ); + + $options = array(); + foreach ($docAccess["groups"] as $grp) { + $options[] = array($grp->getID(), htmlspecialchars($grp->getName())); + } + $tmp = array(); + foreach($receiptStatus as $r) { + if($r['type'] == 1) { + $tmp[] = $r['required']; + } + } + $fieldwrap = array(); + if($tmp) { + $fieldwrap = array('', $this->getSelectPresetButtonHtml("GrpRecipient", $tmp)); + } + $this->formField( + getMLText("groups"), + array( + 'element'=>'select', + 'name'=>'grpRecipients[]', + 'id'=>'GrpRecipient', + 'class'=>'chzn-select', + 'attributes'=>array(array('data-placeholder', getMLText('select_grp_recipients')), array('data-no_results_text', getMLText('unknown_owner'))), + 'multiple'=>true, + 'options'=>$options + ), + array('field_wrap'=>$fieldwrap) + ); + + $this->contentContainerEnd(); } - $this->contentContainerEnd(); $this->formSubmit(getMLText('update_document')); ?>
diff --git a/views/bootstrap/class.UserDefaultKeywords.php b/views/bootstrap/class.UserDefaultKeywords.php index f7931c12e..70f60d8e6 100644 --- a/views/bootstrap/class.UserDefaultKeywords.php +++ b/views/bootstrap/class.UserDefaultKeywords.php @@ -139,7 +139,7 @@ $(document).ready(function() { - + formSubmit(' '.getMLText('rm_default_keyword_category'),'','','danger');?> @@ -151,7 +151,7 @@ $(document).ready(function() { - + formSubmit(' '.getMLText('save'),'','','neutral');?> @@ -170,14 +170,14 @@ $(document).ready(function() { "> "> - + formSubmit(' '.getMLText('save'),'','','neutral');?>
"> - + formSubmit(' '.getMLText('delete'),'','','danger');?>

diff --git a/views/bootstrap/class.UserList.php b/views/bootstrap/class.UserList.php index 5ca7b3a05..ecbe044f8 100644 --- a/views/bootstrap/class.UserList.php +++ b/views/bootstrap/class.UserList.php @@ -52,6 +52,7 @@ class SeedDMS_View_UserList extends SeedDMS_Theme_Style { $httproot = $this->params['httproot']; $quota = $this->params['quota']; $pwdexpiration = $this->params['pwdexpiration']; + $accessobject = $this->params['accessobject']; $this->htmlStartPage(getMLText("admin_tools")); $this->globalNavigation(); @@ -70,7 +71,7 @@ class SeedDMS_View_UserList extends SeedDMS_Theme_Style { echo "isDisabled() ? " class=\"error\"" : "").">"; echo ""; if ($currUser->hasImage()) - print ""; + print "html_url('UserImage', array('userid'=>$currUser->getId()))."\" >"; echo ""; echo ""; echo htmlspecialchars($currUser->getFullName())." (".htmlspecialchars($currUser->getLogin()).")
"; @@ -88,17 +89,7 @@ class SeedDMS_View_UserList extends SeedDMS_Theme_Style { } echo ""; echo ""; - switch($currUser->getRole()) { - case SeedDMS_Core_User::role_user: - printMLText("role_user"); - break; - case SeedDMS_Core_User::role_admin: - printMLText("role_admin"); - break; - case SeedDMS_Core_User::role_guest: - printMLText("role_guest"); - break; - } + echo htmlspecialchars($currUser->getRole()->getName()); echo ""; echo ""; echo SeedDMS_Core_File::format_filesize($currUser->getUsedDiskSpace()); @@ -129,10 +120,12 @@ class SeedDMS_View_UserList extends SeedDMS_Theme_Style { } echo ""; echo ""; - echo "
"; - echo "getID()."\"> "; - echo "getID()."\">"; - echo "
"; + if($accessobject->check_view_access(array('UsrMgr', 'RemoveUser'))) { + echo "
"; + echo $this->html_link('UsrMgr', array('userid'=>$currUser->getID()), array(), '', false); + echo $this->html_link('RemoveUser', array('userid'=>$currUser->getID()), array(), '', false); + echo "
"; + } echo ""; echo ""; } diff --git a/views/bootstrap/class.UsrMgr.php b/views/bootstrap/class.UsrMgr.php index a32e29263..1f6b3488c 100644 --- a/views/bootstrap/class.UsrMgr.php +++ b/views/bootstrap/class.UsrMgr.php @@ -37,6 +37,7 @@ class SeedDMS_View_UsrMgr extends SeedDMS_Theme_Style { header('Content-Type: application/javascript; charset=UTF-8'); parent::jsTranslations(array('js_form_error', 'js_form_errors')); + $this->printFolderChooserJs("form"); ?> function runValidation() { $("#form1").validate({ @@ -96,6 +97,7 @@ $(document).ready( function() { $user = $this->params['user']; $seluser = $this->params['seluser']; $quota = $this->params['quota']; + $settings = $this->params['settings']; $workflowmode = $this->params['workflowmode']; if($seluser) { @@ -165,6 +167,26 @@ $(document).ready( function() { echo "".getMLText('mandatory_approvers')."".count($resArr)."\n"; } } + $resArr = $dms->getDocumentList('ReceiptByMe', $seluser); + if($resArr) { + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $tasks['receipt'][] = array('id'=>$res['id'], 'name'=>$res['name']); + } + } + echo "".getMLText('pending_receipt')."".count($tasks['receipt'])."\n"; + } + $resArr = $dms->getDocumentList('ReviseByMe', $seluser); + if($resArr) { + foreach ($resArr as $res) { + $document = $dms->getDocument($res["id"]); + if($document->getAccessMode($user) >= M_READ && $document->getLatestContent()) { + $tasks['revision'][] = array('id'=>$res['id'], 'name'=>$res['name']); + } + } + echo "".getMLText('pending_revision')."".count($tasks['revision'])."\n"; + } if($workflowmode == 'advanced') { $workflows = $seluser->getWorkflowsInvolved(); echo "".getMLText('workflows_involded')."".count($workflows)."\n"; @@ -177,6 +199,7 @@ $(document).ready( function() { $session = array_shift($sessions); echo "".getMLText('lastaccess')."".getLongReadableDate($session->getLastAccess())."\n"; } + // echo "".getMLText('network_drive')."_httpRoot.'checkout/'.preg_replace('/[^A-Za-z0-9_-]/', '', $seluser->getLogin())."\">".preg_replace('/[^A-Za-z0-9_-]/', '', $seluser->getLogin())."\n"; echo ""; } @@ -227,6 +250,7 @@ $(document).ready( function() { $settings = $this->params['settings']; $users = $this->params['allusers']; $groups = $this->params['allgroups']; + $roles = $this->params['allroles']; $passwordstrength = $this->params['passwordstrength']; $passwordexpiration = $this->params['passwordexpiration']; $httproot = $this->params['httproot']; @@ -283,7 +307,7 @@ $(document).ready( function() { if($passwordexpiration > 0 && (!$currUser || !$currUser->isAdmin())) { $options = array(); if($currUser) - $options[] = array('', getMLText("keep")); + $options[] = array('', getMLText("keep").($currUser->getPwdExpiration() ? ' ('.getLongReadableDate($currUser->getPwdExpiration()).')' : '')); $options[] = array('now', getMLText('now')); $options[] = array(date('Y-m-d H:i:s', time()+$passwordexpiration*86400), getMLText("according_settings")); $options[] = array('never', getMLText("never")); @@ -328,9 +352,9 @@ $(document).ready( function() { ) ); $options = array(); - $options[] = array(SeedDMS_Core_User::role_user, getMLText('role_user')); - $options[] = array(SeedDMS_Core_User::role_admin, getMLText('role_admin'), $currUser && $currUser->getRole() == SeedDMS_Core_User::role_admin); - $options[] = array(SeedDMS_Core_User::role_guest, getMLText('role_guest'), $currUser && $currUser->getRole() == SeedDMS_Core_User::role_guest); + foreach($roles as $role) { + $options[] = array($role->getID(), htmlspecialchars($role->getName()), ($currUser && $currUser->getRole()->getID() == $role->getID())); + } $this->formField( getMLText("role"), array( @@ -420,6 +444,31 @@ $(document).ready( function() { ); } } + $options = array(); + if($currUser) { + $substitutes = $currUser->getSubstitutes(); + } else { + $substitutes = array(); + } + foreach ($users as $usr) { + if ($usr->isGuest() || ($currUser && !$usr->isAdmin() && $currUser->isAdmin()) || ($currUser && $usr->getID() == $currUser->getID())) + continue; + $checked=false; + foreach ($substitutes as $r) if ($r->getID()==$usr->getID()) $checked=true; + + $options[] = array($usr->getID(), htmlspecialchars($usr->getLogin()." - ".$usr->getFullName()), $checked); + } + $this->formField( + getMLText("possible_substitutes"), + array( + 'element'=>'select', + 'name'=>'substitute[]', + 'class'=>'chzn-select', + 'multiple'=>true, + 'attributes'=>array(array('data-placeholder', getMLText('select_users')), array('data-no_result_text', getMLText('unknown_owner'))), + 'options'=>$options + ) + ); if($workflowmode == "traditional" || $workflowmode == 'traditional_only_approval') { if($workflowmode == "traditional") { $this->contentSubHeading(getMLText("mandatory_reviewers")); @@ -558,6 +607,7 @@ $(document).ready( function() { $undeluserids = $this->params['undeluserids']; $workflowmode = $this->params['workflowmode']; $quota = $this->params['quota']; + $accessobject = $this->params['accessobject']; $this->htmlAddHeader(''."\n", 'js'); $this->htmlAddHeader(''."\n", 'js'); @@ -591,13 +641,19 @@ $(document).ready( function() { ); ?> +check_view_access($this, array('action'=>'actionmenu'))) { ?>
getID()."\"" : "") ?>>
+ +check_view_access($this, array('action'=>'info'))) { ?>
getID()."\"" : "") ?>>
columnEnd(); - $this->columnStart(4); - ?> + $this->columnStart(8); +?> +check_view_access($this, array('action'=>'form'))) { ?>
getID()."\"" : "") ?>>
+ columnEnd(); diff --git a/views/bootstrap/class.ViewDocument.php b/views/bootstrap/class.ViewDocument.php index 06614a509..c96d231d7 100644 --- a/views/bootstrap/class.ViewDocument.php +++ b/views/bootstrap/class.ViewDocument.php @@ -18,11 +18,6 @@ */ //require_once("class.Bootstrap.php"); -/** - * Include class to preview documents - */ -require_once("SeedDMS/Preview.php"); - /** * Class which outputs the html page for ViewDocument view * @@ -173,6 +168,9 @@ class SeedDMS_View_ViewDocument extends SeedDMS_Theme_Style { case 'status_change': $msg = getMLText('timeline_'.$item['type'], array('document'=>htmlspecialchars($item['document']->getName()), 'version'=> $item['version'], 'status'=> getOverallStatusText($item['status']))); break; + case 'scheduled_revision': + $msg = getMLText('timeline_'.$item['type'], array('document'=>htmlspecialchars($item['document']->getName()), 'version'=> $item['version'])); + break; default: $msg = '???'; } @@ -242,6 +240,16 @@ class SeedDMS_View_ViewDocument extends SeedDMS_Theme_Style { header('Content-Type: application/javascript; charset=UTF-8'); parent::jsTranslations(array('js_form_error', 'js_form_errors', 'cancel', 'splash_move_document', 'confirm_move_document', 'move_document', 'confirm_transfer_link_document', 'transfer_content', 'link_document', 'splash_move_folder', 'confirm_move_folder', 'move_folder')); +?> + $(document).ready(function(){ + $("#filterRecipientsInput").on("keyup", function() { + var value = $(this).val().toLowerCase(); + $("#filterRecipientsTable tbody tr").filter(function() { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1) + }); + }); + }); +isAdmin()) { $latestContent = $this->callHook('documentLatestContent', $document); if($latestContent === null) @@ -369,11 +377,11 @@ $(document).ready( function() { print "
  • params['settings']->_httpRoot."op/op.ViewOnline.php?documentid=".$documentid."&file=". $file->getID()."\">" . getMLText("view_online") . "
  • "; } } - } + } else print "
  • getMimeIcon($file->getFileType())."\" title=\"".htmlspecialchars($file->getMimeType())."\">"; echo "
      "; if (($document->getAccessMode($user) == M_ALL)||($file->getUserID()==$user->getID())) { - print $this->html_link('RemoveDocumentFile', array('documentid'=>$document->getID(), 'fileid'=>$file->getID()), array(), ''.getMLText("delete"), false, true, array('
    • ', '
    • ')); - print $this->html_link('EditDocumentFile', array('documentid'=>$document->getID(), 'fileid'=>$file->getID()), array(), ''.getMLText("edit"), false, true, array('
    • ', '
    • ')); + print $this->html_link('RemoveDocumentFile', array('documentid'=>$document->getID(), 'fileid'=>$file->getID()), array(), ''.getMLText("delete"), false, false, array('
    • ', '
    • ')); + print $this->html_link('EditDocumentFile', array('documentid'=>$document->getID(), 'fileid'=>$file->getID()), array(), ''.getMLText("edit"), false, false, array('
    • ', '
    • ')); } print "
    "; @@ -388,13 +396,35 @@ $(document).ready( function() { function documentInfos() { /* {{{ */ $dms = $this->params['dms']; $user = $this->params['user']; + $settings = $this->params['settings']; $document = $this->params['document']; + $checkoutdir = $this->params['checkOutDir']; $txt = $this->callHook('documentInfos', $document); if(is_string($txt)) echo $txt; else { $this->contentHeading(htmlspecialchars($document->getName())); + $txt = $this->callHook('checkOutInfo', $document); + if(is_string($txt)) { + echo $txt; + } elseif($infos = $document->getCheckOutInfo()) { + $session = $this->params['session']; + if($session->getSu()) { + $origuser = $dms->getUser($session->getUser()); + $checkoutpath = sprintf($checkoutdir, preg_replace('/[^A-Za-z0-9_-]/', '', $origuser->getLogin())); + } else { + $origuser = $user; + $checkoutpath = sprintf($checkoutdir, preg_replace('/[^A-Za-z0-9_-]/', '', $user->getLogin())); + } + foreach($infos as $info) { + $checkoutuser = $dms->getUser($info['userID']); + $checkoutstatus = $document->checkOutStatus(); + echo ""; + } + } $txt = $this->callHook('preDocumentInfos', $document); if(is_string($txt)) echo $txt; @@ -423,14 +453,20 @@ $(document).ready( function() { getComment()) { + if($settings->_markdownComments) { + $Parsedown = new Parsedown(); + $comment = $Parsedown->text(htmlspecialchars($document->getComment())); + } else { + $comment = htmlspecialchars($document->getComment()); + } ?> : - getComment());?> +
    isAdmin() || $document->getAccessMode($user) > M_READ) { + if($document->getAccessMode($user) == M_ALL) { echo ""; echo "".getMLText('default_access').":"; echo "".$this->getAccessModeText($document->getDefaultAccess()).""; @@ -690,6 +726,9 @@ $(document).ready( function() { $accessobject = $this->params['accessobject']; $viewonlinefiletypes = $this->params['viewonlinefiletypes']; $enableownerrevapp = $this->params['enableownerrevapp']; + $enablereceiptworkflow = $this->params['enablereceiptworkflow']; + $enablereceiptreject = $this->params['enablereceiptreject']; + $enablerevisionworkflow = $this->params['enablerevisionworkflow']; $workflowmode = $this->params['workflowmode']; $previewwidthdetail = $this->params['previewWidthDetail']; @@ -769,7 +808,14 @@ $(document).ready( function() { echo $txt; } else { if($latestContent->getComment()) - print "

    ".htmlspecialchars($latestContent->getComment())."

    "; + if($this->params['settings']->_markdownComments) { + $Parsedown = new Parsedown(); + $comment = $Parsedown->text(htmlspecialchars($latestContent->getComment())); + print "
    ".$comment."
    "; + } else { + $comment = htmlspecialchars($latestContent->getComment()); + print "

    ".$comment."

    "; + } } print "
      \n"; $this->printVersionAttributes($folder, $latestContent); @@ -796,38 +842,51 @@ $(document).ready( function() { $items = array(); if ($file_exists){ - if($islatest && $accessobject->mayEditVersion()) { + if($islatest && $accessobject->mayEditVersion($latestContent->getDocument())) { $items[] = array('link'=>$this->html_url('EditOnline', array('documentid'=>$latestContent->getDocument()->getId(), 'version'=>$latestContent->getVersion())), 'icon'=>'edit', 'label'=>'edit_version'); } } /* Only admin has the right to remove version in any case or a regular * user if enableVersionDeletion is on */ - if($accessobject->mayRemoveVersion()) { + if($accessobject->mayRemoveVersion($latestContent->getDocument())) { $items[] = array('link'=>$this->html_url('RemoveVersion', array('documentid'=>$latestContent->getDocument()->getId(),'version'=>$latestContent->getVersion())), 'icon'=>'remove', 'label'=>'rm_version'); } - if($islatest && $accessobject->mayOverwriteStatus()) { + if($islatest && $accessobject->mayOverrideStatus($latestContent->getDocument())) { $items[] = array('link'=>$this->html_url('OverrideContentStatus', array('documentid'=>$latestContent->getDocument()->getId(),'version'=>$latestContent->getVersion())), 'icon'=>'align-justify', 'label'=>'change_status'); } + if($islatest && $enablereceiptworkflow && $accessobject->check_controller_access('SetRecipients')) + if($accessobject->maySetRecipients($latestContent->getDocument())) { + $items[] = array('link'=>"../out/out.SetRecipients.php?documentid=".$latestContent->getDocument()->getId()."&version=".$latestContent->getVersion(), 'icon'=>'check', 'label'=>'change_recipients'); + } + if($islatest && $enablerevisionworkflow && $accessobject->check_controller_access('SetRevisors')) + if($accessobject->maySetRevisors($latestContent->getDocument())) { + $items[] = array('link'=>$this->params['settings']->_httpRoot."out/out.SetRevisors.php?documentid=".$latestContent->getDocument()->getId()."&version=".$latestContent->getVersion(), 'icon'=>'refresh', 'label'=>'change_revisors'); + } if($workflowmode == 'traditional' || $workflowmode == 'traditional_only_approval') { // Allow changing reviewers/approvals only if not reviewed - if($accessobject->maySetReviewersApprovers()) { + if($accessobject->maySetReviewersApprovers($latestContent->getDocument())) { $items[] = array('link'=>$this->html_url('SetReviewersApprovers', array('documentid'=>$latestContent->getDocument()->getId(),'version'=>$latestContent->getVersion())), 'icon'=>'edit', 'label'=>'change_assignments'); } } elseif($workflowmode == 'advanced') { - if($accessobject->maySetWorkflow()) { + if($accessobject->maySetWorkflow($latestContent->getDocument())) { $workflow = $latestContent->getWorkflow(); if(!$workflow) { $items[] = array('link'=>$this->html_url('SetWorkflow', array('documentid'=>$latestContent->getDocument()->getId(),'version'=>$latestContent->getVersion())), 'icon'=>'random', 'label'=>'set_workflow'); } } } + if($accessobject->check_controller_access('AddToTransmittal')) + if($dms->getAllTransmittals($user)) { + if($accessobject->check_view_access('AddToTransmittal')) + $items[] = array('link'=>"out.AddToTransmittal.php?documentid=".$latestContent->getDocument()->getId()."&version=".$latestContent->getVersion(), 'icon'=>'list', 'label'=>'add_to_transmittal'); + } if($accessobject->check_view_access('EditComment')) - if($accessobject->mayEditComment()) { + if($accessobject->mayEditComment($latestContent->getDocument())) { $items[] = array('link'=>$this->html_url('EditComment', array('documentid'=>$latestContent->getDocument()->getId(),'version'=>$latestContent->getVersion())), 'icon'=>'comment', 'label'=>'edit_comment'); } if($accessobject->check_view_access('EditAttributes')) - if($accessobject->mayEditAttributes()) { + if($accessobject->mayEditAttributes($latestContent->getDocument())) { $items[] = array('link'=>$this->html_url('EditAttributes', array('documentid'=>$latestContent->getDocument()->getId(),'version'=>$latestContent->getVersion())), 'icon'=>'edit', 'label'=>'edit_attributes'); } if(!$islatest) @@ -859,6 +918,10 @@ $(document).ready( function() { $enableDropUpload = $this->params['enableDropUpload']; $enableownerrevapp = $this->params['enableownerrevapp']; $enableremoverevapp = $this->params['enableremoverevapp']; + $enableownerreceipt = $this->params['enableownerreceipt']; + $enablereceiptworkflow = $this->params['enablereceiptworkflow']; + $enablereceiptreject = $this->params['enablereceiptreject']; + $enablerevisionworkflow = $this->params['enablerevisionworkflow']; $workflowmode = $this->params['workflowmode']; $cachedir = $this->params['cachedir']; $conversionmgr = $this->params['conversionmgr']; @@ -933,6 +996,8 @@ $(document).ready( function() { $reviewStatus = $latestContent->getReviewStatus(); $approvalStatus = $latestContent->getApprovalStatus(); + $receiptStatus = $latestContent->getReceiptStatus(); + $revisionStatus = $latestContent->getRevisionStatus(); $this->rowStart(); $this->columnStart(4); @@ -952,7 +1017,7 @@ $(document).ready( function() { ?> \n"; echo "\n"; @@ -952,7 +1012,7 @@ background-image: linear-gradient(to bottom, #882222, #111111);; $accessobject = $this->params['accessobject']; $menuitems = array(); - if (!$this->params['user']->isGuest()) + if($accessobject->check_view_access(array('AddEvent'))) $menuitems['addevent'] = array('link'=>$this->params['settings']->_httpRoot."out/out.AddEvent.php", 'label'=>getMLText('add_event')); /* Do not use $this->callHook() because $menuitems must be returned by the the @@ -1108,17 +1168,21 @@ background-image: linear-gradient(to bottom, #882222, #111111);; } elseif(is_array($value)) { switch($value['element']) { case 'select': + $allowempty = empty($value['allow_empty']) ? false : $value['allow_empty']; echo '"; if(isset($value['options']) && is_array($value['options'])) { + if($allowempty) + echo ""; foreach($value['options'] as $val) { if(is_string($val)) { echo ''; @@ -1210,6 +1274,12 @@ background-image: linear-gradient(to bottom, #882222, #111111);; case 'danger': $class = 'btn-danger'; break; + case 'secondary': + $class = 'btn-secondary'; + break; + case 'neutral': + $class = ''; + break; case 'primary': default: $class = 'btn-primary'; @@ -1342,6 +1412,15 @@ function getOverallStatusIcon($status) { /* {{{ */ case S_EXPIRED: $icon = 'fa-circle expired'; break; + case S_IN_REVISION: + $icon = 'fa-refresh'; + break; + case S_DRAFT: + $icon = 'fa-circle-o'; + break; + case S_NEEDS_CORRECTION: + $icon = 'fa-circle in-workflow'; + break; default: $icon = 'fa fa-question'; break; @@ -1361,8 +1440,10 @@ function getOverallStatusIcon($status) { /* {{{ */ function getModalBoxLinkAttributes($config) { /* {{{ */ $attrs = array(); $attrs[] = array('data-target', '#'.$config['target']); - if(isset($config['remote'])) + if(isset($config['remote'])) { $attrs[] = array('href', $config['remote']); + $attrs[] = array('data-remote', $config['remote']); + } $attrs[] = array('data-toggle', 'modal'); $attrs[] = array('role', 'button'); if(isset($config['class'])) { @@ -1513,17 +1594,19 @@ $(document).ready(function() { echo self::getDateChooser($defDate, $varName, $lang, $dateformat, $startdate, $enddate, $weekstart); } /* }}} */ - function getDateChooser($defDate = '', $varName, $lang='', $dateformat='', $startdate='', $enddate='', $weekstart=null) { /* {{{ */ + function getDateChooser($defDate = '', $varName, $lang='', $dateformat='', $startdate='', $enddate='', $weekstart=null, $placeholder='', $nogroup=false) { /* {{{ */ if(!$dateformat) $dateformat = getConvertDateFormat(); - $content = ' -
      - -
      - -
      -
      -'; + $content = ''; + if(!$nogroup) + $content .= '
      '; + $content .= ''; + if(!$nogroup) { + $content .= '
      + +
      '; + $content .= '
      '; + } return $content; } /* }}} */ @@ -1597,12 +1680,15 @@ $(document).ready(function() { $content .= ""; return $content; } /* }}} */ - - function getDocumentChooserHtml($form, $default=false, $formname='', $skiptree=false) { /* {{{ */ + + function getDocumentChooserHtml($form, $accessMode=M_READ, $exclude = -1, $default = false, $formname = '', $folder='', $partialtree=0, $skiptree=false) { /* {{{ */ if(!$formname) $formname = "docid"; $formid = md5($formname.$form); - + if(!$folder) + $folderid = $this->params['dms']->getRootFolder()->getId(); + else + $folderid = $folder->getID(); $content = ''; $content .= "getID() : "") ."\">"; $content .= "
      \n"; @@ -1613,7 +1699,7 @@ $(document).ready(function() { $content .= $this->getModalBoxLink( array( 'target' => 'docChooser'.$formid, - 'remote' => $this->params['settings']->_httpRoot."out/out.DocumentChooser.php?form=".$formid."&folderid=".$this->params['dms']->getRootFolder()->getId(), + 'remote' => $this->params['settings']->_httpRoot."out/out.DocumentChooser.php?form=".$formid."&folderid=".$folderid."&partialtree=".$partialtree, 'class' => 'btn btn-secondary', 'title' => getMLText('document').'…' )); @@ -1631,8 +1717,8 @@ $(document).ready(function() { return $content; } /* }}} */ - function printDocumentChooserHtml($formName) { /* {{{ */ - echo self::getDocumentChooserHtml($formName); + function printDocumentChooserHtml($form, $accessMode=M_READ, $exclude = -1, $default = false, $formname = '', $folder='', $partialtree=0) { /* {{{ */ + echo self::getDocumentChooserHtml($form, $accessMode, $exclude, $default, $formname, $folder, $partialtree); } /* }}} */ /** @@ -1655,8 +1741,8 @@ function folderSelected(id, name) { printDocumentChooserHtml($form); + function printDocumentChooser($form, $accessMode=M_READ, $exclude = -1, $default = false, $formname = '', $folder='', $partialtree=0) { /* {{{ */ + $this->printDocumentChooserHtml($form, $accessMode, $exclude, $default, $formname, $folder, $partialtree); ?>