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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 52 additions & 30 deletions action/banner.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@
*/
class action_plugin_structpublish_banner extends DokuWiki_Action_Plugin
{
/** @var \helper_plugin_structpublish_db */
/**
* @var \helper_plugin_structpublish_db
*/
protected $dbHelper;

/** @var bool */
/**
* @var bool
*/
protected $compactView;

/** @inheritDoc */
/**
* @inheritDoc
*/
public function register(Doku_Event_Handler $controller)
{
$controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'renderBanner');
Expand Down Expand Up @@ -68,11 +74,10 @@ public function renderBanner(Doku_Event $event)
$banner .= $this->getBannerText('previous_publish', $prevpubRevision, $shownRevision->getRev());
}

// link to newest draft, if exists, is not shown already and user has a role
if (
$newestRevision->getRev() != $shownRevision->getRev() &&
$newestRevision->getStatus() != Constants::STATUS_PUBLISHED &&
$this->dbHelper->checkAccess($ID)
// 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.

) {
$banner .= $this->getBannerText('latest_draft', $newestRevision, $shownRevision->getRev());
}
Expand All @@ -92,8 +97,8 @@ public function renderBanner(Doku_Event $event)
/**
* Fills place holder texts with data from the given Revision
*
* @param string $name
* @param Revision $rev
* @param string $name
* @param Revision $rev
* @return string
*/
protected function getBannerText($name, $rev, $diff = '')
Expand Down Expand Up @@ -128,9 +133,9 @@ protected function getBannerText($name, $rev, $diff = '')
/**
* Create a HTML link to a specific revision
*
* @param string $id page id
* @param int $rev revision to link to
* @param int $text the link text to use
* @param string $id page id
* @param int $rev revision to link to
* @param int $text the link text to use
* @return string
*/
protected function makeLink($id, $rev, $text)
Expand All @@ -142,44 +147,61 @@ protected function makeLink($id, $rev, $text)
/**
* Create the form for approval and publishing
*
* @param string $status current status
* @param string $newVersion suggested new Version
* @param string $status current status
* @param string $newVersion suggested new Version
* @return string
*/
protected function actionButtons($status, $newVersion)
{

global $ID;

// If the status is published, return an empty string
if ($status === Constants::STATUS_PUBLISHED) {
return '';
}

// Create a new form instance
$form = new dokuwiki\Form\Form();

if (
$status !== Constants::STATUS_APPROVED &&
$this->dbHelper->checkAccess($ID, [Constants::ACTION_APPROVE])

if ($status !== Constants::STATUS_APPROVED
&& $this->dbHelper->checkAccess($ID, [Constants::ACTION_APPROVE])
) {
$form->addButton(
'structpublish[' . Constants::ACTION_APPROVE . ']',
$this->getLang('action_' . Constants::ACTION_APPROVE)
)->attr('type', 'submit');
$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

}

if ($this->dbHelper->checkAccess($ID, [Constants::ACTION_PUBLISH])) {
$form->addTextInput('version', $this->getLang('newversion'))->val($newVersion);
$form->addButton(
'structpublish[' . Constants::ACTION_PUBLISH . ']',
$this->getLang('action_' . Constants::ACTION_PUBLISH)
)->attr('type', 'submit');
}
// Add the publish button only if the status is approved and the user has access
if ((bool)$this->getConf('publish_needs_approve')) {
if ($status === Constants::STATUS_APPROVED && $this->dbHelper->checkAccess($ID, [Constants::ACTION_PUBLISH])) {
$form->addTextInput('version', $this->getLang('newversion'))->val($newVersion);
$form->addButton(
'structpublish[' . Constants::ACTION_PUBLISH . ']',
$this->getLang('action_' . Constants::ACTION_PUBLISH)
)->attr('type', 'submit');
}
} else {
if ($this->dbHelper->checkAccess($ID, [Constants::ACTION_PUBLISH])) {
$form->addTextInput('version', $this->getLang('newversion'))->val($newVersion);
$form->addButton(
'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


}
// Return the HTML representation of the form
return $form->toHTML();
}


/**
* Tries to increase a given version
*
* @param string $version
* @param string $version
* @return string
*/
protected function increaseVersion($version)
Expand Down
125 changes: 125 additions & 0 deletions action/dw2pdf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

use dokuwiki\plugin\structpublish\meta\Constants;
use dokuwiki\plugin\structpublish\meta\Revision;

/**
* Action component providing (localized) dw2pdf replacements
*
* @STATUS@ draft / approved / published
* @APPROVER@ user that approved the draft
* @APPROVALDATE@ date of approval
* @PUBLISHER@ user that published the page
* @PUBLISHDATE@ date of publishing
* @VERSION@ shown version
* @REVISION@ shown revision
* @LATESTPUBLISHEDVERSION@ latest published version
* @LATESTPUBLISHEDREVISION@ latest published revision
*
* @author Josquin Dehaene <jo@foobarjo.org>
* @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
*/

class action_plugin_structpublish_dw2pdf extends DokuWiki_Action_Plugin
{
/**
* @var \helper_plugin_structpublish_db
*/
protected $dbHelper;

public function register(Doku_Event_Handler $controller)
{
$controller->register_hook('PLUGIN_DW2PDF_REPLACE', 'BEFORE', $this, 'provide_structpublish_replacements');
$controller->register_hook('PLUGIN_DW2PDF_REPLACE', 'AFTER', $this, 'clean_structpublish_replacements');
}

/**
* Provide StructPublish values for DW2PDF replacements
*/
public function provide_structpublish_replacements(Doku_Event $event)
{
global $ID;
global $INFO;
global $REV;

//force reload of the globals. usefull when coming from bookcreator
$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.


$this->dbHelper = plugin_load('helper', 'structpublish_db');

if (!$this->dbHelper->isPublishable()) {
return;
}

// get revisions
$newestRevision = new Revision($ID, $INFO['currentrev']);
if ($REV) {
$shownRevision = new Revision($ID, $REV);
} else {
$shownRevision = $newestRevision;
}
$latestpubRevision = $newestRevision->getLatestPublishedRevision();
$prevpubRevision = $shownRevision->getLatestPublishedRevision($REV ?: $INFO['currentrev']);
$prevapprovedRevision = $shownRevision->getLatestApprovedRevision($REV ?: $INFO['currentrev']);

// 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

}else{
$event->data['replace']['@LATESTPUBLISHEDVERSION@'] = $this->getLang("status_na");
$event->data['replace']['@LATESTPUBLISHEDREVISION@'] = $this->getLang("status_na");;
}

// get status and revision
$event->data['replace']['@STATUS@'] = $this->getLang("status_" . $shownRevision->getStatus());
$event->data['replace']['@REVISION@'] = $shownRevision->getRev();

// status draft
if ($event->data['replace']['@STATUS@'] === $this->getLang("status_draft")) {
$event->data['replace']['@VERSION@'] = $this->getLang("status_draft");
$event->data['replace']['@APPROVER@'] = $this->getLang("status_na");
$event->data['replace']['@APPROVALDATE@'] = $this->getLang("status_na");
$event->data['replace']['@PUBLISHER@'] = $this->getLang("status_na");
$event->data['replace']['@PUBLISHDATE@'] = $this->getLang("status_na");
}

// status approved
if ($event->data['replace']['@STATUS@'] === $this->getLang("status_approved")) {
$event->data['replace']['@VERSION@'] = $this->getLang("status_approved");
$event->data['replace']['@APPROVER@'] = $shownRevision->getUser();
$event->data['replace']['@APPROVALDATE@'] = $shownRevision->getDateTime();
$event->data['replace']['@PUBLISHER@'] = $this->getLang("status_na");
$event->data['replace']['@PUBLISHDATE@'] = $this->getLang("status_na");
}

// status published
if ($event->data['replace']['@STATUS@'] === $this->getLang("status_published")) {
$event->data['replace']['@VERSION@'] = $shownRevision->getVersion();
$event->data['replace']['@APPROVER@'] = $prevapprovedRevision->getUser();
$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

$ID = $keep;
}

/**
* Clean up replacements in DW2PDF content
*/
public function clean_structpublish_replacements(Doku_Event $event)
{
$event->data['content'] = str_replace(
['@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');

}
}
12 changes: 9 additions & 3 deletions action/save.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
*/
class action_plugin_structpublish_save extends DokuWiki_Action_Plugin
{
/** @inheritDoc */
/**
* @inheritDoc
*/
public function register(Doku_Event_Handler $controller)
{
$controller->register_hook('COMMON_WIKIPAGE_SAVE', 'AFTER', $this, 'handleSave');
Expand All @@ -19,12 +21,14 @@ public function register(Doku_Event_Handler $controller)
/**
* Handle the page save event to store revision meta data
*
* @param Doku_Event $event
* @param Doku_Event $event
* @return void
*/
public function handleSave(Doku_Event $event)
{
/** @var helper_plugin_structpublish_db $dbHelper */
/**
* @var helper_plugin_structpublish_db $dbHelper
*/
$dbHelper = plugin_load('helper', 'structpublish_db');

$id = $event->data['id'];
Expand All @@ -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.


try {
$revision->save();
Expand Down
33 changes: 20 additions & 13 deletions action/show.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@

class action_plugin_structpublish_show extends DokuWiki_Action_Plugin
{
/** @var int */
/**
* @var int
*/
protected static $latestPublishedRev;

/** @inheritDoc */
/**
* @inheritDoc
*/
public function register(Doku_Event_Handler $controller)
{
$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleShow');
Expand All @@ -18,7 +22,7 @@ public function register(Doku_Event_Handler $controller)
/**
* Decide which revision to show based on role assignments
*
* @param Doku_Event $event
* @param Doku_Event $event
* @return void
*/
public function handleShow(Doku_Event $event)
Expand All @@ -31,20 +35,22 @@ public function handleShow(Doku_Event $event)
global $REV;
global $INFO;

/** @var helper_plugin_structpublish_db $dbHelper */
/**
* @var helper_plugin_structpublish_db $dbHelper
*/
$dbHelper = plugin_load('helper', 'structpublish_db');

if (
!$dbHelper->isPublishable() ||
(auth_isadmin() && !$this->getConf('restrict_admin'))
if (!$dbHelper->isPublishable()
|| (auth_isadmin() && !$this->getConf('restrict_admin'))
) {
return;
}

$currentRevision = new Revision($ID, $REV ?: $INFO['currentrev']);
$isPublished = $currentRevision->getStatus() === Constants::STATUS_PUBLISHED;

if (!$dbHelper->isPublisher($ID)) {
//Show published version or nothing to user with no Role and no ACL write
if (!$dbHelper->isPublisher($ID) && auth_quickaclcheck($ID) < 2) {
$latestPublished = $currentRevision->getLatestPublishedRevision();
// there is no published revision, show nothing
if (!$isPublished && is_null($latestPublished)) {
Expand All @@ -67,17 +73,18 @@ public function handleShow(Doku_Event $event)
* Suppress message about viewing an old revision if it is the latest one
* that the current user is allowed to see.
*
* @param Doku_Event $event
* @param Doku_Event $event
* @return void
*/
public function handleShowrev(Doku_Event $event)
{
/** @var helper_plugin_structpublish_db $dbHelper */
/**
* @var helper_plugin_structpublish_db $dbHelper
*/
$dbHelper = plugin_load('helper', 'structpublish_db');

if (
!$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.

) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions conf/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
$conf['email_enable'] = 0;
$conf['email_status'] = '';
$conf['compact_view'] = 0;
$conf['publish_needs_approve'] = 0;
1 change: 1 addition & 0 deletions conf/metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
$meta['email_enable'] = ['onoff'];
$meta['email_status'] = ['multicheckbox', '_other' => 'never', '_choices' => ['approve', 'publish']];
$meta['compact_view'] = ['onoff'];
$meta['publish_needs_approve'] = ['onoff'];
Loading