[TikiWiki-commits] [Git][tikiwiki/tiki][29.x] [ENH][FIX] Trackers: Parse wiki link syntax in URL field output (optional)
"Sammy Ndabo \(@ndabosam084\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <69a14c6613b5a_3b187928309c@gitlab-sidekiq-low-urgency-cpu-bound-v2-7f6f7b97c5-v4z4k.mail> |
Sammy Ndabo pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki
Commits:
e60185cd by Sammy Ndabo at 2026-02-27T07:41:08+00:00
[ENH][FIX] Trackers: Parse wiki link syntax in URL field output (optional)
---
* [ENH][FIX] Trackers: Parse wiki link syntax in URL field output (optional)
---
* [FIX] Url.php: simplify url field validation and client-side checks
* [ENH] Url.php: normalize stored value to url before rendering options
* [FIX] Url.php: include resolved href in invalid wiki-link URL error
* [FIX] Url.php: php lint issue
* [FIX] URL field: normalize wiki-link values to resolved href in CSV output
* [ENH] URL field: rename wiki-wrapper validation data attributes for clearer intent
* [ENH] URL field: align frontend wiki-link validation with backend
* [ENH] trackerFieldUrl: implement wiki syntax validation and feedback in URL input
* [ENH] Url.php: enhance URL validation and add wiki link handling; update url.tpl for wiki support syntax warnings
* [ENH] Url.php: add validation for wiki link syntax in URL fields
* [ENH] trackerFieldUrl: document limited wiki-syntax handling in tracker field URL field and add unit tests
* [REM] Url trackerfield: remove pref tracker_url_parse_wiki
* [ENH] tracker: rename and add tracker_url_parse_wiki pref to admin tpl
* [FIX] Url.php: fix php lints issues
* [ENH] tracker: Add preference to enable parsing of wiki syntax in URL fields
* [FIX] tracker: Parse wiki link syntax in URL field output
See merge request tikiwiki/tiki!9650
(cherry picked from commit 71ce39ba9ee39ebd1848c94925bb7e6d6e4009c1)
See merge request tikiwiki/tiki!9654
- - - - -
5 changed files:
- lib/core/Tracker/Field/Url.php
- lib/jquery_tiki/tiki-trackers.js
- lib/parser/parserlib.php
- + lib/test/core/Tracker/Field/UrlTest.php
- templates/trackerinput/url.tpl
Changes:
=====================================
lib/core/Tracker/Field/Url.php
=====================================
@@ -74,9 +74,9 @@ class Tracker_Field_Url extends \Tracker\Field\AbstractItemField implements \Tra
{
$smarty = TikiLib::lib('smarty');
- $url = $this->getConfiguration('value');
+ $url = self::normalizeStoredValueToUrl((string) $this->getConfiguration('value'));
- if (empty($url) || $context['list_mode'] == 'csv' || $this->getOption('linkToURL') == 1) {
+ if ($url === '' || ($context['list_mode'] ?? '') === 'csv' || $this->getOption('linkToURL') == 1) {
return $url;
} elseif ($this->getOption('linkToURL') == 2) { // Site title as link
return smarty_function_object_link(
@@ -117,9 +117,52 @@ class Tracker_Field_Url extends \Tracker\Field\AbstractItemField implements \Tra
}
}
+ protected static function isWikiSyntaxLink(string $value): bool
+ {
+ return (str_starts_with($value, '((') && str_ends_with($value, '))'))
+ || (str_starts_with($value, '[') && str_ends_with($value, ']'));
+ }
+
+ public function isValid($ins_fields_data)
+ {
+ $fieldId = $this->getFieldId();
+ $value = $ins_fields_data[$fieldId]['value'] ?? $this->getValue();
+ $trimmed = trim((string) $value);
+
+ if ($trimmed === '') {
+ return true;
+ }
+
+ if (self::isWikiSyntaxLink($trimmed)) {
+ $resolvedHref = self::extractFirstHrefFromParsedWikiLink($trimmed);
+ if ($resolvedHref === null) {
+ return tr('Invalid wiki syntax. The link target could not be resolved.');
+ }
+ if (! self::isSyntacticallyValidUrl($resolvedHref)) {
+ return tr('Invalid wiki syntax. The resolved link target "%0" is not a valid URL.', $resolvedHref);
+ }
+ // Non-blocking warning for internal non existing yet wiki page target
+ $target = self::extractWikiLinkTarget($trimmed);
+ if ($target !== null && ! self::looksLikeExternalUrl($target) && ! TikiLib::lib('tiki')->page_exists($target)) {
+ Feedback::warning(tr('Warning: Target wiki page "%0" does not exist yet.', $target));
+ }
+ return true;
+ }
+
+ if (! self::isSyntacticallyValidUrl($trimmed)) {
+ return tr('Invalid URL syntax.');
+ }
+
+ return true;
+ }
+
public function renderInput($context = [])
{
- return $this->renderTemplate("trackerinput/url.tpl", $context);
+ $templateData = [
+ 'wikiSyntaxInfo' => tr('You can also use complete wiki-link syntax: ((PageName)) or [url|text].'),
+ ];
+
+ return $this->renderTemplate("trackerinput/url.tpl", $context, $templateData);
}
public function importRemote($value)
@@ -155,4 +198,86 @@ class Tracker_Field_Url extends \Tracker\Field\AbstractItemField implements \Tra
return $schema;
}
+
+ // Keep wiki-syntax handling intentionally limited to full-value wrappers like ((PageName)) or [url|text].
+ // For full wiki parsing consistency (escaping, multilingual behavior, shared parsing path),
+ // consider refactoring URL to inherit Tracker_Field_Text.
+ protected static function normalizeStoredValueToUrl(string $value): string
+ {
+ $trimmed = trim($value);
+
+ if (! self::isWikiSyntaxLink($trimmed)) {
+ return $value;
+ }
+
+ $resolvedHref = self::extractFirstHrefFromParsedWikiLink($trimmed);
+ return $resolvedHref ?? $value;
+ }
+
+ protected static function extractFirstHrefFromParsedWikiLink(string $value): ?string
+ {
+ $parsed = TikiLib::lib('parser')->parse_data_simple($value);
+
+ if (! preg_match('/<a\b[^>]*\bhref=(["\'])(.*?)\1/i', $parsed, $matches)) {
+ return null;
+ }
+
+ return html_entity_decode($matches[2], ENT_QUOTES, 'UTF-8');
+ }
+
+ protected static function isSyntacticallyValidUrl(string $url): bool
+ {
+ if ($url === '') {
+ return true;
+ }
+
+ if (filter_var($url, FILTER_VALIDATE_URL)) {
+ return true;
+ }
+
+ if (str_starts_with($url, '/')) {
+ return ! preg_match('/\s/', $url);
+ }
+
+ $parsed = parse_url($url);
+ if ($parsed === false) {
+ return false;
+ }
+
+ if (isset($parsed['scheme'])) {
+ return (bool) preg_match('/^[a-z][a-z0-9+.-]*$/i', $parsed['scheme']) && ! preg_match('/\s/', $url);
+ }
+
+ return ! preg_match('/\s/', $url);
+ }
+
+ protected static function extractWikiLinkTarget(string $value): ?string
+ {
+ if (str_starts_with($value, '((') && str_ends_with($value, '))')) {
+ $inside = trim(substr($value, 2, -2));
+ if ($inside === '') {
+ return null;
+ }
+
+ $parts = preg_split('/[|#]/', $inside, 2);
+ return trim($parts[0] ?? '');
+ }
+
+ if (str_starts_with($value, '[') && str_ends_with($value, ']')) {
+ $inside = trim(substr($value, 1, -1));
+ if ($inside === '') {
+ return null;
+ }
+
+ $parts = explode('|', $inside, 2);
+ return trim($parts[0]);
+ }
+
+ return null;
+ }
+
+ protected static function looksLikeExternalUrl(string $value): bool
+ {
+ return (bool) preg_match('/^(https?:\/\/|ftp:\/\/|mailto:|news:)/i', $value);
+ }
}
=====================================
lib/jquery_tiki/tiki-trackers.js
=====================================
@@ -591,6 +591,89 @@
});
// Global tracker field functions
+ function updateTrackerFormSubmitState($input) {
+ const $form = $input.closest('form');
+ if (! $form.length) {
+ return;
+ }
+
+ const hasInvalidUrlSyntax = $form.find('input[data-url-wiki-wrapper-validation-input]').filter(function () {
+ return !! $(this).data('urlWikiSyntaxInvalid');
+ }).length > 0;
+
+ $form.find('input[type="submit"], button[type="submit"], .item-submit-btn').prop('disabled', hasInvalidUrlSyntax);
+ $form.closest('.modal').find('.modal-footer .auto-btn').prop('disabled', hasInvalidUrlSyntax);
+ }
+
+ function validateUrlInput($input) {
+ const rawValue = ($input.val() || '');
+ const value = rawValue.trim();
+
+ const feedbackId = $input.data('url-wiki-wrapper-feedback-id');
+ const $feedback = feedbackId ? $('#' + feedbackId) : $();
+
+ if (! $feedback.length) {
+ return;
+ }
+
+ const defaultInfo = $feedback.data('default-info') || '';
+
+ if (value === '') {
+ $feedback
+ .removeClass('text-warning')
+ .addClass('text-muted')
+ .text(defaultInfo);
+ $input.data('urlWikiSyntaxInvalid', false);
+ updateTrackerFormSubmitState($input);
+ return;
+ }
+
+ const isWrappedDouble = value.startsWith('((') && value.endsWith('))');
+ const isWrappedBracket = value.startsWith('[') && value.endsWith(']');
+ const isWikiWrapped = isWrappedDouble || isWrappedBracket;
+ let message = '';
+
+ if (! isWikiWrapped) {
+ try {
+ new URL(value, window.location.origin);
+ } catch (err) {
+ message = tr('Invalid URL syntax.');
+ }
+ }
+
+ if (message) {
+ $feedback
+ .removeClass('text-muted')
+ .addClass('text-warning')
+ .text(message);
+ $input.data('urlWikiSyntaxInvalid', true);
+ } else {
+ $feedback
+ .removeClass('text-warning')
+ .addClass('text-muted')
+ .text(defaultInfo);
+ $input.data('urlWikiSyntaxInvalid', false);
+ }
+
+ updateTrackerFormSubmitState($input);
+ }
+
+ $(document).on('input blur', 'input[data-url-wiki-wrapper-validation-input]', function () {
+ validateUrlInput($(this));
+ });
+
+ $(document).on('tiki.modal.redraw', '.modal.fade', function () {
+ $(this).find('input[data-url-wiki-wrapper-validation-input]').each(function () {
+ validateUrlInput($(this));
+ });
+ });
+
+ $(function () {
+ $('input[data-url-wiki-wrapper-validation-input]').each(function () {
+ validateUrlInput($(this));
+ });
+ });
+
$(document).on('mouseenter', '.currency_output', function(){
$('.'+$(this).attr('id')).removeClass('d-none');
});
=====================================
lib/parser/parserlib.php
=====================================
@@ -1510,6 +1510,8 @@ class ParserLib extends TikiDb_Bridge
/** Simpler and faster parse than parse_data()
* This is only called from the parse Smarty modifier, for preference definitions.
+ * Also called in Url.php when parsing the title of a page, to allow wikilinks in titles, but without
+ * allowing plugins or other complex syntax.
*/
public function parse_data_simple($data)
{
=====================================
lib/test/core/Tracker/Field/UrlTest.php
=====================================
@@ -0,0 +1,57 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+namespace TikiTests;
+
+class TrackerFieldUrlTest extends \PHPUnit\Framework\TestCase
+{
+ /**
+ * @dataProvider supportedWikiSyntaxProvider
+ */
+ public function testIsWikiSyntaxLinkSupportedInputs(string $value): void
+ {
+ $this->assertTrue($this->invokeIsWikiSyntaxLink(trim($value)));
+ }
+
+ /**
+ * @dataProvider unsupportedWikiSyntaxProvider
+ */
+ public function testIsWikiSyntaxLinkUnsupportedInputs(string $value): void
+ {
+ $this->assertFalse($this->invokeIsWikiSyntaxLink(trim($value)));
+ }
+
+ public static function supportedWikiSyntaxProvider(): array
+ {
+ return [
+ 'wikilink' => ['((PageName))'],
+ 'wikilink with spaces around input' => [' ((PageName)) '],
+ 'external link with label' => ['[https://example.org|Example]'],
+ 'external link with spaces around input' => [' [https://example.org|Example] '],
+ ];
+ }
+
+ public static function unsupportedWikiSyntaxProvider(): array
+ {
+ return [
+ 'plain url' => ['https://example.org'],
+ 'single parenthesis syntax is unsupported' => ['(PageName)'],
+ 'broken wikilink prefix only' => ['((PageName'],
+ 'broken bracket syntax suffix only' => ['https://example.org|Example]'],
+ 'escaped bracket syntax is out of scope' => ['\[https://example.org|Example]'],
+ 'mixed content around syntax is out of scope' => ['prefix ((PageName)) suffix'],
+ ];
+ }
+
+ private function invokeIsWikiSyntaxLink(string $value): bool
+ {
+ $method = new \ReflectionMethod(\Tracker_Field_Url::class, 'isWikiSyntaxLink');
+ $method->setAccessible(true);
+
+ return (bool) $method->invoke(null, $value);
+ }
+}
=====================================
templates/trackerinput/url.tpl
=====================================
@@ -1,6 +1,8 @@
<div{if !empty($field.options_map.labelasplaceholder)} class="input-group"{/if}>
<input type="text" class="form-control{if !empty($field.options_map.labelasplaceholder)} labelasplaceholder{/if}"
name="{$field.ins_id}" id="{$field.ins_id}" value="{$field.value|escape}" size="60"
+ data-url-wiki-wrapper-validation-input=""
+ data-url-wiki-wrapper-feedback-id="{$field.ins_id}_wikiSyntaxLive"
{if !empty($field.options_map.labelasplaceholder)}placeholder="{$field.name}"{/if}
>
{if $field.options_map.labelasplaceholder and $field.isMandatory eq 'y'}
@@ -9,3 +11,7 @@
</span>
{/if}
</div>
+<div id="{$field.ins_id}_wikiSyntaxLive"
+ class="form-text text-muted js-url-wiki-syntax-feedback"
+ data-default-info="{$data.wikiSyntaxInfo|escape:'htmlattr'}"
+>{$data.wikiSyntaxInfo|escape}</div>
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/e60185cd18b15a765b42a5a45facd005e455a7c2
--
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/e60185cd18b15a765b42a5a45facd005e455a7c2
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help
_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs