Skip to content
2 changes: 1 addition & 1 deletion front/helpdesk.faq.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
if (isset($_GET["id"])) {
$kb = new KnowbaseItem();
if ($kb->getFromDB($_GET["id"])) {
$kb->showFull();
$kb->showFull(['token' => $_GET['token'] ?? ""]);
}
} else {
// Manage forcetab : non standard system (file name <> class name)
Expand Down
3 changes: 3 additions & 0 deletions install/migrations/update_10.0.x_to_11.0.0/knowbaseitem.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@
$migration->addField('glpi_knowbaseitems', 'show_in_service_catalog', 'tinyint NOT NULL DEFAULT 0', ['after' => 'view']);

$migration->addKey('glpi_knowbaseitems', 'forms_categories_id');

$migration->addField('glpi_knowbaseitems', 'allow_access_using_token', 'bool');
$migration->addField('glpi_knowbaseitems', 'token', 'text');
2 changes: 2 additions & 0 deletions install/mysql/glpi-empty.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4051,6 +4051,8 @@ CREATE TABLE `glpi_knowbaseitems` (
`name` text,
`answer` longtext,
`is_faq` tinyint NOT NULL DEFAULT '0',
`allow_access_using_token` tinyint NOT NULL DEFAULT '0',
`token` text,
`users_id` int unsigned NOT NULL DEFAULT '0',
`view` int NOT NULL DEFAULT '0',
`show_in_service_catalog` tinyint NOT NULL DEFAULT '0',
Expand Down
8 changes: 8 additions & 0 deletions src/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,14 @@ public function getDownloadLink($linked_item = null, $len = 20): string
throw new InvalidArgumentException();
} elseif ($linked_item !== null) {
$link_params = sprintf('&itemtype=%s&items_id=%s', $linked_item::class, $linked_item->getID());
if (
$linked_item instanceof KnowbaseItem
&& $linked_item->fields["allow_access_using_token"]
&& isset($_GET['token'])
&& hash_equals($linked_item->fields["token"], $_GET['token'])
) {
$link_params .= sprintf('&token=%s', $_GET['token']);
}
}

$splitter = $this->fields['filename'] !== null ? explode("/", $this->fields['filename']) : [];
Expand Down
62 changes: 52 additions & 10 deletions src/KnowbaseItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ public function canViewItem(): bool
return true;
}

if (
$this->fields["allow_access_using_token"]
&& isset($_GET['token'])
&& hash_equals($this->fields["token"], $_GET['token'])
Comment on lines +143 to +144
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 prefer to not have to rely on $_GET here. I do not have time right now to check if we can change this, but this is clearly I want to check before validating this PR.

) {
return true;
}

if ($this->fields["is_faq"]) {
return ((Session::haveRightsOr(self::$rightname, [READ, self::READFAQ])
&& $this->haveVisibilityAccess())
Expand Down Expand Up @@ -709,10 +717,7 @@ private static function getVisibilityCriteriaKB_Entity(): array

public function prepareInputForAdd($input)
{
// set title for question if empty
if (isset($input["name"]) && empty($input["name"])) {
$input["name"] = __('New item');
}
$input = self::prepareInputForUpdate($input);

if (
Session::haveRight(self::$rightname, self::PUBLISHFAQ)
Expand All @@ -735,6 +740,20 @@ public function prepareInputForUpdate($input)
if (isset($input["name"]) && empty($input["name"])) {
$input["name"] = __('New item');
}

if (
(
isset($input['allow_access_using_token'])
&& $input['allow_access_using_token']
&& empty($this->fields['token'])
) || (
isset($input['_regenerate_token'])
&& $input['_regenerate_token']
)
) {
$input['token'] = Toolbox::getRandomString(20);
}

return $input;
}

Expand Down Expand Up @@ -872,15 +891,31 @@ public function showFull($options = [])
{
global $CFG_GLPI, $DB;

if (!$this->can($this->fields['id'], READ)) {
return false;
}

$default_options = [
'display' => true,
'token' => '',
];
$options = array_merge($default_options, $options);

if (
$this->fields['allow_access_using_token']
&& hash_equals($this->fields['token'], $options['token'])
) {
$answer = preg_replace(
[
'/<img([^>]+src=["\'])([^"\']*\/front\/document\.send\.php[^"\']*)(["\'][^>]*)>/',
'/<a([^>]+href=["\'])([^"\']*\/front\/document\.send\.php[^"\']*)(["\'][^>]*)>/',
],
[
'<img$1$2&token=' . $options['token'] . '$3>',
'<a$1$2&token=' . $options['token'] . '$3>',
],
$this->getAnswer()
);
} elseif (!$this->can($this->fields['id'], READ)) {
return false;
}

$linkusers_id = true;
if (
((Session::getLoginUserID() === false) && $CFG_GLPI["use_public_faq"])
Expand Down Expand Up @@ -916,7 +951,7 @@ public function showFull($options = [])
$downloadlink = htmlescape(NOT_AVAILABLE);

if ($document->getFromDB($docID)) {
$downloadlink = $document->getDownloadLink();
$downloadlink = $document->getDownloadLink($this);
}

if (!isset($heading_names[$data["documentcategories_id"]])) {
Expand Down Expand Up @@ -944,7 +979,7 @@ public function showFull($options = [])
'item' => $this,
'categories' => $article_categories,
'subject' => KnowbaseItemTranslation::getTranslatedValue($this, 'name'),
'answer' => $this->getAnswer(),
'answer' => $answer ?? $this->getAnswer(),
'attachments' => $attachments,
'writer_link' => $writer_link,
]);
Expand Down Expand Up @@ -1090,6 +1125,13 @@ public static function getListRequest(array $params, $type = 'search')
];
}

if (isset($_GET['token'])) {
$criteria['WHERE']['OR'][] = [
'glpi_knowbaseitems.allow_access_using_token' => 1,
'glpi_knowbaseitems.token' => $_GET['token'],
];
}

if ($params['knowbaseitemcategories_id'] !== KnowbaseItemCategory::SEEALL) {
$criteria['LEFT JOIN'][KnowbaseItem_KnowbaseItemCategory::getTable()] = [
'FKEY' => [
Expand Down
7 changes: 7 additions & 0 deletions templates/components/form/fields_macros.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@
})) }}
{% endset %}

{% if options.can_regenerate %}
{% set regenerate_chk %}
<input type="checkbox" name="_regenerate_{{ name }}" id="_regenerate_{{ name }}">&nbsp;<label for="_regenerate_{{ name }}">{{ __('Regenerate') }}</label>
{% endset %}
{% set field = field ~ regenerate_chk %}
{% endif %}

{{ _self.field(name, field, label, options) }}
{% endmacro %}

Expand Down
8 changes: 8 additions & 0 deletions templates/pages/tools/kb/article.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@
{{ item.fields['is_faq'] ? __('This item is part of the FAQ') : __('This item is not part of the FAQ') }}
</div>
</div>
{% if item.fields['allow_access_using_token'] %}
<div class="d-flex row">
<div class="col-sm-6 col-12">
{% set token_url = config('url_base') ~ '/front/helpdesk.faq.php?id=' ~ item.fields['id'] ~ '&token=' ~ item.fields['token'] %}
{{ __('Unique URL') }}: <a href="{{ token_url }}">{{ token_url }}</a>
</div>
</div>
{% endif %}
{{ include('components/form/dates.html.twig') }}
</div>
</div>
13 changes: 13 additions & 0 deletions templates/pages/tools/kb/knowbaseitem.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@
{{ fields.nullField() }}
{% endif %}

{{ fields.dropdownYesNo('allow_access_using_token',
item.fields['allow_access_using_token'],
__('Share with a unique URL')) }}
{% if item.fields['allow_access_using_token'] %}
{% set token_url = config('url_base') ~ '/front/helpdesk.faq.php?id=' ~ item.fields['id'] ~ '&token=' ~ item.fields['token'] %}
{{ fields.urlField('token', token_url, __('Unique URL'), {
readonly: true,
full_width: true,
can_regenerate: true,
is_copyable: true
}) }}
{% endif %}

{% if linked_item is defined and linked_item is not null and not linked_item.isNewItem() %}
{{ inputs.hidden('_itemtype', get_class(linked_item)) }}
{{ inputs.hidden('_items_id', linked_item.getID()) }}
Expand Down
39 changes: 39 additions & 0 deletions tests/functional/KnowbaseItemTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1748,4 +1748,43 @@ public function testVisibilityRestrictionsInSearch()
]);
$this->assertTrue($fn_can_tech_see_kb());
}

public function testAllowAccessUsingToken()
{
$kbi = $this->createItem(
'KnowbaseItem',
[
'name' => __METHOD__,
'answer' => __METHOD__,
'is_faq' => false,
'entities_id' => 0,
'is_recursive' => 1,
'allow_access_using_token' => true,
],
);

$token = $kbi->getField('token');

// Missing token
$this->assertFalse($kbi->canViewItem());

// Good token
$_GET['token'] = $token;
$this->assertTrue($kbi->canViewItem());

// Wrong token
$_GET['token'] = "wrong_token_value";
$this->assertFalse($kbi->canViewItem());

// allow_access_using_token disabled
$kbi = $this->updateItem(
'KnowbaseItem',
$kbi->getID(),
[
'allow_access_using_token' => false,
]
);
$_GET['token'] = $token;
$this->assertFalse($kbi->canViewItem());
}
}
Loading