Skip to content

Add publish_needs_approve option to enforce approval before publishing #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

moka-dev
Copy link

This PR introduces a new configuration option, publish_needs_approve, that enforces approval before a page can be published. When this option is enabled, the "Publish" button will only be available if the page has been approved.

foobarjo and others added 12 commits March 13, 2025 15:40
This PR introduces a new configuration option, `publish_needs_approve`, that enforces approval before a page can be published.
When this option is enabled, the "Publish" button will only be available if the page has been approved.
…, F5, or back navigation

This PR adds a safeguard to ensure that a revision is not approved or published multiple times due to form resubmission (e.g., F5 refresh or browser back navigation).
Before committing to the database, the system now checks whether the page has already been approved or published.
This PR provide  `dw2pdf` replacements  for the `StructPublish` plugin. The following placeholders are now supported:

- `@STATUS@`        - Page status (`draft`, `approved`, `published`)
- `@APPROVER@`      - User who approved the draft
- `@APPROVALDATE@`  - Date of approval
- `@PUBLISHER@`     - User who published the page
- `@PUBLISHDATE@`   - Date of publishing
- `@VERSION@`       - Currently shown version
- `@LATESTVERSION@` - Latest published version

These replacements will be available when generating PDFs via DW2PDF, ensuring that the document reflects the current StructPublish metadata.
…f role

Behavior Changes

    Editors (users with no role) can create and modify drafts in DokuWiki sections where they have ACL write permissions.
    Readers (users with no role) can only read published versions or see nothing in sections where they lack ACL write permissions.

Changes Implemented

    Removed role-based checks before accessing struct data to achieve the expected banner behavior.
    Replaced role-based checks with ACL-based validation for access control.
    drafts in the database, storing user and timestamp
Add revision metadata to replacements

    Add the revision number of the currently shown page as @revision@

    Add the revision number of the latest published version as @LATESTVERSIONREVISION@
Avoid overriding $REV value when a specific revision is requested
Copy link
Member

@splitbrain splitbrain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I think all of this makes sense but needs a bit clean up. The unnecessary reformatting makes it hard to review and there are multiple things mixed into one PR.

I would like to see this PR replaced by multiple smaller PRs for:

  • bug fixes (one PR per fix)
  • PDF Replacements
  • Prevention of double submission
  • New Translations

All of them without reformatting existing code and with the issues addressed in my comments

// link to newest draft, if exists, if not shown already, if user has ACL write
if ($newestRevision->getRev() != $shownRevision->getRev()
&& $newestRevision->getStatus() != Constants::STATUS_PUBLISHED
&& auth_quickaclcheck($ID) >= 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the AUTH_EDIT constant, not a number.

I assume this change is what you mentioned here:

I also modified the behavior for users with no role because I believed the original behavior was a bug. In my version, I corrected it so that users without a role can still access their pages after creation.

@moka-dev Can you explain the scenario you're addressing here?

@annda can you have a look at this change from $this->dbHelper->checkAccess() to auth_quickaclcheck() and chime in with your thoughts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, a user without a publish role but with write ACL could create a new page in a StructPublish-controlled section. However, once the page was saved, the user could no longer access it, which I assume is not the intended behavior. Any user with write ACL on a section should be able to create and edit pages within it—especially the ones they created—considering that approvers and publishers may be different users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That explanation does make sense to me and I vaguely remember discussion between me and @annda about this but not the outcome. @annda I think we should discuss this and then write down what we decide on the wiki page. I think this mechanism needs to be thought through and then be implemented, tested and documented.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we have been aware of this problem for some time, see #5 and #15

I will post more in the relevant issues, since this PR will be closed anyway and split it into smaller ones.

$form->addButton(
'structpublish[' . Constants::ACTION_APPROVE . ']',
$this->getLang('action_' . Constants::ACTION_APPROVE)
)->attr('type', 'submit');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: this part seems to be the same, just reformatted

'structpublish[' . Constants::ACTION_PUBLISH . ']',
$this->getLang('action_' . Constants::ACTION_PUBLISH)
)->attr('type', 'submit');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button output is the same. Makes no sense to repeat it. The code should decide if the button is to be shown or not (based on config, access and status) and then just do it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok will change that

@@ -38,6 +42,8 @@ public function handleSave(Doku_Event $event)

$revision = new Revision($id, $event->data['newRevision']);
$revision->setStatus(Constants::STATUS_DRAFT);
$revision->setUser($_SERVER['REMOTE_USER']);
$revision->setTimestamp(time());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this about? It seems to be unrelated to this PR. Is it fixing a bug? Under which circumstances? I feel like this should be a separate PR?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It simply updates the metadata stored in the StructPublish database. Previously, the user and timestamp were available for approved and published revisions, but not for drafts, which remained anonymous.

!$dbHelper->isPublishable() ||
(auth_isadmin() && !$this->getConf('restrict_admin'))
if (!$dbHelper->isPublishable()
|| (auth_isadmin() && !$this->getConf('restrict_admin'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: there seem to be no changes here. just formatting.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I made a change on line 47. This is where we instruct StructPublish to show only the published revision of a page (or nothing) to the user, based on the ACL rather than their role. (I’ve already change the 2 for AUTH_EDIT)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$keep = $ID;
$ID = $event->data['id'];
$INFO = pageinfo();
$REV = $event->data['rev'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you changing the globals here? An action handler for the PDF Plugin should not change globals.

If that change is somehow necessary for rendering within a bookcreator process, that should probably be fixed in book creator or if that's not possible, at a proper place in dw2pdf, struct or structpublish (depending on who is at fault). But it should not be a side effect of handling PDF generation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When coming from BookCreator, $ID refers to the BookCreator page, so we cannot retrieve the metadata of the page currently being rendered. This is why we need to manually initialize the variables.


// get redactor
$pageMeta = p_get_metadata($ID);
$event->data['replace']['@REDACTOR@'] = $pageMeta['last_change']['user'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redactor is the one who approved the the change? I am not opposed to the term but I don't think we use it anywhere else. Might make sense to streamline the naming.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the variable name could be improved—for example, $EDITOR for clarity—as it refers to the last editor of the page. approver and publisher are potentially different users

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, then "editor" is the better name.

// get lastest published version & revision
if ($latestpubRevision != null) {
$event->data['replace']['@LATESTPUBLISHEDVERSION@'] = $latestpubRevision->getVersion();
$event->data['replace']['@LATESTPUBLISHEDREVISION@'] = $latestpubRevision->getRev();
Copy link
Member

@splitbrain splitbrain Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a couple of underscores for better readability, but that's nitpicking.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

$event->data['replace']['@APPROVALDATE@'] = $prevapprovedRevision->getDateTime();
$event->data['replace']['@PUBLISHER@'] = $shownRevision->getUser();
$event->data['replace']['@PUBLISHDATE@'] = $shownRevision->getDateTime();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also prefix all variables with STRUCTAPPROVE_ to avoid any accidental clashes with other plugins.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

['@APPROVER@', '@APPROVALDATE@', '@LATESTPUBLISHEDREVISION@', '@REVISION@', '@PUBLISHER@', '@PUBLISHDATE@', '@VERSION@', '@STATUS@', '@REDACTOR@' , '@LATESTPUBLISHEDVERSION@'],
['', '', '', '', '', '', '', '', '', ''],
$event->data['content']
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this handler. Why is is it necessary? Shouldn't the BEFORE handler already handle all of those?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clears the StructPublish variables to ensure they are not accidentally reused where they shouldn’t be.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it runs a second replace operation on the page content. It does not clear any variables.

What exactly is the issue you're trying to fix here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have miss something, but I was thinking this could clean up the replacements since it’s called on an AFTER hook:

$controller->register_hook('PLUGIN_DW2PDF_REPLACE', 'AFTER', $this, 'clean_structpublish_replacements');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants